2026-05-29 15:58:00 +08:00
|
|
|
|
# D8 测试文档
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
> 测试日期:2026-05-29(单元测试);Swagger 2026-06-03;登录逻辑 v1.1 复测 2026-06-05(28 场景)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
> 测试人员:Claude AI + 人工审核
|
|
|
|
|
|
> 测试环境:Windows / Python 3.14 / Django 5.0 / MySQL 8 / Redis
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
> **v1.1 变更(2026-06-05)**:登录拆分为「移动端 U4 / CMS 端 U3」。U3 改为账号(用户名或手机号)+密码+角色;U4 仅试用机构「北大医学部(实验室)试用」可自动注册,其它机构须 CMS 先录入学生且机构需匹配;新增机构列表接口 `GET /api/user/institution_list/`;**U2 代注册收紧为仅超级管理员/医院管理员**(超管建所有角色、可任意机构;医院管理员建内容管理员/医生/学生、**仅限本机构**),代注册响应**不再返回 tokens**。新增测试文件 `test_login_mobile_cms.py`(11 条);负向测试新增代注册权限/机构范围 6 条(N-REG1~6)。
|
|
|
|
|
|
|
2026-05-29 15:58:00 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 测试环境
|
|
|
|
|
|
|
|
|
|
|
|
| 项目 | 值 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| Python | 3.14 |
|
|
|
|
|
|
| Django | 5.0+ |
|
|
|
|
|
|
| DRF | 3.14+ |
|
|
|
|
|
|
| 数据库 | MySQL 8 (test_medical_training) |
|
|
|
|
|
|
| 缓存 | Redis(与生产环境一致,`django_redis`) |
|
|
|
|
|
|
| 运行命令 | `.venv\Scripts\python.exe manage.py test test -v2 --keepdb` |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 测试总览
|
|
|
|
|
|
|
|
|
|
|
|
| 类别 | 测试文件 | 用例数 | 通过 | 失败 |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| 用户域 happy-path | `test_user_happy.py` | 11 | 11 | 0 |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| 登录 v1.1(移动端/CMS) | `test_login_mobile_cms.py` | 11 | 11 | 0 |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| 病例域 happy-path | `test_case_happy.py` | 2 | 2 | 0 |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| 用户域 negative | `test_user_negative.py` | 23 | 23 | 0 |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| 病例域 negative | `test_case_negative.py` | 12 | 12 | 0 |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| **合计** | | **59** | **59** | **0** |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Happy-Path 测试结果
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 用户域(11 条流程)
|
|
|
|
|
|
|
|
|
|
|
|
| ID | 测试方法 | 测试什么 | 结果 |
|
|
|
|
|
|
|---|---|---|---|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| HP-1 | `test_flow_register_login_me` | **管理员代注册 → CMS 密码登录**:管理员注册一个 CMS 角色(doctor)账号(手机号+姓名+机构,无需验证码,密码自动为 Pass+手机号)→ 用「账号+密码+角色」登录 → 查看个人信息(确认手机号、姓名、机构正确) | PASS |
|
|
|
|
|
|
| HP-2 | `test_flow_code_login` | **验证码登录(已录入学生)**:预创建学生并关联机构 → 发送登录验证码 → 用手机号+验证码+所选机构编码登录 → 确认 `is_new_user=false` → 查看个人信息确认身份正确 | PASS |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| HP-3 | `test_flow_reset_password` | **忘记密码重置**:发送重置验证码 → 用验证码设置新密码 → 用新密码能登录成功 → 用旧密码登录失败(旧密码已失效) | PASS |
|
|
|
|
|
|
| HP-4 | `test_flow_change_password` | **登录后修改密码**:先用旧密码登录拿到 token → 调用修改密码接口 → 等 1 秒后旧 token 被系统自动作废(返回 401)→ 用新密码重新登录,新 token 正常可用 | PASS |
|
|
|
|
|
|
| HP-5 | `test_admin_list_all_users` | **管理员看用户列表**:创建管理员+2 个学生,管理员调用用户列表接口,确认能看到系统里所有用户(包括自己和两个学生) | PASS |
|
|
|
|
|
|
| HP-6 | `test_teacher_list_own_students_only` | **教师只看到自己的学生**:创建教师+自己的学生+别人的学生,教师调用用户列表,确认只能看到与自己有师生关系的学生,看不到别人的学生,也看不到自己 | PASS |
|
|
|
|
|
|
| HP-7 | `test_teacher_list_excludes_ended_relation` | **已结束关系的学生不可见**:教师名下有一个活跃学生和一个已毕业学生(关系状态=已结束),教师调用列表,确认只能看到活跃学生,已毕业的不显示 | PASS |
|
|
|
|
|
|
| HP-8 | `test_admin_retrieve_any_user` | **管理员查看任意用户详情**:管理员可以通过 /users/{id}/ 查看系统中任何用户的详细信息,确认返回的姓名正确 | PASS |
|
|
|
|
|
|
| HP-9 | `test_self_retrieve` | **用户查看自己的详情**:学生通过 /users/{自己的id}/ 查看自己的信息,确认能正常返回 | PASS |
|
|
|
|
|
|
| HP-10 | `test_teacher_retrieve_own_student` | **教师查看名下学生详情**:教师和学生建立师生关系后,教师可以查看该学生的详细信息 | PASS |
|
|
|
|
|
|
| HP-11 | `test_admin_list_filter_and_search` | **列表筛选和搜索**:创建管理员+张同学(学生)+李同学(学生)+张老师(教师),管理员用 `role_type=student&search=张` 筛选,确认只返回张同学(李同学不姓张被排除,张老师不是学生被排除) | PASS |
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
### 3.1.1 登录 v1.1(移动端 U4 / CMS 端 U3)— `test_login_mobile_cms.py`(11 条)
|
|
|
|
|
|
|
|
|
|
|
|
| ID | 测试方法 | 测试什么 | 期望 | 结果 |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| L-1 | `InstitutionListTest.test_list_unpaginated_with_trial_flag` | **机构列表不分页**:创建普通机构 + 试用机构,GET `institution_list` 返回数组,试用机构 `is_trial=true`、普通机构 `is_trial=false` | 200 | PASS |
|
|
|
|
|
|
| L-2 | `MobileLoginCodeTest.test_trial_first_register_then_login` | **试用机构首次注册→再次登录**:新手机号选试用机构,首次 `is_new_user=true`(自动建 student),二次 `is_new_user=false` | 201/200 | PASS |
|
|
|
|
|
|
| L-3 | `MobileLoginCodeTest.test_non_trial_unregistered_403` | **非试用未录入拒绝**:未录入手机号选普通机构登录,拒绝 `AUTH_NOT_REGISTERED` | 403 | PASS |
|
|
|
|
|
|
| L-4 | `MobileLoginCodeTest.test_non_trial_institution_mismatch_403` | **机构不匹配拒绝**:学生录入在机构 A,却选机构 B 登录,拒绝 `AUTH_INSTITUTION_MISMATCH` | 403 | PASS |
|
|
|
|
|
|
| L-5 | `MobileLoginCodeTest.test_non_trial_registered_match_ok` | **非试用已录入且机构匹配**:学生录入在机构 A 选机构 A 登录成功,`is_new_user=false` | 200 | PASS |
|
|
|
|
|
|
| L-6 | `MobileLoginCodeTest.test_unknown_institution_code` | **机构编码不存在**:传不存在的机构编码,`USER_INSTITUTION_NOT_FOUND` | 400 | PASS |
|
|
|
|
|
|
| L-7 | `CmsPasswordLoginTest.test_login_by_phone_ok` | **CMS 手机号登录**:doctor 用手机号+密码+角色登录成功 | 200 | PASS |
|
|
|
|
|
|
| L-8 | `CmsPasswordLoginTest.test_login_by_username_ok` | **CMS 用户名登录**:super_admin(无手机号)用用户名+密码+角色登录成功 | 200 | PASS |
|
|
|
|
|
|
| L-9 | `CmsPasswordLoginTest.test_missing_role_400` | **缺少角色**:只传账号+密码不传角色,`AUTH_BAD_CREDENTIALS` | 400 | PASS |
|
|
|
|
|
|
| L-10 | `CmsPasswordLoginTest.test_invalid_role` | **非法角色**:role=student(非 CMS 角色),`AUTH_INVALID_ROLE` | 400 | PASS |
|
|
|
|
|
|
| L-11 | `CmsPasswordLoginTest.test_role_mismatch` | **角色不符**:doctor 账号传 role=content_admin,通用 `AUTH_BAD_CREDENTIALS`(不暴露真实角色) | 400 | PASS |
|
|
|
|
|
|
|
2026-05-29 15:58:00 +08:00
|
|
|
|
### 3.2 病例域(2 条流程)
|
|
|
|
|
|
|
|
|
|
|
|
| ID | 测试方法 | 测试什么 | 结果 |
|
|
|
|
|
|
|---|---|---|---|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| HP-5 | `test_flow_form_create_read_update` | **手工录入病例全流程**:表单创建传统病例(含 2 条评分规则 + 1 条 `exam_items`)→ C4 确认 `exam_items` → 改标题/评分规则 → 校验 `case_exam_item` 表条数 | PASS |
|
|
|
|
|
|
| HP-6 | `test_flow_pdf_mock_full_pipeline` | **PDF 流水线(mock AI)**:C1 解析含 `exam_items` → C2 评分规则 → C3 落库 → C4 校验响应中检查项条数 | PASS |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Negative 测试结果
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
### 4.1 用户域(23 条)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
| ID | 测试方法 | 测试什么 | 期望 | 结果 |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| N1 | `test_rate_limit_sms_429` | **短信发送频率超限**:模拟 1 分钟内已发过验证码,再次请求发送时系统拒绝,返回"请求太频繁" | 429 | PASS |
|
|
|
|
|
|
| N2 | `test_unauth_change_password_401` | **没登录就想改密码**:不带任何 token 直接调用修改密码接口,系统拒绝并要求先登录 | 401 | PASS |
|
|
|
|
|
|
| N3 | `test_unauth_me_401` | **没登录就想看个人信息**:不带 token 调用 /me 接口,系统拒绝 | 401 | PASS |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| N4 | `test_register_invalid_phone_400` | **手机号格式错误**:超管代注册时用 "123"(不是 11 位手机号),系统拒绝并提示手机号不合法 | 400 | PASS |
|
|
|
|
|
|
| N5 | `test_register_missing_institution_400` | **注册缺少机构**:超管代注册时不传机构编码,系统拒绝并提示机构编码不能为空 | 400 | PASS |
|
|
|
|
|
|
| N6 | `test_register_duplicate_phone_400` | **手机号已被注册**:先创建一个用户,超管再用同一手机号注册,系统拒绝并提示该手机号已注册 | 400 | PASS |
|
|
|
|
|
|
| N7 | `test_login_wrong_password` | **密码错误**:CMS 账号用正确账号+角色但错误密码登录,系统拒绝并提示账号、密码或角色错误 | 400 | PASS |
|
|
|
|
|
|
| N8 | `test_login_account_lock_423` | **连续输错密码被锁定**:CMS 账号连续 5 次输入错误密码(账号+角色正确),第 6 次登录时系统锁定账号,返回"账号已锁定"(防暴力破解) | 423 | PASS |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| N9 | `test_reset_wrong_code` | **重置密码时验证码错误**:真实验证码是 123456,但提交 999999,系统拒绝并提示验证码不匹配 | 400/401 | PASS |
|
|
|
|
|
|
| N10 | `test_refresh_revoked_token_401` | **退出登录后 token 失效**:先退出登录(logout 会吊销 refresh token),再用那个已吊销的 refresh token 去刷新,系统拒绝 | 401 | PASS |
|
|
|
|
|
|
| N11 | `test_student_list_403` | **学生不能看用户列表**:学生角色调用用户列表接口,系统拒绝(只有管理员和教师才能看) | 403 | PASS |
|
|
|
|
|
|
| N12 | `test_doctor_list_403` | **医生不能看用户列表**:医生角色调用用户列表接口,系统同样拒绝(医生也没有列表权限) | 403 | PASS |
|
|
|
|
|
|
| N13 | `test_unauth_list_401` | **没登录不能看用户列表**:不带 token 调用用户列表接口,系统要求先登录 | 401 | PASS |
|
|
|
|
|
|
| N14 | `test_unauth_detail_401` | **没登录不能看用户详情**:不带 token 调用用户详情接口,系统要求先登录 | 401 | PASS |
|
|
|
|
|
|
| N15 | `test_student_view_other_student_403` | **学生不能看别人的详情**:学生 A 试图查看学生 B 的个人信息,系统拒绝(只能看自己的) | 403 | PASS |
|
|
|
|
|
|
| N16 | `test_teacher_view_unrelated_student_403` | **教师不能看非名下学生**:教师试图查看一个和自己没有师生关系的学生的信息,系统拒绝 | 403 | PASS |
|
|
|
|
|
|
| N17 | `test_teacher_view_ended_relation_student_403` | **教师不能看已毕业学生**:教师和学生的师生关系已结束(status=0,如学生已毕业),教师再查看该学生详情,系统拒绝 | 403 | PASS |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| N-REG1 | `test_register_unauth_401` | **未登录代注册**:不带 token 调用代注册接口,系统要求先登录 | 401 | PASS |
|
|
|
|
|
|
| N-REG2 | `test_register_non_admin_403` | **非管理员代注册**:医生(doctor)调用代注册,系统拒绝 `USER_NO_REGISTER_PERMISSION`(仅超管/医院管理员可代注册) | 403 | PASS |
|
|
|
|
|
|
| N-REG3 | `test_register_hospital_admin_cannot_create_super_admin_403` | **医院管理员越权建超管**:医院管理员尝试创建超级管理员,系统拒绝 `USER_NO_REGISTER_ROLE_PERMISSION`(只能建内容管理员/医生/学生) | 403 | PASS |
|
|
|
|
|
|
| N-REG4 | `test_register_hospital_admin_creates_student_ok` | **医院管理员在本机构建学生**:医院管理员创建学生账号成功,且新用户机构=管理员所属机构 | 201 | PASS |
|
|
|
|
|
|
| N-REG5 | `test_register_hospital_admin_cross_institution_403` | **医院管理员跨机构建账号**:医院管理员(属机构A)指定机构B建账号,系统拒绝 `USER_INSTITUTION_SCOPE_FORBIDDEN`(只能在本机构内) | 403 | PASS |
|
|
|
|
|
|
| N-REG6 | `test_register_hospital_admin_no_institution_403` | **无机构的医院管理员代注册**:医院管理员未归属任何机构时代注册,系统拒绝 `USER_NO_REGISTER_INSTITUTION` | 403 | PASS |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
### 4.2 病例域(12 条)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
| ID | 测试方法 | 测试什么 | 期望 | 结果 |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| N10 | `test_invalid_case_type_400` | **病例类型不合法**:创建病例时 case_type 传 "invalid"(只支持 traditional 和 teaching),系统拒绝 | 400 | PASS |
|
|
|
|
|
|
| N11 | `test_empty_scoring_rules_400` | **评分规则为空**:创建病例时 scoring_rules 传空数组 `[]`,系统要求至少有 1 条评分规则 | 400 | PASS |
|
|
|
|
|
|
| N12 | `test_subtable_conflict_400` | **子表类型冲突**:创建传统病例时同时传了 traditional 和 teaching 两个子表数据,系统拒绝(一个病例只能有一种类型的子表) | 400 | PASS |
|
|
|
|
|
|
| N13 | `test_missing_subtable_400` | **缺少必要子表**:声明 case_type=traditional 但没有传 traditional 子表数据,系统拒绝(类型和子表必须对应) | 400 | PASS |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| N13b | `test_duplicate_exam_items_deduped_on_create` | **检查项 item_code 重复**:同一 `exam_items` 中重复 `item_code`,归一化后只保留一条并成功创建 | 201 | PASS |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| N15 | `test_patch_published_case_400` | **已发布的病例不能编辑**:先创建病例并将其发布(publish_status=1),再尝试修改标题,系统拒绝(发布后不允许编辑) | 400 | PASS |
|
|
|
|
|
|
| N14 | `test_unauth_full_create_401` | **没登录不能创建病例**:不带 token 直接调用创建病例接口,系统要求先登录 | 401 | PASS |
|
|
|
|
|
|
| N17 | `test_view_other_draft_403` | **不能看别人的草稿**:用户 A 创建了一个草稿病例,用户 B 试图查看,系统拒绝(草稿只有创建者自己能看) | 403 | PASS |
|
|
|
|
|
|
| N18 | `test_rate_limit_pdf_parse_429` | **PDF 解析频率超限**:模拟用户短时间内已多次调用 PDF 解析,再次调用时系统拒绝并提示"请求太频繁" | 429 | PASS |
|
|
|
|
|
|
| N19 | `test_transaction_rollback` | **数据库事务回滚**:创建病例时模拟评分规则写入数据库失败(IntegrityError),验证病例主表也一起回滚(不会出现"有病例但没有评分规则"的残留数据) | 回滚成功 | PASS |
|
|
|
|
|
|
| N20 | `test_ai_bad_json_500` | **AI 返回乱码**:模拟 DeepSeek AI 返回的不是合法 JSON 格式,系统返回 500 并明确告知错误原因是 AI 输出异常 | 500 | PASS |
|
|
|
|
|
|
| N21 | `test_ai_schema_violation_500` | **AI 返回数据缺字段**:模拟 DeepSeek 返回了合法 JSON 但缺少必填字段(如 title),系统校验后返回 500 并提示 AI 输出不符合预期格式 | 500 | PASS |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Bug 修复记录
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-1: 限流 mock 导致 500 而非 429
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:N1 `test_rate_limit_sms_429`
|
|
|
|
|
|
- **现象**:mock `SmsPhoneMinuteThrottle.allow_request` 返回 `False` 后,DRF 调用 `throttle.wait()` 获取重试时间,但因 `allow_request` 被完整 mock,`self.history` 未初始化,导致 `AttributeError` → 500
|
|
|
|
|
|
- **根因**:DRF 的 `check_throttles` 在 `allow_request=False` 后会调用 `wait()` 方法读取 `self.history`,mock 只替换了 `allow_request` 未处理 `wait`
|
|
|
|
|
|
- **修复**:同时 mock `wait` 方法返回固定值 `60`
|
|
|
|
|
|
- **影响文件**:`test/test_user_negative.py`、`test/test_case_negative.py`
|
|
|
|
|
|
- **严重度**:低(仅影响测试代码,非业务代码 Bug)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-2: PDF mock 路径不正确
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:HP-6 `test_flow_pdf_mock_full_pipeline`
|
|
|
|
|
|
- **现象**:mock `apps.case.services.pdf_reader.extract_text_from_pdfs` 无效,真实 PDF 解析仍被执行,伪造 PDF 内容触发 `CASE_PDF_EMPTY` 错误
|
|
|
|
|
|
- **根因**:`case_importer.py` 使用 `from .pdf_reader import extract_text_from_pdfs` 直接导入函数,mock 必须 patch 导入位置 `apps.case.services.case_importer.extract_text_from_pdfs` 而非定义位置
|
|
|
|
|
|
- **修复**:更改 patch 路径为 `apps.case.services.case_importer.extract_text_from_pdfs`
|
|
|
|
|
|
- **影响文件**:`test/test_case_happy.py`、`test/test_case_negative.py`
|
|
|
|
|
|
- **严重度**:低(仅影响测试代码,非业务代码 Bug)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-3: drf-spectacular 无法识别自定义认证类
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:Swagger 文档生成时控制台日志
|
|
|
|
|
|
- **现象**:所有 ViewSet 和函数视图均产生 `Warning: could not resolve authenticator RedisBlacklistJWTAuthentication`,Swagger UI 上缺少认证标识和 Authorize 按钮
|
|
|
|
|
|
- **根因**:`RedisBlacklistJWTAuthentication` 继承自 `JWTAuthentication`,drf-spectacular 没有注册对应的 `OpenApiAuthenticationExtension`,不知道如何将其映射为 OpenAPI `securitySchemes`
|
|
|
|
|
|
- **修复**:创建 `apps/user/openapi.py`,定义 `RedisBlacklistJWTScheme` 扩展类,将该认证类映射为 `type: http, scheme: bearer, bearerFormat: JWT`;在 `apps/user/apps.py` 的 `ready()` 中 import 触发自动注册
|
|
|
|
|
|
- **影响文件**:`apps/user/openapi.py`(新建)、`apps/user/apps.py`
|
|
|
|
|
|
- **严重度**:低(不影响接口功能,仅影响 Swagger 文档展示)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-4: 函数视图缺少 Swagger 请求/响应 Schema
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:Swagger 文档生成时控制台日志
|
|
|
|
|
|
- **现象**:`send_code`、`register`、`login_password`、`login_code`、`logout`、`reset_password` 6 个 `@api_view` 函数视图产生 `Error: unable to guess serializer`,Swagger UI 上这些接口没有请求体/响应体描述
|
|
|
|
|
|
- **根因**:函数视图直接读 `request.data`,未声明 `serializer_class`,drf-spectacular 无法自动推断 schema
|
|
|
|
|
|
- **修复**:为 6 个函数视图添加 `@extend_schema` 装饰器,通过 `inline_serializer` 声明请求和响应字段;`refresh.py` 的类视图也加了 `@extend_schema(tags=['认证'])`
|
|
|
|
|
|
- **影响文件**:`apps/user/auth/send_code.py`、`register.py`、`login.py`、`logout.py`、`reset_password.py`、`refresh.py`
|
|
|
|
|
|
- **严重度**:低(不影响接口功能,仅影响 Swagger 文档展示)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-5: 同名枚举冲突导致 Swagger 命名混乱
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:Swagger 文档生成时控制台日志
|
|
|
|
|
|
- **现象**:`case_type` 和 `status` 字段在不同 model/serializer 中有不同 choices 值集,drf-spectacular 自动生成带哈希后缀的名称(如 `CaseType629Enum`、`StatusDb0Enum`)
|
|
|
|
|
|
- **根因**:`CaseBase.CASE_TYPE_CHOICES`(4 值)与 C2/C3 内联序列化器 `ChoiceField(choices=['traditional','teaching'])`(2 值)同名不同值;`CaseBase.STATUS_CHOICES` / `User.STATUS_CHOICES`(相同值)与 `TrainingRecord.STATUS_CHOICES` / `TeacherStudentRelation.STATUS_CHOICES`(不同值)同名不同值
|
|
|
|
|
|
- **修复**:在 `SPECTACULAR_SETTINGS['ENUM_NAME_OVERRIDES']` 中显式命名:`CaseTypeEnum`(4 值)、`CreatableCaseTypeEnum`(2 值)、`CommonStatusEnum`、`TrainingStatusEnum`、`TeacherStudentStatusEnum` 等
|
|
|
|
|
|
- **影响文件**:`config/settings.py`
|
|
|
|
|
|
- **严重度**:极低(不影响接口功能,仅影响 Swagger 文档中枚举名称的可读性)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-6: 审计日志文件在 Windows 上写入失败
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:手动检查 `logs/audit.log` 发现文件为空(0 字节)
|
|
|
|
|
|
- **现象**:`TimedRotatingFileHandler` 在首次写入时尝试按日期轮转(rename `audit.log` → `audit.log.2026-05-27`),但 dev server 进程正占着文件,Windows 文件锁导致 `PermissionError: [WinError 32]`,轮转失败,日志丢失
|
|
|
|
|
|
- **根因**:`TimedRotatingFileHandler` 的轮转机制依赖 `os.rename()`,Windows 上文件被其他进程打开时不允许 rename(Linux 无此问题)
|
|
|
|
|
|
- **修复**:自定义 `DailyFileHandler`(`config/logging_handlers.py`),直接按日期命名文件(`audit-YYYY-MM-DD.log`),日期切换时打开新文件,**不 rename 旧文件**,彻底避免文件锁问题;保留 `backup_count=30` 自动清理超过 30 天的旧日志
|
|
|
|
|
|
- **影响文件**:`config/logging_handlers.py`(新建)、`config/settings.py`(LOGGING handler 配置)
|
|
|
|
|
|
- **严重度**:中(审计日志完全丢失,影响安全审计能力)
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
### Bug-7: 验证码登录自动注册缺少并发竞态保护
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:代码 Review(登录/注册逻辑重构后)
|
|
|
|
|
|
- **现象**:`login_code` 中 `User.DoesNotExist` 后直接 `create_user()`,无 `IntegrityError` 兜底。两个请求同时用同一手机号到达时,第二个 `create_user` 因 phone UNIQUE 约束报 500
|
|
|
|
|
|
- **根因**:`register.py` 有 `transaction.atomic()` + `IntegrityError` 兜底,但 `login_code` 的自动注册路径遗漏了这个保护
|
|
|
|
|
|
- **修复**:`create_user` 外包 `try/except IntegrityError`,捕获后重新 `get()` 走登录路径
|
|
|
|
|
|
- **影响文件**:`apps/user/auth/login.py`
|
|
|
|
|
|
- **严重度**:高(并发场景下会 500)
|
|
|
|
|
|
|
|
|
|
|
|
### Bug-8: 验证码登录中 institution_type 校验晚于验证码消耗
|
|
|
|
|
|
|
|
|
|
|
|
- **发现阶段**:代码 Review(登录/注册逻辑重构后)
|
|
|
|
|
|
- **现象**:验证码在第 150 行被删除,之后第 155 行 `resolve_or_create_institution` 才校验 `institution_type`。如果 type 不合法,验证码已被消耗,用户需重新获取
|
|
|
|
|
|
- **根因**:校验顺序问题,入参校验应全部在验证码消耗之前完成
|
|
|
|
|
|
- **修复**:将 `institution_type` 枚举校验提前到验证码校验之前
|
|
|
|
|
|
- **影响文件**:`apps/user/auth/login.py`
|
|
|
|
|
|
- **严重度**:中(影响用户体验,不影响数据安全)
|
|
|
|
|
|
|
2026-05-29 15:58:00 +08:00
|
|
|
|
### 修复验证
|
|
|
|
|
|
|
|
|
|
|
|
**Bug-3/4/5 Swagger 修复验证:**
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 修复前
|
|
|
|
|
|
Schema generation summary:
|
|
|
|
|
|
Warnings: 104 (20 unique)
|
|
|
|
|
|
Errors: 1 (1 unique)
|
|
|
|
|
|
|
|
|
|
|
|
# 修复后
|
|
|
|
|
|
Schema generation summary:
|
|
|
|
|
|
Warnings: 0
|
|
|
|
|
|
Errors: 0
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `python manage.py spectacular --validate --fail-on-warn` 退出码 0
|
|
|
|
|
|
|
|
|
|
|
|
**Bug-6 日志修复验证:**
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 修复前:audit.log 0 字节,PermissionError: [WinError 32]
|
|
|
|
|
|
# 修复后:audit-2026-05-29.log 正常写入 29 条审计记录
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 全部 41 条单元测试通过,业务逻辑零影响
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. .env.example 审计
|
|
|
|
|
|
|
|
|
|
|
|
对比 `config/settings.py` 中所有 `os.getenv()` 调用,确认 `.env.example` 覆盖完整:
|
|
|
|
|
|
|
|
|
|
|
|
| 环境变量 | 是否覆盖 | 备注 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| DB_NAME / DB_USER / DB_PASSWORD / DB_HOST / DB_PORT | ✅ | |
|
|
|
|
|
|
| REDIS_URL | ✅ | |
|
|
|
|
|
|
| SMS_PROVIDER | ✅ | mock / aliyun |
|
|
|
|
|
|
| ALIYUN_SMS_ACCESS_KEY_ID / SECRET | ✅ | |
|
|
|
|
|
|
| ALIYUN_SMS_SIGN_NAME / TEMPLATE_* | ✅ | |
|
|
|
|
|
|
| DEEPSEEK_API_KEY | ✅ | |
|
|
|
|
|
|
| DEEPSEEK_BASE_URL / MODEL / TIMEOUT / MAX_RETRIES | ✅ | |
|
|
|
|
|
|
|
|
|
|
|
|
**安全修复**:已将 `.env.example` 中的真实密码和 API Key 替换为占位符:
|
|
|
|
|
|
- `DB_PASSWORD=your-db-password`
|
|
|
|
|
|
- `DEEPSEEK_API_KEY=your-deepseek-api-key`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 测试覆盖的接口清单
|
|
|
|
|
|
|
|
|
|
|
|
### 用户端
|
|
|
|
|
|
|
|
|
|
|
|
| 接口 | URL | happy-path | negative |
|
|
|
|
|
|
|---|---|---|---|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| U1 发送验证码 | POST /api/user/auth/send-code/ | HP-2,3 | N1(限流) |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| U2 管理员代注册 | POST /api/user/auth/register/ | HP-1 | N4,N5,N6 / N-REG1~6 |
|
|
|
|
|
|
| U3 密码登录(CMS) | POST /api/user/auth/login/ | HP-1,3,4 / L-7,8 | N7,N8 / L-9,10,11 |
|
|
|
|
|
|
| U4 验证码登录(移动端) | POST /api/user/auth/login-code/ | HP-2 / L-2,5 | L-3,4,6 |
|
|
|
|
|
|
| 机构列表(移动端) | GET /api/user/institution_list/ | L-1 | — |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| U5 重置密码 | POST /api/user/auth/reset-password/ | HP-3 | N9 |
|
|
|
|
|
|
| U6 修改密码 | POST /api/user/users/change-password/ | HP-4 | N2 |
|
|
|
|
|
|
| U7 退出登录 | POST /api/user/auth/logout/ | — | N10(辅助) |
|
|
|
|
|
|
| U8 刷新 Token | POST /api/user/auth/refresh/ | — | N10 |
|
|
|
|
|
|
| /me | GET /api/user/users/me/ | HP-1,2,4 | N3 |
|
|
|
|
|
|
| U9 用户列表 | GET /api/user/users/ | HP-5,6,7,11 | N11,N12,N13 |
|
|
|
|
|
|
| U10 用户详情 | GET /api/user/users/{id}/ | HP-8,9,10 | N14,N15,N16,N17 |
|
|
|
|
|
|
|
|
|
|
|
|
### 病例端
|
|
|
|
|
|
|
|
|
|
|
|
| 接口 | URL | happy-path | negative |
|
|
|
|
|
|
|---|---|---|---|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| C1 PDF 解析 | POST /api/case/cases/parse-pdf/ | HP-6(含 `exam_items`) | N18,N20,N21 |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| C2 生成评分规则 | POST /api/case/cases/generate-scoring-rules/ | HP-6 | — |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| C3 创建病例 | POST /api/case/cases/full-create/ | HP-5,6(写入 `case_exam_item`) | N11-N14,N13b,N16,N19 |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
| C4 完整查看 | GET /api/case/cases/{id}/full/ | HP-5,6 | N17 |
|
|
|
|
|
|
| C5 编辑草稿 | PATCH /api/case/cases/{id}/full/ | HP-5,6 | N15 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Swagger Try-it-out 接口验证
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
> **脚本**:`test/swagger_tryout.py`
|
|
|
|
|
|
> **最近验证**:2026-06-03(25/25 PASS,含 `case_exam_item` 落库校验)
|
|
|
|
|
|
> **运行方式**:`python manage.py runserver 8000` 后执行 `.venv\Scripts\python.exe test\swagger_tryout.py`
|
|
|
|
|
|
> **日志**:`logs/test-swagger-YYYY-MM-DD.log`(完整请求/响应体)
|
|
|
|
|
|
> **PDF**:项目根目录 `儿科 病例样例(SOAP+循证).pdf`(真实临床 PDF)
|
|
|
|
|
|
|
|
|
|
|
|
### 8.0 与单元测试的差异
|
|
|
|
|
|
|
|
|
|
|
|
| 项目 | 单元测试(`manage.py test`) | Swagger 脚本 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| 数据库 | `test_medical_training`(事务回滚) | `.env` 中 `DB_NAME`(如 `medical_platform`),**数据会落库** |
|
|
|
|
|
|
| 服务形态 | Django TestClient,无 HTTP | 真实 HTTP 请求 `http://127.0.0.1:8000` |
|
|
|
|
|
|
| 验证码 | 测试内 mock / 读 Redis | 启动前 `cache.clear()`,必要时 `inject_sms_code` 注入 `123456` |
|
|
|
|
|
|
| AI | happy-path 中 mock | C1/C2 调用真实 DeepSeek(失败时 C1/C2 记 PASS 若 500/429,C3 回退手工载荷) |
|
|
|
|
|
|
|
|
|
|
|
|
**脚本内置测试数据**(每次运行前清理主测号,病例段会 `get_or_create` 机构/科室):
|
|
|
|
|
|
|
|
|
|
|
|
| 常量 | 值 | 用途 |
|
|
|
|
|
|
|---|---|---|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| `SUPER_PHONE` | `13700000090` | 超级管理员,执行 U2 代注册(`django_eval` 预置) |
|
|
|
|
|
|
| `PHONE` | `13700000099` | 主流程 CMS 用户(角色 `doctor`,走 U3) |
|
|
|
|
|
|
| `STUDENT_PHONE_U4` | `13700000097` | 已录入学生,走 U4(机构匹配,测完删除) |
|
|
|
|
|
|
| `PHONE_ALT` | `13700000098` | U4-new 试用机构自动注册(测完删除) |
|
|
|
|
|
|
| `INST_CODE` / `INST_NAME` | `SWAG_TEST_HOSP` / `Swagger测试医院` | U2 建机构、U4 所选机构 |
|
|
|
|
|
|
| `TRIAL_INST_CODE` / `TRIAL_INST_NAME` | `PKU_LAB_TRIAL` / `北大医学部(实验室)试用` | 试用机构(脚本预置) |
|
|
|
|
|
|
| `CMS_ROLE` | `doctor` | PHONE 用户的 CMS 登录角色 |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| `PASSWORD` | `Pass13700000099` | U2 默认密码、U3 登录 |
|
|
|
|
|
|
| `DEPT_NAME` | `Swagger儿科` | C3 科室名(避免库内多个「儿科」→ `CASE_DEPARTMENT_AMBIGUOUS`) |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| `ADMIN/TEACHER/STUDENT_PHONE` | `13700000088/77/66` | U9/U10 角色夹具(`django_eval` 创建后删除;teacher 直接签发 token) |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
**执行顺序(用户端)**:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-06-05 15:36:31 +08:00
|
|
|
|
INST-LIST(机构列表) → U1(login发码)
|
|
|
|
|
|
→ U2(超管代注册 doctor, 不返回 tokens) → U2-tok(校验无 tokens) → U2-neg1(未登录 401)
|
|
|
|
|
|
→ U3(CMS 账号+密码+角色登录) → U3-neg(缺角色 400) → U2-neg2(doctor 越权代注册 403)
|
|
|
|
|
|
→ U2-ha(医院管理员本机构 201) → U2-ha-neg(医院管理员跨机构 403)
|
|
|
|
|
|
→ U4(已录入学生+机构匹配 200) → U4-neg(非试用未录入 403)
|
|
|
|
|
|
→ U4-pre/U4-new(试用机构自动注册 201) → U5(重置) → [sleep 1.2s] 重登
|
2026-06-03 17:34:47 +08:00
|
|
|
|
→ U6(改密) → [sleep 1.2s] 重登 → U8(refresh) → /me
|
|
|
|
|
|
→ U9/U9-b/U9-c/U10/U10-b/U10-c → U7(logout)
|
|
|
|
|
|
→ [sleep 1.2s] 病例段用 FINAL_PASSWORD 重登 → C1→C2→C3→C4→C5
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
### 8.1 用户端(25 个接口/场景)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
| ID | Method | URL | 测试什么 | 期望 | 结果 |
|
|
|
|
|
|
|---|---|---|---|---|---|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| INST-LIST | GET | /api/user/institution_list/ | 不分页机构列表,含 `is_trial` 标识 | 200 | PASS |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| U1 | POST | /api/user/auth/send-code/ | `scene=login` 发码(未注册用户也可) | 200 | PASS |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| U2 | POST | /api/user/auth/register/ | **超管**代注册 doctor:`phone`+`real_name`+`role_type`+机构字段,**无验证码**;默认密码 `Pass{phone}`,**不返回 tokens** | 201 | PASS |
|
|
|
|
|
|
| U2-tok | CHECK | register 响应 | 响应体不含 `tokens` 字段 | 不含 | PASS |
|
|
|
|
|
|
| U2-neg1 | POST | /api/user/auth/register/ | 未登录代注册 → `AUTH_UNAUTHORIZED` | 401 | PASS |
|
|
|
|
|
|
| U3 | POST | /api/user/auth/login/ | CMS 登录:`account`+`password`+`role=doctor` | 200 | PASS |
|
|
|
|
|
|
| U3-neg | POST | /api/user/auth/login/ | 缺少 `role` → `AUTH_BAD_CREDENTIALS` | 400 | PASS |
|
|
|
|
|
|
| U2-neg2 | POST | /api/user/auth/register/ | doctor(非管理员)代注册 → `USER_NO_REGISTER_PERMISSION` | 403 | PASS |
|
|
|
|
|
|
| U2-ha | POST | /api/user/auth/register/ | 医院管理员在**本机构**建学生 → 201 | 201 | PASS |
|
|
|
|
|
|
| U2-ha-neg | POST | /api/user/auth/register/ | 医院管理员**跨机构**建账号 → `USER_INSTITUTION_SCOPE_FORBIDDEN` | 403 | PASS |
|
|
|
|
|
|
| U4 | POST | /api/user/auth/login-code/ | 已录入学生 + 机构匹配:`code` + `institution_code`,`is_new_user=false` | 200 | PASS |
|
|
|
|
|
|
| U4-neg | POST | /api/user/auth/login-code/ | 非试用机构 + 未录入手机号 → `AUTH_NOT_REGISTERED` | 403 | PASS |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| U4-pre | POST | /api/user/auth/send-code/ | 备用号 `13700000098` 发 login 码 | 200 | PASS |
|
2026-06-05 15:36:31 +08:00
|
|
|
|
| U4-new | POST | /api/user/auth/login-code/ | 试用机构 + 未注册备用号 → 自动注册 `is_new_user=true` | 201 | PASS |
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| U5 | POST | /api/user/auth/reset-password/ | `scene=reset` 验证码 + 新密码 `SwagNew1`(8–32 位含字母数字) | 200 | PASS |
|
|
|
|
|
|
| U6 | POST | /api/user/users/change-password/ | 已登录改密:`SwagNew1` → `SwagFin1` | 200 | PASS |
|
|
|
|
|
|
| U8 | POST | /api/user/auth/refresh/ | refresh 换 access | 200 | PASS |
|
|
|
|
|
|
| /me | GET | /api/user/users/me/ | 当前用户信息 | 200 | PASS |
|
|
|
|
|
|
| U9 | GET | /api/user/users/ | 超级管理员列表(全员可见) | 200 | PASS |
|
|
|
|
|
|
| U9-b | GET | /api/user/users/ | 教师列表(仅名下 1 名学生) | 200 | PASS |
|
|
|
|
|
|
| U9-c | GET | /api/user/users/ | 普通学生列表 → `USER_NO_LIST_PERMISSION` | 403 | PASS |
|
|
|
|
|
|
| U10 | GET | /api/user/users/{id}/ | 管理员查看学生详情 | 200 | PASS |
|
|
|
|
|
|
| U10-b | GET | /api/user/users/{id}/ | 教师查看名下学生 | 200 | PASS |
|
|
|
|
|
|
| U10-c | GET | /api/user/users/{id}/ | 学生查看自己 | 200 | PASS |
|
|
|
|
|
|
| U7 | POST | /api/user/auth/logout/ | 吊销 refresh(放用户段最后,病例段前会重登) | 200 | PASS |
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
### 8.2 病例端(5个接口+3个场景)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
| ID | Method | URL | 测试什么 | 期望 | 结果 |
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
|
| C1 | POST | /api/case/cases/parse-pdf/ | 上传真实 PDF,解析 `data`(含 `exam_items`,PDF 无则 `[]`) | 200 / 500 / 429 | PASS |
|
|
|
|
|
|
| C2 | POST | /api/case/cases/generate-scoring-rules/ | 用 C1 的 `data`(或手工兜底)生成 `scoring_rules` | 200 / 500 / 429 | PASS |
|
|
|
|
|
|
| C3 | POST | /api/case/cases/full-create/ | C1 `data` + C2 `scoring_rules`;`department_name=Swagger儿科` | 201 | PASS |
|
|
|
|
|
|
| C3-exam | CHECK | C3 响应 `exam_items` | 条数与提交载荷一致 | 一致 | PASS |
|
|
|
|
|
|
| C3-db | CHECK | `case_exam_item` 表 | 条数与提交一致,`item_code` 与载荷一致 | 一致 | PASS |
|
|
|
|
|
|
| C4 | GET | /api/case/cases/{id}/full/ | 完整病例(含 `exam_items`) | 200 | PASS |
|
|
|
|
|
|
| C4-exam | CHECK | C4 响应 `exam_items` | 条数与 C3 提交一致 | 一致 | PASS |
|
|
|
|
|
|
| C5 | PATCH | /api/case/cases/{id}/full/ | 修改标题(不改编检查项) | 200 | PASS |
|
|
|
|
|
|
|
|
|
|
|
|
### 8.3 汇总与注意事项
|
|
|
|
|
|
|
2026-06-05 15:36:31 +08:00
|
|
|
|
- **总计 33个接口/场景**(`medical_platform` 实跑;含 C3-exam / C3-db 库表校验),全部 PASS
|
|
|
|
|
|
- 用户端覆盖 v1.1 认证 API:机构列表、CMS 登录(账号+密码+角色、缺角色拒绝)、代注册权限与机构范围(超管 201 且无 tokens / 未登录 401 / doctor 越权 403 / 医院管理员本机构 201、跨机构 403)、移动端验证码登录(已录入学生+机构匹配、非试用未录入拒绝、试用机构自动注册)、重置/改密、Token 刷新与吊销时序
|
2026-06-03 17:34:47 +08:00
|
|
|
|
- 病例端 **C1→C2→C3**:AI 解析检查项 → 随病例写入 `case_exam_item`;C3 将「儿科」覆盖为 `Swagger儿科`,避免 `CASE_DEPARTMENT_AMBIGUOUS`
|
|
|
|
|
|
- 脚本行为:`cache.clear()`、删除残留测试用户、`django_eval` 建 U9/U10 角色与病例机构科室、`time.sleep(1.2)` 等待 `invalidate_user_tokens`
|
|
|
|
|
|
- **前提**:dev server @ 8000、Redis(`.env` 的 `REDIS_URL`)、`.env` 指向已 `migrate` 的业务库;`SMS_PROVIDER=mock` 时验证码固定为 `123456`
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 运行方式
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-06-03 17:34:47 +08:00
|
|
|
|
# 全量单元测试(42 条)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
.venv\Scripts\python.exe manage.py test test -v2 --keepdb
|
|
|
|
|
|
|
|
|
|
|
|
# 分模块运行
|
|
|
|
|
|
.venv\Scripts\python.exe manage.py test test.test_user_happy -v2 --keepdb
|
|
|
|
|
|
.venv\Scripts\python.exe manage.py test test.test_case_happy -v2 --keepdb
|
|
|
|
|
|
.venv\Scripts\python.exe manage.py test test.test_user_negative -v2 --keepdb
|
|
|
|
|
|
.venv\Scripts\python.exe manage.py test test.test_case_negative -v2 --keepdb
|
|
|
|
|
|
|
|
|
|
|
|
# 单个测试
|
|
|
|
|
|
.venv\Scripts\python.exe manage.py test test.test_user_happy.UserAuthHappyPathTest.test_flow_register_login_me -v2 --keepdb
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
# Swagger Try-it-out(需先启动 dev server,走 .env 业务库而非 test_*)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
.venv\Scripts\python.exe manage.py runserver 8000
|
|
|
|
|
|
.venv\Scripts\python.exe test/swagger_tryout.py
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**前提条件**:
|
2026-06-03 17:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
**单元测试**:
|
2026-05-29 15:58:00 +08:00
|
|
|
|
1. MySQL 运行,`test_medical_training` 数据库已创建(首次运行去掉 `--keepdb` 自动创建)
|
|
|
|
|
|
2. 虚拟环境已激活
|
|
|
|
|
|
3. Redis **需要**运行(测试直接使用 Redis 缓存)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
**Swagger 脚本**(见第 8 节):
|
|
|
|
|
|
1. `.env` 中 `DB_*`、`REDIS_URL`、`DEEPSEEK_API_KEY` 等已配置(常用库名 `medical_platform`)
|
|
|
|
|
|
2. 已对业务库执行 `migrate`
|
|
|
|
|
|
3. Redis 与 dev server(`http://127.0.0.1:8000`)已启动
|
|
|
|
|
|
4. 根目录存在 PDF:`儿科 病例样例(SOAP+循证).pdf`
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 测试日志记录
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 单元测试 — API 访问日志
|
|
|
|
|
|
|
|
|
|
|
|
通过 `APIAccessLogMiddleware`(`config/middleware.py`)自动记录所有 `/api/` 请求和响应到日志文件。
|
|
|
|
|
|
|
|
|
|
|
|
| 项目 | 说明 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 日志文件 | `logs/api-access-YYYY-MM-DD.log` |
|
|
|
|
|
|
| 记录内容 | 请求方法、路径、请求头、Content-Type、用户 ID、查询参数、状态码、耗时、请求体、响应体(完整原文,含 token、密码等) |
|
|
|
|
|
|
| 截断阈值 | 请求体/响应体超过 2000 字符自动截断 |
|
|
|
|
|
|
| Multipart 处理 | 提取表单字段+ 文件名和大小,不记录原始二进制内容 |
|
|
|
|
|
|
|
|
|
|
|
|
**注意**:单元测试使用 Django TestCase,每个测试结束后事务自动回滚,测试数据不会持久化到数据库。但中间件在视图执行过程中已将日志写入文件,因此日志是完整的。
|
|
|
|
|
|
|
|
|
|
|
|
### 10.2 Swagger 脚本 — 独立日志
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
`test/swagger_tryout.py` 将每个接口调用的完整请求体和响应体记录到独立日志文件(与 `api-access` 日志分离,便于对照 Swagger 手测)。
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
| 项目 | 说明 |
|
|
|
|
|
|
|---|---|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
| 日志文件 | `logs/test-swagger-YYYY-MM-DD.log`(按日追加) |
|
|
|
|
|
|
| 记录内容 | 接口 ID(如 U4-new、U9-b、C3)、方法、URL、期望/实际状态码、请求头、请求体 JSON、响应体 JSON;过长响应截断 2000 字符 |
|
|
|
|
|
|
| 控制台输出 | 仅摘要行(PASS/FAIL + `parse_id`/`is_new_user`/`count` 等);`[INFO]` 行说明 C3 载荷来源或 SKIP 原因 |
|
|
|
|
|
|
| 失败退出码 | 任一接口 FAIL → `sys.exit(1)`,汇总列出失败项 |
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 10.3 日志示例
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-06-03 17:34:47 +08:00
|
|
|
|
# api-access 日志(单元测试 — 管理员代注册)
|
|
|
|
|
|
POST /api/user/auth/register/ | user=None | status=201 | 399ms
|
|
|
|
|
|
>>> headers: {"Content-Type": "multipart/form-data"}
|
|
|
|
|
|
>>> body: {"phone": "13900000001", "real_name": "张三", "institution_name": "测试医院", "institution_type": "hospital"}
|
|
|
|
|
|
<<< body: {"message": "注册成功", "user": {...}, "tokens": {...}}
|
|
|
|
|
|
|
|
|
|
|
|
# api-access 日志(验证码登录 — 自动注册新用户)
|
|
|
|
|
|
POST /api/user/auth/login-code/ | user=None | status=201 | 12ms
|
2026-05-29 15:58:00 +08:00
|
|
|
|
>>> headers: {"Content-Type": "application/json"}
|
2026-06-03 17:34:47 +08:00
|
|
|
|
>>> body: {"phone": "13900000002", "code": "481108", "institution_name": "测试医院", "institution_type": "hospital"}
|
|
|
|
|
|
<<< body: {"message": "注册并登录成功", "user": {...}, "tokens": {...}, "is_new_user": true}
|
2026-05-29 15:58:00 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 测试结论
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
- ✅ 全部 **42 条** 单元测试通过(13 happy-path + 29 negative)
|
|
|
|
|
|
- ✅ **25 个** Swagger Try-it-out 场景全部通过(含 C1 `exam_items` 解析、C3→`case_exam_item` 落库校验)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
- ✅ 用户端 11 个接口功能正常(含 U9 用户列表、U10 用户详情的角色分级权限)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
- ✅ 病例端 C1/C3 支持检查项;C3 与 `case_exam_item` 表写入已验证(`medical_platform`)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
- ✅ 限流、越权、字段校验、事务回滚、AI Schema 校验 均有覆盖
|
|
|
|
|
|
- ✅ U9/U10 权限矩阵验证:管理员全员可见、教师仅名下活跃学生、学生/医生 403、已结束关系 403
|
|
|
|
|
|
- ✅ `.env.example` 与代码完全一致,敏感信息已替换为占位符
|
2026-06-03 17:34:47 +08:00
|
|
|
|
- ✅ 测试过程中发现 8 个问题,均已修复(见第 5 节,Bug-7/8 为登录注册重构后 Review 发现)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
- ✅ 完整的测试日志记录:单元测试 → API 访问日志,Swagger 脚本 → 独立日志文件
|