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.
 
 
 
 
 

279 lines
18 KiB

# -*- coding: utf-8 -*-
# t10-apply.py — 票 10 bruno-sync 应用器(结构听源码,表述听人)
# 1) 复制 t10-payloads/*.bru → E:\code\crm-api-docs(6 个新端点,已存在则跳过)
# 2) 对既有 .bru 做结构 patch(锚不命中即记 fail 不动该处;报告 done/fails,fails>0 exit 1)
import io, os, re, sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DOCS = r'E:\code\crm-api-docs'
PAY = os.path.join(os.path.dirname(os.path.abspath(__file__)), 't10-payloads')
done, fails = [], []
def load(rel):
p = os.path.join(DOCS, rel)
raw = open(p, 'rb').read()
bom = raw[:3] == b'\xef\xbb\xbf'
txt = raw.decode('utf-8-sig' if bom else 'utf-8')
nl = '\r\n' if '\r\n' in txt else '\n'
txt = txt.replace('\r\n', '\n') # 归一为 LF,save 时按 nl 还原(否则 CRLF 文本会被写成 \r\r\n)
return p, txt, nl, bom
def save(p, txt, nl, bom):
if nl == '\r\n':
txt = txt.replace('\n', '\r\n')
open(p, 'wb').write((b'\xef\xbb\xbf' if bom else b'') + txt.encode('utf-8'))
def m(s, nl):
return s.replace('\n', '\r\n') if nl == '\r\n' else s
def sub1(txt, old, new, tag, nl='\n'):
old, new = m(old, nl), m(new, nl)
n = txt.count(old)
if n != 1:
fails.append('[%s] anchor x%d: %r' % (tag, n, old[:70]))
return txt
done.append(tag)
return txt.replace(old, new, 1)
def drop_re(txt, pattern, tag):
rx = re.compile(pattern, re.M)
hits = rx.findall(txt)
if len(hits) != 1:
fails.append('[%s] drop x%d (期望1): %s' % (tag, len(hits), pattern))
return txt
done.append(tag)
return rx.sub('', txt)
def copy_new(src, dst_rel):
dst = os.path.join(DOCS, dst_rel)
if os.path.exists(dst):
done.append('SKIP(已存在) ' + dst_rel)
return
s = open(os.path.join(PAY, src), 'rb').read().decode('utf-8').replace('\r\n', '\n')
os.makedirs(os.path.dirname(dst), exist_ok=True)
open(dst, 'wb').write(s.encode('utf-8'))
done.append('NEW ' + dst_rel)
def patch(rel, fn):
p, txt, nl, bom = load(rel)
t2 = fn(txt, nl, rel)
if t2 != txt:
save(p, t2, nl, bom)
done.append('SAVED ' + rel)
NEW = [
('customer-add.bru', r'商机模块\商机详情\客户\添加关联客户.bru'),
('customer-search.bru', r'商机模块\商机详情\客户\候选客户搜索.bru'),
('customer-set-primary.bru',r'商机模块\商机详情\客户\设为主要意向客户.bru'),
('team-add.bru', r'商机模块\商机详情\团队\批量添加团队成员.bru'),
('team-update.bru', r'商机模块\商机详情\团队\编辑团队成员.bru'),
('team-delete.bru', r'商机模块\商机详情\团队\移除团队成员.bru'),
]
VT_ROW = ' | viewType | string | 否 | - | 视图口径:RECENT/MINE/PARTICIPATED/FOLLOWED/MANAGE/PUBLIC_POOL(与列表视图同口径,不传=默认视图;PUBLIC_POOL=公海看板) |'
VT_NOTE = 'viewType 与列表视图同口径(票 06 D-15;不传=默认视图,PUBLIC_POOL=公海看板)。'
def board_cards(txt, nl, rel, v):
txt = sub1(txt, ' stageId: 747831302777995264', ' viewType: %s\n stageId: 747831302777995264' % v, rel+'/body', nl)
txt = sub1(txt, ' | stageId | string | 是 | - | 阶段节点 ID |', VT_ROW + '\n | stageId | string | 是 | - | 阶段节点 ID |', rel+'/table', nl)
txt = sub1(txt, ' stageId=747831302777995264&offset=0&limit=20', ' viewType=%s&stageId=747831302777995264&offset=0&limit=20' % v, rel+'/ex', nl)
txt = sub1(txt, '(offset+limit 滚动翻页)。', '(offset+limit 滚动翻页)。' + VT_NOTE, rel+'/desc', nl)
return txt
def board_summary(txt, nl, rel, v):
txt = sub1(txt, ' stageTemplateId: 747831302652166144', ' viewType: %s\n stageTemplateId: 747831302652166144' % v, rel+'/body', nl)
txt = sub1(txt, ' | stageTemplateId | string | 否 | - | 阶段模板版本 ID(不传则按默认模板) |', VT_ROW + '\n | stageTemplateId | string | 否 | - | 阶段模板版本 ID(不传则按默认模板) |', rel+'/table', nl)
txt = sub1(txt, ' stageTemplateId=747831302652166144', ' viewType=%s&stageTemplateId=747831302652166144' % v, rel+'/ex', nl)
txt = sub1(txt, '(商机数量 + 项目金额合计)。', '(商机数量 + 项目金额合计)。' + VT_NOTE, rel+'/desc', nl)
return txt
def versions(txt, nl, rel, x):
txt = sub1(txt, ' 实体直出(无 DTO 包装),不含 %s 子表。' % x,
' 出参 DTO 白名单化(票 06 D-22,剥离 creatorId/updaterId/deleted 审计字段),不含 %s 子表。' % x, rel+'/decl', nl)
for f in ('creatorId', 'updaterId', 'deleted'):
txt = drop_re(txt, r'^ \| %s \|.*\r?\n' % f, rel+'/t-'+f)
txt = drop_re(txt, r'^ +"%s": .*\r?\n' % f, rel+'/e-'+f)
return txt
def claim(txt, nl, rel, batch):
txt = drop_re(txt, r'^ ownerNameSnapshot: .*\r?\n', rel+'/b-snap')
txt = drop_re(txt, r'^ ownerDeptId: .*\r?\n', rel+'/b-dept')
txt = drop_re(txt, r'^ \| ownerNameSnapshot \|.*\r?\n', rel+'/t-snap')
txt = drop_re(txt, r'^ \| ownerDeptId \|.*\r?\n', rel+'/t-dept')
txt = drop_re(txt, r'&ownerNameSnapshot=.*$', rel+'/ex')
if batch:
txt = sub1(txt, '每条成功/失败独立记录;返回 null 前端轮询刷新列表。',
'每条成功/失败独立记录;返回 null 前端轮询刷新列表。领取人姓名/部门快照由服务端生成,请求不再传递(票 03 D-13);执行前对全部 id 逐一禁领预检,任一命中 allowFreeClaim≠1 即整批拒绝 66014(票 05 D-09)。', rel+'/desc', nl)
else:
txt = sub1(txt, '写入领取人快照,绑定阶段模板。',
'写入领取人快照,绑定阶段模板。领取人姓名/部门快照由服务端生成,请求不再传递(票 03 D-13);命中发布中公海规则且 allowFreeClaim≠1 时拒绝 66014,走分配 E2(票 05 D-09)。', rel+'/desc', nl)
return txt
def assign(txt, nl, rel, batch):
txt = sub1(txt, ' toUserId: 744842566742048768', ' userId: 744842566742048768', rel+'/body', nl)
txt = sub1(txt, ' | toUserId | string | 是 | - | 被分配人用户 ID |',
' | userId | string | 是 | - | 被分配人用户 ID |', rel+'/t-user', nl)
txt = drop_re(txt, r'^ toUserNameSnapshot: .*\r?\n', rel+'/b-snap')
txt = drop_re(txt, r'^ toDeptId: .*\r?\n', rel+'/b-dept')
txt = drop_re(txt, r'^ \| toUserNameSnapshot \|.*\r?\n', rel+'/t-snap')
txt = drop_re(txt, r'^ \| toDeptId \|.*\r?\n', rel+'/t-dept')
if batch:
txt = sub1(txt, ' ids=748556137065873408,748556138085089280&toUserId=744842566742048768&toUserNameSnapshot=肖琴&toDeptId=744841308677341184',
' ids=748556137065873408,748556138085089280&userId=744842566742048768', rel+'/ex', nl)
txt = sub1(txt, '逐条执行分配逻辑,独立成功/失败。',
'逐条执行分配逻辑,独立成功/失败。被分配人姓名/部门快照由服务端生成,请求不再传快照参数,toUserId 更名 userId(票 03 D-13)。', rel+'/desc', nl)
else:
txt = sub1(txt, ' id=748556137065873408&toUserId=744842566742048768&toUserNameSnapshot=肖琴&toDeptId=744841308677341184',
' id=748556137065873408&userId=744842566742048768', rel+'/ex', nl)
txt = sub1(txt, '负责人变更,记操作日志。',
'负责人变更,记操作日志。被分配人姓名/部门快照由服务端生成,请求不再传快照参数,toUserId 更名 userId(票 03 D-13)。', rel+'/desc', nl)
return txt
def handover(txt, nl, rel):
txt = sub1(txt, ' toUserId: 744842566742048768', ' newOwnerUserId: 744842566742048768', rel+'/body', nl)
txt = sub1(txt, ' | toUserId | string | 是 | - | 接收人用户 ID |',
' | newOwnerUserId | string | 是 | - | 接收人用户 ID |', rel+'/t-user', nl)
txt = drop_re(txt, r'^ toUserNameSnapshot: .*\r?\n', rel+'/b-snap')
txt = drop_re(txt, r'^ toDeptId: .*\r?\n', rel+'/b-dept')
txt = drop_re(txt, r'^ \| toUserNameSnapshot \|.*\r?\n', rel+'/t-snap')
txt = drop_re(txt, r'^ \| toDeptId \|.*\r?\n', rel+'/t-dept')
txt = sub1(txt, ' id=748556125825138688&toUserId=744842566742048768&toUserNameSnapshot=肖琴&toDeptId=744841308677341184&expectedOwnerUserId=744842565802524672',
' id=748556125825138688&expectedOwnerUserId=744842565802524672&newOwnerUserId=744842566742048768', rel+'/ex', nl)
txt = sub1(txt, '`expectedOwnerUserId` 做乐观并发校验防脏移交。',
'`expectedOwnerUserId` 做乐观并发校验防脏移交。接收人姓名/部门快照由服务端生成,请求不再传快照参数,toUserId 更名 newOwnerUserId(票 03 D-13)。', rel+'/desc', nl)
return txt
def pause(txt, nl, rel):
txt = sub1(txt, ' remark: 客户预算冻结,预计9月中旬恢复',
' pauseReason: pause_reason_01\n pauseExpectedRestartDate: 2026-09-15\n pauseRemark: 客户预算冻结,预计9月中旬恢复', rel+'/body', nl)
txt = sub1(txt, ' | remark | string | 否 | - | 暂缓原因说明 |',
' | pauseReason | string | 否 | - | 暂缓原因字典 code |\n | pauseExpectedRestartDate | string | 否 | - | 预计重启日期(ISO yyyy-MM-dd) |\n | pauseRemark | string | 否 | - | 暂缓补充说明 |', rel+'/table', nl)
txt = sub1(txt, ' id=748556137065873408&remark=客户预算冻结,预计9月中旬恢复',
' id=748556137065873408&pauseReason=pause_reason_01&pauseExpectedRestartDate=2026-09-15&pauseRemark=客户预算冻结,预计9月中旬恢复', rel+'/ex', nl)
txt = sub1(txt, '商机暂停推进但不进公海,阶段轴 2 冻结。',
'商机暂停推进但不进公海,阶段轴 2 冻结。暂缓期间其余写端点一律 66003 拒绝(暂缓态写禁守卫,票 05 D-07)。', rel+'/desc', nl)
return txt
def close(txt, nl, rel, batch):
if not batch:
txt = sub1(txt, ' remark: 客户立项取消', ' closeRemark: 客户立项取消', rel+'/body', nl)
txt = sub1(txt, '&remark=客户立项取消', '&closeRemark=客户立项取消', rel+'/ex', nl)
txt = sub1(txt, ' | closeReason | string | 是 | - | 关闭原因字典 code |',
' | closeReason | string | 否 | - | 关闭原因字典 code |', rel+'/t-reason', nl)
txt = sub1(txt, ' | remark | string | 否 | - | 关闭说明 |',
' | closeRemark | string | 否 | - | 关闭说明 |', rel+'/t-remark', nl)
return txt
def release_pool(txt, nl, rel):
txt = sub1(txt, ' | poolReason | string | 是 | - | 进入公海原因字典 code |',
' | poolReason | string | 否 | - | 进入公海原因字典 code |', rel+'/t', nl)
return txt
BODY_OLD = ''' oppName: e2e-A-推进-智慧园区三期
oppSource: opp_source_02
oppType: opp_type_01
industryCode: gov
projectAmount: 1200000
primaryCustomerId: 920000000000000100'''
BODY_NEW = ''' opportunityName: e2e-A-推进-智慧园区三期
oppSource: opp_source_02
oppType: opp_type_01
industryCode: gov
bidForm: bid_form_01
localityType: locality_type_01
customerId: 920000000000000100
provinceCode: 510000
cityCode: 510100'''
TBL_OLD = ''' | oppName | string | 是 | - | 商机名称 |
| oppSource | string | 是 | - | 商机来源字典 code |
| oppType | string | 是 | - | 商机类型字典 code |
| industryCode | string | 否 | - | 行业字典 code |
| bidForm | string | 否 | - | 招标形式字典 code |
| localityType | string | 否 | - | 项目属地字典 code |
| partyAClear | number | 否 | 0 | 甲方是否明确:1是 0否 |
| partyA | string | 否 | - | 甲方名称(partyAClear=1 时建议填) |
| provinceCode | string | 否 | - | 省份国标 code |
| cityCode | string | 否 | - | 市/区国标 code |
| projectAmount | number | 否 | - | 预计项目金额 |
| ownerUserId | string | 否 | - | 负责人用户 ID(不传则默认当前登录人) |
| sourceLeadId | string | 否 | - | 来源线索 ID(从线索转商机时带入) |
| primaryCustomerId | string | 否 | - | 主要意向客户 ID |
| remark | string | 否 | - | 备注 |'''
TBL_NEW = ''' | opportunityName | string | 是 | - | 商机名称 |
| oppSource | string | 是 | - | 商机来源字典 code(opp_source_01=线索转入触发 sourceLeadId 必填) |
| sourceLeadId | string | 否 | - | 来源线索 ID(从线索转商机时带入) |
| oppType | string | 否 | - | 商机类型字典 code |
| industryCode | string | 否 | - | 行业字典 code |
| bidForm | string | 是 | - | 招标形式字典 code |
| localityType | string | 是 | - | 项目属地字典 code(本地/异地) |
| partyAClear | number | 否 | 0 | 甲方是否明确:1是 0否 |
| partyA | string | 否 | - | 甲方名称 |
| customerId | string | 否 | - | 主要意向客户 ID(引用 A4 客户模块,未建;可空) |
| customerName | string | 否 | - | 主要意向客户名快照 |
| provinceCode | string | 是 | - | 省份国标 code |
| cityCode | string | 是 | - | 市/区国标 code |
| remark | string | 否 | - | 备注 |'''
def create_opp(txt, nl, rel):
txt = sub1(txt, BODY_OLD, BODY_NEW, rel+'/body', nl)
txt = sub1(txt, TBL_OLD, TBL_NEW, rel+'/table', nl)
txt = sub1(txt, ' oppName=e2e-A-推进-智慧园区三期&oppSource=opp_source_02&oppType=opp_type_01&industryCode=gov&projectAmount=1200000&primaryCustomerId=920000000000000100',
' opportunityName=e2e-A-推进-智慧园区三期&oppSource=opp_source_02&oppType=opp_type_01&industryCode=gov&bidForm=bid_form_01&localityType=locality_type_01&customerId=920000000000000100&provinceCode=510000&cityCode=510100', rel+'/ex', nl)
txt = sub1(txt, '(不传 ownerUserId 按当前用户部门)', '(负责人=当前登录人,姓名/部门快照服务端生成,票 03 D-13)', rel+'/desc1', nl)
txt = sub1(txt, '否则进入「待领取」公海池。', '否则进入「待领取」公海池。核心六项必填:opportunityName/oppSource/bidForm/provinceCode/cityCode/localityType,缺失 66001(票 03 D-14)。', rel+'/desc2', nl)
return txt
def edit_opp(txt, nl, rel):
txt = sub1(txt, '仅传需变更的字段(null 不更新),字段级 oplog 自动记录变更前后值。',
'仅传需变更的字段(null 不更新),字段级 oplog 自动记录变更前后值。名称字段为 oppName(新建接口为 opportunityName);必填校验对合并后的最终值执行(六项缺一 66001,票 03 D-14)。', rel+'/desc', nl)
return txt
def attach_add(txt, nl, rel):
txt = sub1(txt, ' | bizType | string | 否 | - | 业务归属类型 |',
' | bizType | string | 是 | - | 业务归属类型:OPP/FOLLOW_UP/SITE_SURVEY/SCHEME_CARD 四值封闭(缺省或其他值 66001) |', rel+'/t', nl)
return txt
def team_list(txt, nl, rel):
txt = sub1(txt, ' | projectRole | string | 项目角色字典 code(纯标签,不带权限) |',
' | projectRole | string | 项目角色字典 code(纯标签,不带权限) |\n | duty | string | 职责(非必填文本手工填写,不来源于字典;负责人行编辑仅允许改此列) |', rel+'/t', nl)
txt = sub1(txt, ' "projectRole": "project_role_01",',
' "projectRole": "project_role_01",\n "duty": null,', rel+'/e', nl)
return txt
def main():
for src, rel in NEW:
copy_new(src, rel)
for rel, v in ((r'商机模块\商机公海\看板列表\看板阶段卡片.bru', 'PUBLIC_POOL'),
(r'商机模块\商机管理\看板列表\看板阶段卡片.bru', 'MANAGE'),
(r'商机模块\销售机会\看板列表\看板阶段卡片.bru', 'MINE')):
patch(rel, lambda t, n, r, v=v: board_cards(t, n, r, v))
for rel, v in ((r'商机模块\商机公海\看板列表\看板阶段汇总.bru', 'PUBLIC_POOL'),
(r'商机模块\商机管理\看板列表\看板阶段汇总.bru', 'MANAGE'),
(r'商机模块\销售机会\看板列表\看板阶段汇总.bru', 'MINE')):
patch(rel, lambda t, n, r, v=v: board_summary(t, n, r, v))
patch(r'商机模块\商机管理\商机规则\阶段模板\阶段模板版本列表.bru', lambda t, n, r: versions(t, n, r, 'nodes/deptIds/deptNames'))
patch(r'商机模块\商机管理\商机规则\方案卡模板\方案卡模板版本列表.bru', lambda t, n, r: versions(t, n, r, 'fields/deptIds/deptNames'))
patch(r'商机模块\商机管理\商机规则\公海规则\公海规则版本列表.bru', lambda t, n, r: versions(t, n, r, 'deptIds/deptNames'))
patch(r'商机模块\状态流转\领取商机.bru', lambda t, n, r: claim(t, n, r, False))
patch(r'商机模块\状态流转\批量领取.bru', lambda t, n, r: claim(t, n, r, True))
patch(r'商机模块\状态流转\分配商机.bru', lambda t, n, r: assign(t, n, r, False))
patch(r'商机模块\状态流转\批量分配.bru', lambda t, n, r: assign(t, n, r, True))
patch(r'商机模块\状态流转\移交商机.bru', handover)
patch(r'商机模块\状态流转\暂缓.bru', pause)
patch(r'商机模块\状态流转\关闭商机.bru', lambda t, n, r: close(t, n, r, False))
patch(r'商机模块\状态流转\批量关闭.bru', lambda t, n, r: close(t, n, r, True))
patch(r'商机模块\状态流转\抛公海.bru', release_pool)
patch(r'商机模块\新建商机\新建商机.bru', create_opp)
patch(r'商机模块\商机详情\主体\编辑商机.bru', edit_opp)
patch(r'商机模块\商机详情\附件\新增附件.bru', attach_add)
patch(r'商机模块\商机详情\团队\团队成员列表.bru', team_list)
print('==== done (%d) ====' % len(done))
for d in done:
print(' ' + d)
if fails:
print('==== FAILS (%d) ====' % len(fails))
for f in fails:
print(' ' + f)
sys.exit(1)
main()