feat: update medical training case and auth modules

This commit is contained in:
2026-06-03 17:34:47 +08:00
parent b4bb38b7be
commit fd0b3e1982
45 changed files with 1459 additions and 812 deletions
+49 -4
View File
@@ -13,7 +13,7 @@ from apps.user.permissions import IsCaseOperationPermitted
from apps.user.throttling import PdfParseUserThrottle, ScoringRuleGenerateUserThrottle
from .models import (
CaseBase, TraditionalCase, ScriptCase,
TeachingCase, CaseStage, ScoringRule
TeachingCase, CaseStage, ScoringRule, CaseExamItem,
)
from .serializers import (
CaseBaseListSerializer, CaseBaseDetailSerializer,
@@ -23,6 +23,9 @@ from .serializers import (
)
from .services import case_importer, scoring_rule_generator
from .services.department_resolver import resolve_department
from .services.exam_items import (
normalize_exam_items, assert_no_duplicate_exam_items, EXAM_ITEM_FIELDS,
)
audit = logging.getLogger('audit')
@@ -135,6 +138,10 @@ class CaseBaseViewSet(viewsets.ModelViewSet):
'traditional': drf_serializers.DictField(required=False),
'teaching': drf_serializers.DictField(required=False),
'scoring_rules': drf_serializers.ListField(child=drf_serializers.DictField(), help_text='评分规则(≥1 条,必填)'),
'exam_items': drf_serializers.ListField(
child=drf_serializers.DictField(), required=False,
help_text='检查项(可选;同一病例 item_code 不可重复)',
),
'parse_id': drf_serializers.CharField(required=False, help_text='来自 parse-pdf 的 parse_id(审计用)'),
'auto_publish': drf_serializers.BooleanField(required=False, default=False),
}),
@@ -168,6 +175,9 @@ class CaseBaseViewSet(viewsets.ModelViewSet):
raise AppError('CASE_VALIDATION_ERROR', 'scoring_rules 必填且至少 1 条', status_code=400)
_validate_scoring_rules(scoring_rules_data)
exam_items_data = normalize_exam_items(data.get('exam_items') or [])
assert_no_duplicate_exam_items(exam_items_data)
department = resolve_department(data.get('department_name', ''))
with transaction.atomic():
@@ -189,9 +199,20 @@ class CaseBaseViewSet(viewsets.ModelViewSet):
]
ScoringRule.objects.bulk_create(rule_objs)
if exam_items_data:
exam_objs = [
CaseExamItem(
case=case,
**{k: v for k, v in item.items() if k in EXAM_ITEM_FIELDS},
)
for item in exam_items_data
]
CaseExamItem.objects.bulk_create(exam_objs)
audit.info(
'CASE_CREATE case_id=%s from=%s by=%s scoring_rules=%d',
case.id, data.get('parse_id', 'form'), request.user.id, len(rule_objs),
'CASE_CREATE case_id=%s from=%s by=%s scoring_rules=%d exam_items=%d',
case.id, data.get('parse_id', 'form'), request.user.id,
len(rule_objs), len(exam_items_data),
)
return Response(_build_full_response(case), status=status.HTTP_201_CREATED)
@@ -214,7 +235,7 @@ class CaseBaseViewSet(viewsets.ModelViewSet):
def _full_detail(self, request, pk):
case = CaseBase.objects.select_related(
'department', 'created_by'
).prefetch_related('scoring_rules').filter(pk=pk).first()
).prefetch_related('scoring_rules', 'exam_items').filter(pk=pk).first()
if not case:
raise AppError('NOT_FOUND', '病例不存在', status_code=404)
@@ -384,6 +405,13 @@ def _validate_scoring_rules(rules):
def _build_full_response(case):
if hasattr(case, '_prefetched_objects_cache') and 'exam_items' in getattr(
case, '_prefetched_objects_cache', {}
):
exam_qs = case.exam_items.all()
else:
exam_qs = CaseExamItem.objects.filter(case_id=case.id).order_by('display_order', 'id')
result = {
'case': {
'id': case.id,
@@ -456,4 +484,21 @@ def _build_full_response(case):
for r in rules
]
result['exam_items'] = [
{
'id': e.id,
'item_code': e.item_code,
'item_name': e.item_name,
'item_type': e.item_type,
'category': e.category,
'result_text': e.result_text,
'result_structured': e.result_structured,
'is_key': e.is_key,
'is_abnormal': e.is_abnormal,
'score_weight': float(e.score_weight),
'display_order': e.display_order,
}
for e in exam_qs
]
return result