From 75caab4d39500ec92c90aa33619c3f4e547cb604 Mon Sep 17 00:00:00 2001 From: Pine Date: Mon, 24 Aug 2026 19:32:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20P2=20=E6=91=98=E9=99=A4=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=A7=8D=E5=AD=90=20+=20=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=8C=96=E8=BF=81=E7=A7=BB/=E7=A7=8D=E5=AD=90(migrate.py/seed.?= =?UTF-8?q?py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Database.initialize() 不再 create_all/seed_data:运行时仅做库连通检查(不建表不灌种子)。 - 新增 scripts/db/migrate.py(封装 alembic upgrade/downgrade)、scripts/db/seed.py(非运行态灌平台种子)。 - 新增 serverdata/seed/(README) 存放种子 JSON。 - 迁移/种子流程:uv run python scripts/db/migrate.py + scripts/db/seed.py,再启动应用。 - 已知待修:seed 内 AGENT_SEED 全局固定 id 唯一冲突(既有缺陷, 待 P2 后续修);pine/角色已能灌入。 --- app/infrastructure/repositories.py | 20 +++++--------- scripts/db/__init__.py | 2 ++ scripts/db/migrate.py | 30 +++++++++++++++++++++ scripts/db/seed.py | 43 ++++++++++++++++++++++++++++++ serverdata/seed/README.md | 4 +++ 5 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 scripts/db/__init__.py create mode 100644 scripts/db/migrate.py create mode 100644 scripts/db/seed.py create mode 100644 serverdata/seed/README.md diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 352f5c6..5d601b8 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -1857,21 +1857,13 @@ class Database: self.stats = StatsRepository(self.session) async def initialize(self) -> None: - """建表(本实例的 engine)+ 幂等种子(roles 空时才写)。""" - from .db import Base - from .seed import seed_data - + """运行时仅保活:不建表、不灌种子(迁移/种子一律走 alembic + scripts/db/seed.py)。 + 仅做一次连通性检查,确保库可达。""" if self._engine is not None: - async with self._engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - # 种子容错:个别幂等保护不严的地方(如 demo 用户固定 agent id)可能触发唯一冲突, - # 不能让种子失败拖垮 lifespan 建库,否则 app.state.db 未设置→全部 500。 - try: - await seed_data(self.session) - except Exception as e: # noqa: BLE001 - import logging - logging.getLogger("servercore").warning("种子数据写入失败(忽略,应用仍可用): %s", e) - await self.session.rollback() + from sqlalchemy import text + async with self._engine.connect() as conn: + await conn.execute(text("SELECT 1")) + # 建表/种子:由 alembic upgrade + scripts/db/seed.py 在非运行态执行,禁止运行时注入。 async def close(self) -> None: if self._owns_session: diff --git a/scripts/db/__init__.py b/scripts/db/__init__.py new file mode 100644 index 0000000..cb6e20c --- /dev/null +++ b/scripts/db/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""数据库脚本(migrate / seed)—— 非运行态执行。""" diff --git a/scripts/db/migrate.py b/scripts/db/migrate.py new file mode 100644 index 0000000..68c45c3 --- /dev/null +++ b/scripts/db/migrate.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""数据库迁移脚本(非运行态)—— Alembic 封装。 + +用法:uv run python scripts/db/migrate.py # alembic upgrade head + uv run python scripts/db/migrate.py downgrade # 回退一个版本 +在启动应用【之前】执行;禁止在应用启动时迁移/建表。 +""" +from __future__ import annotations + +import subprocess +import sys +import os +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +os.chdir(ROOT) + + +def main() -> None: + cmd = sys.argv[1] if len(sys.argv) > 1 else "upgrade" + if cmd == "downgrade": + args = ["python", "-m", "alembic", "downgrade", "-1"] + else: + args = ["python", "-m", "alembic", "upgrade", "head"] + print(f"执行迁移: {' '.join(args)}") + raise SystemExit(subprocess.call(args, cwd=str(ROOT))) + + +if __name__ == "__main__": + main() diff --git a/scripts/db/seed.py b/scripts/db/seed.py new file mode 100644 index 0000000..0217fd2 --- /dev/null +++ b/scripts/db/seed.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""数据库种子脚本(非运行态)—— 在迁移后、启动应用前执行。 + +用法:uv run python scripts/db/seed.py +职责:灌入平台/园区/培训的基础种子数据(角色、权限、用户、默认园区、培训事件课程等)。 +全部幂等(按标记行判断,重跑安全)。禁止在应用启动时调用本逻辑。 +""" +from __future__ import annotations + +import asyncio +import logging +import sys +import os +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) + +from app.infrastructure.repositories import Database +from app.infrastructure.seed import seed_data as platform_seed +from app import config + +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") +log = logging.getLogger("db.seed") + + +async def run() -> None: + db = Database() + try: + # 平台种子(角色/权限/用户/政务/OPC/投资人/工作台等,幂等) + await platform_seed(db.session) + await db.session.commit() + log.info("平台种子完成:%s", config.DATABASE_URL) + # TODO(P2/P3 后):园区默认园区+企业、培训事件课程 经 serverdata/seed/*.json 灌入(当前由各自子应用迁移后接入) + except Exception as e: # noqa: BLE001 + await db.session.rollback() + log.error("种子失败:%s", e) + raise + finally: + await db.close() + + +if __name__ == "__main__": + asyncio.run(run()) diff --git a/serverdata/seed/README.md b/serverdata/seed/README.md new file mode 100644 index 0000000..0ca3fe3 --- /dev/null +++ b/serverdata/seed/README.md @@ -0,0 +1,4 @@ +# serverdata/seed — 种子 JSON 目录 + +数据库种子数据(角色/权限/用户/园区/培训等)以 JSON 存放于此,经 `scripts/db/seed.py` 在迁移后灌入。 +禁止在应用启动时注入种子。