2026-05-29 15:58:00 +08:00
|
|
|
|
from django.db import models
|
|
|
|
|
|
from apps.common.models import BaseModel
|
|
|
|
|
|
from apps.user.models import User
|
|
|
|
|
|
from apps.case.models import CaseBase
|
|
|
|
|
|
|
2026-06-11 10:37:29 +08:00
|
|
|
|
# ─── 只读镜像(fastapi 属主)────────────────────────────────────────────────────
|
|
|
|
|
|
# 训练相关表(training_record / training_session / training_submission /
|
|
|
|
|
|
# user_learning_profiles 等)的 schema 属主是 fastapi 服务。Django 侧一律 managed=False、
|
2026-06-12 11:11:48 +08:00
|
|
|
|
# 只读接入,供 CMS 查询训练记录、移动端个人中心统计/分析,不写。
|
|
|
|
|
|
# ✅ 已用 `python manage.py inspectdb training_record training_score_detail` 对本地同步库
|
|
|
|
|
|
# (medical_platform) 反向校准:下方字段与真实列一致;两张表均已存在。
|
|
|
|
|
|
# 注意 training_score_detail 真实表「无 max_score 列」,故得分率需从
|
|
|
|
|
|
# training_record.ai_feedback_structured.dimension_scores(带 max_score)取数。
|
2026-06-11 10:37:29 +08:00
|
|
|
|
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
class TrainingRecord(BaseModel):
|
2026-06-11 10:37:29 +08:00
|
|
|
|
"""训练记录表(只读,fastapi 属主,managed=False)"""
|
2026-05-29 15:58:00 +08:00
|
|
|
|
TRAINING_MODE_CHOICES = [
|
|
|
|
|
|
('novice', '新手'),
|
|
|
|
|
|
('practice', '练习'),
|
|
|
|
|
|
('exam', '考试'),
|
|
|
|
|
|
]
|
|
|
|
|
|
EVALUATION_LEVEL_CHOICES = [
|
|
|
|
|
|
('excellent', '优秀'),
|
|
|
|
|
|
('good', '良好'),
|
|
|
|
|
|
('average', '一般'),
|
|
|
|
|
|
('poor', '较差'),
|
|
|
|
|
|
]
|
|
|
|
|
|
STATUS_CHOICES = [
|
|
|
|
|
|
('in_progress', '进行中'),
|
|
|
|
|
|
('completed', '已完成'),
|
|
|
|
|
|
('aborted', '已中断'),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
|
User, on_delete=models.CASCADE,
|
|
|
|
|
|
related_name='training_records', verbose_name='用户'
|
|
|
|
|
|
)
|
|
|
|
|
|
case = models.ForeignKey(
|
|
|
|
|
|
CaseBase, on_delete=models.CASCADE,
|
|
|
|
|
|
related_name='training_records', verbose_name='病例'
|
|
|
|
|
|
)
|
|
|
|
|
|
training_mode = models.CharField('训练模式', max_length=50, choices=TRAINING_MODE_CHOICES)
|
|
|
|
|
|
case_type = models.CharField('病例类型', max_length=30, blank=True)
|
|
|
|
|
|
teacher = models.ForeignKey(
|
|
|
|
|
|
User, on_delete=models.SET_NULL,
|
|
|
|
|
|
null=True, blank=True, related_name='supervised_records',
|
|
|
|
|
|
verbose_name='带教老师'
|
|
|
|
|
|
)
|
|
|
|
|
|
start_time = models.DateTimeField('开始时间', auto_now_add=True)
|
|
|
|
|
|
end_time = models.DateTimeField('结束时间', null=True, blank=True)
|
|
|
|
|
|
duration_seconds = models.IntegerField('训练时长', null=True, blank=True)
|
|
|
|
|
|
total_score = models.DecimalField('总分', max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
|
|
ai_score = models.DecimalField('AI评分', max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
|
|
teacher_score = models.DecimalField('教师评分', max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
|
|
evaluation_level = models.CharField('评价等级', max_length=20, choices=EVALUATION_LEVEL_CHOICES, blank=True)
|
|
|
|
|
|
status = models.CharField('状态', max_length=30, choices=STATUS_CHOICES, default='in_progress')
|
|
|
|
|
|
feedback = models.TextField('总评', blank=True)
|
|
|
|
|
|
thinking_chain = models.TextField('临床推理链', blank=True)
|
|
|
|
|
|
diagnosis_path = models.TextField('诊断路径', blank=True)
|
|
|
|
|
|
wrong_points = models.JSONField('错误知识点', default=list, blank=True)
|
|
|
|
|
|
missed_questions = models.JSONField('漏问项', default=list, blank=True)
|
|
|
|
|
|
recommendation_result = models.JSONField('AI推荐', default=dict, blank=True)
|
|
|
|
|
|
ai_feedback_structured = models.JSONField('AI结构化反馈', default=dict, blank=True)
|
|
|
|
|
|
osce_station_score = models.JSONField('OSCE各站点成绩', default=dict, blank=True)
|
|
|
|
|
|
interruption_count = models.IntegerField('中断次数', default=0)
|
|
|
|
|
|
emotion_analysis = models.JSONField('情绪分析', default=dict, blank=True)
|
|
|
|
|
|
prompt_version = models.CharField('Prompt版本', max_length=50, blank=True)
|
|
|
|
|
|
rag_context_version = models.CharField('知识上下文版本', max_length=50, blank=True)
|
2026-06-12 11:11:48 +08:00
|
|
|
|
# 与公网真实表对齐(inspectdb 校准补充列)
|
|
|
|
|
|
external_user_id = models.CharField('宿主系统用户ID', max_length=128, blank=True, default='')
|
|
|
|
|
|
session_id = models.BigIntegerField('训练会话ID', null=True, blank=True)
|
|
|
|
|
|
evaluation_record_id = models.BigIntegerField('评价记录ID', null=True, blank=True)
|
|
|
|
|
|
score_type = models.CharField('分数类型', max_length=20, blank=True, default='percentage')
|
|
|
|
|
|
pdf_file_path = models.CharField('报告PDF路径', max_length=512, null=True, blank=True)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
2026-06-11 10:37:29 +08:00
|
|
|
|
managed = False
|
2026-05-29 15:58:00 +08:00
|
|
|
|
db_table = 'training_record'
|
|
|
|
|
|
verbose_name = '训练记录'
|
|
|
|
|
|
verbose_name_plural = '训练记录'
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"{self.user.username} - {self.case.title}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 01:44:31 +08:00
|
|
|
|
class TrainingSession(BaseModel):
|
|
|
|
|
|
"""训练会话表(只读,fastapi 属主,managed=False)。
|
|
|
|
|
|
|
|
|
|
|
|
用于**平台级**「发起/完成」统计:`COUNT(*)`=累计发起(含未完成),`completed_at` 非空=已完成。
|
|
|
|
|
|
⚠️ 只有 `external_user_id`(varchar)、**无 `user.id`、无机构外键** → 不能按机构/科室聚合;
|
|
|
|
|
|
需按机构聚合的训练统计一律改用 `TrainingRecord`(有 `user_id`)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
|
|
|
|
case_id = models.BigIntegerField('病例ID', null=True, blank=True)
|
|
|
|
|
|
case_type = models.CharField('病例类型', max_length=30, blank=True)
|
|
|
|
|
|
training_mode = models.CharField('训练模式', max_length=50, blank=True)
|
|
|
|
|
|
status = models.CharField('状态', max_length=30, blank=True)
|
|
|
|
|
|
external_user_id = models.CharField('宿主系统用户ID', max_length=128, blank=True)
|
|
|
|
|
|
started_at = models.DateTimeField('开始时间', null=True, blank=True)
|
|
|
|
|
|
completed_at = models.DateTimeField('完成时间', null=True, blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
managed = False
|
|
|
|
|
|
db_table = 'training_session'
|
|
|
|
|
|
verbose_name = '训练会话'
|
|
|
|
|
|
verbose_name_plural = '训练会话'
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 15:58:00 +08:00
|
|
|
|
class TrainingScoreDetail(BaseModel):
|
2026-06-12 11:11:48 +08:00
|
|
|
|
"""评分明细表(只读,managed=False,fastapi 属主)。
|
|
|
|
|
|
|
|
|
|
|
|
经 inspectdb 校准:真实表含 record_id/rule_id/dimension/score/deducted_reason/
|
|
|
|
|
|
evidence_message_ids/ai_confidence/comment 等列,**无 max_score 列**。
|
|
|
|
|
|
雷达/得分率改从 training_record.ai_feedback_structured.dimension_scores 取数(带 max_score)。
|
|
|
|
|
|
"""
|
2026-05-29 15:58:00 +08:00
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
|
|
|
|
record = models.ForeignKey(
|
|
|
|
|
|
TrainingRecord, on_delete=models.CASCADE,
|
|
|
|
|
|
related_name='score_details', verbose_name='训练记录'
|
|
|
|
|
|
)
|
|
|
|
|
|
rule = models.ForeignKey(
|
|
|
|
|
|
'case.ScoringRule', on_delete=models.CASCADE,
|
|
|
|
|
|
null=True, blank=True, verbose_name='评分规则'
|
|
|
|
|
|
)
|
|
|
|
|
|
dimension = models.CharField('评分维度', max_length=50)
|
|
|
|
|
|
score = models.DecimalField('分数', max_digits=5, decimal_places=2)
|
|
|
|
|
|
deducted_reason = models.TextField('扣分原因', blank=True)
|
|
|
|
|
|
evidence_message_ids = models.JSONField('对应对话证据', default=list, blank=True)
|
|
|
|
|
|
ai_confidence = models.DecimalField('AI评分置信度', max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
|
|
comment = models.TextField('评语', blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
2026-06-11 10:37:29 +08:00
|
|
|
|
managed = False
|
2026-05-29 15:58:00 +08:00
|
|
|
|
db_table = 'training_score_detail'
|
|
|
|
|
|
verbose_name = '评分明细'
|
|
|
|
|
|
verbose_name_plural = '评分明细'
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"{self.record} - {self.dimension}: {self.score}"
|