# -*- coding: utf-8 -*- """票 11 t11-5 — verify_sync.py:bruno-sync 口径对账验证(三向差集)。 1. 源码侧:正则扫 crm-opportunity 全部 8 控制器 + crm-rule 商机规则 3 控制器 + crm-preference SavedViewController 的 @(Get|Post)Mapping,拼类级 @RequestMapping 得全量钥匙集。 2. 文件侧:遍历 D:\\code\\crm-api-docs\\商机模块 全部 .bru(排除 folder.bru), 有 meta tags:[generated] 者从 docs 首行反引号提钥匙(对账钥匙 = METHOD + 路径)。 3. 报告三向差集:源码有文件无 / 文件有源码无 / 两侧都有。 预期(handoff §4,口径勘误):源码唯一钥匙 71 全命中、双零差集; 接口文件 77 = 71 唯一 + 3 接口(page / board/cards / board/stage-summary) × 3 页面副本多出的 6 文件。 结论口径:sync 若重跑,结构已镜像源码 → 全部「不动」;实参值与示例在 docs 块属表述 (铁律 1)→ 不会丢。票面「跑一次 /bruno-sync 验证示例不丢」以此对账 + 抽查达成。 正则覆盖说明:@PostMapping 允许无参(如 OpportunityCreateController 新建商机, 端点路径=类级 base 本身),故 Mapping 后的 ("...") 为可选。 """ import io import os import re import sys from collections import defaultdict sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') REPO = r'd:\code\crm-backend-matt' DOCS = os.path.join(r'D:\code\crm-api-docs', '商机模块') # ---------- 1. 源码侧 ---------- SCAN = [ (os.path.join(REPO, r'crm-opportunity\src\main\java\com\crm\opportunity\controller'), None), (os.path.join(REPO, r'crm-rule\src\main\java\com\crm\rule\controller'), ['OpportunityPoolRuleController.java', 'OpportunityStageTemplateController.java', 'OpportunitySchemeCardTemplateController.java']), (os.path.join(REPO, r'crm-preference\src\main\java\com\crm\preference\controller'), ['SavedViewController.java']), ] re_base = re.compile(r'@RequestMapping\("([^"]+)"\)') # Mapping 路径可选:@PostMapping(无参,端点=类级 base)与 @PostMapping("/xxx") 两种形态都覆盖 re_map = re.compile(r'@(Get|Post)Mapping\b(?:\("([^"]*)"\))?') src_keys = set() per_file = [] for cdir, only in SCAN: for fn in sorted(os.listdir(cdir)): if not fn.endswith('.java'): continue if only is not None and fn not in only: continue txt = open(os.path.join(cdir, fn), encoding='utf-8').read() mb = re_base.search(txt) base = mb.group(1) if mb else '' eps = re_map.findall(txt) per_file.append((fn, base, len(eps))) for m, p in eps: # 无参形态 p=None → 端点路径就是类级 base src_keys.add('%s %s' % (m.upper(), base + (p or ''))) # ---------- 2. 文件侧 ---------- re_key = re.compile(r'`(GET|POST) ([^`]+)`') re_tags = re.compile(r'tags:\s*\[([^\]]*)\]', re.S) file_keys = defaultdict(list) n_files = 0 for dp, dn, fn in os.walk(DOCS): for f in sorted(fn): if not f.endswith('.bru') or f == 'folder.bru': continue path = os.path.join(dp, f) txt = open(path, encoding='utf-8').read() n_files += 1 mt = re_tags.search(txt) if not mt or 'generated' not in mt.group(1): print(' ! 非 generated(私产,跳过):', os.path.relpath(path, DOCS)) continue mk = re_key.search(txt) if not mk: print(' ! 钥匙提取失败:', os.path.relpath(path, DOCS)) continue file_keys[mk.group(1) + ' ' + mk.group(2).strip()].append( os.path.relpath(path, DOCS)) # ---------- 3. 三向对比 ---------- only_src = sorted(src_keys - set(file_keys)) only_file = sorted(set(file_keys) - src_keys) both = src_keys & set(file_keys) print('== 源码侧(12 控制器) ==') for fn, base, n in per_file: print(' %-46s %-28s %2d 端点' % (fn, base, n)) print('源码钥匙数:', len(src_keys)) print() print('== 文件侧 ==') print('商机模块 .bru 接口文件:', n_files, ' 唯一钥匙:', len(file_keys)) dup = {k: v for k, v in file_keys.items() if len(v) > 1} if dup: print('同钥匙多副本(页面归属合法,各副本独立对账):') for k, v in sorted(dup.items()): print(' %s ×%d → %s' % (k, len(v), '; '.join(v))) print() print('== 三向对比 ==') print('两侧都有:', len(both)) print('源码有文件无:', len(only_src)) for k in only_src: print(' -', k) print('文件有源码无:', len(only_file)) for k in only_file: print(' +', k, '→', '; '.join(file_keys[k])) print() ok = (len(only_src) == 0 and len(only_file) == 0 and len(src_keys) == 71 and n_files == 77) print('结论:', '✅ 71 唯一钥匙全命中、双零差集;77 文件 = 71 + 3 接口 × 3 页面副本多 6 —— ' '结构已镜像源码,sync 重跑全部「不动」;实参值与示例在 docs 块属表述(铁律 1),' '不会被 sync 丢弃。' if ok else '❌ 存在差集或计数不符,见上清单,需人工裁决(铁律 4)。') sys.exit(0 if ok else 1)