setup_business_roles.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env python3
  2. """重置业务角色:保留超级管理员,创建 5 个销售体系角色并授权菜单、绑定用户。
  3. 用法:
  4. cd backend && ./venv/bin/python scripts/setup_business_roles.py --env-file .env.production
  5. ./venv/bin/python scripts/setup_business_roles.py --dry-run --env-file .env.production
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import json
  10. import os
  11. import sys
  12. from datetime import datetime
  13. from pathlib import Path
  14. from typing import Dict, List, Set
  15. from sqlalchemy import create_engine, text
  16. from sqlalchemy.engine import Engine
  17. BACKEND_ROOT = Path(__file__).resolve().parents[1]
  18. if str(BACKEND_ROOT) not in sys.path:
  19. sys.path.insert(0, str(BACKEND_ROOT))
  20. from app.core.utils import generate_uuid
  21. TENANT_NO = "shanxia2.0"
  22. ADMIN_USER_ID = "d3000000000000000000000000000001"
  23. SUPER_ADMIN_ROLE_ID = "097bd2c6-4e72-11f1-9f47-00163e3bdd96"
  24. # 四个一级业务模块根菜单
  25. MODULE_MENU_ROOTS = [
  26. "CRM00000000000000000000000000001", # CRM管理
  27. "BX00000000000000000000000000001", # 报销管理
  28. "DB00000000000000000000000000001", # 地磅管理
  29. "PL_MENU_ROOT", # 运输管理
  30. ]
  31. NEW_ROLES: Dict[str, str] = {
  32. "管理员": "R65CAAE057914CCCAE2A81BE627A5001",
  33. "销售主管": "R65CAAE057914CCCAE2A81BE627A5002",
  34. "销售计划员": "R65CAAE057914CCCAE2A81BE627A5003",
  35. "销售实录员": "R65CAAE057914CCCAE2A81BE627A5004",
  36. "销售办证员": "R65CAAE057914CCCAE2A81BE627A5005",
  37. }
  38. USER_ROLE_ASSIGNMENTS: Dict[str, List[str]] = {
  39. "管理员": ["吴良萍", "吴九华", "汪璇"],
  40. "销售主管": ["方金强", "吴九华"],
  41. "销售计划员": ["程欢欢"],
  42. "销售实录员": ["郑丽娜"],
  43. "销售办证员": ["刘丹"],
  44. }
  45. def load_runtime_settings():
  46. from app.core.config import Settings
  47. return Settings()
  48. def load_settings_env(env_file: str) -> None:
  49. if env_file:
  50. os.environ["ENV_FILE"] = env_file
  51. from dotenv import load_dotenv
  52. path = Path(env_file) if env_file else BACKEND_ROOT / ".env.production"
  53. if not path.is_absolute():
  54. path = BACKEND_ROOT / path
  55. if path.is_file():
  56. load_dotenv(path, override=True)
  57. def collect_subtree_menu_ids(conn, root_ids: List[str], tenant_no: str) -> Set[str]:
  58. """BFS 收集模块根下全部菜单(含中间目录)。"""
  59. all_ids: Set[str] = set(root_ids)
  60. frontier = list(root_ids)
  61. while frontier:
  62. placeholders = ", ".join(f":p{i}" for i in range(len(frontier)))
  63. params = {f"p{i}": frontier[i] for i in range(len(frontier))}
  64. params["tenant"] = tenant_no
  65. rows = conn.execute(
  66. text(
  67. f"""
  68. SELECT id FROM sys_menu
  69. WHERE del_flag = 0 AND tenant_no = :tenant
  70. AND parent_id IN ({placeholders})
  71. """
  72. ),
  73. params,
  74. ).fetchall()
  75. new_ids = [str(r[0]) for r in rows if str(r[0]) not in all_ids]
  76. if not new_ids:
  77. break
  78. all_ids.update(new_ids)
  79. frontier = new_ids
  80. return all_ids
  81. def perm_menu_ids(conn, menu_ids: Set[str], tenant_no: str) -> List[str]:
  82. """持久化所有带 perms 的菜单(C 页面 :list + F 按钮),与前端 menuCode 过滤对齐。"""
  83. if not menu_ids:
  84. return []
  85. placeholders = ", ".join(f":m{i}" for i in range(len(menu_ids)))
  86. params = {f"m{i}": mid for i, mid in enumerate(sorted(menu_ids))}
  87. params["tenant"] = tenant_no
  88. rows = conn.execute(
  89. text(
  90. f"""
  91. SELECT id FROM sys_menu
  92. WHERE del_flag = 0 AND tenant_no = :tenant AND id IN ({placeholders})
  93. AND perms IS NOT NULL AND TRIM(perms) <> ''
  94. ORDER BY id
  95. """
  96. ),
  97. params,
  98. ).fetchall()
  99. return [str(r[0]) for r in rows if r and r[0]]
  100. def leaf_menu_ids(conn, menu_ids: Set[str], tenant_no: str) -> List[str]:
  101. """与角色管理页一致:仅持久化叶子菜单(无子节点的菜单)。"""
  102. if not menu_ids:
  103. return []
  104. placeholders = ", ".join(f":m{i}" for i in range(len(menu_ids)))
  105. params = {f"m{i}": mid for i, mid in enumerate(sorted(menu_ids))}
  106. params["tenant"] = tenant_no
  107. rows = conn.execute(
  108. text(
  109. f"""
  110. SELECT id, parent_id FROM sys_menu
  111. WHERE del_flag = 0 AND tenant_no = :tenant AND id IN ({placeholders})
  112. """
  113. ),
  114. params,
  115. ).mappings().all()
  116. id_set = {str(r["id"]) for r in rows}
  117. parents_with_children: Set[str] = set()
  118. for r in rows:
  119. pid = str(r.get("parent_id") or "")
  120. if pid in id_set:
  121. parents_with_children.add(pid)
  122. leaves = sorted(mid for mid in id_set if mid not in parents_with_children)
  123. return leaves
  124. def fetch_user_id_map(conn, tenant_no: str) -> Dict[str, str]:
  125. rows = conn.execute(
  126. text(
  127. """
  128. SELECT id, user_no, dept_id FROM sys_user
  129. WHERE tenant_no = :tenant AND del_flag = 0
  130. """
  131. ),
  132. {"tenant": tenant_no},
  133. ).mappings().all()
  134. return {str(r["user_no"]): {"id": str(r["id"]), "dept_id": str(r.get("dept_id") or "")} for r in rows}
  135. def purge_non_super_roles(conn, tenant_no: str, dry_run: bool) -> int:
  136. cnt = conn.execute(
  137. text(
  138. """
  139. SELECT COUNT(*) FROM sys_role
  140. WHERE tenant_no = :tenant AND del_flag = 0 AND id <> :super_id
  141. """
  142. ),
  143. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID},
  144. ).scalar()
  145. if dry_run:
  146. return int(cnt or 0)
  147. now = datetime.now()
  148. conn.execute(
  149. text(
  150. """
  151. UPDATE sys_role_menu
  152. SET del_flag = 1, update_time = :now
  153. WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
  154. """
  155. ),
  156. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
  157. )
  158. conn.execute(
  159. text(
  160. """
  161. DELETE FROM sys_role_menu
  162. WHERE tenant_no = :tenant AND role_id <> :super_id
  163. """
  164. ),
  165. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID},
  166. )
  167. conn.execute(
  168. text(
  169. """
  170. UPDATE sys_role_data_scope_detail
  171. SET del_flag = 1, update_time = :now
  172. WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
  173. """
  174. ),
  175. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
  176. )
  177. conn.execute(
  178. text(
  179. """
  180. UPDATE sys_user_role
  181. SET del_flag = 1, update_time = :now
  182. WHERE tenant_no = :tenant AND role_id <> :super_id AND del_flag = 0
  183. """
  184. ),
  185. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now},
  186. )
  187. conn.execute(
  188. text(
  189. """
  190. UPDATE sys_role
  191. SET del_flag = 1, update_time = :now, update_by = :admin
  192. WHERE tenant_no = :tenant AND del_flag = 0 AND id <> :super_id
  193. """
  194. ),
  195. {"tenant": tenant_no, "super_id": SUPER_ADMIN_ROLE_ID, "now": now, "admin": ADMIN_USER_ID},
  196. )
  197. return int(cnt or 0)
  198. def ensure_new_roles(conn, tenant_no: str, dry_run: bool) -> None:
  199. order = 10
  200. for role_name, role_id in NEW_ROLES.items():
  201. exists = conn.execute(
  202. text(
  203. """
  204. SELECT id FROM sys_role
  205. WHERE id = :id AND tenant_no = :tenant AND del_flag = 0
  206. LIMIT 1
  207. """
  208. ),
  209. {"id": role_id, "tenant": tenant_no},
  210. ).first()
  211. if exists:
  212. order += 10
  213. continue
  214. if not dry_run:
  215. conn.execute(
  216. text(
  217. """
  218. INSERT INTO sys_role (
  219. id, role_name, data_scope, status, tenant_no, order_num, remark,
  220. create_by, create_dept_id, create_time, update_by, update_time, del_flag
  221. ) VALUES (
  222. :id, :name, '1', '1', :tenant, :order_num,
  223. '用户迁移后业务角色初始化', :admin, '', :now, :admin, :now, 0
  224. )
  225. """
  226. ),
  227. {
  228. "id": role_id,
  229. "name": role_name,
  230. "tenant": tenant_no,
  231. "order_num": order,
  232. "admin": ADMIN_USER_ID,
  233. "now": datetime.now(),
  234. },
  235. )
  236. order += 10
  237. def grant_module_menus(conn, role_id: str, leaf_ids: List[str], tenant_no: str, dry_run: bool) -> int:
  238. if dry_run:
  239. return len(leaf_ids)
  240. conn.execute(
  241. text("DELETE FROM sys_role_menu WHERE role_id = :rid"),
  242. {"rid": role_id},
  243. )
  244. now = datetime.now()
  245. for mid in leaf_ids:
  246. conn.execute(
  247. text(
  248. """
  249. INSERT INTO sys_role_menu (
  250. id, role_id, menu_id, tenant_no, order_num, remark,
  251. create_by, create_dept_id, create_time, update_by, update_time, del_flag
  252. ) VALUES (
  253. :id, :role_id, :menu_id, :tenant, 0, 'setup_business_roles',
  254. :admin, '', :now, :admin, :now, 0
  255. )
  256. """
  257. ),
  258. {
  259. "id": generate_uuid(),
  260. "role_id": role_id,
  261. "menu_id": mid,
  262. "tenant": tenant_no,
  263. "admin": ADMIN_USER_ID,
  264. "now": now,
  265. },
  266. )
  267. return len(leaf_ids)
  268. def assign_user_roles(
  269. conn,
  270. user_map: Dict[str, dict],
  271. tenant_no: str,
  272. dry_run: bool,
  273. ) -> List[dict]:
  274. rows_out: List[dict] = []
  275. for role_name, user_nos in USER_ROLE_ASSIGNMENTS.items():
  276. role_id = NEW_ROLES[role_name]
  277. for user_no in user_nos:
  278. info = user_map.get(user_no)
  279. if not info:
  280. raise ValueError(f"用户不存在: {user_no}")
  281. exists = conn.execute(
  282. text(
  283. """
  284. SELECT id FROM sys_user_role
  285. WHERE user_id = :uid AND role_id = :rid AND tenant_no = :tenant AND del_flag = 0
  286. LIMIT 1
  287. """
  288. ),
  289. {"uid": info["id"], "rid": role_id, "tenant": tenant_no},
  290. ).first()
  291. if exists:
  292. rows_out.append({"user_no": user_no, "role_name": role_name, "status": "exists"})
  293. continue
  294. if not dry_run:
  295. conn.execute(
  296. text(
  297. """
  298. INSERT INTO sys_user_role (
  299. id, user_id, role_id, tenant_no, order_num, remark,
  300. create_by, create_dept_id, create_time, update_by, update_time, del_flag
  301. ) VALUES (
  302. :id, :uid, :rid, :tenant, 0, 'setup_business_roles',
  303. :admin, :dept_id, :now, :admin, :now, 0
  304. )
  305. """
  306. ),
  307. {
  308. "id": generate_uuid(),
  309. "uid": info["id"],
  310. "rid": role_id,
  311. "tenant": tenant_no,
  312. "admin": ADMIN_USER_ID,
  313. "dept_id": info["dept_id"],
  314. "now": datetime.now(),
  315. },
  316. )
  317. rows_out.append({"user_no": user_no, "role_name": role_name, "status": "inserted"})
  318. return rows_out
  319. def main() -> int:
  320. parser = argparse.ArgumentParser(description="初始化业务角色与菜单权限")
  321. parser.add_argument("--env-file", default=".env.production")
  322. parser.add_argument("--dry-run", action="store_true")
  323. parser.add_argument(
  324. "--report-out",
  325. default=str(BACKEND_ROOT.parent / "docs/data-migration/mappings/role_setup_result.json"),
  326. )
  327. args = parser.parse_args()
  328. load_settings_env(args.env_file)
  329. settings = load_runtime_settings()
  330. engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)
  331. print("=== setup_business_roles ===")
  332. print(f"模式: {'DRY-RUN' if args.dry_run else 'EXECUTE'}")
  333. print(f"租户: {TENANT_NO}")
  334. print()
  335. with engine.begin() as conn:
  336. removed = purge_non_super_roles(conn, TENANT_NO, args.dry_run)
  337. ensure_new_roles(conn, TENANT_NO, args.dry_run)
  338. subtree = collect_subtree_menu_ids(conn, MODULE_MENU_ROOTS, TENANT_NO)
  339. grant_ids = perm_menu_ids(conn, subtree, TENANT_NO)
  340. menu_grants = {}
  341. for role_name, role_id in NEW_ROLES.items():
  342. menu_grants[role_name] = grant_module_menus(conn, role_id, grant_ids, TENANT_NO, args.dry_run)
  343. user_map = fetch_user_id_map(conn, TENANT_NO)
  344. assignments = assign_user_roles(conn, user_map, TENANT_NO, args.dry_run)
  345. report = {
  346. "generated_at": datetime.now().isoformat(timespec="seconds"),
  347. "removed_old_roles": removed,
  348. "module_menu_roots": MODULE_MENU_ROOTS,
  349. "subtree_menu_count": len(subtree),
  350. "perm_menu_count": len(grant_ids),
  351. "menu_grants_per_role": menu_grants,
  352. "user_role_assignments": assignments,
  353. "new_roles": NEW_ROLES,
  354. }
  355. if not args.dry_run:
  356. Path(args.report_out).write_text(
  357. json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
  358. )
  359. print(f"清理旧角色(非超级管理员): {removed}")
  360. print(f"模块子树菜单数: {len(subtree)},带 perms 菜单数: {len(grant_ids)}")
  361. for role_name, cnt in menu_grants.items():
  362. print(f" {role_name}: {cnt} 个菜单权限")
  363. print("用户-角色绑定:")
  364. for item in assignments:
  365. print(f" {item['user_no']} -> {item['role_name']} ({item['status']})")
  366. if not args.dry_run:
  367. print(f"报告: {args.report_out}")
  368. print("完成.")
  369. return 0
  370. if __name__ == "__main__":
  371. raise SystemExit(main())