JavaWeb研究生实验室管理系统:组会排期、设备预约与权限闭环设计

发布时间:2026/10/30 2:49:53
JavaWeb研究生实验室管理系统:组会排期、设备预约与权限闭环设计 简介这是一套面向高校研究生实验室场景的 JavaWeb 管理系统源码与运行依赖包围绕导师与学生两类角色组织功能可用于课程设计、毕业设计参考或实验室内部协作管理学习。系统覆盖学生工作汇报与代码上传、教师查看与答复、会议创建及限时讨论、消息提醒、历史会议查询以及请假等事务处理学生端还提供项目创建、公开代码浏览、组会汇报与消息查看较完整地呈现了权限区分和流程闭环。压缩包共 246 个文件约 30.63MB包含 62 个 Java 源文件与 62 个 class 编译文件、24 个 JSP 页面、16 个 XML 配置、8 个 properties 配置以及 54 个 jar 依赖和少量图片、CSS 等静态资源便于直接部署调试或按模块拆解学习。已有 589 人学习下载。读者可从中梳理角色权限、汇报与会议讨论、项目及代码可见性等业务实现也能参考 JSP 页面与 Java 分层代码的组织方式适合具备 JavaWeb 基础、希望理解中小型管理系统落地的学习者。1. 实验室里最贵的不是设备是协调成本组会排期靠群里接龙谁请假、谁改时间全靠人工记忆设备预约写在白板上两个人同时写上去就只能比谁先到学期末统计论文和专利导师翻遍聊天记录回收 Excel新生入学想找去年的组会纪要最后找到的是三份命名不同的 Word。这些事单看都不难凑在一起就是每学期开学第一周的固定混乱。所以要做一套 javaweb 研究生实验室管理系统核心不是把纸质表格搬到网页上而是把实验室管理里最依赖口头约定的四件事——会议排期、设备预约、成果归档、角色权限——变成有状态、有归属、有操作记录的数据。它的适用面很实在正在找毕设或课设题目的同学可以拿它当一个模块闭环完整、边界清晰、答辩时有话可讲的选题刚接手实验室日常事务的管理员也能照着这套结构搭一套内部工具。2. 需求拆解与技术选型四类角色、八张功能清单和一套答辩时讲得清的技术栈需求阶段最容易翻车的地方是把“实验室管理系统”写成一串功能名词的罗列。真正决定后面代码结构的是三个问题谁在用、他在一天里会点几次、点错了会有什么后果。学生每周点两次看组会安排、约设备导师每周点一次审成果、看纪要管理员每天点排期、耗材、公告系统管理员一个学期点两次加人、调权限。点击频率不同页面复杂度就该不同权限粒度也该不同。2.1 角色与权限矩阵谁在什么位置上能做什么角色划分成四类足够用再细分会让课设阶段的权限表失控学生、导师、实验室管理员、系统管理员。学生是数据的主要生产者导师是数据的审核者实验室管理员是资源的分配者系统管理员只碰账号和字典。把“审核”和“分配”分开是为了避免一个账号既当运动员又当裁判——这是答辩时最容易被追问的点。功能模块学生导师实验室管理员系统管理员会议排期 / 报名查看、报名、请假创建、改期、取消查看全部、代排无会议纪要查看本组撰写、发布归档、检索无设备预约申请、取消自己的审批本组全量审批、维护台账无成果登记提交论文/专利/软著审核本组汇总导出无耗材申领提交审批采购入库、扣减无公告通知查看发布本组发布全室无用户与角色改自己密码无重置学生密码增删改、分配角色数据字典 / 日志无无查看操作日志维护字典这张矩阵要落到代码里就是“角色—权限码—接口”三层映射而不是在页面上用v-if把按钮藏起来。后面 4.3 节会给出具体实现这里先记住一条矩阵里每一个“审批”格子都必须对应后端一个独立的接口和一条数据范围条件。2.2 技术选型为什么是 Spring Boot MyBatis 这一套后端选 Spring Boot理由不是它流行而是它在课设和毕设这个体量下能把配置成本压到最低内嵌容器直接java -jar跑起来答辩换台电脑不用装 Tomcatstarter 依赖把 JDBC、事务、JSON 序列化一次带齐application.yml里改端口、改数据源不用动代码。版本上 Spring Boot 2.7 这一代对 JDK 8 友好学校机房和老笔记本都能跑如果本机已经是 JDK 17用 3.x 也没问题注意 3.x 里javax.*换成了jakarta.*网上抄来的拦截器代码容易在HttpServletRequest这一行报错。持久层用 MyBatis 而不是 JPA是这类系统的数据形态决定的。实验室管理里 80% 的查询是“多条件筛选 多表联查 分页”比如“查某导师名下学生在某学期的组会出席情况”写成Query会很难读写成 XML 或注解 SQL 反而一目了然答辩时还能指着 SQL 说清走了哪个索引。前端两条路时间紧就用 Thymeleaf 做服务端渲染一套模板搞定列表和表单不用处理跨域和 token想让界面好看、顺便练一下前后端分离就用 Vue 3 Element Plus后端统一返回{code, msg, data}。其余组件的取舍权限框架Spring Security 完整但学习曲线陡课设阶段用“登录拦截器 自定义RequireRole注解 AOP”完全够用也更好讲如果题目要求里明确写了安全框架再上 Spring Security JWT。缓存Redis 只在两个地方真正需要——会议提醒的延迟队列、高频访问的字典数据。没有这两个场景就别引入多一个中间件就多一个答辩时要解释的东西。定时任务会议提前提醒、超时未审批的预约自动作废用 Spring 自带的Scheduled就够别上 Quartz。文件存储本地磁盘按年月分目录数据库只存相对路径理由在 5.4 节展开。2.3 功能清单与优先级先做闭环再做加分项课设和毕设的时间窗口通常只有 8 到 12 周功能必须分层。分层原则是“主流程能不能从头走到尾”而不是“功能多不多”。会议模块是这套系统的骨架因为它串起了角色谁主持、谁参加、资源哪个会议室、产出纪要、成果把它做透其他模块都是同构的增删改查。模块必做闭环加分项有余力再做演示价值认证与权限登录、角色、菜单过滤、接口鉴权找回密码、操作日志高答辩必看会议管理排期、冲突检测、报名、请假、纪要周期组会批量生成、日历视图最高核心亮点设备管理台账、按时段预约、审批、归还二维码扫码借用、超期提醒高成果管理登记、审核、按人/学年统计导出 Excel、图表中高耗材管理申领、审批、库存扣减低库存预警中通知公告发布、置顶、已读标记站内信 会议提醒联动中统计报表组会出勤率、设备使用率大屏看板中必做列里每一项都要能独立演示一遍加分项只挑两个做。我一般会建议学生把“周期组会批量生成”和“导出 Excel”这两个做掉因为前者能体现业务理解后者在答辩现场最容易做出效果。3. 总体架构设计三层结构、十二张表和一条会议状态机架构设计这部分很多方案会画一张很漂亮的图然后写“采用分层架构便于扩展”。这话没错但没用。真正需要在设计阶段定下来的只有三件事一次请求从浏览器到数据库经过哪几层、每层负责什么数据表之间靠什么字段关联有状态的对象比如一次会议允许在哪些状态之间跳转。这三件事定完剩下的都是填空。3.1 分层结构与包组织一次请求要经过哪五道关采用经典的 Controller–Service–Mapper 三层中间补上 DTO/VO 转换和统一异常处理实际是五道关。分层不是为了好看是为了让“改一个地方不影响另一个地方”这件事成立。比如后来要求会议室冲突规则从“同房间不重叠”改成“同房间且同类型不重叠”只改 Service 里的一个判断Controller 和 Mapper 都不用动。层职责典型类不该做的事Controller接参、校验、返回统一结构MeetingController写业务判断、直接调 MapperService业务规则、事务边界、数据范围MeetingServiceImpl拼 HTML、处理HttpServletRequestMapperSQL、结果映射MeetingMapper写 if-else 业务分支DTO / VO入参校验、出参脱敏MeetingForm、MeetingVO直接暴露实体里的password公共层统一返回、异常、拦截器、工具Result、BizException、AuthInterceptor依赖具体业务 Service包组织按“业务模块优先、技术分层其次”来切比纯按controller/service/mapper平铺更好维护。一个模块一个包包里再分层新增“耗材管理”时就是复制一个目录的事不会在三个大包里到处找文件。3.2 数据库表设计十二张表撑起整套业务表设计的关键是别把“会议”和“会议纪要”塞进一张表也别把“角色”写成用户表里的一个role字符串字段。前者会导致一条会议记录反复被 UPDATE 覆盖历史后者会导致一个人只能有一个角色、加一个角色就要改表结构。-- 用户表只放账号本身角色通过关联表挂 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 学号/工号登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt 加密后的密文, real_name VARCHAR(32) NOT NULL, user_type TINYINT NOT NULL DEFAULT 1 COMMENT 1学生 2导师 3实验室管理员 4系统管理员, tutor_id BIGINT NULL COMMENT 学生的导师ID用于数据范围过滤, grade VARCHAR(16) NULL COMMENT 年级如 2023级, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_tutor (tutor_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 角色表 用户角色关联表一个人可以既是导师又是管理员 CREATE TABLE sys_role ( id BIGINT NOT NULL AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL COMMENT STUDENT/TUTOR/LAB_ADMIN/SYS_ADMIN, role_name VARCHAR(32) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_code (role_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表; -- 会议主表一条记录就是一次组会/学术报告状态字段驱动流转 CREATE TABLE lab_meeting ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(120) NOT NULL COMMENT 会议主题, meeting_type TINYINT NOT NULL DEFAULT 1 COMMENT 1组会 2学术报告 3预答辩, room_id BIGINT NOT NULL COMMENT 会议室/实验室ID, host_id BIGINT NOT NULL COMMENT 主持人, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已发布 2进行中 3已结束 4已取消, remind_before INT NOT NULL DEFAULT 30 COMMENT 提前多少分钟提醒, create_by BIGINT NOT NULL, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), KEY idx_room_time (room_id, start_time, end_time), KEY idx_status_time (status, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会议表; -- 参会表报名/请假状态存在这里不存到会议表上 CREATE TABLE lab_meeting_attendee ( id BIGINT NOT NULL AUTO_INCREMENT, meeting_id BIGINT NOT NULL, user_id BIGINT NOT NULL, join_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未响应 1参加 2请假, leave_reason VARCHAR(200) NULL, sign_time DATETIME NULL COMMENT 实际签到时间, PRIMARY KEY (id), UNIQUE KEY uk_meeting_user (meeting_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会议参会表;上面五张表加设备、预约、成果、耗材、公告、日志、字典共十二张是一套能跑通全流程的最小集合。几个刻意的设计点值得在答辩时讲lab_meeting_attendee上的唯一索引uk_meeting_user是从数据库层面阻止同一个人被重复拉进同一个会议比在 Service 里if (exists)查一次再插更可靠——并发下查和插之间是有窗口的。idx_room_time这个联合索引是为冲突检测的 SQL 服务的冲突查询一定是“按房间 按时间区间”索引顺序不能反。用户的角色用关联表是为了让“导师兼任实验室管理员”这种真实情况不用改表结构。一个人多角色在高校场景里是常态不是例外。所有业务表统一带deleted逻辑删除因为设备、会议这些数据一旦被物理删除历史统计就对不上了到时候没有后悔药。3.3 会议状态机五个状态和四条非法跳转会议是这套系统里唯一有明确生命周期的对象。把状态画出来业务规则就不用散落在各个if里。允许的流转是草稿 → 已发布 → 进行中 → 已结束以及草稿/已发布 → 已取消。非法跳转包括“已结束改回已发布”“已取消重新激活”“草稿直接跳到已结束”。public enum MeetingStatus { DRAFT(0, 草稿), PUBLISHED(1, 已发布), ONGOING(2, 进行中), FINISHED(3, 已结束), CANCELED(4, 已取消); private final int code; private final String label; MeetingStatus(int code, String label) { this.code code; this.label label; } public int getCode() { return code; } public String getLabel() { return label; } // 用一个静态表描述合法流转新增状态时只改这里 private static final MapMeetingStatus, SetMeetingStatus TRANSFER Map.of( DRAFT, Set.of(PUBLISHED, CANCELED), PUBLISHED, Set.of(ONGOING, CANCELED), ONGOING, Set.of(FINISHED), FINISHED, Set.of(), CANCELED, Set.of() ); public static void checkTransfer(MeetingStatus from, MeetingStatus to) { if (!TRANSFER.getOrDefault(from, Set.of()).contains(to)) { throw new BizException(会议状态不允许从[ from.label ]变更为[ to.label ]); } } }这段枚举的价值在于状态合法性和状态码定义放在一起Service 里只需一行MeetingStatus.checkTransfer(current, target)不用每加一个操作就复制一遍判断。TRANSFER用Map.of声明成不可变结构防止某处代码顺手往里塞一条规则导致行为漂移。实际项目中还会在lab_meeting表旁边挂一张lab_meeting_log每次状态变更写一条“谁、何时、从什么状态到什么状态、备注”这张表在答辩演示“取消会议后通知参会人”时特别有用也是排查线上问题的黑匣子。4. 核心模块实现组会排期、设备预约、权限拦截和文件归档前面三章定的是骨架这一章填肉。四个模块分别对应四类典型问题时间区间怎么判重叠、并发怎么防超卖、权限怎么防越权、文件怎么防丢。这四件事在任何一个管理系统里都会遇到做完这套换一个题目也能直接搬。4.1 组会排期时间区间重叠到底怎么判新手常见的写法是先查当天的会议列表再在 Java 里 for 循环比较。这种写法在数据量小时看不出问题一旦会议室和会议数上去每次排期都要把当天所有记录拉进内存而且容易漏掉“跨天会议”这种边界情况。正确做法是把判断交给数据库用一条带索引的 SQL 解决。判断两个时间区间[s1, e1)和[s2, e2)是否重叠等价条件是s1 e2 AND e1 s2。注意这里是严格小于和严格大于表示左闭右开区间——10:00–11:00 和 11:00–12:00 不算冲突这样排期才符合直觉。-- 冲突检测同一会议室、状态为已发布或进行中、且时间区间有交集 SELECT COUNT(1) FROM lab_meeting WHERE room_id #{roomId} AND status IN (1, 2) AND deleted 0 AND start_time #{endTime} AND end_time #{startTime} AND id IFNULL(#{excludeId}, 0); -- 编辑时排除自己对应 Service 层创建和修改共用同一段校验区别只在excludeId传不传Transactional(rollbackFor Exception.class) public Long saveMeeting(MeetingForm form, Long userId) { // 1. 基础时间校验结束必须晚于开始且不能排到过去 if (!form.getEndTime().isAfter(form.getStartTime())) { throw new BizException(结束时间必须晚于开始时间); } if (form.getStartTime().isBefore(LocalDateTime.now())) { throw new BizException(不能给已经过去的时间排会); } // 2. 会议室冲突走 idx_room_time 索引 Long excludeId form.getId() null ? null : form.getId(); if (meetingMapper.countConflict(form.getRoomId(), form.getStartTime(), form.getEndTime(), excludeId) 0) { throw new BizException(该时段会议室已被占用请更换时间或场地); } // 3. 参会人冲突主持人同一时段不能有另一场会跨会议室也要拦 if (meetingMapper.countHostConflict(form.getHostId(), form.getStartTime(), form.getEndTime(), excludeId) 0) { throw new BizException(主持人在该时段已有其他会议); } // 4. 落库 生成参会记录默认拉入主持人名下的学生 ... // 5. 状态从草稿变为已发布时才推送通知草稿不发 if (form.getStatus() MeetingStatus.PUBLISHED.getCode()) { notifyService.pushMeetingNotice(meetingId, CREATE); } return meetingId; }这段代码有四个参数值得说明清楚。roomId是会议室主键不是实验室主键一个实验室可以有多个会议室粒度要提前定死。excludeId在新增时为nullSQL 里用IFNULL把它兜成 0避免id null恒为假导致编辑时永远检测不到冲突——这是很常见的一个静默 bug。status IN (1,2)表示只有“已发布”和“进行中”的会议占场地草稿和已取消不占这样管理员可以先把几套方案存成草稿再逐个发布。第 3 步的主持人冲突容易被漏掉但它才是现实里最常见的冲突同一导师名下两个学生在同一时间各约了一场组会。编辑场景还有个细节把会议从 10:00 改到 15:00如果先 UPDATE 再校验校验时查到的是自己改后的记录永远通过。所以顺序必须是先校验、后更新而且校验要排除自身。4.2 设备预约把并发压在一条唯一索引上设备预约和会议室排期看起来是同一个问题其实不是。会议室是“一个时间段只能一场会”设备是“一台设备在一个时段只能被一个人借”而用户点“预约”按钮的动作是并发的。两个学生同时打开同一台显微镜的预约页都看到可借都点提交先查后插的写法就会双双成功一条设备被借两次。悲观锁SELECT ... FOR UPDATE能解决但会把并发压成串行还要小心死锁。更轻的方案是把时段离散化用唯一索引兜底把一天切成固定长度的槽位比如按 2 小时一格一天 12 格每次预约往槽位表里插一条记录靠唯一索引让第二次插入直接失败。-- 设备预约槽位表一条记录 某台设备某天某个时段被占用 CREATE TABLE lab_device_slot ( id BIGINT NOT NULL AUTO_INCREMENT, device_id BIGINT NOT NULL, slot_date DATE NOT NULL COMMENT 预约日期, slot_index TINYINT NOT NULL COMMENT 第几个时段0-11, user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1占用 0已释放, PRIMARY KEY (id), UNIQUE KEY uk_device_slot (device_id, slot_date, slot_index, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备预约槽位表;Transactional(rollbackFor Exception.class) public void reserve(Long deviceId, LocalDate date, ListInteger slots, Long userId) { // 1. 设备状态与用户资格校验是否禁用、是否需要培训认证 DeviceVO device deviceService.getAvailable(deviceId); // 2. 逐槽位插入唯一索引 uk_device_slot 负责并发互斥 for (Integer slot : slots) { try { slotMapper.insert(deviceId, date, slot, userId, 1); } catch (DuplicateKeyException e) { // 捕获唯一键冲突转成业务提示事务整体回滚 throw new BizException(时段 slot 已被预约请刷新后重选); } } // 3. 生成预约单主记录挂上本次占用的槽位方便后续整体取消 reservationMapper.insert(buildReservation(deviceId, date, slots, userId)); }把status也放进唯一索引是为了解决“取消后可再约”的问题直接删槽位记录会让历史查不到把status改成 0 又会撞唯一键所以用(device_id, slot_date, slot_index, status)四列组合一条被释放的槽位是status0新的占用是status1两者共存互不冲突。代价是同一时段最多只能有一条释放记录如果业务要求“取消三次还能再约”就把status换成自增的occupy_seq逻辑一样。批量预约时要注意事务边界整批要么全成、要么全败不能出现“三个时段占了两个”。所以Transactional加在方法上中间任何一次DuplicateKeyException都抛出去让整批回滚——用户看到的是“有一个时段冲突请重选”而不是稀里糊涂成功了两个。4.3 权限与数据范围注解加 AOP 只解决了一半接口级鉴权用自定义注解 拦截器代码量小、逻辑直观答辩时也好讲。但很多方案做到这里就停了漏掉了更关键的一半数据范围。同样是“查询会议列表”导师只能看自己名下的会管理员能看全室学生只能看自己报名过的——这是三个不同的 SQL 条件不是三个不同的按钮。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); // 允许访问的角色码 } // 拦截器里做接口级鉴权 public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { if (!(handler instanceof HandlerMethod)) return true; HandlerMethod hm (HandlerMethod) handler; RequireRole anno hm.getMethodAnnotation(RequireRole.class); if (anno null) return true; // 未标注的接口默认放行登录用户 LoginUser user UserContext.get(); if (user null) throw new BizException(401, 未登录); boolean pass Arrays.stream(anno.value()).anyMatch(user.getRoles()::contains); if (!pass) throw new BizException(403, 没有该操作权限); return true; } }数据范围则写进 Service用一个统一的“可见用户集合”来收口// 根据当前登录人算出他能看到的用户ID范围null 表示不限制 private ListLong visibleUserIds() { LoginUser user UserContext.get(); if (user.hasRole(SYS_ADMIN) || user.hasRole(LAB_ADMIN)) return null; if (user.hasRole(TUTOR)) { // 导师自己 名下学生 ListLong ids userMapper.listStudentIdsByTutor(user.getId()); ids.add(user.getId()); return ids; } // 学生只有自己 return Collections.singletonList(user.getId()); }visibleUserIds()返回null表示不限制这个约定必须在 Mapper 的if里对应上否则null被当成空集合拼进IN ()会直接报语法错。这类“约定两端不一致”的问题在课设里非常常见我的习惯是在 Mapper 的注释里把null的含义写死谁改谁看得见。4.4 成果与纪要归档文件怎么存才不给自己留后患会议纪要和论文成果都涉及文件上传这里有两个坑把文件存成数据库 BLOB和把文件丢进项目里的webapp/upload目录。前者会让数据库迅速膨胀、备份变慢、查询变卡后者在重新部署或清空 target 目录时会连文件一起删掉。正确做法是文件落磁盘、数据库存相对路径目录按年月切分文件名用 UUID 重命名并保留原始文件名做展示public String upload(MultipartFile file, Long meetingId) { // 1. 白名单校验后缀不用 getContentType它可被伪造 String original file.getOriginalFilename(); String ext original null ? : original.substring(original.lastIndexOf(.) 1).toLowerCase(); if (!Set.of(pdf, doc, docx, ppt, pptx, zip).contains(ext)) { throw new BizException(不支持的文件类型 ext); } if (file.getSize() 30L * 1024 * 1024) { throw new BizException(单个文件不能超过 30MB); } // 2. 按年月分目录避免单目录文件过多 String month LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMM)); String storeName UUID.randomUUID().toString().replace(-, ) . ext; Path target Paths.get(uploadRoot, month, storeName); Files.createDirectories(target.getParent()); // 3. 落盘后写库库里只存相对路径 file.transferTo(target.toFile()); fileMapper.insert(meetingId, month / storeName, original, file.getSize()); return month / storeName; }uploadRoot建议从配置读不要写死成D:/upload换机器就要改代码。下载接口必须再做一次鉴权拿着文件 id 去查它挂在哪个会议下再判断当前用户对这个会议有没有可见权限不能直接暴露一个GET /file/{id}让谁都能下。这一条在毕设答辩里被问到的概率很高。5. 常见问题与避坑五个最容易在答辩现场暴露的设计缺陷这些坑的共同点是功能演示时看不出来被追问两句就露馅或者上线第一天就出问题。按“现象—原因—解决”逐条记下来比事后补要省事得多。5.1 按钮藏起来了接口却裸奔现象用学生账号登录页面上看不到“审批设备预约”的按钮但把浏览器地址栏里的接口地址复制出来直接请求居然审批成功了。原因权限只做在模板层th:if或者v-if把按钮隐藏了后端接口没有任何校验。前端隐藏只影响“看不见”不影响“点得到”。解决所有写操作接口必须标注RequireRole并且在拦截器里统一拦截同时把“菜单权限”和“接口权限”做成同源数据从同一张权限表读避免菜单配了、接口忘了。验收时用低权限账号做一次全接口遍历这一条能挡住 90% 的越权问题。5.2 时间字段存成字符串区间查询全线崩现象会议列表按日期筛选选 3 月 1 日到 3 月 31 日结果 3 月 5 日的记录查不出来换台机器跑结果又不一样。原因start_time用VARCHAR存2024-3-5 9:00这种格式字符串比较是逐字符比大小2024-3-5和2024-03-05排序结果完全不同再加上时区没统一LocalDateTime和Date混用差 8 小时。解决时间字段一律DATETIMEJava 侧统一用LocalDateTime前后端传输统一yyyy-MM-dd HH:mm:ss格式并在全局配置里注册JsonFormat数据库连接串里显式写serverTimezoneAsia/Shanghai。改完之后顺带检查一遍所有BETWEEN条件BETWEEN 2024-03-01 AND 2024-03-31会漏掉 3 月 31 日当天带时分秒的数据要么改成 2024-04-01要么把右边界补成23:59:59。5.3 事务里塞了发邮件一次超时全线回滚现象发布会议时如果邮件服务响应慢整条会议记录都存不进去页面报 500但数据库里其实什么都没留下。原因把“推送通知”写在了Transactional方法体内通知超时抛异常事务回滚会议也没了。通知本质上是一个“尽力而为”的动作不该和主业务同生共死。解决把通知移出事务边界。简单做法是用TransactionSynchronizationManager.registerSynchronization在事务提交后再执行更稳妥的做法是写一张notify_task表事务内只插任务记录另起一个Scheduled任务扫描未发送的任务重试三次。后者虽然多一张表但演示时能讲清“消息最终一致”是个不错的加分点。5.4 上传文件丢进 web 目录重启就没了现象前一天上传的会议纪要第二天重新部署后点下载报 404磁盘上确实找不到文件。原因文件存到了项目的src/main/webapp/upload或target/classes/static/upload前者在重新打包时被覆盖后者在mvn clean时被整个删掉。解决上传根目录配到项目外的独立路径比如/data/lab-upload或D:/lab-upload通过application.yml的lab.upload.root注入如果需要外部直接访问用WebMvcConfigurer的addResourceHandlers把这个目录映射成/files/**而不是把文件搬进项目。另外记得给这个目录配定期备份毕设环境至少每周拷一次。5.5 演示现场被 N1 查询拖垮现象本地测试 20 条数据时列表秒开答辩前导入了三个学期的真实数据几百场会议会议列表页转圈十几秒翻页更慢。原因列表查出 20 条会议后在 Java 循环里逐条查主持人姓名、会议室名称、参会人数一次请求打了 60 多次数据库。数据量小时感知不到数据一多就指数级恶化。解决列表查询用一次 JOIN 把展示字段带出来或者用IN批量查关联表再在内存里组装分页必须走数据库LIMIT不要在内存里subList。改完在 MySQL 里开一次慢查询日志把所有超过 1 秒的 SQL 都过一遍重点看EXPLAIN的type是不是ALL。6. 验收与答辩一份能照着走的验收清单和八分钟演示脚本验收这件事最怕的是“自己跑通了就算完”。自己跑通只覆盖了正常路径而答辩老师和实际使用者专门挑异常路径。我的做法是准备一套固定验收清单每个功能点写清“怎么验、验到什么程度算过”逐条打勾验完再动手写答辩稿。验收项验收方式通过标准登录与鉴权三个角色各登录一次用低权限账号直接请求管理接口返回 403 且无数据泄露会议排期同一会议室排两场重叠的会第二次被拒绝并给出明确提示会议边界排 10:00–11:00 和 11:00–12:00 两场两场都能创建成功状态流转把已结束的会议改回已发布被拦截提示状态非法设备并发两个浏览器同时提交同一时段只有一个成功另一个提示冲突数据范围导师账号查会议列表只出现本人及名下学生的会议文件归档上传 pdf 后重启服务再下载文件仍可下载大小一致统计准确手工核对某学生一学期出勤次数与页面统计一致演示脚本按八分钟设计顺序是“登录 → 排一场组会当场触发冲突提示→ 学生端报名和请假 → 导师端写纪要并上传附件 → 管理员端看设备预约冲突 → 切到统计页看数据”。冲突提示那一步一定要现场触发这是整套系统里最有说服力的十几秒比讲十页架构图都管用。演示前把数据准备好三个角色账号、两场历史会议、一条已审批的预约、一份真实点的 pdf 纪要别用“测试1”“aaa”这种数据观感差很多。答辩追问高频集中在三个方向提前准备回答一是“并发怎么处理的”答唯一索引 事务别说“用了 Redis 锁”但其实没写二是“权限怎么防越权”答接口注解 数据范围双层最好能当场用两个账号演示三是“如果实验室扩到三个校区怎么办”答会议室、设备都挂在实验室实体下加一个校区字段就能横向扩别答“要重构”。从那以后我每次带人做这类系统都会强制走一遍“三个账号 两条脏数据 一次断网”的验收流程三个账号验权限两条脏数据重复预约、重叠会议验约束一次断网验事务和提示是否友好。走完这一遍答辩现场基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取