ARTICLE DETAIL

资讯详情

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

Django+Vue+MySQL:构建前后端分离新闻推荐系统实战

Django+Vue+MySQL:构建前后端分离新闻推荐系统实战 简介一套面向计算机专业毕业设计场景的新闻推荐系统完整项目基于Python、Django、Vue与MySQL实现前后端分离适合需要快速搭建内容分发平台或完成毕设课题的学生参考。资源覆盖管理员与用户双角色包含新闻排行、收藏、评论、搜索等核心功能并配有数据库脚本与视频教程便于从环境搭建到功能演示全流程跟进。压缩包共736个文件以js、svg、vue、py、css、sql等类型为主其中前端组件与后台逻辑代码划分清晰sql脚本可直接初始化数据库视频教程则辅助理解运行步骤与项目结构整体包体约55.12MB。目前已有214人学习对于希望掌握DjangoVue全栈开发思路、需要一份可运行课设范本的读者而言具备较强的参考价值。1. 毕业设计选型为什么是 PythonDjangoVueMySql 的前后端分离新闻推荐系统每年毕业季都能看到大量新闻类管理系统但绝大多数停留在「增删改查」的层面后台管文章、前台列列表完事。而新闻推荐系统的难点从来不在 CRUD而在「怎么把用户感兴趣的新闻推到首页」和「前后端分离之后鉴权、跨域、联调这些工程问题怎么处理」。用 PythonDjangoVueMySql 这套组合做毕业设计恰恰能在有限时间内同时覆盖推荐算法、RESTful API 设计和前端交互三条线。Django 负责把推荐逻辑封装成接口Vue 负责把推荐结果渲染成可交互的页面MySQL 存储用户行为日志和新闻内容整个链路清晰且容易答辩时演示。这套方案适合三类人想拿企业级开发流程当亮点加分的学生、刚入门全栈想跑通前后端分离的开发者、以及需要给团队做内部新闻聚合工具但不想引入重级推荐平台的工程师。2. 系统架构与数据库设计先把推荐系统的地基打牢2.1 前后端分离架构下 Django 和 Vue 各自承担什么前后端分离的核心是「约定接口各管各的」。Django 端不再返回 HTML 模板而是用djangorestframeworkDRF序列化查询结果返回 JSONVue 端通过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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户行为日志表;参数说明behavior_type用 TINYINT 而不是 VARCHAR是为了后续算权重时直接做数值运算idx_user_behavior_time是高频查询索引查「某用户最近的行为序列」时避免全表扫描duration字段在某些冷启动方案里比点赞更能反映真实兴趣——用户可能懒得点赞但愿意花 3 分钟看完的新闻一定是有价值的。建议在开发阶段就直接建好这三张表不要等代码写完了再补。3. Django REST Framework 后端实现从模型到推荐接口3.1 用 Django ORM 定义模型并迁移到 MySQLDjango 的 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_modeSTRICT_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_length255, verbose_name标题) summary models.TextField(verbose_name摘要) content models.TextField(verbose_name正文) category models.CharField(max_length50, db_indexTrue, verbose_name分类) tags models.CharField(max_length255, blankTrue, verbose_name标签) publish_time models.DateTimeField(auto_now_addTrue, verbose_name发布时间) like_count models.IntegerField(default0, verbose_name点赞数) read_count models.IntegerField(default0, 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可能连不上优先用mysqlclient2.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_k10): 基于用户行为计算相似用户 # 1. 找出与目标用户有过相同新闻交互的用户 user_news set(UserBehavior.objects.filter( user_iduser_id ).values_list(news_id, flatTrue)) candidates UserBehavior.objects.filter( news_id__inuser_news ).exclude(user_iduser_id).values(user_id).annotate( common_countCount(news_id) ).order_by(-common_count)[:top_k] return [c[user_id] for c in candidates] def recommend(user_id, top_n10): 召回排序先取相似用户看过的新闻再按热度排序 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_iduser_id).values_list(news_id, flatTrue)) candidates News.objects.filter( userbehavior__user_id__insimilar_users, userbehavior__behavior_type__in[1, 2, 3] ).exclude(id__inseen).annotate( scoreCount(userbehavior) / len(similar_users) ).order_by(-score)[:top_n] return candidates逻辑说明get_similar_users里用values_list把目标用户看过的新闻 ID 取成一个集合然后用__in查找到所有和它有交集的用户再用annotate(common_countCount(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/?limit10 返回当前用户的推荐列表 user_id request.user.id limit int(request.query_params.get(limit, 10)) news_items recommend(user_id, top_nlimit) 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 vuelatest选上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 classrecommend-page !-- 热门轮播区 -- el-carousel height200px v-ifhotNews.length el-carousel-item v-foritem in hotNews :keyitem.id div classhot-item clickgoDetail(item.id) h3{{ item.title }}/h3 /div /el-carousel-item /el-carousel !-- 个性化推荐列表无限滚动 -- div classnews-list scrollhandleScroll reflistRef div v-foritem in recommendList :keyitem.id classnews-card h4{{ item.title }}/h4 p{{ item.summary }}/p div classmeta 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_iduser_id ).values(news__category).annotate( weighted_scoreSum(F(behavior_type).bitand(0) F(duration) * 0.01) ) # 实际开发中用下面这种方式更直观 behaviors UserBehavior.objects.filter(user_iduser_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_n10): 混合策略新用户用热点随机探索 import random from django.db.models import F, Q from datetime import datetime, timedelta hot_news list(News.objects.filter( publish_time__gtedatetime.now() - timedelta(days3) ).annotate( hot_scoreF(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__gtedatetime.now() - timedelta(hours24) ).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(days3)限制了热点新闻的时间范围避免老新闻一直占着推荐位。随机探索的样本池限定在 24 小时内发布的新内容防止用户看到明显随意推荐的旧新闻。5.3 Redis 缓存推荐列表降低数据库压力推荐接口的瓶颈通常在 MySQL用户行为表的数据量会快速增长每次实时计算开销不小。如果只有十万级数据Django 查询速度尚可但新闻场景有「短时高并发」的特点——一条热点新闻发布后大量用户同时刷新首页。常见做法是用 Redis 做推荐结果的缓存5 分钟过期。# 安装依赖 pip install django-redis # 缓存推荐结果键用 user_id 隔离 redis_client.setex(frecommend:{user_id}, 300, json.dumps(news_ids))逻辑说明缓存键设计为recommend:{user_id}过期时间设为 300 秒这样即使推荐算法没有实时更新用户也不会在 5 分钟内看到剧烈变化的结果体验上反而更稳定。5.4 在线验证推荐效果A/B 测试和满意度收集推荐系统上线后怎么证明它有效最简单的方式是做一个用户反馈按钮——把新闻卡片的「不感兴趣」点击率作为负向指标。在前后端分离架构里这个反馈的行为记录接口可以复用user_behavior表往里加一条behavior_type5的记录即可。# 伪代码不感兴趣反馈 class FeedbackView(APIView): def post(self, request): news_id request.data.get(news_id) UserBehavior.objects.create( user_idrequest.user.id, news_idnews_id, behavior_type5 # 用户点“不感兴趣” ) return Response({code: 0, message: 已反馈})建议在答辩演示时准备两个浏览器账号一个账号先浏览「科技」类新闻两三分钟另一个账号直接刷新首页对比推荐列表的差异。这个演示比任何图表都直观能有力说明推荐系统不是摆设真的在跟踪用户行为。注意A/B 测试要避免「幸存者偏差」——只对比了活跃用户的结果却没统计新用户冷启动的流失率。观察指标不仅要看推荐点击率也要看「人均浏览时长」和「次日回访率」。本文还有配套的精品资源点击获取
返回列表