简介这套项目是一份完整的电商精准营销推荐系统源码开发语言为 Java后端框架使用 Spring Boot前端则采用 Vue.js 构建实现了前后端分离架构内容面向正在学习 Java 全栈技术的学生和开发者适合作为毕业设计、课程设计、大作业或工程实训的起点项目。压缩包中共有 525 个文件整体大小约 13.44MB其中包含 159 个 Java 文件对应控制层、服务层和实体类等后端代码119 个 Vue 文件连同 JavaScript、CSS、JSON 等资源构成前端页面另有 SQL 数据库脚本、XML/YML 配置文件以及 Markdown、Word 格式的说明文档目录划分清晰。系统运行环境为 JDK8、MySQL 5.7、Tomcat7、Maven 3.3.9配合 Eclipse 或 IDEA 即可导入调试当前已有 103 人学习使用。源码经过精心调试能够直接运行数据库脚本可快速完成建库初始化代码分层清楚、模块边界明确便于理解 Spring Boot 与 Vue 之间的接口调用和数据传递也方便在此基础上升级推荐策略或扩展其他电商营销功能。1. 电商精准营销推荐系统它到底解决什么问题凭什么用 Spring Boot Vue做电商推荐系统的人最容易犯的一个错是一上来就盯着算法模型觉得协同过滤跑通了项目就成了一半。真上线才发现算法只占一半价值。数据没埋对、规则卡太死、接口响应慢任何一环掉链子都能让推荐变成“鸡肋”。基于 Java 技术的电商精准营销推荐系统就是围绕 Spring Boot 做后端服务、Vue 做前端展示的一整套工程落地采集用户行为、构建商品画像用 ItemCF 协同过滤做召回再通过价格带、会员等级、品类偏好等营销规则做过滤和排序最后把“猜你喜欢”变成带推荐理由的精准商品流。这套方案适合作为毕业设计选题也适合中小型电商团队搭推荐系统第一版。下面按数据层、算法层、接口层、前端层和部署验证逐步拆开说。2. 先把数据层立住商品画像、用户行为和埋点上报要怎么设计第一版推荐系统到底建几张表通常取决于你想怎么跑算法。很多项目资料里写了十几张表真正让推荐引擎跑起来的核心其实只有三张商品表、用户画像表、用户行为表。订单信息可以从行为表里的“下单”记录关联出来没必要在推荐库里再放一份业务订单模型营销规则也可以独立成配置在接口层做过滤就够了。这一章的重点不是建表本身而是想清楚哪些字段对推荐算法有意义。商品表里加不加会员价、用户表里放不放地域编码都会直接影响后面推荐排序的质量。下面这两张表是我在多个类似项目里反复调过的结构照抄可以减少不少返工。2.1 商品表和用户画像表字段怎么建才不返工先看商品表。推荐系统关心的是商品能不能被匹配、能不能被排序而不是财务关心的一堆库存批次。所以核心字段应该是品类、价格、标签、上下架状态而不是把采购信息全塞进来。CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL COMMENT 商品标题, category_id BIGINT NOT NULL COMMENT 三级品类ID冗余到商品表避免推荐时频繁连表, price DECIMAL(10,2) NOT NULL COMMENT 当前售价推荐排序要用, tags VARCHAR(500) COMMENT 逗号分隔的标签如新鲜,进口,低脂, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品基础表;category_id 一定要存 ID 而不是中文名。因为品类一旦改名存名字会导致历史和推荐结果全部对不上存 ID 后品类名变化只是字典表的事。price 字段要的是用户实际看到的展示价如果系统里有营销价、券后价推荐引擎排序时也应该以这个价格为准否则用户看到的价格和推荐依据对不上体验会很怪。tags 用逗号分隔是第一版可以接受的方案标签数量不多时没必要上 JSON 字段查询反而更直观。用户画像表的字段选择比商品表更容易被忽略。精准营销推荐有个特性同一个用户在不同会员等级下看到的推荐位优先级应该不同。金卡用户对 9.9 包邮商品不敏感普通用户对高客单价商品有压力如果不记录会员等级排序阶段就做不了这种分层。CREATE TABLE user_profile ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL, member_level TINYINT NOT NULL DEFAULT 1 COMMENT 1普通 2银卡 3金卡, region_code VARCHAR(16) COMMENT 地域编码用于地域化推荐, preferred_category VARCHAR(200) COMMENT 新用户注册时选择的兴趣品类, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户画像表;member_level 不是只给运营看的推荐排序里可以直接用它做权重修正。region_code 的价值在大促场景特别明显南方用户对除湿机更敏感北方用户更关注加湿器同样的协同过滤结果加上地域权重后点击率会有可感知的提升。preferred_category 则是为冷启动准备的新用户没有行为数据时这是推荐引擎唯一能用的信号。这两张表建完后不要过度设计。第一版就用单表宽字段别急着拆多级分类表、标签中间表等推荐逻辑稳定了再优化也不迟。2.2 用户行为表和埋点上报浏览、加购、下单怎么进推荐引擎行为数据是推荐的原材料。ItemCF 协同过滤本质上只消费三元组哪个用户、在什么时间、对哪件商品产生了什么行为。所以行为表设计得干不干净直接决定后面算法代码能简化到什么程度。CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, item_id BIGINT NOT NULL, behavior_type TINYINT NOT NULL COMMENT 1浏览 2收藏 3加购 4下单, scene VARCHAR(20) COMMENT 来源场景home_page/detail/search/order_list, create_time DATETIME NOT NULL COMMENT 行为时间由服务端写入, KEY idx_user_time (user_id, create_time), KEY idx_item_time (item_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户行为日志表;行为类型为什么分成四类而不是只记“点击”因为不同行为表达的兴趣强度完全不同。浏览可能是误触加购和下单基本意味着明确购买意图。排序阶段的行为权重就要依赖这个字段。scene 字段经常被省略但它在排查问题时非常关键同一件商品用户从搜索页点进来的还是从推荐位点进来的对推荐效果的归因是完全不同的。埋点接口在后端要做两件事一是用服务端时间覆盖前端时间二是从登录态解析用户 ID而不是信任前端传过来的 userId。我把这个 Controller 写出来这是第一版最简能用的形态。RestController RequestMapping(/api/log) public class BehaviorController { Resource private BehaviorMapper behaviorMapper; Resource private TokenService tokenService; PostMapping(/collect) public ResultVoid collect(RequestBody BehaviorDTO dto, HttpServletRequest request) { Long userId tokenService.resolveUserId(request); if (userId null) { return Result.fail(登录状态失效); } UserBehavior behavior new UserBehavior(); behavior.setUserId(userId); behavior.setItemId(dto.getItemId()); behavior.setBehaviorType(dto.getBehaviorType()); behavior.setScene(dto.getScene()); // 统一用服务器时间避免前端本地时间和服务器差出八个小时 behavior.setCreateTime(LocalDateTime.now()); behaviorMapper.insert(behavior); return Result.success(); } }把时间字段放到服务端写入是埋点设计里最重要的一条纪律。前端本地时间经常因为手机时区、用户手动调时间而失真一旦行为时间不准后面所有按时间衰减的排序都会跟着错。用户 ID 更明显前端传上来的 ID 可以被伪造必须从登录态里解析。第一版直接向 MySQL 逐条插入没问题秒级几千条完全扛得住。后面如果行为量涨到每天上百万条再考虑引入消息队列削峰或者改成批量上报接口。还有个实用做法前端 SDK 把行为攒 5 秒一次性打包上报并发压力会小一个量级。3. 推荐引擎核心在 Spring Boot 里实现 ItemCF 协同过滤的召回、加权和排序数据层解决的是“喂什么”这一章解决“怎么算”。电商推荐里最常用也最容易讲清楚的算法是 ItemCF也就是基于物品的协同过滤。它比 UserCF 适合电商的原因很朴素用户数量比商品数量大得多而且用户的兴趣是漂移的商品相对稳定物品之间的相似关系更新频率低缓存的收益更高。3.1 物品相似度计算共现矩阵与余弦相似度的 Java 实现ItemCF 的核心逻辑是如果很多用户都同时看过 A 和 B那 A 和 B 就相似。计算相似度之前先把用户行为按用户聚合把每个用户对同一商品的行为去重避免用户狂刷同一件商品导致 A-B 的共现次数虚高。/** * 输入近 30 天用户行为列表 * 输出itemA - (itemB - similarity) */ public MapLong, MapLong, Double calcItemSimilarity(ListUserBehavior behaviors) { MapLong, SetLong userItems new HashMap(); for (UserBehavior b : behaviors) { userItems .computeIfAbsent(b.getUserId(), k - new HashSet()) .add(b.getItemId()); } MapLong, MapLong, Integer coCount new HashMap(); MapLong, Integer itemFreq new HashMap(); for (SetLong items : userItems.values()) { ListLong list new ArrayList(items); for (Long itemId : list) { itemFreq.merge(itemId, 1, Integer::sum); } for (int i 0; i list.size(); i) { for (int j i 1; j list.size(); j) { Long a list.get(i); Long b list.get(j); coCount.computeIfAbsent(a, k - new HashMap()).merge(b, 1, Integer::sum); coCount.computeIfAbsent(b, k - new HashMap()).merge(a, 1, Integer::sum); } } } MapLong, MapLong, Double simMatrix new HashMap(); for (Map.EntryLong, MapLong, Integer entry : coCount.entrySet()) { Long itemA entry.getKey(); for (Map.EntryLong, Integer e : entry.getValue().entrySet()) { Long itemB e.getKey(); Integer co e.getValue(); double sim co / Math.sqrt( (double) itemFreq.getOrDefault(itemA, 0) * itemFreq.getOrDefault(itemB, 0)); if (sim 0.01) { simMatrix.computeIfAbsent(itemA, k - new HashMap()).put(itemB, sim); } } } return simMatrix; }这套实现里有几个参数是实际项目里的经验值。相似度阈值 0.01 用于砍掉那些共现次数很少、相似度极低的边矩阵体积能小一半以上。用户行为取近 30 天是平衡时效性和数据量之后的选择取 7 天数据太稀疏取 90 天又容易把用户早就不感兴趣的旧商品带回来。相似度计算不要每次用户请求都跑。常见做法是离线任务每天算一次把结果序列化到 RedisJava 侧直接读缓存。3.2 召回、加权和排序把行为权重调成推荐系统里的第一组玄学参数相似度矩阵准备好后推荐过程分成三步召回、过滤、排序。召回是找到用户最近感兴趣的物品的相似物品过滤是去掉用户已经买过的和下架的商品排序是把评分算出来取 Top N。行为权重是这套系统里最像“玄学”的参数但背后有清晰逻辑。权重不能照搬别人项目的值不同电商品类差异非常大。下面这张表是第一版常用的起点。行为类型behaviorType权重说明浏览11.0数量多但噪声大权重最低收藏22.0主动表达兴趣比浏览可靠加购33.0购买意向强烈下单45.0最强正反馈信号排序阶段还需要时间衰减。只看 30 天行为不代表 30 天内的行为价值相同昨天的浏览和 20 天前的加购对当前兴趣的代表性完全不一样。public ListLong recommendForUser(Long userId, int limit) { ListUserBehavior recent behaviorMapper.listByUserAndTime( userId, LocalDateTime.now().minusDays(30)); MapLong, Double scoreMap new HashMap(); for (UserBehavior b : recent) { MapLong, Double simItems simCache.getOrDefault( b.getItemId(), Collections.emptyMap()); if (simItems.isEmpty()) { continue; } double weight behaviorWeight.getOrDefault(b.getBehaviorType(), 1.0); long ageDays ChronoUnit.DAYS.between(b.getCreateTime(), LocalDateTime.now()); double timeDecay Math.exp(-ageDays / 7.0); double factor weight * timeDecay; for (Map.EntryLong, Double e : simItems.entrySet()) { Long candidateId e.getKey(); if (candidateId.equals(b.getItemId())) { continue; } scoreMap.merge(candidateId, e.getValue() * factor, Double::sum); } } ListLong boughtList behaviorMapper.findBoughtItems(userId); SetLong boughtSet new HashSet(boughtList); scoreMap.keySet().removeIf(boughtSet::contains); return scoreMap.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(limit) .map(Map.Entry::getKey) .collect(Collectors.toList()); }时间衰减的半衰期我一般设为 7 天。也就是说行为发生后第 7 天这个行为的贡献降到最初的 exp(-1) 也就是大约 0.37 倍。这个参数适合大多数电商场景生鲜类可以再缩短到 3 天因为补货周期短3C 数码类可以放宽到 14 天。评分累加时有两个容易忽略的细节。第一个是排除目标商品自身否则浏览最多的商品会把自己刷到推荐第一位。第二个是过滤的逻辑要在排序前做不推荐买过的商品是电商推荐系统的底线要求否则用户会认为系统“不看记录瞎推”。打分机制第一版用线性加权就够上线了不需要上机器学习排序模型。3.3 接口封装和缓存策略推荐接口的响应结构为什么要带 strategy推荐接口有两个设计细节直接影响前端联调体验。一个是返回结构要带 strategy 字段另一个是响应里必须拼上推荐理由。原因后面章节会细讲这里先把结构定下来。RestController RequestMapping(/api/recommend) public class RecommendController { Resource private RecommendService recommendService; Resource private TokenService tokenService; GetMapping(/list) public ResultRecommendPage list(RequestParam(defaultValue 20) Integer limit, HttpServletRequest request) { Long userId tokenService.resolveUserId(request); ListLong itemIds recommendService.recommendForUser(userId, limit); ListRecommendItem items itemIds.stream() .map(id - recommendService.assembleRecommendItem(id, userId)) .collect(Collectors.toList()); RecommendPage page new RecommendPage(); page.setItems(items); page.setStrategy(itemIds.isEmpty() ? hot : itemcf); return Result.success(page); } }strategy 字段用来告诉前端这次推荐走的是算法还是兜底策略。用户行为为空时后端走热门榜兜底前端可以根据 strategy 显示“热门推荐”而不是“猜你喜欢”避免展示逻辑和策略错位。这个字段排查线上问题价值特别大运维同学看日志时一眼就知道用户为什么看到的是这套内容。相似度缓存直接决定接口的响应快慢。我给这种情况的建议是两层缓存本地 Caffeine 缓存最容易热访问的那批相似度数据Redis 放全量相似度结果本地没命中再查 Redis。Spring Boot 里用 Cacheable 可以把缓存逻辑收敛到一行注解里操作成本不高。4. Vue 端把推荐结果做成商品流接口联调、推荐理由和降级展示怎么做后端输出的是商品 ID 加推荐理由Vue 端负责把数据变成用户愿意点下去的商品流。这个环节最容易被看轻实际上推荐位点击率高低前端展示占了很大因素。4.1 首页推荐流的组件拆分和数据加载推荐流不要做成一个大页面而是拆成两层最外层是推荐区域内层是单个商品卡片。这样后续要换推荐位、加展示位只需要替换商品卡片组件。template section classrecommend-section h2 classsection-title为你推荐/h2 div classproduct-grid product-card v-foritem in items :keyitem.itemId :productitem :reason-textitem.reason clickhandleItemClick(item) / /div div v-ifloading classloading-tip推荐加载中.../div div v-else-ifitems.length 0 classempty-tip button clickloadHot先看看热门商品/button /div /section /template script setup import { ref, onMounted } from vue; import { getRecommendList, getHotList } from /api/recommend; import ProductCard from /components/ProductCard.vue; const items ref([]); const loading ref(false); const loadRecommend async () { loading.value true; try { const res await getRecommendList({ limit: 20 }); items.value res.data.items; } catch (error) { // 后端挂了不能让用户看到空页面降级到热门列表 const hotRes await getHotList({ limit: 20 }); items.value hotRes.data.items; } finally { loading.value false; } }; const handleItemClick (item) { // 点击行为上报后续可以分析推荐位点击率 trackEvent({ type: click, itemId: item.itemId, scene: recommend_flow }); }; onMounted(loadRecommend); /script这段代码里有几个细节值得注意。catch 里做降级而不是直接报错这个逻辑必须放在最前面因为推荐接口依赖用户历史行为冷启动用户和接口异常都会导致拿不到数据前端不能把错误态直接抛给用户。trackEvent 上报点击行为不是可选项没有点击数据你没法验证推荐效果到底是好是坏。4.2 推荐理由渲染为什么用户更愿意点开带说明的推荐同样是推荐位带“因为您浏览过低脂牛奶”和只展示商品图点击率差出两个点并不奇怪。推荐理由给用户一个点进来的心理预期也减少了“为什么给我推这个”的困惑感。接口返回结构里建议带上这两个字段。{ strategy: itemcf, items: [ { itemId: 100123, title: 低脂纯牛奶 250ml*12, price: 39.9, reason: 因为您浏览过低脂牛奶, tags: [低脂, 早餐, 盒装] } ] }商品卡片组件里把 reason 单独放一行展示样式上和价格、标题做区分。template div classproduct-card click$emit(click, product) img :srcproduct.coverUrl :altproduct.title / div classtitle{{ product.title }}/div div classprice¥{{ product.price }}/div div v-ifreasonText classreason-tag{{ reasonText }}/div div classtags span v-fortag in product.tags :keytag{{ tag }}/span /div /div /template script setup defineProps({ product: { type: Object, required: true }, reasonText: { type: String, default: } }); defineEmits([click]); /script这里 reasonText 用单独 prop 而不是直接读 product.reason是为了让这个组件能被复用。热门榜场景也可以复用 ProductCard只是不传 reasonText 而已。4.3 axios 封装、CORS 预检和联调阶段的降级配置前后端分离联调时90% 的报错都集中在 CORS 上。浏览器对跨域 POST 请求且带 Content-Type: application/json 时会先发一个 OPTIONS 预检请求后端如果没有正确响应前端主请求根本发不出去。axios 封装这边建议这么做。import axios from axios; const http axios.create({ baseURL: import.meta.env.VITE_API_BASE || http://localhost:8080, timeout: 5000 }); http.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); export const getRecommendList (params) http.get(/api/recommend/list, { params });后端对应的 CORS 配置也要跟上。Spring Boot 里一个简单的配置类就能解决。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOrigins 不要写成 *因为 allowCredentials(true) 时会和通配符冲突浏览器会直接拒绝。maxAge 设置为 3600 秒是让浏览器缓存预检结果避免每个请求都先走一轮 OPTIONS联调时接口能明显感觉变快。如果项目里还集成了 Spring SecurityCorsConfigurationSource 要显式注册到 Security 的过滤链前面否则预检请求会被拦在安全认证之外这是我踩过的实实在在的坑。5. 常见问题和部署避坑这类系统跑不起来的 5 个排查点下面的问题都不是教科书里的理论是我在做类似 Spring Boot 加 Vue 推荐系统时真遇过的。每个都按现象、原因、解决三个步骤写排错时可以直接对着找。5.1 ItemCF 计算时 JVM 堆溢出服务直接挂掉现象第一次启动程序调用相似度计算接口后服务器 CPU 跑满控制台抛出 java.lang.OutOfMemoryError: Java heap space。原因共现矩阵虽然用了 HashMap 存稀疏数据但商品数量上万后商品对组合数是乘积级增长。默认堆内存不够时计算还没结束就 OOM 了。解决把相似度计算从在线请求中拆出去做成离线任务。启动时只加载序列化好的相似度结果文件到 Redis 或本地缓存不重复计算。如果你的数据量还没到需要离线计算的程度至少把 JVM 堆参数调大并在计算完成后立刻释放临时变量。我一般会在配置里加上 -Xmx2g 起步再大就得拆任务了。5.2 新用户推荐列表为空前端只能显示空态现象刚开始注册的新用户调用推荐接口items 数组是空的前端展示“暂时没有适合您的推荐”运营看到后以为系统坏了。原因ItemCF 的召回源是用户历史行为新用户一条行为都没有scoreMap 自然为空排序结果自然为空。解决在推荐服务里加兜底策略。行为为空时直接用热门榜替换热门榜可以按品类维度拆开至少让用户看到的是本店热卖而不是全站随机。if (scoreMap.isEmpty()) { return hotProductService.topByCategory( userProfile.getPreferredCategory(), limit); }5.3 埋点时间字段对不上推荐结果总是滞后大半天现象用户上午浏览了一件商品晚上打开首页推荐流里还是没有出现相关商品。查日志发现行为记录确实写进库了。原因详情页前端上报用的是浏览器本地时间有的用户手机时间快几分钟有的慢几个小时导致 create_time 字段出现“未来时间”。过滤条件查近 30 天时扫描范围没问题但时间衰减因子会因为异常时间变得不可预测。解决后端埋点接口统一用 LocalDateTime.now() 覆盖前端传值前端只上报行为类型和商品 ID。同时给 create_time 加一个检查如果发现时间比服务器还晚就自动改为服务器当前时间。5.4 前端联调时报 CORS error预检 OPTIONS 请求直接 403现象前端页面请求推荐接口Network 面板能看到 OPTIONS 请求返回 403主请求根本发不出去。单独用 curl 访问后端接口却是正常的。原因如果项目集成了 Spring SecurityCorsConfig 只配置了 WebMvcConfigurer但 Security 的过滤器链在 MVC 之前执行预检请求被安全认证拦截了。解决需要显式注册 CorsConfigurationSource并在 Security 配置里调用 http.cors()。我建议第一版干脆先不集成 Spring Security或者把不需要登录的接口放进白名单减少联调阶段的干扰项。5.5 Spring Boot 3.x 版本坑javax 包全部编译报错现象从网上下载的参考项目用的是 Spring Boot 2.x自己新建项目选了 Spring Boot 3.x把代码复制过来后发现一大堆 import 报错。原因Spring Boot 3.0 基于 Jakarta EE 9包名从 javax.servlet 变成了 jakarta.servlet。旧代码里所有 javax.servlet.http.HttpServletRequest 都需要换成 jakarta.servlet.http.HttpServletRequest。解决两个选择。一是继续使用 Spring Boot 2.7.x毕业设计和中小型项目完全够用二是花十分钟全局替换 import。注意不只是 servletvalidation、annotation 等很多包都换了命名空间。如果用到一些第三方 starter还要确认它是否发布了兼容 Boot 3 的版本。6. 效果怎么验证离线评测指标和分桶 A/B 的落地做法推荐系统最容易变成“黑匣子”功能上线了界面也推荐了但推荐到底准不准谁也说不清。我习惯在项目里放一个离线评测类把推荐结果和真实订单做对比用数字说话。离线评测最常用的指标是 PrecisionN。它计算推荐列表前 N 个商品里有多少个是用户真的下单的。public static double precisionAtN(ListLong recList, ListLong realBuyList, int n) { if (recList null || recList.size() 0) { return 0.0; } ListLong topN recList.size() n ? recList.subList(0, n) : recList; long hit topN.stream().filter(realBuyList::contains).count(); return (double) hit / n; }跑评测的时候取最近 7 天有下单行为的用户用他们上次下单之前的浏览记录作为输入生成推荐列表再去和这次真实下单的商品比对。这个逻辑写成一个测试类每次改完行为权重或者排序策略先跑一遍离线指标比上线后临时看数据要稳得多。在线验证建议做最轻量的分桶。按 userId 取模 10用户 ID 尾号是 0 到 2 的作为实验组走新的推荐策略尾号 3 到 9 的保持旧策略放一周后对比两组的推荐位点击率和下单转化率。这是不用引入复杂实验平台的低配做法但足够验证一次策略改动的效果。这套离线加在线的验证链路我在做推荐系统项目时是必留的。给用户看的推荐理由、给运营调的行为权重、给后端改的排序逻辑每一处改动都应该能对应到数字变化。说句实在话推荐系统做到最后比的不是算法多高深而是你能不能把每次改动带来的收益说清楚。我个人的习惯是每次提交代码前把离线评测脚本跑一遍把 PrecisionN 和覆盖率记在提交说明里。改完权重发现指标下降就立刻回退不抱侥幸心理。这样做最大的好处是项目做到后期你不会靠“感觉”判断推荐效果而是有一份自己都能看懂的数字记录。希望这篇笔记能帮你在 Spring Boot 和 Vue 的这套技术栈里把一个能上线、能量化效果的电商推荐系统从零搭出来。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?