简介:这份资源是面向计算机相关专业毕设学生与Java项目实战学习者的实验室管理系统全套交付包,可用于毕业设计、课程设计或期末大作业,解决选题缺方案、缺文档、缺可运行代码的问题。压缩包约80.62MB,包含项目源码、数据库脚本、开发说明文档、论文与代码注释等,源码基于SpringBoot与MySQL,在IntelliJ IDEA下开发,经调试可直接运行。功能覆盖登录注册、实验室申请、设备报备、消耗品领取与新闻资讯,后台侧提供用户、实验室申请、设备、消耗品及新闻资讯管理,论坛信息模块亦可作为扩展参考。读者可据此理清实体关系与接口分层,参照建表脚本还原数据,并借助论文与说明文档梳理需求分析、模块设计与测试章节的写作思路。该资源已有512人学习,适合需要完整项目骨架与文档范式的同学参考。
1. 实验室管理系统这套毕设包:一份能跑通的源码抵三天搭架子的时间
每年三四月,「实验室管理系统」这个题目就会被重新翻出来一遍。实验室预约、设备借用、耗材领用、角色权限,功能边界清楚,需求文档好写,看上去是个稳题目。真上手才知道稳的是题目不是实现:登录鉴权、菜单权限、预约冲突判断这几块拼起来,熟练工也要两三天,还得同步产出数据库设计、计算机论文和操作文档。
这套资源把四样东西放在一起:一份能启动的实验室管理系统源码、一套配套数据库脚本、一份按毕设章节组织的论文文稿、一份给非技术使用者看的操作文档。它解决的不是「从零学框架」,而是「我已经知道要做什么,但没时间把架子搭完、把论文骨架立起来」。适合三类人:赶毕业设计进度的、做课程设计不想在环境上耗掉一周的、以及拿到半成品想在它基础上做二次开发的。下面按「源码结构 → 数据库 → 论文文档 → 踩坑 → 验收」的顺序拆开。
2. 源码分层拆解:后端四层加前端三块,启动顺序反了就是一堆红字
拿到一个陌生源码包,最容易犯的错是双击 IDE 打开直接点运行。目录没看完、配置没核对、数据库还是空的,点下去满屏红字,然后开始怀疑包是坏的。血泪经验是:先看目录结构判断技术栈和分层习惯,再看配置文件确认端口和数据源,最后才按「库 → 后端 → 前端」的顺序启动。这个顺序不是讲究,是排错成本最低的路径。
2.1 后端为什么按 controller / service / mapper / entity 切四层
这个包的常见组合是 Spring Boot + MyBatis-Plus + Vue,后端按经典四层切开。controller 只做三件事:接参数、做@Valid校验、把结果包成统一的Result返回,业务判断一律不写在这里。service 层承载真正的逻辑,事务注解@Transactional也落在这一层,因为只有这一层能同时看到「预约表」和「设备状态表」,把事务放在 controller 会横跨 HTTP 边界,回滚不干净。
mapper 层用 MyBatis-Plus 的BaseMapper拿单表 CRUD,复杂的联表查询单独写在resources/mapper/*.xml里。选 MyBatis-Plus 而不是 JPA 的理由很实在:SQL 可控、分页插件开箱即用、答辩被问「这条 SQL 怎么写的」时能直接翻 XML 给人看,而 JPA 自动生成的那串 SQL 在现场很难解释清楚。entity 与表一一对应,dto 接前端入参,vo 是裁剪后的返回对象——这三层对象分开是最容易被新手省掉、也是后面最容易翻车的一步。
lab-management/ ├── lab-server/ # 后端 │ ├── src/main/java/com/example/lab/ │ │ ├── controller/ # 接口层:参数校验 + 结果封装 │ │ ├── service/ # 业务接口 │ │ │ └── impl/ # 业务实现,@Transactional 落在这里 │ │ ├── mapper/ # MyBatis-Plus Mapper 接口 │ │ ├── entity/ # 与表一一对应的持久化对象 │ │ ├── dto/ # 接收前端入参 │ │ ├── vo/ # 返回前端的裁剪对象 │ │ ├── config/ # 跨域、登录拦截器、分页插件 │ │ ├── common/ # Result、全局异常、常量 │ │ └── utils/ # JWT、日期、文件上传 │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ # 自定义 SQL 的 XML ├── lab-web/ # 前端 │ ├── src/api/ # 按模块拆的请求函数 │ ├── src/views/ # 页面 │ ├── src/router/ # 路由与菜单 │ ├── src/store/ # 用户信息与权限 │ └── src/utils/request.js # axios 拦截器,统一带 token └── sql/ └── lab_init.sql # 建库建表 + 初始化数据上面这份结构不是让你背,是让你在 IDE 里快速定位。改一个接口先看 controller 有没有对应方法,没有就往 service 找,再没有就看前端src/api里请求路径写的是不是后端真实的路径。新人最常见的时间浪费是「以为功能没实现」,实际是前端请求的 URL 少了一级前缀,后端根本没收到请求,日志里干干净净,一点线索都没有。
2.2 前端三块目录与接口代理的对应关系
前端按api / views / router三块组织。api目录里一个模块一个 js 文件,函数名就是接口语义,页面里只调函数不写完整 URL,这样后端换端口只改一处。views按角色和业务分目录,管理员、教师、学生看到的页面不是三套代码,而是同一套组件靠路由权限过滤。菜单权限常见做法是配置式:菜单数据存在数据库的菜单表里,前端登录后拉一次菜单树,动态注册路由——加一个页面不需要改前端路由文件,往菜单表插一行数据就行,这种插件式配置在二次开发时特别省事。
跨域是这一步绕不开的。前后端分开跑在不同端口,浏览器同源策略直接拦请求,所以开发期一定要配代理。代理的关键是rewrite与后端context-path必须对齐:后端没有前缀,转发时就去掉/api;后端配了/lab前缀,转发时就不能去,否则路径变成/lab/lab/xxx。
// lab-web/vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { // 前端所有 /api 开头的请求统一转发给后端,浏览器看到的还是同源 '/api': { target: 'http://localhost:8080', changeOrigin: true, // 后端接口本身没有 /api 前缀,转发时把它去掉 // 若后端 application.yml 里配了 context-path: /lab, // 这一行要改成 path.replace(/^\/api/, '/lab') rewrite: path => path.replace(/^\/api/, '') } } } })target指向后端实际监听地址,changeOrigin: true把请求头里的 Host 改成目标地址,某些网关或鉴权中间件会校验这个字段,不开就会 403。rewrite是纯粹的前缀改写,正则写错的表现是后端返回 404 且日志里能看到一条形如/lab/lab/reservation/list的请求——看到重复前缀,直接回来查这一行。
2.3 从 MySQL 到 8080 再到前端:启动顺序不能反
后端启动时会初始化数据源连接池,数据库没建、账号密码不对、驱动类名写错,三种情况报的错各不相同但都发生在启动阶段,和后端代码本身没关系。所以顺序固定为:先建库导数据,再改配置,再启后端,最后启前端。跳过第一步直接跑,报的是Unknown database;库建了但没导表,启动能过,第一次请求才炸Table doesn't exist,排查起来更绕。
# lab-server/src/main/resources/application.yml server: port: 8080 servlet: context-path: / # 改成 /lab 时,前端 proxy 的 rewrite 要同步改 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lab_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: 换成你自己的密码 servlet: multipart: max-file-size: 10MB # 设备照片、耗材凭证上传 max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 表字段 room_id 自动映射到 roomId log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 调试期开,答辩前关掉逐项说清楚。driver-class-name必须是com.mysql.cj.jdbc.Driver,老的com.mysql.jdbc.Driver在新驱动下会报警告甚至连不上。serverTimezone=Asia/Shanghai决定 JDBC 在 Java 时间和数据库时间之间怎么换算,漏写或写错的表现是页面上的预约时间整体偏 8 小时,而且只在某些机器上出现,非常玄学。map-underscore-to-camel-case关掉的话,所有下划线字段查出来都是 null,看起来像 SQL 写错了,实际是映射没开。log-impl打开后控制台会打印每条 SQL 和参数,调预约冲突那类问题时必开,但它会把密码等敏感参数一起打出来,交付前记得注释掉。
# 1. 建库并导入结构与初始数据 mysql -uroot -p -e "CREATE DATABASE lab_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p lab_manage < sql/lab_init.sql # 2. 后端:进入 lab-server 目录 mvn clean package -DskipTests java -jar target/lab-server.jar # 开发期也可以直接 mvn spring-boot:run # 3. 前端:进入 lab-web 目录 npm install npm run dev-DskipTests是为了跳过打包阶段的测试用例,有些包里带的测试依赖外部环境,不跳过会卡在打包。npm install失败优先看 Node 版本和镜像源,而不是怀疑包有问题——这类前端工程对 Node 大版本比较敏感,版本过高时构建工具会直接报语法错。
2.4 一份配置里必须改的五个参数
| 参数 | 位置 | 默认值 | 改成什么 | 不改的后果 |
|---|---|---|---|---|
| 数据源 url | application.yml | localhost:3306/lab_manage | 自己的库地址与库名 | 启动即失败 |
| 数据库密码 | application.yml | 空或示例值 | 本机密码 | Access denied |
| 服务端口 | application.yml | 8080 | 被占用时换 8081 | 端口冲突启动失败 |
| 前端代理 target | vite.config.js | http://localhost:8080 | 与后端端口一致 | 前端全部接口 404 |
| JWT 密钥 | application.yml 或常量类 | 示例字符串 | 自己换一串 | 谁都能伪造 token |
JWT 密钥这一项最容易被忽略。示例里的密钥是公开的,任何人拿到源码就能自己签一个管理员 token 直接调接口,本地演示无所谓,真部署到校内服务器上就是明摆着的洞。换成随机长字符串,改完重启后端,前端重新登录一次即可。
3. 数据库建表与初始化:七张核心表的外键、索引和时区怎么设
这套库的表不算多,十来张,真正绕不开的是七张:用户、角色、用户角色关联、菜单、菜单角色关联、实验室、预约。设备和耗材可以按需加,加了就是在预约表旁边多两张主表,关系模型不变。看数据库不要一上来就逐字段读,先看关联表把角色权限那条线捋顺,再看业务表之间的引用关系,最后才回头看字段类型和索引。
3.1 七张核心表的关系与字段取舍
权限那条线是三张表:sys_user存账号,sys_role存角色(管理员、教师、学生),sys_user_role是中间表。菜单那条线同样是三张:sys_menu存菜单项和按钮权限标识,sys_role_menu关联角色与菜单,前端登录后按当前角色查菜单树。这两条线拆开的好处是加角色不用改代码,插几行关联数据就行。
业务那条线以lab_room为中心,lab_reservation引用room_id和user_id。设备表lab_device挂在实验室下面,用room_id表示归属,不建物理外键,只建索引——这是这套代码里比较务实的取舍,物理外键在批量导入初始数据时会因为顺序问题报错,而毕设场景里数据一致性靠 service 层的事务保证就够了。
字段类型上有两个值得说的点。主键统一BIGINT AUTO_INCREMENT,不用 UUID,原因是自增主键在分页和联表时索引效率更好,而且前端传参不会出现一长串字符串。状态字段统一用TINYINT加注释,不用枚举字符串,因为 MySQL 里改枚举要改表结构,而TINYINT加一层常量类就能在 Java 侧约束取值范围。
3.2 建表脚本里的字符集、引擎与索引
建表脚本里最容易被跳过、出问题又最难查的三行是:ENGINE=InnoDB、CHARSET=utf8mb4、COLLATE=utf8mb4_general_ci。引擎用 InnoDB 是因为要事务和行锁,预约冲突那块完全依赖它;字符集必须是utf8mb4而不是utf8,后者在 MySQL 里其实只有三字节,存不了 emoji 和一些生僻字,实验室名称里带个特殊符号就插不进去,报错信息还特别含糊。
CREATE TABLE `lab_reservation` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `room_id` BIGINT NOT NULL COMMENT '实验室ID', `user_id` BIGINT NOT NULL COMMENT '申请人ID', `start_time` DATETIME NOT NULL COMMENT '开始时间', `end_time` DATETIME NOT NULL COMMENT '结束时间', `purpose` VARCHAR(200) DEFAULT NULL COMMENT '用途说明', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回 3已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_room_time` (`room_id`, `start_time`, `end_time`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='实验室预约表';idx_room_time这个联合索引的字段顺序不是随便排的。冲突检测的查询条件永远是「某个实验室 + 某个时间段」,room_id放在最左能先把范围缩到一个实验室的所有预约,再在索引内比较时间区间,避免回表。如果顺序反成(start_time, room_id, end_time),遇到热门实验室时扫描的行数会明显变多,数据量小的时候感觉不出来,初始化几百条测试数据后就能看出差别。
create_time用DEFAULT CURRENT_TIMESTAMP而不是在 Java 里new Date(),是为了保证不管从哪个入口写数据,时间都由数据库统一生成,避免应用服务器和数据库服务器时钟不一致带来的偏差。update_time加ON UPDATE CURRENT_TIMESTAMP同理,改一行数据不用手动维护这个字段。
3.3 初始化数据的顺序与幂等
初始数据导入的顺序由引用关系决定:先角色,再用户,再用户角色关联,然后菜单,最后角色菜单关联。顺序反过来会导不进去或者导进去一堆脏数据。另一个容易被忽略的点是幂等——如果脚本不是一次性执行,而是调试过程中反复跑,用INSERT会主键冲突报错,用INSERT IGNORE又可能悄悄跳过本该更新的数据。
-- 角色:固定 ID,方便后续关联,重复执行不报错 INSERT INTO `sys_role` (`id`, `role_code`, `role_name`) VALUES (1, 'ADMIN', '管理员'), (2, 'TEACHER', '教师'), (3, 'STUDENT', '学生') ON DUPLICATE KEY UPDATE `role_name` = VALUES(`role_name`); -- 用户:密码字段存的是加密后的串,导入后首次登录需要按文档说明重置 INSERT INTO `sys_user` (`id`, `username`, `password`, `real_name`, `status`) VALUES (1, 'admin', '导入后按文档重置', '系统管理员', 1) ON DUPLICATE KEY UPDATE `real_name` = VALUES(`real_name`); -- 关联:先插用户角色,再插角色菜单 INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1) ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);ON DUPLICATE KEY UPDATE依赖主键或唯一索引,所以初始化数据里的 ID 必须写死,不能靠自增。这一点在扩充数据时要注意:手工插一条角色不指定 ID,下次再跑脚本就可能和写死的 ID 撞上。我一般会在脚本开头加一段注释,写清楚「本文件可重复执行,但新增数据请顺延 ID」。
密码字段是另一个高频问题。初始化脚本里存的通常是加密后的串,直接拿明文去登录必然失败。操作文档里一般会写清楚默认密码和重置方式,导入后第一件事就是按文档改一次密码,别在这上面耗时间。
3.4 预约冲突的判断逻辑与并发边界
预约功能的核心不是「能不能提交」,而是「两个人同时提交同一时间段,会不会都成功」。判断区间是否重叠有一个很简洁的写法:两个区间重叠,当且仅当一个的开始时间早于另一个的结束时间,且这个的结束时间晚于另一个的开始时间。两个条件都必须是严格不等号,等号意味着首尾相接,比如 10:00–12:00 和 12:00–14:00 不算冲突。
SELECT COUNT(1) FROM lab_reservation WHERE room_id = #{roomId} AND status IN (0, 1) -- 待审和已通过都占用时段,驳回和取消不占 AND start_time < #{endTime} -- 严格小于:12:00 结束不挡 12:00 开始 AND end_time > #{startTime} -- 严格大于:10:00 开始不挡 10:00 结束参数roomId来自前端选择的实验室,startTime和endTime是本次申请的时段,全部走预编译占位符,不要字符串拼接。这条查询返回大于 0 就表示冲突,service 层直接抛业务异常,前端提示「该时段已被占用」。
但这条 SQL 单独用是挡不住并发的。两个请求几乎同时进来,都查到 0,都执行插入,最后两条都写进去了。常见做法有两种:一种是在 service 方法上加事务,并对lab_room表里对应那行做SELECT ... FOR UPDATE,用行锁把同一实验室的预约请求串行化;另一种是给预约表加一个由room_id + start_time组成的唯一索引,插入冲突时捕获DuplicateKeyException转成友好提示。前者锁粒度可控但要注意加锁顺序,后者实现简单但只能挡住开始时间完全相同的场景,跨时段的冲突还得靠查询。这个包默认给的是第一种,二次开发时别把事务注解删了,删了就等于把并发保护一起删了。
4. 论文与操作文档的用法:把功能点翻译成系统设计与测试章节
论文文稿在这个包里不是给你直接交的范文模板,是一副骨架。骨架的价值在于章节结构和功能点的对应关系已经排好了,你要做的是把里面的通用描述换成自己系统的实际内容。直接整段照搬的风险很实在:需求分析那一章是重复率最高的地方,模板里的用例描述被无数人用过,查重系统一比就出来。
4.1 六章骨架与功能点的映射关系
多数院校的本科毕设模板都是六章结构,这个文稿也按这个结构组织。关键是把系统里每个模块落到具体章节,写的时候心里有一张对照表,就不会出现「实现章节写了一半发现设计章节没提这个模块」的情况。
| 论文章节 | 对应系统模块 | 建议配图 | 源码/文件位置 |
|---|---|---|---|
| 第一章 绪论 | 选题背景、研究现状、主要工作 | 无 | 无 |
| 第二章 需求分析 | 三类角色的功能需求与非功能需求 | 用例图、功能结构图 | 前端路由表 |
| 第三章 系统设计 | 架构分层、模块划分、数据库设计 | 架构图、ER 图 | sql/lab_init.sql |
| 第四章 系统实现 | 登录鉴权、预约管理、设备与耗材 | 时序图、界面截图 | controller / service |
| 第五章 系统测试 | 功能测试、边界测试、并发测试 | 测试用例表 | 无 |
| 第六章 总结与展望 | 已完成工作、不足与改进方向 | 无 | 无 |
对照表里最值得注意的是第三章和第四章的分工。设计章节讲「为什么这么设计」,实现章节讲「怎么实现的」。ER 图、架构图放第三章,时序图和界面截图放第四章,别混着放。时序图不用每个功能都画,挑预约这一条主链路画完整就够了:用户提交 → 拦截器校验 token → controller 收参 → service 查冲突 → mapper 插入 → 返回结果,六个参与者,画清楚比画多有用。
4.2 图怎么出:从建表脚本反推比手工画快
ER 图最省事的做法不是对着表一个个手工拖,而是用建模工具连上本地库反向生成,再手工调整布局。字段多的时候手工画容易漏关系,反过来生成至少保证字段和类型是对的。生成之后重点检查三处:关联表有没有画成多对多、主键外键标注是否清楚、每张表的注释有没有带上——注释带上,图看起来就专业很多。
用例图按角色分三块画,管理员、教师、学生各一个边界,连线到具体用例。功能结构图用树形,一级是模块名,二级是具体功能,层级不要超过三层,超过三层打印出来就糊了。界面截图有个细节:截图前把浏览器窗口调成统一宽度,把地址栏和书签栏隐藏,截图后用同一套边框处理,几页截图放在一起观感完全不一样。这项不费什么技术,但答辩时老师翻论文的第一印象就在这。
4.3 测试章节怎么写才不像凑字数
测试章节最容易写成「点击登录按钮,登录成功,测试通过」这种流水账。有说服力的写法是把用例拆成「前置条件 / 操作步骤 / 预期结果 / 实际结果」四列,并且专门留几条边界用例。边界用例是加分项,也是这套系统真正有测试价值的地方:
| 用例编号 | 测试场景 | 输入 | 预期结果 |
|---|---|---|---|
| TC-01 | 正常预约 | 空闲时段、合法用户 | 提交成功,状态为待审 |
| TC-02 | 时段完全重叠 | 与已有预约同室同时段 | 提示时段冲突,不落库 |
| TC-03 | 时段首尾相接 | 上一段 12:00 结束,本段 12:00 开始 | 允许提交 |
| TC-04 | 开始时间晚于结束时间 | 开始 14:00,结束 10:00 | 参数校验拦截 |
| TC-05 | 并发提交同一时段 | 两个账号同时提交 | 仅一条成功,另一条提示冲突 |
| TC-06 | 未登录访问接口 | 不带 token 直接请求 | 返回未授权,不泄露数据 |
TC-05 这条能写出来并且附上实际测试结果,说明你真的想过并发问题,比写十条「点击按钮成功」有用。测的时候可以用两个浏览器窗口,或者用接口工具同时发两个请求,把结果截图放进去。
4.4 操作文档的读者不是答辩老师
操作文档和技术文档是两回事。技术文档写给开发者看,讲架构讲接口;操作文档写给使用者看,讲「我要预约一间实验室,第一步点哪里」。这个包里的操作文档按角色分节:管理员管用户和实验室、教师审预约、学生提交申请,每节从登录开始,一步一张图,图下面一句话说明。
写操作文档有个反直觉的点:不要写「点击左侧菜单栏第三个选项」这种位置描述,菜单顺序一改文档就废了。改成写菜单名称,「点击左侧【预约管理】」,位置变了文字还对。同理,不要写「输入用户名和密码后点击蓝色按钮」,写「填写账号密码后点击【登录】」。文档的寿命取决于它有多不依赖界面细节。
5. 跑不起来与答辩翻车:五个高频坑的现象、原因与解决
这套包从解压到跑通,绝大多数人卡在下面五个地方。按出现频率排,前三个基本人人都会遇到一次。每一条我都按「现象 → 原因 → 解决」写,照着对症状找就行。
5.1 启动报数据库连不上,改密码也没用
现象:后端启动直接失败,控制台抛Communications link failure或者Access denied for user,反复确认密码没错。
原因:八成不是密码问题。Communications link failure通常是 MySQL 服务没起、端口不是 3306、或者 url 里的库名写了一个不存在的库。Access denied除了密码错,还有一个常见原因是allowPublicKeyRetrieval没开,新版本驱动在非 SSL 连接下需要这个参数才能完成认证。
解决:先单独验证数据库通不通,mysql -uroot -p -e "show databases;"能列出来说明服务和账号都没问题,问题就在 url 上。逐段核对 url:主机、端口、库名、时区参数、useSSL=false、allowPublicKeyRetrieval=true,五段全对再启动。库名拼错一个字母,报的错和密码错长得几乎一样,别在这上面反复试密码。
5.2 前端页面能打开,所有接口却 404
现象:浏览器里页面正常渲染,登录按钮点下去没反应,F12 网络面板里请求全是 404,后端日志一条记录都没有。
原因:请求根本没到后端。要么代理没配,要么代理的rewrite写错,要么前端请求的路径和后端context-path对不上。后端日志干净是判断「请求没到达」的关键信号——如果请求到了后端但路径不对,日志里会有 404 记录。
解决:先在网络面板看请求的实际 URL。如果是http://localhost:5173/api/xxx且返回 404,说明代理没生效,检查vite.config.js有没有被正确加载,改完配置必须重启 dev server。如果请求 URL 里出现重复前缀如/lab/lab/xxx,就是rewrite和后端前缀打架,按 2.2 节那行注释改。还有一种情况是前端request.js里的baseURL自己又拼了一层/api,和代理叠加,找出来删掉一层即可。
5.3 登录成功,刷新页面就退回登录页
现象:登录后能进主页,按 F5 刷新一下直接跳回登录页,token 好像丢了。
原因:token 只存在了内存里(比如 Vuex/Pinia 的 state),刷新页面状态重置,路由守卫读不到 token 就判定未登录。另一种原因是路由守卫读的是localStorage,但登录时写的是sessionStorage,两个存储对不上。
解决:登录成功后同时写localStorage,路由守卫统一从localStorage读,请求拦截器也从这个位置取 token 塞进 header。改完检查三处是否一致:写入位置、路由守卫读取位置、axios 拦截器读取位置。三处不一致是这类问题最常见的成因,改一处没用。
5.4 两个人同时预约同一时段,两条都成功了
现象:测试时开两个浏览器,同一实验室同一时段同时提交,居然都显示预约成功,列表里出现两条重叠记录。
原因:3.4 节说的先查后插竞态。查询和插入之间有时间窗口,两个请求都查到了「无冲突」。如果代码里还把@Transactional删了或者加在了 controller 上,连基本的事务边界都没有。
解决:确认@Transactional在 service 实现类的方法上,并且是 public 方法。然后在查询前对实验室行加FOR UPDATE,或者按 3.4 节加唯一索引兜底。改完必须重测:两个窗口同时提交,预期一个成功一个失败,把这个结果截图存进论文的测试章节,一举两得。
5.5 论文查重偏高,降重又降得语句不通
现象:文稿拿去查,重复率卡在警戒线附近,改了几轮改得自己都读不懂。
原因:需求分析和国内外现状这两章照模板保留了太多原句,这两块本来就是公共描述最多的地方。另一类重复来自代码截图和表格,有些系统会把连续多行代码也算进重复。
解决:需求分析那章不要保留模板原句,改成用自己的系统功能反推——你的系统里学生能做什么、教师能做什么,按你实际的路由表写,写出来天然和别人不一样。代码不要整段贴,只贴关键方法,前后加两句自己的解释,解释的话是你写的,重复率就下来了。图表里的文字也要检查,直接从模板复制来的表格标题同样会被算进去。
6. 二次开发验收:从能登录到能答辩的一次完整走查
跑通不等于能交。答辩现场老师不会只看你点一下登录,多半会让你现场演示一条完整业务链路,再问一句「这个功能如果加个审批你怎么改」。所以最后一件事是把系统按「一条业务链路」走一遍,并且真的动手加一个小功能,验证自己理解了这套代码的扩展方式。
拿「设备借用审批」举例,这套表结构里lab_reservation的status已经预留了 0/1/2 三个状态值,加审批不用改表。后端在 controller 加一个@PutMapping("/audit")接口,入参是预约 ID 和审核结果,service 里更新状态并写一条操作日志,权限用拦截器加角色注解控制,只有教师和管理员能调。前端在列表页的每行操作列里按当前角色决定是否渲染「审核」按钮,点击后弹一个确认框调接口,成功后刷新列表。
// 审核接口的核心逻辑,事务保证状态与日志同时成功或同时失败 @Transactional(rollbackFor = Exception.class) public void audit(Long reservationId, Integer status, Long auditorId) { LabReservation db = reservationMapper.selectById(reservationId); if (db == null) { throw new BizException("预约记录不存在"); } // 只有待审状态能被审核,防止重复点击把已通过的改成驳回 if (db.getStatus() != 0) { throw new BizException("该预约已处理,请刷新后重试"); } db.setStatus(status); // 1 通过,2 驳回 reservationMapper.updateById(db); usageLogService.record(auditorId, reservationId, status); // 操作留痕 }这段代码里有两个细节值得说。rollbackFor = Exception.class是为了让受检异常也触发回滚,默认只回滚运行时异常,漏写的话日志写失败但状态已改,数据就对不上。状态判断放在更新前,是为了挡住重复提交,前端连点两下按钮在真实场景里太常见了。
答辩前的走查清单我一般会固定跑一遍:
| 检查项 | 操作路径 | 通过标准 |
|---|---|---|
| 三种角色登录 | 学生/教师/管理员各登录一次 | 菜单不同,越权接口返回未授权 |
| 预约主链路 | 提交 → 审核 → 查看结果 | 状态流转正确,列表实时刷新 |
| 冲突拦截 | 同室同时段提交两次 | 第二次提示冲突 |
| 空数据与异常 | 删除一条被引用的实验室 | 提示无法删除,不出现 500 |
| 时间显示 | 跨时区改系统时间后查看 | 预约时间不偏移 |
| 日志开关 | 检查 application.yml | 交付版本已关闭 SQL 打印 |
最后一项是我踩过坑之后才加进清单的。早期交付时忘了关log-impl,演示时控制台刷刷打印 SQL,把用户表里的密码字段一起打在了大屏上,场面很难看。从那以后我每次交付前都强制走一遍这份清单,尤其是日志开关和时间显示这两项,功能对不对一眼能看出来,这两项不出声,出事就是当众出事。希望帮到你。
本文还有配套的精品资源,点击获取