ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django美食推荐管理系统毕设:推荐算法设计与远程调试全流程解析

Django美食推荐管理系统毕设:推荐算法设计与远程调试全流程解析 做毕设辅导这几年我常跟同学说的一句话是美食推荐管理系统最大的风险不是“做不出来”而是把推荐做成了普通的查询列表。很多人拿到这个题目第一反应是用户登录、菜品管理、分类筛选、后台增删改查等答辩时老师问一句“你的推荐算法在哪里”就只剩下“根据分类推荐”可以讲。这篇文章我会把当年带一个学员从零搭这套系统的全过程挑出来讲包括数据模型怎么设计、推荐逻辑怎么落地、远程调试时最常踩的坑以及文档和演示怎么配合。标题写了“源码文档、远程调试、讲解、定制”这些都不该是挂牌子而是要落到具体工作流里。适合正准备做Django毕设、或者想把这个题目做得有竞争力的同学参考。1. 选题拆解这个毕设真正的得分点不在增删改查1.1 为什么“美食推荐管理”是个稀缺但安全的选题毕设选题最忌讳两件事一是题目太大做不完二是题目太小没话说。“美食推荐管理系统”刚好卡在中间——业务范围清晰用户能感知到的功能很直观但内部又存在“推荐”这个可以讲深的空间。大部分同学写这类系统默认功能清单是用户注册登录、菜品列表、分类浏览、菜品详情、后台管理。这套东西做出来就是标准CRUD代码量不小但答辩时很难形成亮点。老师一看就知道你是照着脑补的需求写的没有真实业务逻辑支撑。我更建议把系统拆成两条线用户可见的功能线和系统内部的策略线。功能线是注册、搜索、评分、收藏、浏览策略线是冷启动推荐、基于偏好的排序、热度加权、行为反馈更新。功能和策略各占一半工作量文档也能写得充实。1.2 用户角色与核心流程必须一开始就定死我先定义三类角色普通游客、注册用户、系统管理员。游客只能浏览公开内容注册用户额外拥有评分、收藏、推荐管理员通过Django Admin管理菜品、分类、用户状态不需要单独写后台前端。核心流程我给学员画成了这样一个循环用户注册登录后系统根据他的收藏、评分和浏览历史生成一份“推荐列表”用户查看推荐后继续产生评分或收藏这些新行为再参与下一轮推荐更新。这个闭环能让推荐系统“活”起来而不是一次性算完就结束。有一点要提前说明推荐系统不一定要做成请求时实时计算的全量协同过滤。对毕设而言能解释清楚“为什么给这个用户推荐了这几道菜”比单纯调一个现成库重要得多。你需要让老师看到你的逻辑链路。1.3 框架选型为什么是Django而不是Flask美食推荐系统用Flask也能做但如果目标是“完整源码文档远程调试”Django明显更合适。Django自带Admin后台、用户认证、ORM、模板系统、分页工具这些正好覆盖管理系统的大部分需求。Flask灵活但灵活意味着大量组件要自己拼写文档时也得多解释一层。我自己的选型理由是毕设阶段框架自带的能力越完整留给你研究核心业务的时间越多。Django的auth直接给你用户表、登录状态、权限装饰器django.contrib.admin直接给你数据管理后台django.core.paginator直接解决分页。把这些系统组件用熟比重复造轮子更能体现你对Web开发的整体理解。2. 数据模型设计一张表的结构就能决定推荐逻辑好不好写2.1 核心表结构用户、分类、菜品、评分、收藏先给大家看我最后敲定的模型代码这是整个项目的地基from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length50, uniqueTrue) sort models.IntegerField(default0, verbose_name排序) class Meta: ordering [sort] class Dish(models.Model): name models.CharField(max_length100) category models.ForeignKey( Category, on_deletemodels.SET_NULL, nullTrue, related_namedishes ) cover models.ImageField(upload_tocovers/, blankTrue, nullTrue) description models.TextField(blankTrue) ingredients models.TextField(blankTrue) tags models.CharField(max_length200, blankTrue, help_text多个标签用英文逗号分隔) avg_rating models.DecimalField(max_digits3, decimal_places1, default0) rating_count models.IntegerField(default0) favorite_count models.IntegerField(default0) created_at models.DateTimeField(auto_now_addTrue) class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) dish models.ForeignKey(Dish, on_deletemodels.CASCADE, related_nameratings) score models.PositiveSmallIntegerField(choices[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]) created_at models.DateTimeField(auto_nowTrue) class Meta: unique_together (user, dish) class Favorite(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namefavorites) dish models.ForeignKey(Dish, on_deletemodels.CASCADE, related_namefav_users) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, dish)这段模型有几个关键取舍。菜品表里我写了avg_rating、rating_count、favorite_count三个冗余字段目的很明确推荐排序、排行榜、菜品列表都要按评分和热度排序如果每次请求都用ORM做聚合计算Count和Avg数据少的时候看不出问题数据一多页面就会明显变慢。冗余字段要用代码保证一致性后面视图里每次产生评分和收藏时同步更新。2.2 外键关系里的细节related_name和on_delete不能偷懒很多新手写外键只写ForeignKey(Category)后面查询就会变成dish.category_id、dish.category.name这种混合写法。我在模型里刻意加了related_name这样正反查都非常直观通过分类找菜品用category.dishes.all()通过菜品找分类直接dish.category。on_delete的选择也值得解释一下。菜品对分类我用SET_NULL因为某些管理场景下删除分类后菜品记录不应该跟着消失而评分和收藏对用户、菜品都用CASCADE用户删除后他的行为记录随之清理这样数据不会残留脏记录。答辩时老师如果问“删除分类会不会影响历史菜品”你就能直接回答出设计意图。2.3 为什么我不用单独的用户画像表不少网上教程喜欢搞一个UserProfile表把用户的偏好在单独一张表里存一堆字段。我的做法是不建静态画像表从行为表里动态聚合。用户收藏了哪些菜、评了哪些分、浏览过哪些菜这些行为本身就能生成推荐信号而且删除行为记录后会自动纠偏不需要手动维护用户“喜欢辣”还是“喜欢甜”的标签。好处是代码更少、逻辑更透明。代价是推荐时要做几次数据库查询但对一个毕设项目来说完全可接受。后面谈到推荐算法时你会发现所有输入都来自Favorite和Rating两张行为表。3. 推荐算法落地从协同过滤到混合排序的一次完整实践3.1 先做冷启动推荐全局热度兜底推荐系统的第一个问题是冷启动新用户没有任何收藏和评分给不出个性化推荐。我的处理是先做全局商品热度排序用一条ORM查询解决def hot_dishes(limit8): return Dish.objects.order_by(-avg_rating, -favorite_count)[:limit]简单归简单但这套逻辑能解释“为什么用户不了解你、系统也能先给他看一批优质内容”。热度可以用评分和收藏量加权得出更讲究一点可以把两个指标归一化再加权比如score 0.6 * avg_rating 0.4 * log(favorite_count 1)但毕设阶段直接用order_by就够了。3.2 基于用户行为的偏好推荐不用框架也能写当用户有了行为记录才开始进入推荐核心逻辑。我给学员的推荐函数分三步走第一步收集用户的正向行为评分大于等于4分的菜品和所有收藏菜品被视为“喜欢”。第二步统计这些喜欢菜品的分类分布得到用户对哪些分类有偏好。第三步从这些分类里排除用户已经交互过的菜品对候选菜打分再把热度因素混进去最后截取Top N。代码我用纯Python写了一遍不依赖任何推荐框架from collections import defaultdict def recommend_for_user(user, top_n6): scor_id_set set( Rating.objects.filter(useruser, score__gte4) .values_list(dish_id, flatTrue) ) fav_id_set set( Favorite.objects.filter(useruser) .values_list(dish_id, flatTrue) ) liked_ids scor_id_set | fav_id_set if not liked_ids: return hot_dishes(top_n) liked_dishes Dish.objects.filter(pk__inliked_ids).select_related(category) cat_weight defaultdict(float) for dish in liked_dishes: # 分类权重喜欢菜品数量越多这个分类权重越高 cat_weight[dish.category_id] 1.0 candidate_dishes ( Dish.objects .filter(category_id__incat_weight.keys()) .exclude(pk__inliked_ids) .select_related(category) ) result [] for dish in candidate_dishes: cat_score cat_weight.get(dish.category_id, 0) hot_score float(dish.avg_rating) min(dish.favorite_count, 50) / 10 total_score cat_score * 0.7 hot_score * 0.3 result.append((total_score, dish)) result.sort(keylambda x: x[0], reverseTrue) return [dish for _, dish in result[:top_n]]这段代码的好处是每一行都能在答辩时讲清楚。select_related会减少N1查询算是一个性能意识展示cat_weight体现协同过滤里“用户相似兴趣”的雏形hot_score体现流行度兜底权重0.7和0.3是超参数可以人工调。3.3 混合排序的思路把“相关”和“热门”结合如果只按分类偏好推荐很可能推荐出一堆评价很差的菜。我项目里引入了混合排序把分类匹配度作为主要信号、热度作为次要信号。上面代码中total_score cat_score * 0.7 hot_score * 0.3就是最简单的混合加权。更完整的做法是加一道时间衰减防止“很久以前评过5分的菜”持续影响推荐结果。我一般建议用timezone.now()和created_at差值的自然对数做衰减因子但只加在打分计算里不改变模型表结构。这样可以在文档里多写一小节“推荐结果的影响因素分析”答辩时内容更丰满。3.4 推荐结果在页面上怎么渲染推荐不能让用户感觉是随机数。我在每一道推荐菜下方都展示理由“因为你收藏/喜欢了XX分类的美食”“该菜评分4.8热度很高”。做法是推荐函数返回菜品对象的同时额外返回命中分类的名称reason 因为你喜欢%s分类 % dish.category.name模板渲染时把reason跟在菜品卡片下面。这个细节能让推荐系统在演示时显得有“思考能力”老师印象会深很多。用户看到理由后也更愿意点击形成更多行为数据系统会越来越准。4. 功能闭环实现注册、搜索、收藏、评分、管理后台4.1 URL设计一套符合直觉的路由项目URL设计直接影响代码可读性和演示流畅度。我常用的路由是这样from django.urls import path from . import views urlpatterns [ path(, views.index, nameindex), path(dish/int:pk/, views.dish_detail, namedish_detail), path(category/int:pk/, views.category_list, namecategory_list), path(search/, views.search, namesearch), path(favorite/toggle/int:pk/, views.toggle_favorite, nametoggle_favorite), path(rating/submit/int:pk/, views.submit_rating, namesubmit_rating), path(recommend/, views.recommend_view, namerecommend), ]URL里用int:pk而不是pkDjango会强制参数类型写错不会走错视图。收藏和评分用POST请求提交所以我没有设计成dish/toggle_favorite这种GET因为切换状态属于写操作用POST更规范还能避免爬虫误触发。所有需要登录的视图统一加login_required装饰器未登录用户会被引导到登录页。4.2 收藏和评分视图同步冗余字段是重点收藏和评分的视图是这套系统里最容易出Bug的地方。我先给一段收藏切换的示例from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect from django.http import JsonResponse from django.views.decorators.http import require_POST login_required require_POST def toggle_favorite(request, pk): dish get_object_or_404(Dish, pkpk) favorite, created Favorite.objects.get_or_create( userrequest.user, dishdish ) if created: # 新收藏热度 1 dish.favorite_count 1 dish.save(update_fields[favorite_count]) else: # 已收藏取消收藏热度 -1 favorite.delete() dish.favorite_count max(dish.favorite_count - 1, 0) dish.save(update_fields[favorite_count]) return redirect(dish_detail, pkpk)用get_or_create而不是先filter再create能避免并发重复点击产生的脏数据。update_fields只更新一个字段减少数据库写入压力。加分操作要防止负数所以取消收藏时用max(favorite_count - 1, 0)兜底。评分视图类似但多了一步更新avg_rating和rating_count。不能直接存新的平均分覆盖旧值应该先算出新平均分再写回login_required require_POST def submit_rating(request, pk): dish get_object_or_404(Dish, pkpk) score int(request.POST.get(score, 0)) if score 1 or score 5: return redirect(dish_detail, pkpk) rating_obj, created Rating.objects.update_or_create( userrequest.user, dishdish, defaults{score: score} ) if created: dish.rating_count 1 dish.avg_rating round( (room_avg(dish, score)) / 1.0, 1 ) dish.save(update_fields[avg_rating, rating_count]) return redirect(dish_detail, pkpk)为了演示方便这里用一个帮助函数room_avg重新算整道菜的平均分代码更直观def room_avg(dish, new_score): # 重新聚合评分保证精度 total dish.avg_rating * (dish.rating_count - 1) new_score return total / dish.rating_count注意update_or_create如果是已存在createdFalse不能再次增加rating_count否则计数会虚高。这段逻辑我踩过坑调试时发现评分人数越变越多最后追到是这里的问题。4.3 Admin后台定制不写一行前端就拥有管理端Django Admin是毕设演示的“免费午餐”。我强烈建议不要自己另外写一套后台管理页面除非你的选题必须。把Django Admin配置好效果已经很专业from django.contrib import admin from .models import Category, Dish, Rating, Favorite admin.register(Dish) class DishAdmin(admin.ModelAdmin): list_display (name, category, avg_rating, rating_count, favorite_count) list_filter (category,) search_fields (name, tags, ingredients) list_per_page 20 ordering (-avg_rating,) admin.register(Rating) class RatingAdmin(admin.ModelAdmin): list_display (user, dish, score, created_at)list_filter可以让管理员按分类筛选菜品search_fields支持关键字搜索list_per_page避免菜品太多时页面卡顿。管理后台再加上createsuperuser创建的账号演示时展示“管理员能直接看到最新收藏数据和评分分布”比自制后台更省心。4.4 搜索和分页小功能也要写出最佳实践搜索我统一走GET参数视图里用icontains做模糊匹配def search(request): keyword request.GET.get(q, ).strip() if keyword: dish_list Dish.objects.filter( models.Q(name__icontainskeyword) | models.Q(tags__icontainskeyword) | models.Q(category__name__icontainskeyword) ) else: dish_list Dish.objects.all() paginator Paginator(dish_list, 8) page_obj paginator.get_page(request.GET.get(page)) return render(request, search.html, {page_obj: page_obj})用Q对象把菜品名、标签、分类名三个条件合并搜索比只搜一个字段人性化得多。Paginator搭配get_page即使page参数不是数字也不会报错模板里直接遍历page_obj就能处理上一页下一页。这里也顺带提醒一个安全细节模板渲染用户输入时Django会自动转义{{ keyword }}比format_html拼字符串安全。你要确保所有从POST/GET进来的数据都通过模板变量输出而不是用|safe过滤器尤其当字段内容来自用户提交时。5. 远程调试与交付真正让项目能跑起来的那些细节5.1 环境隔离是第一优先级远程调试最头疼的往往不是代码逻辑而是对方或者你的另一个电脑跑不起来。接手上本机正常、换环境就挂的项目时我先检查环境python --version是否一致有没有激活虚拟环境requirements.txt是否完整依赖里有没有缺了Pillow这类常见库数据库是SQLite还是MySQL连接配置对不对我习惯在交付前用pip freeze requirements.txt生成依赖清单然后在本机新建虚拟环境验证一遍python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser python manage.py runserver这五步能跑通再谈功能和调试。很多同学远程调试半天最后发现只是环境变量或虚拟环境没激活。5.2 远程调试时最常见的四个坑实际调试里出现频率最高的四类问题我列了张表和对应检查顺序现象可能原因检查顺序浏览器打开报500视图异常、模板找不到看终端traceback确认是否是NoReverseMatch或TemplateDoesNotExist管理后台/页面样式丢失静态文件路径问题DEBUGFalse时是否执行过collectstaticSTATIC_URL是否配置登录后跳转404LOGIN_URL或重定向错误检查项目根urls.py里的LOGIN_REDIRECT_URL数据库相关报错迁移漏了或数据库版本不一致先执行python manage.py makemigrations再migrate确认models.py和迁移文件对应collectstatic这个坑值得多说两句。开发时DEBUGTrueDjango会自己处理静态文件等你把DEBUG关掉模拟上线会发现CSS全部挂掉。因为生产模式下Django不再接管静态文件服务必须执行python manage.py collectstatic把静态资源收集到STATIC_ROOT。远程操作时我一般建议先让对方发浏览器F12控制台的报错信息再发终端最后十几行traceback这两个几乎能覆盖90%的问题。不要一上来就改代码先定位是环境问题、配置问题还是代码逻辑问题。5.3 远程调试工具的组合用法远程调试不等于必须装某个特定IDE插件。我的做法是先用语音通话加远程桌面工具共享屏幕让对方操作我在旁边看着遇到需要改文件时直接要求用VSCode打开项目目录配合Remote-SSH或远程开发插件连到服务器这样我既能看代码又能改文件。如果你的“客户”是另一个学校的同学通常没有服务器那就把远程调试重点放在“教会对方手动执行命令并反馈结果”上。写一份部署文档里面包含每步命令的预期输出能省下大量来回沟通时间。5.4 如何用注释和日志降低远程沟通成本我在项目里加了一个简单的日志模块在推荐视图开始和结束分别打点import logging logger logging.getLogger(__name__) def recommend_view(request): user request.user logger.info(User %s start recommend, history count: %d, user.username, Favorite.objects.filter(useruser).count()) dishes recommend_for_user(user) logger.info(User %s get %d recommend dishes, user.username, len(dishes)) return render(request, recommend.html, {dishes: dishes})日志能帮双方快速确认问题出在推荐逻辑里还是页面模板里。如果日志里start recommend有输出、get recommend没有说明推荐函数内部抛异常两条都有但页面空白则模板层问题。这个排查思路比盲目翻代码有效得多。6. 项目文档与答辩演示源码之外的另一半分数6.1 毕业设计文档的核心结构“全套源码文档”里的文档不是随便写个需求说明书。毕设论文一般需要包含选题背景、需求分析、系统设计、功能实现、系统测试、总结。在我辅导的项目里我会特别强调系统设计部分要放三张图业务流程图、功能模块图、数据库ER图。这三张图能快速让老师理解你的工作量。技术介绍部分不要堆大段源码而是挑两三个亮点讲透。比如“推荐算法采用基于分类偏好的加权策略”“通过冗余字段提升列表页性能”“Admin后台减少重复开发”分别对应算法、性能、工程化三个方向。每个亮点配代码块加运行结果截图这样整个文档的含金量立刻上一个台阶。6.2 演示脚本从登录开始到推荐理由结束很多同学答辩时现场乱点点到哪里讲到哪里。演示前一定要准备一条“流程线”用测试账号登录先展示首页顶部导航进入推荐页展示冷启动推荐此时全部是热门菜再点击一道菜收藏并评分回到推荐页刷新后那道菜已经从列表消失同时同分类的菜排到了前面最后进入Admin后台展示菜品和评分管理。这条线走完系统功能、推荐逻辑、后台管理全部覆盖。演示时记得关闭无关窗口打开数据库清空推荐缓存确保每次演示都是从零开始、可以重复。6.3 答辩高频问题怎么准备老师针对这套系统最常问的问题我总结了五类为什么推荐算法要用分类权重而不用复杂算法数据库冗余字段是如何保证一致性的用户行为数据量变大后推荐性能怎么优化如何防止恶意用户刷评分影响推荐结果系统有哪些功能可以继续扩展准备这些问题不需要背答案关键是理解自己的设计决策。比如问题一回答要点是毕设场景中用户行为数据稀疏分类级特征比菜品级协同过滤更稳定而且代码可解释性强调参成本低。这个问题问完顺便可以讲“将来数据量增大后可以替换成基于矩阵分解的算法”既体现思考又不露怯。6.4 定制需求的边界控制标题里写了“定制”但这并不意味着什么要求都往项目里塞。我通常把定制分成三类界面样式调整、字段与筛选条件完善、额外功能模块开发。前两类改动成本低接口直观第三类要评估复杂度比如“做一个月热度趋势图”就需要新增数据采集和时间序列展示不是简单加个表就行。接到定制需求我建议先问三个问题要在哪个页面看到结果数据从哪张表来希望用户看到什么交互效果这三个问题能过滤掉一大半语焉不详的需求。改完务必跑一遍原有测试账号确认新增功能没有影响推荐主流程毕竟项目的主体依然是那套管理系统。最后再分享一个实际体会我带过的项目里最快出问题的往往不是算法而是环境不通、字段写错、迁移不顺之类的“地基问题”。如果你正在做或准备做美食推荐管理系统先把环境的五步命令练得滚瓜烂熟再动手改模型结构大多数远程调试都能在十分钟内收工。至于推荐逻辑真的不必追求多高深能让演示时说得清“为什么推荐这道菜”已经超过一多半同类作品了。
返回列表