
简介这是一套面向Java全栈开发学习者与毕业设计实践者的鲜花电商推荐系统完整工程基于SpringBootVue实现前后端分离架构并集成协同过滤用户相似度算法提升个性化推荐能力。资源覆盖买家、多商户及后台管理员三类角色完整支撑鲜花商城核心业务闭环适用于课程设计、毕设开发或推荐算法落地实践。压缩包共818个文件含81个Java后端逻辑文件、84个JavaScript前端交互脚本、39个CSS样式文件含Layui、icomoon等UI组件、111个XML配置文件及1个SQL建库脚本整体大小为9.48MB结构清晰、模块划分明确。已有41人学习下载提供可直接运行的源码、数据库脚本、多角色权限控制逻辑及日历控件、轮播图、购物车、订单全流程等真实电商功能模块开箱即用便于理解推荐系统与电商系统融合实现的关键路径。1. 项目概述与方案选型1.1 这个项目到底解决什么问题我前前后后带过不少学生和刚转行的朋友做毕设或者课设Spring Boot 协同过滤 鲜花商城这种组合出镜率是真的高。一开始我还没太当回事觉得无非就是一个 CRUD 套电商壳直到有人拿着推荐模块来问我“这个协同过滤到底怎么写”我才意识到这个项目的关键不在商城本身而在“推荐”这两个字上。鲜花商城这种业务其实很典型商品生命周期短鲜花保鲜期就那几天、SKU 不算多、用户购买周期不固定可能节日买一次平时买一次、客单价偏低但复购潜力大。这些特点决定了它不适合上太重的推荐引擎比如 TensorFlow 那套深度学习排序模型杀鸡用牛刀也不适合纯靠人工运营去推至少对毕设和个人实战项目来说没有那个运营资源。协同过滤这种经典算法反而刚刚好——实现思路清晰、代码量可控、数据要求不苛刻而且能和 Spring Boot 这套 Java 技术栈完美融合。一句话概括这个项目用 Spring Boot 搭一个完整的鲜花电商后端配合 MySQL 存储业务数据和用户行为数据再用协同过滤算法给用户推荐可能感兴趣的鲜花商品前后端分离附带完整源代码和数据库脚本clone 下来就能跑。适合谁来参考三类人第一类是正在做毕业设计、需要“电商 推荐算法 增删改查”元素都凑齐的在校生第二类是自学 Java 后端想找个完整项目练手、顺便把推荐算法实战一遍的初级开发第三类是公司内部需要快速搭一个带推荐功能的演示 demo、验证业务可行性的技术同学。这个项目的价值在于它把算法和工程缝合在一起既不是纯算法玩具也不是普通的管理系统而是两者都有。1.2 为什么选协同过滤而不是其他推荐方案很多人在网上搜“推荐系统”相关的东西会看到一堆名词协同过滤、基于内容的推荐、知识图谱推荐、深度学习召回、SDM 序列召回、MIND 多兴趣网络……这些算法各有各的适用场景但你要在一个 Spring Boot 单体项目里落地就得考虑一个核心问题投入产出比。协同过滤的核心思想特别朴素物以类聚人以群分。用户协同过滤UserCF就是找和你口味相似的用户把那些用户买过而你还没买的商品推荐给你物品协同过滤ItemCF就是找和你买过商品相似的其他商品把“买了 A 的人还买了 B”这种关联推荐给你。那为什么不用基于内容的推荐因为鲜花商品的属性标签太稀疏。你说“红玫瑰”“香槟玫瑰”“白百合”属性差异怎么量化颜色、花语、适用场景、价格区间这些特征不是不能做但做完之后你会发现它其实是在做分类而不是推荐个性化程度很有限。而协同过滤不需要理解商品本身只看用户行为矩阵谁和谁的行为像就够了。为什么不用深度学习那套因为数据量不够。协同过滤在几千个用户、几百个商品的规模上就能跑出效果神经网络在这种数据规模下根本训不出什么泛化能力反而容易过拟合。而且训练一套深度学习模型还得配 GPU、配 Python 环境、做模型服务化这对一个单体 Java 项目来说太重了。具体到 UserCF 和 ItemCF 怎么选我后面在算法实现部分会详细展开这里先给结论如果项目希望上线后能根据用户历史行为做个性化推荐建议以 UserCF 为主、ItemCF 为辅做融合如果只是当作课设演示ItemCF 一个就够了因为它的推荐结果可解释性更强“买了玫瑰的人还买了百合”答辩和演示的时候更容易讲清楚。2. 系统整体架构与核心模块设计2.1 技术栈选型为什么是 Spring Boot 这套组合拳技术栈这块没什么悬念Spring Boot 就是当前 Java 后端的事实标准。我选型的版本搭配如下直接照着配就行组件版本/选型说明JDK1.8 或 11推荐 1.8兼容性最好毕设服务器部署不容易踩坑Spring Boot2.7.x不要一上来用 3.xjavax 迁移到 jakarta 会有一些麻烦构建工具Maven 3.6比 Gradle 更适合新手仓库依赖好拉数据库MySQL 5.7 / 8.05.7 入门稳8.0 也行注意驱动配置差异ORMMyBatis-Plus单表 CRUD 几乎零 SQL内置分页插件效率很高权限认证Spring Security JWT轻量级无状态登录适合前后端分离缓存Redis可选用于缓存热门推荐结果提升接口响应速度前端Vue 2 Element UI 或 Bootstrap配合 Node.js 环境Vue 更主流这里要重点说一个大家很容易犯的错误Spring Boot 版本不要盲目追新。网上好多教程直接让你用最新版但 Spring Boot 3.x 把 javax 换成了 jakarta很多老依赖会出现兼容问题。我带着学员做这个项目时有人用了 3.1.2结果 MyBatis-Plus 的 starter 版本不支持折腾了一整天。Spring Boot 2.7.x JDK 1.8 MyBatis-Plus 3.5.x这个组合最稳网上能搜到的资料也最多。2.2 项目分层与目录结构一个规范的后端项目目录结构一定要清晰。我习惯这样分flower-shop-recommend/ ├── src/main/java/com/example/flowershop/ │ ├── controller/ # 控制层接收请求返回结果 │ │ ├── UserController.java │ │ ├── FlowerController.java │ │ ├── OrderController.java │ │ └── RecommendController.java │ ├── service/ # 业务层核心业务逻辑 │ │ ├── UserService.java │ │ ├── FlowerService.java │ │ ├── OrderService.java │ │ └── RecommendService.java │ ├── mapper/ # 数据访问层Mapper 接口 │ │ ├── UserMapper.java │ │ ├── FlowerMapper.java │ │ ├── OrderMapper.java │ │ └── RatingMapper.java │ ├── entity/ # 实体类 │ │ ├── User.java │ │ ├── Flower.java │ │ ├── Order.java │ │ └── OrderItem.java │ ├── common/ # 公共类 │ │ ├── Result.java # 统一返回结果 │ │ └── JwtUtil.java # JWT 工具类 │ ├── config/ # 配置类 │ │ ├── RedisConfig.java │ │ └── WebMvcConfig.java │ └── recommend/ # 推荐算法模块 │ ├── UserCF.java │ ├── ItemCF.java │ ├── SimilarityUtil.java │ └── RecommendRunner.java ├── src/main/resources/ │ ├── application.yml │ └── mapper/ # MyBatis XML 文件 ├── sql/ │ └── flower_shop.sql # 完整数据库脚本 └── pom.xml这个结构没什么高深的但有几个点要注意Controller 只做参数接收和结果返回不要写业务代码。我见过太多人把 SQL 写死在 Controller 里确实能跑但后面想加推荐算法的时候你会发现代码乱得像一锅粥。推荐算法单独扔到一个 recommend 包里面和业务代码解耦以后你想换成别的算法直接替换这个包就行不会影响到订单、用户这些模块。2.3 核心功能模块拆解整个系统的功能模块可以拆成四大块用户模块注册、登录、JWT 鉴权、用户信息维护。登录成功后前端拿着 token 访问需要认证的接口推荐接口也需要带上用户 ID这样才能针对不同用户返回不同的推荐结果。商品模块鲜花商品的增删改查、上下架、分类浏览、分页展示、按销量/价格/上架时间排序。数据库增删改查是基本功但商品列表的接口要支持多条件筛选分类、价格区间、花语标签SQL 的 where 条件要动态拼接MyBatis-Plus 的 LambdaQueryWrapper 用起来很方便。订单模块加入购物车、生成订单、订单支付状态流转模拟、订单列表。订单是整个推荐算法的重要数据来源用户买了什么花、买了多少、什么时候买的这些行为数据都会转换为用户对商品的评分或者隐式反馈后面协同过滤全靠这张订单表。推荐模块这是整个系统的灵魂。用户登录后进入首页会看到一个“为你推荐”的列表背后就是协同过滤算法在跑。新用户没有行为数据时走热门推荐策略兜底老用户跑 UserCF/ItemCF 产生个性化结果。这里我会在第四节详细拆解代码实现。3. 数据库设计与数据准备3.1 核心表结构详解数据库这一步非常关键我见过太多项目把表设计得一塌糊涂导致后端代码怎么写怎么别扭。一个鲜花商城推荐系统核心表就五张用户表、鲜花表、订单表、订单明细表、用户行为评分表。用户表userCREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(255) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, phone varchar(20) DEFAULT NULL COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表;鲜花表flowerCREATE TABLE flower ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 商品ID, name varchar(100) NOT NULL COMMENT 商品名称, category varchar(50) DEFAULT NULL COMMENT 分类如玫瑰/百合/郁金香, price decimal(10,2) DEFAULT NULL COMMENT 价格, stock int(11) DEFAULT 0 COMMENT 库存, image varchar(255) DEFAULT NULL COMMENT 商品图片地址, description text COMMENT 商品描述, sales_count int(11) DEFAULT 0 COMMENT 销量, status tinyint(1) DEFAULT 1 COMMENT 上架状态 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT鲜花商品表;这款商品表我特意加了sales_count销量字段两个用途第一排序场景可以直接按销量排第二冷启动的时候“热门推荐”就是按销量和浏览热度排的不需要额外写复杂 SQL。订单表orders和订单明细表order_itemCREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 用户ID, total_amount decimal(10,2) DEFAULT NULL COMMENT 订单总金额, status tinyint(1) DEFAULT 0 COMMENT 订单状态 0待支付 1已支付 2已发货 3已完成 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL COMMENT 订单ID, flower_id bigint(20) NOT NULL COMMENT 商品ID, quantity int(11) DEFAULT 1 COMMENT 购买数量, price decimal(10,2) DEFAULT NULL COMMENT 成交单价, PRIMARY KEY (id), KEY idx_order_id (order_id), KEY idx_flower_id (flower_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;设计成订单主表和明细表两张表是电商系统的标准做法好处是订单的维度数据和商品明细数据分离对账、统计、扩展都方便。推荐算法取数据的时候可以 join 这两张表拿到“某个用户买了哪些商品”。用户行为评分表user_ratingCREATE TABLE user_rating ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, flower_id bigint(20) NOT NULL COMMENT 商品ID, rating tinyint(4) DEFAULT 0 COMMENT 评分 1-5实际应用中可由行为转换而来, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_flower_id (flower_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户评分表;这张表是推荐算法的核心数据源。有人可能会问商城一般没有让用户打分的功能啊评分从哪来这是个好问题实际落地的时候有两种方案第一种方案是直接给用户展示可点击的评分按钮百合、玫瑰这种单价不高的商品买完之后弹窗问一下“给这束花打个分吧”简单粗暴适合毕设和 Demo。第二种方案是隐式反馈转换把订单行为转换成评分买过 1 次记 3 分买过 2 次记 4 分买过 3 次及以上记 5 分加入购物车但没下单记 1 分浏览过详情页记 2 分。这个方案更贴近真实场景数据不需要用户额外操作但我建议在初版项目里先做第一种因为数据比较干净算法效果也更容易验证。等你要扩展的时候再加一个定时任务把订单表的数据转换进评分表。3.2 造数据让协同过滤跑起来的关键一步推荐系统最怕的是没数据。我见过很多人把项目代码写完了数据库里只有十几条商品、两三个测试用户运行推荐接口结果推荐列表是空的或者推荐结果完全没意义。协同过滤是基于统计的算法数据稀疏等于算法失灵。造数据有两个层面的要求数量要求用户至少 20 个以上鲜花商品 30 个以上评分数据 200 条以上这个规模才能看出协同过滤的效果。数据量太少的时候用户之间的相似矩阵全是 0根本找不出相似用户。规律要求数据要“有规律”不能纯随机。我造数据的时候会给用户打标签——比如“喜好玫瑰系”的用户集中给红玫瑰、粉玫瑰、香槟玫瑰高分“喜好清新系”的用户集中给百合、雏菊、满天星高分。这样相似度计算出来才会呈现“聚类”效应推荐结果才有意义。纯随机数据的推荐结果基本等于随机推荐答辩演示的时候很尴尬。造数据我建议直接用 SQL 脚本或者写个 Java 工具类批量插入不要手动一条条录入。我提供了一个参考脚本里面预设了 30 个用户、50 种鲜花、500 条评分记录克隆项目后直接导入sql/flower_shop.sql就能跑出效果。4. 协同过滤推荐算法实现4.1 相似度计算协同过滤的数学基础前面说过协同过滤的核心是“找相似”。无论是 UserCF 还是 ItemCF都需要先计算相似度。最常用的相似度计算方式是余弦相似度。拿 UserCF 举例子假设有三个用户 A、B、C他们对一些鲜花商品的评分如下0 表示没有买过也没有评过分用户红玫瑰白百合向日葵郁金香满天星A53010B40012C05403A 和 B 的余弦相似度怎么算公式是cos(A, B) (A · B) / (|A| * |B|)分子是向量点积5×4 3×0 0×0 1×1 0×2 20 0 0 1 0 21分母是模长的乘积|A| sqrt(5² 3² 0² 1² 0²) sqrt(35) ≈ 5.92|B| sqrt(4² 0² 0² 1² 2²) sqrt(21) ≈ 4.58所以 cos(A, B) 21 / (5.92 × 4.58) ≈ 0.775A 和 B 的相似度还挺高。而 A 和 C 的点积5×0 3×5 0×4 1×0 0×3 15|C| sqrt(0 25 16 0 9) sqrt(50) ≈ 7.07cos(A, C) 15 / (5.92 × 7.07) ≈ 0.358。显然 A 和 B 更相似。相似度范围在 [-1, 1] 之间值越大越相似。在协同过滤代码中我们一般只关心正相关的用户所以算出来大于 0 的才参与后续计算。4.2 UserCF 用户协同过滤完整实现UserCF 的算法流程分四步第一步构建用户-物品评分矩阵从user_rating表里查出来所有评分记录组装成一个 Map 结构MapInteger, MapInteger, Double外层 key 是用户 ID内层 key 是商品 IDvalue 是评分。这一步我建议在 Service 启动后或者推荐接口调用时从数据库加载数据量小直接全量加载不需要搞缓存。第二步计算用户相似度矩阵对每一对用户计算余弦相似度得到MapInteger, MapInteger, Double表示用户之间的相似度。注意这个矩阵是对称的实际代码里只用存一半或者全存也行我图省事就全存了。public class UserCF { // 用户ID - (商品ID - 评分) private MapInteger, MapInteger, Double userItemMatrix; // 用户ID - (相似用户ID - 相似度) private MapInteger, ListMap.EntryInteger, Double userSimilarityMatrix; public UserCF(MapInteger, MapInteger, Double userItemMatrix) { this.userItemMatrix userItemMatrix; this.userSimilarityMatrix new HashMap(); buildSimilarityMatrix(); } private void buildSimilarityMatrix() { ListInteger userIds new ArrayList(userItemMatrix.keySet()); for (int i 0; i userIds.size(); i) { for (int j i 1; j userIds.size(); j) { int u1 userIds.get(i); int u2 userIds.get(j); double similarity SimilarityUtil.cosine( userItemMatrix.get(u1), userItemMatrix.get(u2)); if (similarity 0) { userSimilarityMatrix .computeIfAbsent(u1, k - new ArrayList()) .add(new AbstractMap.SimpleEntry(u2, similarity)); userSimilarityMatrix .computeIfAbsent(u2, k - new ArrayList()) .add(new AbstractMap.SimpleEntry(u1, similarity)); } } } // 按相似度降序排序 userSimilarityMatrix.values().forEach(list - list.sort((a, b) - Double.compare(b.getValue(), a.getValue()))); } }第三步为目标用户找到 TopN 相似用户假设要给用户 A 推荐先从相似度矩阵里取出 A 的相似用户列表取相似度最高的前 K 个一般取 5~10 个。K 太小推荐结果会局限K 太大噪声用户会给推荐带偏。我调参的时候发现 K8 比较合适你也可以根据实际数据量调整。第四步候选物品打分并生成推荐列表这一步是精髓推荐公式是“相似用户的评分 × 相似权重”的加权求和。对每个相似用户买过或评过分而目标用户没买过的商品计算预测评分pred(user, item) Σ(sim(user, u) * rating(u, item)) / Σ|sim(user, u)|说白了就是越相似的用户他的评分在预测结果里权重越大。public ListInteger recommend(int userId, int topN, int kNeighbors) { // 1. 获取相似用户 TopK ListMap.EntryInteger, Double neighbors userSimilarityMatrix.getOrDefault(userId, Collections.emptyList()) .stream() .limit(kNeighbors) .collect(Collectors.toList()); if (neighbors.isEmpty()) { return Collections.emptyList(); } // 2. 找出目标用户已经买过的商品 MapInteger, Double userItems userItemMatrix.getOrDefault(userId, new HashMap()); SetInteger purchasedItems userItems.keySet(); // 3. 候选物品加权打分 MapInteger, Double scoreMap new HashMap(); MapInteger, Double weightSumMap new HashMap(); for (Map.EntryInteger, Double neighbor : neighbors) { int neighborId neighbor.getKey(); double similarity neighbor.getValue(); MapInteger, Double neighborItems userItemMatrix.get(neighborId); for (Map.EntryInteger, Double itemEntry : neighborItems.entrySet()) { int itemId itemEntry.getKey(); double rating itemEntry.getValue(); if (purchasedItems.contains(itemId)) { continue; // 排除已购买的商品 } scoreMap.merge(itemId, similarity * rating, Double::sum); weightSumMap.merge(itemId, Math.abs(similarity), Double::sum); } } // 4. 归一化并排序 ListMap.EntryInteger, Double result new ArrayList(); for (Map.EntryInteger, Double entry : scoreMap.entrySet()) { double normalizedScore entry.getValue() / weightSumMap.getOrDefault(entry.getKey(), 1.0); result.add(new AbstractMap.SimpleEntry(entry.getKey(), normalizedScore)); } result.sort((a, b) - Double.compare(b.getValue(), a.getValue())); // 5. 返回 TopN 结果 return result.stream().limit(topN).map(Map.Entry::getKey).collect(Collectors.toList()); }注意第 4 步的归一化如果不除以权重和评分数量多的用户天然会有更高的累积分值这对物品不公平。归一化之后不同用户之间的预测分才有可比性。4.3 ItemCF 物品协同过滤与混合策略ItemCF 的思路和 UserCF 正好反过来先找物品之间的相似关系再给用户推荐和他“喜欢过的物品”相似的其他物品。具体实现的时候物品相似度矩阵的构建方式是用购买/评分行为把物品映射成向量每个物品的向量维度是所有用户值是对应用户的评分然后算两两之间余弦相似度。换句话说在 ItemCF 里“物品”变成了向量“用户”变成了维度。public class ItemCF { // 物品ID - (用户ID - 评分) private MapInteger, MapInteger, Double itemUserMatrix; private MapInteger, ListMap.EntryInteger, Double itemSimilarityMatrix; public ItemCF(MapInteger, MapInteger, Double userItemMatrix) { this.itemUserMatrix transpose(userItemMatrix); buildSimilarityMatrix(); } // 用户-物品矩阵转置为物品-用户矩阵 private MapInteger, MapInteger, Double transpose( MapInteger, MapInteger, Double userItemMatrix) { MapInteger, MapInteger, Double result new HashMap(); for (Map.EntryInteger, MapInteger, Double entry : userItemMatrix.entrySet()) { int userId entry.getKey(); for (Map.EntryInteger, Double itemEntry : entry.getValue().entrySet()) { result.computeIfAbsent(itemEntry.getKey(), k - new HashMap()) .put(userId, itemEntry.getValue()); } } return result; } public ListInteger recommend(int userId, int topN, int kSimilarItems) { MapInteger, Double userItems userItemMatrix.getOrDefault(userId, new HashMap()); if (userItems.isEmpty()) return Collections.emptyList(); // 对用户已经产生行为的物品找到相似物品并累加分数 MapInteger, Double scoreMap new HashMap(); for (Integer purchasedItem : userItems.keySet()) { ListMap.EntryInteger, Double similarItems itemSimilarityMatrix.getOrDefault(purchasedItem, Collections.emptyList()) .stream() .limit(kSimilarItems) .collect(Collectors.toList()); for (Map.EntryInteger, Double simItem : similarItems) { int simItemId simItem.getKey(); if (userItems.containsKey(simItemId)) continue; // 跳过已买过的 double weight simItem.getValue() * userItems.get(purchasedItem); scoreMap.merge(simItemId, weight, Double::sum); } } return scoreMap.entrySet().stream() .sorted((a, b) - Double.compare(b.getValue(), a.getValue())) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } }ItemCF 的效果在鲜花这种品类特色明显的商品上表现得很好因为用户买花的场景往往很集中生日送花、表白送花、日常插花买过红玫瑰的用户大概率对百合、向日葵这种“常见花束主花”也有兴趣。那这两个算法到底怎么选我最终采用的是混合推荐策略先用 ItemCF 出 60% 的推荐位再用 UserCF 出 30%剩下 10% 留给热门商品兜底。这样既保证推荐的多样性又不会因为某一个算法的数据盲区导致推荐结果太窄。4.4 冷启动问题新用户、新商品怎么处理协同过滤有一个绕不开的硬伤——冷启动。新用户没有任何行为数据算不出相似用户新商品没有任何评分算不出相似商品。这时候如果不做兜底策略推荐接口就会返回空列表。我的处理方案分两种情况用户冷启动新注册用户直接走“热门推荐”从flower表里按销量和浏览量倒序取 TopN。注意为了推荐效果的合理性不只是按销量排序我还会把不同品类各取一些避免整个推荐页全是红玫瑰。具体 SQL 可以这样写SELECT * FROM ( SELECT f.*, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales_count DESC) rn FROM flower f WHERE f.status 1 ) t WHERE t.rn 2 LIMIT 10;用窗口函数按品类分组取销量前 2 名推荐列表的品类就多样性了。商品冷启动如果一个新上架的鲜花商品没有任何行为数据它永远不会被协同过滤推荐形成“马太效应”。解决办法是给新商品加一个“新品加权分”在推荐列表的评分叠加一个时间衰减因子——上架时间越近加成分越高。比如// 商品上架时间越近附加的加权分越高 double timeBoost Math.max(0, 3 - daysSinceCreated(itemId) / 7.0); scoreMap.merge(itemId, timeBoost, Double::sum);这个逻辑可以单独写在推荐结果的 post-processing 阶段不用改协同过滤主体代码。5. 推荐接口与业务集成5.1 推荐接口设计推荐模块要和 Spring Boot 业务层打通接口设计很关键。我提供的代码里主要有两个接口RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private RecommendService recommendService; /** * 首页个性化推荐 */ GetMapping(/home) public ResultListFlowerVO recommendHome( RequestParam(defaultValue 10) int limit) { // 从 Token 中获取当前登录用户的 ID Integer userId JwtUtil.getCurrentUserId(); ListFlowerVO recommendList recommendService.recommendForUser(userId, limit); return Result.success(recommendList); } /** * 商品详情页“看了又看”推荐 */ GetMapping(/related/{flowerId}) public ResultListFlowerVO recommendRelated( PathVariable Integer flowerId, RequestParam(defaultValue 5) int limit) { ListFlowerVO relatedList recommendService.recommendRelated(flowerId, limit); return Result.success(relatedList); } }第一个接口用在首页“猜你喜欢”每次进入首页展示 10 条个性化推荐第二个接口用在商品详情页展示“购买了这束花的人也买了这些”这个就是纯 ItemCF 的应用可解释性强用户接受度高。5.2 推荐结果缓存与定时更新协同过滤的计算虽然不复杂但如果用户量大实时计算相似度矩阵还是会卡顿。我的优化策略是结果缓存 定时刷新方案一启动时加载计算。在 Spring Boot 的ApplicationRunner中触发一次全量计算把每个用户的推荐结果提前算好放到内存 Map 里接口直接查内存返回。这种方式适合数据量不是特别大的场景我提供的项目默认采用这种方案好处是接口响应极快坏处是新增行为数据后推荐结果不会自动更新。方案二定时任务刷新。如果你引入了 Redis可以在用户下单后实时更新评分表然后通过Scheduled定时任务每 10 分钟重新计算一次推荐结果放到 Redis 里。这样既保证了推荐结果的新鲜度又不会因为每次请求都跑算法而拖垮服务。Component public class RecommendTask { Autowired private RecommendService recommendService; Scheduled(fixedDelay 600000) // 每10分钟刷新一次 public void refreshRecommendCache() { recommendService.rebuildAllRecommendations(); } }这两种方案我都在项目里实现了默认走方案一Redis 可选开启。如果你是做毕设建议把两种方案都在论文里写一下涉及“系统优化”的部分就有内容可讲了。5.3 推荐接口的业务兜底逻辑线上系统最忌接口报错。推荐算法涉及矩阵计算如果数据不规范比如用户评分表为空、相似度全为 0容易出现异常。我在 Service 层做了多层兜底public ListFlowerVO recommendForUser(Integer userId, int limit) { // 第一层用户未登录返回热门推荐 if (userId null) { return hotFlowerService.getHotFlowers(limit); } try { ListInteger recommendIds recommendEngine.recommend(userId, limit); // 第二层协同过滤结果为空降级为热门推荐 if (recommendIds null || recommendIds.isEmpty()) { return hotFlowerService.getHotFlowers(limit); } // 第三层过滤已下架商品 ListFlowerVO result flowerMapper.selectByIds(recommendIds) .stream() .filter(f - f.getStatus() 1) .map(FlowerConverter::toVO) .collect(Collectors.toList()); return result; } catch (Exception e) { log.error(推荐算法执行异常降级为热门推荐, e); return hotFlowerService.getHotFlowers(limit); } }推荐接口的好用程度不在于算法有多炫而在于任何情况下都不会返回空白页面。兜底逻辑是必须写的这不仅是工程素养也是答辩的时候老师会问到的点。6. 常见问题与踩坑记录6.1 数据库连接与版本适配问题问题 1MySQL 8.0 连接报错 Public Key Retrieval is not allowedMySQL 8.0 默认使用 caching_sha2_password 认证插件JDBC 连接需要显式设置allowPublicKeyRetrievaltrue。在application.yml里加上参数就能解决spring: datasource: url: jdbc:mysql://localhost:3306/flower_shop?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver问题 2数据库中文乱码建库语句一定要指定 utf8mb4 字符集CREATE DATABASE flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;我之前见过一个学员库建好了但没指定字符集默认 latin1结果页面上一堆问号排查了半天。连接串里的 characterEncodingutf8 也要加上双保险。6.2 协同过滤算法结果不理想问题 1每个人的推荐结果都一样这种情况十有八九是评分数据没造好用户之间的行为高度一致或者数据量太少导致相似度矩阵全部相同。我的建议把评分数据删掉重新造造数据的时候让用户有明确的“偏好分组”比如 5 个用户偏好玫瑰类、5 个偏好百合类、5 个偏好混合类这样推荐结果才有差异化。问题 2推荐列表总是一些冷门商品协同过滤有个特点如果一个商品的评分记录特别少但只要有个别相似用户买过它它的加权归一化分数就可能非常高。解决思路是给候选物品加一个“支持度”过滤——必须至少有 N 个相似用户买过的商品才进入候选集。在代码里就是加一行过滤条件if (neighborCount.getOrDefault(itemId, 0) 2) { continue; }6.3 项目启动与依赖问题问题IDEA 导入 Maven 项目后依赖爆红这类问题 90% 是 Maven 仓库没有配置阿里云镜像导致依赖下载超时。在settings.xml里加镜像mirror idaliyunmaven/id mirrorOf*/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror另外注意 JDK 版本Spring Boot 2.7 在 JDK 17 上虽然能跑但我还是建议用 JDK 1.8避免一些老依赖的兼容问题。6.4 前端部署与跨域问题如果你用 Vue 开发前端联调时浏览器会报跨域错误。Spring Boot 后端的处理方式是在 WebMvcConfig 里加跨域配置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); } }6.5 数据库导出与同步问题有些同学会在 IDEA 里使用数据库工具导出脚本。这里提醒一下导出数据库脚本时要勾选 “Include Create Database” 和 “Include Drop Database”这样别人导入的时候直接执行整个 SQL 文件就能搞定不会出现“表已存在”或者“数据库不存在”的报错。IDEA 的操作路径右侧 Database 面板 → 右键数据库 →SQL Scripts→Generate SQL Scripts勾选相应选项后导出。7. 项目部署与上线验证7.1 打包发布从本地到服务器项目开发完成后打 jar 包部署是必经环节。用 Maven 打包mvn clean package -DskipTests打包完成后在target/目录下会生成flower-shop-0.0.1-SNAPSHOT.jar。这个 jar 里内嵌了 Tomcat所以在服务器上只需要装 JDK 和 MySQL不需要额外装 Tomcat。上传到服务器后启动nohup java -jar flower-shop-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod flask.log 21 需要注意的是如果你本地的 MySQL 和服务器上的 MySQL 账号密码不一样要确保application-prod.yml里的配置是正确的。用--spring.profiles.activeprod切到生产环境配置而不是在本地配置上改来改去。7.2 推荐效果验证从“跑通”到“跑对”项目部署完成接口能返回数据这叫“跑通”。但推荐结果是否合理还需要验证。我常用的验证方法有两种方法一行为验证。注册一个测试用户先手动购买 2~3 个同一品类的鲜花比如红玫瑰、粉玫瑰然后看首页推荐列表里是否出现了其他玫瑰类或者搭配类鲜花比如百合、向日葵。出现说明 ItemCF 生效了不出现就需要检查评分数据是不是没写进user_rating表。方法二数据佐证。直接从数据库看相似度矩阵的数值。跑一段测试代码或者写个查询找出某个用户的 Top 5 相似用户看他们的历史购买记录是否有重合。如果相似用户之间完全没有共同行为那要么是数据有问题要么是算法实现有 bug。7.3 扩展方向还能往哪些方向深入项目跑通之后如果想在论文或者项目中体现更多深度我提供几个方向第一引入 Redis 缓存优化把推荐结果和热门列表缓存起来通过接口响应时间的对比数据说明优化效果这是论文里“性能优化”章节的现成素材。第二混合推荐策略调参。UserCF 和 ItemCF 的推荐结果按照 4:6 还是 3:7 加权融合哪个效果更好你可以用“用户真实点击率”或者“推荐物品转化率”作为评估指标做几组对比实验这个数据非常有说服力。第三加一个简单的评分预测评估。把评分数据按 8:2 切分成训练集和测试集用 MAE平均绝对误差或 RMSE均方根误差评估协同过滤的预测精度画出不同邻居数量 K 下的误差曲线。这个是推荐系统论文里最标准的实验做出来很加分。我个人在实际操作中的感触是这类项目最大的学习价值不在 CRUD而在从 0 到 1 走通“算法建模 → 数据准备 → 工程落地 → 效果验证”这条完整链路。很多人卡在中间某一步比如数据造不好、相似度算出来全是零、推荐结果没有区分度这些坑我都踩过最后发现绝大多数问题都出在“数据质量”而不是“算法代码”上。如果你照着这个思路一步步来从数据库脚本导入到推荐接口调通一晚上就能跑出结果剩下的时间可以慢慢调优。最后再分享一个小技巧写论文或者项目文档的时候把推荐模块单独画一张时序图把你的算法细节和兜底逻辑写清楚老师一眼就知道这是你自己做的不是网上随便抄的。本文还有配套的精品资源点击获取