基于Django与Vue的餐饮推荐系统实战:协同过滤与前后端分离实现
简介一套基于Python Django与Vue.js构建的个性化餐饮场所推荐系统源码包面向Web开发学习者、毕业设计选题者及推荐系统入门者覆盖从用户行为采集、偏好分析到餐饮场所智能推荐的完整流程。资源共999个文件压缩包大小约53.59MB包含82个Python后端脚本、152个Vue前端组件、HTML/JS/CSS等静态资源以及SQL数据库脚本、运行配置和演示视频MP4目录结构清晰。系统已实现用户注册登录、餐饮场所信息管理、行为数据记录与个性化推荐等功能后端采用Django提供接口前端由Vue动态渲染数据库基于MySQL体现典型的前后端分离模式。通过项目源码可深入理解Django接口设计、Vue组件化开发及推荐算法从数据处理到结果展示的工程落地方法演示视频直观展示从登录到获取推荐结果的操作流程适合课程设计与推荐系统入门。目前已有113人学习下载对学习Web全栈与推荐系统具有较强参考价值。1. 这不是一个普通的CRUD项目Django-vue推荐系统到底在推荐什么拿到这个源码包你最先看到的不是推荐算法而是一套完整的前后端分离骨架Django 提供 RESTful APIVue 负责页面交互中间夹着一个「个性化推荐」模块。但真正值得关注的是餐饮场所这个场景下推荐系统是怎么被塞进业务里的——它不是简单地按评分从高到低排序而是要通过用户历史行为、场所标签、距离和价格带做混合召回再精排输出给前端。适合看这篇的人有两类一类是想把推荐系统从论文里搬到真实 Web 项目里的后端工程师另一类是手里有类似「基于Python的Django-vue」选题需要把协同过滤落地的学生或初级开发者。我不会替你把源码逐行念一遍而是按这条链路拆开讲推荐算法怎么选、Django 怎么把算法包成接口、Vue 怎么把结果渲染成「像样的个性化页面」最后给出让这个项目从「能跑」变成「能用」的关键参数和排错经验。2. 餐饮推荐的核心基于用户的协同过滤与基于内容的混合推荐2.1 为什么餐饮场景不能只按评分排序餐饮场所和电影、商品有一个显著区别用户对「吃」的决策是低成本的今天吃腻了川菜明天可能就想换粤菜评分只能反映整体口碑不能反映「这个人现在想吃什么」。如果推荐系统只做全局 TOP-N那么一个从不吃辣的用户也会频繁看到重庆火锅店的 4.8 分高分推荐这种推荐在真实场景里毫无价值。所以餐饮推荐的第一原则是「个性化优先于热门化」。常见的做法是构建两个信号源一是用户显式行为收藏、评分、点赞二是隐式行为浏览时长、下单频率、搜索关键词。源码包里一般会把这两类行为写入同一张行为表用action_type字段区分后续算法才有的放矢。2.2 用户-物品评分矩阵与物品相似度计算推荐系统的数据基础是「用户-物品」矩阵行是用户列是场所值是该用户对场所的打分。餐饮场景里评分往往很稀疏1000 个用户可能只覆盖 300 家店中的 5%这会导致直接计算用户相似度时大量用户之间没有共同评分项。为了解决稀疏问题业界最常见的做法是把「隐式反馈」也折算成分值。比如一次收藏记 1 分一次下单记 3 分一次浏览标记记 0.5 分。这样矩阵的稠密度会显著提升。我一般会在数据预处理阶段做一个叫build_matrix的函数把行为表聚合成 Pandas DataFrameimport pandas as pd from collections import defaultdict def build_user_item_matrix(behavior_df): behavior_df 必须包含 user_id, shop_id, action_type, created_at action_type 权重: browse0.5, fav1.0, order3.0 weight_map {browse: 0.5, fav: 1.0, order: 3.0} matrix defaultdict(dict) for row in behavior_df.itertuples(): user row.user_id shop row.shop_id weight weight_map.get(row.action_type, 0.1) # 同一用户对同一店的多次行为取最大值避免被刷 matrix[user][shop] max(matrix[user].get(shop, 0), weight) return pd.DataFrame.from_dict(matrix, orientindex).fillna(0)这里的核心参数是weight_map它决定了隐式行为的权重配比。如果order的权重太高推荐结果会偏向高频消费场所压制长尾门店如果browse权重太高噪声会被放大。常见工程做法是先按业务经验初始化再用线上反馈修正而不是一次定死。2.3 推荐算法选型UserCF 还是 ItemCF得到了矩阵之后面临第一个选型问题用基于用户的协同过滤UserCF还是基于物品的协同过滤ItemCF。两者在餐饮场景的表现差异很大我直接给出一张对比表维度UserCF基于用户ItemCF基于物品适用场景用户兴趣变化快如资讯、短视频用户兴趣稳定如电商、餐饮口味实时性用户新行为需要重算相似度物品相似度可离线预计算冷启动新用户无历史行为无法推荐新店无评分无法被推荐个性化程度强能看到「和你像的人」的新店弱推荐结果偏「同类型」计算开销用户数增长后开销剧增物品数远小于用户开销可控可解释性较差解释成本高较强可直接说「因为你喜欢某店」餐饮项目的用户量通常远大于场所数量城市级项目可能有几十万用户但有记录的餐饮场所只有几千家所以我一般把 ItemCF 作为主力UserCF 作为补充召回。两种算法算相似度的方式一样核心是余弦相似度import math def item_cf_similarity(matrix): 计算物品之间的余弦相似度矩阵 matrix: indexuser, columnsshop, 值为评分 items list(matrix.columns) sim_matrix {} for i, item_a in enumerate(items): sim_matrix[item_a] {} vec_a matrix[item_a].values norm_a math.sqrt(sum(v * v for v in vec_a)) if norm_a 0: continue for item_b in items[i1:]: vec_b matrix[item_b].values norm_b math.sqrt(sum(v * v for v in vec_b)) if norm_b 0: continue dot sum(a * b for a, b in zip(vec_a, vec_b)) sim_matrix[item_a][item_b] dot / (norm_a * norm_b) sim_matrix[item_b][item_a] sim_matrix[item_a][item_b] return sim_matrix注意这个版本是全量计算只适合千级物品的演示项目。物品超过一万时我一般会改成只计算「共同评分人数大于阈值」的物品对否则每次重跑都会让 Django 接口卡死。阈值通常取 2低于 2 的物品对相似度不可信。3. 用Django把推荐引擎变成可调用的API3.1 Django项目初始化与会话保持后端模块第一步是有一个标准的 Django 工程。无论源码包里的结构多复杂核心对象就两个项目根配置和 App 应用。我习惯先建一个虚拟环境并装好依赖再做django-admin startproject backend创建项目然后进入目录执行python manage.py startapp recommend创建推荐应用这就是网上搜「django创建app」时最常见的标准流程。关键配置在settings.py里以下三个点必须确认INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, # DRF corsheaders, # 跨域 recommend, # 推荐应用 ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ] # 开发阶段直接放开跨域上线换成白名单 CORS_ALLOW_ALL_ORIGINS True # 数据库使用 MySQL 时需要指定引擎 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: restaurant_recommend, USER: root, PASSWORD: yourpassword, HOST: 127.0.0.1, PORT: 3306, } }CORS 中间件必须放在 MIDDLEWARE 列表靠前的位置否则 Vue 发起的跨域请求会被浏览器拦截前端表现为 Network 面板里请求显示 pending 后直接 fail。另外MySQL 需要提前安装mysqlclient在 Windows 上如果编译报错我一般直接安装预编译轮子pip install mysqlclient。开发时偷懒用 SQLite 也行但 dataset 一旦超过几万条SQLite 对并发写的锁问题会频繁出现。3.2 构建场所模型与用户行为记录推荐系统的数据模型聚焦在三个表上场所表Restaurant、用户行为表UserBehavior、用户画像表UserProfile。场所表的核心字段包括类别、价格带、经度纬度、评分和标签行为表记录的是用户对场所的每一次交互画像表则保存计算好的用户偏好向量避免每次请求都从头算。写模型时要特别给行为表加索引因为协同过滤的第一步就是按user_id分组拿行为数据。不加索引的话用户量到五万级别时单次查询就会超过 200ms。模型定义如下from django.db import models class Restaurant(models.Model): name models.CharField(max_length128, verbose_name门店名) category models.CharField(max_length64, db_indexTrue, verbose_name餐饮类别) price_level models.IntegerField(default2, verbose_name价格带 1-5) rating models.FloatField(default0.0, verbose_name平均评分) tags models.CharField(max_length255, blankTrue, verbose_name标签逗号分隔) longitude models.FloatField(default0.0) latitude models.FloatField(default0.0) created_at models.DateTimeField(auto_now_addTrue) class UserBehavior(models.Model): user_id models.IntegerField(db_indexTrue) restaurant models.ForeignKey(Restaurant, on_deletemodels.CASCADE) action_type models.CharField(max_length16, choices[ (browse, 浏览), (fav, 收藏), (order, 下单) ]) weight models.FloatField(default0.0) created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [ models.Index(fields[user_id, action_type]), ]这里注意action_type的权重没有直接写死在模型里而是预留了weight字段。这样做的目的是方便后续做 A/B 测试不同版本的用户可以有不同的权重映射表不需要改表结构。3.3 推荐接口的实现从数据库取数到计算相似度推荐接口不需要走 Django REST Framework 里复杂的 ViewSet 体系一个简单的APIView就够。核心链路是三步取出当前用户的最近行为、拉取所有用户的行为矩阵、调用 ItemCF 计算候选集。为了演示效果更好我给接口加了category和price_level两个硬性过滤条件先做规则过滤再做算法排序。from rest_framework.views import APIView from rest_framework.response import Response from .models import Restaurant, UserBehavior from .recommend_engine import build_user_item_matrix, item_cf_similarity, recommend_for_user class RecommendView(APIView): def get(self, request): user_id request.query_params.get(user_id) category request.query_params.get(category) # 可选限定品类 price_level request.query_params.get(price_level) # 可选限定价格带 if not user_id: return Response({code: 1, msg: user_id is required}) # 1. 构建全量用户-物品矩阵演示项目可直接全量计算 qs UserBehavior.objects.select_related(restaurant).all() behavior_df build_user_item_matrix_from_qs(qs) # 见 2.2 的逻辑只是换成从 ORM 读取 # 2. 如果已有缓存则直接读缓存否则计算物品相似度 sim_matrix cache.get(item_sim_matrix) if sim_matrix is None: sim_matrix item_cf_similarity(behavior_df) cache.set(item_sim_matrix, sim_matrix, timeout3600) # 3. 生成候选集 candidates recommend_for_user(user_id, behavior_df, sim_matrix, top_n20) # 4. 规则过滤 组装返回结果 result [] for shop_name, score in candidates: rest Restaurant.objects.filter(nameshop_name).first() if not rest: continue if category and rest.category ! category: continue if price_level and rest.price_level ! int(price_level): continue result.append({ id: rest.id, name: rest.name, category: rest.category, rating: rest.rating, reason: 因为你和张三都收藏过这家店, # 解释可选 score: round(score, 4), }) return Response({code: 0, data: result[:10]})recommend_for_user函数的实现逻辑是拿到相似度矩阵后找出当前用户评分过的所有物品累加这些物品的相似物品得分再排除掉已经消费过的场所。这里最容易踩的坑是「重复推荐」如果用户已经收藏了 A 店A 店的相似店 B 又和 A 高度相似B 可能在候选集里反复出现。解决办法是在循环里用seen集合去重并设置一个最小相似度阈值我一般取 0.3低于这个值的物品对不参与累加。接口写完后用 Django 自带的cache_page还是手动缓存取决于你对缓存粒度的需求。推荐结果每个用户不同所以不能用页面级缓存我一般只缓存物品相似度矩阵它一天算一次就够了。3.4 缓存与冷启动处理餐饮推荐最尴尬的场景是「新用户打开 App后端查不到任何行为记录」。这时候协同过滤直接返回空列表前端就会展示一片空白。常见处理方案是给冷启动用户做一个默认策略按距离近、评分高、热度高三个条件召回逻辑在接口里加一个分支if behavior_df.loc[user_id].sum() 0: # 冷启动按热度排序兜底 from django.db.models import Count hot_shops Restaurant.objects.annotate( behavior_countCount(userbehavior) ).order_by(-behavior_count, -rating)[:10] return Response({code: 0, data: [build_rest_item(s) for s in hot_shops]})这里我用了behavior_count做热度指标而不是单看评分。原因是餐饮门店的评分区分度低很多店集中在 4.2~4.8 之间行为数却可能有百倍差距。冷启动兜底的质量会直接决定新用户的次日留存值得单独调参。4. Vue前端把推荐结果变成个性化页面4.1 Vue环境准备与依赖安装前端部分我默认使用 Vue 3 Vite Pinia 的组合这是目前「python django搭建web项目」里最常见的配套方案。如果源码包里是 Vue 2 Webpack思路也完全一致差异只在 API 风格上。先确认本机有 Node.js 环境然后执行依赖安装# 创建项目已有 package.json 则直接跳过初始化 npm create vitelatest frontend -- --template vue # 安装项目依赖 cd frontend npm install # 安装路由、状态管理和 HTTP 库 npm install vue-router4 pinia axios这里npm install经常出现卡住或慢的情况尤其是在国内网络环境下光改镜像还不够。我一般先把package.json里的依赖版本改成当前稳定版本区间再让 npm 重新解析。如果遇到ERESOLVE unable to resolve dependency tree的报错优先检查是不是 vue-router 和 vue 版本不匹配而不是盲目加--force。4.2 封装axios请求对接Django API前端和 Django 交互的核心就是 axios。不要把 axios 直接写在页面组件里而是统一封装成一个模块。这样以后接口地址变了、需要统一加 token 或处理错误码时只需要改一个文件。我在项目里一般是这样的结构// src/api/request.js import axios from axios const service axios.create({ baseURL: http://127.0.0.1:8000/api, timeout: 8000 }) // 请求拦截器统一带上认证信息开发阶段先传 user_id service.interceptors.request.use(config { const userId localStorage.getItem(user_id) || 1 config.params config.params || {} if (!config.params.user_id) { config.params.user_id userId } return config }) // 响应拦截器把业务码非 0 的响应直接当成错误抛出 service.interceptors.response.use( response { const res response.data if (res.code ! 0) { return Promise.reject(new Error(res.msg || 请求失败)) } return res }, error { return Promise.reject(error) } ) export default service这里的核心细节是拦截器里的user_id透传。推荐接口必须知道「当前推荐给谁」而前端在真实项目里应该从登录态拿这个值开发阶段从 localStorage 拿可以默认固定成 1 号用户。生产环境需要替换为 JWT 解析出的用户 ID否则所有用户拿到的推荐结果都一样。然后在具体的推荐页面里调用// src/api/recommend.js import service from ./request // 获取个性化推荐列表 export function getRecommendList(params) { return service.get(/recommend/, { params: { category: params.category || , price_level: params.price_level || } }) }4.3 推荐结果展示与交互轮播、标签筛选、无限滚动有了接口数据前端要做的是把「推荐结果」可视化成一个让用户感觉「这很懂我」的页面。我的做法是分三块顶部是「为你推荐」的轮播大图卡片中间是按餐饮类别做的标签筛选栏下面是推荐列表的无限滚动排列。轮播区域展示的是评分最高的前三个推荐结果用 Vue 的过渡动画做切换。大卡片上除了门店名和评分外还要展示推荐理由。推荐理由是这类系统的灵魂一句「因为你看过海底捞所以推荐你小龙坎」比任何算法分数都更有说服力。在 Django 接口里这个字段是reason前端直接渲染即可。无限滚动用 Vue 3 的IntersectionObserver实现比引入第三方滚动库要轻template div classrecommend-page el-carousel height200px el-carousel-item v-foritem in topThree :keyitem.id div classcard clickgoDetail(item.id) span classname{{ item.name }}/span span classrating{{ item.rating }}分/span span classreason{{ item.reason }}/span /div /el-carousel-item /el-carousel div classfilter-bar el-tag v-fortag in categoryList :keytag :typecurrentCategory tag ? primary : info clickfilterByCategory(tag) {{ tag }}/el-tag /div div classlist-container reflistContainer div classlist-item v-forshop in shopList :keyshop.id {{ shop.name }} / {{ shop.category }} /div /div /div /template这个页面里最容易忽略的是「过滤条件变了以后推荐结果要不要重新计算」。我的建议是类别筛选走前端过滤而不是每次点击都重新请求后端。原因很简单后端推荐算法已经在全局算好了一堆候选前端只需要在候选里按category过滤就能得到「某个品类的个性化推荐」既快又不给 Django 增加压力。「vue路由参数」在这个页面同样重要从推荐列表点进门店详情页时通过路由把门店 ID 传过去goDetail(id) { this.$router.push({ path: /shop/${id} }) }然后在详情路由配置里用props: true接收参数比在组件里直接读this.$route.params.id更利于组件复用。4.4 Vite代理与Django联调前端开发服务器默认跑在 5173 端口Django 跑在 8000 端口直接请求必然跨域。虽然 Django 里配了CORS_ALLOW_ALL_ORIGINS但开发体验更顺滑的方式是让 Vite 把/api开头的请求代理到后端// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })配置之后前端请求路径保持/api/recommend/不需要写全域名。上线时再用 Nginx 把/api反向代理到 Django前端只部署静态文件。这种结构下前后端联调时谁都不用改代码。如果出现跨域报错先确认请求是不是走了代理浏览器 Network 里Request URL的 host 是 5173 还是 8000。5. 源码包运行与部署的几个硬核细节5.1 从 zip 到本地跑通的完整步骤拿到「基于Python的Django-vue个性化餐饮场所推荐系统源码」压缩包后第一件事不是解压运行而是核对目录结构。正常项目应该有backend/和frontend/两个根目录或者把前端直接放在templates和static里。如果是前后端分离的源码包按以下顺序跑通# 第一步创建并激活 Python 虚拟环境 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 第二步安装后端依赖先升级 pip避免旧版装不上新版包 pip install -r requirements.txt pip install --upgrade pip setuptools wheel # 第三步执行数据库迁移并启动 Django python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000 # 第四步另开一个终端安装前端依赖并启动 Vite cd frontend npm install npm run dev前后端都启动后浏览器访问http://localhost:5173能看到页面且浏览器 Console 没有报错说明联调成功。如果页面白屏先看后端接口是否能直接访问再检查 Vite 代理是否生效。如果在「python安装」或「vue安装及环境配置」环节卡住优先确认版本Python 3.8~3.12 都行但 Django 版本必须与requirements.txt一致Node.js 建议 18 以上。5.2 常见报错对照表项目跑不起来时90% 的问题集中在几个固定点上。下面这张表是我排查大多数源码包时常用的对照清单报错现象触发原因解决方案ModuleNotFoundError: No module named mysqlclient未安装 MySQL 驱动pip install mysqlclientWindows 装不上则换pymysql并在__init__.py里写pymysql.install_as_MySQLdb()django.db.migrations.exceptions.InconsistentMigrationHistory数据表与迁移文件冲突删除每个 app 的migrations目录里除__init__.py外的文件重新migrateUnicodeDecodeError: utf-8 codec cant decodeCSV 或 JSON 数据文件编码不一致统一转成 UTF-8读取时指定encodingutf-8-signpm ERR! ERESOLVE unable to resolve dependency treeVue 插件版本冲突手动指定兼容版本如vue-router4配vue3[Vue warn]: Failed to resolve component: el-carouselElement Plus 没按需注册app.use(ElementPlus)全量注册或单独引入组件并注册推荐列表一直返回空用户行为表里没有当前用户的记录检查冷启动兜底逻辑是否触发以及user_id是否从前端正确传入5.3 推荐参数调优让推荐结果更像「人的判断」协同过滤能跑通只说明工程完成推荐质量需要调三个参数隐式行为权重weight_map、最近邻数K、相似度阈值min_sim。weight_map我在前面的代码里给了一套默认值但在真实项目里需要根据业务调整。如果产品目标是提高下单转化率order权重应该设在 5 以上如果目标是提升浏览深度browse权重可以提到 1.0。这三个值的比例关系比绝对值重要因为后续归一化会抹平绝对数值。最近邻数K控制在 5 到 20 之间。K 太小推荐结果偶然性大K 太大推荐结果趋近全站热门个性化变弱。我用经验值 K10 作为起点然后逐次减半或加倍对比「用户点击率」和「推荐多样性」两个指标。相似度阈值min_sim一般取 0.3~0.5阈值越高推荐越保守越不容易出现「八竿子打不着」的跨界推荐但召回量也会下降。5.4 验证推荐效果的「笨办法」很多源码包里的推荐系统没有评估脚本验收全靠肉眼。我通常会在本地写一段简单的回测脚本把用户行为数据按时间分成两部分前 70% 做训练集后 30% 做测试集。对测试集中的每个用户把其真实消费的场所和推荐列表做交集算一个命中率看这个命中率是否显著高于「随机推荐」的命中率。这才是评估推荐系统的底线方法。更简单的线上验证是直接看两个指标推荐位的点击率CTR和推荐页到详情页的转化率。把推荐位放在首页第二屏或底部连续观察三天数据如果 CTR 低于 2%优先怀疑冷启动兜底效果不够其次检查相似度是否计算了未归一化的原始权重。推荐系统项目里没有「调完就收工」这回事数据驱动调整才是常规节奏。本文还有配套的精品资源点击获取