毕设体检管理系统实战:从需求拆解到高可用架构落地
最近在帮学校信息中心做一个毕设体检管理系统,从需求分析到最终上线,踩了不少坑,也积累了一些实战经验。这个系统主要解决学生毕业体检时预约冲突、数据分散、高峰期系统卡顿这些老问题。今天就把整个实现过程梳理一下,希望能给有类似需求的同学一些参考。
1. 背景与痛点:不只是做个预约表单那么简单
最开始接到需求,觉得不就是个“增删改查”吗?但深入分析后,发现高校毕设体检场景有几个很典型的问题:
- 高并发预约:几千名学生集中在几天内抢有限的体检时间段,瞬间流量很高,容易导致超卖(同一个时间段被多人预约)和系统响应慢。
- 状态不一致:一个学生的体检流程涉及“预约成功 -> 到场签到 -> 各科室检查 -> 报告生成 -> 报告审核”等多个状态。这些状态可能由不同模块或操作触发,如何保证状态流转的准确性和实时同步是个挑战。
- 导出性能差:辅导员或院系管理员经常需要批量导出学生的体检报告(PDF格式),如果同步生成,很容易导致请求超时,影响其他操作。
- 数据孤岛:学生的基本信息可能来自教务系统,体检结果来自医院LIS(实验室信息系统),如何安全、高效地整合这些数据,并做好权限隔离,也需要仔细设计。
2. 技术选型:为什么是这套组合拳?
明确了问题,接下来就是选型。我们团队主要技术栈是Java,所以后端框架很自然地考虑了Spring Boot和Django。
Spring Boot vs. Django:两者都是优秀的快速开发框架。最终选择Spring Boot,主要基于几点考虑:一是团队对Java生态更熟悉,Spring Boot在微服务、事务管理、数据库集成方面有非常成熟和强大的生态(如Spring Data JPA, MyBatis-Plus);二是考虑到后期可能与学校其他Java系系统(如统一身份认证)做集成会更方便;三是Spring Boot在应对复杂业务逻辑和并发控制方面,通过注解(如
@Transactional,@Async)可以更清晰、更声明式地管理。Django的ORM和Admin虽然开发效率极高,但在应对我们这种有复杂状态机和事务要求的场景时,感觉Spring Boot的掌控力更强一些。前端 Vue3 + TypeScript:前端选择了Vue3组合式API + TypeScript。Vue3的响应式系统更高效,组合式API让逻辑复用(比如预约状态管理、表单验证)非常清晰。TypeScript的加入是点睛之笔,它能在编码阶段就发现很多潜在的类型错误,尤其是对于体检报告这种数据结构复杂的对象,定义好Interface后,前后端协作和代码维护成本大大降低。相比于React,Vue3的学习曲线对团队更友好,能快速上手。
数据库与缓存:主数据库是MySQL 8.0,利用其窗口函数方便做一些统计查询。缓存用了Redis,主要做三件事:热门时间段库存缓存、分布式锁、以及用户会话信息存储。
3. 核心实现细节拆解
3.1 预约模块的幂等性设计
这是防止重复提交和超卖的核心。我们采用了“Token + Redis分布式锁 + 数据库唯一索引”三重保障。
- 前端按钮防重与Token:用户进入预约页面,前端向后端请求一个全局唯一的
submitToken,后端将其存入Redis并设置较短过期时间(如30秒)。提交预约请求时,必须携带此Token。 - Redis分布式锁:在预约扣减库存的关键代码段,我们使用Redis的
SETNX命令(Spring Boot中常用Redisson或Lettuce实现)对“日期+时间段”这个资源Key加锁。确保同一时间只有一个请求能处理某个时间段的库存扣减。 - 数据库唯一索引:在预约记录表上,我们建立了
(student_id, exam_date)的唯一索引。这是最后一道,也是最可靠的防线,从数据库层面杜绝了同一个学生在同一天重复预约。
关键代码片段(分布式锁部分):
@Service public class AppointmentService { @Autowired private RedissonClient redissonClient; public boolean makeAppointment(Long studentId, LocalDate examDate, String timeSlot, String submitToken) { // 1. 验证Token有效性 if (!validateToken(submitToken)) { throw new BusinessException("无效的提交令牌或已过期"); } String lockKey = "APPOINTMENT_LOCK:" + examDate + ":" + timeSlot; RLock lock = redissonClient.getLock(lockKey); try { // 2. 尝试获取锁,等待3秒,锁持有时间5秒 boolean isLocked = lock.tryLock(3, 5, TimeUnit.SECONDS); if (!isLocked) { throw new BusinessException("系统繁忙,请稍后重试"); } // 3. 核心业务逻辑:查询并扣减库存、创建预约记录 return doAppointment(studentId, examDate, timeSlot); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException("预约进程被中断"); } finally { // 4. 无论如何,最终必须释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private boolean doAppointment(Long studentId, LocalDate examDate, String timeSlot) { // 这里执行具体的库存检查和插入操作,需在事务内完成 // ... } }3.2 体检报告异步生成机制
报告生成是个耗时操作(组装数据、生成PDF)。我们采用Spring的@Async注解实现异步处理,并结合数据库状态字段来跟踪生成进度。
- 触发与状态记录:当所有科室检查结果都录入完毕,系统触发报告生成请求。此时,立即将报告记录的状态更新为“生成中”,并返回报告生成的“任务ID”给前端。
- 异步任务执行:服务层方法标记
@Async,在一个独立的线程池中执行PDF生成、文件存储(我们存到了MinIO对象存储)等耗时操作。 - 状态同步与通知:生成成功后,更新报告状态为“已生成”,并存储文件访问URL。同时,可以通过WebSocket或前端轮询的方式,通知用户报告已就绪,可以查看或下载。
这样做的好处是,主请求快速返回,用户体验好,系统吞吐量也上去了。
3.3 数据权限控制(RBAC + 数据范围)
系统用户角色多:学生、辅导员、院系管理员、校医院医生、超级管理员。我们采用了基于角色的访问控制(RBAC),并在其上扩展了数据范围过滤。
- 角色权限:定义角色(如
ROLE_STUDENT,ROLE_TEACHER),并关联权限点(如appointment:create,report:view)。 - 数据范围:这是关键。例如,辅导员只能查看自己学院学生的数据。我们在查询数据时,会自动通过MyBatis-Plus的
Interceptor(拦截器)或者直接在Service层,根据当前登录用户的角色和所属部门(学院),动态地向SQL查询条件中添加college_id = #{userCollegeId}这样的过滤条件。这样就在数据查询层面实现了隔离,安全又省心。
4. 性能与安全考量
- 接口限流:对于预约接口这类高并发入口,我们使用了Guava的
RateLimiter做单机限流,同时考虑在API网关层(如Nginx)配置全局限流,防止恶意刷接口。 - SQL注入防护:坚持使用MyBatis的
#{}参数绑定,严禁字符串拼接SQL。同时,定期用SQL注入扫描工具对代码进行审计。 - 敏感字段脱敏:在返回学生列表或报告信息时,对于身份证号、手机号等敏感信息,在DTO层或通过Jackson的
JsonSerializer进行脱敏处理(如110101****1234),确保数据安全。
5. 生产环境避坑指南
项目上线后,遇到了几个典型问题:
- 冷启动延迟:应用刚启动时,第一次访问数据库或远程接口特别慢。我们通过启动后执行一些预热查询(如加载基础数据到缓存),并合理配置数据库连接池的初始连接数来缓解。
- 事务传播失效:在同一个类内部,方法A调用方法B(B有
@Transactional),如果A没有通过代理调用(比如直接this.methodB()),B的事务注解会失效。务必确保通过Spring代理对象调用,或者将方法B放到另一个Service中。 - 跨域配置陷阱:开发环境配置了
@CrossOrigin,但生产环境用了Nginx反向代理。注意两者不要冲突,生产环境建议在Nginx统一处理CORS头,更安全高效。 - Redis连接池耗尽:在高并发下,如果Redis操作没有及时释放连接,会导致连接池耗尽。确保使用
try-with-resources(对于Jedis)或正确配置Lettuce连接池参数,并监控连接数。
6. 总结与扩展思考
整个项目做下来,最大的体会是:清晰的领域建模和合理的分治策略比炫技更重要。我们把系统拆分为“用户中心”、“预约服务”、“体检执行”、“报告服务”等几个相对独立的领域,通过领域事件进行松耦合通信,这让代码结构清晰,也便于后续维护。
这个系统目前是单校区部署。如果未来要扩展为多校区通用系统,我觉得可以从这几个方向思考:
- 数据隔离:采用数据库分库分表,按校区
school_id进行分片。或者在设计之初就预留多租户字段,通过中间件实现数据路由。 - 服务部署:每个校区可以部署一套独立的应用实例,共享核心的用户和权限中心。或者采用微服务架构,将各校区可独立的功能模块服务化。
- 配置化:将体检项目、时间段规则、报告模板等都做成可配置的,通过管理后台为不同校区进行差异化设置。
纸上得来终觉浅,绝知此事要躬行。如果你也在做类似的管理系统,强烈建议动手复现一下预约的幂等性控制和报告的异步生成这两个核心模块,里面涉及的并发、事务、异步编程思想,对能力提升非常有帮助。希望这篇笔记能对你有所启发。
