# -*- coding: utf-8 -*- """ Generate complete Chinese menu tree (M/C/F) from tenant_sys_menu + view title hints. Output: adminUI_menu_tree_zh.md """ import os import re from collections import defaultdict from datetime import date try: import pymysql except ImportError: pymysql = None ROOT = os.path.normpath( os.path.join(os.path.dirname(__file__), '..', '..', 'ylrz_saas_his_scrm_adminUI') ) OUT = os.path.join(os.path.dirname(__file__), 'adminUI_menu_tree_zh.md') MENU_JS = os.path.join(ROOT, 'src', 'views', 'admin', 'menu.js') DB = dict( host='cq-cdb-8fjmemkb.sql.tencentcdb.com', port=27220, user='root', password='Ylrz_1q2w3e4r5t6y', database='ylrz_saas', charset='utf8mb4', ) TYPE_LABEL = {'M': '\u76ee\u5f55', 'C': '\u83dc\u5355', 'F': '\u6309\u94ae'} # Root menu_id -> fixed Chinese name ROOT_ZH = { 32361: '\u4f01\u5fae\u7ba1\u7406', 32380: '\u5fae\u4fe1\u7ba1\u7406', 32347: 'CRM\u5ba2\u6237', 32357: '\u4f1a\u5458\u7ba1\u7406', 32351: '\u8bca\u6240\u7ba1\u7406', 32369: '\u5546\u57ce\u7ba1\u7406', 32353: '\u76f4\u64ad\u7ba1\u7406', 32345: '\u8bfe\u7a0b\u7ba1\u7406', 32348: 'AI\u804a\u5929', 32355: '\u9f99\u8678\u5f15\u64ce', 32331: '\u5e7f\u544a\u6295\u653e', 32372: '\u7cfb\u7edf\u7ba1\u7406', 32339: '\u8d22\u52a1\u7ba1\u7406', 32341: '\u65e5\u7a0b\u7ba1\u7406', 32368: '\u6570\u636e\u7edf\u8ba1', 32379: '\u76d1\u63a7\u7ba1\u7406', 35300: '\u5176\u4ed6', 32333: '\u5e73\u53f0\u7ba1\u7406\uff08\u5f52\u6863\uff09', 32644: '\u9996\u9875', 35100: '\u7ec4\u7ec7\u7ba1\u7406', 35101: '\u6743\u9650\u7ba1\u7406', 35102: '\u901a\u4fe1\u7ba1\u7406', 35105: '\u65e5\u5fd7\u7ba1\u7406', 35106: '\u7cfb\u7edf\u8bbe\u7f6e', 35001: '\u6d88\u606f\u7ba1\u7406', 35002: '\u5ba2\u6237\u7ba1\u7406', 35003: '\u7fa4\u804a\u7ba1\u7406', 35004: '\u670b\u53cb\u5708', 35005: '\u5f15\u6d41\u7ba1\u7406', 35006: '\u6807\u7b7e\u7ba1\u7406', 35007: '\u4f01\u5fae\u8bbe\u7f6e', 35040: '\u8ba2\u5355\u7ba1\u7406', 35041: '\u5546\u54c1\u7ba1\u7406', 35042: '\u95e8\u5e97\u8fd0\u8425', 35010: '\u5fae\u4fe1\u8d26\u53f7', 35011: '\u5fae\u4fe1\u5bf9\u8bdd', 35012: '\u5fae\u4fe1\u7528\u6237', 35013: '\u5fae\u4fe1\u7528\u6237\u7ec4', 35020: 'CRM\u5ba2\u6237\u7ba1\u7406', 35050: '\u76f4\u64ad\u8fd0\u8425', 35060: '\u8bfe\u7a0b\u5185\u5bb9', 35070: 'AI\u5bf9\u8bdd\u7ba1\u7406', 35080: '\u9f99\u8678\u5de5\u4f5c\u6d41', 35090: '\u6295\u653e\u8fd0\u8425', 35111: '\u5145\u503c\u7ba1\u7406', 35112: '\u6263\u8d39\u7ba1\u7406', 35113: '\u5206\u8d26\u7ba1\u7406', 35114: '\u8d44\u91d1\u65e5\u5fd7', 35201: '\u7528\u6237\u7ba1\u7406', 35202: '\u89d2\u8272\u7ba1\u7406', 35203: '\u83dc\u5355\u7ba1\u7406', 35204: '\u90e8\u95e8\u7ba1\u7406', 35205: '\u5c97\u4f4d\u7ba1\u7406', 35206: '\u5b57\u5178\u7ba1\u7406', 35207: '\u53c2\u6570\u8bbe\u7f6e', 35208: '\u901a\u77e5\u516c\u544a', 35209: '\u8fdd\u89c4\u8bcd\u8bed', } # component suffix / module -> Chinese COMP_ZH = { 'externalContact': '\u5916\u90e8\u8054\u7cfb\u4eba', 'QwWorkTask': '\u4f01\u5fae\u5de5\u4f5c\u4efb\u52a1', 'groupMsg': '\u7fa4\u6d88\u606f', 'contactWay': '\u6e20\u9053\u7801', 'friendCircle': '\u670b\u53cb\u5708', 'storeOrder': '\u8ba2\u5355\u7ba1\u7406', 'storeProduct': '\u5546\u54c1\u7ba1\u7406', 'storeShop': '\u95e8\u5e97\u7ba1\u7406', 'liveConsole': '\u76f4\u64ad\u63a7\u5236\u53f0', 'liveOrder': '\u76f4\u64ad\u8ba2\u5355', 'videoResource': '\u89c6\u9891\u8d44\u6e90', 'userCourse': '\u7528\u6237\u8bfe\u7a0b', 'fastGptRole': 'AI\u89d2\u8272', 'fastGptDataset': 'AI\u77e5\u8bc6\u5e93', 'companyUser': '\u4f01\u4e1a\u7528\u6237', 'companyRole': '\u4f01\u4e1a\u89d2\u8272', 'companyMenu': '\u4f01\u4e1a\u83dc\u5355', 'companyDept': '\u4f01\u4e1a\u90e8\u95e8', 'companyRecharge': '\u5145\u503c\u8bb0\u5f55', 'companyProfit': '\u5206\u8d26\u8bb0\u5f55', 'companyMoneyLogs': '\u8d44\u91d1\u6d41\u6c34', 'sysCompany': '\u79df\u6237\u7ba1\u7406', 'tenantMenu': '\u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355', 'operlog': '\u64cd\u4f5c\u65e5\u5fd7', 'logininfor': '\u767b\u5f55\u65e5\u5fd7', 'workflow-generate': 'AI\u751f\u6210\u5de5\u4f5c\u6d41', 'dead-letter': '\u6b7b\u4fe1\u961f\u5217', 'workflow-canvas': '\u5de5\u4f5c\u6d41\u753b\u5e03', } WORD_ZH = { 'user': '\u7528\u6237', 'users': '\u7528\u6237', 'role': '\u89d2\u8272', 'menu': '\u83dc\u5355', 'dept': '\u90e8\u95e8', 'post': '\u5c97\u4f4d', 'dict': '\u5b57\u5178', 'config': '\u914d\u7f6e', 'notice': '\u516c\u544a', 'keyword': '\u5173\u952e\u8bcd', 'order': '\u8ba2\u5355', 'store': '\u5546\u57ce', 'product': '\u5546\u54c1', 'customer': '\u5ba2\u6237', 'company': '\u4f01\u4e1a', 'tenant': '\u79df\u6237', 'proxy': '\u4ee3\u7406', 'admin': '\u5e73\u53f0', 'live': '\u76f4\u64ad', 'course': '\u8bfe\u7a0b', 'doctor': '\u533b\u751f', 'patient': '\u60a3\u8005', 'inquiry': '\u95ee\u8bca', 'statistics': '\u7edf\u8ba1', 'report': '\u62a5\u8868', 'log': '\u65e5\u5fd7', 'logs': '\u65e5\u5fd7', 'record': '\u8bb0\u5f55', 'tag': '\u6807\u7b7e', 'group': '\u7fa4', 'msg': '\u6d88\u606f', 'voice': '\u8bed\u97f3', 'sms': '\u77ed\u4fe1', 'coupon': '\u4f18\u6263\u5238', 'export': '\u5bfc\u51fa', 'import': '\u5bfc\u5165', 'package': '\u5957\u9910', 'wallet': '\u94b1\u5305', 'bill': '\u8d26\u5355', 'calendar': '\u65e5\u7a0b', 'monitor': '\u76d1\u63a7', 'watch': '\u76d1\u63a7', 'material': '\u7d20\u6750', 'welcome': '\u6b22\u8fce\u8bed', 'sop': 'SOP', 'advertiser': '\u5e7f\u544a\u4e3b', 'channel': '\u6e20\u9053', 'domain': '\u57df\u540d', 'site': '\u7ad9\u70b9', 'recharge': '\u5145\u503c', 'deduct': '\u6263\u8d39', 'profit': '\u5206\u8d26', 'withdraw': '\u63d0\u73b0', 'article': '\u6587\u7ae0', 'video': '\u89c6\u9891', 'comment': '\u8bc4\u8bba', 'question': '\u95ee\u9898', 'answer': '\u7b54\u6848', 'schedule': '\u6392\u73ed', 'traffic': '\u6d41\u91cf', 'workflow': '\u5de5\u4f5c\u6d41', 'lobster': '\u9f99\u8678', 'prompt': '\u63d0\u793a\u8bcd', 'instance': '\u5b9e\u4f8b', 'template': '\u6a21\u677f', 'shipping': '\u8fd0\u8d39', 'prescribe': '\u5904\u65b9', 'integral': '\u79ef\u5206', 'member': '\u4f1a\u5458', 'blacklist': '\u9ed1\u540d\u5355', 'complaint': '\u6295\u8bc9', 'transfer': '\u8f6c\u79fb', 'external': '\u5916\u90e8', 'contact': '\u8054\u7cfb\u4eba', 'qw': '\u4f01\u5fae', 'wx': '\u5fae\u4fe1', 'crm': 'CRM', 'his': '\u8bca\u6240', 'adv': '\u5e7f\u544a', 'ad': '\u5e7f\u544a', 'tool': '\u5de5\u5177', 'gen': '\u4ee3\u7801\u751f\u6210', 'job': '\u5b9a\u65f6\u4efb\u52a1', 'online': '\u5728\u7ebf\u7528\u6237', 'cache': '\u7f13\u5b58', 'server': '\u670d\u52a1\u5668', 'druid': '\u6570\u636e\u76d1\u63a7', 'iot': '\u7269\u8054\u7f51', 'device': '\u8bbe\u5907', 'index': '\u9996\u9875', 'list': '\u5217\u8868', 'manage': '\u7ba1\u7406', 'setting': '\u8bbe\u7f6e', 'info': '\u4fe1\u606f', 'detail': '\u8be6\u60c5', 'data': '\u6570\u636e', 'temp': '\u6a21\u677f', 'api': 'API', 'Actual': '\u5b9e\u9645', 'Statistic': '\u7edf\u8ba1', 'OnJob': '\u5728\u804c', 'Live': '\u76f4\u64ad', 'Code': '\u7801', 'Circle': '\u5708', 'Task': '\u4efb\u52a1', 'Comments': '\u8bc4\u8bba', 'Customer': '\u5ba2\u6237', 'Item': '\u660e\u7ec6', 'Advertising': '\u5e7f\u544a', 'Apply': '\u7533\u8bf7', 'Ipad': 'iPad', 'Behavior': '\u884c\u4e3a', 'Push': '\u63a8\u9001', 'Count': '\u7edf\u8ba1', 'Assign': '\u5206\u914d', 'assign': '\u5206\u914d', 'Rule': '\u89c4\u5219', 'Batch': '\u6279\u6b21', 'Way': '\u65b9\u5f0f', 'Link': '\u94fe\u63a5', 'Drainage': '\u5f15\u6d41', 'drainage': '\u5f15\u6d41', 'Loss': '\u6d41\u5931', 'Stage': '\u9636\u6bb5', 'Transfer': '\u8f6c\u79fb', 'Audit': '\u5ba1\u6838', 'Unassigned': '\u672a\u5206\u914d', 'AfterSales': '\u552e\u540e', 'Promotion': '\u63a8\u5e7f', 'Health': '\u5065\u5eb7', 'Inquiry': '\u95ee\u8bca', 'Category': '\u5206\u7c7b', 'DarkRoom': '\u5c0f\u9ed1\u5c4b', 'Recharge': '\u5145\u503c', 'Template': '\u6a21\u677f', 'Follow': '\u968f\u8bbf', 'Audit': '\u5ba1\u6838', 'Offline': '\u7ebf\u4e0b', 'Audit': '\u5ba1\u6838', 'Staff': '\u5458\u5de5', 'Cart': '\u8d2d\u7269\u8f66', 'Visit': '\u8bbf\u95ee', 'Relation': '\u5173\u8054', 'Reply': '\u56de\u590d', 'Attr': '\u5c5e\u6027', 'Details': '\u8be6\u60c5', 'Category': '\u5206\u7c7b', 'Group': '\u5206\u7ec4', 'Console': '\u63a7\u5236\u53f0', 'Config': '\u914d\u7f6e', 'Coupon': '\u4f18\u6263\u5238', 'Question': '\u95ee\u9898', 'Reward': '\u5956\u52b1', 'Favorite': '\u6536\u85cf', 'Watch': '\u89c2\u770b', 'Talent': '\u8fbe\u4eba', 'Training': '\u57f9\u8bad', 'Camp': '\u8425', 'Material': '\u7d20\u6750', 'Period': '\u671f\u6570', 'Resource': '\u8d44\u6e90', 'Finish': '\u5b8c\u7ed3', 'Temp': '\u6a21\u677f', 'Bank': '\u9898\u5e93', 'Answer': '\u7b54\u6848', 'RedPacket': '\u7ea2\u5305', 'Traffic': '\u6d41\u91cf', 'Collection': '\u6536\u85cf', 'Dataset': '\u77e5\u8bc6\u5e93', 'Session': '\u4f1a\u8bdd', 'Keyword': '\u5173\u952e\u8bcd', 'Role': '\u89d2\u8272', 'Replace': '\u66ff\u6362', 'Words': '\u8bcd\u6761', 'Quality': '\u8d28\u68c0', 'Gateway': '\u7f51\u5173', 'Sip': 'SIP', 'Call': '\u547c\u53eb', 'Client': '\u5ba2\u6237\u7aef', 'Offline': '\u7ebf\u4e0b', 'Item': '\u660e\u7ec6', } PERM_ACTION_ZH = { 'add': '\u65b0\u589e', 'edit': '\u4fee\u6539', 'update': '\u4fee\u6539', 'remove': '\u5220\u9664', 'delete': '\u5220\u9664', 'query': '\u67e5\u8be2', 'list': '\u5217\u8868', 'export': '\u5bfc\u51fa', 'import': '\u5bfc\u5165', 'view': '\u67e5\u770b', 'detail': '\u8be6\u60c5', 'reset': '\u91cd\u7f6e', 'auth': '\u6388\u6743', 'assign': '\u5206\u914d', 'changeStatus': '\u72b6\u6001\u53d8\u66f4', 'audit': '\u5ba1\u6838', 'approve': '\u5ba1\u6279', 'reject': '\u9a73\u56de', 'sync': '\u540c\u6b65', 'refresh': '\u5237\u65b0', 'execute': '\u6267\u884c', 'cancel': '\u53d6\u6d88', 'submit': '\u63d0\u4ea4', 'publish': '\u53d1\u5e03', 'copy': '\u590d\u5236', 'download': '\u4e0b\u8f7d', 'upload': '\u4e0a\u4f20', 'bind': '\u7ed1\u5b9a', 'unbind': '\u89e3\u7ed1', 'enable': '\u542f\u7528', 'disable': '\u505c\u7528', } def has_chinese(s): return bool(s and re.search(r'[\u4e00-\u9fff]', s)) def split_camel(s): parts = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', s or '') parts = re.sub(r'[-_/]', ' ', parts) return [p for p in parts.split() if p] def translate_tokens(text): if not text: return '' out = [] for tok in split_camel(text): low = tok.lower() if tok in WORD_ZH: out.append(WORD_ZH[tok]) elif low in WORD_ZH: out.append(WORD_ZH[low]) elif tok in COMP_ZH: out.append(COMP_ZH[tok]) elif re.match(r'^[A-Z][a-z]+', tok): out.append(translate_tokens(tok[0].lower() + tok[1:]) or tok) else: out.append(tok) return ''.join(out) if out else text def load_admin_titles(): titles = {} if not os.path.exists(MENU_JS): return titles with open(MENU_JS, 'r', encoding='utf-8') as f: content = f.read() for m in re.finditer( r"import\('@/views/([^']+)'\)[^}]*title:\s*'([^']+)'", content ): comp, title = m.group(1).replace('.vue', ''), m.group(2) if comp.endswith('/index'): comp = comp[:-6] titles[comp] = title return titles def load_menus(): conn = pymysql.connect(**DB) cur = conn.cursor(pymysql.cursors.DictCursor) cur.execute( 'SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, perms ' 'FROM tenant_sys_menu ORDER BY parent_id, order_num, menu_id' ) rows = cur.fetchall() cur.close() conn.close() return rows def comp_key(component): if not component: return '' c = component.replace('/index/index', '/index').replace('/index', '') return c.strip('/') def to_chinese(menu, admin_titles, comp_zh=None, parent_zh=''): comp_zh = comp_zh or COMP_ZH mid = menu['menu_id'] if mid in ROOT_ZH: return ROOT_ZH[mid] name = (menu.get('menu_name') or '').strip() if has_chinese(name) and not re.match(r'^[a-zA-Z\s]+$', name): # clean mixed English-only names if not re.fullmatch(r'[A-Za-z0-9\s\-_]+', name): return name comp = comp_key(menu.get('component') or '') if comp in admin_titles: return admin_titles[comp] if comp in comp_zh: return comp_zh[comp] # component last segment if comp: seg = comp.split('/')[-1] if seg in comp_zh: return comp_zh[seg] zh = translate_tokens(seg) if zh and zh != seg: return zh # two segments if '/' in comp: zh2 = translate_tokens(comp.split('/')[-2] + seg) if has_chinese(zh2): return zh2 if menu['menu_type'] == 'F': perms = menu.get('perms') or '' if perms: parts = perms.split(':') action = parts[-1] if parts else '' act_zh = PERM_ACTION_ZH.get(action, PERM_ACTION_ZH.get(action.lower(), '')) if act_zh: if parent_zh and parent_zh not in ('\u6309\u94ae', act_zh): return parent_zh + act_zh if len(parts) >= 2: res = translate_tokens(parts[-2]) if has_chinese(res): return res + act_zh return act_zh if name and name != '#': zh = translate_tokens(name) return zh if has_chinese(zh) or zh != name else (act_zh or '\u6309\u94ae') if name: zh = translate_tokens(name.replace(' ', '')) if has_chinese(zh): return zh # title case words if ' ' in name: parts = [WORD_ZH.get(w.lower(), w) for w in name.split()] joined = ''.join(parts) if has_chinese(joined): return joined path = menu.get('path') or '' if path and path != '#': zh = translate_tokens(path) if has_chinese(zh): return zh return name or comp or ('\u672a\u547d\u540d\u83dc\u5355%d' % mid) def build_children_map(menus): ch = defaultdict(list) by_id = {} for m in menus: by_id[m['menu_id']] = m ch[m['parent_id']].append(m) for pid in ch: ch[pid].sort(key=lambda x: (x.get('order_num') or 0, x['menu_id'])) return ch, by_id def build_comp_zh_from_db(menus): comp_zh = dict(COMP_ZH) for m in menus: if m['menu_type'] not in ('C', 'M'): continue name = (m.get('menu_name') or '').strip() comp = comp_key(m.get('component') or '') if comp and has_chinese(name) and not re.fullmatch(r'[A-Za-z0-9\s\-_:.]+', name): comp_zh[comp] = name seg = comp.split('/')[-1] if seg and has_chinese(name): comp_zh[seg] = name return comp_zh def collect_reachable(ch_map, roots): seen = set() stack = [r['menu_id'] for r in roots] while stack: mid = stack.pop() if mid in seen: continue seen.add(mid) for child in ch_map.get(mid, []): stack.append(child['menu_id']) return seen def render_node(menu, zh_name, depth, lines, ch_map, zh_cache, admin_titles, comp_zh): mtype = menu['menu_type'] label = TYPE_LABEL.get(mtype, mtype) vis = '\u9690\u85cf' if menu.get('visible') == '1' else '\u663e\u793a' comp = menu.get('component') or '-' perms = menu.get('perms') or '-' mid = menu['menu_id'] indent = ' ' * depth if mtype == 'F': lines.append('%s- \u2514\u2500 [%s] **%s** `perms=%s`' % (indent, label, zh_name, perms)) else: lines.append('%s- [%s] **%s** `id=%s` path=`%s` component=`%s` %s' % ( indent, label, zh_name, mid, menu.get('path') or '', comp, vis)) for child in ch_map.get(mid, []): if child['menu_id'] not in zh_cache: parent_name = zh_name if mtype == 'C' else '' zh_cache[child['menu_id']] = to_chinese( child, admin_titles, comp_zh, parent_zh=parent_name if child['menu_type'] == 'F' else '') render_node(child, zh_cache[child['menu_id']], depth + 1, lines, ch_map, zh_cache, admin_titles, comp_zh) def main(): menus = load_menus() admin_titles = load_admin_titles() comp_zh = build_comp_zh_from_db(menus) ch_map, by_id = build_children_map(menus) zh_cache = {m['menu_id']: to_chinese(m, admin_titles, comp_zh) for m in menus} roots = ch_map.get(0, []) biz_roots = [r for r in roots if r['menu_id'] != 32333 and r.get('visible') == '0'] hidden_roots = [r for r in roots if r not in biz_roots] biz_roots.sort(key=lambda x: (x.get('order_num') or 0, x['menu_id'])) hidden_roots.sort(key=lambda x: (x.get('order_num') or 0, x['menu_id'])) ordered_roots = biz_roots + hidden_roots reachable = collect_reachable(ch_map, roots) orphans = [m for m in menus if m['menu_id'] not in reachable] orphan_groups = defaultdict(list) for m in orphans: orphan_groups[m['parent_id']].append(m) for pid in orphan_groups: orphan_groups[pid].sort(key=lambda x: (x.get('order_num') or 0, x['menu_id'])) stats = defaultdict(int) for m in menus: stats[m['menu_type']] += 1 lines = [] w = lines.append w('# \u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355\u5b8c\u6574\u6811\uff08\u4e2d\u6587\uff09') w('') w('> \u6570\u636e\u6765\u6e90\uff1a`ylrz_saas.tenant_sys_menu` + adminUI \u89c6\u56fe/\u6743\u9650\u8865\u5168') w('> \u751f\u6210\u65e5\u671f\uff1a%s' % date.today().isoformat()) w('> \u603b\u8ba1\uff1a**%d** \u6761\uff08\u76ee\u5f55 %d / \u83dc\u5355 %d / \u6309\u94ae %d\uff09' % ( len(menus), stats['M'], stats['C'], stats['F'])) w('> \u6811\u4e2d\u53ef\u8fbe\uff1a**%d** \u6761\uff0c\u5b64\u7acb\u8282\u70b9\uff08parent_id \u65e0\u6548\uff09\uff1a**%d** \u6761' % ( len(reachable), len(orphans))) w('') w('## \u56fe\u4f8b') w('') w('- `[\u76ee\u5f55]` = M \u7c7b\u578b\uff0c\u9876\u680f/\u4fa7\u680f\u5206\u7ec4') w('- `[\u83dc\u5355]` = C \u7c7b\u578b\uff0c\u53ef\u8def\u7531\u9875\u9762') w('- `[\u6309\u94ae]` = F \u7c7b\u578b\uff0c\u9875\u9762\u5185\u6743\u9650\u6309\u94ae\uff08`perms`\uff09') w('- \u663e\u793a/\u9690\u85cf = visible 0/1') w('') w('---') w('') w('## \u5b8c\u6574\u6811\u72b6\u7ed3\u6784') w('') sec = 1 for root in ordered_roots: zh = zh_cache[root['menu_id']] w('### %d. %s' % (sec, zh)) w('') render_node(root, zh, 0, lines, ch_map, zh_cache, admin_titles, comp_zh) w('') sec += 1 if orphans: w('### %d. \u5b64\u7acb\u83dc\u5355\uff08parent_id \u5728\u5e93\u4e2d\u4e0d\u5b58\u5728\uff09' % sec) w('') w('> \u5171 **%d** \u6761\uff0c\u6309\u539f parent_id \u5206\u7ec4\u5c55\u793a\u3002\u5efa\u8bae\u540e\u7eed\u6570\u636e\u6e05\u7406\u65f6\u4fee\u590d parent_id \u6216\u5220\u9664\u5e9f\u5f03\u8282\u70b9\u3002' % len(orphans)) w('') for pid in sorted(orphan_groups.keys()): w('#### parent_id = %s\uff08\u65e0\u6548\uff09' % pid) w('') for node in orphan_groups[pid]: zh = zh_cache[node['menu_id']] render_node(node, zh, 0, lines, ch_map, zh_cache, admin_titles, comp_zh) w('') sec += 1 w('---') w('') w('## \u9644\u5f55\uff1a\u6309\u6a21\u5757\u7edf\u8ba1') w('') w('| \u9876\u7ea7\u6a21\u5757 | \u76ee\u5f55 | \u83dc\u5355 | \u6309\u94ae | \u5408\u8ba1 |') w('|----------|------|------|------|------|') def count_subtree(root_id): c = {'M': 0, 'C': 0, 'F': 0} seen = set() def walk(pid): if pid in seen: return seen.add(pid) for node in ch_map.get(pid, []): c[node['menu_type']] += 1 walk(node['menu_id']) walk(root_id) return c for root in biz_roots: c = count_subtree(root['menu_id']) total = sum(c.values()) - 1 # exclude self w('| %s | %d | %d | %d | %d |' % ( zh_cache[root['menu_id']], c['M'], c['C'], c['F'], total)) w('') w('*Generated by `sql/generate_menu_tree_zh.py`*') with open(OUT, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) print('Wrote', OUT, 'lines', len(lines)) if __name__ == '__main__': main()