feat:cms relation hospital bug fix

This commit is contained in:
2026-06-17 10:22:51 +08:00
parent 2bb9ff50d8
commit 89ab844343
5 changed files with 274 additions and 39 deletions
+144
View File
@@ -0,0 +1,144 @@
"""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)
+40 -15
View File
@@ -36,9 +36,16 @@ class CmsRelationTest(CacheTestCase):
self.other = ensure_institution(name='他院', code='REL-B')
self.admin = create_test_user(phone='13932000001', role_type='hospital_admin', institution=self.inst)
self.client = get_auth_client(self.admin)
self.doc = create_test_user(phone='13932000002', role_type='doctor', institution=self.inst)
self.stu = create_test_user(phone='13932000003', role_type='student', institution=self.inst)
self.other_stu = create_test_user(phone='13932000004', role_type='student', institution=self.other)
self.doc = create_test_user(phone='13932000002', real_name='张医生', role_type='doctor', institution=self.inst)
self.stu = create_test_user(phone='13932000003', real_name='李同学', role_type='student', institution=self.inst)
self.other_stu = create_test_user(phone='13932000004', real_name='王同学', role_type='student', institution=self.other)
# 完整新增载荷(姓名+手机号均必填)
def _payload(self, **over):
p = {'teacher_name': '张医生', 'teacher_phone': '13932000002',
'student_name': '李同学', 'student_phone': '13932000003'}
p.update(over)
return p
def test_permission(self):
self.assertEqual(APIClient().get(REL_URL).status_code, 401)
@@ -46,37 +53,55 @@ class CmsRelationTest(CacheTestCase):
self.assertEqual(stu_client.get(REL_URL).status_code, 403)
def test_create_and_list(self):
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
resp = self.client.post(REL_URL, self._payload())
self.assertEqual(resp.status_code, 201, resp.content)
self.assertEqual(resp.json()['teacher_phone'], '13932000002')
self.assertEqual(resp.json()['student_phone'], '13932000003')
self.assertEqual(resp.json()['teacher_name'], '张医生')
self.assertEqual(resp.json()['student_name'], '李同学')
# 列表
resp = self.client.get(REL_URL)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json()['results']), 1)
def test_create_missing_phone_400(self):
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002'})
def test_create_missing_name_400(self):
resp = self.client.post(REL_URL, self._payload(student_name=''))
self.assertEqual(resp.status_code, 400, resp.content)
self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR')
def test_create_name_phone_mismatch_400(self):
# 姓名与手机号不符 → 解析不到
resp = self.client.post(REL_URL, self._payload(teacher_name='不存在'))
self.assertEqual(resp.status_code, 400, resp.content)
self.assertEqual(resp.json()['code'], 'CMS_REL_TEACHER_NOT_FOUND')
def test_create_duplicate(self):
self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
self.client.post(REL_URL, self._payload())
resp = self.client.post(REL_URL, self._payload())
self.assertEqual(resp.status_code, 400, resp.content)
self.assertEqual(resp.json()['code'], 'CMS_REL_EXISTS')
def test_teacher_must_be_doctor(self):
# 用学生手机号当带教老师 → 解析不到 doctor
resp = self.client.post(REL_URL, {'teacher_phone': '13932000003', 'student_phone': '13932000003'})
# 用学生姓名+手机号当带教老师 → 解析不到 doctor
resp = self.client.post(REL_URL, self._payload(teacher_name='李同学', teacher_phone='13932000003'))
self.assertEqual(resp.status_code, 400, resp.content)
self.assertEqual(resp.json()['code'], 'CMS_REL_TEACHER_NOT_FOUND')
def test_scope_other_institution_student_rejected(self):
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000004'})
resp = self.client.post(REL_URL, self._payload(student_name='王同学', student_phone='13932000004'))
self.assertEqual(resp.status_code, 403, resp.content)
self.assertEqual(resp.json()['code'], 'CMS_REL_SCOPE_FORBIDDEN')
def test_dropdown_lists(self):
# 2 个下拉数据源:本院 doctor / student,姓名+手机号一并返回
docs = self.client.get(REL_URL + 'doctors/').json()
self.assertEqual(docs, [{'id': self.doc.id, 'real_name': '张医生', 'phone': '13932000002'}])
studs = self.client.get(REL_URL + 'students/').json()
# 他院学生不在内(本院仅李同学)
self.assertEqual(studs, [{'id': self.stu.id, 'real_name': '李同学', 'phone': '13932000003'}])
# 学生无权访问
self.assertEqual(get_auth_client(self.stu).get(REL_URL + 'doctors/').status_code, 403)
def test_soft_delete(self):
r = TeacherStudentRelation.objects.create(teacher=self.doc, student=self.stu, status=1)
resp = self.client.post(rel_disable(r.id))
@@ -85,10 +110,10 @@ class CmsRelationTest(CacheTestCase):
self.assertTrue(TeacherStudentRelation.all_objects.get(id=r.id).is_deleted)
def test_import_and_export(self):
f = make_xlsx(['带教医生手机号', '学生手机号'], [
['13932000002', '13932000003'], # ✅
['13932000002', '13932000004'], # 学生他院 → 失败
['00000000000', '13932000003'], # 医生不存在 → 失败
f = make_xlsx(['带教医生姓名', '带教医生手机号', '学生姓名', '学生手机号'], [
['张医生', '13932000002', '李同学', '13932000003'], # ✅
['张医生', '13932000002', '王同学', '13932000004'], # 学生他院 → 失败
['不存在', '00000000000', '李同学', '13932000003'], # 医生不存在 → 失败
])
resp = self.client.post('/api/cms/teacher-student-relations/import/', {'file': f}, format='multipart')
self.assertEqual(resp.status_code, 200, resp.content)