Initial MVP for QA asset backend

This commit is contained in:
2026-07-05 17:44:15 +08:00
commit 95b5e09fd4
71 changed files with 6546 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
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)