feat: update cms case create
This commit is contained in:
@@ -51,10 +51,12 @@ def validate_scoring_rules(rules):
|
||||
_UNSET = object()
|
||||
|
||||
|
||||
def create_case_full(data, user, institution=_UNSET):
|
||||
"""主表 + 子表 + scoring_rules(≥1) + exam_items 同一事务入库。
|
||||
def create_case_full(data, user, institution=_UNSET, require_scoring_rules=True):
|
||||
"""主表 + 子表 + scoring_rules + exam_items 同一事务入库。
|
||||
|
||||
institution:病例所属机构;默认取创建者所属机构(`user.institution`)。
|
||||
require_scoring_rules:是否强制评分规则(≥1 条)。CMS 新增病例时为 False
|
||||
(评分规则延迟到「发布」时由 AI 生成并落库);业务域 full-create 仍为 True。
|
||||
返回 (case, scoring_rule_count, exam_item_count)。校验失败抛 AppError。
|
||||
"""
|
||||
if institution is _UNSET:
|
||||
@@ -74,9 +76,10 @@ def create_case_full(data, user, institution=_UNSET):
|
||||
raise AppError('CASE_SUBTYPE_CONFLICT', '不允许同时传入两种子表数据', status_code=400)
|
||||
|
||||
scoring_rules_data = data.get('scoring_rules', [])
|
||||
if not scoring_rules_data:
|
||||
if require_scoring_rules and not scoring_rules_data:
|
||||
raise AppError('CASE_VALIDATION_ERROR', 'scoring_rules 必填且至少 1 条', status_code=400)
|
||||
validate_scoring_rules(scoring_rules_data)
|
||||
if scoring_rules_data:
|
||||
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)
|
||||
@@ -113,6 +116,119 @@ def create_case_full(data, user, institution=_UNSET):
|
||||
return case, len(rule_objs), len(exam_items_data)
|
||||
|
||||
|
||||
# 与 scoring_rule_generator.CONTEXT_FIELDS 对应的主表字段(发布时据此构建生成入参)
|
||||
_SCORING_CONTEXT_FIELDS = (
|
||||
'title', 'chief_complaint', 'description', 'patient_age', 'patient_gender',
|
||||
'icd_codes', 'symptom_tags', 'disease_tags', 'competency_tags',
|
||||
'guideline_tags', 'knowledge_points',
|
||||
)
|
||||
|
||||
|
||||
def build_scoring_rule_input(case):
|
||||
"""由已落库的病例对象构建评分规则生成入参(供发布时调用 AI 生成评分规则)。"""
|
||||
data = {'case_type': case.case_type}
|
||||
for field in _SCORING_CONTEXT_FIELDS:
|
||||
data[field] = getattr(case, field, None)
|
||||
|
||||
if case.case_type == 'traditional':
|
||||
try:
|
||||
tc = case.traditionalcase
|
||||
data['traditional'] = {
|
||||
'standard_diagnosis': tc.standard_diagnosis,
|
||||
'standard_treatment': tc.standard_treatment,
|
||||
'guideline_reference': tc.guideline_reference,
|
||||
}
|
||||
except TraditionalCase.DoesNotExist:
|
||||
data['traditional'] = {}
|
||||
else:
|
||||
try:
|
||||
tc = case.teachingcase
|
||||
data['teaching'] = {
|
||||
'teaching_goal': tc.teaching_goal,
|
||||
'discussion_questions': tc.discussion_questions,
|
||||
'teacher_guide': tc.teacher_guide,
|
||||
'scoring_focus': tc.scoring_focus,
|
||||
}
|
||||
except TeachingCase.DoesNotExist:
|
||||
data['teaching'] = {}
|
||||
return data
|
||||
|
||||
|
||||
def update_case_full(case, data, user):
|
||||
"""更新草稿病例内容(主表 + 子表 + exam_items,scoring_rules 可选)。
|
||||
|
||||
供 CMS「编辑录入后的表单再次保存」复用:三种录入方式(表单 / PDF / AI)的病例数据
|
||||
都汇总到前端表单,再次保存时整体更新草稿。仅草稿(publish_status=0)可更新。
|
||||
- 主表字段:传入即覆盖(CASE_BASE_FIELDS 白名单)。
|
||||
- 子表:按 case_type 局部更新;不允许传入另一种子表。
|
||||
- exam_items:传入即整表替换(去重校验);不传则保持不变。
|
||||
- scoring_rules:通常发布时才生成;如传入则整表替换(校验)。
|
||||
返回更新后的 case。校验失败抛 AppError。
|
||||
"""
|
||||
if case.publish_status != 0:
|
||||
raise AppError('CASE_NOT_EDITABLE', '仅草稿可编辑,请先下架', status_code=400)
|
||||
|
||||
if 'stages' in data:
|
||||
raise AppError('CASE_FIELD_NOT_ALLOWED', '本接口不接收 stages 字段', status_code=400)
|
||||
|
||||
case_type = case.case_type
|
||||
new_type = data.get('case_type')
|
||||
if new_type and new_type != case_type:
|
||||
raise AppError('CASE_TYPE_NOT_SUPPORTED', '不支持修改 case_type', status_code=400)
|
||||
|
||||
other_type = 'teaching' if case_type == 'traditional' else 'traditional'
|
||||
if data.get(other_type):
|
||||
raise AppError('CASE_SUBTYPE_CONFLICT', '不允许传入另一种子表数据', status_code=400)
|
||||
|
||||
scoring_rules_data = data.get('scoring_rules')
|
||||
if scoring_rules_data is not None:
|
||||
validate_scoring_rules(scoring_rules_data)
|
||||
|
||||
exam_items_data = None
|
||||
if 'exam_items' in data:
|
||||
exam_items_data = normalize_exam_items(data.get('exam_items') or [])
|
||||
assert_no_duplicate_exam_items(exam_items_data)
|
||||
|
||||
with transaction.atomic():
|
||||
changed = []
|
||||
for field in CASE_BASE_FIELDS:
|
||||
if field in data:
|
||||
setattr(case, field, data[field])
|
||||
changed.append(field)
|
||||
if 'department_name' in data:
|
||||
case.department = resolve_department(data['department_name'])
|
||||
changed.append('department')
|
||||
if changed:
|
||||
case.save()
|
||||
|
||||
sub_data = data.get(case_type)
|
||||
if sub_data:
|
||||
sub_model = TraditionalCase if case_type == 'traditional' else TeachingCase
|
||||
allowed_sub = TRADITIONAL_FIELDS if case_type == 'traditional' else TEACHING_FIELDS
|
||||
sub_obj, _ = sub_model.objects.get_or_create(case=case)
|
||||
for k, v in sub_data.items():
|
||||
if k in allowed_sub:
|
||||
setattr(sub_obj, k, v)
|
||||
sub_obj.save()
|
||||
|
||||
if scoring_rules_data is not None:
|
||||
case.scoring_rules.all().delete()
|
||||
ScoringRule.objects.bulk_create([
|
||||
ScoringRule(case=case, **{k: v for k, v in rule.items() if k in SCORING_RULE_FIELDS})
|
||||
for rule in scoring_rules_data
|
||||
])
|
||||
|
||||
if exam_items_data is not None:
|
||||
case.exam_items.all().delete()
|
||||
CaseExamItem.objects.bulk_create([
|
||||
CaseExamItem(case=case, **{k: v for k, v in item.items() if k in EXAM_ITEM_FIELDS})
|
||||
for item in exam_items_data
|
||||
])
|
||||
|
||||
case.refresh_from_db()
|
||||
return case
|
||||
|
||||
|
||||
def build_full_response(case):
|
||||
"""组装病例完整结构(主表 + 子表 + scoring_rules + exam_items)。"""
|
||||
if hasattr(case, '_prefetched_objects_cache') and 'exam_items' in getattr(
|
||||
|
||||
Reference in New Issue
Block a user