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