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