From 6016a99bcf93f58577b1563b9e1531e17e15a358 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Tue, 22 Sep 2026 20:43:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=8E=A5=E5=8F=A3=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=86=99=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/学生模块答辩演示步骤.md | 472 +++++++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 docs/学生模块答辩演示步骤.md diff --git a/docs/学生模块答辩演示步骤.md b/docs/学生模块答辩演示步骤.md new file mode 100644 index 0000000..e6606bc --- /dev/null +++ b/docs/学生模块答辩演示步骤.md @@ -0,0 +1,472 @@ +# 学生模块答辩演示步骤 + +> 演示目标:通过 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. 启动项目 + +在项目根目录执行: + +```powershell +python main.py +``` + +看到服务正常启动后,在浏览器打开: + +```text +http://127.0.0.1:23333/docs +``` + +在 Swagger 页面找到“学生接口”。 + +## 二、正式演示流程 + +建议严格按照下面的顺序演示。每次操作时先展开接口,点击 **Try it out**,填写参数或请求体,再点击 **Execute**。 + +## 步骤 1:演示新增参数校验 + +展开: + +```text +PUT /students +``` + +先输入一条性别不合法的数据: + +```json +{ + "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:演示日期校验 + +仍然使用新增接口,将毕业日期设置为早于入学日期: + +```json +{ + "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:新增一名合法学生 + +继续使用: + +```text +PUT /students +``` + +输入以下合法数据: + +```json +{ + "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` 的学生编号。 + +预期响应: + +```json +{ + "code": 200, + "detail": "添加学生成功" +} +``` + +讲解词: + +> 新增请求先由 StudentCreate 完成字段校验,再通过 model_dump 转成字典。DAO 创建 ORM 实例并执行 flush 获取自增 ID,如果没有手动传入学号,就根据 ID 自动生成学号,最后提交事务。 + +## 步骤 4:分页查询并记录演示学生 ID + +展开: + +```text +GET /students +``` + +填写: + +```text +page = 1 +page_size = 5 +``` + +其他查询条件暂时留空,然后执行。 + +预期结果: + +- `code` 为 `200`; +- `total` 表示未删除学生的总数; +- `page` 为 `1`; +- `page_size` 为 `5`; +- `data` 最多返回 5 条; +- 数据按照学生 ID 倒序排列,刚新增的学生通常在第一条。 + +从返回结果中找到 `student_name` 为“答辩演示学生”的记录,并记下: + +```text +id = ________ +student_no = ________ +``` + +后续所有 `{id}` 都替换成这里记录的真实学生 ID。 + +讲解词: + +> 查询时先添加 is_deleted 等于 0 的条件,确保逻辑删除的数据不可见;然后在分页之前调用 count 获取符合条件的总数,最后使用 offset 和 limit 完成分页。固定按 ID 倒序可以保证分页顺序稳定。 + +分页计算公式: + +```text +offset = (page - 1) * page_size +``` + +## 步骤 5:演示姓名模糊查询 + +继续使用: + +```text +GET /students +``` + +填写: + +```text +student_name = 答辩 +page = 1 +page_size = 10 +``` + +预期结果:只返回姓名中包含“答辩”的未删除学生。 + +讲解词: + +> 查询参数都是可选的。前端传入姓名时,DAO 使用 LIKE 完成模糊查询;没有传入的条件不会拼接到 SQL 中。 + +也可以补充展示以下任意一种查询: + +```text +student_no = 上一步记录的学号 +``` + +或者: + +```text +class_id = 1 +``` + +## 步骤 6:查询单个学生详情 + +展开: + +```text +GET /students/{id} +``` + +将 `id` 填写为步骤 4 记录的学生 ID,然后执行。 + +预期响应结构: + +```json +{ + "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:局部更新学生 + +展开: + +```text +POST /students/{id} +``` + +将 `id` 填写为步骤 4 记录的学生 ID,请求体只输入需要修改的字段: + +```json +{ + "age": 23, + "gender": "女" +} +``` + +预期结果:返回“修改学生成功”,其他未传字段保持不变。 + +讲解词: + +> 更新模型中的字段都是可选字段,接口通过 model_dump 的 exclude_unset 参数只提取前端实际传入的字段,DAO 再使用 setattr 循环完成局部更新。 + +更新完成后,再执行一次: + +```text +GET /students/{id} +``` + +确认: + +```text +age = 23 +gender = 女 +``` + +其余字段没有变化。 + +## 步骤 8:演示更新参数校验 + +再次调用: + +```text +POST /students/{id} +``` + +输入: + +```json +{ + "age": 200, + "gender": "其他" +} +``` + +预期结果:HTTP 状态码为 `422`。 + +讲解词: + +> 年龄限制在 0 到 150 之间,性别限制为男或女。参数校验失败时,请求不会进入更新 DAO,因此数据库中的数据不会被修改。 + +## 步骤 9:逻辑删除学生 + +展开: + +```text +DELETE /students/{id} +``` + +填写步骤 4 记录的学生 ID,然后执行。 + +预期响应: + +```json +{ + "code": 200, + "detail": "删除学生成功" +} +``` + +讲解词: + +> 删除操作不是物理删除,而是把 is_deleted 更新为 1。这样可以保留历史数据,同时所有正常查询都会通过 is_deleted 等于 0 将其过滤掉。 + +## 步骤 10:验证逻辑删除 + +删除后,再执行: + +```text +GET /students/{id} +``` + +预期结果:HTTP 状态码为 `404`: + +```json +{ + "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` 格式。 + +合法示例: + +```json +{ + "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` 是否被错误地重复嵌套。正确写法应该只有一层: + +```python +class StudentUpdate(BaseModel): + student_name: str | None = None + # 其他字段…… +``` + +## 六、演示结束检查表 + +- [ ] 服务可以正常启动; +- [ ] Swagger 可以打开; +- [ ] 已确认一个真实存在的班级 ID; +- [ ] 性别非法数据返回 422; +- [ ] 日期非法数据返回 422; +- [ ] 合法学生新增成功; +- [ ] 已从列表中记录新增学生 ID 和学号; +- [ ] 分页和条件查询成功; +- [ ] 详情查询不暴露内部字段; +- [ ] 局部更新后未传字段保持不变; +- [ ] 逻辑删除成功; +- [ ] 删除后详情查询返回 404。 +