Initial MVP for QA asset backend
This commit is contained in:
270
hy_qa_asset_backend/backend/app/models.py
Normal file
270
hy_qa_asset_backend/backend/app/models.py
Normal 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)
|
||||
Reference in New Issue
Block a user