# D8 测试文档 > 测试日期:2026-05-29(U9/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