资讯动态

SpringBoot+Vue3前后端分离的师生健康信息管理系统

发布时间:2026/9/17 22:44:31 来源:尧图企业网站定制
这套“Java Web 师生健康信息管理系统”我自己从需求梳理到技术选型再到前后端编码完整过了一遍技术栈用的是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0前后端分离带一套完整文档。它不是那种只搭了个登录页的演示项目而是把师生信息管理、每日健康上报、体温打卡、异常预警、出入校审批这些真实场景全部串起来了。如果你正打算接触前后端分离项目或者学校、企业内部需要一个轻量级的信息采集管理后台这篇文章可以作为一份直接参考的实操笔记。1. 系统定位与核心需求梳理1.1 为什么需要一套独立的师生健康管理系统很多人会问健康信息上报这类功能用问卷工具或者企业微信接龙不就行了说实话临时用几次确实可以但一旦涉及“师生信息台账”“连续体温记录”“异常情况跟踪”“按班级汇总统计”这些长期运营需求通用工具就会暴露问题数据分散在表格里、无法按人归档、权限不好控制、统计分析基本靠人工。单独做一套系统的价值是把“上报”这个动作变成“数据积累”把零散的记录变成可以查询、可以追溯、可以预警的信息资产。这套系统的核心逻辑可以归纳为三条线人师生档案— 健康数据每日上报、体温记录、异常标记— 管理动作审批、通知、统计。所有功能都围绕这三条线展开不堆砌无关模块。1.2 数据模型设计核心表与字段规划数据库设计直接决定系统能走多远这里我拆解一下最核心的几张表。用户表sys_user主键 id、用户名、密码BCrypt 加密存储、真实姓名、角色类型admin/teacher/student、所属班级 ID、手机号、状态0禁用 1正常、逻辑删除标记。用户表不单独区分教师表和学生表而是用 role_type 区分身份这样的设计可以减少冗余权限控制也更灵活。班级表sys_classid、班级名称、年级、班主任 ID、创建时间。班级表是师生关系和组织归属的核心后面所有按班级维度的统计都依赖它。健康信息表health_infoid、用户 ID、上报日期、早间体温、午间体温、晚间体温、健康状况正常/感冒/腹泻/其他、是否接触异常人员0否 1是、备注、上报时间。这里刻意做成“一人一天一条记录”利用日期字段做唯一约束避免重复数据。异常记录表health_abnormalid、用户 ID、异常类型、异常描述、处理状态待处理/已处理、处理人、处理时间。当健康信息中体温超过阈值或者健康状况异常时系统自动生成一条异常记录推送给管理员处理。出入校审批表access_recordid、用户 ID、出入类型出校/入校、申请时间、审批状态待审批/通过/驳回、审批人、审批意见、出门时间、进门时间。这个表是为了满足校园出入管理的追溯需求。这套模型的取舍点在于没有为体温数据单独建表存储历史明细而是直接和每日上报合并。对多数校园场景来说每天三次体温记录足够用拆太细反而增加维护成本。提示设计数据表时一定要把create_time、update_time、deleted这三个字段作为所有业务表的标配。后续做数据排查和软删除会省非常多麻烦。1.3 功能模块全景三类角色的使用场景系统按三类角色划分功能边界每个角色看到的菜单和操作完全不同。管理员用户管理增删改查、重置密码、分配角色、班级管理、健康数据总览按班级、日期筛选、异常记录处理、出入校审批、系统日志。教师查看本班学生列表、录入或导入学生健康数据、查看本班健康统计报表、审批本班学生的出入校申请。学生每日健康上报选择身体状况 填写体温、查看个人历史记录、发起出入校申请、查看审批结果。这里最核心的设计思路是“数据权限”而非简单“菜单权限”。普通角色管理系统的权限控制只控制“能看到哪个菜单”这个系统里还要控制“数据行级别”教师登录后只能看到自己班级学生的数据管理员才能看全校。这个在 MyBatis-Plus 里通过拦截器拼 SQL 条件实现后面会细讲。2. 技术选型拆解这套组合为什么合适2.1 SpringBoot2搭服务最稳的地基SpringBoot 2.x 在 Java Web 领域已经是非常成熟的选择社区资料多、踩坑案例全、第三方集成丰富。相比 SpringBoot3 JDK17 的组合SpringBoot2 对 JDK8 的支持更友好而很多学校或企业内部服务器还停留在 JDK8 环境从兼容性角度考虑SpringBoot2 是当下最稳的选择。SpringBoot2 带来的最大价值是“自动配置”引入 spring-boot-starter-web 后内嵌 Tomcat引入 spring-boot-starter-validation 后自动装配参数校验引入 mybatis-plus-boot-starter 后数据源和 MyBatis 环境自动就绪。开发者只需要关注业务代码不需要操心一堆 XML 配置。这里我使用的版本组合是 SpringBoot 2.7.18 JDK8 Maven 3.8这套组合跑得非常稳定也是生产环境中验证过的组合。2.2 MyBatis-Plus告别手写单表 CRUD选 MyBatis-Plus 而不是纯 MyBatis核心原因是开发效率。纯 MyBatis 写一个标准的分页查询要写 Mapper 接口 XML 映射文件 SQL 语句 分页插件配置四步少一步都不行。而 MyBatis-Plus 内置了 BaseMapper提供了 insert/deleteById/updateById/selectById/selectPage 这些现成方法单表 CRUD 完全不需要手写 SQL。举个例子查询某个学生最近七天的健康记录纯 MyBatis 要写select idselectRecentSevenDays resultTypeHealthInfo SELECT * FROM health_info WHERE user_id #{userId} AND report_date DATE_SUB(CURDATE(), INTERVAL 7 DAY) ORDER BY report_date DESC /selectMyBatis-Plus 直接用 LambdaQueryWrapperListHealthInfo list healthInfoMapper.selectList( new LambdaQueryWrapperHealthInfo() .eq(HealthInfo::getUserId, userId) .ge(HealthInfo::getReportDate, DateUtil.offsetDay(new Date(), -7)) .orderByDesc(HealthInfo::getReportDate) );不需要 XML不需要写 SQL代码即查询条件可读性和维护性都好很多。对于这个体量的项目MyBatis-Plus 能把 Mapper 层代码量压缩 70% 以上。2.3 Vue3 Vite前端开发效率的关键前端选 Vue3 是趋势但我更看重的是组合拳Vue3 的 Composition API 配合 Vite 的开发服务器热更新速度在毫秒级别几乎保存即刷新。对比 Vue2 Webpack 时代的几秒等待开发体验是质变。这套系统前端用 Vue3 Vite Element Plus Pinia Vue Router目录结构如下vue3-health/ ├── public/ ├── src/ │ ├── api/ # 接口请求封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── layout/ # 后台布局框架 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面组件 │ │ ├── admin/ # 管理员页面 │ │ ├── teacher/ # 教师页面 │ │ └── student/ # 学生页面 │ ├── utils/ # 工具函数 │ ├── App.vue │ └── main.js └── vite.config.jsVue3 里最让我觉得顺手的是script setup语法写业务代码时不用再像 Vue2 那样在 data/computed/methods 之间来回跳转所有逻辑都集中在 setup 作用域里特别是写表单提交这种业务场景代码逻辑清晰很多。2.4 MySQL8.0 与数据安全设计MySQL 8.0 相比 5.7 有几个升级点值得专门说明默认字符集变成了 utf8mb4对生僻字符和 emoji 的支持更友好新增了窗口函数性能优化器更强安全性上默认使用 caching_sha2_password 认证插件。这套系统里有一处依赖 MySQL8.0 特性的设计健康信息表使用report_date字段做唯一索引。ALTER TABLE health_info ADD UNIQUE KEY uk_user_date (user_id, report_date);这个唯一索引可以防止前端重复提交或者接口被恶意刷请求时产生重复的健康数据。请求到达后端后先 try 插入如果捕获到 DuplicateKeyException 就返回“今日已上报”不需要额外写查询判断减少了一次数据库交互。另外密码存储用的是 BCrypt 加密这个是 spring-security-crypto 提供的工具类即使用户表被拖库攻击者也无法直接还原明文密码。哪怕两条记录的密码明文相同BCrypt 每次生成的哈希值也不一样增加了撞库难度。2.5 前后端分离架构下的目录约定前后端分离架构下最容易被忽视的是“目录约定”。很多项目刚开始规范越做越乱最后前端组件乱放、后端类文件堆在一起。这个项目从一开始就定下了几个约定。后端SpringBoot2 工程标准目录src/main/java/com/example/health/ ├── HealthApplication.java # 启动类 ├── config/ # 配置类 ├── controller/ # 控制层 ├── service/ # 业务逻辑层 │ └── impl/ # 业务实现类 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 实体类 ├── dto/ # 数据传输对象 ├── vo/ # 视图对象 ├── common/ # 统一返回结果、异常处理 ├── utils/ # 工具类 └── interceptor/ # 拦截器前后端分离不只是“后端写接口、前端调接口”这么简单必须做到三个分离代码仓库分离前端一个仓库、后端一个仓库、部署分离前端静态文件由 Nginx 托管后端 Java 服务独立运行、权限校验分离前端控制页面展示后端控制数据访问。这套系统里后端接口统一以/api前缀开头全部需要 token 校验前端页面按角色路由守卫控制访问。这样就算有人绕过前端直连接口后端也会拦截掉。3. 后端核心实现SpringBoot2 MyBatis-Plus3.1 工程结构与启动配置后端创建工程时我建议直接用 Spring Initializrstart.spring.io生成基础骨架手动选择以下依赖Spring Web、Spring Boot Actuator、Lombok、Validation。生成后手动加入 MyBatis-Plus 和 MySQL 驱动依赖。pom.xml里最关键的几个依赖如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.4.0/version /dependencyKnife4j 是接口文档工具基于 Swagger 增强自动生成在线接口文档调试接口方便。给前端联调的人用比 Postman 里手填参数高效很多。application.yml里需要注意的配置项spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/health_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 id-type: assign_id configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezoneAsia/Shanghai这个参数必须加上MySQL8.0 默认时区是 UTC不加的话会在日期插入和查询时出现 8 小时的偏差。allowPublicKeyRetrievaltrue是解决 MySQL8.0 使用 caching_sha2_password 认证时客户端连接报错的关键参数。3.2 公共字段自动填充create_time/update_time每张表都有 create_time 和 update_time如果每个 insert 和 update 都手动 set代码会非常啰嗦。MyBatis-Plus 提供了 MetaObjectHandler 接口可以统一处理公共字段。首先在实体类字段上加上注解Data public class BaseEntity { TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }然后实现自定义处理器Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这样一来业务代码里完全不需要关心这两个时间字段插入和更新的时间自动维护。如果你要记录“最后登录时间”或者“审批时间”这类单独的时间字段不建议走公共字段自动填充逻辑因为它们的填充规则不同在业务代码里单独 set 更明确。注意继承 BaseEntity 会让单表查询返回的字段里多出三个列如果查询结果需要直接转 Map记得用TableField(select false)隐藏不需要返回的字段避免数据冗余。3.3 逻辑删除方案TableLogic 与查询隔离这个系统的用户、健康记录、审批记录都不允许物理删除原因很简单健康数据的核心价值在于追溯删了就查不到了。所以统一采用逻辑删除删除只是把 deleted 字段从 0 改为 1。MyBatis-Plus 的TableLogic注解让逻辑删除变得透明调用deleteById时自动执行 UPDATE 语句将 deleted 置为 1执行 select 时自动追加WHERE deleted 0条件。但这里有一个坑如果你写了自定义 SQL比如多表联查MyBatis-Plus 不会自动追加逻辑删除条件。举个例子查询学生及其最近一条健康记录Select(SELECT u.*, h.health_status, h.temperature FROM sys_user u LEFT JOIN health_info h ON u.id h.user_id WHERE u.role_type student) ListMapString, Object selectStudentWithHealth();这条 SQL 会把已删除的 health_info 也查出来。解决办法是手动加条件或者用 MyBatis-Plus 的 InterceptorIgnore 注解配合Select(SELECT u.*, h.health_status, h.temperature FROM sys_user u LEFT JOIN health_info h ON u.id h.user_id AND h.deleted 0 WHERE u.role_type student AND u.deleted 0) ListMapString, Object selectStudentWithHealth();多表联查时逻辑删除条件必须自己拼。3.4 健康上报与异常预警应对同一时刻的高并发写入每日健康上报是系统的核心高频接口学生可能会在早上 7 点到 8 点集中操作需要接口足够健壮。上报接口的完整逻辑如下PostMapping(/report) public ResultString report(RequestBody Validated HealthReportDTO dto) { // 1. 获取当前登录用户 Long userId SecurityUtils.getUserId(); // 2. 组装实体deleted 字段默认 0由自动填充处理时间 HealthInfo healthInfo new HealthInfo(); healthInfo.setUserId(userId); healthInfo.setReportDate(dto.getReportDate()); healthInfo.setMorningTemp(dto.getMorningTemp()); healthInfo.setAfternoonTemp(dto.getAfternoonTemp()); healthInfo.setEveningTemp(dto.getEveningTemp()); healthInfo.setHealthStatus(dto.getHealthStatus()); healthInfo.setContactFlag(dto.getContactFlag()); healthInfo.setRemark(dto.getRemark()); // 3. 插入捕获唯一索引冲突 try { healthInfoMapper.insert(healthInfo); } catch (DuplicateKeyException e) { return Result.error(今日已上报请勿重复提交); } // 4. 判断是否触发异常预警 if (dto.getMorningTemp() 37.3 || dto.getAfternoonTemp() 37.3 || dto.getEveningTemp() 37.3 || StringUtils.hasText(dto.getAbnormalDesc())) { saveAbnormalRecord(userId, dto); } return Result.success(上报成功); }这套逻辑的关键点是不先查再插直接 try 插入。靠数据库的唯一索引兜底避免并发场景下查询后插入的竞态窗口。体温阈值 37.3 度是疾控口径的发热标准配置成常量后续如果需要调整改一处即可。3.5 报表模块按班级聚合健康数据教师端需要看到本班学生的健康数据统计比如今日上报人数、正常人数、发热人数、未上报人数。这个统计如果用 Java 查全部数据再在内存里 group by学生一多就卡。正确做法是把聚合操作下推到数据库。MyBatis-Plus 中可以使用 QueryWrapper 的 select 方法直接写聚合查询public HealthStatVO getClassHealthStat(Long classId, String date) { QueryWrapperHealthInfo wrapper new QueryWrapper(); wrapper.select(COUNT(*) as total_cnt, SUM(CASE WHEN health_status 正常 THEN 1 ELSE 0 END) as normal_cnt, SUM(CASE WHEN morning_temp 37.3 OR afternoon_temp 37.3 OR evening_temp 37.3 THEN 1 ELSE 0 END) as fever_cnt, SUM(contact_flag 1) as contact_cnt) .eq(c.class_id, classId) .eq(h.report_date, date); MapString, Object map healthInfoMapper.selectMaps(wrapper).get(0); // 组装 VO 返回 }注意这里是需要多表联查的health_info 需要关联 sys_user 拿到 class_id所以不能用单表的 Wrapper 硬写直接用自定义 SQL 更清晰。这里我给一个建议报表接口的返回值不要直接返回 Map建议定义 VO 类字段名与前端约定好避免前端拿着一个不知道有哪些 key 的 Map 去调试不容易维护。4. Vue3 前端实现与联调细节4.1 工程初始化与路由设计前端工程用 Vite 创建npm create vitelatest vue3-health -- --template vue cd vue3-health npm install vue-router4 pinia element-plus axios sassVite 创建工程的过程很快几秒钟就能完成。相比 Vue CLIVite 不需要 webpack 打包过程开发服务器启动速度和热更新速度有明显优势。路由设计遵循“布局 页面 权限”的模式const routes [ { path: /login, component: () import(/views/Login.vue), meta: { title: 登录 } }, { path: /, component: () import(/layout/Index.vue), redirect: /home, children: [ { path: /home, component: () import(/views/Home.vue), meta: { title: 首页, roles: [admin, teacher, student] } }, { path: /admin/user, component: () import(/views/admin/UserManage.vue), meta: { title: 用户管理, roles: [admin] } }, { path: /teacher/class-health, component: () import(/views/teacher/ClassHealth.vue), meta: { title: 班级健康统计, roles: [teacher] } }, { path: /student/report, component: () import(/views/student/HealthReport.vue), meta: { title: 每日上报, roles: [student] } } ] } ]注意到路由配置里用meta.roles标记哪些角色可以访问该路由配合全局守卫做权限控制。4.2 登录态与权限管理动态路由方案登录流程是这套系统的第一个关键点。常规做法是登录成功后后端返回 token 和用户信息前端把 token 存到 localStorage 或 sessionStorage后续请求在 axios 拦截器里带 token。后端生成 token 用的是 jjwt 库Service public class TokenService { private final String secret your-256-bit-secret-key-your-256-bit-secret-key; private final long expireTime 24 * 60 * 60 * 1000; // 24小时 public String createToken(Long userId, String username, String roleType) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, roleType) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } }前端 axios 拦截器统一携带axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })前端在路由守卫里做的是“页面级”权限控制后端接口还要做“数据级”权限控制。比如教师访问学生列表接口后端需要从 token 里解析出 userId再查出该教师关联的班级最后 SQL 里带上 class_id 条件。动态路由的核心是用户信息里包含角色和菜单权限点前端根据这些信息过滤路由表只注册该角色可见的路由。这样教师登录后不会看到“用户管理”菜单学生登录后不会看到“班级统计”菜单。4.3 axios 封装与统一异常处理axios 封装得好不好直接决定前端代码量。这套系统里我把 axios 封装在一个 request.js 文件里统一处理请求头、超时时间、响应拦截器。核心代码如下import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 15000 }) // 响应拦截器 request.interceptors.response.use( response { const res response.data // 后端统一返回 { code: 200, data: ..., msg: ... } if (res.code 200) { return res.data } if (res.code 401) { // token 过期清理缓存并跳转登录 localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) ElMessage.error(登录已过期请重新登录) return Promise.reject(new Error(Unauthorized)) } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request提示baseURL 配置为/api然后在开发环境 vite.config.js 里配置 proxy 代理到后端服务这样联调时不需要开启后端 CORS。生产环境里由 Nginx 把/api前缀反向代理到 Java 服务。前后端联调时最烦的问题就是接口报错信息看不懂。好在这套系统后端用了全局异常处理器RestControllerAdvice所有异常都会被统一转换成{ code, msg, data }格式的返回值前端拦截器拿到后直接弹出提示。4.4 体温打卡表格组件v-model 与表单校验学生端每日上报页面是核心交互页面我用 Element Plus 的 Form 组件实现数据校验。关键点在于体温输入的小数位校验和范围校验。template el-form refreportFormRef :modelreportForm :rulesrules label-width100px el-form-item label早间体温 propmorningTemp el-input-number v-modelreportForm.morningTemp :precision1 :step0.1 :min35 :max42 / /el-form-item el-form-item label健康状况 prophealthStatus el-select v-modelreportForm.healthStatus el-option label正常 value正常 / el-option label感冒 value感冒 / el-option label腹泻 value腹泻 / el-option label其他 value其他 / /el-select /el-form-item el-form-item label备注 propremark el-input v-modelreportForm.remark typetextarea :rows3 maxlength200 show-word-limit / /el-form-item el-form-item el-button typeprimary clicksubmitReport提交上报/el-button /el-form-item /el-form /template校验规则const rules { morningTemp: [ { required: true, message: 请输入早间体温 }, { validator: (rule, value, callback) { if (value 35 || value 42) { callback(new Error(体温范围应在35-42度之间)) } else { callback() } }, trigger: blur } ], healthStatus: [{ required: true, message: 请选择健康状况, trigger: change }] }这里有两个细节体温范围卡的 35-42 是一个很宽的范围但基本覆盖了人体体温的极端情况el-input-number的precision设为 1保证只能输入一位小数避免 36.22 这种无效精度数据进入数据库。4.5 教师端数据看板ECharts 可视化教师端数据看板用 ECharts 展示班级健康趋势需要按日期统计某段时间内本班的上报人数和异常人数走势。后端返回按日期聚合的数据格式[ { reportDate: 2025-06-01, totalCount: 45, abnormalCount: 1 }, { reportDate: 2025-06-02, totalCount: 46, abnormalCount: 0 } ]前端画折线图const chart echarts.init(document.getElementById(trendChart)) chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [ { name: 上报人数, type: line, data: totalCounts, areaStyle: { opacity: 0.1 } }, { name: 异常人数, type: line, data: abnormalCounts, lineStyle: { color: #ff4d4f } } ] })ECharts 在 Vue3 里使用不需要额外的封装库直接引入 echarts 包在 onMounted 里初始化实例在组件卸载时调用 dispose 方法释放资源。如果发现图表出现布局错乱通常是容器高度没有设置给容器 div 加一个固定高度比如 400px问题就解决了。5. 部署、联调与环境问题的排查实录5.1 MySQL8.0 时区问题时区问题几乎每个用 MySQL8.0 的人都会遇到一次。现象是 Java 代码里写入的时间到数据库里查看少了 8 小时或者反过来查询出来的时间比数据库里的值多了 8 小时。原因MySQL8.0 服务端默认时区是 UTC而中国在东八区。解决方式有两个方案方案一修改连接 URL 加上serverTimezoneAsia/Shanghai这个方案只影响当前应用连接。方案二修改 MySQL 全局时区SET GLOBAL time_zone 08:00; SET time_zone 08:00;这样即使多个应用连接时区也是正确的。生产环境建议直接用方案二因为它不依赖每个应用的连接配置是否写对。5.2 跨域配置的正确姿势前后端分离开发时前端跑在 5173 端口后端跑在 8080 端口浏览器出于同源策略会拦截跨域请求。我在早期调试时用的是在后端加全局 CORS 配置后来发现这种方式在带 token 的请求中容易出问题预检请求 OPTIONS 会被拦截器拦截于是改成了一劳永逸的方式开发环境用 Vite 代理生产环境用 Nginx 反向代理。Vite 代理配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })代理方案的好处是不需要后端额外配置 CORS浏览器的同源策略认为请求是同源的不会预检不用处理 OPTIONS 请求。注意如果后端接口文档调试工具比如 Knife4j需要跨域那还是要在后端单独配一个 CORS 过滤器但范围只限定在/doc.html相关路径其他接口不用开。5.3 前端构建后刷新 404前端构建后部署到 Nginx点击按钮跳转正常但按 F5 刷新就报 404。这是因为 Vue Router 用了 history 模式刷新时浏览器会向当前 URL 发请求Nginx 找不到对应的静态文件就返回 404。解决办法Nginx 配置 try_files 把所有路径都回退到 index.html。location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这个配置会让所有前端路由的请求都返回 index.html由前端 router 接管页面渲染。5.4 逻辑删除字段引发的查询“幽灵数据”有一次我在排查一个 bug学生详情页面显示了其他班级学生的健康数据排查发现是写的自定义 SQL 只关联了 health_info 却忘了加deleted 0条件导致已删除的数据也出现在结果集中。这个问题的根因是 MyBatis-Plus 的单表方法会自动加逻辑删除条件但自定义多表 SQL 不会自动加。解决办法就是前面提到的写自定义 SQL 时手动把deleted 0加到关联表条件中这个坑踩一次就不想再踩了。建议在项目里做一个强制约定——所有自定义 SQL 必须写逻辑删除条件可以在 Code Review 阶段人工检查也可以给 Mapper 接口加注解提示。小项目人工检查完全够用优先级高于引入复杂的插件机制。5.5 密码加密与记住登录态因为系统涉及校园健康数据密码存储必须安全。我用的是 BCrypt 加密Spring Security 提供的BCryptPasswordEncoder可以直接用。Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 注册时加密存储 user.setPassword(passwordEncoder.encode(dto.getPassword())); // 登录校验 if (passwordEncoder.matches(dto.getPassword(), user.getPassword())) { // 密码正确生成 token }前端默认记住登录状态 24 小时如果需要更长时间可以调整 TokenService 里的 expireTime或者做“记住我”功能把 token 有效时间设为 7 天。6. 这份源码的价值与二次开发建议6.1 最值得复用的部分拿到一套源码不要急着跑起来先读代码。这套系统里最值得学习的部分我觉得有三个。第一统一响应体的设计。所有接口不管成功失败都返回{ code: 200, data: ..., msg: ... }这个结构前端拦截器统一处理不需要每个页面单独判断。这种设计在团队协作中能省很多沟通成本。第二业务异常与全局异常处理。系统定义了BusinessException业务逻辑里需要中断时就抛异常全局异常处理器统一拦截并转换成友好提示返回不会把堆栈信息直接暴露给前端。第三逻辑删除 唯一索引的组合方案。这个组合在数据管理类系统里非常实用既保证了数据可追溯又防止了重复数据属于值得直接照搬的实践。6.2 未来可以扩展的方向源码只是起点这个系统在真实使用中可以继续扩展的方向很多。消息通知模块当学生体温异常触发预警时自动给辅导员发送系统消息和邮件替代现在的人工查看。批量导入导出教师手动录入学生体温效率太低可以做成 Excel 导入导出班级健康统计报表。多校园区支持目前是一所学校的单校园区模型可以加一个 campus 字段支持多校园区数据隔离。移动端适配Vue3 前端目前只适配了 PC 端学生每天上报这个动作其实更多在手机上进行可以加一个移动端适配方案或者直接套一个移动端的壳。健康档案趋势分析针对单个学生在时间维度上做体温趋势分析和异常频率统计为健康管理提供数据支撑。二次开发时建议先把核心实体关系图画清楚再动代码。健康系统的数据逻辑其实不复杂但里面涉及角色权限、状态流转、数据关联画一遍图能发现很多设计上的盲区。7. 写在最后的一点体会这套系统开发过程中我印象最深的是健康上报时那个唯一索引的设计。当时前端联调时反复提交重复数据后端查一遍再插性能也不行后来干脆改成直接插、靠数据库挡。事实证明这个方案极其干净利落这也让我更坚信一个判断很多业务问题的答案不在 Java 代码里而在数据库设计里。另外前后端分离项目联调阶段最容易出问题的地方往往不是代码逻辑错误而是约定不一致——字段名大小写、日期格式、返回结构。这套系统从后端返回结构的定义开始就统一用驼峰命名、时间统一用字符串格式yyyy-MM-dd HH:mm:ss、分页参数统一用 pageNum/pageSize联调阶段几乎没有因为格式对不上返过工。如果你也准备做类似的管理系统建议一开始就把这些约定写在 README 里会省掉大量不必要的沟通成本。健康管理类系统有一个特点大多数功能是低频操作但遇到特殊时期或者集中上报时流量会瞬间上来。所以它的技术核心不在于高并发架构而在于数据准确性、权限隔离和可追溯性。这套系统的设计恰好在这三点上做了比较扎实的处理这也是我把它整理出来分享给大家的原因。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价