# -*- coding: utf-8 -*- """bruno-sync 冷启动全量生成:crm-backend-matt 源码 → D:/code/crm-api-docs 按 SKILL.md Step 2 模板生成;grouping=tag,无 tag 接口回退 urlPattern。 Bruno 2 单仓布局(collection 在仓根);线索域每页实挂(ADR-0024 修订)。 """ import json, os WSROOT = r"D:\code\crm-api-docs" # Bruno 2 单仓根(bruno.json 所在,前端实际消费仓) ROOT = WSROOT # collection 根 = 仓根 SEQ = {} import shutil # Bruno 2 collection 脚手架幂等确保(已存在则不碰;local.bru 的 baseUrl 由仓维护,不覆盖) 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("bruno.json", '{"version":"1","name":"crm-api-docs","type":"collection","ignore":["node_modules",".git"]}') _ensure("environments/local.bru", "vars {\n baseUrl: https://ai.itc.vip/crm-api\n token: \n}\n") # 每页实挂重建(ADR-0024 修订):清理线索域旧平铺文件夹 + 根目录重复「权限点管理」(保留 系统管理/ 下那份) 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 同源):folder 即菜单文件夹; # 每页副本自动绑定 viewType/scopeKey 示例值 + desc 加【xx 页】前缀(ADR-0024 修订:每页实挂,不再全量四菜单) MENUS = [ ("线索管理/线索公海", "PUBLIC_POOL", "lead.public_pool", "公海"), ("线索管理/我的线索", "MY_LEAD", "lead.my_lead", "我的线索"), ("线索管理/我的关注", "MY_FOLLOW", "lead.my_follow", "我的关注"), ("线索管理/线索管理", "MANAGE", "lead.manage", "线索管理"), ] MENU_BIND = {m[0]: (m[1], m[2], m[3]) for m in MENUS} 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_BIND 时自动绑定该页 viewType/scopeKey 与【xx 页】前缀(每页实挂,ADR-0024 修订)。""" _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): if folder in MENU_BIND: _vt, _sk, _label = MENU_BIND[folder] params = bindv(bindv(params, "viewType", _vt), "scopeKey", _sk) desc = "【%s 页】%s" % (_label, desc) 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", "%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", "失败明细列表"), (" 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": "当前线索状态不允许该操作"}], } # ==================== 线索四页(每页实挂,ADR-0024 修订) ==================== # 接口×页面矩阵真相源:.scratch/lead-docs-audit/research/02-page-endpoint-matrix.md # 每个接口只挂有真实页面入口的菜单;四视图接口四页各一份副本(viewType/scopeKey 自动绑定)。 PP, ML, MF, MG = "线索管理/线索公海", "线索管理/我的线索", "线索管理/我的关注", "线索管理/线索管理" IDS = [("ids", "string[]", "是", "-", "线索ID列表(逗号分隔)", "1,2")] # ---- 四页共有:page / detail / history / stats ---- for _f in (PP, ML, MF, MG): bru(_f, "线索分页", "线索分页", "POST", "/api/lead/page", "form", BASE_PARAM + LEAD_PAGE_PARAM, "线索分页(四视图:PUBLIC_POOL 公海 / MY_LEAD 我的线索 / MY_FOLLOW 我的关注 / MANAGE 线索管理;本文件夹副本 viewType 已固定为该页视图),数据隔离由 @DataScope 自动注入。", *page_of(LEAD_EXAMPLE, LEAD_RESP_ROWS, "LeadDTO")) bru(_f, "线索详情", "线索详情", "GET", "/api/lead/detail", "query", [("id", "string", "是", "-", "线索ID", "1")], "线索详情(详情弹窗「线索详情」Tab)。", LEAD_RESP_ROWS, LEAD_EXAMPLE) bru(_f, "历史记录", "历史记录", "GET", "/api/lead/history", "query", [("leadId", "string", "是", "-", "线索ID", "1")], "线索操作历史(详情弹窗「历史记录」Tab;只展示 5 种:CLAIM 领取 / RELEASE 释放 / FEEDBACK 反馈 / CONVERT 转商机 / EDIT 编辑)。", [ ("[]", "array", "历史记录列表"), (" 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/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}) # ---- 线索公海页:领取(与我的关注共)+ 关注/批量关注 + 释放(与我的线索共,详情弹窗)+ 取关(与我的关注共,详情弹窗)---- for _f in (PP, MF): bru(_f, "领取线索", "领取线索", "POST", "/api/lead/claim", "form", [("id", "string", "是", "-", "线索ID", "1")], "领取线索(待领取 → 已领取;列表行/操作列按钮)。", None) bru(_f, "批量领取", "批量领取", "POST", "/api/lead/claim-batch", "form", IDS, "批量领取(勾选待领取行;非原子、逐条 CAS、部分成功,失败弹窗逐条列明)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) bru(PP, "关注线索", "关注线索", "POST", "/api/lead/follow", "form", [("id", "string", "是", "-", "线索ID", "1")], "关注线索(操作列/右面板按钮,任意状态可关注,进入「我的关注」视图)。", None) bru(PP, "批量关注", "批量关注", "POST", "/api/lead/follow-batch", "form", IDS, "批量关注(勾选任意状态行执行;逐条幂等,非原子、部分成功)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) for _f in (PP, ML): bru(_f, "释放线索", "释放线索", "POST", "/api/lead/release", "form", [("id", "string", "是", "-", "线索ID", "1")], "释放线索(已领取/跟进中 → 待领取,回公海池;公海页入口在详情弹窗,我的线索页在操作列)。", None) for _f in (PP, MF): bru(_f, "取消关注", "取消关注", "POST", "/api/lead/unfollow", "form", [("id", "string", "是", "-", "线索ID", "1")], "取消关注(公海页入口在详情弹窗,我的关注页在操作列;幂等)。", None) # ---- 我的线索页:编辑/反馈×2/转商机/批量释放 ---- 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(ML, "编辑线索", "编辑线索", "POST", "/api/lead/edit", "form", [("id", "string", "是", "-", "线索ID", "1")] + LEAD_WRITE_FIELDS, "编辑线索(操作列「编辑」进编辑页,非终态可编辑)。", None) bru(ML, "保存反馈草稿", "保存反馈草稿", "POST", "/api/lead/feedback-draft", "form", FEEDBACK_FIELDS, "保存反馈草稿(操作列「反馈」弹窗草稿态,不改状态机,可反复保存)。", None) bru(ML, "提交反馈", "提交反馈", "POST", "/api/lead/feedback-submit", "form", FEEDBACK_FIELDS, "提交反馈(已领取 → 跟进中;反馈无效时 → 线索作废)。", None) bru(ML, "转商机", "转商机", "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(ML, "批量释放", "批量释放", "POST", "/api/lead/release-batch", "form", IDS, "批量释放(顶部按钮,勾选已领取/跟进中 → 待领取)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) # ---- 新建/删除(我的线索 + 线索管理两页共有入口)---- for _f in (ML, MG): bru(_f, "新建线索", "新建线索", "POST", "/api/lead/create", "form", LEAD_WRITE_FIELDS + [ ("claimOnCreate", "boolean", "否", "false", "true=表单勾选「是否领取」,销售新增即领取(已领取态)", "false"), ("followOnCreate", "boolean", "否", "false", "true=表单勾选「是否关注」,创建即关注(PRD §6.1)", "false")], "新建线索(顶部「+ 新增线索」表单;省市区划数据源见「公共依赖/行政区划」)。", [("(data)", "string", "新线索ID(雪花,字符串下发)")], "9007199254740993") bru(_f, "删除线索", "删除线索", "POST", "/api/lead/delete", "form", [("id", "string", "是", "-", "线索ID", "1")], "删除线索(我的线索页仅过期失效行可删;线索管理页管理员可删,已转商机禁止删除,已领取/已转商机二次强确认)。", None) # ---- 我的关注页:批量取关 ---- bru(MF, "批量取关", "批量取关", "POST", "/api/lead/unfollow-batch", "form", IDS, "批量取关(顶部按钮,勾选执行;幂等,非原子、部分成功)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) # ---- 线索管理页:分配×4 + 激活/批量激活 + 批量删除 ---- bru(MG, "分配线索到池", "分配线索到池", "POST", "/api/lead/assign-pool", "form", [("id", "string", "是", "-", "线索ID", "1"), ("poolId", "string", "是", "-", "目标公海池ID", "1")], "分配线索到池(操作列「分配」弹窗,可不选人=只入池;未分发 → 待领取)。", None) bru(MG, "分配线索到销售", "分配线索到销售", "POST", "/api/lead/assign-user", "form", [("id", "string", "是", "-", "线索ID", "1"), ("userId", "string", "是", "-", "目标销售用户ID", "100")], "分配线索到销售(分配弹窗池→团队→部门→人员四级联动;未分发/待领取 → 已领取;作废态可自环重新分配)。", None) bru(MG, "批量分配到池", "批量分配到池", "POST", "/api/lead/assign-pool-batch", "form", IDS + [("poolId", "string", "是", "-", "目标公海池ID", "1")], "批量分配到池(顶部「批量分配」;未分发 → 待领取)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) bru(MG, "批量分配到销售", "批量分配到销售", "POST", "/api/lead/assign-user-batch", "form", IDS + [("userId", "string", "是", "-", "目标销售用户ID(统一分配给同一销售)", "100")], "批量分配到销售(顶部「批量分配」;同池限制,部分成功弹窗)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) bru(MG, "激活线索", "激活线索", "POST", "/api/lead/activate", "form", [("id", "string", "是", "-", "线索ID", "1")], "激活线索(过期失效行操作列;过期失效 → 恢复失效前状态,计时重置)。", None) bru(MG, "批量激活", "批量激活", "POST", "/api/lead/activate-batch", "form", IDS, "批量激活(顶部按钮,与「显示过期失效/线索作废」筛选联动;两种失效来源恢复语义不同;非原子、部分成功)。", BATCH_LEAD_ROWS, BATCH_LEAD_EXAMPLE) bru(MG, "批量删除", "批量删除", "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", "负责省份区划ID列表(sys_region level=1)"), ("cityRegionIds", "array", "负责市/区区划ID列表(sys_region level=2/3)"), ("ownerUserId", "string", "负责人用户ID(1池1负责人)"), ("collaboratorUserIds", "array", "协作人用户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", "失败明细列表"), (" 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,无独立菜单,ADR-0024 修订) ==================== REGION_ROWS = [ ("[]", "array", "区划列表"), (" 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查子区划(级联下拉数据源)。公共依赖:被 我的线索/新增、线索管理/新增、线索设置 三处表单使用(ADR-0024 修订)。", REGION_ROWS, REGION_EXAMPLE) bru(FR, "按层级查区划", "按层级查区划", "GET", "/api/rule/region/level", "query", [("level", "int", "是", "-", "层级:1=省 2=市 3=区", "1")], "按层级查区划。公共依赖:被 我的线索/新增、线索管理/新增、线索设置 三处表单使用(ADR-0024 修订)。", REGION_ROWS, REGION_EXAMPLE) # ==================== 列偏好(每页 scope 独立一份,ADR-0024 修订) ==================== for _f in MENU_BIND: bru(_f, "读取列偏好", "读取列偏好", "GET", "/api/preference/get", "query", [("scopeKey", "string", "是", "-", "偏好范围 code(本页固定)", "lead.public_pool")], "读取当前用户在本页列表的列偏好(「列表设置」弹窗回显;用户私有,userId 取自登录态;无偏好时 data 为 null)。", [ ("visibleKeys", "array", "用户勾选的字段 key 列表(语义为「显示」)"), ("columnOrder", "array", "用户拖拽后的字段 key 顺序列表"), ], {"visibleKeys": ["leadName", "phone"], "columnOrder": ["phone", "leadName"]}) bru(_f, "保存列偏好", "保存列偏好", "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", "ResourceNodeDTO 扁平数组(前端按 parentId 组树),字段见「权限点管理/资源树列表」")], []) SD = "system/depts" DEPT_ROWS = [ ("id", "string", "部门ID"), ("parentId", "string", "上级部门 ID,根节点为 0"), ("deptName", "string", "部门名称"), ("sort", "int", "排序"), ("leaderUserId", "string", "部门负责人用户 ID"), ("children[]", "array", "子部门列表(同 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", "用户列表"), (" id", "string", "用户ID"), (" username", "string", "姓名"), (" account", "string", "登录账号(OA 来源,只读)"), (" title", "string", "职务(OA 来源,只读)"), (" mobile", "string", "手机号"), (" primaryDeptId", "string", "主部门ID"), (" primaryDeptName", "string", "主部门名"), (" partTimeDepts[]", "array", "兼职部门(主部门之外)"), (" id", "string", "部门ID"), (" name", "string", "部门名"), (" employmentStatus", "string", "在职状态 active/resigned"), (" enabled", "boolean", "账号是否启用(与在职状态分离,ADR-0007)"), (" roles[]", "array", "所持角色"), (" 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", "已补全祖先的完整授权集合(仅 detail 出参)"), ("moduleScopes[]", "array", "角色×模块数据范围档位列表(仅 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", "模块列表"), (" 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", "子节点列表(仅 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", "已上传分片序号列表(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: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 名即菜单文件夹(含 / 按目录嵌套);一个接口被 N 个页面用到就有 N 份副本,每份带该页专属参数值(viewType/scopeKey 已绑定,见各 .bru 参数表「本页固定传」)。 线索域四页绑定:线索公海=viewType:PUBLIC_POOL / scopeKey:lead.public_pool · 我的线索=MY_LEAD / lead.my_lead · 我的关注=MY_FOLLOW / lead.my_follow · 线索管理=MANAGE / lead.manage。 无独立菜单的公共依赖(如「公共依赖/行政区划」)单独归档,被多页面表单共用。 副本由生成器幂等维护(重跑覆盖),手改会在下次同步时丢失。 ## 前端使用指引 1. 按「菜单文件夹」找接口:文件夹里的接口 = 该页面实际需要的全部接口,不缺不多; 2. 先确认 environments/local.bru 的 baseUrl,再跑「登录」文件夹的钉钉登录拿 token(响应后自动写入环境变量); 3. 之后任意接口直接 Send(鉴权已继承 collection 级 Bearer {{token}})。 } """ 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)