如果你接手过校园类管理系统的开发,应该知道师生健康信息这种需求看着简单,真做起来坑不少。数据维度多,学生、教师、班级、学院、健康档案、每日体温上报、异常预警全都搅在一起,还得兼容学生端填报和管理端统计两套完全不同的操作视角。这套源码项目把整个链路完整落地了,技术栈就是标题里那套:Java Web方向的SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0,同时附带需求文档、数据库设计文档和部署说明。如果你正在做一个类似的信息管理系统,或者想拿一套结构完整的项目做毕设、做二次开发、做练手,这篇文章会帮你快速跑通,也会重点讲清楚那些文档里不会写的选型理由和实际踩坑。
1. 拿到这套源码的第一步:项目结构与技术栈拆解
1.1 源码目录整体感知
拿到源码先别急着启动,先把目录结构捋一遍。后端是标准的 Maven Spring Boot 项目,按开发时的常用布局拆成下面这样:
health-system/ ├── src/main/java/com/example/health/ │ ├── controller/ # REST接口层 │ ├── service/ # 业务逻辑接口与实现 │ ├── mapper/ # MyBatis-Plus Mapper接口 │ ├── entity/ # 数据库实体 │ ├── dto/ # 前端入参封装 │ ├── vo/ # 返回给前端的视图对象 │ ├── config/ # 跨域、拦截器、MyBatis-Plus配置 │ └── utils/ # JWT、日期处理工具类 ├── src/main/resources/ │ ├── mapper/ # 复杂统计SQL的XML文件(按需存在) │ ├── application.yml │ └── sql/health_system.sql └── src/main/java/.../HealthSystemApplication.java前端 Vue3 部分通常用 Vite 组织工程:
health-web/ ├── src/ │ ├── api/ # axios请求统一封装 │ ├── views/ # 页面组件(登录、上报、后台看板等) │ ├── components/ # 公共组件(表格、图表、筛选器等) │ ├── router/ # 路由和权限守卫 │ ├── store/ # Pinia状态管理 │ ├── utils/ # token存储、日期格式化 │ └── App.vue ├── vite.config.js └── package.json新手拿到源码最容易踩的坑是:看到 src/main/resources 下没有 mapper 目录,就以为整个项目没写SQL。其实 MyBatis-Plus 的 BaseMapper 已经把单表 CRUD 内置了,只有统计报表类的聚合查询才需要 XML 或注解SQL。这套项目大部分接口走的是 LambdaQueryWrapper 条件构造器,XML 只出现在少数统计场景,所以不要上来就翻文件夹找"完整SQL"。
1.2 技术栈为什么是这四件套
技术选型是有讲究的,不是随便拼一组流行框架就完事。
SpringBoot2 是 Java Web 后端目前最稳妥的版本段,尤其是 2.7.x。相比更早的 SSM/SSH 时代,SpringBoot 自动装配省掉了大量 XML 配置。有人问为什么不用 SpringBoot3?3.x 强制要求 JDK17,而很多学校、公司服务器还在跑 JDK8,SpringBoot2 + JDK8 的部署环境要求低得多,兼容性也更宽。这个项目选 2.7.x,就是求一个"能跑、好部署、资料多"。
Vue3 + Vite 相比 Vue2 + Webpack 的核心优势是构建速度。Vite 基于 ESModule,开发时冷启动基本秒开,热更新也是毫秒级,调试后台管理页面时体感非常明显。Vue3 的组合式 API 在写复杂表单和状态联动时也比 Options API 清晰。切到 Vue3 后最需要适应的是 ref/reactive 的响应式心智模型,后面讲上报页面时会具体演示。
MyBatis-Plus 选它而不是 Spring Data JPA,核心原因:JPA 在中小型管理系统里,懒加载、N+1查询、自动建表这些环节经常让人头疼。MyBatis-Plus 的 BaseMapper + LambdaQueryWrapper 能覆盖80%的CRUD,剩下20%复杂报表用XML手写SQL,所有执行逻辑都在你自己的掌控范围内。Spring Data JPA 架构确实优雅,但在这个业务场景下,MyBatis-Plus 的学习成本和排错便利性更友好。
MySQL8.0 相比5.7不只是性能提升,8.0的窗口函数对统计报表非常有用。后面讲健康上报统计时,按天分组、连续异常天数这类需求,用窗口函数写会简洁很多。当然8.0的坑也不少,默认认证插件 caching_sha2_password 导致的连接问题,我在最后一个章节专门讲。
1.3 系统角色与功能模块划分
这套系统承载三种角色:学生、教师、管理员。
| 角色 | 核心功能 |
|---|---|
| 学生 | 每日健康上报、查看个人档案、查看历史上报记录 |
| 教师 | 查看本班学生上报情况、处理班级异常预警 |
| 管理员 | 维护全校健康档案、查看统计报表、处置预警、发布通知 |
每个角色的菜单、可访问接口都有权限控制。权限这块没用 Spring Security 全家桶,而是 JWT + 自定义拦截器,对于前后端分离的校园内部系统,轻量权限方案完全够用。如果学校后续要对接统一身份认证,这套JWT结构也可在登录入口直接改造。
2. 健康数据怎么建模:核心表设计与字段细节
2.1 四张核心表撑起完整闭环
数据库设计的核心是四张表:
- sys_user:用户表,字段包括账号、姓名、BCrypt密码哈希、角色编号、所属学院/班级
- health_profile:健康档案表,存放学号、血型、身高体重、过敏史、慢性病史、紧急联系人
- health_report:健康上报表,存储每天的体温、症状、健康码状态、是否接触高风险人群、当前位置、备注
- health_alert:异常预警表,记录异常类型、处理状态、处理人、处理意见
关系上,sys_user 与 health_profile 是一对一,与 health_report 是一对多;health_report 里出现异常后,由后端规则生成 health_alert 预警记录。档案、上报、预警、处置,四张表构成一个完整业务闭环。
角色字段至少在 sys_user 里有 role,这是菜单权限的基础。密码别想存明文,统一用 BCrypt 加密,项目里直接引入 spring-security-crypto 工具包即可,几行代码就完成加密校验。
2.2 健康上报表的最关键设计:一人一天一条
health_report 是整套系统的核心表,字段设计上有两个关键点必须想清楚。
第一个是单日多次上报的问题。真实校园场景里,学生早晨上报一次,下午发烧还要补报一次。如果每次上报都 insert 一条记录,统计"今日全校上报率"时就麻烦了:分子是今日有上报记录的人数,同一个人多条记录会导致统计口径必须先做去重。
我采用的方案是:建唯一索引(user_id, report_date),配合 update_time 字段,当天第二次上报直接做更新而不是新增。这样"每人每天一条状态"被数据库兜底保证,统计时直接 count 就行。
第二个关键点是异常判定规则。这套系统里的规则是:
- 体温 >= 37.3℃ 判定发热异常
- 体温 >= 37.3℃ 且伴有咳嗽/腹泻任一症状,判定高风险
- 近3天接触过风险人群,直接预警
- 健康码状态非绿码,预警
这些规则全部放在后端 service 层统一判断,前端只做展示和基本格式校验。
2.3 关键建表语句参考
健康上报表的 DDL 大致如下,字符集统一 utf8mb4:
CREATE TABLE health_report ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '上报人ID', report_date DATE NOT NULL COMMENT '上报日期', temperature DECIMAL(4,1) COMMENT '体温', has_cough TINYINT DEFAULT 0 COMMENT '是否咳嗽 0否 1是', has_fever TINYINT DEFAULT 0 COMMENT '是否发热', has_diarrhea TINYINT DEFAULT 0 COMMENT '是否腹泻', health_code VARCHAR(10) COMMENT '健康码状态', location VARCHAR(255) COMMENT '当前地址', contact_risk TINYINT DEFAULT 0 COMMENT '是否接触风险人群', remark VARCHAR(500) COMMENT '备注', create_time DATETIME COMMENT '创建时间', update_time DATETIME COMMENT '更新时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除 0正常 1删除', UNIQUE KEY uk_user_date (user_id, report_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;唯一索引 uk_user_date 是这个表的灵魂。没有它,后端的"防重复"代码碰上并发请求照样会插出两条数据,只有数据库约束才能真正兜底。
2.4 实体类与自动填充配置
后端实体类用 @TableName 注解映射表名,主键策略用 IdType.AUTO 让数据库自增。
@TableName("health_report") public class HealthReport { @TableId(type = IdType.AUTO) private Long id; private Long userId; @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate reportDate; private BigDecimal temperature; private Integer hasCough; private Integer hasFever; private Integer hasDiarrhea; private String healthCode; private String location; private Integer contactRisk; private String remark; @TableLogic @TableField(fill = FieldFill.INSERT) private Integer deleted; }MyBatis-Plus 的 MetaObjectHandler 建议配一下,创建时间和更新时间自动填充,不用每次手动 set。同时 @TableLogic 逻辑删除一定要加,这种管理系统最好做逻辑删除而不是物理删除,学生误操作删掉档案,数据恢复成本太高。
字段数量上不要贪多。很多管理系统设计表时喜欢把"未来可能用到的字段"全加上,结果一张表三十多个字段,大部分是 null。这个项目尽量做到"用得上才建字段",比如地址字段只做字符串存储,不拆省市县多表,对校级健康管理这个粒度已经足够。
3. 后端接口链路:从登录到每日上报与异常预警
3.1 接口总览与统一返回格式
后端按 controller-service-mapper 三层结构。接口按模块划分如下:
| 模块 | 接口示例 | 说明 |
|---|---|---|
| 认证 | POST /api/auth/login | 登录签发 JWT |
| 健康档案 | GET /api/profile/{userId} | 查看个人健康档案 |
| 每日上报 | POST /api/report/submit | 提交今日健康上报 |
| 上报查询 | GET /api/report/today | 查询今日上报状态 |
| 上报历史 | GET /api/report/history | 查看个人历史记录 |
| 班级概览 | GET /api/teacher/class/report | 教师查看班级上报情况 |
| 统计看板 | GET /api/admin/stats/school | 全校上报率、体温趋势 |
| 预警处理 | GET /api/admin/alert/page | 分页查询异常预警 |
RESTful 风格上,统一返回一个Result<T>对象,包含 code、message、data 三个字段。业务异常用统一错误码返回,前端拦截器只需要对 401 做统一跳转登录,其余错误交给页面提示即可。
3.2 每日上报完整业务链路
一次上报请求到达后端后,处理流程是这样的:
- JWT 拦截器校验 token,拿到当前 userId
- 查当天是否已有上报记录,有则走更新,没有则新增
- 执行异常判定规则
- 插入或更新 health_report
- 若判定异常,同时生成 health_alert 预警记录
- 返回前端需要的结果文案
service 层的核心代码逻辑如下:
public ReportResultVO submit(ReportSubmitDTO dto, Long userId) { LocalDate today = LocalDate.now(); HealthReport report = healthReportMapper.selectOne( new LambdaQueryWrapper<HealthReport>() .eq(HealthReport::getUserId, userId) .eq(HealthReport::getReportDate, today) ); boolean hasAbnormal = analyzeAbnormal(dto); if (report == null) { report = new HealthReport(); // 设置各字段... healthReportMapper.insert(report); } else { // 更新体温、症状等字段... healthReportMapper.updateById(report); } if (hasAbnormal) { generateAlert(report, dto); } return new ReportResultVO(); }这里最关键的就是"防重复、判异常、联动预警"三步。前端按钮做 loading 防连点,后端 service 判断加唯一索引兜底,双重保险。实际测试中连点提交按钮的场景很容易复现,如果少了任何一道防线,数据库就会出现同一人同一天多条记录。
3.3 统计报表:MyBatis-Plus 与原生SQL的配合
统计"今日全校上报率",用 MyBatis-Plus 的 QueryWrapper 就能完成:分母是 sys_user 表学生 count,分子是 health_report 表今日上报 count。
但"近7日体温趋势""各学院上报率排行"这类聚合统计,我建议直接在 Mapper 里写原生SQL,参数明确,可读性也好:
@Mapper public interface StatsMapper { @Select("SELECT report_date, AVG(temperature) AS avgTemp, COUNT(*) AS cnt " + "FROM health_report " + "WHERE report_date BETWEEN #{start} AND #{end} " + "GROUP BY report_date ORDER BY report_date") List<Map<String, Object>> temperatureTrend(@Param("start") LocalDate start, @Param("end") LocalDate end); }用 Map 接收统计结果,好处是不用为每个报表单独建 VO。项目节奏紧的时候这是务实选择。MyBatis-Plus 的 PaginationInnerInterceptor 分页插件一定要配,不配的话 Page 参数不会生效,分页接口就会把全量数据查出来然后内存里截断,数据量一大直接卡死。
还有个小建议:统计接口如果后续要上大屏展示,加个 10 秒 Redis 缓存就够了。校园系统量级不大,但大屏刷新频率高,缓存能明显减轻数据库压力。
3.4 JWT 权限拦截器与角色控制方案
自定义拦截器里做三件事:
- 从请求头取 Authorization: Bearer token
- 解析 JWT 拿到 userId 和 role
- 把 userId、role 放入 ThreadLocal 上下文,后续业务方法从 UserContext 获取当前用户
接口权限直接用路径规则控制,比扫描注解更直观:
registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/captcha"); // 在拦截器内部按请求路径前缀做角色校验 // /api/admin/** 仅管理员 // /api/teacher/** 教师及以上 // /api/report/** 所有登录用户路径规则匹配足够清晰,也不容易漏配。如果后续角色变多,再考虑引入注解式权限,但当前三种角色的系统没必要过度设计。
4. Vue3 页面落地:学生填报端与管理看板
4.1 前端请求封装与本地代理
前端工程用 Vue3 + Vite + Pinia + Element Plus。axios 封装这一步最重要,request.js 里做了三件事:
- baseURL 写 /api,通过 Vite 代理转发到后端,前端代码不写死 IP
- 请求拦截器从 localStorage 取 token,加到 Authorization 头
- 响应拦截器统一处理 code 和 401:401 清空登录态并跳转登录页,业务错误统一用 Element Plus 的 Message 弹出提示
vite.config.js 代理配置:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }生产环境部署时用 Nginx 的 location /api proxy_pass 做反向代理,原理和本地代理一致。这一步解决了前后端联调最大的跨域痛点。
4.2 学生上报页面的 Vue3 写法
上报页面是学生每天都会用的核心页面,交互原则是"快到不能再快"。登录后直接进入今日上报卡片,只展示体温、健康状态选择、当前位置、备注,点提交即可。不要搞一个三四十字段的长表单,学生会烦躁,填写率会断崖式下跌。
体温输入用 el-input-number,小数位固定 1 位,范围限制在 35.0 到 42.0。健康状态用按钮组即时切换。核心逻辑代码:
<script setup> const form = reactive({ temperature: 36.5, hasCough: 0, hasFever: 0, hasDiarrhea: 0, contactRisk: 0, location: '', remark: '' }); const submitting = ref(false); async function handleSubmit() { submitting.value = true; try { const res = await submitReport(form); ElMessage.success(res.msg || '上报成功'); await fetchTodayStatus(); } finally { submitting.value = false; } } </script>这里 reactive 和 ref 的混用是 Vue3 新手最容易懵的地方。我的经验是:对象用 reactive,基本类型和需要重新赋值的值用 ref。模板里 ref 会自动解包,但 script 里必须写.value。
4.3 管理员看板与 ECharts 趋势图
管理员首页做成统计看板,包含上报率总览、今日体温异常数、近7日温度趋势折线、学院上报率排行榜。图表用 ECharts,封装成一个 BaseChart 公共组件,接收 option 参数,内部负责初始化、resize、销毁。这样看板页面代码很干净,图表配置隔离在各页面里维护。
温度趋势图里加了一条 markLine 标注 37.3℃ 阈值线,这个视觉提示很有用,老师一眼就能看出哪一天发烧人数异常。数据从后端 /api/admin/stats/temperatureTrend 接口拿,切换学院时重新请求。
做看板时有个技巧:表格和图表共用同一个日期范围筛选器,管理员可以按自定义日期段看数据,而不是写死"近7天"。筛选器做成公共组件放在管理员布局顶部栏,切换时通过路由 query 传参,刷新页面后状态还能保留。
4.4 路由守卫与动态菜单
Vue3 前端通过路由守卫控制页面访问:
- 未登录访问任何页面,redirect 到 /login
- 学生登录后只有 /report、/profile、/history 三个菜单
- 教师登录后有班级上报、预警处理菜单
- 管理员登录后展示全部菜单
实现方案是前端按角色过滤菜单配置,配合路由 meta.roles 做守卫判断。角色数量固定时不需要后端返回动态路由。但有一个细节容易忽略:页面里按权限隐藏按钮。比如"处理预警"按钮有 v-if 权限判断,不然普通用户能看到但调用接口失败,体验很差。按钮级权限判断可以封装成一个自定义指令,用起来更简洁。
5. MySQL8.0 环境搭建、配置踩坑与打包部署
5.1 安装 MySQL8.0 的几个关键点
开发环境安装 MySQL8.0,Windows 下直接选 Server only 类型,避免装一堆用不到的东西。注意端口冲突,如果之前装过 5.7,先停掉旧服务再装。
Linux 服务器上我习惯用 Docker 方式,一条命令拉起来:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ mysql:8.0MySQL8.0 默认字符集已经是 utf8mb4,但连接串里的 characterEncoding=utf8mb4 建议写清楚,避免老驱动乱码。生产环境强烈建议关闭 root 远程访问,用独立账号,只允许内网 IP 连接。初期为了省事直接开 root,后面安全审计被点名了,老老实实建了专用账号。
5.2 连接 8.0 的认证插件问题
MySQL8.0 默认认证插件是 caching_sha2_password。Navicat 老版本或某些旧客户端会报"无法加载身份验证插件"。
解决方案有两种。一是修改用户认证插件:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;二是升级客户端版本,或者连接串里加 allowPublicKeyRetrieval=true。我更推荐第二种,mysql_native_password 虽然开发方便,但安全测评可能要求更强的加密。生产环境我最后用 8.0 默认插件,JDBC 连接串补上公钥检索参数即可。
5.3 SpringBoot 连接 MySQL8.0 的配置陷阱
这块十个人有八个会踩:
- 驱动类名变了。MySQL Connector/J 8.x 的驱动类是 com.mysql.cj.jdbc.Driver,老的是 com.mysql.jdbc.Driver。SpringBoot2.7 加 8.x 驱动必须用新的,写老的在启动时直接报错。
- 必须带 serverTimezone。url 里不写 serverTimezone=Asia/Shanghai,JDBC 驱动会拿服务器默认时区,报错时间还会差 8 小时。
- SSL 和公钥参数。useSSL=false 关掉 SSL,allowPublicKeyRetrieval=true 解决公钥检索问题。
- HikariCP 连接池。MySQL wait_timeout 默认 8 小时,长连接闲置会被踢掉,连接池 max-lifetime 建议小于数据库 wait_timeout。
我最终使用的配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/health_system?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 hikari: maximum-pool-size: 10 max-lifetime: 1800000还有一个隐藏坑:MySQL8.0 的默认 sql_mode 带 ONLY_FULL_GROUP_BY,统计SQL 里如果 select 了非聚合字段但 group by 没包含,直接报错。开发时可以把 sql_mode 调宽松,但正式库不建议动,改 SQL 写法才是正道。
5.4 打包、部署与配套文档
后端打包命令:
mvn clean package -DskipTests java -jar health-system.jar前端构建产物在 dist 目录,用 Nginx 托管,再把 /api 反向代理到后端 8080 端口。Nginx 配置要注意三个点:开启 gzip 压缩,静态资源加缓存,history 路由配 try_files 回退到 index.html。第三点不配的话,用户刷新非首页路由会直接 404。
配套文档通常包含四类:需求说明、数据库设计文档、接口文档、部署说明。拿到文档先读数据库设计文档,再读部署说明,最后看接口文档。表结构清楚了,整个系统的业务逻辑也就理解了大半,接口文档是前端联调时用的。
5.5 实测部署的环境清单
我最简可用环境的版本组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8.0_202 或 JDK11 | SpringBoot2.7 两者都可 |
| Node.js | 16.20.x | Vite 要求,用 nvm 管理 |
| MySQL | 8.0.33 | 注意 utf8mb4 和时区 |
| Nginx | 1.24 | 生产环境托管前端 |
| 内存 | 2G 以上 | 后端 + MySQL 单机部署 |
有基础可以再加 Redis 做缓存,但最简环境不依赖它也能完整跑起来。这套系统单机部署完全够用,等全校上千学生同时上报,加个缓存层、优化几条 SQL 就能顶住,架构不需要大改。我自己实际部署下来的体会是,这类管理系统最大的隐性成本是环境问题而不是业务代码,MySQL8.0 的驱动和认证坑一旦趟过,后面跑起来反而很顺。希望这篇拆解能帮你把从结构到部署的路走得更短一些。