# -*- coding: utf-8 -*- """票 06 · (d) 类修复主脚本:错误码节回捞 + createTime 行补齐 + 详情公共头部截断修复 + jobTitleLevel 值域。 纪律:说明文字单一真源 = 源码 @Schema(jobTitleLevel 源码注解由独立编辑先行回写,本脚本写文档侧)。 产物:直接写回 D:/code/crm-api-docs 工作树(不提交),全部动作逐文件打印可审计。 """ from __future__ import annotations import io import re import subprocess import sys from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') DOCS = Path('D:/code/crm-api-docs') A4 = DOCS / 'A4 客户管理' WORKLIST = Path('.scratch/customer-integration-ready/assets/doc-gap-worklist.md') report: list[str] = [] def log(msg): print(msg) report.append(msg) # ---------- 0) 解析 worklist:错误码丢失清单 ---------- wl = WORKLIST.read_text(encoding='utf-8').splitlines() ec_files: list[str] = [] for line in wl: if not line.startswith('| 客户'): continue cols = [c.strip() for c in line.split('|')[1:-1]] if len(cols) >= 7 and '错误码丢失' in cols[6]: ec_files.append(cols[0]) log(f'[0] worklist 错误码丢失标注 {len(ec_files)} 文件') # ---------- 1) 错误码节回捞 ---------- VALID_CODE = re.compile(r'\b\d{5}\b') recovered = skipped = missing_head = 0 for rel in ec_files: wt = A4 / rel txt = wt.read_text(encoding='utf-8') if '## 错误码' in txt: log(f'[1] 跳过(现版已有错误码节): {rel}') skipped += 1 continue git_path = f'A4 客户管理/{rel}' proc = subprocess.run(['git', '-C', str(DOCS), 'show', f'HEAD:{git_path}'], capture_output=True) head = proc.stdout.decode('utf-8', errors='replace') lines = head.splitlines() try: start = next(i for i, l in enumerate(lines) if re.match(r'^\s*## 错误码', l)) except StopIteration: log(f'[1] ⚠ HEAD 无错误码节: {rel}') missing_head += 1 continue end = start + 1 while end < len(lines) and not re.match(r'^\s*## ', lines[end]): end += 1 sec = lines[start:end] # 尾部空行并入(下一段标题前的空行不带走) while sec and sec[-1].strip() == '': sec.pop() codes = sorted(set(VALID_CODE.findall('\n'.join(sec)))) anchor = ' ## 响应示例' if anchor not in txt: log(f'[1] ⚠ 工作树无响应示例锚点: {rel}') missing_head += 1 continue insert = '\n' + '\n'.join(sec) + '\n\n' txt = txt.replace(anchor, insert + anchor, 1) wt.write_text(txt, encoding='utf-8', newline='') recovered += 1 log(f'[1] 回捞 {len(sec)} 行 错误码节 codes={codes}: {rel}') log(f'[1] 错误码回捞:恢复 {recovered},现版已有 {skipped},异常 {missing_head}') # ---------- 2) createTime 行补齐 ×9 ---------- CT_PLAN = [ # (文件, 形态, 说明=源码 @Schema 原文) ('客户交割/交接记录分页.bru', 'content', '发起时间'), ('客户交割/交接详情.bru', 'flat', '发起时间'), ('客户交割/发起交接.bru', 'flat', '发起时间'), ('客户公海/任务结果.bru', 'flat', '提交时间'), ('我的客户/客户导入/任务结果.bru', 'flat', '提交时间'), ('客户详情/联系人导入/任务结果.bru', 'flat', '提交时间'), ('客户公海/最近导入记录.bru', 'content', '提交时间'), ('我的客户/客户导入/最近导入记录.bru', 'content', '提交时间'), ('客户详情/联系人导入/最近导入记录.bru', 'content', '提交时间'), ] ct_done = 0 for rel, form, desc in CT_PLAN: wt = A4 / rel txt = wt.read_text(encoding='utf-8') prefix = 'content[].' if form == 'content' else '' row = f' | {prefix}createTime | LocalDateTime | {desc} |' if row in txt: log(f'[2] 跳过(已有): {rel}') continue lines = txt.splitlines() # 数据结构表范围:## 响应 data 结构 → ## 响应示例 之间 try: t0 = next(i for i, l in enumerate(lines) if l.strip() == '## 响应 data 结构') except StopIteration: log(f'[2] ⚠ 无响应 data 结构节: {rel}') continue t1 = next((i for i in range(t0 + 1, len(lines)) if lines[i].strip() == '## 响应示例'), len(lines)) # 锚点:transfer=状态行后;import=creatorName 行后 field = 'status' if desc == '发起时间' else 'creatorName' anchor_idx = None for i in range(t0 + 1, t1): m = re.match(rf'^\s*\|\s*(content\[\]\.)?{field}\s*\|', lines[i]) if m: anchor_idx = i break if anchor_idx is None: log(f'[2] ⚠ 未找到锚点行 {field}: {rel}') continue lines.insert(anchor_idx + 1, row) wt.write_text('\n'.join(lines) + ('\n' if txt.endswith('\n') else ''), encoding='utf-8', newline='') ct_done += 1 log(f'[2] 补行 {row.strip()} @ {rel}(锚点 {field} 后)') log(f'[2] createTime 补行 {ct_done}/9') # ---------- 3) 详情公共头部 7 行截断修复(原文=源码 @Schema) ---------- DH = A4 / '客户详情/详情公共头部.bru' DH_ROWS = { 'opportunityCount': '关联商机数(外部反查,失败场景前端显 "--")', 'projectCount': '关联项目数(A5 项目 seam,恒 null 显 "--")', 'wonProjectAmount': '汇总卡①已中标项目总额(占位恒 null:A5 项目/投标 seam 未接入,显 "--")', 'planEstimateAmount': '汇总卡②跟进中方案预估总额(占位恒 null:方案卡 seam 未接入,显 "--")', 'ongoingOpportunityAmount': '汇总卡③进行中商机总额(占位恒 null:TODO 可经 CustomerOpportunityQueryPort 扩展汇总查询后切真值,显 "--")', 'contractAmount': '汇总卡④合同总额(占位恒 null:合同模块未建,显 "--")', 'paidAmount': '汇总卡④累计回款(占位恒 null:合同/回款模块未建,与合同总额同卡,显 "--")', } txt = DH.read_text(encoding='utf-8') fixed = 0 for field, desc in DH_ROWS.items(): pat = re.compile(rf'^(\s*\|\s*{field}\s*\|\s*\w+\s*\|).*$', re.M) m = pat.search(txt) if not m: log(f'[3] ⚠ 未找到行: {field}') continue if '"--"' in m.group(0): log(f'[3] 跳过(已完整): {field}') continue txt = pat.sub(lambda mm: f'{mm.group(1)} {desc} |', txt, count=1) fixed += 1 DH.write_text(txt, encoding='utf-8', newline='') log(f'[3] 详情公共头部截断行修复 {fixed}/7') # ---------- 4) jobTitleLevel 值域 ×3(说明文字=源码 @Schema 新文本) ---------- LEVEL_DESC = '职务层级(等级真值 1–10,可空,null=未设置等级落右侧栏)' JT_FILES = ['联系人/联系人详情.bru', '联系人/联系人全局搜索.bru', '联系人/顶级联系人列表.bru'] jt_done = 0 for rel in JT_FILES: wt = A4 / rel txt = wt.read_text(encoding='utf-8') pat = re.compile(rf'^(\s*\|\s*(\[\]\.)?(content\[\]\.)?jobTitleLevel\s*\|\s*Integer\s*\|)\s*职务层级(可空)\s*\|$', re.M) if LEVEL_DESC in txt: log(f'[4] 跳过(已有值域): {rel}') continue new_txt, n = pat.subn(lambda mm: f'{mm.group(1)} {LEVEL_DESC} |', txt) if n == 0: log(f'[4] ⚠ 未匹配 jobTitleLevel 行: {rel}') continue wt.write_text(new_txt, encoding='utf-8', newline='') jt_done += 1 log(f'[4] 值域补全 ×{n} 行: {rel}') log(f'[4] jobTitleLevel 值域 {jt_done}/3') # ---------- 5) 验证 ---------- log('== 验证 ==') n_ec = sum(1 for f in A4.rglob('*.bru') if f.name != 'folder.bru' and '## 错误码' in f.read_text(encoding='utf-8')) log(f'[5] 含错误码节 .bru = {n_ec}(目标 ≥ 78)') dh_txt = (A4 / '客户详情/详情公共头部.bru').read_text(encoding='utf-8') dangle = [l for l in dh_txt.splitlines() if l.rstrip().endswith('显 \\ |') or l.rstrip().endswith('显 \\')] log(f'[5] 详情公共头部 dangling 行 = {len(dangle)}') bad_ct = [rel for rel, _, _ in CT_PLAN if not any(('createTime' in l and l.startswith(' | ')) for l in (A4 / rel).read_text(encoding='utf-8').splitlines())] log(f'[5] createTime 仍缺 = {bad_ct}') bad_jt = [rel for rel in JT_FILES if LEVEL_DESC not in (A4 / rel).read_text(encoding='utf-8')] log(f'[5] jobTitleLevel 仍缺 = {bad_jt}') # BOM 扫描(本脚本触碰的文件) touched = set(ec_files) | {r for r, _, _ in CT_PLAN} | {'客户详情/详情公共头部.bru'} | set(JT_FILES) bom = [] for rel in sorted(touched): p = A4 / rel if p.exists() and p.read_bytes()[:3] == b'\xef\xbb\xbf': bom.append(rel) log(f'[5] BOM = {bom if bom else "无"}') log('== 票 06 脚本完成 ==')