news 2026/10/2 9:38:40

SpringBoot+Vue协同过滤化妆品推荐系统搭建实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue协同过滤化妆品推荐系统搭建实战

简介:这是一套基于SpringBoot+Vue构建的化妆品推荐系统完整源代码与数据库资源,采用前后端分离架构,并融入基于用户的协同过滤推荐算法,面向需要完成课程设计、毕业设计或系统学习Java全栈项目的开发者。系统内置买家、卖家、管理员三种角色,功能覆盖商品检索、购物车结算、订单流转、收藏评价、公告管理、多维度销售统计以及个性化推荐等,业务链路完整。资源包共1468个文件,包含81个Java后端源码、111个XML配置、前端Vue/JS/CSS/HTML页面文件、1个SQL数据库脚本以及大量图片素材,压缩包大小68.17MB,目录结构清晰便于按模块查阅。目前已有23人学习,资料还附带运行环境说明与项目介绍文档,能够帮助使用者快速理解协同过滤推荐逻辑和前后端分离项目的实际落地方式,具备较高的参考与复用价值。

1. 一个化妆品推荐系统,为什么值得用 SpringBoot+Vue 从零搭一遍

做过电商类管理系统的人都有同感:用户端、商户端、管理端三个后台叠在一起,项目规模翻倍是常态,推荐算法反而成了最容易被砍的部分。这个项目把前后端分离的 SpringBoot+Vue 骨架与协同过滤推荐引擎放进同一个仓库,你拿到手看到的不是一堆列表页和 CRUD,而是一套推荐系统在真实业务里的完整运转过程——用户行为如何落库、如何换算成评分矩阵、多商户数据如何隔离、管理员如何管控全局。适合正在做毕业设计的人,也适合想搞懂协同过滤怎么在业务里落地的前后端工程师。下文从架构、实现、部署、排错四个层面讲清楚,目标只有一个:让这套代码在你自己的机器上稳定跑起来,并知道它为什么这样设计。

2. 系统架构与三个角色:用户端、多商户、管理员的边界如何切分

先给结论:这类系统的复杂度不在单个接口,而在于“同一套商品数据被三种身份同时操作”时如何不串台。用户端负责浏览、加购、下单、评分,商户端负责自己店铺的商品上下架、改价、发货,管理员端负责审核商户入驻、审核商品、全局统计。三个端的页面组织方式也明显不同:用户端是瀑布流商品页,商户端是表格套表单,管理员端是列表套审核弹窗。这些差异决定了前后端分离在这里不是炫技,是刚需——三个前端工程可以独立打包、独立部署,后端只需对同一组 REST 接口负责,互不干扰。

2.1 三个角色的功能边界与权限模型

第一步是把权限模型画清楚,后面所有代码都绕着它转。这个项目里三个角色共用一张用户表,用 role 字段区分,而不是各自建表。我第一次做多角色系统时为省事建了三张表,后面维护登录状态时吃了大亏——改一次密码要同步三张表,还得防着某张表漏更新。共用一张表、用 role 字段区分、配合拦截器做接口级权限控制,是这类系统最常见的做法,也是这个项目采用的方式。

角色核心操作可见数据范围典型页面形态
用户浏览商品、评分、下单、查看推荐全部上架商品商品瀑布流 + 购物车 + 推荐列表
商户商品管理、订单发货、店铺数据统计仅本店数据表格 + 表单 + 简易图表
管理员商户入驻审核、商品审核、全局统计全库数据列表 + 审核弹窗

权限控制的可执行方案是接口前缀区分加后端拦截器校验。用户端接口走/api/user/**,商户端接口走/api/merchant/**,管理员接口走/api/admin/**。拦截器从 JWT 里取出角色字段,按路径前缀比对,不匹配直接返回 403。这里有一条重要经验:权限不能只在前端菜单和路由里做,前端拦截只是用户体验层面的,接口层不做校验的话,别人用 Postman 直接请求商户接口就能绕过页面逻辑,这在多商户系统里属于致命漏洞。

2.2 为什么选协同过滤 + 前后端分离,而不是堆一套 AI 推荐

化妆品这个品类有几个特性,决定了协同过滤是性价比最高的起步方案。第一,用户行为高度同质化:油皮用户会反复点进控油类产品,干皮用户会持续搜索保湿类,所谓“物以类聚、人以群分”在这个品类上表现得非常明显,这正是基于用户的协同过滤最擅长捕捉的信号。第二,商品库相对稳定,不像资讯或短视频那样分钟级刷新,不需要实时流式计算,离线算好一张推荐结果表就够当天使用。第三,启动成本低,不需要训练样本、不需要 GPU,一张 MySQL 表加一段 Java 计算逻辑就能跑起来。

那为什么还要强调前后端分离?因为三个端口的 UI 差异太大。用户端是 C 端体验,讲究交互流畅度;商户端是 B 端工具,表格密度越高越好;管理员端是平台后台,审核操作要尽量少点击。如果这三个端全部用 JSP 模板渲染,光条件判断和公共组件的复用就能把代码拖垮。Vue 在这类表格、表单密集的场景下生态最成熟,Element UI 的表格组件、表单校验、弹窗确认基本覆盖三个端 80% 的页面需求。这也是这个项目选 Vue 而不是 React 的核心理由,不是技术优劣问题,是团队效率问题。

当然要划清边界。协同过滤不是万能的。如果目标是做一个新用户占比超过七成、商品每天大量上新的产品,纯协同过滤的冷启动问题会非常致命。这个项目在代码里做了热门推荐兜底,解决了冷启动的基本盘,第四章会展开讲。

2.3 数据库整体设计:八张核心表如何撑起三个端

数据库是这个项目的核心资产。标题里带了“数据库”,说明它不只是一堆建表语句,而是带初始化数据、能直接运行的整体。核心表用八张概括:用户表、商户表、商品表、订单主表、订单明细表、评分表、收藏表、管理员操作日志表。

表与表的关系比单表结构更值得关注:

  • 商品表带merchant_id外键,这是多商户数据隔离的基础,所有商户端查询都必须带这个条件;
  • 评分表带user_id和product_id,这是协同过滤引擎的直接数据源;
  • 订单主表带merchant_id,明细表记录商品快照(名称、单价、数量),商家后续改价不影响历史订单还原;
  • 商户表和管理员表分开建,因为商户有店铺名称、入驻状态、结算账号等字段,和系统管理员的属性差异太大,强扭在一张表里只会制造一堆空字段。

字段类型有一条血泪经验:价格字段绝不用 float 或 double,用 Decimal 或直接存整数单位(分)。化妆品订单的满减、折扣、退差价操作非常多,浮点数误差在财务场景里是真实踩过的坑——有一次对账差一分钱查了半天,最后发现是前端传了个 199.9,后端用 double 接收后变成了 199.899999,这类问题在订单和结算场景里几乎每周都在发生。固定用 Decimal(10,2) 或整型存分,能省掉百分之八十的对账麻烦。

3. SpringBoot+Vue 前后端分离骨架:从建库到第一组接口跑通

这一章的目标是让你把项目跑起来,并且理解每一条配置在做什么。别急着写业务代码,先把地基打牢。前后端分离项目最容易翻车的地方集中在三块:数据库连接配置、跨域处理、JWT 拦截器顺序。这三块只要有一块不对,前端能启动、后端能启动,但页面就是调不通接口,你会反复陷入“明明代码没错”的错觉里。

3.1 数据库初始化脚本:核心表结构与初始化数据

先建用户表和商品表。这是整个系统最先落地的两张表,其他表都依赖它们的外键。下面是核心建表 SQL,省略了部分次要字段以保持可读性:

CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(64) NOT NULL COMMENT '登录名', `password` VARCHAR(128) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` VARCHAR(64) DEFAULT NULL COMMENT '昵称', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0用户 1商户 2管理员', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `product` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品ID', `merchant_id` BIGINT NOT NULL COMMENT '所属商户ID', `name` VARCHAR(128) NOT NULL COMMENT '商品名称', `category` VARCHAR(64) DEFAULT NULL COMMENT '品类,如洁面/面霜/口红', `price` DECIMAL(10,2) NOT NULL COMMENT '售价', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架 2待审核', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_merchant` (`merchant_id`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

登录密码建议用 BCrypt 加密存储,不要用 MD5。MD5 在现在算力下几乎等于明文,这个项目如果对外展示的话,密码加密方式会被别人直接看代码,用 BCrypt 是专业性的底线。price用DECIMAL(10,2),就是第二章说的浮点数教训的落地。

初始化数据的思路是:每张表给两三行真实样例,评分表至少给 15 条以上的评分记录,保证协同过滤算法在启动后能算出有区分度的相似度。如果初始化数据太少,比如只有三个用户各评了一条,那你调试推荐接口时会发现所有相似度都是 0,这不是代码问题,是数据问题。

3.2 SpringBoot 配置与第一个 REST 接口

核心配置文件用 YAML 格式。这里给出application.yml的关键片段:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cosmetics_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 5000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

map-underscore-to-camel-case睁开后,数据库的merchant_id自动映射到 Java 字段的merchantId,少写大量映射代码。log-impl换成 StdOut 是为了开发阶段直接看 SQL,上线前记得换成 Slf4j 或直接关掉,否则控制台会被 SQL 刷屏。HikariCP 的连接池参数初期不用调太高,20 个连接足够撑起日常开发,后面第五章会讲到并发情况下的调参。

第一个接口建议做商品列表,因为不涉及登录态,排查问题时干扰最小:

@RestController @RequestMapping("/api/product") public class ProductController { @Resource private ProductMapper productMapper; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String category) { Page<Product> p = new Page<>(page, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getStatus, 1) .eq(StringUtils.hasText(category), Product::getCategory, category) .orderByDesc(Product::getCreateTime); productMapper.selectPage(p, wrapper); return Result.ok(p); } }

这段代码用了 MyBatis-Plus 的分页插件,Page对象封装了当前页和每页条数,LambdaQueryWrapper避免硬编码数据库字段名。注意eq方法的第一个参数是 boolean 条件,StringUtils.hasText(category)为 true 时才会拼接分类过滤条件,这是处理可选查询参数的标准姿势,避免了 if 嵌套地狱。

3.3 Vue 前端搭建、路由与 Axios 封装

前端用 Vue 3 + Vite 起步最快。装完依赖后的第一件事是封装 Axios 实例,把所有接口请求统一走一个入口,后面做拦截器、错误处理都只需要改一个文件:

import axios from 'axios' 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 }) // 响应拦截:统一处理业务码和401 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

这个封装的要点有两个。第一,请求拦截器统一加Authorization头,每个接口都不需要单独传 token。第二,响应拦截器统一处理code !== 200的情况,页面里不用到处写错误提示。这里调了一个坑:后端返回的 401 可能来自 JWT 过期,也可能来自跨域预检失败,所以前端要在拦截器里做好区分,只对真正的 401 做登出处理。

Vue Router 配置时,三个角色建议拆成三个路由模块文件:user-routes.js、merchant-routes.js、admin-routes.js,然后在主路由里按角色动态合并。不要把所有路由写在同一个文件里,这个项目角色多、页面多,路由文件会膨胀到让人不想维护。

3.4 JWT 登录与跨域联调:一次成功请求的完整链路

登录接口的逻辑不复杂,但它是前后端联调的第一个关卡。后端生成 JWT、前端存储 JWT、后续请求带 JWT,这条链路只要断一环,你看到的就是 401 满天飞。关键在于跨域配置和拦截器顺序。

SpringBoot 的跨域配置有两种常见方式。一种是实现WebMvcConfigurer:

@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); } }

另一个方式是使用@CrossOrigin注解加在 Controller 类上,但一是每个 Controller 都要加一遍,二是多商户系统接口数量多,容易漏。全局配置一次搞定,省心。

关键点在于拦截器注册时要放行 OPTIONS 请求。浏览器跨域请求会先发一个 OPTIONS 预检,如果拦截器把 OPTIONS 拦下来返回 401,前端就会报跨域错误,而你还以为是 CORS 没配置对。这是前后端分离项目最著名的迷之报错之一。拦截器里的处理很直接:

if ("OPTIONS".equals(request.getMethod())) { return true; }

放行预检后再校验 JWT,这就是正确顺序。我见过不少项目把拦截器顺序写反了,结果前端崩溃日志全是 CORS error,调了一天才发现是拦截器把预检请求吞了,这种“表面跨域、实际拦截器作怪”的情况在前后端分离项目里非常常见。

4. 协同过滤推荐引擎:评分矩阵构建、余弦相似度与 Top-N 推荐

进入这个项目真正的核心。前面所有用户、商品、订单模块都是为了给推荐引擎喂数据。协同过滤在这套系统里选了基于用户的 UserCF 而不是基于物品的 ItemCF,原因是化妆品消费的“随人走”属性更强——用户换肤质、换季节、换品牌偏好,都会让评分矩阵发生变化。UserCF 能更快捕捉到用户群体行为的变化。这里给一个硬指标:评分数据量低于 100 条时,协同过滤的结果和随机推荐差别不大,所以初始化数据里评分表一定要给足。

4.1 评分矩阵从哪来:显式评分与隐式行为换算

推荐引擎不认“用户点过哪件商品”这种原始记录,它需要的是一个二维矩阵:行是用户,列是商品,交叉点是分数。这个项目里分数有两个来源。第一个来源是显式评分,用户在商品页直接打 1 到 5 分,存入评分表。第二个来源是隐式行为换算:用户下单、收藏、浏览时长都会转换成隐含分数,比如收藏记 3 分、下单记 5 分、浏览超过 30 秒记 1 分。隐式数据的价值在于覆盖没有主动评分的用户,让矩阵不至于太稀疏。

注意,计算推荐结果时不要把所有行为简单相加覆盖,否则同一个用户收藏又下单的商品会重复计算。常见做法是把行为映射成固定分数,取最大值而不是求和。比如某用户收藏了一件商品(3 分)又下单了(4 分),最终矩阵里的分数取 4 而不是 7,这样分数语义才能保持稳定。

从数据库加载评分矩阵的示例代码:

// 读取全部评分记录,构造 userId -> productId -> score 映射 List<Rating> ratings = ratingMapper.selectList(null); Map<Long, Map<Long, Double>> matrix = new HashMap<>(); for (Rating r : ratings) { matrix.computeIfAbsent(r.getUserId(), k -> new HashMap<>()) .put(r.getProductId(), r.getScore().doubleValue()); }

computeIfAbsent处理了用户首次出现时的初始化,避免手写 if 判断。最终得到的matrix是后续所有相似度计算的原料。这部分逻辑无关数据库表结构,只依赖 user_id、product_id、score 三列数据,所以将来换数据源也不影响算法实现。

4.2 基于用户的协同过滤核心代码与关键参数

UserCF 的核心步骤三句话能说清:找相似用户、收集相似用户的物品、按加权分数取 Top-N。核心计算在内存里完成,因为单机 Mono 场景数据量不会太大,不需要引入 Spark 这类重组件。先看完整代码:

public List<Long> recommend(Long userId, int topN, int similarUserK) { Map<Long, Map<Long, Double>> matrix = loadRatingMatrix(); Map<Long, Double> currentUserRatings = matrix.getOrDefault(userId, Collections.emptyMap()); if (currentUserRatings.isEmpty()) { return hotProducts(topN); // 冷启动兜底 } // 1. 计算当前用户与其他用户的余弦相似度 List<SimUser> simUsers = new ArrayList<>(); for (Map.Entry<Long, Map<Long, Double>> entry : matrix.entrySet()) { Long otherUserId = entry.getKey(); if (otherUserId.equals(userId)) { continue; } double sim = cosineSimilarity(currentUserRatings, entry.getValue()); if (sim > 0.01) { simUsers.add(new SimUser(otherUserId, sim)); } } // 2. 取相似度最高的K个用户 simUsers.sort((a, b) -> Double.compare(b.sim, a.sim)); List<SimUser> topK = simUsers.subList(0, Math.min(similarUserK, simUsers.size())); // 3. 聚合K个用户评分过的商品,加权求和 Map<Long, Double> scoreMap = new HashMap<>(); for (SimUser su : topK) { Map<Long, Double> ratings = matrix.get(su.userId); for (Map.Entry<Long, Double> p : ratings.entrySet()) { if (currentUserRatings.containsKey(p.getKey())) { continue; // 已购买/已评分的商品不再推荐 } scoreMap.merge(p.getKey(), p.getValue() * su.sim, Double::sum); } } // 4. 按加权分排序取TopN List<Map.Entry<Long, Double>> sorted = new ArrayList<>(scoreMap.entrySet()); sorted.sort((a, b) -> Double.compare(b.getValue(), a.getValue())); return sorted.stream() .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } private double cosineSimilarity(Map<Long, Double> a, Map<Long, Double> b) { double dot = 0, normA = 0, normB = 0; for (double v : a.values()) { normA += v * v; } for (double v : b.values()) { normB += v * v; } if (normA == 0 || normB == 0) { return 0; } for (Map.Entry<Long, Double> e : a.entrySet()) { Double vb = b.get(e.getKey()); if (vb != null) { dot += e.getValue() * vb; } } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); }

参数上,最关键的三个是topN、similarUserK和相似度过滤阈值。topN是最终给用户展示的推荐数量,这个项目取 10 比较合适,少了显得空,多了用户刷不到底。similarUserK取 20 到 30 之间,取值越大,推荐结果越偏向热门商品,越小越精准但偶然性也越大。代码里sim > 0.01的过滤阈值是经验值,低于这个值的用户关系太弱,带进计算只会引入噪声。

这个实现还有两个改进点,如果要用在生产环境值得补上:第一,相似度矩阵可以缓存起来,不用每次请求都全量重算;第二,currentUserRatings为空时直接走hotProducts兜底,是典型冷启动处理。

4.3 冷启动的兜底方案:热门推荐与规则补偿

冷启动是协同过滤最著名的硬伤,在这个项目里表现为“新用户没有任何评分记录,矩阵里根本找不到他的相似用户”。解决思路不是让算法强行算,而是给一套分层的兜底策略。第一层是热门推荐,统计全站销量和收藏数加权排序,给新用户推热门商品。这是最简单也最可靠的兜底。第二层是品类补偿,新用户注册时可以让他选肤质偏好,后端把这些偏好映射成品类条件,从商品表里按品类筛选。第三层逐步过渡,用户产生了三到五条评分后,协同过滤开始生效。

热门推荐的代码一点都不复杂:

public List<Long> hotProducts(int topN) { return productMapper.selectList( new LambdaQueryWrapper<Product>() .select(Product::getId, Product::getSales) .eq(Product::getStatus, 1) .orderByDesc(Product::getSales) .last("limit " + topN) ).stream().map(Product::getId).collect(Collectors.toList()); }

这里orderByDesc(Product::getSales)按销量倒排,last("limit " + topN)是 MyBatis-Plus 拼 SQL 尾部语句的常见做法。要注意last方法是直接拼接字符串的,topN必须是服务端算好的整数,绝对不能接收前端传参后直接拼进去,否则就是 SQL 注入风险。这个兜底方案的效果决定了冷启动用户的第一印象,看起来朴素,但比那些一上来就空推荐列表的方案强得多。

5. 多商户、权限与推荐落地的 5 个排查记录

这一章是前面所有章节运行后最常遇到的坑,逐条按“现象→原因→解决”拆开写。这些场景我在类似项目里都真实遇到过,有些坑甚至花了整天时间来定位,希望这些记录能帮你少走弯路。

5.1 现象:商户 A 的商品出现在商户 B 的商品列表里

多商户系统最致命的问题。现象是商户 A 登录后台后,看到自己的商品列表里混着商户 B 的商品,甚至能操作发货。原因九成出在 SQL 查询时没有带merchant_id条件,或者在拼接动态查询时把商户 ID 条件写丢了。还有一种隐蔽情况:商品表 JOIN 了商户表后,两个表都有id字段,select *把混淆的 id 映射到了错误的实体属性上。

解决这个问题要从两个层面下手。代码层面,商品 Mapper 的每个查询方法都要强制带上merchant_id,并且把该参数设为必填,不要用if (merchantId != null)来可选拼接。更稳妥的做法是使用 MyBatis-Plus 的拦截器做数据权限自动注入,在框架层强制给每个商户查询追加数据范围条件,这样就算开发人员忘了写条件,拦截器也会把商户 ID 拼进 SQL。

5.2 现象:所有用户的推荐结果都相同,且与热门推荐一模一样

这个问题出现在推荐模块联调时。现象是登录不同用户账号,推荐返回的商品列表完全一致,和热门推荐接口的结果也相同。原因有两个可能:一是评分表数据量太少,余弦相似度计算全部为 0,导致推荐代码走了hotProducts兜底;二是loadRatingMatrix方法里 SQL 条件写错,比如只查到了某一个用户的评分。

解决方法是先检查评分表数据量,低于 20 条就先把数据补够再调算法。其次检查矩阵加载逻辑,在loadRatingMatrix里加日志输出用户数和评分记录数,如果矩阵里只有一个用户,说明查询条件确实写错了。这里给一个参考标准:想让协同过滤推荐出有个人差异的结果,评分数据至少要有 50 个用户各评 10 件商品,否则无论算法多精妙都是空转。

5.3 现象:前端请求接口报 CORS 错误,但后端接口用 Postman 调用完全正常

这是前后端分离项目最容易迷惑人的问题。Postman 不发预检请求,所以后端接口正常;浏览器跨域会先发 OPTIONS 预检,只要预检被拦截器拦截或配置遗漏,前端看到的全是 CORS 错误。原因就是第二章提过的拦截器没有放行 OPTIONS。

解决分两步。第一步,确认 CORS 配置存在且生效,用浏览器开发者工具查看预检请求的响应状态码,如果返回 401 或 403,八成是拦截器问题。第二步,在拦截器 preHandle 里放行 OPTIONS 请求并直接返回 true,同时确认过滤器链里没有其他过滤器在更早位置拒绝了预检请求。这个现象还有一个变种:某些拦截器框架的过滤顺序是先执行自定义过滤器再执行 Spring CORS,顺序反了照样报错。

5.4 现象:本地启动正常,用 Tomcat 部署后访问根路径 404

本地开发用npm run dev起前端时一切正常,但部署到生产环境后,访问域名根路径就 404。原因有两种常见:一种是前端没有构建或构建产物放错了位置,另一种是 Vue Router 用了 history 模式,刷新非根路径时 Tomcat 找不到对应的物理文件。

解决方法是构建前端后把dist目录部署到 Nginx 或 Tomcat 的静态资源目录,配好 try_files 规则让所有非静态资源请求都回退到 index.html。如果用 Tomcat,需要在 web.xml 里配置错误页跳转或使用 rewrite 规则。这里给一个经验:生产环境的前后端分离部署,最省心的方案是 Nginx 做前端静态资源服务和反向代理,遇到非/api开头的请求全部回退到 index.html,遇到/api开头的请求转发到后端服务。Tomcat 只负责跑 SpringBoot jar 包,静态资源交给 Nginx,各司其职。

5.5 现象:压测或多人同时使用后,数据库连接池耗尽,系统假死

现象是系统运行一段时间后,接口响应突然全部变慢,最后直接无响应,控制台报错Connection is not available, request timed out after 5000ms。原因基本是连接池被占满后无法回收。常见诱发因素是两个:一是接口里查询数据库后没有关闭连接(用 MyBatis 时通常会自动管理,但手写 JdbcTemplate 时容易漏),二是慢查询把连接长期占住不放。

解决方法是先调 HikariCP 参数,maximum-pool-size从默认的 10 调到 20,connection-timeout保持 5 秒不要动,同时把leak-detection-threshold设为 60000 毫秒,超过 60 秒未归还的连接会在日志里输出告警。还要排查慢 SQL,在 MyBatis-Plus 里配置slow-sql-millis超过 2 秒的 SQL 打日志。先定位是慢查询还是连接泄漏,再决定是加索引还是改代码。盲目加大连接池上限治标不治本,数据库负载反而会更高。

6. 推荐质量怎么验证:一次离线评估和两条后续路线

推荐系统上线后,最怕听到一句话:“这个推荐好像没什么用。”判断有没有用的标准不能靠感觉,要给一套可复现的验证方法。最常见的做法是离线评估:把评分数据集按照 8:2 切成训练集和测试集,训练集跑协同过滤生成推荐,测试集检验命中率,计算精确率和召回率。

精确率的定义是“推荐列表里有百分之多少被用户真实评分过”,召回率是“测试集里用户评过的商品中有百分之多少被推荐出来”。代码如下:

public void evaluate() { List<Rating> ratings = ratingMapper.selectList(null); Collections.shuffle(ratings); int splitIndex = (int) (ratings.size() * 0.8); List<Rating> train = ratings.subList(0, splitIndex); List<Rating> test = ratings.subList(splitIndex, ratings.size()); // 用train构建矩阵并生成推荐... int hitCount = 0; int testCount = test.size(); for (Rating r : test) { List<Long> recommends = recommend(r.getUserId(), 10, 20); if (recommends.contains(r.getProductId())) { hitCount++; } } double precision = (double) hitCount / (train.size() + test.size()); double recall = (double) hitCount / testCount; System.out.println("精确率: " + precision + ", 召回率: " + recall); }

这个脚本是你调整算法参数的指挥棒。把similarUserK从 10 调到 30,观察精确率和召回率的变化,找到最合适的区间。评估后如果发现效果不理想,两条后续路线值得投入:一条是引入用户画像和商品属性,把协同过滤替换成混合推荐;另一条是把推荐结果从静态表改为实时计算,用缓存框架加速相似度计算。走到这一步,你就不再是“调接口的人”,而是真正在优化一个推荐系统。

做这个项目给我最深的教训是:推荐系统的代码量最少,但最需要耐心。用户管理、订单管理都是确定性逻辑,写完就能跑;推荐算法的效果需要你一遍遍调参、评估、试错。这套系统本身也依赖原始评分数据的质量,数据没养起来之前不要太早判断算法好坏。希望这篇文章能帮你把项目跑起来,也让这些踩坑记录成为你的后悔药——少熬几个通宵,把时间花在正经调优上,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:38:25

Modal平台技术解析:云原生FaaS与Python分布式计算实践

我无法根据您提供的输入内容生成符合要求的博文。原因如下&#xff1a;输入中仅包含一个新闻标题&#xff1a;“Modal Labs 接近以 157.5 亿美元估值完成 7.5 亿美元融资”&#xff0c;以及空置的“相关热搜词”“最新网络热词”和完全空白的“基于标题及热词网络搜索的内容”区…

作者头像 李华
网站建设 2026/10/2 9:38:19

数据中心耗水之谜:冷却塔蒸发、WUE与节水实践全解析

很多人第一次听到“数据中心耗水”这个概念时&#xff0c;脑子里冒出来的问题是&#xff1a;机房不是耗电大户吗&#xff1f;服务器又不喝水&#xff0c;水到底用在哪了&#xff1f;其实机房里真正“喝水”的从来不是IT设备&#xff0c;而是给IT设备散热的那套冷却系统。服务器…

作者头像 李华
网站建设 2026/10/2 9:38:07

从设计文档到可运行系统:学籍管理系统的数据建模与权限设计实战

简介&#xff1a;这是一份供教务管理人员、软件开发人员及数据库课程学习者参考的学生学籍管理系统设计与实现文档&#xff0c;针对手工管理学生信息效率低、查询不便的问题&#xff0c;给出了基于SQL Server 2005的数据库应用系统整体方案。文档按需求分析、概念结构设计、逻辑…

作者头像 李华
网站建设 2026/10/2 9:38:01

Starlink第三代终端 vs 第二代:硬件差异与实测体验全解析

如果你正在关注Starlink的终端迭代&#xff0c;或者手头正好有二代、三代设备在对比选型&#xff0c;那这篇文章应该能帮你省下不少翻资料的时间。我接触Starlink终端不算短&#xff0c;从第一代圆盘、第二代矩形天线到第三代&#xff0c;基本每一代都上手摸过。今天专门把第三…

作者头像 李华
网站建设 2026/10/2 9:38:00

LeetCode 108:有序数组转平衡二叉搜索树的递归实现与边界分析

最近刷题时碰到不少人问LeetCode 108这道"将有序数组转换为二叉搜索树"&#xff0c;第一眼都觉得简单&#xff0c;无非就是二分、递归、取中间值。但真正自己写出边界正确、空间合理、经得起面试官追问的代码&#xff0c;里面还是有不少讲究。这篇文章就当成一份刷题…

作者头像 李华
网站建设 2026/10/2 9:37:43

反射内存卡RFM2g驱动安装完全指南:原理、步骤与排错

第一次被反射内存卡折腾到凌晨两点&#xff0c;不是因为硬件坏了&#xff0c;而是rfm2g的驱动怎么都装不上——设备管理器里一个黄色感叹号&#xff0c;Linux下insmod之后dmesg一堆报错。后来把原理吃透了才发现&#xff0c;这类卡和普通网卡、USB转串口完全不是一回事&#xf…

作者头像 李华