feat: 初始提交
This commit is contained in:
41
backend/app/models/__init__.py
Normal file
41
backend/app/models/__init__.py
Normal 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",
|
||||
]
|
||||
13
backend/app/models/admin_setting.py
Normal file
13
backend/app/models/admin_setting.py
Normal 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)
|
||||
17
backend/app/models/llm_config.py
Normal file
17
backend/app/models/llm_config.py
Normal 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
225
backend/app/models/novel.py
Normal 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")
|
||||
25
backend/app/models/prompt.py
Normal file
25
backend/app/models/prompt.py
Normal 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())
|
||||
14
backend/app/models/system_config.py
Normal file
14
backend/app/models/system_config.py
Normal 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))
|
||||
18
backend/app/models/update_log.py
Normal file
18
backend/app/models/update_log.py
Normal 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)
|
||||
13
backend/app/models/usage_metric.py
Normal file
13
backend/app/models/usage_metric.py
Normal 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)
|
||||
31
backend/app/models/user.py
Normal file
31
backend/app/models/user.py
Normal 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)
|
||||
18
backend/app/models/user_daily_request.py
Normal file
18
backend/app/models/user_daily_request.py
Normal 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)
|
||||
Reference in New Issue
Block a user