
先从我的实际感受说起吧。带过不少同学和同行做毕设、做企业内部的小型电商项目一个很典型的困境是大家把Spring Boot和Vue前后端联调跑通之后项目看起来功能齐全但总觉得少了点“差异化”——没有推荐、没有个性化、没有能写进论文和讲稿里的亮点。而“协同过滤”这四个字说法人人都知道真落到商城业务里大部分人却只会把网上找的MovieLens电影推荐Demo改个商品名。结果就是推荐逻辑脱离实际业务答辩时一问就露馅。这篇我整理的就是一套能直接在商城项目中落地的思路和代码骨架后端用Spring Boot承接口前端用Vue做展示中间用协同过滤算法把“用户-商品”行为数据变成个性化推荐结果。项目编号021功能上覆盖了常见的商城基础模块但重点想讲清楚的是推荐部分怎么接进去、相似度怎么算、冷启动怎么兜底以及部署时那些容易卡壳的细节。1. 商城系统整体设计先画清模块边界再动手写代码很多项目写着写着就乱根源不在代码能力而在动手前没把“谁负责什么”想清楚。网上购物商城尤其如此涉及的模块多、状态流转长如果全部耦合在一堆Controller里后期想加推荐功能几乎寸步难行。1.1 前后端分离下的模块划分我习惯把整套系统按用户视角切成两条主链路普通用户链路和后台管理链路。普通用户链路包含注册登录、商品浏览与搜索、商品详情、购物车、订单结算、订单列表、个人中心、推荐商品流后台管理链路包含商品管理增删改查、上下架、分类管理、库存管理、订单处理发货、取消、用户管理、行为数据统计与推荐结果预览。这两条链路在物理上共用同一套Spring Boot接口但在逻辑上要用不同的访问权限约束。我的做法是在后端设置/api/user/**和/api/admin/**两组路由配合JWT拦截器做鉴权前端则用Vue Router的导航守卫配合动态路由把管理端页面单独放进一个Layout里避免普通用户看到管理入口。很多同学喜欢把推荐模块单独做成一个微服务对于毕设或中小型业务来说这纯属过度设计。推荐模块完全可以在现有Spring Boot工程里以一个独立recommend包存在算完之后把商品Id列表返回给Controller再由前端渲染。这样既保持了业务独立性又不会引入分布式带来的部署复杂度。1.2 数据库与表结构设计推荐算法能不能算得准一大半取决于你落库的行为数据全不全。很多项目的用户表、商品表、订单表都齐了唯独没有“用户行为流水表”结果协同过滤算法没有输入数据只能硬编码几条推荐结果这在答辩时特别容易被问穿。我建议至少建这六张核心表用户表t_user主键、用户名、密码BCrypt加密、昵称、头像、手机号、注册时间、最后登录时间。商品表t_product主键、商品名称、副标题、分类Id、主图、详情图列表JSON字段或独立表、价格、库存、销量、上架状态、创建时间。商品分类表t_category主键、分类名称、父级Id支持二级分类、排序号。购物车表t_cart用户Id、商品Id、数量、勾选状态、加入时间唯一键设置为(user_id, product_id)。订单主表t_order订单号雪花算法生成、用户Id、订单总金额、订单状态、收货人信息快照不要关联用户表、创建时间、支付时间。订单明细表t_order_item订单号、商品Id、商品快照名称、价格、图片、购买数量、小计金额。用户行为表t_user_behavior用户Id、商品Id、行为类型BROWSE/FAVORITE/CART/PURCHASE、行为时间。这张表是推荐算法的数据来源行为数据一般通过埋点从用户操作中写入后面第五部分我会详细说明。另外如果系统需要支持用户对商品的显式评分星级评价可以加一张t_rating表。不过我要提醒一句绝大多数商城用户没有打分习惯冷启动阶段评分数据会非常稀疏所以这个表在我的方案里只是可选主力数据来源还是隐式行为。1.3 为什么选Spring Boot Vue这套组合这套组合在选题阶段被反复选择自然有其道理Spring Boot让后端开发免去了大量XML配置内嵌Tomcat打一个Jar就能跑部署成本低Vue是渐进式框架拿来渲染商城这类中等复杂度的前端页面开发效率明显比传统JQuery高生态里有Element UI这类现成的后台管理系统UI组件直接复用。更重要的是这两者在国内的学习资料和社区方案极为丰富遇到问题几乎都能搜到答案——这对工期紧张的项目来说是生死攸关的事情。当年带过的同学里有人非要选React全家桶写商城结果一个路由权限问题卡了两天最后还是换成Vue才按期交付。这不是说React不好而是在项目型场景里选熟悉度和生态完善度更高的技术才是务实的选择。2. Spring Boot后端从零搭建依赖、认证、三大业务模块后端部分我按“环境准备→用户认证→商品检索→购物车与订单”的顺序来讲这个顺序对应的是实际开发中从地基到楼层的推动过程。每一步我都会给出依赖和关键代码同时说明为什么要这么配置。2.1 环境准备与项目初始化环境方面我的建议是JDK 8或11即可Spring Boot选2.7.x系列。热词里有“springboot版本太高”的搜索这确实是个普遍坑——Spring Boot 3.0之后强制要求JDK 17且javax.*包全部迁移到jakarta.*网上大量旧教程和小伙伴自己写的工具类都不兼容排查成本挺高。毕设和中小型项目完全没必要追这个新。创建项目我推荐用Spring Initializr通过IDEA内置或者start.spring.io都行依赖勾选Spring Web、MyBatis Framework、MySQL Driver、Lombok、Validation。考虑到很多新手卡在Maven依赖下载过慢这里直接说结论把Maven的settings.xml里配置阿里云镜像源一个mirror标签的事比你在那等下载等一上午值多了。mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror项目结构我采用经典的controller/service/mapper/entity四层另外加一个config包放全局配置CORS、JWT拦截器、MyBatis-Plus分页插件common包放统一返回类和异常处理recommend包单独放协同过滤相关代码。这样分层的好处是每一个包只干一件事出Bug时定位非常快。2.2 用户模块与JWT认证用户模块本质上是两件事会话维护和接口鉴权。Session方案在前后端分离架构下有跨域场景的天生劣势JWTJSON Web Token方案才是主流做法。所谓JWT可以理解为服务器给你签发了一张带签名和有效期的“通行证”用户之后每次请求把这张“通行证”放进请求头里服务器验签通过就放行不需要在服务端存Session。我在实践里用的组合是Spring Security太重量级对毕设场景纯属自缚手脚更推荐直接手写JWT拦截器。理由很现实Spring Security的过滤器链一旦配置不对静态资源和放行接口全被拦住排查过程非常折磨人。而手写拦截器只需要十来行代码逻辑完全可控。工具类中用io.jsonwebtoken的JWT库生成和解析TokenToken里只放userId和过期时间密钥写在配置文件里通过Value注入。登录接口验证用户名密码通过后签发Token返回前端。前端把它存进localStorage在Axios请求拦截器里统一加到Authorization请求头。后端写一个JwtInterceptor实现HandlerInterceptor在preHandle里解析Token并放行注册到WebMvcConfigurer里时注意排除/api/auth/**、商品查询接口和静态资源路径。这里有个实战细节解析出来的userId要放到ThreadLocal里保存后续Controller和Service里可以直接取当前登录用户Id而不用每个方法都传参。如果使用了JDK 21的虚拟线程ThreadLocal会有些新的行为注意点但JDK 8/11下完全没问题。2.3 商品分类、搜索与分页商品模块是所有商城项目里最简单但对细节要求最多的部分。一个搜索功能前端传关键词后端要做三件事模糊匹配名称和副标题、按分类过滤、按价格或销量排序最后返回分页数据。我用MyBatis-Plus来操作数据库因为它的IService和BaseMapper能省掉大量单表CRUD的样板代码。分页需要先用PaginationInnerInterceptor注册分页插件MySQL选DbType.MYSQL然后就能直接在Service里使用PageProduct对象做查询。搜索接口的Mapper写法本质是一个带条件的SQL查询核心是XML或Select里用if标签动态拼接name like #{keyword}避免空条件拼出语法错误。后台管理的商品上架逻辑需要注意插入商品时要设置默认库存、默认销量、默认上下架状态上架动作本身要校验库存是否大于零。商品图片建议用独立图床或OSS数据库里只存URL。毕设项目里很多人把图片Base64直接存MySQL前期看着方便数据量稍大就会撑爆数据库内存页面加载也会明显变卡这条是我踩过的真实坑。2.4 购物车与订单事务边界和库存扣减购物车表设计我在第一章节已经给过这里重点聊订单的生成逻辑它包含一个电商领域非常经典的“事务边界”问题。用户在结算页点击“提交订单”时后端要一次性完成查询购物车勾选商品、校验每个商品库存和价格、计算总金额、扣减库存、生成订单主表和明细、清空对应购物车项。这七个步骤只要其中一步失败整个订单就不能产生否则会出现“钱付了但库存没扣”或“库存扣了但订单不存在”的脏数据。解决办法就是给下单方法加Transactional(rollbackFor Exception.class)让Spring接管事务任一环节抛异常就整体回滚。但如果你自己在代码里用try-catch把异常吃掉了Transactional就感知不到异常事务自然也不会回滚。这是很多同学第一次做下单功能时最容易踩的隐性Bug排查方式是把异常抛出而不是吞掉。库存扣减我建议用乐观锁思路更新的SQL写成UPDATE t_product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}通过stock count这个条件数据库行锁会自动帮我们挡住“超卖”——两个请求同时扣库存时只有一个UPDATE成功另一个影响行数为0代码里对返回值做判断就能给出“库存不足”的友好提示。对象存储一个细节用户下单成功后购物车对应的记录要删除。我见过有的项目把清空购物车逻辑写在订单查询接口里导致用户每次刷新订单页购物车就被清空这个坑的定位思路是永远不要在一个查询接口里做写操作写操作只存在于专属的Service方法中。3. Vue前端搭建从页面骨架到接口联调前端部分我不打算把所有组件代码贴一遍那样篇幅失控也没人愿意看。重点讲四个最容易卡死的地方环境初始化、路由设计、Axios封装与跨域、打包部署。这四个点踩平了剩下就是写页面的体力活。3.1 环境与项目初始化Vue端的环境要求比较简单Node.js推荐16.x或18.x LTS版本用npm config set registry https://registry.npm.taobao.org换国内源能显著降低安装依赖的时间。脚手架方面新项目推荐Vue 3 Vitenpm create vitelatest选Vue模板即可。但如果你是拿网上已有Vue 2项目改造那vue-cli的维护方式也别急着迁移稳定优先。依赖我建议装这几个vue-router4路由、pinia状态管理、axios网络请求、element-plusUI组件库。热词里“vue安装依赖”的搜索频率很高可见很多人卡在这一步。注意一个常见问题Windows环境下Vite项目对Node版本有要求Node版本过低时启动会直接报cannot find module vite之类错误优先升级Node而不是反复执行npm install。3.2 路由守卫与页面骨架商城的页面可以分两层用户端商城首页、商品列表页、商品详情、购物车、结算页、订单列表这些放在一个没有侧边栏的整洁Layout里后台管理端的商品管理、订单管理、用户管理放在带侧边栏的管理Layout里。实现方式是在Router配置里设置父子路由{ path: /, component: Layout, redirect: /home, children: [ { path: home, component: () import(/views/Home.vue) }, { path: product/:id, component: () import(/views/ProductDetail.vue) } ] }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: product, component: () import(/views/admin/ProductManage.vue) } ] }懒加载的意思是路由匹配时才去加载对应的js chunk避免首屏包体过大() import()语法就是干这个的。路由守卫放在router.beforeEach里两个职责第一检查要跳转的页面是否需要登录需要且没有Token就重定向到登录页第二检查是否要管理员权限把本地存储的角色值与路由meta对照不满足就返回首页并给提示。这样权限控制在前端做了一层用户体验层面的拦截真正的安全控制必须由后端接口鉴权兜底前端守卫只是不让用户看到不该看的页面。3.3 Axios封装统一请求头和错误处理Axios如果不做封装每个页面都直接axios.get请求头拼Token、处理401状态码、消息提示这些逻辑会在几十个组件里重复。我在项目里的做法是单独建一个request.js先创建一个Axios实例baseURL设置为/apitimeout设为10秒请求拦截器从localStorage取Token放进Authorization头响应拦截器先处理后端统一包装的{code, message, data}结构如果code为401就清掉Token跳转登录页code非200则用Element Plus的ElMessage.error弹出后端返回的错误文案。接口封装则按模块拆文件user.js放登录、注册、用户信息接口product.js放商品列表和详情cart.js、order.js类推。组件里只用import { getProductList } from /api/product调一句就能拿到数据出问题也知道直接去对应文件排查。3.4 联调与跨域问题处理前后端分离联调时跨域是绕不开的坎。开发环境下最省事的方案不是在后端开CrossOrigin而是用Vite的代理。在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }原理是开发服务器的Node进程充当中间人转发请求浏览器侧看到的所有请求都是同源的自然不存在跨域报错。后端不用写一行跨域配置。生产环境部署时前后端如果同域部署后文会讲同样不需要额外处理跨域。这里我要特别说一个反面案例有的项目前后端开发时用8080端口跑后端、5173跑前端直接在生产环境把前后端分开部署到不同域名结果浏览器跨域拦截一堆配置闹心还容易漏配。生产环境的最佳实践就一句话——前端打包出来的静态文件丢给Spring Boot托管即可后面第四部分具体说。4. 协同过滤在商城系统中的落地从算法原理到可运行代码终于到这篇博客的重头戏了。前面所有功能模块其实都是为了让推荐算法有“用武之地”。这一部分我会仔细讲协同过滤解决什么问题、两种主流思路怎么选、评分矩阵怎么构建、相似度怎么算、推荐结果怎么生成、冷启动怎么办最后给出一个能跑通的Java实现骨架。4.1 为什么推荐算法敢选协同过滤作为亮点网上商城项目千千万你凭什么让这篇项目有区分度答案就在“个性化”三个字上。协同过滤的思路和现实生活中的“物以类聚、人以群分”完全一致它不关心商品本身是手机还是耳机只依赖用户群体的行为历史来建立“什么商品可能被同一类人喜欢”的映射关系。它最大的优势是无需理解商品内容没有文本处理成本和人工标注成本电商系统的“用户-商品”行为数据天然适合它。对一个毕设或学习型项目来说拿它作为技术亮点工作量适中、讲解起来形象直观复杂度又足够支撑论文的技术章节可以说性价比极高。4.2 基于用户的协同过滤与基于物品的协同过滤怎么选协同过滤业界的两大流派User-based CF基于用户的协同过滤找到和我兴趣相似的用户把那些用户买过而我没买过的商品推荐给我。典型场景是“和你相似的人都买了”。Item-based CF基于物品的协同过滤找到和我历史上喜欢物品相似的物品推荐给我。典型场景是“看了又看”“买了又买”。在商城项目里我推荐的思路是主用Item-based CF因为用户数量通常远大于商品数量而用户兴趣相对稳定物品相似度矩阵可以在离线阶段提前算好在线推荐时直接查表性能最优。同时用户的行为日志数据可以同步支撑User-based CF做为对比实验论文里多一组对照数据也显得更严谨。对于真正需要同时落地两种算法并对比效果的情况可以先离线计算相似度矩阵再在在线推荐时用不同的Cache策略优化性能但对一个学习型项目二选一即可重点是把一条链路讲透。4.3 从行为日志到“用户-商品”评分矩阵协同过滤的输入必须是一个“用户对商品的偏好分数矩阵”但商城用户几乎没人会给商品打分所以我们要从行为数据里去“折算”偏好分。我设计的行为折算规则是浏览算1分收藏算2分加入购物车算3分购买算4分。同一条用户行为记录里同一件商品按最高分值计。如果同一用户对同一商品多次发生同类行为取最近一次时间窗内的行为避免点击了几次曝光就把分数堆上去。这样我们就能构造出类似这样的评分矩阵用户/商品商品A商品B商品C商品DU14301U22410U30243矩阵里每一行是该用户对所有商品的偏好向量每一列是该商品被所有用户偏好的向量。协同过滤所有后续计算都基于这个矩阵展开。4.4 余弦相似度与皮尔逊相关系数相似度计算的核心相似的衡量标准有两种最常用方法。余弦相似度计算两个向量夹角的余弦值公式是cos(θ) (A · B) / (|A| × |B|)用Java代码实现就是分别计算向量的点积和模长再相除。如果用户A和用户B的评分向量方向高度一致余弦值接近1说明两人的偏好结构很相似。皮尔逊相关系数的区别在于它先对每个向量做了“中心化”处理减去均值这样能纠正不同用户的打分习惯差异但也会增加计算复杂度。我的实践结果是在行为折算的整数分体系下余弦相似度已经够用而且比皮尔逊更适合稀疏矩阵。这里有一个实际操作的细节如果两个向量的公共非零项个数太少比如只有一个共同商品算出来的相似度即使接近1其实毫无参考价值。我会给相似度计算加一个过滤条件公共非零项少于2就视为相似度0。下面是用纯Java实现Item-based协同过滤的完整骨架算法的核心就是离线阶段生成“商品相似度矩阵”在线阶段根据用户历史行为加权取Top-N商品public void buildItemSimilarityMatrix() { long start System.currentTimeMillis(); ListBehaviorLog logs behaviorMapper.selectAll(); // 从行为表读取全量行为 MapLong, MapLong, Double itemUserScores new HashMap(); // itemId - (userId - score) // 构建“商品-用户”评分表 for (BehaviorLog log : logs) { double score scoreOf(log.getBehaviorType()); // 行为类型折算为分值 itemUserScores.computeIfAbsent(log.getProductId(), k - new HashMap()) .merge(log.getUserId(), score, Double::max); } ListLong itemIds new ArrayList(itemUserScores.keySet()); MapLong, MapLong, Double similarityMatrix new HashMap(); for (int i 0; i itemIds.size(); i) { Long itemA itemIds.get(i); MapLong, Double vecA itemUserScores.get(itemA); for (int j i 1; j itemIds.size(); j) { Long itemB itemIds.get(j); MapLong, Double vecB itemUserScores.get(itemB); double sim cosineSimilarity(vecA, vecB); if (sim 0.01) { similarityMatrix.computeIfAbsent(itemA, k - new HashMap()).put(itemB, sim); similarityMatrix.computeIfAbsent(itemB, k - new HashMap()).put(itemA, sim); } } } // 结果可写回缓存Redis或本地Map在线推荐时直接读取 similarityCache.save(similarityMatrix); System.out.println(相似度矩阵构建完成耗时 (System.currentTimeMillis() - start) ms); } private double cosineSimilarity(MapLong, Double vecA, MapLong, Double vecB) { long common 0; double dot 0; for (Map.EntryLong, Double e : vecA.entrySet()) { if (vecB.containsKey(e.getKey())) { common; dot e.getValue() * vecB.get(e.getKey()); } } if (common 2) return 0; // 公共非零项太少时无参考价值 double normA vecA.values().stream().mapToDouble(v - v * v).sum(); double normB vecB.values().stream().mapToDouble(v - v * v).sum(); if (normA 0 || normB 0) return 0; return dot / (Math.sqrt(normA) * Math.sqrt(normB)); }4.5 生成推荐结果加权打分与Top-N截断有了“商品-商品”相似度矩阵之后给用户做推荐就是两步找到用户历史上有过行为的商品集合用正反馈行为排除浏览量这类弱信号用它们作为“种子”累加相似商品的相似度加权得分然后按总分排序取前N个再过滤掉用户已经买过或看过的商品。public ListLong recommendForUser(Long userId, int topN) { ListBehaviorLog userLogs behaviorMapper.selectByUserId(userId); SetLong interacted userLogs.stream().map(BehaviorLog::getProductId).collect(Collectors.toSet()); MapLong, Double seedItems new HashMap(); for (BehaviorLog log : userLogs) { if (hasStrongSignal(log.getBehaviorType())) { // 收藏/加购/购买 seedItems.put(log.getProductId(), scoreOf(log.getBehaviorType())); } } MapLong, Double scores new HashMap(); MapLong, Double simMatrix similarityCache.getMatrix(); for (Map.EntryLong, Double seed : seedItems.entrySet()) { MapLong, Double neighbors simMatrix.getOrDefault(seed.getKey(), Collections.emptyMap()); for (Map.EntryLong, Double nb : neighbors.entrySet()) { if (interacted.contains(nb.getKey())) continue; // 过滤已交互商品 scores.merge(nb.getKey(), seed.getValue() * nb.getValue(), Double::sum); } } return scores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }这个在线推荐的耗时其实很短因为相似度矩阵已经离线算好了线上只是做几次HashMap查表和累加。这也是推荐工程里标准的“离线计算在线读取”套路写论文和答辩时把这句话说清楚分量会比“我瞎调的”强很多。4.6 冷启动问题新用户和新商品怎么办如果用户没有行为历史或者商品没有和任何用户发生过交互协同过滤的矩阵是空的推荐结果为空列表。这时候必须用兜底策略顶上。新用户推荐一些热门商品热度分可以按销量、浏览量、近七日加购数加权计算hotScore 0.5 * ln(viewCount 1) 0.3 * ln(salesCount 1) 0.2 * cartCount这个公式里取对数是为了压制爆款对排序的碾压效应让长尾商品也有机会露脸。新商品则给予随机加权曝光。对新商品冷启动另一个思路是把商品品类和价格区间作为“伪行为”补充进矩阵例如同品类同价位段的商品初始相似度可以设为一个较小的基础值这个方法在中小数据量下能改善效果实际项目中可以试试看。冷启动不是算法问题而是策略问题策略设计得合理与否直接体现在首次打开商品流时用户愿不愿意继续逛下去。4.7 行为数据采集不埋点就没有推荐前文多次提到行为日志但如果没有埋点方案它就是纸上谈兵。前端埋点的方法很直接在商品卡片点击事件、商品详情页onMounted、加入购物车点击、提交订单成功这四个时机分别调用/api/behavior/report接口把productId和行为类型发给后端后端在t_user_behavior表里插入一条记录。这里有一个实操细节浏览行为的插入频率非常密集用户每次上下滑就会触发好几个商品曝光如果每个曝光都实时入库数据库压力大且数据噪声高。建议对“浏览”行为做节流同一用户对同一商品当天最多记录一次浏览行为这是写SQL时用唯一索引ON DUPLICATE KEY UPDATE就能做到的。收藏、加购、购买则每次实时记录因为它们本身不是高频操作而且信号强度高。4.8 推荐接口与前端展示推荐功能对前端暴露两个接口/api/recommend/home返回首页商品流的商品Id列表/api/recommend/detail?productIdxxx返回商品详情页的“看了又看”推荐列表。后端拿到推荐Id列表后再批量查询商品完整信息组装成统一响应结构。前端首页在Vue组件挂载时调一次getHomeRecommend()渲染一个横向滚动的商品卡片区域点击卡片跳详情页。详情页里的“看了又看”可以复用同一个接口参数传当前商品Id这正好是Item-based CF的最佳展示场景因为Item-based CF本身就是基于“当前看的这个商品”来推荐相似商品逻辑非常自洽。5. 协同过滤算法写在Java里还是写在Python里——性能与工程实践的平衡点很多人在做完基础功能后面对的核心纠结是算法代码到底应该放哪。直接在Spring Boot项目里用Java写好处是部署零成本一个Jar包全搞定缺点是同样的代码Java写起来比Python啰嗦而且离线计算大数据量时Java代码里缺少pandas那种现成的矩阵运算库完全手写两层循环性能不佳。我的建议是看数据规模。对于学习和毕设演示的数据量几千条行为记录Java直接在Service里算完全没问题。但如果行为数据有几十万条以上我更推荐的方式是单独写一个Python脚本离线算完相似度矩阵之后把结果JSON或CSV导出再让Java项目启动时加载这份预计算结果。这种“Python离线算Java在线查”的分工在实际工业界也很常见因为在线推荐对延迟敏感必须轻量而离线计算用Python科学计算工具链开发速度快很多。下面给出Python端的实现核心用pandas和numpy可以非常简洁地完成评分矩阵构建和相似度计算import pandas as pd import numpy as np def build_similarity(logs: pd.DataFrame, weight_map: dict) - pd.DataFrame: logs[score] logs[behavior_type].map(weight_map) # 构造 用户-商品 评分矩阵行是用户列是商品 matrix logs.pivot_table(indexuser_id, columnsproduct_id, valuesscore, aggfuncmax, fill_value0) item_vec matrix.T.values # 商品向量矩阵 # 余弦相似度 norm np.linalg.norm(item_vec, axis1, keepdimsTrue) sim item_vec item_vec.T / (norm norm.T 1e-9) sim_df pd.DataFrame(sim, indexmatrix.columns, columnsmatrix.columns) return sim_df这段代码里aggfuncmax对应前面说过的“同用户同商品取最高行为分”的去重逻辑矩阵乘法item_vec item_vec.T一步完成所有商品两两之间的余弦相似度计算在性能上远比手写循环优秀。真实线上计算时还要考虑稀疏矩阵优化用scipy.sparse这里不展开数据量到了再研究也不迟。6. 功能联调、打包部署与典型问题排查最后一个部分把前面所有模块串到一起看一个普通Vue项目如何真正“跑起来”以及这个过程中最折磨人的几个问题。这些坑不是我从网上复制来的是我确实带项目时反复帮人排查过的高频故障。6.1 开发环境联调顺序先说联调顺序按顺序来能极大省时间先调通登录接口和用户信息接口因为几乎所有页面的请求头都要带Token再调商品列表与搜索接口正常情况下后端返回一个分页结构前端渲染出第一页就算通接着调购物车和订单接口重点关注事务和库存校验用两个浏览器同时下单同一个商品验证库存不会变负数最后调推荐接口先手工造几条用户行为数据再看推荐结果是否符合预期。一个调试技巧前端控制台的Network面板是排查接口问题最快的入口。先看请求是否发出去了如果压根没有网络请求问题在前端路由或Axios封装如果请求发出去了但返回了红色直接看状态码和响应体500就是后端抛异常把后端控制台的堆栈信息拿过来看比自己瞎猜效率高一百倍。6.2 前端打包后放进Spring Boot部署结构说明部署最稳妥的方式是在Spring Boot的src/main/resources/static目录下放置Vue打包产物。前端执行npm run build之后把生成的dist/目录里的所有文件复制到static目录下重新打包Spring Boot Jar这样一个进程就同时托管了前端页面和后端接口。用户访问http://服务器IP:8080/就直接进入商城首页接口请求路径是/api/**被Spring Boot正确识别为接口路由。这里有个关键细节Vue路由如果用的是HTML5的history模式路由路径不带#用户从首页跳到/product/123后再刷新该页面Tomcat会去把/product/123当成一个服务端资源去找结果404。解决方案有三个在路由上退回hash模式路径带#不太好看但零成本为Spring Boot写一个转发配置把除/api以外的所有路径转发到index.html或者把前端单独交给Nginx托管用try_files指令兜底。新手最省心的选择是第一种等理解了机制再切换history模式不迟。6.3 运行期常见问题速查中文乱码问题。Spring Boot内置Tomcat对请求体里的中文一般在Spring Boot 2.7里默认UTF-8问题不大。后端返回给前端乱码检查接口返回时是否正确地设置了UTF-8使用RequestMapping的produces属性直接指定或者在统一返回类里确保编码。插入数据库乱码检查application.yml里的characterEncodingutf-8配置和MySQL表本身的字符集是否为utf8mb4三处同时配置正确。跨域问题。生产环境前后端同域后不存在跨域开发环境用Vite代理即可。如果你用了Nginx做反向代理需要在Nginx配置里加上proxy_set_header几个头把Host和X-Real-IP传递到后端不然对接支付回调这类需要真实IP和Host的功能时会出问题。缓存问题。改了前端代码刷新页面还是旧的这是浏览器缓存了静态资源。Vite打包默认生成带hash的文件名内容变了文件名就变理论上不会缓存旧文件。真正会遇到的是后端接口变更后前端反复拿缓存的数据可以在Axios配置里给请求头加Cache-Control: no-cache或者排查是不是Nginx开了静态缓存。MyBatis-Plus的Mapper文件扫描路径问题。经常有同学明明写了XML里的SQL却报Invalid bound statement多数是配置文件里mapper-locations路径写错了或者实体类没有加TableId注解导致主键策略错乱。这类报错最有效的排查方式是看启动日志里是否有解析到XML文件的提示没解析到就检查路径和文件名。6.4 答辩/汇报时的推荐算法讲解思路如果是毕设项目算法部分肯定会被问到。我建议准备一条清晰的讲解链路你们系统用的推荐算法是什么、输入是什么、输出是什么、冷启动怎么办、效果怎么评估。把这条链路背熟之后问到任何相关细节都能从容作答。额外提一个加分项准备一个“效果验证”的环节。找两个行为差异明显的测试账号一个专门收藏购买电子类商品一个专门收藏购买图书类商品录屏演示两个账号的商品推荐流确实不同。这个演示比任何口头解释都有说服力也说明你是真的从数据到结果完整跑通过整个推荐链路而不是只背了算法公式。按我个人的经验一个Shop系统真正做到“能用”不难真正做到“能讲清楚”却需要你对每个模块都有完整的理解和数据串联。推荐算法恰好是把整个系统从“增删改查仓库”变成“有智能感的产品”的那一环。希望这篇整理能让你少踩几个坑尤其是推荐算法落地时那些“看书都会、一写就废”的坎。