"""移动端病例列表 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_only_published(self): resp = self.client.get(REC_URL) self.assertEqual(resp.status_code, 200, resp.content) ids = set(self._ids(resp)) self.assertEqual(resp.json()['count'], 3) self.assertEqual(ids, {self.pub_trad1.id, self.pub_teach.id, self.pub_trad2.id}) def test_recommended_untrained_first(self): # pub_teach 未训练且命中薄弱标签+同科室 → 应排在已训练病例之前 resp = self.client.get(REC_URL) self.assertEqual(self._ids(resp)[0], self.pub_teach.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])