from __future__ import annotations from datetime import datetime from decimal import Decimal from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, JSON, Numeric, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base from app.models.mixins import TimestampMixin BIGINT_PK = BigInteger().with_variant(Integer, "sqlite") class TrainingRecord(TimestampMixin, Base): """训练记录表:完整完成问诊、诊断、治疗和评价后写入长期记录。""" __tablename__ = "training_record" id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="训练记录ID") training_mode: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="训练模式") case_type: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="病例/训练类型") start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="训练开始时间") end_time: Mapped[datetime | None] = mapped_column(DateTime, comment="训练结束时间") duration_seconds: Mapped[int | None] = mapped_column(Integer, comment="训练持续秒数") total_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="总分") ai_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="AI评分") teacher_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="教师评分") evaluation_level: Mapped[str] = mapped_column(String(20), nullable=False, default="", comment="评价等级") status: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="记录状态") feedback: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="总体反馈") thinking_chain: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="诊断循证与评分依据摘要") diagnosis_path: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="诊断路径摘要") wrong_points: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="错误点/扣分点") missed_questions: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="遗漏问题") recommendation_result: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="改进建议和导出结果") ai_feedback_structured: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="AI结构化评价") osce_station_score: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="OSCE站点评分") interruption_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="中断次数") emotion_analysis: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="情绪分析") prompt_version: Mapped[str] = mapped_column(String(50), nullable=False, default="v1", comment="提示词版本") rag_context_version: Mapped[str] = mapped_column(String(50), nullable=False, default="none", comment="RAG上下文版本") case_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True, comment="病例ID") teacher_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="教师ID") user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="Django用户中心数字ID") external_user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="Django用户中心ID") session_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="训练会话ID") evaluation_record_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="兼容旧评价记录ID") score_type: Mapped[str] = mapped_column(String(20), nullable=False, default="percentage", comment="分数类型") pdf_file_path: Mapped[str | None] = mapped_column(String(512), comment="PDF报告路径") __table_args__ = ( UniqueConstraint("session_id", name="uk_training_record_session"), {"comment": "训练记录表"}, ) score_details = relationship("TrainingScoreDetail", back_populates="record", cascade="all, delete-orphan") class TrainingScoreDetail(TimestampMixin, Base): """评分明细表:保存每条 scoring_rule 对应的 AI 评分、扣分原因、证据和置信度。""" __tablename__ = "training_score_detail" id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="评分明细ID") record_id: Mapped[int] = mapped_column(ForeignKey("training_record.id"), nullable=False, index=True, comment="训练记录ID") rule_id: Mapped[int | None] = mapped_column(ForeignKey("scoring_rule.id"), nullable=True, index=True, comment="评分规则ID") dimension: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="评分维度") score: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=0, comment="分数") deducted_reason: Mapped[str | None] = mapped_column(Text, comment="扣分原因") evidence_message_ids: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="对应对话证据") ai_confidence: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="AI评分置信度") comment: Mapped[str | None] = mapped_column(Text, comment="评语") record = relationship("TrainingRecord", back_populates="score_details") __table_args__ = {"comment": "评分明细表"}