Files
medical_training/apps/training/models.py
T

142 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from apps.common.models import BaseModel
from apps.user.models import User
from apps.case.models import CaseBase
# ─── 只读镜像(fastapi 属主)────────────────────────────────────────────────────
# 训练相关表(training_record / training_session / training_submission /
# user_learning_profiles 等)的 schema 属主是 fastapi 服务。Django 侧一律 managed=False、
# 只读接入,供 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)取数。
class TrainingRecord(BaseModel):
"""训练记录表(只读,fastapi 属主,managed=False"""
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)
# 与公网真实表对齐(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)
class Meta:
managed = False
db_table = 'training_record'
verbose_name = '训练记录'
verbose_name_plural = '训练记录'
def __str__(self):
return f"{self.user.username} - {self.case.title}"
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 = '训练会话'
class TrainingScoreDetail(BaseModel):
"""评分明细表(只读,managed=Falsefastapi 属主)。
经 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)。
"""
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:
managed = False
db_table = 'training_score_detail'
verbose_name = '评分明细'
verbose_name_plural = '评分明细'
def __str__(self):
return f"{self.record} - {self.dimension}: {self.score}"