# -*- coding: utf-8 -*- r"""票 06 续跑步骤 4:对账 verify(HANDOFF-2026-09-04 §3.4)。 - 正则扫 9 个 crm-customer Controller + crm-rule CustomerReminderRuleController 的 class @RequestMapping + method @XxxMapping,得 (METHOD, path) 钥匙集(应 57) - 与生成 .bru 的 docs 首行钥匙 `METHOD path` 双向比对,全命中收口(防笔误漏端点) - crm-preference 6 端点不生成副本(源码 tag 挂 A3),不参与本对账 """ import io, sys, os, re sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') WS = r'd:\code\crm-backend-matt' CONTROLLERS = [ WS + r'\crm-customer\src\main\java\com\crm\customer\controller\%s' % n for n in ('CustomerController.java', 'CustomerWorkspaceController.java', 'CustomerOwnershipController.java', 'CustomerDetailController.java', 'CustomerContactController.java', 'CustomerFocusController.java', 'CustomerTransferController.java', 'CustomerImportController.java', 'CustomerPingController.java') ] + [WS + r'\crm-rule\src\main\java\com\crm\rule\controller\CustomerReminderRuleController.java'] DOCS = r'D:\code\crm-api-docs' SCAN_ROOTS = [os.path.join(DOCS, 'A4 客户管理'), os.path.join(DOCS, '客户管理')] # ---- 源码钥匙集 ---- re_class = re.compile(r'@RequestMapping\(\s*"([^"]+)"') re_method = re.compile(r'@(Get|Post|Put|Delete)Mapping(?:\(\s*(?:"([^"]*)")?[^)]*\))?') src_keys = set() for f in CONTROLLERS: txt = open(f, encoding='utf-8').read() m = re_class.search(txt) base = m.group(1) if m else '' for mm in re_method.finditer(txt): verb = mm.group(1).upper() sub = mm.group(2) or '' path = (base + sub) if not sub.startswith(base) else sub path = re.sub(r'/{2,}', '/', path) src_keys.add(f'{verb} {path}') print(f'source keys: {len(src_keys)} (expect 57)') # ---- .bru 钥匙集 ---- re_key = re.compile(r'`\s*(GET|POST|PUT|DELETE) (/\S+?)`') bru_keys = {} for root in SCAN_ROOTS: for dp, dn, fn in os.walk(root): for f in fn: if not f.endswith('.bru') or f == 'folder.bru': continue p = os.path.join(dp, f) txt = open(p, encoding='utf-8').read() m = re_key.search(txt) if not m: print(f' !! no key line: {os.path.relpath(p, DOCS)}') continue bru_keys.setdefault(f'{m.group(1)} {m.group(2)}', []).append(os.path.relpath(p, DOCS)) print(f'bru keys: {len(bru_keys)} (expect 57)') missing = sorted(src_keys - set(bru_keys)) extra = sorted(k for k in bru_keys if k not in src_keys) dupes = {k: v for k, v in bru_keys.items() if len(v) > 1} print(f'missing in bru (source has, doc lacks): {len(missing)}') for k in missing: print(' -', k) print(f'extra in bru (doc has, source lacks): {len(extra)}') for k in extra: print(' -', k, '->', bru_keys[k]) print(f'duplicate keys: {len(dupes)}') for k, v in dupes.items(): print(' -', k, '->', v) ok = not missing and not extra and not dupes and len(src_keys) == 57 and len(bru_keys) == 57 print('VERIFY:', 'PASS ✅ 全命中收口' if ok else 'FAIL ❌')