☰
基于Python+Django的错题管理系统:从数据库设计到部署实战
2026/10/1 11:52:36 网站建设 项目流程

学生时代最让人头疼的,不是没题做,而是做错的题下次还错。纸质错题本抄一页要五分钟,到了期末想按科目翻、按错因翻、统计一下哪类错误最多,基本等于不可能。后来我把这套东西做成一个基于Python + Django的错题管理系统,带源码、带数据库、带文档,既能自己日常用,也能直接作为课程设计或毕业设计的完整项目。

这个项目适合两类人。一类是计算机专业的在校生,需要交课设、毕设,手头缺一个"功能完整、能演示、有文档有数据库"的项目;另一类是真的想给自己搭一个电子化错题本、但不知道从哪下手的同学。系统本身不复杂,但它把Django最常用的一套东西都串起来了:ORM建模、视图函数、模板渲染、表单校验、图片上传、后台管理、数据库迁移。把这一套吃透,Django实战算是真正入了门。

下面我把这套系统从需求拆解到数据库设计,再到核心代码和部署步骤,完整展开讲一遍。

1. 为什么是"学渣工具"却值得认真做:需求拆解与技术选型

1.1 错题管理的真实痛点

先聊一个反直觉的事:错题管理这个需求,看着小,实际上特别适合做成Web系统。我上大学时自己抄过错题本,抄了半学期就放弃了,原因很实际——纸质本子没法检索。物理错题和英语语法混在一起,想复习哪一块得从头翻;想统计"我到底因为粗心丢了多少分",更是无从谈起。

用Excel也不是不行,但Excel的体验足够劝退:手机上不方便录、图片粘贴进去乱糟糟、多人共享更麻烦。而Web系统天然解决这些问题——有表单可以规范录入,有数据库可以任意筛选,有模板可以展示成卡片式复习界面,有后台可以直接管理数据。

这个项目的第一版核心需求,我当时只定了四条:

  • 用户可以录入错题,包括题目内容、正确答案、错误答案、错因分类、所属科目,以及题目截图;
  • 可以按科目筛选、按错因筛选、按关键词搜索;
  • 可以统计每个科目的错题数量,以及按错因类型的分布;
  • 提供一个"随机复习"功能,每次抽一道题出来,模拟抽卡式刷错题。

看清楚,这个系统最核心的价值不是"记录",是"复习"。录入只是手段,把错题重新翻出来做对,才是目的。所以后面数据库设计时,专门留了复习次数字段和最近复习时间字段,就为了支撑这个核心逻辑。

1.2 Django,而不是Flask或Spring

技术选型上,我没有纠结太久就选了Django,而且如果你是新手上路,我也建议选它。原因有三个。

第一,Django自带的东西足够全。用户认证、后台管理、ORM、表单处理、模板引擎全部内置,不需要像Flask那样自己去拼第三方库。对于一个课设级别的管理系统,Django的开箱即用体验是最好的,省下来的时间足够你多写几个功能页面。

第二,ORM让数据库操作变得极其友好。你不需要手写SQL,定义好模型类,迁移命令一跑,表就建好了。对很多刚学完Python基础、数据库经验比较少的学生来说,这一步能大幅降低挫败感。比如后面讲到的按科目筛选错题,一句filter(subject_id=1)就够了。

第三,Django写出来的项目结构非常规整,适合作为教学和答辩演示。MTV模式分层清晰,models管数据、views管逻辑、templates管展示,评委老师一看就知道你掌握了Web开发的规范流程。相比之下,Java Spring那套配置对新手来说太重了,一个Hello World都可能卡你半天环境。

1.3 第一版需要做什么,不需要做什么

我得先给大家划个边界,避免一上来就想做"全平台错题管理App"。第一版系统,我的建议是只做单用户核心流程,也就是:

  • 注册/登录;
  • 错题的增删改查;
  • 按科目和错因的筛选;
  • 一个随机抽题复习页;
  • Django自带Admin后台做数据管理。

不需要做的东西,包括但不限于:多班级/多学校机构、成绩分析图表、微信小程序端、批量导入、PDF导出、定时推送提醒。这些不是不好,而是会严重拖慢你的进度。做完第一版跑通以后,再挑一两个扩展方向加进去,效果反而更好。这个项目里,这部分边界想清楚了,后面的开发速度会快很多。

2. 数据库设计:五张表把"错题"变成"数据"

2.1 数据模型拆解

数据库设计是整个系统的地基,也是文档里最先要写清楚的部分。我这套系统核心就四张业务表加一张Django自带的用户表,先看整体关系:

表名作用关键字段
auth_user用户(Django内置)username, password
subject科目表name
wrong_question错题主表subject外键, title, answer, wrong_type, image, review_count
tag标签表(可扩展)name
wrong_question_tags错题-标签多对多中间表wrong_question外键, tag外键

这里有个设计心得:科目千万不要直接存在错题表里当一个字符串字段。很多第一次做这个项目的人会图省事,给WrongQuestion加一个subject_name = models.CharField(),等做到"按科目统计错题数量"的时候就开始难受了,因为字符串没法规范归类,"数学"和"数学 "是两个值。独立一张Subject表,后续加科目、改科目名、统计,全都方便。

错题主表是整个系统的核心,字段设计要覆盖完整的学习场景。题目内容用TextField而不是CharField,因为理科题目可能很长;题目图片用ImageField,这是Django处理文件上传的标准方式;错因类型用choices选项,限制在几个固定值里,方便后面做统计。复习次数和最近复习时间这两个字段,是给复习逻辑用的,这个后面会细讲。

2.2 models.py代码与字段选择逻辑

直接上代码,这是我实际项目里的模型定义简化版:

from django.db import models from django.contrib.auth.models import User class Subject(models.Model): name = models.CharField('科目名称', max_length=50) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: verbose_name = '科目' verbose_name_plural = '科目' ordering = ['name'] def __str__(self): return self.name class WrongQuestion(models.Model): WRONG_TYPE_CHOICES = ( ('concept', '概念不清'), ('careless', '粗心大意'), ('method', '思路错误'), ('other', '其他'), ) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') subject = models.ForeignKey(Subject, on_delete=models.PROTECT, verbose_name='科目') title = models.TextField('题目内容') wrong_answer = models.TextField('当时的错误答案', blank=True) answer = models.TextField('正确答案与解析', blank=True) wrong_type = models.CharField('错因类型', max_length=20, choices=WRONG_TYPE_CHOICES, default='concept') source = models.CharField('题目来源', max_length=100, blank=True) image = models.ImageField('题目图片', upload_to='questions/', blank=True, null=True) created_at = models.DateTimeField('录入时间', auto_now_add=True) last_reviewed_at = models.DateTimeField('最近复习时间', null=True, blank=True) review_count = models.IntegerField('复习次数', default=0) class Meta: verbose_name = '错题' verbose_name_plural = '错题' ordering = ['-created_at'] def __str__(self): return self.title[:30]

有几个设计细节值得展开说。

user外键用的是Django内置的User模型,on_delete=models.CASCADE,意思是用户被删掉时,他的错题也一起删除。这里我建议用CASCADE,因为个人错题数据跟着账号走,账号没了保留无主数据没有意义。

subject外键我用的是on_delete=models.PROTECT,这个和CASCADE相反,作用是:如果该科目下还有错题,则禁止删除科目。为什么?因为误删一个科目会连带所有错题消失,这对用户来说是灾难性操作。PROTECT会拦截这种操作,提示你先清理错题再删科目。这个细节在课设答辩时很容易被问到,值得留意。

upload_to='questions/'是指图片上传后存放在MEDIA_ROOT/questions/目录下。ImageField依赖Pillow库,如果之后跑项目报ModuleNotFoundError: No module named 'PIL',就是因为没装它。

2.3 复习机制的数据层设计

"随机复习"这个功能看起来简单,但要在数据层支撑它,就需要刚才那三个字段:last_reviewed_at、review_count、created_at。

最朴素的抽题逻辑是WrongQuestion.objects.order_by('?').first(),也就是数据库随机排序取一条。这个思路没问题,问题是完全没有"复习"概念——可能连续五天抽到同一道题,而别的错题一次都没翻过牌子。

稍微改进一点的做法,是按优先级抽题。思路很简单:

  • 从来没复习过的题,优先级最高;
  • 距离上次复习时间越久,优先级越高;
  • 复习次数越少,优先级越高。

实现上不需要复杂的机器学习算法,用Django的ORM筛选就可以:

from django.utils import timezone from datetime import timedelta from .models import WrongQuestion def get_review_question(user): # 超过3天未复习,或者从未复习过的题 expired_time = timezone.now() - timedelta(days=3) pool = WrongQuestion.objects.filter(user=user).filter( models.Q(last_reviewed_at__isnull=True) | models.Q(last_reviewed_at__lt=expired_time) ) if not pool.exists(): pool = WrongQuestion.objects.filter(user=user) return pool.order_by('last_reviewed_at', 'review_count').first()

配合每次复习后执行review_count += 1并更新last_reviewed_at,这套个人用的复习系统已经比纸质错题本科学多了。如果以后想升级成艾宾浩斯间隔重复,只需要再加一个"下次复习时间"字段,定期筛选next_review_date <= now的题目即可。数据层的扩展性在设计阶段就留好了。

3. 核心功能实现:从空白模板到真正能用的系统

3.1 项目初始化与App划分

搭建一个Django项目的标准命令,我直接列出来:

# 创建项目错题管理项目,命名为 wrongbook_project django-admin startproject wrongbook_project # 进入项目目录 cd wrongbook_project # 创建错题app python manage.py startapp wrong

App划分是个见仁见智的事。这套系统的特点是业务单一,所以我把所有业务代码都放在一个wrongapp里,里面包含模型、视图、表单、URL。如果你的需求里还有"班级管理""成绩报表"这样独立的业务模块,那再单独拆app也不迟,不要为了结构复杂而拆。

创建好app之后,要把它注册进settings.py的INSTALLED_APPS:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'wrong', # 这里 ]

同时把语言和时区改成本地,不然后台显示英文,录入时间也会比北京时间慢8小时:

LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

这里提醒一下,USE_TZ保持True且TIME_ZONE设为Asia/Shanghai是正确的组合。Django会以UTC存储时间,显示时自动转换成北京时间。千万不要图省事把USE_TZ改成False,在复杂查询里会有各种时区坑。

3.2 错题增删改查与筛选检索的实现

项目跑起来的第一步,我先在wrong/views.py里写列表页视图。一个合格的错题列表页要支持三种筛选:按科目下拉筛选、按错因类型筛选、按关键词搜索。直接看代码:

from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.db.models import Q, Count from .models import WrongQuestion, Subject from .forms import WrongQuestionForm @login_required def question_list(request): questions = WrongQuestion.objects.filter(user=request.user) subject_id = request.GET.get('subject') wrong_type = request.GET.get('type') keyword = request.GET.get('q') if subject_id: questions = questions.filter(subject_id=subject_id) if wrong_type: questions = questions.filter(wrong_type=wrong_type) if keyword: questions = questions.filter( Q(title__icontains=keyword) | Q(source__icontains=keyword) ) # 统计信息,用于页面顶部展示 total = questions.count() subject_stats = ( questions.values('subject__name') .annotate(count=Count('id')) .order_by('-count') ) subjects = Subject.objects.all() context = { 'questions': questions, 'subjects': subjects, 'total': total, 'subject_stats': subject_stats, } return render(request, 'wrong/list.html', context)

注意几个关键点。questions这个QuerySet是惰性的,一层层filter不会真的执行SQL,直到模板里遍历时才查询,所以连续三个if筛选不会造成性能浪费。Q对象组合多条件搜索是Django官方推荐的做法,title__icontains对英文大小写不敏感,中文也没有问题。

新增和编辑错题,用Django的ModelForm最省事,直接在wrong/forms.py里定义:

from django import forms from .models import WrongQuestion class WrongQuestionForm(forms.ModelForm): class Meta: model = WrongQuestion fields = ['subject', 'title', 'wrong_answer', 'answer', 'wrong_type', 'source', 'image'] widgets = { 'title': forms.Textarea(attrs={'rows': 3, 'placeholder': '粘贴题目内容'}), 'answer': forms.Textarea(attrs={'rows': 3}), 'wrong_answer': forms.Textarea(attrs={'rows': 2}), }

然后在视图里:

@login_required def question_create(request): if request.method == 'POST': form = WrongQuestionForm(request.POST, request.FILES) if form.is_valid(): question = form.save(commit=False) question.user = request.user question.save() return redirect('wrong:list') else: form = WrongQuestionForm() return render(request, 'wrong/form.html', {'form': form})

这里commit=False是个非常关键的小细节。表单里的字段并不包含user,但错题必须归属到当前登录用户,所以先不提交数据库,把user字段赋值后再save()。不这样做,新增错题就会报NOT NULL constraint failed。

3.3 图片上传、模板渲染与URL配置

图片上传这一块,新手踩坑概率极高。最大的坑有三个:忘记enctype、忘记配MEDIA、忘记在URL里挂媒体路径。

模板里,凡是包含文件上传的表单,<form>标签必须写:

<form method="post" enctype="multipart/form-data">

没有这个属性,浏览器只会提交文件名,Django的request.FILES永远是空的,表单验证还不会报错,你会看到图片栏一片空白却找不到原因。

settings.py里要做两处配置:

MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

然后在项目的urls.py里加上:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

MEDIA_ROOT是文件实际存储的绝对路径,MEDIA_URL是浏览器访问图片时的URL前缀。两者缺一不可,MEDIA_ROOT没配文件存不下来,MEDIA_URL没配图片显示不出来。

列表页要把题号、科目、错因、录入时间展示清楚,并给每一行加编辑/删除按钮。这里别忘了在模板顶部加Django模板标签:

{% load static %}

静态文件也一样,在settings.py中指定静态文件搜索路径:

STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

URL路由我习惯用app_name+path命名的方式,模板里用{% url %}反向解析,这样以后改URL路径不会牵一发而动全身:

app_name = 'wrong' urlpatterns = [ path('', views.question_list, name='list'), path('create/', views.question_create, name='create'), path('<int:pk>/edit/', views.question_edit, name='edit'), path('<int:pk>/delete/', views.question_delete, name='delete'), path('review/', views.review_random, name='review'), path('stats/', views.question_stats, name='stats'), ]

3.4 一个容易被忽略的细节:删除对象

Django里执行查询和删除对象,最简单的方式是Question.objects.get(pk=pk).delete()。但这个操作在Web环境下必须处理两个问题:归属校验和请求方法限制。

归属校验是为了防止用户A登录后,猜/wrong/3/delete/这个URL把用户B的错题删掉。这属于典型的越权漏洞,也是最容易被答辩老师问到的安全问题。所以删除视图里必须加一层user=request.user过滤:

from django.http import HttpResponseForbidden @login_required def question_delete(request, pk): question = get_object_or_404(WrongQuestion, pk=pk, user=request.user) if request.method == 'POST': question.delete() return redirect('wrong:list') # 非POST请求直接返回禁止访问,或者展示确认页 if request.method == 'GET': return render(request, 'wrong/confirm_delete.html', {'question': question}) return HttpResponseForbidden()

这里有两个细节。第一,用get_object_or_404加上user=request.user的过滤条件,查询不到就直接404,连判断都不需要写。第二,删除操作不要用GET提交,否则搜索引擎爬虫或预加载链接都可能误删数据。标准做法是列表页用一个小表单提交POST请求,或者在GET时先渲染确认页,等用户点"确认删除"后再POST。

顺带说一句on_delete的行为。前面模型里定的是:用户被删时级联删除他的错题;科目被删时如果有错题引用就禁止删除。这两条规则定义了数据完整性的边界,实际使用中非常合理。你在文档里把这几条on_delete策略写清楚,阅卷老师会认为你考虑问题比较全面。

4. 部署运行:拿到源码后让系统跑起来的完整步骤

4.1 环境准备与安装依赖

我假设你本地已经装了Python 3.8以上版本。如果还没有,需要先去官网下载安装包,安装时勾选"Add Python to PATH"。装好之后,在命令行验证:

python --version

接下来是虚拟环境。这不是可选项,我强烈建议从第一步就养成用虚拟环境的习惯。虚拟环境的作用是把项目依赖和系统全局Python环境隔离,避免不同项目之间的包版本冲突,也能保证你在文档里写清楚依赖清单。

# 在项目根目录创建虚拟环境 python -m venv venv # Windows下激活 venv\Scripts\activate # macOS / Linux 下激活 source venv/bin/activate

激活后命令行前面会多出(venv)字样。然后安装依赖:

pip install -r requirements.txt

requirements.txt是项目中必须附带的文件,内容大致如下:

Django>=3.2,<5.0 Pillow

Django 3.2是LTS版本,对新手最友好,4.x和5.x也能用,功能差异不大。Pillow是ImageField的底层依赖,没它图片功能直接瘫痪。

4.2 数据库迁移与初始化

这套系统默认用的是SQLite,也就是Django开箱即用的内置数据库。SQLite的优点是不需要额外安装数据库服务,数据库就是一个文件,特别适合单用户或者小规模课设场景。项目根目录下会生成一个db.sqlite3文件,这就是你的整个数据库。

在跑服务器之前,必须先把模型同步到数据库:

python manage.py makemigrations python manage.py migrate

makemigrations是根据你写的models生成迁移文件,migrate是把迁移文件真正应用到数据库。如果提示No changes detected,说明模型没有变化;如果报字段名错误,说明代码里有拼写问题,要回去检查。

接着创建管理员账号:

python manage.py createsuperuser

按提示输入用户名、邮箱(可留空)、密码。这个账号属于超级管理员,登录http://127.0.0.1:8000/admin/后可以直接在后台增删改查错题数据,不用走页面表单。

启动开发服务器:

python manage.py runserver

浏览器访问http://127.0.0.1:8000/就能看到系统的登录页了。开发服务器的默认端口是8000,如果用课程设计的演示环境,保持默认即可。

4.3 启动调试与常见报错

我把自己跑这个项目时遇到的几个高频报错整理成一张表,方便大家对号入座:

报错信息原因解决方案
ModuleNotFoundError: No module named 'PIL'未安装Pillowpip install Pillow
OperationalError: no such table: wrong_wrongquestion没有执行migrate执行python manage.py migrate
TemplateDoesNotExist模板文件路径不对检查templates目录和render参数
AttributeError: 'WrongQuestion' object has no attribute '...'模板中引用了不存在的字段对照models.py核对字段名
DisallowedHost访问的域名不在ALLOWED_HOSTSsettings中加ALLOWED_HOSTS = ['*']

其中一个特别容易踩的坑是ALLOWED_HOSTS。默认情况下Django只允许localhost和127.0.0.1访问,如果同学想用局域网IP访问你的系统做演示,就需要在settings.py里改成:

ALLOWED_HOSTS = ['*']

不要在生产环境这样干,但课设演示场景下这是最快的方式。

4.4 切换到MySQL的进阶配置

SQLite用于开发和演示足够了,但如果你想在项目里体现"我懂MySQL",或者数据量确实大到几千上万条,那可以切换数据库。前提是你本地或服务器上已经装好了MySQL服务,并且用CREATE DATABASE wrongbook DEFAULT CHARACTER SET utf8mb4;创建了数据库。字符集务必要指定,否则中文会乱码。

然后修改settings.py:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'wrongbook', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', } }

同时安装MySQL驱动:

pip install mysqlclient

装完以后再执行一次python manage.py migrate,所有表就会迁移到MySQL中。切换数据库时注意:以前存在SQLite里的数据不会自动搬过去,需要的话得先导出再导入。

关于"数据库连接池"这个词,很多文章会提到。实际在Django里,连接池属于部署优化层面的东西,课设阶段不需要纠结。你只要知道默认配置下每次数据库操作都新建连接,生产环境下多线程高并发时会有性能损耗,到时候加一个连接池配置或使用代理中间件即可。这些可以作为文档中的"扩展知识"来写。

5. 从源码到二次开发:读懂这套系统的正确姿势

5.1 源码目录结构怎么读

这套系统的源码文件结构,基本上是这样的:

wrongbook_project/ ├── manage.py # 项目管理入口 ├── db.sqlite3 # SQLite数据库文件 ├── requirements.txt # 依赖清单 ├── wrongbook_project/ # 项目配置目录 │ ├── settings.py # 全局配置 │ ├── urls.py # 根路由 │ └── wsgi.py ├── wrong/ # 错题业务app │ ├── models.py # 数据模型 │ ├── views.py # 视图函数 │ ├── forms.py # 表单定义 │ ├── urls.py # 业务路由 │ ├── admin.py # 后台管理注册 │ └── templates/ │ └── wrong/ # 模板文件 │ ├── list.html │ ├── form.html │ └── review.html └── media/ └── questions/ # 上传的错题图片

拿到源码以后,我建议大家不要急着运行,先花20分钟按照"配置 -> 模型 -> 路由 -> 视图 -> 模板"的顺序读一遍。Django的请求流程非常简单清晰:浏览器输入URL,路由匹配到视图函数,视图操作模型数据,再把数据渲染到模板返回。把这条链路走通一次,整个项目就理解得八九不离十了。

5.2 值得扩展的三个方向

第一版做完了,一定有精力继续扩展。我个人觉得下面三个方向性价比最高。

方向一:给错题加标签和知识点关联。一道题可能同时属于"数列"和"函数单调性"两个知识点,多对多关系在Django里实现起来不难,加一个Tag模型和ManyToManyField就行。这样复习时就能按知识点精准筛选,比单纯按科目更有效。

方向二:错题统计可视化。列表页的数据统计目前只是数字,如果引入ECharts或者Chart.js,把"各科目错题数量""错因类型分布"画成饼图和柱状图,整个系统演示效果会瞬间上一个档次。Django视图里只需要把统计数据以JSON格式返回,前端图表库负责渲染。

方向三:对接WebSocket做实时提醒。Django 3.0以后支持ASGI,可以用channels库实现WebSocket。比如用户打开复习页后,后端有新的待复习错题时,前端实时推送到页面上。这个属于进阶玩法,能体现你的技术广度,但需要额外学习通道层的概念,建议排在前面两个方向之后再做。

5.3 实操中踩过的坑与经验

最后分享几个我实际开发中真正踩过的坑,这些东西在官方文档里你是找不到的。

第一个坑:Admin后台注册了但页面空白。原因是没有在admin.py里注册模型。很多人不知道Django的Admin后台并不会自动识别你写的模型,需要手动加:

from django.contrib import admin from .models import Subject, WrongQuestion @admin.register(Subject) class SubjectAdmin(admin.ModelAdmin): list_display = ['name', 'created_at'] @admin.register(WrongQuestion) class WrongQuestionAdmin(admin.ModelAdmin): list_display = ['title', 'subject', 'wrong_type', 'created_at'] list_filter = ['subject', 'wrong_type'] search_fields = ['title']

list_display和list_filter是后台列表页体验的核心,配好以后在后台筛选错题会非常顺手。课设答辩演示Admin后台也是个加分项。

第二个坑:模板里图片显示不出来。排查顺序是:先看浏览器URL是不是/media/questions/xxx.png,如果是但打不开,检查MEDIA_ROOT配置;如果能打开但列表页不显示,检查模板里用的是不是{{ question.image.url }}而不是{{ question.image }}。这两个字段一个输出完整URL,一个输出文件路径字符串,用错是最常见的问题。

第三个坑:makemigrations后数据库结构变了,但旧数据对不上。做二次开发时如果改了模型的字段,比如加了source字段,要让老数据平滑过渡,迁移文件里需要提供default值,或者允许字段为blank=True。否则迁移会问你Field 'source' doesn't have a default value,然后卡在那里。

第四个坑:表单校验失败时页面没有错误提示。这一般是模板里没渲染表单的错误信息。Django的Form会把错误放在form.errors里,模板中需要手动展示:

{% if form.errors %} <div class="alert alert-danger"> {{ form.errors }} </div> {% endif %}

不然用户填错了什么都看不到,只能干瞪眼。

做完这个项目,我自己最大的体会是:错题管理系统虽然小,但它把"需求分析、数据库建模、后端逻辑、前端展示、部署运行"整个软件开发的链路完整走了一遍。拿到源码的同学,不要只盯着代码抄,先把第二章的数据库设计看懂,再去改第三章的功能,最后扩展第五章的方向。能独立把这些流程走通,你收获的就不只是一份课设,而是一个能自己造轮子的能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询