简介:本资源是一份面向高校计算机专业毕业生及Web全栈开发者的新闻推荐系统毕业设计完整文档,聚焦Spring Boot后端与Vue前端协同开发的B/S架构实践,解决个性化新闻分发场景下的系统设计、功能实现与工程落地问题。资源为单个1.8MB PDF文件,内容涵盖系统可行性分析、双角色(管理员/用户)功能设计、MySQL数据库ER图与表结构、前后端接口说明、界面原型截图及全流程测试报告,目录清晰包含系统概述、技术选型、数据库设计、功能模块详解与关键词中英文摘要等核心章节。已有94人学习下载,适合具备Java与前端基础的学习者深入理解推荐系统业务逻辑、掌握Spring Boot+Vue项目分层开发规范,并获取从需求分析到性能验证的完整毕业设计参考范式,尤其对新闻管理、收藏评论、排行榜等典型功能模块的实现细节有详实展开。
1. 为什么用 Spring Boot + Vue 做新闻推荐系统,是毕业论文里「稳中带亮」的选题?
很多计算机专业同学在开题时卡在「技术够用但没亮点」「业务真实但太重」「算法炫酷但跑不起来」三难之间。而「基于 Spring Boot 与 Vue 的新闻推荐系统」恰恰落在一个黄金交叉点:后端用 Spring Boot 快速构建高内聚、可测试、易部署的服务层,前端用 Vue 实现响应式、组件化、状态可追踪的用户交互界面,中间嵌入轻量级推荐逻辑(如协同过滤+热度加权),既避开深度学习模型训练的硬件与数据门槛,又比「增删改查管理系统」更能体现工程整合能力与用户价值理解。它不是纯算法论文,也不是纯界面演示,而是用真实新闻数据流(标题、分类、点击、停留时长)驱动闭环反馈——用户越用,推荐越准;系统越准,用户停留越久。适合本科毕设快速落地,也支撑硕士论文在冷启动策略、实时行为解析、AB 测试框架等方向做纵深延展。尤其当答辩老师看到「用户画像标签动态更新」「新闻相似度在线计算」「Vue 路由级权限控制+推荐卡片懒加载」这些细节时,技术扎实感和问题意识会自然浮现。
2. 后端架构设计:Spring Boot 如何支撑新闻推荐的核心服务链路
新闻推荐系统不是简单地“查数据库然后返回列表”,它需要分层解耦:数据接入层负责清洗原始新闻源(RSS/爬虫/API)、特征提取层生成用户-新闻交互向量、策略调度层决定本次请求用热度排序还是协同过滤、接口层提供标准化 REST 契约。Spring Boot 的优势在于用约定优于配置的方式,让每一层都能快速具象化,且天然支持后续扩展(如接入 Redis 缓存相似新闻、用 RabbitMQ 解耦日志埋点)。
2.1 模块划分与核心依赖选择
项目采用 Maven 多模块结构,根目录下划分为 news-api (Web 接口)、 news-recommender (推荐引擎)、 news-data (数据访问)、 news-common (通用工具)。关键依赖如下:
<!-- news-api pom.xml 片段 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
提示 :H2 数据库仅用于开发与毕设演示阶段,其内存模式支持快速启动与数据重置;若需提交答辩或部署到云服务器,应替换为 MySQL 并在
application.yml中修改spring.datasource.url与驱动类名。spring-boot-starter-cache是为后续「相似新闻缓存」「用户最近浏览缓存」预留的统一入口,避免手写 Map 或 RedisTemplate 造成逻辑散落。
2.2 新闻实体与推荐上下文建模
新闻不是静态内容,而是具备多维属性的动态对象。我们定义 NewsEntity 时,不仅包含基础字段,更预留推荐所需信号:
@Entity
@Table(name = "t_news")
public class NewsEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title; // 标题(用于 TF-IDF 向量化)
private String contentDigest; // 摘要(500字内,用于语义相似度初筛)
private String category; // 分类(科技/体育/财经),作为协同过滤的强特征
private LocalDateTime publishTime;
private Integer readCount = 0; // 累计阅读量(热度基线)
private Integer likeCount = 0; // 点赞数(正向反馈信号)
private Double freshnessScore; // 新鲜度得分(publishTime 加权衰减)
// 关联用户行为(非 JPA 映射,用于推荐计算)
@Transient
private List<UserBehavior> userBehaviors; // 用户对该新闻的点击、停留、分享记录
// getter/setter 省略
}
同时定义 RecommendContext 作为推荐请求的上下文载体,它封装了触发推荐的全部输入:
public class RecommendContext {
private Long userId; // 当前用户ID(匿名用户用设备指纹)
private String deviceType; // mobile/web,影响卡片尺寸与加载策略
private List<String> userInterestTags; // 用户当前兴趣标签(来自历史行为聚类)
private LocalDateTime requestTime; // 请求时间戳(用于新鲜度计算)
private Integer limit = 10; // 返回条数(默认10,支持前端传参覆盖)
// 构造函数与 builder 模式省略
}
2.3 推荐策略调度器:可插拔的推荐算法容器
不把所有逻辑塞进一个 Service,而是抽象出 RecommendStrategy 接口,让不同算法成为可替换的 Bean:
public interface RecommendStrategy {
String getStrategyName(); // 如 "hot", "cf", "hybrid"
List<NewsEntity> recommend(RecommendContext context);
}
@Service
public class HybridRecommendStrategy implements RecommendStrategy {
private final HotNewsService hotNewsService;
private final CollaborativeFilteringService cfService;
public HybridRecommendStrategy(HotNewsService hotNewsService,
CollaborativeFilteringService cfService) {
this.hotNewsService = hotNewsService;
this.cfService = cfService;
}
@Override
public List<NewsEntity> recommend(RecommendContext context) {
// 新用户或冷启动:主推热点+分类匹配
if (context.getUserId() == null || context.getUserInterestTags().isEmpty()) {
return hotNewsService.getHotByCategory(context.getDeviceType(), 8);
}
// 热度兜底 + 协同过滤结果融合(按 3:7 权重)
List<NewsEntity> hotList = hotNewsService.getHotByCategory(
context.getDeviceType(), 3);
List<NewsEntity> cfList = cfService.recommendByUser(context.getUserId(), 7);
return Stream.concat(hotList.stream(), cfList.stream())
.distinct()
.limit(context.getLimit())
.collect(Collectors.toList());
}
}
注意 :
@Service注解使 Spring 容器自动管理该 Bean;@Autowired可注入其他策略实现(如ContentBasedRecommendStrategy),只需在application.yml中通过recommend.strategy=hybrid控制启用项,无需改代码。这种设计让毕设答辩时能清晰展示「策略可切换」这一工程能力点。
3. 前端交互实现:Vue 如何承载新闻推荐的动态体验与用户反馈闭环
Vue 的响应式系统与组件化思想,天然适配新闻推荐场景中的高频状态变更:用户滑动刷新、点击收藏、停留时长上报、实时推荐卡片加载。关键不在「用 Vue」,而在「用对 Vue 的机制」——比如用 watch 监听路由参数变化触发新推荐请求,用 v-infinite-scroll 实现无感知分页,用 provide/inject 在推荐卡片组件中共享用户行为上报方法。
3.1 路由与推荐入口设计:从 URL 到个性化首页
使用 Vue Router v4(Composition API 风格),定义 /recommend/:userId? 为主推荐页,并支持无参访问(匿名用户):
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import RecommendView from '../views/RecommendView.vue'
const routes = [
{
path: '/recommend',
name: 'Recommend',
component: RecommendView,
props: route => ({ userId: route.query.userId || null })
},
{
path: '/news/:id',
name: 'NewsDetail',
component: () => import('../views/NewsDetailView.vue'),
props: true
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
RecommendView.vue 使用 onMounted 和 watch 组合式 API,确保路由参数变更时重新拉取推荐:
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { fetchRecommendations } from '@/api/recommend'
const route = useRoute()
const recommendations = ref([])
const loading = ref(false)
const error = ref(null)
const loadRecommendations = async () => {
loading.value = true
try {
const res = await fetchRecommendations({
userId: route.params.userId || null,
deviceType: 'mobile', // 可根据 window.innerWidth 动态判断
limit: 10
})
recommendations.value = res.data
} catch (e) {
error.value = e.response?.data?.message || '加载推荐失败'
} finally {
loading.value = false
}
}
onMounted(() => {
loadRecommendations()
})
// 监听路由参数变化(如用户登录后跳转带 userId)
watch(() => route.params.userId, () => {
loadRecommendations()
})
</script>
提示 :
fetchRecommendations封装了 Axios 请求,统一处理 baseURL、token 注入与错误拦截;watch监听route.params.userId而非route.query,是因为 Vue Router 默认将 query 参数解析为route.query,而 params 对应动态路径段。此处为保持一致性,建议后端接口统一接收 query 参数,前端改为watch(() => route.query.userId, ...)。
3.2 推荐卡片组件:状态驱动渲染与行为埋点一体化
每个新闻卡片不仅是展示单元,更是数据采集节点。 NewsCard.vue 组件内部集成「曝光上报」与「交互上报」:
<script setup>
import { ref, onMounted, inject } from 'vue'
const props = defineProps({
news: {
type: Object,
required: true
}
})
const emit = defineEmits(['click'])
const reportBehavior = inject('reportBehavior') // 从父组件 provide 注入
const isLoaded = ref(false)
const viewStartTime = ref(null)
onMounted(() => {
// 卡片进入视口即上报曝光(IntersectionObserver 优化版)
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoaded.value) {
isLoaded.value = true
viewStartTime.value = Date.now()
reportBehavior('expose', {
newsId: props.news.id,
position: props.$attrs.index // 由父组件 v-for 传入
})
}
})
observer.observe($refs.card)
})
const handleClick = () => {
emit('click')
const duration = Date.now() - viewStartTime.value
reportBehavior('click', {
newsId: props.news.id,
duration: duration // 毫秒级停留时长
})
}
</script>
<template>
<div ref="card" class="news-card" @click="handleClick">
<h3>{{ news.title }}</h3>
<p class="digest">{{ news.contentDigest }}</p>
<div class="meta">
<span class="category">{{ news.category }}</span>
<span class="time">{{ news.publishTime | formatTime }}</span>
</div>
</div>
</template>
注意 :
reportBehavior通过provide/inject从RecommendView注入,避免在每个卡片中重复写 Axios 请求;formatTime是全局过滤器,将LocalDateTime字符串转为「2小时前」等可读格式;v-for渲染时需显式传递index属性:<NewsCard v-for="(item, index) in recommendations" :key="item.id" :news="item" :index="index" @click="handleNewsClick(item)" />。
3.3 用户行为上报服务:轻量级埋点与后端接收
前端行为数据需结构化上报,后端提供统一 /api/behavior 接口接收:
// api/behavior.js
import request from './request'
export function reportUserBehavior(data) {
return request.post('/api/behavior', data)
}
对应后端 Controller:
@RestController
@RequestMapping("/api")
public class BehaviorController {
@PostMapping("/behavior")
public ResponseEntity<Void> recordBehavior(@RequestBody UserBehaviorDTO dto) {
// 校验必要字段
if (dto.getUserId() == null && dto.getDeviceId() == null) {
return ResponseEntity.badRequest().build();
}
if (dto.getNewsId() == null || StringUtils.isBlank(dto.getType())) {
return ResponseEntity.badRequest().build();
}
// 异步保存(避免阻塞主流程)
behaviorService.asyncSave(dto);
return ResponseEntity.ok().build();
}
}
UserBehaviorDTO 定义行为类型与元数据:
public class UserBehaviorDTO {
private Long userId; // 登录用户ID
private String deviceId; // 匿名设备ID(localStorage 生成)
private Long newsId; // 新闻ID
private String type; // "expose", "click", "like", "share"
private Integer duration; // 停留毫秒数(仅 click/expose 有)
private Integer position; // 在推荐列表中的位置(0-based)
private LocalDateTime timestamp = LocalDateTime.now();
}
提示 :
asyncSave方法使用@Async注解标记,需在主类添加@EnableAsync;实际存储可先写入 Kafka 或 Redis List,再由消费端批量落库,避免高并发下数据库压力。毕设阶段可用@Scheduled(fixedDelay = 5000)定时刷入 H2,体现异步解耦思想。
4. 推荐效果验证:如何用真实指标衡量「推荐是否有效」
毕业论文不能只说「系统实现了推荐功能」,必须回答「推荐比随机好多少?」——这需要定义可测量指标、设计对照实验、采集真实数据并可视化对比。Spring Boot + Vue 架构天然支持 AB 测试:后端通过 @Value("${recommend.ab-test.enabled:false}") 控制是否启用分流,前端通过 localStorage 记录用户分组,行为日志中打标 group: "A" 或 "B" 。
4.1 核心评估指标与数据采集口径
| 指标名 | 计算方式 | 采集来源 | 毕设可达成性 |
|---|---|---|---|
| CTR(点击率) | 点击新闻数 / 曝光新闻数 | 前端 expose + click 日志 | ★★★★★(直接可得) |
| 平均停留时长 | Σ(单次点击停留毫秒) / 点击次数 | click 日志中的 duration 字段 | ★★★★☆(需前端准确计时) |
| 多样性(Diversity) | 推荐列表中不同分类数 / 总条数 | 后端推荐结果中 category 字段去重 | ★★★☆☆(需在推荐 Service 中统计) |
| 新颖性(Novelty) | 推荐列表中用户历史未读新闻占比 | 关联 t_user_read 表查询 | ★★☆☆☆(需额外建表,毕设可简化为「7天内未读」) |
注意 :毕设阶段优先实现 CTR 与平均停留时长,二者直接反映用户兴趣匹配度;多样性可通过前端卡片颜色区分分类(如科技蓝、体育红、财经金),截图佐证;新颖性可简化为「推荐结果中,用户过去24小时未点击过的新闻比例」,用 SQL 快速统计。
4.2 AB 测试分组与后端分流逻辑
在 RecommendController 中加入分流开关:
@RestController
@RequestMapping("/api")
public class RecommendController {
@Value("${recommend.ab-test.enabled:false}")
private boolean abTestEnabled;
@Value("${recommend.ab-test.group-a-ratio:0.5}")
private double groupARatio;
@GetMapping("/recommend")
public ResponseEntity<List<NewsEntity>> getRecommendations(
@RequestParam(required = false) Long userId,
@RequestParam(defaultValue = "mobile") String deviceType) {
String group = "A";
if (abTestEnabled && userId != null) {
// 用户ID哈希取模,保证同一用户始终在同一组
int hash = Math.abs(userId.hashCode());
group = (hash % 1000) < (groupARatio * 1000) ? "A" : "B";
}
RecommendContext context = RecommendContext.builder()
.userId(userId)
.deviceType(deviceType)
.group(group) // 透传分组信息
.build();
List<NewsEntity> result = recommendService.recommend(context);
// 记录日志供后续分析
log.info("AB Test: userId={}, group={}, size={}", userId, group, result.size());
return ResponseEntity.ok(result);
}
}
前端在首次加载时确定分组并持久化:
// utils/ab-test.js
export function getABGroup() {
const saved = localStorage.getItem('abGroup')
if (saved) return saved
const group = Math.random() < 0.5 ? 'A' : 'B'
localStorage.setItem('abGroup', group)
return group
}
// RecommendView.vue 中调用
const abGroup = getABGroup()
// 请求时带上 group 参数
fetchRecommendations({ ..., group: abGroup })
4.3 指标可视化:用 ECharts 在 Vue 中绘制对比折线图
安装 echarts 并在 RecommendView.vue 中引入:
npm install echarts
<script setup>
import * as echarts from 'echarts'
import { onMounted, onUnmounted, ref } from 'vue'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
renderChart()
}
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
const renderChart = () => {
const option = {
title: { text: 'AB测试核心指标对比(近7天)' },
tooltip: { trigger: 'axis' },
legend: { data: ['CTR-A组', 'CTR-B组', '平均停留时长-A', '平均停留时长-B'] },
xAxis: { type: 'category', data: ['Day1', 'Day2', 'Day3', 'Day4', 'Day5', 'Day6', 'Day7'] },
yAxis: [{ type: 'value', name: 'CTR (%)' }, { type: 'value', name: '停留时长 (s)', position: 'right' }],
series: [
{ name: 'CTR-A组', type: 'line', data: [2.1, 2.3, 2.5, 2.7, 2.8, 2.9, 3.0], yAxisIndex: 0 },
{ name: 'CTR-B组', type: 'line', data: [1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4], yAxisIndex: 0 },
{ name: '平均停留时长-A', type: 'line', data: [45, 47, 49, 51, 53, 55, 57], yAxisIndex: 1 },
{ name: '平均停留时长-B', type: 'line', data: [38, 39, 40, 41, 42, 43, 44], yAxisIndex: 1 }
]
}
chartInstance.setOption(option)
}
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>
提示 :真实数据需从行为日志表中聚合查询,毕设可模拟 7 天数据填入
series.data;图表需在onUnmounted中销毁实例,防止内存泄漏;yAxisIndex实现双 Y 轴,左侧显示百分比,右侧显示秒数,直观体现两组差异。
5. 毕设落地关键技巧:从代码到论文的衔接要点与避坑指南
毕业论文不是代码堆砌,而是用文字讲清「为什么这么选」「哪里体现了创新」「数据怎么证明有效」。Spring Boot + Vue 新闻推荐系统在写作中需聚焦三个衔接点:技术选型论证、模块职责边界、评估方法可信度。以下为具体技巧。
5.1 技术选型章节:用对比表格替代主观描述
避免写「Spring Boot 开发效率高」「Vue 上手简单」这类空泛话。应列出可验证的对比维度:
| 对比项 | Spring Boot | 传统 SSM(Spring+SpringMVC+MyBatis) | 说明 |
|---|---|---|---|
| 项目初始化耗时 | < 2 分钟(start.spring.io) | > 30 分钟(手动配置 XML/注解) | 毕设周期紧张,快速验证核心逻辑 |
| 内置 Web 容器 | Tomcat 10(无需外置) | 需单独部署 WAR 包 | 演示时一键 java -jar 启动 |
| 缓存抽象层 | @Cacheable 注解一行启用 | 需手写 Ehcache/RedisTemplate | 热点新闻缓存实现成本降低 80% |
| Actuator 监控 | /actuator/health 等端点开箱即用 | 需自定义 Controller | 答辩时可现场演示服务健康状态 |
同样,Vue 对比 React 应聚焦毕设场景:
| 维度 | Vue 3(Composition API) | React 18(Hooks) | 毕设适配性 |
|---|---|---|---|
| 学习曲线 | 模板语法接近 HTML, ref/reactive 概念集中 | JSX 与 Hooks 组合需理解闭包/依赖数组 | 本科学生 2 周可独立开发 |
| 调试支持 | Vue Devtools 直观查看响应式状态、组件树 | React Devtools 需理解 Fiber 架构 | 答辩现场快速定位状态异常 |
| 打包体积 | vue.runtime.esm-bundler.js ≈ 32KB | react.development.js ≈ 52KB(生产版略小) | 移动端首屏加载更快 |
注意 :表格数据需标注来源(如 Spring 官方文档、Vue Benchmarks GitHub 仓库),不可编造;「毕设适配性」列要结合自身开发经历写,例如「在实现新闻卡片懒加载时,Vue 的
defineAsyncComponent语法比 React 的React.lazy + Suspense更少嵌套层级」。
5.2 系统架构图:用分层泳道图替代 UML 类图
答辩 PPT 和论文中的架构图,应体现「数据流向」而非「类关系」。推荐使用分层泳道图(Swimlane Diagram),横向为技术栈层(数据层、服务层、网关层、前端层),纵向为关键流程(用户请求 → 推荐调度 → 算法执行 → 结果组装 → 前端渲染 → 行为上报 → 数据回流):
┌─────────────┬───────────────────────────────────────────────────────────┐
│ 数据层 │ MySQL(新闻主表) + H2(用户行为临时表) + Redis(缓存) │
├─────────────┼───────────────────────────────────────────────────────────┤
│ 服务层 │ RecommendService → HotNewsService / CFService → BehaviorService │
├─────────────┼───────────────────────────────────────────────────────────┤
│ 网关层 │ Spring Cloud Gateway(可选,毕设阶段用 Nginx 代理即可) │
├─────────────┼───────────────────────────────────────────────────────────┤
│ 前端层 │ Vue Router → RecommendView → NewsCard → BehaviorReporter │
└─────────────┴───────────────────────────────────────────────────────────┘
提示 :图中每个组件名需与代码中类名/文件名严格一致(如
RecommendService而非「推荐服务」);箭头标注协议(HTTP/REST、WebSocket 实时通知);Redis 缓存旁注明 key 设计:news:similar:${newsId}、user:recent:${userId}。
5.3 实验数据分析:用「控制变量法」强化结论可信度
论文中展示 CTR 提升 15%,必须说明「相比什么基准」。标准做法是设置三组对照:
- Control 组 :纯热度排序(
ORDER BY readCount DESC LIMIT 10) - Treatment A 组 :协同过滤推荐(基于用户历史点击的 Item-CF)
- Treatment B 组 :混合推荐(本系统实现的 Hybrid 策略)
采集每组连续 5 天数据,计算日均 CTR 与停留时长,用 Excel 生成带误差线的柱状图。关键是要写出控制变量细节:
- 所有组使用相同新闻池(排除内容质量干扰)
- 用户样本随机分配(
Math.random() < 0.33为 Control,0.33~0.66为 A,>0.66为 B) - 每日 00:00 重置分组(避免长期记忆效应)
- 过滤机器人流量(UA 包含
bot或crawl的请求不计入)
最终结论表述为:「在控制新闻内容、用户分布、时间窗口的前提下,Hybrid 策略相较纯热度排序,CTR 提升 14.7%(p<0.01,t 检验),平均停留时长提升 12.3 秒(±1.2s)」。括号内统计显著性是答辩加分项。
5.4 附录代码规范:Git 提交记录与关键文件清单
论文附录不放全部代码,而是提供可追溯的交付物:
- GitHub 仓库地址 (设为私有,答辩后开放)
- 核心文件清单 (带行数与功能说明):
-
news-api/src/main/java/com/example/news/controller/RecommendController.java(128 行):推荐入口,含 AB 分流逻辑 -
news-recommender/src/main/java/com/example/news/recommender/HybridRecommendStrategy.java(96 行):混合策略实现 -
frontend/src/views/RecommendView.vue(215 行):推荐页主组件,含路由监听与埋点 -
frontend/src/components/NewsCard.vue(142 行):卡片组件,含曝光/点击上报
-
- 关键 Git 提交记录 (证明开发过程):
-
git log --oneline -n 10输出示例:a1b2c3d (HEAD -> main) feat: add AB test group persistence in localStorage e4f5g6h refactor: extract behavior reporting to composable i7j8k9l fix: resolve NPE in CollaborativeFilteringService when no history ...
-
注意 :提交信息用英文,遵循 Conventional Commits 规范(
feat:fix:refactor:);答辩前确保main分支mvn clean package与npm run build全部通过;.gitignore中排除target/node_modules/dist/,避免仓库臃肿。
283



被折叠的 条评论
为什么被折叠?



