资讯动态

Java Web实验室管理系统:会议室预约冲突检测与审批并发控制

发布时间:2026/10/30 5:06:29 来源:尧图企业网站定制
简介这份资源是一套面向高校计算机专业课程实践与毕业设计的 JavaWeb 实验室管理系统完整源码围绕实验室日常科研协作场景设计了教师端与学生端两类角色。教师可查看学生历次工作汇报并逐条答复、浏览学生上传的代码及可见性设置、发起带起止时间的会议并指定汇报人、参与会议讨论与追评、接收汇报与请假等消息提醒、回看历史会议记录学生则能查看同学公开汇报与公开代码、参与进行中的会议讨论、创建并管理个人项目、上传代码并设定公开范围、按周提交工作汇报与PPT附件、查看他人对自己的回复评价功能链路较为完整。压缩包共 246 个文件约 30.63MB涵盖 62 个 java 源文件、62 个 class 字节码、54 个 jar 依赖、24 个 jsp 页面、16 个 xml 配置与 8 个 properties 配置另有少量图片与前端样式资源属于典型 SSM/Hibernate 分层结构便于对照理解 MVC 分层与权限控制设计。目前已有 589 人学习下载适合课程设计参考、功能二次开发与 JavaWeb 综合能力训练。1. 从一张贴满便利贴的排班表说起Java Web 实验室管理系统到底要管什么某高校实验室的墙上曾经贴着一张 A3 纸正面是本周会议室排班背面是设备借用登记右下角还挤着两行耗材领用记录。三个人同时改同一张表冲突几乎是必然的——一周里第三次出现两个课题组预约同一间会议室做中期汇报谁也不让谁。做一套 Java Web 实验室管理系统动机并不复杂把纸面上的预约、借用、领用、审批搬到线上让同一份数据只保留一个版本。这套方案面向两类人一类是正在做课程设计或毕业设计、需要一个能跑起来又能讲清楚的完整系统的同学另一类是刚接手实验室日常事务、想拿一套现成结构去改的工程师。它要解决的核心问题就四个会议室预约不撞车、设备借用可追溯、耗材与文档有人管、审批流程留痕迹。会议预约是整套系统里并发最集中、边界条件最多的模块把它做扎实其余模块基本是同类问题的重复。2. 需求拆解与技术选型先把角色和数据表定死再谈写代码动手之前先做一件很容易被跳过的事把角色和实体画清楚。跳过这一步的后果通常是写到一半发现「导师能不能替学生取消预约」这种问题没人拍板回头改表、改接口工期直接翻倍。这一章把权限边界、技术栈、数据表三件事定下来后面几章才有稳定的地基。2.1 四类角色的权限边界怎么划实验室管理系统的角色数量不多但边界容易糊。常见的划分是四类研究生学生、导师、实验室管理员、系统管理员。划分依据不是「谁级别高」而是「谁对哪份数据负责」。研究生提交自己的会议室预约、设备借用申请、耗材领用申请查看本组会议安排上传和下载本组文档取消自己尚未开始的预约。导师审批本组学生的预约与借用申请查看本组全部记录不能直接修改审批结果只能驳回后由学生重新提交。实验室管理员管理会议室与设备台账处理跨组的冲突预约强制取消需填写理由发放耗材并扣减库存维护文档分类。系统管理员只管账号、角色、日志不参与业务审批。把这张权限矩阵写成表格贴在设计文档里比写在脑子里靠谱功能点研究生导师实验室管理员系统管理员会议室预约提交/取消本人审批本组审批全部、强制取消无业务权限设备借用申请/归还登记审批本组台账维护、逾期催还无业务权限耗材领用申请查看本组发放并扣减库存无业务权限文档资料上传/下载本组上传/下载本组分类与清理无业务权限用户与角色仅本人信息仅本人信息无增删改查表里有一格值得注意「审批全部、强制取消」只给实验室管理员不给导师。原因是导师只对自己组的资源使用负责跨组冲突必须由掌握全局排班的人裁决否则两个导师互相驳回学生在中间来回跑。这个细节在答辩时经常被问到能答上来说明真的想过业务。2.2 技术栈选型为什么不用纯 Servlet JSP 手写课程设计常见两种做法一种是纯 Servlet JSP JDBC另一种是框架组合。前者的好处是「什么都是自己写的」答辩时每一行都能解释坏处是事务、连接池、参数校验、文件上传全要手写一个学期的时间大概率不够最后交上去的系统连登录失败提示都做不完整。这套方案选的是 Spring Boot MyBatis MySQL Thymeleaf或前后端分离时的 Vue axiosMaven 管理依赖。理由有三条事务边界清晰。会议审批、设备借用、库存扣减都是「多条写操作要么全成要么全败」Spring 的Transactional一行注解解决手写 JDBC 要自己管 autoCommit 和回滚。参数绑定和校验省事。表单十几个字段用Valid加实体注解就能挡住空值和格式错误不用在每个 Servlet 里写一长串 if。部署简单。打成可执行 jarjava -jar就能跑答辩现场不用装 Tomcat。依赖部分保持精简够用就行!-- Web 场景内嵌 Tomcat打成 jar 直接启动 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 持久层MyBatis 官方 starter配合 XML 写动态 SQL -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- 分页插件预约记录、借用记录都需要分页 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency选 MyBatis 而不是 JPA是个有争议但站得住脚的决定。实验室管理的查询几乎全是「多表关联 动态条件」按会议室、按时间段、按状态、按申请人姓名模糊匹配四个条件可能同时存在也可能一个都没有。MyBatis 的if标签把这种动态 SQL 写得很直白而 JPA 要么拼 Specification要么写一堆Query对初学者反而更绕。前端这块看项目定位。如果是课程设计Thymeleaf 模板 Bootstrap 加一点 jQuery 足够页面少、联调快如果是毕业设计想体现工作量做成前后端分离后端只返回 JSON前端用 Vue 加 axios登录态用 JWT 或 Session 都行。两种做法后端代码几乎不变差别只在 Controller 返回的是视图名还是Result对象。2.3 数据表设计11 张表撑起四类业务表设计的原则是「一条业务记录一张主表状态变更单独留痕」。整理下来是 11 张表名作用关键字段sys_user用户账号id, username, password, real_name, role_id, group_idsys_role角色字典id, role_code, role_namelab_room实验室/会议室id, room_name, capacity, location, statusmeeting_booking会议预约主表id, room_id, user_id, start_time, end_time, topic, statusequipment设备台账id, equip_name, total_count, stock, statusequipment_borrow设备借用记录id, equip_id, user_id, borrow_num, borrow_time, return_time, statusconsumable耗材台账id, name, stock, unitconsumable_record耗材领用记录id, consumable_id, user_id, num, create_timedoc_file文档资料id, file_name, file_path, uploader_id, category, group_idnotice站内通知id, receiver_id, content, read_flag, create_timeapproval_log审批流水id, biz_type, biz_id, approver_id, action, remark, create_timeapproval_log这张表很容易被省掉省掉的代价是「谁在什么时候驳回了我的预约」说不清楚学生来找管理员对质管理员只能翻聊天记录。加上它业务表只存当前状态历史全在流水里审计和答辩演示都好讲。会议预约表的建表语句大致如下注意时间字段用DATETIME而不是字符串后面的冲突检测全靠它做比较CREATE TABLE meeting_booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL COMMENT 会议室ID, user_id BIGINT NOT NULL COMMENT 申请人ID, group_id BIGINT NOT NULL COMMENT 所属课题组用于导师审批范围, topic VARCHAR(120) NOT NULL COMMENT 会议主题, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1已通过 2已驳回 3已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 组合索引直接服务冲突检测查询顺序不能随意调换 KEY idx_room_time (room_id, start_time, end_time), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会议预约;索引顺序(room_id, start_time, end_time)是有讲究的。冲突检测的查询条件固定以room_id打头再对时间区间做范围过滤把这个顺序写反索引只能用到第一列会议室一多查询就慢下来。至于为什么状态字段用数字不用枚举字符串是因为status IN (0,1)这种判断在索引里比字符串比较便宜且后续加状态不用改表结构。3. 总体架构设计三层结构下每个包该放什么框架搭起来之后最常见的问题是「代码全堆在 Controller 里」。一个人写还看不出毛病等到要加一个新角色或者改审批规则才发现业务逻辑和数据访问搅在一起动一处牵连三处。分层不是为了好看是为了让改动有地方落。3.1 分层结构与包组织按职责切成四层包名跟着业务模块走不跟着技术名词走com.lab.manage ├── common 通用返回体、异常、常量、工具类 ├── config Web 配置、拦截器注册、跨域、MyBatis 配置 ├── controller 只做参数接收、调用 service、返回结果 ├── service 业务编排、事务边界、状态流转校验 │ └── impl ├── mapper MyBatis 接口 ├── entity 与表对应的实体 ├── dto 前端传参与返回的封装对象 └── task 定时任务会议提醒、逾期催还有几条纪律值得提前定下不然写到后期一定乱Controller 里不出现SqlSession、不出现Transactional、不做业务判断。它拿到的参数要么直接可用要么交给 service 校验后抛业务异常由全局异常处理器统一转成Result返回。Service 是唯一允许开事务的地方。一个审批动作可能同时写meeting_booking和approval_log和notice三张表必须在同一个事务里否则会出现「审批成功了但通知没发、日志没记」。Mapper 只写 SQL不写 if-else 业务分支。动态条件用 XML 标签表达复杂计算挪回 service。DTO 和 entity 分开。前端提交预约时传的是roomId、startTime、topic返回时还要带上申请人姓名、会议室名称这些关联字段直接拿 entity 当出入参字段一多就分不清哪些是必填。统一返回体长这样前端只判断code一个字段public class ResultT { private int code; // 200 成功500 业务失败401 未登录 private String msg; private T data; // 静态工厂方法省略 }3.2 会议预约模块的调用链与职责边界一次预约从提交到通过涉及五个环节每个环节归谁管要提前说清楚Controller 接收表单做基础格式校验时间非空、结束时间晚于开始时间。Service 先查会议室是否可用、申请人当天是否有冲突预约再调 Mapper 查目标时段是否被占。冲突检测通过后插入预约记录状态置为 0待审批同时写一条approval_log标记「提交」。审批人打开待办列表Controller 调 service 的approve方法service 内部做状态校验、更新状态、写日志、发通知。审批通过后定时任务在会议开始前 30 分钟扫一遍往notice表插提醒。这套链路里唯一不能放到 Controller 的是第 2 步和第 4 步因为它们都要读当前状态再决定能不能写属于典型的「检查后执行」必须在同一个事务里完成否则并发下会出问题。第 4 章讲并发时会展开。模块之间的依赖尽量单向会议预约不依赖设备借用两者只在通知模块汇合。这样拆的好处是答辩时被问到「如果要去掉设备模块代码要改多少」答案是「删掉对应的 controller/service/mapper 和两张表其余不动」。3.3 设备与耗材模块的库存设计设备和耗材是两类东西但库存模型可以共用一套思路台账表存总量和余量流水表存每一次变动。设备的特点是「借出还要还回来」所以equipment_borrow表要有borrow_time和return_time两个字段状态分「借用中、已归还、已逾期」。余量stock在借出时减一、归还时加一这两个动作都必须走「条件更新」而不是「先查再改」。耗材的特点是「领了就不还」所以只有减法没有加法consumable_record只记录领用余量扣到 0 就不能再领。这两个模块的库存字段都不允许直接UPDATE ... SET stock 5必须写成UPDATE ... SET stock stock - #{num} WHERE id #{id} AND stock #{num}然后判断影响行数是不是 1。写成固定值赋值两个人同时领用就会互相覆盖这就是后面避坑章节要展开的「超卖」。4. 关键模块实现拦截器、冲突检测、状态机三块硬骨头前面两章定的是结构这一章落到具体代码。三块内容分别是登录与权限拦截、会议室冲突检测、审批状态机与并发防重都是绕不过去、也最容易在答辩时被追问的地方。4.1 登录会话与角色拦截器登录不做复杂加密演示的话用BCrypt或 Spring 自带的DigestUtils做一次哈希足够数据库里绝不存明文。登录成功把用户对象放进 Session后续每次请求由拦截器统一校验。权限判断用注解 拦截器组合比在每个方法里写 if 干净得多// 标记在 Controller 方法上声明访问该方法需要的角色 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); // 例如 {ADMIN, LAB_MANAGER} }拦截器在preHandle里做两件事先判断有没有登录再看目标方法上有没有RequireRoleComponent public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { // 1. 未登录统一打回登录页AJAX 请求返回 401 JSON HttpSession session req.getSession(false); SysUser user session null ? null : (SysUser) session.getAttribute(loginUser); if (user null) { writeJson(resp, 401, 登录已失效请重新登录); return false; } if (!(handler instanceof HandlerMethod)) { return true; } // 2. 方法上没有角色注解说明登录即可访问 HandlerMethod hm (HandlerMethod) handler; RequireRole anno hm.getMethodAnnotation(RequireRole.class); if (anno null) { return true; } // 3. 角色不匹配直接拦截不抛异常避免把栈信息暴露给前端 for (String role : anno.value()) { if (role.equals(user.getRoleCode())) { return true; } } writeJson(resp, 403, 无权访问该功能); return false; } }这段代码有两个容易忽略的点。一是getSession(false)而不是getSession()后者在会话不存在时会新建一个导致每次匿名请求都在服务端堆一个空会话。二是 AJAX 请求不能重定向到登录页否则前端拿到的是一段 HTMLJSON.parse直接报错页面表现就是「点了没反应」。判断请求类型可以看X-Requested-With头或Accept头。注册拦截器时记得把登录接口、静态资源、错误页排除掉Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /doLogin, /css/**, /js/**, /error); } }4.2 会议预约冲突检测的 SQL 与边界条件冲突检测的本质是区间重叠判断。两个时间段[s1, e1)和[s2, e2)重叠的充要条件是s1 e2 AND s2 e1。用这个条件写查询比「先查出当天所有预约再在 Java 里循环比较」可靠得多后者在数据量上来之后会明显变慢而且容易漏掉跨天的预约。SELECT COUNT(1) FROM meeting_booking WHERE room_id #{roomId} AND status IN (0, 1) -- 待审批和已通过都算占用驳回和取消不算 AND start_time #{endTime} -- 已有预约的开始 早于 新预约的结束 AND end_time #{startTime} -- 已有预约的结束 晚于 新预约的开始四个参数都用了严格不等号这是关键。如果写成和一场 9:00–10:00 的会和一场 10:00–11:00 的会被判为冲突实际上前一场结束的时刻正好是后一场开始的时刻并不冲突。这个边界在演示时特别容易被老师抓住改起来只需要两个符号但想不到就是想不到。status IN (0,1)这一句也有讲究。只查已通过1会出现两笔待审批的预约互相不感知管理员一通过就撞车只查待审批0已通过的会议反而挡不住新申请。两个状态都要算占用驳回2和取消3则释放时段。Service 层的写法Transactional(rollbackFor Exception.class) public void createBooking(MeetingBookingDTO dto, SysUser user) { // 1. 基础校验结束必须晚于开始且不能预约过去的时间 if (!dto.getEndTime().isAfter(dto.getStartTime())) { throw new BizException(结束时间必须晚于开始时间); } if (dto.getStartTime().isBefore(LocalDateTime.now())) { throw new BizException(不能预约已经过去的时间段); } // 2. 冲突检测count 0 直接拒绝 int conflict bookingMapper.countConflict(dto.getRoomId(), dto.getStartTime(), dto.getEndTime()); if (conflict 0) { throw new BizException(该时段已被占用请更换时间或会议室); } // 3. 落库 写审批流水两步在同一事务 MeetingBooking entity convert(dto, user); bookingMapper.insert(entity); approvalLogMapper.insert(buildLog(MEETING, entity.getId(), user.getId(), 提交, null)); }注意Transactional只对RuntimeException默认回滚rollbackFor Exception.class是为了兜住受检异常。这个参数平时看不出作用等到某次文件上传失败抛出IOException、前面插入的记录却没回滚时就会明白它的价值。还有一个现实问题两台机器同时提交第 2 步的冲突检测可能同时返回 0然后两条记录都插进去了。单机部署、用户量小的实验室场景出现概率很低但如果要做得严谨可以在meeting_booking上加一个基于room_id start_time的唯一索引兜底插入时捕获DuplicateKeyException转成友好提示。这个做法只挡得住开始时间完全相同的预约挡不住部分重叠属于「聊胜于无」的补充真正的解法是第 4.3 节讲的锁。4.3 审批状态机与并发防重预约状态只有四个但流转关系必须写死不能任由代码随意赋值待审批(0) → 已通过(1) | 已驳回(2) 待审批(0) → 已取消(3) 申请人主动撤回 已通过(1) → 已取消(3) 会议开始前 已驳回(2)、已取消(3) → 终态不可再变用枚举把状态和合法流转固化下来比散落在各处的if (status 0)好维护public enum BookingStatus { PENDING(0, 待审批), APPROVED(1, 已通过), REJECTED(2, 已驳回), CANCELED(3, 已取消); private final int code; private final String desc; // 构造、getter 省略 // 判断当前状态能否流转到目标状态 public static boolean canTransfer(int from, int to) { if (from PENDING.code) { return to APPROVED.code || to REJECTED.code || to CANCELED.code; } if (from APPROVED.code) { return to CANCELED.code; } return false; // 驳回和取消都是终态 } }真正防止重复审批的不是这段判断而是 SQL 里的条件更新。审批接口被连点两次、或者两个管理员同时打开同一条待办先查状态再更新的写法一定会出问题两次查询都读到 0两次更新都执行日志写了两条通知发了两遍。正确做法是把状态判断交给数据库Transactional(rollbackFor Exception.class) public void approve(Long bookingId, SysUser approver, boolean pass, String remark) { BookingStatus target pass ? BookingStatus.APPROVED : BookingStatus.REJECTED; // 条件更新只有当前仍是待审批才会命中影响行数即为判断依据 int rows bookingMapper.updateStatus(bookingId, target.getCode(), approver.getId()); if (rows 0) { // 要么记录不存在要么已被别人处理过 throw new BizException(该预约已被处理请刷新后重试); } approvalLogMapper.insert(buildLog(MEETING, bookingId, approver.getId(), pass ? 通过 : 驳回, remark)); noticeMapper.insert(buildNotice(bookingId, remark)); }对应 XMLupdate idupdateStatus UPDATE meeting_booking SET status #{targetStatus}, approver_id #{approverId}, approve_time NOW() WHERE id #{id} AND status 0 !-- 核心只有待审批状态才允许更新 -- /updaterows 0时抛异常事务回滚日志和通知都不会写。这就是把「检查」和「执行」合并成一条原子 SQL 的思路比加synchronized更可靠——后者只在单实例下有效多实例部署直接失效。设备库存扣减同理UPDATE equipment SET stock stock - #{num} WHERE id #{equipId} AND stock #{num} -- 余量不足时影响行数为 0由 service 抛业务异常4.4 会议提醒与站内通知通知模块做两件事业务动作触发的即时通知审批结果和时间触发的定时提醒会议开始前 30 分钟。即时通知在审批事务里一起写保证「审批成功」和「通知生成」同生共死。定时提醒用Scheduled扫表Scheduled(cron 0 */5 * * * ?) // 每 5 分钟扫一次 public void remindUpcomingMeeting() { LocalDateTime now LocalDateTime.now(); LocalDateTime deadline now.plusMinutes(30); // 查 30 分钟内即将开始、且尚未提醒过的已通过预约 ListMeetingBooking list bookingMapper.selectUpcoming(now, deadline); for (MeetingBooking b : list) { noticeMapper.insert(buildRemindNotice(b)); bookingMapper.markReminded(b.getId()); // 标记字段避免重复提醒 } }这里必须有「已提醒」标记否则每 5 分钟扫一次同一场会议会被提醒六遍用户直接把这个功能当成骚扰关掉。标记字段可以加在meeting_booking上叫remind_flag也可以单独建一张提醒记录表前者简单后者好追溯。定时任务在多实例部署时会重复执行单机答辩环境不用管。如果确实要处理简单的办法是在任务开头抢一把数据库行锁或者用一张task_lock表记录「今天这次扫描谁抢到了」。5. 常见问题排查上线前一定要过的六道坎下面这几条都是实际写这套系统时最容易卡住的地方共同特点是「代码看着没错结果就是不对」。每条按现象、原因、解决三段写遇到问题时可以对着排查。5.1 相邻时段被判为冲突现象9:00–10:00 的会议已经通过再提交 10:00–11:00 的预约系统提示「该时段已被占用」。原因冲突检测的 SQL 用了和把「端点相接」也算进了重叠区间。数学上两个左闭右开区间的重叠条件是严格不等号。解决把start_time #{endTime}改成start_time #{endTime}end_time #{startTime}改成end_time #{startTime}。改完拿三个用例回归完全重叠、部分重叠、端点相接前两个应拦第三个应放行。5.2 审批按钮点两下日志出现两条现象网络稍慢管理员连点两次「通过」approval_log里出现两条「通过」记录申请人的通知也收到两条。原因先select查状态、再update改状态的写法两次请求之间存在时间窗口两次查询都读到「待审批」。解决改成UPDATE ... WHERE id ? AND status 0的条件更新用影响行数判断是否命中。前端再加一层按钮点击后置灰但那只是体验优化服务端不改照样会重复。5.3 中文乱码和时区偏移八小时现象会议主题存进数据库变成问号或者页面显示的开始时间比实际早/晚 8 小时。原因乱码一般是数据库连接串没带字符集或表用了latin1时区问题多半是 JVM 时区、JDBC 连接串时区、Jackson 序列化时区三者不一致MySQL 驱动 8.x 默认按 UTC 解析DATETIME。解决连接串写全?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai建表统一utf8mb4Spring 配置里加spring.jackson.time-zoneGMT8实体时间字段统一用LocalDateTime而不是java.util.Date。三处对齐之后这个问题基本不再出现。5.4 接口返回 404 或静态资源加载不出来现象后端接口用 Postman 能通浏览器里页面打开是一片空白控制台报 404或者样式全丢。原因前后端分离时路径前缀没对齐后端/api/booking前端请求/bookingThymeleaf 模板放错目录必须在resources/templates下拦截器把/css/**、/js/**也拦了。解决统一约定接口前缀并在前端 axios 的baseURL里配一次检查模板和静态资源目录拦截器的excludePathPatterns补上静态资源路径。开发阶段把 Spring 的logging.level.org.springframework.webDEBUG打开请求打到哪个 handler 一目了然。5.5 设备余量扣成负数现象两台设备两个人同时借用最后库存显示 -1。原因用「先查stock再 set 新值」的写法两个请求读到同样的库存各自减一后写回后写的覆盖先写的。解决改成UPDATE equipment SET stock stock - #{num} WHERE id ? AND stock #{num}判断影响行数为 0 就抛「库存不足」。这条和 5.2 是同一类问题的两种表现本质都是「检查与执行分离」带来的竞态。5.6 删除会议室报外键错误现象管理员想下线一间不再使用的会议室点击删除后页面报 500。原因meeting_booking里有历史预约记录引用这个room_id物理删除违反外键约束。解决会议室、设备这类台账数据不做物理删除加一个status字段表示「启用/停用」停用后不出现在预约下拉框里历史记录照常可查。真要做删除也是软删除加deleted标记查询时统一过滤。6. 验收与答辩用一张清单证明这套系统真的能跑系统写完只是开始能不能过验收、能不能答上追问取决于有没有提前把「怎么证明它可用」想清楚。这里给一份可以直接照着走的方法。先做功能验收清单每一项都要有明确的动作和通过标准不能写「预约功能正常」这种没法验证的话验收项操作动作通过标准登录与权限用学生账号访问用户管理页返回 403不跳白屏时段冲突先约 9:00–10:00再约 9:30–10:30 同房间提示冲突记录未写入端点边界再约 10:00–11:00 同房间预约成功状态为待审批审批防重打开两个浏览器标签同时点通过只有一条成功日志仅一条设备超卖库存 1 时并发提交两笔借用一笔成功一笔提示库存不足通知审批通过后查看申请人通知列表出现对应通知未读数 1数据追溯查看某条预约的审批流水提交、审批各一条含操作人与时间演示流程控制在五到八分钟顺序按「学生提交 → 导师审批 → 管理员查看排班 → 触发冲突 → 查看通知」走一遍覆盖四类角色和三次数据库写入。演示前把数据准备好尤其是「已有一个通过的预约」这条现场临时造数据很容易手忙脚乱。答辩追问通常集中在四个方向提前准备好答案冲突检测怎么做的答区间重叠的充要条件说清为什么用严格不等号能举端点相接的例子最好。两个人同时审批会怎样答条件更新的影响行数判断说明为什么不靠synchronized。权限是怎么控的答注解加拦截器说明为什么不在每个方法里写 if。数据能不能追溯答approval_log表的设计业务表存状态、流水表存历史。还有一件我自己的血泪经验答辩前一晚一定要在干净环境里重新部署一次从建库脚本开始跑不要用开发机上那个跑了一学期的数据库。我见过太多次「本机好好的、换台机器就起不来」原因无非是连接串写死了、建表脚本没更新、某个依赖只在本地仓库里。从那以后我每次交项目前都会把这份验收清单打印出来一条条打勾打完才敢关电脑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑