251 lines
9.4 KiB
Python
251 lines
9.4 KiB
Python
"""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())
|