
1. 项目核心思路与需求拆解1.1 高校讲座预约的真实痛点先讲个身边的场景。某高校每学期都会安排几十场讲座涵盖学术前沿、就业指导、心理健康、通识素养等方向。以前是怎么管的学生会发通知大家到现场排队签到热门讲座经常出现两种情况一是报告厅挤爆了过道里都站满人消防通道都被堵住二是某些冷门讲座来了不到三分之一台上专家讲得尴尬台下学生玩手机。后来有人尝试用在线文档接龙报名但信息混乱、容易串改、无法统计到场率管理员光是核对名单就要花小半天。这个系统的核心价值就是把“讲座预约”这件事从人工排队和Excel表里解放出来形成一个学生在线预约、后台统一管理、签到数据自动统计的闭环。它不是一个花里胡哨的Demo而是真正能部署到校医院、图书馆报告厅、教学楼多媒体教室这类场景里用的管理系统。目标用户很明确学生、教师讲座主讲人、院系管理员、系统超级管理员四类角色。面向的学生可能几千人甚至上万人所以并发预约不能被一个简单的开关按钮带崩。从技术上我选了Spring Boot作为后端框架配合MyBatis Plus操作数据库前端用Vue或者Thymeleaf模板引擎做页面。数据库用MySQL部署在服务器上用Maven打包成jar直接跑。整套技术栈不新但胜在稳定、资料多、团队协作时上手快学生开发者照着官方文档也能七天落地。1.2 角色划分与核心业务流程这个系统不是孤立的预约工具它要覆盖讲座从发布到结束的完整生命周期。我们梳理下来核心角色分成四类学生用户浏览讲座列表、查看详情、在线预约、取消预约、讲座签到、查看自己的预约记录和学分/学时累计。讲座主讲人教师提交讲座申请填写主题、内容简介、时间、地点、容量等信息查看自己讲座的预约情况和到场率。院系管理员审核主讲人提交的讲座申请排定讲座计划查看本院系的预约数据统计导出参与名单。系统超级管理员维护基础数据教室、时间段、管理用户和角色权限、处理异常数据、查看全局统计报表。核心流程其实只有一条主线主讲人提交讲座申请管理员审核通过后讲座状态变为“可预约”学生在预约开放时间内抢名额讲座开始前或现场进行签到讲座结束后管理员确认讲座完成并统计实际到场人数。整个流程里有一个容易被忽略的细节预约不是无限开放的必须区分“预约开始时间”和“预约截止时间”否则学生会提前好几天把名额占满后面想听的人进不来。这个需求我在第一版设计时漏掉了结果被某院系老师反馈“学生下午四点半的讲座凌晨十二点就把票抢光了”后来才补上预约窗口的字段。1.3 功能边界与优先级取舍很多人做管理系统容易犯一个毛病啥功能都想加结果代码一团乱麻。我理功能清单的时候把需求分成“必须有”和“可以有”两档。必须有登录认证、角色权限控制、讲座CRUD、预约/取消、签到、预约记录查询、用户管理、基础数据统计。可以有但优先级低消息通知站内信、邮件、二维码电子票、黑名单机制、讲座评价、打印签到表、数据导出Excel。实际开发中我建议把二维码电子票和Excel导出放到第二阶段。不是说不重要而是第一版先把预约主流程跑通让学生能约、管理员能统计这就是上线的最小可用闭环。我们第一版上线时连二维码都没做签到用的是管理员手机端搜索学号手工确认虽然原始了一点但核心数据链路已经走通了。后来第二版加了二维码签到入场速度从每人20秒降到了3秒体验差异非常明显。2. 技术选型与架构设计解析2.1 为什么是Spring Boot而非其他方案我遇到过不少同学问为什么不用PHP或者Node.js甚至有人建议用Python的Flask。我的回答是Spring Boot在这个场景下不是“最好”的但它是“最不容易出错”的。首先高校里的系统往往要运行很多年维护的人可能是下一届的学生、另一个部门的老师甚至外包团队。Spring Boot加Java的技术栈是市场保有量最大的接手的人大概率见过这类代码不至于完全看不懂。其次Spring Boot的生态太完整了做权限有Spring Security做接口文档有Knife4j做数据库操作有MyBatis Plus出问题百度一搜全是解决方案。最后高校系统通常部署在校内服务器环境多半是老旧的CentOS或者Windows ServerJava的跨平台能力在这种环境下非常省心一个JDK加一个Jar包就完事了。对比一下其他方案的短板技术方案优势在本项目中的短板PHP部署简单、上手快长期维护的代码规范性往往不如Spring体系复杂业务逻辑容易写成面条代码Node.js高并发IO能力强高校运维团队对Node生态相对陌生部署和排障成本高Flask轻量灵活缺乏完整的权限、事务、ORM生态做管理系统需要自己拼装太多零件2.2 整体技术栈构成这个项目的技术栈我可以直接列出来都是很主流的组合JDK 8我用的是JDK 8稳定且兼容性好新项目也可以直接上JDK 17Spring Boot 2.7.x3.x也OK但要注意部分第三方starter的兼容性MyBatis Plus 3.5.x单表CRUD几乎不需要写SQL节省大量时间MySQL 5.7或8.08.0的窗口函数做统计非常方便Redis用于缓存讲座列表和分布式锁如果数据量不大也可以先不引入Vue 2 Element UI前端管理端学生端我用的是Thymeleaf加Bootstrap因为学生端的页面比较轻不需要太大的前端工程Maven依赖管理和打包这套组合的核心思路是后端业务逻辑用Spring Boot的依赖注入和事务管理来组织数据库操作交给MyBatis Plus减少样板代码缓存和锁用Redis解决热点数据的并发问题。比如讲座详情页会被频繁刷新第一次查完放进Redis后面直接命中缓存数据库的压力会大大降低。2.3 工程结构与分层设计工程结构上我采用了经典的四层架构没有过度设计。如果你拉过太多开源项目会发现很多人喜欢建一堆模块结果一个几百兆的小项目拆成七八个Maven子模块看起来高大上实际维护起来想哭。我建议用单模块加包名分层的方案简单直接的代码树com.example.lecture ├── controller // 接口入口只做参数接收和结果封装 ├── service // 业务逻辑层事务边界都在这一层 │ └── impl ├── mapper // MyBatis Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互的数据传输对象 ├── vo // 视图对象聚合多表查询结果 ├── config // 配置类Redis、拦截器、跨域等 ├── common // 通用工具类、统一返回结果、异常处理 └── enums // 状态枚举分层设计最重要的一个原则Controller只做参数校验和调用Service不要在里面写业务逻辑。我见过太多人把查询数据库的代码写在Controller里导致后面想复用逻辑的时候只能复制粘贴。Service层的每个方法代表一个完整的业务动作方法上标注事务注解。DTO和VO分开的好处是数据库字段变化不会直接暴露给前端接口。举个例子讲座实体类里有“备注”字段但前端展示时不需要这个字段VO里不包含就可以了返回给用户的JSON就干干净净。3. 数据库设计与核心表结构3.1 概念模型与ER分析数据库设计是整个项目的基石表结构没设计好后面写业务代码就是在烂地基上盖楼。我画ER图时先列出核心实体用户、讲座、预约记录、教室/场地、学院。关系也很清晰一个用户学生可以预约多个讲座一个讲座可以被多个用户预约这是典型的多对多关系通过中间表“预约记录表”来解除多对多关联。讲座与教室是多对一教室与学院是一对多。这里有一个建模时容易犯的错误把预约人数当成字段直接存在讲座表里比如reserved_count而不是通过预约记录表实时count。如果并发量不大直接count也能用但每次查询都要全表联合统计数据库会很吃力。我的做法是在讲座表里维护一个冗余字段reserved_count预约成功时加1取消预约时减1。这种用空间换时间的做法在中小型系统里非常实用配合事务控制数据一致性是可以保证的。3.2 核心表结构详解直接上核心建表SQL这几个表就是系统的骨架CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 登录账号学号或工号, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) NOT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色1学生 2教师 3院系管理员 4系统管理员, college_id bigint(20) DEFAULT NULL COMMENT 所属学院ID, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;CREATE TABLE lecture ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, title varchar(200) NOT NULL COMMENT 讲座标题, speaker varchar(50) NOT NULL COMMENT 主讲人姓名, speaker_intro varchar(500) DEFAULT NULL COMMENT 主讲人简介, content text COMMENT 讲座内容简介, location varchar(100) NOT NULL COMMENT 讲座地点, start_time datetime NOT NULL COMMENT 开始时间, end_time datetime NOT NULL COMMENT 结束时间, reserve_start_time datetime NOT NULL COMMENT 预约开始时间, reserve_end_time datetime NOT NULL COMMENT 预约截止时间, capacity int(11) NOT NULL DEFAULT 0 COMMENT 最大可预约人数, reserved_count int(11) NOT NULL DEFAULT 0 COMMENT 已预约人数, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审核 1已审核/可预约 2已截止 3已完成 4已取消, creator_id bigint(20) NOT NULL COMMENT 创建人主讲人ID, audit_remark varchar(200) DEFAULT NULL COMMENT 审核备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_start_time (start_time), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT讲座表;CREATE TABLE reservation ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, lecture_id bigint(20) NOT NULL COMMENT 讲座ID, user_id bigint(20) NOT NULL COMMENT 用户ID, reserve_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 预约时间, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0已预约 1已签到 2已取消 3爽约, check_time datetime DEFAULT NULL COMMENT 签到时间, source tinyint(4) DEFAULT 0 COMMENT 来源0网页 1移动端, PRIMARY KEY (id), UNIQUE KEY uk_lecture_user (lecture_id, user_id), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约记录表;这三张表是核心其他表比如学院表、教室表、操作日志表就按常规设计。需要注意的有几点第一预约记录表里的联合唯一索引uk_lecture_user它的作用是防止同一个人对同一场讲座重复预约这是数据库层面的最后一道防线即使业务逻辑有bug这条索引也能拦住脏数据。第二所有表的主键都用bigint自增不用雪花算法也不用UUID因为并发量没那么大自增主键在InnoDB里性能和存储上都更好。第三状态字段用tinyint而不是varchar省空间、查询快配合项目里的枚举类做对应映射可读性也不差。3.3 状态字段设计的重要性状态字段在整个系统里扮演的角色比我最初设想的更重要。讲座表里的status字段我特意设计了从0到4五个状态值分别对应待审核、已审核可预约、已截止、已完成、已取消。每个状态的流转方向是有约束的比如“已完成”的讲座不能再变回“可预约”“已取消”的讲座不允许再被预约。这就像现实中的流程一样讲座一旦结束数据就是历史数据不能随便改。预约记录表的status字段也有讲究0已预约、1已签到、2已取消、3爽约。区分“已取消”和“爽约”特别重要因为很多高校会把讲座考勤纳入第二课堂学分或综合素质评价。如果学生预约了但没去系统应该记录为“爽约”而不是简简单单删除记录。后来做统计报表时这个设计的价值就体现出来了辅导员可以直接看到每个学生的爽约次数不再需要人工翻聊天记录。4. 核心功能实现与关键代码解析4.1 预约流程控制与库存扣减预约是整个系统最核心的接口也是并发压力最大的地方。想象一下一场热门讲座放出来200个名额学生们在同一时间点疯狂点击预约按钮如果代码写得不对超出了预约数量上限超卖问题就来了。我见过很多新手写的预约逻辑是这样的先查讲座的reserved_count判断是否小于capacity如果小于就加1然后插入预约记录。这代码表面上逻辑没问题但在高并发下会出大问题。两个请求同时查到reserved_count199都判断小于200于是都执行了加1操作结果reserved_count变成了201超出了容量限制。为什么会这样因为查询和更新之间不是一个原子操作中间有间隙。正确的做法是利用数据库的行锁或乐观锁机制。我推荐的做法是直接在SQL层面做原子扣减Transactional(rollbackFor Exception.class) public ReserveResult reserveLecture(ReserveRequest request) { // 1. 校验讲座存在且状态为可预约 Lecture lecture lectureMapper.selectById(request.getLectureId()); if (lecture null || lecture.getStatus() ! LectureStatus.REVIEWED.getCode()) { return ReserveResult.fail(讲座不存在或不在预约时间内); } // 2. 核心原子扣减库存条件里带上 reserved_count capacity int updateRows lectureMapper.reduceStock(request.getLectureId()); if (updateRows 0) { return ReserveResult.fail(手速慢了名额已被抢完); } // 3. 再插入预约记录 Reservation reservation new Reservation(); reservation.setLectureId(request.getLectureId()); reservation.setUserId(request.getUserId()); reservation.setStatus(ReserveStatus.RESERVED.getCode()); reservationMapper.insert(reservation); return ReserveResult.success(); }对应的Mapper SQLupdate idreduceStock UPDATE lecture SET reserved_count reserved_count 1 WHERE id #{lectureId} AND status 1 AND reserved_count lt; capacity /update这个update语句利用了MySQL的行锁机制当多个事务同时更新同一行数据时它们会排队执行后执行的事务会基于前一个事务更新后的值重新判断条件。如果reserved_count已经等于capacity了条件reserved_count capacity就不成立了影响行数为0说明名额抢完了。这种方式既保证了不超卖又不需要手动加锁性能和安全性兼顾。4.2 冲突检测同一时间段多讲座另一个容易被忽略的需求是同一个学生能不能同时预约两个时间重叠的讲座从情理上讲学生是不能分身同时出现在两个会场的所以系统应该阻止这种冲突。实现思路也简单在插入预约记录之前先查一下该用户已预约且状态为“已预约”或“已签到”的讲座看其中是否有与新讲座时间重叠的记录。Transactional(rollbackFor Exception.class) public ReserveResult reserveLecture(ReserveRequest request) { // ... 省略前面的库存校验逻辑 ... // 新增时间冲突检测 Lecture newLecture lectureMapper.selectById(request.getLectureId()); ListLecture userLectures lectureMapper.selectOverlappingLectures( request.getUserId(), newLecture.getStartTime(), newLecture.getEndTime(), Arrays.asList(ReserveStatus.RESERVED.getCode(), ReserveStatus.CHECKED.getCode()) ); if (!userLectures.isEmpty()) { return ReserveResult.fail(您已预约了时间冲突的讲座 userLectures.get(0).getTitle()); } }对应的SQLSELECT DISTINCT l.* FROM reservation r INNER JOIN lecture l ON r.lecture_id l.id WHERE r.user_id #{userId} AND r.status IN (0, 1) AND l.status IN (1, 2) AND NOT (#{newEndTime} lt; l.start_time OR #{newStartTime} l.end_time)这个“NOT (结束时间小于等于对方开始时间 或 开始时间大于等于对方结束时间)”的判断就是区间重叠检测的经典写法涵盖了包含、被包含、交叉等所有重叠情况。4.3 签到逻辑与状态流转签到功能在第二版才加入二维码。管理员手机上打开一个小程序页面其实就是一个适配移动端的网页扫描学生出示的二维码识别学号后调用签到接口。签到接口的逻辑不复杂Transactional(rollbackFor Exception.class) public CheckResult checkIn(CheckInRequest request) { // 1. 判断当前时间是否在讲座开始前30分钟到结束后1小时之间 Lecture lecture lectureMapper.selectById(request.getLectureId()); Date now new Date(); if (now.before(DateUtil.addMinutes(lecture.getStartTime(), -30)) || now.after(DateUtil.addHours(lecture.getEndTime(), 1))) { return CheckResult.fail(不在签到时间段内); } // 2. 查询预约记录 Reservation reservation reservationMapper.selectByLectureIdAndUserId( request.getLectureId(), request.getUserId()); if (reservation null) { return CheckResult.fail(未查询到该学生的预约记录); } if (reservation.getStatus() ReserveStatus.CHECKED.getCode()) { return CheckResult.fail(该学生已签到请勿重复操作); } if (reservation.getStatus() ReserveStatus.CANCELED.getCode()) { return CheckResult.fail(该学生的预约已取消); } // 3. 更新签到状态 reservation.setStatus(ReserveStatus.CHECKED.getCode()); reservation.setCheckTime(now); reservationMapper.updateById(reservation); return CheckResult.success(); }签到状态的变化会联动讲座表的一个统计字段实际到场人数。讲座结束后管理员把这些到场人数对比预约人数就能得出到场率。这个数据在学期末做第二课堂学分认定时很有用。4.4 管理端讲座发布与审核管理端的核心操作是讲座的发布与审核。主讲人提交讲座申请后需要院系管理员审核。审核通过后讲座状态变为“可预约”系统会给学生推送消息如果有消息模块的话。审核的操作很简单就是一个update操作但审核后有一个关键动作初始化预约时间窗口。在讲座表里reserve_start_time和reserve_end_time这两个字段是在审核通过时由系统自动设置的。默认情况下预约开始时间是审核通过时刻预约截止时间是讲座开始前2小时。这样设计的考虑是主讲人提前一周提交申请管理员审核通过后立即开放预约到讲座开始前2小时截止让学生能从容安排时间。如果某个院系想设置自定义的预约窗口也可以在审核页面手动修改。审核这里还有一个细节讲座状态是“待审核”时学生端是看不到这条讲座的。我第一版没注意结果学生端把待审核的讲座也展示出来了学生点了预约结果提示“讲座暂未开放预约”搞得大家一头雾水。后来我在查询讲座列表时加了一个过滤条件学生端只能看到status为1可预约和2已截止的讲座避免数据混乱。5. 前端页面与交互设计思路5.1 页面结构与路由组织这个系统我做了两个端管理端和学生端。管理端的页面比较重用了Vue加Element UI典型的后台管理布局左侧菜单顶部导航中间内容区。菜单包括讲座管理、预约管理、用户管理、统计报表、系统设置。学生端则轻量很多用Thymeleaf模板渲染页面包括首页讲座列表、讲座详情、我的预约、个人中心。学生端的讲座列表页是整个系统访问量最高的页面必须做分页。我一开始做的是全量查询结果讲座多了以后页面加载明显变慢后来改成了MyBatis Plus自带的分页插件每页10条配合标题模糊搜索和时间排序体验就好多了。列表页每个讲座卡片上要展示讲座标题、主讲人、时间、地点、剩余名额。剩余名额这个字段很关键它会刺激学生的紧迫感也是预约转化率的重要推动因素。讲座详情页则需要更完整的信息讲座简介、主讲人背景、内容大纲、往期评价如果有的话。页面上最重要的按钮是“立即预约”这个按钮不是永远可点的要根据讲座状态动态变灰或变红。状态为“可预约”且当前时间在预约窗口内按钮显示“立即预约”名额满了显示“已满员”预约截止了显示“预约已结束”讲座已完成显示“查看回放”如果没有回放就显示“已结束”。这些状态判断在前端做一次后端接口再做一次双保险。5.2 移动端适配与用户体验必须强调学生端一定要优先考虑移动端体验。现在大学生基本都是用手机刷讲座信息没人会打开电脑登录系统去预约。我在第二版改版时专门把学生端做成了响应式布局Bootstrap栅格系统搭配媒体查询让页面在手机浏览器上能流畅操作。具体来说几个关键页面的移动端适配方案讲座列表卡片式设计一张卡片就是一整屏宽度的信息块标题在顶部中间是讲座信息底部是预约按钮。讲座详情头部大标题下方时间地点信息用图标加文字排列简介部分可折叠默认只显示两行点击展开。我的预约用Tab切换“已预约”“已签到”“已取消”“爽约记录”四类每条记录展示讲座信息、状态标签和操作按钮取消预约、查看电子票。还有一个小细节预约按钮的点击反馈要即时。我加了按钮防重复点击的机制前端通过disabled属性控制点击一次后按钮变成“处理中...”等接口响应后再更新状态。不然学生们连点五下后端就要处理五次请求虽然事务能兜底但日志里全是重复操作的记录排查问题时看着就心烦。6. 常见问题与排查技巧实录6.1 并发预约导致的超卖问题第一个要说的坑肯定是超卖。我上面虽然讲了原子扣减的写法但第一次上线时用的就是经典的“先查后写”结果就被现实教育了。当时某学院发布了一场知名校友的讲座开放预约后几十秒内就涌入几百个请求后台日志里出现了reserved_count超过capacity的情况一条讲座数据被预约了201人容量只有200。排查过程是这样的打开后台日志搜索对应讲座的预约操作记录发现同一秒内有三个请求都查到了reserved_count199都满足了小于200的条件然后依次执行了更新和插入。虽然每条记录都成功了但库存已经超了。解决方式就是前面提到的把“查询更新”合并成一条原子UPDATE语句。改完之后我又加了一个保险在插入预约记录前先用联合唯一索引做一次捕获如果插重复了就报错回滚双保险更稳。6.2 事务失效的场景说到事务很多人只知道在方法上加Transactional注解但注解不是万能的。我遇到过事务不生效的情况排查了好久才发现原因方法内部自己调了自己。Spring的事务代理机制是基于AOP的只有通过代理对象调用方法时事务注解才会生效。如果你在一个Service方法里直接调用同一个类的另一个带事务注解的方法事务是不会启用的因为调用的是this对象的方法没有走代理。那个场景是这样的我在Service里写了一个公开方法createLectureAndPublish它调用了同一个类里的createLecture方法该方法上有Transactional注解。结果createLecture里的操作没被事务管理哪怕抛了异常前面插入的数据也不会回滚。解决办法有两个要么把需要事务的方法拆到另一个Service类里要么在调用时通过Spring注入的代理对象调用。我选择了拆类的方案更清晰也更符合单一职责。另外还有一个容易踩的坑事务方法内部catch了异常不往外抛。事务回滚的触发条件是异常传播超过了事务边界如果你在方法内部把异常吞掉了事务自然认为一切正常不会回滚。所以catch块里至少要抛出RuntimeException或者用rollbackFor显式指定回滚的异常类型。6.3 状态不一致的现场还原还有一个数据一致性问题是签到和预约统计对不上。有一次管理员反馈某场讲座预约显示了180人但实际签到表只有120人差了整整60人。我第一反应是签到功能有bug但查了半天代码逻辑没问题。后来发现原因在业务规则上系统允许学生在讲座开始前取消预约取消后reserved_count会减1但讲座的“实际到场人数”统计的是签到表中状态为“已签到”的记录。所以预约人数180人包含了中途取消又新增的预约实际到场120人是最终的签到人数两者之间没有直接关系。这不是bug是统计口径的问题。解决方案是在统计报表里分别展示“累计预约人次”“当前有效预约人数”“实际签到人数”“取消次数”“爽约次数”五个指标让管理员根据自己的需要选择使用哪个口径。同时在报表页面加了一个文字说明解释每个指标的计算方式避免再被当成bug提过来。6.4 常见问题速查表问题现象可能原因解决方案预约人数超容量查询和更新非原子操作改用UPDATE原子扣减SQL事务不起作用同类内部方法调用拆分Service类或使用代理对象调用用户能重复预约一场讲座业务校验遗漏加联合唯一索引兜底学生端可以看到待审核讲座查询过滤条件缺失列表查询条件限制status范围签到时间判断不准确时区或DateFormat问题统一用服务器时区传参用时间戳预约了但名单上没有预约记录被误删用状态标记替代物理删除同一个时间段预约了多场讲座冲突检测逻辑缺失加区间重叠检测SQL管理员导出名单乱码Excel工具类未设置编码使用EasyExcel并指定UTF-8提示排查问题的时候不要一上来就去看代码先复现问题、确认现象再根据现象反推代码路径。很多“bug”其实是使用方式和业务口径的问题先和提出方对齐需求能省下大量无用功。我个人在实际操作中的体会是这个系统的技术难度其实不大真正的难点在于把业务规则想清楚然后把数据模型设计得足够稳。很多看起来很复杂的功能比如并发预约、时间冲突检测、签到统计本质上都是几个字段和几条SQL的事。反而是那些不起眼的小细节——预约窗口的时间控制、状态流转的约束、统计口径的统一——决定了整个系统好不好用。后面如果要扩展可以在这个基础上加消息推送预约成功通知、讲座开始前提醒、讲座评价打分、线上直播链接集成甚至对接学校的统一身份认证那基本上就是一个贴近生产的完整平台了。你可以先把核心流程跑通再根据实际反馈迭代这是做这类管理系统最务实的一条路。