Files
DoubaoTeam/docs/学生模块答辩演示步骤.md

11 KiB
Raw Permalink Blame History

学生模块答辩演示步骤

演示目标:通过 Swagger 依次展示学生新增、参数校验、分页查询、条件查询、详情查询、局部更新和逻辑删除。

一、答辩前检查

1. 确认数据库和基础数据

  1. 确认 MySQL 已启动。
  2. 确认项目配置的数据库可以连接。
  3. 确认班级表中至少存在一条未删除的班级记录。
  4. 记住一个真实存在的班级 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

优先检查:

  1. MySQL 是否启动;
  2. 数据库连接配置是否正确;
  3. class_id 是否在班级表中真实存在;
  4. 手动填写的 student_no 是否与已有记录重复;
  5. 如果填写了 consultant_id,该顾问是否真实存在。

查询详情返回 404

检查:

  1. ID 是否填写正确;
  2. 该学生是否已经被逻辑删除;
  3. 是否误用了列表中的班级 ID 或顾问 ID。

Swagger 请求体只显示 {}

检查 StudentUpdate 是否被错误地重复嵌套。正确写法应该只有一层:

class StudentUpdate(BaseModel):
    student_name: str | None = None
    # 其他字段……

六、演示结束检查表

  • 服务可以正常启动;
  • Swagger 可以打开;
  • 已确认一个真实存在的班级 ID;
  • 性别非法数据返回 422;
  • 日期非法数据返回 422;
  • 合法学生新增成功;
  • 已从列表中记录新增学生 ID 和学号;
  • 分页和条件查询成功;
  • 详情查询不暴露内部字段;
  • 局部更新后未传字段保持不变;
  • 逻辑删除成功;
  • 删除后详情查询返回 404。