Spring Boot 加 Vue 3 这套组合,现在基本是国内中小型管理系统开发的事实标准了。学生健康体检管理这块,很多高校的卫生所和信息化中心还在用 Excel 甚至纸质登记表,每年新生入学、毕业生答辩前后那波集中体检,数据录入和异常追踪简直是一场灾难。我前阵子就帮某高校卫生所做了一套大学生身体健康体检管理系统,从体检批次创建、学生批量导入、分项指标录入,到异常自动判定、复查提醒和统计报表,全流程跑通。今天把整个设计和落地过程完整梳理一遍,包括数据库怎么设计、异常判定规则怎么灵活配置、Vue 3 组合式 API 怎么组织、以及实际部署中踩过的几个典型坑,希望能给正在做类似项目的同学一点参考。
1. 项目整体设计与核心功能拆解
1.1 体检业务场景分析
大学生体检和常规医疗体检有个明显区别:它面向的是健康人群的大规模筛查,而不是个体的疾病诊断。一个万人大校,每年至少两次集中体检(入学和毕业,部分学校还有每学年一次),单次产生的体检记录可能覆盖数千甚至上万人。每次体检涉及的基础项目就包括身高体重、视力、血压、心率、内科听诊、外科检查、血常规、尿常规、肝功能、胸部影像等十几项,每项又包含多个指标字段。
这个场景有几个突出的痛点。数据来源极其庞杂——校卫生所的校医录入一部分,外聘体检机构导出一部分,部分项目还有便携设备自动采集的数据。另外异常追踪完全靠人工,某同学转氨酶偏高需要复查,记录在纸质单子上,之后有没有去复查没人跟进。加上不同年级、不同学院的健康状况需要横向对比,以便安排健康教育或疫苗接种,这些靠 Excel 透视表来做,效率低且容易出错。
所以我做这套系统的定位很明确:它不是要替代专业 HIS 系统,而是做一个轻量、贴合高校场景的体检数据全流程管理平台,重点解决三个问题,一个是把录入效率提上来,一个是不让任何一个异常指标被漏掉,再一个就是让统计分析变得即时可行。
1.2 核心功能模块划分
整体功能按照使用流程拆解成了下面这几个模块。
系统管理模块管的是基础数据,包括学院、专业、年级、班级的层级维护,用户账号与角色配置,以及体检项目库的维护。这里有个容易忽略的细节:体检项目库不是简单地存一个项目名称,还要存这个项目的单位、参考范围、数据类型、是否必检、排序号等信息,因为后续的异常判定和报告展示都依赖这些元数据。
批次管理模块负责创建体检批次,设定批次名称(比如 "2025级新生入学体检")、体检日期范围、适用学院和年级。一个批次本质上是一次体检活动的完整定义,所有学生记录都挂在批次下面,这样体检历史才能按时间维度追溯。
学生信息模块支持单人录入和 Excel 批量导入,字段包括学号、姓名、性别、出生日期、学院、专业、班级、联系方式、既往病史、过敏史等。学号作为唯一业务键,导入时要做重名和重复学号的校验。
体检记录模块是核心,分为两种操作模式。单个学生逐项录入适合补检和个别复查;批量录入适合大量学生集中录入,支持表格编辑、Excel 导入、按学号快速跳转。每个体检项录入时自动做范围校验,超标的字段在界面上立刻标红。
健康评估模块基于录入的指标做自动判定,同时支持校医手动复核确认。疑似异常的学生自动进入复查提醒列表,生成复查建议和复查截止日期。
统计分析模块提供体检完成率、异常分布、各学院对比、BMI 分布、视力异常趋势等统计图表,支持按批次、学院、年级多维度筛选和 Excel 报表导出。
1.3 角色权限矩阵设计
这套系统涉及三类角色,每个角色看到的界面和能做的操作差异很大。
校医端是最核心的操作端,拥有学生档案查看编辑、体检数据录入、异常判定、复查管理、统计查看的完整权限。校医是每天接触系统最久的人,所以操作效率优先,凡是高频操作必须支持全键盘和批量动作。
学院管理端只允许查看本学院学生的体检状态和统计结果,不能修改任何体检数据,主要用于辅导员跟进本院学生的体检完成情况。这个设计避免了数据被非专业人员误改,同时满足了"学院要知道自己学生体检做没做"的真实需求。
系统管理员端负责基础数据维护和账号分配,不参与具体的体检业务操作。对于学生本人,还可以提供一个自主查询端,登录后查看自己的体检结果和复查建议。不过出于保护隐私的考虑,学生只能看到自己的记录。
角色权限在实现上用 RBAC 模型,表结构分为用户、角色、权限三级,权限细化到菜单和按钮级别。后端每次请求都会经过安全拦截器做 Token 解析和权限校验,前端路由也做了一一对应的守卫,双重保障数据安全。
2. 技术选型与架构决策
2.1 后端技术栈选择
后端采用 Spring Boot 3.x,JDK 17。相比 Spring Boot 2.x,3.x 最大的变化是 Jakarta EE 命名空间迁移,其他方面对业务开发影响不大,但生态支持更长久,所以新项目直接用 3.x。
持久层框架选了 MyBatis Plus,而不是纯 MyBatis 或 JPA。MyBatis Plus 在单表 CRUD、分页、条件构造方面做得非常顺手,内置的LambdaQueryWrapper让查询条件写得非常简洁,比如按学院和学号模糊查询学生列表,几行代码就搞定,不需要手写 XML。但在多表关联查询和复杂聚合场景下,还是要用自定义 SQL 或者直接在 Mapper 层写方法,不能过度依赖框架。
数据库用 MySQL 8.x,由于体检数据基本不会频繁改删,所以表结构相对稳定,不需要引入复杂的读写分离或分库分表。缓存选了 Redis,主要用来存 JWT Token 黑名单、验证码以及高频读取的基础数据。注意一点,体检录入是低频写操作,没必要做缓存雪崩级别的复杂防护,但像学院列表、体检项目库这种每次进页面都会查询的数据,缓存能显著减少数据库压力。
权限认证用的是 Sa-Token,而不是 Spring Security。原因很务实:Sa-Token 提供开箱即用的登录认证和权限校验功能,对中小型项目来说配置量比 Spring Security 小得多,学习成本也更低。当然,如果项目后续有对接统一身份认证或者更复杂的安全需求,Spring Security 的生态会更完善,但就这个项目而言,Sa-Token 完全够用。
2.2 前端框架与工程化方案
前端选择了 Vue 3 组合式 API + Vite + Pinia + Element Plus + ECharts。
Vite 的开发启动速度和热更新体验比 Webpack 时代提升了几个档次,开箱即用,不需要额外配置。组合式 API 让每个页面的业务逻辑可以按功能聚合,比如体检录入页的"指标范围校验""异常标红""批量提交"这些逻辑可以拆分成各自的composable函数,而不是像 Vue 2 选项式 API 那样被迫分散在datamethodscomputed里。
商店状态管理选择了 Pinia,逻辑模型非常清晰。相比 Vuex,Pinia 去掉了庞大的概念体系,defineStore定义、storeToRefs解构响应式,使用体验更接近普通 JavaScript 模块。实际开发中主要用 Pinia 管理用户状态、角色权限信息和全局的体检配置常量。
UI 件库用 Element Plus,表单类组件丰富,对表格的封装也比较好用,特别适合管理后台类项目。统计页面图表选用 ECharts,其中体检完成率环形图、异常分布的柱状图和雷达图都有现成配置可以快速实现。
前端请求层封装了统一的axios实例,做了请求拦截器和响应拦截器。请求拦截器自动携带 Token,响应拦截器统一处理后端返回的code字段,只有code === 200才resolve,其他情况统一抛出错误提示。这里有一个经验:错误提示不应该都用ElMessage.error(res.message),有些场景需要静默处理,比如后台刷新 Token 失败。所以封装时要把普通接口报错和文件下载流分开处理。
关于前端的工程化,还有几个值得注意的细节。路由用动态addRoute的方式根据角色加载,而不是提前把全部路由写死,这样学生用户根本看不到管理菜单。开发环境通过 Vite 代理解决跨域,生产环境用 Nginx 做反向代理和静态资源服务。
2.3 数据库设计与核心表结构
数据库设计决定了这套系统能不能撑住后续的查询和统计需求。我按业务域拆出了下面这些关键表。
学生信息表student,核心字段是学号student_no,这个字段必须建唯一索引。另外学院、专业通过外键关联基础数据表,这样统计时可以比较方便地做聚合查询。需要注意的一点是,学生表里不要冗余存年龄,只存birth_date,年龄通过计算得出,避免学生生日过了之后年龄数据不准确。
用户表sys_user和学生表的关系是一对一,但要独立,因为系统管理员的账号不属于任何学生。账号初始密码设置成身份证后六位之类的规则,第一次登录后强制修改。
体检批次表physical_batch,保存批次名、开始和结束日期、状态。批次状态用枚举,0-未开始,1-进行中,2-已结束。批次结束之后录入操作会被禁止,但可以追加补检批次,这样从流程上保证了数据的严肃性。
体检项目定义表exam_item是这套系统里灵活性的关键。表字段包括项目编码、项目名称、单位、指标类型(数值型或文本型)、参考最小值、最大小值、是否启用等。比如"收缩压"的参考区间是 90-140 mmHg,如果某地区人群标准有差异,管理员可以直接改数据库里的参考值,而不用改一行代码。
体检记录主表exam_record保存每次体检的关联关系,字段包括学生 ID、体检批次 ID、体检日期、体检状态(0-未完成,1-已完成,2-有异常待复查)、录入人、录入时间。这个状态字段会被反复查询,所以必须建索引。
体检明细表exam_result保存每个学生每项体检项目的具体数值或文本描述,字段包括record_id、exam_item_id、result_value、result_text、is_abnormal。如果某一项是辅助检查文本描述,就存到result_text,数值型指标存到result_value。另外加了一个is_abnormal冗余字段,一开始是录入时自动判定,后续人工修改判定时更新,这样查询所有异常记录只需要一个普通索引。
异常记录表abnormal_follow单独保存所有被标记为异常的学生记录和复查管理信息,字段包括学生 ID、体检批次 ID、异常项目、异常值、建议、复查截止日期、复查状态。这张表就是为了追踪复查流程用的,核心查询场景是"列出当前批次所有需要复查但还没复查的学生"。
2.4 前后端交互与数据流设计
前后端交互遵循一套统一的响应协议,所有后端接口返回结构统一为{ code, message, data }。200表示成功,401表示未认证,403表示无权限,500表示系统异常。前端拦截器拿到非 200 的 code,根据 code 类型做对应的跳转或提示。
关于批量录入这个核心流程,数据流的走向是这样处理的。前端页面拉取当批次的体检项目和可检学生列表,校医在表格中逐行录入。前端先根据项目定义表里的参考范围做本地预校验,超标的单元格标红但可以保存——这是故意的,因为有时候录入有误差需要先记录后面再修改,不能因为校验就彻底拦截。点击保存按钮时,前端把整张表格数据打包提交,由后端循环插入体检明细并更新体检记录的状态。为了保证数据一致性,这个操作必须放在一个数据库事务里:要么全部成功,要么整体回滚。
调接口性能方面,一个批次 2000 个学生、每个学生约 20 个体检项,如果逐个插入会产生 4 万条 SQL,太慢。解决办法是后端做批量插入,MyBatis Plus 的saveBatch本质上还是逐条插入的封装,性能提升有限。实际项目里我改为在 Mapper 里写了自定义的批量插入 SQL,用 MySQL 的多值插入语法,一次插 500 条,明显更快。
另外,前后端交互的健壮性有一个容易被忽视的细节:Excel 导入编解码。批量导入学生信息和批量导入体检结果时,要处理同一批数据里部分行格式错误的情况。我的策略是逐行解析、逐行校验,错误的行不参与插入,组装一个前端可读的row + error message列表返回给前端,让校医下载错误报告去修改,而不是整批文件退回。这种方式的用户体验好很多。
3. 核心模块实操流程与实现细节
3.1 体检批次创建与全流程状态机设计
体检流程不是简单的"创建批次 - 录入数据 - 看报表"线性流程,中间有很多状态转换。我把批次流程设计成这样一个状态机:
批次创建后进入待开始状态,此时可以批量导入学生、编辑批次信息。批次开始日期到达后自动进入进行中状态,录入功能开放。管理员手动关闭批次后进入已结束状态,这个状态下普通校医无法再修改数据,只有管理员可以开启白名单回退修改权限。
录入数据的维度上,每一位学生的体检记录有一个独立状态:未检表示批次中还没有这位学生的任何数据;体检中表示已录入部分但未完成;已完成表示所有必检项都已录入;已异常表示有指标超出参考范围,等待复核。
这个状态机实现的复杂度在于"必检项是否全部录入"的判定。体检项目定义表里有一个is_required字段。状态计算不能用简单的前端判断,因为后端要保证数据的权威性。我写了这样一个逻辑服务:每次插入或更新体检明细后,查询该record_id下所有必检项的录入状态,如果全部存在且不为空,就把主记录状态更新为已完成,并触发异常判定逻辑;如果异常判定发现超标项,再更新为已异常。
需要特别说明的一点是,状态改变和异常识别的时间点不一致。体质指数(BMI)这种由身高体重计算出来的派生指标,不能只是录入时算个值就完事,要设计成实时计算存入库中。这样统计报表查询时直接基于exam_result表过滤item_code = 'BMI'即可,不用每次现场分组计算。
3.2 批量导入功能设计与 Excel 模板规范
Excel 导入是这套系统使用频率最高的入口之一,做得不好会直接导致操作人员打退堂鼓。我踩过不少坑后整理出了一套固定方案。
导入模板设计时,除标题行外,固定从第三行开始放数据。前两行分别是表头说明和示例行,示例行用斜体标示并注明"此行为示例,导入时将忽略"。这样操作人员看一眼就明白每一列该怎么填。
模板里的下拉选项用 Excel 数据有效性实现,比如性别列提供"男、女"下拉,这在导出模板时通过 Java 代码写进单元格的DataValidation对象。否则操作员手输"男 "带个空格、输入"M"这类问题会导致数据库脏数据。
模板定义的核心字段如下表所示。
| 字段名 | 是否必填 | 格式说明 | 校验规则 |
|---|---|---|---|
| 学号 | 是 | 文本 | 必须存在且唯一 |
| 姓名 | 是 | 文本 | 长度不超过 50 字符 |
| 性别 | 是 | 下拉 | 必须为"男"或"女" |
| 出生日期 | 否 | 日期 | 格式 yyyy-MM-dd 或 yyyy/M/d |
| 学院代码 | 是 | 文本 | 必须存在于基础数据表 |
| 专业代码 | 是 | 文本 | 必须存在于基础数据表 |
| 既往病史 | 否 | 文本 | 最大长度 255 |
导入流程后端实现是这样组织的。接收上传文件后,先用 EasyExcel 的监听器逐行读取,一行一行解析,错误收集到一个List<String>对象中,每一条错误信息包含行号和原因,比如"第 12 行:学号不存在"。解析结束后若存在错误,就直接返回错误列表并要求修改后重新上传,不执行任何数据写入。若全部通过,则分批(一次 500 行)插入数据库并返回导入成功数量。
这里有一个非常关键的经验:校验学号是否存在不能一条条查询数据库,要在内存里先把 Excel 里的学号全部收集好,一次性SELECT出一批已存在的学号,构建哈希集合去匹配,否则 3000 多行的文件逐条查库会拖到十几秒甚至超时。
3.3 体检数据录入与异常实时标红
录入页面是整个系统交互最重的模块。我用的是一个自适应表格布局:左侧有两列固定的常规项目区,右侧是"快捷分类标签页",按"一般检查、内科、外科、实验室、影像"分组展示。默认视图按身高、体重、血压、视力等常见项目排列,校医录入时主要操作是鼠标点击单元格后用数字键盘输入,回车自动跳到下一格。
前端实现实时校验靠的是响应式计算。每个数据单元格对应一个"原始值"和"编译后的判定结果"两个状态。用户输入一个数后,触发watch或直接在入接口方法里执行validateItem(item, value)逻辑函数。这个校验函数从后端下发的体检项目配置中读取参考区间上下限、是否是数值型、是否是必选项,返回pass / warn / error三种结果。warn用于超出范围但仍在允许录入的边缘值,error用于严重超出范围的明显异常值。
录入界面标红不是简单的 CSS 类切换,还需要联动右侧的"异常摘要面板"。校医点击某个标红单元格时,摘要面板同步展示该项目的参考范围和异常程度描述。为了方便复查,一旦出现异常标红,系统会自动为该条记录生成一条abnormal_follow记录,并将"特此留痕"的操作人、操作时间存入日志表。
关于录完之后的完整性检查还隐藏着一个体验细节。如果校医一次性录了 80 个学生,其中有 5 个学生的必检项没填完,页面不能只给一句"请检查未完成项"。我在前端维护了一个"未完成学生索引集合",统计缺哪些必填项目,在提交按钮旁边弹出"还有 5 位学生未完成,缺项分别是:xxx"的提示条,点击可以跳转到对应学生所在的表格行并定位到缺失项单元格。
3.4 健康评估报告生成与导出
体检报告的生成分为两种形态。第一种是在线预览报告,供学生端查询体检结果时查看。第二种是批量 PDF 报告,供卫生所归档和打印。在线预览报告我的做法是做一个只读页面,按体检项目分组展示所有数值和参考值,异常项用红色标记。
批量 PDF 生成用到的技术选型是itext7或openhtmltopdf。我最终选了openhtmltopdf,因为可以直接把一段 HTML 模板转成 PDF 文件,模板可以用 Thymeleaf 或字符串模板渲染,对排版控制力更强。之前在 iText 里手动设置表格坐标的体验太痛苦,生成的定位还容易错乱。
导出逻辑里要考虑一个并发问题:校医点击"导出全班 PDF"按钮后,如果一次性生成几百份报告,耗时几十秒到数分钟,前端必须异步处理。我设计了一套简单的任务队列:点击时立即返回task_id,前端轮询后端查询任务状态,后端用线程池去分批渲染 PDF 并打包成 ZIP。任务完成后,前端提示下载链接。这个方案虽然原始但非常实用,比引入消息队列的开销小得多。
4. 异常判定规则引擎与统计报表的实现
4.1 灵活的指标判定规则配置
异常判定是体检系统的灵魂,但它不能是硬编码的if...else。不同地区、不同机构对某些指标的正常范围可能存在差异,甚至同一指标对不同性别的参考区间也不同。考虑到这些因素,我设计的判定规则采用"独立于业务代码的配置化方案"。
具体做法是新建一张exam_item_reference表。因为同一种体检项目可能在不同性别、不同年龄段有多个参考区间,所以用一张单独的表来维护区间,而不在exam_item表里只存一对最小最大值。表的关键字段包括项目 ID、适用性别、适用年龄段最小值和最大值、参考最小值、参考最大值、判定优先级和一条"异常提示文案"。
判定逻辑执行的时候要注意优先级问题。举个例子,收缩压的参考区间男性是 90-140,女性是 85-135,同时存在一个通用区间 90-140。判定请求进来时,后端按"性别 + 年龄区间匹配度"的顺序筛选,命中性别专属区间就优先使用。具体匹配逻辑分三步:
匹配性别,设置gender = 'M' or 'F'。按年龄范围过滤,寻找年龄落在[age_floor, age_ceiling]范围内的记录。全部命中后,如果存在多条,按priority字段取数值最小的那条作为最终标准。
每次判定结果不只存入is_abnormal字段,还要把判定时使用的参考区间、上下边界原始值也存入一个冗余字段。这样做的好处是当后续修改了参考区间时,历史数据的异常记录不会被重新计算而改变,保证了历史记录的可追溯性。这是医疗类场景数据审计很重要的一环。
4.2 复查闭环与提醒机制
异常被标记出来只是第一步,体检流程最怕的是"查到异常没下文"。我在系统里设计了复查闭环追踪模块。
当一个学生被标记有异常项,系统会立刻自动创建一个复查任务,赋予一个默认的复查截止日期(默认配置为原始体检后 14 天)。校医可以手动修改复查日期和复查建议。复查任务状态有待复查、已预约、已复查、已结案四个状态。
提醒机制分为系统内短信提醒和消息中心提醒。校园场景下短信提醒受限于短信网关对接,不一定每个学校都有,所以消息中心提醒是标配。首页工作台做了一块"待办事项"卡片,默认查询条件就是复查截止日期 <= 当前日期 + 7天 且 复查状态 = 待复查的所有学生记录,按截止日期倒序排列,红色高亮展示超期未复查的条目。
这里有一个常见的需求分歧:有些卫生所希望异常追踪由指定负责人统一处理,而不是所有校医都能看到全部异常名单。所以我加了一个"追踪责任人"字段,可以指定具体校医。首页的待办列表只显示当前登录校医"负责或者自己录入"的异常记录,而统计报表和导出则保留全部数据权限给管理员。
4.3 可视化统计报表与 ECharts 集成
统计模块的信息密度要控制好。打开一个体检批次统计页面,不应该甩十几个图表让校医看得头晕。我最终设计为三层。
第一层是指标卡,显示四组数字:应检人数、已检人数、完成率、异常人数占比。这些数据全部由后端一个聚合查询接口返回,前端不做任何二次筛选计算。第二层是趋势类图表,包括体检完成趋势折线图(按天统计录入人数)和异常分类柱状图。第三层是明细表,展示按学院聚合的体检完成率排名,支持点击下钻查看学院内班级维度的数据。
图表数据来源通过一个通用的统计接口聚合。后端写一个原生 SQL 查询,比如副问某个批次中各学院完成率的 SQL 大致是这种形态:
SELECT c.name AS college_name, COUNT(DISTINCT s.id) AS total_count, COUNT(DISTINCT CASE WHEN er.status = 1 THEN s.id END) AS finished_count FROM student s JOIN college c ON s.college_id = c.id LEFT JOIN exam_record er ON er.student_id = s.id AND er.batch_id = #{batchId} WHERE s.batch_id = #{batchId} OR s.status = 1 GROUP BY c.id, c.name ORDER BY finished_count DESC要注意 LEFT JOIN 的过滤条件。WHERE er.batch_id = #{batchId}不能直接放在 WHERE 子句,那样会把没有体检数据的学生行过滤掉,必须写在 JOIN ON 条件里面。这是 SQL 菜鸟容易掉进去的大坑。
ECharts 实例的封装上,我写了一个useChart的组合式函数,传入elementId和option对象,负责初始化实例、自适应尺寸、销毁实例。实践中要特别注意 Vue 3 中如果父组件多次切换 tabs,ECharts 实例没有正确销毁的话,会残留大量隐藏的 canvas 节点,内存一路涨上去。所以组件卸载时一定要调用chart.dispose()。
图表配色统一维护在一个chartTheme.js文件里,颜色用一套柔和过渡的调色板,比如异常用的红色系和正常的蓝色系,保证同一指标在不同页面呈现一致的色彩逻辑,而不是每张图各用各的颜色,操作人员看久了会混乱。
5. 常见问题与排查技巧实录
5.1 批量插入导致的内存溢出与 SQL 超长
最初用 MyBatis Plus 的saveBatch一次性插入上万条体检明细时,生产环境直接触发了内存飙升和PacketTooBigException。原因很明显:saveBatch本质上是循环调用单条插入,每次插入都经历一次网络往返和事务提交。而用自定义多值插入虽然快,但一次插入百万条数据拼接出来的 SQL 文本可能超过 MySQL 的max_allowed_packet限制。
解决办法是对自定义的多值插入做分片,每次插入 500 条左右。实测下来,2000 学生的全部体检明细大约 4 万条记录,分 80 批插入,一个事务完成全部提交,总耗时能够压缩到 3 到 5 秒。这个量级的性能体验已经足够。如果你对接的是几十万人的超大院校,才需要考虑用批量加载工具或者真正的异步队列。
5.2 Vue 3 响应式数据丢失
开发时遇到一个很隐蔽的状态丢失bug:在体检录入页面,用reactive定义了一个大对象,内部某个子属性在赋值时整个视图没有更新。排查后发现我在赋值时用了obj.detail = { ... }的方式,在 Vue 3 中reactive对属性的新增和删除是支持响应式的,问题可能出在赋值来源。真正的问题其实是某个对象字段来自接口返回后,我在代码里又做了一次解构:
const { resultValue } = row resultValue = newValue解构出来的resultValue是普通变量,不再具备响应式跟踪。类似这种"从响应式对象中解构赋值"的操作是 Vue 3 开发新手最容易踩的坑。正确写法是直接操作row.resultValue = newValue,不要先解构再赋值。
另外,reactive嵌套数组如果直接用arr = newArr整体替换旧引用,视图不会更新,因为它没有触发 setter。需要改为Object.assign填充或者用ref定义整个数组。
5.3 Token 过期与并发请求刷新竞态
登录认证细节值得单独说说。我设置的 JWT 有效期是 2 小时,但实际上校医录入数据时往往长时间不操作。一开始我做了简单的方案:请求拦截器统一检查 Token 过期时间,如果过期就立即跳转登录页。结果引来不少抱怨,录着录着数据突然被踢出去,而且提示是"请重新登录",刚录入的内容全丢了。
后来改为无感刷新方案。后端提供刷新 Token 接口,前端配置一个"白名单机制"。响应拦截器收到401时,判断当前请求是否匹配白名单里的刷新接口,如果不是刷新接口则尝试携带 refresh token 去获取新的 access token,成功后重新发送原请求。同时要处理并发场景,多个请求同时返回 401 时只能发起一次刷新请求,其余的等待刷新完成后再重发。我用一个简单的状态变量isRefreshing加上一个等待队列解决了这个问题,比较经典,无需引入复杂库。
5.4 Nginx 部署与跨域配置
生产环境部署用的是前后端分离的标准架构。前端构建后静态资源放到 Nginx 的html目录,后端打 Jar 包用宝塔或 systemd 守护进程运行。关键配置是同一域名的反向代理,避免浏览器跨域限制也解决 Cookie 携带问题。
核心 Nginx 片段大致是下面这样:
server { listen 80; server_name your-domain.example.com; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /upload/ { proxy_pass http://127.0.0.1:8080; } location / { root /opt/frontend/dist; try_files $uri $uri/ /index.html; } }有个细节问题得注意,proxy_pass后是否带路径分隔符。location /api/加尾斜杠和不加会导致请求路径是否保留/api前缀的不同行为。如果后端用@RequestMapping("/api")作为统一前缀,前端请求都带/api/,那么上面配置就保留前缀。否则容易造成 404 且不容易排查。
另外部署时后端文件上传的路径要做映射配置,/upload/对应后端存储目录。一般我会把上传文件目录完全放在项目目录外并通过一个系统配置项指定,这样下次升级替换 Jar 包时不会误删学生上传的头像或导入模板文件。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 录入页表格卡顿,滚动掉帧 | 行的全新组件数量过多,输入实时校验频繁触发 | 表格分页显示,每页控制在 50 行以内;校验逻辑节流处理 |
| 批量导入提示"导入成功"但实际只有部分数据 | 事务回滚没生效或部分行插入被忽略 | 检查事务边界;导入解析时对可选字段允许跳过而非全部抛出 |
| 统计报表数据与明细不一致 | 状态字段有误或使用了多表统计 SQL 过滤条件不正确 | 用置空的数据核对 SQL,重点检查 JOIN ON 条件是否把未检学生过滤掉 |
| 刷新 Token 后页面跳转 404 | 前端路由没有保留当前页面路由参数 | 用序列化的location对象保存完整路由地址,刷新后重放跳转 |
| 学生查询学号带空格匹配不到 | 数据库字段带回车换行或空格 | 查询逻辑统一做TRIM(),导入时清理字符串首尾空白 |
| PDF 报告中文乱码 | 环境缺少中文字体 | 服务器安装中文字体包,或在模板嵌入@font-face字体文件 |
管理后台这类项目,不会天天有新功能需求,但稳定性运维比开发更重要。体检期集中使用的时候,接口响应变慢或者偶尔操作超时,操作员立刻会反馈"系统变卡了"。提前在这些基础环节打好底子,后面能少操很多心。
再扯一句使用习惯层面的经验。体检高峰期我建议给系统加一个简单的负载提示,在首页顶部显示"当前待办复查 X 条,即将截止 X 条"。这个东西不花多少开发时间,但能显著降低卫生所老师漏掉复查的概率。做了几次体检季回访,卫生所反馈这个小小的首页卡片,比那些复杂的统计图表更实在。