# 学生模块答辩演示步骤 > 演示目标:通过 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。