ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot新闻推荐系统:UserCF协同过滤实战与避坑指南

Spring Boot新闻推荐系统:UserCF协同过滤实战与避坑指南 简介基于SpringBoot与Vue框架打造的新闻推荐系统毕业设计项目包含完整可运行源码和一篇对应论文面向Java方向毕业生、Spring Boot初学者以及需要实现内容推荐功能的开发者。压缩包文件总量748个大小约15.15MB内部按功能分为Java后端代码、Vue前端页面、JS交互脚本、CSS样式、数据库SQL脚本等同时提供了Maven工程配置和可执行的安装、运行、打包批处理方便直接导入开发工具启动项目。配套论文从研究背景、目的和设计思想入手依次梳理了相关技术、可行性分析、系统性能与安全性、数据完整性、界面与流程设计然后进入数据库实体和表结构设计并对管理员模块、用户模块的各个子功能做了详细实现说明最后通过功能测试、可用性测试和性能测试验证结果形成完整闭环。项目包含用户信息管理、排行榜管理、新闻信息管理以及首页、新闻详情、我的收藏等实际页面适合用于课程设计、毕业设计或二次开发参考。目前已有34人学习下载可作为快速理解Spring BootVue前后端分离项目的实用范例。1. 拿到“源码论文”的Spring Boot新闻推荐系统先别急着解压花两个通宵把项目跑通再花一个通宵把推荐结果调得不像“热门新闻排行榜”最后发现论文里的截图和实际页面对不上——这是每个拿到“基于Spring Boot新闻推荐系统”这类打包项目的人大概率要走的三道坎。这类资源包在Java课程设计和毕业设计里出现频率极高压缩包里装着一套Spring Boot后端、一份前端页面和一篇配套论文核心卖点不是“新闻管理”而是“推荐”两个字。我拆过不少这类项目也帮人修过不少翻车现场。这套东西的真实构成通常不复杂Spring Boot管接口和业务MyBatis管数据库协同过滤算法算相似度前端用Vue或Bootstrap套一个后台模板论文则把系统设计、数据库设计和算法原理串成一篇能过查重的文档。适合谁适合需要快速交付一个“有推荐算法、有Spring Boot框架、有前后端闭环”的JavaWeb项目的人或者想拿现成代码做二次开发、把推荐逻辑换掉的开发者。但“能跑”和“能交”之间隔着不少坑尤其是版本不匹配、数据稀疏、论文和代码脱节这三类问题本章先把整体脉络理清后面每一章都往下落一层。2. 系统拆解新闻推荐系统的表结构、模块划分与算法选型拿到压缩包之后第一件事不是跑mvn spring-boot:run而是先把代码结构读明白。这类系统的整体架构基本都是“前台用户端 后台管理端 推荐引擎”三件套前端通过HTTP接口调Spring BootSpring Boot再通过MyBatis访问MySQL。推荐引擎不在独立服务里而是作为一个Service模块嵌在业务代码中定时或按需触发。2.1 三类模块前台展示、推荐引擎、后台管理各管什么前台展示模块负责用户的注册登录、新闻列表、新闻详情、分类浏览、收藏点赞操作接口路径通常以/api开头。后台管理模块负责新闻的增删改查、分类管理、用户管理走独立的/admin路径一般靠拦截器做登录校验没有引入Spring Security这类框架因为毕设项目的管理员就一个账号直接写在配置里更省事。推荐引擎是这套系统的“算法门面”也是论文里最值钱的部分。它一般长这样用户浏览或点击新闻时行为数据被写进一张行为表推荐Service定时读取这些行为数据计算出用户之间的相似度再找出邻居用户喜欢的、当前用户没看过的新闻生成推荐列表存入Redis或直接实时计算返回。这个模块的代码量通常不大一百行上下就能写完但数据结构设计和阈值参数才是真正的论文素材很多人的项目挂在这上面。提示拿到源码先找controller、service、mapper三层目录看包名就能判断代码质量。好的包结构是com.xxx.news.controller / service / mapper / entity如果全堆在一个包下后期改代码会痛苦很多。后台管理里还会藏一个“新闻审核”或“置顶管理”的小功能本质是对新闻表的status字段做更新。这类模块技术含量低但页面多论文的功能模块图全靠它撑场面跑通后截图时别漏掉。2.2 六张基础表用户、新闻、行为与分类的关系设计数据库是论文里必须交的硬通货但几十张表反而扣分真正核心的就六张。用户表t_user存用户基本信息新闻表t_news存标题、摘要、正文、分类ID、发布时间、点击量分类表t_category存新闻分类。行为表是整个推荐系统的数据地基常见命名是t_user_behavior或t_news_click字段一般包括用户ID、新闻ID、行为类型、行为时间行为类型用数字标识1表示浏览2表示点赞3表示收藏。用户表与新闻表是多对多关系但这套系统通常不建中间表而是靠行为表间接表达。因为推荐算法读的就是行为表一张扁平的宽表比范式化设计更方便写SQL。下面是这类系统最常见的表结构表名关键字段作用说明t_userid, username, password, nickname, create_time用户账号与基础信息t_newsid, title, summary, content, category_id, clicks, status, create_time新闻内容与状态控制t_categoryid, name, sort新闻分类如时政、体育、娱乐t_user_behaviorid, user_id, news_id, behavior_type, create_time推荐算法唯一数据来源t_commentid, user_id, news_id, content, create_time评论展示与推荐无直接关系t_adminid, username, password后台登录账号注意行为表的时间字段create_time要建索引推荐算法做时间衰减时要用它来过滤“三个月前的浏览记录”。很多项目在这张表上不建索引数据量一旦过万推荐接口的响应时间会从百毫秒级掉到秒级这个细节写到论文的“数据库优化”一节是非常好用的加分项。2.3 算法选型新闻场景为什么优先UserCF而不是ItemCF协同过滤分两类基于用户的UserCF和基于物品的ItemCF。新闻推荐场景绝大多数用UserCF理由是新闻更新太快ItemCF需要先算物品相似度矩阵而新闻的“半衰期”只有几天昨天算好的新闻相似度矩阵今天可能一半新闻已经被下架了维护成本极高。UserCF的核心逻辑是“跟你兴趣相似的人在看什么也推给你”。实现分三步先构建用户-新闻评分矩阵再算用户之间的相似度最后取出相似度最高的K个邻居把邻居有行为而当前用户没有行为的新闻按得分排序返回。这个过程天然适合新闻场景因为用户的兴趣相对稳定看体育新闻的人大概率还会看体育新闻而新闻本身的生命周期短不适合作为相似度计算的锚点。算法选型是论文第一章的核心卖点。写论文时把选择理由拉成对比说明ItemCF在新闻场景下“物品更新快导致相似度矩阵失效”再说UserCF的冷启动问题可以用“热门新闻兜底”缓解这一套逻辑下来答辩老师基本不会再追问算法问题。3. 核心代码落地UserCF协同过滤在Spring Boot里的实现与调参算法模块是这套系统的灵魂也是你唯一需要“真读懂”的部分。Spring Boot本身只是壳把UserCF写成Service、用Mapper查数据、把推荐结果包成JSON返回给前端才是完整的落地链路。这一章按三层代码逐段拆解每一段都可以直接抄进自己的项目里改改用。3.1 UserCF的核心Service相似度计算与推荐生成推荐Service通常叫RecommendService里面就三个方法读行为数据、算相似度、生成推荐。下面是一段精简可跑的UserCF实现用余弦相似度计算用户之间的距离Service public class RecommendService { Resource private UserBehaviorMapper behaviorMapper; Resource private NewsMapper newsMapper; private static final int TOP_N 10; // 最终推荐条数 private static final int K_NEIGHBORS 5; // 相似邻居个数 public ListNews recommendForUser(Long userId) { // 1. 读取全部用户行为构建 用户-(新闻-行为分) 的评分矩阵 ListUserBehavior behaviors behaviorMapper.selectAll(); MapLong, MapLong, Double userItemMatrix new HashMap(); for (UserBehavior behavior : behaviors) { userItemMatrix .computeIfAbsent(behavior.getUserId(), k - new HashMap()) .put(behavior.getNewsId(), scoreOf(behavior.getBehaviorType())); } // 2. 如果没有当前用户的行为记录返回热门兜底 MapLong, Double currentUserItems userItemMatrix.get(userId); if (currentUserItems null || currentUserItems.isEmpty()) { return newsMapper.selectHotNews(TOP_N); } // 3. 计算当前用户与其他用户的余弦相似度取前K个邻居 MapLong, Double similarityMap new HashMap(); for (Map.EntryLong, MapLong, Double entry : userItemMatrix.entrySet()) { Long otherUserId entry.getKey(); if (otherUserId.equals(userId)) { continue; } double similarity cosineSimilarity(currentUserItems, entry.getValue()); if (similarity 0.1) { // 相似度阈值过滤无关用户 similarityMap.put(otherUserId, similarity); } } ListMap.EntryLong, Double neighbors similarityMap.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(K_NEIGHBORS) .collect(Collectors.toList()); // 4. 累加邻居的行为分过滤掉当前用户已读新闻排序取TOP_N MapLong, Double scoreMap new HashMap(); for (Map.EntryLong, Double neighbor : neighbors) { MapLong, Double neighborItems userItemMatrix.get(neighbor.getKey()); for (Map.EntryLong, Double item : neighborItems.entrySet()) { if (currentUserItems.containsKey(item.getKey())) { continue; } scoreMap.merge(item.getKey(), item.getValue() * neighbor.getValue(), Double::sum); } } ListLong newsIds scoreMap.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(TOP_N) .map(Map.Entry::getKey) .collect(Collectors.toList()); return newsMapper.selectBatchByIds(newsIds); } private double cosineSimilarity(MapLong, Double a, MapLong, Double b) { double dot 0.0, normA 0.0, normB 0.0; for (Double score : a.values()) normA score * score; for (Double score : b.values()) normB score * score; for (Map.EntryLong, Double entry : a.entrySet()) { Double scoreB b.get(entry.getKey()); if (scoreB ! null) dot entry.getValue() * scoreB; } return normA 0 || normB 0 ? 0 : dot / (Math.sqrt(normA) * Math.sqrt(normB)); } private double scoreOf(Integer behaviorType) { switch (behaviorType) { case 2: return 2.0; // 点赞 case 3: return 3.0; // 收藏 default: return 1.0; // 浏览 } } }逻辑说明分四步走每一步对应一个数据流转阶段。第一步是全表扫描构建评分矩阵数据量小的时候直接查全表没问题数据过万后这里必须改成只查近30天的行为记录靠SQL的where create_time date_sub(now(), interval 30 day)兜住。第二步是热门兜底用来处理冷启动。第三步的相似度阈值0.1是个关键参数设太小会把所有用户都拉进来当邻居推荐结果趋同设太大则找不到邻居直接降级成热门推荐。第四步的累加公式中item.getValue() * neighbor.getValue()是邻居行为分和相似度的加权这一步直接决定推荐的个性化程度。参数说明TOP_N10是前端首页展示的推荐条数这个值建议跟分页条数保持一致否则前端翻页会出现“推荐位满了但下一页是空的”的尴尬情况。K_NEIGHBORS5是邻居数论文里写“选取相似度最高的5个用户”是合理的常规值不需要刻意加大因为新闻场景下用户的行为重叠度很低Top5之后基本是低相似度用户。3.2 行为权重设计浏览、点赞、收藏该各记几分行为权重在代码里就是scoreOf方法那三行但它在论文里的价值远大于代码本身。常见的分值是浏览1分、点赞2分、收藏3分也可以把评论算作5分但评论量的稀疏度太高刷一两万条行为数据也攒不出几条评论对评分矩阵的贡献极低。权重的设计逻辑要能自圆其说收藏是用户主动显式表达兴趣权重最高点赞是轻量正向反馈权重居中浏览是被动行为可能只是误触或标题党吸引点击权重最低。这套逻辑写进论文“推荐算法优化”一节配合上面的分值代码截图既显得算法有思考又有代码支撑。实际调的时候有个坑浏览行为占比通常在90%以上如果分值差距太小比如1和2点赞收藏记录会被浏览淹没推荐结果约等于“大家都在看什么”。把收藏调成3分甚至5分才能让少数高质量行为在相似度计算中真正起作用。这一段的参数调优过程本身就值得写进论文的实验对比部分。3.3 推荐接口与MyBatis分页Controller怎么写、参数怎么传Service写好之后Controller层就是一层薄薄的壳。推荐接口一般长这样RestController RequestMapping(/api/recommend) public class RecommendController { Resource private RecommendService recommendService; GetMapping(/{userId}) public ResultListNewsVO recommend(PathVariable Long userId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { ListNews newsList recommendService.recommendForUser(userId); // 分页逻辑拿全量推荐结果做内存分页 int start (page - 1) * size; int end Math.min(start size, newsList.size()); return Result.success(newsList.subList(start, end)); } }这个接口把分页参数page和size直接暴露给前端前端首页加载时传page1size10下拉加载更多时逐页递增。MyBatis分页插件在推荐场景里其实用不上因为推荐结果是算完排序后的List直接在内存里截取子列表就行但不影响项目里其他管理端接口使用PageHelper或MybatisPlus的分页插件。注意行为数据的异步落库比同步落库更合理。如果每点一次新闻都同步写一次行为表高并发下数据库会拖垮接口响应常见做法是用Spring Boot整合ActiveMQ或RabbitMQ把行为数据丢进消息队列消费者异步写库。论文里加一段“基于消息队列的异步行为收集”设计技术档次立刻不一样。4. 从7z到在线可点数据库导入、Spring Boot配置与启动验证压缩包里最值钱的不是代码而是“能跑到能交”的完整闭环。很多人第一步就卡在启动上翻来覆去看了三四个小时的报错最后发现是MySQL版本或JDK版本不对付。这一章按版本对照、配置文件、启动验证的顺序走完整个流程。4.1 环境版本对照JDK、Maven、MySQL和IDEA怎么匹配这类打包项目有一个通病默认环境是老的但你的电脑装的是新的。最常见的版本搭配是JDK 1.8、Maven 3.6.x、MySQL 5.7、Spring Boot 2.x。如果你本地装的是JDK 17和MySQL 8.0直接跑必然翻车其中MySQL 8.0的坑最隐蔽。组件推荐版本高版本常见问题JDK1.8JDK 17下Spring Boot 2.3以下版本启动直接报错Maven3.6.x3.8对镜像源和插件校验更严格MySQL5.78.0后驱动类名变化、时区配置必填Spring Boot2.x3.x要求JDK 17且javax包名改为jakartaIDEA2021老版本对Maven wrapper识别差解决方案就两句话要么按源码默认版本搭环境要么用IDEA打开项目后右键pom.xml把java.version改成自己本地的JDK版本同时升级Spring Boot父版本并全局替换javax.*的包名引用。这里最推荐的是前者因为论文里写的“系统运行环境”通常也是老版本保持一致答辩时不心虚。4.2 修改application.yml的4个关键配置项src/main/resources/application.yml是Spring Boot的入口配置压缩包里大概率给的是本机开发环境配置你需要改成自己的数据库账号和密码。这个文件里有四个地方必须仔细核对。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/news_recommend?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.news.entity logging: level: com.example.news.mapper: debugurl末尾的serverTimezoneAsia/Shanghai是MySQL 8.0的必填参数不写会报“The server time zone value is unrecognized”的错。driver-class-name在MySQL 5.7版本下是com.mysql.jdbc.Driver8.0之后变成com.mysql.cj.jdbc.Driver这行是版本升级后最容易忽略的配置项。mapper-locations指向XML文件目录如果项目用的是注解SQL这行删掉也没问题。logging.level建议保留成debug级别启动时能看到每个Mapper执行的SQL排错效率翻倍。注意如果压缩包里引了Redis做缓存但本机没装Redis启动会一直报连接拒绝。最快的后悔药是顺手把spring.redis的自动配置排除掉或者在本地装一个Redis Windows版二者选其一别卡在这上面。4.3 数据库脚本导入与全链路启动验证数据库脚本一般在压缩包的sql或db文件夹下文件名类似news_recommend.sql。用Navicat或命令行导入时先确认脚本里有没有CREATE DATABASE语句有就直接跑没有就手动创建一个同名数据库再导入。导入完成后查一下三张核心表的数据量很多压缩包的t_news表只给了几十条测试数据够跑通流程但撑不起推荐算法的验证。启动顺序有讲究先启动MySQL再启动Redis如果项目里有然后点IDEA的绿色启动按钮或执行命令行mvn spring-boot:run。看到Started Application in x.xxx seconds的日志并监听在8080端口后打开浏览器访问首页。如果首页能显示新闻列表再随便点两三篇新闻然后去个人的“推荐”页面刷新看到的应该是基于刚才浏览行为的推荐结果。验证推荐算法生效有个土办法用两个不同账号分别浏览完全不同分类的新闻刷新推荐页如果两个账号的推荐结果明显不同说明UserCF是真正在跑如果两个账号推荐结果一模一样那基本可以断定算法被写成了“查热门新闻”回到第三章找问题。整个过程从零到通半小时内解决超过这个时间通常是某个版本或依赖没有对上。5. 避坑排查新闻推荐系统跑通到答辩的5个典型事故从“代码能跑”到“答辩能过”中间隔着几个高频事故。每一条都是很多人踩过的按“现象→原因→解决”的顺序写清楚看完能少熬两个通宵。5.1 启动即崩溃数据库连接失败与MySQL 8驱动差异现象Spring Boot启动日志里出现Cannot create PoolableConnectionFactory或Access denied for user rootlocalhost应用秒退。原因分两类一是数据库本身没启动或账号密码不对二是MySQL从5.7换到8.0后驱动类和时区配置没跟上。解决先确认MySQL服务是否在运行命令行执行mysql -uroot -p能连上再谈其他。然后检查application.yml的driver-class-name是否为com.mysql.cj.jdbc.Driverurl里是否带上serverTimezoneAsia/Shanghai。用IDEA的Database面板直接测试连接如果IDE能连上但Spring Boot连不上问题就在配置文件。5.2 推荐结果全是热门评分矩阵稀疏与冷启动现象推荐接口能返回数据但所有用户的推荐结果都是同几篇点击量最高的新闻换个账号结果一模一样。原因行为数据太少或评分矩阵太稀疏。冷启动用户没有行为记录代码走了热门兜底分支而老用户行为少到不足以算出有区分度的相似度最终也退化到热门。解决先把行为表的数据灌起来写一个SQL脚本批量生成近三个月的模拟行为数据至少2000条以上让用户之间有交集。然后调低相似度阈值从0.1降到0.05让更多弱关联用户进入邻居候选。最后在scoreOf里加大收藏和点赞的权重让少量高质量行为主导相似度计算。5.3 前端页面跨域报错前后端分离的CORS配置现象前端页面能打开但点击登录或加载新闻列表时浏览器控制台报Access-Control-Allow-Origin错误接口数据一片空白。原因前端跑在http://localhost:63342或http://localhost:8081后端跑在8080端口跨域请求被浏览器拦截。解决在Spring Boot里加一个配置类实现WebMvcConfigurer重写addCorsMappings方法允许所有来源跨域访问。这类打包项目十有八九没写CORS配置原因可能是之前用同一端口部署了前后端。注意如果项目里加了全局过滤器处理XSS攻击跨域配置要放在过滤器链之前执行否则会被过滤器拦在门外。5.4 论文截图与运行界面不一致答辩现场翻车现象论文里的系统截图和实际跑出来的页面在样式、数据、功能按钮上对不上答辩现场老师随便点一个功能论文没写或者论文写了系统里找不到。原因论文和源码版本不同步当时写论文用的是旧版截图源码后来改过。这是打包项目最致命的伤。解决拿到压缩包后把论文里出现的每一个功能点列成清单一个个在系统里找到并实际操作一遍截图全部重新生成替换论文里的旧图。不要偷懒用论文原图风格不一样反而露出马脚。论文里的表结构设计也要和数据库脚本核对字段名、字段类型、注释缺一不可。5.5 相似度为0导致的除零异常阈值与分母保护现象推荐接口在某个用户访问时抛出ArithmeticException: / by zero控制台显示异常发生在相似度计算那一行。原因评分矩阵中某个用户的行为得分全为0或者余弦相似度分母出现0向量。很多项目的原始代码没做这个保护一旦行为表里出现异常数据比如行为类型字段为NULL导致权重映射为默认0直接翻车。解决在cosineSimilarity方法里加if (normA 0 || normB 0) return 0保护代码已在第三章给出。这个修复合情合理写进论文里反而能体现“代码健壮性”意识。如果不想让行为类型为NULL的数据混入计算也可以在读取行为数据时加一条SQL过滤条件WHERE behavior_type IN (1,2,3)。6. 进阶调优把推荐结果从“全是热门”调到“值得写进论文”基础跑通后推荐效果能不能形成“个性化差异”直接决定答辩时是“被动防守”还是“主动展示”。我一般会做三件事冷启动兜底、混合召回、离线评估。这三步在代码上改动都不大但在论文里能各占一小节。冷启动兜底最省事的做法是“分类热门”替代“全局热门”——不推整个系统的热门新闻而是推当前用户最近浏览的分类下的热门新闻。改动很小在热门兜底分支里加一个按category_id过滤的查询效果却天差地别用户看到的不再是“全站头条”而是“你常看的那一类”个性化感知瞬间拉满。混合召回的思路是不只靠UserCF把基于内容的关键词匹配也拉进来。常见做法是用HanLP分词对新闻标题和摘要做关键词提取然后和用户历史行为的关键词做余弦相似度两类结果用加权公式融合比如finalScore 0.7 * userCFScore 0.3 * contentScore。这一步能让推荐结果“既像相似用户在看的也像你自己在看的”论文的算法创新点也不用另找融合策略本身就是个可以拆开讲的卖点。离线评估最简单的方法是留一法把每个用户的最后一条行为记录下来不出现在训练集里跑完推荐后看这条记录是否落在推荐结果中统计命中率。代码里写个测试类循环所有用户计算平均命中率把这个数字写进论文的“实验与结果分析”一章。我用这个方法测过纯热门兜底命中率只有3%上下加了混合召回能拉到15%到25%。这个对比数据比任何文字都有说服力。之前接一个朋友的求助他的系统跑通了但论文里“实验分析”全是空话我让他按这个方法跑了一组对比数据用表格贴进论文答辩时老师追着问了十分钟算法细节他照着论文结构答得滴水不漏。那次之后我养成了习惯任何推荐系统项目先写评估再调参而不是调完参再想怎么吹效果——评估数据会替你说真话。希望这篇笔记能帮你在同样的路上少走几步。本文还有配套的精品资源点击获取
返回列表