Files

117 lines
5.0 KiB
Python
Raw Permalink Normal View History

# -*- 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(幂等:已存在账号跳过并保证身份)。
迁移旧小程序账号 → userswx_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())