Files
loom/backend/app/scripts/seed.py
T
2026-04-24 17:46:03 +08:00

251 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""P2 初始化种子:
- 1 个根公司(Loom)+ 1 个部门(总部)
- 超级管理员角色(is_builtin=True)
- 业务员角色(仅任务/Skill 基础权限,用于演示动态菜单差异)
- admin 超管账号(密码 admin123)
- operator 演示账号(密码 operator123,绑定业务员角色)
- 完整菜单树(覆盖 19 个业务页面 + 按钮权限)
执行:
python -m app.scripts.seed
可重复运行,根据 username/code 幂等处理。
"""
from __future__ import annotations
import asyncio
from typing import Any
from sqlalchemy import delete, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import hash_password
from app.db.session import AsyncSessionLocal
from app.models import Menu, Organization, Role, User, role_menu, user_role
# ---------- 菜单结构 ----------
# 每项:(name, type, path, icon, perm_code, children)
MENU_TREE: list[dict[str, Any]] = [
{
"name": "仪表盘", "type": "menu", "path": "/dashboard",
"icon": "DashboardOutlined", "perm_code": "dashboard:view",
},
{
"name": "权限管理", "type": "dir", "path": "/auth", "icon": "SafetyOutlined",
"children": [
{"name": "组织架构", "type": "menu", "path": "/auth/org",
"perm_code": "auth:org:view"},
{"name": "角色管理", "type": "menu", "path": "/auth/role",
"perm_code": "auth:role:view"},
{"name": "用户管理", "type": "menu", "path": "/auth/user",
"perm_code": "auth:user:view"},
{"name": "菜单管理", "type": "menu", "path": "/auth/menu",
"perm_code": "auth:menu:view"},
],
},
{
"name": "产品管理", "type": "dir", "path": "/product", "icon": "AppstoreOutlined",
"children": [
{"name": "产品", "type": "menu", "path": "/product/list",
"perm_code": "product:view"},
{"name": "渠道码", "type": "menu", "path": "/product/channel",
"perm_code": "product:channel:view"},
{"name": "平台套件", "type": "menu", "path": "/product/suite",
"perm_code": "product:suite:view"},
],
},
{
"name": "平台账号", "type": "menu", "path": "/platform-account",
"icon": "KeyOutlined", "perm_code": "platform-account:view",
},
{
"name": "Skill 中心", "type": "dir", "path": "/skill", "icon": "ApiOutlined",
"children": [
{"name": "Skill 管理", "type": "menu", "path": "/skill/list",
"perm_code": "skill:view"},
{"name": "调用日志", "type": "menu", "path": "/skill/log",
"perm_code": "skill:log:view"},
],
},
{
"name": "任务中心", "type": "dir", "path": "/task", "icon": "ThunderboltOutlined",
"children": [
{"name": "创建任务", "type": "menu", "path": "/task/create",
"perm_code": "task:create"},
{"name": "任务列表", "type": "menu", "path": "/task/list",
"perm_code": "task:list"},
],
},
{
"name": "日志观测", "type": "dir", "path": "/log", "icon": "LineChartOutlined",
"children": [
{"name": "操作日志", "type": "menu", "path": "/log/op",
"perm_code": "log:op:view"},
{"name": "统计报表", "type": "menu", "path": "/log/report",
"perm_code": "log:report:view"},
],
},
{
"name": "系统设置", "type": "dir", "path": "/system", "icon": "SettingOutlined",
"children": [
{"name": "执行器配置", "type": "menu", "path": "/system/executor",
"perm_code": "system:executor:view"},
{"name": "脚本模板", "type": "menu", "path": "/system/script",
"perm_code": "system:script:view"},
{"name": "定时任务", "type": "menu", "path": "/system/cron",
"perm_code": "system:cron:view"},
],
},
]
# 业务员角色仅授予这些菜单/权限
OPERATOR_PERMS = {
"dashboard:view",
"task:create",
"task:list",
"skill:view",
}
async def upsert_org(db: AsyncSession, code: str, name: str, type_: str,
parent_id: int | None = None) -> Organization:
stmt = select(Organization).where(Organization.code == code)
org = (await db.execute(stmt)).scalar_one_or_none()
if org is None:
org = Organization(code=code, name=name, type=type_, parent_id=parent_id)
db.add(org)
await db.flush()
return org
async def upsert_menu(db: AsyncSession, node: dict[str, Any],
parent_id: int | None, sort_start: int = 0) -> list[Menu]:
path = node.get("path")
name = node["name"]
stmt = select(Menu).where(
(Menu.path == path) if path else (Menu.name == name)
)
m = (await db.execute(stmt)).scalar_one_or_none()
if m is None:
m = Menu(
name=name,
type=node["type"],
path=path,
component=node.get("component"),
icon=node.get("icon"),
perm_code=node.get("perm_code"),
parent_id=parent_id,
sort=sort_start,
)
db.add(m)
await db.flush()
else:
m.parent_id = parent_id
m.sort = sort_start
m.type = node["type"]
m.icon = node.get("icon")
m.perm_code = node.get("perm_code")
created = [m]
for i, child in enumerate(node.get("children", [])):
created.extend(await upsert_menu(db, child, m.id, sort_start=i))
return created
async def upsert_role(db: AsyncSession, code: str, name: str, *,
dept_id: int | None, is_builtin: bool) -> Role:
stmt = select(Role).where(Role.code == code)
stmt = stmt.where(Role.dept_id.is_(None) if dept_id is None else Role.dept_id == dept_id)
role = (await db.execute(stmt)).scalar_one_or_none()
if role is None:
role = Role(code=code, name=name, dept_id=dept_id, is_builtin=is_builtin)
db.add(role)
await db.flush()
return role
async def _replace_assoc(
db: AsyncSession, table, left_col: str, left_val: int, right_col: str, right_vals: list[int]
) -> None:
"""清空再插入关联表,避免 async 下 relationship 集合属性的 lazy-load 限制。"""
await db.execute(delete(table).where(table.c[left_col] == left_val))
if right_vals:
await db.execute(
insert(table),
[{left_col: left_val, right_col: rv} for rv in right_vals],
)
async def upsert_user(db: AsyncSession, username: str, password: str, *,
nickname: str, dept_id: int | None, is_superuser: bool) -> User:
stmt = select(User).where(User.username == username)
u = (await db.execute(stmt)).scalar_one_or_none()
if u is None:
u = User(
username=username,
password_hash=hash_password(password),
nickname=nickname,
dept_id=dept_id,
is_superuser=is_superuser,
)
db.add(u)
await db.flush()
return u
async def seed() -> None:
async with AsyncSessionLocal() as db:
# 组织
company = await upsert_org(db, "LOOM", "Loom 公司", "company")
hq = await upsert_org(db, "HQ", "总部", "department", parent_id=company.id)
# 菜单
all_menus: list[Menu] = []
for i, node in enumerate(MENU_TREE):
all_menus.extend(await upsert_menu(db, node, parent_id=None, sort_start=i))
await db.flush()
# 角色
super_role = await upsert_role(
db, "super_admin", "超级管理员", dept_id=None, is_builtin=True
)
operator_role = await upsert_role(
db, "operator", "业务员", dept_id=hq.id, is_builtin=False
)
# 把所有菜单授权给超管;业务员仅授权 OPERATOR_PERMS + 祖先目录
operator_menu_ids: set[int] = set()
menu_by_id = {m.id: m for m in all_menus}
for m in all_menus:
if m.perm_code in OPERATOR_PERMS:
cur: Menu | None = m
while cur and cur.id not in operator_menu_ids:
operator_menu_ids.add(cur.id)
cur = menu_by_id.get(cur.parent_id) if cur.parent_id else None
# 用直接操作关联表的方式写权限,避免 async session 对集合属性的 lazy-load 麻烦
await _replace_assoc(db, role_menu, "role_id", super_role.id, "menu_id",
[m.id for m in all_menus])
await _replace_assoc(db, role_menu, "role_id", operator_role.id, "menu_id",
list(operator_menu_ids))
# 用户
admin = await upsert_user(
db, "admin", "admin123",
nickname="超级管理员", dept_id=hq.id, is_superuser=True,
)
operator = await upsert_user(
db, "operator", "operator123",
nickname="业务员", dept_id=hq.id, is_superuser=False,
)
await _replace_assoc(db, user_role, "user_id", admin.id, "role_id", [super_role.id])
await _replace_assoc(db, user_role, "user_id", operator.id, "role_id", [operator_role.id])
await db.commit()
print("[seed] done.")
print(" 超管 admin / admin123 (全部菜单)")
print(" 业务员 operator / operator123 (仪表盘 + 任务 + Skill)")
if __name__ == "__main__":
asyncio.run(seed())