当前位置: 首页 > news >正文

基于用户行为的SpringBoot商品推荐系统(含协同过滤算法、MySQL脚本与完整开发文档)

本文还有配套的精品资源,点击获取

简介:直接可运行的Java推荐系统项目,用SpringBoot搭建,核心是用户-物品协同过滤算法,能根据用户浏览、收藏、购买等历史行为,实时生成个性化商品推荐列表。项目结构清晰:pom.xml已配置好所有依赖,MySQL数据库脚本(springboot300z2.sql)一键导入即可初始化数据;src/main/java下是完整的后端逻辑,包括推荐服务、用户行为分析、相似度计算等模块;templates和static目录支持基础前端展示;test目录包含关键业务单元测试;配套的springboot开发文档.docx详细说明了算法原理、模块调用关系、接口设计及部署步骤。项目自带Maven Wrapper(mvnw/mvnw.cmd),Windows和Linux下无需预装Maven就能打包运行;.gitignore文件已配置,适配基础Git版本管理;target目录为编译输出占位,实际部署需执行mvn clean package生成jar包。适合用于课程设计、毕设实现或中小电商后台推荐功能的技术原型验证。

1. 项目概述:这不是一个“玩具系统”,而是一套能跑通真实推荐闭环的工程化实践

我带过六届毕业设计,也帮三所高校信息学院做过课程设计指导包,见过太多标着“推荐系统”的Java项目——点开一看,User类里只有id和name,Item类里只有id和title,RecommendService里写着return new ArrayList<>(); // TODO: implement later。这种代码连编译都未必通过,更别说解释清楚“为什么协同过滤要先算用户相似度,而不是直接算商品相似度”。今天这个SpringBoot商品推荐系统,是我去年给一家区域生鲜电商做技术验证时沉淀下来的最小可行原型(MVP),它不追求算法SOTA,但每一步都踩在真实业务的痛点上:用户行为稀疏怎么处理?冷启动问题怎么兜底?MySQL里一张user_behavior表如何支撑实时推荐请求?这些不是论文里的假设,而是每天凌晨三点数据库慢查询日志里跳出来的数字。

核心关键词就五个:SpringBoot、协同过滤、商品推荐、Java推荐系统、MySQL推荐——它们不是并列关系,而是层层咬合的技术栈链条。SpringBoot是骨架,让整个系统能在5分钟内从零启动;协同过滤是心脏,决定推荐结果是否“像人”;商品推荐是目标,所有代码最终要输出一个List ;Java推荐系统是实现语言与范式约束,意味着我们必须面对JVM内存模型、线程安全、事务边界这些现实问题;MySQL推荐则是数据底座,它决定了我们不能把“计算用户相似度”写成一个O(n²)的嵌套循环,而必须拆解成可索引、可分页、可缓存的SQL片段。这套系统真正能用起来,是因为它默认就规避了三个新手最容易栽跟头的坑:第一,它没用Redis做临时存储,所有中间状态都走MySQL+内存缓存组合,避免学生部署时卡在“Redis配置不会写”;第二,它的协同过滤不是教科书式的纯矩阵分解,而是做了行为权重分级——购买行为权重为3,收藏为2,浏览为1,这个细节在开发文档.docx第17页有公式推导;第三,它预留了接口扩展点,比如你把UserBehavior实体里的behaviorType字段从枚举改成字符串,就能无缝接入“加购”“分享”“评论”等新行为类型,不用动推荐引擎主逻辑。如果你是大三学生正在赶毕设 deadline,或者小公司后端想给现有商城加个“猜你喜欢”模块,这套代码不是让你抄作业,而是给你一个能摸到、能改、能压测、能上线的实体参照物。

2. 整体架构与设计思路:为什么选用户-物品协同过滤,而不是内容推荐或深度学习?

2.1 算法选型背后的业务权衡

很多人一提推荐系统就默认“必须上深度学习”,但我在生鲜电商项目里做过AB测试:用LightGBM训练的CTR模型,在首页Banner位点击率提升12%,但在商品详情页底部的“看了又看”模块,效果反而比基础协同过滤低3%。原因很简单——详情页用户意图明确,他刚看完“五常大米”,大概率还想看“东北黑土地有机米”或“电饭煲”,这时候基于历史行为的相似用户群体画像,比用商品标题TF-IDF向量匹配更稳。所以本项目坚定采用用户-物品协同过滤(User-Based Collaborative Filtering),不是因为它多先进,而是因为它最贴近中小场景的“性价比三角”:开发成本低、运维压力小、业务解释性强。

具体来说,我们放弃物品协同过滤(Item-Based),因为它的冷启动问题更致命。新上架一款“云南松茸”,物品协同过滤需要等至少10个用户买过它才能建立关联,而用户协同过滤只要有一个老用户A买过松茸,再找到和A相似的用户B,就能立刻把松茸推荐给B——这对日均上新20款商品的生鲜平台至关重要。算法流程被拆成三步硬性落地:
1.行为采集层:拦截Controller层的/api/v1/user/{userId}/behavior接口,将浏览、收藏、购买事件写入MySQL的user_behavior表,字段包含user_id,product_id,behavior_type,weight,create_time
2.相似度计算层:定时任务(@Scheduled)每4小时执行一次,用皮尔逊相关系数(Pearson Correlation Coefficient)计算用户两两之间的相似度,结果存入user_similarity表,只保留相似度>0.6的Top50记录;
3.推荐生成层:用户请求/api/v1/recommend/{userId}时,查user_similarity表拿到5个最相似用户,聚合他们的行为权重,按SUM(weight)降序取前10个未交互商品返回。

这个设计刻意回避了“实时计算相似度”的陷阱。我试过用Stream API在内存里实时算,单次请求耗时从80ms飙到1.2s,QPS直接掉到3以下。现在用预计算+缓存策略,实测200并发下P95延迟稳定在110ms以内,MySQL慢查询日志里再也看不到SELECT * FROM user_behavior WHERE user_id IN (...)这类全表扫描。

2.2 技术栈组合的务实选择

SpringBoot版本锁定在3.1.12,不是追新,而是因为3.2+的虚拟线程(Virtual Threads)在我们的MySQL连接池(HikariCP)上会出现连接泄漏,这个问题在Spring Boot官方GitHub Issue #35281里有详细复现。pom.xml里所有依赖都经过生产环境压测:MyBatis-Plus 3.5.3.1用于简化CRUD,但关键的相似度计算SQL我们手写在Mapper XML里,避免LambdaQueryWrapper生成的SQL无法利用复合索引;Lombok 1.18.30解决样板代码,但@Data注解被禁用在Entity类上,只允许用@Getter/@Setter分开声明,防止JSON序列化时循环引用;JUnit 5.9.2的单元测试覆盖了推荐服务主干路径,特别是RecommendService.calculateSimilarity()方法,我们用Mockito模拟了10万行行为数据,验证皮尔逊公式在稀疏矩阵下的数值稳定性。

MySQL脚本(springboot300z2.sql)的设计直击电商推荐痛点。user_behavior表主键是(user_id, product_id, behavior_type)联合唯一索引,而不是自增ID——这样既能防重复埋点,又能让SELECT * FROM user_behavior WHERE user_id = ?走索引。user_similarity表的similarity_value字段用DECIMAL(5,4)类型,精度控制在0.0001,避免浮点数误差导致相似用户排序错乱。最关键是product表里加了sales_volumereview_score两个冗余字段,它们不在ER图里,但推荐结果排序时会参与加权计算:“相似用户买的商品”权重×0.7 + “该商品销量排名”权重×0.2 + “评分均值”权重×0.1,这个混合排序逻辑写在RecommendController的getHybridRanking()方法里,开发文档.docx第23页有完整权重分配表。

2.3 工程化细节:为什么自带mvnw却不用Docker?

项目根目录下的mvnwmvnw.cmd不是摆设。我亲眼见过学生在实验室Windows电脑上装JDK17失败,折腾半天连java -version都报错,最后发现是环境变量PATH里混进了中文路径。mvn wrapper把Maven二进制包打包进.mvn/wrapper/maven-wrapper.jar,执行./mvnw clean package时自动下载对应版本(3.9.6),全程不依赖系统Maven。但为什么没配Dockerfile?因为中小型电商的运维同学往往只熟悉Linux基础命令,让他们学Docker Compose编排MySQL+SpringBoot+Redis,不如直接给一份deploy.sh脚本——它会自动检测端口占用、创建数据库、导入SQL、启动jar包,并把日志轮转配置写死在application-prod.yml里。这个细节藏在开发文档.docx附录B的“一键部署指南”中,连nohup java -jar -Xmx512m springboot300z2.jar --spring.profiles.active=prod > /dev/null 2>&1 &这种命令都给了完整参数说明,包括为什么堆内存限定在512MB(测试发现超过800MB会导致GC停顿超200ms,影响推荐接口SLA)。

3. 核心模块解析与实操要点:从数据库建模到推荐结果生成的全链路拆解

3.1 MySQL数据库设计:如何让一张表同时扛住写入与查询压力?

springboot300z2.sql脚本共创建5张表,但真正承载推荐逻辑的是三张核心表:user_behavioruser_similarityproduct。它们的设计不是照搬教科书,而是针对“读多写少、写有峰值、查要精准”做了专项优化。

user_behavior表结构如下:

CREATE TABLE `user_behavior` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键,仅用于分页', `user_id` bigint NOT NULL COMMENT '用户ID', `product_id` bigint NOT NULL COMMENT '商品ID', `behavior_type` tinyint NOT NULL DEFAULT '1' COMMENT '行为类型:1浏览 2收藏 3购买', `weight` tinyint NOT NULL DEFAULT '1' COMMENT '行为权重,购买=3,收藏=2,浏览=1', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product_behavior` (`user_id`,`product_id`,`behavior_type`), KEY `idx_user_time` (`user_id`,`create_time`) USING BTREE, KEY `idx_product_time` (`product_id`,`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户行为日志表';

这里的关键设计点有三个:第一,UNIQUE KEY uk_user_product_behavior强制约束“同一用户对同一商品的同一行为只能发生一次”,这解决了前端重复提交导致的权重虚高问题——比如用户狂点收藏按钮,后端只记第一次;第二,KEY idx_user_time是复合索引,让SELECT * FROM user_behavior WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20能走索引,这是生成用户画像的基础查询;第三,id字段虽是自增主键,但实际业务中几乎不用,所有关联都走user_id+product_id组合,避免JOIN时因主键过大拖慢性能。

user_similarity表更精巧:

CREATE TABLE `user_similarity` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id_a` bigint NOT NULL COMMENT '用户A ID', `user_id_b` bigint NOT NULL COMMENT '用户B ID', `similarity_value` decimal(5,4) NOT NULL COMMENT '皮尔逊相似度,范围[-1.0000, 1.0000]', `last_update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_pair` (`user_id_a`,`user_id_b`), KEY `idx_user_a` (`user_id_a`), KEY `idx_user_b` (`user_id_b`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户相似度表';

注意similarity_value用DECIMAL(5,4),不是FLOAT。我吃过亏:用FLOAT存0.8765,查出来变成0.8764999999999999,排序时0.8765和0.8764被当成不同值,导致Top50用户列表每次都不一样。UNIQUE KEY uk_user_pair确保(123,456)(456,123)只存一条,避免冗余计算——相似度是对称的,没必要存两份。定时任务计算时,SQL是这样的:

INSERT INTO user_similarity (user_id_a, user_id_b, similarity_value) SELECT ub1.user_id AS user_id_a, ub2.user_id AS user_id_b, ROUND( (COUNT(*) * SUM(ub1.weight * ub2.weight) - SUM(ub1.weight) * SUM(ub2.weight)) / (SQRT(COUNT(*) * SUM(ub1.weight * ub1.weight) - SUM(ub1.weight) * SUM(ub1.weight)) * SQRT(COUNT(*) * SUM(ub2.weight * ub2.weight) - SUM(ub2.weight) * SUM(ub2.weight))), 4) AS sim FROM user_behavior ub1 INNER JOIN user_behavior ub2 ON ub1.product_id = ub2.product_id AND ub1.user_id < ub2.user_id GROUP BY ub1.user_id, ub2.user_id HAVING sim > 0.6 ON DUPLICATE KEY UPDATE similarity_value = VALUES(similarity_value), last_update_time = NOW();

这段SQL的ub1.user_id < ub2.user_id条件就是保证(a,b)(b,a)只算一次,HAVING sim > 0.6过滤掉无效相似对,ON DUPLICATE KEY UPDATE处理并发写入冲突。开发文档.docx第31页有这个SQL的执行计划截图,可以看到type列为rangekey列为idx_product_time,证明它真的走了索引。

3.2 协同过滤算法实现:皮尔逊公式的Java落地与边界处理

RecommendService.calculateSimilarity()方法是整个系统的算法心脏。它没用Apache Commons Math库,而是手写了皮尔逊相关系数计算,原因很实在:那个库的Correlation.pcor()方法在用户行为数据极度稀疏时(比如10万用户只产生5万条行为记录),会抛出NotPositiveDefiniteMatrixException异常,而我们的手写版本用BigDecimal做中间计算,能优雅降级。

核心代码逻辑如下:

public BigDecimal calculatePearsonSimilarity(Long userIdA, Long userIdB) { // 1. 获取两用户的共同商品行为列表(交集) List<UserBehavior> behaviorsA = userBehaviorMapper.selectByUserId(userIdA); List<UserBehavior> behaviorsB = userBehaviorMapper.selectByUserId(userIdB); Map<Long, Integer> commonProducts = new HashMap<>(); // 构建商品ID -> 权重映射,只取共同商品 for (UserBehavior bA : behaviorsA) { for (UserBehavior bB : behaviorsB) { if (bA.getProductId().equals(bB.getProductId())) { commonProducts.put(bA.getProductId(), bA.getWeight() + bB.getWeight()); // 简单相加,非必须,仅为示例 break; } } } if (commonProducts.size() < 3) { // 共同行为少于3个,相似度无意义 return BigDecimal.ZERO; } // 2. 计算皮尔逊分子分母(BigDecimal版) BigDecimal sumXY = BigDecimal.ZERO; BigDecimal sumX = BigDecimal.ZERO; BigDecimal sumY = BigDecimal.ZERO; BigDecimal sumX2 = BigDecimal.ZERO; BigDecimal sumY2 = BigDecimal.ZERO; int n = commonProducts.size(); for (Long productId : commonProducts.keySet()) { int weightA = getWeightForUserProduct(userIdA, productId); int weightB = getWeightForUserProduct(userIdB, productId); BigDecimal x = BigDecimal.valueOf(weightA); BigDecimal y = BigDecimal.valueOf(weightB); sumXY = sumXY.add(x.multiply(y)); sumX = sumX.add(x); sumY = sumY.add(y); sumX2 = sumX2.add(x.multiply(x)); sumY2 = sumY2.add(y.multiply(y)); } // 3. 皮尔逊公式:r = [n*Σxy - Σx*Σy] / sqrt{[n*Σx²-(Σx)²] * [n*Σy²-(Σy)²]} BigDecimal numerator = BigDecimal.valueOf(n).multiply(sumXY) .subtract(sumX.multiply(sumY)); BigDecimal denominatorX = BigDecimal.valueOf(n).multiply(sumX2) .subtract(sumX.multiply(sumX)); BigDecimal denominatorY = BigDecimal.valueOf(n).multiply(sumY2) .subtract(sumY.multiply(sumY)); if (denominatorX.compareTo(BigDecimal.ZERO) <= 0 || denominatorY.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; // 分母为0,返回0 } BigDecimal denominator = sqrt(denominatorX.multiply(denominatorY)); if (denominator.compareTo(BigDecimal.ZERO) == 0) { return BigDecimal.ZERO; } return numerator.divide(denominator, 4, RoundingMode.HALF_UP); }

这里藏着三个实战经验:第一,commonProducts.size() < 3的判断不是拍脑袋,而是基于统计——当共同行为少于3个时,皮尔逊系数的标准差超过0.4,结果不可信;第二,sqrt()方法是我们自己写的牛顿迭代法,因为Math.sqrt()对BigDecimal不支持,而BigDecimal.sqrt()是Java 9+才有的API,我们得兼容JDK8;第三,所有除法都用RoundingMode.HALF_UP,这是金融级四舍五入,避免0.87655被截成0.8765导致排序错乱。这些细节在开发文档.docx第35页的“算法鲁棒性测试报告”里有10组对比数据,比如用户A和B共同行为为[1,2,3]时,手写版结果0.9999,Commons Math版抛异常。

3.3 推荐服务接口设计:如何让一个HTTP请求完成从查相似用户到返回商品列表的全过程?

RecommendController.recommendByUserId()是对外暴露的唯一推荐入口,它的设计原则是“快、稳、可追溯”。快,指P95延迟<200ms;稳,指任何异常都不能导致空数组返回;可追溯,指每个推荐结果都带traceId,方便排查“为什么给用户123推荐了商品456”。

接口逻辑分五步原子化执行:
1.参数校验与缓存穿透防护:检查userId是否为正整数,若为负数或0,直接返回Result.fail("用户ID非法");同时用布隆过滤器(BloomFilter)预判该用户是否存在,避免缓存穿透——布隆过滤器初始化时加载所有user_id到内存,误判率控制在0.01%,代码在BloomFilterUtil.java里;
2.相似用户查询:执行userSimilarityMapper.selectTopKByUserId(userId, 5),SQL走idx_user_a索引,结果按similarity_value DESC排序;
3.候选商品聚合:遍历5个相似用户,对每个用户调用userBehaviorMapper.selectByUserId(similarUserId),把所有product_idweight累加到Map<Long, Integer>里,key是商品ID,value是总权重;
4.冷启动兜底:如果聚合后candidateMap.size() == 0(比如新用户没相似用户,或相似用户都没行为),则触发兜底策略——查product表按sales_volume DESC取前20个热销商品,再随机打乱顺序返回;
5.结果组装与脱敏:调用productMapper.selectBatchIds(candidateMap.keySet())批量查商品详情,组装RecommendResponse对象,其中traceId字段填入UUID.randomUUID().toString().replace("-", ""),日志里用log.info("recommend traceId={} userId={} candidates={}", traceId, userId, candidateMap.size())记录。

这个设计让接口具备强可观测性。我在开发文档.docx第42页放了一张真实日志截图:recommend traceId=8a9b3c4d5e6f7g8h9i0j1k2l3m4n5o6p userId=123 candidates=17,后面跟着17个商品ID。如果运营反馈“用户123收到了不该推荐的商品”,运维同学只要grep这条日志,就能定位到是哪个相似用户的行为导致的,而不是对着一堆NullPointerException发呆。

4. 实操过程与核心环节实现:从环境搭建到本地运行的完整手把手指南

4.1 环境准备:三步搞定零基础运行

别被“SpringBoot”“MySQL”这些词吓住,这套系统对环境要求极低。我用一台2015款MacBook Air(8GB内存,Intel i5)实测过,全程不需要管理员权限。

第一步:安装JDK8或JDK17
去Oracle官网下载JDK8u202或OpenJDK17,安装完执行java -version,看到类似openjdk version "17.0.1" 2021-10-19即可。注意Windows用户务必把JAVA_HOME指向JDK根目录,不是JRE目录,否则mvnw会报错The JAVA_HOME environment variable is not defined correctly

第二步:启动MySQL 5.7+
推荐用Docker快速启动:docker run -d --name mysql-recomm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=springboot300z2 -v $(pwd)/db:/docker-entrypoint-initdb.d mysql:5.7。如果你不用Docker,就去MySQL官网下社区版,安装时勾选“Add MySQL to PATH”,然后用Navicat或MySQL Workbench连上localhost:3306,用户名root,密码123456。

第三步:导入数据库脚本
打开MySQL客户端,执行source /path/to/springboot300z2.sql。注意路径要用绝对路径,Windows下是source C:/Users/xxx/springboot300z2.sql。导入成功后,执行SELECT COUNT(*) FROM user_behavior;,应该返回1287条记录——这是预置的模拟数据,包含200个用户、100个商品、以及他们之间的真实行为关系。

做完这三步,环境就ready了。整个过程我掐表计时,最快的一次是11分36秒,包括下载JDK的时间。开发文档.docx第5页有每一步的截图,连MySQL安装向导的第7步“Configure MySQL Server”界面都截了,避免你卡在“Set Root Password”那里。

4.2 项目构建与启动:一行命令跑起来

进入项目根目录(就是有pom.xml的那个文件夹),打开终端,执行:

./mvnw clean package -DskipTests

Linux/macOS用户用./mvnw,Windows用户用mvnw.cmd。这个命令会做三件事:第一,下载Maven 3.9.6到.mvn/wrapper/目录;第二,解析pom.xml,下载所有依赖(约127个jar包,首次执行需5-8分钟);第三,编译src/main/java下的代码,运行test目录下的单元测试(-DskipTests跳过,节省时间),最后在target/目录生成springboot300z2-1.0.0.jar

生成jar包后,执行:

java -jar target/springboot300z2-1.0.0.jar

你会看到控制台刷出SpringBoot启动日志,最后一行是Started Application in 3.212 seconds (process running for 3.789)。这时打开浏览器访问http://localhost:8080/swagger-ui.html,就能看到完整的API文档——没错,项目内置了Swagger3,所有接口都有在线调试功能。

重点测试推荐接口:在Swagger页面找到GET /api/v1/recommend/{userId},点“Try it out”,把userId填成1,点Execute。几秒后返回JSON:

{ "code": 200, "message": "success", "data": [ { "productId": 45, "productName": "五常大米", "price": 68.00, "imageUrl": "/images/rice.jpg", "similarityScore": 0.8765 } ] }

看到这个,恭喜你,第一个推荐结果诞生了!开发文档.docx第12页有这个返回结果的逐字段解释,比如similarityScore不是用户相似度,而是该商品在推荐排序中的综合得分。

4.3 关键配置解读:application.yml里的每一个参数都经过压测验证

src/main/resources/application.yml不是模板,而是生产环境调优后的结果。我把关键配置摘出来,告诉你为什么这么设:

spring: datasource: url: jdbc:mysql://localhost:3306/springboot300z2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 # 连接池最大20,压测发现超过25会导致MySQL CPU飙升 minimum-idle: 5 # 最小空闲5,避免频繁创建销毁连接 connection-timeout: 30000 # 连接超时30秒,防止网络抖动拖垮服务 idle-timeout: 600000 # 空闲连接600秒后释放 max-lifetime: 1800000 # 连接最长存活30分钟,避免MySQL wait_timeout断连 mybatis-plus: configuration: map-underscore-to-camel-case: true default-statement-timeout: 30 # SQL执行超时30秒,防止单条慢查询拖垮全局 recommend: similarity: top-k: 5 # 每次只取5个最相似用户,更多不提升效果反增延迟 min-common-behavior: 3 # 共同行为少于3个,跳过相似度计算 cache: ttl: 3600 # 推荐结果缓存1小时,单位秒

特别提醒hikari.maximum-pool-size: 20这个值。我做过对比测试:设成50时,200并发下MySQL连接数打满,出现大量Too many connections错误;设成10时,QPS卡在80上不去。20是平衡点,既满足并发需求,又留有余量给后台定时任务。这些压测数据在开发文档.docx第68页的“性能基准测试报告”里,表格列出了不同连接池大小对应的TPS、平均延迟、错误率。

4.4 定时任务与数据更新:如何让推荐结果随用户行为实时进化?

系统里有两个定时任务,都在ScheduleConfig.java里定义:

@Scheduled(cron = "0 0 */4 * * ?") // 每4小时执行一次 public void calculateUserSimilarity() { log.info("Start calculating user similarity..."); recommendService.batchCalculateSimilarity(); log.info("User similarity calculation completed."); } @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void updateHotProductsCache() { log.info("Start updating hot products cache..."); recommendService.refreshHotProductsCache(); log.info("Hot products cache updated."); }

第一个任务计算用户相似度,第二个任务刷新热销商品缓存(用于冷启动兜底)。cron表达式0 0 */4 * * ?表示“每4小时的0分0秒”,比如02:00、06:00、10:00……这样错开业务高峰。batchCalculateSimilarity()方法不是一次性算所有用户,而是分页处理:每次查100个用户,计算他们与其余用户的相似度,避免单次事务过大锁表。代码里有Thread.sleep(100)的微调,防止MySQL CPU瞬间拉满。

如果你想手动触发相似度计算,Swagger里有个POST /api/v1/similarity/calculate接口,传{"userId": 123}就能单独算某个用户的相似用户。这个接口在开发文档.docx第75页有调用示例,连curl命令都写好了:

curl -X POST "http://localhost:8080/api/v1/similarity/calculate" \ -H "Content-Type: application/json" \ -d '{"userId": 123}'

5. 常见问题与排查技巧实录:那些让我熬夜改了三版的坑

5.1 数据库导入失败:字符集与时间戳的双重陷阱

问题现象:执行source springboot300z2.sql时报错ERROR 1067 (42000): Invalid default value for 'create_time'

根本原因:MySQL 5.7默认开启了严格模式(STRICT_TRANS_TABLES),而SQL脚本里create_time字段定义为DEFAULT CURRENT_TIMESTAMP,但表引擎是InnoDB,某些旧版本MySQL不支持timestamp字段的CURRENT_TIMESTAMP默认值。

解决方案
1. 登录MySQL,执行SET sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));临时关闭严格模式;
2. 再执行source springboot300z2.sql
3. 导入成功后,执行SET sql_mode='STRICT_TRANS_TABLES';恢复。

这个坑我踩过两次。第一次在CentOS 7上,MySQL版本5.7.28,第二次在Windows WSL2里,MySQL 5.7.33。开发文档.docx第82页把这个解决方案做成带编号的步骤,连SET sql_mode命令的复制按钮都加了。

5.2 启动报错:NoClassDefFoundError与依赖冲突

问题现象:执行java -jar target/springboot300z2-1.0.0.jar后,控制台报java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

根本原因:pom.xml里spring-boot-starter-webmybatis-plus-boot-starter都依赖了commons-logging,但版本不一致,Maven随机选了一个,导致类加载失败。

解决方案:在pom.xml的<dependencies>里显式声明commons-logging依赖:

<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>

然后重新执行./mvnw clean package。这个方案比排除传递依赖更稳妥,因为commons-logging是基础日志门面,所有组件都认它。

5.3 推荐结果为空:相似用户没行为的静默失败

问题现象:调用/api/v1/recommend/1返回空数组[],日志里也没报错。

排查路径
1. 查user_similarity表:SELECT * FROM user_similarity WHERE user_id_a = 1;,发现没记录——说明定时任务没跑,或用户1没足够相似用户;
2. 查用户1的行为:SELECT * FROM user_behavior WHERE user_id = 1;,发现只有2条浏览记录;
3. 查共同行为阈值:SELECT COUNT(*) FROM user_behavior ub1 INNER JOIN user_behavior ub2 ON ub1.product_id = ub2.product_id WHERE ub1.user_id = 1 AND ub2.user_id != 1;,结果是0——用户1浏览的商品,其他用户都没碰过。

终极解法
- 短期:手动插入一条行为记录,比如INSERT INTO user_behavior (user_id, product_id, behavior_type, weight) VALUES (1, 45, 3, 3);(给用户1加个购买行为);
- 长期:在RecommendService.recommendByUserId()里加日志埋点,当candidateMap.size() == 0时,打印log.warn("No candidate products for userId={}, trigger fallback to hot products", userId);,这样一眼就能看出是兜底逻辑生效了。

这个案例被我写进开发文档.docx第89页的“故障树分析(FTA)”,用决策树形式展示:从“返回空数组”开始,一级分支是“查相似用户表”,二级分支是“查行为表”,三级分支是“查共同商品”,每条路径都标了SQL命令和预期结果。

5.4 性能瓶颈:为什么推荐接口突然变慢?

问题现象:压测时,QPS从200掉到50,P95延迟从110ms飙到1.8s。

根因定位:用jstack抓线程快照,发现大量线程卡在RecommendService.calculatePearsonSimilarity()for循环里;用jstat -gc看,Young GC频率从10秒1次变成1秒3次。

真相:用户行为数据被恶意刷单,user_behavior表里出现10万条user_id=999, product_id=1, behavior_type=3, weight=3的重复记录。皮尔逊计算时,commonProductsMap里塞了10万个相同key,sumXY等BigDecimal变量内存暴涨。

修复方案
1. 在userBehaviorMapper.selectByUserId()的SQL里加DISTINCT关键字;
2. 在定时任务batchCalculateSimilarity()里加数据清洗:DELETE FROM user_behavior WHERE create_time < DATE_SUB(NOW(), INTERVAL 90 DAY);(只保留90天内行为);
3. 前端加防刷机制:同一个IP 1分钟内对同一商品的行为不超过5次。

这个教训让我在开发文档.docx第95页加了“数据质量红线”章节,明确规定:行为表单条记录weight最大值为3,behavior_type只允许1/2/3,create_time不能早于2020年1月1日——所有这些都在MyBatis-Plus的@TableField注解里加了@TableLogic@TableId(type = IdType.AUTO)校验。

6. 扩展与定制指南:如何把它变成你自己的推荐系统?

6.1 算法升级:从用户协同过滤到混合推荐

如果你需要更高精度,可以基于本项目快速升级。我在开发文档.docx第102页写了“混合推荐接入指南”,核心是三步:

  1. 新增内容特征表:建product_feature表,字段product_id,category_vector(JSON存品类向量,如{"rice": 0.9, "grain": 0.7}),用TF-IDF从商品标题和描述提取;
  2. 修改推荐服务:在RecommendService.recommendByUserId()里,先走协同过滤得到候选集,再用productFeatureMapper.selectByProductIdList()批量查特征,计算余弦相似度,最后按协同得分×0.6 + 内容得分×0.4加权排序;
  3. 接口兼容:新加GET /api/v1/recommend/hybrid/{userId},老接口保持不变,前端可灰度切流。

这个方案实测在生鲜电商场景下,点击率提升8.3%,代码改动不到200行,所有新增SQL都写在hybrid-recomm.sql脚本里,和原脚本完全隔离。

6.2 前端对接:如何把推荐结果嵌入现有商城?

项目里的templates/recommend.html只是演示页面,真实业务要嵌入Vue或React商城。关键就两点:

  • 跨域配置:在application.yml里加cors:配置,允许https://your-shop.com域名访问;
  • 轻量接口:推荐接口返回JSON,字段精简为[{ "id": 45, "name": "五常大米", "price": 68.00 }],去掉similarityScore等内部字段,前端只管渲染。

我在开发文档.docx第108页给了Vue3的调用示例:

const { data } = await axios.get('http://localhost:8080/api/v1/recommend/123'); this.recommendList = data.map(item => ({ id: item.productId, name: item.productName, price: item.price }));

连axios的CDN地址都给了:<script src="https://unpkg.com/axios@1.6.7/dist/axios.min.js"></script>

6.3 生产部署:从jar包到高可用集群的平滑演进

单机jar包只是起点。我在文档第115页画了演进路线图:

  • 阶段1(当前):单台服务器,MySQL主从,jar包用systemd托管;
  • 阶段2(半年后):Nginx负载均衡两台应用服务器,MySQL读写分离,Redis缓存user_similarity表;
  • 阶段3(一年后):Kubernetes集群,Spring Cloud Gateway网关,Flink实时计算用户行为流,推荐服务拆成similarity-serviceranking-service两个微服务。

每个阶段的配置变更都列了清单,比如阶段2的Redis配置,在application-prod.yml里加:

spring: redis: host: 192.168.1.100 port: 6379 database: 0 lettuce: pool: max-active: 20

连Redis的安装命令都写了:apt-get install redis-server && systemctl enable redis-server

这套系统不是终点,而是你推荐系统之旅的起点。它不炫技,但每行代码都经得起线上流量的拷问;它不复杂,但每个设计都藏着对真实业务的理解。当你在毕设答辩时,评委问“你们怎么解决冷启动”,你不必背概念,只需打开开发文档.docx第45页,指着那行if (candidateMap.isEmpty()) { return hotProductsFallback(); }说:“我们用热销商品兜底,这是线上验证过的方案。”——那一刻,你讲的不是代码,而是工程思维。

本文还有配套的精品资源,点击获取

简介:直接可运行的Java推荐系统项目,用SpringBoot搭建,核心是用户-物品协同过滤算法,能根据用户浏览、收藏、购买等历史行为,实时生成个性化商品推荐列表。项目结构清晰:pom.xml已配置好所有依赖,MySQL数据库脚本(springboot300z2.sql)一键导入即可初始化数据;src/main/java下是完整的后端逻辑,包括推荐服务、用户行为分析、相似度计算等模块;templates和static目录支持基础前端展示;test目录包含关键业务单元测试;配套的springboot开发文档.docx详细说明了算法原理、模块调用关系、接口设计及部署步骤。项目自带Maven Wrapper(mvnw/mvnw.cmd),Windows和Linux下无需预装Maven就能打包运行;.gitignore文件已配置,适配基础Git版本管理;target目录为编译输出占位,实际部署需执行mvn clean package生成jar包。适合用于课程设计、毕设实现或中小电商后台推荐功能的技术原型验证。


本文还有配套的精品资源,点击获取

http://www.gsyq.cn/news/1451801.html

相关文章:

  • 如何永久保存你的微信聊天记录?WeChatMsg完全免费解决方案
  • 从Stable Diffusion到DiT:一文看懂adaLN-Zero如何让扩散模型学会“条件生成”
  • 应对数据洪流:从分层架构到湖仓一体的实战指南
  • 保姆级教程:在OpenStack上从镜像、安全组到浮动IP,一步步创建能上网的虚拟机
  • 2025-2026年KTOS酷特AI企业应用操作系统电话查询:企业数智化转型需关注实施路径与风险 - 品牌推荐
  • 抖音直播数据采集终极指南:3分钟实现实时弹幕监控与数据分析
  • ROS小车纯视觉避障脚本包:OpenCV实时处理+树莓派友好型运动控制
  • 基于Arduino与3D打印的四足机器人:从机械设计到逆运动学步态实现
  • 地球科学数据叙事层构建:从多源异构数据到交互式故事线
  • MATLAB新手也能搞定的雷达信号处理:手把手教你实现CA-CFAR仿真(附完整代码)
  • 微软亚洲研究院2011年技术转化:从Kinect到必应词典的产学研闭环实践
  • ATtiny85三引脚驱动nRF24L01:SPI协议优化与嵌入式资源极限设计
  • 深入DolphinScheduler事件循环:从一次日志刷屏事故,看懂ProcessInstanceExecCacheManager的设计与缺陷
  • Word化学插件:无缝集成绘图与计算,革新化学文档工作流
  • CLion调试Keil老项目的避坑指南:从printf报错到成功下载的完整配置
  • 告别 Anaconda 臃肿安装!在 macOS 上快速部署轻量级 Miniconda 并管理多 Python 环境
  • MATLAB中三个开箱即用的短时傅里叶逆变换函数实现
  • 构建智能代码搜索系统:从语义理解到IDE集成,提升开发效率
  • 端到端语音识别技术:从原理到实战,构建流式ASR系统
  • Sora 2赋能县域文旅爆火的7个关键动作:从方言配音到实景三维重建,手把手拆解省级示范案例
  • 数据科学入门:从零构建女性学习者的技术成长体系
  • Godot4 3D游戏实战:如何给你的跳跃小游戏加上计分板和死亡重玩机制
  • Beyond Compare 5密钥生成器:5分钟解决文件对比工具激活难题
  • sql.js WASM 深度解析
  • 四足机器人地形自适应运动规划技术解析
  • 别再只会conda info --envs了!这5个隐藏技巧帮你高效管理Python环境
  • Halcon仿射变换保姆级教程:从旋转、平移到缩放,手把手搞定图像变形
  • 如何让10美元鼠标秒变苹果触控板:Mac Mouse Fix终极配置指南
  • FPGA BRAM不够用?试试这个手写多端口RAM的优化技巧,资源再省20%
  • 别再手动调参数了!用UE5材质函数快速搞定下雨积水动态水波纹(附完整材质蓝图)