简介:一套面向计算机专业毕业设计场景的新闻推荐系统完整项目,基于Python、Django、Vue与MySQL实现前后端分离,适合需要快速搭建内容分发平台或完成毕设课题的学生参考。资源覆盖管理员与用户双角色,包含新闻排行、收藏、评论、搜索等核心功能,并配有数据库脚本与视频教程,便于从环境搭建到功能演示全流程跟进。压缩包共736个文件,以js、svg、vue、py、css、sql等类型为主,其中前端组件与后台逻辑代码划分清晰,sql脚本可直接初始化数据库,视频教程则辅助理解运行步骤与项目结构,整体包体约55.12MB。目前已有214人学习,对于希望掌握Django+Vue全栈开发思路、需要一份可运行课设范本的读者而言,具备较强的参考价值。
1. 毕业设计选型:为什么是 Python+Django+Vue+MySql 的前后端分离新闻推荐系统
每年毕业季都能看到大量新闻类管理系统,但绝大多数停留在「增删改查」的层面:后台管文章、前台列列表,完事。而新闻推荐系统的难点从来不在 CRUD,而在「怎么把用户感兴趣的新闻推到首页」和「前后端分离之后,鉴权、跨域、联调这些工程问题怎么处理」。用 Python+Django+Vue+MySql 这套组合做毕业设计,恰恰能在有限时间内同时覆盖推荐算法、RESTful API 设计和前端交互三条线。Django 负责把推荐逻辑封装成接口,Vue 负责把推荐结果渲染成可交互的页面,MySQL 存储用户行为日志和新闻内容,整个链路清晰且容易答辩时演示。这套方案适合三类人:想拿企业级开发流程当亮点加分的学生、刚入门全栈想跑通前后端分离的开发者、以及需要给团队做内部新闻聚合工具但不想引入重级推荐平台的工程师。
2. 系统架构与数据库设计:先把推荐系统的地基打牢
2.1 前后端分离架构下 Django 和 Vue 各自承担什么
前后端分离的核心是「约定接口,各管各的」。Django 端不再返回 HTML 模板,而是用djangorestframework(DRF)序列化查询结果,返回 JSON;Vue 端通过axios请求接口,拿到数据后用v-for渲染。两者的唯一契约就是 API 文档。这样做最大的好处是:推荐算法的迭代不需要动前端页面,前端改版也不影响后端逻辑。常见做法是 Django 跑在 8000 端口,Vue 开发服务器跑在 8080 端口,联调时用proxy转发解决跨域。
2.1.1 目录结构上如何规划前后端代码
news_recommend/ ├── backend/ # Django 项目根目录 │ ├── manage.py │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── news/ # 新闻模块 │ │ └── recommend/ # 推荐模块 │ └── db.sqlite3 # 开发环境可用 SQLite,生产换 MySQL ├── frontend/ # Vue 项目根目录 │ ├── src/ │ │ ├── api/ # axios 封装和接口定义 │ │ ├── views/ # 页面组件 │ │ └── router/ # 路由配置 │ └── package.json └── sql/ # 数据库脚本 └── init.sql提示:不要把 Vue 打包后的
dist目录放进 Django 的static里,这和前后端分离的初衷相悖。后面部署时用 Nginx 分别代理静态文件和 API 请求即可。
2.2 MySQL 表结构设计:用户、新闻、行为日志三张核心表
推荐系统要算「用户对什么感兴趣」,必须得有行为数据。新闻表存内容特征,用户表存基本属性,行为日志表记录「谁在什么时间看了哪条新闻、停留了多久、有没有点赞收藏」。这三张表缺一不可。
-- 用户行为日志表:推荐算法的数据来源 CREATE TABLE `user_behavior` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` INT NOT NULL COMMENT '用户ID', `news_id` INT NOT NULL COMMENT '新闻ID', `behavior_type` TINYINT NOT NULL DEFAULT 1 COMMENT '行为类型:1浏览 2点赞 3收藏 4分享', `duration` INT DEFAULT 0 COMMENT '停留时长(秒)', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '行为时间', PRIMARY KEY (`id`), KEY `idx_user_behavior_time` (`user_id`, `create_time`), KEY `idx_news_behavior` (`news_id`, `behavior_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为日志表';参数说明:behavior_type用 TINYINT 而不是 VARCHAR,是为了后续算权重时直接做数值运算;idx_user_behavior_time是高频查询索引,查「某用户最近的行为序列」时避免全表扫描;duration字段在某些冷启动方案里比点赞更能反映真实兴趣——用户可能懒得点赞,但愿意花 3 分钟看完的新闻一定是有价值的。建议在开发阶段就直接建好这三张表,不要等代码写完了再补。
3. Django REST Framework 后端实现:从模型到推荐接口
3.1 用 Django ORM 定义模型并迁移到 MySQL
Django 的 ORM 可以把 Python 类自动转换成数据库表,但连接 MySQL 前需要先装驱动并配置数据库连接。常见做法是使用mysqlclient或pymysql,前者性能更好但需要编译依赖,后者更省事但要注意在__init__.py里注册。
# settings.py 数据库配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'news_recommend', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" } } }模型定义的核心是把新闻的分类、标签、发布时间等特征字段建好,因为后面推荐算法要用到:
# apps/news/models.py from django.db import models from django.contrib.auth.models import User class News(models.Model): title = models.CharField(max_length=255, verbose_name='标题') summary = models.TextField(verbose_name='摘要') content = models.TextField(verbose_name='正文') category = models.CharField(max_length=50, db_index=True, verbose_name='分类') tags = models.CharField(max_length=255, blank=True, verbose_name='标签') publish_time = models.DateTimeField(auto_now_add=True, verbose_name='发布时间') like_count = models.IntegerField(default=0, verbose_name='点赞数') read_count = models.IntegerField(default=0, verbose_name='阅读数') class Meta: db_table = 'news' ordering = ['-publish_time'] def __str__(self): return self.title执行迁移命令时,建议分两步走:python manage.py makemigrations生成迁移文件,python manage.py migrate同步到数据库。很多新手在这一步报错,十有八九是 MySQL 版本和驱动不匹配。MySQL 8.0 以上默认使用caching_sha2_password认证,旧版pymysql可能连不上,优先用mysqlclient==2.2.0及以上版本。
3.2 基于用户协同过滤的推荐接口实现
协同过滤的核心思想是「和你行为相似的人喜欢的新闻,你大概率也喜欢」。实现路径分三步:构建用户-新闻行为矩阵、计算用户相似度、取 Top N 推荐。这里用 Python 实现一个简化版:
# apps/recommend/services.py import numpy as np from django.db.models import Count from apps.news.models import News from apps.users.models import UserBehavior def get_similar_users(user_id, top_k=10): """基于用户行为计算相似用户""" # 1. 找出与目标用户有过相同新闻交互的用户 user_news = set(UserBehavior.objects.filter( user_id=user_id ).values_list('news_id', flat=True)) candidates = UserBehavior.objects.filter( news_id__in=user_news ).exclude(user_id=user_id).values('user_id').annotate( common_count=Count('news_id') ).order_by('-common_count')[:top_k] return [c['user_id'] for c in candidates] def recommend(user_id, top_n=10): """召回+排序:先取相似用户看过的新闻,再按热度排序""" similar_users = get_similar_users(user_id) if not similar_users: # 冷启动:新用户没行为数据,按热度推荐 return News.objects.order_by('-read_count')[:top_n] # 2. 筛选相似用户看过的新闻,排除目标用户已看过的 seen = set(UserBehavior.objects.filter(user_id=user_id).values_list('news_id', flat=True)) candidates = News.objects.filter( userbehavior__user_id__in=similar_users, userbehavior__behavior_type__in=[1, 2, 3] ).exclude(id__in=seen).annotate( score=Count('userbehavior') / len(similar_users) ).order_by('-score')[:top_n] return candidates逻辑说明:get_similar_users里用values_list把目标用户看过的新闻 ID 取成一个集合,然后用__in查找到所有和它有交集的用户,再用annotate(common_count=Count('news_id'))统计共同看过多少条新闻,按共同数量降序取前 10 个作为相似用户。recommend函数先判断冷启动场景——新用户没有任何行为数据时,走热度排序兜底;有行为数据就按相似用户的行为做召回,最后用一个简单的score字段表示新闻的推荐强度,按分排序后返回 Top N。
注意:这里的相似度是「杰卡德系数」的简化版,只算了交集数量没算并集。如果在答辩中想讲得更细,用
len(交集) / len(并集)效果更好,但要注意除零保护。
3.3 用 DRF ViewSet 写推荐接口
有了推荐服务函数,还需要暴露成 HTTP 接口供 Vue 调用。用 DRF 的ViewSet而不是普通APIView,是因为它天然支持列表和详情两种操作,路由注册也方便。
# apps/recommend/views.py from rest_framework.viewsets import ViewSet from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from .services import recommend class RecommendViewSet(ViewSet): permission_classes = [IsAuthenticated] def list(self, request): """GET /api/recommend/?limit=10 返回当前用户的推荐列表""" user_id = request.user.id limit = int(request.query_params.get('limit', 10)) news_items = recommend(user_id, top_n=limit) data = [{ 'id': n.id, 'title': n.title, 'summary': n.summary[:100], 'category': n.category, 'like_count': n.like_count, 'read_count': n.read_count } for n in news_items] return Response({'code': 0, 'data': data, 'message': 'success'})这里给list方法加了limit参数,是为了让前端可以控制每次加载多少条,方便做「下拉加载更多」。permission_classes = [IsAuthenticated]表示必须携带有效 Token 才能访问,防止未登录用户无限刷接口。
4. Vue 前端工程化:从接口联调到推荐流渲染
4.1 Vite 初始化项目和 axios 二次封装
Vue 3 项目推荐用 Vite 而不是 Vue CLI,原因很简单:Vite 冷启动快一个数量级,开发体验好得多。初始化命令是npm create vue@latest,选上Router和Pinia就够用了。接下来最关键的一步是封装 axios,统一处理 Token 注入和响应拦截。
// src/api/request.js import axios from 'axios' import { useUserStore } from '@/stores/user' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', // 开发环境走 Vite proxy,生产环境由 Nginx 转发 timeout: 10000 }) // 请求拦截器:自动附加 Token request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截器:统一处理 401 和业务错误码 request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { // Token 过期,跳转登录页 window.location.href = '/login' } return Promise.reject(error) } ) export default request参数说明:baseURL设为/api而不是完整 URL,是为了开发环境通过 Vite proxy 转发到 Django,生产环境由 Nginx 统一代理,这样代码不用改。请求拦截器里从 Pinia store 取 Token,拼成Bearer <token>格式。响应拦截器统一解包{ code, data, message }结构,业务代码直接拿到data,不用每个组件都做判空。
4.1.1 Vite proxy 配置避免跨域问题
// vite.config.js export default defineConfig({ server: { port: 8080, proxy: { '/api': { target: 'http://127.0.0.1:8000', // Django 开发服务器 changeOrigin: true, // 不需要 rewrite,因为 Django 端接口就带 /api 前缀 } } } })这里最常踩的坑是忘记配changeOrigin: true,导致后端收到的Host头还是前端域名,Django 的ALLOWED_HOSTS校验会直接拒绝请求。每次改完vite.config.js需要重启npm run dev才能生效,Vite 不会自动 reload 配置文件。
4.2 推荐流页面:轮播图和列表无限滚动
推荐流页面要做两个交互:头部放一个基于 Swiper 的新闻轮播展示热门推荐,下面用「下拉加载更多」方式渲染个性化推荐列表。核心逻辑在onMounted里调接口,滚动时判断是否触底然后追加:
<!-- src/views/Recommend.vue --> <template> <div class="recommend-page"> <!-- 热门轮播区 --> <el-carousel height="200px" v-if="hotNews.length"> <el-carousel-item v-for="item in hotNews" :key="item.id"> <div class="hot-item" @click="goDetail(item.id)"> <h3>{{ item.title }}</h3> </div> </el-carousel-item> </el-carousel> <!-- 个性化推荐列表:无限滚动 --> <div class="news-list" @scroll="handleScroll" ref="listRef"> <div v-for="item in recommendList" :key="item.id" class="news-card"> <h4>{{ item.title }}</h4> <p>{{ item.summary }}</p> <div class="meta"> <span>{{ item.category }}</span> <span>阅读 {{ item.read_count }}</span> </div> </div> </div> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import { getHotNews, getRecommend } from '@/api/news' const hotNews = ref([]) const recommendList = ref([]) const page = ref(1) const loading = ref(false) async function loadRecommend() { if (loading.value) return loading.value = true try { const data = await getRecommend(page.value, 10) recommendList.value.push(...data) // 追加到列表尾部 page.value++ } finally { loading.value = false } } function handleScroll() { const el = this.$refs.listRef // 滚动到距底部 50px 时触发加载 if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) { loadRecommend() } } onMounted(() => { loadRecommend() }) </script>提示:
el-carousel是 Element Plus 的组件,需要在 main.js 里完整注册ElementPlus。如果不想引入完整 UI 库导致打包体积过大,可以用unplugin-vue-components做按需加载,配置方法在网上搜「Element Plus 按需引入」即可。
5. 推荐算法进阶与参数调优:让新闻「越刷越懂你」
5.1 从隐式反馈到兴趣权重:给不同行为分配分值
上一章的协同过滤把所有行为一律按Count统计,这有个明显问题:点开新闻 3 秒就关闭和认真读完 3 分钟,反映出的兴趣强度完全不同。改进方向是引入行为权重矩阵。
BEHAVIOR_WEIGHT = {1: 1.0, 2: 3.0, 3: 4.0, 4: 2.0} # 浏览/点赞/收藏/分享 def build_user_profile(user_id): """构建用户兴趣向量:统计各分类加权得分""" from django.db.models import Sum, F profile = UserBehavior.objects.filter( user_id=user_id ).values('news__category').annotate( weighted_score=Sum(F('behavior_type').bitand(0) + F('duration') * 0.01) ) # 实际开发中用下面这种方式更直观 behaviors = UserBehavior.objects.filter(user_id=user_id).select_related('news') category_score = {} for b in behaviors: score = BEHAVIOR_WEIGHT.get(b.behavior_type, 0) # 停留时长超过 60 秒额外加 1 分 if b.duration > 60: score += 1 category_score[b.news.category] = category_score.get(b.news.category, 0) + score return category_score常见的误用是试图在 SQL 层做所有计算,用F表达式和annotate硬凑。对于毕业设计这种十万行数据以内的场景,select_related把新闻分类一次性 join 出来,在 Python 里算权重,既简单又不容易出错。权重值的设定没有标准答案,一般先设点赞 > 收藏 > 分享 > 浏览,再通过问卷调查或 AB 测试调整。
5.2 冷启动问题的三种解法
协同过滤有一个绕不开的痛点:新用户没有任何行为数据,新新闻没有任何用户看过,系统无法推荐。常见解法有三类:热度推荐、基于内容推荐、随机探索。
| 场景 | 推荐策略 | 实现要点 |
|---|---|---|
| 新用户首次访问 | 热度推荐 | 按read_count + like_count * 5综合排序,限定最近 3 天新闻 |
| 新新闻刚发布 | 基于内容推荐 | 提取标题和标签的关键词,与用户历史浏览新闻做文本相似度匹配 |
| 用户行为稀疏 | 随机探索 + 热度混合 | 60% 热度推荐 + 40% 随机抽取,保证新内容有曝光机会 |
def cold_start_recommend(user_id, top_n=10): """混合策略:新用户用热点+随机探索""" import random from django.db.models import F, Q from datetime import datetime, timedelta hot_news = list(News.objects.filter( publish_time__gte=datetime.now() - timedelta(days=3) ).annotate( hot_score=F('read_count') + F('like_count') * 5 ).order_by('-hot_score')[:int(top_n * 0.6)]) # 从最近 24 小时发布的新闻中随机选 40% recent_news = list(News.objects.filter( publish_time__gte=datetime.now() - timedelta(hours=24) ).exclude(id__in=[n.id for n in hot_news])) explore = random.sample(recent_news, min(int(top_n * 0.4), len(recent_news))) return hot_news + explore参数说明:hot_score = read_count + like_count * 5,给点赞设了更高的权重,因为点赞的「信息含量」比浏览高。timedelta(days=3)限制了热点新闻的时间范围,避免老新闻一直占着推荐位。随机探索的样本池限定在 24 小时内发布的新内容,防止用户看到明显随意推荐的旧新闻。
5.3 Redis 缓存推荐列表,降低数据库压力
推荐接口的瓶颈通常在 MySQL:用户行为表的数据量会快速增长,每次实时计算开销不小。如果只有十万级数据,Django 查询速度尚可,但新闻场景有「短时高并发」的特点——一条热点新闻发布后大量用户同时刷新首页。常见做法是用 Redis 做推荐结果的缓存,5 分钟过期。
# 安装依赖 pip install django-redis # 缓存推荐结果,键用 user_id 隔离 redis_client.setex(f"recommend:{user_id}", 300, json.dumps(news_ids))逻辑说明:缓存键设计为recommend:{user_id},过期时间设为 300 秒,这样即使推荐算法没有实时更新,用户也不会在 5 分钟内看到剧烈变化的结果,体验上反而更稳定。
5.4 在线验证推荐效果:A/B 测试和满意度收集
推荐系统上线后怎么证明它有效?最简单的方式是做一个用户反馈按钮——把新闻卡片的「不感兴趣」点击率作为负向指标。在前后端分离架构里,这个反馈的行为记录接口可以复用user_behavior表,往里加一条behavior_type=5的记录即可。
# 伪代码:不感兴趣反馈 class FeedbackView(APIView): def post(self, request): news_id = request.data.get('news_id') UserBehavior.objects.create( user_id=request.user.id, news_id=news_id, behavior_type=5 # 用户点“不感兴趣” ) return Response({'code': 0, 'message': '已反馈'})建议在答辩演示时,准备两个浏览器账号:一个账号先浏览「科技」类新闻两三分钟,另一个账号直接刷新首页,对比推荐列表的差异。这个演示比任何图表都直观,能有力说明推荐系统不是摆设,真的在跟踪用户行为。
注意:A/B 测试要避免「幸存者偏差」——只对比了活跃用户的结果,却没统计新用户冷启动的流失率。观察指标不仅要看推荐点击率,也要看「人均浏览时长」和「次日回访率」。
本文还有配套的精品资源,点击获取