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.

266 lines
10 KiB

1 week ago
# -*- coding: utf-8 -*-
"""
Generate Bruno .bru files for crm-opportunity module into D:/code/crm-api-docs.
All files are page-tag-based (config grouping = tag).
Strictly follows the bruno-sync SKILL template.
"""
import io, os, sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DOCS = 'D:/code/crm-api-docs'
# ---------- utility ----------
def ensure_dir(p):
os.makedirs(p, exist_ok=True)
def write_bru(folder_rel, filename, content):
d = os.path.join(DOCS, folder_rel)
ensure_dir(d)
p = os.path.join(d, filename)
with open(p, 'w', encoding='utf-8', newline='\n') as f:
f.write(content)
print(f' wrote {folder_rel}/{filename}')
def write_folder_bru(folder_rel, name, seq):
d = os.path.join(DOCS, folder_rel)
ensure_dir(d)
p = os.path.join(d, 'folder.bru')
if os.path.exists(p):
return
with open(p, 'w', encoding='utf-8', newline='\n') as f:
f.write(f'meta {{\n name: {name}\n seq: {seq}\n}}\n')
print(f' wrote {folder_rel}/folder.bru')
# ---------- template pieces ----------
def bru_form(seq, name, method, url, params_rows, docs_body):
"""
method: 'post' / 'get'
params_rows: list of (key, example) for form / query block
docs_body: full docs { ... } inner (without wrapping braces)
"""
body_kw = 'formUrlEncoded' if method == 'post' else 'none'
lines = []
lines.append('meta {')
lines.append(f' name: {name}')
lines.append(' type: http')
lines.append(f' seq: {seq}')
lines.append(' tags: [')
lines.append(' generated')
lines.append(' ]')
lines.append('}')
lines.append('')
lines.append(f'{method} {{')
lines.append(f' url: {{{{baseUrl}}}}{url}')
lines.append(f' body: {body_kw}')
lines.append(' auth: inherit')
lines.append('}')
lines.append('')
if method == 'post' and params_rows:
lines.append('body:form-urlencoded {')
for k, v in params_rows:
lines.append(f' {k}: {v}')
lines.append('}')
lines.append('')
elif method == 'get' and params_rows:
lines.append('params:query {')
for k, v in params_rows:
lines.append(f' {k}: {v}')
lines.append('}')
lines.append('')
lines.append('docs {')
for line in docs_body.rstrip().split('\n'):
lines.append(' ' + line if line else '')
lines.append('}')
return '\n'.join(lines) + '\n'
def build_docs(title, method, path, desc, params_table, req_example, resp_data_table, resp_example_json_or_text, resp_is_text=False):
"""
params_table: list of (name, type, required, default, desc)
resp_data_table: list of (field, type, desc); or None for null; or a special marker
resp_example_json_or_text: str (either JSON block content without fences, or a plain sentence when resp_is_text=True)
"""
lines = []
lines.append(f'# {title}')
lines.append(f'`{method} {path}`')
lines.append(desc)
lines.append('')
lines.append('## 请求参数')
lines.append('')
if params_table:
lines.append('| 参数 | 类型 | 必填 | 默认 | 说明 |')
lines.append('| --- | --- | --- | --- | --- |')
for name, typ, req, dflt, d in params_table:
lines.append(f'| {name} | {typ} | {req} | {dflt} | {d} |')
else:
lines.append('无参数。')
lines.append('')
lines.append('## 请求示例')
lines.append('')
for line in req_example.rstrip().split('\n'):
lines.append(line)
lines.append('')
lines.append('## 响应 data 结构')
lines.append('')
if resp_data_table is None:
lines.append('null')
elif isinstance(resp_data_table, str):
lines.append(resp_data_table)
else:
lines.append('| 字段 | 类型 | 说明 |')
lines.append('| --- | --- | --- |')
for f, t, d in resp_data_table:
lines.append(f'| {f} | {t} | {d} |')
lines.append('')
lines.append('## 响应示例')
lines.append('')
if resp_is_text:
lines.append(resp_example_json_or_text)
else:
lines.append('// 示例数据,字段结构以上表为准,非真实返回')
lines.append('```json')
for line in resp_example_json_or_text.rstrip().split('\n'):
lines.append(line)
lines.append('```')
return '\n'.join(lines)
def env_json(data_literal):
"""Wrap data literal (already indented as body) in the standard success envelope."""
return (
'{\n'
' "code": 0,\n'
' "success": true,\n'
' "message": "success",\n'
f' "data": {data_literal}\n'
'}'
)
def req_form_example(method, path, kv_pairs, public=False):
header = '请求头:无需鉴权' if public else '请求头:Authorization: Bearer {{token}}'
body = '&'.join(f'{k}={v}' for k, v in kv_pairs)
return (
f'{header}\n'
f'Content-Type: application/x-www-form-urlencoded\n\n'
f'{method} {path}\n'
f'{body}'
)
def req_query_example(method, path, kv_pairs):
header = '请求头:Authorization: Bearer {{token}}'
if kv_pairs:
qs = '&'.join(f'{k}={v}' for k, v in kv_pairs)
return f'{header}\n\n{method} {path}?{qs}'
return f'{header}\n\n{method} {path}'
# ---------- top-level folder scaffolds ----------
print('== folders ==')
write_folder_bru('商机管理', '商机管理', 20)
write_folder_bru('销售机会', '销售机会', 21)
write_folder_bru('商机公海', '商机公海', 22)
# sub-folders under 商机管理
for name in ['商机新建', '商机详情', '商机流转', '协作', '方案卡', '商机阶段', '详情子域']:
write_folder_bru(f'商机管理/{name}', name, 10)
# sub-folders under 销售机会
for i, name in enumerate(['最近访问', '我负责的', '我参与的', '我关注的'], start=10):
write_folder_bru(f'销售机会/{name}', name, i)
# ================================================================
# Reusable pieces — big response tables
# ================================================================
# OpportunityDTO fields (used for detail & inside page/PageResult content)
OPP_DTO_ROWS = [
('id', 'string', '商机ID(Long,序列化为 string)'),
('createTime', 'string', '创建时间 yyyy-MM-dd HH:mm:ss'),
('updateTime', 'string', '更新时间 yyyy-MM-dd HH:mm:ss'),
('oppName', 'string', '商机名称'),
('oppSource', 'string', '商机来源字典 code'),
('oppType', 'string', '商机类型字典 code'),
('industryCode', 'string', '行业字典 code'),
('bidForm', 'string', '招标形式字典 code'),
('localityType', 'string', '项目属地字典 code'),
('partyAClear', 'number', '甲方是否明确 0/1'),
('partyA', 'string', '甲方名称'),
('provinceCode', 'string', '省份国标 code'),
('cityCode', 'string', '市/区国标 code'),
('remark', 'string', '备注'),
('projectAmount', 'number', '项目金额(列表展示为「预计金额」)'),
('ownerUserId', 'string', '负责人ID(Long,序列化为 string)'),
('ownerNameSnapshot', 'string', '负责人姓名快照'),
('ownerDeptId', 'string', '负责人部门ID快照'),
('poolReason', 'string', '进入公海原因字典 code'),
('oppStatus', 'number', '商机状态(轴1:1待领取/2推进中/3暂缓中/4已关闭/5已转项目)'),
('currentStageId', 'string', '当前阶段节点ID(轴2)'),
('stageTemplateVersion', 'string', '阶段模板版本号冗余'),
('sourceLeadId', 'string', '来源线索ID'),
('primaryCustomerId', 'string', '主要意向客户ID'),
('primaryCustomerNameSnapshot', 'string', '主客户名快照(列表「意向客户」列)'),
('claimTime', 'string', '领取时刻'),
('lastValidFollowTime', 'string', '最近有效跟进时间'),
('oppSourceName', 'string', '商机来源名称(出参回显,字典快照)'),
('industryName', 'string', '行业名称(出参回显,字典快照)'),
('provinceName', 'string', '省份名称(出参回显)'),
('cityName', 'string', '市/区名称(出参回显)'),
('ownerDeptName', 'string', '负责人部门名称(出参回显)'),
('currentStageName', 'string', '当前工作节点名称(出参回显)'),
('stageStayDays', 'number', '节点停留时间(天,出参回显)'),
('nextStageName', 'string', '下一节点名称(出参回显,展示用建议下一步)'),
('lastFollowTime', 'string', '最近跟进时间(出参回显)'),
('schemeCardStatus', 'number', '方案卡状态(出参回显;无主客户卡=未创建0)'),
('schemeBudget', 'number', '方案预算(出参回显)'),
('followed', 'boolean', '当前用户是否已关注(出参回显)'),
('poolStageNameSnapshot', 'string', '入池前工作节点名称(出参回显,公海视图专属)'),
]
OPP_DTO_JSON = (
'{\n'
' "id": "1",\n'
' "createTime": "2024-01-01 12:00:00",\n'
' "updateTime": "2024-01-01 12:00:00",\n'
' "oppName": "示例商机",\n'
' "oppSource": "opp_source_01",\n'
' "oppType": "opp_type_01",\n'
' "industryCode": "industry_01",\n'
' "bidForm": "bid_01",\n'
' "localityType": "local_01",\n'
' "partyAClear": 1,\n'
' "partyA": "示例甲方",\n'
' "provinceCode": "110000",\n'
' "cityCode": "110100",\n'
' "remark": "备注示例",\n'
' "projectAmount": 1000000,\n'
' "ownerUserId": "1",\n'
' "ownerNameSnapshot": "张三",\n'
' "ownerDeptId": "1",\n'
' "poolReason": null,\n'
' "oppStatus": 2,\n'
' "currentStageId": "1",\n'
' "stageTemplateVersion": "v1",\n'
' "sourceLeadId": "1",\n'
' "primaryCustomerId": "1",\n'
' "primaryCustomerNameSnapshot": "示例客户",\n'
' "claimTime": "2024-01-01 12:00:00",\n'
' "lastValidFollowTime": "2024-01-02 10:00:00",\n'
' "oppSourceName": "线索转入",\n'
' "industryName": "示例行业",\n'
' "provinceName": "北京市",\n'
' "cityName": "北京市",\n'
' "ownerDeptName": "华北大区",\n'
' "currentStageName": "需求确认",\n'
' "stageStayDays": 3,\n'
' "nextStageName": "方案报价",\n'
' "lastFollowTime": "2024-01-02 10:00:00",\n'
' "schemeCardStatus": 1,\n'
' "schemeBudget": 800000,\n'
' "followed": true,\n'
' "poolStageNameSnapshot": null\n'
' }'
)
print('done setup, will emit in next files')