| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- #!/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())
|