Files
life-echo/api/app/features/memoir/models.py
2026-03-20 15:15:35 +08:00

221 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from sqlalchemy import (
JSON,
Boolean,
Column,
DateTime,
ForeignKey,
Integer,
String,
Text,
)
from sqlalchemy.orm import relationship
from app.core.db import Base, utc_now
class Chapter(Base):
"""章节阅读与导出视图canonical_markdown 为正文真源。"""
__tablename__ = "chapters"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
book_id = Column(String, ForeignKey("books.id", ondelete="SET NULL"), nullable=True)
title = Column(String, nullable=False)
category = Column(String, nullable=True)
order_index = Column(Integer, nullable=False)
summary = Column(Text, nullable=True)
canonical_markdown = Column(Text, nullable=True) # 当前生效正文markdown-first
status = Column(String, default="draft") # active / draft / archived
cover_image = Column(JSON, nullable=True) # 兼容旧数据,逐步迁移到 cover_asset_id
cover_asset_id = Column(String, nullable=True)
current_version_id = Column(String, nullable=True) # FK 在 migration 中分步添加
created_at = Column(DateTime(timezone=True), default=utc_now)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
# 兼容旧运行时,迁移后废弃
is_new = Column(Boolean, default=True)
is_active = Column(Boolean, default=True)
source_segments = Column(JSON, nullable=True)
# story-backed 章节story 变更后标 True由 Celery 重组 canonical_markdown
markdown_compose_dirty = Column(Boolean, default=False, nullable=False)
markdown_composed_at = Column(DateTime(timezone=True), nullable=True)
user = relationship("User", back_populates="chapters")
book = relationship("Book", back_populates="chapters")
images = relationship(
"MemoirImage",
back_populates="chapter",
foreign_keys="MemoirImage.chapter_id",
cascade="all, delete-orphan",
)
versions = relationship(
"ChapterVersion",
back_populates="chapter",
foreign_keys="ChapterVersion.chapter_id",
cascade="all, delete-orphan",
)
current_version = relationship(
"ChapterVersion",
primaryjoin="Chapter.current_version_id == ChapterVersion.id",
foreign_keys="ChapterVersion.id",
)
story_links = relationship(
"ChapterStoryLink",
back_populates="chapter",
cascade="all, delete-orphan",
)
cover_intents = relationship(
"ChapterCoverIntent",
back_populates="chapter",
cascade="all, delete-orphan",
)
class MemoirImage(Base):
__tablename__ = "memoir_images"
id = Column(String, primary_key=True)
chapter_id = Column(
String, ForeignKey("chapters.id", ondelete="CASCADE"), nullable=False
)
order_index = Column(Integer, nullable=False, default=0)
placeholder = Column(Text, nullable=True)
description = Column(Text, nullable=True)
status = Column(String, nullable=False, default="pending")
prompt = Column(Text, nullable=True)
url = Column(Text, nullable=True)
storage_key = Column(Text, nullable=True)
provider = Column(String, nullable=True)
style = Column(String, nullable=True)
size = Column(String, nullable=True)
error = Column(Text, nullable=True)
retryable = Column(Boolean, nullable=True)
created_at = Column(DateTime(timezone=True), nullable=True)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
chapter = relationship("Chapter", back_populates="images")
class ChapterVersion(Base):
"""Chapter 版本快照,记录正文变更与来源。"""
__tablename__ = "chapter_versions"
id = Column(String, primary_key=True)
chapter_id = Column(
String,
ForeignKey("chapters.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
version_no = Column(Integer, nullable=False)
markdown_snapshot = Column(Text, nullable=False)
change_summary = Column(Text, nullable=True)
actor_type = Column(String, nullable=True) # ai / user / editor / system
source_type = Column(
String, nullable=True
) # generate / rewrite / merge / manual / migration
parent_version_id = Column(
String, ForeignKey("chapter_versions.id", ondelete="SET NULL"), nullable=True
)
prompt_meta = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=utc_now)
chapter = relationship(
"Chapter", back_populates="versions", foreign_keys=[chapter_id]
)
parent_version = relationship(
"ChapterVersion",
remote_side="ChapterVersion.id",
foreign_keys=[parent_version_id],
)
class ChapterCoverIntent(Base):
"""Chapter 封面意图(结构化)。"""
__tablename__ = "chapter_cover_intents"
id = Column(String, primary_key=True)
chapter_id = Column(
String,
ForeignKey("chapters.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
chapter_version_id = Column(
String,
ForeignKey("chapter_versions.id", ondelete="SET NULL"),
nullable=True,
)
story_ids = Column(JSON, nullable=True)
prompt_brief = Column(Text, nullable=True)
status = Column(String, nullable=False)
claim_token = Column(String, nullable=True)
claimed_at = Column(DateTime(timezone=True), nullable=True)
attempt_count = Column(Integer, nullable=False, default=0)
asset_id = Column(String, nullable=True)
error = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=utc_now)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
chapter = relationship("Chapter", back_populates="cover_intents")
class ChapterStoryLink(Base):
"""Chapter 与 Story 的编排关联。"""
__tablename__ = "chapter_story_links"
id = Column(String, primary_key=True)
chapter_id = Column(
String,
ForeignKey("chapters.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
story_id = Column(
String, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False, index=True
)
order_index = Column(Integer, nullable=False)
role = Column(String, nullable=True) # core / bridge / appendix
created_at = Column(DateTime(timezone=True), default=utc_now)
chapter = relationship("Chapter", back_populates="story_links")
story = relationship("Story", back_populates="chapter_links")
class Book(Base):
__tablename__ = "books"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
title = Column(String, nullable=False)
total_pages = Column(Integer, default=0)
total_words = Column(Integer, default=0)
cover_image_url = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
has_update = Column(Boolean, default=False)
last_update_chapter_id = Column(String, nullable=True)
user = relationship("User", back_populates="books")
chapters = relationship(
"Chapter",
back_populates="book",
foreign_keys="Chapter.book_id",
)
class MemoirState(Base):
__tablename__ = "memoir_states"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), unique=True, nullable=False)
stage_order = Column(JSON, default=list)
current_stage = Column(String, default="childhood")
covered_stages = Column(JSON, default=list)
slots = Column(JSON, nullable=False)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
user = relationship("User", back_populates="memoir_state")