Files
medical_training/test/test_mobile_case_list.py
T
2026-06-13 14:17:54 +08:00

184 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""移动端病例列表 5 接口测试。
- 5.1 推荐病例 GET /api/case/mobile/recommended/
- 5.2 科室专项 GET /api/case/mobile/specialty/
- 5.3 薄弱环节 GET /api/case/mobile/weak/
- 5.4 教学互动 GET /api/case/mobile/teaching/
- 5.5 教师任务 GET /api/case/mobile/teacher-task/
均只取「已发布」病例(publish_status=2 & status=1 & is_deleted=0)。
training_record 是 managed=False 只读表,迁移占位结构缺新列,故在 setUpClass 里按真实列
重建该表(仅测试库),用 TransactionTestCase 允许 DDL(与 test_cms_training 一致)。
"""
import json
from django.core.cache import cache
from django.db import connection
from django.test import TransactionTestCase
from django.utils import timezone
from rest_framework.test import APIClient
from apps.user.models import Department
from apps.case.models import CaseBase
from .conftest import create_test_user, get_auth_client, ensure_institution
CREATE_TR = """
CREATE TABLE training_record (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL, case_id BIGINT NULL, teacher_id BIGINT NULL,
training_mode VARCHAR(50) NULL, case_type VARCHAR(30) NULL,
start_time DATETIME NULL, end_time DATETIME NULL, duration_seconds INT NULL,
total_score DECIMAL(5,2) NULL, ai_score DECIMAL(5,2) NULL, teacher_score DECIMAL(5,2) NULL,
evaluation_level VARCHAR(20) NULL, status VARCHAR(30) NULL,
feedback TEXT NULL, thinking_chain TEXT NULL, diagnosis_path TEXT NULL,
wrong_points JSON NULL, missed_questions JSON NULL, recommendation_result JSON NULL,
ai_feedback_structured JSON NULL, osce_station_score JSON NULL,
interruption_count INT NULL, emotion_analysis JSON NULL,
prompt_version VARCHAR(50) NULL, rag_context_version VARCHAR(50) NULL,
external_user_id VARCHAR(128) NULL, session_id BIGINT NULL, evaluation_record_id BIGINT NULL,
score_type VARCHAR(20) NULL, pdf_file_path VARCHAR(512) NULL,
created_at DATETIME NULL, updated_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
"""
REC_URL = '/api/case/mobile/recommended/'
SPEC_URL = '/api/case/mobile/specialty/'
WEAK_URL = '/api/case/mobile/weak/'
TEACH_URL = '/api/case/mobile/teaching/'
TASK_URL = '/api/case/mobile/teacher-task/'
class MobileCaseListTest(TransactionTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
with connection.cursor() as c:
c.execute('SET FOREIGN_KEY_CHECKS=0')
c.execute('DROP TABLE IF EXISTS training_record')
c.execute(CREATE_TR)
c.execute('SET FOREIGN_KEY_CHECKS=1')
def _insert(self, user_id, case_id, total_score, status='completed', score_type='percentage'):
now = timezone.now()
with connection.cursor() as c:
c.execute(
"INSERT INTO training_record (user_id, case_id, training_mode, status, "
"total_score, end_time, start_time, score_type, ai_feedback_structured, "
"created_at, updated_at) VALUES (%s,%s,'practice',%s,%s,%s,%s,%s,%s,%s,%s)",
[user_id, case_id, status, total_score, now, now, score_type,
json.dumps({}), now, now],
)
return c.lastrowid
def setUp(self):
cache.clear()
with connection.cursor() as c:
c.execute('DELETE FROM training_record')
self.inst = ensure_institution(name='测试医院', code='MCL-H1')
self.dept1 = Department.objects.create(name='心内科', category='临床')
self.dept2 = Department.objects.create(name='呼吸科', category='临床')
# 已发布病例
self.pub_trad1 = CaseBase.objects.create(
title='急性心梗', case_type='traditional', department=self.dept1,
institution=self.inst, chief_complaint='胸痛', tags='心内科,胸痛',
publish_status=2, status=1)
self.pub_teach = CaseBase.objects.create(
title='医患沟通教学', case_type='teaching', department=self.dept1,
institution=self.inst, competency_tags=['沟通人文', '医患沟通'],
publish_status=2, status=1)
self.pub_trad2 = CaseBase.objects.create(
title='肺炎诊治', case_type='traditional', department=self.dept2,
institution=self.inst, tags='呼吸科', publish_status=2, status=1)
# 不应出现:草稿 / 禁用 / 已下架(软删)
CaseBase.objects.create(title='草稿病例', case_type='traditional',
department=self.dept1, institution=self.inst, publish_status=0, status=1)
CaseBase.objects.create(title='禁用病例', case_type='traditional',
department=self.dept1, institution=self.inst, publish_status=2, status=0)
deleted = CaseBase.objects.create(title='已下架病例', case_type='teaching',
department=self.dept1, institution=self.inst, publish_status=2, status=1)
deleted.delete() # 软删除
self.stu = create_test_user(phone='13980000001', real_name='学生甲',
role_type='student', institution=self.inst)
self.stu.department = self.dept1
self.stu.weak_dimensions = ['沟通人文']
self.stu.save(update_fields=['department', 'weak_dimensions'])
# stu 已训练:pub_trad1 低分(60→薄弱)pub_trad2 高分(90)
self._insert(self.stu.id, self.pub_trad1.id, 60)
self._insert(self.stu.id, self.pub_trad2.id, 90)
self.client = get_auth_client(self.stu)
def _ids(self, resp):
return [r['id'] for r in resp.json()['results']]
# ── 鉴权 ────────────────────────────────────────────────────────────────
def test_unauthenticated_401(self):
for url in (REC_URL, SPEC_URL, WEAK_URL, TEACH_URL, TASK_URL):
self.assertEqual(APIClient().get(url).status_code, 401, url)
# ── 5.1 推荐(返回病例库全部已发布病例)───────────────────────────────────
def test_recommended_returns_all_published(self):
# 返回全部已发布病例;草稿/禁用/已下架(软删) 均不返回
resp = self.client.get(REC_URL)
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['count'], 3)
ids = set(self._ids(resp))
self.assertEqual(ids, {self.pub_trad1.id, self.pub_teach.id, self.pub_trad2.id})
titles = {r['title'] for r in resp.json()['results']}
self.assertNotIn('草稿病例', titles)
self.assertNotIn('禁用病例', titles)
self.assertNotIn('已下架病例', titles)
def test_recommended_supports_search(self):
resp = self.client.get(REC_URL, {'search': '心梗'})
self.assertEqual(self._ids(resp), [self.pub_trad1.id], resp.content)
# ── 5.2 科室专项 ──────────────────────────────────────────────────────────
def test_specialty_default_user_dept(self):
resp = self.client.get(SPEC_URL)
self.assertEqual(set(self._ids(resp)), {self.pub_trad1.id, self.pub_teach.id})
def test_specialty_explicit_department(self):
resp = self.client.get(SPEC_URL, {'department': self.dept2.id})
self.assertEqual(self._ids(resp), [self.pub_trad2.id])
# ── 5.3 薄弱环节 ──────────────────────────────────────────────────────────
def test_weak_low_score_cases(self):
resp = self.client.get(WEAK_URL)
self.assertEqual(resp.status_code, 200, resp.content)
results = resp.json()['results']
self.assertEqual([r['id'] for r in results], [self.pub_trad1.id])
self.assertEqual(results[0]['my_best_score'], 60.0)
self.assertEqual(results[0]['my_train_count'], 1)
def test_weak_cold_start_fallback(self):
# 新用户无训练记录,但 weak_dimensions 命中 pub_teach 能力标签 → 回退命中
fresh = create_test_user(phone='13980000009', role_type='student', institution=self.inst)
fresh.weak_dimensions = ['沟通人文']
fresh.save(update_fields=['weak_dimensions'])
resp = get_auth_client(fresh).get(WEAK_URL)
self.assertEqual(self._ids(resp), [self.pub_teach.id], resp.content)
# ── 5.4 教学互动 ──────────────────────────────────────────────────────────
def test_teaching_only_teaching_type(self):
resp = self.client.get(TEACH_URL)
self.assertEqual(self._ids(resp), [self.pub_teach.id])
# ── 5.5 教师任务(暂同教学互动)────────────────────────────────────────────
def test_teacher_task_same_as_teaching(self):
resp = self.client.get(TASK_URL)
self.assertEqual(self._ids(resp), [self.pub_teach.id])
# ── 通用过滤 ──────────────────────────────────────────────────────────────
def test_search_filter(self):
resp = self.client.get(REC_URL, {'search': '心梗'})
self.assertEqual(self._ids(resp), [self.pub_trad1.id])
def test_case_type_filter(self):
resp = self.client.get(REC_URL, {'case_type': 'teaching'})
self.assertEqual(self._ids(resp), [self.pub_teach.id])