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.
 
 
 
 
 

684 lines
34 KiB

# -*- coding: utf-8 -*-
"""票 11 t11-4 — gen-rules-views.py:生成规则族 25 + 自定义视图 4 新 .bru(真实示例落地)。
数据源:.scratch/opportunity-e2e/specimens.json(32 标本 = 30 首采 + stage/scheme versions 补采 20260829)。
落点:D:\\code\\crm-api-docs\\商机模块\\商机管理\\商机规则\\{公海规则,阶段模板,方案卡模板}\\
D:\\code\\crm-api-docs\\商机模块\\销售机会\\自定义视图\\
目录落点裁决(handoff §3.1):源码 @Tag 是「商机管理/商机规则」「销售机会/自定义视图」,
遵循现有集合树(顶层「商机模块/」)避免顶层重复文件夹。
纪律(handoff §3.5 / bruno-sync SKILL):
- UTF-8 无 BOM、\\n 换行;幂等:目标文件已存在则跳过
- docs 块所有行 2 空格缩进;首行 `# 动作语义名`、第二行 `METHOD 路径`(对账钥匙勿动)
- 节顺序:说明段 → ## 请求参数 → ## 请求示例 → ## 响应 data 结构 → ## 响应示例
- 有标本:`// 实测响应摘录(20260828 · 用户X · e2e 种子库;数组字段只示第 1 条)`
无标本(写操作):`// 示例数据,字段结构以上表为准,非真实返回`
- Result<Void> 写操作:「响应 data 结构」段写 null(照 商机模块/状态流转/领取商机.bru 惯例)
- folder.bru 无 type、无 tags;seq 动态取同父目录现有 folder.bru max+10
"""
import io
import json
import os
import re
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
FORCE = '--force' in sys.argv
HERE = os.path.dirname(os.path.abspath(__file__))
DOCS_ROOT = r'D:\code\crm-api-docs'
MOD_ROOT = os.path.join(DOCS_ROOT, '商机模块')
SP = json.load(open(os.path.join(HERE, 'specimens.json'), encoding='utf-8'))
# ---------- 真实值(全部从标本现取,不硬编码) ----------
POOL_ID = SP['pool_detail']['params']['id'] # 748507245062389760
SCHEME_ID = SP['scheme_detail']['params']['id'] # 748508098259320832
STAGE_ID = SP['stage_detail']['params']['id'] # 747831302652166144
_seed_view = next(v for v in SP['view_list']['resp']['data'] if '测试视图' in v['name'])
VIEW_ID = _seed_view['viewId']
_fdefs = SP['field_defs']['resp']['data']
FK0, FK1 = _fdefs[0]['fieldKey'], _fdefs[1]['fieldKey'] # customer_name / customer_type
MECH = '// 示例数据,字段结构以上表为准,非真实返回'
def measured(key):
return '// 实测响应摘录(20260828 · 用户%s · e2e 种子库;数组字段只示第 1 条)' % SP[key]['who']
def j2(obj):
return json.dumps(obj, ensure_ascii=False, indent=2)
def env(data):
return {'code': 0, 'success': True, 'message': 'success', 'data': data}
def slim_page(key):
d = SP[key]['resp']['data']
return dict(d, content=d['content'][:1])
def slim_list(key):
return SP[key]['resp']['data'][:1]
def table(rows, headers):
out = ['| ' + ' | '.join(headers) + ' |',
'| ' + ' | '.join(['---'] * len(headers)) + ' |']
out += ['| ' + ' | '.join(r) + ' |' for r in rows]
return out
def prefix_rows(rows, p):
return [(p + n, t, d) for n, t, d in rows]
# ---------- 响应 data 结构字段表(源码 DTO/实体逐字段) ----------
PAGE_SHELL_ROWS = [
('total', 'number', '总条数'),
('size', 'number', '每页条数'),
('current', 'number', '当前页码'),
('pages', 'number', '总页数'),
('empty', 'boolean', '是否为空'),
]
POOL_DTO_ROWS = [
('id', 'string', '版本行 ID(Long 序列化为 string)'),
('createTime', 'string', '创建时间'),
('updateTime', 'string', '更新时间'),
('ruleCode', 'string', '规则编码(系统生成,同规则多版本共用)'),
('ruleName', 'string', '规则名称'),
('versionNo', 'string', '版本号(系统生成只读:V1.0/V1.1…)'),
('status', 'number', '状态:1草稿 2发布中 3已停用'),
('applyScope', 'number', '适用范围:1所有部门通用 2对应部门专用'),
('isDefault', 'number', '是否默认:1是 0否(仅通用可设)'),
('allowManualPool', 'number', '允许负责人手动转入公海:1是 0否'),
('autoRecycleEnabled', 'number', '启用自动回收:1是 0否'),
('recycleDays', 'number', '自动回收天数(启用回收时必填,≥1;未启用为 null)'),
('allowFreeClaim', 'number', '允许自由领取:1是 0否'),
('recycleRemindEnabled', 'number', '自动回收前提醒:1是 0否(仅回收启用时可开)'),
('remindDays', 'number', '提前提醒天数(启用提醒时必填,≥1 且 < 回收天数)'),
('ruleDesc', 'string', '规则说明'),
('deptIds', 'array', '适用部门 ID 列表(Long 序列化为 string;applyScope=1 时为空数组)'),
('deptNames', 'array', '适用部门名称列表(出参回显)'),
]
STAGE_DTO_ROWS = [
('id', 'string', '版本行 ID(Long 序列化为 string)'),
('createTime', 'string', '创建时间'),
('updateTime', 'string', '更新时间'),
('templateCode', 'string', '模板编码(系统生成,同模板多版本共用)'),
('templateName', 'string', '模板名称'),
('versionNo', 'string', '版本号(系统生成只读:V1.0/V1.1…)'),
('status', 'number', '状态:1草稿 2发布中 3已停用'),
('applyScope', 'number', '适用范围:1所有商机 2指定部门'),
('isDefault', 'number', '是否默认:1是 0否(仅所有商机范围可设)'),
('templateDesc', 'string', '模板说明'),
('deptIds', 'array', '适用部门 ID 列表(Long 序列化为 string;applyScope=1 时为空数组)'),
('deptNames', 'array', '适用部门名称列表(出参回显)'),
('nodes', 'array', '阶段节点有序列表(seqNo 按列表顺序重算;末位固定节点系统保证)'),
('nodes[].id', 'string', '节点 ID(出参回显)'),
('nodes[].seqNo', 'number', '阶段序号'),
('nodes[].stageDictCode', 'string', '阶段字典值(引用「商机阶段」字典分组项)'),
('nodes[].customNodeName', 'string', '自定义节点名称(空则带入字典名)'),
('nodes[].workGoal', 'string', '阶段工作目标'),
('nodes[].isFixed', 'number', '固定节点:1固定(已转项目)0普通(出参回显,入参忽略)'),
]
SCHEME_DTO_ROWS = [
('id', 'string', '版本行 ID(Long 序列化为 string)'),
('createTime', 'string', '创建时间'),
('updateTime', 'string', '更新时间'),
('templateCode', 'string', '模板编码(系统生成,同模板多版本共用)'),
('templateName', 'string', '模板名称'),
('versionNo', 'string', '版本号(系统生成只读:V1.0/V1.1…)'),
('status', 'number', '状态:1草稿 2发布中 3已停用'),
('applyScope', 'number', '适用范围:1所有部门通用 2对应部门专用'),
('isDefault', 'number', '是否默认:1是 0否(仅所有部门通用可设)'),
('templateDesc', 'string', '模板说明'),
('deptIds', 'array', '适用部门 ID 列表(Long 序列化为 string;applyScope=1 时为空数组)'),
('deptNames', 'array', '适用部门名称列表(出参回显)'),
('fields', 'array', '字段项有序列表(seqNo 按列表顺序重算)'),
('fields[].id', 'string', '字段项 ID(出参回显)'),
('fields[].seqNo', 'number', '序号'),
('fields[].fieldKey', 'string', '字段键(引用系统字段定义)'),
('fields[].fieldName', 'string', '展示字段名(系统字段快照回显;入参忽略)'),
('fields[].fieldType', 'string', '字段类型(系统字段快照回显;入参忽略)'),
('fields[].isVisible', 'number', '显示开关:1显示 0不显示(关闭则不出现在填写页且清必填)'),
('fields[].isRequired', 'number', '必填:1必填 0非必填(仅显示时可为 1)'),
]
VER_BASE_ROWS = [
('id', 'string', '版本行 ID(Long 序列化为 string)'),
('creatorId', 'string', '创建人 ID(实体直出内部字段;D-19 P2 已登记待收敛)'),
('createTime', 'string', '创建时间'),
('updaterId', 'string', '更新人 ID(实体直出内部字段)'),
('updateTime', 'string', '更新时间'),
('deleted', 'boolean', '逻辑删除标记(实体直出,正常恒为 false)'),
]
POOL_VER_ROWS = VER_BASE_ROWS + [
('ruleCode', 'string', '规则编码'),
('ruleName', 'string', '规则名称'),
('versionNo', 'string', '版本号'),
('status', 'number', '状态:1草稿 2发布中 3已停用'),
('applyScope', 'number', '适用范围:1所有部门通用 2对应部门专用'),
('isDefault', 'number', '是否默认:1是 0否'),
('allowManualPool', 'number', '允许负责人手动转入公海:1是 0否'),
('autoRecycleEnabled', 'number', '启用自动回收:1是 0否'),
('recycleDays', 'number', '自动回收天数'),
('allowFreeClaim', 'number', '允许自由领取:1是 0否'),
('recycleRemindEnabled', 'number', '自动回收前提醒:1是 0否'),
('remindDays', 'number', '提前提醒天数'),
('ruleDesc', 'string', '规则说明'),
]
STAGE_VER_ROWS = VER_BASE_ROWS + [
('templateCode', 'string', '模板编码'),
('templateName', 'string', '模板名称'),
('versionNo', 'string', '版本号'),
('status', 'number', '状态:1草稿 2发布中 3已停用'),
('applyScope', 'number', '适用范围:1所有商机 2指定部门'),
('isDefault', 'number', '是否默认:1是 0否'),
('templateDesc', 'string', '模板说明'),
]
FIELD_DEF_ROWS = [
('id', 'string', '字段定义 ID'),
('createTime', 'string', '创建时间'),
('updateTime', 'string', '更新时间'),
('fieldKey', 'string', '字段键(模板字段项引用它)'),
('fieldName', 'string', '字段名(展示名)'),
('fieldType', 'string', '类型:RELATED_OBJECT/SINGLE_SELECT/AUTOFILL/YES_NO/ATTACHMENT/AMOUNT/TEXT_SINGLE/TEXT_MULTI/DATE'),
('optionSource', 'string', '选项来源(单选下拉类:字典分组 code)'),
('refTarget', 'string', '关联对象指向(关联对象类:目标实体语义,如 customer/contact)'),
('autofillSource', 'string', '自动回填来源(自动回填类:来源字段语义)'),
('defaultValue', 'string', '默认值'),
('isDeletable', 'number', '是否可删除:1可删 0内置不可删'),
]
VIEW_ROWS = [
('viewId', 'string', '视图 ID(服务端生成;save 新建时返回,编辑/删除/设默认须回传)'),
('name', 'string', '检索名称(用户填)'),
('conditions', 'array', '检索条件列表(本期前端只提交 1 组)'),
('conditions[].field', 'string', '字段 key(业务方字段池内的稳定 code)'),
('conditions[].operator', 'string', '操作符(业务方自定义 code,如 eq 等于 / like 包含)'),
('conditions[].value', 'string', '条件值(为空/不为空操作符时可为 null)'),
('sortField', 'string', '排序字段 key(可空)'),
('sortDirection', 'string', '排序方向 asc/desc(可空)'),
('isDefault', 'boolean', '是否默认视图(单值互斥)'),
('seqNo', 'number', '视图在列表中的排序序号'),
]
# ---------- 请求参数表 ----------
PAGE_IN_BASE = [
('current', 'number', '', '1', '当前页码,从 1 开始'),
('size', 'number', '', '10', '每页条数,上限 500'),
('keyword', 'string', '', '-', 'KW_NOTE'),
('orderBy', 'string', '', '-', '排序字段(驼峰或下划线均可,如 createTime)'),
('asc', 'boolean', '', 'false', '是否升序(默认 false,新数据在前)'),
]
POOL_SAVE_IN = [
('id', 'string', '', '-', '编辑草稿时传版本行 ID;新建不传'),
('ruleName', 'string', '', '-', '规则名称'),
('applyScope', 'number', '', '-', '适用范围:1所有部门通用 2对应部门专用'),
('isDefault', 'number', '', '0', '是否默认:1是 0否(仅通用可设)'),
('allowManualPool', 'number', '', '1', '允许负责人手动转入公海:1是 0否'),
('autoRecycleEnabled', 'number', '', '1', '启用自动回收:1是 0否;=0 时 recycleRemindEnabled 须显式传 0(省略按开启处理)'),
('recycleDays', 'number', '条件', '-', '自动回收天数(启用回收时必填,≥1)'),
('allowFreeClaim', 'number', '', '1', '允许自由领取:1是 0否'),
('recycleRemindEnabled', 'number', '条件', '0', '自动回收前提醒:1是 0否(仅回收启用时可开)'),
('remindDays', 'number', '条件', '-', '提前提醒天数(启用提醒时必填,≥1 且 < 回收天数)'),
('ruleDesc', 'string', '', '-', '规则说明'),
('deptIds', 'string', '条件', '-', '适用部门 ID 列表,逗号分隔(applyScope=2 时必填至少一个)'),
]
STAGE_SAVE_IN = [
('id', 'string', '', '-', '编辑草稿时传版本行 ID;新建不传'),
('templateName', 'string', '', '-', '模板名称'),
('applyScope', 'number', '', '-', '适用范围:1所有商机 2指定部门'),
('isDefault', 'number', '', '0', '是否默认:1是 0否(仅所有商机范围可设)'),
('templateDesc', 'string', '', '-', '模板说明'),
('deptIds', 'string', '条件', '-', '适用部门 ID 列表,逗号分隔(applyScope=2 时必填至少一个)'),
('nodes[N].stageDictCode', 'string', '', '-', '第 N 个节点的阶段字典值(N 从 0 递增的索引式表单绑定,引用「商机阶段」字典分组项)'),
('nodes[N].customNodeName', 'string', '', '-', '自定义节点名称(空则带入字典名)'),
('nodes[N].workGoal', 'string', '', '-', '阶段工作目标'),
]
SCHEME_SAVE_IN = [
('id', 'string', '', '-', '编辑草稿时传版本行 ID;新建不传'),
('templateName', 'string', '', '-', '模板名称'),
('applyScope', 'number', '', '-', '适用范围:1所有部门通用 2对应部门专用'),
('isDefault', 'number', '', '0', '是否默认:1是 0否(仅所有部门通用可设)'),
('templateDesc', 'string', '', '-', '模板说明'),
('deptIds', 'string', '条件', '-', '适用部门 ID 列表,逗号分隔(applyScope=2 时必填至少一个)'),
('fields[N].fieldKey', 'string', '', '-', '第 N 个字段项的字段键(索引式表单绑定,必须取「系统字段库列表」实测 fieldKey,非自由造)'),
('fields[N].isVisible', 'number', '', '1', '显示开关:1显示 0不显示(关闭则不出现在填写页且清必填)'),
('fields[N].isRequired', 'number', '', '0', '必填:1必填 0非必填(仅显示时可为 1)'),
]
# ---------- 写操作实测构造(e2e-rules.py / seed-opportunity.py 实测通过口径) ----------
POOL_SAVE_PAIRS = [
('ruleName', 'e2e-演示-公海规则'),
('applyScope', '1'),
('isDefault', '0'),
('allowManualPool', '1'),
('autoRecycleEnabled', '1'),
('recycleDays', '30'),
('allowFreeClaim', '1'),
('recycleRemindEnabled', '1'),
('remindDays', '7'),
('ruleDesc', '票11演示'),
]
STAGE_SAVE_PAIRS = [
('templateName', 'e2e-演示-阶段模板'),
('applyScope', '1'),
('isDefault', '0'),
('templateDesc', '票11演示'),
('nodes[0].stageDictCode', 'OPP_STAGE_01'),
('nodes[0].workGoal', '需求确认'),
('nodes[1].stageDictCode', 'OPP_STAGE_02'),
('nodes[1].customNodeName', '方案深化'),
]
SCHEME_SAVE_PAIRS = [
('templateName', 'e2e-演示-方案卡模板'),
('applyScope', '1'),
('isDefault', '0'),
('templateDesc', '票11演示'),
('fields[0].fieldKey', FK0),
('fields[0].isVisible', '1'),
('fields[0].isRequired', '1'),
('fields[1].fieldKey', FK1),
('fields[1].isVisible', '1'),
('fields[1].isRequired', '0'),
]
VIEW_SAVE_OBJ = {
'name': 'e2e-B-测试视图-我的推进',
'conditions': [
{'field': 'opportunityName', 'operator': 'like', 'value': 'e2e-'},
{'field': 'oppStatus', 'operator': 'eq', 'value': '2'},
],
'sortField': 'createTime',
'sortDirection': 'desc',
'isDefault': False,
'seqNo': 99,
}
# ---------- .bru 组装 ----------
CREATED, SKIPPED, REWRITTEN = [], [], []
# 仅限本脚本生成的 4 个新目录内允许 --force 重写(防误伤既有集合文件)
OWN_DIRS = ('商机规则', '自定义视图')
def write_file(path, content):
rel = os.path.relpath(path, DOCS_ROOT)
exists = os.path.exists(path)
if exists and not (FORCE and any(seg in rel for seg in OWN_DIRS)):
SKIPPED.append(rel)
return
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8', newline='\n') as f:
f.write(content)
(CREATED if not exists else REWRITTEN).append(rel)
def next_seq(parent):
mx = 0
if os.path.isdir(parent):
for d in sorted(os.listdir(parent)):
fp = os.path.join(parent, d, 'folder.bru')
if os.path.isfile(fp):
m = re.search(r'seq:\s*(\d+)', open(fp, encoding='utf-8').read())
if m:
mx = max(mx, int(m.group(1)))
return mx + 10
def write_folder(parent, name):
seq = next_seq(parent)
write_file(os.path.join(parent, name, 'folder.bru'),
'meta {\n name: %s\n seq: %d\n}\n' % (name, seq))
def qblock(pairs):
return '\n'.join(['params:query {'] + [' %s: %s' % kv for kv in pairs] + ['}'])
def fblock(pairs):
return '\n'.join(['body:form-urlencoded {'] + [' %s: %s' % kv for kv in pairs] + ['}'])
def jblock(json_text):
return '\n'.join(['body:json {'] + [' ' + l for l in json_text.splitlines()] + ['}'])
def make_bru(name, seq, method, path, transport, param_text, docs_text):
body = {'GET': 'none', 'FORM': 'formUrlEncoded', 'JSON': 'json'}[transport]
verb = 'get' if method == 'GET' else 'post'
L = ['meta {', ' name: ' + name, ' type: http', ' seq: ' + str(seq),
' tags: [', ' generated', ' ]', '}', '',
verb + ' {', ' url: {{baseUrl}}' + path, ' body: ' + body,
' auth: inherit', '}', '']
if param_text:
L += [param_text, '']
L += [docs_text]
return '\n'.join(L) + '\n'
def make_docs(title, key, intro, req_md, ex_md, struct_md, resp_comment, resp_json):
struct_lines = struct_md if isinstance(struct_md, list) else [struct_md]
L = ['# ' + title, '`' + key + '`']
L += list(intro)
L += ['', '## 请求参数', ''] + list(req_md)
L += ['', '## 请求示例', ''] + list(ex_md)
L += ['', '## 响应 data 结构', ''] + struct_lines
L += ['', '## 响应示例', '', resp_comment, '```json']
L += resp_json.splitlines()
L += ['```']
return 'docs {\n' + '\n'.join((' ' + l) if l else '' for l in L) + '\n}'
def ex_form(method, path, pairs):
qs = '&'.join('%s=%s' % kv for kv in pairs)
return ['请求头:Authorization: Bearer {{token}}',
'Content-Type: application/x-www-form-urlencoded',
'', method + ' ' + path, qs]
def ex_get(path, pairs):
qs = '&'.join('%s=%s' % kv for kv in pairs)
return ['请求头:Authorization: Bearer {{token}}', '',
'GET ' + path + '?' + qs]
def ex_json(path, query_pairs, json_text):
qs = ('?' + '&'.join('%s=%s' % kv for kv in query_pairs)) if query_pairs else ''
L = ['请求头:Authorization: Bearer {{token}}', '',
'POST ' + path + qs, 'Content-Type: application/json', '', '```json']
L += json_text.splitlines()
L += ['```']
return L
def page_struct_rows(biz_rows, row_noun):
return ([('content', 'array', 'PageResult.content ' + row_noun)]
+ prefix_rows(biz_rows, 'content[].') + PAGE_SHELL_ROWS)
def struct_table(rows):
return table(rows, ('字段', '类型', '说明'))
# ---------- 规则三族生成 ----------
def build_family(cfg):
sub, base, title = cfg['sub'], cfg['base'], cfg['title']
tpl_id = cfg['tpl_id']
parent = os.path.join(MOD_ROOT, '商机管理', '商机规则')
write_folder(parent, sub)
fdir = os.path.join(parent, sub)
def emit(name, seq, method, path, transport, param_text, key_line,
intro, req_md, ex_md, struct_md, resp_data, resp_key):
comment = measured(resp_key) if resp_key else MECH
docs = make_docs(name, key_line, intro, req_md, ex_md, struct_md,
comment, j2(env(resp_data)))
write_file(os.path.join(fdir, name + '.bru'),
make_bru(name, seq, method, path, transport, param_text, docs))
code_col = cfg['code_col']
page_rows = [r if r[4] != 'KW_NOTE' else (r[0], r[1], r[2], r[3], cfg['keyword_note'])
for r in PAGE_IN_BASE]
page_rows += [
('applyScope', 'number', '', '-', '适用范围筛选:' + cfg['scope_desc']),
('status', 'number', '', '-', '状态筛选:1草稿 2发布中 3已停用'),
]
id_only = lambda desc: [('id', 'string', '', '-', desc)]
# 1 分页
emit(title + '分页', 1, 'POST', base + '/page', 'FORM',
fblock([('current', '1'), ('size', '10')]),
'POST ' + base + '/page',
['%s配置列表,按版本维度展示(同 %s 草稿/发布中/停用各占一行)。' % (title, code_col),
cfg['keyword_note'] + ''],
table(page_rows, ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/page', [('current', '1'), ('size', '10')]),
struct_table(page_struct_rows(cfg['biz_rows'], cfg['row_noun'])),
slim_page(cfg['page_key']), cfg['page_key'])
# 2 详情
emit(title + '详情', 2, 'GET', base + '/detail', 'GET',
qblock([('id', tpl_id)]),
'GET ' + base + '/detail',
['%s详情(含适用部门%s)。' % (title, cfg['detail_extra'])],
table(id_only('版本行 ID'), ('参数', '类型', '必填', '默认', '说明')),
ex_get(base + '/detail', [('id', tpl_id)]),
struct_table(cfg['biz_rows']),
SP[cfg['detail_key']]['resp']['data'], cfg['detail_key'])
seq = 3
if cfg.get('field_defs'):
# 方案卡模板专属:系统字段库列表
emit('系统字段库列表', seq, 'GET', base + '/field-defs', 'GET',
None, 'GET ' + base + '/field-defs',
['系统字段库列表:模板字段项 fieldKey 的合法取值源(只读,无参数)。'],
['无参数。'],
ex_get(base + '/field-defs', []),
struct_table(FIELD_DEF_ROWS),
slim_list('field_defs'), 'field_defs')
seq += 1
# 保存草稿
emit('保存%s草稿' % title, seq, 'POST', base + '/save-draft', 'FORM',
fblock(cfg['save_pairs']), 'POST ' + base + '/save-draft',
['保存草稿:新建 / 原位编辑草稿 / 从发布中停用生成新草稿(三态复用)。']
+ cfg['save_extra'],
table(cfg['save_in_rows'], ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/save-draft', cfg['save_pairs']),
'null', None, None)
seq += 1
# 发布
pub_rows = list(cfg['save_in_rows'])
pub_rows[0] = ('id', 'string', '', '-', '须传 save-draft 产出的草稿版本行 ID')
pub_pairs = [('id', '1')] + list(cfg['save_pairs'])
emit('发布' + title, seq, 'POST', base + '/publish', 'FORM',
fblock(pub_pairs), 'POST ' + base + '/publish',
['保存并发布:原发布中转停用、原默认取消默认、部门覆盖校验。',
'⚠ 发布将顶替同编码发布中版本;id 须替换为 save-draft 产出的草稿 id,'
'严禁对种子「%s」直接执行。' % cfg['seed']],
table(pub_rows, ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/publish', pub_pairs),
'null', None, None)
seq += 1
# 复制
emit('复制' + title, seq, 'POST', base + '/copy', 'FORM',
fblock([('id', tpl_id)]), 'POST ' + base + '/copy',
['复制:独立新%s(新编码 + V1.0 草稿%s)。' % (title, cfg['copy_extra']),
'对发布中示例 id 照抄 = 新建草稿副本(无害),可用「删除%s」练手清掉。' % title],
table(id_only('被复制版本行 ID'), ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/copy', [('id', tpl_id)]),
'string', '1', None)
seq += 1
# 停用
emit('停用' + title, seq, 'POST', base + '/disable', 'FORM',
fblock([('id', '1')]), 'POST ' + base + '/disable',
['停用:仅发布中可停用。',
'⚠ id 须替换为发布中版本行 id(勿误停种子「%s」)。' % cfg['seed']],
table(id_only('发布中版本行 ID'), ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/disable', [('id', '1')]),
'null', None, None)
seq += 1
# 删除
emit('删除' + title, seq, 'POST', base + '/delete', 'FORM',
fblock([('id', '1')]), 'POST ' + base + '/delete',
[cfg['delete_note'],
'⚠ id 须替换为草稿 id(可先用「复制%s」造一条草稿练手)。' % title],
table(id_only('草稿版本行 ID'), ('参数', '类型', '必填', '默认', '说明')),
ex_form('POST', base + '/delete', [('id', '1')]),
'null', None, None)
seq += 1
# 版本列表
emit(title + '版本列表', seq, 'GET', base + '/versions', 'GET',
qblock([('id', tpl_id)]), 'GET ' + base + '/versions',
['同族版本列表:按 %s 查所有版本。' % code_col,
'实体直出(无 DTO 包装),不含%s子表。' % cfg['ver_missing']],
table(id_only('任一版本行 ID(按其编码查全族)'), ('参数', '类型', '必填', '默认', '说明')),
ex_get(base + '/versions', [('id', tpl_id)]),
struct_table(cfg['ver_rows']),
slim_list(cfg['versions_key']), cfg['versions_key'])
FAMILIES = [
dict(
sub='公海规则', title='公海规则', base='/api/rule/opp-pool-rule',
code_col='ruleCode', row_noun='规则版本行列表',
page_key='pool_page', detail_key='pool_detail', versions_key='pool_versions',
tpl_id=POOL_ID, seed='e2e-seed公海规则(POOL_RULE_01)',
scope_desc='1所有部门通用 2对应部门专用', keyword_note='模糊搜索(匹配规则名称/规则编码/版本号)',
detail_extra='',
biz_rows=POOL_DTO_ROWS, ver_rows=POOL_VER_ROWS,
save_pairs=POOL_SAVE_PAIRS, save_in_rows=POOL_SAVE_IN,
save_extra=['V-CONFIG 语义:同 ruleCode 至多一草稿一发布中;'
'autoRecycleEnabled=0 时 recycleRemindEnabled 须显式传 0(省略按开启处理)。'],
copy_extra='', delete_note='删除:仅草稿可删。',
ver_missing=' deptIds/deptNames ',
),
dict(
sub='阶段模板', title='阶段模板', base='/api/rule/opp-stage-template',
code_col='templateCode', row_noun='模板版本行列表',
page_key='stage_page', detail_key='stage_detail', versions_key='stage_versions',
tpl_id=STAGE_ID, seed='全公司通用阶段模板(OPP_STAGE_TPL_01)',
scope_desc='1所有商机 2指定部门', keyword_note='模糊搜索(匹配模板名称/模板编码/版本号)',
detail_extra=' + 阶段节点有序列表',
biz_rows=STAGE_DTO_ROWS, ver_rows=STAGE_VER_ROWS,
save_pairs=STAGE_SAVE_PAIRS, save_in_rows=STAGE_SAVE_IN,
save_extra=['节点为索引式表单绑定(nodes[0].xxx = 第 1 个节点…),节点全量替换;',
'末位固定节点 OPP_STAGE_05(已转项目)系统保证,入参不可伪造(伪造被 64000 类校验拒绝)。'],
copy_extra=',节点随复制', delete_note='删除:仅草稿可删(级联删节点与适用部门)。',
ver_missing=' nodes/deptIds/deptNames ',
),
dict(
sub='方案卡模板', title='方案卡模板', base='/api/rule/opp-scheme-template',
code_col='templateCode', row_noun='模板版本行列表',
page_key='scheme_page', detail_key='scheme_detail', versions_key='scheme_versions',
tpl_id=SCHEME_ID, seed='全公司通用方案卡模板(OPP_SCHEME_TPL_04)',
scope_desc='1所有部门通用 2对应部门专用', keyword_note='模糊搜索(匹配模板名称/模板编码/版本号)',
detail_extra=' + 字段项有序列表',
biz_rows=SCHEME_DTO_ROWS, ver_rows=STAGE_VER_ROWS,
save_pairs=SCHEME_SAVE_PAIRS, save_in_rows=SCHEME_SAVE_IN,
save_extra=['字段项为索引式表单绑定(fields[0].xxx = 第 1 个字段项…),字段项全量替换,'
'fieldKey 必须取「系统字段库列表」实测值,非自由造;',
'fieldName/fieldType 入参忽略(取系统字段快照回显),isVisible=0 会清必填。'],
copy_extra=',字段配置随复制', delete_note='删除:仅草稿可删(级联删字段项与适用部门)。',
ver_missing=' fields/deptIds/deptNames ',
field_defs=True,
),
]
# ---------- 自定义视图生成 ----------
def build_views():
parent = os.path.join(MOD_ROOT, '销售机会')
write_folder(parent, '自定义视图')
fdir = os.path.join(parent, '自定义视图')
base = '/api/preference/view'
def emit(name, seq, method, path, transport, param_text, key_line,
intro, req_md, ex_md, struct_md, resp_data, resp_key):
comment = measured(resp_key) if resp_key else MECH
docs = make_docs(name, key_line, intro, req_md, ex_md, struct_md,
comment, j2(env(resp_data)))
write_file(os.path.join(fdir, name + '.bru'),
make_bru(name, seq, method, path, transport, param_text, docs))
param_table = lambda rows: table(rows, ('参数', '类型', '必填', '默认', '说明'))
# 1 列表
emit('自定义视图列表', 1, 'GET', base + '/list', 'GET',
qblock([('scopeKey', 'opportunity')]), 'GET ' + base + '/list',
['当前用户在某 scope 下的自建视图(内置视图不走本接口),按 seqNo 排序。',
'出参不回显 scopeKey;商机域实测 scopeKey=opportunity。'],
param_table([('scopeKey', 'string', '', '-', '视图域标识;商机域实测传 opportunity')]),
ex_get(base + '/list', [('scopeKey', 'opportunity')]),
struct_table(VIEW_ROWS),
slim_list('view_list'), 'view_list')
# 2 保存(query scopeKey + body:json)
vjson = j2(VIEW_SAVE_OBJ)
emit('保存自定义视图', 2, 'POST', base + '/save', 'JSON',
qblock([('scopeKey', 'opportunity')]) + '\n\n' + jblock(vjson),
'POST ' + base + '/save',
['保存自定义视图:viewId 空=新建,非空=覆盖编辑(须回传原 viewId);'
'isDefault 单值互斥(设默认自动取消原默认)。',
'scopeKey 走 query 参数,JSON body 才是视图本体;商机域实测 scopeKey=opportunity。'],
param_table([
('scopeKey', 'string', '', '-', 'query 参数:视图域标识(opportunity)'),
('viewId', 'string', '', '-', 'JSON body:空=新建;非空=覆盖编辑'),
('name', 'string', '', '-', 'JSON body:检索名称(用户填)'),
('conditions', 'array', '', '-', 'JSON body:检索条件列表(本期前端只提交 1 组)'),
('conditions[].field', 'string', '条件', '-', '字段 key(商机字段池稳定 code,如 opportunityName)'),
('conditions[].operator', 'string', '条件', '-', '操作符(如 eq 等于 / like 包含)'),
('conditions[].value', 'string', '条件', '-', '条件值'),
('sortField', 'string', '', '-', 'JSON body:排序字段 key(可空)'),
('sortDirection', 'string', '', '-', 'JSON body:排序方向 asc/desc(可空)'),
('isDefault', 'boolean', '', 'false', 'JSON body:是否默认视图(单值互斥)'),
('seqNo', 'number', '', '-', 'JSON body:列表排序序号'),
]),
ex_json(base + '/save', [('scopeKey', 'opportunity')], vjson),
struct_table([('viewId', 'string', '新建或覆盖后视图的 viewId(服务端生成)')]),
VIEW_ID, None)
# 3 删除 / 4 设为默认
del_pairs = [('scopeKey', 'opportunity'), ('viewId', '1')]
for name, path, intro in [
('删除自定义视图', base + '/delete',
['删除本人名下自定义视图。',
'⚠ viewId 须替换为本人视图 id(「自定义视图列表」可查;勿对他人/种子视图执行)。']),
('设为默认视图', base + '/set-default',
['图钉:设为该 scope 的默认视图(单值互斥)。',
'⚠ viewId 须替换为本人视图 id(「自定义视图列表」可查;勿对他人/种子视图执行)。']),
]:
emit(name, 3 if path.endswith('delete') else 4, 'POST', path, 'FORM',
fblock(del_pairs), 'POST ' + path,
intro,
param_table([
('scopeKey', 'string', '', '-', '视图域标识(opportunity)'),
('viewId', 'string', '', '-', '视图 ID(须替换为本人视图 id)'),
]),
ex_form('POST', path, del_pairs),
'null', None, None)
# ---------- 主流程 ----------
if __name__ == '__main__':
for cfg in FAMILIES:
build_family(cfg)
build_views()
print('created:', len(CREATED), ' rewritten:', len(REWRITTEN), ' skipped:', len(SKIPPED))
for rel in CREATED:
print(' +', rel)
for rel in REWRITTEN:
print(' ~', rel, '(--force 重写)')
for rel in SKIPPED:
print(' =', rel, '(已存在,跳过)')
bom = []
for rel in CREATED + REWRITTEN:
with open(os.path.join(DOCS_ROOT, rel), 'rb') as f:
if f.read(3) == b'\xef\xbb\xbf':
bom.append(rel)
if bom:
print('!! BOM 文件:', bom)
sys.exit(1)
total = sum(1 for dp, dn, fn in os.walk(MOD_ROOT)
for x in fn if x.endswith('.bru') and x != 'folder.bru')
print('商机模块 .bru 接口文件总数:', total)