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.
 
 
 
 
 

185 lines
12 KiB

# -*- 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)