Files
shanxia2.0/backend/scripts/setup_business_roles.py
T
2026-07-13 20:26:43 +08:00

413 lines
14 KiB
Python

#!/usr/bin/env python3
"""重置业务角色:保留超级管理员,创建 5 个销售体系角色并授权菜单、绑定用户。
用法:
cd backend && ./venv/bin/python scripts/setup_business_roles.py --env-file .env.production
./venv/bin/python scripts/setup_business_roles.py --dry-run --env-file .env.production
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Set
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
from app.core.utils import generate_uuid
TENANT_NO = "shanxia2.0"
ADMIN_USER_ID = "d3000000000000000000000000000001"
SUPER_ADMIN_ROLE_ID = "097bd2c6-4e72-11f1-9f47-00163e3bdd96"
# 四个一级业务模块根菜单
MODULE_MENU_ROOTS = [
"CRM00000000000000000000000000001", # CRM管理
"BX00000000000000000000000000001", # 报销管理
"DB00000000000000000000000000001", # 地磅管理
"PL_MENU_ROOT", # 运输管理
]
NEW_ROLES: Dict[str, str] = {
"管理员": "R65CAAE057914CCCAE2A81BE627A5001",
"销售主管": "R65CAAE057914CCCAE2A81BE627A5002",
"销售计划员": "R65CAAE057914CCCAE2A81BE627A5003",
"销售实录员": "R65CAAE057914CCCAE2A81BE627A5004",
"销售办证员": "R65CAAE057914CCCAE2A81BE627A5005",
}
USER_ROLE_ASSIGNMENTS: Dict[str, List[str]] = {
"管理员": ["吴良萍", "吴九华", "汪璇"],
"销售主管": ["方金强", "吴九华"],
"销售计划员": ["程欢欢"],
"销售实录员": ["郑丽娜"],
"销售办证员": ["刘丹"],
}
def load_runtime_settings():
from app.core.config import Settings
return Settings()
def load_settings_env(env_file: str) -> None:
if env_file:
os.environ["ENV_FILE"] = env_file
from dotenv import load_dotenv
path = Path(env_file) if env_file else BACKEND_ROOT / ".env.production"
if not path.is_absolute():
path = BACKEND_ROOT / path
if path.is_file():
load_dotenv(path, override=True)
def collect_subtree_menu_ids(conn, root_ids: List[str], tenant_no: str) -> Set[str]:
"""BFS 收集模块根下全部菜单(含中间目录)。"""
all_ids: Set[str] = set(root_ids)
frontier = list(root_ids)
while frontier:
placeholders = ", ".join(f":p{i}" for i in range(len(frontier)))
params = {f"p{i}": frontier[i] for i in range(len(frontier))}
params["tenant"] = tenant_no
rows = conn.execute(
text(
f"""
SELECT id FROM sys_menu
WHERE del_flag = 0 AND tenant_no = :tenant
AND parent_id IN ({placeholders})
"""
),
params,
).fetchall()
new_ids = [str(r[0]) for r in rows if str(r[0]) not in all_ids]
if not new_ids:
break
all_ids.update(new_ids)
frontier = new_ids
return all_ids
def perm_menu_ids(conn, menu_ids: Set[str], tenant_no: str) -> List[str]:
"""持久化所有带 perms 的菜单(C 页面 :list + F 按钮),与前端 menuCode 过滤对齐。"""
if not menu_ids:
return []
placeholders = ", ".join(f":m{i}" for i in range(len(menu_ids)))
params = {f"m{i}": mid for i, mid in enumerate(sorted(menu_ids))}
params["tenant"] = tenant_no
rows = conn.execute(
text(
f"""
SELECT id FROM sys_menu
WHERE del_flag = 0 AND tenant_no = :tenant AND id IN ({placeholders})
AND perms IS NOT NULL AND TRIM(perms) <> ''
ORDER BY id
"""
),
params,
).fetchall()
return [str(r[0]) for r in rows if r and r[0]]
def leaf_menu_ids(conn, menu_ids: Set[str], tenant_no: str) -> List[str]:
"""与角色管理页一致:仅持久化叶子菜单(无子节点的菜单)。"""
if not menu_ids:
return []
placeholders = ", ".join(f":m{i}" for i in range(len(menu_ids)))
params = {f"m{i}": mid for i, mid in enumerate(sorted(menu_ids))}
params["tenant"] = tenant_no
rows = conn.execute(
text(
f"""
SELECT id, parent_id FROM sys_menu
WHERE del_flag = 0 AND tenant_no = :tenant AND id IN ({placeholders})
"""
),
params,
).mappings().all()
id_set = {str(r["id"]) for r in rows}
parents_with_children: Set[str] = set()
for r in rows:
pid = str(r.get("parent_id") or "")
if pid in id_set:
parents_with_children.add(pid)
leaves = sorted(mid for mid in id_set if mid not in parents_with_children)
return leaves
def fetch_user_id_map(conn, tenant_no: str) -> Dict[str, str]:
rows = conn.execute(
text(
"""
SELECT id, user_no, dept_id FROM sys_user
WHERE tenant_no = :tenant AND del_flag = 0
"""
),
{"tenant": tenant_no},
).mappings().all()
return {str(r["user_no"]): {"id": str(r["id"]), "dept_id": str(r.get("dept_id") or "")} for r in rows}
def purge_non_super_roles(conn, tenant_no: str, dry_run: bool) -> int:
cnt = conn.execute(
text(
"""
SELECT COUNT(*) FROM sys_role
WHERE tenant_no = :tenant AND del_flag = 0 AND id <> :super_id
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID},
).scalar()
if dry_run:
return int(cnt or 0)
now = datetime.now()
conn.execute(
text(
"""
UPDATE sys_role_menu
SET del_flag = 1, update_time = :now
WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
)
conn.execute(
text(
"""
DELETE FROM sys_role_menu
WHERE tenant_no = :tenant AND role_id <> :super_id
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID},
)
conn.execute(
text(
"""
UPDATE sys_role_data_scope_detail
SET del_flag = 1, update_time = :now
WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
)
conn.execute(
text(
"""
UPDATE sys_user_role
SET del_flag = 1, update_time = :now
WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
)
conn.execute(
text(
"""
UPDATE sys_role
SET del_flag = 1, update_time = :now, update_by = :admin
WHERE tenant_no = :tenant AND del_flag = 0 AND id <> :super_id
"""
),
{"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now, "admin": ADMIN_USER_ID},
)
return int(cnt or 0)
def ensure_new_roles(conn, tenant_no: str, dry_run: bool) -> None:
order = 10
for role_name, role_id in NEW_ROLES.items():
exists = conn.execute(
text(
"""
SELECT id FROM sys_role
WHERE id = :id AND tenant_no = :tenant AND del_flag = 0
LIMIT 1
"""
),
{"id": role_id, "tenant": tenant_no},
).first()
if exists:
order += 10
continue
if not dry_run:
conn.execute(
text(
"""
INSERT INTO sys_role (
id, role_name, data_scope, status, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
) VALUES (
:id, :name, '1', '1', :tenant, :order_num,
'用户迁移后业务角色初始化', :admin, '', :now, :admin, :now, 0
)
"""
),
{
"id": role_id,
"name": role_name,
"tenant": tenant_no,
"order_num": order,
"admin": ADMIN_USER_ID,
"now": datetime.now(),
},
)
order += 10
def grant_module_menus(conn, role_id: str, leaf_ids: List[str], tenant_no: str, dry_run: bool) -> int:
if dry_run:
return len(leaf_ids)
conn.execute(
text("DELETE FROM sys_role_menu WHERE role_id = :rid"),
{"rid": role_id},
)
now = datetime.now()
for mid in leaf_ids:
conn.execute(
text(
"""
INSERT INTO sys_role_menu (
id, role_id, menu_id, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
) VALUES (
:id, :role_id, :menu_id, :tenant, 0, 'setup_business_roles',
:admin, '', :now, :admin, :now, 0
)
"""
),
{
"id": generate_uuid(),
"role_id": role_id,
"menu_id": mid,
"tenant": tenant_no,
"admin": ADMIN_USER_ID,
"now": now,
},
)
return len(leaf_ids)
def assign_user_roles(
conn,
user_map: Dict[str, dict],
tenant_no: str,
dry_run: bool,
) -> List[dict]:
rows_out: List[dict] = []
for role_name, user_nos in USER_ROLE_ASSIGNMENTS.items():
role_id = NEW_ROLES[role_name]
for user_no in user_nos:
info = user_map.get(user_no)
if not info:
raise ValueError(f"用户不存在: {user_no}")
exists = conn.execute(
text(
"""
SELECT id FROM sys_user_role
WHERE user_id = :uid AND role_id = :rid AND tenant_no = :tenant AND del_flag = 0
LIMIT 1
"""
),
{"uid": info["id"], "rid": role_id, "tenant": tenant_no},
).first()
if exists:
rows_out.append({"user_no": user_no, "role_name": role_name, "status": "exists"})
continue
if not dry_run:
conn.execute(
text(
"""
INSERT INTO sys_user_role (
id, user_id, role_id, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
) VALUES (
:id, :uid, :rid, :tenant, 0, 'setup_business_roles',
:admin, :dept_id, :now, :admin, :now, 0
)
"""
),
{
"id": generate_uuid(),
"uid": info["id"],
"rid": role_id,
"tenant": tenant_no,
"admin": ADMIN_USER_ID,
"dept_id": info["dept_id"],
"now": datetime.now(),
},
)
rows_out.append({"user_no": user_no, "role_name": role_name, "status": "inserted"})
return rows_out
def main() -> int:
parser = argparse.ArgumentParser(description="初始化业务角色与菜单权限")
parser.add_argument("--env-file", default=".env.production")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--report-out",
default=str(BACKEND_ROOT.parent / "docs/data-migration/mappings/role_setup_result.json"),
)
args = parser.parse_args()
load_settings_env(args.env_file)
settings = load_runtime_settings()
engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)
print("=== setup_business_roles ===")
print(f"模式: {'DRY-RUN' if args.dry_run else 'EXECUTE'}")
print(f"租户: {TENANT_NO}")
print()
with engine.begin() as conn:
removed = purge_non_super_roles(conn, TENANT_NO, args.dry_run)
ensure_new_roles(conn, TENANT_NO, args.dry_run)
subtree = collect_subtree_menu_ids(conn, MODULE_MENU_ROOTS, TENANT_NO)
grant_ids = perm_menu_ids(conn, subtree, TENANT_NO)
menu_grants = {}
for role_name, role_id in NEW_ROLES.items():
menu_grants[role_name] = grant_module_menus(conn, role_id, grant_ids, TENANT_NO, args.dry_run)
user_map = fetch_user_id_map(conn, TENANT_NO)
assignments = assign_user_roles(conn, user_map, TENANT_NO, args.dry_run)
report = {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"removed_old_roles": removed,
"module_menu_roots": MODULE_MENU_ROOTS,
"subtree_menu_count": len(subtree),
"perm_menu_count": len(grant_ids),
"menu_grants_per_role": menu_grants,
"user_role_assignments": assignments,
"new_roles": NEW_ROLES,
}
if not args.dry_run:
Path(args.report_out).write_text(
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"清理旧角色(非超级管理员): {removed}")
print(f"模块子树菜单数: {len(subtree)},带 perms 菜单数: {len(grant_ids)}")
for role_name, cnt in menu_grants.items():
print(f" {role_name}: {cnt} 个菜单权限")
print("用户-角色绑定:")
for item in assignments:
print(f" {item['user_no']} -> {item['role_name']} ({item['status']})")
if not args.dry_run:
print(f"报告: {args.report_out}")
print("完成.")
return 0
if __name__ == "__main__":
raise SystemExit(main())