Files
medical_training/test/测试文档-D8.md
T

365 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# D8 测试文档
> 测试日期:2026-05-29U9/U10 补充)
> 测试人员:Claude AI + 人工审核
> 测试环境:Windows / Python 3.14 / Django 5.0 / MySQL 8 / Redis
---
## 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 |
| 病例域 happy-path | `test_case_happy.py` | 2 | 2 | 0 |
| 用户域 negative | `test_user_negative.py` | 17 | 17 | 0 |
| 病例域 negative | `test_case_negative.py` | 11 | 11 | 0 |
| **合计** | | **41** | **41** | **0** |
---
## 3. Happy-Path 测试结果
### 3.1 用户域(11 条流程)
| ID | 测试方法 | 测试什么 | 结果 |
|---|---|---|---|
| HP-1 | `test_flow_register_login_me` | **新用户注册全流程**:发送短信验证码 → 用验证码+密码注册账号 → 用密码登录 → 查看个人信息(确认手机号和姓名正确) | PASS |
| HP-2 | `test_flow_code_login` | **验证码登录**:已有账号的用户,发送登录验证码 → 用手机号+验证码登录(不需要密码)→ 查看个人信息确认身份正确 | PASS |
| 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 |
### 3.2 病例域(2 条流程)
| ID | 测试方法 | 测试什么 | 结果 |
|---|---|---|---|
| HP-5 | `test_flow_form_create_read_update` | **手工录入病例全流程**:用表单数据创建一个传统病例(含 2 条评分规则)→ 查看完整病例确认数据正确 → 修改标题+诊断+减少为 1 条评分规则 → 再次查看确认修改生效 → 检查数据库记录是否一致 | PASS |
| HP-6 | `test_flow_pdf_mock_full_pipeline` | **PDF 上传到创建病例的完整流水线**(AI 部分用 mock 模拟):上传 PDF 文件 → AI 解析出病例结构化数据 → 用解析结果生成评分规则 → 组装数据创建病例 → 查看完整病例 → 修改标题 → 确认修改生效 | PASS |
---
## 4. Negative 测试结果
### 4.1 用户域(17 条)
| 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 |
| N4 | `test_register_invalid_phone_400` | **手机号格式错误**:用 "123"(不是 11 位手机号)去注册,系统拒绝并提示手机号不合法 | 400 | PASS |
| N5 | `test_register_weak_password_400` | **密码太简单**:用 "123" 作为密码注册,系统拒绝并提示密码强度不够(要求大小写字母+数字,至少 8 位) | 400 | PASS |
| N6 | `test_register_duplicate_phone_400` | **手机号已被注册**:先创建一个用户,再用同一个手机号注册第二次,系统拒绝并提示该手机号已注册 | 400 | PASS |
| N7 | `test_login_wrong_password` | **密码错误**:用正确的手机号但错误的密码登录,系统拒绝并提示账号或密码错误 | 400 | PASS |
| N8 | `test_login_account_lock_423` | **连续输错密码被锁定**:连续 5 次输入错误密码,第 6 次登录时系统锁定账号,返回"账号已锁定"(防暴力破解) | 423 | PASS |
| 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 |
### 4.2 病例域(11 条)
| 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 |
| 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 配置)
- **严重度**:中(审计日志完全丢失,影响安全审计能力)
### 修复验证
**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 |
|---|---|---|---|
| U1 发送验证码 | POST /api/user/auth/send-code/ | HP-1,2,3 | N1(限流) |
| U2 注册 | POST /api/user/auth/register/ | HP-1 | N4,N5,N6 |
| U3 密码登录 | POST /api/user/auth/login/ | HP-1,3,4 | N7,N8 |
| U4 验证码登录 | POST /api/user/auth/login-code/ | HP-2 | — |
| 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 |
|---|---|---|---|
| C1 PDF 解析 | POST /api/case/cases/parse-pdf/ | HP-6 | N18,N20,N21 |
| C2 生成评分规则 | POST /api/case/cases/generate-scoring-rules/ | HP-6 | — |
| C3 创建病例 | POST /api/case/cases/full-create/ | HP-5,6 | N11-N14,N16,N19 |
| 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 接口验证
> 脚本:`test/swagger_tryout.py`
> 运行方式:启动 `python manage.py runserver 8000` 后执行 `.venv\Scripts\python.exe test/swagger_tryout.py`
> PDF 文件:项目根目录 `儿科 病例样例(SOAP+循证).pdf`(真实临床 PDF
### 8.1 用户端(15 个接口/场景)
| 接口 | Method | URL | 测试什么 | 期望 | 实际 | 结果 |
|---|---|---|---|---|---|---|
| U1 发送验证码 | POST | /api/user/auth/send-code/ | 向手机号发送注册验证码 | 200 | 200 | PASS |
| U2 注册 | POST | /api/user/auth/register/ | 用验证码+密码+姓名注册新账号 | 201 | 201 | PASS |
| U3 密码登录 | POST | /api/user/auth/login/ | 用手机号+密码登录,拿到 JWT token | 200 | 200 | PASS |
| U4 验证码登录 | POST | /api/user/auth/login-code/ | 用手机号+验证码免密登录 | 200 | 200 | PASS |
| U5 重置密码 | POST | /api/user/auth/reset-password/ | 忘记密码后用验证码设置新密码 | 200 | 200 | PASS |
| U6 修改密码 | POST | /api/user/users/change-password/ | 已登录用户修改密码(需旧密码验证) | 200 | 200 | PASS |
| U8 刷新 Token | POST | /api/user/auth/refresh/ | 用 refresh token 换取新的 access token | 200 | 200 | PASS |
| /me 个人信息 | GET | /api/user/users/me/ | 查看当前登录用户的完整个人信息 | 200 | 200 | PASS |
| U9 管理员列表 | GET | /api/user/users/ | 管理员获取用户列表,确认能看到全部用户 | 200 | 200 | PASS |
| U9-b 教师列表 | GET | /api/user/users/ | 教师获取用户列表,确认只能看到自己名下的 1 个学生 | 200 | 200 | PASS |
| U9-c 普通用户列表 | GET | /api/user/users/ | 普通用户(学生)获取列表被拒绝,没有权限 | 403 | 403 | PASS |
| U10 管理员查看详情 | GET | /api/user/users/{id}/ | 管理员查看任意用户的详细信息 | 200 | 200 | PASS |
| U10-b 教师查看学生 | GET | /api/user/users/{id}/ | 教师查看自己名下学生的详细信息 | 200 | 200 | PASS |
| U10-c 用户查看自己 | GET | /api/user/users/{id}/ | 普通用户查看自己的详细信息 | 200 | 200 | PASS |
| U7 退出登录 | POST | /api/user/auth/logout/ | 退出登录,吊销 refresh token 使其失效 | 200 | 200 | PASS |
### 8.2 病例端(5 个接口)
| 接口 | Method | URL | 测试什么 | 期望 | 实际 | 结果 |
|---|---|---|---|---|---|---|
| C1 PDF 解析 | POST | /api/case/cases/parse-pdf/ | 上传真实 PDF 文件,DeepSeek AI 解析出病例结构化数据(病名、症状、诊断等) | 200 | 200 | PASS |
| C2 生成评分规则 | POST | /api/case/cases/generate-scoring-rules/ | 用 C1 的解析结果让 AI 自动生成评分规则(如"诊断准确性""治疗方案"等维度) | 200 | 200 | PASS |
| C3 创建病例 | POST | /api/case/cases/full-create/ | 把 C1 的病例数据 + C2 的评分规则组装起来,创建完整病例 | 201 | 201 | PASS |
| C4 完整查看 | GET | /api/case/cases/{id}/full/ | 查看刚创建的病例完整信息(主表+子表+评分规则) | 200 | 200 | PASS |
| C5 编辑草稿 | PATCH | /api/case/cases/{id}/full/ | 修改草稿病例的标题,确认修改成功 | 200 | 200 | PASS |
### 8.3 汇总
- **总计 20 个接口/场景,全部 PASS**
- C1→C2→C3 走完了真实 PDF 上传 → DeepSeek AI 解析 → AI 生成评分规则 → 创建病例的完整流水线
- U9/U10 验证了管理员、教师、普通用户三种角色的列表和详情权限控制
- 脚本自动清理 Redis 缓存、注入验证码、处理 token 失效时序(`time.sleep(1.2)`
---
## 9. 运行方式
```bash
# 全量单元测试(41 条)
.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
# Swagger Try-it-out(需先启动 dev server
.venv\Scripts\python.exe manage.py runserver 8000
.venv\Scripts\python.exe test/swagger_tryout.py
```
**前提条件**
1. MySQL 运行,`test_medical_training` 数据库已创建(首次运行去掉 `--keepdb` 自动创建)
2. 虚拟环境已激活
3. Redis **需要**运行(测试直接使用 Redis 缓存)
4. Swagger Try-it-out 脚本额外需要 Django dev server 运行在 8000 端口
---
## 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 脚本 — 独立日志
`test/swagger_tryout.py` 将每个接口调用的完整请求体和响应体记录到独立日志文件。
| 项目 | 说明 |
|---|---|
| 日志文件 | `logs/test-swagger-YYYY-MM-DD.log` |
| 记录内容 | 接口 ID、方法、URL、期望状态码、实际状态码、请求头、请求体 JSON、响应体 JSON(完整原文) |
| 控制台输出 | 仅显示摘要行(PASS/FAIL + 关键信息),详细请求/响应体仅写入日志文件 |
### 10.3 日志示例
```
# api-access 日志(单元测试)
2026-05-29 11:40:52,353 INFO [api_access] POST /api/user/auth/register/ | user=None | status=201 | 399ms
>>> headers: {"Content-Type": "application/json"}
>>> body: {"phone": "13900000001", "code": "308868", "password": "TestPass1", "real_name": "张三"}
<<< body: {"message": "注册成功", "user": {...}, "tokens": {"access": "eyJhbGci...", "refresh": "eyJhbGci..."}}
# test-swagger 日志(Swagger 脚本)
PASS U2 POST /api/user/auth/register/ expect=201 got=201
>>> body: {"phone": "13700000099", "code": "877405", "password": "TestPass1", "real_name": "Swagger测试"}
<<< body: {"message": "注册成功", "user": {...}, "tokens": {"access": "eyJhbGci...", "refresh": "eyJhbGci..."}}
```
---
## 11. 测试结论
- ✅ 全部 **41 条** 单元测试通过(13 happy-path + 28 negative
-**20 个** Swagger Try-it-out 接口验证全部通过(含真实 PDF + DeepSeek AI 完整流水线)
- ✅ 用户端 11 个接口功能正常(含 U9 用户列表、U10 用户详情的角色分级权限)
- ✅ 病例端 5 个接口功能正常
- ✅ 限流、越权、字段校验、事务回滚、AI Schema 校验 均有覆盖
- ✅ U9/U10 权限矩阵验证:管理员全员可见、教师仅名下活跃学生、学生/医生 403、已结束关系 403
-`.env.example` 与代码完全一致,敏感信息已替换为占位符
- ✅ 测试过程中发现 6 个问题,均已修复(见第 5 节)
- ✅ 完整的测试日志记录:单元测试 → API 访问日志,Swagger 脚本 → 独立日志文件
- ✅ 未发现业务代码 Bug