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 修复验证:
python manage.py spectacular --validate --fail-on-warn 退出码 0
Bug-6 日志修复验证:
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. 运行方式
前提条件:
- MySQL 运行,
test_medical_training 数据库已创建(首次运行去掉 --keepdb 自动创建)
- 虚拟环境已激活
- Redis 需要运行(测试直接使用 Redis 缓存)
- 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 日志示例
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