generate_menu_tree_zh.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. # -*- coding: utf-8 -*-
  2. """
  3. Generate complete Chinese menu tree (M/C/F) from tenant_sys_menu + view title hints.
  4. Output: adminUI_menu_tree_zh.md
  5. """
  6. import os
  7. import re
  8. from collections import defaultdict
  9. from datetime import date
  10. try:
  11. import pymysql
  12. except ImportError:
  13. pymysql = None
  14. ROOT = os.path.normpath(
  15. os.path.join(os.path.dirname(__file__), '..', '..', 'ylrz_saas_his_scrm_adminUI')
  16. )
  17. OUT = os.path.join(os.path.dirname(__file__), 'adminUI_menu_tree_zh.md')
  18. MENU_JS = os.path.join(ROOT, 'src', 'views', 'admin', 'menu.js')
  19. DB = dict(
  20. host='cq-cdb-8fjmemkb.sql.tencentcdb.com',
  21. port=27220,
  22. user='root',
  23. password='Ylrz_1q2w3e4r5t6y',
  24. database='ylrz_saas',
  25. charset='utf8mb4',
  26. )
  27. TYPE_LABEL = {'M': '\u76ee\u5f55', 'C': '\u83dc\u5355', 'F': '\u6309\u94ae'}
  28. # Root menu_id -> fixed Chinese name
  29. ROOT_ZH = {
  30. 32361: '\u4f01\u5fae\u7ba1\u7406', 32380: '\u5fae\u4fe1\u7ba1\u7406', 32347: 'CRM\u5ba2\u6237',
  31. 32357: '\u4f1a\u5458\u7ba1\u7406', 32351: '\u8bca\u6240\u7ba1\u7406', 32369: '\u5546\u57ce\u7ba1\u7406',
  32. 32353: '\u76f4\u64ad\u7ba1\u7406', 32345: '\u8bfe\u7a0b\u7ba1\u7406', 32348: 'AI\u804a\u5929',
  33. 32355: '\u9f99\u8678\u5f15\u64ce', 32331: '\u5e7f\u544a\u6295\u653e', 32372: '\u7cfb\u7edf\u7ba1\u7406',
  34. 32339: '\u8d22\u52a1\u7ba1\u7406', 32341: '\u65e5\u7a0b\u7ba1\u7406', 32368: '\u6570\u636e\u7edf\u8ba1',
  35. 32379: '\u76d1\u63a7\u7ba1\u7406', 35300: '\u5176\u4ed6', 32333: '\u5e73\u53f0\u7ba1\u7406\uff08\u5f52\u6863\uff09',
  36. 32644: '\u9996\u9875', 35100: '\u7ec4\u7ec7\u7ba1\u7406', 35101: '\u6743\u9650\u7ba1\u7406',
  37. 35102: '\u901a\u4fe1\u7ba1\u7406', 35105: '\u65e5\u5fd7\u7ba1\u7406', 35106: '\u7cfb\u7edf\u8bbe\u7f6e',
  38. 35001: '\u6d88\u606f\u7ba1\u7406', 35002: '\u5ba2\u6237\u7ba1\u7406', 35003: '\u7fa4\u804a\u7ba1\u7406',
  39. 35004: '\u670b\u53cb\u5708', 35005: '\u5f15\u6d41\u7ba1\u7406', 35006: '\u6807\u7b7e\u7ba1\u7406',
  40. 35007: '\u4f01\u5fae\u8bbe\u7f6e', 35040: '\u8ba2\u5355\u7ba1\u7406', 35041: '\u5546\u54c1\u7ba1\u7406',
  41. 35042: '\u95e8\u5e97\u8fd0\u8425', 35010: '\u5fae\u4fe1\u8d26\u53f7', 35011: '\u5fae\u4fe1\u5bf9\u8bdd',
  42. 35012: '\u5fae\u4fe1\u7528\u6237', 35013: '\u5fae\u4fe1\u7528\u6237\u7ec4', 35020: 'CRM\u5ba2\u6237\u7ba1\u7406',
  43. 35050: '\u76f4\u64ad\u8fd0\u8425', 35060: '\u8bfe\u7a0b\u5185\u5bb9', 35070: 'AI\u5bf9\u8bdd\u7ba1\u7406',
  44. 35080: '\u9f99\u8678\u5de5\u4f5c\u6d41', 35090: '\u6295\u653e\u8fd0\u8425',
  45. 35111: '\u5145\u503c\u7ba1\u7406', 35112: '\u6263\u8d39\u7ba1\u7406', 35113: '\u5206\u8d26\u7ba1\u7406',
  46. 35114: '\u8d44\u91d1\u65e5\u5fd7',
  47. 35201: '\u7528\u6237\u7ba1\u7406', 35202: '\u89d2\u8272\u7ba1\u7406', 35203: '\u83dc\u5355\u7ba1\u7406',
  48. 35204: '\u90e8\u95e8\u7ba1\u7406', 35205: '\u5c97\u4f4d\u7ba1\u7406', 35206: '\u5b57\u5178\u7ba1\u7406',
  49. 35207: '\u53c2\u6570\u8bbe\u7f6e', 35208: '\u901a\u77e5\u516c\u544a', 35209: '\u8fdd\u89c4\u8bcd\u8bed',
  50. }
  51. # component suffix / module -> Chinese
  52. COMP_ZH = {
  53. 'externalContact': '\u5916\u90e8\u8054\u7cfb\u4eba', 'QwWorkTask': '\u4f01\u5fae\u5de5\u4f5c\u4efb\u52a1',
  54. 'groupMsg': '\u7fa4\u6d88\u606f', 'contactWay': '\u6e20\u9053\u7801', 'friendCircle': '\u670b\u53cb\u5708',
  55. 'storeOrder': '\u8ba2\u5355\u7ba1\u7406', 'storeProduct': '\u5546\u54c1\u7ba1\u7406', 'storeShop': '\u95e8\u5e97\u7ba1\u7406',
  56. 'liveConsole': '\u76f4\u64ad\u63a7\u5236\u53f0', 'liveOrder': '\u76f4\u64ad\u8ba2\u5355', 'videoResource': '\u89c6\u9891\u8d44\u6e90',
  57. 'userCourse': '\u7528\u6237\u8bfe\u7a0b', 'fastGptRole': 'AI\u89d2\u8272', 'fastGptDataset': 'AI\u77e5\u8bc6\u5e93',
  58. 'companyUser': '\u4f01\u4e1a\u7528\u6237', 'companyRole': '\u4f01\u4e1a\u89d2\u8272', 'companyMenu': '\u4f01\u4e1a\u83dc\u5355',
  59. 'companyDept': '\u4f01\u4e1a\u90e8\u95e8', 'companyRecharge': '\u5145\u503c\u8bb0\u5f55',
  60. 'companyProfit': '\u5206\u8d26\u8bb0\u5f55', 'companyMoneyLogs': '\u8d44\u91d1\u6d41\u6c34',
  61. 'sysCompany': '\u79df\u6237\u7ba1\u7406', 'tenantMenu': '\u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355',
  62. 'operlog': '\u64cd\u4f5c\u65e5\u5fd7', 'logininfor': '\u767b\u5f55\u65e5\u5fd7', 'workflow-generate': 'AI\u751f\u6210\u5de5\u4f5c\u6d41',
  63. 'dead-letter': '\u6b7b\u4fe1\u961f\u5217', 'workflow-canvas': '\u5de5\u4f5c\u6d41\u753b\u5e03',
  64. }
  65. WORD_ZH = {
  66. 'user': '\u7528\u6237', 'users': '\u7528\u6237', 'role': '\u89d2\u8272', 'menu': '\u83dc\u5355',
  67. 'dept': '\u90e8\u95e8', 'post': '\u5c97\u4f4d', 'dict': '\u5b57\u5178', 'config': '\u914d\u7f6e',
  68. 'notice': '\u516c\u544a', 'keyword': '\u5173\u952e\u8bcd', 'order': '\u8ba2\u5355', 'store': '\u5546\u57ce',
  69. 'product': '\u5546\u54c1', 'customer': '\u5ba2\u6237', 'company': '\u4f01\u4e1a', 'tenant': '\u79df\u6237',
  70. 'proxy': '\u4ee3\u7406', 'admin': '\u5e73\u53f0', 'live': '\u76f4\u64ad', 'course': '\u8bfe\u7a0b',
  71. 'doctor': '\u533b\u751f', 'patient': '\u60a3\u8005', 'inquiry': '\u95ee\u8bca', 'statistics': '\u7edf\u8ba1',
  72. 'report': '\u62a5\u8868', 'log': '\u65e5\u5fd7', 'logs': '\u65e5\u5fd7', 'record': '\u8bb0\u5f55',
  73. 'tag': '\u6807\u7b7e', 'group': '\u7fa4', 'msg': '\u6d88\u606f', 'voice': '\u8bed\u97f3', 'sms': '\u77ed\u4fe1',
  74. 'coupon': '\u4f18\u6263\u5238', 'export': '\u5bfc\u51fa', 'import': '\u5bfc\u5165', 'package': '\u5957\u9910',
  75. 'wallet': '\u94b1\u5305', 'bill': '\u8d26\u5355', 'calendar': '\u65e5\u7a0b', 'monitor': '\u76d1\u63a7',
  76. 'watch': '\u76d1\u63a7', 'material': '\u7d20\u6750', 'welcome': '\u6b22\u8fce\u8bed', 'sop': 'SOP',
  77. 'advertiser': '\u5e7f\u544a\u4e3b', 'channel': '\u6e20\u9053', 'domain': '\u57df\u540d', 'site': '\u7ad9\u70b9',
  78. 'recharge': '\u5145\u503c', 'deduct': '\u6263\u8d39', 'profit': '\u5206\u8d26', 'withdraw': '\u63d0\u73b0',
  79. 'article': '\u6587\u7ae0', 'video': '\u89c6\u9891', 'comment': '\u8bc4\u8bba', 'question': '\u95ee\u9898',
  80. 'answer': '\u7b54\u6848', 'schedule': '\u6392\u73ed', 'traffic': '\u6d41\u91cf', 'workflow': '\u5de5\u4f5c\u6d41',
  81. 'lobster': '\u9f99\u8678', 'prompt': '\u63d0\u793a\u8bcd', 'instance': '\u5b9e\u4f8b', 'template': '\u6a21\u677f',
  82. 'shipping': '\u8fd0\u8d39', 'prescribe': '\u5904\u65b9', 'integral': '\u79ef\u5206', 'member': '\u4f1a\u5458',
  83. 'blacklist': '\u9ed1\u540d\u5355', 'complaint': '\u6295\u8bc9', 'transfer': '\u8f6c\u79fb', 'external': '\u5916\u90e8',
  84. 'contact': '\u8054\u7cfb\u4eba', 'qw': '\u4f01\u5fae', 'wx': '\u5fae\u4fe1', 'crm': 'CRM', 'his': '\u8bca\u6240',
  85. 'adv': '\u5e7f\u544a', 'ad': '\u5e7f\u544a', 'tool': '\u5de5\u5177', 'gen': '\u4ee3\u7801\u751f\u6210',
  86. 'job': '\u5b9a\u65f6\u4efb\u52a1', 'online': '\u5728\u7ebf\u7528\u6237', 'cache': '\u7f13\u5b58', 'server': '\u670d\u52a1\u5668',
  87. 'druid': '\u6570\u636e\u76d1\u63a7', 'iot': '\u7269\u8054\u7f51', 'device': '\u8bbe\u5907', 'index': '\u9996\u9875',
  88. 'list': '\u5217\u8868', 'manage': '\u7ba1\u7406', 'setting': '\u8bbe\u7f6e', 'info': '\u4fe1\u606f',
  89. 'detail': '\u8be6\u60c5', 'data': '\u6570\u636e', 'temp': '\u6a21\u677f', 'api': 'API', 'Actual': '\u5b9e\u9645', 'Statistic': '\u7edf\u8ba1', 'OnJob': '\u5728\u804c',
  90. 'Live': '\u76f4\u64ad', 'Code': '\u7801', 'Circle': '\u5708', 'Task': '\u4efb\u52a1',
  91. 'Comments': '\u8bc4\u8bba', 'Customer': '\u5ba2\u6237', 'Item': '\u660e\u7ec6',
  92. 'Advertising': '\u5e7f\u544a', 'Apply': '\u7533\u8bf7', 'Ipad': 'iPad',
  93. 'Behavior': '\u884c\u4e3a', 'Push': '\u63a8\u9001', 'Count': '\u7edf\u8ba1',
  94. 'Assign': '\u5206\u914d', 'assign': '\u5206\u914d', 'Rule': '\u89c4\u5219', 'Batch': '\u6279\u6b21',
  95. 'Way': '\u65b9\u5f0f', 'Link': '\u94fe\u63a5', 'Drainage': '\u5f15\u6d41', 'drainage': '\u5f15\u6d41',
  96. 'Loss': '\u6d41\u5931', 'Stage': '\u9636\u6bb5', 'Transfer': '\u8f6c\u79fb',
  97. 'Audit': '\u5ba1\u6838', 'Unassigned': '\u672a\u5206\u914d', 'AfterSales': '\u552e\u540e',
  98. 'Promotion': '\u63a8\u5e7f', 'Health': '\u5065\u5eb7', 'Inquiry': '\u95ee\u8bca',
  99. 'Category': '\u5206\u7c7b', 'DarkRoom': '\u5c0f\u9ed1\u5c4b', 'Recharge': '\u5145\u503c',
  100. 'Template': '\u6a21\u677f', 'Follow': '\u968f\u8bbf', 'Audit': '\u5ba1\u6838',
  101. 'Offline': '\u7ebf\u4e0b', 'Audit': '\u5ba1\u6838', 'Staff': '\u5458\u5de5',
  102. 'Cart': '\u8d2d\u7269\u8f66', 'Visit': '\u8bbf\u95ee', 'Relation': '\u5173\u8054',
  103. 'Reply': '\u56de\u590d', 'Attr': '\u5c5e\u6027', 'Details': '\u8be6\u60c5',
  104. 'Category': '\u5206\u7c7b', 'Group': '\u5206\u7ec4', 'Console': '\u63a7\u5236\u53f0',
  105. 'Config': '\u914d\u7f6e', 'Coupon': '\u4f18\u6263\u5238', 'Question': '\u95ee\u9898',
  106. 'Reward': '\u5956\u52b1', 'Favorite': '\u6536\u85cf', 'Watch': '\u89c2\u770b',
  107. 'Talent': '\u8fbe\u4eba', 'Training': '\u57f9\u8bad', 'Camp': '\u8425',
  108. 'Material': '\u7d20\u6750', 'Period': '\u671f\u6570', 'Resource': '\u8d44\u6e90',
  109. 'Finish': '\u5b8c\u7ed3', 'Temp': '\u6a21\u677f', 'Bank': '\u9898\u5e93',
  110. 'Answer': '\u7b54\u6848', 'RedPacket': '\u7ea2\u5305', 'Traffic': '\u6d41\u91cf',
  111. 'Collection': '\u6536\u85cf', 'Dataset': '\u77e5\u8bc6\u5e93', 'Session': '\u4f1a\u8bdd',
  112. 'Keyword': '\u5173\u952e\u8bcd', 'Role': '\u89d2\u8272', 'Replace': '\u66ff\u6362',
  113. 'Words': '\u8bcd\u6761', 'Quality': '\u8d28\u68c0', 'Gateway': '\u7f51\u5173',
  114. 'Sip': 'SIP', 'Call': '\u547c\u53eb', 'Client': '\u5ba2\u6237\u7aef',
  115. 'Offline': '\u7ebf\u4e0b', 'Item': '\u660e\u7ec6',
  116. }
  117. PERM_ACTION_ZH = {
  118. 'add': '\u65b0\u589e', 'edit': '\u4fee\u6539', 'update': '\u4fee\u6539', 'remove': '\u5220\u9664',
  119. 'delete': '\u5220\u9664', 'query': '\u67e5\u8be2', 'list': '\u5217\u8868', 'export': '\u5bfc\u51fa',
  120. 'import': '\u5bfc\u5165', 'view': '\u67e5\u770b', 'detail': '\u8be6\u60c5', 'reset': '\u91cd\u7f6e',
  121. 'auth': '\u6388\u6743', 'assign': '\u5206\u914d', 'changeStatus': '\u72b6\u6001\u53d8\u66f4',
  122. 'audit': '\u5ba1\u6838', 'approve': '\u5ba1\u6279', 'reject': '\u9a73\u56de', 'sync': '\u540c\u6b65',
  123. 'refresh': '\u5237\u65b0', 'execute': '\u6267\u884c', 'cancel': '\u53d6\u6d88', 'submit': '\u63d0\u4ea4',
  124. 'publish': '\u53d1\u5e03', 'copy': '\u590d\u5236', 'download': '\u4e0b\u8f7d', 'upload': '\u4e0a\u4f20',
  125. 'bind': '\u7ed1\u5b9a', 'unbind': '\u89e3\u7ed1', 'enable': '\u542f\u7528', 'disable': '\u505c\u7528',
  126. }
  127. def has_chinese(s):
  128. return bool(s and re.search(r'[\u4e00-\u9fff]', s))
  129. def split_camel(s):
  130. parts = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', s or '')
  131. parts = re.sub(r'[-_/]', ' ', parts)
  132. return [p for p in parts.split() if p]
  133. def translate_tokens(text):
  134. if not text:
  135. return ''
  136. out = []
  137. for tok in split_camel(text):
  138. low = tok.lower()
  139. if tok in WORD_ZH:
  140. out.append(WORD_ZH[tok])
  141. elif low in WORD_ZH:
  142. out.append(WORD_ZH[low])
  143. elif tok in COMP_ZH:
  144. out.append(COMP_ZH[tok])
  145. elif re.match(r'^[A-Z][a-z]+', tok):
  146. out.append(translate_tokens(tok[0].lower() + tok[1:]) or tok)
  147. else:
  148. out.append(tok)
  149. return ''.join(out) if out else text
  150. def load_admin_titles():
  151. titles = {}
  152. if not os.path.exists(MENU_JS):
  153. return titles
  154. with open(MENU_JS, 'r', encoding='utf-8') as f:
  155. content = f.read()
  156. for m in re.finditer(
  157. r"import\('@/views/([^']+)'\)[^}]*title:\s*'([^']+)'", content
  158. ):
  159. comp, title = m.group(1).replace('.vue', ''), m.group(2)
  160. if comp.endswith('/index'):
  161. comp = comp[:-6]
  162. titles[comp] = title
  163. return titles
  164. def load_menus():
  165. conn = pymysql.connect(**DB)
  166. cur = conn.cursor(pymysql.cursors.DictCursor)
  167. cur.execute(
  168. 'SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, perms '
  169. 'FROM tenant_sys_menu ORDER BY parent_id, order_num, menu_id'
  170. )
  171. rows = cur.fetchall()
  172. cur.close()
  173. conn.close()
  174. return rows
  175. def comp_key(component):
  176. if not component:
  177. return ''
  178. c = component.replace('/index/index', '/index').replace('/index', '')
  179. return c.strip('/')
  180. def to_chinese(menu, admin_titles, comp_zh=None, parent_zh=''):
  181. comp_zh = comp_zh or COMP_ZH
  182. mid = menu['menu_id']
  183. if mid in ROOT_ZH:
  184. return ROOT_ZH[mid]
  185. name = (menu.get('menu_name') or '').strip()
  186. if has_chinese(name) and not re.match(r'^[a-zA-Z\s]+$', name):
  187. # clean mixed English-only names
  188. if not re.fullmatch(r'[A-Za-z0-9\s\-_]+', name):
  189. return name
  190. comp = comp_key(menu.get('component') or '')
  191. if comp in admin_titles:
  192. return admin_titles[comp]
  193. if comp in comp_zh:
  194. return comp_zh[comp]
  195. # component last segment
  196. if comp:
  197. seg = comp.split('/')[-1]
  198. if seg in comp_zh:
  199. return comp_zh[seg]
  200. zh = translate_tokens(seg)
  201. if zh and zh != seg:
  202. return zh
  203. # two segments
  204. if '/' in comp:
  205. zh2 = translate_tokens(comp.split('/')[-2] + seg)
  206. if has_chinese(zh2):
  207. return zh2
  208. if menu['menu_type'] == 'F':
  209. perms = menu.get('perms') or ''
  210. if perms:
  211. parts = perms.split(':')
  212. action = parts[-1] if parts else ''
  213. act_zh = PERM_ACTION_ZH.get(action, PERM_ACTION_ZH.get(action.lower(), ''))
  214. if act_zh:
  215. if parent_zh and parent_zh not in ('\u6309\u94ae', act_zh):
  216. return parent_zh + act_zh
  217. if len(parts) >= 2:
  218. res = translate_tokens(parts[-2])
  219. if has_chinese(res):
  220. return res + act_zh
  221. return act_zh
  222. if name and name != '#':
  223. zh = translate_tokens(name)
  224. return zh if has_chinese(zh) or zh != name else (act_zh or '\u6309\u94ae')
  225. if name:
  226. zh = translate_tokens(name.replace(' ', ''))
  227. if has_chinese(zh):
  228. return zh
  229. # title case words
  230. if ' ' in name:
  231. parts = [WORD_ZH.get(w.lower(), w) for w in name.split()]
  232. joined = ''.join(parts)
  233. if has_chinese(joined):
  234. return joined
  235. path = menu.get('path') or ''
  236. if path and path != '#':
  237. zh = translate_tokens(path)
  238. if has_chinese(zh):
  239. return zh
  240. return name or comp or ('\u672a\u547d\u540d\u83dc\u5355%d' % mid)
  241. def build_children_map(menus):
  242. ch = defaultdict(list)
  243. by_id = {}
  244. for m in menus:
  245. by_id[m['menu_id']] = m
  246. ch[m['parent_id']].append(m)
  247. for pid in ch:
  248. ch[pid].sort(key=lambda x: (x.get('order_num') or 0, x['menu_id']))
  249. return ch, by_id
  250. def build_comp_zh_from_db(menus):
  251. comp_zh = dict(COMP_ZH)
  252. for m in menus:
  253. if m['menu_type'] not in ('C', 'M'):
  254. continue
  255. name = (m.get('menu_name') or '').strip()
  256. comp = comp_key(m.get('component') or '')
  257. if comp and has_chinese(name) and not re.fullmatch(r'[A-Za-z0-9\s\-_:.]+', name):
  258. comp_zh[comp] = name
  259. seg = comp.split('/')[-1]
  260. if seg and has_chinese(name):
  261. comp_zh[seg] = name
  262. return comp_zh
  263. def collect_reachable(ch_map, roots):
  264. seen = set()
  265. stack = [r['menu_id'] for r in roots]
  266. while stack:
  267. mid = stack.pop()
  268. if mid in seen:
  269. continue
  270. seen.add(mid)
  271. for child in ch_map.get(mid, []):
  272. stack.append(child['menu_id'])
  273. return seen
  274. def render_node(menu, zh_name, depth, lines, ch_map, zh_cache, admin_titles, comp_zh):
  275. mtype = menu['menu_type']
  276. label = TYPE_LABEL.get(mtype, mtype)
  277. vis = '\u9690\u85cf' if menu.get('visible') == '1' else '\u663e\u793a'
  278. comp = menu.get('component') or '-'
  279. perms = menu.get('perms') or '-'
  280. mid = menu['menu_id']
  281. indent = ' ' * depth
  282. if mtype == 'F':
  283. lines.append('%s- \u2514\u2500 [%s] **%s** `perms=%s`' % (indent, label, zh_name, perms))
  284. else:
  285. lines.append('%s- [%s] **%s** `id=%s` path=`%s` component=`%s` %s' % (
  286. indent, label, zh_name, mid, menu.get('path') or '', comp, vis))
  287. for child in ch_map.get(mid, []):
  288. if child['menu_id'] not in zh_cache:
  289. parent_name = zh_name if mtype == 'C' else ''
  290. zh_cache[child['menu_id']] = to_chinese(
  291. child, admin_titles, comp_zh, parent_zh=parent_name if child['menu_type'] == 'F' else '')
  292. render_node(child, zh_cache[child['menu_id']], depth + 1, lines, ch_map, zh_cache, admin_titles, comp_zh)
  293. def main():
  294. menus = load_menus()
  295. admin_titles = load_admin_titles()
  296. comp_zh = build_comp_zh_from_db(menus)
  297. ch_map, by_id = build_children_map(menus)
  298. zh_cache = {m['menu_id']: to_chinese(m, admin_titles, comp_zh) for m in menus}
  299. roots = ch_map.get(0, [])
  300. biz_roots = [r for r in roots if r['menu_id'] != 32333 and r.get('visible') == '0']
  301. hidden_roots = [r for r in roots if r not in biz_roots]
  302. biz_roots.sort(key=lambda x: (x.get('order_num') or 0, x['menu_id']))
  303. hidden_roots.sort(key=lambda x: (x.get('order_num') or 0, x['menu_id']))
  304. ordered_roots = biz_roots + hidden_roots
  305. reachable = collect_reachable(ch_map, roots)
  306. orphans = [m for m in menus if m['menu_id'] not in reachable]
  307. orphan_groups = defaultdict(list)
  308. for m in orphans:
  309. orphan_groups[m['parent_id']].append(m)
  310. for pid in orphan_groups:
  311. orphan_groups[pid].sort(key=lambda x: (x.get('order_num') or 0, x['menu_id']))
  312. stats = defaultdict(int)
  313. for m in menus:
  314. stats[m['menu_type']] += 1
  315. lines = []
  316. w = lines.append
  317. w('# \u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355\u5b8c\u6574\u6811\uff08\u4e2d\u6587\uff09')
  318. w('')
  319. w('> \u6570\u636e\u6765\u6e90\uff1a`ylrz_saas.tenant_sys_menu` + adminUI \u89c6\u56fe/\u6743\u9650\u8865\u5168')
  320. w('> \u751f\u6210\u65e5\u671f\uff1a%s' % date.today().isoformat())
  321. w('> \u603b\u8ba1\uff1a**%d** \u6761\uff08\u76ee\u5f55 %d / \u83dc\u5355 %d / \u6309\u94ae %d\uff09' % (
  322. len(menus), stats['M'], stats['C'], stats['F']))
  323. w('> \u6811\u4e2d\u53ef\u8fbe\uff1a**%d** \u6761\uff0c\u5b64\u7acb\u8282\u70b9\uff08parent_id \u65e0\u6548\uff09\uff1a**%d** \u6761' % (
  324. len(reachable), len(orphans)))
  325. w('')
  326. w('## \u56fe\u4f8b')
  327. w('')
  328. w('- `[\u76ee\u5f55]` = M \u7c7b\u578b\uff0c\u9876\u680f/\u4fa7\u680f\u5206\u7ec4')
  329. w('- `[\u83dc\u5355]` = C \u7c7b\u578b\uff0c\u53ef\u8def\u7531\u9875\u9762')
  330. w('- `[\u6309\u94ae]` = F \u7c7b\u578b\uff0c\u9875\u9762\u5185\u6743\u9650\u6309\u94ae\uff08`perms`\uff09')
  331. w('- \u663e\u793a/\u9690\u85cf = visible 0/1')
  332. w('')
  333. w('---')
  334. w('')
  335. w('## \u5b8c\u6574\u6811\u72b6\u7ed3\u6784')
  336. w('')
  337. sec = 1
  338. for root in ordered_roots:
  339. zh = zh_cache[root['menu_id']]
  340. w('### %d. %s' % (sec, zh))
  341. w('')
  342. render_node(root, zh, 0, lines, ch_map, zh_cache, admin_titles, comp_zh)
  343. w('')
  344. sec += 1
  345. if orphans:
  346. w('### %d. \u5b64\u7acb\u83dc\u5355\uff08parent_id \u5728\u5e93\u4e2d\u4e0d\u5b58\u5728\uff09' % sec)
  347. w('')
  348. 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))
  349. w('')
  350. for pid in sorted(orphan_groups.keys()):
  351. w('#### parent_id = %s\uff08\u65e0\u6548\uff09' % pid)
  352. w('')
  353. for node in orphan_groups[pid]:
  354. zh = zh_cache[node['menu_id']]
  355. render_node(node, zh, 0, lines, ch_map, zh_cache, admin_titles, comp_zh)
  356. w('')
  357. sec += 1
  358. w('---')
  359. w('')
  360. w('## \u9644\u5f55\uff1a\u6309\u6a21\u5757\u7edf\u8ba1')
  361. w('')
  362. w('| \u9876\u7ea7\u6a21\u5757 | \u76ee\u5f55 | \u83dc\u5355 | \u6309\u94ae | \u5408\u8ba1 |')
  363. w('|----------|------|------|------|------|')
  364. def count_subtree(root_id):
  365. c = {'M': 0, 'C': 0, 'F': 0}
  366. seen = set()
  367. def walk(pid):
  368. if pid in seen:
  369. return
  370. seen.add(pid)
  371. for node in ch_map.get(pid, []):
  372. c[node['menu_type']] += 1
  373. walk(node['menu_id'])
  374. walk(root_id)
  375. return c
  376. for root in biz_roots:
  377. c = count_subtree(root['menu_id'])
  378. total = sum(c.values()) - 1 # exclude self
  379. w('| %s | %d | %d | %d | %d |' % (
  380. zh_cache[root['menu_id']], c['M'], c['C'], c['F'], total))
  381. w('')
  382. w('*Generated by `sql/generate_menu_tree_zh.py`*')
  383. with open(OUT, 'w', encoding='utf-8') as f:
  384. f.write('\n'.join(lines))
  385. print('Wrote', OUT, 'lines', len(lines))
  386. if __name__ == '__main__':
  387. main()