ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SpringBoot的宿舍管理系统实战:从需求到部署全流程解析

基于SpringBoot的宿舍管理系统实战:从需求到部署全流程解析 基于SpringBoot的宿舍管理系统从零到可交付的项目实战复盘每年这时候都会有人问宿舍管理系统怎么选题SpringBoot毕设怎么下手这个题目确实经典但经典不等于简单。我去年完整做了一版基于SpringBoot的宿舍管理系统从需求梳理、数据库建模、接口开发到前端联调、部署上线前后花了三周。这篇文章把我整个实现过程、关键代码、以及那些只有真正动手才会踩到的坑全部整理出来。无论你是拿它做课程设计、毕业设计还是想在公司内部落地一套宿舍管理工具都可以照着这套思路走至少能少走一半弯路。这个系统最终交付的样子是一套前后端分离的单体应用后端SpringBoot MyBatis-Plus MySQL前端Vue ElementUI部署在轻量服务器上。支撑了学生管理、宿舍分配、退宿调宿、报修跟踪、访客登记、卫生检查、公告发布、管理员权限管理等核心业务。功能不算花哨但每一块都有完整的业务闭环不是那种只搭了个列表增删改查就敢叫管理系统的拼凑货。1. 项目整体设计与技术选型思路1.1 为什么选SpringBoot不是跟风是它真的能把成本压下来先聊选型。现在做这类管理系统SpringBoot几乎成了标配网上教程铺天盖地但这并不意味着无脑选它。我选择SpringBoot的核心原因有三个。第一开发效率。宿舍管理系统的本质是CRUD加少量业务状态流转SpringBoot的自动装配机制把大量样板配置都吃掉了。你可以回忆一下用SSHSpringMVC Hibernate时代写一个Web项目要配多少XML数据源、事务、视图解析器、扫描包路径每样都有一堆配置。SpringBoot把这些全部变成了约定优于配置的默认值写一个Controller加一个Service两分钟就能跑通一个接口。这对团队人数少、工期紧张的项目来说是决定性的优势。第二生态成熟度。做系统不只是写代码登录要鉴权、文件要上传、数据要校验、操作要记日志、接口要对接前端。SpringBoot生态里这些都有现成的Starter或者成熟第三方库。我后面会详细讲文件上传用了本地存储和OSS两种方案数据校验直接用Bean Validation注解日志用了Logback这块的选型成本几乎为零。第三部署价值。SpringBoot内置Tomcat打包成可执行JAR以后服务器上只要装了JDK就能跑不需要单独装和配置Tomcat。我第一次部署的时候服务器和解压环境只有Java运行环境一个java -jar命令直接起来了没有复杂的容器配置和上下文路径问题。但也要说清楚它的边界。宿舍管理系统如果超大规模并发比如几千人同时操作SpringBoot默认的同步Servlet模型就会吃力不过这类内部管理系统并发量通常很低QPS能到两位数已经很夸张了完全够用。如果你做的是互联网级的产品那才需要考虑SpringCloud微服务拆分、消息队列削峰这些方案——在宿舍管理这个场景里属于过度设计。1.2 技术栈选型前后端分离还是服务端渲染这个题目下面另一个人人都会纠结的点是页面到底用Thymeleaf服务端渲染还是Vue前后端分离。两种我都和各人聊过我的结论是如果是单人或小团队开发、时间紧张、以功能完成为目标其实Thymeleaf的方案如果你的后端功底扎实确实能拉低前端门槛但如果你要为答辩或面试展示完整的工程能力或者后续有扩展计划Vue前后端分离会是更好的选择。我做的是前后端分离。原因主要有几个宿舍管理系统里有很多交互比较重的操作比如宿舍分配时需要按楼栋点击查看房间状态、床位的空余情况这会是一个类似地图点选的交互报修模块需要上传图片、跟踪状态动态交互很频繁。前后端分离以后前端专注交互和数据展示后端只管RESTful接口接口文档用Swagger自动生成前端照着文档联调就行不用再等后端改页面。实际的技术栈清单如下后端SpringBoot 2.7.x、MyBatis-Plus 3.5.x、MySQL 8.0、Redis用于验证码和Token缓存、JJWT生成Token、Hutool工具类、EasyExcel学生批量导入前端Vue 2 Vue Router Vuex ElementUI Axios其他Maven、Git、Swagger/knife4j、Logback这个组合的好处是每一层都有非常成熟的实践路径网上资料密度高遇到问题几乎都能搜到现成方案。我后面所有代码都基于这套组合来写如果你用的版本不同注意看一下对应版本的API变化比如MyBatis-Plus新旧版本的分页插件写法就完全不一样。2. 功能模块设计与数据库建模2.1 功能模块拆解先做减法再做加法宿舍管理系统如果一股脑把所有可能的功能都加进去最后一定是一个无人能维护的怪物。我设计功能模块的顺序是先把核心主线画出来再往上面挂辅助功能。核心主线是什么是学生的入住生命周期。一个学生入校分配宿舍入住后产生了水电、报修、卫生检查等日常事务离校后办理退宿。整条主线的起点是入住终点是退宿中间是各种和宿舍相关的事务记录。系统所有表设计都应该围绕这条主线展开。拆成模块就是这样基础资料模块宿舍楼管理、宿舍房间管理、床位管理。这三层是层级关联的一个宿舍楼含多间房一间房含多个床位。房间要有性别属性、可住人数、当前人数、剩余床位。学生档案与入住模块学生信息管理、入住登记、退宿登记、调宿申请。这块是最核心的关联了学生表和房间表还要考虑并发场景两个管理员同时分配同一间房的最后一个空位。日常事务模块报修管理申请、指派、维修、确认完成、卫生检查查寝评分、整改记录、访客登记、晚归登记。这几块是独立的业务线但都关联到学生或宿舍楼。系统管理模块管理员账号、角色权限超级管理员、楼栋管理员、普通管理员、操作日志、公告管理。如果你做的是课程设计可以砍掉一些比如访客登记和晚归登记不是必须的。但无论如何主线功能学生、楼栋、房间、入住退宿、报修必须完整这是系统能自圆其说的底线。2.2 数据库设计每一张表都要能回答为什么我建库的时候遵循一个原则每张表都要能回答为什么需要这张表、它的唯一标识是什么、它和哪些表有关联。数据库设计如果偷懒后面写代码会噩梦不断。直接看核心表结构这里是简化版但涵盖了关键字段学生表student字段名类型说明idbigint主键自增student_novarchar(20)学号唯一的用来登录namevarchar(50)姓名gendertinyint性别这里是1男2女注意跟房间的性别属性联动collegevarchar(100)学院majorvarchar(100)专业phonevarchar(20)手机号statustinyint状态1在读2离校3休学create_timedatetime创建时间宿舍楼表dormitory_building字段名类型说明idbigint主键building_namevarchar(50)楼栋名称例如1号楼gendertinyint楼栋性别属性男女分栋floor_countint层数manager_namevarchar(50)楼栋管理员姓名房间表dormitory_room字段名类型说明idbigint主键building_idbigint所属宿舍楼room_novarchar(20)房间号capacityint可住人数occupiedint已住人数gendertinyint房间性别冗余字段但很有必要避免每次join楼栋statustinyint状态1可用0维修中2停用入住记录表dormitory_resident字段名类型说明idbigint主键student_idbigint学生room_idbigint房间bed_novarchar(10)床位号check_in_timedatetime入住时间check_out_timedatetime退宿时间为空表示未退宿is_activetinyint是否当前有效记录报修表repair_order字段名类型说明idbigint主键student_idbigint报修学生room_idbigint报修房间descriptionvarchar(500)问题描述image_urlvarchar(255)报修图片statustinyint状态1待受理2维修中3待确认4已完成5已取消assigneevarchar(50)维修人create_timedatetime报修时间finish_timedatetime完成时间管理员表admin_user字段名类型说明idbigint主键usernamevarchar(50)登录账号唯一passwordvarchar(255)BCrypt加密后的密码roletinyint1超级管理员2楼栋管理员3普通管理员real_namevarchar(50)真实姓名phonevarchar(20)联系方式几个我在设计时的关键决策学生表和用户表不合并。很多新手会把学生信息直接当账号用但学生信息有很强的业务属性学院、专业、状态而账号只关心安全认证两者将来可能以不同方式扩展。所以我是让student.student_no作为登录账号密码字段也放在学生表里通过BCrypt存储密码散列值。这里如果想做得更规整一点可以把认证信息单独拆成一张sys_user表但考虑到系统规模不大学生表直接承担登录是可行的能减少一次join。房间表必须加occupied字段。你可以实时去dormitory_resident表计算已住人数但入住判断非常频繁每次都要查聚合不如直接维护一个计数器。代价是这个计数器可能有并发问题我会在事务里通过行锁或者乐观锁来保证一致性这个后面会在Section 3单独讲。床位不单独建表。最初我也考虑过为每张床位建表每间宿舍4-6张床会更精细但实际管理方很少要求到床位级别的独立台账都会用bed_no这种字符串字段来描述。简化模型带来的好处是代码量减少好多。还有公共设计所有业务表都带create_time和update_time用MyBatis-Plus的自动填充功能插入和更新时框架自动赋值不用手动写。删除尽量采用逻辑删除用一个deleted标记位防止手滑把数据删没了。3. 核心功能实现与关键代码3.1 登录认证与权限控制从JWT到拦截器登录认证我用的JWTJSON Web Token不是Session。为什么前后端分离以后前端和后端可能不在同一个域也可能将来拆成多个服务Session在分布式环境下的共享问题很麻烦。JWT把用户信息加密签名放在Token里服务端无状态只要校验签名合法、有效期内就认这个请求。我引入的依赖基于JJWT 0.9.1比较简单核心代码如下// JwtUtil.java public class JwtUtil { // 密钥实际项目中放到配置文件用base64或HS256格式 private static final String SECRET your-secret-key-here-change-me; private static final long EXPIRE_TIME 1000 * 60 * 60 * 24; // 24小时 public static String generateToken(Long adminId, String username, Integer role) { return Jwts.builder() .setSubject(String.valueOf(adminId)) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }然后写一个JwtInterceptor拦截器实现SpringMVC的HandlerInterceptor核心逻辑是从请求头Authorization取Token解析成功则放行解析失败返回401。同时把解析出的用户信息放到ThreadLocal或Request属性里后续Controller通过RequestContextHolder就能拿到当前操作人不用每个接口都传管理员的ID。这里有个容易被忽略的细节拦截器放行的URL要仔细设计。登录接口、前端静态资源和Swagger文档必须放行其余接口统一拦截。我的做法是registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/logout);还有一个是权限控制。我的角色只有三级超级管理员、楼栋管理员、普通管理员。超级管理员能访问所有接口楼栋管理员只能操作自己管的楼栋普通管理员只读权限。这个如果用数据库型的权限表用户-角色-菜单-按钮会很复杂考虑到我只做了三级角色直接用自定义注解RequireRole加拦截器判断就行。写一次后面在Controller方法上加注解就好。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); // 允许访问的角色列表 }拦截器里解析出Token里的role字段和注解要求的角色比对不符合就抛一个401或403异常由全局异常处理器统一返回。安全方面还要补充强调密码存库必须加密我这里用BCryptPasswordEncoder同一个密码每次生成不同的哈希值防止彩虹表攻击。登录失败要统一提示账号或密码错误而不是用户不存在或密码错误避免攻击者通过提示信息探测合法账号。3.2 学生入住与退宿一个需要并发控制的业务场景「入住」这个动作的流程是这样的管理员选择学生选择楼栋筛选出有空位的房间选择床位确认入住。系统需要做几件事校验学生状态必须是在读状态且没有未退宿的入住记录。校验房间性别和孩子性别一致防止男学生安排到女生宿舍这种错误如果发生现实里会非常严重。校验房间occupied capacity。更新房间occupied 1。插入一条dormitory_resident记录。这几步必须放在一个数据库事务里。另外要考虑并发两个管理员同时操作同一个房间的最后一个床位两个人查到的都是还剩1个床位同时1结果房间occupied变成capacity1产生了超员数据。解决方式我用了数据库行锁在查询房间时加上FOR UPDATE// 在service中事务内使用 Transactional public void checkIn(Long studentId, Long roomId, String bedNo) { DormitoryRoom room roomMapper.selectByIdForUpdate(roomId); // selectByIdForUpdate 底层SQLSELECT ... FROM dormitory_room WHERE id ? FOR UPDATE if (room.getOccupied() room.getCapacity()) { throw new BusinessException(该房间已满); } // 更新occupied roomMapper.updateOccupied(roomId, room.getOccupied() 1); // 插入入住记录 residentMapper.insert(...); }这个selectByIdForUpdate是关键。事务A查到房间并锁定事务B想查同一行就会阻塞等A提交后B才读到最新值。这样就不会出现超员。注意这个锁要求在事务里才有效而且必须把方法标上Transactional调用方和被调方法通过代理调用时事务注解才生效同类内部的this调用是不行的这是个经典坑。退宿流程相对简单把当前有效的入住记录的check_out_time设为当前时间is_active改成0同时房间occupied - 1学生状态保持不变离校状态另设。但退宿要检查学生是否有未完成的报修有的话系统提示存在未完成报修请先处理——现实场景中退宿前必须检查宿舍设施完好这个业务约束不加的话宿舍家具坏了没法追责。调宿更复杂一些学生在A房间退宿先做退宿流程然后重新走一次入住到B房间。我在代码里是把这两个操作封装成一个事务方法保证调宿要么完全成功、要么完全失败不会出现学生没有宿舍住的情况。3.3 报修流程的完整状态机设计宿舍报修模块最能体现一个系统的活的程度。我之前见过很多类似项目报修功能就是插入一条记录然后就没有然后了顶多管理员看到后改个状态。这根本没解决实际问题。我完整设计的报修状态机是待受理 - 维修中 - 待确认 - 已完成 ^ | | |---------|----------|---- 取消仅待受理状态下学生可以取消每个状态的流转都有明确的操作人和触发条件学生提交报修生成待受理可以附一张照片。楼栋管理员看到待受理的工单指派给维修人状态变成维修中。维修人这块系统没有单独的维修人账号实际是管理员在界面上代为登记完成维修后把状态置为待确认填写维修结果。学生确认维修质量没问题点击确认状态变成已完成学生觉得没修好可以把状态退回到维修中并填写原因。为什么要设置待确认这一步因为报修的最终目的是学生满意。如果管理员自己修完直接点完成学生投诉没修好但系统已经关了就失去了闭环。加一个确认环节多一次交互但能避免大量扯皮。状态流转我在Service层统一封装了一个transitionRepairStatus方法传入当前状态、目标状态、操作人角色内部用switch匹配合法路径非法流转直接抛异常。这样状态机的规则是集中管理、不可绕过前端只是调用接口没办法自己改状态。报修列表的数据范围我做了细粒度控制超级管理员能看所有报修楼栋管理员只能看自己负责的楼栋通过room.building_id关联。这部分SQL我用了MyBatis-Plus的QueryWrapper动态拼条件或者直接写一个分页查询的XML两种都可以建议用XML写稍微复杂的联查可读性更好。还有一个顺手做的小功能报修受理时给报修人发一条站内消息不是短信系统内部消息表。学生登录后首页右上角有未读消息角标。这块多花了一点时间但对管理系统是否完整的观感提升非常大。3.4 学生批量导入用EasyExcel一次性解决如果系统数据全靠管理员手动录入录入一个学院上千人的学生信息会崩溃。所以必须支持Excel批量导入。我用的是阿里的EasyExcel3.x版本不是Apache POI直接操作。原因还是工作量差异POI做Excel解析要手动处理单元格、样式、空值校验EasyExcel提供了模型映射写一个数据类加几个注解Excel就能自动映射到对象。核心流程是前端上传Excel - 后端校验表头是否匹配 - 逐行解析 - 构建学生对象 - 批量插入 - 返回导入结果统计成功条数、失败行、失败原因。做一个简单的模型类public class StudentExcelData { ExcelProperty(学号) private String studentNo; ExcelProperty(姓名) private String name; ExcelProperty(性别) private String gender; ExcelProperty(学院) private String college; // ... }一个容易踩的坑是性别解析。Excel里填写的是男/女数据库存的是1/2。我在监听器里做转换并且做了容错空值、非男非女的非法值要记录下来这一行是哪一行、为什么失败最后把失败原因反馈给前端让用户去改Excel重新上传。不能假装没看到就跳过也不能直接抛异常让整个流程中断。EasyExcel提供AnalysisEventListener的invoke方法逐行回调错误信息我存放在ListString errors里后面一次性返回。批量导入我用的是saveBatch的批量插入方式一次1000条以内毫无压力。另一个经验是导入前先按学号批量查询已有学生构造一个已存在学号的Set避免逐条去查数据库这种批量操作能省很多时间。3.5 移动端适配做一个简易版本宿舍管理系统的用户实际上分两类管理端楼栋管理员、宿管老师和普通学生。让学生下载APP不现实装一堆软件太重了。我的做法是做了一个手机浏览器可访问的H5简易端其实就是一个精简的Vue页面学生可以在这上面提交报修、查看通知、个人信息里的住宿信息。后端复用同一套API不需要额外开发。这块给这个项目的评分提升很有帮助因为很多同类毕设往往只有一套桌面端管理系统能覆盖移动端PC端双端访问的隐含需求会显得完成度高一个档次。前端通过Chrome的手机模拟模式就能演示不用真的部署原生App。4. 配置文件与多环境部署4.1application.yml设计的几个关键点SpringBoot核心是配置文件宿舍管理系统里我把它拆成了三套开发环境dev、测试环境test、生产环境prod。通过spring.profiles.active来激活不同配置。注意配置文件不要把所有环境塞在一起虽然用注释也可以区分但维护起来容易出问题。一个典型的application.yml骨架如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dormitory?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: ${MYSQL_PASSWORD} redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath*:mapper/*.xml logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 server: port: 8080几个值得说明的配置项用户名密码这一类的敏感信息千万不要硬编码在代码库里推送到Git远程仓库即使项目是个人项目也不建议习惯这么做。我用的方式是${MYSQL_PASSWORD}占位符在服务器上通过环境变量注入本地开发时设置本地的环境变量。这样代码仓库被别人拿到也泄露不了生产密码。serverTimezoneAsia/Shanghai是必须的MySQL 8的时区问题导致的时间差闹过好多次不加的话你会看到数据库时间和业务时间差了8小时。文件上传的大小限制报修图片一般不超过2MB学生头像一般几百KB所以max-file-size设5MB够用max-request-size要稍微大一点因为有批量上传的场景。MyBatis-Plus的逻辑删除配置是全局的一旦配置所有实体类加TableLogic注解的字段在查询时自动过滤已删除记录。这个功能很好用但要注意避免在唯一索引上使用逻辑删除。比如学生的学号加了唯一索引逻辑删除后学号不能重复插入现实中如果一个学生退学后清理数据再录入一个相同学号的新学生就会冲突。解决方法是给学号字段加一个删除标记作为联合唯一索引或者干脆物理删除——我自己后来选择了对student表做物理删除因为宿舍系统的数据量不大历史痕迹通过日志补偿。4.2 从JAR到服务器一条命令启动打包部署流程是我在整个项目里最顺畅的部分这里记一次完整的实操记录。后端使用Maven打包执行命令mvn clean package -DskipTests这里有个坑如果不加-DskipTestsMaven会跑所有测试类只要有一个测试环境配置不对打包就失败白白浪费时间。所以打包时跳过测试是常规操作测试质量靠代码走查和一个合适的测试类来保证而不是靠打包时机械地跑一遍。打包完成后目标目录下生成dormitory-system-0.0.1-SNAPSHOT.jar上传到服务器放到/opt/app目录下。使用一个简单的启动命令nohup java -Xms512m -Xmx1024m -jar dormitory-system-0.0.1-SNAPSHOT.jar \ --spring.profiles.activeprod app.log 21 解释一下几个参数-Xms512m是初始堆大小-Xmx1024m是最大堆大小。宿舍管理系统这种中小型应用给1GB绰绰有余。如果服务器内存只有2GB可以降到512MB。设置初始堆和最大堆相等或接近可以减少JVM动态扩容带来的性能损耗。用nohup和让进程在后台运行输出到app.log这样关掉SSH连接服务也不会停。要查看运行状态tail -f app.log如果日志正常出现SpringBoot的启动Banner然后出现Tomcat started on port 8080就说明启动成功了。前面的JWT密钥、数据库密码如果配置在环境变量里启动前用export设置好。关于前端部署我是把Vue项目执行npm run build之后生成的dist目录放到Nginx的html目录下然后通过Nginx反向代理后端接口。这块如果不熟悉可以先研究几个概念再看Nginx的location、proxy_pass、静态资源缓存。我在Nginx里的关键配置大概是这样的思路server { listen 80; server_name your-server-ip; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; # 前端路由模式是history时必须加 } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里最容易被忽略的是try_files那行。Vue的history路由模式在浏览器直接访问某个子路由路径比如/student/list时Nginx如果找不到对应的物理文件会返回404。加了try_files把请求重新导向index.html让前端路由自己处理这个路径问题就解决了。如果你不喜欢研究这行也可以把Vue路由改成hash模式URL带#不需要服务端配置但这在小团队内部演示时问题不大正式使用还是建议history模式。4.3 Docker部署可选的实践方式如果你的服务器有Docker环境还可以直接把后端做成一个镜像来跑写一个简单的DockerfileFROM openjdk:8-jre-alpine COPY dormitory-system-0.0.1-SNAPSHOT.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]镜像构建好后运行docker build -t dormitory-system:1.0 . docker run -d -p 8080:8080 --name dormitory-container \ -e MYSQL_PASSWORDyourpassword \ dormitory-system:1.0用Docker的好处是环境隔离Java版本、依赖库都封装在镜像里不会污染宿主机。但如果你不熟悉Docker用上文的手动java -jar方式也完全没毛病。我自己的经验是如果服务器上还跑着别的服务为避免端口冲突用Docker更顺手如果是全新的轻量服务器专门部署这一个系统直接JAR方式足够了。5. 踩坑记录与常见问题排查5.1 我实际踩过的最典型的几个坑第一个坑跨域请求失败。前端跑在8080端口后端跑在9090端口浏览器直接访问前端页面发起AJAX请求到后端浏览器拦截了——这是标准的跨域问题。解决方案是后端配置一个全局的CorsFilter或者在Controller类上使用CrossOrigin注解。我用的是一个WebMvcConfigurer的全局配置统一允许所有来源和方法并允许携带凭证。注意如果你在Spring Security里也做了CORS配置两处要一致否则可能双重放行或配置冲突表现就是明明没有跨域错误了却出现奇怪的OPTIONS预检请求403。第二个坑时间字段从数据库取出来传到前端后莫名其妙少了8小时。这个坑我之前提到过是时区问题。数据库DATETIME类型本身不包头时区MyBatis的LocalDateTime转成JSON时如果没有明确时区会采用JVM默认时区。我当时本地JVM默认是Asia/Shanghai东八区但服务器上JVM默认是UTC于是同一份代码在不同环境显示的时间差8小时。解决方案是不依赖JVM默认时区在Jackson配置里显式指定spring.jackson.time-zone: Asia/Shanghai spring.jackson.date-format: yyyy-MM-dd HH:mm:ss然后把MySQL连接串里serverTimezoneAsia/Shanghai也写上两头都锁死就永远不会乱。第三个坑Swagger文档界面能打开但点调试旁边的Try it out发请求时一直401。原因当然是拦截器把Swagger请求拦截了。排查思路很明确先看Swagger相关的URL路径是不是包含在excludePathPatterns里。检查后发现我只放行了/swagger-ui.html但knife4j的静态资源路径是/doc.html对应资源还有/webjars/**和/swagger-resources/**漏掉了这些。补充放行以后就正常了。用Swagger/knife4j时建议把这几个路径统一加到白名单里并且把生产环境是否暴露API文档考虑清楚上线以后文档内容里的字段信息可能泄露数据结构要小心。5.2 前端联调阶段最容易耗时间的问题前端和后端联调的时候最耗时间的往往不是逻辑错误而是小问题反复来回。第一个是字段名不一致。比如后端返回的字段是occupied前端写成了occupancy这种问题看接口文档一眼就能发现但如果前后端各做各的就会在调试工具里发现返回值都是undefined。所以接口文档Swagger要在后端写完接口后第一时间同步更新前端严格按照文档里的字段名开发。如果改字段名一起改完以后同步确认一遍。第二个是JSON格式的date字段处理。前端拿到2024-05-01 12:00:00这种字符串要展示在表格里没什么问题但如果拿它去比较时间、过滤列表就需要解析成Date对象这可能跨时区导致展示偏移。小团队通常就用字符串直接比较只要格式固定YYYY-MM-DD HH:mm:ss不会出问题。第三个是文件上传返回403。排查下来是Nginx的client_max_body_size默认是1MB报修图片超过1MB直接被Nginx拒绝了后端日志还看不到任何痕迹。把client_max_body_size 10m;加到Nginx配置的server或location块里才解决。现在很多系统的上传功能时好时坏大概率就是这一层在拦数据。第四个是Token过期导致页面静默失败。前端Axios请求返回401后用户界面没有任何提示看起来像功能坏了。我在Axios的响应拦截器里做了全局处理返回401时自动跳转到登录页同时清掉本地存储的记录。这就是一个让联调体验显著变好的小改造。5.3 性能排查一个自定义查询的优化案例有一次管理员反映报修列表在数据量到两万条以后打开很慢。我去查SQL发现直接用MyBatis-Plus的QueryWrapper做联查时IN子查询没有索引。比如查询某个楼栋的所有报修MyBatis-Plus自动生成的是SELECT * FROM repair_order WHERE room_id IN (SELECT id FROM dormitory_room WHERE building_id ?)这种子查询容易导致全表扫描。优化方案有两种一种是用JOIN代替IN子查询SELECT r.* FROM repair_order r JOIN dormitory_room dr ON r.room_id dr.id WHERE dr.building_id ?另一种是给repair_order.room_id建索引同时给dormitory_room.building_id建索引。因为业务量不大我两个方案都用上了一段SQL改写配合复合索引最后接口响应时间从八百多毫秒降到了几十毫秒。排查这类性能问题时不要凭感觉优化。我的方式是先打开MySQL日志把慢查询记录打开slow_query_logONlong_query_time1等待用户反馈问题然后查看慢查询日志找出耗时超过1秒的SQL执行EXPLAIN看执行计划确认是全表扫描还是索引失效。带上ETLExecution Time数据以后优化才有依据。如果上来就乱加索引反而会把写性能拖垮。6. 做这类系统我最后要分享的几条经验这个项目做完了我总结几条自己的体会。第一做管理系统一开始不要去抠技术细节先把业务流程在文档里画清楚。学生入住这个动作影响哪些表调宿和退宿有没有冲突报修取消后维修记录怎么保留这些业务规则比代码值钱得多代码只是规则的翻译器。第二不要执着于用最新版本。SpringBoot 2.7和3.x虽然都能做但3.x基于Jakarta EE部分老教程的代码不能直接用。做项目图的是稳定SpringBoot 2.7经过充分验证资料丰富遇到问题几乎都能搜到答案。如果你的学校或者公司对版本没有强制要求选一个成熟稳定的版本就好。第三安全不能再被忽视。宿舍管理系统看起来是内部工具但一样要处理好密码加密、SQL注入MyBatis不搞${}拼接就基本安全、越权访问学生只能调自己的报修接口不能用遍历ID的方式查别人的信息。我见过太多管理系统接口没有任何鉴权随便调一下接口就能看所有人的手机号和宿舍号。这部分工作不显眼但直接影响项目的质量评价。最后一个小技巧给正在做这类项目的朋友尽量在首个工作日把主流程跑通后面功能和细节往上面叠。如果你今天做一个模糊搜索明天研究一个列表分页项目的体感和完成度会很差。先跑通学生列表-录入-分配宿舍-查询入住信息这条链路哪怕界面丑一点整个系统就已经像样了。剩下的时间都花在打磨和扩展上你会松一口气的。
返回列表