Files
medical_training/test/swagger_cms.py
T

246 lines
15 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
CMS 端全接口 Swagger Try-it-out 脚本(真实 HTTP)。
- 覆盖:超级管理员(用户/医院/科室)、医院管理员(人员/师生关系)、带教医生(我的学生)。
- 详细请求/响应写入 logs/test-swagger-cms-YYYY-MM-DD.log。
- 导入/导出的 .xlsx 真实文件保存到 docx/CMS-excel样例/。
- 真实请求/响应样例写入 logs/cms-swagger-examples.json(供回填 CSV)。
前提:Django dev server 运行在 http://127.0.0.1:8000Redis 已启动。
运行:.venv\\Scripts\\python.exe test/swagger_cms.py
"""
import io
import os
import sys
import json
import subprocess
from datetime import datetime
import requests
from openpyxl import Workbook
sys.stdout.reconfigure(encoding='utf-8')
BASE = 'http://127.0.0.1:8000'
PYTHON = r'D:\01Agent\medical_training\.venv\Scripts\python.exe'
CWD = r'D:\01Agent\medical_training'
LOG_DIR = os.path.join(CWD, 'logs')
EXCEL_DIR = os.path.join(CWD, 'docx', 'CMS-excel样例')
os.makedirs(EXCEL_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, f'test-swagger-cms-{datetime.now():%Y-%m-%d}.log')
EXAMPLES_FILE = os.path.join(LOG_DIR, 'cms-swagger-examples.json')
_fh = open(LOG_FILE, 'w', encoding='utf-8')
examples = {}
results = []
def w(text=''):
_fh.write(text + '\n'); _fh.flush(); print(text)
def django_eval(code):
pre = ('import django,os;os.environ.setdefault("DJANGO_SETTINGS_MODULE","config.settings");django.setup();')
p = subprocess.run([PYTHON, '-c', pre + code], capture_output=True, text=True, cwd=CWD, encoding='utf-8')
return (p.stdout or '').strip()
def make_xlsx_bytes(headers, rows):
wb = Workbook(); ws = wb.active
ws.append(headers)
for r in rows:
ws.append(r)
buf = io.BytesIO(); wb.save(buf); return buf.getvalue()
def call(code, name, method, path, token=None, json_body=None, params=None,
file_bytes=None, file_name=None, save_as=None, expect=200):
"""发起请求 + 记录日志 + 收集样例。"""
headers = {'Authorization': f'Bearer {token}'} if token else {}
files = {'file': (file_name, file_bytes, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')} if file_bytes else None
r = requests.request(method, BASE + path, headers=headers, json=json_body, params=params, files=files)
ct = r.headers.get('content-type', '')
is_json = ct.startswith('application/json')
resp = r.json() if is_json else f'<xlsx 文件 {len(r.content)} 字节>'
if save_as:
full = os.path.join(EXCEL_DIR, save_as)
with open(full, 'wb') as f:
f.write(r.content)
# 请求样例(给前端看:实际传了什么)
if json_body is not None:
req_example = json.dumps(json_body, ensure_ascii=False)
elif file_bytes is not None:
req_example = f'multipart/form-data: file={file_name}'
elif params:
req_example = '?' + '&'.join(f'{k}={v}' for k, v in params.items())
else:
req_example = '(无 body,仅 Header Authorization)' if '{' not in path else '(路径参数 + Header Authorization)'
ok = 'PASS' if r.status_code in (expect if isinstance(expect, (list, tuple)) else [expect]) else 'FAIL'
results.append((code, ok, r.status_code))
examples[code] = {'name': name, 'method': method, 'path': path, 'status': r.status_code,
'req': req_example, 'resp': resp, 'saved': save_as}
w(f'\n[{ok}] {code} {name} -> {method} {path} (status={r.status_code})')
w(f' 请求: {req_example}')
body_str = json.dumps(resp, ensure_ascii=False, indent=2) if is_json else resp
if len(body_str) > 1500:
body_str = body_str[:1500] + f' ...(截断, 共{len(body_str)}字符)'
w(f' 响应: {body_str}')
return r
# ─────────────────────────────────────────────────────────────────────────────
w('=' * 90)
w(f' CMS 全接口 Swagger 测试 {datetime.now():%Y-%m-%d %H:%M:%S}')
w('=' * 90)
# 1) 准备数据:两机构、两科室、超管/医院管理员/医生/学生/他院学生,并签发 token
w('\n[准备] 建测试机构/科室/用户、签发 token ...')
setup = django_eval(r'''
from apps.user.models import User, Institution, Department, TeacherStudentRelation
from rest_framework_simplejwt.tokens import RefreshToken
# 物理清理可能的残留(含软删),保证脚本可重复运行
ph = ["13800009000","13800009001","13800009002","13800009003","13800009004","13800009010","13800009011","13800009012","13800009020","13800009021","13800009022"]
for u in User.all_objects.filter(phone__in=ph): u.hard_delete()
Institution.all_objects.filter(code__startswith="SWG-CMS").hard_delete()
for d in Department.all_objects.filter(name__startswith="Swagger"): d.hard_delete()
instA,_ = Institution.objects.get_or_create(code="SWG-CMS-A", defaults={"name":"Swagger甲医院","type":"hospital","level":"三甲","province":"北京","city":"北京"})
instB,_ = Institution.objects.get_or_create(code="SWG-CMS-B", defaults={"name":"Swagger乙医院","type":"hospital"})
dN,_ = Department.objects.get_or_create(name="内科", defaults={"category":"临床"})
dW,_ = Department.objects.get_or_create(name="外科", defaults={"category":"临床"})
sup = User.objects.create_user(username="13800009000", password="x", phone="13800009000", real_name="Swagger超管", role_type="super_admin", status=1)
hos = User.objects.create_user(username="13800009001", password="x", phone="13800009001", real_name="Swagger院管", role_type="hospital_admin", institution=instA, status=1)
doc = User.objects.create_user(username="13800009002", password="x", phone="13800009002", real_name="Swagger医生", role_type="doctor", institution=instA, status=1)
stu = User.objects.create_user(username="13800009003", password="x", phone="13800009003", real_name="Swagger学生", role_type="student", institution=instA, department=dN, status=1)
stuB = User.objects.create_user(username="13800009004", password="x", phone="13800009004", real_name="他院学生", role_type="student", institution=instB, status=1)
TeacherStudentRelation.objects.create(teacher=doc, student=stu, relation_type="指导", status=1)
def tok(u): return str(RefreshToken.for_user(u).access_token)
print("|".join([tok(sup), tok(hos), tok(doc), str(instA.id), str(instB.id), str(dN.id), str(stu.id), str(doc.id)]))
''')
T_SUP, T_HOS, T_DOC, INST_A, INST_B, DEPT_N, STU_ID, DOC_ID = setup.split('|')
w(f'[准备] 完成 instA={INST_A} instB={INST_B} deptN={DEPT_N} student={STU_ID} doctor={DOC_ID}')
# ═══ 超级管理员 - 用户管理 ═══
w('\n' + '#' * 70 + '\n# 超级管理员 - 用户管理\n' + '#' * 70)
call('CMS-USER-1', '用户列表', 'GET', '/api/cms/users/', T_SUP, params={'role_type': 'doctor', 'page': 1})
r = call('CMS-USER-2', '新增用户', 'POST', '/api/cms/users/', T_SUP,
json_body={'phone': '13800009010', 'real_name': 'Swagger新医生', 'role_type': 'doctor', 'institution': int(INST_A)}, expect=201)
NEW_UID = r.json().get('id')
call('CMS-USER-3', '编辑用户', 'PATCH', f'/api/cms/users/{NEW_UID}/', T_SUP, json_body={'real_name': 'Swagger改名', 'title_name': '主治医师'})
call('CMS-USER-5', '重置密码', 'POST', f'/api/cms/users/{NEW_UID}/reset-password/', T_SUP, json_body={})
ufile = make_xlsx_bytes(['手机号', '姓名', '角色', '机构编码'],
[['13800009011', '导入医生', '医生', 'SWG-CMS-A'], ['13800009012', '导入学生', '学生', 'SWG-CMS-A']])
with open(os.path.join(EXCEL_DIR, '用户导入_样例.xlsx'), 'wb') as f:
f.write(ufile)
call('CMS-USER-6', '导入用户', 'POST', '/api/cms/users/import/', T_SUP, file_bytes=ufile, file_name='用户导入_样例.xlsx')
call('CMS-USER-7', '导入模板', 'GET', '/api/cms/users/import-template/', T_SUP, save_as='用户导入_模板.xlsx')
call('CMS-USER-8', '导出用户', 'GET', '/api/cms/users/export/', T_SUP, params={'role_type': 'doctor'}, save_as='用户导出_结果.xlsx')
call('CMS-USER-4', '停用用户', 'DELETE', f'/api/cms/users/{NEW_UID}/', T_SUP, expect=204)
# ═══ 超级管理员 - 医院管理 ═══
w('\n' + '#' * 70 + '\n# 超级管理员 - 医院管理\n' + '#' * 70)
call('CMS-INST-1', '机构列表', 'GET', '/api/cms/institutions/', T_SUP, params={'search': 'Swagger'})
r = call('CMS-INST-2', '新增机构', 'POST', '/api/cms/institutions/', T_SUP,
json_body={'code': 'SWG-CMS-NEW', 'name': 'Swagger新医院', 'type': 'hospital', 'level': '二甲', 'province': '上海', 'city': '上海'}, expect=201)
NEW_INST = r.json().get('id')
call('CMS-INST-3', '编辑机构', 'PATCH', f'/api/cms/institutions/{NEW_INST}/', T_SUP, json_body={'level': '三甲'})
ifile = make_xlsx_bytes(['机构编码', '名称', '类型', '等级', '', ''],
[['SWG-CMS-IMP1', '导入医院A', 'hospital', '三甲', '广东', '广州']])
with open(os.path.join(EXCEL_DIR, '机构导入_样例.xlsx'), 'wb') as f:
f.write(ifile)
call('CMS-INST-5', '导入机构', 'POST', '/api/cms/institutions/import/', T_SUP, file_bytes=ifile, file_name='机构导入_样例.xlsx')
call('CMS-INST-6', '导入模板', 'GET', '/api/cms/institutions/import-template/', T_SUP, save_as='机构导入_模板.xlsx')
call('CMS-INST-7', '导出机构', 'GET', '/api/cms/institutions/export/', T_SUP, save_as='机构导出_结果.xlsx')
call('CMS-INST-4', '停用机构', 'DELETE', f'/api/cms/institutions/{NEW_INST}/', T_SUP, expect=204)
# ═══ 超级管理员 - 科室管理 ═══
w('\n' + '#' * 70 + '\n# 超级管理员 - 科室管理\n' + '#' * 70)
call('CMS-DEPT-1', '科室列表', 'GET', '/api/cms/departments/', T_SUP)
r = call('CMS-DEPT-2', '新增科室', 'POST', '/api/cms/departments/', T_SUP, json_body={'name': 'Swagger儿科', 'category': '临床'}, expect=201)
NEW_DEPT = r.json().get('id')
call('CMS-DEPT-3', '编辑科室', 'PATCH', f'/api/cms/departments/{NEW_DEPT}/', T_SUP, json_body={'category': '儿童医学'})
dfile = make_xlsx_bytes(['科室名称', '分类'], [['Swagger放射科', '医技']])
with open(os.path.join(EXCEL_DIR, '科室导入_样例.xlsx'), 'wb') as f:
f.write(dfile)
call('CMS-DEPT-5', '导入科室', 'POST', '/api/cms/departments/import/', T_SUP, file_bytes=dfile, file_name='科室导入_样例.xlsx')
call('CMS-DEPT-7', '导入模板', 'GET', '/api/cms/departments/import-template/', T_SUP, save_as='科室导入_模板.xlsx')
call('CMS-DEPT-6', '导出科室', 'GET', '/api/cms/departments/export/', T_SUP, save_as='科室导出_结果.xlsx')
call('CMS-DEPT-4', '停用科室', 'DELETE', f'/api/cms/departments/{NEW_DEPT}/', T_SUP, expect=204)
# ═══ 医院管理员 - 人员管理(本院 doctor/student/content_admin)═══
w('\n' + '#' * 70 + '\n# 医院管理员 - 人员管理\n' + '#' * 70)
call('CMS-HUSER-1', '用户列表(本院)', 'GET', '/api/cms/users/', T_HOS, params={'role_type': 'student'})
r = call('CMS-HUSER-2', '新增用户(本院)', 'POST', '/api/cms/users/', T_HOS,
json_body={'phone': '13800009020', 'real_name': '院管建的内容员', 'role_type': 'content_admin'}, expect=201)
HU_UID = r.json().get('id')
call('CMS-HUSER-3', '编辑用户(本院)', 'PATCH', f'/api/cms/users/{HU_UID}/', T_HOS, json_body={'title_name': '内容主管'})
call('CMS-HUSER-5', '重置密码(本院)', 'POST', f'/api/cms/users/{HU_UID}/reset-password/', T_HOS, json_body={})
hufile = make_xlsx_bytes(['手机号', '姓名', '角色', '机构编码'], [['13800009021', '院管导入生', '学生', '忽略']])
with open(os.path.join(EXCEL_DIR, '本院用户导入_样例.xlsx'), 'wb') as f:
f.write(hufile)
call('CMS-HUSER-6', '导入用户(本院)', 'POST', '/api/cms/users/import/', T_HOS, file_bytes=hufile, file_name='本院用户导入_样例.xlsx')
call('CMS-HUSER-7', '导入模板', 'GET', '/api/cms/users/import-template/', T_HOS, save_as='本院用户导入_模板.xlsx')
call('CMS-HUSER-8', '导出用户(本院)', 'GET', '/api/cms/users/export/', T_HOS, save_as='本院用户导出_结果.xlsx')
call('CMS-HUSER-4', '停用用户(本院)', 'DELETE', f'/api/cms/users/{HU_UID}/', T_HOS, expect=204)
# ═══ 医院管理员 - 师生关系 ═══
w('\n' + '#' * 70 + '\n# 医院管理员 - 师生关系\n' + '#' * 70)
call('CMS-REL-1', '师生关系列表', 'GET', '/api/cms/teacher-student-relations/', T_HOS)
# 先把已存在的 doc×stu 关系拿来演示编辑/停用;新增用一对新的(再建一个本院学生)
new_stu = django_eval(r'''
from apps.user.models import User, Institution
instA = Institution.objects.get(code="SWG-CMS-A")
u,_ = User.objects.get_or_create(phone="13800009022", defaults=dict(username="13800009022", real_name="师生关系新学生", role_type="student", institution=instA, status=1))
print(u.id)
''')
r = call('CMS-REL-2', '新增师生关系', 'POST', '/api/cms/teacher-student-relations/', T_HOS,
json_body={'teacher': int(DOC_ID), 'student': int(new_stu), 'relation_type': '指导'}, expect=201)
REL_ID = r.json().get('id')
call('CMS-REL-3', '编辑师生关系', 'PATCH', f'/api/cms/teacher-student-relations/{REL_ID}/', T_HOS, json_body={'status': 0})
rfile = make_xlsx_bytes(['带教医生手机号', '学生手机号'], [['13800009002', '13800009003']])
with open(os.path.join(EXCEL_DIR, '师生关系导入_样例.xlsx'), 'wb') as f:
f.write(rfile)
call('CMS-REL-5', '导入师生关系', 'POST', '/api/cms/teacher-student-relations/import/', T_HOS, file_bytes=rfile, file_name='师生关系导入_样例.xlsx')
call('CMS-REL-6', '导入模板', 'GET', '/api/cms/teacher-student-relations/import-template/', T_HOS, save_as='师生关系导入_模板.xlsx')
call('CMS-REL-7', '导出师生关系', 'GET', '/api/cms/teacher-student-relations/export/', T_HOS, save_as='师生关系导出_结果.xlsx')
call('CMS-REL-4', '停用师生关系', 'DELETE', f'/api/cms/teacher-student-relations/{REL_ID}/', T_HOS, expect=204)
# ═══ 带教医生 - 我的学生 ═══
w('\n' + '#' * 70 + '\n# 带教医生 - 我的学生\n' + '#' * 70)
call('CMS-TEA-1', '我的学生列表', 'GET', '/api/cms/students/', T_DOC)
call('CMS-TEA-2', '学生基础信息', 'GET', f'/api/cms/students/{STU_ID}/', T_DOC)
# ─── 清理 ───
w('\n[清理] 删除测试数据 ...')
django_eval(r'''
from apps.user.models import User, Institution, Department, TeacherStudentRelation
ph = ["13800009000","13800009001","13800009002","13800009003","13800009004","13800009010","13800009011","13800009012","13800009020","13800009021","13800009022"]
TeacherStudentRelation.all_objects.filter(teacher__phone__in=ph).hard_delete()
for u in User.all_objects.filter(phone__in=ph): u.hard_delete()
Institution.all_objects.filter(code__startswith="SWG-CMS").hard_delete()
Department.all_objects.filter(name__startswith="Swagger").hard_delete()
print("cleaned")
''')
# ─── 落盘样例 JSON + 汇总 ───
with open(EXAMPLES_FILE, 'w', encoding='utf-8') as f:
json.dump(examples, f, ensure_ascii=False, indent=2)
w('\n' + '=' * 90)
total = len(results); passed = sum(1 for _, ok, _ in results if ok == 'PASS')
w(f' 总计 {total} 个接口 | 通过 {passed} | 失败 {total - passed}')
fails = [(c, s) for c, ok, s in results if ok == 'FAIL']
if fails:
w(' 失败: ' + ', '.join(f'{c}({s})' for c, s in fails))
w(f' 日志: {LOG_FILE}')
w(f' 样例 JSON: {EXAMPLES_FILE}')
w(f' Excel 文件目录: {EXCEL_DIR}')
_fh.close()
sys.exit(0 if not fails else 1)