资讯动态

协同过滤在白酒销售管理系统中的工程落地:SpringBoot+Vue+MySQL实践

发布时间:2026/10/22 3:43:24 来源:尧图企业网站定制
接手过几个销售管理系统后我越来越觉得协同过滤这四个字在不少项目里只是撑场面的概念真正能落地、能跑出可用结果的很少。这次要聊的这套白酒销售信息管理系统演示项目名黔醉酒业是少数让我觉得算法和业务真正咬合在一起的项目SpringBoot 做后端服务Vue 做前端管理界面MySQL 存业务数据整个系统导入订单数据后不但能做常规的进销存和客户管理还内置了一套基于用户行为的协同过滤推荐引擎可以直接给不同客户推荐他可能感兴趣的白酒商品。这套系统最吸引我的地方在于它不是为了炫技而塞一个算法进去而是切中了白酒销售的一个真实痛点——客户群体差异极大有人只认酱香有人偏爱浓香有人常年喝 100 元以内的口粮酒也有人专门找年份老酒。如果只是按销量排行榜推荐大概率会出现卖得最好的酒推荐给所有人但转化率并不高的尴尬局面。本文我会从项目结构、算法落地、数据库设计、启动排错这几个角度展开把整个系统拆开讲清楚适合正在做电商类毕业设计、中小型销售管理系统或者想在业务系统里引入推荐算法的开发者参考。1. 白酒销售场景下的推荐需求到底卡在哪里很多人在做管理系统时推荐模块往往是最后一个才考虑的加分项但白酒这种商品天然适合做个性化推荐因为它不是一个爆款通吃的品类。1.1 白酒订单数据与普通电商推荐的区别普通电商的推荐逻辑通常是看了又看、买了又买商品替换成本低用户今天买零食明天买数码行为序列非常杂。但白酒销售不一样它的订单数据有几个明显的特征客户复购周期长但忠诚度高。一个习惯喝某款口粮酒的客户可能连续半年都在买同一款中间偶尔换换口味。商品维度强结构化。香型酱香、浓香、清香、度数、价格带、年份、礼盒装还是光瓶酒这些属性对用户决策的影响很大。购买行为有场景属性。自己喝、请客、送礼、婚宴、商务宴请不同场景买的东西完全不同。同一个客户可能平时买 50 元的酒自己喝但一到过节就买 500 元的礼盒送人。这就意味着单纯按购买次数最多或者销售额最高去做推荐对白酒商品来说往往不精准。比如某款高端酱香酒销量很高但买它的人绝大多数是送礼场景把它推荐给一个平时只喝光瓶浓香酒的用户基本不会产生点击。1.2 推荐模块在销售管理系统里的位置这套系统的业务模块包括客户管理、商品管理、订单管理、库存管理、数据统计以及一个独立的推荐中心。推荐中心不是单独跑一个算法服务而是嵌套在订单模块和客户模块之间每当用户登录或者业务员查看客户详情页时后端会根据该客户的历史订单行为实时算出一份猜你喜欢酒品列表展示在客户详情页和首页的推荐位里。更实际的价值在于对业务员而言跟进老客户之前看一眼推荐列表就知道这客户最近可能对什么价位、什么香型的酒有需求带着目标去沟通成单概率比盲推高很多。这也是这套系统把协同过滤从技术演示变成业务工具的关键。2. 系统整体架构SpringBoot后端、Vue前端、MySQL三层怎么分工这套系统的架构选择很主流前后端分离后端 SpringBoot 负责业务接口和算法计算前端 Vue 负责管理界面展示MySQL 统一存储业务数据和推荐计算依赖的行为数据。2.1 后端服务与模块划分后端并不是一个大而全的单体应用直接把所有代码堆在一起而是按照业务域做了模块划分。我比较欣赏的是它把推荐引擎单独拆成了一个 service 包和订单、客户、商品这些 CRUD 模块平级。这样做的直接好处是如果后续想把算法替换成更复杂的模型或者把推荐计算搬到独立服务上只需要改这个包不影响其他业务接口。后端核心模块大致如下系统管理登录认证、用户权限、操作日志。客户管理客户档案、等级、联系人信息、购买历史。商品管理白酒商品档案包含香型、度数、价格、年份、库存等字段。订单管理订单创建、订单明细、订单状态流转、退货处理。统计报表销售额、品类销售分布、客户贡献度。推荐引擎读取用户行为数据计算相似度矩阵产出推荐列表。这里有个细节值得注意推荐引擎模块使用了策略模式接口层面定义了一个RecommendStrategy下面分别有基于用户的协同过滤实现、基于物品的协同过滤实现以及一个冷启动时使用的热门商品兜底实现。业务侧可以配置使用哪一种策略也可以组合使用。这个设计让算法不再是一段写死的代码而是可以按数据量和业务阶段灵活切换。2.2 前端管理端的页面组织前端采用标准 Vue 单页应用结构路由按后台管理端习惯组织。页面主要包括登录页、工作台首页、客户列表与客户详情、商品管理、订单管理、推荐结果预览等。推荐结果预览这个页面比较有意思它不是给客户看的而是给管理员和业务员做效果验证用的。页面左侧可以选择任意客户右侧展示推荐引擎算出的 TopN 酒品列表同时展示这个客户的最近购买记录和相似客户群体画像。这个页面帮了大忙因为算法效果好坏不能只看离线指标直接拿真实客户的行为去对比推荐结果是最直观的验证方式。前端通过 Axios 调用后端的 RESTful 接口接口路径统一以/api开头前后端联调时只需在 Vue 的.env.development文件里配置代理地址不需要额外处理跨域。生产环境下用 Nginx 把/api反向代理到后端服务即可。3. 协同过滤算法在白酒订单数据上的落地实现这部分是整个系统最核心的内容。标题里写了协同过滤算法但真正把它搬到工程里时有几个环节必须想清楚否则算法代码写得再漂亮线上也是废的。3.1 从订单行为构造用户-酒品评分矩阵大部分协同过滤教程用的是用户对物品的评分比如电影评分 1 到 5 分。但白酒销售系统根本没有评分数据唯一的用户行为是买过什么、买了几次、买了多少瓶。所以第一步是把订单行为转化成评分。我在这个项目里采用的标定方式如下用户每购买某一款酒一次记 1 个行为分用户累计购买该酒的瓶数作为数量权重订单金额参与加权买得越贵比如整箱、礼盒装说明对该商品的偏好越强评分公式简化为score 1 ln(累计购买瓶数)再用订单金额做系数缩放。为什么不直接用购买瓶数因为少数客户可能一次性团购大量酒水用于宴席这种单次大单不能说明他对该酒有长期偏好。取对数可以把这类极端值压缩让评分更能反映稳定偏好。对应的 Java 实现大致如下public class UserWineScoreBuilder { /** * 从订单明细构建用户-酒品评分表 * key: 用户ID, value: (酒品ID, 加权评分) */ public MapInteger, MapInteger, Double buildFromOrders(ListOrderItem orderItems) { MapInteger, MapInteger, Double userWineScore new HashMap(); for (OrderItem item : orderItems) { Integer userId item.getUserId(); Integer wineId item.getWineId(); int buyNum item.getBuyNum(); double amount item.getTotalAmount(); double score 1 Math.log(buyNum); score score * (1 amount / 500.0); // 金额系数500元为一个基准档 userWineScore .computeIfAbsent(userId, k - new HashMap()) .merge(wineId, score, Double::sum); } return userWineScore; } }数据来源上后端每次生成推荐列表时可以直接从订单明细表实时聚合也可以提前跑一个定时任务把聚合结果写入单独的评分表。这套系统默认是在推荐接口被调用时实时聚合因为演示数据量不大实时算反而免去了评分表数据过期的烦恼。3.2 用户相似度计算与推荐打分代码有了评分矩阵接下来就是标准的协同过滤流程先算用户之间的相似度再根据相似用户的行为为目标用户推荐。相似度计算用了余弦相似度因为它在处理稀疏向量时表现稳定而且实现简单。核心代码如下public double cosineSimilarity(MapInteger, Double user1, MapInteger, Double user2) { SetInteger commonKeys new HashSet(user1.keySet()); commonKeys.retainAll(user2.keySet()); if (commonKeys.isEmpty()) { return 0.0; } double dotProduct 0.0; double norm1 0.0; double norm2 0.0; for (double v : user1.values()) { norm1 v * v; } for (double v : user2.values()) { norm2 v * v; } for (Integer key : commonKeys) { dotProduct user1.get(key) * user2.get(key); } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }推荐打分时不能简单地拿相似用户买过的酒去排序因为每个相似用户对目标用户的影响权重不同。常见的做法是加权求和目标用户对某款酒的预测偏好 所有相似用户对该酒的评分 × 用户相似度的总和。代码如下public ListInteger recommendForUser( Integer targetUserId, MapInteger, MapInteger, Double userWineScore, int topN) { MapInteger, Double targetUserScores userWineScore.getOrDefault(targetUserId, Collections.emptyMap()); if (targetUserScores.isEmpty()) { // 冷启动阶段走热门商品兜底策略 return getHotWineIds(topN); } MapInteger, Double recommendScore new HashMap(); for (Map.EntryInteger, MapInteger, Double entry : userWineScore.entrySet()) { Integer otherUserId entry.getKey(); if (otherUserId.equals(targetUserId)) { continue; } MapInteger, Double otherUserScores entry.getValue(); double similarity cosineSimilarity(targetUserScores, otherUserScores); if (similarity 0.0) { continue; } for (Map.EntryInteger, Double wineEntry : otherUserScores.entrySet()) { Integer wineId wineEntry.getKey(); if (targetUserScores.containsKey(wineId)) { continue; // 已购买过的酒不再推荐 } recommendScore.merge(wineId, similarity * wineEntry.getValue(), Double::sum); } } return recommendScore.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }注意这里有个隐藏问题如果两个用户都只买过同一款酒相似度会高达 1.0但它们的偏好未必真的接近。所以项目里加了一个惩罚项——两个用户共同购买的商品数越少相似度越不可信要乘以一个衰减系数。具体做法是sim cosineSimilarity * (commonCount / (commonCount 1))这样共同行为只有 1 次时相似度直接打五折防止偶发行为带来的虚假相似。3.3 时间衰减与热门兜底白酒消费有很强的季节性和时效性。春节前是送礼酒、宴席酒的购买高峰中秋前后也是。如果系统不考虑时间因素一个用户在去年春节买过某款高端礼盒酒系统可能一年后还在给他推荐同一款这显然不合理。这个项目的处理方式是给行为数据加了时间衰减权重。在计算评分矩阵时不是简单累加所有历史订单而是按订单时间做指数衰减double timeDecayFactor Math.pow(0.9, daysBetween(orderTime, now) / 30.0); score baseScore * timeDecayFactor;意思是每过 30 天行为的参考价值衰减为原来的 90%半年后就只剩 0.9 的 6 次方约 0.53。这样推荐结果会更贴近客户当前的偏好。冷启动问题也要单独处理。新客户没有任何订单行为协同过滤算不出任何相似用户。这个系统的兜底方案是品类热门 高毛利商品组合取最近 30 天内销量最高的 10 款酒再混入 2 款价格较高但库存充足的商品保证推荐列表既不冷冰冰又兼顾利润。这个策略虽然简单但真实业务场景里很管用。3.4 物品协同过滤作为补充视角基于用户的协同过滤UserCF更适合社区属性强的场景比如客户之间有明显的群体特征。但白酒销售里还有一个常见情况客户的偏好会迁移。比如一个客户从低端口粮酒逐步升级到中高端酒此时基于物品的协同过滤ItemCF能更好地捕捉喜欢 A 酒的人也会喜欢 B 酒的规律。这套系统里留了策略切换的配置项在 appplication.yml 里可以配置推荐策略recommend: strategy: user-cf # user-cf / item-cf / hot-fallback top-n: 8 user-cf: min-common-count: 1 time-decay-days: 180 item-cf: max-similar-items: 20我用同样的一批白酒订单数据分别跑过 UserCF 和 ItemCF一个很直观的感受是在用户行为稀疏的时候ItemCF 的推荐结果更稳定一旦核心客户订单足够密集UserCF 的个性化更明显。所以项目初期可以用 ItemCF等订单量积累上来之后再切 UserCF这个切换过程因为有了配置项变得非常轻松。4. 数据库建模让推荐引擎拿得到干净可用的训练数据说句实话很多推荐系统死在数据上不是死在算法上。如果订单表和商品表设计得一团糟算法再强也白搭。这套系统的表结构设计有几个可圈可点的地方。4.1 核心表结构设计下面是几张核心表和它们的关键字段表名关键字段设计意图customerid, name, phone, level, region客户档案level 区分普通客户和重点客户winesid, wine_name, brand, aroma_type, degree, price, year, stock白酒商品档案aroma_type 区分香型用于后续属性过滤ordersid, customer_id, order_time, total_amount, status订单主表order_time 用于时间衰减计算order_itemsid, order_id, wine_id, buy_num, unit_price, total_amount订单明细推荐算法的主要数据来源user_wine_scoreid, user_id, wine_id, score, update_time用户-酒品评分持久化表定时任务可写入推荐引擎读取数据时一条 SQL 就能把用户的购买行为完整取出来SELECT o.customer_id AS user_id, oi.wine_id, SUM(oi.buy_num) AS total_buy_num, SUM(oi.total_amount) AS total_amount, MAX(o.order_time) AS last_buy_time FROM orders o JOIN order_items oi ON o.id oi.order_id WHERE o.status COMPLETED GROUP BY o.customer_id, oi.wine_id;这个查询把用户、商品、数量、金额、最近购买时间都拿到了推荐引擎拿这份结果做矩阵输入基本是零阻碍。4.2 订单记录与评分的关系处理这里有一个容易踩坑的地方直接把订单表当评分表用。订单是业务数据会随着退货、作废、修改而变动评分是推荐依据需要稳定。如果每次算法计算都实时扫描订单表看似方便但等到数据量增长之后推荐接口会变得越来越慢因为这个查询要全表关联。我的建议是平时实时算没问题但一定要留一个user_wine_score表作为评分缓存用 Spring 的 Scheduled 定时任务每天凌晨跑一次全量聚合把评分结果写进去。推荐接口优先读评分表评分表没有命中时才实时聚合。这套系统的演示版本实时聚合和定时任务两种方式都写了切换开关也在配置文件里效果不错。另外设计 wines 表时给aroma_type、degree、price都建了索引。推荐结果出来之后业务端可能需要按香型、度数、价格带做二次过滤有索引的话过滤效率高很多。这个细节在前期不显眼数据量到几万条之后差别非常明显。5. 环境准备与启动排错把可直接运行落到实处标题里可直接运行四个字是这套系统的卖点之一。但可直接运行不是嘴上说说我实际从零开始跑了一遍把整个流程整理出来顺便记录了启动过程中容易踩的坑。5.1 环境版本清单与初始化步骤先用一句话总结运行需要的东西JDK 1.8 以上、Maven 3.6 以上、Node.js 16 以上、MySQL 5.7 或 8.0。具体步骤如下第一步初始化数据库。在 MySQL 中创建数据库并导入初始化 SQLmysql -u root -p database/init.sqlinit.sql 里包含了建库、建表、插入演示数据的全部语句。我特意确认了一下演示数据里包含了多个客户在不同时间点的订单记录数据量不算大但足够跑出有区分度的推荐结果这一点做得很贴心。第二步配置数据库连接。打开后端项目的application.yml修改数据库地址、用户名、密码spring: datasource: url: jdbc:mysql://localhost:3306/baijiu_sales?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword jpa: hibernate: ddl-auto: none注意ddl-auto一定要设置为 none因为表结构已经由 init.sql 建好了如果让 JPA 自动建表反而可能因为字段类型不匹配而出问题。第三步启动后端服务cd backend mvn clean package -DskipTests java -jar target/baijiu-sales-1.0.0.jar后端默认跑在 8080 端口启动日志里能看到 Spring Boot 的启动 banner然后会自动执行推荐引擎的初始化预热把热门商品数据加载到内存缓存里。第四步启动前端开发服务cd frontend npm install npm run dev前端默认跑在 3000 端口启动后在浏览器访问http://localhost:3000用管理员账号登录就能进入系统。5.2 启动过程常见报错与解决办法我在实际运行过程中遇到的几个问题在这里列成表格方便刚接触这类项目的人直接对号入座报错现象根本原因解决办法后端启动失败提示Access denied for user rootlocalhost数据库密码配置错误或者 MySQL 账号不允许远程连接核对 application.yml 的密码确认本地能正常连库前端npm run dev页面白屏控制台报跨域错误前端代理没有配好检查 frontend 根目录下.env.development中的代理目标地址是否为http://localhost:8080推荐接口返回空列表数据库里没有该用户的任何订单记录冷启动逻辑兜底未生效检查recommend.strategy配置确认是否开启了hot-fallback中文乱码数据库连接串缺少characterEncodingutf8在 JDBC 连接串中加入 utf8 编码参数端口被占用8080 或 3000 已被其他程序占用修改 application.yml 的server.port或前端 vite.config.js 中的端口号其中一个坑值得单独提醒如果你本机装的是 MySQL 8.0而项目里引用的 MyBatis 或 JPA 驱动版本较老很可能会提示时区相关的报错The server time zone value is unrecognized。解决方案就是在连接串后面加serverTimezoneAsia/Shanghai这也是上面配置文件里已经写好的默认值。如果你是从别的地方抄来的配置没有这段记得补上。6. 跑通之后的推荐效果调试与下一步优化方向系统跑起来只是第一步推荐算法最花时间的地方在这里怎么判断推荐结果看起来靠谱怎么在数据不足时不让推荐列表显得太傻以及性能怎么保持稳定。6.1 数据量不足时的推荐失真演示数据集大概只有几百条订单记录分布在几十个客户身上。这种数据量下协同过滤很容易出现全员相似的问题因为总共就那几款热门酒大家多多少少都买过余弦相似度算出来普遍偏高推荐结果趋向于中庸个性化不足。遇到这种情况我的调试思路是先用系统自带的客户详情 - 推荐预览页面挑几个行为差异明显的客户比如一个只买酱香、一个只买清香看推荐结果是否真的呈现出香型差异。如果没有差异优先检查相似度衰减系数是否生效以及时间衰减是否把短期的偏好信号压得过低。另外数据量不足时还有一个常见问题推荐结果里反复出现已售罄商品。因为协同过滤只关注用户行为匹配不太关注库存是否充足。这个系统的做法是在推荐接口返回前做一次库存过滤把stock 0的商品直接剔除。这个过滤要放在算法计算之后、返回前端之前千万别放在算法输入阶段否则会破坏评分矩阵的完整性。6.2 接口性能与缓存策略推荐接口是读密集型的每次调用都要算相似度矩阵如果用户量到几千人、商品量到几百款在线计算的时间就可能从几十毫秒涨到几百毫秒。对一个后台管理系统来说几百毫秒尚能接受但不能再多。为了给后续用户增长留出余量我在项目中给推荐接口加了 Redis 缓存缓存 key 是recommend:{userId}:{strategy}缓存值是推荐商品 ID 列表过期时间设置为 30 分钟。这样即使短时间内有大量客户详情页被访问真正触发全量计算的概率也大大降低。同时商品库存和价格信息不应该随推荐结果一起缓存因为库存是实时变动的。推荐接口只缓存商品 ID 列表拿到 ID 后再实时查库填充商品详情这样既保证了推荐逻辑的时效性又避免了推荐了但已经卖完的尴尬。6.3 从在线计算到离线预计算的扩展最后聊一下扩展方向。当前这套实现是在线计算每次请求现算用户相似度。在小规模场景下完全够用但如果白酒销售系统的客户和商品数量继续增长在线计算迟早会成为瓶颈。更成熟的方案是离线预计算 在线检索用一个定时任务每天凌晨跑一次全量相似度矩阵计算把每个用户的 TopN 推荐列表写入 Redis 或数据库白天的推荐接口只是做一次缓存查询扛几千 QPS 都不成问题。如果将来数据量到了百万级可以把相似度计算部分换成 Spark 或者 Flink 来实现但存储层和接口层基本不用动——这正是把推荐逻辑独立封装成服务层的好处。跑完这个项目我的体会是协同过滤在白酒销售管理系统里不是一个展示型功能它是真正能影响业务动作的工具。业务员看一眼推荐列表就知道该给老客户推荐什么酒管理员看一眼推荐报表就知道哪些酒在哪些客户群里渗透率还不高。这套系统的代码结构、模块划分和算法落地方式完全可以作为一个中小型推荐系统的参考样板希望这篇拆解能给正在做类似项目的你一些思路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑