Spring Boot 智能健康管理系统这名字换到哪个计算机专业群里大概率都能看到有人在找、在问、在求源码。这类项目几乎年年出现在毕业设计、课程设计和简历项目里原因很直接业务不绕、场景真实、做完以后能拿出可视化图表、能讲出完整技术链路从登录鉴权到数据分析一张图全串起来。我最近正好把一个带完整源码的 Spring Boot 智能健康管理系统重新梳理了一遍源码编号 13392整理过程中发现很多值得单独写出来的细节所以这篇就当一次项目复盘适合准备做类似系统、或者拿到源码打算二次开发的朋友参考。我不打算堆概念也不打算把这篇文章写成 Spring Boot 官方文档的复读。下面所有内容都按实际做这个系统时的顺序来讲先想清楚系统该有哪些模块再设计数据库后端怎么写前端怎么接最后说运行和排坑。尤其是排坑部分基本是网络上搜不到、但做项目时大概率会撞上的东西能帮大家省下不少时间。1. 项目定位这不该只是一个“增删改查”的跟风项目先别急着打开 IDE 写代码。我见过太多人拿到类似源码后直接npm install然后mvn spring-boot:run跑起来以后对着页面说“会了”但实际上项目里的业务关系和设计逻辑完全没看懂。换一个题目、换一个场景马上就不会变通了。所以我在拿到这套源码、准备重新整理时第一件事是把业务闭环拆清楚。1.1 用户端和管理端并存的权限结构这套系统的核心使用对象是两类人普通用户和系统管理员。普通用户负责日常健康数据维护比如记录血压、血糖、心率、体重、步数以及查看系统给出的健康分析和建议管理员则负责内容与数据层面的管理比如维护健康资讯、管理注册用户、查看统计数据。它和普通图书管理系统的本质区别在于图书管理系统只有“管理员录入 读者借阅”两个动作而健康管理系统是让用户持续产生数据再靠这些数据形成反馈。如果系统只有记录没有分析那和 Excel 没区别如果只有分析没有记录入口那又没法形成闭环。所以源码里把“健康档案—指标记录—趋势分析—评估建议”串成主线这条主线就是整个项目区别于其他管理系统的核心价值。在代码层面用户端和管理端我没有拆成两个后端服务而是做成一个 Spring Boot 单体应用。这里有一个很现实的考虑做毕业设计或中小型项目单体完全够用部署也省事。如果能在一个进程里通过角色区分解决权限问题就没有必要为了“微服务”三个字引入 Nacos、Gateway 之类的东西给自己徒增复杂度。很多同学在简历里写微服务其实项目里就两张表面试官一问服务拆分边界就露馅远不如把一个单体系统的模块边界讲透。1.2 “智能”体现在哪里别让两个字变成摆设现在项目名不带“智能”好像都拿不出手但“智能”不能只存在于标题里。在这套系统里智能主要体现在三块健康指标自动判级、健康趋势可视化和个性化建议生成。举个例子用户录入血压后后端不是简单保存一条记录就结束而是会根据高血压分级标准判断当前数值属于正常、偏高还是高危再把判断结果写入评估记录同时触发建议文案。BMI 同样不是在页面写死公式而是后端根据最近一条身高体重数据实时计算并归档。这类逻辑不涉及深度学习也不需要搞什么训练模型但它能让整套系统看起来是“有逻辑”的而不是一个空壳。我建议如果你准备拿这套源码去答辩或者写简历一定要重点准备这个问题你项目里的智能逻辑是什么怎么实现的为什么选择这种阈值判断而不是用模型答案不需要复杂但必须能讲清楚。因为大多数评审老师不指望你做出一套真正的 AI 医疗系统他们更关心你自己写的那部分代码是不是真的理解了。2. 数据库设计先定版本和表结构代码才能少返工Spring Boot 项目动手写接口之前有两件事要确定技术版本组合和数据库表结构。版本组合影响你后面会踩多少坑表结构影响所有业务代码怎么写。这两件事我在跑这套源码时都调整过花了不少时间所以单独拿出来讲。2.1 技术版本搭配别盲目追新我在检查这套源码时第一件事就是看pom.xml的版本。这里有个跟热词里“springboot版本太高”完全对应的问题不少人在网上看到 Spring Boot 3.x 的新特性就忍不住把版本调高结果启动时报一堆错然后又到处搜 “springboot jdk1.8打包” 或者 “spring-boot-starter-web 3.x 找不到 javax.servlet” 之类的问题。我当时重跑项目用的版本组合是Spring Boot 2.7.18 JDK 8 MyBatis-Plus 3.5.x MySQL 8.0 Vue 2.7 Element UI。为什么没有上 Spring Boot 3原因很实际Spring Boot 3 把包名从javax.*改成了jakarta.*很多旧依赖和网上的教程都还对不上加上 MyBatis-Plus、Druid、Swagger 这些老搭档在高版本下的适配都有坑如果一个项目只是为了做毕业设计或学习框架完全没必要花大量时间去填版本兼容的坑。一个常见误区是以为 JDK 版本越高越好Spring Boot 版本越高越好。实际上学习阶段最需要考虑的是资料丰富度。Spring Boot 2.7 有着数量庞大的教程和 Stack Overflow 问答遇到问题基本都能搜到现成方案。如果你是本地跑源码我建议就先按项目原本的版本跑通不要一上来就升级。等整个项目跑顺了、理解了再去尝试升级才是合理的节奏。2.2 核心表拆解一个健康系统到底要几张表这套系统的表结构不算复杂核心表大概就六张。如果面试被问到项目能把这六张表的关系说清楚基本就成功了一半。我整理了最核心的表清单表名用途核心字段sys_user用户表区分管理员和普通用户id, username, password, role, status, create_timeuser_profile用户健康档案存基础身体信息id, user_id, gender, birthday, height, weighthealth_record健康指标记录表存血压、血糖、心率等id, user_id, record_type, metric_value, record_timehealth_assessment评估结果表存每次分析得出的结论id, user_id, assessment_type, result_level, advice, create_timehealth_plan健康计划表id, user_id, plan_title, plan_content, plan_date, statusarticle健康资讯文章表id, title, cover, content, category, status这里要特别说明health_record表的设计。有一种方案是给每种指标建一张表比如血压表、血糖表、心率表分开存另一种是把所有指标放一张表里用 record_type 字段区分。这套源码采用的是第二种也叫行转列思路。好处是以后要加“血氧”指标时不需要新建表也不需要改表结构只需要在代码里加一个 record_type 的枚举值坏处是同一时间点查多个指标时 SQL 稍微复杂一点需要自己写行转列逻辑。如果指标类型非常固定比如只做血压管理宽表方案查询性能会更好。但作为通用健康管理系统我用动态指标方案更合理。由于用户端主要查的是“某个指标在时间范围内的趋势”所以我在user_id和record_time上建了联合索引实测数据量不大的情况下查询很快基本没有可感知的延迟。2.3 MyBatis-Plus 的自动填充和逻辑删除这源码里使用了 MyBatis-Plus它带来的最大价值是单表 CRUD 基本不用写 SQL。但有两个小设计很值得借鉴。第一个是公共字段自动填充create_time、update_time这种字段通过TableField(fill FieldFill.INSERT)配合MetaObjectHandler自动完成不用在 Service 层反复setCreateTime(new Date())。第二个是逻辑删除在用户表和记录表都加上deleted字段通过TableLogic注解让删除操作变成UPDATE而不是真正的DELETE。逻辑删除对健康管理类系统挺重要。一方面用户误删数据时还有恢复的可能另一方面统计历史数据时不会因为物理删除造成结果断档。缺点是每次查询都要多带一个deleted0条件但 MyBatis-Plus 已经自动处理了这个逻辑对开发者无感。如果你是照着类似项目自己写建议保留这个习惯。3. 后端实现Spring Boot 项目里最值得看的几段代码后端代码是整个系统的中枢。很多同学拿到源码喜欢从 Controller 开始看但其实最有价值的是统一返回体、鉴权拦截器、异常处理和业务分析逻辑。把这几块看懂了系统的扩展思路也就清晰了。3.1 统一返回、全局异常和 JWT 拦截先给项目打好地基如果一个项目里的接口返回值五花八门有的直接返回对象、有的返回 Map、有的报错时返回一段裸字符串前端联调时就会非常痛苦。这套源码的做法是封装了一个通用的ResultT返回体包含code、message、data三个字段。成功时 code 为 200业务异常时 code 为 500前端在 Axios 拦截器里根据 code 做统一处理不需要每个页面单独写报错弹窗。配合统一返回体的是全局异常处理通过RestControllerAdviceExceptionHandler兜底捕获异常。这样做的意义在于Service 层抛出业务异常后不需要 Controller 层到处写try...catch代码会干净很多。比如用户注册时用户名已存在就抛一个ServiceException(用户名已被注册)全局异常处理器捕获后自动转成Result.error(...)。安全方面没有引入 Spring Security而是用 JWT HandlerInterceptor 做了一个轻量级登录态校验。用户登录成功后会生成 Token后续请求在请求头里带Authorization: Bearer token拦截器会从 Token 中解析出用户 ID 和角色放入 ThreadLocal 供后续业务使用。管理员接口再额外判断角色。这里给一段核心拦截器代码作为参考源码逻辑类似public class JwtInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; public JwtInterceptor(JwtUtil jwtUtil) { this.jwtUtil jwtUtil; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { throw new ServiceException(401, 未登录或登录已过期); } String token authHeader.substring(7); Long userId jwtUtil.parseUserId(token); String role jwtUtil.parseRole(token); UserContext.set(UserContextDto.of(userId, role)); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }使用拦截器而不是每个 Controller 方法都写一遍 Token 解析逻辑好处是登录状态识别和业务代码解耦。需要注意拦截器里要手动添加白名单比如登录接口、注册接口、健康资讯浏览接口、文件访问接口等。如果你集成了 Swagger还要把/swagger-ui/**、/v3/api-docs/**这样的路径也加进白名单否则会出现“接口文档打开正常但点 try it out 全部 401”的诡异问题。3.2 为什么 Controller 要做得“薄”Service 才是业务重地我见过不少同学习惯把业务逻辑直接写在 Controller 方法里参数一进来就开始 if else虽然项目能跑但后面根本没法维护。这套源码的分层习惯比较好Controller 负责接收参数、调用服务、返回统一结果Service 负责业务判断和数据组装Mapper 只负责数据库交互。这样拆分后新增接口时你只需要在 Service 层复用逻辑不至于把同一个指标判断代码复制三个地方。以新增健康记录为例Controller 里的代码很薄PostMapping(/record) public ResultLong addRecord(RequestBody Valid HealthRecordAddDTO dto) { Long recordId healthRecordService.addRecord(dto); return Result.success(recordId); }真正干活的是healthRecordService.addRecord方法它要完成这几件事将 DTO 里的指标类型转成枚举并校验根据指标类型做数值范围检查比如收缩压不可能小于 30 也不应该大于 300插入记录如果该指标涉及评分则触发一次评估任务。这套流程如果全部塞在 Controller方法会变得又长又乱而且复用性很差。3.3 评估建议从“记录数据”到“输出结论”的临门一脚健康管理系统能不能体现出“智能”关键就在评估模块。这套源码里评估逻辑写了一层独立的 Service没有和记录接口强耦合而是采用“记录后触发”的方式。比如用户录入体重后系统会主动取用户档案里的身高计算 BMI然后根据标准范围给出偏瘦、正常、超重、肥胖的结论和相应的建议。我不放完整代码说下核心判断逻辑。源码里的HealthAssessStrategy本质上就是一组规则引擎以血压为例public AssessmentResult evaluateBloodPressure(Integer systolic, Integer diastolic) { if (systolic null || diastolic null) { return AssessmentResult.insufficientData(); } if (systolic 180 || diastolic 110) { return AssessmentResult.warn(血压已达到高危水平请立即休息并尽早就医); } if (systolic 140 || diastolic 90) { return AssessmentResult.warn(血压偏高建议连续监测 3 天并记录饮食情况); } if (systolic 90 || diastolic 60) { return AssessmentResult.info(血压偏低注意补充营养避免久坐后突然站立); } return AssessmentResult.ok(血压在正常范围内请继续保持规律作息); }这段逻辑非常简单但它解决了一个真实问题把“血压 145/95”这种用户看不懂的数字变成用户能理解的结论。如果你做类似系统评估策略可以把所有规则放到一个 Map 或策略类集合里由指标类型分发到不同的方法这样以后调整判断阈值时只需要改一个地方。3.4 趋势查询给前端图表准备数据图表是健康管理系统最直观的门面。前端要画“最近 7 天血压变化”后端不能把 1000 条记录全部甩给前端让前端自己算而应该在 SQL 层就做好聚合。源码里这段查询用得比较频繁大概逻辑是给定用户 ID 和指标类型按天分组统计平均值。如果完全没有做聚合前端拿到一堆乱序的明细数据既要在 JS 里写一堆日期格式化逻辑又要自己循环求平均值很容易出错。所以后端接口在返回数据前会先以日期作为分组维度做一次处理得到一个有序数组[ { date: 04-01, avgValue: 118.5 }, { date: 04-02, avgValue: 121.0 } ]前端拿到这个格式后几乎不需要做额外加工直接把 date 映射到 X 轴avgValue 映射到 Y 轴ECharts 的折线图就出来了。我见过很多项目在这一层偷懒导致图表代码又长又难查这正是要避免的。4. 前端可视化Spring Boot 接口如何和 Vue 页面配合这套源码的前端用的是 Vue Element UI整体页面包括登录页、用户首页、数据录入、趋势分析、我的计划、后台管理。前端部分最核心的是路由权限控制和图表联调这里挑重点说。4.1 路由设计用户和管理员看到同一个系统却不看到同一套菜单用户登录后前端拿到的用户信息里包含 role 字段前端根据 role 渲染不同的菜单。具体做法其实不复杂。路由表分成两部分一部分是所有人可访问的路由比如首页和个人中心另一部分是必须在登录后访问的路由。Vue Router 的全局前置守卫里判断localStorage.getItem(token)如果没有 Token 就跳转登录页面。菜单是根据角色过滤的后端接口也会二次校验权限前端只是优化体验真正兜底的是后端权限。4.2 Axios 统一拦截器如果每个页面单独写axios.get、单独处理 Token要不了多久代码就会散架。源码里做了统一的request.js在 Axios 请求拦截器中把 Token 加到请求头在响应拦截器里统一判断返回码。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录过期)) } if (res.code ! 200) { Message.error(res.message || 系统异常) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(网络连接失败) return Promise.reject(error) } )这段代码是一个很经典的前端拦截模式。登录过期后自动清 Token、跳转登录页比每个页面单独判断 401 强太多。如果你做前后端分离项目这套写法可以直接抄走。4.3 用 ECharts 画趋势图前端最关注的数据格式我要强调的是前后端在开发前必须先约定图表数据格式否则两头都会返工。这套系统里后端返回给图表的是“日期平均值”的列表前端代码只用一次循环就能转成 ECharts 需要的 X 轴和 Y 轴数据。下面是典型的 ECharts 折线图配置const chart echarts.init(document.getElementById(trendChart)) chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: trendData.map(item item.date) }, yAxis: { type: value, name: mmHg }, series: [ { name: 收缩压, type: line, smooth: true, areaStyle: { opacity: 0.15 }, data: trendData.map(item item.avgValue) } ] })如果后端返回的数据多包了一层或少了一个字段前端这里都要额外处理。因此我强烈建议在做类似项目时后端直接给出“前端拿来即用”的 DTO而不是把实体类原封不动序列化出去。这样接口会稳定很多后续改动字段也不会立即影响页面。5. 本地运行指南拿到这套源码后如何快速跑起来很多人拿到源码包后第一反应是双击启动类然后发现数据库没配、前端依赖没装、Node 版本不对各种报错。下面这份运行顺序是我实际测过的照着来基本能避开大部分问题。5.1 环境准备清单本地运行这套源码只需要装这些JDK 8 或 JDK 11、Maven 3.6、MySQL 8.0、Node.js 14。前端如果用的 Vue 2.7 Element UI不要随便升级到 Vue 3否则组件库得换成 Element Plus代码改动面会很大。检查环境的命令java -version mvn -v node -v npm -v mysql --version环境准备阶段最容易犯的错误是 JDK 版本与 Spring Boot 版本不匹配。你看到源码里是 JDK 8却非要用本机的 JDK 17 去启动有时确实能跑但 Maven 编译时可能出现source 1.8 中不支持 ...之类的报错也可能出现 Java 17 模块化限制导致反射访问失败的问题。最稳妥的方案是安装一个 JDK 8并确保JAVA_HOME指向它。5.2 后端启动步骤第一步创建数据库。打开 MySQL 客户端执行源码包里的sql/health_system.sql会自动建库建表并插入管理员账号。mysql -uroot -p sql/health_system.sql第二步修改后端配置文件application.yml把数据库地址、用户名和密码改成你本机的配置。很容易忘的小细节是时区参数spring: datasource: url: jdbc:mysql://localhost:3306/health_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果不加serverTimezoneAsia/Shanghai在部分 MySQL 驱动版本下连接会直接报时区错误如果你加了却还是差 8 小时就要检查是不是 MySQL 系统时区本身设置成了 UTC。这个问题在踩坑部分还会展开说。第三步启动后端。在项目根目录执行mvn spring-boot:run看到Started HealthApplication日志后说明后端启动成功。可以访问登录接口验证一下curl -X POST http://localhost:8080/api/user/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123}正常会返回包含 Token 的 JSON 数据。如果你看不懂curl直接拿 Postman 或者 Apifox 测也一样重点是看返回的 code 是不是 200。5.3 前端启动步骤前端是独立 Vue 工程和后端通过 HTTP 接口交互所以必须单独启动。进入frontend目录后先安装依赖npm install如果速度慢可以替换成淘宝镜像源npm config set registry https://registry.npmmirror.com。这一步不是必须的但能明显缩短安装时间。依赖装完以后直接启动开发服务器npm run dev前端默认端口如果在 Vite 或 vue-cli 配置里是 8081而后端是 8080需要确认前端代理是否配好。通常开发环境会在vue.config.js里配置 proxy把/api开头的请求转发给后端devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }如果没配代理前端页面会请求localhost:8081/api/...然后得到 404。配置代理后浏览器里请求的还是 8081但实际由 Node 开发服务器转发到了 8080这样就不会有跨域问题了。源码默认账号里有一个管理员和一个测试用户管理员进入后台管理普通用户进入健康记录页面登录后最好第一时间把初始密码改掉。5.4 前后端启动顺序小建议实际启动时先启动后端再启动前端因为前端登录时要立刻发送请求如果前端起来了后端还没起首次请求会失败容易让人误以为代码有问题。另外后端启动时可以关注控制台里有没有打印 Mapper XML 加载失败或者数据源连接失败的警告有的话不要急着开前端先把后端日志里的红字清干净。6. 常见问题与排查经验这套源码最容易踩的坑这一部分我想写最实用内容。无论是跑这套源码还是以后自己从零搭类似系统这些坑大概率都会遇到。我把它们整理成速查经验每条都是实际操作过的。6.1 Spring Boot 版本调高后连锁问题一个接一个“Spring Boot 版本太高”是源码使用者最容易碰到的问题。源码原配 Spring Boot 2.7.18如果手欠改成 3.2.0通常会出现以下连锁反应第一spring-boot-starter-web内部的 servlet 包引用从javax.servlet改成了jakarta.servlet项目里所有用到HttpServletRequest、HttpServletResponse的类都要从import javax.servlet.*改为import jakarta.servlet.*。第二MyBatis-Plus 需要从 3.5.3 以上版本才支持 Spring Boot 3因为 starter 引用的路径变了。第三如果还用了旧版 Swagger 或 knife4j在 Spring Boot 3 下大概率直接启动报错需要换成 springdoc 相关依赖。第四JDK 8 编译会直接失败Spring Boot 3 强制要求 JDK 17。所以我在跑源码时一律不升级核心版本先把业务跑通才是最重要。术语上这就是“稳定优先于激进升级”等项目理解透了再单独拉分支升级也不迟。6.2 查询数据库发现时间差 8 小时健康记录大多和时间强相关如果时间字段差 8 小时趋势图和记录列表看起来都会很怪比如明明下午录的数据页面上显示凌晨。这个问题的根源通常在三个地方一是数据库连接串里没配置serverTimezoneAsia/Shanghai二是 JDBC 驱动把服务器时区当作 UTC三是后端服务器和数据库服务器时区不同。我查这类问题的步骤是先用命令行执行SELECT NOW()看 MySQL 当前时间如果显示 UTC 时间在连接串里加serverTimezoneAsia/Shanghai然后再看后端返回给前端的 JSON如果时间仍然少 8 小时检查application.yml中 Jackson 的time-zone有没有设置GMT8。前端也要注意JavaScript 在解析 ISO 格式时间戳时如果时间字符串带Z后缀会默认按 UTC 转本地时间这也会造成显示偏差。最省事的做法是由后端统一输出yyyy-MM-dd HH:mm:ss格式前端不要轻易new Date()去转换避免二次偏差。6.3 前端拿到的用户 ID 精度不对最后几位都变成 0这套系统的主键我没有用数据库自增而是用了 MyBatis-Plus 自带的雪花 ID。雪花 ID 是一个 19 位长的 Long 型数字而 JavaScript 的 Number 只能安全表示 16 位左右的整数超出安全范围后后几位会被自动舍入成 0。这就导致一个很隐蔽的 bug列表接口返回的用户 ID 看起来没什么异常但点“编辑”按钮后前端带着这个已经失真 ID 去调后端后端发现查不到数据。解决方案有几种最简单的是在后端做 Jackson 全局配置将 Long 型字段序列化为字符串Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; }这样前端收到的 ID 是字符串不会再精度丢失。需要注意如果前端代码里有直接对 ID 做数字运算的逻辑改为字符串后可能需要适配但实际项目中绝大多数场景只是把 ID 当唯一标识传递不参与运算所以影响不大。6.4 MyBatis-Plus 分页不生效数据查全量了如果你在这个源码或者自己写的项目里使用 MyBatis-Plus 分页查询发现调用selectPage返回的数据总条数正确但记录列表却是全部数据那基本是分页插件没有装配。MyBatis-Plus 3.5.x 版本需要显式添加分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个配置selectPage不会在 SQL 后面自动拼接 LIMIT而是先查全表到内存再做内存分页。数据量小的时候看不出问题数据一多接口响应会越来越慢。如果你改了配置还是不生效还要检查 Mapper 方法第一个参数是否真的传了Page对象如果方法参数里没有 PageMyBatis-Plus 也没法帮忙拼接分页。6.5 上传图片能成功但页面显示 404健康资讯或者用户头像如果涉及图片上传这里有一个常见问题文件已经保存到了本地磁盘目录但页面访问图片地址时返回 404。这是因为浏览器访问图片时走的是 HTTP 请求而磁盘上的目录不在 Spring Boot 静态资源映射范围内后端没有把本地路径暴露成可访问的 URL。解决方式是在项目里增加一个本地资源映射配置。我实际操作时用的是自定义 WebMvcConfigurer把磁盘上的上传目录映射成/files/**的访问路径。如果是 Spring Boot 2.7代码类似Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadDir /); }配置完后文件保存返回的 URL 必须是/files/xxxx.jpg这种相对路径或者完整拼接当前域名。如果不做映射文件其实还在磁盘上只是前端拿不到。这个问题在部署到服务器时更容易被忽略因为本地路径和服务器路径往往不一致最好的做法是把上传路径放到配置文件中动态读取。6.6 拦截器把所有接口都拦截了登录接口都进不去新加 HandlerInterceptor 以后如果注册拦截器时直接写registry.addInterceptor(jwtInterceptor).addPathPatterns(/**)那登录接口也会被拦截导致前端一调用登录接口就报“未登录”。我一般会把接口分为公开接口和私有接口两批把不需要登录的路径通过excludePathPatterns排除掉。registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/article/list, /swagger-ui/**, /v3/api-docs/**, /files/** );这里必须注意坑如果你集成了 Swagger 但没把资源路径加入排除列表接口文档页面可以正常打开但点“Authorize”后依然没法访问接口因为请求被拦截器先挡了。排查时最容易误判为跨域问题实际上就是白名单没加全。这套源码我在整理与复跑过程中花在“调版本、调时区、调文件映射”上的时间其实比写业务逻辑还多。如果你也是第一次跑 Spring Boot 前后端分离项目建议先把环境问题解决干净再谈二次开发。代码可以照着改但只有把工程启动的每一个环节都理清楚这套源码才真正属于你。当前版本技术栈对新手来说相当友好后续如果你想让“智能”部分更有看点还可以在评估建议模块里引入更多规则维度例如把连续一周的睡眠时长和心率变异性做成关联分析这样系统就不再只是数据库的搬运工而真正像一位能给出参考建议的助手。