init medical training project
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
"""D8 测试共享工具:URL 常量、用户/科室夹具、SMS 注入、载荷构建。"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from apps.user.models import User, Institution, Department, TeacherStudentRelation
|
||||
|
||||
|
||||
class CacheTestCase(TestCase):
|
||||
"""所有测试基类:setUp 自动 clear Redis,隔离 SMS 验证码/限流/JWT 黑名单。"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
cache.clear()
|
||||
|
||||
|
||||
class CacheTransactionTestCase(TransactionTestCase):
|
||||
"""事务测试基类:setUp 自动 clear Redis。"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
cache.clear()
|
||||
|
||||
# ─── URL 常量 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# 用户认证
|
||||
USER_SEND_CODE_URL = '/api/user/auth/send-code/'
|
||||
USER_REGISTER_URL = '/api/user/auth/register/'
|
||||
USER_LOGIN_URL = '/api/user/auth/login/'
|
||||
USER_LOGIN_CODE_URL = '/api/user/auth/login-code/'
|
||||
USER_LOGOUT_URL = '/api/user/auth/logout/'
|
||||
USER_REFRESH_URL = '/api/user/auth/refresh/'
|
||||
USER_RESET_PWD_URL = '/api/user/auth/reset-password/'
|
||||
USER_CHANGE_PWD_URL = '/api/user/users/change-password/'
|
||||
USER_ME_URL = '/api/user/users/me/'
|
||||
USER_LIST_URL = '/api/user/users/'
|
||||
|
||||
# 病例
|
||||
CASE_PARSE_URL = '/api/case/cases/parse-pdf/'
|
||||
CASE_GENERATE_RULES_URL = '/api/case/cases/generate-scoring-rules/'
|
||||
CASE_FULL_CREATE_URL = '/api/case/cases/full-create/'
|
||||
|
||||
|
||||
def user_detail_url(user_id):
|
||||
return f'/api/user/users/{user_id}/'
|
||||
|
||||
|
||||
def case_full_url(case_id):
|
||||
return f'/api/case/cases/{case_id}/full/'
|
||||
|
||||
|
||||
# ─── SMS 注入 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def inject_sms_code(phone, scene, code='123456'):
|
||||
"""直接将验证码写入 Redis,绕过 SMS 服务。"""
|
||||
cache.set(f'sms:{scene}:{phone}', code, timeout=300)
|
||||
|
||||
|
||||
# ─── 用户工具 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def create_test_user(phone='13900000001', password='TestPass1',
|
||||
real_name='测试用户', role_type='student', status=1):
|
||||
"""创建测试用户(已知密码),返回 User 实例。"""
|
||||
user = User.objects.create_user(
|
||||
username=phone,
|
||||
password=password,
|
||||
phone=phone,
|
||||
real_name=real_name,
|
||||
role_type=role_type,
|
||||
status=status,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
def get_tokens(user):
|
||||
"""返回 {'access': '...', 'refresh': '...'} 字符串。"""
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return {'access': str(refresh.access_token), 'refresh': str(refresh)}
|
||||
|
||||
|
||||
def get_auth_client(user):
|
||||
"""返回已携带 JWT Bearer 的 APIClient。"""
|
||||
tokens = get_tokens(user)
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f'Bearer {tokens["access"]}')
|
||||
return client
|
||||
|
||||
|
||||
# ─── 师生关系 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def create_teacher_student_relation(teacher, student, status=1):
|
||||
"""创建师生关系记录。"""
|
||||
return TeacherStudentRelation.objects.create(
|
||||
teacher=teacher,
|
||||
student=student,
|
||||
relation_type='指导',
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
# ─── 科室工具 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def ensure_institution(name='测试医院'):
|
||||
inst, _ = Institution.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={'type': 'hospital', 'province': '北京', 'city': '北京'},
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
def ensure_department(name='儿科', institution_name='测试医院'):
|
||||
inst = ensure_institution(institution_name)
|
||||
dept, _ = Department.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={'institution': inst, 'category': '临床'},
|
||||
)
|
||||
return dept
|
||||
|
||||
|
||||
# ─── 病例载荷构建 ─────────────────────────────────────────────────────────────
|
||||
|
||||
def build_traditional_payload(department_name='儿科', scoring_rules_count=2):
|
||||
"""构建合法的传统病例 full-create 载荷。"""
|
||||
rules = [
|
||||
{
|
||||
'dimension': f'测试维度{i + 1}',
|
||||
'score_weight': round(1.0 / scoring_rules_count, 2),
|
||||
'ai_auto_score': True,
|
||||
'scoring_standard': f'评分标准{i + 1}',
|
||||
}
|
||||
for i in range(scoring_rules_count)
|
||||
]
|
||||
return {
|
||||
'title': '测试传统病例-表单录入',
|
||||
'case_type': 'traditional',
|
||||
'difficulty': 'medium',
|
||||
'chief_complaint': '发热 3 天',
|
||||
'description': '患儿,男,4 岁,因发热 3 天就诊。',
|
||||
'patient_age': 4,
|
||||
'patient_gender': 'male',
|
||||
'department_name': department_name,
|
||||
'estimated_minutes': 30,
|
||||
'osce_enabled': False,
|
||||
'tags': '儿科,发热',
|
||||
'traditional': {
|
||||
'standard_diagnosis': '上呼吸道感染',
|
||||
'standard_treatment': '对症治疗,退热处理',
|
||||
'guideline_reference': '《儿科学》第 9 版',
|
||||
},
|
||||
'scoring_rules': rules,
|
||||
}
|
||||
|
||||
|
||||
def build_teaching_payload(department_name='儿科', scoring_rules_count=2):
|
||||
"""构建合法的教学病例 full-create 载荷。"""
|
||||
rules = [
|
||||
{
|
||||
'dimension': f'教学维度{i + 1}',
|
||||
'score_weight': round(1.0 / scoring_rules_count, 2),
|
||||
'ai_auto_score': False,
|
||||
'scoring_standard': f'教学评分标准{i + 1}',
|
||||
}
|
||||
for i in range(scoring_rules_count)
|
||||
]
|
||||
return {
|
||||
'title': '测试教学病例-表单录入',
|
||||
'case_type': 'teaching',
|
||||
'difficulty': 'hard',
|
||||
'chief_complaint': '腹痛 2 天',
|
||||
'description': '患者,女,28 岁,因腹痛 2 天就诊。',
|
||||
'patient_age': 28,
|
||||
'patient_gender': 'female',
|
||||
'department_name': department_name,
|
||||
'estimated_minutes': 45,
|
||||
'osce_enabled': False,
|
||||
'teaching': {
|
||||
'teaching_goal': '掌握急腹症鉴别诊断',
|
||||
'discussion_questions': '如何鉴别急性阑尾炎与其他急腹症?',
|
||||
'teacher_guide': '引导学生按 SOAP 格式分析',
|
||||
'scoring_focus': '鉴别诊断思路',
|
||||
},
|
||||
'scoring_rules': rules,
|
||||
}
|
||||
|
||||
|
||||
# ─── AI Mock 数据 ─────────────────────────────────────────────────────────────
|
||||
|
||||
MOCK_C1_PARSE_RESULT = {
|
||||
'data': {
|
||||
'title': 'Mock-儿科发热病例',
|
||||
'case_type': 'traditional',
|
||||
'difficulty': 'medium',
|
||||
'chief_complaint': '发热 3 天',
|
||||
'description': '患儿,男,4 岁,因发热 3 天就诊。',
|
||||
'patient_age': 4,
|
||||
'patient_gender': 'male',
|
||||
'department_name': '儿科',
|
||||
'estimated_minutes': 30,
|
||||
'osce_enabled': False,
|
||||
'tags': '儿科,发热',
|
||||
'traditional': {
|
||||
'standard_diagnosis': 'Mock 上呼吸道感染',
|
||||
'standard_treatment': 'Mock 对症治疗',
|
||||
'guideline_reference': 'Mock 指南',
|
||||
},
|
||||
},
|
||||
'usage': {'prompt_tokens': 100, 'completion_tokens': 200, 'total_tokens': 300},
|
||||
}
|
||||
|
||||
MOCK_C2_SCORING_RULES = {
|
||||
'data': {
|
||||
'scoring_rules': [
|
||||
{
|
||||
'dimension': '诊断准确性',
|
||||
'competency_dimension': '临床推理',
|
||||
'score_weight': 0.4,
|
||||
'ai_auto_score': True,
|
||||
'osce_dimension': False,
|
||||
'scoring_standard': '能准确判断上呼吸道感染',
|
||||
},
|
||||
{
|
||||
'dimension': '治疗方案合理性',
|
||||
'competency_dimension': '治疗决策',
|
||||
'score_weight': 0.3,
|
||||
'ai_auto_score': True,
|
||||
'osce_dimension': False,
|
||||
'scoring_standard': '治疗方案符合指南推荐',
|
||||
},
|
||||
{
|
||||
'dimension': '医患沟通',
|
||||
'competency_dimension': '沟通技巧',
|
||||
'score_weight': 0.3,
|
||||
'ai_auto_score': False,
|
||||
'osce_dimension': True,
|
||||
'scoring_standard': '能向家属解释病情和治疗方案',
|
||||
},
|
||||
],
|
||||
},
|
||||
'usage': {'prompt_tokens': 150, 'completion_tokens': 250, 'total_tokens': 400},
|
||||
}
|
||||
|
||||
|
||||
def make_fake_pdf():
|
||||
"""创建一个假 PDF 上传文件(仅用于触发 multipart 解析)。"""
|
||||
return SimpleUploadedFile(
|
||||
'test.pdf',
|
||||
b'%PDF-1.4 fake content for testing',
|
||||
content_type='application/pdf',
|
||||
)
|
||||
Reference in New Issue
Block a user