简介:基于SpringBoot与Vue框架打造的新闻推荐系统毕业设计项目,包含完整可运行源码和一篇对应论文,面向Java方向毕业生、Spring Boot初学者,以及需要实现内容推荐功能的开发者。压缩包文件总量748个,大小约15.15MB,内部按功能分为Java后端代码、Vue前端页面、JS交互脚本、CSS样式、数据库SQL脚本等,同时提供了Maven工程配置和可执行的安装、运行、打包批处理,方便直接导入开发工具启动项目。配套论文从研究背景、目的和设计思想入手,依次梳理了相关技术、可行性分析、系统性能与安全性、数据完整性、界面与流程设计,然后进入数据库实体和表结构设计,并对管理员模块、用户模块的各个子功能做了详细实现说明,最后通过功能测试、可用性测试和性能测试验证结果,形成完整闭环。项目包含用户信息管理、排行榜管理、新闻信息管理,以及首页、新闻详情、我的收藏等实际页面,适合用于课程设计、毕业设计或二次开发参考。目前已有34人学习下载,可作为快速理解Spring Boot+Vue前后端分离项目的实用范例。
1. 拿到“源码+论文”的Spring Boot新闻推荐系统,先别急着解压
花两个通宵把项目跑通,再花一个通宵把推荐结果调得不像“热门新闻排行榜”,最后发现论文里的截图和实际页面对不上——这是每个拿到“基于Spring Boot新闻推荐系统”这类打包项目的人,大概率要走的三道坎。这类资源包在Java课程设计和毕业设计里出现频率极高,压缩包里装着一套Spring Boot后端、一份前端页面和一篇配套论文,核心卖点不是“新闻管理”,而是“推荐”两个字。
我拆过不少这类项目,也帮人修过不少翻车现场。这套东西的真实构成通常不复杂:Spring Boot管接口和业务,MyBatis管数据库,协同过滤算法算相似度,前端用Vue或Bootstrap套一个后台模板,论文则把系统设计、数据库设计和算法原理串成一篇能过查重的文档。适合谁?适合需要快速交付一个“有推荐算法、有Spring Boot框架、有前后端闭环”的JavaWeb项目的人,或者想拿现成代码做二次开发、把推荐逻辑换掉的开发者。
但“能跑”和“能交”之间隔着不少坑,尤其是版本不匹配、数据稀疏、论文和代码脱节这三类问题,本章先把整体脉络理清,后面每一章都往下落一层。
2. 系统拆解:新闻推荐系统的表结构、模块划分与算法选型
拿到压缩包之后,第一件事不是跑mvn spring-boot:run,而是先把代码结构读明白。这类系统的整体架构基本都是“前台用户端 + 后台管理端 + 推荐引擎”三件套,前端通过HTTP接口调Spring Boot,Spring 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_user | id, username, password, nickname, create_time | 用户账号与基础信息 |
| t_news | id, title, summary, content, category_id, clicks, status, create_time | 新闻内容与状态控制 |
| t_category | id, name, sort | 新闻分类,如时政、体育、娱乐 |
| t_user_behavior | id, user_id, news_id, behavior_type, create_time | 推荐算法唯一数据来源 |
| t_comment | id, user_id, news_id, content, create_time | 评论展示,与推荐无直接关系 |
| t_admin | id, 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 List<News> recommendForUser(Long userId) { // 1. 读取全部用户行为,构建 用户->(新闻->行为分) 的评分矩阵 List<UserBehavior> behaviors = behaviorMapper.selectAll(); Map<Long, Map<Long, Double>> userItemMatrix = new HashMap<>(); for (UserBehavior behavior : behaviors) { userItemMatrix .computeIfAbsent(behavior.getUserId(), k -> new HashMap<>()) .put(behavior.getNewsId(), scoreOf(behavior.getBehaviorType())); } // 2. 如果没有当前用户的行为记录,返回热门兜底 Map<Long, Double> currentUserItems = userItemMatrix.get(userId); if (currentUserItems == null || currentUserItems.isEmpty()) { return newsMapper.selectHotNews(TOP_N); } // 3. 计算当前用户与其他用户的余弦相似度,取前K个邻居 Map<Long, Double> similarityMap = new HashMap<>(); for (Map.Entry<Long, Map<Long, 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); } } List<Map.Entry<Long, Double>> neighbors = similarityMap.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(K_NEIGHBORS) .collect(Collectors.toList()); // 4. 累加邻居的行为分,过滤掉当前用户已读新闻,排序取TOP_N Map<Long, Double> scoreMap = new HashMap<>(); for (Map.Entry<Long, Double> neighbor : neighbors) { Map<Long, Double> neighborItems = userItemMatrix.get(neighbor.getKey()); for (Map.Entry<Long, Double> item : neighborItems.entrySet()) { if (currentUserItems.containsKey(item.getKey())) { continue; } scoreMap.merge(item.getKey(), item.getValue() * neighbor.getValue(), Double::sum); } } List<Long> newsIds = scoreMap.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(TOP_N) .map(Map.Entry::getKey) .collect(Collectors.toList()); return newsMapper.selectBatchByIds(newsIds); } private double cosineSimilarity(Map<Long, Double> a, Map<Long, 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.Entry<Long, 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_N=10是前端首页展示的推荐条数,这个值建议跟分页条数保持一致,否则前端翻页会出现“推荐位满了但下一页是空的”的尴尬情况。K_NEIGHBORS=5是邻居数,论文里写“选取相似度最高的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 Result<List<NewsVO>> recommend(@PathVariable Long userId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { List<News> 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直接暴露给前端,前端首页加载时传page=1&size=10,下拉加载更多时逐页递增。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的坑最隐蔽。
| 组件 | 推荐版本 | 高版本常见问题 |
|---|---|---|
| JDK | 1.8 | JDK 17下Spring Boot 2.3以下版本启动直接报错 |
| Maven | 3.6.x | 3.8+对镜像源和插件校验更严格 |
| MySQL | 5.7 | 8.0后驱动类名变化、时区配置必填 |
| Spring Boot | 2.x | 3.x要求JDK 17且javax包名改为jakarta |
| IDEA | 2021+ | 老版本对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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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末尾的serverTimezone=Asia/Shanghai是MySQL 8.0的必填参数,不写会报“The server time zone value is unrecognized”的错。driver-class-name在MySQL 5.7版本下是com.mysql.jdbc.Driver,8.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 'root'@'localhost',应用秒退。原因分两类:一是数据库本身没启动或账号密码不对,二是MySQL从5.7换到8.0后驱动类和时区配置没跟上。
解决:先确认MySQL服务是否在运行,命令行执行mysql -uroot -p能连上再谈其他。然后检查application.yml的driver-class-name是否为com.mysql.cj.jdbc.Driver,url里是否带上serverTimezone=Asia/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%。这个对比数据,比任何文字都有说服力。
之前接一个朋友的求助,他的系统跑通了但论文里“实验分析”全是空话,我让他按这个方法跑了一组对比数据,用表格贴进论文,答辩时老师追着问了十分钟算法细节,他照着论文结构答得滴水不漏。那次之后我养成了习惯:任何推荐系统项目,先写评估再调参,而不是调完参再想怎么吹效果——评估数据会替你说真话。希望这篇笔记能帮你在同样的路上少走几步。
本文还有配套的精品资源,点击获取