| 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': '目录', 'C': '菜单', 'F': '按钮'}
- # Root menu_id -> fixed Chinese name
- ROOT_ZH = {
- 32361: '企微管理', 32380: '微信管理', 32347: 'CRM客户',
- 32357: '会员管理', 32351: '诊所管理', 32369: '商城管理',
- 32353: '直播管理', 32345: '课程管理', 32348: 'AI聊天',
- 32355: '龙虸引擎', 32331: '广告投放', 32372: '系统管理',
- 32339: '财务管理', 32341: '日程管理', 32368: '数据统计',
- 32379: '监控管理', 35300: '其他', 32333: '平台管理(归档)',
- 32644: '首页', 35100: '组织管理', 35101: '权限管理',
- 35102: '通信管理', 35105: '日志管理', 35106: '系统设置',
- 35001: '消息管理', 35002: '客户管理', 35003: '群聊管理',
- 35004: '朋友圈', 35005: '引流管理', 35006: '标签管理',
- 35007: '企微设置', 35040: '订单管理', 35041: '商品管理',
- 35042: '门店运营', 35010: '微信账号', 35011: '微信对话',
- 35012: '微信用户', 35013: '微信用户组', 35020: 'CRM客户管理',
- 35050: '直播运营', 35060: '课程内容', 35070: 'AI对话管理',
- 35080: '龙虸工作流', 35090: '投放运营',
- 35111: '充值管理', 35112: '扣费管理', 35113: '分账管理',
- 35114: '资金日志',
- 35201: '用户管理', 35202: '角色管理', 35203: '菜单管理',
- 35204: '部门管理', 35205: '岗位管理', 35206: '字典管理',
- 35207: '参数设置', 35208: '通知公告', 35209: '违规词语',
- }
- # component suffix / module -> Chinese
- COMP_ZH = {
- 'externalContact': '外部联系人', 'QwWorkTask': '企微工作任务',
- 'groupMsg': '群消息', 'contactWay': '渠道码', 'friendCircle': '朋友圈',
- 'storeOrder': '订单管理', 'storeProduct': '商品管理', 'storeShop': '门店管理',
- 'liveConsole': '直播控制台', 'liveOrder': '直播订单', 'videoResource': '视频资源',
- 'userCourse': '用户课程', 'fastGptRole': 'AI角色', 'fastGptDataset': 'AI知识库',
- 'companyUser': '企业用户', 'companyRole': '企业角色', 'companyMenu': '企业菜单',
- 'companyDept': '企业部门', 'companyRecharge': '充值记录',
- 'companyProfit': '分账记录', 'companyMoneyLogs': '资金流水',
- 'sysCompany': '租户管理', 'tenantMenu': '租户管理端菜单',
- 'operlog': '操作日志', 'logininfor': '登录日志', 'workflow-generate': 'AI生成工作流',
- 'dead-letter': '死信队列', 'workflow-canvas': '工作流画布',
- }
- WORD_ZH = {
- 'user': '用户', 'users': '用户', 'role': '角色', 'menu': '菜单',
- 'dept': '部门', 'post': '岗位', 'dict': '字典', 'config': '配置',
- 'notice': '公告', 'keyword': '关键词', 'order': '订单', 'store': '商城',
- 'product': '商品', 'customer': '客户', 'company': '企业', 'tenant': '租户',
- 'proxy': '代理', 'admin': '平台', 'live': '直播', 'course': '课程',
- 'doctor': '医生', 'patient': '患者', 'inquiry': '问诊', 'statistics': '统计',
- 'report': '报表', 'log': '日志', 'logs': '日志', 'record': '记录',
- 'tag': '标签', 'group': '群', 'msg': '消息', 'voice': '语音', 'sms': '短信',
- 'coupon': '优扣券', 'export': '导出', 'import': '导入', 'package': '套餐',
- 'wallet': '钱包', 'bill': '账单', 'calendar': '日程', 'monitor': '监控',
- 'watch': '监控', 'material': '素材', 'welcome': '欢迎语', 'sop': 'SOP',
- 'advertiser': '广告主', 'channel': '渠道', 'domain': '域名', 'site': '站点',
- 'recharge': '充值', 'deduct': '扣费', 'profit': '分账', 'withdraw': '提现',
- 'article': '文章', 'video': '视频', 'comment': '评论', 'question': '问题',
- 'answer': '答案', 'schedule': '排班', 'traffic': '流量', 'workflow': '工作流',
- 'lobster': '龙虸', 'prompt': '提示词', 'instance': '实例', 'template': '模板',
- 'shipping': '运费', 'prescribe': '处方', 'integral': '积分', 'member': '会员',
- 'blacklist': '黑名单', 'complaint': '投诉', 'transfer': '转移', 'external': '外部',
- 'contact': '联系人', 'qw': '企微', 'wx': '微信', 'crm': 'CRM', 'his': '诊所',
- 'adv': '广告', 'ad': '广告', 'tool': '工具', 'gen': '代码生成',
- 'job': '定时任务', 'online': '在线用户', 'cache': '缓存', 'server': '服务器',
- 'druid': '数据监控', 'iot': '物联网', 'device': '设备', 'index': '首页',
- 'list': '列表', 'manage': '管理', 'setting': '设置', 'info': '信息',
- 'detail': '详情', 'data': '数据', 'temp': '模板', 'api': 'API', 'Actual': '实际', 'Statistic': '统计', 'OnJob': '在职',
- 'Live': '直播', 'Code': '码', 'Circle': '圈', 'Task': '任务',
- 'Comments': '评论', 'Customer': '客户', 'Item': '明细',
- 'Advertising': '广告', 'Apply': '申请', 'Ipad': 'iPad',
- 'Behavior': '行为', 'Push': '推送', 'Count': '统计',
- 'Assign': '分配', 'assign': '分配', 'Rule': '规则', 'Batch': '批次',
- 'Way': '方式', 'Link': '链接', 'Drainage': '引流', 'drainage': '引流',
- 'Loss': '流失', 'Stage': '阶段', 'Transfer': '转移',
- 'Audit': '审核', 'Unassigned': '未分配', 'AfterSales': '售后',
- 'Promotion': '推广', 'Health': '健康', 'Inquiry': '问诊',
- 'Category': '分类', 'DarkRoom': '小黑屋', 'Recharge': '充值',
- 'Template': '模板', 'Follow': '随访', 'Audit': '审核',
- 'Offline': '线下', 'Audit': '审核', 'Staff': '员工',
- 'Cart': '购物车', 'Visit': '访问', 'Relation': '关联',
- 'Reply': '回复', 'Attr': '属性', 'Details': '详情',
- 'Category': '分类', 'Group': '分组', 'Console': '控制台',
- 'Config': '配置', 'Coupon': '优扣券', 'Question': '问题',
- 'Reward': '奖励', 'Favorite': '收藏', 'Watch': '观看',
- 'Talent': '达人', 'Training': '培训', 'Camp': '营',
- 'Material': '素材', 'Period': '期数', 'Resource': '资源',
- 'Finish': '完结', 'Temp': '模板', 'Bank': '题库',
- 'Answer': '答案', 'RedPacket': '红包', 'Traffic': '流量',
- 'Collection': '收藏', 'Dataset': '知识库', 'Session': '会话',
- 'Keyword': '关键词', 'Role': '角色', 'Replace': '替换',
- 'Words': '词条', 'Quality': '质检', 'Gateway': '网关',
- 'Sip': 'SIP', 'Call': '呼叫', 'Client': '客户端',
- 'Offline': '线下', 'Item': '明细',
- }
- PERM_ACTION_ZH = {
- 'add': '新增', 'edit': '修改', 'update': '修改', 'remove': '删除',
- 'delete': '删除', 'query': '查询', 'list': '列表', 'export': '导出',
- 'import': '导入', 'view': '查看', 'detail': '详情', 'reset': '重置',
- 'auth': '授权', 'assign': '分配', 'changeStatus': '状态变更',
- 'audit': '审核', 'approve': '审批', 'reject': '驳回', 'sync': '同步',
- 'refresh': '刷新', 'execute': '执行', 'cancel': '取消', 'submit': '提交',
- 'publish': '发布', 'copy': '复制', 'download': '下载', 'upload': '上传',
- 'bind': '绑定', 'unbind': '解绑', 'enable': '启用', 'disable': '停用',
- }
- def has_chinese(s):
- return bool(s and re.search(r'[一-鿿]', 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 ('按钮', 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 '按钮')
- 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 ('未命名菜单%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 = '隐藏' if menu.get('visible') == '1' else '显示'
- 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('# 租户管理端菜单完整树(中文)')
- w('')
- w('> 数据来源:`ylrz_saas.tenant_sys_menu` + adminUI 视图/权限补全')
- w('> 生成日期:%s' % date.today().isoformat())
- w('> 总计:**%d** 条(目录 %d / 菜单 %d / 按钮 %d)' % (
- len(menus), stats['M'], stats['C'], stats['F']))
- w('> 树中可达:**%d** 条,孤立节点(parent_id 无效):**%d** 条' % (
- len(reachable), len(orphans)))
- w('')
- w('## 图例')
- w('')
- w('- `[目录]` = M 类型,顶栏/侧栏分组')
- w('- `[菜单]` = C 类型,可路由页面')
- w('- `[按钮]` = F 类型,页面内权限按钮(`perms`)')
- w('- 显示/隐藏 = visible 0/1')
- w('')
- w('---')
- w('')
- w('## 完整树状结构')
- 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. 孤立菜单(parent_id 在库中不存在)' % sec)
- w('')
- w('> 共 **%d** 条,按原 parent_id 分组展示。建议后续数据清理时修复 parent_id 或删除废弃节点。' % len(orphans))
- w('')
- for pid in sorted(orphan_groups.keys()):
- w('#### parent_id = %s(无效)' % 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('## 附录:按模块统计')
- w('')
- w('| 顶级模块 | 目录 | 菜单 | 按钮 | 合计 |')
- 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()
|