作为一个在Java Web开发里摸爬滚打好几年的老家伙,最近帮一位做毕设的同学梳理了一个挺典型的项目:SpringBoot + Vue前后端分离的东北特产销售系统平台,里面还集成了协同过滤推荐算法。很多同学一看到“协同过滤”四个字就发怵,觉得算法高不可攀,其实落地到毕业设计这个层级,核心就是数据处理和相似度计算那几板斧。这篇文章就把这个项目从技术选型、数据库设计、算法实现到部署排坑的完整链路拆开讲清楚,适合正在准备Java Web毕设、或者想快速上手前后端分离电商类项目的同学直接参考复现。
1. 项目定位与整体设计思路
1.1 这个系统到底解决了什么问题
东北特产销售系统,说白了就是一个垂直类目的电商平台,卖的是东北特色的农产品、土特产、手工艺品这类商品。普通的电商系统只解决“展示商品-下单支付-订单管理”这条交易链路,这个项目在标准电商功能之上,叠加了一个“猜你喜欢”的推荐模块,也就是利用协同过滤算法,根据用户历史行为(浏览、收藏、购买、评分)计算用户之间的相似性,把相似用户喜欢的商品推荐给目标用户。
站在毕设的角度,这个系统的功能层次非常清晰:基础层是用户管理、商品展示、购物车、订单管理,这是Java Web课程设计里最常见的模块;进阶层是评论评分、推荐算法、销售数据可视化,这是给答辩加分的差异化亮点。一套代码既能覆盖常规功能开发,又有算法落地,应付开题报告和答辩的“创新点”完全够用。
1.2 技术选型背后的考量
技术栈选的是SpringBoot + Vue,这是当前Java Web毕设的绝对主流组合,核心原因有三个。
第一个原因是前后端分离的开发模式能大幅拉高效率。前端工程师和后端工程师(或者一个人身兼两职)可以并行开发,后端只需要提供稳定的RESTful接口,前端用Vue的组件化开发把页面拆成一个个独立单元,互不阻塞。对一个人单挑整个项目的学生来说,这种模式也能减少后端模板引擎和前端代码纠缠不清带来的混乱。
第二个原因是SpringBoot生态太成熟了。集成MyBatis做数据库操作、Spring Security做权限控制、Redis做缓存(虽然这个项目不一定用),几乎都有现成的starter,配置简化到极致。对于毕设这种工期紧张的项目,减少造轮子就是最大的胜利。
第三个原因是Vue的学习曲线和社区资源非常适合学生。Element UI组件库一套下来,后台管理界面基本就是拼积木。而且Vue的响应式数据绑定对电商类页面的交互(购物车加减、筛选联动)特别友好,不用像JQuery时代那样手动操作DOM。
1.3 前后端分离架构下的模块划分
这个项目在架构上分成了两个独立的子工程:前端工程和后端工程。后端工程内部按经典三层架构组织:Controller层接收请求、Service层处理业务逻辑、Mapper层操作数据库。前端工程按Vue的标准结构组织:views目录放页面组件、router配置路由、api目录封装axios请求。
模块划分上,典型的功能模块包括:用户模块(注册、登录、个人信息、收货地址管理)、商品模块(商品列表、商品详情、分类筛选、关键字搜索)、购物车模块(加入购物车、修改数量、删除、结算)、订单模块(提交订单、订单列表、订单状态流转)、评论评分模块(针对已购商品评价、打分)、推荐模块(协同过滤算法生成个性化推荐列表)、管理后台(商品管理、订单管理、用户管理、数据统计)。
这里有个设计上的小技巧,推荐模块不要做成一个独立的“推荐系统”黑盒,而是把它嵌入到商品列表和首页的展示逻辑里面。用户在首页看到的“猜你喜欢”区域、在商品详情页看到的“看了又看”区域,都是推荐模块在提供数据。这样推荐功能就自然融入了电商主流程,而不是一个看起来像是硬塞进去的演示功能。
2. 协同过滤算法的核心拆解
2.1 基于用户的协同过滤原理
协同过滤分两类:基于用户的(UserCF)和基于物品的(ItemCF)。这个项目采用的是基于用户的协同过滤,核心假设是:跟你兴趣相似的人喜欢的商品,你也大概率会喜欢。
算法执行分三步走。第一步,建立“用户-商品”评分矩阵。评分来源可以是显式的(用户给商品打分1到5分),也可以是隐式的(用户浏览了商品、收藏了商品、购买了商品,转化成加权分值)。第二步,计算用户之间的相似度,挑选出与目标用户最相似的N个邻居用户。第三步,根据邻居用户对商品的评分,预测目标用户对该商品的评分,把预测评分最高的商品推荐出去。
在毕设的演示场景下,数据量可能不大,但我强烈建议准备一套模拟评分数据(比如20个用户、50件商品、几百条评分记录),方便测试算法的推荐效果。某开发者在试跑的时候发现,如果评分矩阵太稀疏(用户数量少、评分记录更少),相似度计算结果会非常不稳定,推荐结果基本靠缘分。
2.2 相似度计算与预测评分的具体实现
相似度计算最常用的方案是余弦相似度。把每个用户的评分记录看成一个向量,向量维度是商品的ID,维度上的值是评分(没有评分的商品的值为0)。用户A和用户B的相似度,就是这两个向量的余弦夹角值,公式是:
similarity(A, B) = sum(A_i * B_i) / (sqrt(sum(A_i^2)) * sqrt(sum(B_i^2)))A_i和B_i分别是用户A和用户B对第i个商品的评分值。余弦相似度的好处是取值范围在-1到1之间,值越大说明两个用户的评分习惯越像,计算逻辑清晰,在答辩时特别好讲解,也方便用手算给评委做推演。
但在实际代码里有一个容易踩的坑:评分向量里0值太多。两个用户可能只在同一家店买过东西,其余的商品都没交互过,如果按全量商品维度计算余弦相似度,零值的占比会稀释掉真正重要的共同评分项。更稳妥的做法是只取两个用户共同评过分的商品集合来计算,也就是改良版的余弦相似度公式。
预测评分的公式也很关键。目标用户u对商品p的预测评分,是其邻居集合N中对商品p有评分的用户的评分的加权平均:
prediction(u, p) = sum(over v in N) sim(u, v) * score(v, p) / sum(over v in N) |sim(u, v)|这里需要特别注意,只有那些对商品p评过分的邻居才参与计算,没评过分的不拉入分子。代码实现里通常先筛选出邻居中对p有评分的用户子集,再套公式。
我在给同学看代码的时候习惯让他画一张表,把用户、商品、评分列出来,然后手工算一遍余弦相似度和预测评分,对着代码把每一步对应上。真理解了这个过程,答辩环节评委问什么细节都不慌。
2.3 冷启动问题的处理策略
协同过滤最大的短板就是冷启动。新用户没有任何行为记录,没法给他找邻居;新商品没有任何用户评分,没法推荐给任何人。毕设答辩时很多评委都会盯着这个问题问,所以项目里至少要做一个简单的兜底方案。
我在这个项目里落的方案是冷启动用户走“热门商品推荐”:计算商品的用户交互总量(购买次数、收藏次数、评分次数加权重),按权重从高到低排列,取前N个推荐给没有任何行为记录的新用户。这个方案的合理性在于,热门商品代表的是整个平台用户的集体偏好,在没有个性信息时,推荐热门商品的行为相当于公司让新人入职时先跟着团队默认方案走,虽然不是最优解,但大概率不会犯大错。
对冷启动商品,处理方式就更直接了:在算法的候选商品池里排除掉没有评分记录的商品,等有用户对它产生行为后再纳入协同过滤的候选集。商品详情页的“看了又看”模块用基于物品的协同过滤来做——统计浏览了商品A的用户还浏览了哪些商品,这算是一个变种的热门相似度,实现成本极低,但效果直观。
3. 数据库设计与后端接口实现
3.1 SQL脚本里的核心表结构与设计思路
项目的SQL脚本里一般包含8到10张表,核心的表必须覆盖用户、商品、评分、订单这几块。我建议重点关注评分表,因为它是协同过滤算法的数据源。
核心表结构可以参考这个模式:
- sys_user用户表:id、username、password、nickname、avatar、phone、create_time。密码一定要存加密后的值,用MD5或者BCrypt都行,别存明文。
- product商品表:id、name、category_id、price、stock、image、description、sales_count、status。price字段用Decimal类型,别用Float,否则金额浮点误差迟早出问题。
- category分类表:id、name、sort_order。商品和分类是一对多关系。
- cart_item购物车表:id、user_id、product_id、quantity、checked、create_time。加个checked字段来标记选中结算状态。
- orders订单表:id、order_no、user_id、total_amount、status、create_time、receiver_name、receiver_phone、receiver_address。订单号和用户ID建联合索引,订单查询会快很多。
- order_item订单明细表:id、order_id、product_id、product_name、price、quantity。注意这里冗余了product_name和price字段,目的是防止商品改名或者改价之后历史订单显示错误。
- rating评分表:id、user_id、product_id、score、content、create_time。这条表是协同过滤算法的核心数据源,user_id和product_id要建联合唯一索引,防止同一个用户对同一商品重复评分。
- user_favorite收藏表:id、user_id、product_id、create_time。收藏行为也可以作为隐式评分的来源。
SQL脚本里除了建表语句,别忘了把基础数据一并准备好。我见过太多同学项目能跑起来,但页面上看不到任何商品,就是因为没有初始化数据。建议在脚本里至少准备20个用户、50件商品、几百条评分记录,这样首页和推荐模块一打开就有效果,演示体验完全不同。
3.2 后端推荐接口的数据流设计
后端接口里最核心的推荐接口是“获取我的推荐列表”,路径一般设计成GET /api/recommend/{userId}。整个数据流是这样的:
- Controller层接收用户ID,调用推荐Service;
- Service先从rating表中查出当前用户的历史评分记录;
- 如果历史记录为空,走冷启动逻辑,调用商品Service查热门商品集合;
- 如果历史记录非空,查出所有其他用户的评分记录,计算用户间相似度,筛选Top K邻居;
- 汇总邻居的评分商品,过滤掉当前用户已经买过或者评过分的商品,保证推荐结果中不出现用户已交互过的商品;
- 用预测评分公式计算候选商品的预测值,按预测值从高到低排序,取Top N返回。
代码里还有一个细节,就是查询效率问题。如果直接查出全部评分再在Java内存里计算相似度,数据量小的时候没什么感觉,但数据量稍微上来之后响应时间会明显变慢。建议在Mapper层用SQL做一次预处理,只查出与当前用户有共同评分商品的用户ID,大幅缩小参与相似度计算的数据集。
3.3 前端Vue页面与接口联调要点
前端页面的划分与后端接口一一对应。首页(Home)包含商品推荐区和普通商品列表;商品详情页展示商品信息、用户评分和评论列表;购物车页面操作购物车条目和结算;个人中心展示订单、收藏和评分记录。管理后台包含商品管理、订单管理、用户管理和数据看板。
接口联调阶段最需要留心的是跨域问题。前端Vue开发服务器跑在8080端口(或者5173,取决于Vite配置),后端SpringBoot跑在8081或9090端口,两个端口不同必然产生跨域请求。在后端写一个CORS配置类,放行前端地址的跨域请求就行。这个细节很多同学在开发阶段没在意,前端请求全被浏览器拦截,白屏半天才醒悟是跨域问题。
前端axios请求还有一个老问题:每次请求都要带上token来表示用户身份。建议在axios拦截器里统一从localStorage取token,加到请求头里面,不要在每一个具体的请求方法里手工拼接。后端的拦截器统一拦截非登录接口校验token,避免把认证逻辑散落到每个Controller方法里。
3.4 接口文档的落地方式
毕设项目要求提供接口文档,这个不能敷衍。常用的落地方式有两种:一是写一个Markdown文档,按模块列出每个接口的URL、请求方法、请求参数、返回结构;二是集成Swagger(springfox或者springdoc),让接口文档自动生成,页面访问/swagger-ui.html就能看到在线接口列表。
我个人更推荐把Swagger集成了之后,再手动写一份精简版接口文档,专门给答辩评委看的。Swagger展示的是全量技术细节,手动文档可以只讲核心接口的调用逻辑(比如注册登录流程、推荐接口的入参和出参),更能体现你对业务流程的掌握程度。答辩时如果评委想看系统整体接口结构,打开Swagger现场点几下,印象分会加分不少。
4. 项目导入运行与核心环节实测
4.1 本地环境准备清单
运行这套系统需要准备的环境非常标准:
| 组件 | 版本建议 | 备注 |
|---|---|---|
| JDK | 1.8或11 | SpringBoot 2.x搭配JDK 1.8最稳,没必要上太高 |
| Maven | 3.6+ | 用来拉依赖和打包 |
| MySQL | 5.7或8.0 | 导入SQL脚本 |
| Node.js | 14以上 | 运行Vue前端,建议用16或18 LTS版 |
| IDE | IDEA或VS Code | 后端用IDEA,前端用VS Code,也可以全用IDEA |
有个小坑提醒一下:MySQL 8.0的驱动类名和连接URL写法跟5.7不一样,数据库驱动用com.mysql.cj.jdbc.Driver,连接URL要加serverTimezone=Asia/Shanghai参数,否则可能会报时区错误。
4.2 后端SpringBoot项目的导入配置
后端工程导入IDEA之后,第一步不是急着运行,而是检查配置文件。在application.yml或者application.properties里,核心要改的是数据源配置:
spring.datasource.url=jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=你自己的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver再检查一下MyBatis的mapper扫描路径配置,确保@MapperScan("com.example.shop.mapper")指向的是你实际的Mapper包位置。接着用Maven的clean和package命令编译一遍代码,确认没有编译错误再启动。
启动成功的标志是控制台输出SpringBoot的启动日志,并且端口监听成功。常见的启动失败原因就几个:端口被占用(改server.port配置)、MySQL服务没启动、数据库名和配置里的不一致、依赖下载不完整(在IDEA里刷新Maven重新拉取)。
4.3 前端Vue工程的安装与联调
前端工程打开后,先在终端执行npm install安装依赖。如果网络状况不稳定,装到一半报错,换成淘宝镜像源重装即可。
依赖装好后,重点检查前端配置文件里后端接口的地址。Vue项目通常会把请求地址放在.env.development文件或者api工具模块里,确保baseURL指向后端的实际地址(比如http://localhost:9090/api)。经常有同学后端跑起来了,前端页面发请求却一直404,原因就是baseURL配的端口跟后端启动端口不一致。
执行npm run dev启动Vue开发服务器,浏览器访问提示的本地地址,页面能正常打开,登录接口能通,说明前后端联调成功了。然后再验证一下推荐模块:注册一个新账号登录,看看首页推荐位是否显示热门商品;用同一个账号给几件商品评分后再次刷新首页,看看推荐结果是否发生了变化。
4.4 协同过滤算法的参数调优实录
协同过滤算法里有几个关键参数需要按实际数据调整。第一个是邻居数量K,通常取值的参考范围是10到50,数据量小的时候取10-20,数据量大可以适当放宽。第二个是推荐商品数量N,演示场景一般取8到12件,展示效果饱满。这两个参数的取值本质上是精确度和召回率的博弈,K太大,推荐结果趋于大众化,个性化变弱;K太小,邻居样本不足,预测不稳定。
我在实际调参时的做法是写一个简单的评测脚本:把评分数据按时间切分,前80%做训练集,后20%做测试集,用召回率和平均绝对误差这两个指标评估不同K值下的算法效果。虽然毕设不需要交出严谨的评测报告,但手里有这组数据,答辩的时候说“我通过离线实验对比了K=10、20、30的效果差异”,瞬间比空口说“算法效果好”有说服力得多。
5. 常见问题与排查技巧实录
5.1 部署运行高频问题速查
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 后端启动报端口被占用 | 8080端口被其他进程占用 | 改SpringBoot的server.port,或者杀掉占用进程 |
| 前端列表页空白 | baseURL配置指向错误的后端地址 | 检查前端接口基础路径与后端实际端口是否一致 |
| 登录接口返回401 | token校验逻辑误拦截了登录请求 | 检查拦截器配置,登录接口应放行 |
| 上传商品图片失败 | 静态资源映射路径与存储目录不一致 | 检查WebMvcConfigurer中addResourceHandlers配置 |
| JSON序列化报错 | 实体类存在循环引用或日期格式问题 | 添加@JsonFormat注解规范日期格式,使用DTO收窄字段范围 |
| npm install卡住 | 默认源连接不稳定 | 切换淘宝镜像源后删除node_modules重装 |
排查思路上我想再强调一点:毕设项目的报错绝大多数能从堆栈日志的第一行找到答案,不需要一上来就去搜索引擎抄方案。把完整的报错信息复制下来,看是数据库连接拒绝还是空指针异常,定位到具体代码行,解决路径自然会浮现出来。
5.2 推荐结果不合理的调整策略
有同学在实测时发现,推荐列表出来的东西跟用户喜好完全不搭边。这种问题通常不是算法代码写错了,而是数据层面的问题。我遇到过三种典型情况:
第一种情况是评分数据太稀疏。20个用户里大部分都只给1-2件商品评过分,共同评分的用户对少得可怜,余弦相似度的分母接近于零,算出来的相似度几乎全等于0。调整方案是加大模拟评分数据的密度,让每个用户至少对5件以上商品评分,并且预设一些“兴趣圈层”数据(比如一组用户都喜欢参类特产,另一组用户都喜欢木耳蘑菇类),算法立刻就能输出可见的差异化推荐。
第二种情况是忽略商品本身的品类约束。比如用户已经在浏览界面上明确选择了需要的是野生菌类商品,但推荐结果却推荐了人参产品。如果是基于全局评分计算,推荐结果确实可能跨品类漂移。调整方案是在推荐接口里加一个可选的品类条件过滤参数,用户在分类页请求推荐时,先按品类缩小候选商品池,再做协同过滤排序。
第三种情况是用户行为分数权重设计不合理。很多项目把购买、评分、收藏、浏览都转成统一分数,但权重的设定却拍脑袋。我建议的权重映射是:购买记为5分、评分直接用用户打的价值、收藏记为3分、浏览记为1分。这个策略的考量是,购买行为代表最强偏好信号,评分是显式偏好表达,收藏属于中强度兴趣,浏览可能是误点,权重逐级递减。
5.3 数据一致性相关的隐藏坑
业务代码中还有一个学生很容易忽略的点:订单提交时的库存扣减操作不是原子的。很多同学先查询库存判断是否够用,然后更新库存,最后创建订单记录,这中间如果并发请求同时到达,就会发生超卖。解决办法是使用SQL的原子更新语句来实现扣减库存,比如:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这种方式利用数据库行级锁保证同一件商品的库存扣减在同一时间只被一个事务成功执行,天然规避了并发超卖问题。虽然答辩场景不一定有压测环节,但在代码里体现安全意识,评委问起来也有底气。
评分表的唯一索引也是容易漏掉的坑。如果同一个用户对同一件商品存在两条评分记录,协同过滤的数据源就出现了脏数据,推荐计算会引入偏差。建议在rating表建UNIQUE KEY uk_user_product(user_id, product_id),并且在前端交互上限制同一个用户对同一商品只能评分一次。数据库约束和前端限制双保险,才能保证算法数据源干净。
6. 从毕设到项目的扩展建议
最后分享一点超出毕设本身价值的思考。这个项目的架构和代码模式,其实是一个可以复用的电商系统基座。等答辩结束,如果你想让它继续成长,有几个低成本、高价值的扩展方向。
第一个方向是引入Redis做缓存。把商品列表、热门商品、推荐结果这类读多写少的数据缓存起来,每次请求不用都打数据库,系统的并发承载能力立刻上一个台阶。代码改动幅度不算大,主要是在Service层加缓存读写逻辑。
第二个方向是把基于物品的协同过滤补上。现在的推荐只做了基于用户的协同过滤,基于物品的协同过滤可以实现在商品详情页的“相关推荐”区域,根据用户对当前商品的评分历史,找到相似商品做推荐。两者组合起来,推荐功能的覆盖面会更完整。
第三个方向是管理后台的数据统计模块加几张可视化图表。用ECharts画销售趋势折线图、分类占比饼图、用户增长柱状图。这个扩展工作量不大,但视觉冲击力很强,答辩或演示的时候放在最后展示,能给人留下“系统做得很完整”的印象。
我自己带过好几届学生的毕设,最大的体会是:毕业设计不是比谁的技术名词多,而是比谁能把技术名词真正落地成一个能跑通、能演示、能讲清楚逻辑的完整系统。像协同过滤这种算法,只要你把评分矩阵构建、相似度计算、邻居选择、预测评分这四个环节真正自己码过一遍,理解就完全不一样了。这个项目建议你拿到手先别急着跑,对照着SQL脚本把表结构过一遍,再打开后端代码找到推荐模块的那个Service类,单步调试走一遍完整流程,收获会远超“能跑起来”本身。