Files
arboris-novel/backend/app/models/novel.py
2025-10-21 09:51:27 +08:00

226 lines
10 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import JSON, BigInteger, DateTime, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base import Base
# 自定义列类型:兼容跨数据库环境
BIGINT_PK_TYPE = BigInteger().with_variant(Integer, "sqlite")
LONG_TEXT_TYPE = Text().with_variant(LONGTEXT, "mysql")
class _MetadataAccessor:
"""Descriptor 用于将 `metadata` 访问重定向到 `metadata_`,且保持 Base.metadata 可用。"""
def __get__(self, instance, owner):
if instance is None:
return Base.metadata
return instance.metadata_
def __set__(self, instance, value):
instance.metadata_ = value
class NovelProject(Base):
"""小说项目主表,仅存放轻量级元数据。"""
__tablename__ = "novel_projects"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
initial_prompt: Mapped[Optional[str]] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), default="draft")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
owner: Mapped["User"] = relationship("User", back_populates="novel_projects")
blueprint: Mapped[Optional["NovelBlueprint"]] = relationship(
back_populates="project", cascade="all, delete-orphan", uselist=False
)
conversations: Mapped[list["NovelConversation"]] = relationship(
back_populates="project", cascade="all, delete-orphan", order_by="NovelConversation.seq"
)
characters: Mapped[list["BlueprintCharacter"]] = relationship(
back_populates="project", cascade="all, delete-orphan", order_by="BlueprintCharacter.position"
)
relationships_: Mapped[list["BlueprintRelationship"]] = relationship(
back_populates="project", cascade="all, delete-orphan", order_by="BlueprintRelationship.position"
)
outlines: Mapped[list["ChapterOutline"]] = relationship(
back_populates="project", cascade="all, delete-orphan", order_by="ChapterOutline.chapter_number"
)
chapters: Mapped[list["Chapter"]] = relationship(
back_populates="project", cascade="all, delete-orphan", order_by="Chapter.chapter_number"
)
class NovelConversation(Base):
"""对话记录表,存储概念阶段的连续对话。"""
__tablename__ = "novel_conversations"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
project_id: Mapped[str] = mapped_column(ForeignKey("novel_projects.id", ondelete="CASCADE"), nullable=False)
seq: Mapped[int] = mapped_column(Integer, nullable=False)
role: Mapped[str] = mapped_column(String(32), nullable=False)
content: Mapped[str] = mapped_column(LONG_TEXT_TYPE, nullable=False)
metadata_: Mapped[Optional[dict]] = mapped_column("metadata", JSON)
metadata = _MetadataAccessor()
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
project: Mapped[NovelProject] = relationship(back_populates="conversations")
class NovelBlueprint(Base):
"""蓝图主体信息(标题、风格等)。"""
__tablename__ = "novel_blueprints"
project_id: Mapped[str] = mapped_column(
ForeignKey("novel_projects.id", ondelete="CASCADE"), primary_key=True
)
title: Mapped[Optional[str]] = mapped_column(String(255))
target_audience: Mapped[Optional[str]] = mapped_column(String(255))
genre: Mapped[Optional[str]] = mapped_column(String(128))
style: Mapped[Optional[str]] = mapped_column(String(128))
tone: Mapped[Optional[str]] = mapped_column(String(128))
one_sentence_summary: Mapped[Optional[str]] = mapped_column(Text)
full_synopsis: Mapped[Optional[str]] = mapped_column(LONG_TEXT_TYPE)
world_setting: Mapped[Optional[dict]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
project: Mapped[NovelProject] = relationship(back_populates="blueprint")
class BlueprintCharacter(Base):
"""蓝图角色信息。"""
__tablename__ = "blueprint_characters"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
project_id: Mapped[str] = mapped_column(ForeignKey("novel_projects.id", ondelete="CASCADE"), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
identity: Mapped[Optional[str]] = mapped_column(String(255))
personality: Mapped[Optional[str]] = mapped_column(Text)
goals: Mapped[Optional[str]] = mapped_column(Text)
abilities: Mapped[Optional[str]] = mapped_column(Text)
relationship_to_protagonist: Mapped[Optional[str]] = mapped_column(Text)
extra: Mapped[Optional[dict]] = mapped_column(JSON)
position: Mapped[int] = mapped_column(Integer, default=0)
project: Mapped[NovelProject] = relationship(back_populates="characters")
class BlueprintRelationship(Base):
"""角色之间的关系。"""
__tablename__ = "blueprint_relationships"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
project_id: Mapped[str] = mapped_column(ForeignKey("novel_projects.id", ondelete="CASCADE"), nullable=False)
character_from: Mapped[str] = mapped_column(String(255), nullable=False)
character_to: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
position: Mapped[int] = mapped_column(Integer, default=0)
project: Mapped[NovelProject] = relationship(back_populates="relationships_")
class ChapterOutline(Base):
"""章节纲要。"""
__tablename__ = "chapter_outlines"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
project_id: Mapped[str] = mapped_column(ForeignKey("novel_projects.id", ondelete="CASCADE"), nullable=False)
chapter_number: Mapped[int] = mapped_column(Integer, nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
summary: Mapped[Optional[str]] = mapped_column(Text)
project: Mapped[NovelProject] = relationship(back_populates="outlines")
class Chapter(Base):
"""章节正文状态,指向选中的版本。"""
__tablename__ = "chapters"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
project_id: Mapped[str] = mapped_column(ForeignKey("novel_projects.id", ondelete="CASCADE"), nullable=False)
chapter_number: Mapped[int] = mapped_column(Integer, nullable=False)
real_summary: Mapped[Optional[str]] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), default="not_generated")
word_count: Mapped[int] = mapped_column(Integer, default=0)
selected_version_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("chapter_versions.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
project: Mapped[NovelProject] = relationship(back_populates="chapters")
versions: Mapped[list["ChapterVersion"]] = relationship(
"ChapterVersion",
back_populates="chapter",
cascade="all, delete-orphan",
order_by="ChapterVersion.created_at",
primaryjoin="Chapter.id == ChapterVersion.chapter_id",
foreign_keys="[ChapterVersion.chapter_id]",
)
selected_version: Mapped[Optional["ChapterVersion"]] = relationship(
"ChapterVersion",
foreign_keys=[selected_version_id],
primaryjoin="Chapter.selected_version_id == ChapterVersion.id",
post_update=True,
)
evaluations: Mapped[list["ChapterEvaluation"]] = relationship(
back_populates="chapter", cascade="all, delete-orphan", order_by="ChapterEvaluation.created_at"
)
class ChapterVersion(Base):
"""章节生成的不同版本文本。"""
__tablename__ = "chapter_versions"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
chapter_id: Mapped[int] = mapped_column(ForeignKey("chapters.id", ondelete="CASCADE"), nullable=False)
version_label: Mapped[Optional[str]] = mapped_column(String(64))
provider: Mapped[Optional[str]] = mapped_column(String(64))
content: Mapped[str] = mapped_column(LONG_TEXT_TYPE, nullable=False)
metadata_: Mapped[Optional[dict]] = mapped_column("metadata", JSON)
metadata = _MetadataAccessor()
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
chapter: Mapped[Chapter] = relationship(
"Chapter",
back_populates="versions",
foreign_keys=[chapter_id],
)
evaluations: Mapped[list["ChapterEvaluation"]] = relationship(
back_populates="version", cascade="all, delete-orphan"
)
class ChapterEvaluation(Base):
"""章节评估记录。"""
__tablename__ = "chapter_evaluations"
id: Mapped[int] = mapped_column(BIGINT_PK_TYPE, primary_key=True, autoincrement=True)
chapter_id: Mapped[int] = mapped_column(ForeignKey("chapters.id", ondelete="CASCADE"), nullable=False)
version_id: Mapped[Optional[int]] = mapped_column(ForeignKey("chapter_versions.id", ondelete="CASCADE"))
decision: Mapped[Optional[str]] = mapped_column(String(32))
feedback: Mapped[Optional[str]] = mapped_column(Text)
score: Mapped[Optional[float]] = mapped_column(Float)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
chapter: Mapped[Chapter] = relationship(back_populates="evaluations")
version: Mapped[Optional[ChapterVersion]] = relationship(back_populates="evaluations")