规范接口方法写法
This commit is contained in:
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user