# -*- coding: utf-8 -*- """票 11:把实测真实值注入现有商机模块 .bru(实参块值 + 请求示例 + 响应示例=实测摘录)。 铁律:只改既有行的「值」,不增删参数行(结构归 sync);响应示例只替换 code=0 的实测标本,content/list 截 1 条。""" import io, sys, os, re, json sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') SCR = os.path.dirname(os.path.abspath(__file__)) DOCS = r'D:\code\crm-api-docs\商机模块' spec = json.load(open(os.path.join(SCR, 'specimens.json'), encoding='utf-8')) # ---- 从标本提取真实值 ---- det = spec['detail']['resp']['data'] or {} card = spec['card_detail']['resp']['data'] or {} stage_tpl_id = (spec['stage_detail']['resp']['data'] or {}).get('id', '1') att = spec['attachment_list']['resp']['data'] or [] att0 = att[0] if att else {} cust = spec['customer_list']['resp']['data'] or [] cust_id = (cust[0].get('customerId') if cust else None) or '0' A2 = '748556126794022912' # e2e-A-推进-智慧园区二期 A1 = '748556125825138688' PAUSE_A = '748556128685654016' # A-暂缓-数据中心改造(status=3) CLOSED_A = '748556130615033856' # A-关闭(status=4) POOL_OPP = '748556129620983808' # 公海待领取 B1 = '748556131550363648' # B-推进(A 已关注) C1, C2 = '748556137065873408', '748556138085089280' STG1, STG2 = '747831302777995264', '747831302870269952' DEPT_A, DEPT_B = '744841308564094976', '744841308677341184' UA, UB = '744842565802524672', '744842566742048768' FOLLOW_ID, SURVEY_ID, CARD_ID = '748556143311192064', '748556144355573760', '748556146574360576' VALUES_JSON = json.dumps(card.get('valuesJson') or [], ensure_ascii=False) # ---- SPEC:(相对路径) -> form/params 值替换 + resp 标本 ---- SPEC = { '商机管理/商机分页.bru': dict(form={'viewType': 'MANAGE', 'current': 1, 'size': 10}, resp='page_manage', who='A'), '商机公海/商机分页.bru': dict(form={'viewType': 'PUBLIC_POOL', 'current': 1, 'size': 10}, resp='page_public_pool', who='A'), '销售机会/商机分页.bru': dict(form={'viewType': 'MINE', 'current': 1, 'size': 10}, resp='page_mine', who='A'), '商机管理/看板列表/看板阶段汇总.bru': dict(form={'stageTemplateId': stage_tpl_id}, resp='board_summary', who='A'), '商机公海/看板列表/看板阶段汇总.bru': dict(form={'stageTemplateId': stage_tpl_id}, resp='board_summary', who='A'), '销售机会/看板列表/看板阶段汇总.bru': dict(form={'stageTemplateId': stage_tpl_id}, resp='board_summary', who='A'), '商机管理/看板列表/看板阶段卡片.bru': dict(form={'stageId': STG1, 'offset': 0, 'limit': 20}, resp='board_cards', who='A'), '商机公海/看板列表/看板阶段卡片.bru': dict(form={'stageId': STG1, 'offset': 0, 'limit': 20}, resp='board_cards', who='A'), '销售机会/看板列表/看板阶段卡片.bru': dict(form={'stageId': STG1, 'offset': 0, 'limit': 20}, resp='board_cards', who='A'), '商机详情/主体/商机详情.bru': dict(params={'id': A2}, resp='detail', who='A'), '商机详情/主体/阶段推进指引.bru': dict(params={'oppId': A2}, resp='stage_progress', who='A'), '商机详情/主体/阶段推进历史.bru': dict(params={'oppId': A2}, resp='stage_history', who='A'), '商机详情/主体/上报访问.bru': dict(form={'oppId': A2}), '商机详情/主体/关注商机.bru': dict(form={'oppId': B1}), '商机详情/主体/取关商机.bru': dict(form={'oppId': B1}), '商机详情/主体/批量关注.bru': dict(form={'ids': f'{C1},{C2}'}), '商机详情/主体/切换阶段.bru': dict(form={'oppId': A2, 'toStageId': STG2, 'remark': '推进到关系摸排'}), '商机详情/主体/编辑商机.bru': dict(form={'id': A2, 'oppName': 'e2e-A-推进-智慧园区二期', 'projectAmount': det.get('projectAmount') if det.get('projectAmount') is not None else 0}), '商机详情/勘察/勘察分页.bru': dict(form={'oppId': A2, 'pageNum': 1, 'pageSize': 10}, resp='survey_page', who='A'), '商机详情/勘察/新增勘察.bru': dict(form={'oppId': A2, 'surveyDate': '2026-09-01 10:00:00', 'engineerUserId': UB, 'applyWay': 'apply_way_01', 'surveySeq': 2}), '商机详情/勘察/删除勘察.bru': dict(form={'id': SURVEY_ID}), '商机详情/团队/团队成员列表.bru': dict(params={'oppId': A2}, resp='team_list', who='A'), '商机详情/客户/客户列表.bru': dict(params={'oppId': A2}, resp='customer_list', who='A'), '商机详情/方案卡/保存方案卡.bru': dict(form={'oppId': A2, 'customerId': cust_id, 'templateId': '748508098259320832', 'schemeBudget': 880000, 'biddingForm': card.get('biddingForm') or 'bid_form_01', 'enterStage': card.get('enterStage') or 'opp_stage_01', 'valuesJson': VALUES_JSON}), '商机详情/方案卡/提交方案卡.bru': dict(form={'id': CARD_ID}), '商机详情/方案卡/方案卡列表.bru': dict(params={'oppId': A2}, resp='card_list', who='A'), '商机详情/方案卡/方案卡详情.bru': dict(params={'id': CARD_ID}, resp='card_detail', who='A'), '商机详情/方案卡/可选模板版本.bru': dict(params={'oppId': A2}, resp='selectable_templates', who='A'), '商机详情/日志/操作日志分页.bru': dict(form={'oppId': A2, 'pageNum': 1, 'pageSize': 10}, resp='oplog_page', who='A'), '商机详情/跟进/新增跟进.bru': dict(form={'oppId': A2, 'followContent': '电话回访:确认招标形式与预算区间', 'followTime': '2026-08-28 16:00:00', 'followWay': 'follow_way_01', 'customerId': 0}), '商机详情/跟进/删除跟进.bru': dict(form={'id': FOLLOW_ID}), '商机详情/跟进/跟进分页.bru': dict(form={'oppId': A2, 'pageNum': 1, 'pageSize': 10}, resp='follow_page', who='A'), '商机详情/附件/附件列表.bru': dict(params={'oppId': A2, 'bizType': 'OPP'}, resp='attachment_list', who='A'), '商机详情/附件/新增附件.bru': dict(form={'oppId': A2, 'fileId': att0.get('fileId', '1'), 'bizType': 'OPP', 'bizId': A2, 'materialType': ''}), '商机详情/附件/删除附件.bru': dict(form={'id': att0.get('id', '1')}), '新建商机/可关联线索下拉.bru': dict(resp='convertible_leads', who='A'), '新建商机/新建商机.bru': dict(form={'oppName': 'e2e-A-推进-智慧园区三期', 'oppSource': det.get('oppSource') or 'opp_source_02', 'oppType': det.get('oppType') or 'opp_type_01', 'industryCode': det.get('industryCode') or 'industry_gov', 'projectAmount': 1200000, 'primaryCustomerId': cust_id}), '状态流转/领取商机.bru': dict(form={'id': POOL_OPP, 'ownerNameSnapshot': '赖永利', 'ownerDeptId': DEPT_A}), '状态流转/批量领取.bru': dict(form={'ids': f'{POOL_OPP},748556135216185344', 'ownerNameSnapshot': '赖永利', 'ownerDeptId': DEPT_A}), '状态流转/分配商机.bru': dict(form={'id': C1, 'toUserId': UB, 'toUserNameSnapshot': '肖琴', 'toDeptId': DEPT_B}), '状态流转/批量分配.bru': dict(form={'ids': f'{C1},{C2}', 'toUserId': UB, 'toUserNameSnapshot': '肖琴', 'toDeptId': DEPT_B}), '状态流转/暂缓.bru': dict(form={'id': C1, 'remark': '客户预算冻结,预计9月中旬恢复'}), '状态流转/取消暂缓.bru': dict(form={'id': PAUSE_A}), '状态流转/抛公海.bru': dict(form={'id': C1, 'poolReason': 'pool_reason_02'}), '状态流转/关闭商机.bru': dict(form={'id': C2, 'closeReason': 'close_reason_01', 'remark': '客户立项取消'}), '状态流转/批量关闭.bru': dict(form={'ids': f'{C1},{C2}', 'closeReason': 'close_reason_01'}), '状态流转/重启商机.bru': dict(form={'id': CLOSED_A}), '状态流转/移交商机.bru': dict(form={'id': A1, 'toUserId': UB, 'toUserNameSnapshot': '肖琴', 'toDeptId': DEPT_B, 'expectedOwnerUserId': UA}), } PAT_BLOCK = re.compile(r'((?:params:query|body:form-urlencoded) \{)([^}]*)(\})') PAT_KEYLINE = re.compile(r'^(\s*)([A-Za-z_][A-Za-z0-9_\[\]]*):(.*)$') def slim(data): """响应示例裁剪:分页 content / 顶层数组只留 1 条元素。""" if isinstance(data, dict) and 'content' in data: d = dict(data) c = d.get('content') or [] d['content'] = c[:1] return d if isinstance(data, list): return data[:1] return data changed, kept, warns = 0, 0, [] for rel, s in SPEC.items(): p = os.path.join(DOCS, *rel.split('/')) txt = open(p, encoding='utf-8').read() orig = txt # 1) 参数块值替换(只改既有行值) newvals = {} # block_kind -> {key: value} 供请求示例重造 def repl_block(m): head, body, tail = m.group(1), m.group(2), m.group(3) kind = 'params' if head.startswith('params:query') else 'form' out_lines = [] for line in body.splitlines(): km = PAT_KEYLINE.match(line) if km and km.group(2) in s.get(kind, {}): v = s[kind][km.group(2)] out_lines.append(f'{km.group(1)}{km.group(2)}: {v}') newvals.setdefault(kind, {})[km.group(2)] = v else: if km and kind == 'form' and km.group(2) not in s.get(kind, {}): warns.append(f'{rel}: 行 {km.group(2)} 无 SPEC 值,保留原样') out_lines.append(line) return head + '\n' + '\n'.join(out_lines) + '\n' + tail txt = PAT_BLOCK.sub(repl_block, txt, count=1) # 2) 请求示例重造(与替换后的块一致):定位「## 请求示例」段,重写最后一个非空行 # (form 类末行=kv 行;query 类末行=GET 行;docs 标题带 2 空格缩进,边界正则须容忍) if newvals.get('form') or newvals.get('params'): mreq = re.search(r'(## 请求示例\n.*?)(\n\n[ \t]*## 响应 data 结构)', txt, re.S) if mreq: lines = mreq.group(1).splitlines() for i in range(len(lines) - 1, -1, -1): if not lines[i].strip(): continue if newvals.get('form'): kv = '&'.join(f'{k}={v}' for k, v in newvals['form'].items()) lines[i] = ' ' + kv else: gm = re.match(r'([ \t]*)(GET|POST) (\S+)', lines[i]) if gm: path = gm.group(3).split('?')[0] qs = '&'.join(f'{k}={v}' for k, v in newvals['params'].items()) lines[i] = f'{gm.group(1)}{gm.group(2)} {path}?{qs}' break txt = txt.replace(mreq.group(1), '\n'.join(lines), 1) # 3) 响应示例替换(code=0 的实测标本) rk = s.get('resp') if rk and rk in spec and spec[rk]['resp'].get('code') == 0: who = s.get('who', '?') slim_data = slim(spec[rk]['resp'].get('data')) envelope = {'code': 0, 'success': True, 'message': 'success', 'data': slim_data} body = json.dumps(envelope, ensure_ascii=False, indent=2) body_ind = '\n'.join((' ' + ln) if ln else ln for ln in body.split('\n')) note = f'// 实测响应摘录(20260828 · 用户{who} · e2e 种子库;数组字段只示第 1 条)' mre = re.search(r'([ \t]*## 响应示例\n\n)[ \t]*// 示例数据,字段结构以上表为准,非真实返回\n[ \t]*```json\n.*?\n[ \t]*```', txt, re.S) if mre: txt = txt[:mre.start()] + f'{mre.group(1)} {note}\n ```json\n{body_ind}\n ```' + txt[mre.end():] else: warns.append(f'{rel}: 未找到机械推导响应示例段,跳过响应替换') if txt != orig: open(p, 'w', encoding='utf-8', newline='\n').write(txt) changed += 1 else: kept += 1 print(f'changed={changed} kept={kept}') print('warnings:') for w in warns[:40]: print(' -', w)