
简介这是一套基于VueSpringBootMySQL的个人健康管理系统毕业设计源码面向计算机专业学生及需要快速搭建权限管理类项目的开发者。系统除健康档案、体检档案、健康咨询三大业务模块外还内置用户管理、部门管理、角色管理、菜单管理、日志管理、数据字典、文件管理与图表展示等通用功能采用RBAC访问控制支持自定义角色并将权限精确到按钮级别适合有精细权限约束需求的场景。包体共300个文件压缩包仅1.12MB其中151个Java文件对应后端逻辑66个Vue文件承载前端页面另有JavaScript脚本、代码生成模板、SQL数据库脚本及YML配置文件等结构清晰便于分模块研读。已有995人学习该资源。资源提供完整前后端源码、数据库初始化脚本与配置文件便于导入运行和二次开发项目功能覆盖业务与系统管理两大层面权限设计细致是一份适合毕业设计参考与实际演练的完整资料。1. 个人健康管理系统凭什么值得做成毕业设计一门课讲不清的完整工程链路临近毕设选题很多同学卡在同一个问题上想做一个能体现技术深度、又不会把工作量撑爆的系统。个人健康管理系统就是个被低估的选题它的业务核心是把体重、血压、血糖、运动记录这些零散数据变成随时能查、能比、能预警的健康档案。听起来简单真做起来要同时处理多用户登录、数据权限、前后端分离和报表统计几乎覆盖一整套完整工程链路。它适合学过 SpringBoot 和 Vue 基础、想用完整项目把两层串起来的同学也适合答辩时有界面可演示、有数据可解释的准毕业生。难点不在算法而在工程细节登录态怎么维持、接口怎么写不混乱、图表数据从哪来、部署时哪些坑会拖到答辩前最后一晚才爆出来。下面按“最小可用但结构不缩水”的路子讲照着做不返工。2. 先把系统设计写成能落地的方案功能边界、表结构与接口约定2.1 功能模块怎么划分控制好边界才能控制工作量毕业设计最容易翻车的不是代码写不出来而是功能越加越多最后每张表都半成品、每个页面都提示功能开发中。做个人健康管理系统我的习惯是先把功能切成四个模块用户中心、健康档案、日常记录、趋势统计。用户中心负责注册、登录、修改密码核心是登录态和密码安全健康档案管理身高体重、血压、血糖、心率这些体征是整套系统的主数据日常记录包含运动和饮食用来给趋势统计提供历史数据趋势统计负责把记录聚合成长周期图表比如最近三十天的体重曲线、每周平均睡眠时长。再加一个健康预警血压或血糖超过阈值时在首页弹提示。这四个模块加一个预警已经足够支撑一场 30 分钟的答辩演示也不会让开发周期失控。用表格把功能边界列清楚后面建表、写接口都会按这个边界走不会跑偏。模块核心功能涉及角色备注用户中心注册、登录、修改密码用户不设管理员后台避免工作量膨胀健康档案体征记录、编辑、删除、查询用户一人多条记录按时间倒序展示日常记录运动时长、饮食热量、睡眠时长用户支持批量查询和按日期筛选趋势统计体重/血压/血糖趋势图、均值用户后端聚合前端绘图健康预警指标超阈值提醒用户用简单条件判断不引入规则引擎功能边界定了之后有一个容易被忽略的原则不要给健康档案加“分享到社区”、也不要给运动记录加“好友排名”。一旦引入社交关系表结构、接口数量、权限模型全线改变毕业设计的工作量会翻倍。克制住加功能的冲动把核心四大块做深比铺一堆半成品值钱得多。预警模块我建议单独做一个 service 方法不要在 Controller 里散落一堆 if 判断。健康指标阈值是可变的比如血压正常范围对老年人和青年人就不同。把这些阈值抽成配置或数据库字段答辩时你能讲出“规则可动态调整”的设计思路而不是写死常量。2.2 数据库表设计把健康字段拆成可扩展的事实表个人健康管理系统的数据模型核心是“一个用户对应多个健康记录事实”。我建议不要把所有字段都塞进一张 user 表而是把用户信息、体征记录、运动记录、饮食记录拆成多张表每张表都加上 user_id 做外键关联。这样写 SQL 时语义清晰后续加字段也不会动主表结构。以下是初始化建库脚本的核心部分实际写毕设时可以在此基础上扩展CREATE TABLE user_account ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, gender TINYINT DEFAULT 0 COMMENT 0未知,1男,2女, birth_date DATE DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户账号表; CREATE TABLE health_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, record_date DATE NOT NULL, height_cm DECIMAL(5,1), weight_kg DECIMAL(5,1), systolic_pressure INT COMMENT 收缩压, diastolic_pressure INT COMMENT 舒张压, blood_glucose DECIMAL(4,1) COMMENT 空腹血糖, heart_rate INT COMMENT 静息心率, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, record_date), CONSTRAINT fk_health_user FOREIGN KEY (user_id) REFERENCES user_account(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康体征记录表; CREATE TABLE exercise_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, exercise_date DATE NOT NULL, exercise_type VARCHAR(30) COMMENT 跑步/游泳/骑行等, duration_minutes INT, distance_km DECIMAL(5,2), calories_burned INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_exercise (user_id, exercise_date), CONSTRAINT fk_exercise_user FOREIGN KEY (user_id) REFERENCES user_account(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT运动记录表;这段 SQL 用到了两个关键设计。第一DECIMAL 存身高体重和血糖避免 FLOAT 带来的精度抖动血压用 INT 存毫米汞柱这是健康类系统的约定俗成第二exercise_log 单独成表不把运动记录塞进 health_record因为健康档案一条记录要对应用户记录对应时间段的多条运动明细。体重和血糖这类字段学生经常用 DOUBLE但在 Java 后端拿 BigDecimal 接收时容易出现小数位不一致的问题。用 DECIMAL(5,1) 和 DECIMAL(4,1) 限定了范围和精度界面显示时直接控制一位小数省去一堆格式化的罗嗦代码。注意 health_record 上加了唯一索引 uk_user_date。这意味着同一用户同一天只能有一条体征记录符合“每天记录一次核心指标”的业务常态。万一用户有多次测量需求就应该把表粒度拆成“一次测量一行记录”而不是在同一行里用多个字段反复覆盖这是数据建模阶段的取舍直接影响后面代码复杂度。唯一索引同时是防重复数据的最终兜底后面避坑章节还会再提到。2.3 前后端接口约定统一返回结构与状态码设计接口时最怕的是前端每个模块各自定义返回格式导致联调时花大量时间对齐。我一般会先定一个统一返回结构所有接口都返回同样的壳子{ code: 0, message: success, data: { } }code 为 0 表示成功非 0 表示业务异常message 供前端直接展示data 是实际业务数据。登录态失效时固定返回 HTTP 401前端通过 axios 拦截器统一跳转登录页而不是在每个页面里单独判断。接口路径按资源命名这是前后端都能记住的约定POST /api/health/record 表示新增体征记录PUT /api/health/record 表示修改DELETE /api/health/record/{id} 表示删除GET /api/health/record/list 表示分页查询。所有列表接口统一接收 pageNo、pageSize 两个分页参数趋势接口接收 startDate、endDate 两个日期参数。这套约定从第一天就定下来联调阶段会省掉非常多重复沟通。这样做还有一个附加好处答辩时被问到“如果数据量大了怎么办”你可以直接说“所有列表查询都做了分页趋势统计由后端聚合前端不会一次性加载全量数据”。这是任何评委都愿意听到的工程意识。3. SpringBoot 后端落地从工程骨架到健康数据接口3.1 工程骨架与依赖最小可行配置后端我选用 SpringBoot 2.7 常见的稳定组合Spring Web MyBatis-Plus JWT MySQL Driver Lombok。MyBatis-Plus 可以省掉大量单表 CRUD 的 XML让代码量少一截同时对多表查询仍然保留手写 SQL 的空间对毕设来讲是性价比最高的选择。下面是 pom.xml 里最核心的依赖段版本号统一由 spring-boot-starter-parent 管理只有 MyBatis-Plus 和 JWT 需要显式指定版本dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖选型时有几个默认选择JWT 用 jjwt 而不是 shiro因为 jjwt 只做 token 的生成和解析不绑定权限框架逻辑透明可控MyBatis-Plus 的版本要和 SpringBoot 2.7 匹配3.5.3.1 这个组合经过大量项目验证。如果你用的是 SpringBoot 3.xMyBatis-Plus 需要换成对应新版本但整体用法一致。不引入完整 Spring Security是因为毕业设计阶段的权限粒度只有“已登录”和“未登录”两种JWT 拦截器足够用。引入安全框架之后配置类、过滤器链、授权表达式会让很多新手陷入配置地狱对答辩讲清楚核心逻辑没有帮助。但密码加密不能省单独引入 spring-security-crypto 只拿来用 BCrypt属于性价比最高的选择。application.yml 里真正需要动的是数据源、MyBatis-Plus 和端口三块server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/health?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai jwt: secret: your-256-bit-secret-key-please-change-in-production expire-hours: 4 mybatis-plus: global-config: db-config: id-type: auto configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl 里必须带 serverTimezoneAsia/Shanghai 和 characterEncodingutf8不然 MySQL 8 的时区问题会在启动时报错。jwt.secret 我建议至少 32 个字符因为 HS256 算法会校验密钥长度太短启动解析就会报错。expire-hours 设成 4 小时答辩演示时不容易中途过期。map-underscore-to-camel-case 必须为 true这样 health_record 的字段能自动映射到实体属性。log-impl 配置成 StdOutImpl开发阶段能在控制台看到每条 SQL联调排错非常有用。3.2 基于 JWT 的登录与鉴权拦截器里做的一次性投入登录逻辑本质是三步接收用户名密码、比对加密后的密码、签发 token。密码不要用 MD5直接用 BCryptBCrypt 的每次哈希都带随机盐即使两条相同密码的密文也不一样这是答辩时能讲清楚的加分点。我一般会写一个 JwtUtil 工具类负责生成和解析 token再用一个拦截器统一校验。工具类核心代码Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire-hours}) private Long expireHours; public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireHours * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }这个工具类只有一个职责生成和解析 token。secret 从配置文件里读取不写死在代码里expire-hours 单独配置让 token 过期时间成为一个可调参数。登录接口在 Service 层调用 generateToken返回给前端字符串前端存到 localStorage 里后续每个请求带上即可。拦截器负责把 token 从请求头里拿出来放进请求上下文核心逻辑如下public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { Claims claims jwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }这里有个血泪经验拦截器里只校验 token 和放 userId 到 request 属性不要做数据库查询。因为每个请求都查一遍用户表列表接口频繁调用时无谓的数据库交互会拖慢响应而且拦截器里做查询会让代码分层变得混乱。真正的用户信息在业务代码里按需获取。登录接口本身也要注意异常语义。用户不存在或密码错误统一返回“用户名或密码错误”不要区分提示防止被用来探测已注册账号。负责登录的 Service 方法里用 passwordEncoder.matches(rawPassword, user.getPassword()) 做 BCrypt 校验匹配成功才签发 token。3.3 健康数据 CRUD 接口校验、事务与常见参数说明健康档案接口是对外暴露最频繁的核心接口。一个合格的后端接口要做四件事接收参数、校验合法性、落库、返回统一结构。用 MyBatis-Plus 时Service 层继承 IService 就能获得大部分单表方法但业务校验必须自己写。以一个“新增体征记录”接口为例RestController RequestMapping(/api/health) public class HealthRecordController { Autowired private HealthRecordService healthRecordService; PostMapping(/record) public ResultVoid addRecord(RequestBody HealthRecord record, RequestAttribute(userId) Long userId) { record.setUserId(userId); if (record.getWeightKg() ! null record.getWeightKg() 20) { return Result.fail(体重数据不合法); } healthRecordService.addWithCheck(record); return Result.success(); } }Controller 只做参数接收和简单校验具体业务逻辑放在 Service这是前后端分离项目的基本分层。RequestAttribute(userId) 取的是拦截器塞进去的当前登录用户强制把 user_id 绑定到登录态而不是信任前端传上来的 userId。这个细节非常重要它堵住了最常见的越权漏洞用户 A 伪造一个 userId2 的参数去改用户 B 的健康数据。后端一律以登录态为准前端传的 userId 直接忽略。对应的 Service 方法要处理“同一用户同一天重复录入”的业务约束Service public class HealthRecordServiceImpl extends ServiceImplHealthRecordMapper, HealthRecord implements HealthRecordService { Override Transactional(rollbackFor Exception.class) public void addWithCheck(HealthRecord record) { LambdaQueryWrapperHealthRecord query Wrappers.lambdaQuery(); query.eq(HealthRecord::getUserId, record.getUserId()) .eq(HealthRecord::getRecordDate, record.getRecordDate()); Long count this.baseMapper.selectCount(query); if (count 0) { throw new BizException(当天已存在健康记录请编辑而不是重复新增); } this.save(record); } }Transactional 保证“查重 插入”两个操作要么都成功要么都回滚避免并发请求下插入两条同日期记录。BizException 是自定义业务异常由全局异常处理器统一转成 Result.fail 返回。数据库端的唯一索引 uk_user_date 是最终兜底就算 Service 层查重因并发漏掉数据库也会拒绝第二条重复记录。查询接口需要支持分页和时间范围筛选pageNo、pageSize、startDate、endDate 是列表页的基本参数public PageHealthRecord queryRecordList(Long userId, Integer pageNo, Integer pageSize, String startDate, String endDate) { PageHealthRecord page new Page(pageNo, pageSize); LambdaQueryWrapperHealthRecord query Wrappers.lambdaQuery(); query.eq(HealthRecord::getUserId, userId) .ge(StringUtils.hasText(startDate), HealthRecord::getRecordDate, startDate) .le(StringUtils.hasText(endDate), HealthRecord::getRecordDate, endDate) .orderByDesc(HealthRecord::getRecordDate); return this.page(page, query); }这里有一个使用 MyBatis-Plus 容易犯的错ge 和 le 条件如果直接写在 wrapper 里而前端没传日期参数就会生成record_date null的比较查询结果直接为空而且报错不明显。用 condition 重载ge 方法的第一个布尔参数可以在参数为空时自动跳过这个条件。startDate 和 endDate 建议用 String 类型接收配合yyyy-MM-dd格式避免 LocalDate 反序列化时因格式不匹配报 400。4. 用 Vue 写前端页面路由、请求封装与数据录入页4.1 Vue 工程结构与路由设计前端我推荐 Vue 3 Vite Element Plus 的组合。Vite 启动快Element Plus 组件现成写页面效率比手写 HTML 高太多。工程结构按“页面 组件 请求层”三层拆views每个路由对应的页面components可复用的卡片、图表组件api和后端接口一一对应的请求函数router路由配置utilsaxios 实例和工具函数路由需要区分需要登录和不需要登录的页面。登录页、注册页放公共路由其他页面全部挂在一个带全局布局的路由下const routes [ { path: /login, component: LoginPage }, { path: /register, component: RegisterPage }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, component: DashboardPage, meta: { title: 健康概览 } }, { path: records, component: RecordListPage, meta: { title: 健康档案 } }, { path: records/add, component: RecordEditPage, meta: { title: 新增记录 } }, { path: trends, component: TrendPage, meta: { title: 趋势统计 } } ] } ]路由的嵌套结构决定了页面骨架是否清晰。children 里的页面都渲染在 Layout 的侧边栏和内容区里登录页独立在布局之外。meta.title 会配合菜单高亮和面包屑使用建议一开始就加上避免后面再逐个补。路由守卫在每次跳转时检查有没有 tokenrouter.beforeEach((to) { const token localStorage.getItem(health_token) if (!token to.path ! /login to.path ! /register) { return { path: /login } } if (token (to.path /login || to.path /register)) { return { path: /dashboard } } return true })这套守卫逻辑足够覆盖毕设场景。token 存在 localStorage 里刷新页面不会丢失登录后访问登录页会自动跳回首页这个细节能避免“登录成功后退回登录页还能看到表单”的尴尬。4.2 Axios 请求封装token 注入与统一错误处理axios 封装是整个前端最值得先写的部分。请求拦截器统一加 Authorization 头响应拦截器统一处理业务码和 HTTP 状态码。这层写不好后面每个页面都会复制粘贴“处理 401”的代码。import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(health_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code 0) { return res.data } if (res.code 401) { localStorage.removeItem(health_token) window.location.href /login } return Promise.reject(new Error(res.message || 请求失败)) }, error { if (error.response error.response.status 401) { localStorage.removeItem(health_token) window.location.href /login } return Promise.reject(error) } ) export default service封装之后的调用方只需要关心业务数据不需要在每个页面判断登录态。这里有一个容易混淆的点业务 code 为 401 和 HTTP 状态为 401 是两回事。后端 AuthInterceptor 返回的是 HTTP 401业务中手动返回的 Result.fail 不会触发 HTTP 状态变化所以两个入口都处理 401 才稳妥。对应每个接口的 api 函数单独建文件保持一一对应关系import service from ../utils/request export function addHealthRecord(data) { return service.post(/health/record, data) } export function getHealthRecordList(params) { return service.get(/health/record/list, { params }) }baseURL 在开发环境配成/api由 Vite 代理转发到 8080部署时由 Nginx 做同样的代理。这样前端代码里不出现具体 IP 和端口换环境只需要改代理配置。4.3 健康数据录入页表单校验与接口联调前端页面里最关键的是表单校验。Element Plus 的 form rules 要配置的不仅是“必填”还要把字段的取值范围和格式约定写清楚比如体重范围 20300 kg、血压范围 40250 mmHg。这样前端不合法数据根本到不了后端。一个最小可用的录入表单片断template el-form refformRef :modelform :rulesrules label-width80px el-form-item label记录日期 proprecordDate el-date-picker v-modelform.recordDate typedate value-formatYYYY-MM-DD / /el-form-item el-form-item label体重 propweightKg el-input-number v-modelform.weightKg :min20 :max300 :precision1 / /el-form-item el-form-item label收缩压 propsystolicPressure el-input-number v-modelform.systolicPressure :min40 :max250 / /el-form-item el-button typeprimary :loadingsubmitting clicksubmitForm保存/el-button /el-form /template script setup import { reactive, ref } from vue import { addHealthRecord } from ../api/health import { ElMessage } from element-plus const formRef ref() const submitting ref(false) const form reactive({ recordDate: new Date().toISOString().slice(0, 10), weightKg: undefined, systolicPressure: undefined }) const rules { recordDate: [{ required: true, message: 请选择记录日期, trigger: change }], weightKg: [{ required: true, message: 请填写体重, trigger: blur }] } async function submitForm() { await formRef.value.validate() submitting.value true try { await addHealthRecord(form) ElMessage.success(保存成功) } finally { submitting.value false } } /script使用 el-input-number 的好处是自带最大值、最小值和精度控制比 el-input 手动校验省事得多。日期组件用 value-format 把值固定成 YYYY-MM-DD避免默认的 Date 对象序列化后出现时间偏移。submitting 变量的作用不只是显示加载状态更重要的是在请求返回前禁用按钮防止用户连点两次导致重复提交。联调阶段最常出现的问题是前端传的字段名和后端实体属性对不上。后端 JSON 属性名是 heightCm 这类驼峰前端表单如果定义成 height_cm后端 Jackson 默认也能映射但最好还是统一用驼峰。遇到字段对不上的情况先打开浏览器 F12 看请求 payload再对照后端实体类字段一次能定位。5. 集成 MySQL 与本地部署从建库到跑通的 5 个避坑记录5.1 建库脚本与连接配置时区、字符集、驱动三件事在写代码之前先把数据库建好能让后续联调少一半报错。个人健康管理系统用 MySQL 8.0 即可安装时选 utf8mb4 字符集。建库语句CREATE DATABASE health DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;建库之后用 Navicat 或命令行导入第 2 章的建表脚本。这里需要特别注意mysql-connector-j 驱动在 8.x 版本下驱动类是 com.mysql.cj.jdbc.Driver不是旧版的 com.mysql.jdbc.Driverurl 里必须带 serverTimezoneAsia/Shanghai。这两项写错SpringBoot 启动会直接报无法建立连接。数据库连接配置和 3.1 节一致但还有一个字符集层面的细节如果 MySQL 服务端字符集是 latin1即使 jdbc url 写了 utf8中文也会出现乱码。配置服务端时直接用 utf8mb4它能覆盖 emoji 和一些特殊字符健康记录的 remark 字段就算用户输入表情符号也不会炸。5.2 避坑记录现象、原因与解决办法第一条MySQL 8 连接报错“Server returns invalid timezone”。现象SpringBoot 启动时数据源初始化直接抛异常控制台提示需要设置 serverTimezone。原因MySQL 8 对时区要求严格驱动和服务器默认时区不一致。解决在 jdbc url 末尾加上 serverTimezoneAsia/Shanghai并确认 MySQL 所在机器时区是东八区。如果加了还报错在 MySQL 命令行执行 set global time_zone 08:00。第二条MyBatis-Plus 查询返回的实体字段全是 null。现象数据库表有数据接口也执行了 SQL但 JSON 里返回的字段是 null。原因实体字段是驼峰命名表字段是下划线命名而 map-underscore-to-camel-case 没开启。解决在 application.yml 里把 mybatis-plus.configuration.map-underscore-to-camel-case 设为 true。注意这个配置直接影响所有实体的映射别在单个 mapper 的 XML 里用 resultMap 手工映射字段多时维护成本高。第三条前端请求能访问后端但浏览器报 CORS 跨域错误。现象Vite 运行在 5173 端口SpringBoot 运行在 8080 端口前端 axios 请求后端时被浏览器拦截。原因前后端分离项目没有配置跨域。解决允许跨域可以在后端加 CorsFilter也可以在前端 Vite 配置 proxy 代理。我建议用 Vite proxy把 /api 前缀的请求转发到 8080部署时也只需要部署一个前端静态目录加一个后端服务不需要额外开跨域。Vite 配置片断export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })注意Vite proxy 只在开发环境生效部署到 Nginx 时必须用 Nginx 侧的代理配置两边不要混用。第四条列表接口偶发“当天已存在健康记录”提示但数据库里明明没有重复数据。现象前端连续两次提交第二次提交被后端判定重复。原因第一次请求返回较慢用户等不及点了两次保存同时后端查重和插入不是原子操作极端并发下可能同时通过查重。解决前端在提交按钮上设置 loading 状态并禁用重复点击这是 4.3 节代码里 submitting 变量的作用。后端再加唯一索引 (user_id, record_date)数据库层面兜底才能真正根治。第五条前端打包后访问接口 404 或白屏。现象把 dist 目录放到 Nginx 后页面能打开但所有接口请求都返回 404刷新子路由也 404。原因Nginx 没有配置前端路由 history 模式的回退也没把 /api 请求转发给后端。解决Nginx 配置里加上 try_files 和 proxy_pass用两个 location 分别处理静态资源和接口代理location / { root /opt/health/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意 proxy_pass 后面不要带路径location /api/ 会把完整 URI 原样传给后端。如果后端接口前缀本身就是 /api这行配置不用做任何改写。另外 try_files 的 /index.html 是 history 路由模式的关键缺失时刷新二级页面会 404这个细节在答辩演示时特别容易翻车。6. 进阶健康趋势图表与接口验证让毕业设计从“能用”变“耐看”6.1 用一个聚合接口喂 ECharts 折线图健康记录页只是把表格列出来还停留在 CRUD 阶段。加上趋势统计后系统才像个“管理系统”。做法是在后端新增一个聚合接口不需要在 Java 代码里写循环统计直接用 SQL 的 GROUP BY 按日期求平均值SELECT record_date, AVG(weight_kg) AS avg_weight, AVG(systolic_pressure) AS avg_systolic FROM health_record WHERE user_id #{userId} AND record_date #{startDate} GROUP BY record_date ORDER BY record_date;SQL 返回的每一行是一天的均值前端拿到后直接映射成 ECharts 的 xAxis 和 series。这里要注意如果某一天没有记录SQL 里不会出现该日期图表上会形成断点。处理方式是在前端补全日期序列缺失点用 null 填充ECharts 的 connectNulls 设置为 false 时折线会在断点处断开视觉上更诚实。6.2 两类验证方法接口测试与字段合法性校验交付之前做验证能让你在答辩时从容很多。第一类验证是接口测试不要只靠浏览器点页面。用 Postman 跑通主流程注册 → 登录 → 新增记录 → 查询记录 → 修改记录 → 删除记录。每个步骤保存一个请求样例答辩时现场演示比截图更有说服力。第二类验证是数据合法性校验。健康数据不同于普通业务数据数值越界会影响图表显示。在实体类上用 Jakarta Validation 注解public class HealthRecord { NotNull private LocalDate recordDate; DecimalMin(value 20.0, message 体重不能低于20kg) DecimalMax(value 300.0, message 体重不能高于300kg) private BigDecimal weightKg; Min(value 40, message 收缩压不能低于40) Max(value 250, message 收缩压不能高于250) private Integer systolicPressure; }这样做的好处是校验规则集中声明在实体上Controller 加一个 Valid 注解就生效不会出现前端校验后端不校验的灰色地带。数值上下限可以根据用户群体调整答辩时可以说明这些参数是可配置的。最后说一个我自己的教训我当年做类似系统时所有注意力都放在页面漂不漂亮上结果答辩演示时图表接口因为日期参数格式不匹配直接返回空数据现场只能看表格非常尴尬。从那以后我养成了一个习惯把“存储、接口、展示”三个环节各留一个最小验证用例改完任何参数都跑一遍整个过程不到十分钟。这个习惯也一并送给你希望帮到你。本文还有配套的精品资源点击获取