# -*- 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") def _seed_training() -> None: """培训种子(唯一总库 app.db):排期 + 在线课程(幂等)。 不再向旧 ``accounts`` 表种子账号 —— 平台规范账号已由 platform_seed 写入 ``users``(如 u_demo_01/pine),旧 accounts 为「多余账号仓」已被清理 (见 scripts/db/cleanup_accounts.py),此处不再重复造账号。 """ import sqlite3 from app.training import db as tdb conn = sqlite3.connect(tdb.DB_PATH) conn.row_factory = sqlite3.Row try: if conn.execute("SELECT COUNT(*) AS c FROM events").fetchone()["c"] == 0: for e in tdb.SEED_EVENTS: conn.execute("INSERT INTO events (id,type,mode,title,subtitle,desc,location,host,image,link,start_at,duration_min,capacity,status) VALUES (:id,:type,:mode,:title,:subtitle,:desc,:location,:host,:image,:link,:start_at,:duration_min,:capacity,:status)", e) if conn.execute("SELECT COUNT(*) AS c FROM courses").fetchone()["c"] == 0: for c in tdb.SEED_COURSES: conn.execute("INSERT INTO courses (id,category,level,title,subtitle,desc,price,status,start_at,end_at,venue,quota,image,host,created_at) VALUES (:id,:category,:level,:title,:subtitle,:desc,:price,:status,:start_at,:end_at,:venue,:quota,:image,:host,:created_at)", c) conn.commit() log.info("培训种子完成(账号/排期/课程)") finally: conn.close() async def _migrate_accounts_to_users(db) -> None: """把培训端 accounts 存量并入平台 users(幂等:已存在账号跳过并保证身份)。 迁移旧小程序账号 → users(wx_mini_openid/phone 关联),使微信/手机登录仍命中, 且并入全局唯一账号体系。迁移/种子由用户执行(铁律),禁止运行态调用。 """ import secrets import sqlite3 from app.training import db as tdb from app.api.routers.auth import _ensure_opc_identity conn = sqlite3.connect(tdb.DB_PATH) conn.row_factory = sqlite3.Row migrated = created = 0 try: for row in conn.execute("SELECT * FROM accounts"): username = (row["username"] or "").strip() wxid = (row["wxid"] or "").strip() phone = (row["phone"] or "").strip() 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), nickname=(row["name"] or uname), avatar=(row["avatar"] or ""), 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"]) # 迁移即接管:删掉旧 accounts 行,避免同一逻辑账号落在两套表(多余账号)。 conn.execute("DELETE FROM accounts WHERE id=?", (row["id"],)) migrated += 1 conn.commit() log.info("accounts→users 迁移完成:共 %s 条,新建 %s 条(旧 accounts 行已清理)", migrated, created) finally: conn.close() async def run() -> None: db = Database() try: await platform_seed(db.session) await db.session.commit() log.info("平台种子完成:%s", config.DATABASE_URL) _seed_training() await _migrate_accounts_to_users(db) 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())