feat: 初始提交

This commit is contained in:
anonymous
2025-10-21 09:38:26 +08:00
committed by t59688
parent 2965b8e28f
commit c9fc816fab
175 changed files with 23968 additions and 87 deletions

View File

@@ -0,0 +1,41 @@
"""集中导出 ORM 模型,确保 SQLAlchemy 元数据在初始化时被正确加载。"""
from .admin_setting import AdminSetting
from .llm_config import LLMConfig
from .novel import (
BlueprintCharacter,
BlueprintRelationship,
Chapter,
ChapterEvaluation,
ChapterOutline,
ChapterVersion,
NovelBlueprint,
NovelConversation,
NovelProject,
)
from .prompt import Prompt
from .update_log import UpdateLog
from .usage_metric import UsageMetric
from .user import User
from .user_daily_request import UserDailyRequest
from .system_config import SystemConfig
__all__ = [
"AdminSetting",
"LLMConfig",
"NovelConversation",
"NovelBlueprint",
"BlueprintCharacter",
"BlueprintRelationship",
"ChapterOutline",
"Chapter",
"ChapterVersion",
"ChapterEvaluation",
"NovelProject",
"Prompt",
"UpdateLog",
"UsageMetric",
"User",
"UserDailyRequest",
"SystemConfig",
]

View File

@@ -0,0 +1,13 @@
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class AdminSetting(Base):
"""后台配置项,采用简单的 KV 结构。"""
__tablename__ = "admin_settings"
key: Mapped[str] = mapped_column(String(64), primary_key=True)
value: Mapped[str] = mapped_column(Text, nullable=False)

View File

@@ -0,0 +1,17 @@
from sqlalchemy import ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base import Base
class LLMConfig(Base):
"""用户自定义的 LLM 接入配置。"""
__tablename__ = "llm_configs"
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
llm_provider_url: Mapped[str | None] = mapped_column(Text())
llm_provider_api_key: Mapped[str | None] = mapped_column(Text())
llm_provider_model: Mapped[str | None] = mapped_column(Text())
user: Mapped["User"] = relationship("User", back_populates="llm_config")

225
backend/app/models/novel.py Normal file
View File

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

View File

@@ -0,0 +1,25 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class Prompt(Base):
"""提示词表,支持后台 CRUD 操作。"""
__tablename__ = "prompts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
title: Mapped[Optional[str]] = mapped_column(String(255))
content: Mapped[str] = mapped_column(Text, nullable=False)
tags: Mapped[Optional[str]] = mapped_column(String(255))
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,14 @@
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class SystemConfig(Base):
"""系统级配置项,例如默认 LLM API Key、模型名称等。"""
__tablename__ = "system_configs"
key: Mapped[str] = mapped_column(String(100), primary_key=True)
value: Mapped[str] = mapped_column(Text, nullable=False)
description: Mapped[str | None] = mapped_column(String(255))

View File

@@ -0,0 +1,18 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class UpdateLog(Base):
"""更新日志表,供公告与后台管理使用。"""
__tablename__ = "update_logs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
content: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_by: Mapped[str | None] = mapped_column(String(64))
is_pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class UsageMetric(Base):
"""通用计数器表,目前用于记录 API 请求次数等统计数据。"""
__tablename__ = "usage_metrics"
key: Mapped[str] = mapped_column(String(64), primary_key=True)
value: Mapped[int] = mapped_column(Integer, nullable=False, default=0)

View File

@@ -0,0 +1,31 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base import Base
class User(Base):
"""用户主表,记录账号及权限信息。"""
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
email: Mapped[Optional[str]] = mapped_column(String(128), unique=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
external_id: Mapped[Optional[str]] = mapped_column(String(255), unique=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=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(),
)
# 关系映射
novel_projects: Mapped[list["NovelProject"]] = relationship("NovelProject", back_populates="owner")
llm_config: Mapped[Optional["LLMConfig"]] = relationship("LLMConfig", back_populates="user", uselist=False)

View File

@@ -0,0 +1,18 @@
from datetime import date
from sqlalchemy import Date, ForeignKey, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base import Base
class UserDailyRequest(Base):
"""记录每位用户每日使用次数的限流表。"""
__tablename__ = "user_daily_requests"
__table_args__ = (UniqueConstraint("user_id", "request_date", name="uq_user_daily"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
request_date: Mapped[date] = mapped_column(Date, nullable=False)
request_count: Mapped[int] = mapped_column(Integer, default=0)