實(shí)戰(zhàn):權(quán)限設(shè)計與審批流全解)
這個項(xiàng)目是我在帶本科畢業(yè)設(shè)計時經(jīng)常被學(xué)生拿來問的一款典型Java后端系統(tǒng)學(xué)生德育獎懲管理系統(tǒng)同時也叫綜合素質(zhì)測評與獎助管理系統(tǒng)。說白了就是把過去輔導(dǎo)員手動記錄德育分、紙質(zhì)審批獎懲材料、人工排獎助學(xué)金名單這些事搬到Web端去做成一個可以量化、可追溯、能導(dǎo)出報表的管理平臺。我?guī)н^不少用SpringBoot做畢設(shè)的學(xué)生這類題目的生命力在于它不算難但業(yè)務(wù)鏈條長涉及角色多非常貼近真實(shí)的高校管理場景。德育分的錄入與申訴、獎懲事項(xiàng)的審批流、獎助學(xué)金名額分配與發(fā)放記錄這些功能前后能串到一起做完以后無論是寫論文還是答辯都有大量可展開的素材。這篇文章我會按我實(shí)際帶項(xiàng)目的思路來拆這個系統(tǒng)先講需求邊界和角色權(quán)限怎么定再看SpringBoot如何撐起整個Web端然后是數(shù)據(jù)庫表設(shè)計和核心模塊的實(shí)打?qū)嵶龇ㄗ詈蟀褜W(xué)生最容易踩的坑拉出來說一遍。如果你現(xiàn)在正在為畢設(shè)選題發(fā)愁或者已經(jīng)選了類似題目但還不知道從哪兒下手這篇應(yīng)該能幫你省不少時間。1. 內(nèi)容整體設(shè)計與思路拆解1.1 從業(yè)務(wù)場景反推系統(tǒng)邊界很多人一上來就打開IDE開始建表這是最忌諱的。德育獎懲管理系統(tǒng)聽起來不大但如果你把“德育”和“獎懲”都往大了做功能能膨脹到讓你寫不完。我的建議是先畫一張業(yè)務(wù)流程圖把參與者、觸發(fā)條件和結(jié)果落點(diǎn)理清楚。以高校的典型場景為例輔導(dǎo)員每個學(xué)期要給學(xué)生打德育測評分基礎(chǔ)分由班級評議小組給有日常加分項(xiàng)和扣分項(xiàng)學(xué)生如果做了好人好事或者獲得表彰可以提交加分申請違反校紀(jì)校規(guī)則要由學(xué)工辦發(fā)起扣分或處分流程。獎助學(xué)金模塊則是在德育總分的基礎(chǔ)上結(jié)合學(xué)業(yè)成績、家庭經(jīng)濟(jì)情況按名額和排序規(guī)則生成初選名單再由院系審核、公示、最終確定發(fā)放記錄。這些場景能拆出五類角色學(xué)生、輔導(dǎo)員、院系管理員學(xué)工辦、校級管理員學(xué)生處、系統(tǒng)超級管理員。不要五類角色都建獨(dú)立的表去存權(quán)限那樣后期維護(hù)會非常痛苦用Spring Security RBAC角色-權(quán)限-資源模型就夠了。1.2 為什么選SpringBoot而不是SSH或者Node選擇SpringBoot不是因?yàn)樗傲餍小倍沁@個項(xiàng)目需要的幾個能力它恰好最穩(wěn)。第一是權(quán)限控制。Web端要區(qū)分學(xué)生和輔導(dǎo)員的操作邊界Spring Security結(jié)合JWT令牌能實(shí)現(xiàn)無狀態(tài)認(rèn)證前端每次請求帶token后端通過注解就能控制接口權(quán)限不需要像Session方案那樣考慮集群共享的問題。第二是業(yè)務(wù)復(fù)雜度的支撐。德育量化考核涉及加分項(xiàng)、減分項(xiàng)、指標(biāo)維度每個維度又有權(quán)重和閾值獎助學(xué)金要處理名額、金額、公示狀態(tài)這些數(shù)據(jù)關(guān)系用Spring Data JPA或MyBatis-Plus來操作都相當(dāng)順手。如果你用Node.js寫也不是不行但遇到事務(wù)嵌套、批量更新這類操作調(diào)試成本會明顯高一些。第三是生態(tài)和參考資源。畢設(shè)階段你大概率會遇到BugSpringBoot在社區(qū)里的解決方案是最多的任何報錯只要你復(fù)制到搜索引擎基本都能找到現(xiàn)成的答案。這一點(diǎn)對時間緊張的學(xué)生來說價值比技術(shù)本身更大。1.3 前端選型的現(xiàn)實(shí)考量這個項(xiàng)目是Web端系統(tǒng)對于畢設(shè)而言我不建議去搞微服務(wù)或者高并發(fā)架構(gòu)老老實(shí)實(shí)做單體應(yīng)用即可。前端我一般推薦兩種方案一種是服務(wù)端模板引擎Thymeleaf適合不熟悉前端的同學(xué)好處是部署簡單把項(xiàng)目打成jar包就能跑另一種是前后端分離模式Vue Element UI適合愿意多花些時間在界面上、想給答辯加分的同學(xué)。我個人的傾向是如果題目要求里有“前后端分離”或“Web端交互流暢”的字樣就用Vue如果沒有明確要求優(yōu)先用Thymeleaf減少Node環(huán)境的依賴也縮短聯(lián)調(diào)時間。因?yàn)檫@個項(xiàng)目的核心價值在管理流程的閉環(huán)而不是頁面炫酷程度。德育分被記錄在案、審批流能正常流轉(zhuǎn)、獎助名單能導(dǎo)出Excel這些才是答辯評委關(guān)心的事。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 德育量化考核的指標(biāo)設(shè)計德育量化考核看起來簡單其實(shí)最容易做含糊。很多學(xué)生把德育分設(shè)計成“管理員手動加或減”這種方案答辯時會被問住因?yàn)樵u委必然會追問“加分的依據(jù)是什么權(quán)重怎么定的”正確的做法是把德育指標(biāo)拆成維度表。常見維度包括思想品德、遵紀(jì)守法、集體活動、志愿服務(wù)、學(xué)術(shù)競賽、宿舍文明等。每個維度下設(shè)具體條目比如志愿服務(wù)下面可以細(xì)分“校內(nèi)志愿活動每小時0.5分上限5分”“無償獻(xiàn)血每次2分”等。每條加分/扣分記錄都要關(guān)聯(lián)一個指標(biāo)條目并且記錄操作人和證明附件。數(shù)據(jù)庫上我建議就五張核心表moral_score學(xué)生德育總分表保存當(dāng)前有效總分。moral_record加減分明細(xì)表每產(chǎn)生一條記錄就入表附帶指標(biāo)id、分?jǐn)?shù)變化量、狀態(tài)、審批人、證明材料地址。moral_indicator指標(biāo)條目表維護(hù)維度、條目名稱、分值、類型加分/減分、是否啟用。permission_info學(xué)生某個學(xué)期的匯總測評信息用于生成學(xué)期綜測報告。approval_flow審批流實(shí)例表這個不一定所有畢業(yè)設(shè)計都做但如果德育分變化需要兩級審核建議加這張表。2.2 獎助學(xué)金模塊的狀態(tài)機(jī)設(shè)計獎助學(xué)金發(fā)放平臺是這類系統(tǒng)的另一個重頭。這里最好把“名額配置-申請?zhí)峤?初選評定-院系審核-學(xué)校審核-名單公示-資金發(fā)放-歸檔”做成一個明確的狀態(tài)機(jī)。狀態(tài)一般有未開始、申請中、初步評定、院系審核通過/退回、校級審核通過/退回、公示中、異議處理中、發(fā)放完成、已歸檔。每個狀態(tài)對應(yīng)可操作的角色例如“院系審核通過”只能由輔導(dǎo)員執(zhí)行校級管理員不能越權(quán)直接通過“初步評定”。SpringBoot里做狀態(tài)機(jī)不需要引入額外框架你只要在設(shè)計實(shí)體時保留一個status字段然后在Service層寫一個狀態(tài)流轉(zhuǎn)方法規(guī)定當(dāng)前狀態(tài)允許跳轉(zhuǎn)到的目標(biāo)狀態(tài)集合即可。這樣代碼直觀答辯時也容易講清楚。2.3 權(quán)限設(shè)計的幾個坑我在幫學(xué)生檢查代碼時發(fā)現(xiàn)最典型的問題是只做了登錄攔截沒做接口級權(quán)限。比如學(xué)生知道/api/moral/update這個接口后直接繞過前端去請求就能給自己加分。這是非常嚴(yán)重的安全漏洞。解決方式很簡單Spring Security的PreAuthorize(hasRole(ADMIN))注解就能實(shí)現(xiàn)。在Controller方法上標(biāo)注所需角色再開啟方法級安全配置。同時操作記錄一定要寫入日志表保留操作人IP方便追溯。另外德育分敏感操作建議加“二次確認(rèn)”機(jī)制。比如扣分超過5分時需要填寫具體原因和佐證材料否則后端直接拒絕。這不算復(fù)雜邏輯但能讓系統(tǒng)看起來更規(guī)范也能減少誤操作。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 項(xiàng)目初始化與依賴配置我以一個真實(shí)的SpringBoot MyBatis-Plus Vue項(xiàng)目為例先把后端骨架搭出來。用IDEA新建Spring Initializr項(xiàng)目Java版本建議JDK 1.8或11不要追新版本SpringBoot選擇2.7.x即可。2.7版本太低的換成3.x后有些依賴兼容性會出問題臨到答辯前再踩這個坑就很冤枉。核心依賴包括dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency注意SpringBoot 2.7.x對應(yīng)的MyBatis-Plus要用3.5.x版本不要再沿用3.4的老版本。數(shù)據(jù)庫連接池直接用HikariCPSpringBoot默認(rèn)已經(jīng)集成不用額外配置。3.2 數(shù)據(jù)庫表設(shè)計的落地腳本下面這張數(shù)據(jù)庫設(shè)計是我認(rèn)為比較能支撐整個系統(tǒng)運(yùn)轉(zhuǎn)的核心你可以直接參考再按自己學(xué)校的規(guī)則調(diào)整。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, role VARCHAR(20) NOT NULL, -- STUDENT / COUNSELOR / COLLEGE_ADMIN / SCHOOL_ADMIN student_no VARCHAR(30), college_id BIGINT, status TINYINT DEFAULT 1 ); CREATE TABLE moral_indicator ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dimension VARCHAR(30) NOT NULL, item_name VARCHAR(100) NOT NULL, score_value DECIMAL(5,2) NOT NULL, score_type TINYINT NOT NULL, -- 1加分 2減分 upper_limit DECIMAL(5,2) DEFAULT 0, enabled TINYINT DEFAULT 1 ); CREATE TABLE moral_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, indicator_id BIGINT NOT NULL, score_change DECIMAL(5,2) NOT NULL, reason VARCHAR(500), attachment_url VARCHAR(200), status TINYINT DEFAULT 0, -- 0待審核 1通過 2駁回 operator_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME ); CREATE TABLE scholarship_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, project_id BIGINT NOT NULL, moral_score DECIMAL(5,2), academic_score DECIMAL(5,2), apply_reason VARCHAR(2000), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE scholarship_project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_name VARCHAR(100) NOT NULL, quota INT NOT NULL, amount DECIMAL(10,2) NOT NULL, apply_start DATETIME, apply_end DATETIME, status TINYINT DEFAULT 0 ); CREATE TABLE scholarship_disbursement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, project_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, disburse_time DATETIME, bank_card_no VARCHAR(30), status TINYINT DEFAULT 0 -- 0未發(fā)放 1已發(fā)放 );這張設(shè)計做下來整個系統(tǒng)的數(shù)據(jù)流動已經(jīng)閉環(huán)了。這里有個關(guān)鍵點(diǎn)moral_score不要單獨(dú)設(shè)計表直接用SQL執(zhí)行SELECT student_id, SUM(score_change) FROM moral_record WHERE status 1 GROUP BY student_id來實(shí)時計算總成績放在單獨(dú)的匯總表里定時任務(wù)刷新即可。這樣能避免人工改總分導(dǎo)致明細(xì)對不上賬的情況。3.3 登錄認(rèn)證與JWT實(shí)現(xiàn)Spring Security JWT是項(xiàng)目里比較復(fù)雜的一塊我建議不要抄太復(fù)雜的方案。網(wǎng)上有很多擴(kuò)展幾十個類的教程看著很厲害但對畢設(shè)來說過于沉重。簡單做法是登錄接口驗(yàn)證用戶名密碼成功后用Jwts.builder()生成token把用戶ID和角色塞進(jìn)claims里返回給前端。前端在后續(xù)請求的請求頭Authorization里帶上Bearer token后端寫一個OncePerRequestFilter解析token把用戶信息放進(jìn)SecurityContextHolder。核心代碼大概是這個形態(tài)Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); String username claims.get(username, String.class); Long userId claims.get(userId, Long.class); String role claims.get(role, String.class); // 構(gòu)建Authentication對象并放入上下文 } catch (Exception e) { // token校驗(yàn)失敗不設(shè)置上下文即可 } } chain.doFilter(request, response); } }這里要注意secretKey長度必須大于等于256位否則JJWT 0.11.x版本會報錯。我見過好幾個學(xué)生在這里卡了一個下午其實(shí)就是把key改長一點(diǎn)就行了。3.4 德育加減分的審批流程德育加分不能讓輔導(dǎo)員一鍵直接通過要通過兩級審核否則評優(yōu)評先的時候?qū)W生就有意見。我的實(shí)現(xiàn)方式是學(xué)生提交加分申請→輔導(dǎo)員初審→院系管理員終審。如果終審駁回記錄會退回給輔導(dǎo)員同時學(xué)生端能看到駁回原因。在Service層每次狀態(tài)流轉(zhuǎn)都做一次合法性校驗(yàn)比如狀態(tài)是待輔導(dǎo)員審核時只有輔導(dǎo)員能調(diào)用審核接口狀態(tài)是待院系終審時院系管理員才能操作。這里不要只依賴前端隱藏按鈕后端一定要校驗(yàn)當(dāng)前操作者的角色和當(dāng)前記錄狀態(tài)是否匹配。if (!currentRole.equals(COUNSELOR)) { throw new BusinessException(當(dāng)前用戶無審核權(quán)限); } if (!record.getStatus().equals(RecordStatus.PENDING_COUNSELOR)) { throw new BusinessException(記錄當(dāng)前狀態(tài)不允許該操作); }3.5 獎助學(xué)金名額排序算法獎學(xué)金初選名單是按綜合成績排序來生成的。綜合成績公式通常為綜合成績 德育成績 × 30% 學(xué)業(yè)成績 × 70%。每個項(xiàng)目權(quán)重要做成可配置不要寫死。有了成績后按項(xiàng)目內(nèi)所有人從高到低排序取前N名進(jìn)入初選名單。這里要注意一個很現(xiàn)實(shí)的問題如果第N名和第N1名成績相同怎么辦我的方案是增加排序條件學(xué)業(yè)成績高的優(yōu)先如果還相同就按照提交申請時間早的優(yōu)先。這樣程序不會出現(xiàn)并列分發(fā)名單時也沒有爭議。ListStudentScoreVO sortedList scoreList.stream() .sorted(Comparator.comparing(StudentScoreVO::getEligible) .thenComparing(StudentScoreVO::getAcademicScore).reversed() .thenComparing(StudentScoreVO::getApplyTime)) .collect(Collectors.toList()); ListStudentScoreVO selected sortedList.subList(0, Math.min(quota, sortedList.size()));當(dāng)?shù)掠煽兒蛯W(xué)業(yè)成績都是0時要排除掉未提交申請的學(xué)生這部分邏輯切記要提前過濾。3.6 公示與異議處理名單生成后需要進(jìn)入公示狀態(tài)。公示期內(nèi)允許學(xué)生提交異議不直接改名單而是生成一條異議記錄由院系管理員復(fù)核。復(fù)核通過后被駁回的申請狀態(tài)改回“待審核”并標(biāo)記異議原因。這個功能雖然不起眼但在答辯中很容易成為亮點(diǎn)因?yàn)樗w現(xiàn)了系統(tǒng)設(shè)計時對實(shí)際業(yè)務(wù)場景的考慮。我做的時候還加了個小功能公示期滿后系統(tǒng)自動將名單狀態(tài)更新為已生效并給相關(guān)學(xué)生發(fā)站內(nèi)消息。4. 常見問題與排查技巧實(shí)錄4.1 SpringBoot啟動時報端口被占用這類問題在Windows開發(fā)機(jī)上經(jīng)常出現(xiàn)。明明上一個項(xiàng)目已經(jīng)停掉了但端口還是被占用。先執(zhí)行下面命令查看netstat -ano | findstr 8080后面顯示PID然后去任務(wù)管理器結(jié)束對應(yīng)進(jìn)程即可。如果不想每次手動找就在application.yml里設(shè)置隨機(jī)端口server: port: ${PORT:8080}部署時指定-DPORT8081就能切換這個技巧對后續(xù)把項(xiàng)目放到服務(wù)器上演示非常有用。4.2 MyBatis-Plus分頁查詢失效很多學(xué)生寫分頁時前面配置了PaginationInnerInterceptor但查詢一頁返回所有數(shù)據(jù)原因是這個攔截器要引入最新版本的mybatis-plus-boot-starter并且注入時指定了數(shù)據(jù)庫類型。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }還有些同學(xué)用的是自定義SQL join查詢分頁對象必須作為第一個參數(shù)傳進(jìn)去否則不生效。這個點(diǎn)尤其容易忽視。4.3 前后端聯(lián)調(diào)時跨域問題用Vue開發(fā)時axios請求后端接口會報跨域。最簡單的辦法是在SpringBoot里配置CORS。注意CrossOrigin注解寫單個Controller是能解決但要處理所有Controller我建議寫一個全局配置類Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }開發(fā)環(huán)境下直接允許所有來源就行正式部署時再收緊。4.4 導(dǎo)出Excel亂碼問題獎助名單必須支持導(dǎo)出Excel很多同學(xué)用POI導(dǎo)出后打開出現(xiàn)亂碼這是因?yàn)轫憫?yīng)頭沒設(shè)置或者文件名里包含中文導(dǎo)致的。正確設(shè)置是這樣的response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(UTF-8); String fileName URLEncoder.encode(獎助名單.xlsx, UTF-8); response.setHeader(Content-Disposition, attachment; filename\ fileName \);文件名不要直接拼中文否則在下載時就會被截斷成亂碼。4.5 頭像等文件上傳路徑問題文件上傳功能很多學(xué)生用絕對路徑保存真正部署到服務(wù)器后就找不到文件。我建議配置一個統(tǒng)一的存儲根路徑并通過一個/files/**映射來訪問app: file-root: D:/upload/然后寫資源映射器Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: appFileRoot); }這樣上傳的證明材料和頭像都能通過相對路徑訪問換環(huán)境時只需要修改配置項(xiàng)。5. 這個系統(tǒng)還可以怎么擴(kuò)展5.1 接入手寫簽名或電子簽章德育獎懲審批流如果希望更貼近真實(shí)辦公場景可以接入手寫簽名板輔導(dǎo)員和院系管理員審核時直接在頁面上簽名簽名圖片存入審批流表。這樣整個審批鏈條不僅有時間記錄還有真實(shí)的簽批形態(tài)打眼一看就比普通CRUD系統(tǒng)高級一截。5.2 增加綜合測評報告自動生成每個學(xué)期結(jié)束后系統(tǒng)可以按學(xué)生維度生成德育綜合素質(zhì)報告以PDF或Word格式導(dǎo)出。內(nèi)容包含學(xué)期得分、各項(xiàng)指標(biāo)明細(xì)、獎懲記錄、排名情況作為評獎評優(yōu)的歸檔材料。這個功能實(shí)現(xiàn)起來其實(shí)就是排版加數(shù)據(jù)聚合技術(shù)上不難但工作量能明顯撐起畢設(shè)論文的“系統(tǒng)實(shí)現(xiàn)”章節(jié)。5.3 消息通知與移動端適配獎助學(xué)金申請狀態(tài)變化、德育分審批結(jié)果這些都需要通知學(xué)生。我一般建議加一個簡單的站內(nèi)信模塊不要輕易接入短信或微信模板消息因?yàn)槟切枰髽I(yè)資質(zhì)或額外費(fèi)用。做移動端適配時前端使用Vue的話可以用響應(yīng)式布局讓W(xué)eb端在手機(jī)上也能看應(yīng)付日常瀏覽和審核足夠了。6. 寫在最后的一些真實(shí)體會帶過這么多屆畢設(shè)我發(fā)現(xiàn)學(xué)生做這類管理系統(tǒng)最終成績拉開差距的地方往往不在代碼量而在業(yè)務(wù)邏輯的完整性和細(xì)節(jié)考慮。德育獎懲管理系統(tǒng)的核心不只是一個增刪改查而是你能不能把“德育分變化可追溯、獎懲審批有狀態(tài)、獎助名單有依據(jù)”這三件事講清楚并且用代碼實(shí)現(xiàn)出來。我自己在指導(dǎo)過程中有個習(xí)慣做每個模塊前先問學(xué)生三個問題這個操作誰來做什么時候能做做完以后會影響到哪些數(shù)據(jù)把這三個問題想通了整個系統(tǒng)的骨架自然就出來了SpringBoot只是把它落地的工具而已。如果你現(xiàn)在正在寫這個題目我建議你先把數(shù)據(jù)庫表建好把審批流邏輯跑通再去裝飾頁面。先把最難啃的骨頭啃掉后面的時間就比較從容了。如果這個過程中遇到具體的報錯歡迎在評論區(qū)帶上日志和代碼片段我看到后會幫你分析。做這類畢業(yè)設(shè)計一次成功的完整跑通比反復(fù)推翻重來要重要得多。