资讯动态

SpringBoot+Vue二手交易平台:协同过滤推荐系统实战落地

发布时间:2026/10/16 16:53:58 来源:尧图企业网站定制
项目标题把“协同过滤推荐”放到二手交易平台里本身就是一件挺有意思的事。市面上很多同类型的毕业设计、课程项目说是有推荐功能实际打开数据库一看就是一个“猜你喜欢”的假接口随机捞几条数据塞给前端。而这个基于SpringBootVue搭建的闲置二手物品交易平台是真的把协同过滤算法落到了实处有用户行为采集、有相似度计算、有定时批量更新推荐结果还有冷启动兜底方案。再加上完整源码和万字开发文档无论你是想完成一个合格的毕设项目还是想在简历里写出“有推荐算法落地经验”这套东西都很有参考价值。这套项目的技术选型思路和大多数主流全栈项目一致SpringBoot负责后端接口、业务逻辑和事务控制Vue负责前端页面渲染和用户交互中间通过RESTful API通信。MySQL存放业务数据Redis做缓存加速。整个系统不是一上来就堆各种高大上的组件而是把精力集中在“交易闭环跑通”和“推荐真正可用”这两件事上。这篇文章我会从系统设计、数据库建模、推荐算法实现、后端核心接口、前端交互、部署调试到踩坑记录一条线完整讲清楚。1. 项目定位与整体设计思路1.1 技术选型背后的取舍先说后端。SpringBoot在这个项目里的优势其实不在于它“新”而在于它足够稳。SpringBoot自带的自动配置让项目不用像传统SSH整合那样写一堆XML注解驱动开发对初学者和后端开发者来说都友好。配合MyBatis-Plus操作数据库CRUD几乎不用写SQL开发效率能提升一大截。但这套项目没有无脑依赖MyBatis-Plus自定义的SQL、联表查询还是保留手写的方式这样在数据量和查询逻辑变复杂以后依然有足够的控制力。前端选择Vue而不是React核心原因是Vue的上手曲线平缓模板语法接近HTML直觉配合Element UI做后台管理页面几乎就是搭积木。Vuex管理全局状态Vue Router做页面路由axios做网络请求代理整体工程化程度适中不会像大型中后台项目那样复杂到劝退新人又能让学的人真正感受到“工程化开发”是怎么回事。数据库选MySQL 8.0理由就不用多说了。这个体量的交易平台用MySQL完全够而且在索引优化、事务控制、数据一致性这几个核心点上MySQL的资料和工具链最成熟遇到问题随便查都能找到答案。1.2 功能模块划分从用户角色角度看这个系统分成普通用户端和管理员端。普通用户的核心场景包括注册登录、浏览分类、搜索商品、查看商品详情、收藏商品、发布闲置、编辑下架商品、下单购买、确认订单、评价交易对象。这些功能基本覆盖了一个C2C交易平台的核心链路。管理员端的功能更偏运维用户管理禁用异常账号、商品管理审核和上下架、订单管理查看和标记状态、数据统计用户数、商品数、成交额等简单看板。这个模块划分没有堆砌“社区”“聊天”“积分商城”这类花哨功能边界控制得很好。从项目完成度角度来说把交易闭环和推荐模块扎扎实实做完比塞一堆半成品功能要值钱得多。1.3 推荐模块在系统中的位置推荐模块在系统里不是独立的微服务而是作为后端Service层的一个模块存在。这种设计有几个实际考虑第一推荐逻辑需要直接读取用户行为表、商品表、订单表放在Service层可以复用业务数据不用搞复杂的跨服务调用。第二推荐结果的生成是离线完成的和用户请求路径隔离即使推荐计算失败也不会影响交易主流程。第三把算法部分封装成独立Service后续想替换成基于物品的协同过滤或者接入别的算法框架改动范围都限定在一个模块内。2. 数据库设计与核心表结构2.1 基础业务表设计系统的核心表大概有11张。我挑重点说几个。用户表和普通系统的区别不大主键自增ID用户名建唯一索引密码存的是BCrypt加密后的密文而不是明文这一点很重要。状态字段用int0正常、1禁用方便管理员操作也方便后期扩展更多状态。商品表的字段设计得更贴近二手交易场景。除了标题、描述、价格、分类这些基础字段专门加了原价字段original_price用来展示价格折扣力度。图片字段用逗号分隔存储多个图片URL虽然不算最规范的范式做法但在这个场景下够用而且查出来以后直接分割字符串就能用。商品状态用int表示0草稿、1上架、2已售、3下架。这里要注意商品表里的user_id字段是发布者ID必须建索引因为后续商品列表页、卖家个人页都要按这个字段查询。订单表的order_no字段设计成独立的业务编号没有直接用自增ID。原因是自增ID会暴露订单量和业务数据规模而且以后对接第三方支付时业务编号是必须的。金额字段用decimal(10,2)这是老生常谈但确实重要float和double在金额计算上会出精度问题。收藏表是一个典型的关联表核心字段就id、user_id、product_id、create_time。为了防止用户重复收藏同一件商品建了联合唯一索引uk_user_product。这也算是数据库设计里的基本功但很多新人会漏掉这一步导致后期要写一堆去重逻辑。2.2 推荐相关表结构协同过滤推荐需要三类数据支撑用户行为、相似度矩阵、推荐结果。用户行为表behavior_log记录四类行为浏览、收藏、下单、评价。字段有user_id、product_id、behavior_type、behavior_time。这张表是推荐算法的原始原料数据量会持续增长项目里预留了定时清理策略只保留最近三个月的行为记录。这个思路很重要因为过期的行为数据对推荐准确性其实是有负作用的用户最近三个月不感兴趣的东西未必代表他现在不感兴趣。相似度矩阵表存的是用户和用户之间的相似度值。字段设计成user_a、user_b、similarity保留四位小数。这张表的规模是用户数的平方所以只适合中小规模系统。对于几百用户的演示项目完全没问题如果未来用户量激增还是要走分布式计算引擎或者图数据库方案。推荐结果表是“空间换时间”的核心。每次离线计算完成后直接为每个用户生成TopN推荐商品列表存入recommend_result表。用户访问首页时推荐接口就查这张表再把商品信息拼装完整返回。这样推荐接口的耗时几乎是恒定的不会因为用户数量和商品数量的增长而变慢。2.3 索引设计心得这个项目实际建的索引我整理了一份方便你们直接抄表名索引名字段类型productidx_user_iduser_id普通索引productidx_category_idcategory_id普通索引ordersidx_buyer_idbuyer_id普通索引ordersidx_seller_idseller_id普通索引ordersidx_order_noorder_no唯一索引favoriteuk_user_productuser_id, product_id联合唯一索引behavior_logidx_user_timeuser_id, behavior_time联合普通索引recommend_resultuk_user_productuser_id, product_id联合唯一索引注意不要给text类型字段建索引。商品描述这种长文本字段如果想做搜索用MySQL的LIKE模糊查询已经足够应付几千条数据了。真要上全文检索得等到数据量到十万级以上再考虑Elasticsearch之类的组件别在项目初期引入没必要的复杂度。3. 协同过滤推荐算法的落地实现3.1 为什么用离线批处理而不是实时计算一开始我确实动过“实时推荐”的念头用户看完一个商品就马上推相关的。但算一笔账就明白了假设系统里有500个用户、2000个商品每次请求都要实时找相似用户、计算推荐分数光是扫描用户行为表就要遍历几万条记录。在没有大数据组件加持的单机应用里接口响应时间会直接飙到几百毫秒甚至秒级首页加载效率无法接受。所以这个项目采用定时任务离线计算的方案每天凌晨2点SpringBoot结合Scheduled注解执行推荐任务扫描当天新增的用户行为更新相似度矩阵为活跃用户计算TopN推荐列表写入recommend_result表。白天所有推荐接口直接读结果表。3.2 用户相似度计算余弦相似度实现推荐模块先实现的是基于用户的协同过滤UserCF。二手交易平台的行为数据很稀疏用户就收藏了几个商品行为矩阵大量是零值这种场景下余弦相似度比较合适因为它不受用户行为绝对值大小的影响只看方向一致性。计算过程分三步构建“物品-用户”倒排表统计用户两两之间共同交互的物品数量代入余弦相似度公式公式是sim(A,B) (A向量·B向量) / (|A向量| × |B向量|)在Java里用字典和循环手工实现了这个过程没有引入重量级矩阵库。代码逻辑简洁而且每步都能打印结果调试对于这个数据规模性能完全够用public MapString, MapString, Double calcUserSimilarity(ListBehavior behaviors) { // 物品到用户的倒排表 MapLong, ListLong productToUsers new HashMap(); for (Behavior b : behaviors) { productToUsers.computeIfAbsent(b.getProductId(), k - new ArrayList()).add(b.getUserId()); } // 统计用户两两共同交互的物品数量 MapString, Integer cooccurrence new HashMap(); MapLong, Integer userItemCount new HashMap(); for (ListLong users : productToUsers.values()) { for (int i 0; i users.size(); i) { Long u1 users.get(i); userItemCount.merge(u1, 1, Integer::sum); for (int j i 1; j users.size(); j) { Long u2 users.get(j); String key u1 u2 ? u1 _ u2 : u2 _ u1; cooccurrence.merge(key, 1, Integer::sum); } } } // 套公式 MapString, MapString, Double result new HashMap(); for (Map.EntryString, Integer entry : cooccurrence.entrySet()) { String[] pair entry.getKey().split(_); Long u1 Long.parseLong(pair[0]); Long u2 Long.parseLong(pair[1]); int cnt entry.getValue(); double sim cnt / (Math.sqrt(userItemCount.get(u1)) * Math.sqrt(userItemCount.get(u2))); result.computeIfAbsent(String.valueOf(u1), k - new HashMap()) .put(String.valueOf(u2), sim); result.computeIfAbsent(String.valueOf(u2), k - new HashMap()) .put(String.valueOf(u1), sim); } return result; }这里有个重要的细节行为类型不是一视同仁的。项目中把收藏行为的权重设为1浏览行为权重为0.5购买行为权重为2。为什么要这样做因为浏览可能是误点参考价值低收藏代表明确的兴趣信号购买行为意味着真实的购买意愿和信任权重自然最高。这个权重调整对推荐效果的影响比换一个高级算法框架要明显得多。3.3 推荐分数预测与商品过滤在拿到用户相似度矩阵之后为目标用户生成推荐列表用的是加权求和公式score(U, I) Σ(sim(U,V) × r(V,I)) / Σsim(U,V)也就是说目标用户U对未交互过的商品I的预测兴趣分等于所有相似用户V对商品I评分的加权平均权重就是U和V的相似度。算法层处理完还有一道过滤环节已经买过的商品必须过滤掉已经下架的商品必须过滤掉自己发布的商品也必须过滤掉。这个过滤条件要是漏了就会出现一个用户买完手机第二天推荐里还推着这台已售手机的尴尬情况。3.4 冷启动问题的工程化兜底新注册用户没有任何行为数据协同过滤算法对他没有任何可计算的输入。这个项目用了一个务实的方案用户行为记录少于3条时返回热门商品列表按照商品浏览量降序取前20行为记录达到3条后才启用协同过滤推荐结果热门商品数据怎么来定时任务里额外算出一张“热门商品缓存表”商品按一个综合热度值排序热度值综合考虑浏览数和收藏数。这个兜底方案简单有效用户从第一个页面开始就有内容可看不会因为“暂无推荐”而流失。4. SpringBoot后端核心模块实现4.1 JWT认证与权限拦截后端使用JWT做无状态认证。登录接口校验成功后生成一个有效期24小时的token返回。前端在请求头中携带 token后端通过拦截器解析并取出当前用户ID。拦截器的核心逻辑并不复杂public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; // 放行跨域预检 } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { Claims claims JwtUtils.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } response.setStatus(401); return false; } }使用JWT方案时有几个细节需要注意。token必须设置过期时间不能一劳永逸。所以过期时间设置为24小时用户长期不访问后再发请求必须重新登录。还有敏感操作比如修改密码、确认收货除了token校验后端还应该做二次校验防止账号被盗后的高风险操作。4.2 商品发布与下单的事务控制商品发布流程前端上传图片后端保存到本地或者OSS填写标题、描述、价格、分类后端校验参数合法性标题2到50字符价格大于0默认状态为“上架”。下单流程是整个系统里对事务要求最严格的部分。整个操作必须放在同一个事务里校验商品状态是否为“上架”校验买家不能购买自己发布的商品更新商品状态为“已售”生成订单记录。任一步失败都要回滚不能出现“商品卖出去了但订单没生成”的数据不一致状态。C2C模式下一个商品只有一件所以没有设计库存数字段而是用商品状态字段直接保证“每件商品只能被一个买家买走”。这比引入库存表更简单也更贴合闲置交易的实际情况。实现方式很简单在Service方法上加Transactional注解默认的传播行为就够了。4.3 推荐接口的响应组装推荐接口的路径是/api/recommend/home逻辑分几步从请求上下文中解析userId查recommend_result表取用户TopN推荐商品ID如果结果为空改用热门商品表兜底根据商品ID批量查询商品详情、卖家信息判断当前用户是否已收藏该商品拼接favorited字段返回统一格式这里最值得说的是“批量查询”这一点。拼装推荐列表时对商品ID集合用MyBatis-Plus的listByIds一次查出所有商品而不是在循环里逐条查询。这个细节直接影响接口性能尤其当前端一次要渲染20个推荐商品时循环查询的N1问题会瞬间暴露出来。5. Vue前端页面与交互实现5.1 前端路由与页面结构前端工程使用vue/cli创建目录组织清晰。主要页面组件包括首页Home.vue搜索栏、分类导航、轮播图、推荐商品瀑布流商品详情页ProductDetail.vue轮播图、价格、描述、卖家信息、相似推荐发布商品页Publish.vue图片上传、表单校验个人中心My.vue我的商品、我的订单、我的收藏、个人信息后台管理页用户管理、商品管理、订单管理、数据统计路由守卫是保证页面访问安全的重要一环需要登录才能访问的页面设置meta.requiresAuth路由守卫里检查token是否存在不存在就跳转登录页。router.beforeEach((to, from, next) { if (to.meta.requiresAuth !store.state.token) { next(/login); } else { next(); } });5.2 推荐位的设计首页推荐区域采用“猜你喜欢”的瀑布流布局商品卡片显示封面图、标题、价格、卖家昵称、浏览量和收藏数。点击卡片进入商品详情页。推荐位做了分页加载每次请求20条。由于推荐结果表本身已经按分数排好序翻页时不会出现重复商品。这个交互设计看似简单但和后台的分页接口逻辑是对齐的前端不用做复杂的去重处理。另外商品详情页也接了一个“相似推荐”区域。这里调用的是同一套推荐接口但传入的参数不同取的是当前商品分类下其他推荐商品。这样设计让推荐模块不只是首页的装饰而是贯穿用户浏览路径的实用功能。5.3 axios封装与Vuex状态管理项目里对axios实例做统一封装设置baseURL和请求拦截器const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; });Vuex主要管理两类全局数据登录后的用户token和用户基本信息、购物车和搜索结果这类跨组件共享的数据。token存localStorage但注意只存token和userID这类不敏感信息密码、手机号、地址等敏感信息一律从后端接口获取不持久化在前端。6. 运行部署与源码文档使用指南6.1 环境要求与启动步骤把这套源码跑起来环境要求如下JDK 1.8Maven 3.6MySQL 8.0Node.js 14IDE推荐IntelliJ IDEA VS Code后端的启动步骤创建数据库执行项目中的schema.sql初始化脚本修改application.yml中的数据库用户名和密码在backend目录执行mvn install运行启动类端口默认8080前端的启动步骤命令行进入frontend目录执行npm install安装依赖执行npm run serve浏览器访问localhost:8081前端dev server自动代理API请求到8080端口整个启动流程比较简单没有依赖外部缓存或中间件。推荐算法跑的定时任务也是在后端应用内实现的单机就能跑通。6.2 压缩包内容与开发文档说明项目压缩包含以下内容backend/SpringBoot后端完整源码frontend/Vue前端完整源码docs/万字开发文档Word格式sql/数据库初始化脚本README.md快速开始说明、功能演示截图、测试账号万字开发文档里最值得关注的是接口文档部分每个接口都标注了请求方法、请求参数、响应示例。这对二次开发特别友好接手项目的人不需要去翻源码猜某个接口的入参结构直接对照文档就能联调。另外系统设计部分也把数据库ER图和模块划分讲得比较清楚适合用来写毕设论文。6.3 二次开发方向建议如果拿到源码想改成自己的项目和简历项目这几个扩展方向比较推荐在协同过滤基础上增加基于物品的ItemCF和现有UserCF结果加权合并提升推荐多样性接入第三方支付模拟接口把“下单”完善成“付款-发货-确认收货”完整闭环接入云存储把商品图片从本地存储迁移到OSS解决服务端磁盘空间越攒越满的问题增加站内信或WebSocket通知买家下单后给卖家发提醒7. 踩坑记录与常见问题排查7.1 推荐结果过于集中热门商品霸屏第一次跑完推荐任务后发现部分热门商品几乎出现在所有用户的推荐列表里推荐多样性很差。热门商品被大量用户收藏相似用户对它的评分权重很高算法天然偏向它们。解决办法是给热门商品加惩罚因子score_final score_original / (1 log(1 productPopularity))productPopularity用商品的收藏数或浏览量衡量。这个惩罚项执行之后推荐列表里长尾商品的曝光明显上升。这也是真实推荐系统里“去热门化”的常见做法。7.2 JWT过期导致前端循环跳转调试时遇到一个诡异问题前端反复跳到登录页明明token没过期但每次刷新都跳。排查发现是axios响应拦截器在遇到401时调用router.push但token刷新逻辑没处理好导致循环触发。解决方案是加一个跳转标志位同一时间只允许跳转一次let isRedirecting false; service.interceptors.response.use( response response, error { if (error.response error.response.status 401 !isRedirecting) { isRedirecting true; router.push(/login); } return Promise.reject(error); } );7.3 跨域请求Cookie丢失开发环境下前端跑8081端口后端跑8080端口axios发请求必然跨域。解决方案是后端配置CorsFilter允许指定来源和常用方法。但有个容易被忽略的细节默认跨域配置不会携带Cookie。如果以后想用session或者登录态前端axios要设置withCredentials: true后端CorsFilter也要设置allowCredentialstrue。7.4 列表接口响应体过大导致加载慢商品描述和详情信息一开始都放在列表接口里返回导致接口响应体很大页面加载明显卡顿。后来把列表接口和详情接口做了字段拆分列表只返回标题、价格、封面图、浏览量、收藏数这些核心字段完整描述放到详情接口单独查询。这个优化让列表接口的响应时间从约400ms降到了约120ms。以下是一个常见问题速查表方便直接参考问题现象排查重点解决方案推荐列表全是热门商品查看算法是否加了热门惩罚引入热度惩罚因子token没过期却401跳登录检查axios响应拦截器循环调用加isRedirecting标志位跨域请求没有Cookie检查CorsFilter和axios中的withCredentials两端同时开启credentials列表接口加载慢检查是否返回了大字段内容拆分列表接口和详情接口推荐结果重复检查过滤条件是否有商品状态判断过滤已购买、已下架、自己的商品定时任务没有执行检查是否开启EnableScheduling启动类加该注解这套系统从设计到实现核心原则一直是“务实”。推荐算法用经典方法不追求花哨前后端选主流框架不搞冷门方案数据库模型按真实业务场景建模不加冗余设计。如果你正在做类似项目先把基础交易链路跑通再琢磨推荐模块的细节最后用真实数据测一测推荐效果你会发现这种“一步一个脚印”的推进方式比一上来就搭复杂架构要靠谱得多。这套源码和开发文档拿到手之后第一遍按README把项目跑起来第二遍对照开发文档逐模块看代码第三遍把数据库表结构和推荐算法跑一遍数据验证三遍下来你对整个系统的理解就算彻底到位了。

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

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

免费获取报价 →
↑