# -*- 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:8000,Redis 已启动。 运行:.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'' 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)