實戰(zhàn):權(quán)限控制、數(shù)據(jù)庫設(shè)計與業(yè)務(wù)閉環(huán))
簡介這是一份面向高校計算機相關(guān)專業(yè)畢業(yè)設(shè)計的完整論文資源以SpringBoot框架為核心結(jié)合MySQL數(shù)據(jù)庫與B/S架構(gòu)系統(tǒng)講解學(xué)生宿舍管理系統(tǒng)的設(shè)計實現(xiàn)全過程。資源適合需要完成畢業(yè)設(shè)計、課程設(shè)計或?qū)W習(xí)SpringBoot項目開發(fā)的學(xué)生參考。論文圍繞管理員、宿管員、學(xué)生、維修員四類角色覆蓋樓宇管理、宿舍管理、學(xué)生管理、申請換寢、請假報備、報修申請、問題反饋、缺寢登記、遷出記錄、公告管理等核心業(yè)務(wù)模塊從需求分析、系統(tǒng)設(shè)計到編碼測試均有詳細論述。內(nèi)容預(yù)覽中可見中英文摘要、目錄及功能劃分結(jié)構(gòu)完整邏輯清晰可作為項目開發(fā)與論文撰寫的直接范本。壓縮包共1個文件為docx格式電子文檔大小3.03MB便于編輯與排版。目前已有81人學(xué)習(xí)瀏覽適合正在籌備畢業(yè)設(shè)計或希望了解宿舍管理業(yè)務(wù)流程的讀者下載使用有助于快速搭建同類管理系統(tǒng)的認知框架并節(jié)省前期調(diào)研時間。1. 一個 SpringBoot 宿舍管理系統(tǒng)到底要管什么學(xué)生宿舍管理是個典型的「看著簡單、做起來碎」的業(yè)務(wù)。樓宇、宿舍、床位、學(xué)生、報修、請假、換寢、缺寢、遷出、公告這些對象彼此關(guān)聯(lián)而且使用人群分四類管理員、宿管員、學(xué)生、維修員。如果靠 Excel 和微信群信息必然零散報修進度靠催缺寢情況靠問換寢有沒有床位靠猜。這套 SpringBoot 學(xué)生宿舍管理系統(tǒng)就是把上述所有動作從線下搬到線上管理員維護樓宇宿舍和學(xué)生檔案宿管員處理缺寢登記和報修申請學(xué)生自助提交換寢、請假、報修和問題反饋維修員接收報修通知并回填記錄。技術(shù)棧是 SpringBoot MySQLB/S 架構(gòu)四類角色共用一套登錄入口按角色動態(tài)渲染菜單和權(quán)限。要拆這個項目先別急著看頁面核心價值在數(shù)據(jù)模型的設(shè)計和幾個高頻業(yè)務(wù)閉環(huán)的實現(xiàn)上。2. 先別寫代碼四角色權(quán)限模型與核心表結(jié)構(gòu)設(shè)計2.1 角色權(quán)限用 RBAC 還是直接字段區(qū)分很多畢設(shè)項目一上來就引入 Spring Security RBAC 五張表結(jié)果業(yè)務(wù)沒做多少光配權(quán)限就耗掉一半時間。這個系統(tǒng)的角色是固定的四種不存在動態(tài)創(chuàng)建角色、給角色分配菜單的需求所以更務(wù)實的做法是在用戶表上直接用一個role字段區(qū)分角色配合攔截器做 URL 級別的訪問控制。這樣表設(shè)計簡單查詢高效調(diào)試的時候一眼就能看出登錄用戶是什么身份。但要注意一個邊界如果后續(xù)要做「宿管員只能管理自己負責(zé)樓宇的數(shù)據(jù)」就不能只靠role字段了需要增加樓宇和用戶的關(guān)聯(lián)關(guān)系。我的建議是用戶表加一個building_id字段宿管員創(chuàng)建時綁定樓宇查詢學(xué)生數(shù)據(jù)時按building_id過濾。這個設(shè)計不影響當(dāng)前功能但給數(shù)據(jù)隔離留了口子。2.2 核心數(shù)據(jù)表從業(yè)務(wù)對象反推建表語句這個系統(tǒng)的核心業(yè)務(wù)對象不算多但表間關(guān)系要理清楚。先看角色學(xué)生屬于某個宿舍宿舍屬于某棟樓宇樓宇有宿管員負責(zé)。再看業(yè)務(wù)流轉(zhuǎn)學(xué)生提交請假、報修、換寢申請宿管員審批或登記維修員處理報修所有操作產(chǎn)生記錄。從這些關(guān)系反推最少需要這幾張表表名用途關(guān)鍵字段sys_user四類角色的統(tǒng)一用戶表role,building_id,dorm_idbuilding樓宇信息building_no,name,floorsdormitory宿舍信息building_id,room_no,bed_count,used_countstudent_info學(xué)生擴展信息user_id,student_no,major,class_namerepair_record報修單apply_user_id,dorm_id,type,status,handler_idleave_apply請假報備student_id,start_time,end_time,reason,statuschange_bed_apply換寢申請student_id,from_dorm_id,to_dorm_id,statusabsence_record缺寢登記student_id,date,reason這里有個取舍學(xué)生信息單獨建一張student_info還是直接合并進sys_user如果只是畢設(shè)演示合并成一張表完全夠用但如果要體現(xiàn)學(xué)生換班、轉(zhuǎn)專業(yè)、學(xué)號變更這些歷史軌跡拆開更合理。我在這個項目里選擇拆開因為學(xué)生管理的字段學(xué)號、專業(yè)、班級和賬號字段用戶名、密碼的變更頻率完全不同拆開后sys_user表保持精簡登錄查詢性能更好。2.3 建表 SQL 與字段約束說明直接給出核心表的建表語句注意幾個容易踩坑的點CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt加密存儲, real_name varchar(50) DEFAULT NULL, role tinyint NOT NULL COMMENT 1管理員 2宿管員 3學(xué)生 4維修員, building_id bigint DEFAULT NULL COMMENT 宿管員綁定的樓宇I(lǐng)D, dorm_id bigint DEFAULT NULL COMMENT 學(xué)生所在宿舍ID, phone varchar(20) DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dormitory ( id bigint NOT NULL AUTO_INCREMENT, building_id bigint NOT NULL, room_no varchar(20) NOT NULL, bed_count int NOT NULL DEFAULT 4, used_count int NOT NULL DEFAULT 0, gender tinyint NOT NULL COMMENT 1男生宿舍 2女生宿舍, PRIMARY KEY (id), UNIQUE KEY uk_building_room (building_id, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密碼字段用 BCrypt 加密存儲不要用 MD5。MD5 查重成本極低泄露一次就等于明文。Spring Security Crypto 包里自帶BCryptPasswordEncoder不需要引入完整的安全框架也能用。uk_building_room唯一索引很關(guān)鍵防止同一棟樓錄入兩個相同房號。dormitory表的used_count是一個冗余計數(shù)添加學(xué)生入住時1遷出時-1。這個字段的維護必須和宿舍分配邏輯放在同一個事務(wù)里否則會出現(xiàn)「宿舍實際住滿但系統(tǒng)顯示還有床位」的問題。后文換寢部分會專門講這個事務(wù)邊界。3. SpringBoot 骨架搭建與登錄鑒權(quán)實現(xiàn)3.1 依賴選型與工程目錄SpringBoot 版本選擇 2.7.x原因很實際官方文檔全、社區(qū)問題沉淀多、對 JDK 8 兼容最好。別一上來就用 SpringBoot 3JDK 17 的要求會把一部分部署環(huán)境卡死尤其是有些機房服務(wù)器還跑著 JDK 8。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies用 MyBatis-Plus 而不是原生 MyBatis是因為這個項目里有大量單表 CRUDMyBatis-Plus 的BaseMapper可以直接省掉 mapper XML 的編寫分頁插件也是現(xiàn)成的。但不建議把業(yè)務(wù)邏輯寫死在 QueryWrapper 鏈?zhǔn)秸{(diào)用里復(fù)雜查詢還是要寫 SQL 映射。工程目錄按職責(zé)分包而不是按技術(shù)層分包com.dormitory ├── controller # 接口層 ├── service # 業(yè)務(wù)邏輯層 │ └── impl ├── mapper # MyBatis-Plus 數(shù)據(jù)訪問層 ├── entity # 實體類 ├── dto # 入?yún)?出參對象 ├── config # 攔截器、全局異常、CORS 配置 └── common # 統(tǒng)一返回結(jié)果、常量、工具類3.2 登錄接口從 Controller 到 Service 的完整鏈路登錄接口是整個系統(tǒng)的入口四類角色共用。核心邏輯是接收用戶名密碼查詢用戶校驗密碼寫入 Session。用 Session 而不是 JWT是因為這個系統(tǒng)是服務(wù)端渲染頁面為主Session 的過期管理由容器接管代碼量最少。如果前后端分離才考慮 JWT。RestController RequestMapping(/api/auth) public class AuthController { Resource private AuthService authService; PostMapping(/login) public ResultUserVO login(RequestBody LoginDTO dto, HttpServletRequest request) { // 1. 非空校驗 if (StringUtils.isBlank(dto.getUsername()) || StringUtils.isBlank(dto.getPassword())) { return Result.error(用戶名和密碼不能為空); } // 2. 調(diào)登錄服務(wù) UserVO user authService.login(dto.getUsername(), dto.getPassword()); if (user ! null) { // 3. 寫入 Session request.getSession().setAttribute(loginUser, user); return Result.ok(user); } return Result.error(用戶名或密碼錯誤); } }Service 層是核心關(guān)注密碼校驗和用戶狀態(tài)檢查Service public class AuthServiceImpl implements AuthService { Resource private SysUserMapper sysUserMapper; private final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); Override public UserVO login(String username, String rawPassword) { SysUser user sysUserMapper.selectOne( new LambdaQueryWrapperSysUser() .eq(SysUser::getUsername, username) ); if (user null) { return null; } if (user.getStatus() ! 1) { throw new BusinessException(賬號已被禁用請聯(lián)系管理員); } // 用 BCrypt 校驗密碼 if (!encoder.matches(rawPassword, user.getPassword())) { return null; } // 組裝 VO 時剔除敏感字段 UserVO vo new UserVO(); vo.setId(user.getId()); vo.setUsername(user.getUsername()); vo.setRealName(user.getRealName()); vo.setRole(user.getRole()); if (user.getRole() 3) { // 學(xué)生角色帶上宿舍信息供前端展示 vo.setDormId(user.getDormId()); } return vo; } }encoder.matches(rawPassword, user.getPassword())的作用是拿明文密碼和數(shù)據(jù)庫中已加密的密文做比對BCrypt 每次生成的鹽不同但matches可以正確校驗。UserVO里不要回傳password字段這是最低要求更穩(wěn)妥的做法是直接用JsonIgnore注解在實體類的密碼字段上從序列化源頭杜絕泄露。3.3 基于攔截器的 URL 權(quán)限控制四類角色能訪問的接口不同用攔截器控制 URL 模式最直接。SpringBoot 的自動配置機制會自動注冊WebMvcConfigurer的實現(xiàn)類我們只需要在配置類里添加攔截器規(guī)則。Component public class AuthInterceptor implements HandlerInterceptor { private static final MapInteger, String ROLE_PREFIX Map.of( 1, /admin, 2, /dormitory, 3, /student, 4, /maintainer ); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserVO loginUser (UserVO) request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } String uri request.getRequestURI(); String prefix ROLE_PREFIX.get(loginUser.getRole()); // 管理員可以訪問所有接口其他角色只能訪問對應(yīng)前綴 if (loginUser.getRole() 1 || uri.startsWith(prefix)) { return true; } response.setStatus(403); return false; } }Configuration public class WebConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /login, /css/**, /js/**); } }這段攔截器的設(shè)計哲學(xué)是「角色前綴即權(quán)限」學(xué)生的接口 URL 都以/student開頭宿管員以/dormitory開頭維修員以/maintainer開頭只有管理員可以跨越前綴限制訪問全部接口。好處是權(quán)限規(guī)則一眼可見新增一個學(xué)生端接口時不需要改配置文件壞處是如果未來要讓學(xué)生也能調(diào)用宿管員的某幾個接口這個設(shè)計就不夠用了需要改成基于注解的細粒度權(quán)限控制。4. 報修與換寢兩個高頻業(yè)務(wù)閉環(huán)的落地4.1 報修流程的狀態(tài)機設(shè)計報修是這個系統(tǒng)里流轉(zhuǎn)鏈路最長的一個業(yè)務(wù)學(xué)生發(fā)起、宿管員審核、維修員接單、維修完成回填。這種多角色協(xié)作的業(yè)務(wù)最忌諱在代碼里散落魔法數(shù)字判斷狀態(tài)。先用狀態(tài)機把流轉(zhuǎn)理清。待處理(0) - 已派單(1) - 維修中(2) - 已完成(3) | | v v 已取消(4) 已驗收(5)學(xué)生提交報修單狀態(tài)為待處理(0)宿管員審核通過并指派給維修員狀態(tài)變?yōu)橐雅蓡?1)維修員接單開始維修狀態(tài)變?yōu)榫S修中(2)維修完成回填記錄狀態(tài)變?yōu)橐淹瓿?3)學(xué)生在宿舍端確認狀態(tài)變?yōu)橐羊炇?5)如果宿管員審核不通過狀態(tài)直接置為已取消(4)。狀態(tài)流轉(zhuǎn)在后端要做合法性校驗不允許跳躍。比如維修員不能把待處理(0)直接改成已完成(3)必須經(jīng)過派單環(huán)節(jié)。這個校驗寫在 Service 層不寫在 Controller。4.2 報修接口實現(xiàn)與事務(wù)邊界Service public class RepairServiceImpl implements RepairService { Resource private RepairRecordMapper repairRecordMapper; Resource private DormitoryMapper dormitoryMapper; Override Transactional(rollbackFor Exception.class) public Long createRepair(RepairApplyDTO dto, Long studentId) { RepairRecord record new RepairRecord(); record.setApplyUserId(studentId); record.setDormId(dto.getDormId()); record.setType(dto.getType()); record.setDescription(dto.getDescription()); record.setStatus(0); // 初始狀態(tài)待處理 record.setCreateTime(LocalDateTime.now()); repairRecordMapper.insert(record); return record.getId(); } Override Transactional(rollbackFor Exception.class) public void dispatch(Long recordId, Long maintainerId) { // 校驗當(dāng)前狀態(tài)必須是待處理 RepairRecord record repairRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BusinessException(當(dāng)前狀態(tài)不允許派單); } record.setStatus(1); record.setHandlerId(maintainerId); record.setDispatchTime(LocalDateTime.now()); repairRecordMapper.updateById(record); } }Transactional加在 Service 方法上而不是 Controller 方法上。原因在于 Controller 只做參數(shù)接收和結(jié)果返回事務(wù)邊界應(yīng)該劃在業(yè)務(wù)邏輯的起止位置。createRepair里面未來如果還要維護一條報修日志表或者通知記錄表同一個事務(wù)保證報修單和日志要么一起成功要么一起回滾。狀態(tài)校驗放在更新之前但這里有個并發(fā)風(fēng)險兩個請求同時讀到status0同時執(zhí)行更新就會導(dǎo)致狀態(tài)錯亂。悲觀的做法是在selectById時加上FOR UPDATE行鎖在 MySQL 的 InnoDB 引擎下selectById走主鍵索引鎖的是一行記錄不會影響其他宿舍的報修單并發(fā)處理。-- 在 Mapper 中手動處理 SELECT * FROM repair_record WHERE id #{id} FOR UPDATE4.3 換寢申請如何保證宿舍資源不超賣換寢是宿舍管理系統(tǒng)里最容易出并發(fā)問題的功能。學(xué)生 A 申請從 401 換到 502學(xué)生 B 同時申請從 301 換到 502如果 502 只剩一個空位兩個申請都通過了「剩余床位 0」的校驗就會超賣。解決辦法是把「校驗空位 扣減床位 更新學(xué)生宿舍」放在同一個事務(wù)里并且對宿舍行加鎖。Override Transactional(rollbackFor Exception.class) public void approveChangeBed(Long applyId, Long approverId) { // 1. 獲取申請單 ChangeBedApply apply changeBedApplyMapper.selectById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BusinessException(申請單不存在或已處理); } // 2. 悲觀鎖鎖定目標(biāo)宿舍防止并發(fā)超賣 Dormitory targetDorm dormitoryMapper.selectByIdForUpdate(apply.getToDormId()); if (targetDorm.getUsedCount() targetDorm.getBedCount()) { throw new BusinessException(目標(biāo)宿舍已滿員); } // 3. 原宿舍釋放床位 Dormitory fromDorm dormitoryMapper.selectById(apply.getFromDormId()); if (fromDorm.getUsedCount() 0) { fromDorm.setUsedCount(fromDorm.getUsedCount() - 1); dormitoryMapper.updateById(fromDorm); } // 4. 目標(biāo)宿舍占用床位 targetDorm.setUsedCount(targetDorm.getUsedCount() 1); dormitoryMapper.updateById(targetDorm); // 5. 更新學(xué)生的宿舍關(guān)聯(lián) SysUser student sysUserMapper.selectById(apply.getStudentId()); student.setDormId(apply.getToDormId()); sysUserMapper.updateById(student); // 6. 申請單置為通過寫入遷出記錄 apply.setStatus(1); apply.setApproveTime(LocalDateTime.now()); changeBedApplyMapper.updateById(apply); MoveOutRecord moveOut new MoveOutRecord(); moveOut.setStudentId(apply.getStudentId()); moveOut.setFromDormId(apply.getFromDormId()); moveOut.setToDormId(apply.getToDormId()); moveOut.setCreateTime(LocalDateTime.now()); moveOutRecordMapper.insert(moveOut); }selectByIdForUpdate是關(guān)鍵對應(yīng) SQL 是SELECT * FROM dormitory WHERE id ? FOR UPDATE。在事務(wù)里這行記錄會被加上排他鎖直到事務(wù)提交才釋放。第二個申請就算同時進來也只能在這個鎖后面排隊等第一個事務(wù)提交后才能讀到最新數(shù)據(jù)此時used_count已更新超賣自然被攔住。換寢事務(wù)的邊界還需注意如果目標(biāo)宿舍和原宿舍是同一棟樓先釋放再占用的順序沒問題如果后續(xù)要支持跨校區(qū)換寢需要考慮原宿舍釋放失敗時的補償邏輯當(dāng)前這個項目用同一個事務(wù)包住任何一個環(huán)節(jié)拋異常都會回滾。另外approveChangeBed的入?yún)pproverId當(dāng)前只做記錄用途如果要做更嚴(yán)格的權(quán)限控制還需要校驗審批人是否為該生所在樓宇的宿管員。5. 功能驗收清單與部署排錯技巧5.1 四類角色的核心驗收點系統(tǒng)開發(fā)完不能直接交按角色過一遍驗收清單每個功能都要能走通正向和反向流程。角色核心功能驗收點管理員樓宇/宿舍管理新增宿舍時bed_count used_count必須被攔截刪除宿舍時若有學(xué)生關(guān)聯(lián)應(yīng)提示管理員學(xué)生管理學(xué)生的宿舍變更后sys_user.dorm_id和宿舍used_count必須一致宿管員缺寢登記同一天同一學(xué)生不能重復(fù)登記兩次宿管員報修申請?zhí)幚聿煌ㄟ^時必須填寫原因?qū)W生端能看到拒絕理由學(xué)生請假報備請假結(jié)束時間必須晚于開始時間沖突時明確提示學(xué)生問題反饋提交后學(xué)生能看到反饋狀態(tài)待處理/已回復(fù)維修員報修通知維修員只能看到已派給自己的報修單不能看到全部最值得仔細驗收的是數(shù)據(jù)一致性隨便找一個學(xué)生看他所在的宿舍再查這個宿舍的used_count兩邊必須對得上。如果出現(xiàn)不一致八成是某個直接改數(shù)據(jù)庫的 SQL 沒有同步更新冗余字段。5.2 常見問題排查啟動報錯先看端口占用和 MySQL 連接八成的問題都出在這兩處。# 檢查 8080 端口是否被占用 lsof -i :8080 # 查看 MySQL 是否在運行Linux 下使用 systemctl status mysqldapplication.yml里最容易配錯的是時區(qū)和 SSL 參數(shù)。MySQL 8 的連接字符串如果不加時區(qū)參數(shù)會報The server time zone value錯誤。推薦直接使用下面的配置spring: datasource: url: jdbc:mysql://localhost:3306/dormitory?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8頁面加載慢但接口響應(yīng)快優(yōu)先看靜態(tài)資源是不是走到了 SpringBoot 默認的 classpath 路徑部署到服務(wù)器后訪問不到先確認是不是防火墻或安全組沒放行端口再看server.address是否綁定到了0.0.0.0。日志級別調(diào)成 DEBUG 后啟動觀察 MyBatis-Plus 打印的 SQL很多數(shù)據(jù)不一致問題通過 SQL 日志能直接定位到是哪一步更新操作丟了這個字段。日志文件過大時在application.yml里加logging.file.namelogs/dormitory.log之外用logback-spring.xml配置按天滾動和保留天數(shù)避免一張表的問題變成一個占用磁盤的問題。演示環(huán)境可用--spring.profiles.activedev快速切換開發(fā)/生產(chǎn)配置。本文還有配套的精品資源點擊獲取