Spring Boot开放实验室预约系统实战:冲突检测、并发控制与避坑指南

📅 发布时间:2026/10/11 6:50:29
Spring Boot开放实验室预约系统实战:冲突检测、并发控制与避坑指南
简介面向高校实验室管理人员、Java开发学习者及毕业设计选题学生文档围绕开放实验室管理系统的完整设计流程解决实验室预约、设备管理、数据统计等业务需求帮助读者掌握基于Spring Boot的B/S架构开发思路。包体为单个docx文档大小2.15MB内容依次讲解Spring Boot核心架构、VUE前端框架、MySQL数据库、eclipse工具使用以及需求分析中的功能需求、业务流程和可行性分析同时提供E-R图、数据库表设计和系统模块总体设计便于对照搭建实际项目。文档从绪论、开发技术简介、需求分析、数据库设计到系统详细设计逐层展开结构完整可作为课程设计、毕业设计或自学参考。目前已有189人学习下载适合需要快速梳理系统设计文档、理解前后端分离开发与数据库建模的读者使用。1. 开放实验室为什么会变成“预约黑洞”一套 Spring Boot 系统的破局点开放实验室管理系统听着像教务系统下的一个功能点但真正把“开放”两个字落地的项目做起来远比想象中麻烦。在大多数学校或企业实训中心实验室的开放要么靠门口一张纸质登记表要么靠管理员手工在群里接龙排班一个小时内的实验需求往往要排两三天实验数据也基本没法追溯。我接触过的不少团队最初只是想做一个“能预约”的页面最后都被时间冲突、审批链路、门禁联动和数据统计拖成了半年项目。这篇笔记不讲空架构只讲基于 Spring Boot 的开放实验室管理系统从零到一怎么落地业务怎么拆、表怎么建、核心预约接口怎么写、上线会踩什么坑。目标人群是有 Spring Boot 基础、准备把课程设计或内部工具做成真正能投用的开发者也适合刚接手类似项目的同学快速对齐技术方案。如果你只关心其中某一环可以跳过概念直接看对应章节如果你想照着一套还能用的源码结构搭起来建议从头到尾按章节顺序过一遍数据库脚本和接口骨架都在下文给出。2. 需求范围与数据模型把“开放实验室”拆成可落地的核心表做这类系统最容易犯的错是一开始就对着“管理员系统”的通用框架铺页面用户管理、角色管理、菜单管理先写一周。开放实验室最大的业务特点是“资源时间维度强约束”所以必须先把业务链路拆出来再决定表结构。一条完整的使用链路是这样的学生查看可预约的实验室和时段提交预约申请管理员审核审核通过后到点门禁放行学生进入实验室并记录实验过程完成实验后生成实验记录系统沉淀使用数据用于之后的排班与开放决策。把这七个动作落到系统里最终只需要围绕用户、实验室、预约、实验记录四张核心表转。2.1 角色与权限三种角色起步就够不要一开始做复杂数据权限角色怎么定直接决定后续权限代码的复杂度。常见做法是三角色模型角色核心操作明确不做的事学生查实验室、提交预约、取消、查记录审批、改实验室开放状态实验室管理员审批预约、维护开放时段、查看使用记录新建用户、改系统配置系统管理员用户、实验室、数据报表、日志替代所有管理员审批具体预约三张角色需要五张表十一个实体的 RBAC 框架来做对三个角色的系统来说等于自找苦吃。在 Spring Security 里把角色码放role_code字段用hasRole()或自定义拦截器判断即可。一句话原则权限模型做到“够用 易改”等真正出现跨实验室数据权限诉求时再加数据权限过滤插件不要把未来的复杂度提前搬到现在。2.2 数据库建模用户、实验室、预约、实验记录四张表的建表 SQL下面这套建表 SQL 是整理过的稳定版本直接放到初始化脚本里就能用CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT BCrypt 加密存储, role_code VARCHAR(32) NOT NULL COMMENT STUDENT/LAB_ADMIN/SYSTEM_ADMIN, real_name VARCHAR(64) DEFAULT NULL, enabled TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 用户表; CREATE TABLE lab ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL, location VARCHAR(255) DEFAULT NULL, capacity INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1开放 0关闭, open_start TIME DEFAULT 08:00:00, open_end TIME DEFAULT 22:00:00, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 实验室表; CREATE TABLE reservation ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, lab_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, purpose VARCHAR(255) DEFAULT NULL, status VARCHAR(32) NOT NULL DEFAULT PENDING COMMENT PENDING/APPROVED/REJECTED/CANCELED/IN_USE/FINISHED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_lab_time (lab_id, start_time, end_time), KEY idx_user_status (user_id, status) ) COMMENT 预约单; CREATE TABLE experiment_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, reservation_id BIGINT NOT NULL, user_id BIGINT NOT NULL, lab_id BIGINT NOT NULL, experiment_name VARCHAR(200) NOT NULL, material_description VARCHAR(500) DEFAULT NULL, result_summary TEXT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 实验记录;建表时我特意把reservation.status用字符串而不是数字原因是六个状态直接拼进 SQL 可读性好排查问题时不用翻字典表。idx_lab_time这个联合索引是后面时间冲突检测的命根子没有它随着预约量上升查询会全表扫到时再来补索引又得锁表。实验记录是不是非要和预约强关联常见做法是一开始就加reservation_id外键因为开放实验室的定位是“预约可追溯”没有预约的记录不应该允许入库。如果某些场景允许临时进入则需要额外准备一张“临时入场”表别把实验记录的reservation_id做成可空否则统计口径会乱。2.3 选型依据为什么是 MyBatis-Plus 而不是 JPA 或纯 XMLORM 选型没有对错只有匹配度。开放实验室这种系统查询条件几乎都是“实验室名称模糊 状态 时间范围”的组合用 MyBatis-Plus 的LambdaQueryWrapper拼条件比 JPA 的Specification更直白代码量也少团队里新来的成员看两眼就能维护。用 MyBatis-Plus 有两个容易误用的点要提前留意。第一分页插件不是配置了依赖就生效必须显式注册MybatisPlusInterceptor第二逻辑删除全局开关默认关闭更好否则刚导入的旧库没有deleted字段查询 SQL 自动带上deleted0直接报错。如果你确定需要逻辑删除就给每张表统一加deleted TINYINT DEFAULT 0并在配置里打开全局配置。下面这段配置是从项目里直接抽出来的放到配置类即可Configuration MapperScan(com.example.lab.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(500L)是保护性限制防止有人把 pageSize 传成 999999 直接把数据库打爆。DbType.MYSQL不能省略分页插件靠它决定拼接哪种方言。这里如果项目将来要兼容 PostgreSQL最省事的办法是按数据库拆配置而不是写一个自动判断的复杂逻辑。3. 用 Spring Boot 快速跑通开放实验室最小闭环登录、列表与预约接口一套系统能不能在一天内看到效果取决于你怎么拆迭代节奏。我的习惯是先做“登录 → 实验室分页列表 → 提交预约”这三个接口预约审批、门禁、实验记录全部放后面这样第一天就能拿前端页面联调第二天再往里填复杂逻辑。3.1 项目初始化pom.xml 依赖清单与启动类用常规的 Maven 工程结构即可Spring Boot 版本可以用 2.7.x 的稳定线代码里是javax.*如果团队已经切到 3.x只要批量把javax换成jakarta其他差异不大。依赖部分直接抄下面这份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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies上面这份依赖里mysql-connector-j 的版本由 Spring Boot 父级统一管理不需要再写 version在 2.7.18 里默认的是 mysql-connector-j 8.0.x对应 MySQL 8 服务端没有问题。如果生产库还是 MySQL 5.7更建议在服务端参数不动的前提下用 8.0.24 以上驱动避免出现 SSL 连接相关故障。pom 里最容易出问题的是 jjwt 三个包版本不一致三个模块版本不同会在运行期抛SignatureException表面看像密钥错误实际是依赖冲突所以版本号我直接统一写成 0.11.5。启动类和普通 Spring Boot 工程没有区别放在主包根目录保证它能扫描到下面的 controller、service、mapper 即可SpringBootApplication public class LabApplication { public static void main(String[] args) { SpringApplication.run(LabApplication.class, args); } }3.2 application.yml 配置数据源、时区、日期格式一个都不能少配置文件的重点不在代码量而在于把时区和日期格式这几件容易出错的事一次性约定好。下面这份是实际项目里最常用的最小配置server: port: 8080 servlet: context-path: /lab-api spring: datasource: url: jdbc:mysql://localhost:3306/lab_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: falseserverTimezoneAsia/Shanghai一定要显式写这是项目启动后最常见的翻车场景Windows 本地时区、MySQL 系统时区、JDBC 驱动时区三者不一致数据库存入的时间比预期早 8 小时。部署到服务器后还要检查容器的TZ环境变量否则同一个 jar 换个环境又错。log-impl开发期保留它会在控制台打印每条 SQL 和参数排查“为什么查不到数据”时能看到 MyBatis-Plus 真实拼接的 SQL上线后删掉或改成日志文件输出否则高流量下System.out会成为性能瓶颈。3.3 登录接口用 JWT 做无状态 token密码用 BCrypt登录接口看起来简单却是权限体系的入口。下面是最小实现没有引入完整 Spring Security而是用拦截器校验 token对没有复杂授权需求的中小型系统足够RestController RequestMapping(/auth) public class AuthController { Resource private UserMapper userMapper; Resource private JwtUtil jwtUtil; PostMapping(/login) public ResultString login(RequestBody Valid LoginReq req) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, req.getUsername())); if (user null || !BCrypt.checkpw(req.getPassword(), user.getPassword())) { return Result.fail(用户名或密码错误); } if (user.getEnabled() ! 1) { return Result.fail(账号已停用); } String token jwtUtil.createToken(user.getId(), user.getUsername(), user.getRoleCode()); return Result.ok(token); } }逻辑顺序要注意先查用户再比对密码再查停用状态。为什么不先查停用因为用户名不存在和密码错误不应该给不同提示避免被用来撞库判断账号是否存在。selectOne查询要求username在表上有唯一索引如果没有唯一索引selectOne会直接抛TooManyResultsException。这个异常很奇怪问题往往不出在代码而在 DDL。下面是 JwtUtil 的精简版本只保留创建 token 的方法解析和校验方法在实际项目中按需补充核心思路是把用户身份放进 token下次请求通过拦截器还原用户Component public class JwtUtil { Value(${jwt.secret}) private String secret; public String createToken(Long userId, String username, String roleCode) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, roleCode) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .compact(); } }配置里必须有jwt.secret。这个值最少 32 字节否则 jjwt 在启动或第一次签名时报WeakKeyException这是很多新手以为“加依赖加错了”而实际是密钥长度不够的玄学问题。生产环境密钥用环境变量注入不要写进 yml 提交到 git。3.4 实验室列表与预约提交能支撑前端联调的最短接口实验室列表用 MyBatis-Plus 分页返回整合后的 VO 更便于前端直接渲染GetMapping(/lab) public ResultPageLabVO list( RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, String name, Integer status) { LambdaQueryWrapperLab qw new LambdaQueryWrapper(); qw.like(StringUtils.hasText(name), Lab::getName, name) .eq(status ! null, Lab::getStatus, status) .orderByAsc(Lab::getId); PageLab labPage labMapper.selectPage(new Page(page, size), qw); // 这里省略 Lab - LabVO 的转换直接转字段名一致即可 return Result.ok(labPage); }like(StringUtils.hasText(name), ...)这种三参条件第一个参数为真才拼接该条件否则忽略。这是 MyBatis-Plus 最实用的特性避免写一堆if。预约提交的 controller 不写业务逻辑直接调 service真正的时间校验和冲突检测都在 service 层完成PostMapping(/reservation) public ResultLong reserve(RequestBody Valid ReserveReq req, RequestAttribute(userId) Long userId) { return Result.ok(reservationService.createReservation(userId, req)); }ReserveReq里时间字段用LocalDateTime接收配合JsonFormat(pattern yyyy-MM-dd HH:mm:ss)前端传字符串就能自动绑定。真正的校验如“开始时间必须晚于当前时间”“结束时间要大于开始时间”放在 service 层做因为接口层只做参数非空校验。4. 让开放实验室的预约不打架冲突检测 SQL 与状态机设计系统里最容易被低估的就是这块。实验室开放预约真正的复杂度集中在两点时间重叠判断和并发重复提交。把这两个点做扎实系统就成功了大半。4.1 预约状态机六个状态流转关系用一张表说清预约状态不是“提交了就行”它要支撑审批、门禁、实验记录多个环节。常用六状态设计状态码状态可流转到PENDING待审核APPROVED / REJECTED / CANCELEDAPPROVED已通过IN_USE / CANCELEDIN_USE使用中FINISHEDFINISHED已完成终态REJECTED已拒绝终态CANCELED已取消终态如果代码里每个流转都用if (from A to B)写五个还能忍受状态多了就是灾难。我一般用一个 Map 维护所有允许的流转private static final MapReservationStatus, SetReservationStatus ALLOWED_TRANSITIONS Map.of( ReservationStatus.PENDING, Set.of(ReservationStatus.APPROVED, ReservationStatus.REJECTED, ReservationStatus.CANCELED), ReservationStatus.APPROVED, Set.of(ReservationStatus.IN_USE, ReservationStatus.CANCELED), ReservationStatus.IN_USE, Set.of(ReservationStatus.FINISHED) ); public void validateTransition(ReservationStatus from, ReservationStatus to) { if (!ALLOWED_TRANSITIONS.getOrDefault(from, Set.of()).contains(to)) { throw new BizException(非法的状态流转: from - to); } }这个写法比 if-else 多了一个好处以后要加“超时未使用自动取消”只需要在ALLOWED_TRANSITIONS加一行不用到处找逻辑。4.2 冲突检测时间重叠条件的正确写法判断两个时间段有没有重叠条件是start existEnd AND end existStart也就是新预约开始时间早于已有预约的结束时间且新预约结束时间晚于已有预约的开始时间。这个条件很容易被记错写成start existStart AND end existEnd只判断了包含关系漏掉了“部分重叠”的边界。SELECT COUNT(*) AS cnt FROM reservation WHERE lab_id #{labId} AND status IN (APPROVED, IN_USE) AND start_time #{endTime} AND end_time #{startTime}为什么排除 PENDING 状态如果待审核的预约也参与冲突检测学生 A 提交了一个 PENDING学生 B 提交同一个时段就被拦截但管理员同时可以驳回 A 的申请实验室就空出来了这种“占坑不拉屎”的体验很差。常见设计是 PENDING 不占资源只有 APPROVED 和 IN_USE 才参与冲突检查。注意边界是开区间也就是 10:00-11:00 和 11:00-12:00 是允许连续预约的不冲突。如果业务要求必须留 10 分钟打扫时间把条件改成start_time #{endTime} - 10分钟即可。4.3 并发重复提交用数据库行锁解决“超卖”冲突检测的 SQL 本身没法防止并发。两个请求同一时刻都查不到重叠然后都插入成功这就是典型的竞态。我尝试过乐观锁发现这里不适合冲突判断依赖的是“是否存在重叠记录”不是对同一行数据的 version 校验乐观锁根本锁不住这个条件。常见可靠的方案是悲观锁先锁实验室那行再插入预约Transactional public Long createReservation(Long userId, ReserveReq req) { Lab lab labMapper.selectForUpdate(req.getLabId()); if (lab null || lab.getStatus() ! 1) { throw new BizException(实验室不存在或未开放); } // 时间参数校验省略 long conflict reservationMapper.countConflict( req.getLabId(), req.getStartTime(), req.getEndTime()); if (conflict 0) { throw new BizException(该时段已被预约); } Reservation reservation new Reservation(); reservation.setUserId(userId); reservation.setLabId(req.getLabId()); reservation.setStartTime(req.getStartTime()); reservation.setEndTime(req.getEndTime()); reservation.setPurpose(req.getPurpose()); reservation.setStatus(ReservationStatus.PENDING.name()); reservationMapper.insert(reservation); return reservation.getId(); }selectForUpdate的 Mapper 方法Select(SELECT * FROM lab WHERE id #{id} FOR UPDATE) Lab selectForUpdate(Param(id) Long id);FOR UPDATE会对查询出的行加排他锁直到事务提交或回滚才释放。第二个请求执行同样的selectForUpdate会阻塞等前一个事务结束再继续此时它做的冲突查询就能看见前一个事务插入的预约记录从而正确拦截。使用时有几个关键点方法必须有Transactional没有事务行锁会在方法结束后立刻释放等于没锁锁的对象是lab行而不是reservation行因为预约可能根本不存在锁不到任何数据只有锁一个一定会存在的资源才有效事务里面不要调远程服务比如短信通知否则会无谓地长时间占用数据库锁。5. 上线避坑开放实验室项目最容易翻车的 5 个问题前四章讲的是照着写就能跑通的正向路径这一章是反向路径。下面每一条都是实际调试或交流中总结过的血泪经验现象写得通俗一些便于对照排查。5.1 预约时间差 8 小时现象前端选“今天 18:00-20:00”接口收到的也是 18:00可数据库里存的是 10:00-12:00等于预约到凌晨去了。原因JDBC URL 没带serverTimezoneMySQL 的系统时区又是默认的 SYSTEM驱动用了本地时区转换而 MySQL 内部认为是 UTC两边一加减就差出 8 小时。解决三步走。第一步JDBC URL 显式加serverTimezoneAsia/Shanghai第二步把数据库的time_zone也设为08:00第三步Jackson 的序列化时区在 yml 里加time-zone: GMT8。三步都做了不管前端浏览器什么时区后端存取都不会再变。5.2 建表报语法错误是保留字坑现象CREATE TABLE reservation里写了start和end字段工具和 SQL 都报错但网上查这两个词又不像 MySQL 关键字。原因START在 MySQL 8 里有特殊含义END直接是存储过程保留字作为列名时必须用反引号包起来。解决不要指望团队里的每一个人都记得反引号建表就直接用start_time、end_time。这个坑在写 XML 时会第二次出现所以越早改名越好。reservation表名本身也要尽量避免用order、group这类高频保留词。5.3 前端跨域调试时接口 403现象前端项目配置了 CORS后端也写了addCorsMappings结果浏览器打了 OPTIONS 预检请求接口直接返回 403浏览器显示跨域失败。原因如果后端还同时加了登录拦截器或 Spring Security过滤器的执行顺序在跨域处理器之前预检请求带着Origin头拦截器校验 token 必失败就会直接拒绝。解决在拦截器注册时把OPTIONS请求直接放行Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 其他请求再做 token 校验 return true; }如果用了 Spring Security在安全过滤链里配cors()是更规范的做法但项目没引入 Security 的话上面的方式最省事。5.4 门禁授权错位学生到点了进不去现象预约审核通过了但到了预约时间门禁刷不开管理员一查门禁授权队列里根本没有这条记录。原因门禁做了定时任务每天凌晨同步当天预约但实验室开放状态是运营时手动改的如果当天早上管理员把实验室临时关闭又开放定时任务只跑了一次授权就漏了。解决门禁授权改成按需拉取门禁端到点调开放实验室系统接口传实验室 id 加时间点后端实时返回这个实验室此刻有哪些人获准进入。当场失败当场重试不要再依赖每天一次的定时同步。5.5 并发申请出现死锁现象压测或者实际使用高峰日志里出现Deadlock found when trying to get lock; try restarting transaction重试又能成功。原因两个事务同时提交各自锁了不同的 lab 行然后又要求对方已锁的行。以预约场景看最可能是同时预约两个不同实验室时代码里锁的顺序不一致。解决让所有预约在加锁前先对 labId 排序按固定顺序加锁就不会形成循环等待。另一个是缩小事务范围把 Redis 操作、发消息、短信提醒全部移出事务只保留数据库操作。我踩过把发送企业微信通知写在事务里的坑消息服务超时 3 秒数据库锁就要多占 3 秒并发一大就是死锁重灾区。6. 从设计到真用门禁适配器、异步通知与统计三件事核心模块跑通后开放实验室才刚有“管理”的雏形真正让它和普通预约软件区分开的是接入真实环境的三件事。6.1 门禁对接先抽象接口再对接具体设备不要把门禁厂商 SDK 写进预约 Service。我一般定义一个接口业务层只依赖接口public interface AccessGateway { boolean authorize(Long userId, Long labId, LocalDateTime startTime, LocalDateTime endTime); }本地开发用一个假实现直接返回 true现场部署再写对接厂商 SDK 的实现通过配置切换。这样预约流程和门禁设备完全解耦换门禁厂商不动业务代码只动实现类。接口设计上只传时间和用户、实验室 id不要把整个预约实体传进去避免实现类和新版实体强耦合。6.2 审批通知改成异步事件审批通过后要发邮件、推企业微信如果同步写在审批方法里消息服务一旦慢审批接口就跟着卡。用 Spring 事件解耦// 审批通过后发布事件 applicationEventPublisher.publishEvent(new ReservationApprovedEvent(reservationId)); Async EventListener public void onApproved(ReservationApprovedEvent event) { // 组装消息内容调用消息服务 }启动类上别忘了EnableAsync。事件监听方法如果需要查数据库里刚更新的预约状态建议用TransactionalEventListener(phase AFTER_COMMIT)否则事务还没提交监听器查到的还是旧状态。6.3 一周热度统计按实验室聚合预约量运营侧最常用的统计是“哪个实验室最忙”可以先把 7 天预约量排个序SELECT lab_id, COUNT(*) AS total_cnt FROM reservation WHERE created_at DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND status NOT IN (REJECTED, CANCELED) GROUP BY lab_id ORDER BY total_cnt DESC;这里status NOT IN (REJECTED,CANCELED)是把没占资源的单子排除掉看的是真实容量消耗。统计结果可以做成每日定时快照也可以让前端按需请求量小的时候直接实时查也没问题。我早年做一个类似项目时总想把门禁、实验报告、耗材管理全部一次性做完结果三个月没上线。后来只保留预约主链路两周就交付了第一版门禁和统计在试运行期间才陆续加上。开放实验室系统的本质是“资源开放 秩序可控”数据库和状态机的优先级永远高于华丽界面把核心链路做稳扩展点留好这个方向就值得继续投入。希望帮到你。本文还有配套的精品资源点击获取