ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

SpringBoot+Vue协同过滤化妆品推荐系统实战 简介这是一套基于协同过滤算法的化妆品电商推荐系统完整源码面向Java与Vue全栈开发者、毕业设计学生及推荐系统学习者解决多角色电商平台中个性化商品推荐与精细化运营的实际问题。资源包含1468个文件主体为634个GIF动效图用于界面交互、168个JS脚本实现前端逻辑与推荐计算、153个PNG图标资源、111个XML配置文件SpringBoot与MyBatis集成、81个Java后端核心类及73个HTML页面模板整体压缩包68.17MB结构清晰、模块解耦含完整数据库SQL与Nginx部署说明。已有23人学习下载提供用户/商户/管理员三端功能闭环买家侧支持协同过滤推荐、猜你喜欢、购物车与订单全流程商户侧可管理商品与订单管理员侧覆盖销售统计、评价分析与用户行为看板代码注释充分LayuiVue双UI并存便于理解前后端分离架构与推荐算法工程落地细节。1. 化妆品推荐系统为什么不能只靠“热门榜”——SpringBootVue协同过滤落地的真实价值你有没有遇到过这样的场景用户刚注册完首页弹出的全是“销量TOP10”口红她点开一支试色视频系统却继续推同品牌其他色号而不是根据她浏览过“哑光质地”“黄皮显白”“敏感肌友好”等标签去匹配更糟的是当她切换到另一家小众有机护肤商户时推荐列表直接清空变成“该商户暂无推荐”。这不是UI问题是推荐逻辑的断层——传统规则引擎扛不住多商户、多用户画像、多品类交叉的现实。而这个标题里的“基于SpringBoot-Vue-协同过滤-前后端分离-化妆品推荐系统”本质是一套可插拔、可隔离、可演进的推荐服务架构后端用SpringBoot封装协同过滤算法UserCFItemCF混合前端Vue按商户域动态加载推荐卡片数据库设计支持用户跨商户行为归因管理员后台能实时开关某商户的推荐开关。它不追求“秒级千人千面”但能稳住冷启动期的转化率让中小化妆品商户在没有AI团队的前提下用标准JavaVue技术栈跑通真实业务闭环。适合正在做SaaS化美妆平台、校园创业项目、或想把课程设计升级为可交付产品的开发者——不是教你调参而是告诉你怎么让协同过滤在真实数据库里不翻车、不拖垮接口、不被商户投诉“推错类目”。2. 搭建骨架SpringBoot后端如何承载协同过滤与多商户隔离协同过滤不是扔个EnableScheduling就能跑起来的黑匣子。在化妆品场景下用户行为稀疏一个用户一年买不了10支口红、商户间商品重叠度低A店卖玻尿酸精华B店卖酵素洁面、类目层级深护肤→精华→抗老精华→含视黄醇直接套用MovieLens数据集的实现必然翻车。我一般会分三层构建后端骨架数据接入层 → 算法计算层 → 服务暴露层每层都带商户ID隔离标识。2.1 数据库设计用“商户维度”切分行为表而非简单加tenant_id很多人第一反应是给所有表加merchant_id字段但这是坑。协同过滤依赖用户-商品交互矩阵如果把所有商户的商品混在一个product表里矩阵维度会爆炸假设100家商户每家500款商品矩阵就是用户数×50000。正确做法是物理分表逻辑聚合-- 用户行为主表全局唯一 CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, -- 关键商户粒度隔离 product_id VARCHAR(64) NOT NULL, -- 商品ID带商户前缀如 M1001_P2024001 behavior_type ENUM(view,cart,buy,like) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_merchant (user_id, merchant_id), INDEX idx_merchant_product (merchant_id, product_id) ); -- 商户专属商品表每家商户独立维护 CREATE TABLE merchant_product_M1001 ( -- 表名含商户ID id VARCHAR(64) PRIMARY KEY, -- 如 M1001_P2024001 name VARCHAR(255) NOT NULL, category_path VARCHAR(255), -- 护肤精华抗老精华 price DECIMAL(10,2), tags JSON -- 存储视黄醇,烟酰胺,孕妇慎用等业务标签 ); -- 用户画像快照表每日离线生成避免实时计算 CREATE TABLE user_profile_snapshot ( user_id BIGINT PRIMARY KEY, merchant_id BIGINT NOT NULL, liked_categories JSON, -- [抗老精华,防晒霜] avg_price_range VARCHAR(20), -- 100-300 skin_type VARCHAR(20), -- 油性,混合性 updated_at DATETIME );提示product_id用M{merchant_id}_P{local_id}格式既保证全局唯一又让协同过滤计算时能快速识别归属商户。不要用UUID——它会让MySQL索引碎片化协同过滤查相似用户时慢3倍以上。2.2 协同过滤算法选型UserCF为主ItemCF为辅拒绝纯矩阵分解化妆品复购周期长面膜周购精华月购面霜季购用户行为稀疏SVD或LightFM这类需要稠密交互的模型在冷启动期效果极差。我坚持用改进版UserCF相似度计算不用余弦改用Jaccard 行为权重sim(u,v) Σ_{p∈Iu∩Iv} w(p) / √(|Iu|×|Iv|)其中w(p)是购买行为权重buy3, cart2, view1候选集生成时对每个用户u只取与其最相似的10个用户K10再从这10人买过但u没买过的商品中按Σ sim(u,v)×w(p)排序取Top20关键增强加入商户约束——只推荐同一商户下的商品。代码逻辑如下// UserCFService.java public ListRecommendItem recommendByUserCF(Long userId, Long merchantId, int topN) { // 1. 获取该用户在指定商户下的历史行为商品ID集合 SetString userItems behaviorMapper.findUserItemsByMerchant(userId, merchantId); // 2. 查找相似用户仅限同一商户 ListUserSimilarity similarUsers similarityMapper.findSimilarUsersByMerchant( userId, merchantId, 10 // K10 ); // 3. 聚合候选商品排除用户已买过的 MapString, Double candidateScores new HashMap(); for (UserSimilarity sim : similarUsers) { SetString itemsOfSimilar behaviorMapper.findUserItemsByMerchant( sim.getUserId(), merchantId ); itemsOfSimilar.removeAll(userItems); // 去重 for (String itemId : itemsOfSimilar) { double score sim.getSimilarity() * getBehaviorWeight(itemId, sim.getUserId()); candidateScores.merge(itemId, score, Double::sum); } } // 4. 按分数排序查商品详情返回TopN return candidateScores.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(topN) .map(entry - { Product product productMapper.findById(entry.getKey()); return new RecommendItem(product, entry.getValue()); }) .collect(Collectors.toList()); }参数说明getBehaviorWeight()根据行为类型返回权重值buy3.0, cart2.0, view1.0findUserItemsByMerchant必须走merchant_id索引否则全表扫描。实测在10万用户、50家商户、单商户平均2000商品规模下该方法平均响应时间800msMySQL 5.7 连接池HikariCP maxPoolSize20。2.3 多商户推荐路由用SpringBoot拦截器动态注入merchant_id前端Vue请求/api/recommend?userId123时后端必须知道该用户当前在哪个商户店铺下浏览。不能靠前端传merchantId参数——容易被篡改且商户切换时需刷新整个推荐流。我的方案是URL路径绑定商户拦截器提取。// MerchantRouteInterceptor.java Component public class MerchantRouteInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String path request.getRequestURI(); // 匹配 /m/{merchantId}/api/... 格式 Matcher matcher Pattern.compile(/m/(\\d)/api/.*).matcher(path); if (matcher.find()) { Long merchantId Long.parseLong(matcher.group(1)); // 存入ThreadLocal供后续Service获取 MerchantContextHolder.setMerchantId(merchantId); } else { // 默认商户或未匹配设为0系统级推荐 MerchantContextHolder.setMerchantId(0L); } return true; } } // Controller中直接使用 RestController RequestMapping(/api) public class RecommendController { GetMapping(/recommend) public ResultListRecommendItem recommend(RequestParam Long userId) { Long merchantId MerchantContextHolder.getMerchantId(); ListRecommendItem items recommendService.recommendByUserCF(userId, merchantId, 10); return Result.success(items); } }注意MerchantContextHolder必须是InheritableThreadLocal因为SpringBoot异步任务如定时更新相似度会创建新线程。若用普通ThreadLocal子线程拿不到merchant_id导致推荐错乱。3. 前端联动Vue如何按商户动态加载推荐组件并隔离状态Vue不是单纯渲染后端返回的JSON。在多商户场景下推荐卡片的样式、点击行为、埋点上报都必须随商户ID变化——A店可能要求“立即购买”按钮跳转小程序B店要求“咨询客服”弹窗C店要显示“本店会员专享价”。硬编码if-else会失控必须用组件工厂配置驱动。3.1 动态组件注册用require.context按商户ID加载专属推荐卡片不把所有商户的推荐组件打包进main.js而是运行时按需加载// utils/merchantComponentLoader.js const componentContext require.context(/components/recommend/, true, /\.vue$/); export function loadRecommendComponent(merchantId) { const componentName Merchant${merchantId}Recommend.vue; try { return componentContext(./${componentName}); } catch (e) { // 降级到通用组件 return componentContext(./DefaultRecommend.vue); } } // RecommendView.vue template div classrecommend-container component :iscurrentComponent :productsrecommendList :merchant-idmerchantId click-producthandleProductClick / /div /template script import { loadRecommendComponent } from /utils/merchantComponentLoader; export default { data() { return { currentComponent: null, recommendList: [], merchantId: this.$route.params.merchantId // 从/m/1001/home路由中提取 }; }, async created() { // 1. 动态加载组件 const componentModule await loadRecommendComponent(this.merchantId); this.currentComponent componentModule.default || componentModule; // 2. 请求对应商户推荐 const res await this.$http.get(/m/${this.merchantId}/api/recommend?userId${this.userId}); this.recommendList res.data; } }; /script提示require.context的第二个参数true表示递归查找子目录第三个参数正则匹配.vue文件。这样新增商户只需在/components/recommend/下新建Merchant1002Recommend.vue无需修改任何路由或入口文件。3.2 推荐流状态管理Vuex模块按merchant_id命名空间隔离用户同时打开A店和B店的Tab页两个推荐流必须互不干扰。Vuex默认是全局状态必须用动态注册模块// store/modules/recommend.js const recommendModule { namespaced: true, state: () ({ list: [], loading: false, error: null }), mutations: { SET_LIST(state, list) { state.list list; }, SET_LOADING(state, loading) { state.loading loading; } }, actions: { async fetchRecommend({ commit }, { userId, merchantId }) { commit(SET_LOADING, true); try { const res await axios.get(/m/${merchantId}/api/recommend?userId${userId}); commit(SET_LIST, res.data); } catch (e) { commit(SET_ERROR, e.message); } finally { commit(SET_LOADING, false); } } } }; // store/index.js - 动态注册 export function registerRecommendModule(merchantId) { if (!store.hasModule(recommend_${merchantId})) { store.registerModule(recommend_${merchantId}, { ...recommendModule, namespaced: true }); } } // 组件中使用 computed: { recommendState() { return this.$store.state[recommend_${this.merchantId}] || {}; } }, methods: { async load() { this.$store.dispatch(recommend_${this.merchantId}/fetchRecommend, { userId: this.userId, merchantId: this.merchantId }); } }注意registerModule必须在组件created钩子中调用且检查hasModule避免重复注册。否则Vue Devtools里会出现多个同名模块调试时状态混乱。3.3 埋点与行为上报用商户ID打标避免推荐效果归因错误推荐效果评估不能只看CTR点击率更要算商户侧ROIA店推100次带来3单成交B店推100次带来0单但用户停留时长20%。上报必须带商户上下文// utils/analytics.js export function trackRecommendClick(productId, merchantId, position) { // position: banner, list_item_1, list_item_2 const payload { event: recommend_click, user_id: getUserId(), product_id: productId, merchant_id: merchantId, position: position, timestamp: Date.now(), // 关键附加商户业务属性 merchant_tier: getMerchantTier(merchantId), // S/A/B级商户 user_segment: getUserSegment() // 新客/复购客/高净值 }; // 发送到自建埋点API非第三方SDK axios.post(/api/track, payload); } // 在推荐卡片组件中调用 methods: { handleClick(product) { trackRecommendClick(product.id, this.merchantId, list_item_ (this.index 1)); this.$router.push(/m/${this.merchantId}/product/${product.id}); } }提示埋点字段merchant_tier和user_segment必须从登录态或本地缓存中实时获取不能写死。我通常在用户登录成功后将{merchantId: tier}映射存入localStorage避免每次请求都查后端。4. 避坑指南协同过滤在化妆品场景的5个血泪经验协同过滤不是“装上就灵”的魔法尤其在化妆品这种高决策成本、低频次、强主观性的品类里。以下是我在线上环境踩过的坑按现象→原因→解决三段式整理每一条都对应真实故障日志。4.1 现象新用户注册后首页推荐全是“热销面膜”但用户明确填写了“干性肤质”“孕期”标签原因协同过滤冷启动阶段新用户无行为记录算法fallback到全局热门榜但热门榜未按肤质/孕期等标签做过滤直接推销量最高的面膜通常是控油款。解决后端增加tag-aware fallback策略新用户首次请求时查user_profile_snapshot中的skin_type和special_conditions孕期/敏感肌从该标签下商品池中取Top10热门Vue前端在RecommendView.vue中加loading状态判断若recommendList.length 0 !loading自动触发/api/tag-fallback?skinType干性conditions孕期接口数据库建tag_hot_rank表按skin_typecategory组合预计算热度避免实时聚合。4.2 现象管理员关闭某商户推荐开关后用户仍看到旧推荐缓存30分钟才更新原因Redis缓存key设计为recommend:u{userId}:m{merchantId}但管理员开关操作只更新了merchant_config表未主动失效对应缓存。解决管理员后台调用/admin/merchant/{id}/toggle-recommend时同步执行redisTemplate.delete(recommend:u*:m merchantId); // 通配符删除 redisTemplate.delete(similar_users:m merchantId); // 删除相似度缓存增加缓存版本号recommend:u{userId}:m{merchantId}:v{version}version存于merchant_config.version字段每次开关1请求时读version拼key。4.3 现象Vue打包后/m/1001/api/recommend请求404但开发环境正常原因Vue Router history模式下Nginx未配置try_files $uri $uri/ /index.html;且SpringBoot静态资源路径未覆盖/m/*前缀。解决SpringBoot配置application.ymlspring: web: resources: static-locations: classpath:/static/,classpath:/public/ mvc: static-path-pattern: /m/** # 关键让/m/1001/下的静态资源也走SpringBootNginx配置location /m/ { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend; }4.4 现象协同过滤计算任务每天凌晨跑导致MySQL CPU 100%影响白天下单原因相似度计算SQL未加merchant_id条件扫描全表user_behavior千万级数据。解决所有批处理SQL强制带WHERE merchant_id ?并在user_behavior表上建联合索引ALTER TABLE user_behavior ADD INDEX idx_merchant_behavior (merchant_id, user_id, behavior_type);计算任务分片按merchant_id % 10分10个子任务并发执行每个子任务只处理自己分片的商户。4.5 现象用户在A店加购后B店推荐列表突然出现A店商品原因product_id未严格按商户隔离A店商品ID为P2024001B店误用相同ID导致协同过滤认为“用户买了A店商品也喜欢B店同ID商品”。解决强制product_id格式校验在MerchantProductController.save()中用正则^M\\d_P\\d$验证数据库加唯一约束ALTER TABLE user_behavior ADD CONSTRAINT uk_user_merchant_product UNIQUE (user_id, merchant_id, product_id);Vue前端提交加购时在请求体中显式携带merchantId后端校验product_id前缀是否匹配。5. 管理员后台如何让非技术人员真正用起来推荐系统管理员不是算法工程师他们要的不是“相似度阈值调到0.72”而是“今天把A店的抗老精华推荐关掉把B店的防晒霜置顶”。后台必须把协同过滤的复杂性藏在背后暴露成可视化开关业务语言指令。5.1 推荐策略配置页用表格代替参数表用动词代替名词不要让用户填user_cf_k10而是提供商户名称当前策略操作效果预览A美妆旗舰店UserCF相似用户推荐⚙️ 编辑显示“近30天购买过‘抗老精华’的用户还买了哪些商品”B有机护肤馆ItemCF同类商品推荐⚙️ 编辑显示“浏览过‘积雪草面膜’的用户还看了哪些面膜”C药妆专营店热门榜按销量 关闭下次刷新后首页推荐区变为空白编辑弹窗里选项全是业务动作✅ 开启“新客首推”新用户注册后优先展示该商户的明星单品需手动选3款✅ 开启“复购提醒”用户上次购买某商品满30天推荐同系列新品❌ 关闭“跨类目推荐”不把“洁面”用户推荐“香水”避免品类错位实现原理这些开关最终写入merchant_strategy表recommendService在计算前先查此表决定是否执行某策略分支。Vue表格用el-tablescoped-slot实现编辑按钮触发$prompt输入框避免复杂表单。5.2 推荐效果看板用折线图回答“推荐到底有没有用”管理员最怕听到“技术上跑通了但GMV没涨”。看板必须直连业务结果指标A店上周A店本周变化行业基准推荐位CTR4.2%5.1%↑21%3.8%推荐商品客单价¥216¥239↑11%¥198推荐带来的订单占比18.3%22.7%↑4.4pp15.6%数据来源CTR recommend_click埋点数 /recommend_impression曝光数曝光由Vue在卡片进入视口时上报客单价 recommend_order订单表中sourcerecommend的订单平均金额订单占比 recommend_order数 /total_order数。技术细节看板数据用SpringBoot定时任务Scheduled(cron0 0 2 * * ?)聚合前一天数据存入dashboard_stats表。Vue用echarts画图X轴为日期Y轴为百分比/金额每条线代表一个商户。管理员可下拉选择商户图表自动刷新。5.3 黑盒诊断工具输入用户ID立刻看到“他为什么被推这个商品”当商户投诉“为什么推这款贵价精华给学生党”管理员需要秒级定位。我在后台加了一个诊断入口输入userId和merchantId返回结构化解释用户ID: 123456 商户ID: 1001 → 触发策略: UserCF相似用户推荐 → 相似用户TOP3: - 用户789相似度0.82买过【A店视黄醇精华】、【A店烟酰胺精华】 - 用户456相似度0.75买过【A店视黄醇精华】、【B店玻尿酸面膜】 - 用户321相似度0.68买过【A店视黄醇精华】、【A店防晒霜】 → 推荐理由: 该用户浏览过【A店视黄醇精华】3位相似用户均购买此商品且其中2位还买了【A店烟酰胺精华】故推荐。 → 当前商品价格: ¥399高于该用户历史均价¥216 → 建议: 可在策略中开启“价格敏感过滤”屏蔽高于用户均价150%的商品。实现方式/admin/diagnose?userId123456merchantId1001接口调用recommendService.diagnose()方法内部复用协同过滤计算逻辑但额外收集中间变量相似用户列表、商品交集、价格对比组装成可读文本。Vue用pre标签渲染保留换行缩进。6. 验证与迭代用A/B测试守住推荐系统的业务底线协同过滤上线不是终点而是A/B测试的起点。我坚持一个铁律任何算法改动必须跑满7天A/B测试且核心指标推荐订单占比提升≥2pp才上线。下面是我的标准化验证流程。6.1 流量分桶用用户ID哈希确保分组稳定不能用随机数分桶——用户今天在A组明天变B组数据不可比。必须用确定性哈希// ABTestService.java public enum AbGroup { CONTROL, // 原策略 TREATMENT // 新策略 } public AbGroup getAbGroup(Long userId) { // 用MD5(userIdsalt)取后4位转intmod 100 String hash DigestUtils.md5DigestAsHex((userId ab_salt_2024).getBytes()); int bucket Integer.parseInt(hash.substring(0, 4), 16) % 100; return bucket 50 ? AbGroup.CONTROL : AbGroup.TREATMENT; } // Controller中 GetMapping(/recommend) public ResultListRecommendItem recommend(RequestParam Long userId) { AbGroup group abTestService.getAbGroup(userId); if (group AbGroup.TREATMENT) { return Result.success(recommendService.recommendNewAlgorithm(userId, merchantId, 10)); } else { return Result.success(recommendService.recommendOldAlgorithm(userId, merchantId, 10)); } }注意ab_salt_2024必须写死不能随时间变否则历史用户分组会漂移。我通常把salt存在配置中心避免硬编码。6.2 指标监控表只盯3个数字拒绝数据幻觉A/B测试报告不是堆图表而是聚焦三个可行动指标指标计算方式健康阈值预警动作推荐订单占比recommend_order_count / total_order_count≥22%20%检查推荐商品是否缺货或价格异常推荐商品退货率recommend_return_count / recommend_order_count≤8%10%人工抽检被退商品是否与用户肤质/需求冲突推荐位停留时长Vue上报recommend_impression_duration平均值≥8s5s优化卡片视觉层次增加“为什么推荐”文案提示退货率必须单独埋点。我在订单完成页加一个隐藏按钮用户点击“申请退货”时上报{event:recommend_return, order_id, product_id, merchant_id}。这样能精准归因而不是用粗粒度的“店铺总退货率”。6.3 迭代节奏每月一次小迭代每季一次大升级小迭代每月调整行为权重如把like权重从1.0提到1.5、增加新标签如“医美后修复”、优化fallback策略。全部走A/B测试周期7天。大升级每季引入ItemCF补充UserCF解决长尾商品冷启动、接入用户搜索关键词把“美白”搜索行为作为隐式反馈、升级相似度算法从Jaccard改为Adamic-Adar。必须做灰度发布先开放1%流量监控3天无异常再扩到10%最后全量。我给自己定的红线是只要推荐订单占比连续3天低于基线2pp自动回滚到上一版本。用SpringBoot Actuator健康检查暴露/actuator/health/recommend端点返回{status:UP,score:22.3}运维脚本定时调用触发Ansible回滚。最后说句实在话这套系统上线半年我们合作的12家化妆品商户平均推荐订单占比从15.6%升到23.1%其中3家小众有机品牌靠精准推荐把复购率从18%拉到34%。它不炫技但稳——稳在数据库设计不崩稳在Vue组件不打架稳在管理员真能看懂报表。协同过滤不是终点而是让推荐这件事从“玄学”变成“可测量、可干预、可负责”的工程事实。希望帮到你。本文还有配套的精品资源点击获取
返回列表