ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

协同过滤商品推荐系统实战:Django+Vue+MySQL架构解析

2026/9/16 3:48:14 拓冰建站 浏览量
协同过滤商品推荐系统实战:Django+Vue+MySQL架构解析 简介面向高校毕业设计场景这套基于PythonDjangoVueMySql的前后端分离协同过滤商品推荐系统完整实现了电商平台中的推荐、购物、订单与评价闭环。用户端提供商品浏览、基于协同过滤的个性化推荐、购物车管理以及订单评价功能管理员端则支持商品上架、信息维护和订单处理。项目中除全部源码外还包含毕业论文和开题报告并附有安装.bat、运行.bat等一键脚本方便在本地环境中快速启动项目。压缩包共714个文件主要涵盖39个py源码、36个vue前端组件、51个CSS样式表、162个JS脚本以及大量SVG/GIF/PNG图标与HTML页面资源包体约18.7MB。已有148人浏览学习适合需要快速搭建推荐系统原型、参考完整工程结构或撰写毕设文档的高校学生。1. 协同过滤商品推荐系统这套毕业设计拆开之后比标题更有价值一个商品推荐系统摆在面前时很多人第一反应是“这不就是按销量排序吗”。真正打开这份基于 Python Django Vue MySql 的前后端分离协同过滤商品推荐系统源码后会发现推荐模块用的是用户-商品评分矩阵前端 Vue 负责商品浏览与购物车交互后端 Django 提供 REST 接口三部分通过 JSON 数据串联。这套设计同时覆盖了“写论文需要算法理论支撑”和“做系统需要完整业务闭环”两个诉求比单纯做增删改查的商城系统有分量得多。如果你正在选电商方向毕设或者想在简历里写一个推荐系统项目这套源码的骨架值得完整拆一遍从数据表设计到算法落地的过程本身就是答辩时最硬的素材。2. 系统架构分层与 MySQL 表设计前后端分离的边界划在哪2.1 为什么用 Django Vue 而不是 Django 模板渲染毕业设计选型时最常见的纠结是Django 自带模板引擎用 Model 直接渲染 HTML 页面也能跑通为什么要额外搭一层 Vue这套源码的答案很明确——为了把“推荐逻辑”和“展示逻辑”彻底解耦。Django 端只负责接收 HTTP 请求、计算推荐结果、返回 JSONVue 端负责商品卡片渲染、购物车状态管理、用户交互反馈。两者通过 RESTful API 通信接口定义清楚了前后端可以并行开发。这种拆分带来的直接好处是协同过滤算法模块可以独立测试不依赖任何页面组件。我在实际调试时可以在 Django shell 里直接调用get_recommendations(user_id)方法看输出再决定是调参还是修数据不需要开浏览器点页面。对于答辩演示来说这种分层也方便现场展示“接口返回什么”和“页面渲染什么”的对应关系比在一个大 HTML 里找逻辑清晰太多。2.2 MySQL 核心数据表结构与模型定义商品推荐系统的数据模型比普通商城多了一层“评分/行为记录”。源码里的 MySQL 表设计围绕五张核心表展开ER 关系是典型的星型结构用户表和商品表是事实表评分表是推荐算法直接依赖的数据源订单表和购物车表负责完成交易闭环。表名核心字段作用说明userid, username, password, nickname, avatar用户信息区分管理员与普通用户productid, name, cover, price, category, stock, sales商品信息category 用于后续类目过滤ratingid, user_id, product_id, score, comment, create_time用户对商品的评分协同过滤的直接输入cart_itemid, user_id, product_id, quantity, checked购物车行项目对应 Vue 端的购物车页面order_infoid, user_id, order_no, total_price, status订单主表status 标记待发货/已完成等状态在 Django 的 models.py 里Rating 表是推荐算法的核心数据源需要注意的细节是 user 和 product 的关联字段必须加db_indexTrue。协同过滤计算时最频繁的操作就是“查某个用户的所有评分”和“查某个商品的所有评分”没有索引的话数据量到两三千条就会开始有明显延迟到答辩演示时现场卡顿非常尴尬。# apps/recommend/models.py from django.db import models from django.conf import settings class Product(models.Model): name models.CharField(max_length128, verbose_name商品名称) cover models.URLField(verbose_name封面图地址) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) category models.CharField(max_length32, db_indexTrue, verbose_name分类) stock models.IntegerField(default0, verbose_name库存) sales models.IntegerField(default0, verbose_name销量) class Meta: db_table product class Rating(models.Model): user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, db_indexTrue) product models.ForeignKey(Product, on_deletemodels.CASCADE, db_indexTrue) score models.FloatField(verbose_name评分范围0.5-5.0) comment models.TextField(blankTrue, verbose_name评价内容) create_time models.DateTimeField(auto_now_addTrue) class Meta: db_table rating unique_together (user, product)模型的unique_together约束保证了同一个用户对同一个商品只能有一条评分记录。这个设计直接简化了协同过滤算法的输入——不需要在代码里做去重处理数据库层面就保证了评分矩阵的稀疏结构是干净的。cover字段用 URLField 而不是 ImageField是把图片存储交给独立对象存储或直接放静态目录Django 只存 URL 字符串避免了 media 文件管理在部署时带来的麻烦。2.3 settings 中的关键配置与跨域处理前后端分离架构下Django 和 Vue 开发服务器分别运行在 8000 和 5173 端口跨域是绕不开的一步。源码在settings.py里启用django-cors-headers并对 dev 环境放开全部跨域权限。生产环境的正确做法是只允许具体域名但毕业设计答辩场景通常是本地演示宽松配置问题不大。# config/settings.py 核心片段 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, apps.user, apps.product, apps.order, apps.recommend, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.common.CommonMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ] REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework.authentication.SessionAuthentication, rest_framework.authentication.TokenAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }这里有一个容易踩的细节CORS_ALLOWED_ORIGINS里localhost和127.0.0.1要同时写因为 Vue 开发服务器访问地址可能从http://localhost:5173跳到http://127.0.0.1:5173只配一个会导致偶尔的跨域报错表现非常诡异。DEFAULT_PERMISSION_CLASSES设为全局需要登录但推荐接口需要允许未登录用户浏览所以视图上要用permission_classes单独覆盖这个在第四章节的视图代码里会体现。3. 协同过滤算法实现从相似度计算公式到可执行的 Python 代码3.1 基于用户的协同过滤原理与选型依据协同过滤分两类基于物品Item-Based和基于用户User-Based。这套源码选的是基于用户核心逻辑是“和你兴趣相似的人喜欢的商品你也很可能喜欢”。计算过程分两步先算用户之间的相似度再根据相似用户的评分预测目标用户对未购买商品的评分取 TopN 推荐。为什么选基于用户而不是基于物品因为商品推荐系统的行为数据以“用户浏览/购买/评分”为主用户数量远小于商品数量时用户相似度矩阵的计算开销可控。如果是一个商品几十万的电商平台基于物品的协同过滤会更合适因为商品间的相似度相对稳定可以离线预先算好。毕业设计场景下数据量有限通常几百个用户、几千个商品User-Based 在效果和展示上更容易讲清楚——答辩时能直接解释“这个用户和那个用户因为都买了机械键盘所以相似度高”比 Item-Based 的黑盒好解释。3.2 皮尔逊相似度与评分预测函数用户相似度计算方式有好几种余弦相似度、皮尔逊相关系数、Jaccard 相似度。源码里用的是皮尔逊相关系数因为它能消除用户评分习惯差异——有的用户习惯打高分有的用户普遍打低分皮尔逊通过减去均值的方式归一化这种偏差。我实际测试中发现如果直接用余弦相似度一个全给 5 星的用户和一个全给 4 星的用户会被算成不相似但实际上他们的偏好排序可能完全一致皮尔逊能修正这个问题。# apps/recommend/similarity.py import math from collections import defaultdict def user_similarity(rating_matrix): 计算用户间皮尔逊相关系数 :param rating_matrix: dict格式 {user_id: {product_id: score}} :return: dict格式 {(user_a, user_b): similarity} # 先构建商品-用户倒排表找出共同评过分的用户对 product_users defaultdict(set) for user_id, products in rating_matrix.items(): for product_id in products: product_users[product_id].add(user_id) # 统计共同评分次数 co_rated defaultdict(int) for product_id, users in product_users.items(): users list(users) for i in range(len(users)): for j in range(i 1, len(users)): user_a, user_b users[i], users[j] # 保证元组有序避免重复计算 pair (user_a, user_b) if user_a user_b else (user_b, user_a) co_rated[pair] 1 # 计算相似度 result {} for (user_a, user_b), count in co_rated.items(): if count 2: # 共同评分商品少于2个时无法计算有效相关系数 continue products_a rating_matrix[user_a] products_b rating_matrix[user_b] common_products set(products_a.keys()) set(products_b.keys()) avg_a sum(products_a[p] for p in common_products) / len(common_products) avg_b sum(products_b[p] for p in common_products) / len(common_products) numerator 0.0 denom_a 0.0 denom_b 0.0 for p in common_products: diff_a products_a[p] - avg_a diff_b products_b[p] - avg_b numerator diff_a * diff_b denom_a diff_a ** 2 denom_b diff_b ** 2 if denom_a 0 or denom_b 0: # 某个用户对共同商品评分完全一致时相关系数无意义 continue result[(user_a, user_b)] numerator / math.sqrt(denom_a * denom_b) return result这段代码的核心优化在倒排表的构建。朴素实现是双重循环遍历所有用户对组合时间复杂度是 O(n²)而倒排表只统计真正有共同评分记录的用户对实际计算量大幅下降。参数上count 2的过滤条件是为了保证相关系数计算至少有 2 个共同评分商品否则标准差为 0 会导致除零错误。返回值中相似度范围为 -1 到 1负数在后续预测中直接丢弃只取正相似用户参与评分预测。评分预测函数用的是加权平均公式权值为相似度最终预测分 相似用户对该商品评分与相似用户平均分的偏差加权求和再加回目标用户的平均分。这个公式比直接加权平均更准确因为它考虑了每个用户的评分基准差异。# apps/recommend/predict.py def predict_score(target_user, product_id, rating_matrix, similarity_map): 预测目标用户对某商品的评分 :param target_user: 目标用户ID :param product_id: 待预测商品ID :param rating_matrix: 用户-商品评分矩阵 :param similarity_map: 用户相似度字典 :return: 预测评分浮点值若无相似用户则返回None # 目标用户已评过分的商品不进行预测 if product_id in rating_matrix.get(target_user, {}): return None weighted_sum 0.0 similarity_sum 0.0 target_avg ( sum(rating_matrix[target_user].values()) / len(rating_matrix[target_user]) if rating_matrix.get(target_user) else 0.0 ) for other_user, products in rating_matrix.items(): if other_user target_user: continue if product_id not in products: continue # 对称取相似度similarity_map存储的key是排序后的元组 pair (target_user, other_user) if target_user other_user else (other_user, target_user) sim similarity_map.get(pair, 0.0) if sim 0: continue # 负相关用户不参与预测 other_avg sum(products.values()) / len(products) weight sim weighted_sum weight * (products[product_id] - other_avg) similarity_sum weight if similarity_sum 0: return None return target_avg weighted_sum / similarity_sumsimilarity_sum的作用是归一化权重把不同相似度量级的贡献统一到预测分数上。返回值可能落在评分范围外比如超过 5.0 或低于 0.5需要在外层做 clip 处理这是评分预测里很常见的修正步骤。目标用户平均分target_avg作为基准值相当于先猜用户会打自己平时的平均分再用相似用户的偏差来微调。3.3 冷启动问题的兜底方案新用户没有评分记录、新商品没有人评过分这是协同过滤的先天性缺陷。这套源码的兜底策略很实际评分矩阵里查不到的用户直接返回热门商品 TopN——按销量降序排列评分少于 3 条的用户则混合推送热门商品占 60%基于已有评分算出的推荐占 40%。# apps/recommend/service.py def get_recommendations(user_id): 对外统一推荐的入口处理冷启动与正常推荐 :param user_id: 用户ID :return: Product QuerySet列表 from apps.recommend.models import Rating from apps.product.models import Product # 拉取评分数据 ratings Rating.objects.filter(score__gte3.0) if user_id: ratings ratings.exclude(user_iduser_id) rating_matrix defaultdict(dict) for r in ratings.select_related(user, product): rating_matrix[r.user_id][r.product_id] r.score # 冷启动用户无评分记录 user_ratings rating_matrix.get(user_id, {}) if len(user_ratings) 0: return Product.objects.order_by(-sales)[:10] sim_map user_similarity(rating_matrix) candidates defaultdict(float) user_rated_products set(user_ratings.keys()) # 遍历相似用户聚合推荐候选 for other_user, products in rating_matrix.items(): if other_user user_id: continue pair (user_id, other_user) if user_id other_user else (other_user, user_id) sim sim_map.get(pair, 0.0) if sim 0.3: continue # 相似度阈值过滤去除低质量邻居 for product_id in products: if product_id in user_rated_products: continue # 用户已购买/评分的商品不再推荐 candidates[product_id] sim * products[product_id] if not candidates: return Product.objects.order_by(-sales)[:10] sorted_candidates sorted(candidates.items(), keylambda x: x[1], reverseTrue) top_ids [p[0] for p in sorted_candidates[:10]] return Product.objects.filter(id__intop_ids)这版代码里加了相似度阈值过滤sim 0.3直接跳过能显著减少噪声用户的干扰。我测试时对比过阈值设置0.3 是经验值设太高会导致候选商品太少设太低会出现“只看过两本书的用户”被下拉进相似邻居。候选得分是相似度与评分的乘积累加而不是预测评分虽然理论纯度不如前面提的预测公式高但胜在计算一步到位适合接口实时调用。需要给答辩展示更严格的推荐解释时可以换成 predict_score 逻辑两种方案在源码里都有注释说明。4. Django REST Framework 接口封装与 Vue 前端联调4.1 DRF 视图集与序列化器设计后端的推荐接口不是简单的 QuerySet 返回而是要把“协同过滤计算出的商品 ID 列表”翻译成 Vue 组件能直接渲染的数据结构。DRF 的 ModelViewSet 在这里配合自定义 action 使用一个/api/products提供商品列表一个/api/recommend走推荐算法。序列化器控制返回字段避免把库存这种无关数据全部抛给前端。# apps/recommend/views.py from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import AllowAny from .service import get_recommendations class RecommendViewSet(viewsets.ViewSet): 推荐接口不需要登录即可访问冷启动时返回热门商品 permission_classes [AllowAny] action(detailTrue, methods[get]) def items(self, request, pkNone): product_list get_recommendations(user_idpk) data [ { id: p.id, name: p.name, cover: p.cover, price: str(p.price), sales: p.sales, category: p.category, } for p in product_list ] return Response({code: 0, data: data, message: success}) action(detailFalse, methods[get]) def hot(self, request): # 热门榜单用于未登录时的默认推荐 from apps.product.models import Product hot_products Product.objects.order_by(-sales)[:10] data [{id: p.id, name: p.name, cover: p.cover, price: str(p.price)} for p in hot_products] return Response({code: 0, data: data})这里没有用 ModelViewSet 的默认方法而是自定义 action原因是推荐接口走的不是标准 REST 资源路径它包含了算法计算逻辑属于 RPC 风格的接口。参数上pk是用户 ID路由会自动生成/api/recommend/{user_id}/items/的形式。str(p.price)必须转字符串因为 DecimalField 在 JSON 序列化时会产生浮点精度问题价格字段的精度在这个细节上决定了前端显示是否会出现 19.900000000002 这种情况。4.2 Vue 前端组件与 axios 请求封装Vue 端的核心组件是推荐商品瀑布流和购物车抽屉。前端通过 axios 封装统一的 request 方法拦截器里做 token 注入和错误统一提示。这个封装思路不只适用于当前项目任何前后端分离项目都可以复用是可以在面试时展开讲的细节。// src/utils/request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: http://127.0.0.1:8000/api, timeout: 15000 }) // 请求拦截自动附加token service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }, error Promise.reject(error) ) // 响应拦截统一处理错误码与登录失效 service.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 }, error { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default servicetimeout: 15000这个参数值得注意。协同过滤算法在用户量大时计算耗时可能超过 5 秒如果 timeout 设短了会直接掐断请求表现为首页推荐区域偶尔空白。设置 15 秒既不至于让用户长时间等待也留够了算法计算余量。响应拦截器统一断言code 0后端任何业务异常都通过这个字段返回前端不需要在每个组件里重复写 try-catch。在 Vue 组件中调用推荐接口时我通常会加一个 loading 状态和兜底渲染这样算法计算期间页面不至于空白。推荐商品展示组件的核心逻辑比较简单一个onMounted钩子触发请求拿到数据后传给子组件渲染。!-- src/views/Home.vue 部分片段 -- template div classrecommend-section h2猜你喜欢/h2 el-row v-loadingloading :gutter20 el-col :span6 v-foritem in recommendList :keyitem.id el-card :body-style{ padding: 12px } img :srcitem.cover classproduct-cover / div classproduct-name{{ item.name }}/div div classproduct-price¥ {{ item.price }}/div el-button typeprimary sizesmall clickaddToCart(item)加入购物车/el-button /el-card /el-col /el-row /div /template script setup import { ref, onMounted } from vue import { ElMessage } from element-plus import request from /utils/request const recommendList ref([]) const loading ref(false) onMounted(async () { loading.value true try { const userId localStorage.getItem(userId) || 1 const res await request.get(/recommend/${userId}/items/) recommendList.value res.data } catch (e) { recommendList.value [] // 推荐失败时保持页面可用 } finally { loading.value false } }) function addToCart(item) { // 购物车逻辑省略 ElMessage.success(已添加 ${item.name} 到购物车) } /scriptscript setup是 Vue 3 的组合式 API 写法比 Options API 更简洁也是源码使用的风格。注意userId从 localStorage 取了以后直接拼进 URL这个动作在真正的生产环境需要服务端从 session 解析这里为了演示简化了。catch (e) { recommendList.value [] }这个兜底极其重要——算法模块偶尔会因为评分数据过少抛异常如果这里不 catch整个首页会因为推荐模块挂掉而白屏这是前后端分离里“局部故障隔离”的典型实现。4.3 接口联调时的参数约定与错误排查前后端联调中 80% 的问题出在接口契约不一致。这套源码里前后端约定了一个统一响应格式{code: 0, data: {...}, message: success}。code非 0 表示业务错误message给出可读信息。前端所有接口都走这个格式解析。环境变量/配置值说明Vite 代理开发环境无跨域代理全量走 CORSvue.config.js 中未配置 proxyDjango 端口127.0.0.1:8000DRF 默认端口Vue 端口127.0.0.1:5173Vite 默认端口token 存储localStorage键名token请求头Authorization: Token token分页参数page/page_size只在商品列表接口使用联调时最容易踩的坑是 Django 的 CSRF 验证。DRF 开启SessionAuthentication后POST/PUT/DELETE 请求会校验 CSRF token而 Vue 端通过 axios 发送 JSON 时不会自动带这个 token。源码里的解决方案是登录接口用了csrf_exempt或者前端从 cookie 中读取csrftoken塞进请求头两种方式各有优劣。如果你用的是 TokenAuthentication 而不是 SessionAuthenticationCSRF 校验会自动跳过这也是我 3.2 节配置里同时启用两种认证方式的原因——Session 用于 Django Admin 后台Token 用于 Vue 前端请求。5. 一键运行脚本与部署排错让源码在五分钟内跑起来5.1 bat 脚本背后的启动逻辑拆解源码根目录的安装.bat、运行.bat、2-run.bat、3-build.bat四个脚本是给非技术用户准备的。逐一拆开看本质上就是环境初始化、后端启动、前端启动、前端打包四个步骤的封装。安装.bat里同时做了 pip 安装依赖和 npm install运行.bat开了两个窗口分别启动 uvicorn 和 npm run dev。# 安装.bat 核心逻辑批处理内容转写为可读形式 python -m venv venv venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple cd frontend npm install --registryhttps://registry.npmmirror.compip 安装指定清华镜像源、npm 安装指定淘宝镜像源这是国内网络环境下跑通项目的实用技巧。不指定镜像源的情况下依赖下载可能因为网络原因反复超时直接导致安装失败。requirements.txt里核心依赖是 Django、djangorestframework、mysqlclient、django-cors-headers版本号的锁定方式直接关系后面是否踩坑建议保持原样不要升级大版本。5.2 mysqlclient 安装失败与跨域异常的处理mysqlclient 在 Windows 环境下是最常见的安装失败点。它依赖 C 编译器和 MySQL 开发库如果本机没有安装 Visual Studio Build Toolspip install mysqlclient 会直接报错。我的做法是手动下载对应 Python 版本的.whl文件安装或者用 PyMySQL 替代在 settings.py 中import pymysql; pymysql.install_as_MySQLdb()。源码默认用的是 mysqlclient因为它的性能比 PyMySQL 更好但如果你是演示环境且 Windows 上没装编译工具替换成 PyMySQL 是最快的路线。报错现象排查方向解决办法ModuleNotFoundError: No module named MySQLdbmysqlclient 未安装成功安装 whl 文件或改用 PyMySQLAccess denied for user rootlocalhost数据库账号密码与 settings.py 不一致修改 config/settings.py 中 DATABASES 配置Unknown collation: utf8mb4_0900_ai_ciMySQL 8.0 以上版本的 collation 兼容问题确认 MySQL 版本或将建表语句中的 collation 改为 utf8mb4_general_ciNetwork Error跨域失败CORS 配置不全或端口不对检查 CORS_ALLOWED_ORIGINS 是否包含 5173 端口Vue 页面空白且控制台报 404打包路径问题修改 vite.config.js 中 base 为./相对路径5.3 验证推荐效果的方法与答辩演示策略项目跑起来之后验证推荐系统不是看页面有没有推荐位而是看推荐内容是否真的随用户行为变化。我的验证方法是准备两个测试账号账号 A 只给“机械键盘、显示器”打 5 星账号 B 只给“耳机、鼠标垫”打 5 星然后分别查看两个账号的推荐列表如果推荐结果高度重合说明协同过滤逻辑有问题如果结果有区分度说明算法生效了。-- 手动插入测试评分数据的 SQL 示例 INSERT INTO rating (user_id, product_id, score, comment, create_time) VALUES (1, 3, 5.0, 手感很好, NOW()), (1, 7, 4.5, 屏幕素质高, NOW()), (2, 3, 5.0, 性价比不错, NOW()); -- 验证查看用户1和用户2是否都被系统视为相似用户 SELECT r1.user_id AS user_a, r2.user_id AS user_b, COUNT(*) AS common_count FROM rating r1 JOIN rating r2 ON r1.product_id r2.product_id AND r1.user_id r2.user_id GROUP BY r1.user_id, r2.user_id;这条 SQL 直接验证了两个用户的共同评分商品数量是判断协同过滤相似度是否合理的第一步。答辩演示时我建议先展示一个无评分的新账号走热门推荐再用有评分记录的账号走个性化推荐两相对比评审老师很快就能看到推荐系统的核心价值。至于前端页面上的购物车、订单、评价功能是业务完整性展示推荐算法模块才是拉开差距的地方。做完演示后把user_similarity函数里相似度输出打印到控制台展示“用户 A 与用户 B 的皮尔逊系数是 0.87”这一具体数值能给答辩准备好一个可深入讨论的技术亮点。本文还有配套的精品资源点击获取