feat: update medical training case and auth modules
This commit is contained in:
+158
-28
@@ -1,7 +1,12 @@
|
||||
"""
|
||||
Swagger Try-it-out 等效脚本:逐个调用所有接口,验证可达性和基本功能。
|
||||
运行方式:.venv\\Scripts\\python.exe test/swagger_tryout.py
|
||||
前提:Django dev server 已在 http://127.0.0.1:8000 运行,Redis 已启动。
|
||||
前提:Django dev server 已在 http://127.0.0.1:8000 运行,Redis 已启动;.env 指向目标库。
|
||||
认证流程(与当前 API 一致):
|
||||
U1 发码(login) → U2 管理员代注册(机构编码+名称, 默认密码 Pass+手机号)
|
||||
→ U3 密码登录 → U4 验证码登录(机构信息) → U4-new 新号自动注册
|
||||
→ U5 重置密码 → U6 改密 → U8 刷新 → /me → U9/U10 → U7 退出
|
||||
病例段:C1 解析 exam_items → C3 full-create 写入 case_exam_item(校验响应条数与库表条数)
|
||||
日志输出:logs/test-swagger-YYYY-MM-DD.log(含完整请求体和响应体)
|
||||
"""
|
||||
|
||||
@@ -98,6 +103,25 @@ def get_sms_code(phone, scene):
|
||||
return val if val and val != 'None' else None
|
||||
|
||||
|
||||
def count_case_exam_items(case_id):
|
||||
"""查询 medical_platform.case_exam_item 中该病例的检查项条数。"""
|
||||
val = django_eval(
|
||||
f'from apps.case.models import CaseExamItem; '
|
||||
f'print(CaseExamItem.objects.filter(case_id={case_id}).count())'
|
||||
)
|
||||
return int(val) if val and val.isdigit() else 0
|
||||
|
||||
|
||||
def list_case_exam_item_codes(case_id):
|
||||
"""返回该病例在库中的 item_code 列表(逗号分隔)。"""
|
||||
val = django_eval(
|
||||
f'from apps.case.models import CaseExamItem; '
|
||||
f'codes = list(CaseExamItem.objects.filter(case_id={case_id}).order_by("display_order", "id").values_list("item_code", flat=True)); '
|
||||
f'print(",".join(codes))'
|
||||
)
|
||||
return val or ''
|
||||
|
||||
|
||||
def inject_sms_code(phone, scene, code='123456'):
|
||||
"""手动注入短信验证码到 Redis。"""
|
||||
django_eval(
|
||||
@@ -114,63 +138,126 @@ django_eval('from django.core.cache import cache; cache.clear(); print("OK")')
|
||||
|
||||
# 删除上次可能残留的测试用户
|
||||
PHONE = '13700000099'
|
||||
PHONE_ALT = '13700000098' # U4 未注册自动注册专用
|
||||
INST_CODE = 'SWAG_TEST_HOSP'
|
||||
INST_NAME = 'Swagger测试医院'
|
||||
# 全库唯一科室名,避免 resolve_department("儿科") 命中多条 → CASE_DEPARTMENT_AMBIGUOUS
|
||||
DEPT_NAME = 'Swagger儿科'
|
||||
# C3 手工兜底时的检查项(C1 有解析结果时优先用 AI 的 exam_items)
|
||||
SAMPLE_EXAM_ITEMS = [
|
||||
{
|
||||
'item_code': 'blood_routine',
|
||||
'item_name': '血常规',
|
||||
'item_type': 'lab',
|
||||
'category': '实验室检查',
|
||||
'result_text': 'WBC 10×10^9/L',
|
||||
'result_structured': {'wbc': '10×10^9/L'},
|
||||
'is_key': True,
|
||||
'is_abnormal': False,
|
||||
'score_weight': 1.0,
|
||||
'display_order': 1,
|
||||
},
|
||||
{
|
||||
'item_code': 'crp',
|
||||
'item_name': 'CRP',
|
||||
'item_type': 'lab',
|
||||
'category': '实验室检查',
|
||||
'result_text': 'CRP 8 mg/L',
|
||||
'is_key': False,
|
||||
'is_abnormal': False,
|
||||
'score_weight': 1.0,
|
||||
'display_order': 2,
|
||||
},
|
||||
]
|
||||
django_eval(
|
||||
f'from apps.user.models import User; '
|
||||
f'User.objects.filter(phone="{PHONE}").delete(); print("cleaned")'
|
||||
f'User.objects.filter(phone__in=["{PHONE}","{PHONE_ALT}"]).delete(); print("cleaned")'
|
||||
)
|
||||
print('[准备] 完成\n')
|
||||
|
||||
s = requests.Session()
|
||||
PASSWORD = 'SwagTest1'
|
||||
# U2 管理员代注册默认密码:Pass + 手机号
|
||||
PASSWORD = f'Pass{PHONE}'
|
||||
|
||||
|
||||
def _institution_fields():
|
||||
return {'institution_code': INST_CODE, 'institution_name': INST_NAME}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section('用户端接口 (U1-U10)')
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# U1: 发送验证码 (register)
|
||||
u1_body = {'phone': PHONE, 'scene': 'register'}
|
||||
access = ''
|
||||
refresh = ''
|
||||
auth = {}
|
||||
|
||||
# U1: 发送验证码(login 场景,未注册用户也可发码)
|
||||
u1_body = {'phone': PHONE, 'scene': 'login'}
|
||||
r = s.post(f'{BASE}/api/user/auth/send-code/', json=u1_body)
|
||||
log('U1', 'POST', '/api/user/auth/send-code/', 200, r.status_code,
|
||||
req_headers=r.request.headers, req_body=u1_body,
|
||||
resp_headers=dict(r.headers), resp_body=r.json())
|
||||
|
||||
# U2: 注册
|
||||
code = get_sms_code(PHONE, 'register')
|
||||
if not code:
|
||||
code = inject_sms_code(PHONE, 'register')
|
||||
u2_body = {'phone': PHONE, 'code': code, 'password': PASSWORD, 'real_name': 'Swagger测试'}
|
||||
# U2: 管理员代注册(无需验证码,默认密码 Pass+手机号)
|
||||
u2_body = {
|
||||
'phone': PHONE,
|
||||
'real_name': 'Swagger测试',
|
||||
'role_type': 'student',
|
||||
**_institution_fields(),
|
||||
}
|
||||
r = s.post(f'{BASE}/api/user/auth/register/', json=u2_body)
|
||||
log('U2', 'POST', '/api/user/auth/register/', 201, r.status_code,
|
||||
req_headers=r.request.headers, req_body=u2_body,
|
||||
resp_headers=dict(r.headers), resp_body=r.json())
|
||||
if r.status_code == 201:
|
||||
tokens = r.json().get('tokens', {})
|
||||
access = tokens.get('access', '')
|
||||
refresh = tokens.get('refresh', '')
|
||||
|
||||
# U3: 密码登录
|
||||
# U3: 密码登录(Pass+手机号)
|
||||
u3_body = {'phone': PHONE, 'password': PASSWORD}
|
||||
r = s.post(f'{BASE}/api/user/auth/login/', json=u3_body)
|
||||
log('U3', 'POST', '/api/user/auth/login/', 200, r.status_code,
|
||||
req_headers=r.request.headers, req_body=u3_body,
|
||||
resp_headers=dict(r.headers), resp_body=r.json())
|
||||
tokens = r.json().get('tokens', {})
|
||||
access = tokens.get('access', '')
|
||||
refresh = tokens.get('refresh', '')
|
||||
auth = {'Authorization': f'Bearer {access}'}
|
||||
if r.status_code == 200:
|
||||
tokens = r.json().get('tokens', {})
|
||||
access = tokens.get('access', '')
|
||||
refresh = tokens.get('refresh', '')
|
||||
auth = {'Authorization': f'Bearer {access}'}
|
||||
|
||||
# U4: 验证码登录
|
||||
# U4: 验证码登录(已注册用户,需机构信息)
|
||||
r = s.post(f'{BASE}/api/user/auth/send-code/', json={'phone': PHONE, 'scene': 'login'})
|
||||
login_code = get_sms_code(PHONE, 'login')
|
||||
if not login_code:
|
||||
login_code = inject_sms_code(PHONE, 'login', '654321')
|
||||
u4_body = {'phone': PHONE, 'code': login_code}
|
||||
login_code = inject_sms_code(PHONE, 'login')
|
||||
u4_body = {'phone': PHONE, 'code': login_code, **_institution_fields()}
|
||||
r = s.post(f'{BASE}/api/user/auth/login-code/', json=u4_body)
|
||||
log('U4', 'POST', '/api/user/auth/login-code/', 200, r.status_code,
|
||||
req_headers=r.request.headers, req_body=u4_body,
|
||||
resp_headers=dict(r.headers), resp_body=r.json())
|
||||
if r.status_code == 200:
|
||||
if r.status_code in (200, 201):
|
||||
tokens = r.json().get('tokens', {})
|
||||
access = tokens.get('access', access)
|
||||
refresh = tokens.get('refresh', refresh)
|
||||
auth = {'Authorization': f'Bearer {access}'}
|
||||
|
||||
# U4-new: 验证码登录自动注册(新手机号 → 201)
|
||||
django_eval(f'from apps.user.models import User; User.objects.filter(phone="{PHONE_ALT}").delete()')
|
||||
r = s.post(f'{BASE}/api/user/auth/send-code/', json={'phone': PHONE_ALT, 'scene': 'login'})
|
||||
log('U4-pre', 'POST', '/api/user/auth/send-code/ (alt)', 200, r.status_code,
|
||||
req_body={'phone': PHONE_ALT, 'scene': 'login'})
|
||||
alt_code = get_sms_code(PHONE_ALT, 'login') or inject_sms_code(PHONE_ALT, 'login')
|
||||
u4_new_body = {'phone': PHONE_ALT, 'code': alt_code, **_institution_fields()}
|
||||
r = s.post(f'{BASE}/api/user/auth/login-code/', json=u4_new_body)
|
||||
u4_new_detail = ''
|
||||
if r.status_code in (200, 201):
|
||||
u4_new_detail = f'is_new_user={r.json().get("is_new_user")}'
|
||||
log('U4-new', 'POST', '/api/user/auth/login-code/ (auto-register)', [200, 201], r.status_code,
|
||||
u4_new_detail, req_body=u4_new_body, resp_body=r.json() if r.headers.get('content-type', '').startswith('application/json') else None)
|
||||
django_eval(f'from apps.user.models import User; User.objects.filter(phone="{PHONE_ALT}").delete()')
|
||||
|
||||
# U5: 重置密码
|
||||
NEW_PASSWORD = 'SwagNew1'
|
||||
r = s.post(f'{BASE}/api/user/auth/send-code/', json={'phone': PHONE, 'scene': 'reset'})
|
||||
@@ -332,14 +419,14 @@ tokens = r.json().get('tokens', {})
|
||||
access = tokens.get('access', '')
|
||||
auth = {'Authorization': f'Bearer {access}'}
|
||||
|
||||
# 确保科室存在
|
||||
# 确保机构与科室存在(与 U2/U4 同一 institution_code)
|
||||
django_eval(
|
||||
'from apps.user.models import Institution, Department; '
|
||||
'inst, _ = Institution.objects.get_or_create(name="测试医院", '
|
||||
' defaults={"type":"hospital","province":"北京","city":"北京"}); '
|
||||
'Department.objects.get_or_create(name="儿科", '
|
||||
' defaults={"institution":inst,"category":"临床"}); '
|
||||
'print("OK")'
|
||||
f'from apps.user.models import Institution, Department; '
|
||||
f'inst, _ = Institution.objects.get_or_create(code="{INST_CODE}", '
|
||||
f' defaults={{"name":"{INST_NAME}","type":"hospital","province":"北京","city":"北京"}}); '
|
||||
f'Department.objects.get_or_create(name="{DEPT_NAME}", institution=inst, '
|
||||
f' defaults={{"category":"临床"}}); '
|
||||
f'print("OK")'
|
||||
)
|
||||
|
||||
# C1: PDF 解析 — 使用项目真实 PDF 文件
|
||||
@@ -367,6 +454,12 @@ log('C1', 'POST', '/api/case/cases/parse-pdf/', c1_ok, r.status_code, detail,
|
||||
c1_data = None
|
||||
if r.status_code == 200:
|
||||
c1_data = body.get('data', {})
|
||||
if c1_data:
|
||||
c1_exam = c1_data.get('exam_items') or []
|
||||
_write_log(
|
||||
f' [INFO] C1 解析 exam_items: count={len(c1_exam)}, '
|
||||
f'codes={[x.get("item_code") for x in c1_exam if isinstance(x, dict)]}'
|
||||
)
|
||||
|
||||
c2_payload = c1_data if c1_data else {
|
||||
'title': 'Swagger测试病例',
|
||||
@@ -398,6 +491,7 @@ log('C2', 'POST', '/api/case/cases/generate-scoring-rules/', c2_ok, r.status_cod
|
||||
if c1_data and scoring_rules_from_ai:
|
||||
_write_log(' [INFO] C3 使用 C1 AI 解析 + C2 AI 评分规则组装载荷')
|
||||
payload = {**c1_data}
|
||||
payload['department_name'] = DEPT_NAME # C1 常返回「儿科」,库内重名会 400
|
||||
payload['scoring_rules'] = scoring_rules_from_ai
|
||||
else:
|
||||
_write_log(' [INFO] C3 使用手工表单载荷(C1/C2 未全部成功)')
|
||||
@@ -409,7 +503,7 @@ else:
|
||||
'description': '患儿,男,4 岁,因发热 3 天就诊。',
|
||||
'patient_age': 4,
|
||||
'patient_gender': 'male',
|
||||
'department_name': '儿科',
|
||||
'department_name': DEPT_NAME,
|
||||
'estimated_minutes': 30,
|
||||
'osce_enabled': False,
|
||||
'tags': '儿科,发热',
|
||||
@@ -432,20 +526,56 @@ else:
|
||||
'scoring_standard': '治疗方案合理',
|
||||
},
|
||||
],
|
||||
'exam_items': SAMPLE_EXAM_ITEMS,
|
||||
}
|
||||
payload_exam_items = payload.get('exam_items') or []
|
||||
_write_log(
|
||||
f' [INFO] C3 提交 exam_items: count={len(payload_exam_items)}, '
|
||||
f'codes={[x.get("item_code") for x in payload_exam_items if isinstance(x, dict)]}'
|
||||
)
|
||||
r = s.post(f'{BASE}/api/case/cases/full-create/', json=payload, headers=auth)
|
||||
c3_resp = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||
log('C3', 'POST', '/api/case/cases/full-create/', 201, r.status_code,
|
||||
req_body=payload, resp_body=c3_resp)
|
||||
case_id = None
|
||||
expected_exam_count = len(payload_exam_items)
|
||||
if r.status_code == 201:
|
||||
case_id = r.json()['case']['id']
|
||||
resp_exam = (c3_resp or {}).get('exam_items', [])
|
||||
resp_exam_count = len(resp_exam)
|
||||
log(
|
||||
'C3-exam', 'CHECK', f'/api/case/cases/{case_id}/ (resp exam_items)',
|
||||
expected_exam_count, resp_exam_count,
|
||||
f'codes={[e.get("item_code") for e in resp_exam]}',
|
||||
resp_body={'exam_items': resp_exam} if resp_exam else None,
|
||||
)
|
||||
db_exam_count = count_case_exam_items(case_id)
|
||||
db_codes = list_case_exam_item_codes(case_id)
|
||||
log(
|
||||
'C3-db', 'CHECK', f'case_exam_item case_id={case_id}',
|
||||
expected_exam_count, db_exam_count,
|
||||
f'db_codes={db_codes}',
|
||||
)
|
||||
else:
|
||||
_write_log(' SKIP C3-exam/C3-db — C3 未成功')
|
||||
|
||||
# C4: GET full
|
||||
if case_id:
|
||||
r = s.get(f'{BASE}/api/case/cases/{case_id}/full/', headers=auth)
|
||||
c4_detail = ''
|
||||
c4_resp = None
|
||||
if r.headers.get('content-type', '').startswith('application/json'):
|
||||
c4_resp = r.json()
|
||||
c4_exam = c4_resp.get('exam_items', [])
|
||||
c4_detail = f'exam_items={len(c4_exam)}(expect={expected_exam_count})'
|
||||
log('C4', 'GET', f'/api/case/cases/{case_id}/full/', 200, r.status_code,
|
||||
resp_body=r.json() if r.headers.get('content-type', '').startswith('application/json') else None)
|
||||
c4_detail, resp_body=c4_resp)
|
||||
if c4_resp is not None:
|
||||
log(
|
||||
'C4-exam', 'CHECK', f'/api/case/cases/{case_id}/full/ exam_items',
|
||||
expected_exam_count, len(c4_resp.get('exam_items', [])),
|
||||
f'codes={[e.get("item_code") for e in c4_resp.get("exam_items", [])]}',
|
||||
)
|
||||
else:
|
||||
_write_log(' SKIP C4 — C3 未返回 case_id')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user