
选Django书籍管理及推荐系统做毕业设计是不少计算机专业学生一拍即合又容易做砸的题目。原因很简单它看起来“不偏不倚”既有书籍管理的增删改查又有推荐系统这种可以讲算法原理的地方但大多数人的实现其实只停留在“能注册登录、能加书删书、首页挂着一条‘猜你喜欢’的假推荐”这种程度。这篇内容会从整个项目怎么拆模块、数据库怎么建、Django工程怎么初始化开始一步步讲到协同过滤算法怎么真正写进视图里最后汇总我在实际过程中踩过的坑。无论你是第一次用Django还是已经写过几个小项目想在答辩时更有底气这个选题都值得按这套思路完整做一遍。1. 项目整体设计与技术选型1.1 书籍管理加推荐系统这个组合好在哪先聊选题。很多毕业设计题目要么跑偏成“纯后台管理系统”要么跑偏成“纯算法实验”。前者的答辩现场非常尴尬老师看完页面就问了一句“你的算法体现在哪”你只能解释半天。后者的硬伤是工程能力展示不足毕竟论文里光给公式不给可运行系统一样不过关。Django书籍管理及推荐系统把两边都占了书籍管理模块负责把Django的整个开发链路走完推荐模块则负责把“个性化”这种用户在互联网产品里天天见的能力真正落地。我自己带毕业设计的时候最喜欢给基础中等的学生推荐这个题目。原因有三。第一需求边界清晰图书、分类、用户、评分这几张表的业务逻辑没有模糊地带闭着眼睛都能设计出来。第二难度可控Django的ORM帮你屏蔽了大部分数据库操作推荐算法用纯Python实现也只要几十行不需要上TensorFlow这些重量级框架。第三它具备完整的互联网产品形态前台用户能浏览、搜索、评分后台管理员能维护数据推荐算法实时给出结果答辩时随便挑一个环节都能展开讲。扩展空间也大。如果学有余力可以在评分基础上加上评论和点赞把推荐算法从UserCF换成ItemCF或者接入Django Channels做实时推送甚至用Redis缓存住推荐结果。这些花活每一个都能在答辩时成为加分项而底子就是这个题目本身。1.2 核心功能模块拆解一个合格的书籍管理及推荐系统至少要拆成四个模块。第一是用户模块负责注册、登录、退出以及记录当前用户对某本书的评分我建议直接复用Django内置的auth.User省去自己写密码加密的麻烦后面做Token鉴权也方便。第二是图书管理模块这是整个系统的主体包含图书分类、图书列表、图书详情、关键词搜索、分页浏览管理端还需要能增删改查图书信息撑起后台的日常维护。第三是推荐模块也是最容易被做废的部分至少实现一个“热门书籍榜”再实现一个“基于用户相似度的个性化推荐”如果数据少还需要加“基于图书相似度的相关推荐”作为兜底。第四是后台管理模块Django admin可以完成大部分需求list_display、search_fields、list_filter这几项配置好管理页面的专业感直接就出来了。模块与模块之间要避免耦合过深。比如推荐模块只依赖Rating表不直接操作Book表这样后续换算法或者加缓存都不会牵一发动全身。我在项目里会把用户模块和图书模块放进名为users的app把推荐相关的逻辑单独放一个recommend的app视图层只负责调用推荐的接口不写算法细节。1.3 技术栈选型与数据库表设计技术栈方面我推荐Django 4.2加上Python 3.10数据库先用SQLite够用且零配置等答辩部署再换成MySQL成本也不高。前端用Bootstrap 5和原生模板语法不需要额外学Vue或者React毕竟毕业设计的重点是后端和算法。管理员界面用Django admin再做一次视觉美化django-unfold这个主题效果就很好装上之后侧边栏、卡片、表单全都换成现代风格老师截图时观感完全不一样。数据库表是整个项目的根基我见过太多人把字段建得乱七八糟导致后面返工。推荐按下面这张表设计表名核心字段作用auth_userDjango内置存用户账号密码邮箱categoryname, description图书分类booktitle, author, isbn, category外键, info, cover, published_date图书基本信息ratinguser外键, book外键, score, created_at用户评分记录recommend_loguser外键, book外键, reason, created_at记录推荐结果关键的外键约束要提前想清楚Book.category设置为CASCADE分类删了书也没了Rating的user和book都设置为CASCADE用户删了评分记录一起删掉这样才能保证数据不会悬空。评分表上要加UniqueConstraint保证“同一个用户对同一本书只能有一条评分记录”这是评分业务的基本规则不加的话后面的推荐算法会算出重复数据。2. 从零搭建Django项目把地基打好2.1 环境准备与创建app的标准姿势环境上先确认Python版本不低于3.8推荐3.10因为Django 4.2对3.10的兼容性最稳定。用虚拟环境隔离依赖是必须养成的习惯直接写python -m venv venv source venv/bin/activate # Windows下面执行 venv\Scripts\activate pip install django4.2 pillowPillow这个必须装因为Book表里用了ImageField保存封面图。装好之后创建项目django-admin startproject book_system cd book_system python manage.py startapp users python manage.py startapp books python manage.py startapp recommend很多人会把所有代码堆在一个app里这是最要命的。按业务拆appusers管用户与评分books管图书与分类recommend管推荐算法后续维护和答辩讲架构都清晰得多。创建完app后记得去settings.py的INSTALLED_APPS里把它们注册进去这是新手最容易漏的一步漏了之后migrate会找不到表页面也会一直报错。2.2 settings配置里的几个关键点settings.py是Django工程的中枢几个配置值得单独说。DEBUG模式在开发和部署阶段完全相反。本地开发保持DEBUGTrue方便看到完整报错部署时一定要改成False并且配好ALLOWED_HOSTS否则页面直接对外暴露异常信息答辩时如果被老师抓到还显得不专业。语言和时区设置成中国习惯LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True静态文件配置要一次配好BASE_DIR路径、STATIC_URL、STATICFILES_DIRS三处配合。前台页面需要加载CSS和JS开发时Django的runserver会自动找部署时靠的是collectstatic这个后面排坑章节会细讲。如果你打算给推荐结果做缓存建议现在就把Cache配置好。Django默认的LocMemCache够开发用想上生产就配Redis。我自己的经验是开发阶段用默认的就行真到数据量大了再换但缓存代码要提前写这样切换只是改一行配置的事。2.3 模型定义与数据库迁移模型是整个项目里最值得花时间的部分。先写Category和Bookfrom django.db import models class Category(models.Model): name models.CharField(max_length50, uniqueTrue) description models.TextField(blankTrue) class Book(models.Model): title models.CharField(max_length200) author models.CharField(max_length100) isbn models.CharField(max_length20, blankTrue) category models.ForeignKey(Category, on_deletemodels.CASCADE) info models.TextField(blankTrue) cover models.ImageField(upload_tocovers/%Y/%m/, blankTrue, nullTrue) published_date models.DateField(nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.title这里有几个细节我得提醒你。ImageField的upload_to如果写的是固定字符串所有封面会堆在一个目录里按年月分目录更利于管理。blankTrue和nullTrue的语义完全不同blankTrue控制表单是否允许为空nullTrue控制数据库是否允许NULLImageField这两个都要设否则后台提交时容易踩“表单明明填了却报错”的坑。再写Rating这是推荐算法最重要的数据来源from django.contrib.auth.models import User class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) book models.ForeignKey(Book, on_deletemodels.CASCADE) score models.IntegerField(choices[(i, i) for i in range(1, 6)]) created_at models.DateTimeField(auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint( fields[user, book], nameunique_user_book ) ]写完之后执行python manage.py makemigrations python manage.py migratemakemigrations是生成迁移文件migrate是把迁移文件真正应用到数据库。这两个命令对新手来说很容易混你只需要记住一句话“make”是生成方案“migrate”是执行方案。如果以后改了模型字段也必须走这两步不能直接去数据库里改表结构。3. 书籍管理模块从模型到视图再到模板3.1 前台列表、详情与搜索的实现思路图书列表页我推荐用Django的类视图ListView代码量比函数视图少一半而且分页功能是内置的。写起来大概是from django.views.generic import ListView from .models import Book class BookListView(ListView): model Book template_name books/book_list.html context_object_name books paginate_by 12 def get_queryset(self): queryset super().get_queryset() keyword self.request.GET.get(q, ) if keyword: queryset queryset.filter(title__icontainskeyword) return queryset这个view同时完成了列表和搜索两件事靠的就是get_queryset里对q参数的判断。title__icontains是ORM的模糊查询翻译成SQL就是LIKE %keyword%icontains不区分大小写。分类筛选也是同理加一个category_id的参数再用filter即可。类视图的底层逻辑其实不复杂ListView在实际渲染时会自动获取模型的所有对象、自动处理分页、自动把分页数据传到模板。理解了这个才知道什么时候该重写get_queryset什么时候该改get_context_data加额外的上下文数据。我见过很多学生强行用函数视图把列表、搜索、分页全堆在一个函数里几百行代码挤在一起答辩时讲不清楚出问题了也不好排查。图书详情页用DetailView更简单唯一要注意的是书籍封面图在模板里的路径处理。模板中直接写{{ book.cover.url }}但记住如果cover字段为null点进去会报错所以模板里要加判断{% if book.cover %} img src{{ book.cover.url }} alt{{ book.title }} {% else %} div classno-cover暂无封面/div {% endif %}评分功能我习惯放在详情页一起做。用户登录后可以在详情页打分后端视图里先检查当前用户是否已经对这书评过分如果评过就更新分数而不是新增记录这是评分模块最常见的业务细节def rate_book(request, book_id): if request.method POST: score int(request.POST.get(score)) rating, created Rating.objects.get_or_create( userrequest.user, book_idbook_id, defaults{score: score} ) if not created: rating.score score rating.save() return redirect(book_detail, pkbook_id)get_or_create是ORM里非常实用的方法它的逻辑是“查不到就新建查到了就返回已有对象”。配合一个created布尔值就能干净地处理update-or-insert场景这也是为后面的推荐算法铺垫数据基础。3.2 Django admin配置与视觉美化Django admin是整个管理模块的免费午餐。注册模型后在admin.py里加上list_display、search_fields、list_filter这三件套后台的可用性立刻上一个档次admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display (title, author, category, published_date) search_fields (title, author, isbn) list_filter (category, published_date) list_per_page 20list_per_page这行是我后来加上去的因为图书数据一多默认的100条一页会让后台页面变得非常长20条一页浏览和操作都舒服。admin外观方面django-unfold是现在比较成熟的主题方案。安装后直接改settings里的INSTALLED_APPS把unfold放在django.contrib.admin前面即可。装完之后admin后台的表格、表单、侧边栏、操作按钮全部改成新的风格不需要动任何业务代码。这属于典型的“举手之劳、肉眼可见”的加分项答辩截屏的时候尤其好用。3.3 模板结构与页面组织模板这块最大的坑是所有人把HTML、CSS、JS全写在一个文件里。正确做法是建一个base.html模板作为全站骨架导航栏、Bootstrap的CDN、页脚都放里面然后用模板继承{% block content %}{% endblock %}子模板头部写{% extends base.html %}再把列表区、详情区、评分表单分别放进各自block里。这样整套页面的统一风格非常容易维护改一次导航栏全站生效。我见过最典型的失败案例是列表页用了Bootstrap的卡片网格详情页却自己写了一套样式风格完全不统一。这个老师说不上具体问题但看的时候就是觉得“粗糙”。所以做模板时先定死一套风格卡片用圆角阴影按钮用统一主色分页组件放在页面底部居中然后所有页面都沿用这一套。4. 推荐系统实现从协同过滤到落地4.1 推荐算法选型推荐系统是这篇论文的核心创新点也是答辩时最容易出彩的地方但很多人的实现方式只是“按评分取前5本书”这严格来说不算推荐算法只能算排行榜。真正能上台面的最小可用方案有两种。第一种是基于用户的协同过滤UserCF核心思想是“和你相似的人喜欢的书你也可能喜欢”。第二种是基于物品的协同过滤ItemCF核心思想是“和你喜欢的书相似的书你也可能喜欢”。两者都是协同过滤家族成员区别在于计算的是用户相似度还是物品相似度。大学毕业设计用UserCF更经典因为它的推荐结果能明显体现出“个性化”不同用户看到的推荐完全不同。算法计算对象适用场景优点缺点UserCF用户-用户相似度用户少、物品多的场景推荐结果个性化强新用户冷启动难ItemCF物品-物品相似度物品相对稳定的场景解释性强、稳定个性化略弱热门榜无冷启动兜底实现简单完全没有个性化选UserCF还有一个现实原因毕业设计的数据量不大用户数撑死几百个计算全量用户相似度也就几十毫秒的事完全不需要上分布式计算那套。如果你用ItemCF反而要维护物品相似度矩阵展示起来不如UserCF直观。4.2 UserCF核心代码实现整个推荐模块的核心是两件事计算用户相似度生成推荐列表。下面这份代码是我在项目里实际用过的去掉注释大概四十行放在RecommendService.py里from math import sqrt def build_user_similarity(ratings): users list(ratings.keys()) sim_matrix {} for u1 in users: for u2 in users: if u1 u2: continue common set(ratings[u1].keys()) set(ratings[u2].keys()) if not common: sim_matrix.setdefault(u1, {})[u2] 0.0 continue dot sum(ratings[u1][b] * ratings[u2][b] for b in common) norm1 sqrt(sum(ratings[u1][b] ** 2 for b in common)) norm2 sqrt(sum(ratings[u2][b] ** 2 for b in common)) sim dot / (norm1 * norm2) if norm1 and norm2 else 0.0 sim_matrix.setdefault(u1, {})[u2] sim return sim_matrix def recommend_by_user(user_id, ratings, top_k5): sim_matrix build_user_similarity(ratings) if user_id not in sim_matrix: return [] neighbors sorted(sim_matrix[user_id].items(), keylambda x: x[1], reverseTrue)[:5] scores {} for other_user, sim in neighbors: for book_id, score in ratings[other_user].items(): if book_id not in ratings.get(user_id, {}): scores[book_id] scores.get(book_id, 0.0) sim * score ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return [book_id for book_id, _ in ranked]相似度的计算用的是余弦相似度公式是两向量的点积除以模长乘积。不理解也没关系记住直觉如果两个人给同一批书打的分数方向一致向量夹角就小相似度就高。推荐时只考虑“目标用户还没评过分的书”把相似用户的评分按相似度加权汇总分最高的前几本就是推荐结果。这段代码放在Django里使用时先从Rating表里组装出ratings字典再调用recommend_by_user。关键点是ratings的数据格式必须和函数签名一致外层是用户ID到内层字典的映射内层是书籍ID到评分。组装代码如下from collections import defaultdict from .models import Rating def build_ratings_dict(): ratings defaultdict(dict) for r in Rating.objects.all().values(user_id, book_id, score): ratings[r[user_id]][r[book_id]] r[score] return dict(ratings)这里用values方法直接从数据库取三列数据避免实例化大量模型对象数据量大的时候性能能差出一个量级。4.3 推荐结果如何接入视图与性能优化推荐算法单独封装成Service之后视图层调用非常简单class RecommendView(View): def get(self, request): user_id request.user.id if not user_id: books Book.objects.order_by(-created_at)[:8] return render(request, recommend.html, {books: books}) ratings build_ratings_dict() book_ids recommend_by_user(user_id, ratings) recommended_books Book.objects.filter(id__inbook_ids) return render(request, recommend.html, {books: recommended_books})注意我处理了未登录用户的场景直接返回最新图书而不是报错。这个兜底逻辑在答辩演示时特别重要因为很多老师会随手点一下推荐页如果你忘了登录就白屏体验分直接扣没了。性能方面UserCF每次请求都全量算一遍相似度矩阵用户量上去之后会有问题。我的处理方式是加缓存Django自带的cache框架就能做from django.core.cache import cache def get_recommendations(user_id): cache_key frecommend_{user_id} result cache.get(cache_key) if result is not None: return result ratings build_ratings_dict() book_ids recommend_by_user(user_id, ratings) cache.set(cache_key, book_ids, timeout60 * 30) return book_ids这套代码的意思是同一个用户半小时内重复访问推荐页直接读缓存只有缓存过期才重新计算。数据量不大时看不出差别但答辩时你可以非常自然地讲出这个优化点比干巴巴背概念有说服力得多。5. 常见问题与排坑实用技巧5.1 ORM删除对象与外键约束的坑Django执行查询和删除对象非常简单但恰恰是这份简单让人放松警惕。比如删除一本书如果Rating表里还有几千条指向它的评分记录而外键又没设置好系统会抛IntegrityError或者更糟留下孤儿数据。我的经验是模型定义阶段就把on_delete行为想清楚Book的category外键用CASCADE没问题Rating的user和book外键也用CASCADE这样删用户或书时评分自动清理但如果你的业务需要保留历史评分用于统计分析改成SET_NULL再给外键字段加nullTrue即可。这些选择要在答辩时候能说出理由推荐在论文里专门用一段写“数据一致性与级联策略”。批量删除也建议走ORM的QuerySet删除比如Rating.objects.filter(book__category_id2).delete()这句话会删除分类2下所有图书的评分记录借助外键反向查询一条语句搞定。如果你用原生SQL容易漏掉关联表这才是真正的坑。5.2 token认证与前端实时推送登录认证这块答辩老师容易追问“Session和Token有什么区别”。Django默认的登录是Session机制服务端存储会话。但如果你的系统以后要对接小程序或者AppToken方式更合适。Django里设置Token的常见做法是登录成功后用HttpResponse对象的set_cookie写入tokentoken create_token(user) response redirect(home) response.set_cookie(token, token, httponlyTrue, max_age3600)前端后续请求带上这个token后端用中间件解析。这样写的好处是无状态服务端不需要存session水平扩展更方便。做毕业设计不需要真的接小程序但你能写出这套代码说明是真理解了Web鉴权的两种主流方案。再说扩展方向。如果要实现“后台有数据更新时前端实时收到推送”Django原生不支持WebSocket需要装Channels库。基本思路是用ASGI替代WSGI配置好ASGI_APPLICATION再写一个consumer处理WebSocket连接。当Rating表新增了一条记录时在保存逻辑里发一个channel message连接的浏览器就能实时收到更新。这个功能在图书系统里可以做“新书上架实时提醒”或者“评分变化实时刷新”。Channels的配置有两三个坑本地调试时注意两点一是Redis服务要起二是channel layer的配置别写错。如果时间紧张这一部分可以作为系统的扩展功能写在论文“系统扩展”章节只做展示不做核心依赖反而更稳妥。5.3 部署上线最常见的三个问题答辩前一天部署到服务器上最容易翻车的就是静态文件。DEBUGTrue时Django自己处理静态文件一切正常一改DEBUGFalseCSS全丢页面变成纯文本。解决方法是先执行collectstatic收集所有静态文件再让nginx或者gunicorn配好静态目录。如果你用的是Debian系的服务器gunicorn、nginx、Redis这些软件用apt直接安装就很省事Python项目再配合一个虚拟环境就能跑起来。建议学生时代至少完整走一遍本地开发、git推到仓库、服务器拉代码、建虚拟环境、跑迁移、用gunicorn起服务、用nginx反向代理。这一套流程你走通过一次以后面试被问部署流程完全不带怕的。另一个要排查的问题是时区错乱。数据库里的created_at全是UTC存储页面显示如果直接打印会差8小时。Django设置TIME_ZONE为Asia/Shanghai会自动处理但如果你用了原生SQL查询DateTime字段就要注意转换。这个问题很隐蔽答辩演示时如果记录的创建时间显示不正确会显得系统不精细。我建议在投入部署之前先把本地调试脚本写全比如写一个测试数据生成脚本批量生成200个用户、500本书、1万条评分用来验证推荐算法的效率和正确性。这个脚本在我实际测试时帮我发现了不少边界情况比如全零评分、单用户空矩阵之类。5.4 测试数据与答辩演示的准备说到测试数据我多讲几句。毕业设计系统最怕演示时“巧妇难为无米之炊”页面上没数据什么列表、搜索、推荐全都显得空荡荡。我自己做了一套造数据脚本用Faker库生成用户名、书名、ISBN、出版日期随机给每本书分配分类随机让每个用户给5到20本书打分。数据量控制在500本书、1万条评分左右整个推荐计算在本地机器上也就一两秒演示效果最好。造数据时要注意分布评分不能全是5分否则相似度计算失去意义也不能集中在少数几本书上否则热门榜永远只有那几本推荐列表毫无参考性。建议评分按近似正态分布生成4分最多1分和5分少一些这样算出来的相似度和推荐列表才像真实场景。这个细节我是在一次测试时发现的所有用户打的都是5分结果相似度矩阵里大家都是1.0推荐结果变成纯随机。答辩演示前建议排练一条固定路径管理员登录后台添加一本新书前台注册一个新用户给几本热门书评分然后打开推荐页看结果。把这条路走顺比准备一百页PPT都管用。老师追问算法细节时直接打开代码指给他看相似度矩阵怎么算这就是最好的答辩答案。最后再分享一个我自己带毕设时反复讲的观点系统可以简化但逻辑链不能断。你做的每一步操作从用户注册、评分到推荐结果出现中间的数据链路要能讲清楚。这条链路通顺了整个系统的完整度就出来了分数自然差不到哪里去。如果你在实现过程中遇到具体问题把报错信息、代码片段、期望行为记下来带着问题去查比盲目乱试高效得多。祝你这个选题做得顺利答辩时也能稳得住。