You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

119 lines
5.0 KiB

1 week ago
# -*- 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)