11 KiB
学生模块答辩演示步骤
演示目标:通过 Swagger 依次展示学生新增、参数校验、分页查询、条件查询、详情查询、局部更新和逻辑删除。
一、答辩前检查
1. 确认数据库和基础数据
- 确认 MySQL 已启动。
- 确认项目配置的数据库可以连接。
- 确认班级表中至少存在一条未删除的班级记录。
- 记住一个真实存在的班级 ID。下文示例使用
class_id = 1,如果数据库中不存在班级 1,请替换成实际班级 ID。
consultant_id 在演示数据中使用 null,避免因为顾问外键不存在导致新增失败。
2. 确认当前接口方法
当前代码中的学生接口如下:
| 功能 | 请求方法 | 地址 |
|---|---|---|
| 新增学生 | PUT |
/students |
| 更新学生 | POST |
/students/{id} |
| 分页查询 | GET |
/students |
| 详情查询 | GET |
/students/{id} |
| 逻辑删除 | DELETE |
/students/{id} |
注意:常见 REST 规范通常使用 POST 新增、PUT 更新,但当前项目代码正好相反。答辩前如果修改了请求方法,需要同步调整本文档中的操作。
3. 启动项目
在项目根目录执行:
python main.py
看到服务正常启动后,在浏览器打开:
http://127.0.0.1:23333/docs
在 Swagger 页面找到“学生接口”。
二、正式演示流程
建议严格按照下面的顺序演示。每次操作时先展开接口,点击 Try it out,填写参数或请求体,再点击 Execute。
步骤 1:演示新增参数校验
展开:
PUT /students
先输入一条性别不合法的数据:
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "未知"
}
预期结果:HTTP 状态码为 422,提示 gender 只能是“男”或“女”。
讲解词:
请求进入接口之前会先经过 Pydantic 请求模型校验。性别字段使用 Literal 约束,只允许男或女,非法数据不会进入 DAO 层,也不会写入数据库。
步骤 2:演示日期校验
仍然使用新增接口,将毕业日期设置为早于入学日期:
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2024-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
预期结果:HTTP 状态码为 422,提示“毕业日期不能早于入学日期”。
讲解词:
除了单字段类型和范围校验,我还使用模型级校验器校验两个日期之间的业务关系。
步骤 3:新增一名合法学生
继续使用:
PUT /students
输入以下合法数据:
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
这里故意不传 student_no。DAO 在插入并取得自增主键后,会自动生成类似 STU0001 的学生编号。
预期响应:
{
"code": 200,
"detail": "添加学生成功"
}
讲解词:
新增请求先由 StudentCreate 完成字段校验,再通过 model_dump 转成字典。DAO 创建 ORM 实例并执行 flush 获取自增 ID,如果没有手动传入学号,就根据 ID 自动生成学号,最后提交事务。
步骤 4:分页查询并记录演示学生 ID
展开:
GET /students
填写:
page = 1
page_size = 5
其他查询条件暂时留空,然后执行。
预期结果:
code为200;total表示未删除学生的总数;page为1;page_size为5;data最多返回 5 条;- 数据按照学生 ID 倒序排列,刚新增的学生通常在第一条。
从返回结果中找到 student_name 为“答辩演示学生”的记录,并记下:
id = ________
student_no = ________
后续所有 {id} 都替换成这里记录的真实学生 ID。
讲解词:
查询时先添加 is_deleted 等于 0 的条件,确保逻辑删除的数据不可见;然后在分页之前调用 count 获取符合条件的总数,最后使用 offset 和 limit 完成分页。固定按 ID 倒序可以保证分页顺序稳定。
分页计算公式:
offset = (page - 1) * page_size
步骤 5:演示姓名模糊查询
继续使用:
GET /students
填写:
student_name = 答辩
page = 1
page_size = 10
预期结果:只返回姓名中包含“答辩”的未删除学生。
讲解词:
查询参数都是可选的。前端传入姓名时,DAO 使用 LIKE 完成模糊查询;没有传入的条件不会拼接到 SQL 中。
也可以补充展示以下任意一种查询:
student_no = 上一步记录的学号
或者:
class_id = 1
步骤 6:查询单个学生详情
展开:
GET /students/{id}
将 id 填写为步骤 4 记录的学生 ID,然后执行。
预期响应结构:
{
"code": 200,
"detail": "查询成功",
"data": {
"id": 1,
"student_no": "STU0001",
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
}
实际的 id 和 student_no 以数据库返回为准。
讲解词:
DAO 使用主键和 is_deleted 等于 0 两个条件查询。API 层手动组装返回字典,只暴露业务需要的字段,不返回 is_deleted、创建时间等内部字段。
步骤 7:局部更新学生
展开:
POST /students/{id}
将 id 填写为步骤 4 记录的学生 ID,请求体只输入需要修改的字段:
{
"age": 23,
"gender": "女"
}
预期结果:返回“修改学生成功”,其他未传字段保持不变。
讲解词:
更新模型中的字段都是可选字段,接口通过 model_dump 的 exclude_unset 参数只提取前端实际传入的字段,DAO 再使用 setattr 循环完成局部更新。
更新完成后,再执行一次:
GET /students/{id}
确认:
age = 23
gender = 女
其余字段没有变化。
步骤 8:演示更新参数校验
再次调用:
POST /students/{id}
输入:
{
"age": 200,
"gender": "其他"
}
预期结果:HTTP 状态码为 422。
讲解词:
年龄限制在 0 到 150 之间,性别限制为男或女。参数校验失败时,请求不会进入更新 DAO,因此数据库中的数据不会被修改。
步骤 9:逻辑删除学生
展开:
DELETE /students/{id}
填写步骤 4 记录的学生 ID,然后执行。
预期响应:
{
"code": 200,
"detail": "删除学生成功"
}
讲解词:
删除操作不是物理删除,而是把 is_deleted 更新为 1。这样可以保留历史数据,同时所有正常查询都会通过 is_deleted 等于 0 将其过滤掉。
步骤 10:验证逻辑删除
删除后,再执行:
GET /students/{id}
预期结果:HTTP 状态码为 404:
{
"detail": "学生不存在"
}
再执行学生列表查询或姓名模糊查询,也不应该看到刚才删除的学生。
讲解词:
数据库记录仍然存在,但详情查询和列表查询都统一增加了 is_deleted 等于 0 的条件,所以业务层已经无法查询到该记录。
三、建议的答辩讲解顺序
可以用下面这段话概括学生模块:
学生模块采用 API、Schema、DAO 和 Model 分层。Schema 负责请求参数的类型、范围和业务规则校验;API 负责接收请求、处理 HTTP 状态码以及控制响应字段;DAO 负责数据库查询和事务处理;Model 负责学生表与字段的 ORM 映射。列表接口支持学号精确查询、姓名模糊查询、班级查询以及分页,并统一过滤逻辑删除数据。新增支持自动生成学生编号,修改支持局部更新,删除采用逻辑删除。
四、常见提问与回答
1. 为什么分页之前先执行 count?
因为前端需要知道符合条件的总数据量。count() 必须在 offset 和 limit 之前执行,否则统计到的可能只是当前页的数据量。
2. 为什么分页查询要排序?
如果不指定排序,数据库每次返回数据的顺序不一定稳定,可能造成翻页时数据重复或遗漏。当前代码按照学生 ID 倒序排列。
3. 为什么使用逻辑删除?
逻辑删除能够保留历史数据,避免误删除后无法恢复。正常查询通过 is_deleted == 0 过滤已删除记录。
4. 为什么列表和详情要手动组装字典?
手动组装能够明确控制暴露给前端的字段,避免把 is_deleted 等数据库内部字段返回给用户。
5. 为什么修改接口只更新部分字段?
前端可能只修改年龄或手机号等个别字段。使用 exclude_unset=True 只获取实际传入的字段,再通过 setattr 更新,可以避免未传字段被错误覆盖。
6. 为什么非法参数返回 422?
FastAPI 会先调用 Pydantic 校验请求体。数据类型、字段范围或 Literal 约束不满足时,请求不会进入业务函数,而是直接返回 422。
7. 为什么学生编号可以不传?
学生记录执行 flush() 后可以取得数据库生成的自增主键,系统再根据主键生成学生编号,能够减少手工编号冲突。
五、故障排查
请求体报 JSON decode error
检查以下问题:
- 属性名和值必须使用英文双引号;
- 最后一个字段后面不能有逗号;
- JSON 中不能写注释;
- 日期必须使用
YYYY-MM-DD格式。
合法示例:
{
"age": 23,
"gender": "女"
}
新增返回 500
优先检查:
- MySQL 是否启动;
- 数据库连接配置是否正确;
class_id是否在班级表中真实存在;- 手动填写的
student_no是否与已有记录重复; - 如果填写了
consultant_id,该顾问是否真实存在。
查询详情返回 404
检查:
- ID 是否填写正确;
- 该学生是否已经被逻辑删除;
- 是否误用了列表中的班级 ID 或顾问 ID。
Swagger 请求体只显示 {}
检查 StudentUpdate 是否被错误地重复嵌套。正确写法应该只有一层:
class StudentUpdate(BaseModel):
student_name: str | None = None
# 其他字段……
六、演示结束检查表
- 服务可以正常启动;
- Swagger 可以打开;
- 已确认一个真实存在的班级 ID;
- 性别非法数据返回 422;
- 日期非法数据返回 422;
- 合法学生新增成功;
- 已从列表中记录新增学生 ID 和学号;
- 分页和条件查询成功;
- 详情查询不暴露内部字段;
- 局部更新后未传字段保持不变;
- 逻辑删除成功;
- 删除后详情查询返回 404。