from __future__ import annotations import enum from datetime import date as DateType, datetime as DateTimeType from sqlalchemy import Boolean, Date, DateTime, Enum, ForeignKey, Integer, JSON, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base def enum_values(enum_cls: type[enum.Enum]) -> Enum: return Enum(enum_cls, values_callable=lambda members: [member.value for member in members], native_enum=False) class UserRole(str, enum.Enum): admin = "admin" reviewer = "reviewer" operator = "operator" class ProcessStatus(str, enum.Enum): unprocessed = "unprocessed" processing = "processing" parsed = "parsed" pending_review = "pending_review" completed = "completed" failed = "failed" class RiskLevel(str, enum.Enum): low = "low" medium = "medium" high = "high" forbidden = "forbidden" class ReviewStatus(str, enum.Enum): pending = "pending" reviewing = "reviewing" approved = "approved" needs_revision = "needs_revision" rejected = "rejected" forbidden = "forbidden" class DesensitizationStatus(str, enum.Enum): not_needed = "not_needed" needed = "needed" done = "done" unknown = "unknown" class CourseStage(str, enum.Enum): awareness = "觉知" origin = "原生" unity = "合一" vitality = "生机" heart_light = "心光" camp_general = "大本营综合" unknown = "不确定" class PrimaryTopic(str, enum.Enum): parent_child = "亲子关系" marriage = "婚姻关系" family_of_origin = "原生家庭" emotion = "情绪模式" course = "课程学习" growth = "个人成长" family_system = "家族系统" relationship_drain = "关系内耗" action = "行动力与拖延" other = "其他" class AuditStatus(str, enum.Enum): draft = "draft" pending = "pending" approved = "approved" needs_revision = "needs_revision" disabled = "disabled" class CallStatus(str, enum.Enum): callable = "callable" not_callable = "not_callable" need_review = "need_review" forbidden = "forbidden" class TaskType(str, enum.Enum): scheduled_scan = "scheduled_scan" manual_scan = "manual_scan" manual_process = "manual_process" ai_cleaning = "ai_cleaning" feishu_sync = "feishu_sync" class TaskStatus(str, enum.Enum): pending = "pending" running = "running" success = "success" failed = "failed" partial_success = "partial_success" class TargetType(str, enum.Enum): raw_qa = "raw_qa" standard_qa = "standard_qa" session = "session" class AuditAction(str, enum.Enum): approve = "approve" revise = "revise" reject = "reject" forbid = "forbid" mark_high_risk = "mark_high_risk" mark_desensitization = "mark_desensitization" mark_callable = "mark_callable" disable = "disable" class TimestampMixin: created_at: Mapped[DateTimeType] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[DateTimeType] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) class User(Base, TimestampMixin): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) name: Mapped[str] = mapped_column(String(120), nullable=False) email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) role: Mapped[UserRole] = mapped_column(enum_values(UserRole), default=UserRole.reviewer, nullable=False) class FeishuSession(Base, TimestampMixin): __tablename__ = "feishu_sessions" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) session_code: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) date: Mapped[DateType | None] = mapped_column(Date, nullable=True) teachers: Mapped[str | None] = mapped_column(String(255), nullable=True) feishu_doc_url: Mapped[str | None] = mapped_column(String(1024), nullable=True) feishu_record_id: Mapped[str | None] = mapped_column(String(128), nullable=True) source_file_name: Mapped[str | None] = mapped_column(String(255), nullable=True) source_text: Mapped[str | None] = mapped_column(Text, nullable=True) process_status: Mapped[ProcessStatus] = mapped_column( enum_values(ProcessStatus), default=ProcessStatus.unprocessed, nullable=False, index=True ) qa_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) pending_review_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) approved_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) standard_qa_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) failed_reason: Mapped[str | None] = mapped_column(Text, nullable=True) raw_items: Mapped[list["RawQAItem"]] = relationship(back_populates="session", cascade="all, delete-orphan") standard_items: Mapped[list["StandardQAItem"]] = relationship(back_populates="session") class RawQAItem(Base, TimestampMixin): __tablename__ = "raw_qa_items" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) session_id: Mapped[int] = mapped_column(ForeignKey("feishu_sessions.id"), index=True, nullable=False) qa_code: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) raw_question: Mapped[str] = mapped_column(Text, nullable=False) normalized_question: Mapped[str | None] = mapped_column(Text, nullable=True) raw_answer: Mapped[str] = mapped_column(Text, nullable=False) normalized_answer: Mapped[str | None] = mapped_column(Text, nullable=True) suggested_standard_question: Mapped[str | None] = mapped_column(Text, nullable=True) suggested_standard_answer: Mapped[str | None] = mapped_column(Text, nullable=True) answer_person: Mapped[str | None] = mapped_column(String(120), nullable=True) primary_topic: Mapped[PrimaryTopic] = mapped_column(enum_values(PrimaryTopic), default=PrimaryTopic.other, nullable=False) problem_tags: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) course_stage: Mapped[CourseStage] = mapped_column(enum_values(CourseStage), default=CourseStage.unknown, nullable=False) audience_tags: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) emotion_intensity: Mapped[str | None] = mapped_column(String(32), nullable=True) risk_level: Mapped[RiskLevel] = mapped_column(enum_values(RiskLevel), default=RiskLevel.medium, nullable=False, index=True) risk_types: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) risk_notes: Mapped[str | None] = mapped_column(Text, nullable=True) need_desensitization: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) desensitization_status: Mapped[DesensitizationStatus] = mapped_column( enum_values(DesensitizationStatus), default=DesensitizationStatus.unknown, nullable=False ) review_status: Mapped[ReviewStatus] = mapped_column( enum_values(ReviewStatus), default=ReviewStatus.pending, nullable=False, index=True ) reviewer_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) review_notes: Mapped[str | None] = mapped_column(Text, nullable=True) suggested_to_standard_qa: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) source_timestamp: Mapped[str | None] = mapped_column(String(64), nullable=True) session: Mapped[FeishuSession] = relationship(back_populates="raw_items") reviewer: Mapped[User | None] = relationship() standard_item: Mapped["StandardQAItem | None"] = relationship(back_populates="source_raw_qa", uselist=False) class StandardQAItem(Base, TimestampMixin): __tablename__ = "standard_qa_items" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) standard_code: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) source_raw_qa_id: Mapped[int] = mapped_column(ForeignKey("raw_qa_items.id"), index=True, nullable=False) session_id: Mapped[int] = mapped_column(ForeignKey("feishu_sessions.id"), index=True, nullable=False) standard_question: Mapped[str] = mapped_column(Text, nullable=False) standard_answer: Mapped[str] = mapped_column(Text, nullable=False) similar_questions: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) primary_topic: Mapped[PrimaryTopic] = mapped_column(enum_values(PrimaryTopic), default=PrimaryTopic.other, nullable=False) problem_tags: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) course_stage: Mapped[CourseStage] = mapped_column(enum_values(CourseStage), default=CourseStage.unknown, nullable=False) audience_tags: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) answer_boundary: Mapped[str | None] = mapped_column(Text, nullable=True) forbidden_expressions: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False) risk_level: Mapped[RiskLevel] = mapped_column(enum_values(RiskLevel), default=RiskLevel.medium, nullable=False, index=True) audit_status: Mapped[AuditStatus] = mapped_column(enum_values(AuditStatus), default=AuditStatus.approved, nullable=False) call_status: Mapped[CallStatus] = mapped_column(enum_values(CallStatus), default=CallStatus.not_callable, nullable=False) last_reviewer_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) disabled_reason: Mapped[str | None] = mapped_column(Text, nullable=True) source_raw_qa: Mapped[RawQAItem] = relationship(back_populates="standard_item") session: Mapped[FeishuSession] = relationship(back_populates="standard_items") last_reviewer: Mapped[User | None] = relationship() class TaskRun(Base): __tablename__ = "task_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) task_type: Mapped[TaskType] = mapped_column(enum_values(TaskType), nullable=False, index=True) task_status: Mapped[TaskStatus] = mapped_column(enum_values(TaskStatus), default=TaskStatus.pending, nullable=False, index=True) started_at: Mapped[DateTimeType | None] = mapped_column(DateTime(timezone=True), nullable=True) ended_at: Mapped[DateTimeType | None] = mapped_column(DateTime(timezone=True), nullable=True) sessions_scanned: Mapped[int] = mapped_column(Integer, default=0, nullable=False) sessions_processed: Mapped[int] = mapped_column(Integer, default=0, nullable=False) qa_created: Mapped[int] = mapped_column(Integer, default=0, nullable=False) qa_failed: Mapped[int] = mapped_column(Integer, default=0, nullable=False) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) retry_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) created_at: Mapped[DateTimeType] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) target_type: Mapped[TargetType] = mapped_column(enum_values(TargetType), nullable=False, index=True) target_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False) action: Mapped[AuditAction] = mapped_column(enum_values(AuditAction), nullable=False) before_status: Mapped[str | None] = mapped_column(String(64), nullable=True) after_status: Mapped[str | None] = mapped_column(String(64), nullable=True) comment: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[DateTimeType] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) user: Mapped[User | None] = relationship() class SystemSetting(Base, TimestampMixin): __tablename__ = "system_settings" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) key: Mapped[str] = mapped_column(String(128), unique=True, index=True, nullable=False) value: Mapped[str | None] = mapped_column(Text, nullable=True) description: Mapped[str | None] = mapped_column(Text, nullable=True)