Skip to main content

13.表单能提交 ≠ 数据是安全的

我第一次把「新增学生」整个流程跑通的时候,其实挺开心的。

但开心不到 5 分钟,我就意识到一个问题:

这个表单,看起来能用,但真的靠谱吗?

点新增 → 弹窗 → 填信息 → 保存 流程是通了,但细节全是坑。

这一节,我们就专门来补这些“新手最容易忽略,但项目一定会出事的地方”

1.如何显示班级顺序?

当我们点击「新增学生」的时候,班级是一个下拉框,如下图:

image-20260106085432104

这里班级显示的顺序,是如何控制的呢?

Django 默认是按主键 ID 排序的。

你看数据库里的 grades 表:

image-20260106085615258

但是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

【代码解析】

  1. 默认情况下,ModelForm 对 ForeignKey 字段会直接使用 Grade.objects.all(), 而数据库记录通常是按照主键 id 排序的(即插入顺序),这通常不是我们想要的显示顺序。

  2. 代码通过在 init 中重写 queryset,让班级按照 grade_number(年级编号)进行排序, 这样下拉菜单里就会出现比较自然的顺序。

如果需要按照grade_nubmer降序排序,可以使用-grade_number, 代码如下:

self.fields.get('grade').queryset = Grade.objects.all().order_by('-grade_number')

运行效果如下: image-20260106091724528

3.如何对表单数据校验

表单现在看起来已经很完整了,对吧?

但我想问你一句:

你真的相信用户提交的数据吗?

我以前也天真地以为:

  • required 写了
  • 浏览器会校验
  • 用户就一定会乖乖填

例如,当我们什么都不填写,直接点击“保存”按钮时,效果如下:

image-20260106091924851

前端校验,可以被 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,点击保存按钮,效果如下:

image-20260106094817726

服务器返回的完整内容如下:

{
"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 的值是errormessages是具体的错误信息。

把 messages 字段的内容解码/解析后,实际错误信息是:

{
"student_name": [
{
"message": "请填写正确的学生姓名",
"code": ""
}
],
"student_number": [
{
"message": "学号长度应为19位。",
"code": ""
}
],
"birthday": [
{
"message": "输入一个有效的日期。",
"code": "invalid"
}
],
"contact_number": [
{
"message": "联系电话应为11位。",
"code": ""
}
]
}

本章小结

走到这里,你其实已经完成了一次认知升级

  • 前端提交的数据不可信
  • 一定要在后端做验证

而且你现在写的这些校验代码:

  • 以后编辑学生还能复用
  • API 接口也能复用
  • 前端怎么变都不怕

下一步你可以立刻做的事

趁热,强烈建议你现在马上做一件事:

👉 故意填错数据

比如:

  • 名字写 1 个字
  • 学号写 18 位
  • 生日写成未来时间

看看 Django 是怎么把错误一步一步传回前端的。

下一节,我们就顺着这个点继续:

后端校验失败,SweetAlert 怎么优雅地把错误展示出来?

这一步一打通,你的「新增学生」功能就真的“专业级完成”了。