1. 高校医疗场景的特殊性这系统到底该做成什么样1.1 校医院业务预防为主、门诊为辅的特征每年九月的开学季校医院门口排队体检的队伍能从挂号窗口一直排到路边。这个场景相信大家都不陌生。校医院和综合医院最大的区别在于它不需要应付疑难杂症核心业务是预防保健入学体检、毕业体检、疫苗接种、传染病防控、慢性病跟踪。而日常门诊虽然也有但通常集中在感冒发烧、运动损伤、换季过敏这几类常见病上。这个业务特征直接决定了系统的功能图谱和优先级。如果一上来就照搬综合医院信息系统的模块清单那项目会越做越大最后连自己都收不住。我接手这个项目时甲方学校信息中心确实曾经做过一个什么模块都想要的版本结果用了半年就搁置了门诊大夫嫌流程太绕学生嫌预约不便。后来重新梳理时我们只保留了四个核心闭环学生健康档案管理、预约挂号、体检管理、健康预警与转诊跟踪。事实证明这样做反而是最被认可的。1.2 系统边界不是HIS也不是电子病历很多第一次做医疗相关系统的开发者容易被医疗系统四个字吓住或者反过来想一口吃成胖子把药房进销存、财务结算、电子病历全塞进来。我要泼一盆冷水这些内容在校医院场景下大概率是伪需求。校医院通常不具备完整的电子病历管理资质病历数据往往要上报到上级卫生平台校内系统只需要留存一份可供查询的摘要即可。药房管理如果做进这个系统里做一个库存台账没问题但要做到能和财务对账级别的进销存那就是另一套GSP系统的活了。所以我在一开始就给项目画了一条边界线做个人健康档案含体检数据、预约挂号、门诊日志、体检批次管理、异常指标预警、转诊审批、统计报表。不做电子病历全文、药品进销存财务、医保对接、排班考勤。这个边界需要在论文的需求分析章节里写清楚。评审老师最喜欢问的一个问题就是你这个系统和医院HIS有什么区别如果你能讲清楚边界和理由这道题就过了。1.3 从用户视角拆解核心诉求再进一步我把系统涉及的角色和他们的真实诉求列了一个矩阵这个矩阵也是后面做权限设计和功能开发的依据角色日常诉求系统功能映射学生快速预约门诊/体检查看自己的档案和报告预约挂号、健康档案查询、体检报告查看校医处理预约、登记门诊、录入体检结果减少重复劳动接诊管理、体检结果批量导入、门诊日志辅导员知道本学院学生有没有重大健康异常但不需要看细节健康预警通知脱敏、在校生健康统计系统管理员基础数据维护、账号管理、报表导出用户管理、字典管理、统计报表在这个矩阵基础上我建议所有做这个题目的朋友都画一张核心业务流程图。我的流程图其实就是一条主线学生入学 → 建立健康档案 → 参加体检/门诊 → 系统分析指标 → 命中阈值生成预警 → 异常学生进入转诊或跟踪 → 毕业时档案归档。整个系统就是围绕这条主线展开的后面的数据模型设计和接口设计都跟着它走。2. 技术选型与工程结构SpringBoot 2.7为主干的前后端分离方案2.1 版本选型为什么锁定SpringBoot 2.7而非3.x说到技术选型这里分享一个实际决策过程。当时做评估时SpringBoot 3.x已经发布了但我在对比之后还是选了2.7.x主要原因有三个第一3.x基于Jakarta EE 9包名从javax改成了jakarta这意味着很多旧的三方库需要跟着升级。校医院项目并不追求新特性稳定性压倒一切。第二2.7是3.0发布前最后一个成熟的2.x版本社区资料、面试题、毕设参考代码几乎都兼容这个版本学生做毕设时遇到问题更容易搜到答案。第三绝大多数高校的Java课程还停留在Java 8/11SpringBoot 2.7恰好完美兼容Java 8而SpringBoot 3.x要求Java 17起步换运行时环境会带来一堆额外麻烦。所以我的结论很明确如果不是有迫不得已的理由面向校园场景的应用就选SpringBoot 2.7.18 Java 8。你要在论文里写清楚对比过程这比直接写用了SpringBoot要加分得多。2.2 ORM选型MyBatis-Plus比JPA更适合这种项目ORM框架怎么选我对比过MyBatis-Plus和Spring Data JPA最终选了前者。这里不是说我否定JPA而是在这个项目的具体约束下MyBatis-Plus有三个压倒性优势。第一MyBatis-Plus内置了单表CRUD、分页插件、逻辑删除、自动填充这些能力像健康档案表、预约记录表这类单表操作基本不需要手写SQL。第二SQL完全可控一旦涉及多表关联查询比如按学院统计体检异常率你可以直接写原生SQL出了问题也好排查这是JPA的复杂关联查询比不了的。第三国内做Java毕设和中小项目的团队用MyBatis-Plus的比例非常高对于学生来说参考资料多遇到问题在社区一搜就有答案。数据访问层的具体做法是实体类上用TableName、TableId、TableField注解做映射逻辑删除字段加TableLogic创建时间和更新时间用MetaObjectHandler自动填充。分页就通过内置的PaginationInnerInterceptor实现不用自己写PageHelper省了一道桥接的功夫。2.3 前端与中间件Vue3 Element Plus、MySQL、Redis前端框架我选了Vue3配合Element Plus构建工具用Vite。Element Plus的表单组件、表格组件、弹窗组件都直接可用对于管理后台预约页面这种典型界面来说开发效率是最高的。Vue3的组合式API在写预约流程这种带状态流转的页面时逻辑复用比Vue2的选项式API更顺手。这一块没有太多悬念前端不是这个项目的主战场够用、稳定、好看就行。后端配套中间件是MySQL 8.0 Redis。MySQL存所有业务数据Redis承担两个职责一是缓存预约时段的实时余量二是存放登录验证码和JWT黑名单。Redis的引入要克制不要什么数据都往里塞我只在预约高峰和验证码场景使用核心业务数据一律以MySQL为准。文件存储方面体检报告PDF、检验单图片这类附件考虑到服务器资源和部署复杂度我直接存在本地磁盘通过Nginx做静态映射。如果将来真要上云再迁移到MinIO或OSS也方便。2.4 工程结构按业务域分包而不是单纯按技术层分工程结构上很多初学者喜欢把controller、service、mapper、entity各建一个包然后所有业务往里面乱塞。前几个模块还好到了第十个模块就开始混乱。我这里采用的是外层按技术层内层按业务域的混合结构。简单说com.example.univhealth ├── common # 通用返回体、异常、工具类 ├── config # 配置类安全、跨域、MyBatis、Redis ├── security # Spring Security、JWT、UserDetails ├── modules │ ├── profile # 健康档案 │ ├── reserve # 预约挂号 │ ├── exam # 体检管理 │ ├── clinic # 门诊日志 │ ├── alert # 健康预警 │ └── system # 用户、角色、菜单、字典 ├── task # 定时任务体检状态扫描、清理过期时段 └── utils模块之间通过Service接口交互禁止跨模块直接调用Mapper。比如reserve模块的Service要查询学生基本信息只能调用profile模块暴露的接口不能自己去查t_student_health_profile表。这个规矩能保证后续扩展的时候不会把模块间的耦合搅成一锅粥。在论文的系统设计章节里画一张模块依赖图评审老师一看就知道你不是随便分的包。3. 核心数据模型设计看这几张表怎么把业务闭环串起来3.1 学生健康档案表入学的第一份数据资产健康档案是整套系统的基础所有业务都离不开它。表的设计我建议至少包含这些字段CREATE TABLE t_student_health_profile ( id bigint(20) NOT NULL AUTO_INCREMENT, student_no varchar(20) NOT NULL COMMENT 学号唯一索引, name varchar(50) NOT NULL, gender tinyint(1) NOT NULL COMMENT 1男 2女, birthday date DEFAULT NULL, college varchar(100) NOT NULL COMMENT 学院, major varchar(100) DEFAULT NULL COMMENT 专业, class_no varchar(50) DEFAULT NULL COMMENT 班级, blood_type varchar(10) DEFAULT NULL, allergy_history varchar(500) DEFAULT NULL COMMENT 过敏史, chronic_disease varchar(500) DEFAULT NULL COMMENT 慢性病史, emergency_contact varchar(50) DEFAULT NULL, emergency_phone varchar(20) DEFAULT NULL, exam_status tinyint(1) DEFAULT 0 COMMENT 0未体检 1已体检, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生健康档案表;有几个细节值得说。一是student_no必须设唯一索引因为学号是校园里唯一标识一个人的业务键。二是allergy_history和chronic_disease我建议用字符串存而不是用JSON字段。原因很简单这两类信息在体检问诊时只是有没有、是什么的级别不需要结构化到过敏原编码那么细字符串够用了。三是逻辑删除字段deleted一定要加学生转学或退学后档案不能物理删除这是医疗数据的合规要求。四是从这张表延伸出的一个常见面试题如果学生毕业后再次入校读研学号变了健康档案怎么关联我的方案是在表里加一个pre_student_no字段记录前置学号保证历史体检数据的可追溯性。3.2 预约挂号模型时段槽位设计是重点预约挂号能不能做好核心在时段槽位的设计上。我的做法是把医生某一天的某个时间段抽象成一个可预约的Slot时段而不是简单地记录某天有号。CREATE TABLE t_reserve_slot ( id bigint(20) NOT NULL AUTO_INCREMENT, doctor_name varchar(50) NOT NULL COMMENT 坐诊校医, dept_name varchar(100) NOT NULL COMMENT 科室, clinic_date date NOT NULL COMMENT 门诊日期, time_slot varchar(30) NOT NULL COMMENT 时段如09:00-09:15, total_num int(11) NOT NULL COMMENT 总号源, remaining_num int(11) NOT NULL COMMENT 剩余号源, status tinyint(1) DEFAULT 1 COMMENT 1可预约 0停诊, PRIMARY KEY (id), UNIQUE KEY uk_date_slot_doctor (clinic_date, time_slot, doctor_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约时段表;这个设计的核心价值在于系统能精确控制每个时段的最大接诊量比如规定每15分钟最多放3个号这样既保证了校医有充足的接诊时间也避免了学生扎堆排队。对应地学生预约记录表需要一个比较完善的状态机CREATE TABLE t_reserve_record ( id bigint(20) NOT NULL AUTO_INCREMENT, student_no varchar(20) NOT NULL, slot_id bigint(20) NOT NULL, reserve_status tinyint(1) DEFAULT 1 COMMENT 1已预约 2已取消 3已完成 4爽约, reserve_time datetime DEFAULT NULL, cancel_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_slot_student (slot_id, student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约记录表;这里加的uk_slot_student唯一索引是从数据库层面兜底防重复预约的关键。哪怕业务层逻辑写漏了数据库也会拒绝同一个人预约同一个时段两次。3.3 体检批次与结果为什么用两张表而不是一张大宽表体检管理我采用了批次-明细两表设计。一张t_exam_batch存批次信息比如2024级新生入学体检字段包括batch_name、exam_type、target_group、start_date、end_date、status。另一张t_exam_item_result存每个学生每个体检项目的具体结果CREATE TABLE t_exam_item_result ( id bigint(20) NOT NULL AUTO_INCREMENT, batch_id bigint(20) NOT NULL, student_no varchar(20) NOT NULL, item_name varchar(50) NOT NULL COMMENT 检查项目如身高/体重/视力/ALT, exam_value varchar(100) DEFAULT NULL COMMENT 检查值, unit varchar(20) DEFAULT NULL, reference_range varchar(100) DEFAULT NULL COMMENT 参考范围, result_flag tinyint(1) DEFAULT 0 COMMENT 0正常 1异常, checked_by varchar(50) DEFAULT NULL COMMENT 录入医生, PRIMARY KEY (id), KEY idx_batch_student (batch_id, student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检项目结果表;有的同学会问为什么不用JSON字段直接存一个学生的所有体检结果一张表多省事我的回答是如果是纯粹做查看报告功能JSON确实够用但一旦你要做全校学生血压异常的统计、按学院对比体检合格率、连续两年体重的变化趋势用JSON就得把每条数据解析出来再聚合性能和写法都很难受。用明细表后这些统计无非就是SQL的GROUP BY item_name WHERE result_flag1的事。我在论文里专门用了一小节解释这个宽表vs明细表的取舍答辩时老师对这个回答比较认可。3.4 门诊日志与药品台账轻量但必须存在门诊模块我用了一张t_clinic_visit表记录每次门诊的主诉、诊断、处理意见、开具药物等关键信息。注意这里不追求做到电子病历级别只要记录到某个学生某天因什么问题来看过病、处理方案是什么即可。为了后面统计各季节的常见病趋势visit_date和disease_type两个字段要设索引。药品台账t_drug_stock我只做基础库存记录药品名称、规格、库存量、预警阈值。每次门诊开药后通过Service层事务同时更新门诊记录和药品库存。这里要强调一个很容易犯的错开药减库存和门诊记录必须放在同一个事务里不然系统运行一段时间后库存数和实际库存就对不上了。4. 多角色权限体系从RBAC到数据权限的一次完整落地4.1 角色权限矩阵先梳理清楚谁干什么权限设计最忌讳一上来就写代码。我先把角色和权限整理成矩阵后面实现时照着落地就行角色可访问功能学生提交预约、取消预约、查看本人健康档案与体检报告、提交转诊申请校医处理预约、登记门诊、录入/导入体检结果、查看接诊记录、审批转诊辅导员查看本学院学生的健康预警通知脱敏、查看在校生体检完成统计系统管理员用户管理、角色管理、时段配置、药品台账、字典维护、全量报表功能权限我用Spring Security的PreAuthorize(hasRole(DOCTOR))这种注解控制界面菜单由后端返回动态路由根据不同角色渲染不同页面。这里就不展开详细代码了网上的例子很多真正容易翻车的是数据权限。4.2 数据权限隔离辅导员只能看本学院校医只能看自己批次假如你做完功能权限就收工系统会有一个致命漏洞辅导员路由/api/alert/list查到的是全校的健康预警列表。因为功能权限只控制了能不能进这个接口没控制能看哪些数据。这就是数据权限要做的事。我的实现方案是在Mapper层做数据范围拦截利用MyBatis-Plus的DataPermissionInterceptor在SQL执行前自动拼接数据范围条件。具体规则是辅导员角色访问学生列表或预警列表时自动在SQL后面拼AND college 当前辅导员所属学院。校医角色访问体检结果时自动拼AND checked_by 当前登录校医姓名。系统管理员不做任何拼接。如果不想用拦截器也可以用自己的AOP切面在Service层注入查询条件。但实名建议用拦截器方案因为它的作用对业务代码是透明的不会在几十个Service方法里到处漏条件。这个设计我认为是整套系统里最值得在论文里展开写的部分它直接体现了你对权限这个词的理解深度。4.3 Spring Security JWT的授权链路认证授权我用了Spring Security JWT的组合。整体流程是登录接口校验账号密码密码存的是BCrypt哈希成功后签发一个JWT客户端把Token放在请求头的Authorization字段里后续所有请求通过一个OncePerRequestFilter过滤器解析Token把用户ID和角色权限塞进SecurityContext。这里有两个细节必须提醒。第一JWT的密钥不能硬编码在代码里要放到application.yml并用环境变量覆盖防止打包后泄密。第二为了方便退出登录和修改密码后踢下线我在Redis里维护了一个JWT黑名单Token加入黑名单后过滤器就会拒绝。单机部署这套方案够用完全不需要引入独立的授权服务器。spring: security: jwt: secret: ${JWT_SECRET} expire-hours: 12Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(STATELESS) .and() .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/captcha).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .requestMatchers(/api/doctor/**).hasRole(DOCTOR) .requestMatchers(/api/counselor/**).hasRole(COUNSELOR) .anyRequest().authenticated()) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }4.4 转诊审批用状态机管理一条业务链路转诊流程是系统中比较有意思的一个功能。学生提交转诊申请后记录状态经历待辅导员初审 → 待校医复审 → 已通过/已驳回 → 已转诊学生到外院就诊后回填结果。我在代码里用了一个简单的状态机枚举类每个状态定义允许的下一步动作不允许跳转。这样写的好处是流程清晰不会因为代码里一堆if/else判断导致状态混乱。答辩时如果被问到转诊过程的边界情况怎么处理你可以回答同一次申请只能存在一条流转记录通过或驳回后自动发站内信通知申请人学生端还能看到审批意见和材料附件的完整时间线。5. 关键功能实现细节与避坑记录5.1 预约防超卖原子UPDATE比Redis锁更省心先说出我的结论在这个项目的并发量级下数据库行锁原子更新就是最优雅的防超卖方案。校医院预约高峰是什么概念流感疫苗接种日一个上午放出500个号同一秒内的并发请求最多几十个远没到需要上消息队列和分布式锁集群的程度。核心代码很简单Transactional public boolean reserve(Long slotId, String studentNo) { // 原子扣减剩余号源大于0才允许更新影响行数为0说明已约满 int updated reserveSlotMapper.decrRemaining(slotId); if (updated 0) { throw new BusinessException(该时段已约满); } ReserveRecord record new ReserveRecord(); record.setSlotId(slotId); record.setStudentNo(studentNo); record.setReserveStatus(1); reserveRecordMapper.insert(record); return true; }UPDATE t_reserve_slot SET remaining_num remaining_num - 1 WHERE id #{slotId} AND remaining_num 0;这里关键点是UPDATE如果更新的行数为0说明remaining_num已经不满足大于0的条件直接抛异常。加上unique(slot_id, student_no)索引兜底防重复预约加事务保证扣减和创建记录要么都成功要么都失败。我在测试环境用JMeter模拟过100并发抢同一个时段没有出现超卖单次扣减平均耗时在5毫秒以内。有些设计方案喜欢先在Redis里预减库存再异步同步DB这个思路适用于秒杀级别的高并发但校医院场景不需要反而会因为Redis和DB的一致性核对问题增加复杂度。5.2 体检结果导入EasyExcel处理校医院样式混乱的Excel校医院的数据来源非常杂有些是体检机构导出的Excel有些是校医用电子表格自己录的表头格式五花八门。用Apache POI直接读读一遍下来你要写一大堆遍历Cell的样板代码。我改用阿里的EasyExcel开箱即用的监听器模式极大简化了操作。public class ExamResultListener implements AnalysisEventListenerExamResultRow { private final ListExamResultRow batch new ArrayList(); private static final int BATCH_SIZE 1000; Override public void invoke(ExamResultRow row, AnalysisContext context) { batch.add(row); if (batch.size() BATCH_SIZE) { saveBatch(batch); batch.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!batch.isEmpty()) { saveBatch(batch); } } }踩坑来了。校医院提供的Excel表头经常带着合并单元格、全角括号、不可见空格比如身高(cm)和身高cm其实是两列。EasyExcel的ExcelProperty注解默认按表头名匹配遇到这种情况就会失效甚至报错。我的解决办法是优先用index属性指定列序号而不是用name匹配表头同时对学号、姓名这类关键列做非空校验不满足条件的行单独收集起来导入完成后生成一份失败数据清单让校医下载核对。另一个容易被忽略的点是批量插入时不要一条一条insert。我用MyBatis-Plus的saveBatch配合自定义SQL实现批量插入一万条体检数据从解析到入库耗时从几十秒降到了两三秒。体检结果保存后触发Spring的ApplicationEventPublisher发布一个体检结果导入完成事件健康预警模块异步去处理异常指标不用在校医录入的前端页面里傻等着算预警。5.3 健康预警规则不引入规则引擎一张表就够了如果按照企业级做法健康预警通常上Drools这类规则引擎。但对这个项目来说那是杀鸡用牛刀。我只需要一张规则表加一个策略处理器即可。CREATE TABLE t_alert_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, rule_name varchar(100) NOT NULL, item_name varchar(50) NOT NULL COMMENT 指标名称如ALT/血压, condition_type varchar(20) NOT NULL COMMENT GT / LT / BETWEEN, threshold_min varchar(20) DEFAULT NULL, threshold_max varchar(20) DEFAULT NULL, severity tinyint(1) DEFAULT 1 COMMENT 1提示 2警告 3严重, message_template varchar(500) DEFAULT NULL, enabled tinyint(1) DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预警规则表;体检结果入库后异步逻辑遍历当前学生的异常项目去规则表里匹配对应的item_name和condition_type命中就生成一条t_health_alert记录并根据规则等级决定是否推送通知。比如ALT高于正常值上限的2倍会同时给辅导员发一条脱敏预警只显示学院和学号不显示具体数值而体检结果显示疑似传染病这类场景则直接标记严重等级通知校医院管理员人工复核。这样做的好处是校医院想要调整某个指标的预警阈值不用改代码重新部署直接在管理后台的规则表里改一行就行。这块代码量不大但功能价值突出很值得在论文里把它当作系统亮点来写。5.4 文件上传、跨域与全局异常的三件套文件上传踩过的坑主要是中文文件名和格式校验。我的处理是上传的文件一律用UUID重命名后存磁盘文件类型通过白名单检查文件大小限制10MB。华为手机上拍的照片文件名经常带一串奇怪的字符直接存会导致Nginx静态映射时URL乱码UUID重命名能完美避开这种问题。跨域配置也踩过坑。用Spring的CorsFilter配置时allowedOrigins不能设为*因为一旦allowCredentials(true)浏览器会拒绝带凭证的跨域请求。正确做法是指定具体的来源列表比如http://localhost:5173Vite开发服务器地址。全局异常处理我统一用RestControllerAdvice把所有业务异常、参数校验异常、系统异常包装成统一返回体ResultT前端axios拦截器里根据code字段做统一提示。有一个细节业务异常的code不要用负数前端判断成功与否统一看code 200负数的含义容易带来困惑。这种约定看似不起眼但多人协作时非常影响效率。5.5 定时任务与数据统计别小看这两块系统里还有两个容易被低估的功能一是定时任务二是统计报表。定时任务我用Spring自带的Scheduled注解每天凌晨清理已过期的预约时段每周日凌晨统计一次全校体检完成率并把未完成体检的学生名单生成提醒任务。这里不需要上XXL-Job这种分布式任务调度平台单机部署的话Spring自带的调度器完全够用。统计报表则集中在管理员端按学院、年级的体检完成率各科室的门诊量趋势异常指标Top10排名。我都通过聚合SQL实现前端用ECharts展示折线图和柱状图。有些讲究的同学会单独建一张汇总表定时任务跑批更新这样报表页面的查询性能更好。如果数据量不超过10万条直接查明细表临时聚合也完全没问题这个量级不需要引入OLAP引擎。6. 部署落地与安全加固从IDEA到一台2C4G服务器6.1 资源规划一台轻量服务器跑起来真不难做这个项目的学生很多在部署环节出问题。其实校园健康系统的负载量是很明确的一所两万人的高校一天的门诊预约量撑死几百条。我用的是一个2C4G的轻量云服务器跑了后端Jar包、Nginx、MySQL、Redis四样东西实测高峰时段CPU占用率不超过50%内存稳定在3G以内。预算有限的同学部署到腾讯云/阿里云的轻量应用服务器上是完全可以的甚至学习阶段用本地虚拟机跑通就行。6.2 Docker Compose编排一次搞定所有依赖如果你的服务器比较干净我建议直接用Docker Compose编排全部依赖避免手动装MySQL、Redis时踩版本坑。我在项目里提供了一个docker-compose.ymlversion: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD} MYSQL_DATABASE: univ_health volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - 3306:3306 restart: always redis: image: redis:7.0 ports: - 6379:6379 restart: always app: build: . depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql REDIS_HOST: redis ports: - 8080:8080 restart: always nginx: image: nginx:1.24 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html ports: - 80:80 restart: always volumes: mysql_data:这个编排文件的核心思路是后端应用和数据库都容器化前端打包后的dist目录挂载到Nginx容器里这样学生端只需访问服务器IP就能打开系统不需要额外配置Node环境。生产环境的数据库密码一律通过环境变量注入不要写在镜像或代码里。6.3 备份策略医疗数据不能丢医疗数据的备份优先级比普通业务系统高。我的备份方案是每天凌晨用mysqldump全量备份保留最近7份备份文件同步一份到另一台机器或者对象存储。原因很简单学生体检数据一旦丢失让几千人重新体检是不可能的。运维层面再苦再累备份这关不能省。0 2 * * * mysqldump -uroot -p${MYSQL_PASSWORD} univ_health | gzip /backup/univ_health_$(date \%Y\%m\%d).sql.gz find /backup -name *.sql.gz -mtime 7 -delete6.4 安全基线密码加密、HTTPS、登录锁、脱敏安全方面有四个必做项。第一用户密码必须用BCrypt加密存储不能MD5理由不用多说MD5撞库太容易了。第二生产环境一定要配HTTPS学校场景下可以用免费的Lets Encrypt证书学生登录页面如果不加密抓包就能看到密码和JWT这是严肃的事故隐患。第三登录接口需要加验证码和失败次数限制同一个IP或账号连续失败5次就锁定15分钟防止暴力破解。第四涉及个人健康数据时做脱敏展示辅导员端只显示学号20240001学院计算机学院这种信息学生的具体体重、血压数值属于个人隐私辅导员没有权限查看。7. 复盘时的几句实在话这个项目从头到尾走下来我最深的体会是做管理系统的难点从来不在写代码而在理清业务边界和权限边界。SpringBoot、MyBatis-Plus这些技术都是工具三天就能上手但校医院到底需要什么、不需要什么、各角色之间如何协作这些问题需要你真正去跟业务方聊过、去现场看过才能回答得准确。如果你的项目还在起步阶段我建议先花一周时间把业务流程图和数据字典设计好再动手写代码磨刀不误砍柴工。最后分享一个小技巧演示这个系统的时候不要对着PPT念功能列表直接从校医登录 → 创建体检批次 → Excel导入结果 → 系统生成预警 → 辅导员看到脱敏通知这条链路现场走一遍。评审老师看到数据在系统里流转起来比你用二十页架构图都更有说服力。这个系统做完以后也不是终点后续可以扩展的地方还很多比如对接学校的统一身份认证、把体检报告PDF生成做成定时批量任务、引入更丰富的统计可视化大屏。先把当前这个闭环跑稳其他的留到下一轮迭代再说。