
我一个做后端的朋友前阵子接了套带智能推荐功能的卫生健康系统项目技术栈是标准的 SpringBoot Vue MyBatis MySQL 前后端分离组合。他当时最头疼的不是功能怎么实现而是这套东西怎么从前端到后端、从本地到服务器整体串起来跑通。折腾完他跟我说了一句很实在的话这种依托主流框架的完整业务系统难点从来不是某一个单独的技术点而是把整个业务链路、推荐逻辑、权限控制、部署流程全部整合到一起的工程能力。这套“前后端分离智能推荐卫生健康系统”刚好就是这样一个综合性极强的实战项目。它不只是一个简单的增删改查 demo而是把当前企业主流的 Vue3 前端生态、SpringBoot 后端框架、MyBatis 数据持久层、MySQL 数据库再加上基于用户行为的智能推荐模块全部融合成一个可以真正上线运行的完整系统。无论你是准备毕业设计、想积累全栈项目经验冲击暑期实习还是刚入职想摸清前后端分离项目整体架构的新人这套系统的每个角落都值得拆开细看。接下来我不打算做个照着代码念文档的工具人而是按我自己的实操习惯把整个项目从架构设计、用户行为采集、推荐算法落地到后端接口实现、前端页面交互、Nginx部署和上线调优一层一层给你刨开讲清楚。里面所有的表结构设计、SQL片段、核心代码逻辑、部署命令都是我实际跑过的你照着抄也能把项目搭起来。1. 项目整体架构拆解为什么这套业务系统必须用前后端分离1.1 项目功能画像与模块划分先把这个系统到底做了什么讲明白。健康卫生类系统网上其实不少但大部分是简单的“信息展示站”真正能落地推荐逻辑的不多。这套健康管理系统覆盖的是一个完整的服务闭环用户端注册登录、个人健康档案管理、健康资讯浏览、热门内容推荐、个性化推荐、症状自查记录。管理端用户管理、资讯审核发布、健康数据监控、推荐规则配置、系统参数设置。核心亮点会结合用户的浏览历史、收藏行为、健康档案中的标签在首页和资讯列表页展示个性化推荐结果。从工程结构来看前端用 Vue 负责页面渲染和用户交互后端用 SpringBoot 发布 RESTful APIMySQL 存业务数据。两个端独立开发、独立部署前端通过 HTTP 请求调用后端接口完成数据交互。这种模式的直接收益是团队分工边界清晰前端不用关心数据库查询后端不用关心页面长什么样只要把接口约定好两边并行开发互不阻塞。1.2 技术选型背后的取舍逻辑这套技术栈不是随便拼的每一个组件都有它的位置SpringBoot当前 Java 后端开发事实标准内嵌 Tomcat自动配置约定优于配置。你在本地写完代码直接跑 main 方法就能起服务不用像传统 SSM 项目那样折腾一大堆 XML 和 Web 容器。Vue Element UI / Ant Design VueVue 的组件化开发模式特别适合后台管理这类有大量表单、表格、弹窗交互的场景。配合现成的 UI 组件库开发效率能翻一倍。MyBatis国内中小型项目使用率极高的持久层框架。它能把 SQL 写在 XML 文件里统一管理复杂查询一目了然不会出现 JPA 那种“自动生成 SQL 出问题后很难排查”的尴尬。MySQL开源免费、性能稳定、资料多做这种业务规模的系统完全足够。最重要的是部署和运维成本低一台 2G 内存的云服务器就能跑得很稳。我一直跟身边的同事强调一个观点选技术栈不该追新而是要选你最能驾驭、踩坑成本最低的方案。这套组合之所以经典就是因为它的坑基本都被前人踩平了你遇到任何报错几乎都能搜到现成的解决方案。1.3 目录结构与项目初始化说下我推荐的工程目录结构。前后端分离项目一定要在项目根目录下分清楚frontend和backend两个独立子项目千万不能揉在一起不然后续打包部署会非常混乱health-recommend-system/ ├── frontend/ # Vue 前端工程 │ ├── public/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # 状态管理 │ │ ├── views/ # 页面视图 │ │ ├── utils/ # 工具函数如 request.js 封装 │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vite.config.js # 开发代理配置 ├── backend/ # SpringBoot 后端工程 │ ├── src/main/java │ │ └── com/health/recommend │ │ ├── controller/ # 接口控制层 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── config/ # 配置类CORS、拦截器、WebMvc │ │ ├── common/ # 通用返回体、异常处理 │ │ └── recommend/ # 推荐算法模块 │ ├── src/main/resources │ │ ├── mapper/ # MyBatis XML 文件 │ │ └── application.yml │ └── pom.xml ├── sql/ # 数据库初始化脚本 └── docs/ # 部署文档和接口文档后端用 Maven 管理依赖pom.xml里的核心依赖无非是spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok这些。前端我用 Vite 构建开发环境下配置代理把/api开头的请求转发到localhost:8080这样能完美避开跨域问题生产环境再交给 Nginx 统一反转。2. 数据库设计与 MyBatis 持久层落地实践2.1 核心表结构设计从用户行为到推荐特征做智能推荐系统表结构是地基。地基打歪了后面推荐逻辑写得再花哨都是空中楼阁。这套系统的表我分成三大类用户类、内容类、行为类。用户相关核心表-- 用户表 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 加密后密码, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1-普通用户 2-管理员, age INT DEFAULT NULL, gender TINYINT DEFAULT NULL COMMENT 1-男 0-女, health_tags VARCHAR(500) DEFAULT NULL COMMENT 健康标签逗号分隔, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4 COMMENT 用户信息表;内容相关表-- 健康资讯/内容表 CREATE TABLE health_article ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 标题, category VARCHAR(50) DEFAULT NULL COMMENT 分类饮食/运动/心理/疾病预防等, tags VARCHAR(500) DEFAULT NULL COMMENT 内容标签, cover_image VARCHAR(500) DEFAULT NULL COMMENT 封面图URL, content TEXT COMMENT 正文内容, view_count INT DEFAULT 0 COMMENT 浏览量, like_count INT DEFAULT 0 COMMENT 点赞数, status TINYINT NOT NULL DEFAULT 1 COMMENT 0-下架 1-上架, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category), KEY idx_status (status) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 健康资讯表;行为相关表这是推荐系统的“石油”来源记录了用户看文章时的浏览和点赞操作。这里要说明一点很多人把行为日志表设计成“每次操作都要记”结果上线没几天表就膨胀到几百兆查询越来越慢。我的做法是除了记录明细流水还会建一张行为聚合表定时把用户-内容-标签的关联权重算好存起来推荐接口查询时走聚合表速度快一个量级。-- 用户行为记录表 CREATE TABLE user_behavior ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, article_id BIGINT NOT NULL COMMENT 内容ID, behavior_type TINYINT NOT NULL COMMENT 1-浏览 2-点赞 3-收藏, behavior_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_behavior (user_id, behavior_type) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 用户行为流水表;2.2 MyBatis XML 编写要点与踩坑记录MyBatis 的核心战场在 XML 映射文件。这套系统的查询场景不算极端但有几个典型场景值得单独拿出来讲场景一带条件的分页查询。用户管理列表、资讯列表都需要按关键词和分类筛选。这里我推荐用where标签做动态 SQL 拼接它能自动处理AND前缀和多余条件的问题select idselectArticlePage resultTypecom.health.recommend.entity.HealthArticle SELECT * FROM health_article where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR tags LIKE CONCAT(%, #{keyword}, %)) /if if testcategory ! null and category ! AND category #{category} /if if teststatus ! null AND status #{status} /if /where ORDER BY created_time DESC LIMIT #{offset}, #{pageSize} /select注意MyBatis 的${}和#{}千万不要混用。排序字段、表名这类不能用#{}预编译的场合才用${}但必须做好白名单校验否则容易出 SQL 注入风险。#{}走的是PreparedStatement占位符安全能用就用这个。场景二批量插入用户行为数据。用户浏览记录这种高频率写入建议用批量插入。但 MySQL 的max_allowed_packet默认只有 4M单次批量条数过多会直接报错我实测 300~500 条一批是安全区间。insert idbatchInsertBehavior INSERT INTO user_behavior (user_id, article_id, behavior_type) VALUES foreach collectionlist itemitem separator, (#{item.userId}, #{item.articleId}, #{item.behaviorType}) /foreach /insert2.3 表不存在自动建表的实现方案网上经常有人搜“SpringBoot MyBatis 当表不存在自动建表”这套系统也遇到过类似需求——尤其是给客户做演示时数据库是全新环境总不能每次手工导入 SQL 初始化。我的解决方案是直接监听 SpringBoot 的ApplicationReadyEvent在应用完全启动后执行一次建表检查Component public class TableInitializer implements ApplicationRunner { Resource private DataSource dataSource; Override public void run(ApplicationArguments args) { try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement()) { stmt.executeUpdate(CREATE TABLE IF NOT EXISTS health_article (...省略建表语句...)); // 还可以顺便做数据初始化 } catch (SQLException e) { log.error(初始化数据库表结构失败, e); } } }这个方案省去了手工导入 SQL 的麻烦让整套系统拷到任何一台装了 MySQL 的机器上都能直接跑起来对新手特别友好。3. 智能推荐模块从算法选型到工程化落地3.1 推荐逻辑设计为什么选择基于标签的冷启动方案推荐算法这个模块是整套系统里最有含金量的地方。现在大厂用的协同过滤、深度学习推荐那些花活在这套系统里并不完全适用——因为没有足够的用户行为数据做支撑。一个刚上线的健康卫生系统用户量少、行为稀疏你硬上 ALS 协同过滤算出来的结果几乎都是默认推荐用户体验并不好。我最终采用的是基于标签的混合推荐策略用户注册时填写健康档案提炼出健康标签比如“高血压”“糖尿病”“久坐办公”“运动健身”等。内容发布时给文章打标签比如“高血压饮食”“办公室拉伸”“有氧运动”。推荐时计算用户标签与内容标签的匹配度得分。同时结合实时行为数据用户最近点赞和收藏了哪些标签的内容动态调整推荐权重。解决冷启动新用户没有行为数据直接用注册标签和热门内容兜底。这套方案最大的优势是可解释性强你能明明白白跟用户解释“为什么推荐文章给你”而且工程实现复杂度远低于协同过滤不需要离线训练任务一套代码搞定。3.2 核心推荐算法的 Java 实现推荐模块的核心类我放在recommend包下面核心思路是“标签匹配加权 行为偏好修正”。下面是关键代码逻辑Service public class RecommendService { Resource private HealthArticleMapper articleMapper; Resource private UserBehaviorMapper behaviorMapper; /** * 获取推荐资讯列表 * param userId 用户ID * param category 分类筛选 * return 推荐内容列表 */ public ListHealthArticle recommendArticles(Long userId, String category) { // 1. 获取用户标签权重 MapString, Integer userTagWeight getUserTagWeight(userId); // 2. 获取候选文章列表 ListHealthArticle candidates articleMapper.selectPublished(category); // 3. 打推荐分 ListScoredArticle scoredList candidates.stream() .map(article - new ScoredArticle(article, calculateScore(article, userTagWeight))) .sorted((a, b) - Double.compare(b.getScore(), a.getScore())) .collect(Collectors.toList()); // 4. 截断返回前N条 return scoredList.stream() .limit(10) .map(ScoredArticle::getArticle) .collect(Collectors.toList()); } private double calculateScore(HealthArticle article, MapString, Integer userTagWeight) { double score 0.0; // 标签匹配得分 String articleTags article.getTags(); if (StringUtils.hasText(articleTags)) { for (String tag : articleTags.split(,)) { Integer weight userTagWeight.getOrDefault(tag.trim(), 0); score weight * 0.8; } } // 基础热度分 score Math.log(article.getViewCount() 1) * 0.2; // 时效分发布时间越近得分越高 score Math.max(0, 24 * 7 - Duration.between(article.getCreatedTime().toInstant(), Instant.now()).toHours()) * 0.01; return score; } private MapString, Integer getUserTagWeight(Long userId) { MapString, Integer weightMap new HashMap(); // 从用户健康档案标签初始化权重 User user userMapper.selectById(userId); if (user ! null StringUtils.hasText(user.getHealthTags())) { for (String tag : user.getHealthTags().split(,)) { weightMap.merge(tag.trim(), 5, Integer::sum); } } // 行为加分最近收藏的内容标签权重更高 ListString favoriteTags behaviorMapper.selectFavoriteTags(userId); for (String tag : favoriteTags) { weightMap.merge(tag, 10, Integer::sum); } return weightMap; } }评分公式我自己拆解一下标签匹配占最高权重行为偏好点赞收藏权重更大热度浏览量弱参与时间衰减做微调这样能保证推荐结果既符合用户画像又不至于全部推荐老内容。3.3 推荐系统的实时性与性能优化推荐接口有一个天然矛盾你要实时反映用户最新行为但实时计算代价又高。我的优化思路是两级缓存一级缓存本地内存 Caffeine每个用户推荐结果保存 5 分钟。用户点击了几篇文章、点了个赞5 分钟后推荐结果才变化对用户体验影响不大。二级缓存Redis用来存热门内容榜、标签对照表这类全局配置。如果项目资源紧张不引 Redis也可以用ConcurrentHashMap做单机缓存但生产建议还是上 Redis。再补充一个细节推荐列表里要过滤掉用户已经看过的文章不然用户会发现翻来覆去都是那几篇体验很不好。查文章列表时加一句AND id NOT IN (...最近浏览过的20篇...)就能解决。4. SpringBoot 后端核心功能实现4.1 RESTful API 设计与统一返回体封装前后端分离项目里接口设计规范比写接口本身更重要。我给整套系统定了统一约定{ code: 200, message: 操作成功, data: {} }所有接口的返回体都用这个格式包装前端在request.js封装里统一解析。后端对应定义一个通用类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(操作成功); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }接口路径统一加/api前缀比如模块接口路径请求方式说明用户/api/user/loginPOST登录用户/api/user/registerPOST注册用户/api/user/infoGET获取当前用户信息资讯/api/article/listGET分页获取资讯资讯/api/article/detail/{id}GET获取详情推荐/api/recommend/listGET获取个性化推荐行为/api/behavior/recordPOST记录用户行为管理/api/admin/article/savePOST资讯发布/编辑4.2 登录鉴权与拦截器配置有小部分同学做项目时习惯每个请求都传用户 ID完全没有鉴权概念。这套系统我加了基于 Token 的简单认证流程用户登录成功后后端生成一个 UUID 串作为令牌存 Redis 并设置过期时间返回给前端。前端每次请求带在请求头Authorization里后端通过拦截器统一校验。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); if (uri.startsWith(/api/auth) || uri.startsWith(/api/article)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } // 校验 token 并设置 userId 到 request attribute return true; } }这里要提醒放行的接口范围要严格审核。资讯列表虽然可以不登录看但管理端的接口绝不可以在拦截器里被放行。我一般会在管理员接口上额外加一套RequireAdmin注解配合 AOP 做权限二次校验防止普通用户直接调管理接口。4.3 CORS 跨域配置实操开发环境我们用 Vite 代理转发绕开了跨域但有时候前端想直接请求后端的 IP:端口调试就会触发跨域。我的建议是开发环境开启全局 CORS生产环境交给 Nginx 同源代理了后端不开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); } }4.4 MyBatis 分页查询与前端条数匹配分页我用的是手写 LIMIT 的简单方案因为项目数据量不大没必要引入 PageHelper尽量减少依赖。控制层接收pageNum和pageSize算出offset (pageNum - 1) * pageSize传给 Mapper。返回体里带上total总条数前端分页组件才能正确算出总页数public ResultPageResultHealthArticle pageQuery(String keyword, String category, Integer pageNum, Integer pageSize) { if (pageNum null || pageNum 1) pageNum 1; if (pageSize null || pageSize 1) pageSize 10; int offset (pageNum - 1) * pageSize; ListHealthArticle list articleMapper.selectPage(keyword, category, offset, pageSize); int total articleMapper.countPage(keyword, category); PageResultHealthArticle pageResult new PageResult(); pageResult.setList(list); pageResult.setTotal(total); pageResult.setPageNum(pageNum); pageResult.setPageSize(pageSize); return Result.success(pageResult); }5. Vue 前端核心页面与交互实现5.1 环境准备Vue 项目初始化和依赖安装前端我用 Vite 创建 Vue3 项目初始化命令就一行npm create vitelatest frontend -- --template vue cd frontend npm install主要依赖除了vue-router和pinia状态管理还需要 UI 组件库。这里有个小经验管理后台我推荐element-plus用户端如果追求更轻量可以用vant。装依赖时容易遇到版本冲突比如npm install报 ERESOLVE 错误多数是依赖树里存在 peer dependency 冲突用npm install --legacy-peer-deps能绕过去。5.2 前端路由与页面骨架搭建前端路由分两块面向普通用户的健康资讯首页、推荐列表页、个人中心面向管理员的资讯管理、用户管理、数据看板。我通过路由守卫判断登录状态和角色router.beforeEach((to, from, next) { const token localStorage.getItem(token); const role localStorage.getItem(role); if (to.meta.requiresAuth !token) { next(/login); return; } if (to.meta.requiresAdmin role ! 2) { next(/403); return; } next(); });页面骨架则是典型的Layout模式左侧菜单、顶栏、主内容区。用户端首页用卡片式布局展示推荐资讯每条卡片显示封面、标题、标签匹配度提示管理端则是表格加搜索加弹窗表单。5.3 axios 封装与请求拦截器细节前端请求我统一封装在utils/request.js里。这套封装基本是行业标准做法直接复用import axios from axios; import { ElMessage } from element-plus; import router from ../router; 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; }); // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); if (res.code 401) { router.push(/login); } return Promise.reject(new Error(res.message)); } return res; }, error { ElMessage.error(error.message || 网络异常); return Promise.reject(error); } ); export default request;一个容易被忽略的细节开发环境baseURL是/api依赖的是vite.config.js里的代理生产环境baseURL仍然是/api由 Nginx 把/api开头的请求转发到 SpringBoot 服务。两套环境前端代码完全不用改只要后端接口路径都统一带/api前缀。5.4 用户行为上报点击和浏览记录怎么埋点推荐系统的效果依赖用户行为数据。前端在前台资讯列表页和详情页里做了“埋点”。在 Vue 组件里监听用户点击和组件挂载事件调用上报接口template div classarticle-card clickhandleClick(article) img :srcarticle.coverImage alt封面 h3{{ article.title }}/h3 p{{ article.tags }}/p /div /template script setup import request from ../utils/request; function handleClick(article) { request.post(/behavior/record, { articleId: article.id, behaviorType: 1 }); } /script然后详情页组件onMounted里再上报一次浏览行为后端拿到行为数据后写入user_behavior表推荐模块就有了“新鲜素材”。开发的时候可以在浏览器控制台 Network 面板里确认这些请求是否正常发出这是排查推荐不生效最直接的手段。6. 部署上线全流程从本地到云服务器的完整实操6.1 后端打包与运行环境配置后端部署第一步是打包。在backend目录下执行mvn clean package -DskipTests打包完的 jar 在target目录下。小内存服务器上启动时一定要限制 JVM 堆内存否则 2G 内存的服务器光跑个 jar 就可能内存溢出java -Xms256m -Xmx512m -jar health-recommend-system.jar \ --spring.profiles.activeprod生产环境的application-prod.yml要改几个关键配置数据库地址改成云服务器地址Redis 地址内网化日志级别调成 INFO去除 MyBatis SQL 打印。6.2 Nginx 配置前后端联调与反向代理部署阶段有个经典误区只把前端dist和后端 jar 分别起了结果前端访问时浏览器上报跨域错误。正确的姿势是用 Nginx 做同源代理。前端先构建静态资源npm run build然后把dist目录里的文件全部上传到服务器的/usr/share/nginx/html目录下。Nginx 配置如下server { listen 80; server_name your-domain-or-ip; # 静态资源前端 Vue 打包产物 location / { root /usr/share/nginx/html; 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传的图片等静态资源 location /uploads/ { alias /data/uploads/; } }try_files那行是 SPA 路由模式的关键Vue Router 默认用的是 history 模式如果你不配这行刷新页面时 Nginx 找不到对应的真实文件直接返回 404。6.3 HTTPS 配置与常见部署排错现在做线上系统HTTPS 几乎是标配。自己用云服务器的话建议申请免费 SSL 证书续期配置好然后 Nginx 加一个 443 的 server 块server { listen 443 ssl; server_name your-domain; ssl_certificate /etc/nginx/ssl/your-domain.pem; ssl_certificate_key /etc/nginx/ssl/your-domain.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # 其余配置和 80 的 server 块一致 }部署阶段最容易遇到的几个报错我按排查优先级列一下接口能通但页面 404基本是try_files没配置。页面能打开接口 502后端服务没启动或端口不对检查 jar 进程和 Nginxproxy_pass目标地址。接口都是 401token 失效或未传检查浏览器 Application 面板里的 localStorage。图片加载 404检查location /uploads/的 alias 路径是否和代码里保存的文件路径一致。6.4 云服务器部署注意事项我用过的云服务器规格参考1核2G 能跑但偏紧推荐 2核4G带宽看并发量。一点真实经验如果你买了云服务器一定要在安全组/防火墙里同时放行 80、443 和 8080如果后端不经过 Nginx 直接暴露。很多人本地跑通了但外网死活访问不了十有八九是安全组没放行端口。7. 系统压测与排查技巧实录7.1 常见问题速查表现象可能原因解决方案前端 npm run dev 启动失败Node 版本太低或依赖冲突升级 Node 到 16或npm install --legacy-peer-deps启动后端报数据库连接失败MySQL 未启动/密码错误/权限不足检查 MySQL 服务状态确认application.yml连接串MyBatis 报 Invalid bound statementMapper XML 未被打包或 namespace 不正确检查pom.xml是否配置了 resources 包含mapper/*.xml前后端联调跨域后端 CORS 未开或代理配置错误开发环境用 Vite proxy 或后端加 CORS 配置推荐列表跟热门列表一样用户行为数据为空/标签未匹配先检查user_behavior表是否有数据Linux 起 jar 很快被杀内存不足被 OOM Killer 干掉加-Xmx512m限制堆内存或升级内存刷新页面 404SPA 路由直接访问 Nginx 找不到文件加try_files $uri $uri/ /index.html;接口中文乱码数据库连接编码不是 UTF-8JDBC 连接串加characterEncodingutf87.2 MyBatis SQL 排查技巧日志与调试我调试 MyBatis SQL 时最常用两个方法一是打印 SQL 日志二是断点看参数。打印 SQL 的配置很简单logging: level: com.health.recommend.mapper: debug这样 MyBatis 会把执行前的 SQL、参数、结果数量全部打印出来。要是数据量一大控制台会被日志刷屏生产环境一定要关掉。另一个实用技巧是 IDEA 里装 MyBatis Log 插件它能把PreparedStatement的?占位符替换成真实参数值直接复制出来丢到 Navicat 里就能跑排查问题非常方便。7.3 印象最深的一个线上 bug推荐结果全空白调试这套系统时我印象最深的 bug是某天部署完测试推荐接口发现返回的data始终是空数组。我一个个排查流程先看数据库health_article表确认有 20 多条已上架的资讯。再看接口入参用户 ID 传了 1行为数据也有。最后打断点进calculateScore发现根本走不进这个方法——候选人列表为空。原因最后落在了 mapper 的查询上selectPublished(category)里的status 1条件没有问题但 MyBatis XML 里我用了if testcategory ! null and category ! 前端传的 category 是空字符串这个判断可以走进去导致 SQL 变成了AND category 自然一条都查不出来。这个坑特别典型。解决方式很简单前端传null而不是空字符串或者后端对空串做二次处理。从那以后我写代码养成了一个习惯任何外部传入的查询条件下发到数据库前都要判断 null 和空串。8. 项目能力延伸这套系统还能怎么玩做完这套项目如果时间和精力允许我强烈建议你在下面几个方向做点延伸无论是做简历项目还是实际交付都加分不少引入 Redis 做真正的三级缓存。现在系统缓存在本地内存里单机部署没问题但一旦要做集群部署本地缓存就成了瓶颈。提前把 Redis 的缓存策略设计好是分布式架构的合理铺垫。引入定时任务做行为离线统计。在真实业务里推荐结果不用每次都实时算。你可以用 SpringBoot 自带的Scheduled定时每一小时把热门文章、用户标签权重刷新到缓存表推荐接口直接读缓存响应时间能压到 100ms 以内。给系统加一个 WebSocket 健康客服模块。卫生健康系统免不了用户咨询场景。前端用原生 WebSocket 建立连接后端用一个ChatEndpoint做实时消息转发实现用户与医生的在线沟通。这个功能一加系统完整度直接上一个层次。补充数据可视化大屏。管理端用 ECharts 做健康资讯阅读趋势、用户增长、热门内容 Top10 的可视化展示对这类系统的“管理”属性是很大的强化。好多人一听到可视化就犯怵其实 ECharts 专门有一个 Vue 集成版本按官方例子写一天就能出来。这部分内容做好之后你的简历上写的就不仅是“我做一个 CRUD 系统”而是“打造了一个具备个性化推荐能力的完整业务闭环”面试官拿到这个项目聊资直接就丰富了。最后分享个我自己做项目的小习惯代码写完后我会把sql初始化脚本、docs里的部署文档、And 前后端的 README 全部对齐一遍确保在一个全新的环境里按文档操作能一次跑通。很多人觉得这是浪费时间但真正的工程能力恰恰就体现在“别人能不能跟着你的文档把项目跑起来”这件事上。这套系统本身是一个很好的全栈练手项目你把它吃透前后端分离开发模式、推荐系统基础、MyBatis 实战、Linux 部署这些硬技能基本都覆盖到了。找个周末从建库建表开始一步步把代码敲出来、把项目部署上线你会发现这些东西比看一百篇教程都管用。