diff --git a/.gitignore b/.gitignore index ca6db37..e99668c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ venv/ htmlcov/ .jellyfish.db jellyfish.db +.env diff --git a/backend/.env.example b/backend/.env.example index 2820a62..44f0d93 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -12,6 +12,8 @@ API_V1_PREFIX=/api/v1 # DATABASE_URL=sqlite+aiosqlite:///./jellyfish.db # PostgreSQL 示例: # DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/jellyfish +# MySQL 示例(使用 aiomysql 驱动): +# DATABASE_URL=mysql+aiomysql://user:pass@localhost:3306/jellyfish # CORS 前端地址(逗号分隔) # CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/README.md b/backend/README.md index 0e1b19c..822d90c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -58,7 +58,36 @@ cp .env.example .env # 按需编辑 .env ``` -### 4. 启动服务 +### 4. 初始化数据库(可选,MySQL/PostgreSQL 等) + +如果你使用的是 **SQLite 默认配置**(`DATABASE_URL=sqlite+aiosqlite:///./jellyfish.db`),可以跳过本节,首次访问时会自动创建文件。 + +若切换到 **MySQL / PostgreSQL 等外部数据库**,建议先手动初始化表结构: + +1. 在 `.env` 中配置数据库连接(示例): + + ```env + # SQLite(默认) + # DATABASE_URL=sqlite+aiosqlite:///./jellyfish.db + + # MySQL(使用 aiomysql 驱动) + # DATABASE_URL=mysql+aiomysql://user:pass@localhost:3306/jellyfish + + # PostgreSQL + # DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/jellyfish + ``` + +2. 运行初始化脚本(使用 uv): + + ```bash + cd backend + uv sync # 确保依赖已安装 + uv run python init_db.py + ``` + +该脚本会导入所有 ORM 模型并调用 `Base.metadata.create_all()`,在目标数据库中创建所需的 27 张业务表。 + +### 5. 启动服务 ```bash uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 diff --git a/backend/app/core/db.py b/backend/app/core/db.py index 783ceb1..d5614f1 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -33,6 +33,10 @@ class Base(DeclarativeBase): async def init_db() -> None: """创建所有表(开发/迁移用)。""" + # 确保 ORM 模型已导入,从而注册到 Base.metadata + import app.models.llm # noqa: F401 + import app.models.studio # noqa: F401 + async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index db332fa..f5e493e 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -3,7 +3,7 @@ from app.core.db import Base from app.models.base import TimestampMixin -from app.models.llm import Agent, Model, ModelSettings, Provider +from app.models.llm import Model, ModelSettings, Provider from app.models.studio import ( Actor, ActorImage, @@ -58,7 +58,6 @@ __all__ = [ "PromptTemplate", "FileItem", "TimelineClip", - "Agent", "Provider", "Model", "ModelSettings", diff --git a/backend/app/models/studio.py b/backend/app/models/studio.py index e022ece..0666839 100644 --- a/backend/app/models/studio.py +++ b/backend/app/models/studio.py @@ -499,7 +499,7 @@ class Costume(Base, TimestampMixin): ) -class Shot(Base): +class Shot(Base,TimestampMixin): """镜头表(基础信息)。 说明: @@ -572,7 +572,7 @@ class Shot(Base): ) -class ShotDetail(Base): +class ShotDetail(Base,TimestampMixin): """镜头细节(1:1)。 设计点: @@ -642,7 +642,7 @@ class ShotDetail(Base): ) -class ShotDialogLine(Base): +class ShotDialogLine(Base,TimestampMixin): """镜头对话行。 设计点: @@ -789,7 +789,7 @@ class Character(Base, TimestampMixin): ) -class CharacterPropLink(Base): +class CharacterPropLink(Base,TimestampMixin): """角色与道具绑定(多对多)。 应用层保证: @@ -825,7 +825,7 @@ class CharacterPropLink(Base): ) -class ShotCharacterLink(Base): +class ShotCharacterLink(Base,TimestampMixin): """镜头引用角色(多对多)。 应用层保证: @@ -1021,7 +1021,7 @@ class CostumeImage(Base, TimestampMixin): ) -class ShotActorImageLink(Base): +class ShotActorImageLink(Base,TimestampMixin): """镜头引用演员形象/立绘(多对多)。 应用层保证: @@ -1045,7 +1045,7 @@ class ShotActorImageLink(Base): ) -class ShotSceneLink(Base): +class ShotSceneLink(Base,TimestampMixin): """镜头引用场景(多对多)。 应用层保证: @@ -1069,7 +1069,7 @@ class ShotSceneLink(Base): ) -class ShotPropLink(Base): +class ShotPropLink(Base,TimestampMixin): """镜头引用道具(多对多)。 应用层保证: @@ -1093,7 +1093,7 @@ class ShotPropLink(Base): ) -class ShotCostumeLink(Base): +class ShotCostumeLink(Base,TimestampMixin): """镜头引用服装(多对多)。 应用层保证: @@ -1131,7 +1131,7 @@ class PromptCategory(str, Enum): combined = "combined" -class PromptTemplate(Base): +class PromptTemplate(Base,TimestampMixin): """提示词模板表。""" __tablename__ = "prompt_templates" diff --git a/backend/init_db.py b/backend/init_db.py new file mode 100644 index 0000000..2d36988 --- /dev/null +++ b/backend/init_db.py @@ -0,0 +1,14 @@ +import asyncio + +from app.core.db import init_db, close_db + + +async def _main() -> None: + """初始化数据库,创建所有表。""" + await init_db() + await close_db() + + +if __name__ == "__main__": + asyncio.run(_main()) + diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 45bf945..b242b64 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "langgraph>=0.2.0", "sqlalchemy[asyncio]>=2.0.0", "aiosqlite>=0.20.0", + "aiomysql>=0.3.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "python-dotenv>=1.0.0", diff --git a/backend/uv.lock b/backend/uv.lock index f4103b0..9a0eedc 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2,6 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + [[package]] name = "aiosqlite" version = "0.22.1" @@ -318,6 +330,7 @@ name = "jellyfish-backend" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "aiomysql" }, { name = "aiosqlite" }, { name = "fastapi" }, { name = "langchain" }, @@ -348,6 +361,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aiomysql", specifier = ">=0.3.0" }, { name = "aiosqlite", specifier = ">=0.20.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, @@ -890,6 +904,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] + [[package]] name = "pytest" version = "9.0.2"