13.表单能提交 ≠ 数据是安全的
我第一次把「新增学生」整个流程跑通的时候,其实挺开心的。
但开心不到 5 分钟,我就意识到一个问题:
这个表单,看起来能用,但真的靠谱吗?
点新增 → 弹窗 → 填信息 → 保存 流程是通了,但细节全是坑。
这一节,我们就专门来补这些“新手最容易忽略,但项目一定会出事的地方”。
1.如何显示班级顺序?
当我们点击「新增学生」的时候,班级是一个下拉框,如下图:

这里班级显示的顺序,是如何控制的呢?
Django 默认是按主键 ID 排序的。
你看数据库里的 grades 表:

但是id是自增的,如果先添加“1年6班”, 然后再添加“1年5班”,那么这里显示的顺序将会是:
1年1班
1年2班
1年3班
1年4班
1年6班
1年5班
这将是强迫症患者万万不能忍受的。接下来,我们就来修改它。
2.重写 Form 的初始化方法
很多同学这一步会本能地去改 Model。
但我当时踩过坑以后才意识到:
这是“展示顺序”的问题,不是“数据结构”的问题。
所以,改的位置在 Form,因为页面数据是从Form传递的。
我们现在的目标只有一个:
控制 grade 这个下拉框的数据来源和排序方式。
怎么做?
不需要新概念,只走三步:
1️⃣ 重写 __init__
2️⃣ 拿到 grade 这个字段
3️⃣ 给它一个新的 queryset(查询集)
思路是这样的:
- 表单创建时
- 我提前告诉它:
- 👉 班级数据请按
grade_number排序(可以正序或逆序)
这样 Django 渲染模板时,顺序自然就对了。
在students/forms.py中新增代码:
# 代码位置:students/forms.py
from django import forms
from .models import Student
from grades.models import Grade # 从grades模型中引入
class StudentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
# 继承父类初始化方法
super().__init__(*args, **kwargs)
# 重写`grade`字段的查询集
self.fields.get('grade').queryset = Grade.objects.all()
.order_by('grade_number')
class Meta:
pass
【代码解析】
-
默认情况下,ModelForm 对 ForeignKey 字段会直接使用 Grade.objects.all(), 而数据库记录通常是按照主键 id 排序的(即插入顺序),这通常不是我们想要的显示顺序。
-
代码通过在 init 中重写 queryset,让班级按照 grade_number(年级编号)进行排序, 这样下拉菜单里就会出现比较自然的顺序。
如果需要按照grade_nubmer降序排序,可以使用-grade_number, 代码如下:
self.fields.get('grade').queryset = Grade.objects.all().order_by('-grade_number')
运行效果如下:

3.如何对表单数据校验
表单现在看起来已经很完整了,对吧?
但我想问你一句:
你真的相信用户提交的数据吗?
我以前也天真地以为:
- required 写了
- 浏览器会校验
- 用户就一定会乖乖填
例如,当我们什么都不填写,直接点击“保存”按钮时,效果如下:

前端校验,可以被 1 秒钟绕过
但是当你打开了「检查元素」,找到现在这个表单里:
<input required>
看起来好像挺安全。
但我只需要:
1️⃣ 右键 → 检查
2️⃣ 把 required 删掉
3️⃣ 再点保存
👉 字段直接空着就提交成功了。
所以这里一定要记住一句话:
永远不要相信前端提交的数据。
4.真正靠谱的校验,只能在后端做
而且 Django 已经给你准备好了“正确姿势”。
表单字段校验的规则只有一个:
验证哪个字段,就写
clean_字段名
form表单类验证的完整代码如下:
from django import forms
from django.core.exceptions import ValidationError
from .models import Student
from grades.models import Grade
import datetime
class StudentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields.get('grade').queryset = Grade.objects.all().order_by('-grade_number')
def clean_student_name(self):
student_name = self.cleaned_data.get('student_name')
if len(student_name) < 2 or len(student_name) > 50:
raise ValidationError('请填写正确的学生姓名')
return student_name
def clean_student_number(self):
student_number = self.cleaned_data.get('student_number')
if len(student_number) != 19:
raise ValidationError('学号长度应为19位。')
return student_number
def clean_birthday(self):
birthday = self.cleaned_data.get('birthday')
if not isinstance(birthday, datetime.date):
raise ValidationError('生日格式错误,正确格式例如:2020-05-01')
if birthday > datetime.date.today():
raise ValidationError('生日应该在今天之后。')
return birthday
def clean_contact_number(self):
contact_number = self.cleaned_data.get('contact_number')
if len(contact_number) != 11:
raise ValidationError('联系电话应为11位。')
return contact_number
class Meta:
model = Student
# fields = '__all__'
fields = ['student_name', 'student_number', 'grade', 'gender', 'birthday', 'contact_number', 'address']
① 学生姓名(student_name)
这是最典型的一个。
我的规则很简单:
- 至少 2 个字
- 不超过 50 个字
路径也很固定:
1️⃣ 从 cleaned_data 里拿数据
2️⃣ 判断长度
3️⃣ 不合法就抛异常
抛什么?
👉 ValidationError
Django 会自动帮你把错误和字段绑定起来。
代码如下:
# 代码位置:students/forms.py
class StudentForm(forms.ModelForm):
def clean_student_name(self):
student_name = self.cleaned_data.get('student_name')
if len(student_name) < 2 or len(student_name) > 50:
raise ValidationError('请填写正确的学生姓名')
return student_name
② 学号(student_number)
学号是最适合做“严格校验”的字段。
规则很清晰:
- 必须是 19 位
- 少一位、多一位都不行
只要长度不等于 19:
直接抛异常,告诉用户哪里错了。
代码如下:
# 代码位置:students/forms.py
class StudentForm(forms.ModelForm):
def clean_student_number(self):
student_number = self.cleaned_data.get('student_number')
if len(student_number) != 19:
raise ValidationError('学号长度应为19位。')
return student_number
③ 出生日期(birthday)
这个字段,坑也很多。
你至少要校验两件事:
1️⃣ 它是不是一个日期 2️⃣ 它是不是比今天还大
如果一个人的生日在未来,那肯定不对,直接抛出异常
代码如下:
# 代码位置:students/forms.py
class StudentForm(forms.ModelForm):
def clean_birthday(self):
birthday = self.cleaned_data.get('birthday')
if not isinstance(birthday, datetime.date):
raise ValidationError('生日格式错误,正确格式例如:2020-05-01')
if birthday > datetime.date.today():
raise ValidationError('生日应该在今天之后。')
return birthday
④ 手机号(可选)
手机号规则你已经很熟了:
- 固定 11 位
这类校验非常适合放在 Form 里,而不是 JS 里。
代码如下:
# 代码位置:students/forms.py
class StudentForm(forms.ModelForm):
def clean_contact_number(self):
contact_number = self.cleaned_data.get('contact_number')
if len(contact_number) != 11:
raise ValidationError('联系电话应为11位。')
return contact_number
5.测试验证效果
在添加学生信息页面,姓名只填写一个字符,打开浏览器审查元素,查看console,点击保存按钮,效果如下:

服务器返回的完整内容如下:
{
"status": "error",
"messages": "{\"student_name\": [{\"message\": \"\\u8bf7\\u586b\\u5199\\u6b63\\u786e\\u7684\\u5b66\\u751f\\u59d3\\u540d\", \"code\": \"\"}], \"student_number\": [{\"message\": \"\\u5b66\\u53f7\\u957f\\u5ea6\\u5e94\\u4e3a19\\u4f4d\\u3002\", \"code\": \"\"}], \"birthday\": [{\"message\": \"\\u8f93\\u5165\\u4e00\\u4e2a\\u6709\\u6548\\u7684\\u65e5\\u671f\\u3002\", \"code\": \"invalid\"}], \"contact_number\": [{\"message\": \"\\u8054\\u7cfb\\u7535\\u8bdd\\u5e94\\u4e3a11\\u4f4d\\u3002\", \"code\": \"\"}]}"
}
**说明:**在返回的JSON信息中,status 的值是error,messages是具体的错误信息。
把 messages 字段的内容解码/解析后,实际错误信息是:
{
"student_name": [
{
"message": "请填写正确的学生姓名",
"code": ""
}
],
"student_number": [
{
"message": "学号长度应为19位。",
"code": ""
}
],
"birthday": [
{
"message": "输入一个有效的日期。",
"code": "invalid"
}
],
"contact_number": [
{
"message": "联系电话应为11位。",
"code": ""
}
]
}
本章小结
走到这里,你其实已经完成了一次认知升级:
- 前端提交的数据不可信
- 一定要在后端做验证
而且你现在写的这些校验代码:
- 以后编辑学生还能复用
- API 接口也能复用
- 前端怎么变都不怕
下一步你可以立刻做的事
趁热,强烈建议你现在马上做一件事:
👉 故意填错数据
比如:
- 名字写 1 个字
- 学号写 18 位
- 生日写成未来时间
看看 Django 是怎么把错误一步一步传回前端的。
下一节,我们就顺着这个点继续:
后端校验失败,SweetAlert 怎么优雅地把错误展示出来?
这一步一打通,你的「新增学生」功能就真的“专业级完成”了。
【大熊课堂精品课程】
Python零基础入门动画课: https://www.bilibili.com/cheese/play/ss7988
Django+Vue:全栈开发: https://www.bilibili.com/cheese/play/ss8134
PyQT6开发桌面软件: https://www.bilibili.com/cheese/play/ss12314
Python办公自动化: https://www.bilibili.com/cheese/play/ss14990
Cursor AI编程+MCP:零基础实战项目课: https://www.bilibili.com/cheese/play/ss105194189
Pandas数据分析实战: https://www.bilibili.com/cheese/play/ss734522035
AI大模型+Python小白应用实战: https://www.bilibili.com/cheese/play/ss3844