feat: update login api

This commit is contained in:
2026-06-05 15:36:31 +08:00
parent fd0b3e1982
commit ba9fb33062
15 changed files with 714 additions and 163 deletions
+145 -41
View File
@@ -137,10 +137,17 @@ print('\n[准备] 清理 Redis 缓存...')
django_eval('from django.core.cache import cache; cache.clear(); print("OK")')
# 删除上次可能残留的测试用户
PHONE = '13700000099'
PHONE_ALT = '13700000098' # U4 未注册自动注册专用
SUPER_PHONE = '13700000090' # 超级管理员,执行 U2 代注册
SUPER_PWD = 'Super123'
PHONE = '13700000099' # CMS 用户(doctor),走 U3 密码登录
STUDENT_PHONE_U4 = '13700000097' # 已录入学生,走 U4 验证码登录(非试用机构需机构匹配)
PHONE_ALT = '13700000098' # U4 试用机构自动注册专用
INST_CODE = 'SWAG_TEST_HOSP'
INST_NAME = 'Swagger测试医院'
# 预留试用机构(与 apps.user.auth 常量保持一致)
TRIAL_INST_CODE = 'PKU_LAB_TRIAL'
TRIAL_INST_NAME = '北大医学部(实验室)试用'
CMS_ROLE = 'doctor' # PHONE 用户的 CMS 角色
# 全库唯一科室名,避免 resolve_department("儿科") 命中多条 → CASE_DEPARTMENT_AMBIGUOUS
DEPT_NAME = 'Swagger儿科'
# C3 手工兜底时的检查项(C1 有解析结果时优先用 AI 的 exam_items
@@ -171,7 +178,20 @@ SAMPLE_EXAM_ITEMS = [
]
django_eval(
f'from apps.user.models import User; '
f'User.objects.filter(phone__in=["{PHONE}","{PHONE_ALT}"]).delete(); print("cleaned")'
f'User.objects.filter(phone__in=["{SUPER_PHONE}","{PHONE}","{STUDENT_PHONE_U4}","{PHONE_ALT}"]).delete(); print("cleaned")'
)
# 预置超级管理员(U2 代注册需管理员身份)
django_eval(
f'from apps.user.models import User; '
f'User.objects.create_user(username="{SUPER_PHONE}", password="{SUPER_PWD}", '
f' phone="{SUPER_PHONE}", real_name="Swagger超管", role_type="super_admin", status=1); '
f'print("super ok")'
)
# 预置试用机构(仅增数据,不改表)
django_eval(
f'from apps.user.models import Institution; '
f'Institution.objects.get_or_create(code="{TRIAL_INST_CODE}", '
f' defaults={{"name":"{TRIAL_INST_NAME}","type":"hospital"}}); print("trial ok")'
)
print('[准备] 完成\n')
@@ -184,6 +204,16 @@ def _institution_fields():
return {'institution_code': INST_CODE, 'institution_name': INST_NAME}
def get_user_access_token(phone):
"""直接为指定用户签发 access token(用于非 CMS 角色,如 teacher 无法走 U3)。"""
return django_eval(
f'from apps.user.models import User; '
f'from rest_framework_simplejwt.tokens import RefreshToken; '
f'u=User.objects.get(phone="{phone}"); '
f'print(str(RefreshToken.for_user(u).access_token))'
)
# ═══════════════════════════════════════════════════════════════════════════════
section('用户端接口 (U1-U10)')
# ═══════════════════════════════════════════════════════════════════════════════
@@ -192,31 +222,51 @@ access = ''
refresh = ''
auth = {}
# U1: 发送验证码(login 场景,未注册用户也可发码
# INST-LIST: 移动端机构列表(不分页,登录前可调用
r = s.get(f'{BASE}/api/user/institution_list/')
inst_list_detail = ''
if r.status_code == 200:
items = r.json()
trial_flags = [i for i in items if i.get('is_trial')]
inst_list_detail = f'count={len(items)}, trial={len(trial_flags)}'
log('INST-LIST', 'GET', '/api/user/institution_list/', 200, r.status_code, inst_list_detail,
req_headers=r.request.headers, resp_headers=dict(r.headers),
resp_body=r.json() if r.headers.get('content-type', '').startswith('application/json') else None)
# 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: 管理员代注册(无需验证码,默认密码 Pass+手机号)
# U2: 管理员代注册(仅超管/医院管理员)。超管创建 CMS 角色 doctor,并自动创建机构
super_access = get_user_access_token(SUPER_PHONE)
super_auth = {'Authorization': f'Bearer {super_access}'}
u2_body = {
'phone': PHONE,
'real_name': 'Swagger测试',
'role_type': 'student',
'role_type': CMS_ROLE,
**_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,
r = s.post(f'{BASE}/api/user/auth/register/', json=u2_body, headers=super_auth)
u2_detail = 'has_tokens=%s' % ('tokens' in (r.json() if r.headers.get('content-type','').startswith('application/json') else {}))
log('U2', 'POST', '/api/user/auth/register/ (super_admin)', 201, r.status_code, u2_detail,
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: 密码登录(Pass+手机号
u3_body = {'phone': PHONE, 'password': PASSWORD}
# U2-tok: 代注册不返回 tokens(按管理员代注册语义
u2_has_tokens = 'tokens' in (r.json() if r.headers.get('content-type','').startswith('application/json') else {})
log('U2-tok', 'CHECK', 'register response has no tokens', False, u2_has_tokens)
# U2-neg1: 未登录代注册 → 401
r = s.post(f'{BASE}/api/user/auth/register/', json=u2_body)
log('U2-neg1', 'POST', '/api/user/auth/register/ (anon)', 401, r.status_code,
f'code={r.json().get("code","")}' if r.headers.get("content-type","").startswith("application/json") else "",
req_body={'phone': PHONE, '...': '...'}, resp_body=r.json() if r.headers.get('content-type','').startswith('application/json') else None)
# U3: CMS 密码登录(账号 + 密码 + 角色,三者必填)
u3_body = {'account': PHONE, 'password': PASSWORD, 'role': CMS_ROLE}
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,
@@ -227,36 +277,90 @@ if r.status_code == 200:
refresh = tokens.get('refresh', '')
auth = {'Authorization': f'Bearer {access}'}
# 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')
u4_body = {'phone': PHONE, 'code': login_code, **_institution_fields()}
# U3-neg: 缺少角色 → 400 AUTH_BAD_CREDENTIALS
r = s.post(f'{BASE}/api/user/auth/login/', json={'account': PHONE, 'password': PASSWORD})
log('U3-neg', 'POST', '/api/user/auth/login/ (no role)', 400, r.status_code,
f'code={r.json().get("code","")}', req_body={'account': PHONE, 'password': '***'},
resp_body=r.json())
# U2-neg2: 非管理员(doctor)代注册 → 403 USER_NO_REGISTER_PERMISSION
r = s.post(f'{BASE}/api/user/auth/register/',
json={'phone': '13700000095', 'real_name': '越权注册', 'role_type': 'student',
**_institution_fields()}, headers=auth)
log('U2-neg2', 'POST', '/api/user/auth/register/ (doctor)', 403, r.status_code,
f'code={r.json().get("code","")}' if r.headers.get("content-type","").startswith("application/json") else "",
resp_body=r.json() if r.headers.get('content-type','').startswith('application/json') else None)
# U2-ha / U2-ha-neg: 医院管理员仅能在本机构内代注册
HA_PHONE = '13700000094'
HA_STUDENT = '13700000093'
django_eval(
f'from apps.user.models import User, Institution; '
f'inst=Institution.objects.get(code="{INST_CODE}"); '
f'User.objects.filter(phone__in=["{HA_PHONE}","{HA_STUDENT}"]).delete(); '
f'User.objects.create_user(username="{HA_PHONE}", password="Hosp1234", phone="{HA_PHONE}", '
f' real_name="Swagger医院管理员", role_type="hospital_admin", institution=inst, status=1); '
f'print("hadmin ok")'
)
ha_auth = {'Authorization': f'Bearer {get_user_access_token(HA_PHONE)}'}
# U2-ha: 本机构建学生 → 201
ha_body = {'phone': HA_STUDENT, 'real_name': '本院学生', 'role_type': 'student', **_institution_fields()}
r = s.post(f'{BASE}/api/user/auth/register/', json=ha_body, headers=ha_auth)
log('U2-ha', 'POST', '/api/user/auth/register/ (hospital_admin own inst)', 201, r.status_code,
req_body=ha_body, resp_body=r.json() if r.headers.get('content-type','').startswith('application/json') else None)
# U2-ha-neg: 跨机构建账号 → 403 USER_INSTITUTION_SCOPE_FORBIDDEN
ha_neg_body = {'phone': '13700000092', 'real_name': '跨院学生', 'role_type': 'student',
'institution_code': 'SWAG_OTHER_HOSP', 'institution_name': 'Swagger其它医院'}
r = s.post(f'{BASE}/api/user/auth/register/', json=ha_neg_body, headers=ha_auth)
log('U2-ha-neg', 'POST', '/api/user/auth/register/ (hospital_admin cross inst)', 403, r.status_code,
f'code={r.json().get("code","")}' if r.headers.get("content-type","").startswith("application/json") else "",
req_body=ha_neg_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__in=["{HA_PHONE}","{HA_STUDENT}"]).delete()')
# U4: 验证码登录(移动端,已录入学生 + 机构匹配 → 200)
# 预创建学生,institution 与所选机构一致
django_eval(
f'from apps.user.models import User, Institution; '
f'inst=Institution.objects.get(code="{INST_CODE}"); '
f'User.objects.filter(phone="{STUDENT_PHONE_U4}").delete(); '
f'User.objects.create_user(username="{STUDENT_PHONE_U4}", password=None, '
f' phone="{STUDENT_PHONE_U4}", real_name="Swagger学生U4", role_type="student", '
f' institution=inst, status=1); print("student ok")'
)
r = s.post(f'{BASE}/api/user/auth/send-code/', json={'phone': STUDENT_PHONE_U4, 'scene': 'login'})
login_code = get_sms_code(STUDENT_PHONE_U4, 'login') or inject_sms_code(STUDENT_PHONE_U4, 'login')
u4_body = {'phone': STUDENT_PHONE_U4, 'code': login_code, 'institution_code': INST_CODE}
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,
u4_detail = f'is_new_user={r.json().get("is_new_user")}' if r.status_code in (200, 201) else f'code={r.json().get("code","")}'
log('U4', 'POST', '/api/user/auth/login-code/ (registered student)', 200, r.status_code, u4_detail,
req_headers=r.request.headers, req_body=u4_body,
resp_headers=dict(r.headers), resp_body=r.json())
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)
# U4-neg: 非试用机构 + 未录入手机号 → 403 AUTH_NOT_REGISTERED
unreg_phone = '13700000096'
django_eval(f'from apps.user.models import User; User.objects.filter(phone="{unreg_phone}").delete()')
inject_sms_code(unreg_phone, 'login')
u4_neg_body = {'phone': unreg_phone, 'code': '123456', 'institution_code': INST_CODE}
r = s.post(f'{BASE}/api/user/auth/login-code/', json=u4_neg_body)
log('U4-neg', 'POST', '/api/user/auth/login-code/ (unregistered, non-trial)', 403, r.status_code,
f'code={r.json().get("code","")}', req_body=u4_neg_body, resp_body=r.json())
# U4-new: 试用机构验证码登录自动注册(新手机号 → 201 is_new_user=true
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()}
u4_new_body = {'phone': PHONE_ALT, 'code': alt_code, 'institution_code': TRIAL_INST_CODE}
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,
log('U4-new', 'POST', '/api/user/auth/login-code/ (trial auto-register)', 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()')
django_eval(f'from apps.user.models import User; User.objects.filter(phone__in=["{PHONE_ALT}","{STUDENT_PHONE_U4}","{unreg_phone}"]).delete()')
# U5: 重置密码
NEW_PASSWORD = 'SwagNew1'
@@ -273,8 +377,8 @@ log('U5', 'POST', '/api/user/auth/reset-password/', 200, r.status_code,
# reset-password 调用 invalidate_user_tokens(time()+1),必须等 1s 再登录
time.sleep(1.2)
# 用新密码重新登录
r = s.post(f'{BASE}/api/user/auth/login/', json={'phone': PHONE, 'password': NEW_PASSWORD})
# 用新密码重新登录CMS
r = s.post(f'{BASE}/api/user/auth/login/', json={'account': PHONE, 'password': NEW_PASSWORD, 'role': CMS_ROLE})
tokens = r.json().get('tokens', {})
access = tokens.get('access', '')
refresh = tokens.get('refresh', '')
@@ -291,8 +395,8 @@ log('U6', 'POST', '/api/user/users/change-password/', 200, r.status_code,
# change-password 同样 invalidate_user_tokens(time()+1)
time.sleep(1.2)
# 用最终密码重新登录
r = s.post(f'{BASE}/api/user/auth/login/', json={'phone': PHONE, 'password': FINAL_PASSWORD})
# 用最终密码重新登录CMS
r = s.post(f'{BASE}/api/user/auth/login/', json={'account': PHONE, 'password': FINAL_PASSWORD, 'role': CMS_ROLE})
tokens = r.json().get('tokens', {})
access = tokens.get('access', '')
refresh = tokens.get('refresh', '')
@@ -336,8 +440,9 @@ django_eval(
f'print(f"admin={{admin.id}} teacher={{teacher.id}} student={{student.id}}")'
)
# 管理员登录
r = s.post(f'{BASE}/api/user/auth/login/', json={'phone': ADMIN_PHONE, 'password': ROLE_PWD})
# 管理员登录CMSsuper_admin
r = s.post(f'{BASE}/api/user/auth/login/',
json={'account': ADMIN_PHONE, 'password': ROLE_PWD, 'role': 'super_admin'})
admin_tokens = r.json().get('tokens', {})
admin_auth = {'Authorization': f'Bearer {admin_tokens.get("access", "")}'}
admin_refresh = admin_tokens.get('refresh', '')
@@ -353,10 +458,9 @@ log('U9', 'GET', '/api/user/users/', 200, r.status_code, u9_detail,
req_headers=r.request.headers, resp_headers=dict(r.headers), resp_body=r.json())
# U9-b: 教师获取用户列表(仅名下学生)
r_teacher_login = s.post(f'{BASE}/api/user/auth/login/',
json={'phone': TEACHER_PHONE, 'password': ROLE_PWD})
teacher_tokens = r_teacher_login.json().get('tokens', {})
teacher_auth = {'Authorization': f'Bearer {teacher_tokens.get("access", "")}'}
# teacher 角色不在 CMS 登录角色内(U3 仅 CMS 角色),直接签发 token 用于权限演示
teacher_access = get_user_access_token(TEACHER_PHONE)
teacher_auth = {'Authorization': f'Bearer {teacher_access}'}
r = s.get(f'{BASE}/api/user/users/', headers=teacher_auth)
u9b_detail = ''
@@ -414,7 +518,7 @@ section('病例端接口 (C1-C5)')
# 重新登录(logout 吊销了上一个 refresh
time.sleep(1.2)
r = s.post(f'{BASE}/api/user/auth/login/', json={'phone': PHONE, 'password': FINAL_PASSWORD})
r = s.post(f'{BASE}/api/user/auth/login/', json={'account': PHONE, 'password': FINAL_PASSWORD, 'role': CMS_ROLE})
tokens = r.json().get('tokens', {})
access = tokens.get('access', '')
auth = {'Authorization': f'Bearer {access}'}