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.
 
 
 
 
 

837 lines
51 KiB

# -*- coding: utf-8 -*-
"""bruno-sync 冷启动全量生成:crm-backend-matt 源码 → e:/code/crm-api-docs
按 SKILL.md Step 2 模板生成;grouping=tag,无 tag 接口回退 urlPattern。
"""
import json, os
WSROOT = r"e:\code\crm-api-docs" # Bruno 3 workspace 根(workspace.yml 所在)
ROOT = os.path.join(WSROOT, "collections", "crm-api-docs") # collection 根(bruno.json 所在)
SEQ = {}
import shutil
# Bruno 3 workspace 脚手架幂等确保(skill first-run 产出,已存在则不碰)
os.makedirs(os.path.join(WSROOT, "collections"), exist_ok=True)
def _ensure(rel, text):
_p = os.path.join(WSROOT, rel)
if not os.path.isfile(_p):
os.makedirs(os.path.dirname(_p), exist_ok=True)
with open(_p, "w", encoding="utf-8", newline="\n") as _f:
_f.write(text)
_ensure("workspace.yml",
'opencollection: 1.0.0\ninfo:\n name: "crm-api-docs"\n type: workspace\n\n'
'collections:\n - name: "crm-api-docs"\n path: "collections/crm-api-docs"\n')
_ensure("collections/crm-api-docs/bruno.json",
'{"version":"1","name":"crm-api-docs","type":"collection","ignore":["node_modules",".git"]}')
_ensure("collections/crm-api-docs/environments/local.bru",
"vars {\n baseUrl: http://localhost:8080\n token: \n}\n")
# 菜单化重组(ADR-0024)后清理旧 generated 文件夹,再全量重建
for _old in ("线索管理", "列偏好", "线索规则-公海池配置"):
_p = os.path.join(ROOT, _old)
if os.path.isdir(_p):
shutil.rmtree(_p)
def w(rel, text):
full = os.path.join(ROOT, rel.replace("/", os.sep))
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
def env(data=None, msg="success"):
d = {"code": 0, "success": True, "message": msg, "data": data}
return json.dumps(d, ensure_ascii=False, indent=2)
def table(headers, rows):
out = ["| " + " | ".join(headers) + " |", "|" + "|".join([" --- "] * len(headers)) + "|"]
out += ["| " + " | ".join(str(c) for c in r) + " |" for r in rows]
return "\n".join(out)
PH = ["参数", "类型", "必填", "默认", "说明"]
RH = ["字段", "类型", "说明"]
# ---------- 公共参数/结构 ----------
BASE_PARAM = [ # BaseParam
("current", "int", "", "1", "当前页码,从 1 开始", "1"),
("size", "int", "", "10", "每页条数,上限 500", "10"),
("keyword", "string", "", "-", "模糊搜索关键字,匹配列由接口定义", ""),
("orderBy", "string", "", "-", "排序字段,驼峰或下划线均可", "createTime"),
("asc", "boolean", "", "false", "是否升序(默认新数据在前)", "false"),
]
LEAD_PAGE_PARAM = [
("viewType", "string", "", "MANAGE", "视图类型:PUBLIC_POOL/MY_LEAD/MY_FOLLOW/MANAGE,空/非法值回落 MANAGE", "PUBLIC_POOL"),
("statusIn", "int[]", "", "-", "线索状态多值筛选(逗号分隔,如「已被领取」传 3,4)", "3,4"),
("poolId", "string", "", "-", "公海池ID筛选", "1"),
("deptId", "string", "", "-", "归属部门ID筛选", "1"),
("isUrgent", "int", "", "-", "是否加急筛选(0/1)", "1"),
("feedbackStatus", "int", "", "-", "反馈情况筛选(1=有效 2=无效 3=未反馈)", "1"),
("channelCode", "string", "", "-", "渠道字典 code 筛选", "channel_web"),
("brandCode", "string", "", "-", "品牌字典 code 筛选", "brand_a"),
("productCode", "string", "", "-", "需求产品字典 code 筛选", "product_b"),
("provinceCode", "string", "", "-", "省份国标 code 筛选", "110000"),
]
LEAD_WRITE_FIELDS = [ # LeadDTO 写入侧(create/edit 表单)
("leadName", "string", "", "-", "线索名称", "张三的咨询"),
("phone", "string", "", "-", "联系电话", "13800000000"),
("wechat", "string", "", "-", "微信号", "wx_zhang"),
("email", "string", "", "-", "邮箱", "zhang@example.com"),
("provinceCode", "string", "", "-", "省份国标 code", "110000"),
("cityCode", "string", "", "-", "市/区国标 code", "110100"),
("address", "string", "", "-", "详细地址", "朝阳区xx路1号"),
("channelCode", "string", "", "-", "渠道字典 code", "channel_web"),
("brandCode", "string", "", "-", "品牌字典 code", "brand_a"),
("productCode", "string", "", "-", "需求产品字典 code", "product_b"),
("sceneCode", "string", "", "-", "需求场景字典 code", "scene_c"),
("consultContent", "string", "", "-", "咨询内容", "想了解产品报价"),
("isUrgent", "int", "", "-", "是否加急 0/1", "1"),
]
LEAD_RESP_ROWS = [ # LeadDTO 出参骨架
("id", "string", "线索ID(雪花,字符串下发)"),
("createTime", "datetime", "创建时间"),
("updateTime", "datetime", "更新时间"),
("leadName", "string", "线索名称"),
("phone", "string", "联系电话"),
("wechat", "string", "微信号"),
("email", "string", "邮箱"),
("provinceCode", "string", "省份国标 code"),
("cityCode", "string", "市/区国标 code"),
("address", "string", "详细地址"),
("channelCode", "string", "渠道字典 code"),
("brandCode", "string", "品牌字典 code"),
("productCode", "string", "需求产品字典 code"),
("sceneCode", "string", "需求场景字典 code"),
("consultContent", "string", "咨询内容"),
("status", "int", "线索状态(1-7:1 未分发 2 待领取 3 已领取 4 跟进中 5 已转商机 6 过期失效 7 线索作废)"),
("isUrgent", "int", "是否加急 0/1"),
("poolId", "string", "所属公海池ID"),
("poolNameSnapshot", "string", "公海池名称快照"),
("deptId", "string", "归属部门ID"),
("ownerUserId", "string", "领取人ID"),
("ownerNameSnapshot", "string", "领取人姓名快照"),
("claimTime", "datetime", "领取时刻"),
("recycleDeadline", "datetime", "回收截止时刻"),
("expireDeadline", "datetime", "失效截止时刻"),
("feedbackStatus", "int", "最新反馈情况:1=有效 2=无效 3=未反馈"),
("feedbackContent", "string", "最新反馈内容"),
("feedbackTime", "datetime", "最新反馈时刻"),
("provinceName", "string", "省份名称(出参回显)"),
("cityName", "string", "市/区名称(出参回显)"),
("followCount", "int", "关注人数(出参回显,现算)"),
("followed", "boolean", "当前用户是否已关注(出参回显)"),
]
LEAD_EXAMPLE = {
"id": "1", "createTime": "2026-08-14 10:00:00", "updateTime": "2026-08-14 10:00:00",
"leadName": "张三的咨询", "phone": "13800000000", "wechat": "wx_zhang",
"email": "zhang@example.com", "provinceCode": "110000", "cityCode": "110100",
"address": "朝阳区xx路1号", "channelCode": "channel_web", "brandCode": "brand_a",
"productCode": "product_b", "sceneCode": "scene_c", "consultContent": "想了解产品报价",
"status": 2, "isUrgent": 1, "poolId": "1", "poolNameSnapshot": "华北公海池",
"deptId": "10", "ownerUserId": "100", "ownerNameSnapshot": "李四",
"claimTime": "2026-08-14 10:00:00", "recycleDeadline": "2026-08-21 10:00:00",
"expireDeadline": "2026-09-13 10:00:00", "feedbackStatus": 3,
"feedbackContent": None, "feedbackTime": None,
"provinceName": "北京市", "cityName": "北京市", "followCount": 0, "followed": False,
}
def page_rows(item_rows):
return [("content[]", "array<" + item_rows[0][0].split("[]")[0] + ">", "数据列表(元素结构见下)")] if False else None
# 四菜单副本绑定(与 bruno-sync.config.json menuBindings 同源,ADR-0024)
MENUS = [
("线索管理/线索公海", "PUBLIC_POOL", "lead.public_pool", "公海"),
("线索管理/我的线索", "MY_LEAD", "lead.my_lead", "我的线索"),
("线索管理/我的关注", "MY_FOLLOW", "lead.my_follow", "我的关注"),
("线索管理/线索管理", "MANAGE", "lead.manage", "线索管理"),
]
MENU_EXPAND = {"线索管理": MENUS, "列偏好": MENUS} # 触发键:folder 命中则四菜单各出一份副本
def bindv(params, pname, value):
"""把 params 中 pname 行的示例值替换为菜单绑定值,说明追加「本页固定传」"""
if not params or value is None:
return params
return [(p[0], p[1], p[2], p[3], p[4] + "(本页固定传 %s" % value, value) if p[0] == pname else p
for p in params]
def bru(folder, fname, name, method, url, kind="none", params=None, desc="",
resp_rows=None, data_example="__NULL__", req_auth="inherit", multipart_file=None,
nonenvelope_note=None, login_script=False):
"""kind: query | form | multipart | none;params=[(name,type,req,default,note,example)]
folder 命中 MENU_EXPAND 时按四菜单各出一份副本,viewType/scopeKey 绑定为菜单专属值。"""
if folder in MENU_EXPAND:
for mfolder, vt, sk, label in MENU_EXPAND[folder]:
p2 = bindv(bindv(params, "viewType", vt), "scopeKey", sk)
d2 = "%s 页】%s" % (label, desc)
if params and any(pp[0] == "viewType" for pp in params):
d2 += " 本页 viewType 固定 %s" % vt
_write_one(mfolder, fname, name, method, url, kind, p2, d2, resp_rows,
data_example, req_auth, multipart_file, nonenvelope_note, login_script)
return
_write_one(folder, fname, name, method, url, kind, params, desc, resp_rows,
data_example, req_auth, multipart_file, nonenvelope_note, login_script)
def _write_one(folder, fname, name, method, url, kind="none", params=None, desc="",
resp_rows=None, data_example="__NULL__", req_auth="inherit", multipart_file=None,
nonenvelope_note=None, login_script=False):
seq = SEQ.get(folder, 0) + 1
SEQ[folder] = seq
lines = []
lines.append("meta {")
lines.append(" name: %s" % name)
lines.append(" type: http")
lines.append(" seq: %d" % seq)
lines.append(" tags: [")
lines.append(" generated")
lines.append(" ]")
lines.append("}")
lines.append("")
m = method.lower()
body_kind = {"query": "none", "form": "formUrlEncoded", "multipart": "multipartForm", "none": "none"}[kind]
lines.append("%s {" % m)
lines.append(" url: {{baseUrl}}%s" % url)
lines.append(" body: %s" % body_kind)
lines.append(" auth: %s" % req_auth)
lines.append("}")
lines.append("")
if kind == "query" and params:
lines.append("params:query {")
for p in params:
lines.append(" %s: %s" % (p[0], p[5]))
lines.append("}")
lines.append("")
if kind == "form" and params:
lines.append("body:form-urlencoded {")
for p in params:
lines.append(" %s: %s" % (p[0], p[5]))
lines.append("}")
lines.append("")
if kind == "multipart" and params:
lines.append("body:multipart-form {")
for p in params:
if p[0] == multipart_file:
lines.append(" %s: @本地文件" % p[0])
else:
lines.append(" %s: %s" % (p[0], p[5]))
lines.append("}")
lines.append("")
if login_script:
lines.append("script:post-response {")
lines.append(" if (res.body && res.body.success) {")
lines.append(' bru.setEnvVar("token", res.body.data.token);')
lines.append(" }")
lines.append("}")
lines.append("")
# ---- docs ----
lines.append("docs {")
lines.append(" # %s" % name)
lines.append(" `%s %s`" % (method.upper(), url))
lines.append(" %s" % desc)
lines.append("")
if params:
lines.append(" ## 请求参数")
lines.append("")
doc_table = " " + table(PH, [(p[0], p[1], p[2], p[3], p[4]) for p in params])
lines.append(doc_table.replace("\n", "\n "))
lines.append("")
lines.append(" ## 请求示例")
lines.append("")
auth_line = " 请求头:无需鉴权" if req_auth == "none" else " 请求头:Authorization: Bearer {{token}}"
lines.append(auth_line)
if kind == "query":
qs = "&".join("%s=%s" % (p[0], p[5]) for p in params)
lines.append("")
lines.append(" GET %s?%s" % (url, qs))
elif kind == "form":
lines.append(" Content-Type: application/x-www-form-urlencoded")
lines.append("")
lines.append(" POST %s" % url)
lines.append(" " + "&".join("%s=%s" % (p[0], p[5]) for p in params))
elif kind == "multipart":
lines.append(" Content-Type: multipart/form-data")
lines.append("")
lines.append(" POST %s" % url)
for p in params:
if p[0] == multipart_file:
lines.append(" %s=@本地文件" % p[0])
else:
lines.append(" %s=%s" % (p[0], p[5]))
lines.append("")
lines.append(" ## 响应 data 结构")
lines.append("")
if nonenvelope_note is not None:
lines.append(" %s" % nonenvelope_note)
lines.append("")
elif resp_rows is None:
lines.append(" null")
lines.append("")
else:
lines.append(" " + table(RH, resp_rows).replace("\n", "\n "))
lines.append("")
lines.append(" ## 响应示例")
lines.append("")
if nonenvelope_note is not None:
lines.append(" %s" % nonenvelope_note)
elif data_example == "__NULL__":
lines.append(" // 示例数据,字段结构以上表为准,非真实返回")
lines.append(" " + env(None).replace("\n", "\n "))
else:
lines.append(" // 示例数据,字段结构以上表为准,非真实返回")
lines.append(" " + env(data_example).replace("\n", "\n "))
lines.append("}")
w(folder + "/" + fname + ".bru", "\n".join(lines) + "\n")
def page_of(item_example, item_rows, item_name):
"""构造 PageResult 的 resp_rows + data_example"""
rows = [
("content[]", "array<object>", "%s 列表(元素字段见下,缩进行)" % item_name),
] + [(" " + r[0], r[1], r[2]) for r in item_rows] + [
("total", "long", "总条数"),
("size", "long", "每页条数"),
("current", "long", "当前页码(从 1 开始)"),
("pages", "long", "总页数"),
("empty", "boolean", "是否为空结果(total == 0)"),
]
data = {"content": [item_example], "total": 1, "size": 10, "current": 1, "pages": 1, "empty": False}
return rows, data
BATCH_LEAD_ROWS = [
("total", "int", "处理总数(successCount + failCount)"),
("successCount", "int", "成功条数"),
("failCount", "int", "失败条数"),
("failures[]", "array<object>", "失败明细列表"),
(" id", "string", "失败的线索 id"),
(" reason", "string", "失败原因(语义枚举):STATUS_NOT_ALLOWED 状态不允许 / NOT_OWNER 非持有人 / CLAIM_RULE_DENIED 领取规则拒绝 / OVER_DAILY_LIMIT 超日领上限 / OVER_HOLD_LIMIT 超持有上限 / ALREADY_CONVERTED 已转商机 / CONCURRENT_MODIFIED 并发冲突 / UNKNOWN 未归类"),
(" message", "string", "失败文案"),
]
BATCH_LEAD_EXAMPLE = {
"total": 2, "successCount": 1, "failCount": 1,
"failures": [{"id": "2", "reason": "STATUS_NOT_ALLOWED", "message": "当前线索状态不允许该操作"}],
}
# ==================== 线索管理(23) ====================
F = "线索管理"
bru(F, "线索分页", "线索分页", "POST", "/api/lead/page", "form", BASE_PARAM + LEAD_PAGE_PARAM,
"线索分页(四视图:PUBLIC_POOL 公海 / MY_LEAD 我的线索 / MY_FOLLOW 我的关注 / MANAGE 线索管理),数据隔离由 @DataScope 自动注入。",
*page_of(LEAD_EXAMPLE, LEAD_RESP_ROWS, "LeadDTO"))
bru(F, "线索详情", "线索详情", "GET", "/api/lead/detail", "query",
[("id", "string", "", "-", "线索ID", "1")],
"线索详情。", LEAD_RESP_ROWS, LEAD_EXAMPLE)
bru(F, "历史记录", "历史记录", "GET", "/api/lead/history", "query",
[("leadId", "string", "", "-", "线索ID", "1")],
"线索操作历史(只展示 5 种:CLAIM 领取 / RELEASE 释放 / FEEDBACK 反馈 / CONVERT 转商机 / EDIT 编辑)。",
[
("[]", "array<object>", "历史记录列表"),
(" id", "string", "日志ID"),
(" leadId", "string", "线索ID"),
(" opType", "string", "操作类型(CLAIM/RELEASE/FEEDBACK/CONVERT/EDIT)"),
(" opTime", "datetime", "操作时间"),
(" opUserId", "string", "操作人ID"),
(" opUserName", "string", "操作人姓名(出参回显)"),
(" opUserDeptName", "string", "操作人部门名称快照"),
(" detail", "string", "操作详情(JSON 文本)"),
],
[{"id": "11", "leadId": "1", "opType": "CLAIM", "opTime": "2026-08-14 10:00:00",
"opUserId": "100", "opUserName": "李四", "opUserDeptName": "销售一部", "detail": "{}"}])
bru(F, "新建线索", "新建线索", "POST", "/api/lead/create", "form",
LEAD_WRITE_FIELDS + [("claimOnCreate", "boolean", "", "false", "true=销售新增即领取(已领取态)", "false")],
"新建线索;claimOnCreate=true 时创建即领取。",
[("(data)", "string", "新线索ID(雪花,字符串下发)")], "9007199254740993")
bru(F, "编辑线索", "编辑线索", "POST", "/api/lead/edit", "form",
[("id", "string", "", "-", "线索ID", "1")] + LEAD_WRITE_FIELDS,
"编辑线索(非终态可编辑)。", None)
bru(F, "删除线索", "删除线索", "POST", "/api/lead/delete", "form",
[("id", "string", "", "-", "线索ID", "1")],
"删除线索(已转商机禁止删除)。", None)
bru(F, "领取线索", "领取线索", "POST", "/api/lead/claim", "form",
[("id", "string", "", "-", "线索ID", "1")],
"领取线索(待领取 → 已领取)。", None)
bru(F, "分配线索到池", "分配线索到池", "POST", "/api/lead/assign-pool", "form",
[("id", "string", "", "-", "线索ID", "1"), ("poolId", "string", "", "-", "目标公海池ID", "1")],
"分配线索到池(未分发 → 待领取)。", None)
bru(F, "分配线索到销售", "分配线索到销售", "POST", "/api/lead/assign-user", "form",
[("id", "string", "", "-", "线索ID", "1"), ("userId", "string", "", "-", "目标销售用户ID", "100")],
"分配线索到销售(未分发/待领取 → 已领取;作废态可自环重新分配)。", None)
FEEDBACK_FIELDS = [
("id", "string", "", "-", "反馈记录ID(编辑草稿时传)", "1"),
("leadId", "string", "", "-", "线索ID", "1"),
("feedbackStatus", "int", "提交必填", "-", "反馈情况:1=有效 2=无效", "1"),
("content", "string", "", "-", "反馈内容", "客户有明确意向"),
("productCode", "string", "", "-", "需求产品字典 code", "product_b"),
("attachmentIds", "string(json数组)", "", "-", "附件文件ID列表(JSON 数组字符串)", "[\"9001\"]"),
]
bru(F, "保存反馈草稿", "保存反馈草稿", "POST", "/api/lead/feedback-draft", "form", FEEDBACK_FIELDS,
"保存反馈草稿(不改状态机,可反复保存)。", None)
bru(F, "提交反馈", "提交反馈", "POST", "/api/lead/feedback-submit", "form", FEEDBACK_FIELDS,
"提交反馈(已领取 → 跟进中;反馈无效时 → 线索作废)。", None)
bru(F, "转商机", "转商机", "POST", "/api/lead/convert", "form",
[("id", "string", "", "-", "线索ID", "1"),
("opportunityName", "string", "", "-", "商机名称", "张三-产品B采购意向"),
("industryCode", "string", "", "-", "行业字典 code(opportunity.industry)", "industry_mfg"),
("partyA", "string", "", "-", "甲方(自由文本)", "某某公司"),
("intendedCustomer", "string", "", "-", "意向客户(自由文本)", "张三"),
("regionCode", "string", "", "-", "地区国标 code", "110100"),
("remark", "string", "", "-", "备注", "-")],
"线索转商机(已领取/跟进中 → 已转商机)。", None)
bru(F, "释放线索", "释放线索", "POST", "/api/lead/release", "form",
[("id", "string", "", "-", "线索ID", "1")],
"释放线索(已领取/跟进中 → 待领取,回公海池)。", None)
bru(F, "激活线索", "激活线索", "POST", "/api/lead/activate", "form",
[("id", "string", "", "-", "线索ID", "1")],
"激活线索(过期失效 → 恢复失效前状态,计时重置)。", None)
bru(F, "关注线索", "关注线索", "POST", "/api/lead/follow", "form",
[("id", "string", "", "-", "线索ID", "1")],
"关注线索(进入「我的关注」视图)。", None)
bru(F, "取消关注", "取消关注", "POST", "/api/lead/unfollow", "form",
[("id", "string", "", "-", "线索ID", "1")],
"取消关注。", None)
bru(F, "视图统计卡片", "视图统计卡片", "POST", "/api/lead/stats", "form", BASE_PARAM + LEAD_PAGE_PARAM,
"视图统计卡片:与 /page 同一套筛选口径(viewType + 筛选 + @DataScope 部门天花板),按状态分组计数;current/size 不参与统计。",
[
("total", "int", "线索总量"),
("claimed", "int", "已被领取(status IN 已领取/跟进中)"),
("converted", "int", "已转商机"),
("todayNew", "int", "今日新增"),
("undistributed", "int", "未分发"),
],
{"total": 100, "claimed": 40, "converted": 10, "todayNew": 5, "undistributed": 20})
# ---- 线索批量(ADR-0023:非原子、逐条 CAS、部分成功) ----
IDS = [("ids", "string[]", "", "-", "线索ID列表(逗号分隔)", "1,2")]
bru(F, "批量领取", "批量领取", "POST", "/api/lead/claim-batch", "form", IDS,
"批量领取(非原子、逐条 CAS、部分成功)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
bru(F, "批量分配到池", "批量分配到池", "POST", "/api/lead/assign-pool-batch", "form",
IDS + [("poolId", "string", "", "-", "目标公海池ID", "1")],
"批量分配到池(未分发 → 待领取)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
bru(F, "批量分配到销售", "批量分配到销售", "POST", "/api/lead/assign-user-batch", "form",
IDS + [("userId", "string", "", "-", "目标销售用户ID(统一分配给同一销售)", "100")],
"批量分配到销售。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
bru(F, "批量释放", "批量释放", "POST", "/api/lead/release-batch", "form", IDS,
"批量释放(已领取/跟进中 → 待领取)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
bru(F, "批量激活", "批量激活", "POST", "/api/lead/activate-batch", "form", IDS,
"批量激活(过期失效 → 恢复失效前状态)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
bru(F, "批量删除", "批量删除", "POST", "/api/lead/delete-batch", "form", IDS,
"批量删除(已转商机记失败,不中断整批)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE)
# ==================== 线索管理/线索设置(5,即公海池配置页,ADR-0024 改名) ====================
POOL_RESP_ROWS = [
("id", "string", "公海池ID"),
("createTime", "datetime", "创建时间"),
("updateTime", "datetime", "更新时间"),
("poolName", "string", "公海池名称(全局唯一)"),
("deptId", "string", "归属销售部门ID(一部门一池)"),
("teamDeptId", "string", "归属团队部门ID(冗余)"),
("claimRule", "int", "领取规则:1=成员可领+管理员分配 2=仅管理员分配"),
("recycleDays", "int", "超时回收天数 N(≥1)"),
("expireDays", "int", "线索失效天数 M(≥1)"),
("dailyClaimLimit", "int", "每日领取上限(≥0,0=无限)"),
("holdLimit", "int", "个人持有上限(≥1)"),
("provinceRegionIds", "array<string>", "负责省份区划ID列表(sys_region level=1)"),
("cityRegionIds", "array<string>", "负责市/区区划ID列表(sys_region level=2/3)"),
("ownerUserId", "string", "负责人用户ID(1池1负责人)"),
("collaboratorUserIds", "array<string>", "协作人用户ID列表"),
("deptName", "string", "归属部门名称(出参回显)"),
("ownerUserName", "string", "负责人姓名(出参回显)"),
]
POOL_EXAMPLE = {
"id": "1", "createTime": "2026-08-14 10:00:00", "updateTime": "2026-08-14 10:00:00",
"poolName": "华北公海池", "deptId": "10", "teamDeptId": "10", "claimRule": 1,
"recycleDays": 7, "expireDays": 30, "dailyClaimLimit": 20, "holdLimit": 100,
"provinceRegionIds": ["110000"], "cityRegionIds": ["110100"], "ownerUserId": "100",
"collaboratorUserIds": ["101"], "deptName": "销售一部", "ownerUserName": "李四",
}
POOL_WRITE_FIELDS = [
("id", "string", "编辑必填", "-", "公海池ID(新建不传)", "1"),
("poolName", "string", "", "-", "公海池名称(全局唯一)", "华北公海池"),
("deptId", "string", "", "-", "归属销售部门ID(一部门一池)", "10"),
("teamDeptId", "string", "", "-", "归属团队部门ID(冗余)", "10"),
("claimRule", "int", "", "-", "领取规则:1=成员可领+管理员分配 2=仅管理员分配", "1"),
("recycleDays", "int", "", "-", "超时回收天数 N(≥1)", "7"),
("expireDays", "int", "", "-", "线索失效天数 M(≥1)", "30"),
("dailyClaimLimit", "int", "", "-", "每日领取上限(≥0,0=无限)", "20"),
("holdLimit", "int", "", "-", "个人持有上限(≥1)", "100"),
("provinceRegionIds", "string[]", "", "-", "负责省份区划ID列表(逗号分隔)", "110000"),
("cityRegionIds", "string[]", "", "-", "负责市/区区划ID列表(逗号分隔)", "110100"),
("ownerUserId", "string", "", "-", "负责人用户ID(1池1负责人)", "100"),
("collaboratorUserIds", "string[]", "", "-", "协作人用户ID列表(逗号分隔)", "101"),
]
FP = "线索管理/线索设置"
bru(FP, "公海池分页列表", "公海池分页列表", "POST", "/api/rule/pool/page", "form",
BASE_PARAM + [("deptId", "string", "", "-", "归属部门ID筛选", "10"),
("claimRule", "int", "", "-", "领取规则筛选", "1")],
"公海池分页列表。", *page_of(POOL_EXAMPLE, POOL_RESP_ROWS, "LeadPoolDTO"))
bru(FP, "公海池详情", "公海池详情", "GET", "/api/rule/pool/detail", "query",
[("id", "string", "", "-", "公海池ID", "1")],
"公海池详情(含省份/市/区/人员)。", POOL_RESP_ROWS, POOL_EXAMPLE)
bru(FP, "新建编辑公海池", "新建/编辑公海池", "POST", "/api/rule/pool/saveOrUpdate", "form", POOL_WRITE_FIELDS,
"新建/编辑公海池(id 为空新建、非空编辑)。", None)
bru(FP, "删除公海池", "删除公海池", "POST", "/api/rule/pool/delete", "form",
[("id", "string", "", "-", "公海池ID", "1")],
"删除公海池(软删除,级联清关联表;池下有活跃线索拒删)。", None)
bru(FP, "批量删除公海池", "批量删除公海池", "POST", "/api/rule/pool/delete-batch", "form",
[("ids", "string[]", "", "-", "公海池ID列表(逗号分隔)", "1,2")],
"批量删除公海池(非原子、逐条、部分成功)。",
[
("total", "int", "处理总数(successCount + failCount)"),
("successCount", "int", "成功条数"),
("failCount", "int", "失败条数"),
("failures[]", "array<object>", "失败明细列表"),
(" poolId", "string", "失败的公海池 id"),
(" reason", "string", "失败原因(语义枚举):NOT_EXIST 池不存在或已删除 / HAS_ACTIVE_LEAD 池下有活跃线索 / UNKNOWN 未归类"),
(" message", "string", "失败文案"),
],
{"total": 2, "successCount": 1, "failCount": 1,
"failures": [{"poolId": "2", "reason": "NOT_EXIST", "message": "公海池不存在"}]})
# ==================== 线索规则-行政区划(2) ====================
REGION_ROWS = [
("[]", "array<object>", "区划列表"),
(" id", "string", "区划ID"),
(" code", "string", "区划国标 code"),
(" name", "string", "区划名称"),
(" parentId", "string", "上级区划ID"),
(" level", "int", "层级:1=省 2=市 3=区"),
]
REGION_EXAMPLE = [{"id": "110000", "code": "110000", "name": "北京市", "parentId": "0", "level": 1}]
FR = "线索规则-行政区划"
bru(FR, "按上级查子区划", "按上级查子区划", "GET", "/api/rule/region/list", "query",
[("parentId", "string", "", "-", "上级区划ID(不传则查省级)", "110000")],
"按上级ID查子区划(级联下拉数据源)。", REGION_ROWS, REGION_EXAMPLE)
bru(FR, "按层级查区划", "按层级查区划", "GET", "/api/rule/region/level", "query",
[("level", "int", "", "-", "层级:1=省 2=市 3=区", "1")],
"按层级查区划。", REGION_ROWS, REGION_EXAMPLE)
# ==================== 列偏好(2) ====================
FC = "列偏好"
bru(FC, "读取列偏好", "读取列偏好", "GET", "/api/preference/get", "query",
[("scopeKey", "string", "", "-", "偏好范围 code(如 lead.public_pool)", "lead.public_pool")],
"读取当前用户在某 scope 的列偏好(用户私有,userId 取自登录态;无偏好时 data 为 null)。",
[
("visibleKeys", "array<string>", "用户勾选的字段 key 列表(语义为「显示」)"),
("columnOrder", "array<string>", "用户拖拽后的字段 key 顺序列表"),
],
{"visibleKeys": ["leadName", "phone"], "columnOrder": ["phone", "leadName"]})
bru(FC, "保存列偏好", "保存列偏好", "POST", "/api/preference/save", "form",
[("scopeKey", "string", "", "-", "偏好范围 code", "lead.public_pool"),
("visibleKeys", "string[]", "", "-", "显示字段 key 列表(逗号分隔)", "leadName,phone"),
("columnOrder", "string[]", "", "-", "字段顺序列表(逗号分隔)", "phone,leadName")],
"保存当前用户在某 scope 的列偏好(显隐+顺序,一次写入)。", None)
# ==================== 认证 / 登录(方法级 tags=登录 + 类级 @Tag=认证,各一份副本) ====================
USER_INFO_ROWS = [
("id", "string", "用户ID"),
("username", "string", "用户显示名"),
("mobile", "string", "手机号"),
("email", "string", "邮箱"),
("avatar", "string", "头像URL"),
("deptId", "string", "归属部门ID"),
("deptName", "string", "归属部门名称"),
("menus", "array", "当前用户菜单树(仅登录和 /me 时填充,同 GET /api/system/menus/tree)"),
]
USER_INFO_EXAMPLE = {
"id": "100", "username": "李四", "mobile": "13800000000", "email": "li@example.com",
"avatar": "http://minio内网地址:9000/crm/avatar/1.png", "deptId": "10", "deptName": "销售一部", "menus": [],
}
LOGIN_DATA = {"token": "eyJhbGciOiJIUzI1NiJ9.example.token", "userInfo": USER_INFO_EXAMPLE}
LOGIN_ROWS = [
("token", "string", "JWT token,后续请求放入请求头:Authorization: Bearer {token}"),
("userInfo", "object", "用户信息(字段见下,缩进行)"),
] + [(" " + r[0], r[1], r[2]) for r in USER_INFO_ROWS]
for _f in ("登录", "认证"):
bru(_f, "钉钉登录", "钉钉登录", "POST", "/api/auth/login/dingtalk", "form",
[("authCode", "string", "", "-", "前端扫码/免登拿到的三方 authCode", "auth-code-from-dingtalk")],
"钉钉登录(扫码/免登通用);响应后自动把 token 写入环境变量。",
LOGIN_ROWS, LOGIN_DATA, req_auth="none", login_script=True)
bru(_f, "注销", "注销", "POST", "/api/auth/logout", "none", None,
"注销当前 token(删 Redis 会话,强制下线)。", None)
bru(_f, "获取当前用户信息", "获取当前登录用户信息", "GET", "/api/auth/me", "none", None,
"获取当前登录用户信息(含菜单树)。", USER_INFO_ROWS, USER_INFO_EXAMPLE)
# ==================== system(无 tag,回退 urlPattern:/api/{module}/{resource}) ====================
SM = "system/menus"
bru(SM, "菜单树", "当前用户菜单树", "GET", "/api/system/menus/tree", "none", None,
"当前用户可见菜单树(按角色过滤,前端据此控制路由/UI)。结构同「权限资源树」但只含可见节点。",
[("[]", "array<object>", "ResourceNodeDTO 扁平数组(前端按 parentId 组树),字段见「权限点管理/资源树列表」")],
[])
SD = "system/depts"
DEPT_ROWS = [
("id", "string", "部门ID"),
("parentId", "string", "上级部门 ID,根节点为 0"),
("deptName", "string", "部门名称"),
("sort", "int", "排序"),
("leaderUserId", "string", "部门负责人用户 ID"),
("children[]", "array<object>", "子部门列表(同 DeptDTO 结构,可嵌套)"),
]
DEPT_EXAMPLE = {"id": "10", "parentId": "0", "deptName": "销售一部", "sort": 1,
"leaderUserId": "100", "children": [{"id": "11", "parentId": "10", "deptName": "销售一组",
"sort": 1, "leaderUserId": "101", "children": []}]}
bru(SD, "部门树", "部门树", "GET", "/api/system/depts/tree", "none", None,
"全量部门树(前端组织选择器数据源)。", DEPT_ROWS, DEPT_EXAMPLE)
bru(SD, "保存部门", "保存部门", "POST", "/api/system/depts/save", "form",
[("id", "string", "编辑必填", "-", "部门ID(新建不传)", "1"),
("parentId", "string", "", "-", "上级部门 ID,根节点为 0", "0"),
("deptName", "string", "", "-", "部门名称", "销售二部"),
("sort", "int", "", "-", "排序", "2"),
("leaderUserId", "string", "", "-", "部门负责人用户 ID", "100")],
"新建/编辑部门。", None)
bru(SD, "删除部门", "删除部门", "POST", "/api/system/depts/delete", "form",
[("id", "string", "", "-", "部门ID", "1")],
"删除部门(部门下仍有成员拒删)。", None)
SU = "system/users"
USER_LIST_ROWS = [
("content[]", "array<object>", "用户列表"),
(" id", "string", "用户ID"),
(" username", "string", "姓名"),
(" account", "string", "登录账号(OA 来源,只读)"),
(" title", "string", "职务(OA 来源,只读)"),
(" mobile", "string", "手机号"),
(" primaryDeptId", "string", "主部门ID"),
(" primaryDeptName", "string", "主部门名"),
(" partTimeDepts[]", "array<object>", "兼职部门(主部门之外)"),
(" id", "string", "部门ID"),
(" name", "string", "部门名"),
(" employmentStatus", "string", "在职状态 active/resigned"),
(" enabled", "boolean", "账号是否启用(与在职状态分离,ADR-0007)"),
(" roles[]", "array<object>", "所持角色"),
(" id", "string", "角色ID"),
(" name", "string", "角色名"),
(" lastLoginTime", "datetime", "最后登录时间,null=从未登录"),
("total", "long", "总条数"), ("size", "long", "每页条数"),
("current", "long", "当前页码"), ("pages", "long", "总页数"), ("empty", "boolean", "是否空结果"),
]
USER_LIST_EXAMPLE = {"content": [{"id": "100", "username": "李四", "account": "lisi", "title": "销售",
"mobile": "13800000000", "primaryDeptId": "10", "primaryDeptName": "销售一部",
"partTimeDepts": [{"id": "20", "name": "市场部"}], "employmentStatus": "active", "enabled": True,
"roles": [{"id": "3", "name": "销售"}], "lastLoginTime": "2026-08-14 09:00:00"}],
"total": 1, "size": 10, "current": 1, "pages": 1, "empty": False}
bru(SU, "用户分页", "用户分页", "POST", "/api/system/users/page", "form",
BASE_PARAM + [("deptId", "string", "", "-", "部门ID,选中节点含全部子孙;null=全公司", "10"),
("employmentStatus", "string", "", "-", "在职状态 active/resigned/unassigned;null=不限", "active"),
("loginFrom", "date", "", "-", "最后登录起(含当天 00:00)", "2026-08-01"),
("loginTo", "date", "", "-", "最后登录讫(含当天 23:59:59)", "2026-08-14")],
"用户管理-用户列表分页(需 ADMIN 角色)。", USER_LIST_ROWS, USER_LIST_EXAMPLE)
bru(SU, "用户统计", "用户统计", "GET", "/api/system/users/stats", "query",
[("deptId", "string", "", "-", "部门ID(口径含子孙);不传=全公司", "10")],
"用户管理-四指标卡统计(需 ADMIN 角色);口径跟随选中部门子树。",
[("deptCount", "int", "部门数(选中节点及其子孙)"),
("activeCount", "long", "在职用户数"),
("resignedCount", "long", "离职用户数"),
("unassignedCount", "long", "待分配用户数(在职且无角色)")],
{"deptCount": 5, "activeCount": 20, "resignedCount": 2, "unassignedCount": 3})
bru(SU, "分配用户角色", "分配用户角色", "POST", "/api/system/users/assign-roles", "form",
[("userId", "string", "", "-", "用户ID", "100"),
("roleIds", "string", "", "-", "角色ID列表(逗号分隔,全量替换)", "3,4")],
"给用户分配角色(全量替换,需 ADMIN 角色)。", None)
bru(SU, "分配用户部门", "分配用户部门", "POST", "/api/system/users/assign-depts", "form",
[("userId", "string", "", "-", "用户ID", "100"),
("primaryDeptId", "string", "", "-", "主部门 ID(可空)", "10"),
("deptIds", "string", "", "-", "兼职部门 ID 列表(逗号分隔,全量替换)", "20,21")],
"原子设置用户的主部门与全部兼职部门(全量替换)。", None)
# ==================== 系统管理/权限管理/角色管理(6) ====================
FRM = "系统管理/权限管理/角色管理"
ROLE_ROWS = [
("id", "string", "角色ID"),
("createTime", "datetime", "创建时间"),
("updateTime", "datetime", "更新时间"),
("roleName", "string", "角色名称"),
("roleCode", "string", "角色编码"),
("sort", "int", "排序"),
("remark", "string", "备注"),
("builtin", "boolean", "是否内置角色"),
("resourceIds", "array<string>", "已补全祖先的完整授权集合(仅 detail 出参)"),
("moduleScopes[]", "array<object>", "角色×模块数据范围档位列表(仅 detail 出参,page 为 null)"),
(" moduleCode", "string", "模块编码(如 lead)"),
(" dataScope", "int", "档位:1 仅本人 / 2 本部门 / 3 本部门及下属 / 4 全部"),
]
ROLE_EXAMPLE = {"id": "3", "createTime": "2026-08-14 10:00:00", "updateTime": "2026-08-14 10:00:00",
"roleName": "销售", "roleCode": "ROLE_SALES", "sort": 1, "remark": "一线销售",
"builtin": False, "resourceIds": ["1", "2"],
"moduleScopes": [{"moduleCode": "lead", "dataScope": 1}]}
bru(FRM, "角色分页", "分页查询角色", "POST", "/api/roles/page", "form", BASE_PARAM,
"分页查询角色(keyword 匹配角色名称;moduleScopes 为 null)。",
*page_of({k: v for k, v in ROLE_EXAMPLE.items() if k not in ("resourceIds", "moduleScopes")},
[r for r in ROLE_ROWS if not r[0].startswith(" ") and not r[0].startswith("resource") and not r[0].startswith("moduleScopes")],
"RoleDTO"))
bru(FRM, "新增编辑角色", "新增或编辑角色", "POST", "/api/roles/saveOrUpdate", "form",
[("id", "string", "编辑必填", "-", "角色ID(新建不传)", "1"),
("roleName", "string", "", "-", "角色名称", "销售"),
("roleCode", "string", "", "-", "角色编码", "ROLE_SALES"),
("sort", "int", "", "-", "排序", "1"),
("remark", "string", "", "-", "备注", "一线销售"),
("moduleScopesJson", "string(json)", "", "-", "模块档位 JSON 字符串:[{\"moduleCode\":\"...\",\"dataScope\":1}]", "[{\"moduleCode\":\"lead\",\"dataScope\":1}]")],
"新增/编辑角色(含每模块数据范围档位,全量替换)。", None)
bru(FRM, "角色详情", "角色详情", "GET", "/api/roles/detail", "query",
[("roleId", "string", "", "-", "角色ID", "3")],
"角色详情(含授权资源集合 + 每模块档位)。", ROLE_ROWS, ROLE_EXAMPLE)
bru(FRM, "分配角色权限", "分配角色权限资源", "POST", "/api/roles/assign-resources", "form",
[("roleId", "string", "", "-", "角色ID", "3"),
("resourceIds", "string", "", "-", "资源节点ID列表(逗号分隔,祖先自动补全,全量替换)", "1,2,3")],
"分配角色权限资源(祖先补全 + 全量替换)。", None)
bru(FRM, "删除角色", "删除角色", "POST", "/api/roles/delete", "form",
[("id", "string", "", "-", "角色ID", "3")],
"删除角色(级联清理关联表;内置角色拒删)。", None)
bru(FRM, "可配数据权限模块", "列出可配数据权限模块", "GET", "/api/data-scope/modules", "none", None,
"列出可配数据权限模块(status=enabled,按 sort 排序;渲染角色配置表格前先拉取)。",
[("[]", "array<object>", "模块列表"),
(" code", "string", "模块编码(如 lead)"),
(" name", "string", "模块名称"),
(" sort", "int", "排序"),
(" builtin", "boolean", "是否内置")],
[{"code": "lead", "name": "线索", "sort": 1, "builtin": False}])
# ==================== 系统管理/权限点管理(4) ====================
FRC = "系统管理/权限点管理"
RES_ROWS = [
("id", "string", "节点ID"),
("createTime", "datetime", "创建时间"),
("updateTime", "datetime", "更新时间"),
("parentId", "string", "父节点 ID,根节点传 null/0"),
("name", "string", "节点名称"),
("type", "string", "节点类型:CATALOG 目录 / MENU 菜单 / BUTTON 权限点"),
("sort", "int", "排序"),
("description", "string", "描述/备注"),
("route", "string", "前端路由路径,menu 必填"),
("perms", "string", "权限码(如 crm:user:list),button 必填"),
("denyBehavior", "string", "无权限时的前端行为:hide / disable,button 必填"),
("apiUrl", "string", "接口 URL,button 必填"),
("status", "string", "权限点状态:enabled 启用 / disabled 停用"),
("icon", "string", "图标 URL,catalog/menu 可选"),
("children[]", "array<object>", "子节点列表(仅 listAll 返回时填充,同结构可嵌套)"),
]
RES_EXAMPLE = {"id": "1", "createTime": "2026-08-14 10:00:00", "updateTime": "2026-08-14 10:00:00",
"parentId": "0", "name": "系统管理", "type": "CATALOG", "sort": 1,
"description": "系统管理目录", "route": None, "perms": None, "denyBehavior": None,
"apiUrl": None, "status": "enabled", "icon": None,
"children": [{"id": "2", "parentId": "1", "name": "菜单管理", "type": "MENU", "sort": 1,
"route": "/system/menu", "status": "enabled", "children": []}]}
bru(FRC, "资源树列表", "获取权限资源树", "GET", "/api/resources/list", "none", None,
"全量资源树(扁平数组,前端按 parentId 组树;需 ADMIN 角色)。", RES_ROWS, [RES_EXAMPLE])
bru(FRC, "新增编辑资源节点", "新增编辑一级菜单/二级菜单/权限点", "POST", "/api/resources/saveOrUpdate", "form",
[("id", "string", "编辑必填", "-", "节点ID(新建不传)", "1"),
("parentId", "string", "", "-", "父节点 ID,根节点传 null/0", "0"),
("name", "string", "", "-", "节点名称", "系统管理"),
("type", "string", "", "-", "节点类型:CATALOG/MENU/BUTTON", "CATALOG"),
("sort", "int", "", "-", "排序", "1"),
("description", "string", "", "-", "描述/备注", "-"),
("route", "string", "menu 必填", "-", "前端路由路径", "/system/menu"),
("perms", "string", "button 必填", "-", "权限码(如 crm:user:list)", "crm:role:list"),
("denyBehavior", "string", "button 必填", "-", "无权限时前端行为:hide/disable", "hide"),
("apiUrl", "string", "button 必填", "-", "接口 URL", "/api/roles/page"),
("status", "string", "", "-", "权限点状态:enabled/disabled", "enabled"),
("icon", "string", "", "-", "图标 URL", "http://minio内网地址:9000/crm/icon/1.png")],
"新增/编辑资源节点(含校验;需 ADMIN 角色)。", RES_ROWS, RES_EXAMPLE)
bru(FRC, "删除资源节点", "删除一级菜单/二级菜单/权限点", "POST", "/api/resources/delete", "form",
[("id", "string", "", "-", "节点ID", "1")],
"删除节点(含子节点检查 + 级联清理;需 ADMIN 角色)。", None)
FILE_INFO_ROWS = [
("fileId", "string", "文件唯一引用凭据(crm_file_info 主键),业务表只存它"),
("originalName", "string", "原始文件名"),
("size", "long", "文件大小(字节)"),
("contentType", "string", "MIME 类型"),
("bizDomain", "string", "业务域"),
("creatorId", "string", "上传者(无登录态的内部上传为 null)"),
("createTime", "datetime", "上传时间"),
("thumbnailStatus", "string", "缩略图状态(PENDING/READY/FAILED/UNSUPPORTED)"),
]
FILE_INFO_EXAMPLE = {"fileId": "9001", "originalName": "报价单.png", "size": 102400,
"contentType": "image/png", "bizDomain": "lead", "creatorId": "100",
"createTime": "2026-08-14 10:00:00", "thumbnailStatus": "READY"}
bru(FRC, "上传图标", "上传图标", "POST", "/api/resources/icon/upload", "multipart",
[("file", "file", "", "-", "图标文件(格式/大小校验)", "icon.png")],
"上传资源图标(需 ADMIN 角色)。", FILE_INFO_ROWS, FILE_INFO_EXAMPLE, multipart_file="file")
# ==================== 调试(验证专用)(1) ====================
bru("调试(验证专用)", "签发调试token", "【验证专用】按 userId 签发 JWT", "GET", "/api/auth/debug/token", "query",
[("userId", "string", "", "-", "crm_auth_user.id", "100")],
"【验证专用 · 严禁进生产】按 userId 直接签发合法 JWT,绕过钉钉扫码;仅 verify profile 激活时端点存在(ADR-0014)。",
[("(data)", "string", "与钉钉登录同款 JWT,可直接放进 Authorization: Bearer 调试其他接口")],
"eyJhbGciOiJIUzI1NiJ9.debug.token", req_auth="none")
# ==================== 文件(9) ====================
FF = "文件"
bru(FF, "直传上传", "直传上传", "POST", "/api/file/upload", "multipart",
[("file", "file", "", "-", "文件(超直传上限 10MB 走分片)", "报价单.png"),
("bizDomain", "string", "", "-", "业务域(如 lead)", "lead")],
"直传小文件(超上限走分片三阶段)。", FILE_INFO_ROWS, FILE_INFO_EXAMPLE, multipart_file="file")
bru(FF, "文件详情", "文件详情", "GET", "/api/file/info", "query",
[("fileId", "string", "", "-", "文件ID", "9001")],
"文件详情。", FILE_INFO_ROWS, FILE_INFO_EXAMPLE)
bru(FF, "下载文件", "下载文件", "GET", "/api/file/download", "query",
[("fileId", "string", "", "-", "文件ID", "9001")],
"下载文件。",
nonenvelope_note="信封例外:成功返回二进制文件流(Content-Disposition 附件下载);失败返回标准 JSON 信封")
bru(FF, "预览地址", "预览地址", "GET", "/api/file/preview-url", "query",
[("fileId", "string", "", "-", "文件ID", "9001")],
"预览地址(可直接 window.open,由 kkFileView 渲染)。",
[("(data)", "string", "kkFileView onlinePreview 完整 URL")],
"http://kkfileview内网地址:8012/onlinePreview?url=encoded")
bru(FF, "缩略图", "缩略图", "GET", "/api/file/thumbnail", "query",
[("fileId", "string", "", "-", "文件ID", "9001")],
"缩略图(命中缓存返回 200 + max-age=86400,未就绪返回 202 占位图)。",
nonenvelope_note="信封例外:成功返回二进制图片流(200/202,带 Cache-Control);失败返回标准 JSON 信封")
bru(FF, "删除文件", "删除文件", "POST", "/api/file/delete", "form",
[("fileId", "string", "", "-", "文件ID", "9001")],
"删除文件(逻辑删除)。", None)
bru(FF, "分片初始化", "分片上传-init", "POST", "/api/file/multipart/init", "form",
[("originalName", "string", "", "-", "原始文件名", "视频演示.mp4"),
("totalSize", "long", "", "-", "文件总大小(字节)", "104857600"),
("bizDomain", "string", "", "-", "业务域", "lead"),
("uploadId", "string", "", "-", "断点续传时传上次的 uploadId", "u-9001")],
"分片上传-init(带 uploadId 为断点续传,返回已上传分片序号列表)。",
[("uploadId", "string", "上传会话标识,后续 upload/complete 凭它"),
("chunkSize", "long", "分片大小(字节),除末片外每片必须为此值"),
("totalChunks", "int", "总片数"),
("uploadedChunks", "array<int>", "已上传分片序号列表(1 起),断点续传时只补缺失分片;新会话为空列表")],
{"uploadId": "u-9001", "chunkSize": 5242880, "totalChunks": 20, "uploadedChunks": [1, 2]})
bru(FF, "上传分片", "分片上传-上传单片", "POST", "/api/file/multipart/upload", "multipart",
[("uploadId", "string", "", "-", "上传会话标识", "u-9001"),
("chunkNo", "int", "", "-", "分片序号(1 起)", "3"),
("file", "file", "", "-", "分片数据(非末片大小必须等于 init 下发的分片大小)", "chunk-3.bin")],
"分片上传-上传单片。", None, multipart_file="file")
bru(FF, "完成分片", "分片上传-complete", "POST", "/api/file/multipart/complete", "form",
[("uploadId", "string", "", "-", "上传会话标识", "u-9001")],
"分片上传-complete(校验齐全后合并,返回文件详情)。", FILE_INFO_ROWS, FILE_INFO_EXAMPLE)
# ==================== collection.bru(全局契约摘要) ====================
collection = """auth {
mode: bearer
}
auth:bearer {
token: {{token}}
}
docs {
# CRM 接口全局契约
## 响应信封
所有接口统一返回 Result<T>:code / success / message / data;成败判定 `success == true`。HTTP 状态码业务失败也为 200,仅系统异常 500、路径不存在 404、授权拒绝 403。
## 错误码分段
0 成功;400xx 参数类;401xx 认证/身份;403xx 权限;404xx 资源不存在;405xx 请求方式;500xx 系统/外部调用;60001 通用业务错误;61001~61999 认证模块;62001~62999 文件模块;63001~63999 数据字典;64001~64999 公海池配置;65001~65999 线索业务。
## 传参约定
写操作 POST + 动作后缀,表单字段收参(application/x-www-form-urlencoded);读操作参数走 query;复杂字段(列表/对象)以 JSON 字符串放表单字段。Long 类型(雪花 ID)全局序列化为字符串,回传按字符串传。日期时间格式统一 `yyyy-MM-dd HH:mm:ss`(GMT+8)。
## 分页规范
分页入参继承 BaseParam(current=1 / size=10 上限 500 / keyword / orderBy / asc);分页出参 PageResult:content / total / size / current / pages / empty。
## 鉴权
请求头 `Authorization: Bearer {token}`;免鉴权:/api/auth/login/**、/api/auth/debug/token(verify 环境)。登录接口响应后自动写入环境变量 token。
## 页面归属规范(ADR-0024:菜单粒度)
接口按方法级 `@Operation(tags)` 打菜单 tag,tag 名即菜单文件夹(含 / 按目录嵌套),命名以产品原型菜单为准;无 tag 接口按 URL `/api/{module}/{resource}` 回退。一个接口挂 N 个菜单就在 N 个文件夹各有一份副本;线索域四菜单共用同一套接口,副本按 bruno-sync.config.json 的 menuBindings 绑定专属参数值:
线索公海=viewType:PUBLIC_POOL / scopeKey:lead.public_pool · 我的线索=MY_LEAD / lead.my_lead · 我的关注=MY_FOLLOW / lead.my_follow · 线索管理=MANAGE / lead.manage
副本由生成器幂等维护(重跑覆盖),手改会在下次同步时丢失。
}
"""
w("collection.bru", collection)
# ==================== 报告 ====================
print("=== 生成完成 ===")
total = 0
for folder, n in sorted(SEQ.items()):
print(" %-30s %d 个接口" % (folder, n))
total += n
print("合计 %d 个 .bru + collection.bru + environments/local.bru" % total)