2026-08-24 19:32:41 +08:00
|
|
|
|
# -*- 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")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 20:31:57 +08:00
|
|
|
|
def _seed_training() -> None:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
"""培训种子(统一总库):排期 + 在线课程(幂等)。
|
2026-08-25 22:47:27 +08:00
|
|
|
|
|
2026-08-31 22:20:16 +08:00
|
|
|
|
经 training 数据层(SQLAlchemy,方言由 DATABASE_URL 决定)读写,
|
|
|
|
|
|
不再直连 sqlite3。不向旧 ``accounts`` 表种子账号 —— 平台规范账号由
|
|
|
|
|
|
platform_seed 写入 ``users``。
|
2026-08-25 22:47:27 +08:00
|
|
|
|
"""
|
2026-08-24 20:31:57 +08:00
|
|
|
|
from app.training import db as tdb
|
|
|
|
|
|
try:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
if not tdb.list_all("events"):
|
2026-08-24 20:31:57 +08:00
|
|
|
|
for e in tdb.SEED_EVENTS:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
tdb.insert("events", e)
|
|
|
|
|
|
if not tdb.list_all("courses"):
|
2026-08-24 20:31:57 +08:00
|
|
|
|
for c in tdb.SEED_COURSES:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
tdb.insert("courses", c)
|
|
|
|
|
|
log.info("培训种子完成(排期/课程)")
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
log.warning("培训种子跳过(表可能未迁移):%s", e)
|
2026-08-24 20:31:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:59:34 +08:00
|
|
|
|
async def _migrate_accounts_to_users(db) -> None:
|
|
|
|
|
|
"""把培训端 accounts 存量并入平台 users(幂等:已存在账号跳过并保证身份)。
|
|
|
|
|
|
|
|
|
|
|
|
迁移旧小程序账号 → users(wx_mini_openid/phone 关联),使微信/手机登录仍命中,
|
|
|
|
|
|
且并入全局唯一账号体系。迁移/种子由用户执行(铁律),禁止运行态调用。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import secrets
|
|
|
|
|
|
from app.training import db as tdb
|
|
|
|
|
|
from app.api.routers.auth import _ensure_opc_identity
|
|
|
|
|
|
|
|
|
|
|
|
migrated = created = 0
|
|
|
|
|
|
try:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
rows = tdb.list_all("accounts")
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
log.warning("accounts 读取失败,跳过迁移:%s", e)
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
username = (row.get("username") or "").strip()
|
|
|
|
|
|
wxid = (row.get("wxid") or "").strip()
|
|
|
|
|
|
phone = (row.get("phone") or "").strip()
|
2026-08-25 19:59:34 +08:00
|
|
|
|
if not username and not wxid and not phone:
|
|
|
|
|
|
continue
|
|
|
|
|
|
user = (await db.users.get_by_username(username)) if username else None
|
|
|
|
|
|
if user is None and phone:
|
|
|
|
|
|
user = await db.users.get_by_username(phone)
|
|
|
|
|
|
if user is None and wxid:
|
|
|
|
|
|
user = (await db.users.get_by_wx_mini_openid(wxid)
|
|
|
|
|
|
or await db.users.get_by_wx_openid(wxid))
|
|
|
|
|
|
if user is None:
|
|
|
|
|
|
uname = (username or phone or f"wx_{wxid[:24]}")
|
|
|
|
|
|
user = await db.users.create(
|
|
|
|
|
|
username=uname, password=secrets.token_hex(16),
|
2026-08-31 22:20:16 +08:00
|
|
|
|
nickname=(row.get("name") or uname), avatar=(row.get("avatar") or ""),
|
2026-08-25 19:59:34 +08:00
|
|
|
|
phone=phone, wx_mini_openid=wxid or "",
|
|
|
|
|
|
wx_openid=(f"wx_{wxid[:24]}" if wxid else ""),
|
|
|
|
|
|
source="mini_program",
|
|
|
|
|
|
auth_type=("wx_openid" if wxid else ("phone" if phone else "unknown")),
|
|
|
|
|
|
role="opc_member",
|
|
|
|
|
|
)
|
|
|
|
|
|
created += 1
|
|
|
|
|
|
await _ensure_opc_identity(db, user["id"])
|
2026-08-25 22:47:27 +08:00
|
|
|
|
# 迁移即接管:删掉旧 accounts 行,避免同一逻辑账号落在两套表(多余账号)。
|
2026-08-31 22:20:16 +08:00
|
|
|
|
tdb.delete_row("accounts", row["id"])
|
2026-08-25 19:59:34 +08:00
|
|
|
|
migrated += 1
|
2026-08-25 22:47:27 +08:00
|
|
|
|
log.info("accounts→users 迁移完成:共 %s 条,新建 %s 条(旧 accounts 行已清理)", migrated, created)
|
2026-08-25 19:59:34 +08:00
|
|
|
|
finally:
|
2026-08-31 22:20:16 +08:00
|
|
|
|
tdb.reset_engine()
|
2026-08-25 19:59:34 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 19:32:41 +08:00
|
|
|
|
async def run() -> None:
|
|
|
|
|
|
db = Database()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await platform_seed(db.session)
|
|
|
|
|
|
await db.session.commit()
|
|
|
|
|
|
log.info("平台种子完成:%s", config.DATABASE_URL)
|
2026-08-24 20:31:57 +08:00
|
|
|
|
_seed_training()
|
2026-08-25 19:59:34 +08:00
|
|
|
|
await _migrate_accounts_to_users(db)
|
2026-08-24 19:32:41 +08:00
|
|
|
|
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())
|