| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- # -*- 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()
|