feat: update medical training case and auth modules
This commit is contained in:
@@ -1,146 +1,146 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CaseBase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=255, verbose_name='病例标题')),
|
||||
('case_type', models.CharField(choices=[('traditional', '传统病例'), ('script', '剧本病例'), ('teaching', '教学互动病例'), ('osce', 'OSCE')], max_length=30, verbose_name='病例类型')),
|
||||
('difficulty', models.CharField(blank=True, max_length=20, verbose_name='难度')),
|
||||
('difficulty_score', models.IntegerField(blank=True, null=True, verbose_name='AI难度评分')),
|
||||
('chief_complaint', models.TextField(blank=True, verbose_name='主诉')),
|
||||
('description', models.TextField(blank=True, verbose_name='病例简介')),
|
||||
('patient_age', models.IntegerField(blank=True, null=True, verbose_name='患者年龄')),
|
||||
('patient_gender', models.CharField(blank=True, max_length=10, verbose_name='患者性别')),
|
||||
('tags', models.CharField(blank=True, max_length=500, verbose_name='标签')),
|
||||
('symptom_tags', models.JSONField(blank=True, default=list, verbose_name='症状标签')),
|
||||
('disease_tags', models.JSONField(blank=True, default=list, verbose_name='疾病标签')),
|
||||
('competency_tags', models.JSONField(blank=True, default=list, verbose_name='能力标签')),
|
||||
('guideline_tags', models.JSONField(blank=True, default=list, verbose_name='指南标签')),
|
||||
('knowledge_points', models.JSONField(blank=True, default=list, verbose_name='知识点')),
|
||||
('icd_codes', models.CharField(blank=True, max_length=500, verbose_name='ICD编码')),
|
||||
('estimated_minutes', models.IntegerField(blank=True, null=True, verbose_name='预计训练时长')),
|
||||
('osce_enabled', models.BooleanField(default=False, verbose_name='是否OSCE')),
|
||||
('rag_enabled', models.BooleanField(default=False, verbose_name='是否启用知识增强')),
|
||||
('ai_prompt_template', models.TextField(blank=True, verbose_name='AI角色Prompt')),
|
||||
('multimodal_assets', models.JSONField(blank=True, default=dict, verbose_name='图片/影像/附件')),
|
||||
('vector_status', models.SmallIntegerField(default=0, verbose_name='是否向量化')),
|
||||
('publish_status', models.SmallIntegerField(choices=[(0, '草稿'), (1, '已发布'), (2, '已下架')], default=0, verbose_name='发布状态')),
|
||||
('status', models.SmallIntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='状态')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '病例',
|
||||
'verbose_name_plural': '病例',
|
||||
'db_table': 'case_base',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CaseStage',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('stage_type', models.CharField(blank=True, max_length=50, verbose_name='阶段类型')),
|
||||
('stage_name', models.CharField(max_length=100, verbose_name='阶段名称')),
|
||||
('stage_mode', models.CharField(choices=[('dialogue', '对话'), ('osce', 'OSCE'), ('choice', '选择')], default='dialogue', max_length=30, verbose_name='阶段模式')),
|
||||
('stage_goal', models.TextField(blank=True, verbose_name='阶段目标')),
|
||||
('ai_role_prompt', models.TextField(blank=True, verbose_name='AI阶段Prompt')),
|
||||
('standard_action', models.TextField(blank=True, verbose_name='标准动作')),
|
||||
('expected_questions', models.TextField(blank=True, verbose_name='期望问题')),
|
||||
('scoring_points', models.TextField(blank=True, verbose_name='评分点')),
|
||||
('timeout_seconds', models.IntegerField(blank=True, null=True, verbose_name='超时时间')),
|
||||
('unlock_condition', models.CharField(blank=True, max_length=255, verbose_name='解锁条件')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '病例阶段',
|
||||
'verbose_name_plural': '病例阶段',
|
||||
'db_table': 'case_stage',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScoringRule',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('dimension', models.CharField(max_length=50, verbose_name='评分维度')),
|
||||
('competency_dimension', models.CharField(blank=True, max_length=50, verbose_name='能力维度')),
|
||||
('score_weight', models.DecimalField(decimal_places=2, default=1.0, max_digits=5, verbose_name='权重')),
|
||||
('ai_auto_score', models.BooleanField(default=False, verbose_name='AI自动评分')),
|
||||
('osce_dimension', models.BooleanField(default=False, verbose_name='是否OSCE')),
|
||||
('scoring_standard', models.TextField(blank=True, verbose_name='评分标准')),
|
||||
('rubric_json', models.JSONField(blank=True, default=dict, verbose_name='评分Rubric')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评分规则',
|
||||
'verbose_name_plural': '评分规则',
|
||||
'db_table': 'scoring_rule',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScriptCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('scenario_setting', models.TextField(blank=True, verbose_name='场景设定')),
|
||||
('emotional_state', models.CharField(blank=True, max_length=50, verbose_name='情绪状态')),
|
||||
('cultural_level', models.CharField(blank=True, max_length=50, verbose_name='文化水平')),
|
||||
('branch_logic', models.TextField(blank=True, verbose_name='分支逻辑')),
|
||||
('hidden_clues', models.TextField(blank=True, verbose_name='隐藏线索')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '剧本病例',
|
||||
'verbose_name_plural': '剧本病例',
|
||||
'db_table': 'script_case',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeachingCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('teaching_goal', models.TextField(blank=True, verbose_name='教学目标')),
|
||||
('discussion_questions', models.TextField(blank=True, verbose_name='讨论问题')),
|
||||
('teacher_guide', models.TextField(blank=True, verbose_name='教师指南')),
|
||||
('scoring_focus', models.TextField(blank=True, verbose_name='评分重点')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '教学互动病例',
|
||||
'verbose_name_plural': '教学互动病例',
|
||||
'db_table': 'teaching_case',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TraditionalCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('standard_diagnosis', models.TextField(blank=True, verbose_name='标准诊断')),
|
||||
('standard_treatment', models.TextField(blank=True, verbose_name='标准治疗')),
|
||||
('guideline_reference', models.TextField(blank=True, verbose_name='指南参考')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '传统病例',
|
||||
'verbose_name_plural': '传统病例',
|
||||
'db_table': 'traditional_case',
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CaseBase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=255, verbose_name='病例标题')),
|
||||
('case_type', models.CharField(choices=[('traditional', '传统病例'), ('script', '剧本病例'), ('teaching', '教学互动病例'), ('osce', 'OSCE')], max_length=30, verbose_name='病例类型')),
|
||||
('difficulty', models.CharField(blank=True, max_length=20, verbose_name='难度')),
|
||||
('difficulty_score', models.IntegerField(blank=True, null=True, verbose_name='AI难度评分')),
|
||||
('chief_complaint', models.TextField(blank=True, verbose_name='主诉')),
|
||||
('description', models.TextField(blank=True, verbose_name='病例简介')),
|
||||
('patient_age', models.IntegerField(blank=True, null=True, verbose_name='患者年龄')),
|
||||
('patient_gender', models.CharField(blank=True, max_length=10, verbose_name='患者性别')),
|
||||
('tags', models.CharField(blank=True, max_length=500, verbose_name='标签')),
|
||||
('symptom_tags', models.JSONField(blank=True, default=list, verbose_name='症状标签')),
|
||||
('disease_tags', models.JSONField(blank=True, default=list, verbose_name='疾病标签')),
|
||||
('competency_tags', models.JSONField(blank=True, default=list, verbose_name='能力标签')),
|
||||
('guideline_tags', models.JSONField(blank=True, default=list, verbose_name='指南标签')),
|
||||
('knowledge_points', models.JSONField(blank=True, default=list, verbose_name='知识点')),
|
||||
('icd_codes', models.CharField(blank=True, max_length=500, verbose_name='ICD编码')),
|
||||
('estimated_minutes', models.IntegerField(blank=True, null=True, verbose_name='预计训练时长')),
|
||||
('osce_enabled', models.BooleanField(default=False, verbose_name='是否OSCE')),
|
||||
('rag_enabled', models.BooleanField(default=False, verbose_name='是否启用知识增强')),
|
||||
('ai_prompt_template', models.TextField(blank=True, verbose_name='AI角色Prompt')),
|
||||
('multimodal_assets', models.JSONField(blank=True, default=dict, verbose_name='图片/影像/附件')),
|
||||
('vector_status', models.SmallIntegerField(default=0, verbose_name='是否向量化')),
|
||||
('publish_status', models.SmallIntegerField(choices=[(0, '草稿'), (1, '已发布'), (2, '已下架')], default=0, verbose_name='发布状态')),
|
||||
('status', models.SmallIntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='状态')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '病例',
|
||||
'verbose_name_plural': '病例',
|
||||
'db_table': 'case_base',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CaseStage',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('stage_type', models.CharField(blank=True, max_length=50, verbose_name='阶段类型')),
|
||||
('stage_name', models.CharField(max_length=100, verbose_name='阶段名称')),
|
||||
('stage_mode', models.CharField(choices=[('dialogue', '对话'), ('osce', 'OSCE'), ('choice', '选择')], default='dialogue', max_length=30, verbose_name='阶段模式')),
|
||||
('stage_goal', models.TextField(blank=True, verbose_name='阶段目标')),
|
||||
('ai_role_prompt', models.TextField(blank=True, verbose_name='AI阶段Prompt')),
|
||||
('standard_action', models.TextField(blank=True, verbose_name='标准动作')),
|
||||
('expected_questions', models.TextField(blank=True, verbose_name='期望问题')),
|
||||
('scoring_points', models.TextField(blank=True, verbose_name='评分点')),
|
||||
('timeout_seconds', models.IntegerField(blank=True, null=True, verbose_name='超时时间')),
|
||||
('unlock_condition', models.CharField(blank=True, max_length=255, verbose_name='解锁条件')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '病例阶段',
|
||||
'verbose_name_plural': '病例阶段',
|
||||
'db_table': 'case_stage',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScoringRule',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('dimension', models.CharField(max_length=50, verbose_name='评分维度')),
|
||||
('competency_dimension', models.CharField(blank=True, max_length=50, verbose_name='能力维度')),
|
||||
('score_weight', models.DecimalField(decimal_places=2, default=1.0, max_digits=5, verbose_name='权重')),
|
||||
('ai_auto_score', models.BooleanField(default=False, verbose_name='AI自动评分')),
|
||||
('osce_dimension', models.BooleanField(default=False, verbose_name='是否OSCE')),
|
||||
('scoring_standard', models.TextField(blank=True, verbose_name='评分标准')),
|
||||
('rubric_json', models.JSONField(blank=True, default=dict, verbose_name='评分Rubric')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评分规则',
|
||||
'verbose_name_plural': '评分规则',
|
||||
'db_table': 'scoring_rule',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScriptCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('scenario_setting', models.TextField(blank=True, verbose_name='场景设定')),
|
||||
('emotional_state', models.CharField(blank=True, max_length=50, verbose_name='情绪状态')),
|
||||
('cultural_level', models.CharField(blank=True, max_length=50, verbose_name='文化水平')),
|
||||
('branch_logic', models.TextField(blank=True, verbose_name='分支逻辑')),
|
||||
('hidden_clues', models.TextField(blank=True, verbose_name='隐藏线索')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '剧本病例',
|
||||
'verbose_name_plural': '剧本病例',
|
||||
'db_table': 'script_case',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeachingCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('teaching_goal', models.TextField(blank=True, verbose_name='教学目标')),
|
||||
('discussion_questions', models.TextField(blank=True, verbose_name='讨论问题')),
|
||||
('teacher_guide', models.TextField(blank=True, verbose_name='教师指南')),
|
||||
('scoring_focus', models.TextField(blank=True, verbose_name='评分重点')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '教学互动病例',
|
||||
'verbose_name_plural': '教学互动病例',
|
||||
'db_table': 'teaching_case',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TraditionalCase',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('standard_diagnosis', models.TextField(blank=True, verbose_name='标准诊断')),
|
||||
('standard_treatment', models.TextField(blank=True, verbose_name='标准治疗')),
|
||||
('guideline_reference', models.TextField(blank=True, verbose_name='指南参考')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '传统病例',
|
||||
'verbose_name_plural': '传统病例',
|
||||
'db_table': 'traditional_case',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('case', '0001_initial'),
|
||||
('user', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='casebase',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casebase',
|
||||
name='department',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.department', verbose_name='所属科室'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casestage',
|
||||
name='case',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scoringrule',
|
||||
name='case',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scoring_rules', to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scriptcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='teachingcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='traditionalcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('case', '0001_initial'),
|
||||
('user', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='casebase',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casebase',
|
||||
name='department',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.department', verbose_name='所属科室'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casestage',
|
||||
name='case',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scoringrule',
|
||||
name='case',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scoring_rules', to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scriptcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='teachingcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='traditionalcase',
|
||||
name='case',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='case.casebase', verbose_name='病例'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-03 07:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('case', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CaseExamItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('item_code', models.CharField(max_length=64, verbose_name='检查项目编码')),
|
||||
('item_name', models.CharField(max_length=128, verbose_name='检查项目名称')),
|
||||
('item_type', models.CharField(max_length=32, verbose_name='项目类型')),
|
||||
('category', models.CharField(blank=True, default='', max_length=64, verbose_name='项目分类')),
|
||||
('result_text', models.TextField(verbose_name='结果文本')),
|
||||
('result_structured', models.JSONField(blank=True, default=None, null=True, verbose_name='结构化结果')),
|
||||
('is_key', models.BooleanField(default=False, verbose_name='是否关键检查')),
|
||||
('is_abnormal', models.BooleanField(default=False, verbose_name='是否异常')),
|
||||
('score_weight', models.DecimalField(decimal_places=2, default=1.0, max_digits=5, verbose_name='评分权重')),
|
||||
('display_order', models.IntegerField(default=0, verbose_name='展示顺序')),
|
||||
('case', models.ForeignKey(db_column='case_id', on_delete=django.db.models.deletion.CASCADE, related_name='exam_items', to='case.casebase', verbose_name='病例')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '病例检查项',
|
||||
'verbose_name_plural': '病例检查项',
|
||||
'db_table': 'case_exam_item',
|
||||
'ordering': ['display_order', 'id'],
|
||||
'constraints': [models.UniqueConstraint(fields=('case', 'item_code'), name='uk_case_exam_item_code')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,2 +1,2 @@
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -183,3 +183,37 @@ class ScoringRule(BaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.case.title} - {self.dimension}"
|
||||
|
||||
|
||||
class CaseExamItem(BaseModel):
|
||||
"""病例检查/检验项目表(与 consultation 库 case_exam_item 同构)"""
|
||||
case = models.ForeignKey(
|
||||
CaseBase, on_delete=models.CASCADE,
|
||||
related_name='exam_items', db_column='case_id',
|
||||
verbose_name='病例',
|
||||
)
|
||||
item_code = models.CharField('检查项目编码', max_length=64)
|
||||
item_name = models.CharField('检查项目名称', max_length=128)
|
||||
item_type = models.CharField('项目类型', max_length=32)
|
||||
category = models.CharField('项目分类', max_length=64, blank=True, default='')
|
||||
result_text = models.TextField('结果文本')
|
||||
result_structured = models.JSONField('结构化结果', null=True, blank=True, default=None)
|
||||
is_key = models.BooleanField('是否关键检查', default=False)
|
||||
is_abnormal = models.BooleanField('是否异常', default=False)
|
||||
score_weight = models.DecimalField('评分权重', max_digits=5, decimal_places=2, default=1.00)
|
||||
display_order = models.IntegerField('展示顺序', default=0)
|
||||
|
||||
class Meta:
|
||||
db_table = 'case_exam_item'
|
||||
verbose_name = '病例检查项'
|
||||
verbose_name_plural = '病例检查项'
|
||||
ordering = ['display_order', 'id']
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['case', 'item_code'],
|
||||
name='uk_case_exam_item_code',
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.case_id}:{self.item_code}"
|
||||
|
||||
@@ -60,6 +60,28 @@
|
||||
},
|
||||
"osce_enabled": { "type": "boolean" },
|
||||
"department_name": { "type": "string" },
|
||||
"exam_items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["item_code", "item_name", "item_type", "result_text"],
|
||||
"properties": {
|
||||
"item_code": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"item_name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"item_type": { "type": "string", "minLength": 1, "maxLength": 32 },
|
||||
"category": { "type": "string", "maxLength": 64 },
|
||||
"result_text": { "type": "string", "minLength": 1 },
|
||||
"result_structured": {
|
||||
"oneOf": [{ "type": "object" }, { "type": "null" }]
|
||||
},
|
||||
"is_key": { "type": "boolean" },
|
||||
"is_abnormal": { "type": "boolean" },
|
||||
"score_weight": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"display_order": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"traditional": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from config.exceptions import AppError
|
||||
from . import deepseek_client
|
||||
from .pdf_reader import extract_text_from_pdfs
|
||||
from .exam_items import normalize_exam_items
|
||||
from prompts.loader import load_prompt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,6 +39,10 @@ def parse_pdf(files, case_type: str, user) -> dict:
|
||||
data.pop('stages', None)
|
||||
|
||||
data['case_type'] = case_type
|
||||
if 'exam_items' in data:
|
||||
data['exam_items'] = normalize_exam_items(data.get('exam_items') or [])
|
||||
else:
|
||||
data['exam_items'] = []
|
||||
|
||||
_strip_unknown_fields(data)
|
||||
_validate_schema(data)
|
||||
@@ -72,7 +77,7 @@ _SCHEMA_ALLOWED_KEYS = {
|
||||
'patient_age', 'patient_gender', 'tags', 'symptom_tags', 'disease_tags',
|
||||
'competency_tags', 'guideline_tags', 'knowledge_points', 'icd_codes',
|
||||
'estimated_minutes', 'osce_enabled', 'department_name',
|
||||
'traditional', 'teaching',
|
||||
'exam_items', 'traditional', 'teaching',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""病例检查项:C1 解析归一化、C3 落库校验。"""
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from config.exceptions import AppError
|
||||
|
||||
EXAM_ITEM_FIELDS = {
|
||||
'item_code', 'item_name', 'item_type', 'category',
|
||||
'result_text', 'result_structured',
|
||||
'is_key', 'is_abnormal', 'score_weight', 'display_order',
|
||||
}
|
||||
|
||||
|
||||
def normalize_exam_items(raw_items) -> list:
|
||||
"""归一化检查项列表;按 item_code 去重(保留首次出现)。"""
|
||||
if not raw_items:
|
||||
return []
|
||||
if not isinstance(raw_items, list):
|
||||
raise AppError('CASE_VALIDATION_ERROR', 'exam_items 必须为数组', status_code=400)
|
||||
|
||||
seen = set()
|
||||
normalized = []
|
||||
for i, item in enumerate(raw_items):
|
||||
if not isinstance(item, dict):
|
||||
raise AppError('CASE_VALIDATION_ERROR', f'exam_items[{i}] 必须为对象', status_code=400)
|
||||
code = (item.get('item_code') or '').strip()
|
||||
name = (item.get('item_name') or '').strip()
|
||||
item_type = (item.get('item_type') or '').strip()
|
||||
result_text = (item.get('result_text') or '').strip()
|
||||
if not code:
|
||||
raise AppError('CASE_VALIDATION_ERROR', f'exam_items[{i}].item_code 必填', status_code=400)
|
||||
if not name:
|
||||
raise AppError('CASE_VALIDATION_ERROR', f'exam_items[{i}].item_name 必填', status_code=400)
|
||||
if not item_type:
|
||||
raise AppError('CASE_VALIDATION_ERROR', f'exam_items[{i}].item_type 必填', status_code=400)
|
||||
if not result_text:
|
||||
raise AppError('CASE_VALIDATION_ERROR', f'exam_items[{i}].result_text 必填', status_code=400)
|
||||
if code in seen:
|
||||
continue
|
||||
seen.add(code)
|
||||
|
||||
try:
|
||||
weight = item.get('score_weight', 1)
|
||||
weight = Decimal(str(weight))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
raise AppError(
|
||||
'CASE_VALIDATION_ERROR',
|
||||
f'exam_items[{i}].score_weight 须为数字',
|
||||
status_code=400,
|
||||
)
|
||||
if weight <= 0:
|
||||
raise AppError(
|
||||
'CASE_VALIDATION_ERROR',
|
||||
f'exam_items[{i}].score_weight 须大于 0',
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
normalized.append({
|
||||
'item_code': code[:64],
|
||||
'item_name': name[:128],
|
||||
'item_type': item_type[:32],
|
||||
'category': (item.get('category') or '')[:64],
|
||||
'result_text': result_text,
|
||||
'result_structured': item.get('result_structured') if isinstance(
|
||||
item.get('result_structured'), dict
|
||||
) else None,
|
||||
'is_key': bool(item.get('is_key', False)),
|
||||
'is_abnormal': bool(item.get('is_abnormal', False)),
|
||||
'score_weight': float(weight),
|
||||
'display_order': int(item.get('display_order', len(normalized) + 1)),
|
||||
})
|
||||
return normalized
|
||||
|
||||
|
||||
def assert_no_duplicate_exam_items(items: list):
|
||||
"""创建前再次校验同一病例内 item_code 不重复。"""
|
||||
codes = [it['item_code'] for it in items]
|
||||
if len(codes) != len(set(codes)):
|
||||
raise AppError(
|
||||
'CASE_EXAM_ITEM_DUPLICATE',
|
||||
'同一病例内检查项 item_code 不能重复',
|
||||
status_code=400,
|
||||
)
|
||||
+49
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user