
简介这是一份电商精准营销推荐系统的完整源码包后端采用SpringBoot框架前端采用Vue技术实现了前后端分离的现代Web开发模式。项目围绕电商平台的用户推荐场景设计了用户管理、商品展示、推荐策略配置等核心功能模块代码结构清晰适合初学者和进阶学习者作为毕业设计、课程设计、大作业或工程实训课题。压缩包共五百二十五个文件大小约十三点四四兆字节其中一百五十九个Java文件负责后端业务逻辑与数据访问一百一十九个Vue文件构成前端页面组件另有五十个JavaScript文件、二十一个CSS样式文件、XML配置映射文件以及SQL数据库初始化脚本覆盖全栈开发的主要文件类型目录划分明确便于按模块学习与二次开发。项目基于JDK8、Tomcat7与MySQL5.7构建附带SQL脚本可快速搭建运行环境。目前已经有一百零三人学习下载源码经过调试可直接运行在此基础上扩展推荐算法或调整业务模块都比较方便具有较高的学习与参考价值。1. 电商精准营销推荐系统到底是什么先看清这个 Spring Boot Vue 项目能解决谁的问题做电商系统的朋友应该都有体会商品上千件用户进来转一圈就走转化率上不去运营只能靠群发促销邮件和短信硬推。这个标题里的系统解决的正是这个问题——用推荐算法把「用户可能想买的东西」推到首页、商品详情页和购物车旁边让用户多停留、多下单。技术上它拆成两半后端用 Java 的 Spring Boot 写推荐逻辑和接口前端用 Vue 搭用户界面。适合谁适合正在做电商毕设、中小型电商后台开发、或者想在自己项目里接一个推荐模块的 Java 工程师。一句话讲透推荐系统不是玄学它就是把用户行为数据变成商品排序权重的工程化过程而 Spring Boot Vue 是目前做这件事最稳的组合之一。2. 电商精准营销不等于「猜你喜欢」先拆业务模块再定技术选型2.1 推荐系统在电商里的真实链路从行为采集到精准触达很多人一提到推荐系统第一反应就是协同过滤算法然后就开始写相似度计算。这其实是把黑匣子打开了但顺序反了——做电商精准营销推荐第一步不是写算法而是先搞清楚推荐结果用在哪几个位置。我做这类项目时的拆法比较固定按业务链路分四层。第一层是行为采集层。用户在电商网站上的浏览、点击、加购、下单、收藏这些动作都要落成一条结构化的行为日志。常见做法是建一张user_behavior表字段至少包含user_id、item_id、behavior_type、create_time。behavior_type用数字区分1 表示浏览、2 表示加购、3 表示下单。别看这张表简单后期所有推荐逻辑都靠它喂数据。第二层是用户画像与商品画像层。用户画像关注的是「这个用户最近对什么品类感兴趣」——通过行为日志聚合出来。商品画像则靠商家在后台录入的商品类目、价格区间、标签来构建比如「运动鞋 / 300-500 元 / 夏季新款」。这一层不需要很重的大数据体系MySQL 加几张维表就够了除非你要做实时推荐。第三层是推荐引擎层。它负责把画像和实时行为转成候选商品集合。这里才轮到算法上场协通过滤算「和你相似的人买了什么」基于内容的推荐算「你浏览过的商品有哪些相似款」。对中小型电商系统来说这两类算法已经覆盖 80% 的场景不需要上深度学习。第四层是营销触达层。推荐结果不只是首页推荐位。精准营销还包括用户加购没下单三天后给他推送一张优惠券用户浏览了某类商品但没买在购物车页给他推荐同类低价款。落到代码上就是推荐接口返回的数据会被不同的业务模块消费有的走页面接口有的走短信或消息推送。这四层里真正值得花时间设计的是第二层和第三层之间的数据契约——推荐引擎输入什么、输出什么必须提前定义清楚。我一般定义成统一的RecommendResponse里面包含userId、商品列表、每个商品的推荐权重分数和推荐理由比如「与你浏览过的 XX 相似」。这样后续接短信推送、接首页推荐位只需要改调用方不用动算法核心。2.2 Spring Boot 与 Vue 的选型理由为什么这对组合适合推荐系统落地聊完业务链路再回答一个很多人纠结的问题为什么这个标题选 Spring Boot Vue而不是 Python React或者 PHP 一把梭先说后端 Spring Boot。推荐系统不是只有算法还要把算法包成稳定的 HTTP 接口处理并发请求、事务、权限。Spring Boot 在这件事上有天然优势内置 Tomcatspring-boot-starter-web加一个注解就能吐 REST 接口配合 MyBatis-Plus 操作 MySQL 行为日志表不需要写一堆 JDBC 样板代码。另一个实际考虑是国内电商后台的存量系统绝大部分是 Java 系你做一个推荐系统模块最终要嵌到别人的订单系统、商品系统里Spring Boot 的亲和力最强。这里有个常见误区要提醒一下不要一上来就引入 Flink、Kafka 做实时计算。像标题这类系统核心动作是「基于用户的历史行为周期性地算出推荐列表」它是准实时的不是毫秒级的。用 Spring Boot 自带的定时任务Scheduled每天凌晨跑一次推荐结果缓存白天读缓存性能完全够。强行上 Flink 的结果就是运维成本翻倍产出却不明显——这个「头重脚轻」的坑我在早期项目里踩过。前端选 Vue核心原因是生态和上手门槛。电商管理后台的典型界面是「左侧菜单 右侧表格/卡片」Vue 全家桶里的 Vue Router 管路由、Vuex 或 Pinia 管全局状态、Element UI 或 Vant 直接提供商品卡片和推荐位组件三件套拼起来比 React 少写很多胶水代码。另外Vue 对后端工程师更友好模板语法接近 HTML 直觉你写推荐接口的人顺手就能把管理页面调通不必专门等一个前端资源。Vue 2 还是 Vue 3 的问题新项目直接上 Vue 3 配合 Vite性能更好如果项目是维护态的那 Vue 2 也没必要强行升级。选型定下来之后项目结构我会默认拆成三个目录backendSpring Boot 服务、frontendVue 工程、sql初始化脚本。下面一章就照着这个结构把最小系统跑通。3. 把推荐系统跑起来从行为表、协同过滤到 Vue 页面联调3.1 初始化数据库用户行为表与商品表的设计动手写代码之前先把表结构定住。推荐系统最忌讳边写边改表后面算法的 SQL 全依赖表字段。我一般用下面这套建表脚本直接跑在 MySQL 5.7 以上版本。-- 商品表推荐系统的基础数据源 CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 商品ID, title varchar(128) NOT NULL COMMENT 商品标题, category_id bigint(20) NOT NULL COMMENT 商品类目ID关联类目表, price decimal(10,2) NOT NULL COMMENT 商品价格, tags varchar(255) DEFAULT NULL COMMENT 商品标签逗号分隔如夏季,运动,新款, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 上下架状态1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 用户行为表所有推荐算法的数据来源 CREATE TABLE user_behavior ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, item_id bigint(20) NOT NULL COMMENT 商品ID, behavior_type tinyint(4) NOT NULL COMMENT 行为类型1浏览 2加购 3下单, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_id, create_time), KEY idx_item_id (item_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户行为日志表;这份脚本里有三个设计点值得注意。第一个是user_behavior表刻意不加UNIQUE约束因为用户可能对同一个商品在不同时间产生多次浏览行为加了唯一约束会导致行为日志丢失。第二个是idx_user_time这个联合索引它服务的是「查某个用户最近 N 条行为」这个高频查询按用户加时间排序能直接走索引避免文件排序。第三个是behavior_type用数字而不是字符串是为了后续在 SQL 里用SUM聚合不同行为的权重。3.2 Spring Boot 后端骨架编写实体类、Mapper 与推荐接口数据库就绪后创建一个 Spring Boot 工程。JDK 用 1.8 或 11 都行Spring Boot 用 2.7.x 这个版本段——原因在第 4 章避坑里细讲先记住不要追 3.x 最新版就行。pom.xml里引入 Web、MyBatis-Plus、MySQL 驱动、Lombok 四个核心依赖。实体类写法如下。MyBatis-Plus 的TableName注解把类映射到表字段用TableId标记主键。Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String title; private Long categoryId; private BigDecimal price; private String tags; private Integer status; private LocalDateTime createTime; } Data TableName(user_behavior) public class UserBehavior { TableId(type IdType.AUTO) private Long id; private Long userId; private Long itemId; private Integer behaviorType; private LocalDateTime createTime; }逻辑说明Data来自 Lombok自动生成 getter/setter省掉样板代码。IdType.AUTO表示主键由数据库自增对应建表语句里的AUTO_INCREMENT。这里有个细节create_time字段在 Java 侧用LocalDateTime而不是Date配合 MyBatis-Plus 的自动填充避免时区转换的坑——国内服务器用Asia/Shanghai时区如果用Date会出现相差 8 小时的问题。Mapper 接口就三行。MyBatis-Plus 的BaseMapper已经把单表 CRUD 做完了不需要写 XMLMapper public interface ProductMapper extends BaseMapperProduct { } Mapper public interface UserBehaviorMapper extends BaseMapperUserBehavior { }参数说明Mapper注解让 Spring 容器扫描到这个接口并生成代理实现类。如果项目里配置了MapperScan(com.example.recommend.mapper)这个注解可以省略不写。加入它是为了单个文件也能独立识别调试时少一个报错点。3.3 推荐引擎核心代码基于物品的协同过滤实现现在到最关键的部分——推荐算法。这里我用「基于物品的协同过滤ItemCF」来实现它的核心思想是如果用户 A 买了商品 X又买了商品 Y那么给用户 B 推荐商品 X 时Y 也应该有比较高的权重。通俗说就是「买了这个商品的人通常还买了那个商品」。这个算法落地分三步第一步统计商品两两之间的共现次数得到相似度矩阵第二步根据用户的历史行为找到他感兴趣的候选商品第三步按相似度加权排序输出 Top-N 推荐结果。完整代码我拆成三段。先写相似度矩阵计算Service public class ItemCfService { Resource private UserBehaviorMapper behaviorMapper; /** * 计算商品间的共现矩阵遍历所有行为日志统计商品两两共同出现的次数 * 返回 Map: key itemId_1 _ itemId_2, value 共现次数 */ public MapString, Integer buildCoOccurrenceMatrix() { ListUserBehavior behaviors behaviorMapper.selectList( new LambdaQueryWrapperUserBehavior() .select(UserBehavior::getUserId, UserBehavior::getItemId) .in(UserBehavior::getBehaviorType, 2, 3) // 只取加购和下单行为 ); MapString, Integer coCountMap new HashMap(); MapLong, ListLong userItemsMap new HashMap(); // 先按用户分组聚合每个用户购买/加购过的商品列表 for (UserBehavior behavior : behaviors) { userItemsMap.computeIfAbsent(behavior.getUserId(), k - new ArrayList()).add(behavior.getItemId()); } // 对每个用户的商品列表做两两组合累加共现次数 for (ListLong itemList : userItemsMap.values()) { for (int i 0; i itemList.size(); i) { for (int j i 1; j itemList.size(); j) { Long id1 itemList.get(i); Long id2 itemList.get(j); String key id1 _ id2; coCountMap.put(key, coCountMap.getOrDefault(key, 0) 1); } } } return coCountMap; } }逻辑说明LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器in(UserBehavior::getBehaviorType, 2, 3)表示只统计加购和下单行为排除浏览——因为浏览行为噪音太大用户可能误点并不能真实反映购买意向。如果你的场景里浏览数据也能反映兴趣可以把 1 也加进去但推荐精度通常会下降。商品两两组合用双重循环时间复杂度是 O(n²)n 是单个用户的商品数量一般不会超过几十性能没问题。再写针对某个用户的推荐逻辑/** * 根据目标用户的历史行为结合共现矩阵生成推荐列表 * param userId 目标用户ID * param candidateItems 需要排除的商品ID通常是用户已购买过的 * param topN 返回的推荐数量 */ public ListLong recommend(Long userId, ListLong candidateItems, int topN) { // 1. 查询目标用户的行为记录他浏览过、加购过哪些商品 ListUserBehavior userBehaviors behaviorMapper.selectList( new LambdaQueryWrapperUserBehavior() .eq(UserBehavior::getUserId, userId) ); // 2. 提取用户的行为商品集合 SetLong userItemSet new HashSet(); for (UserBehavior behavior : userBehaviors) { userItemSet.add(behavior.getItemId()); } // 3. 遍历用户行为商品去共现矩阵里找相似商品累加相似度分数 MapString, Integer coMatrix buildCoOccurrenceMatrix(); MapLong, Double scoreMap new HashMap(); for (Long userItem : userItemSet) { for (Map.EntryString, Integer entry : coMatrix.entrySet()) { String[] pair entry.getKey().split(_); Long id1 Long.parseLong(pair[0]); Long id2 Long.parseLong(pair[1]); Integer coCount entry.getValue(); if (id1.equals(userItem)) { // 用户行为商品与推荐商品在共现矩阵中共同出现过 // 累加时给不同行为不同权重 scoreMap.put(id2, scoreMap.getOrDefault(id2, 0.0) coCount * getBehaviorWeight(userItem, userId)); } else if (id2.equals(userItem)) { scoreMap.put(id1, scoreMap.getOrDefault(id1, 0.0) coCount * getBehaviorWeight(userItem, userId)); } } } // 4. 过滤掉用户已经买过的商品按分数排序取Top-N return scoreMap.entrySet().stream() .filter(e - !userItemSet.contains(e.getKey())) .filter(e - !candidateItems.contains(e.getKey())) .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } private double getBehaviorWeight(Long itemId, Long userId) { UserBehavior latest behaviorMapper.selectOne( new LambdaQueryWrapperUserBehavior() .eq(UserBehavior::getUserId, userId) .eq(UserBehavior::getItemId, itemId) .orderByDesc(UserBehavior::getCreateTime) .last(LIMIT 1) ); if (latest null) { return 1.0; } // 行为权重下单3加购2浏览1 switch (latest.getBehaviorType()) { case 3: return 3.0; case 2: return 2.0; default: return 1.0; } }参数说明topN一般设 10 到 20首页推荐位放 10 个商品比较合适超过 20 用户要向下滚动很久才看到底部转化率反而下降。candidateItems这个参数用于排除已经在购物车或已经下单的商品避免推荐结果里出现「推荐一个用户刚买过的东西」这种尴尬场景。getBehaviorWeight做了临时查询每次调用都走一次数据库这段在数据量大时有优化空间——可以把用户行为一次性加载到内存但中小系统这个写法最直观。最后写接口层。这里我偷了个懒没把相似度矩阵存进 Redis而是每次请求时临时计算。数据量小的阶段这是最稳的RestController RequestMapping(/api/recommend) public class RecommendController { Resource private ItemCfService itemCfService; Resource private ProductMapper productMapper; /** * 获取用户推荐列表 * param userId 用户ID * param size 推荐数量默认10 */ GetMapping(/{userId}) public Result getRecommend(PathVariable Long userId, RequestParam(defaultValue 10) Integer size) { // 查询在架商品作为候选池排除掉 user 已购的在外层过滤 ListProduct products productMapper.selectList( new LambdaQueryWrapperProduct().eq(Product::getStatus, 1) ); ListLong candidateIds products.stream().map(Product::getId).collect(Collectors.toList()); ListLong recommendIds itemCfService.recommend(userId, candidateIds, size); // 根据推荐ID回查商品完整信息返回给前端 ListProduct result recommendIds.isEmpty() ? products.subList(0, Math.min(size, products.size())) : productMapper.selectBatchIds(recommendIds); return Result.success(result); } }逻辑说明冷启动场景——当用户没有任何行为数据时recommend返回空列表这段代码会自动回退到「默认推荐」即直接返回在架商品的前 N 个。这个降级逻辑很关键宁可推荐热门商品也不能让前端拿到空数组导致页面空白。Result是统一响应体里面包含code、msg、data三个字段前端 axios 拦截器统一处理。3.4 Vue 前端页面开发路由、Axios 请求与商品推荐展示后端接口就绪后前端要做的事情是「推荐位渲染 用户身份传递」。用户身份用模拟的userId就行——真实系统里从登录态里面取。先配路由// router/index.js import { createRouter, createWebHistory } from vue-router; import HomePage from ../views/HomePage.vue; import ProductDetail from ../views/ProductDetail.vue; const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: HomePage }, { path: /product/:id, name: product-detail, component: ProductDetail } ] }); export default router;路由配置注意一点我用了createWebHistory这种 history 模式在开发环境没问题但打包上线后如果服务器没配 try_files 会出现刷新 404第 4 章避坑里会写具体解法。如果你不想碰这个坑可以改用createWebHashHistory。API 请求封装用 axios 实例统一指定baseURL// api/recommend.js import axios from axios; const request axios.create({ baseURL: http://localhost:8080/api, timeout: 5000 }); // 请求拦截器每个请求自动带上用户 ID request.interceptors.request.use(config { const userId localStorage.getItem(userId) || 1; config.params { ...config.params, userId }; return config; }); // 获取推荐商品列表 export function getRecommendList(size 10) { return request.get(/recommend/${localStorage.getItem(userId) || 1}, { params: { size } }); }逻辑说明axios 拦截器里统一塞userId是为了避免每个页面组件里都重复写这个参数。timeout: 5000是 5 秒超时——推荐接口如果 5 秒内没返回前端直接提示「推荐加载失败」不要无限转圈。baseURL如果走前后端同源部署Vue 打包后放进 Spring Boot 的static目录这里可以写空字符串用相对路径/api就行。页面组件里调用推荐接口渲染商品卡片列表template div classrecommend-section h2猜你喜欢/h2 div v-ifloading classloading推荐加载中.../div div v-else-ifproductList.length 0 classproduct-grid div v-forproduct in productList :keyproduct.id classproduct-card router-link :to/product/${product.id} div classproduct-title{{ product.title }}/div div classproduct-price{{ product.price }}/div /router-link /div /div div v-else classempty暂无推荐商品/div /div /template script setup import { ref, onMounted } from vue; import { getRecommendList } from ../api/recommend; const productList ref([]); const loading ref(true); onMounted(async () { try { const res await getRecommendList(10); productList.value res.data.data; } catch (err) { console.error(推荐接口请求失败, err); productList.value []; } finally { loading.value false; } }); /script这是组合式 API 的写法ref定义响应式变量onMounted是组件挂载后的生命周期。要注意res.data.data这个双层的结构——第一层data是 axios 的响应体第二层data是我后端Result.success()里放的业务数据。前后端分离项目经常有人在这个字段名上翻车建议前后端约定好统一用result或者统一用data别混用。3.5 用 Postman 跑通接口联调验证推荐链路完整可用代码写完后先不要急着启动前后端用一个最小数据集验证推荐逻辑是否正确。我在sql目录里放一组测试数据构造了 3 个用户、5 个商品行为如下用户行为商品ID1加购、下单1、22加购1、33浏览4从共现矩阵可以推出来商品 1 与商品 2 在用户 1 的行为里共现商品 1 与商品 3 在用户 2 的行为里共现。那么给用户 3 推荐时因为他浏览过商品 4而商品 4 没有跟任何商品共现系统会降级到热门推荐比如商品 1。这样就能验证冷启动降级逻辑是否生效。启动后端mvn spring-boot:run启动前端npm run dev打开 Postman 请求GET http://localhost:8080/api/recommend/3?size5返回的 JSON 里应包含商品 1 和商品 2 的信息。如果返回结果是空数组或者报 500说明前面某个环节遗漏了——排查顺序是先看数据库行为数据有没有插入成功再看 Mapper 能不能查到数最后看接口缩进有没有配错。4. 实战避坑记录Spring Boot 与 Vue 联调里的 5 个高频问题4.1 Spring Boot 版本太高导致启动失败我遇到过不少照着教程搭项目的人上来就用 Spring Boot 3.x JDK 17结果spring.factories文件不生效、javax包全部变成jakarta代码里一堆红色报错。这个标题对应的实现方案是 2021-2023 年间的经典结构那时候主流的 Spring Boot 版本是 2.5.x 到 2.7.x配合 JDK 8 或 11 最稳妥。如果你用 3.x不仅要改所有import javax.*为import jakarta.*而且很多第三方 starter 还没有跟进适配MyBatis-Plus 的旧版本直接跑不起来。正确的做法是创建项目时指定版本spring initializr里选 Spring Boot 2.7.18这是 2.x 系列的最终维护版本坑最少。如果你的需求确实需要 3.x那 MyBatis-Plus 要用 3.5.3 以上的版本且所有涉及javax.servlet的依赖都要替换成jakarta.servlet。新手我建议别折腾。4.2 vue 打包放进 Spring Boot 后刷新 404这个问题的标准说法是Vue Router 用了history模式打包后的index.html里路由是/product/3这样的路径。前端资源放到 Spring Boot 的src/main/resources/static目录后Spring Boot 内置的 Tomcat 找不到/product/3这个物理文件于是返回 404。原因是 history 路由模式依赖服务器把所有路径都重写到index.html而 Spring Boot 默认不做这个转发。两个解法任选其一。方案一后端加转发配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { // 前端路由直接访问时的 fallback转发到 index.html registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }方案二前端路由改用 hash 模式createWebHashHistory()。hash 模式下 URL 变成/#/product/3浏览器不会向服务器发起对/product/3的请求自然不会 404。缺点是不好看但在内网管理系统里完全可以接受。我的习惯是做对外官网型项目用 history 后端转发做内部管理后台直接用 hash省事。4.3 CORS 跨域前端端口和后端端口互相不认前后端分离开发时前端跑在http://localhost:5173Vite 默认端口后端跑在http://localhost:8080前端通过 axios 发起请求时浏览器会拦截跨域请求控制台报Access-Control-Allow-Origin错误。这不是后端代码的问题而是浏览器同源策略在拦路。解决跨域最稳妥的方式是在后端加全局 CORS 配置而不是在前端代理上绕。Spring Boot 2.7 里实现如下Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); // 允许所有来源生产环境建议换成具体域名 config.addAllowedMethod(*); // 允许所有 HTTP 方法 config.addAllowedHeader(*); // 允许所有请求头 config.setAllowCredentials(true); // 允许携带认证凭证 UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }参数说明addAllowedOriginPattern(*)和setAllowCredentials(true)不能同时用addAllowedOrigin(*)因为 Spring 5.3 之后规定带凭证的请求不能匹配通配符来源。如果你只配了前端开发地址http://localhost:5173那上线后线上域名又会跨域所以开发阶段用*最省心上线前再收紧。4.4 推荐结果只推荐热门商品个性化完全失效这个问题很隐蔽特征是所有用户拿到的推荐列表一模一样都是销量最高的那几件。排查下来八成是相似度计算里把浏览行为也当成正反馈参与了共现矩阵。浏览是最廉价的行为用户可能只是误点、或者被标题吸引但发现详情页货不对板。如果浏览和下单在权重上没有差异热销商品会通过大量浏览行为占据共现矩阵的绝大多数高分千人一面的结果就出现了。解决方法是给行为权重拉开差距下单权重 10、加购权重 5、浏览权重 1并且在构建共现矩阵时跳过浏览行为只统计加购和下单。另一个补充手段是给热门商品做降权比如把每个商品的共现次数再除以sqrt(该商品的全局出现次数)这样热门商品的相似度分数会被拉低长尾商品才有机会露出。4.5 用户没有行为数据时接口返回空列表前端白屏冷启动问题在真实项目里特别常见新用户注册后什么操作都没有推荐接口返回空数据前端v-for渲染空数组页面留白。更严重的情况是用户行为表里确实查不到记录SQL 没问题但接口就因为空结果直接抛了 NPE——products.subList(0, size)当products为空时直接报错。遇到冷启动常规做法有两个代码里都要兜住。第一层是接口层降级检测到推荐列表为空时返回「最新上架」或者「评分最高」的商品列表兜底。第二层是前端兜底productList.value []之后模板里渲染「暂无推荐逛点别的吧」这样一句话配合返回热门商品保证页面永远有东西可看。我见过太多系统因为这一层没做被测试提了个「新用户首页空白」的致命 bug。5. 落地后的推荐质量验证两个比看代码更值得投入的手段系统的推荐接口跑通只能说明技术链路通了但推荐得好不好还需要量化验证。这个环节直接决定你的系统能不能从「能跑」变成「有用」。第一个手段是离线指标验证。在行为数据里按时间切一刀前 70% 的数据做训练集后 30% 做测试集。用训练集跑推荐算法生成每个用户的 Top-10 推荐列表然后看测试集里用户真实产生行为的商品有多少出现在推荐列表里。这个比例叫做「命中率」。举例测试集里有 100 个用户产生了下单行为其中 35 个用户下单的商品出现在推荐列表里命中率就是 35%。一般来说一个电商推荐系统的离线命中率在 10% 到 30% 之间算正常低于 5% 说明算法或数据有问题。把这段逻辑写在RecommendEvaluator类里作为一个独立的测试入口每次改动推荐算法后跑一遍比眼睛看推荐结果可靠得多。第二个手段是在线效果对比。把用户流量分成两份一份走推荐算法一份走原来的「按销量排序」的兜底逻辑对比两组用户的点击率和下单转化率。这个过程不用自己搭 AB 测试平台参考开源做法手动实现在推荐接口返回时加一个strategy字段标记是recommend还是hot前端埋点把曝光和点击都打上这个标记一周后拉数据看哪个策略的转化率高。我自己的习惯是至少跑两周再下结论——新推荐位有新鲜感第一周数据虚高很正常。最后说一个我从实践里悟出来的经验推荐系统最容易翻车的环节不是算法不够高级而是数据质量太脏——行为日志丢失、商品类目混乱、用户 ID 不统一。我吃过一次大亏线上推荐准确率突然暴跌排查了一个多星期最后发现是埋点代码上线后有个分支把浏览行为重复上报了两次共现矩阵被脏数据带偏。所以这个项目真正值得你多花时间的不是琢磨更花哨的深度学习模型而是先把行为数据的采集、清洗、监控做好。数据地基稳了一个简单的 ItemCF 就能跑出不错的效果。希望帮到你。本文还有配套的精品资源点击获取