"""Swagger Try-it-out 等效脚本:CMS 医院管理员·师生关系管理(姓名+手机号版)。 覆盖本轮改动:CMS-REL-2 新增/编辑(必填 带教老师姓名+手机号 / 学生姓名+手机号)、 CMS-REL-4 导入/模板(4 列:带教医生姓名|手机号|学生姓名|手机号)、 CMS-REL-6/7 下拉数据源(doctors / students,姓名+手机号一并返回)。 运行方式:.venv\\Scripts\\python.exe test/swagger_cms_relation.py 前提:Django dev server 已在 http://127.0.0.1:8000 运行,Redis 已启动。 """ import io import sys import json import subprocess import requests from openpyxl import Workbook sys.stdout.reconfigure(encoding='utf-8') sys.stderr.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' REL = '/api/cms/teacher-student-relations/' XLSX_CT = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' PASS, FAIL = 'PASS', 'FAIL' results = [] PH = ['13700007001', '13700007002', '13700007003', '13700007004', '13700007009'] # D1 D2 S1 S2 admin def log(api_id, method, url, expected, actual, detail=''): exp = expected if isinstance(expected, (list, tuple)) else [expected] st = PASS if actual in exp else FAIL results.append((api_id, st)) print(f' {st} {api_id:<16} {method:<5} {url:<44} expect={str(expected):<10} got={actual} {detail}') def django_eval(code): pre = 'import django, os; os.environ.setdefault("DJANGO_SETTINGS_MODULE","config.settings"); django.setup()\n' p = subprocess.run([PYTHON, '-c', pre + code], capture_output=True, text=True, cwd=CWD) if p.returncode != 0: print('[django_eval ERROR]', p.stderr[-1200:]) return p.stdout.strip() def jb(r): return r.json() if r.headers.get('content-type', '').startswith('application/json') else None def make_xlsx(headers, rows): wb = Workbook(); ws = wb.active ws.append(headers) for r in rows: ws.append(r) buf = io.BytesIO(); wb.save(buf); buf.seek(0) return buf.read() print('\n[准备] 建机构 + 医院管理员 + 2 医生 + 2 学生...') setup = django_eval( 'from apps.user.models import User, Institution; from rest_framework_simplejwt.tokens import RefreshToken; ' 'inst,_=Institution.objects.get_or_create(code="SWG_REL", defaults={"name":"师生关系联调院","type":"hospital"}); ' f'[User.all_objects.filter(phone__in={PH!r}).delete()]; ' f'd1=User.objects.create_user(username="{PH[0]}",password=None,phone="{PH[0]}",real_name="王医生",role_type="doctor",institution=inst,status=1); ' f'd2=User.objects.create_user(username="{PH[1]}",password=None,phone="{PH[1]}",real_name="李医生",role_type="doctor",institution=inst,status=1); ' f's1=User.objects.create_user(username="{PH[2]}",password=None,phone="{PH[2]}",real_name="赵同学",role_type="student",institution=inst,status=1); ' f's2=User.objects.create_user(username="{PH[3]}",password=None,phone="{PH[3]}",real_name="钱同学",role_type="student",institution=inst,status=1); ' f'hu=User.objects.create_user(username="{PH[4]}",password=None,phone="{PH[4]}",real_name="师生关系院管",role_type="hospital_admin",institution=inst,status=1); ' 'print(str(RefreshToken.for_user(hu).access_token))' ) HU = {'Authorization': f'Bearer {setup}'} print('[准备] 完成\n') print('=' * 100) print(' CMS 师生关系管理(姓名+手机号)Swagger Try-it-out') print('=' * 100) # 权限 log('anon-401', 'GET', REL, 401, requests.get(f'{BASE}{REL}').status_code) # CMS-REL-6/7 下拉数据源 r = requests.get(f'{BASE}{REL}doctors/', headers=HU); b = jb(r) or [] log('CMS-REL-6', 'GET', REL + 'doctors/', 200, r.status_code, f'count={len(b)} sample={b[0] if b else None}') r = requests.get(f'{BASE}{REL}students/', headers=HU); b = jb(r) or [] log('CMS-REL-7', 'GET', REL + 'students/', 200, r.status_code, f'count={len(b)} sample={b[0] if b else None}') # CMS-REL-2 新增(姓名+手机号) payload = {'teacher_name': '王医生', 'teacher_phone': PH[0], 'student_name': '赵同学', 'student_phone': PH[2], 'relation_type': '指导'} r = requests.post(f'{BASE}{REL}', json=payload, headers=HU); b = jb(r) or {} rid = b.get('id') log('CMS-REL-2-new', 'POST', REL, 201, r.status_code, f'id={rid} t={b.get("teacher_name")}/{b.get("teacher_phone")} s={b.get("student_name")}/{b.get("student_phone")}') # 缺姓名 → 400 r = requests.post(f'{BASE}{REL}', json={**payload, 'student_name': ''}, headers=HU) log('REL-2-missing', 'POST', REL + '(缺学生姓名)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}') # 姓名与手机号不符 → 400 r = requests.post(f'{BASE}{REL}', json={**payload, 'teacher_name': '不存在'}, headers=HU) log('REL-2-mismatch', 'POST', REL + '(姓名手机号不符)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}') # 重复 → 400 r = requests.post(f'{BASE}{REL}', json=payload, headers=HU) log('REL-2-dup', 'POST', REL + '(重复)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}') # CMS-REL-2 编辑(改 status) if rid: r = requests.post(f'{BASE}{REL}{rid}/update/', json={'status': 0}, headers=HU) log('CMS-REL-2-edit', 'POST', f'{REL}{rid}/update/', 200, r.status_code, f'status={(jb(r) or {}).get("status")}') # CMS-REL-4 模板 r = requests.get(f'{BASE}{REL}import-template/', headers=HU) log('CMS-REL-4-tmpl', 'GET', REL + 'import-template/', 200, r.status_code, f'ct={r.headers.get("content-type","")[:40]}') # CMS-REL-4 导入(4 列) xlsx = make_xlsx(['带教医生姓名', '带教医生手机号', '学生姓名', '学生手机号'], [ ['李医生', PH[1], '钱同学', PH[3]], # ✅ ['不存在', '00000000000', '钱同学', PH[3]], # 医生不符 → 失败 ]) r = requests.post(f'{BASE}{REL}import/', files={'file': ('rel.xlsx', xlsx, XLSX_CT)}, headers=HU) b = jb(r) or {} log('CMS-REL-4-import', 'POST', REL + 'import/', 200, r.status_code, f'success={b.get("success")} failed={b.get("failed")}') # CMS-REL-1 列表 r = requests.get(f'{BASE}{REL}', headers=HU) log('CMS-REL-1-list', 'GET', REL, 200, r.status_code, f'count={(jb(r) or {}).get("count")}') # 清理 django_eval( 'from apps.user.models import User; from apps.user.models import TeacherStudentRelation as T; ' f'T.all_objects.filter(teacher__phone__in={PH!r}).delete(); ' f'User.all_objects.filter(phone__in={PH!r}).delete(); print("cleaned")' ) print('=' * 100) total = len(results); passed = sum(1 for _, s in results if s == PASS); failed = total - passed print(f' 总计: {total} | 通过: {passed} | 失败: {failed}') if failed: print(' 失败:', [a for a, s in results if s == FAIL]); sys.exit(1) print(' ALL PASSED — 师生关系(姓名+手机号)接口验证通过!') sys.exit(0)