#!/usr/bin/env python3 """重排 Web 端一级菜单顺序,并生成 sys_menu 初始化 SQL。""" from __future__ import annotations import ast import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] MENU_PY = ROOT / "backend/app/api/menu.py" SQXP_TS = ROOT / "web-admin/src/router/modules/sqxp.ts" SQL_OUT = ROOT / "docs/sql/sys_menu_web_init.sql" BLOCK_ORDER = [2, 3, 4, 5, 7, 9, 10, 1, 6, 8, 11, 12, 13] BLOCK_TITLES = { 1: "市场基础信息", 2: "细胞存储", 3: "科研捐赠", 4: "中试服务", 5: "临床应用", 6: "质检基础信息", 7: "质检管理", 8: "生产基础信息", 9: "生产管理", 10: "运营支撑", 11: "系统基础信息", 12: "工作流管理", 13: "系统管理", } SQXP_HEADER = """import type { AppRouteRecord } from '@/types/router' /** * 前端路由(13 个一级菜单:细胞存储、科研捐赠、中试服务、临床应用、 * 质检管理、生产管理、运营支撑、市场基础信息、质检基础信息、生产基础信息、 * 系统基础信息、工作流管理、系统管理) * 与后端菜单对齐时请同步 `backend/app/api/menu.py`。 */ export const sqxpAdminRoutes: AppRouteRecord[] = [ """ MENU_TAIL = ( "\n]\n\n\n" '@router.get("/list")\n' "async def get_menu_list(_user: dict = Depends(require_admin_or_super_admin)):\n" ' """获取菜单列表(与当前前端路由一致)"""\n' ' return success_response({"menuList": STATIC_MENU_TREE})\n' ) BLOCK_RE_TS = re.compile( r" // ──+\n // (\d+)\. ([^\n]+)\n // ──+\n", re.MULTILINE, ) BLOCK_RE_PY = re.compile( r" # ──+\n # (\d+)\. ([^\n]+)\n # ──+\n", re.MULTILINE, ) def split_menu_blocks( content: str, array_start_marker: str, block_re: re.Pattern ) -> tuple[str, dict[int, str]]: start = content.index(array_start_marker) + len(array_start_marker) depth = 1 i = start while i < len(content) and depth > 0: if content[i] == "[": depth += 1 elif content[i] == "]": depth -= 1 i += 1 array_body = content[start : i - 1] prefix = content[:start] parts = block_re.split(array_body) blocks: dict[int, str] = {} idx = 1 while idx < len(parts): num = int(parts[idx]) block = parts[idx + 2].rstrip() if block.endswith(","): block = block[:-1] blocks[num] = block.rstrip() idx += 3 return prefix, blocks def reorder_blocks(blocks: dict[int, str]) -> list[tuple[int, str, str]]: ordered = [] for new_idx, old_num in enumerate(BLOCK_ORDER, start=1): ordered.append((new_idx, BLOCK_TITLES[old_num], blocks[old_num])) return ordered def assemble_blocks(ordered: list[tuple[int, str, str]], *, ts: bool) -> str: chunks = [] for num, title, body in ordered: if ts: sep = " // " + "─" * 46 chunks.append(f"{sep}\n // {num}. {title}\n{sep}\n{body}") else: sep = " # " + "─" * 44 chunks.append(f"{sep}\n # {num}. {title}\n{sep}\n{body}") return ",\n\n".join(chunks) + "\n" def load_static_menu_tree() -> list: content = MENU_PY.read_text(encoding="utf-8") marker = "STATIC_MENU_TREE = " start = content.index(marker) + len(marker) depth = 0 i = start while i < len(content): ch = content[i] if ch == "[": depth += 1 elif ch == "]": depth -= 1 if depth == 0: i += 1 break i += 1 literal = content[start:i] return ast.literal_eval(literal) def sql_escape(val: str | None) -> str: if val is None: return "NULL" return "'" + str(val).replace("'", "''") + "'" def walk_routes(nodes: list, parent_id: str, order_counter: dict, rows: list) -> None: for node in nodes: node_id = node["id"] meta = node.get("meta") or {} title = meta.get("title") or node.get("name") or "" path = node.get("path") or "" component = node.get("component") or "" children = node.get("children") or [] is_hide = meta.get("isHide") is True menu_code = meta.get("menuCode") perms = menu_code if menu_code else None if parent_id == "2": order_counter["web"] += 1 order_num = order_counter["web"] else: order_counter.setdefault(parent_id, 0) order_counter[parent_id] += 1 order_num = order_counter[parent_id] menu_type = "M" if children else "C" visible = "0" if is_hide else "1" rows.append( ( node_id, title, parent_id, menu_type, path, component or None, perms, visible, order_num, ) ) if children: walk_routes(children, node_id, order_counter, rows) def generate_sql(tree: list) -> str: rows: list = [] order_counter: dict = {"web": 0} walk_routes(tree, "2", order_counter, rows) lines = [ "-- Web 端 sys_menu 初始化(根节点 + 5 个一级分组 + 二级/三级业务菜单)", "-- 生成:backend/scripts/reorder_web_menus.py", "-- 层级:Web端菜单(id=2) → 市场/生产/质检/运营支撑/系统管理(TOP*) → 业务二级 → 页面三级", "-- 一级分组顺序:市场 → 生产 → 质检 → 运营支撑 → 系统管理", "", "SET NAMES utf8mb4;", "", "INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES", "('1', '移动端菜单', '0', 'M', '/mini', NULL, NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),", "('2', 'Web端菜单', '0', 'M', '/web', NULL, NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);", "", "INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES", ] value_lines = [] for row in rows: ( node_id, title, parent_id, menu_type, path, component, perms, visible, order_num, ) = row value_lines.append( f"({sql_escape(node_id)}, {sql_escape(title)}, {sql_escape(parent_id)}, " f"{sql_escape(menu_type)}, {sql_escape(path)}, {sql_escape(component)}, " f"{sql_escape(perms)}, 1, {sql_escape(visible)}, '1', 'zkxw', {order_num}, " f"NULL, NULL, '', NOW(), NULL, NOW(), 0)" ) lines.append(",\n".join(value_lines) + ";") lines.append("") return "\n".join(lines) def main() -> None: menu_content = MENU_PY.read_text(encoding="utf-8") pre, blocks = split_menu_blocks(menu_content, "STATIC_MENU_TREE = [\n", BLOCK_RE_PY) body = assemble_blocks(reorder_blocks(blocks), ts=False) MENU_PY.write_text(pre + body + MENU_TAIL, encoding="utf-8") print(f"updated {MENU_PY.relative_to(ROOT)}") ts_content = SQXP_TS.read_text(encoding="utf-8") _, blocks_ts = split_menu_blocks( ts_content, "export const sqxpAdminRoutes: AppRouteRecord[] = [\n", BLOCK_RE_TS ) body_ts = assemble_blocks(reorder_blocks(blocks_ts), ts=True) SQXP_TS.write_text(SQXP_HEADER + body_ts + "]\n", encoding="utf-8") print(f"updated {SQXP_TS.relative_to(ROOT)}") tree = load_static_menu_tree() sql = generate_sql(tree) SQL_OUT.write_text(sql, encoding="utf-8") row_count = sql.count("('") - 2 # minus root inserts print(f"wrote {SQL_OUT.relative_to(ROOT)} (~{row_count} menu rows, {len(tree)} top-level menus)") if __name__ == "__main__": main()