
简介这是一份基于SpringBoot、MySQL与Vue前后端分离架构的大学生健康档案管理系统源码包面向计算机相关专业学生及毕业设计开发者可直接作为毕设项目使用。项目整合Mybatis、Shiro、ElementUI等主流技术涵盖管理员、普通用户、医生三类角色支持注册登录、个人健康档案、健康建议、健康知识阅读、档案管理、角色管理、用户信息管理与权限管理等8个功能模块并可通过权限管理配置不同角色的按钮级操作权限。压缩包共137个文件其中73个Java后台逻辑文件、20个Vue前端页面文件、12个JavaScript脚本、9个XML配置及1个SQL数据库脚本另含少量图片、HTML与配置文件整体大小约4.51MB目录结构清晰。资源附带完整数据库脚本与部署配置经过严格调试导入后即可快速运行既能支撑毕业设计答辩也适合学习SpringBoot与Vue前后端分离开发实战。目前已有111人学习下载可作为项目开发与论文撰写的有力参考。1. 为什么大学生健康档案管理系统要用前后端分离架构高校的健康档案管理和普通管理系统有一个明显差异数据要持续追踪、被动修改频繁、且多角色读取权限交错。体检数据每年更新疫苗记录零散追加心理咨询档案又有严格的隐私边界。如果做成单体应用后端模板直接渲染页面业务逻辑和展示逻辑耦合在一起每年的体检导入、报告查看、权限调整都要重新发版维护成本会随时间线性上涨。前后端分离在这个场景下的价值在于前端只负责交互和数据展示后端专注业务规则和权限控制两边通过 JSON 接口通信。SpringBoot 负责稳定的接口层MySQL 承载结构化健康数据的持久化Vue 负责路由和组件化页面。这个组合适合团队分工明确的中小型项目也适合课程设计、毕业设计或院级信息系统的落地——一套接口可以被 Web 端、小程序端或后续的移动端复用不需要为每个端重复写后端逻辑。下文按常见做法展开先建立领域模型和数据库设计再实现 SpringBoot 接口层然后搭 Vue 前端最后把两端在联调和部署过程中最常见的坑一次说清。每一步给出可直接运行的最小代码改动参数就能套进自己的项目。2. 健康档案系统的领域建模与 MySQL 表结构设计2.1 健康档案的数据特征为什么不能只建一张学生表健康档案系统的核心不是“记录学生信息”而是“记录学生随时间变化的健康状态”。体重、视力、血压这类指标每年都在变过敏史和既往病史有明确的时间节点疫苗批次要追踪来源。如果只建一张 student 表往里塞字段第二年的体检数据会把历史数据覆盖掉档案就变成了“当前快照”丧失追溯能力——这是健康档案和普通 CRUD 系统最本质的差别。常见做法是采用主表 档案子表的模式student 表存固定的身份信息health_record 表存每次体检/检查的结果一学生对多条每次插入新记录时不影响历史数据查询时默认取 create_time 最新的一条作为当前状态同时支持按日期查看历史。这个设计带来一个额外收益学校的年度体测、医保上报、心理咨询预约三个不同业务方看到的“当前状态”由同一条数据链路提供处理起来是同一套逻辑。2.2 MySQL 建表语句主表、档案子表与字典表以下是健康档案核心表的建表 SQL已在 MySQL 8.0 下验证过基本语法5.7 同样适用。字符集统一用 utf8mb4避免生僻字或特殊符号写入时出现乱码。-- 学生主表 CREATE TABLE student ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT NULL COMMENT 性别 1男 2女, birth_date DATE DEFAULT NULL COMMENT 出生日期, college VARCHAR(100) DEFAULT NULL COMMENT 学院, major VARCHAR(100) DEFAULT NULL COMMENT 专业, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生主表; -- 健康档案表每次体检或检查生成一条记录 CREATE TABLE health_record ( id BIGINT NOT NULL AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 关联student.id, check_date DATE NOT NULL COMMENT 检查日期, height DECIMAL(5,2) DEFAULT NULL COMMENT 身高cm, weight DECIMAL(5,2) DEFAULT NULL COMMENT 体重kg, vision_left DECIMAL(4,2) DEFAULT NULL COMMENT 左眼视力, vision_right DECIMAL(4,2) DEFAULT NULL COMMENT 右眼视力, blood_pressure VARCHAR(10) DEFAULT NULL COMMENT 血压如120/80, allergy_history VARCHAR(255) DEFAULT NULL COMMENT 过敏史, past_history VARCHAR(255) DEFAULT NULL COMMENT 既往病史, remark VARCHAR(500) DEFAULT NULL COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student_date (student_id, check_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康档案记录表;字段设计的几个考虑点health_record 不冗余学生姓名只存 student_id查询时再关联。统一由后端接口层做关联查询避免因为学生改名导致档案表数据不一致血压字段用 VARCHAR 存储因为 120/80 这种格式不适合拆成两个数字列查询时如果需要做区间筛选也可以在 SQL 中用 CAST 转换但频率很低不必为此破坏表的直观性KEYidx_student_date是联合索引覆盖“查某个学生的历史记录”这个最常见查询避免全表扫描。2.3 字典表与状态字段让档案数据可维护如果系统需要支持院系管理、体检项目配置或档案状态流转比如“待完善、已提交、已审核”建议增加一张字典表把枚举值从代码中抽出来放到数据库里方便非开发人员维护CREATE TABLE dict_item ( id BIGINT NOT NULL AUTO_INCREMENT, dict_type VARCHAR(50) NOT NULL COMMENT 字典类型如college、status, item_label VARCHAR(50) NOT NULL COMMENT 显示文本, item_value VARCHAR(50) NOT NULL COMMENT 实际值, sort_order INT DEFAULT 0, PRIMARY KEY (id), KEY idx_dict_type (dict_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT数据字典表;前端页面里的学院下拉框、性别单选、档案状态标签统一从/api/dict/type/{type}接口读取而不是写死在前端代码里。好处是换了个学院名称或新增了一个体检结论只改数据库不用动前后端代码再发一次版。3. SpringBoot 接口层实现从依赖配置到档案接口3.1 SpringBoot 项目的依赖与基础配置SpringBoot 项目的基础依赖按常见做法来web 场景启动器负责 HTTP 接口MyBatis-Plus 做数据访问MySQL 驱动负责底层连接。创建一个 SpringBoot 项目后在 pom.xml 中加入以下依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency使用 MyBatis-Plus 而不是原生 MyBatis 的理由很直接健康档案系统的单表 CRUD 占据大半工作量MyBatis-Plus 的 BaseMapper 自动提供 insert、updateById、selectById 和分页查询方法代码量约减少 40%。而复杂关联查询如“查询学生最新一条档案记录”仍然手写 XML SQL两者互不冲突。application.yml 里的关键配置spring: datasource: url: jdbc:mysql://localhost:3306/health_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里的 url 必须带useUnicodetruecharacterEncodingutf8否则插入中文姓名或过敏史时会出现乱码。serverTimezoneAsia/Shanghai是为了解决 MySQL 驱动 8.0 版本对时区的严格校验而必须加的不加会启动报错。log-impl 只在开发阶段开启上线时改成org.apache.ibatis.logging.slf4j.Slf4jImpl或直接注释掉避免 SQL 日志刷屏。3.2 档案模块的 Controller 与 Service 层核心接口分成两类一是学生信息的增删改查二是档案记录的追加和历史查询。档案记录只提供“新增”和“查询”不提供“删除”这是刻意为之——健康档案是医疗性质的数据物理删除会破坏审计链路如果录错了正确做法是新增一条更正记录。这是健康档案和普通业务系统在接口设计上的一个原则性差异。Controller 层的三个核心接口RestController RequestMapping(/api/student) public class StudentController { Resource private StudentService studentService; // 新增或修改学生基础信息 PostMapping(/save) public Result? save(RequestBody Valid Student student) { return studentService.saveOrUpdateStudent(student); } // 分页查询学生列表支持按学院/专业过滤 GetMapping(/page) public Result? page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String college) { return studentService.pageStudents(pageNum, pageSize, college); } // 查询某学生全部档案记录按检查日期倒序 GetMapping(/{studentId}/records) public Result? records(PathVariable Long studentId) { return studentService.getHealthRecords(studentId); } }参数说明defaultValue 1和defaultValue 10是常见做法前端即使不传也能跑college 允许为空为空时查询全部学院。RequestBody Valid表示请求体中的 JSON 数据在进入方法前先做字段校验比如姓名非空、学号格式合法。Service 层的实现要绕开一个陷阱MyBatis-Plus 的saveOrUpdate方法依赖主键存在与否。新增时主键为空更新时主键有值。所以 Service 里必须判断 student.id 是否为 null而不是直接调 saveOrUpdate否则前端漏传 id 时会把已有记录更新掉Service public class StudentServiceImpl implements StudentService { Override public boolean saveOrUpdateStudent(Student student) { if (student.getId() null) { // 新增前检查学号是否重复 Long count studentMapper.selectCount( new LambdaQueryWrapperStudent() .eq(Student::getStudentNo, student.getStudentNo())); if (count 0) { throw new ServiceException(500, 学号已存在请勿重复添加); } return studentMapper.insert(student) 0; } return studentMapper.updateById(student) 0; } }这里的 LambdaQueryWrapper 是 MyBatis-Plus 提供的条件构造器eq(Student::getStudentNo, ...)对应 SQL 中的WHERE student_no ?。先查重再插入避免数据库层直接抛唯一键冲突异常否则前端拿到的错误信息会是英文栈信息对使用者不友好。3.3 档案增查接口与 MyBatis-Plus 分页插件档案记录的新增接口要防止一个典型错误恶意或误传学生 ID。常见做法是新增前校验 student_id 是否存在PostMapping(/record/save) public Result? saveRecord(RequestBody Valid HealthRecord record) { if (studentMapper.selectById(record.getStudentId()) null) { throw new ServiceException(500, 关联的学生不存在); } return healthRecordMapper.insert(record) 0 ? Result.success(档案记录已添加) : Result.error(添加失败); }分页查询“学生列表 最新健康状态”时需要配置 MyBatis-Plus 的分页插件不加这个配置导致分页不生效Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }PaginationInnerInterceptor 的原理是在拦截层面改写 SQL为原语句拼接 LIMIT 子句。不配这个 Bean 时selectPage方法会执行全量查询数据量一到万级就会产生接口超时和内存压力。4. Vue 前端搭建页面路由、Token 处理与档案展示4.1 Vue 项目的环境准备与依赖安装Vue 前端采用 Vue 3 Vite Element Plus 的技术组合这是目前搭建中后台系统比较省力的一套路子。创建项目和安装依赖分三步npm create vitelatest health-frontend -- --template vue cd health-frontend npm install npm install vue-router4 axios element-plus这一步需要说明网速较慢时npm create可能卡住常见的做法是配置 npm 镜像源npm config set registry https://registry.npmmirror.com然后再执行上面的命令。Vue 项目的运行命令是npm run devVite 默认监听 5173 端口浏览器访问http://localhost:5173即可看到模板页。4.2 路由配置前后端分离下页面路由该怎么设计前端路由负责页面跳转和后端接口是两套独立路径体系。以健康档案管理为例页面结构分为三个区块布局页、学生列表页、档案详情页。路由配置如下// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/MainLayout.vue), redirect: /student, children: [ { path: student, name: StudentList, component: () import(../views/StudentList.vue), meta: { title: 学生档案列表 } }, { path: student/:id/records, name: StudentRecords, component: () import(../views/StudentRecords.vue), meta: { title: 健康档案详情 } } ] } ] const router createRouter({ history: createWebHistory(), routes }) export default routercreateWebHistory()使用 HTML5 History 模式URL 中不显示 hash 的#号比较美观。但注意部署时要让 Nginx 将所有路径都try_files到 index.html否则刷新/student/1/records页面会 404这是前后端分离部署最常见的坑之一第 5 章展开讲。4.3 Axios 请求封装统一处理后端返回与 Token前后端分离项目中axios 封装是必须做的一步否则每个页面都要重复处理状态码和 Token代码冗余且容易漏掉错误分支。对于健康档案这类数据敏感项目Token 处理更要规范化。下面是一个常用的封装// src/utils/request.js import axios from axios import router from ../router // 创建 axios 实例设置基础 URL 和超时时间 const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error) ) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { alert(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request这段代码的几个关键点baseURL: /api配合 Vite 的代理配置把/api前缀转发到后端 8080 端口解决开发环境下的跨域问题请求拦截器每次从 localStorage 取出 Token 放入Authorization头格式采用 Bearer 方式和后端 Spring Boot 的拦截器约定匹配响应拦截器统一判断 code 字段401 表示登录过期直接清除 Token 并跳回登录页。配套的 Vite 代理配置vite.config.jsexport default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })changeOrigin: true表示把请求头中的 Host 改为 target 地址的域名后端不会因为来源域名不正确而拒绝请求。4.4 档案列表页的组件实现表格展示与状态字段映射学生列表页用 Element Plus 的 Table 组件 分页组件从后端拉数据并渲染template div el-card el-form inline el-form-item label学院 el-input v-modelquery.college placeholder请输入学院名称 clearable / /el-form-item el-button typeprimary clickloadData查询/el-button /el-form /el-card el-table :datatableData border stripe el-table-column propstudentNo label学号 width120 / el-table-column propname label姓名 width100 / el-table-column propcollege label学院 / el-table-column propmajor label专业 / el-table-column propphone label联系电话 width140 / el-table-column label操作 width150 template #default{ row } el-button typeprimary link click$router.push(/student/${row.id}/records) 查看档案 /el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal :page-sizes[10, 20, 50] layouttotal, sizes, prev, pager, next changeloadData / /div /template script setup import { ref, reactive } from vue import request from ../utils/request const query reactive({ pageNum: 1, pageSize: 10, college: }) const tableData ref([]) const total ref(0) const loadData async () { const res await request.get(/student/page, { params: query }) tableData.value res.data.records total.value res.data.total } loadData() /scriptv-model:current-page和v-model:page-size是 Element Plus 分页组件的双向绑定写法翻页或切换每页条数时自动更新 query 对象change事件触发 loadData 重新拉数据layout属性决定分页条显示哪些部件这个实例只展示总量、可选每页条数和页码不做多余跳转。5. 联调、部署与排错前后端分离项目的常见问题5.1 跨域配置的三个层面逐个排查前后端分离项目启动后第一个遇到的多半是跨域问题。报错信息是CORS policy: No Access-Control-Allow-Origin header。出现在三个层面按从开发到生产的顺序排查开发阶段由 Vite 代理解决即第 4 章的server.proxy配置。生产阶段有两个方案一是后端允许跨域在 SpringBoot 启动类或配置类中注册跨域映射Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { // 允许前端页面所在域名的跨域请求 registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }二是更推荐的方案Nginx 反向代理后端接口让前端和后端同域完全绕开跨域。Nginx 配置如下server { listen 80; server_name your-domain.com; root /var/www/health-frontend/dist; index index.html; # 前端路由刷新时回退到 index.html location / { try_files $uri $uri/ /index.html; } # 接口请求转发到后端 8080 端口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这份配置同时解决了两个问题try_files负责 SPA 路由刷新不 404proxy_pass负责反向代理接口请求。用同域方案后后端就不需要 CorsConfig 了网络安全边界更窄。5.2 数据库连接与字符集的经典报错排查开发期最常见的数据库相关报错有三类各自对应不同的解决路径报错信息原因处理办法Access denied for user rootlocalhost密码错误或用户无权限检查 application.yml 中密码是否和 MySQL 一致确认 MySQL 服务已启动Unknown database health_db数据库不存在执行CREATE DATABASE health_db DEFAULT CHARSET utf8mb4;后再启动项目Connection refused/Communications link failure端口不对或服务未启动确认 MySQL 监听 3306Windows 上可用netstat -ano | findstr 3306查看端口状态5.3 前后端联调时核对接口参数的必查细节前后端联调时问题往往不在代码逻辑而在参数类型和结构不一致上。以下是几个高频出错的细节后端LocalDate类型默认接收的是yyyy-MM-dd格式字符串但 Java 后端默认 JSON 反序列化要求格式是yyyy-MM-ddTHH:mm:ss.SSSZ。如果前端传的日期格式是2025-03-12后端会报Cannot deserialize value of type java.time.LocalDate。常见做法是在 application.yml 中配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8time-zone也要格外留意不配 GMT8 时后端存储的时间会比北京时间晚 8 小时。分页参数名必须前后端一致。如果后端用的是 pageNum/pageSize前端 axios 请求里的 params 必须同名。如果后端要求 page1limit10而前端传了 pageNum/pageSize数据就永远查不出来。RequestBody接收对象时前端必须设置Content-Type: application/json且传 JSON 字符串而不是 FormData。不能混用一个接口选了RequestBody前端就要放弃application/x-www-form-urlencoded。5.4 档案数据越权访问的最小防护方案健康档案是敏感数据即使系统的使用范围限定在校内也不能在接口层裸奔。用最简单的方式做一版接口权限在 SpringBoot 项目中配置拦截器或过滤器校验请求头中的 Token然后从 Token 中解析出当前用户角色控制接口的访问范围。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析 token判断访问者是否有权限访问该接口 // 学生角色只能访问自己的档案管理员可以访问全部 return true; } }preHandle在请求进入 Controller 之前执行返回 false 则请求被拦截不会触达业务逻辑。这是前后端分离项目中接口安全的最小闭环更严格的权限控制校级角色、院级角色、校医院的读写划分需要引入 Spring Security 或 Sa-Token 来落地但拦截器这一个动作先把“未登录禁止访问接口”这条底线守住。如果你拿到的是项目源码启动时优先检查.sql文件的导入完整性和application.yml的数据库密码字段——两者是搭建阶段报错最多的入口。本文还有配套的精品资源点击获取