
考研学习系统这个题目在计算机毕设里算是一个“长跑型选手”它看起来不难但真做起来涉及的东西一点不少注册登录、学习资料管理、题库刷题、学习计划打卡还有数据分析展示每一块都踩得到Django的真实功能。我基于 Python 和 Django 完整跑了一遍这套系统的源码和文档配合代码讲解和一条龙定制把项目彻底理顺了从设计思路到具体实现、再到后期答辩这篇就做一个完整复盘。适合那些准备做毕设、想快速上手 Django 项目、或者想理解一个完整Web系统如何从零落地的同学参考尤其是想选“在线学习/考研学习”这类题目的可以直接对照这个框架来搭。1. 项目整体设计与技术选型1.1 题目怎么定为什么说它有真实需求大多数毕设选题都会纠结一个问题到底选个热门技术方向还是选个能自圆其说的业务系统我的建议是选有真实场景、功能边界清晰的业务系统考研学习系统恰好符合。考研这个场景有个很明显的痛点资料太多、计划散乱、刷题没有统计。今天下载一套英语真题明天收藏一个政治笔记再过几天刷了两套数学卷子全凭感觉复习效率很差。学习系统要解决的就是“让学习过程可视化、可管理”——把资料集中管理把题库拆成模块把刷题记录和正确率统计出来把每天的学习计划变成打卡记录。这个需求在现实里是成立的做出来之后功能和页面都有具体指向不是那种为了凑功能的“虚系统”。针对毕设而言这个题目还有个好处功能量级可控。太简单的系统比如“单一论坛”不好撑篇幅太复杂的比如“大型电商”又超出个人能力。考研学习系统属于中间档位既能体现用户管理、业务逻辑、数据交互又能加数据可视化亮点论文好写、答辩好讲。1.2 为什么选 Django而不是 Flask 或者 Spring Boot技术栈选型是毕设答辩时老师几乎必问的问题。我在这套系统里选择了Python Django主要基于以下几点考虑。Django最大的特点是“全家桶”ORM、Admin后台、用户认证、表单处理、模板引擎都是内置的。这意味着开发一个业务系统绝大多数底层能力不用重复造轮子专注于业务逻辑本身。对于毕设这种有明确时间节点的项目开发效率是第一位的。对比一下其他常见方案更清楚方案优势劣势适合场景Django自带后台、ORM强大、全栈开发快、Python语法简单框架较重、学习曲线略陡业务管理系统、信息管理系统Flask轻量灵活、自由组合需要自己集成ORM、认证、后台小型接口服务、想展示底层能力Spring Boot企业级、生态完善Java语言门槛高、配置复杂大型企业项目或Java方向毕设如果你已经有Java基础选Spring Boot没问题但如果毕设周期短、想用Python、又希望系统能快速成型Django是更稳的选择。答辩时老师问“为什么不用Flask”你可以回答系统除了接口还需要后台管理、权限控制、数据模型等大量完整功能Django内置体系能保证项目的规范性和稳定性更贴合实际开发中系统化交付的需求。说真话Django的Admin后台也是加分项很多演示环节可以直接在后台展示数据管理。1.3 系统功能模块怎么拆才能撑起整个毕设我把系统拆成六大模块每个模块对应一套完整的Django项目结构用户模块注册、登录、注销、个人信息修改就是把Django自带的认证体系扩展为考研用户角色。学习资料模块上传资料PDF/Word/图片、资料分类政治、英语、数学、专业课、资料列表与下载。题库管理模块题目的增删改查、按科目分类、选项设计区分单选题和多选题。刷题练习模块随机刷题、按科目顺序刷题提交答案即时判分记录作答历史。学习计划模块制定每日学习计划、打卡完成、查看今天或本周未完成项。数据统计模块刷题总量、正确率、各科占比、打卡日历用表格和简单图表展示。模块之间不是孤立的而是有数据关联的。例如刷题记录关联用户和题目学习计划关联日期和完成状态统计模块从刷题记录中聚合数据。这样数据库表之间产生外键关系论文的“数据库设计”部分才有的写答辩时也能讲明白系统不是东拼西凑。用户权限也是一个巧妙的点普通用户只能查看和上传资料、刷题管理员通过Django自带的Admin后台或独立管理页面维护题目和资料。我建议普通系统不用把权限设计得太复杂只要区分“登录用户可以干什么”和“游客不能干什么”已经足够体现完整的权限控制概念。2. 环境准备与项目初始化2.1 Python、Django、数据库版本怎么选版本选择是新手最头疼的问题直接说结论Python 3.8到3.10都没问题Django推荐3.2 LTS或4.x系列数据库开发阶段用Django默认的SQLite就够部署时再切换MySQL或者PostgreSQL。Django 3.2是长期支持版本稳定、坑少、网上资料多适合保守选择Django 4.x在功能上更新但需要注意第三方插件的兼容性。如果你的代码讲解或部署文档是基于某个特定版本写的建议跟着文档版本走不要在环境上强行追新。数据库方面SQLite的特点是零配置、单文件、开发调试方便。缺点是并发能力弱、部分复杂查询稍弱。毕设环境完全扛得住。如果你想用MySQL需要在settings里配置后端驱动推荐用pymysql或者在requirements里加上mysqlclient。依赖文件requirements.txt大概是这样的Django3.2.25 pymysql1.1.0 Pillow10.0.0 django-cors-headersPillow是为了处理头像上传和图片字段django-cors-headers如果在前后端分离模式下才需要纯Django模板渲染可以不装。安装就一行命令pip install -r requirements.txt建议直接新建一个虚拟环境避免和其他项目产生包冲突。开发到一半发现包版本错乱是最浪费时间的事。2.2 创建项目、app与基础配置初始化Django项目非常简单Django-admin工具熟悉后整个过程五分钟搞定。先创建项目和appdjango-admin startproject kaoyan_system cd kaoyan_system python manage.py startapp user python manage.py startapp material python manage.py startapp question python manage.py startapp exam python manage.py startapp plan python manage.py startapp statsapp的划分和功能模块对应而不是一个app装所有代码这是Django项目规范的第一步。如果代码全堆在models.py和views.py里后面每一项功能都可能互相干扰。然后是settings.py的修改。必改的几个点包括INSTALLED_APPS注册appLANGUAGE_CODE设置为zh-hansTIME_ZONE设置为Asia/ShanghaiMEDIA_URL和MEDIA_ROOT配置上传文件目录。注册app这一步漏了会直接报“No module named”之类的错误新手最容易栽在这里。时区问题说一下Django默认时区是UTC如果你不改保存到数据库的时间会差8小时。改语言和时区的操作很简单但是不熟的话很容易忽略等到统计学习时间或者打卡日期时才发现问题。静态文件配置是这样的# settings.py MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media) STATIC_URL /static/ STATICFILES_DIRS [os.path.join(BASE_DIR, static)]主路由urls.py里开发阶段需要单独处理media文件的访问from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这一步不做资料上传后图片或文件无法通过URL访问页面上就会看到破图。2.3 数据库表设计一篇文章讲清楚关系数据库设计是论文的重点章节。我当初设计的时候核心表就这么几张用户表User直接继承Django的AbstractUser、学习资料表StudyMaterial、题目表Question、刷题记录表ExamRecord、学习计划表StudyPlan。先看models.py的核心部分from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length11, blankTrue, nullTrue) avatar models.ImageField(upload_toavatar/, defaultdefault.png) class StudyMaterial(models.Model): CATEGORY_CHOICES [ (politics, 政治), (english, 英语), (math, 数学), (major, 专业课), ] title models.CharField(max_length200) category models.CharField(max_length20, choicesCATEGORY_CHOICES) file models.FileField(upload_tomaterials/) description models.TextField(blankTrue) uploader models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) class Question(models.Model): category models.CharField(max_length20, choicesCATEGORY_CHOICES) stem models.TextField(verbose_name题干) option_a models.CharField(max_length500) option_b models.CharField(max_length500) option_c models.CharField(max_length500, blankTrue) option_d models.CharField(max_length500, blankTrue) answer models.CharField(max_length10, verbose_name正确答案) analysis models.TextField(blankTrue, verbose_name解析) created_at models.DateTimeField(auto_now_addTrue) class ExamRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) question models.ForeignKey(Question, on_deletemodels.CASCADE) selected_answer models.CharField(max_length10) is_correct models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class StudyPlan(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) title models.CharField(max_length200) plan_date models.DateField() is_completed models.BooleanField(defaultFalse)几个关键点解释一下第一用户表继承AbstractUser而不是单独建表。这是Django最优雅的地方之一既保留了自带的用户名密码、权限字段又能扩展手机号、头像这些业务字段登录逻辑完全不用重写。很多新手卡在“我如何自己写注册登录”上实际上用Django自带认证加扩展字段是最省力的路径。第二外键关系要设计清楚。ExamRecord同时外键关联User和Question这就构成“一个用户做了很多题一道题被很多用户做过”的多对多关系通过中间表表达是最规范的。StudyMaterial里的uploader外键表示这个资料是谁上传的删除用户时资料也随之删除因为用了on_deleteCASCADE。第三日期字段的格式。StudyPlan的plan_date使用DateField而不是DateTimeField因为学习计划是按天来算的而刷题记录的created_at使用DateTimeField因为需要精确到秒来分析某个时间段的刷题行为。3. 核心功能模块实现从注册登录到刷题判分3.1 用户注册登录与Token认证注册登录是几乎所有系统都有的功能Django在这里提供了非常成熟的基础方案。用户注册视图的思路是获取表单数据、创建用户实例、自动登录跳转。登录视图直接用Django的authenticate和login方法。from django.shortcuts import render, redirect from django.contrib.auth import authenticate, login from django.contrib.auth.models import User from django.contrib import messages def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return redirect(dashboard) else: messages.error(request, 用户名或密码错误) return render(request, user/login.html)关于登录状态保持这里有一个容易忽视的问题。Django默认用session保持登录状态后端会在数据库或内存生成session记录浏览器通过Cookie存sessionid。对小项目来说直接用session是最稳的不用额外写代码。但如果你想用前后端分离模式比如前端用Vue调用接口那么更推荐用JWT或者Token机制。我在实际项目中做定制时经常用Token方案最简单的做法就是在登录成功后自己生成一个token串写入浏览器Cookie同时把token存到用户表的token字段里。代码如下import uuid from django.http import JsonResponse def login_view_api(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(usernameusername, passwordpassword) if user: token uuid.uuid4().hex user.token token user.save() response JsonResponse({code: 0, message: 登录成功}) response.set_cookie(token, token, max_age7*24*3600, httponlyTrue) return response return JsonResponse({code: 1, message: 登录失败})给Cookie设置Token时有几个细节httponlyTrue可以让前端JavaScript读不到这个Cookie降低XSS窃取Token的风险max_age设置了7天过期正好对应考研学生一周回来看一次学习记录的频率。不过我必须说一句如果你的项目是纯Django模板渲染没有明显的前后端分离需求那就老老实实用Django自带的session认证简单可靠论文写起来也顺畅。不要为了炫技而引入不必要的复杂度答辩时老师问你“为什么用Token”如果你答不上来反而扣分。3.2 学习资料管理与文件上传资料管理模块要解决两个问题上传和分类展示。Django的FileField加上模板中的form表单几行代码就能实现上传逻辑。from django.shortcuts import render, redirect from .models import StudyMaterial from .forms import MaterialForm def upload_material(request): if request.method POST: form MaterialForm(request.POST, request.FILES) if form.is_valid(): material form.save(commitFalse) material.uploader request.user material.save() return redirect(material_list) else: form MaterialForm() return render(request, material/upload.html, {form: form})注意form接收request.FILES参数这一步。很多新手在文件上传时遇到“表单提交后没有文件”的问题原因就是创建表单实例时忘了把request.FILES传进去。这是一个非常典型的低级错误但几乎每个Django新手都会踩一次。展示页面要做分类筛选可以用Django的filter查询def material_list(request): category request.GET.get(category, ) materials StudyMaterial.objects.all() if category: materials materials.filter(categorycategory) return render(request, material/list.html, {materials: materials})文件下载有一个需要提醒的点如果直接用FileField的URL字段让人下载下载路径会直接暴露在浏览器地址栏里而且没有权限校验。更规范的做法是写一个下载视图先判断用户是否登录再通过Django的FileResponse输出文件内容。这样既隐藏了真实路径又能做权限控制。毕设里加上这个细节是很大的加分项。3.3 刷题模块与自动判分刷题是考研学习系统的核心功能实现上分为两步取题和判分。取题逻辑可以有几种从题库随机抽取或者按科目顺序出题。我用的是按科目分类、随机抽取的方式保证学生每次刷题面对的题目顺序不同from django.shortcuts import get_object_or_404 import random from .models import Question def get_question(request, question_id): question get_object_or_404(Question, pkquestion_id) return render(request, exam/question.html, {question: question}) def random_question(request, category): questions list(Question.objects.filter(categorycategory)) if not questions: return render(request, exam/empty.html) question random.choice(questions) return render(request, exam/question.html, {question: question})判分逻辑是核心中的核心。学生提交答案后系统要和Question的answer字段比较同时写入ExamRecord表from django.shortcuts import redirect from .models import ExamRecord, Question def submit_answer(request): if request.method POST: question_id request.POST.get(question_id) selected request.POST.get(answer, ).upper() question Question.objects.get(pkquestion_id) is_correct (selected question.answer.upper()) ExamRecord.objects.create( userrequest.user, questionquestion, selected_answerselected, is_correctis_correct ) return render(request, exam/result.html, { question: question, selected: selected, is_correct: is_correct })这里有一个值得注意的业务细节做题记录不要用update_or_create而是每次提交都新插入一条记录。因为一个学生会反复做同一道题第一次做错了第二次做对了两条记录都值得保留。后面的统计模块正是依赖这些历史记录来计算正确率的变化趋势。题目解析这个字段也别浪费。在结果页面把analysis展示出来学生做错后能直接看到解析这比单纯显示“对/错”更有学习价值也让系统在演示时多一个内容点。3.4 学习计划与数据统计学习计划模块最简单就是一个“待办列表”加打卡功能。但这里要讲的是统计模块的价值。统计模块不是花架子它把刷题日志从“散乱的数据”变成了“可读的结论”。我先用聚合查询从ExamRecord里拿到正确率和刷题量from django.db.models import Count, Avg from django.db.models.functions import Coalesce def stats_view(request): user request.user total_count ExamRecord.objects.filter(useruser).count() correct_count ExamRecord.objects.filter(useruser, is_correctTrue).count() accuracy round(correct_count / total_count * 100, 1) if total_count else 0 category_stats ( ExamRecord.objects.filter(useruser) .values(question__category) .annotate(totalCount(id), correctCount(id, filtermodels.Q(is_correctTrue))) ) return render(request, stats/dashboard.html, { total_count: total_count, correct_count: correct_count, accuracy: accuracy, category_stats: category_stats })这里的Count配合filter条件做条件计数是Django ORM一个很实用的小技巧。业务上算“各科目的正确题目数”不需要先取记录再在Python里循环一条查询就搞定。答辩时老师如果问“统计是怎么实现的”这一句就能讲清楚。如果是按天统计打卡数据可以再配合日历插件。我建议在这个模块里把前端图表技术引入一下统计页面加载后用ECharts画一个柱状图或饼图展示各科刷题占比。ECharts只需要引用一个JavaScript文件后端把category_stats输出为JSON前端就能画图。这个功能视觉效果好实现难度又不高是我最推荐加的“亮点功能”。4. 实操过程中的关键细节ORM、Cookie、分页与安全4.1 ORM查询与删除对象时最容易踩的坑Django的ORM太方便了方便到很多新手忘了它也是有限制的。这里集中说我操作中踩过的几个坑。第一个坑是删除对象时数据“删不干净”。Django的delete()操作和级联删除依赖模型定义中的on_delete参数。我在设计StudyMaterial时uploader使用了on_deleteCASCADE那么删除一个用户后他上传的资料也会被级联删除。如果设置成on_deleteSET_NULL则需要外键字段允许为空。这两种选择直接影响数据完整性。有的同学设计外键时随意指定看起来运行没问题但到答辩演示“删除用户”时数据库里残留了一大堆孤儿记录页面或者统计数据就会出现莫名的空值。第二个坑是get()方法的异常处理。用get()查询记录时如果查不到或者查到多条Django会直接抛异常500错误没有商量的余地。在业务逻辑里更安全的写法是record ExamRecord.objects.filter(useruser, questionquestion).first() if record is None: # 处理未找到的情况 else: # 处理找到的情况第三点是性能问题。如果查询外键关联的数据列表页展示用户名和资料名默认N1查询会很明显拖慢页面。可以用select_related优化materials StudyMaterial.objects.select_related(uploader).all()这个优化在毕设数据量下感受不明显但如果老师现场导入了大量测试数据页面打开速度就是直观的体验分。select_related解决的是“外键对象在同一张SQL查询里提前加载”的问题一句话能讲明白是一个很容易展示的技术点。4.2 分页、搜索与列表页优化的基本操作列表页数据如果越来越多比如题库里几百道题一次全渲染页面就会变慢。用Django内置的Paginator可以轻松解决from django.core.paginator import Paginator def question_list(request): questions Question.objects.all() paginator Paginator(questions, 10) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, question/list.html, {page_obj: page_obj})模板里遍历page_obj然后渲染上一页/下一页的链接这个模式在所有列表页都是通用的。分页解决的不只是性能问题还有“用户体验”——学生一屏看到10道题比一次滚动几百道题舒服得多。搜索功能最简单但有效的方式是用icontains做模糊匹配keyword request.GET.get(keyword, ) if keyword: questions questions.filter(stem__icontainskeyword)需要注意icontains在SQLite里会翻译成LIKE在MySQL里会翻译成LIKE都是大小写不敏感的模糊查询。中文搜索完全没问题。如果要搜索多个字段可以用Q对象组合条件from django.db.models import Q questions questions.filter( Q(stem__icontainskeyword) | Q(analysis__icontainskeyword) )这些都是Django在后面帮我们做了安全转义避免了直接拼接SQL注入的问题。讲安全时我经常和同学说你能用ORM就用ORM自己写原生SQL除非非常确定否则很容易出安全问题。Django ORM生成的SQL已经做了参数化处理是最基础的防线。4.3 上传校验、CSRF与模板中的安全细节网络安全在毕设里不是重点考察项但至少要做到几点基础工作。第一CSRF保护。Django默认在模板表单中要求添加{% csrf_token %}如果你在模板渲染的POST表单中漏掉这个标签提交时会得到403。这个不是bug是你正好踩中了Django的防CSRF机制。如果你的某个视图要支持API方式POST又不方便加CSRF Token可以用csrf_exempt装饰器但我不建议无缘无故地去掉这个保护。讲答辩时“我如何保证表单提交的安全性”是一个能拉开档次的问题。第二上传文件校验。如果允许用户上传任意类型文件理论上存在上传恶意脚本的风险。最简单的校验是检查文件扩展名同时限制大小def validate_file_extension(value): import os ext os.path.splitext(value.name)[1] valid_extensions [.pdf, .doc, .docx, .png, .jpg, .jpeg] if not ext.lower() in valid_extensions: raise ValidationError(不支持的文件类型)第三图片上传路径的处理。我见过不少项目上传图片之后不通过MEDIA_URL读取而是直接在前端拼接路径结果页面显示破图。正确做法是模板中通过{{ material.file.url }}动态获取文件地址。Django会结合MEDIA_URL自动生成完整URL这才是在页面正常访问文件的正确姿势。5. 常见问题排查与避坑实录5.1 高频问题速查表我在整理这套源码和做代码讲解的过程中收集了同学反馈最多的问题做成了下面这个快查表问题现象根本原因解决办法登录后刷新就掉线没有正确调用login()只在session里手动存了用户ID使用Django自带的login(request, user)方法中文乱码数据库编码不是utf8/utf8mb4MySQL建库时指定utf8mb4连接串加上charset参数上传图片后无法访问忘了配置MEDIA_ROOT和MEDIA_URLsettings配置media路径主urls里增加static(media)处理表单提交一直403模板里漏了{% csrf_token %}在所有POST表单中添加csrf_token标签时区差8小时settings里TIME_ZONE没设置成Asia/Shanghaisettings配置时区并确认USE_TZ情况migration报错No migrations to applymodels改了但没生成迁移文件执行python manage.py makemigrations后再migrate后台admin没有想要的表admin.py里没有注册模型在对应app的admin.py里用admin.site.register()注册页面CSS样式丢失STATICFILES配置或DEBUG影响静态文件路由检查STATIC_URL与模板加载路径是否一致这些问题的共性是“配置没到位”不是逻辑多难。养成一个习惯创建项目后先花十分钟检查settings.py里的时区、语言、静态文件和media配置后面能省掉大半的坑。5.2 我实际开发中经历的三个最值得分享的调试记录第一个印象深刻的调试经历是刷题统计中“正确率超过100%”的问题。现象是这样的学生做对了一道题统计页面的正确率会跳到100%以上。排查后发现我的统计逻辑是把总记录数和正确记录数分开统计但中途从ExamRecord表里手动删除了几条错误记录导致正确记录数大于总记录数。真实原因是在测试阶段用delete()删数据时只删了部分记录破坏了统计数据的完整性。这个问题的解决思路很有代表性数据统计功能应该始终以原始记录表为准任何测试数据的清理都应该用统一的方式不能靠手工半删不删。后来我在统计视图里加了防御逻辑正确记录数超过总记录数时直接强制让正确率等于100%。上线发现数据异常时的兜底处理这也是一种工程思维。第二个调试经历是Token过期失效的问题。给学生定制前后端分离版本的时候登录状态经常失效排查后发现是Cookie的path参数问题。我在set_cookie时没指定path导致Cookie只在当前路径下生效用户从首页跳转到其他页面时Cookie就“消失”了。解决方法是明确设置path/response.set_cookie(token, token, max_age7*24*3600, httponlyTrue, path/)这个细节很冷门网上大多数教程都没提但实际接项目的时候非常常见。如果你做一个多页面应用Cookie的path必须好好设置。第三个调试记录是文件上传到一半发现文件名重复导致相互覆盖。默认的FileField上传文件时保留原名如果两个同学上传了同名的“真题.docx”后上传的会把前面的覆盖掉。解决办法有两个一种是给upload_to加时间戳子目录一种是重写文件保存方法。最省事的是用uuid作为文件名前缀def upload_to(instance, filename): ext filename.split(.)[-1] return fmaterials/{uuid.uuid4().hex}.{ext}这个问题在单用户测试时发现不了一旦多人使用就会炸属于经典后端问题。能在毕设答辩时主动讲出这个设计比什么问题都等老师问的强。5.3 部署到服务器时的几个小提醒如果毕设要求不是本地演示而是部署到服务器有几个点需要注意。第一个是Debug模式。本地开发设置DEBUGTrue没问题但部署到公网时必须设成False否则报错页面会直接输出详细信息包括数据库连接、绝对路径和部分源代码这在安全上非常危险。同时记得配置ALLOWED_HOSTS为你的域名或IP。第二个是数据库切换。如果从SQLite换到MySQL最常遇到的坑是Migrate时报错。解决办法是在__init__.py里加上import pymysql pymysql.install_as_MySQLdb()这是一行代码解决Django连接MySQL驱动问题的经典做法。第三个是静态文件。DEBUGFalse后Django不再提供静态文件服务需要收集所有静态文件然后交给Nginx处理python manage.py collectstatic这一步很多人会忘结果部署后页面没有CSS样式裸奔的HTML页面让整个项目看起来像半成品。我提醒所有做部署的同学部署前先跑一遍Nginx静态文件配置再开始演示。6. 文档整理、答辩准备与二次开发方向6.1 毕设文档怎么配合源码写我很重视代码讲解和文档的配套因为源码再完整如果文档逻辑不清答辩时依然很容易被问倒。毕设文档一般包括需求分析、系统设计、数据库设计、系统实现、系统测试这五大部分但我觉得很多同学写文档有个通病把代码抄一遍就当设计说明。更好的写法是用文字描述“为什么这么设计”而不是“代码是什么”。比如数据库设计章节除了贴字段还要解释为什么ExamRecord是外键关联两张表而不是单独存用户名和题干。需求分析章节除了说“学生可以刷题”还要说清楚“刷题后系统记录答案、判断对错、更新统计并在个人中心展示正确率变化”。这种因果逻辑是答辩的核心也是论文和普通代码注释的区别。按这个思路我在整理源码时习惯每一份文档都带上一个“设计决策说明”段落把关键设计的考虑写出来。这个习惯还帮自己在答辩时规避了很多“你这里为什么不做成那样”的提问——因为你已经提前说出了选择依据。6.2 二次开发还能加什么亮点如果学有余力我给你三个扩展方向按性价比排序。第一个是错题本功能。把答题错误的题目自动收集到一个错题列表下次刷题只刷错题。这正好就是“考研人”真实需求中的痛点。实现思路非常简单ExamRecord里筛选is_correctFalse再根据question字段去重一个视图加两个模板就能完成。第二个是推荐刷题功能。基于用户薄弱科目推荐更多题目不一定要做多高深的算法只需要统计各科正确率把正确率最低的科目的题目优先推送。这就算是一个“简单的个性化推荐”作为论文创新点很容易讲清楚。第三个是签到提醒与每日一题。可以用Django的定时任务或者简单的Celery实现每天给学生发一封邮件或者站内通知提醒打卡附带一道题目。这个功能适合做成亮点因为涉及了任务调度和消息推送答辩时有故事可讲。我想强调一点扩展功能并非做得越多越好。毕设评分更看重“完成度”和“逻辑自洽性”一个错题本功能从数据模型到页面展示再到统计汇总能形成完整闭环就比硬塞一个不完整的聊天室有价值得多。这套项目从流程图到数据库设计、从页面原型到代码实现整套闭环做下来我自己最大的感受是Django真正适合这种业务系统开发的场景ORM和Admin后台省掉了很多底层功夫让你有余力把业务逻辑和用户体验做细。如果一定要给后来者一个建议我会说拿到任何一套源码先别急着改功能先跑起来用浏览器把所有页面都点一遍然后打开Admin后台把数据增删改查一遍最后再读代码。当你完整经历过这一遍之后再回头看框架的设计很多东西都会突然通。后面如果时间允许我还会继续分享这个系统的前后端分离改造版和部署全流程有需要的话可以先照着本文把基础版本跑通。