文件
hl-api-changelog/api-docs/supplier/供应商模块 API 接口规范-v2.1-前端联调版.html
T
lc e1b0a667cd
changelog-filename-gate / validate (push) Successful in 2s
changelog-filename-gate / validate (pull_request) Successful in 2s
docs(api): 发布供应商资质有效期契约
2026-08-25 16:23:36 +08:00

2057 行
267 KiB
HTML
原始文件 Blame 文件历史

此文件含有不可见的 Unicode 字符
此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="data:,">
<title>供应商模块 API 接口规范 · v2.1 前端联调版</title>
<style>
:root {
--ink:#20312d; --muted:#667772; --line:#d9e3df; --paper:#fff;
--brand:#08715d; --brand-soft:#eaf7f3; --warn:#9a5b00; --warn-soft:#fff5dc;
--danger:#a33a3a; --danger-soft:#fff0f0; --violet:#6745a5; --violet-soft:#f4efff;
--blue:#275b91; --blue-soft:#eef6ff; --shadow:0 10px 30px rgba(30,65,55,.09);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:#f4f7f6;color:var(--ink);font:14px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
a{color:var(--brand);text-decoration:none}
a:hover{text-decoration:underline}
code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#f2f5f4;border-radius:4px;padding:.1em .35em;word-break:break-word}
.layout{display:grid;grid-template-columns:280px minmax(0,1fr);max-width:1680px;margin:auto}
aside{position:sticky;top:0;height:100vh;overflow:auto;background:#173c34;color:#e9f7f2;padding:22px 18px}
aside h2{font-size:17px;margin:0 0 14px}
aside a{display:block;color:#d6eee6;padding:6px 9px;border-left:3px solid transparent;border-radius:6px;transition:background .16s ease,color .16s ease,border-color .16s ease}
aside a:hover{background:rgba(255,255,255,.1);text-decoration:none}
aside .sub{padding-left:20px;font-size:12px;color:#b9d8cf}
aside a.active{background:#e8f7f2;color:#123f35;border-left-color:#55c7ab;font-weight:750;box-shadow:0 2px 8px rgba(0,0,0,.13)}
aside a.parent-active:not(.active){background:rgba(255,255,255,.11);color:#fff;border-left-color:#83cdbb;font-weight:700}
aside a:focus-visible{outline:2px solid #91dfcc;outline-offset:2px}
main{min-width:0;padding:28px 34px 80px}
.hero{background:linear-gradient(135deg,#0a6a58,#21473f);color:white;border-radius:18px;padding:34px;box-shadow:var(--shadow)}
.hero h1{font-size:34px;line-height:1.2;margin:0 0 8px}
.hero p{margin:7px 0;color:#d9eee8}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.chip,.badge{display:inline-flex;align-items:center;border-radius:999px;padding:4px 9px;font-size:12px;font-weight:650}
.chip{background:rgba(255,255,255,.13);border:1px solid rgba(255,255,255,.24)}
section{scroll-margin-top:20px;margin-top:28px}
.panel{background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:22px;box-shadow:0 4px 18px rgba(30,65,55,.05)}
h2{font-size:24px;margin:0 0 15px;color:#124f43}
h3{font-size:18px;margin:22px 0 10px;color:#245e52}
h4{font-size:15px;margin:17px 0 6px}
p{margin:7px 0}
ul,ol{margin:7px 0;padding-left:22px}
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px}
th,td{border:1px solid var(--line);padding:8px 9px;text-align:left;vertical-align:top}
th{background:#edf6f3;color:#174e43;font-weight:700}
tr:nth-child(even) td{background:#fbfcfc}
.callout{border-left:4px solid var(--brand);background:var(--brand-soft);padding:12px 14px;border-radius:7px;margin:12px 0}
.callout.warn{border-color:#d18417;background:var(--warn-soft)}
.callout.danger{border-color:var(--danger);background:var(--danger-soft)}
.badge.current{background:var(--brand-soft);color:var(--brand)}
.badge.extend{background:var(--blue-soft);color:var(--blue)}
.badge.p2{background:var(--violet-soft);color:var(--violet)}
.badge.out{background:#eef0f0;color:#68716f}
.badge.method{background:#203c36;color:white;min-width:56px;justify-content:center}
.badge.get{background:#286b9b}.badge.post{background:#08715d}.badge.put{background:#8b5a00}
.badge.patch{background:#6a4da0}.badge.delete{background:#a33a3a}
.meta-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:12px 0}
.meta{border:1px solid var(--line);border-radius:8px;padding:8px;background:#fafcfb}
.meta b{display:block;font-size:11px;color:var(--muted);text-transform:uppercase}
.endpoint-card{background:white;border:1px solid var(--line);border-radius:12px;padding:17px;margin:13px 0;box-shadow:0 2px 10px rgba(20,60,50,.04);scroll-margin-top:16px}
.endpoint-head{display:flex;align-items:flex-start;gap:10px;flex-wrap:wrap}
.endpoint-head h3{margin:0;flex:1;min-width:260px}
.path{font:600 13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#f1f5f4;border-radius:6px;padding:5px 8px;word-break:break-all}
.contract-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.contract-grid>div{min-width:0;border-top:1px dashed var(--line);padding-top:9px}
.contract-grid h4{margin-top:0}
.filters{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:8px;position:sticky;top:8px;z-index:5;background:rgba(255,255,255,.96);padding:10px;border:1px solid var(--line);border-radius:10px;backdrop-filter:blur(8px)}
input,select,button{font:inherit;border:1px solid #bfcfca;border-radius:7px;padding:8px 10px;background:white}
button{cursor:pointer;background:#155f50;color:white;border-color:#155f50}
button.secondary{background:white;color:#155f50}
.toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}
.codegen-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:12px 0}
.codegen-card{border:1px solid var(--line);border-radius:10px;padding:13px;background:#fbfdfc}
.codegen-card h3{margin:0 0 8px;font-size:16px}
.contract-preview{max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;background:#173c34;color:#e9f7f2;border-radius:9px;padding:12px;font-size:12px}
.summary-grid{display:grid;grid-template-columns:repeat(5,minmax(120px,1fr));gap:10px}
.metric{padding:14px;border-radius:10px;background:#f4faf8;border:1px solid var(--line)}
.metric strong{display:block;font-size:25px;color:var(--brand)}
.state-map-shell{margin:14px 0;border:1px solid var(--line);border-radius:14px;background:linear-gradient(180deg,#fbfefd,#f6faf8);overflow:hidden;break-inside:avoid}
.state-map-scroll{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}
.state-map-svg{display:block;width:100%;min-width:980px;height:auto}
.state-map-svg text{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
.state-map-svg .edge{fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.state-map-svg .edge.main{stroke:#287263}.state-map-svg .edge.review{stroke:#bf7a1e;stroke-dasharray:7 6}
.state-map-svg .edge.danger{stroke:#bd4b4b}.state-map-svg .edge.archive{stroke:#667b75}
.state-map-svg .node-box{stroke-width:2.2;rx:14}
.state-map-svg .node.normal .node-box{fill:#f2fbf8;stroke:#5c9d8d}
.state-map-svg .node.review .node-box{fill:#fff8e9;stroke:#d49a35}
.state-map-svg .node.danger .node-box{fill:#fff1f1;stroke:#c74c4c}
.state-map-svg .node.end .node-box{fill:#edf2f1;stroke:#637a74}
.state-map-svg .node-cn{font-size:22px;font-weight:780;fill:#153f36}
.state-map-svg .node-code{font-size:13px;font-weight:700;letter-spacing:.8px;fill:#5e736d}
.state-map-svg .node-note{font-size:13px;fill:#536a64}
.state-map-svg .edge-label{font-size:14px;font-weight:700;fill:#294d45;text-anchor:middle}
.state-map-svg .edge-label.danger{fill:#9e3333}.state-map-svg .edge-label.archive{fill:#536762}
.state-map-svg .gate{fill:#fffdf7;stroke:#8f7d4b;stroke-width:2}
.state-map-svg .gate-title{font-size:15px;font-weight:750;fill:#544723;text-anchor:middle}
.state-map-svg .gate-note{font-size:12px;fill:#756846;text-anchor:middle}
.state-map-legend{display:flex;flex-wrap:wrap;gap:16px;padding:10px 16px;border-top:1px solid var(--line);background:white;font-size:12px;color:#536762}
.state-map-legend span{display:inline-flex;align-items:center;gap:6px}
.state-map-legend i{display:inline-block;width:24px;border-top:3px solid #287263}
.state-map-legend .review i{border-top-color:#bf7a1e;border-top-style:dashed}
.state-map-legend .danger i{border-top-color:#bd4b4b}.state-map-legend .archive i{border-top-color:#667b75}
.state-map-hint{display:none;padding:8px 14px;background:#fff8e9;color:#76541c;font-size:12px;border-top:1px solid #eadbb8}
.state-table{table-layout:fixed}.state-table th:nth-child(1){width:14%}.state-table th:nth-child(2){width:25%}
.state-table th:nth-child(3){width:40%}.state-table th:nth-child(4){width:21%}.state-table td:first-child code{white-space:nowrap}
.hidden{display:none!important}
.small{font-size:12px;color:var(--muted)}
.source{font-size:12px;color:#5f716c}
.nowrap{white-space:nowrap}
.footer{color:var(--muted);text-align:center;margin-top:35px}
@media(max-width:1100px){
.layout{grid-template-columns:1fr} aside{display:none} main{padding:18px}
.meta-grid{grid-template-columns:repeat(2,1fr)} .summary-grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:700px){
main{padding:12px}.hero,.panel{padding:16px}.hero h1{font-size:26px}
.contract-grid,.filters{grid-template-columns:1fr}.meta-grid{grid-template-columns:1fr}
.codegen-grid{grid-template-columns:1fr}
.endpoint-head h3{min-width:0}.panel,.endpoint-card{min-width:0;overflow-wrap:anywhere}
table{display:block;max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}
th,td{min-width:120px;overflow-wrap:anywhere}
.state-map-hint{display:block}
}
@media print{
@page{size:A4;margin:13mm}
body{background:white;font-size:10pt} aside,.filters,.no-print{display:none!important}
.layout{display:block}.hero{box-shadow:none;background:white;color:#172f29;border:1px solid #777;padding:18px}
.hero p{color:#435650}.panel,.endpoint-card{box-shadow:none;break-inside:avoid}
main{padding:0}.endpoint-card{page-break-inside:avoid}
.state-map-shell{page-break-inside:avoid}.state-map-svg{min-width:0}.state-map-hint{display:none}
a{color:inherit;text-decoration:none}.badge{border:1px solid #999;background:white!important;color:#222!important}
}
</style>
</head>
<body>
<div class="layout">
<aside>
<h2>供应商 API · v2.0</h2>
<a href="#overview">0. 文档结论</a>
<a href="#scope">1. 范围与依据</a>
<a href="#codegen">1.1 代码生成冻结规则</a>
<a href="#common">2. 通用契约</a>
<a href="#frontend-dicts">2.1 前端字典使用约定</a>
<a href="#permission">3. 权限矩阵</a>
<a href="#state">4. 状态机</a>
<a href="#catalog">5. 接口目录</a>
<a class="sub" href="#group-profile">档案与生命周期</a>
<a class="sub" href="#group-type">类型与附属信息</a>
<a class="sub" href="#group-approval">审批记录(本期 LOCAL_AUTO)</a>
<a class="sub" href="#group-account">收款账户</a>
<a class="sub" href="#group-import">导入(非本期)</a>
<a class="sub" href="#group-export">导出(非本期)</a>
<a class="sub" href="#group-wecom">Supplier 审批出站/回调</a>
<a href="#schemas">6. DTO / VO 字典</a>
<a href="#errors">7. 错误码</a>
<a href="#trace">8. 需求追踪</a>
<a href="#consistency">9. 双来源校验</a>
<a href="#acceptance">10. 验收矩阵</a>
<a href="#appendix-import">附录:非本期导入</a>
</aside>
<main>
<header class="hero">
<h1>供应商模块 API 接口规范</h1>
<p>发行版 v2.1 · 基线 v2.0 / 前端联调版 2026-08-25 · 已部署 TEST</p>
<p>供前端联调使用;契约基线来自 v2.0,运行状态以当前 TEST 验收结论为准。</p>
<div class="chips">
<span class="chip">后端 only</span>
<span class="chip">统一 Result&lt;T&gt;</span>
<span class="chip">本期 LOCAL_AUTO 审批 · TEST</span>
<span class="chip">后续 WECOM 对接</span>
<span class="chip">敏感字段严格边界</span>
<span class="chip">OpenAPI 3.0 内嵌</span>
<span class="chip">单文件离线 HTML</span>
</div>
</header>
<section id="overview" class="panel">
<h2>0. 文档结论</h2>
<div id="metrics" class="summary-grid"></div>
<div class="callout ok">
<b>前端联调状态:</b>本版以当前 TEST 验收结果为准。15 个接口已实现并可联调;SUP-ADM-010 已实现权限门禁和失败关闭,但 Finance 清账提供方未接入,当前不可成功归档。
</div>
<table>
<thead><tr><th>接口范围</th><th>状态</th><th>前端接入说明</th></tr></thead>
<tbody>
<tr><td>SUP-ADM-001、002、003、004、007、011、012、043</td><td>已实现 · 可联调</td><td>按接口卡片请求、响应、权限和错误码接入</td></tr>
<tr><td>SUP-ADM-034、035、036、041</td><td>已实现 · 可联调</td><td>账户证明字段按权限裁剪;账号仅返回脱敏值</td></tr>
<tr><td>SUP-ADM-048、049、050</td><td>已实现 · 可联调</td><td>资源页面调用;改绑/解绑必须携带并发版本字段</td></tr>
<tr><td>SUP-ADM-010</td><td>已实现 · 失败关闭</td><td>权限通过后仍返回 395032;清账提供方就绪前不要按成功链路联调</td></tr>
</tbody>
</table>
<div class="callout">
本文收录本期全部 16 个 HTTP 契约,并补充当前实现状态、权限、TEST 结论与前端接入限制。
15 个接口已实现并可联调;SUP-ADM-010 仅保留失败关闭行为,清账提供方未接入前不可成功联调。
</div>
<div class="callout ok">
<b>审批分期冻结:</b>本期完整实现审批表、候选快照、状态机、审计和统一结果应用器,审批提供方固定为 <code>LOCAL_AUTO</code>。
本地提供方返回真实的标准化 <code>APPROVED</code> 结果,由统一结果应用器完成主体、账户、资质和状态变更;不得伪造 <code>spNo</code>、企微模板、审批人、部门、意见、原始企微状态或回调。
后续仅新增 <code>WECOM</code> 提供方适配器、Feign/回调/对账,不改审批表主模型、业务状态机和结果应用逻辑。
</div>
<div class="callout warn">
<b>可生成范围:</b>本文件用于前端联调,不作为代码生成输入。实现状态以每张接口卡片和下方验收矩阵为准;不得据此推断归档成功链路已开放。
</div>
<ul>
<li>供应商为资源管理之前的独立一级模块,管理端基址为 <code>/admin/supplier</code>。</li>
<li>供应商菜单、页面与按钮由平台配置 <code>supplier:*</code> 权限码;服务端逐接口校验权限并失败关闭,不定义固定本地角色。类型、信用、状态、审批意见和账户证明附件使用独立权限码。</li>
<li>本地不提供 approve、reject 或“我的审批待办”;审批人在企业微信处理。转交、加签和同意并加签本期不建模。</li>
<li>企业微信 Supplier 模板必须增加申请人、必需审批节点和实际审批人的受控部门字段;外部适配器按 controlId 从表单/详情同步,完整追溯统一保存到单个版本化 <code>detail_snapshot_ciphertext</code>,禁止用当前通讯录补写。</li>
<li>Gateway 路由与最小权限已在 #6191/#6194 完成并经 TEST 验证。</li>
<li>注册主体允许 0..N 个初始账户;没有账户也能完成注册,但不具备付款资格。</li>
<li>供应商页面统一使用平台字典并按需加载:供应商类型读取 <code>supplier_type</code>(<code>GET /admin/dict/data/supplier_type</code>),生命周期读取 <code>supplier_lifecycle_status</code>(<code>GET /admin/dict/data/supplier_lifecycle_status</code>);业务接口只传编码,不传中文标签或字典数据 ID。</li>
<li>接口文档冻结 API 与业务逻辑;本期物理表统一使用详细设计冻结的 <code>supplier_*</code>,不增加 <code>resource_</code> 前缀。Entity、Mapper、DDL 和索引仍须在新任务 worktree 中核对当前 migration 与实际 schema。</li>
</ul>
</section>
<section id="scope" class="panel">
<h2>1. 范围与事实依据</h2>
<table>
<thead><tr><th>来源</th><th>负责范围</th><th>使用方式</th></tr></thead>
<tbody>
<tr><td><a href="供应商模块详细设计-v3.0.html">供应商模块详细设计 v3.0</a></td><td>接口、权限、状态、业务规则、审批、幂等、DTO/VO</td><td>接口契约主依据</td></tr>
<tr><td><a href="数据模型.html">数据模型</a></td><td>字段、类型、必填、枚举、默认值、唯一性、索引与迁移边界</td><td>从详细设计 v3.0 数据库章节抽取;字段基线同步《数据模型》v1.9.1,物理落表范围以当前数据模型为准</td></tr>
<tr><td>HL 供应商专项硬规则</td><td>后端边界、平台菜单/按钮权限、删除、日志、菜单缓存</td><td>最高项目边界,不被普通需求放宽</td></tr>
</tbody>
</table>
<h3>严格收敛规则</h3>
<ol>
<li>路径、方法、平台权限码、状态、审批语义以详细设计 v3.0 为准。</li>
<li>字段上限以本文件“共享 DTO / VO 字典”的冻结值为准,并与最新详细设计的字段修订保持一致;不得再回退到历史较小值。</li>
<li>审批完整快照、幂等、防乱序、可靠事件采用 v3.0 的更严格增量。</li>
<li>账号掩码不落库;物理表前缀统一为详细设计冻结的 <code>supplier_*</code>。对外契约锁定“永不返回完整账号”。</li>
<li>本文件未定义的枚举、缓存和降级语义不得自行补造;供应商错误码已冻结为资源服务共享段中的 <code>395xxx</code> 子段。</li>
</ol>
<h3>本期不做</h3>
<ul>
<li>批量导入、导入批次、导入明细、失败清单和失败重导;本期同时不建导入表、不生成 Entity/Mapper/DTO/Controller/Service/Job、空实现或假成功。</li>
<li>履约评价自动采集与信用等级自动聚合。</li>
<li>本地审批流、审批按钮、审批待办。</li>
<li>供应商域中的应付、付款、团核算、余额和付款金额算法。</li>
<li>任何前端源码或资源修改。</li>
</ul>
</section>
<section id="codegen" class="panel">
<h2>1.1 后端代码生成冻结规则</h2>
<div class="callout">
<b>生成结论:</b>本文件是本期供应商后端 API 与业务逻辑的单一生成输入。OpenAPI 冻结路径、方法、认证、operationId 和核心 Schema;
每张接口卡片中的“业务、状态与副作用”、权限矩阵、状态机、错误码和验收矩阵共同冻结 Service 行为。若与当前源码、测试、migration 或实际 schema 冲突,必须停止生成冲突部分并先完成契约审计。
</div>
<div class="codegen-grid">
<div class="codegen-card">
<h3>允许生成</h3>
<ul>
<li><code>hl-resource-service</code> 下 supplier 后端包的 Controller、DTO/VO、Application Service、Domain Guard、Mapper 接口与测试骨架。</li>
<li>允许在 supplier 后端包下按同一业务功能新建子包,将相关 Controller、DTO/VO、Application Service、Domain Guard、Mapper、集成适配器及测试集中管理;测试包结构应与源码对应,不得新建无业务边界的顶级模块,也不得把无关功能混入同一目录。</li>
<li>supplier 包内的出站 Port、本地适配器接口与测试桩;外部能力不可用时必须失败关闭。</li>
<li>Supplier 企微闭环所需的 <code>hl-common-core</code> 公共 DTO、现有 <code>ApprovalFeignClient</code> 新方法与 fallback、<code>hl-user-service</code> 提供方委托/详情规范化/回调调用方及双方契约测试;旧方法签名保持兼容。</li>
</ul>
</div>
<div class="codegen-card">
<h3>禁止自动生成</h3>
<ul>
<li>任何管理端、小程序、Web、H5 或桌面端源码和资源。</li>
<li>未经当前 migration/schema 审计的 Entity 表名、跨 schema 写库、共享库 DDL、Flyway 自动开启。</li>
</ul>
</div>
<div class="codegen-card">
<h3>本方案边界</h3>
<ul>
<li>Supplier 主体业务施工位于 <code>hl-resource-service</code> 的 supplier 后端包及所属 schema migration;Gateway 路由仍作为独立后端任务。VEHICLE 只依赖 Fleet 内部只读能力,本文不冻结新路径;实现前先审计现有契约,不足部分另行评审 common Feign 与 Fleet 提供方变更。</li>
<li>本期定义 <code>SupplierApprovalProvider</code> SPI 并只注册 <code>LocalAutoApprovalProvider</code>。提交先持久化审批实例和候选快照,再由本地提供方返回标准化通过结果,最后由公共 <code>SupplierApprovalResultApplier</code> 锁行、复核并应用;禁止 Controller/Service 直接跳过审批表改状态。</li>
<li>后续 <code>WeComApprovalProvider</code> 才复用/扩展现有 <code>ApprovalFeignClient</code>、回调和企微配置。管理端始终不得传 provider、模板、级次或审批人。</li>
</ul>
</div>
<div class="codegen-card">
<h3>后续 WECOM 配置键预留(本期不读取)</h3>
<ul>
<li><code>approval.supplier.profile.template-id</code>:PROFILE_CREATE。</li>
<li><code>approval.supplier.account.template-id</code>:ACCOUNT_CREATE / ACCOUNT_CHANGE;账户事实包含 settleMode/accountPeriod/invoiceType/taxRate。</li>
<li><code>approval.supplier.status.template-id</code>:bizType=STATUS_CHANGE;subType=STATUS_SUSPEND / STATUS_RESUME / STATUS_FREEZE / STATUS_UNFREEZE / STATUS_BLACKLIST / STATUS_UNBLACKLIST。</li>
<li><code>approval.supplier.*.control-ids.*</code>:requestNo、supplierId、bizType、subType、摘要、applicantDepartments、levelDepartments、approverDepartments 等控件 ID;启动时完整校验,缺失返回 395019。</li>
</ul>
</div>
</div>
<h3>OpenAPI 3.0 机器契约</h3>
<p>仅导出本期 16 个 HTTP operation。所有 operation 都带 <code>x-hl-access</code>、<code>x-hl-permissions</code>、<code>x-hl-rules</code>、<code>x-hl-errors</code> 和原始请求/响应契约,供代码生成器同时生成结构与强制业务策略;不能只读取 path 而忽略扩展字段。管理端菜单和按钮由平台配置,服务端统一调用现有 <code>UserFeignClient.hasPermission</code> 校验权限码且异常失败关闭,不再生成固定角色判断。</p>
<div class="callout">
<b>契约基线扩展:</b>沿用 codegen-r26 的全部审批、账户、软删除和路径规则;保留 <code>supplier_resource_rel</code> 作为统一关系表,但维护入口全部归属资源模块及车队模块。供应商详情只有“基本信息、账号信息”两个页签,不生成资源候选、供应商侧绑定或批量解绑接口;资源页面复用供应商分页/有界列表选择供应商,并通过 SUP-ADM-048~050 查询、设置/改绑或解除关系。同一资源只允许一个有效供应商。VEHICLE 对应菜单“车队管理-车队管理”,仅通过 Fleet 内部只读能力校验,禁止跨 schema SQL,具体 Fleet 接口路径在实施审计后确定。
</div>
<table>
<thead><tr><th>策略</th><th>生成目标</th><th>不可替代项</th></tr></thead>
<tbody>
<tr><td>Guard</td><td>Permission/Profile/Type/Account/Approval/Qualification/Clearance Guard;Controller 不写固定角色 if-else</td><td>可信身份、平台权限码、归属、软删、状态、expectedUpdateTime、字段规则和失败零副作用</td></tr>
<tr><td>状态机</td><td>Supplier/Account COLA Config + Helper + CAS Mapper + 全矩阵测试</td><td>未声明迁移 395005;合法自循环显式白名单;CAS miss 395014</td></tr>
<tr><td>事务</td><td>本地写单事务;外部副作用采用 PREPARE_TX → NO_TX_EXTERNAL → RESULT_TX</td><td>Feign/MQ/文件/企微不得在数据库事务或行锁内调用;禁止 this 自调用绕过代理</td></tr>
<tr><td>锁</td><td>Service 代理入口 <code>@Lock4j</code> + 锁内 FOR UPDATE 重读 + CAS/唯一键</td><td>固定数据库锁序;Redis 锁不是唯一正确性来源</td></tr>
<tr><td>防重与幂等</td><td>管理端写接口沿用现有 <code>@Idempotent</code> Redis 短窗防重;审批统一使用 requestNo</td><td>短窗防重不是持久正确性来源;同时依靠聚合锁、状态机、CAS/唯一约束、requestNo 及结果应用幂等;spNo 仅后续 WECOM 使用</td></tr>
</tbody>
</table>
<div class="toolbar no-print">
<button id="downloadOpenApiBtn">下载 OpenAPI JSON</button>
<button id="copyOpenApiBtn" class="secondary">复制 OpenAPI JSON</button>
</div>
<p id="openApiStatus" class="small">正在生成内嵌契约…</p>
<pre id="openApiPreview" class="contract-preview">正在生成…</pre>
<h3>生成准入门禁</h3>
<ol>
<li>先在新任务 worktree 中重新核对分支、工作区、Supplier 所属 migration/实际 schema,以及现有外部运行条件;外部条件不满足时只关闭对应 Port,不修改其他模块。</li>
<li>Supplier 业务代码已按关联工单完成独立审计与 TEST 验收;Gateway 必须另开后端任务补充 <code>/admin/supplier/**</code> 路由并运行路由审计。企微适配器模仿系统现有调用接口,Supplier 模板/controlId 未完成配置与实证时保持关闭,且不得标记“跨模块联调完成”。</li>
<li>只生成本期 operation;每个写接口必须实现可信身份、服务端授权、状态 Guard、锁/幂等、事务、审计和失败零副作用。</li>
<li>错误码使用本文件的逐项冻结值:395001~395039;实现后由 <code>ErrorCodeRegistry</code> 做范围与重复 FAIL-FAST 校验。</li>
<li>跨服务写只能沿用本地事务 + 可靠事件/对账;不得同步直写其他 schema。</li>
<li>生成结果必须补齐成功、未认证、越权、缺参、边界、非法状态、重复/乱序/超时与副作用测试后,才能称“实现完成”。</li>
</ol>
</section>
<section id="common" class="panel">
<h2>2. 通用接口契约</h2>
<table>
<tbody>
<tr><th>管理端基址</th><td><code>/admin/supplier</code>,必须经 Gateway,认证级别 MANDATORY</td></tr>
<tr><th>内部基址</th><td><code>/internal/supplier</code>,沿用当前 X-Internal-Token;调用方身份/白名单基础设施不在本方案新增</td></tr>
<tr><th>响应</th><td><code>Result&lt;T&gt;</code>:code、message、success、data;业务失败可能仍为 HTTP 200,错误时 data 允许 null</td></tr>
<tr><th>分页</th><td><code>PageResult&lt;T&gt;</code>:records、total、page、pageSize;total/page/pageSize 与当前公共类的 int 对齐;page 默认 1,pageSize 默认 20、最大 100</td></tr>
<tr><th>ID</th><td>所有 Snowflake Long 在 JSON 中序列化为 String</td></tr>
<tr><th>日期/时间</th><td>yyyy-MM-dd / yyyy-MM-dd HH:mm:ss</td></tr>
<tr><th>前端字典</th><td>供应商页面按需调用平台字典接口;供应商类型使用 <code>GET /admin/dict/data/supplier_type</code>,生命周期使用 <code>GET /admin/dict/data/supplier_lifecycle_status</code>。<code>dictValue</code> 是业务编码,<code>dictLabel</code> 仅用于展示。</td></tr>
<tr><th>排序</th><td>sortBy 仅接受接口白名单;sortDirection=ASC/DESC;默认 createTime DESC, supplierId DESC</td></tr>
<tr><th>并发</th><td>更新接口携带 expectedUpdateTime;无 body 的软删除命令锁行后重查软删除标记、当前状态与外部引用,不新增 version 字段</td></tr>
<tr><th>防重与幂等</th><td>管理端写接口沿用 <code>hl-starter-protection</code> 的 <code>@Idempotent</code> Redis 短窗防重,key 禁止拼接敏感明文。业务正确性由聚合锁、状态机、expectedUpdateTime/CAS、数据库唯一约束、唯一 requestNo 和结果应用器幂等保证;本期 LOCAL_AUTO 不产生 spNo,后续 WECOM 才用 requestNo/spNo 对账。</td></tr>
<tr><th>身份</th><td>adminId、applicantAdminId、permissionCodes、createdBy、templateId、approverUserIds、spStatus 不得由管理端请求体传入。Supplier 从可信 X-Admin-Id 取得申请人,按 operation 配置的权限码服务端校验后,仅在 Supplier 出站 Port 的内部 DTO 写 applicantAdminId</td></tr>
<tr><th>敏感字段</th><td>只允许 Body;禁止 Query/Path;请求 DTO、账号、税号、手机号、审批意见不得进入普通日志</td></tr>
</tbody>
</table>
<h3>统一成功包络</h3>
<pre><code>{
"code": 200,
"message": "成功",
"success": true,
"data": {}
}</code></pre>
<h3>写接口统一执行顺序</h3>
<ol>
<li>可信身份、平台菜单/按钮权限、数据范围校验。</li>
<li>目标存在性、归属、软删除和当前状态校验。</li>
<li>expectedUpdateTime、唯一性、业务字段和敏感字段校验。</li>
<li>管理端写接口进入 <code>@Idempotent</code> 短窗防重;锁内继续执行状态、CAS 与唯一约束校验。</li>
<li>本地事务写业务数据、变更日志和待发布事件。</li>
<li>事务提交后才调用已启用的 Supplier 出站 Port;结果不确定时进入对账,不重复发起。DOMAIN_EVENT Publisher Port 未独立接入时保持关闭。</li>
</ol>
<div class="callout"><b>供应商模块统一软删除:</b>凡接口语义为删除数据库业务记录,统一写对应表的 <code>deleted_at=当前时间</code>,禁止执行物理 <code>DELETE FROM supplier_*</code>。默认列表、详情、统计、JOIN、资格校验和内部查询必须对参与查询的每张业务表分别追加 <code>deleted_at IS NULL</code>;原生 SQL/XML Mapper 不得依赖 MyBatis-Plus 自动过滤。归档和合同终止属于状态变化,不属于删除。审批、变更日志和审计证据不得删除。</div>
</section>
<section id="frontend-dicts" class="panel">
<h2>2.1 前端字典使用约定</h2>
<div class="callout ok">
<b>前端接入口径:</b>供应商页面不得在组件内写死类型或生命周期中文文案。下拉框、筛选项、表格状态和详情标签从平台字典读取;供应商业务接口的请求与响应仍只使用稳定业务编码。
</div>
<table>
<thead><tr><th>业务字段</th><th>字典类型</th><th>用途</th><th>提交/匹配值</th></tr></thead>
<tbody>
<tr><td><code>typeCode</code> / <code>types[].typeCode</code></td><td><code>supplier_type</code></td><td>建档多选、列表筛选、列表与详情中文展示</td><td><code>dictValue</code></td></tr>
<tr><td><code>status</code></td><td><code>supplier_lifecycle_status</code></td><td>列表筛选、列表与详情生命周期展示</td><td><code>dictValue</code></td></tr>
<tr><td><code>resourceModule</code></td><td>后端固定枚举,非平台字典</td><td>资源页面路由校验和菜单原文展示</td><td>10 个冻结编码之一</td></tr>
</tbody>
</table>
<h3>查询方式</h3>
<ol>
<li>供应商类型按需调用 <code>GET /admin/dict/data/supplier_type</code>。</li>
<li>供应商生命周期按需调用 <code>GET /admin/dict/data/supplier_lifecycle_status</code>。</li>
<li>两个接口均经 Gateway 访问并需要管理员认证;返回的 <code>data</code> 只包含启用项,并按 <code>sortOrder ASC</code>、<code>dictDataId ASC</code> 排序,页面无需再次按中文排序。</li>
</ol>
<pre><code>// GET /admin/dict/data/supplier_lifecycle_status
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{ "dictType": "supplier_lifecycle_status", "dictLabel": "草稿", "dictValue": "DRAFT", "sortOrder": 10, "status": "ACTIVE" },
{ "dictType": "supplier_lifecycle_status", "dictLabel": "注册审核中", "dictValue": "VETTING", "sortOrder": 20, "status": "ACTIVE" },
{ "dictType": "supplier_lifecycle_status", "dictLabel": "合作中", "dictValue": "ACTIVE", "sortOrder": 30, "status": "ACTIVE" }
]
}</code></pre>
<h3>组件映射规则</h3>
<pre><code>const options = dictData.map(item =&gt; ({
label: item.dictLabel,
value: item.dictValue
}))
// 展示:用业务响应中的 code 匹配 dictValue,再显示 dictLabel
// 提交:只提交 value(例如 ACTIVE / HOTEL),不得提交中文、dictDataId 或整条字典对象</code></pre>
<ul>
<li><b>类型字段:</b>创建、更新和提交注册表单只发送 <code>types: [{ typeCode: "HOTEL" }]</code>;请求不接收 <code>typeName</code>。列表和详情响应中的 <code>typeName</code> 是服务端显示快照,不能作为业务判断依据。</li>
<li><b>生命周期字段:</b>列表筛选提交 <code>status</code> 编码;状态变更按钮仍必须按第 4 节状态机收窄合法目标,不能把 7 个字典项全部当作可跳转状态。</li>
<li><b>资源模块:</b><code>resourceModule</code> 不是 supplier_type 字典;固定为 SCENIC、RESTAURANT、SUPPLIES、SUPPLIES_COMBO、ACTIVITY、HOTEL、SERVICE、COST_ITEM、STAFF、VEHICLE,VEHICLE 的菜单原文为“车队管理-车队管理”。<code>FLEET</code> 仍可作为供应商类型/资格编码,但供应商侧不新增车队或挂接车辆。</li>
<li><b>同名状态:</b>字典接口数据项自身的 <code>status=ACTIVE</code> 表示“该字典项启用”,不是供应商处于合作中;供应商生命周期必须读取该项的 <code>dictValue</code>。</li>
<li><b>未知编码:</b>未匹配到字典时显示原始编码并上报契约异常,不得显示错误中文或静默改写业务值。</li>
<li><b>缓存更新:</b>沿用平台现有字典缓存刷新机制;字典管理修改后不得另维护供应商页面私有枚举副本。</li>
</ul>
<h3>当前启用值(2026-08-21)</h3>
<table>
<thead><tr><th>字典类型</th><th>按排序号冻结的当前值</th></tr></thead>
<tbody>
<tr><td><code>supplier_type</code></td><td><code>SCENIC</code>=景区,<code>RESTAURANT</code>=餐厅,<code>SUPPLIES</code>=备品,<code>ACTIVITY</code>=游玩项目,<code>HOTEL</code>=酒店,<code>SERVICE</code>=服务,<code>FLEET</code>=车队,<code>RENTAL</code>=租车,<code>PERFORMANCE</code>=演艺,<code>INSURANCE</code>=保险,<code>CHANNEL</code>=OTA/渠道,<code>PROFESSIONAL_SERVICE</code>=专业服务,<code>PROPERTY</code>=物业/房租,<code>TICKET</code>=票务,<code>OTHER</code>=其他</td></tr>
<tr><td><code>supplier_lifecycle_status</code></td><td><code>DRAFT</code>=草稿,<code>VETTING</code>=注册审核中,<code>ACTIVE</code>=合作中,<code>SUSPENDED</code>=暂停,<code>FROZEN</code>=冻结,<code>BLACKLIST</code>=黑名单,<code>ARCHIVED</code>=清账归档</td></tr>
</tbody>
</table>
<p class="small">上表用于契约审阅和回归测试;运行时选项仍以字典接口返回的启用项为准。业务状态机和服务端枚举校验不因字典标签修改而改变。</p>
</section>
<section id="permission" class="panel">
<h2>3. 权限与字段可见性</h2>
<table>
<thead><tr><th>业务能力</th><th>平台菜单/按钮权限</th><th>服务端规则</th></tr></thead>
<tbody>
<tr><td>供应商列表与两类详情</td><td><code>supplier:list</code> / <code>supplier:view</code></td><td>查看未删除供应商及脱敏字段;供应商详情仅包含基本信息和账号信息</td></tr>
<tr><td>创建、更新、删除草稿</td><td><code>supplier:create</code> / <code>supplier:update</code> / <code>supplier:delete</code></td><td>同时校验状态机、归属、并发版本和失败零写入</td></tr>
<tr><td>类型及资质规则维护</td><td><code>supplier:type:manage</code></td><td>查看已删除类型另需 <code>supplier:type:history</code></td></tr>
<tr><td>审批提交</td><td><code>supplier:approval:submit</code></td><td>供应商注册审批和账户审批均不提供撤销接口;本地不提供通过/驳回</td></tr>
<tr><td>账户和状态维护</td><td><code>supplier:account:manage</code> / <code>supplier:status:manage</code></td><td>不同能力独立授权,不因拥有任一权限自动获得其他能力</td></tr>
<tr><td>资源侧供应商维护</td><td><code>supplier:update</code></td><td>仅由资源与车队模块发起;同时校验供应商状态、类型、必备资质、资源存在性、数据范围和单资源唯一有效供应商;VEHICLE 依赖失败时关闭</td></tr>
<tr><td>审批意见正文</td><td><code>supplier:approval:opinion</code></td><td>无权限仅返回 <code>hasOpinion</code>;有权限按需解密并记录敏感读取审计</td></tr>
<tr><td>账户证明附件</td><td><code>supplier:account:proof:read</code></td><td>无权限省略 <code>proofFileUrls</code>,账户列表始终不返回</td></tr>
<tr><td>本地通过/驳回</td><td>不配置</td><td>只能在企业微信办理;转交、加签和同意并加签本期不建模</td></tr>
</tbody>
</table>
<div class="callout"><b>列表可见范围:</b>管理员获得 <code>supplier:list</code>/<code>supplier:view</code> 菜单权限后,可查看全部未删除供应商及脱敏账户,不按创建人或供应商类型过滤;未认证或未配置对应权限的账户不可查看,任何管理员都不能通过管理端取得完整账号。</div>
<div class="callout"><b>附件可见范围:</b>账户列表永不返回 proofFileUrls;账户单项详情只有同时具备 <code>supplier:view</code> 与 <code>supplier:account:proof:read</code> 的管理员返回证明附件,否则仅返回其他脱敏信息。</div>
<div class="callout"><b>平台权限口径:</b>供应商不定义固定本地角色矩阵。平台按菜单/按钮配置 <code>supplier:*</code> 权限码;企微模板中的“财务领导”“公司领导”仅是 OA 审批节点,不映射为 HL 固定角色。</div>
<div class="callout"><b>权限代码生成:</b>每个 <code>/admin/supplier/**</code> operation 必须读取 <code>x-hl-permissions</code>,用可信 <code>X-Admin-Id</code> 调用现有 <code>com.hulalv.common.feign.UserFeignClient.hasPermission(Long,String)</code>。返回 false、非成功 Result、超时或异常全部拒绝;服务端只按权限码授权。</div>
<div class="callout warn"><b>高风险动作:</b>类型、信用、状态、审批意见和账户证明附件分别使用独立按钮权限码;是否可操作完全由平台授权结果决定,不生成固定角色策略或角色上限。</div>
<div class="callout danger">隐藏按钮不是授权。未配置权限的管理员直接构造请求时,服务端必须拒绝,且数据库、企微出站 Port 与 DOMAIN_EVENT 均为零副作用。</div>
</section>
<section id="state" class="panel">
<h2>4. 状态机与可用性</h2>
<h3>供应商七态总流程图</h3>
<div class="state-map-shell">
<div class="state-map-scroll">
<svg class="state-map-svg" viewBox="0 0 1450 650" role="img" aria-labelledby="state-map-title state-map-desc">
<title id="state-map-title">供应商七态总流程图</title>
<desc id="state-map-desc">草稿提交后进入注册审批中,审批通过进入合作中,驳回回到草稿。合作中可暂停、冻结或拉黑;暂停和冻结可分别恢复合作;合作中、暂停合作、风险冻结均可拉黑;解除黑名单只进入暂停合作;暂停合作和黑名单在财务清账确认后可归档。</desc>
<defs>
<marker id="arrow-main" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10 Z" fill="#287263"/></marker>
<marker id="arrow-review" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10 Z" fill="#bf7a1e"/></marker>
<marker id="arrow-danger" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10 Z" fill="#bd4b4b"/></marker>
<marker id="arrow-archive" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10 Z" fill="#667b75"/></marker>
</defs>
<path class="edge main" d="M200 305 H250" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="225" y="238">提交建档</text>
<path class="edge main" d="M440 305 H500" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="470" y="238">四级审批通过</text>
<path class="edge review" d="M345 355 C345 425 115 425 115 355" marker-end="url(#arrow-review)"/>
<text class="edge-label" x="230" y="450">驳回</text>
<path class="edge main" d="M650 255 C680 190 720 140 760 115" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="700" y="185">暂停</text>
<path class="edge main" d="M760 78 C700 88 655 175 620 255" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="680" y="70">恢复(资质有效)</text>
<path class="edge main" d="M650 355 C680 420 720 495 760 525" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="700" y="455">冻结</text>
<path class="edge main" d="M760 565 C700 555 655 455 620 355" marker-end="url(#arrow-main)"/>
<text class="edge-label" x="685" y="600">核验恢复</text>
<path class="edge danger" d="M680 305 H1010" marker-end="url(#arrow-danger)"/>
<text class="edge-label danger" x="845" y="292">拉黑(企微一级审批)</text>
<path class="edge danger" d="M950 105 C990 130 1020 200 1060 255" marker-end="url(#arrow-danger)"/>
<text class="edge-label danger" x="1008" y="178">拉黑</text>
<path class="edge danger" d="M950 540 C990 515 1020 415 1060 355" marker-end="url(#arrow-danger)"/>
<text class="edge-label danger" x="1008" y="465">拉黑</text>
<path class="edge danger" d="M1110 255 C1110 175 1040 80 950 80" marker-end="url(#arrow-danger)"/>
<text class="edge-label danger" x="1080" y="60">解除(企微两级)→ 仅到暂停</text>
<path class="edge archive" d="M950 135 C1090 140 1220 190 1285 255" marker-end="url(#arrow-archive)"/>
<text class="edge-label archive" x="1120" y="145">清账归档</text>
<path class="edge archive" d="M1200 305 H1260" marker-end="url(#arrow-archive)"/>
<text class="edge-label archive" x="1230" y="290">清账</text>
<path class="edge archive" d="M1310 360 V485" marker-end="url(#arrow-archive)"/>
<text class="edge-label archive" x="1360" y="430">确认已清</text>
<g class="node normal" transform="translate(30 255)">
<rect class="node-box" width="170" height="100"/>
<text class="node-cn" x="15" y="29">草稿</text><text class="node-code" x="15" y="51">DRAFT</text>
<text class="node-note" x="15" y="73">档案/初始账户可编辑</text><text class="node-note" x="15" y="90">未生成供应商编号</text>
</g>
<g class="node review" transform="translate(250 255)">
<rect class="node-box" width="190" height="100"/>
<text class="node-cn" x="15" y="29">注册审批中</text><text class="node-code" x="15" y="51">VETTING</text>
<text class="node-note" x="15" y="73">企业微信四级审批</text><text class="node-note" x="15" y="90">驳回回草稿</text>
</g>
<g class="node normal" transform="translate(500 255)">
<rect class="node-box" width="180" height="100"/>
<text class="node-cn" x="15" y="29">合作中</text><text class="node-code" x="15" y="51">ACTIVE</text>
<text class="node-note" x="15" y="73">允许维护与新业务关联</text><text class="node-note" x="15" y="90">付款/关联资格实时派生</text>
</g>
<g class="node review" transform="translate(760 45)">
<rect class="node-box" width="190" height="105"/>
<text class="node-cn" x="15" y="30">暂停合作</text><text class="node-code" x="15" y="53">SUSPENDED</text>
<text class="node-note" x="15" y="77">整改后可申请恢复</text><text class="node-note" x="15" y="95">可在清账后归档</text>
</g>
<g class="node review" transform="translate(760 485)">
<rect class="node-box" width="190" height="105"/>
<text class="node-cn" x="15" y="30">风险冻结</text><text class="node-code" x="15" y="53">FROZEN</text>
<text class="node-note" x="15" y="77">补资质后核验</text><text class="node-note" x="15" y="95">不得直接归档</text>
</g>
<g class="node danger" transform="translate(1010 255)">
<rect class="node-box" width="190" height="100"/>
<text class="node-cn" x="15" y="29">黑名单</text><text class="node-code" x="15" y="51">BLACKLIST</text>
<text class="node-note" x="15" y="73">禁止新付款/新资源关联</text><text class="node-note" x="15" y="90">解除只进入暂停合作</text>
</g>
<polygon class="gate" points="1310,250 1360,305 1310,360 1260,305"/>
<text class="gate-title" x="1310" y="295">Finance</text><text class="gate-title" x="1310" y="313">清账确认</text>
<text class="gate-note" x="1310" y="333">不明确 = 未清</text>
<g class="node end" transform="translate(1215 485)">
<rect class="node-box" width="190" height="105"/>
<text class="node-cn" x="15" y="30">已归档</text><text class="node-code" x="15" y="53">ARCHIVED</text>
<text class="node-note" x="15" y="77">只读且不可恢复</text><text class="node-note" x="15" y="95">历史引用永久不断链</text>
</g>
</svg>
</div>
<div class="state-map-legend" aria-label="流程线图例">
<span><i></i>正常迁移/恢复</span><span class="review"><i></i>审批驳回</span>
<span class="danger"><i></i>拉黑与解除</span><span class="archive"><i></i>Finance 清账归档</span>
</div>
<div class="state-map-hint">流程图可左右滑动查看完整节点。</div>
</div>
<div class="callout"><b>总门禁:</b>注册提交先执行 C-02/C-06/C-09,有初始账户时再逐条执行 C-03/C-19。除无 body 的清账归档外,其他状态命令必须校验角色、当前状态、changeReason、expectedUpdateTime;清账归档由服务端锁行重读当前状态并生成审计上下文。任何状态命令都必须在结果应用前锁行重查,未画出的迁移统一返回 <code>SUPPLIER_STATUS_TRANSITION_INVALID</code>。</div>
<table class="state-table">
<thead><tr><th>当前状态</th><th>状态内操作(不发生迁移)</th><th>合法目标状态</th><th>禁止/硬约束</th></tr></thead>
<tbody>
<tr><td>草稿<br><code>DRAFT</code></td><td>编辑档案、类型、资质、联系人和 0..N 初始账户;可软删除</td><td><code>VETTING</code>(提交 PROFILE_CREATE)</td><td>不得供业务选择、付款、生成 supplierNo 或发起注册后独立账户审批</td></tr>
<tr><td>注册审批中<br><code>VETTING</code></td><td>查看注册进度;可按 1.5 更新非主体标识资料</td><td><code>ACTIVE</code>(注册最终通过);<code>DRAFT</code>(注册驳回)</td><td>不得修改 fullName、taxNo 或已提交审批快照</td></tr>
<tr><td>合作中<br><code>ACTIVE</code></td><td>维护联系人、类型、资质规则和注册后账户</td><td><code>SUSPENDED</code>(暂停);<code>FROZEN</code>(冻结);<code>BLACKLIST</code>(拉黑)</td><td>不得修改红字段或直接删除</td></tr>
<tr><td>暂停合作<br><code>SUSPENDED</code></td><td>查看、整改资料;整改本身仍保持 <code>SUSPENDED</code></td><td><code>ACTIVE</code>(资质有效后恢复);<code>BLACKLIST</code>(拉黑);<code>ARCHIVED</code>(Finance 清账确认后归档)</td><td>不得新付款、新业务选择或删除</td></tr>
<tr><td>风险冻结<br><code>FROZEN</code></td><td>查看、补资质;整改本身仍保持 <code>FROZEN</code></td><td><code>ACTIVE</code>(核验恢复);<code>BLACKLIST</code>(拉黑)</td><td>不得新付款、新业务选择或直接归档</td></tr>
<tr><td>黑名单<br><code>BLACKLIST</code></td><td>查看历史与整改材料</td><td><code>SUSPENDED</code>(解除两级审批通过);<code>ARCHIVED</code>(Finance 清账确认后归档)</td><td>不得直接进入 ACTIVE、新付款、新资源关联或删除</td></tr>
<tr><td>已归档<br><code>ARCHIVED</code></td><td>只读查看历史档案、账户和引用</td><td>无</td><td>不可逆;不得恢复、编辑、删除或发起新交易</td></tr>
</tbody>
</table>
<p class="small">“状态内操作”不是新状态;只有“合法目标状态”列表示状态变化。任何未在详细设计状态图中的迁移均拒绝。</p>
</section>
<section id="catalog" class="panel">
<h2>5. 接口目录与统一契约卡片</h2>
<div class="filters no-print">
<input id="search" placeholder="搜索编号、名称、路径、DTO、规则">
<select id="groupFilter"><option value="">全部接口组</option></select>
<select id="statusFilter"><option value="">全部交付状态</option></select>
<button id="printBtn">打印 / 导出 PDF</button>
</div>
<p id="filterResult" class="small"></p>
<div id="endpointList"></div>
</section>
<section id="schemas" class="panel">
<h2>6. 共享 DTO / VO 字典</h2>
<div class="callout">以下字段是代码生成冻结值,已同步最新详细设计字段修订。<code>String(TEXT)</code> 表示物理字段为 MySQL TEXT;服务端必须按各请求明文上限及 UTF-8 字节数 ≤ 65,535 双重校验。内嵌 OpenAPI 的 <code>components.schemas</code> 是完整机器契约:所有本期请求体均关闭 <code>additionalProperties</code>,所有 Query/Header 均结构化;所有 200 响应为“具体成功 <code>Result&lt;T&gt;</code> 或业务错误包络”的 oneOf,错误 data 可为 null,不得退化为 Map/Object。</div>
<h3 id="schema-draft">SupplierDraftUpsertRequest</h3>
<table>
<thead><tr><th>字段</th><th>类型/上限</th><th>草稿</th><th>严格规则</th></tr></thead>
<tbody>
<tr><td>fullName</td><td>String(1..500)</td><td>必填</td><td>执照主体;规范化;审批通过后不可改</td></tr>
<tr><td>shortName</td><td>String(0..300)</td><td>选填</td><td>列表和搜索</td></tr>
<tr><td>taxNo</td><td>String(UTF-8 1..64 bytes)</td><td>必填</td><td>仅请求明文;规范化后按 UTF-8 字节校验;C-02 全库唯一;tax_no 以 TEXT + ascii_bin 保存确定性密文;日志禁止</td></tr>
<tr><td>types</td><td>List&lt;SupplierTypeCodeInput&gt;(1..15)</td><td>必填</td><td>供应商类型列表;输入项只包含 typeCode。前端选项来自 <code>supplier_type</code>,以 <code>dictValue</code> 写入 typeCode;typeName 仅由服务端在响应中返回</td></tr>
<tr><td>legalRepresentative</td><td>String(0..500)</td><td>选填</td><td>允许通过更新供应商接口维护</td></tr>
<tr><td>contactPhone</td><td>String(0..20)</td><td>选填</td><td>法人电话/公司电话;加密/脱敏</td></tr>
<tr><td>establishDate</td><td>LocalDate</td><td>选填</td><td>不得晚于当前日期</td></tr>
<tr><td>registeredCapital</td><td>String(0..50)</td><td>选填</td><td>文本口径</td></tr>
<tr><td>businessScope</td><td>String(0..500)</td><td>选填</td><td>经营范围</td></tr>
<tr><td>address</td><td>String(0..500)</td><td>选填</td><td>地址</td></tr>
<tr><td>staffScale</td><td>String</td><td>选填</td><td>LT50/R50_200/R200_500/GT500</td></tr>
<tr><td>mainCooperation</td><td>String(TEXT,1..65,535 UTF-8 bytes)</td><td>必填</td><td>主要合作内容;使用 UTF-8 字节校验器</td></tr>
<tr><td>licenseImageUrl</td><td>String(0..500)</td><td>草稿可空</td><td>提交时按 C-06 必填;必须是授权 OSS</td></tr>
<tr><td>approveNote</td><td>String(MEDIUMTEXT,UTF-8 ≤ 16,777,215 bytes)</td><td>选填</td><td>审批说明;对应 <code>approve_note MEDIUMTEXT</code></td></tr>
<tr><td>remark</td><td>String(MEDIUMTEXT,UTF-8 ≤ 16,777,215 bytes)</td><td>选填</td><td>备注;对应 <code>remark MEDIUMTEXT</code></td></tr>
<tr><td>contacts</td><td>List&lt;SupplierContactInput&gt;</td><td>选填</td><td>逐条校验</td></tr>
<tr><td>qualifications</td><td>List&lt;SupplierQualificationInput&gt;</td><td>选填</td><td>提交时按所有类型必备规则并集执行 C-09;每项以 permanentValid + expiryDate 表达有效期</td></tr>
<tr><td>initialAccounts</td><td>List&lt;SupplierBankAccountInput&gt;(0..N)</td><td>选填</td><td>仅注册草稿;随 PROFILE_CREATE 共审</td></tr>
<tr><td>duplicateConfirmToken</td><td>String</td><td>条件必填</td><td>存在近似候选时使用;不接受 force=true</td></tr>
<tr><td>expectedUpdateTime</td><td>LocalDateTime</td><td>更新供应商必填</td><td>格式 yyyy-MM-dd HH:mm:ss;创建草稿不传</td></tr>
</tbody>
</table>
<div class="callout"><b>更新边界:</b><code>SupplierUpdateRequest</code> 是独立的增量补全请求,不再继承全量建档 DTO;主体标量字段按 PATCH 语义更新。<code>types</code>、<code>contacts</code>、<code>qualifications</code>、<code>contracts</code>、<code>evaluations</code> 未传时保持不变,一旦传入则代表该集合的完整当前快照:同 ID 项更新、无 ID 项新增、数据库有效记录中未出现在请求内的项写 <code>deleted_at</code> 软删除。联系人、资质、合同、评价传空数组表示软删除该集合全部有效记录;类型至少保留一个,<code>types=[]</code> 参数校验失败。除 <code>changeReason</code>、<code>expectedUpdateTime</code> 外至少提交一个实际变化字段。未提交草稿允许修改 <code>fullName</code>、<code>taxNo</code>;进入审批中或审批完成后,两字段只允许原值回传,任何实际变化均拒绝。请求不接收 <code>creditLevel</code> 或账户字段。</div>
<div class="callout"><b>资质有效期契约:</b><code>permanentValid=true</code> 时 <code>expiryDate</code> 必须为空,<code>permanentValid=false</code> 时 <code>expiryDate</code> 必填,矛盾组合返回 <code>SUPPLIER_QUALIFICATION_VALIDITY_INVALID(395042)</code> 且零写入。历史完整请求未传 <code>permanentValid</code> 时按 <code>expiryDate</code> 是否为空推导;增量更新的既有资质同时省略两字段时保持原有效期。详情固定返回 <code>permanentValid</code>,并按 Asia/Shanghai 当前自然日动态返回有符号 <code>daysUntilExpiry</code>;永久有效时天数为 <code>null</code>。</div>
<h3>嵌套输入</h3>
<table>
<thead><tr><th>DTO</th><th>字段</th><th>严格规则</th></tr></thead>
<tbody>
<tr><td>SupplierContactInput</td><td>contactId?、contactName(1..500)、contactPhone(1..20)、contactRole、remark(0..200)</td><td>既有 ID 必须属于当前供应商;电话不回显明文</td></tr>
<tr><td>SupplierQualificationInput</td><td>qualificationId?、qualType(1..64)、certNo(0..128)、imageUrl(TEXT,0..65,535 UTF-8 bytes)、permanentValid?、expiryDate?</td><td>permanentValid=true 时 expiryDate 必须为空,false 时 expiryDate 必填;未传时按 expiryDate 是否为空兼容推导。isRequired 由服务端规则派生,客户端不得传 isRequired</td></tr>
<tr><td>SupplierBankAccountInput</td><td>accountType、bankName(1..500)、bankBranch(0..500)、accountNo(UTF-8 1..128 bytes)、proofFileUrls?(0..20,每项 1..1000)、settleMode?、accountPeriod?、invoiceType?、taxRate?</td><td>CORPORATE/PERSONAL;结算字段按账户保存;MONTHLY 才允许 accountPeriod,SPECIAL/NORMAL 必填 taxRate,NONE 时 taxRate 为空;accountName 由主体全称派生;无 confirmAccountNo/accountNoMask/isPersonal</td></tr>
<tr><td>SupplierTypeMergeInput</td><td>typeCode、isPrimary?</td><td>集合传入后按 (supplierId,typeCode) 对账;已存在则更新,不存在则新增,未出现在本次完整快照中的有效关联写 deleted_at</td></tr>
<tr><td>SupplierContactMergeInput</td><td>contactId?、联系人字段?、expectedUpdateTime?</td><td>已有项必须同时携带 contactId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有联系人软删除</td></tr>
<tr><td>SupplierQualificationMergeInput</td><td>qualificationId?、资质字段?、permanentValid?、expiryDate?、expectedUpdateTime?</td><td>已有项必须同时携带 qualificationId/expectedUpdateTime;既有项同时省略 permanentValid/expiryDate 时保持当前有效期;新增项不传 ID/版本且双省略时兼容为永久有效;集合快照中缺失的既有资质软删除</td></tr>
<tr><td>SupplierContractMergeInput</td><td>contractId?、合同字段?、expectedUpdateTime?</td><td>已有项必须同时携带 contractId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有合同软删除</td></tr>
<tr><td>SupplierEvaluationMergeInput</td><td>evaluationId?、评价字段?、expectedUpdateTime?</td><td>已有项必须同时携带 evaluationId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有评价软删除</td></tr>
</tbody>
</table>
<h3>供应商响应</h3>
<table>
<thead><tr><th>VO</th><th>核心字段</th><th>禁止字段</th></tr></thead>
<tbody>
<tr><td>SupplierListItemVO</td><td>supplierId/no、full/shortName、types、status、creditLevel/totalScore、activeAccountCount、create/updateTime</td><td>税号/证件/电话/账号明文</td></tr>
<tr><td>SupplierBasicInfoVO</td><td>主体公开业务字段、证件 mask、types、contacts mask、qualifications mask(含类型中文名、permanentValid、daysUntilExpiry、有效状态及只读 isRequired)、信用、状态、updateTime</td><td>isRelatedParty、approveNote、完整账号</td></tr>
<tr><td>SupplierAccountInfoVO</td><td>supplierId、bankAccounts(含当前生效结算口径及账号 mask)、updateTime</td><td>完整账号、密文、候选账号与往来数据</td></tr>
<tr><td>SupplierResourceRelationVO</td><td>relationId、supplierId/no/name、resourceModule/moduleName、resourceId/name、requiredTypeCode/name、remark、available/reasons、create/updateTime</td><td>资源主表名、跨服务内部字段、供应商账户与资质附件</td></tr>
<tr><td>SupplierWriteResultVO</td><td>supplierId、supplierNo?、status、onboardingStage、initialAccounts[{accountId,accountNoMask,status}]、updateTime</td><td>账号明文</td></tr>
</tbody>
</table>
<h3>类型、资质、联系人</h3>
<table>
<thead><tr><th>DTO</th><th>严格字段</th></tr></thead>
<tbody>
<tr><td>QualificationRuleReplaceRequest</td><td>rules[{qualType,isRequired,warningDays≥0,sortOrder}];类型编码由路径 typeCode 提供,必须是平台字典 supplier_type 的启用 dictValue</td></tr>
<tr><td>ContactReplaceRequest</td><td>contacts[]、changeReason?、expectedUpdateTime</td></tr>
</tbody>
</table>
<h3>账户与审批</h3>
<table>
<thead><tr><th>DTO</th><th>核心字段/规则</th></tr></thead>
<tbody>
<tr><td>SupplierBankAccountVO</td><td>accountId/name/type、bank/branch、accountNoMask、settleMode/accountPeriod/invoiceType/taxRate、status、isDefault、updateTime;isDefault 使用 YES/NO 表示是/否,一个供应商可返回多条账户,但未删除账户中最多一条 isDefault=YES;永不返回密文/明文/候选值</td></tr>
<tr><td>SupplierBankAccountBatchCreateRequest</td><td>accounts(1..50);每项包含 accountType、bankName(1..500)、bankBranch?、accountNo(1..128)、proofFileUrls?(0..20)、settleMode?、accountPeriod?、invoiceType?、taxRate?。一次请求可新增多个账户;每项独立生成 accountId 和 ACCOUNT_CREATE 审批,不生成可编辑草稿;户名/掩码由服务端派生</td></tr>
<tr><td>BankAccountChangeRequest</td><td>bankName?、bankBranch?、accountNo?、proofFileUrls?、settleMode?、accountPeriod?、invoiceType?、taxRate?、changeReason、expectedUpdateTime;至少一个账户候选字段,accountType 不可改。结算字段与账号字段一起进入 ACCOUNT_CHANGE 候选快照,永不回传候选值</td></tr>
<tr><td>ApprovalCommandResultVO</td><td>approvalLogId、requestNo、provider(本期固定 LOCAL_AUTO)、approvalStatus、spNo?/spStatus?、syncStatus、submittedAt?/finishedAt?;LOCAL_AUTO 时企微字段必须为空</td></tr>
<tr><td>SupplierApprovalSnapshotDTO</td><td>schemaVersion(一期固定 1)、spNo/templateId、supplierId/approvalLogId/requestNo、bizType/subType、spStatus(String,最长 64,未知值原样保留)、applicant、apply/finishedAt、sourceRevision、detailDigest、detailSyncStatus、levels、sourceEventKey;未知版本拒绝同步和终态应用</td></tr>
</tbody>
</table>
<h3>其余严格命令 DTO</h3>
<table>
<thead><tr><th>DTO</th><th>字段与必填规则</th></tr></thead>
<tbody>
<tr><td>SupplierUpdateRequest</td><td>主体可编辑字段?、types?/contacts?/qualifications?/contracts?/evaluations?、changeReason、expectedUpdateTime;主体字段增量更新;集合字段缺省表示不处理,传入表示完整快照,缺失项及空数组对应项统一软删除;不接收 deletedIds</td></tr>
<tr><td>SupplierSubmitRequest</td><td>完整复用 SupplierDraftUpsertRequest 的供应商表单字段,另含 submitNote?、expectedUpdateTime;fullName、taxNo、types、mainCooperation、licenseImageUrl、expectedUpdateTime 必填,qualifications 按 types 的必备规则条件校验</td></tr>
<tr><td>SupplierStatusRequest</td><td>targetStatus、changeReason、expectedUpdateTime;服务端结合当前状态收窄合法目标</td></tr>
<tr><td>SupplierResourceReassignRequest</td><td>supplierId、requiredTypeCode?、remark?、expectedCurrentSupplierId?、expectedRelationUpdateTime?、changeReason;已有关系改绑时两个 expected 字段必填</td></tr>
<tr><td>SupplierResourceUnbindRequest</td><td>expectedCurrentSupplierId、expectedRelationUpdateTime、changeReason;仅由资源或车队页面解除当前资源与供应商的关系</td></tr>
<tr><td>导出 DTO</td><td>本期不生成;请求、响应、权限与任务方式未来独立立项重新评审</td></tr>
<tr><td>WeComApprovalSubmitRequest</td><td>requestNo、applicantAdminId、supplierId、approvalLogId、bizType、subType?、formFacts(oneOf);仅 SupplierApprovalPort 使用。requestNo 是唯一端到端审批号,不再传 clientRequestId;applicantAdminId 必须来自管理端入口可信 X-Admin-Id;不得携带 templateId/controlId/approver</td></tr>
</tbody>
</table>
<h3>完整返回 VO 补充</h3>
<table>
<thead><tr><th>VO</th><th>生产返回字段</th></tr></thead>
<tbody>
<tr><td>ArchiveResultVO</td><td>supplierId、status=ARCHIVED、clearanceRequestId、checkedAt、ledgerRevision、updateTime</td></tr>
<tr><td>SupplierApprovalRecordVO</td><td>changeLogId、supplierId、approvalLogId?、operationType、targetType/id?、fieldName、old/newValueMasked、changeReason、status?、operatorId?、createTime;逐行对应 supplier_change_log,不返回审批意见或敏感明文</td></tr>
<tr><td>QualificationRuleVO</td><td>规则 ID、平台字典类型编码 typeCode、资质类型、是否必备、预警天数、排序和 updateTime</td></tr>
<tr><td>SupplierQualificationVO / SupplierContactVO</td><td>各自稳定 ID、业务字段、脱敏证照/电话、状态和 updateTime;不得返回敏感明文</td></tr>
<tr><td>List&lt;BankAccountSubmitResultVO&gt;</td><td>与请求 accounts 顺序一致;每项返回 accountId、approvalLogId、requestNo、provider、approvalStatus、syncStatus、accountStatus、isDefault=NO、submittedAt?、finishedAt?;LOCAL_AUTO 正常完成时 accountStatus=ACTIVE</td></tr>
<tr><td>ApprovalSnapshotAcceptVO</td><td>accepted、processStatus、sourceRevision、detailDigest、businessApplied、message?</td></tr>
<tr><td>WeComApprovalSubmitResultVO</td><td>spNo、applyUserName?、acceptedAt</td></tr>
<tr><td>WeComApprovalStatusVO</td><td>Supplier 适配器把现有 <code>ApprovalFeignClient#getApprovalStatus</code> 返回映射为 spNo、templateId、spStatus(String,最长 64,保留原始状态码)、申请人、applyTime;仅作轻量探测</td></tr>
</tbody>
</table>
</section>
<section id="errors" class="panel">
<h2>7. 业务错误码(代码生成冻结)</h2>
<div class="callout warn">冻结使用 <code>hl-resource-service</code> 的 <code>390000–399999</code> 共享段。供应商当前占用 395001~395039,不额外声明后续号码归供应商独占。实现 <code>SupplierErrorCode</code> 时标注完整资源共享段,启动和测试必须由 <code>ErrorCodeRegistry</code> 校验越界与重复。</div>
<table id="errorTable">
<thead><tr><th>错误码常量</th><th>数字码</th><th>触发</th><th>message</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section id="trace" class="panel">
<h2>8. 需求—接口追踪</h2>
<table>
<thead><tr><th>需求编号</th><th>业务要求</th><th>接口覆盖</th><th>结论</th></tr></thead>
<tbody>
<tr><td>REQ-01</td><td>手工建档、草稿、提交、状态与归档</td><td>SUP-ADM-001~004、007、010~012、043</td><td>完整映射</td></tr>
<tr><td>REQ-02</td><td>供应商菜单和按钮完全由平台权限配置;类型、信用、状态、审批意见及证明附件分别使用独立权限码</td><td>全部管理端接口</td><td>服务端以可信 <code>X-Admin-Id</code> 调用 <code>UserFeignClient.hasPermission</code>,异常失败关闭;不检查固定角色名</td></tr>
<tr><td>REQ-05</td><td>本期 LOCAL_AUTO 完整审批模型与应用</td><td>SUP-ADM-007、010、035</td><td>统一 Provider SPI 与结果应用器,无伪造企微数据</td></tr>
<tr><td>REQ-06</td><td>注册可带 0..N 初始账户;注册后新增账户直接独立提交审批;默认账户唯一</td><td>SUP-ADM-003/004/007、034~036、041</td><td>完整映射;不提供注册后账户草稿、删除或停用入口</td></tr>
<tr><td>REQ-07</td><td>锁、幂等、失败零写入、字段级审计、事件重放</td><td>所有写接口、SUP-ADM-012</td><td>Supplier 内部契约统一约束;不规划外部消息链路</td></tr>
<tr><td>REQ-08</td><td>资源与车队模块在资源页面选择、查看、改绑或解除供应商;供应商详情不维护资源</td><td>SUP-ADM-001/043、SUP-ADM-048~050</td><td>统一使用 supplier_resource_rel;单资源唯一有效供应商;VEHICLE 内部只读校验</td></tr>
</tbody>
</table>
<div class="callout">接口追踪没有发现“需求需要但完全无系统入口”的本期业务环节;导入和二期能力均明确标注,不伪装成当前接口。</div>
</section>
<section id="consistency" class="panel">
<h2>9. 双来源一致性与内部实现边界</h2>
<table>
<thead><tr><th>差异</th><th>API 层处理</th><th>不可放宽项</th></tr></thead>
<tbody>
<tr><td>名称、银行及密文字段历史长度不同</td><td>采用最新冻结值:fullName 500、shortName 300、legalRepresentative 500、contactName 500、bank/branch 500;mainCooperation、typeName、extraFields、资格影像 URL、tax_no、account_no_enc 为 TEXT,其中 mainCooperation 明文限制 UTF-8 ≤65,535 bytes。tax_no/account_no_enc 的确定性密文列使用 ascii_bin,明文分别限制 UTF-8 ≤64/128 bytes</td><td>DTO、OpenAPI、Bean Validation、Entity、migration、列排序规则和前缀唯一索引必须同向;不得再使用历史较小值或只校验字符数</td></tr>
<tr><td>accountNoMask 动态生成或落库</td><td>API 只承诺掩码,不披露存储策略</td><td>永不返回账号明文/密文/候选值</td></tr>
<tr><td>isPersonal 是否落列</td><td>API 只接受 accountType;服务端内部派生</td><td>客户端不得传 isPersonal</td></tr>
<tr><td>审批流水还是全量快照</td><td>对外采用 v3.0 的完整 levels/actions/events、完整性和版本门禁</td><td>只凭顶层 spStatus 不得应用终态</td></tr>
<tr><td>审批部门取当前值还是历史值</td><td>企业微信 Supplier 模板增加申请人、节点和实际审批人部门字段;外部适配器按 controlId 从审批表单/详情解析为 ApprovalDepartmentSnapshot,并写入单个 detailSnapshotCiphertext</td><td>支持一人多部门和唯一主部门;字段缺失时 INCOMPLETE,禁止查询当前通讯录补写历史</td></tr>
<tr><td>审批记录查询事实源</td><td>SUP-ADM-012 只读取 supplier_change_log;一条返回记录对应一条 change_log_id,不跨表拼装企微审批详情</td><td>supplier_change_log 为追加式证据表,不得物理删除;审批详情仍由独立审批接口负责</td></tr>
<tr><td>supplierNo 生成规则</td><td>客户端视为服务端生成的 opaque String;不得请求传入</td><td>只在 PROFILE_CREATE 最终通过后产生,驳回时为空</td></tr>
<tr><td>导入一表或两表</td><td>本期不实现,不影响当前 API</td><td>不得创建占位接口或假成功响应</td></tr>
<tr><td>物理表名前缀</td><td>本期物理表统一为详细设计冻结的 <code>supplier_*</code>;Entity、Mapper、DDL、索引和 SQL 必须同名</td><td>禁止生成 <code>resource_supplier*</code>、<code>resource_*</code> 兼容表/视图或其他额外前缀;若当前源码、migration 或实际 schema 与冻结口径冲突,停止数据库生成并先解决迁移方案</td></tr>
</tbody>
</table>
</section>
<section id="acceptance" class="panel">
<h2>10. 验收矩阵</h2>
<table>
<thead><tr><th>接口类型</th><th>最低场景</th><th>必须核对</th></tr></thead>
<tbody>
<tr><td>所有管理端接口</td><td>成功、未认证、角色越权、缺参、边界、目标不存在</td><td>Result code/success、数据库、敏感字段、失败零写入</td></tr>
<tr><td>查询</td><td>空结果、分页边界、非法排序、组合筛选</td><td>ID 字符串、默认排序、脱敏、无 N+1</td></tr>
<tr><td>普通写</td><td>合法状态、非法状态、expectedUpdateTime 冲突、重复请求</td><td>锁、事务、字段级日志、零旁路</td></tr>
<tr><td>资源侧供应商维护</td><td>资源页面复用供应商列表、查询当前关系、首次设置、显式改绑、解除关系、重复及并发设置、类型或资质不满足、VEHICLE 依赖不可用</td><td>供应商详情无资源入口;单资源唯一有效关系;失败零写入;解除后可重绑;名称不冗余落表;无跨 schema SQL;Long 使用字符串</td></tr>
<tr><td>供应商审批记录</td><td>按 supplierId、approvalLogId、operationType、targetType、fieldName、status、时间范围分页查询</td><td>仅读取 supplier_change_log;默认 createTime DESC、changeLogId DESC;old/new 只返回脱敏值,不返回审批意见正文</td></tr>
<tr><td>拉黑/解除</td><td>合法/非法迁移、LOCAL_AUTO 重复结果、应用失败恢复</td><td>按 targetStatus 选择 subType;统一结果应用器复核状态机;失败时业务和 DOMAIN_EVENT 零部分写入</td></tr>
<tr><td>本期 LOCAL_AUTO</td><td>正常通过、Provider 异常、重复结果、APPLY_FAILED 与恢复</td><td>真实审批行;systemDecision=true;企微字段全空;APPROVED 只经统一结果应用器达到 APPLIED</td></tr>
<tr><td>后续 WECOM</td><td>扫描本期 OpenAPI、源码与配置</td><td>无 WECOM Feign、回调、模板、对账 Job 或假 spNo;仅保留排除契约</td></tr>
<tr><td>短窗防重与业务幂等</td><td>短窗重复请求、事务回滚、服务重启、重复 Provider 结果、APPLY_FAILED 重试</td><td><code>@Idempotent</code> + 聚合锁 + 状态机 + CAS/唯一约束 + requestNo + 结果应用幂等;不重复业务事实或事件</td></tr>
<tr><td>账户</td><td>0/1/N 账户、C-19、默认切换</td><td>明文不落日志、默认唯一、失败关闭</td></tr>
<tr><td>Internal</td><td>有效/缺失/错误 X-Internal-Token、参数边界、依赖不可用</td><td>沿用当前内部 Token、失败关闭、无跨 schema 写;本期不新增调用方白名单</td></tr>
<tr><td>归档</td><td>清账通过、存在 blocker、Finance 不可用、重复归档</td><td>ARCHIVED 不可逆、历史不断链</td></tr>
<tr><td>导出</td><td>扫描本期 OpenAPI、源码与配置</td><td>无路径、无 DTO/Service/Job、无占位响应</td></tr>
</tbody>
</table>
</section>
<p class="footer">发行日期:2026-08-25 · 基线:v2.0 契约基线 · 前端联调版;归档成功依赖尚未接入。</p>
</main>
</div>
<script>
function E(id,method,path,name,group,access,request,response,rules,errors,source,delivery,consumer,tables){
return {id:id,method:method,path:path,name:name,group:group,access:access,request:request,response:response,
rules:rules,errors:errors,source:source,delivery:delivery,consumer:consumer||"管理端",tables:tables||""};
}
const endpoints = [
E("SUP-ADM-001","GET","/admin/supplier/items/page","供应商分页列表","档案与生命周期","平台菜单/按钮权限",
"SupplierListQuery:keyword、status、typeCode、creditLevel、creatorId、page、pageSize;不开放 payable 或客户端排序字段",
"Result<PageResult<SupplierListItemVO>>",
["keyword 按 supplierNo/fullName/shortName 做包含式模糊查询;统一 trim 并转义百分号、下划线和反斜杠,税号仅允许规范化后精确匹配","creatorId 保持 Snowflake Long 的 JSON String,按 supplier_main.created_by 精确筛选","按供应商类型编码精确筛选;筛选 types 集合中包含该 typeCode 的供应商,关联关系来自 supplier_type_rel;返回的 types 包含供应商的全部类型","前端 typeCode 筛选项来自 supplier_type,status 筛选项与展示标签来自 supplier_lifecycle_status;请求只传 dictValue","不提供 payable 筛选;默认 createTime DESC, supplierId DESC","列表响应返回供应商基础信息、全部类型、状态、信用及账户统计"],
[],"详细设计 §5.2/§5.11;数据模型 supplier_main/type_rel","已实现 · 可联调"),
E("SUP-ADM-043","GET","/admin/supplier/items/list","查询供应商列表","档案与生命周期","平台菜单/按钮权限",
"SupplierSimpleListQuery:keyword?、status?、typeCode?、limit?;limit 默认 50、最大 200",
"Result<List<SupplierListItemVO>>",
["用于供应商档案中的非分页列表查询;keyword 按 supplierNo/fullName/shortName 做包含式模糊查询并转义 LIKE 元字符","typeCode 必须是平台字典 supplier_type 的启用 dictValue,status 必须是 supplier_lifecycle_status 的启用 dictValue;页面按需加载两个字典","仅返回未软删除供应商;默认 createTime DESC, supplierId DESC;limit 默认 50、最大 200,禁止无界全量返回","返回供应商基础信息、全部类型、生命周期状态、信用及账户统计;不返回账号、合同或往来详情"],
[],"详细设计 §5.2/§5.11;数据模型 supplier_main/type_rel","已实现 · 可联调"),
E("SUP-ADM-002","GET","/admin/supplier/items/{supplierId}/basic-info/view","查询供应商详细信息","档案与生命周期","平台菜单/按钮权限",
"Path supplierId(String)","Result<SupplierBasicInfoVO>",
["仅返回主体档案、多类型、联系人、资质、信用和生命周期状态","资质同时返回 qualType 稳定值与 qualTypeName 中文名,以及 validityStatus/validityStatusName 有效状态;字典未命中时中文名回退原值","每条资质固定返回 permanentValid;非永久资质按 Asia/Shanghai 自然日返回有符号 daysUntilExpiry,永久资质返回 null;expired 仅在 daysUntilExpiry<0 时为 true,isRequired 保持只读必备规则语义","前端按 supplier_type 翻译 types[].typeCode,按 supplier_lifecycle_status 翻译 status;不得在详情组件写死中文","不返回账号或资源字段;三个详情页签接口互不混装","SupplierBasicInfoVO 不返回 isRelatedParty 和 approveNote;两字段仅保留在写入、候选快照和服务端业务处理中"],
["SUPPLIER_NOT_FOUND"],"财务控制台原型三页签;详细设计 §5.2/§5.11;supplier_main/type_rel/contact/qualification","已实现 · 可联调"),
E("SUP-ADM-003","POST","/admin/supplier/items/add","创建供应商注册草稿","档案与生命周期","平台菜单/按钮权限",
"SupplierDraftUpsertRequest","Result<SupplierWriteResultVO>",
["保存供应商注册表单草稿,供后续 1.6 提交注册审批使用","资质 permanentValid=true 时清空并持久化 NULL 到期日,false 时必须提供 expiryDate;历史请求未传 permanentValid 时按 expiryDate 是否为空推导","创建 supplier_main 并将 status 固定写为 DRAFT;同一事务新增 supplier_change_log,operation_type=CREATE、status=DRAFT、approval_log_id=NULL。草稿不是审批申请,创建阶段不写 supplier_approval_log","同一事务保存 supplier_main、supplier_type_rel、supplier_contact、supplier_qualification 和 0..N 条 supplier_account;初始账户 status=DRAFT、不分配 supplierNo,并分别写 supplier_account_change_log 的 CREATE 留痕","服务端固定初始化 creditLevel=B,请求不得传入信用等级;未配置 supplier:create 时拒绝且零写入","主体、关联表或供应商/账户变更留痕任一步失败时整笔事务回滚,不得留下不完整草稿"],
["SUPPLIER_WRITE_FORBIDDEN","SUPPLIER_IDENTITY_DUPLICATE","SUPPLIER_DUPLICATE_CONFIRM_REQUIRED","SUPPLIER_TYPE_PRIMARY_INVALID","SUPPLIER_INITIAL_ACCOUNT_INVALID","SUPPLIER_QUALIFICATION_VALIDITY_INVALID"],
"详细设计 §5.2/§5.11;数据模型 main/type/contact/qualification/account","已实现 · 可联调"),
E("SUP-ADM-004","PUT","/admin/supplier/items/{supplierId}/update","更新供应商","档案与生命周期","平台菜单/按钮权限",
"SupplierUpdateRequest","Result<SupplierWriteResultVO>",
["用于补全或更新供应商资料;供应商注册草稿可能只录入最低必填信息,除 ARCHIVED(已归档)外,DRAFT、VETTING、ACTIVE、SUSPENDED、FROZEN、BLACKLIST 状态均允许维护 supplier_main、supplier_type_rel、supplier_contact、supplier_qualification、supplier_contract 和 supplier_evaluation","主体标量字段采用 PATCH 增量更新;types、contacts、qualifications、contracts、evaluations 未传时保持不变,一旦传入即代表该集合的完整当前快照","既有资质同时省略 permanentValid 和 expiryDate 时保持当前有效期;显式 true 清空到期日,显式 false 必须同时提交到期日;矛盾组合在任何写入前失败","集合对账规则固定为:已有记录携带稳定 ID 时更新,无 ID 时新增;数据库中 deleted_at IS NULL 且未出现在本次集合快照中的既有记录写 deleted_at=当前时间,禁止物理 DELETE。contacts、qualifications、contracts、evaluations 传空数组表示软删除该集合全部有效记录;types 至少保留一项,空数组参数校验失败","联系人从提交列表中去掉、资质/合同/评价从各自提交列表中去掉,以及类型编码从 types 中去掉,均属于明确删除动作,必须写相应表 deleted_at 并记录脱敏变更日志","未提交注册审批的 DRAFT 可修改 fullName、taxNo;进入 VETTING 或审批完成后,fullName、taxNo 为不可变主体标识,只允许原值回传,规范化后任一值发生变化均返回 SUPPLIER_IDENTITY_IMMUTABLE","顶层业务字段全部选填,但 changeReason、expectedUpdateTime 必填,且除两字段外至少存在一个实际变化字段","已有子记录必须传对应稳定 ID 和该记录的 expectedUpdateTime;新增子记录不传 ID,由服务端生成 Snowflake ID;types 使用 (supplierId,typeCode) 识别已有关系,重复 typeCode 不得重复插入","集合差异软删除前仍须执行归属、状态、必备资质、外部引用和业务引用 Guard;任一删除不允许时整笔请求失败且零写入。类型集合传入时至少保留一个有效类型","本接口不接收 deletedIds;账户字段继续使用账户专用接口,不得通过本接口删除收款账户","服务端按 supplier_main、supplier_type_rel、supplier_contact、supplier_qualification、supplier_contract、supplier_evaluation 的固定顺序加锁;校验主体 expectedUpdateTime。所有子表写操作都必须同步推进 supplier_main.update_time,使主体 expectedUpdateTime 覆盖集合差异并发;逐条校验请求内既有子记录的归属、软删除状态和 expectedUpdateTime;新增、更新、软删除及审计在同一事务内完成,任一步失败整笔回滚"],
["SUPPLIER_NOT_FOUND","SUPPLIER_IDENTITY_IMMUTABLE","SUPPLIER_ARCHIVED_IMMUTABLE","SUPPLIER_RESOURCE_TYPE_MISMATCH","SUPPLIER_CONCURRENT_MODIFICATION","SUPPLIER_QUALIFICATION_VALIDITY_INVALID"],
"详细设计 §5.2/§5.11;数据模型 supplier_main/supplier_type_rel/supplier_resource_rel/supplier_contact/supplier_qualification/supplier_contract/supplier_evaluation","已实现 · 可联调"),
E("SUP-ADM-007","POST","/admin/supplier/items/{supplierId}/submit","提交供应商注册审批","档案与生命周期","平台菜单/按钮权限",
"SupplierSubmitRequest:完整供应商表单字段、duplicateConfirmToken?、submitNote?、expectedUpdateTime;fullName、taxNo、types、mainCooperation、licenseImageUrl、expectedUpdateTime 必填",
"Result<ApprovalCommandResultVO>",
["提交供应商注册表单,生成注册审批流水,并使供应商进入合作中状态","提交表单的资质 permanentValid/expiryDate 使用与创建草稿相同的完整请求契约;矛盾组合在审批、主档、资质和变更日志写入前失败","仅 supplier_main.status=DRAFT(草稿)允许提交;非 DRAFT 时页面不展示“提交”按钮,服务端仍拒绝并返回 SUPPLIER_STATUS_TRANSITION_INVALID(当前状态不允许执行该操作)","接口使用单一数据库事务;锁定 supplier_main 后再次确认 status=DRAFT,并使用 expectedUpdateTime 校验并发;提交前重查主体、营业执照、必备资质和初始账户","将本次完整表单与当前草稿逐字段对照并保存,变化字段写 supplier_change_log;taxNo、contactPhone、certNo、accountNo 等敏感值只允许保存脱敏留痕","同一事务内向 supplier_approval_log 新增注册审批流水,biz_type=PROFILE_CREATE、provider=LOCAL_AUTO、approval_status=APPROVED,并保存本次提交表单快照","同一事务内将 supplier_main.status 从 DRAFT 更新为 ACTIVE(合作中)并生成 supplierNo;随单账户由 PENDING 更新为 ACTIVE;表单保存、变更留痕、审批流水或状态更新任一步失败时整笔事务回滚"],
["SUPPLIER_IDENTITY_DUPLICATE","SUPPLIER_QUALIFICATION_REQUIRED","SUPPLIER_QUALIFICATION_EXPIRED","SUPPLIER_INITIAL_ACCOUNT_INVALID","SUPPLIER_CONCURRENT_MODIFICATION","SUPPLIER_STATUS_TRANSITION_INVALID","SUPPLIER_QUALIFICATION_VALIDITY_INVALID"],"详细设计 §5.2/§5.4/§5.11/§6;数据模型 supplier_main/supplier_approval_log/supplier_change_log","已实现 · 可联调"),
E("SUP-ADM-010","POST","/admin/supplier/items/{supplierId}/archive","清账归档","档案与生命周期","平台菜单/按钮权限",
"Path supplierId","Result<ArchiveResultVO>",
["无 body;归档已停止合作的供应商,记录归档审批流水并更新主体状态","仅 supplier_main.status=SUSPENDED(暂停)或 BLACKLIST(黑名单)时允许归档;其他状态返回 SUPPLIER_STATUS_TRANSITION_INVALID(当前状态不允许归档)","服务端锁行重读供应商状态并生成归档审计上下文;客户端不得提交 changeReason 或 expectedUpdateTime","同一事务内向 supplier_approval_log 新增归档审批流水,biz_type=STATUS_CHANGE、provider=LOCAL_AUTO、approval_status=APPROVED","同一事务内将 supplier_main.status 更新为 ARCHIVED(已归档),并将状态变化写 supplier_change_log;审批流水、主体状态或变更留痕任一步失败时整笔事务回滚","ARCHIVED 不可恢复,历史引用和审批流水不得删除"],
["SUPPLIER_STATUS_FORBIDDEN","SUPPLIER_STATUS_TRANSITION_INVALID","SUPPLIER_ARCHIVED_IMMUTABLE","SUPPLIER_CLEARANCE_CHECK_UNAVAILABLE"],"详细设计 §3.2/§5.2/§5.11;数据模型 supplier_main/supplier_approval_log/supplier_change_log;#6174 失败关闭","已实现 · 失败关闭"),
E("SUP-ADM-011","DELETE","/admin/supplier/items/{supplierId}/del","软删除未提交草稿","档案与生命周期","平台菜单/按钮权限",
"Path supplierId","Result<Void>",
["仅 supplier_main.status=DRAFT、从未生成 PROFILE_CREATE 审批记录或 requestNo 且无外部引用时允许软删除;是否存在企微 spNo 不能作为唯一判断条件","连带处理初始账户时逐条锁行校验状态:仅 supplier_account.status=DRAFT,或 status=PENDING 且不存在 approval_status=APPROVED / apply_status=APPLIED 的账户,才属于未审批账户并允许写 deleted_at","任一初始账户已经审批通过、apply_status=APPLIED 或 supplier_account.status=ACTIVE 时,视为已启用账户;不得删除该账户,也不得继续删除供应商,整笔请求返回 SUPPLIER_STATUS_TRANSITION_INVALID 且零写入","锁定 supplier_main、全部有效初始账户及相关审批记录,重查 deleted_at、当前状态、审批结果和外部引用;任一门禁不满足时拒绝且零写入","同一事务将 supplier_main、supplier_type_rel、supplier_contact、supplier_qualification、supplier_contract、supplier_evaluation 以及符合未审批状态门禁的初始 supplier_account 有效记录写 deleted_at=当前时间,禁止物理 DELETE","supplier_approval_log、supplier_change_log、supplier_account_approval_log、supplier_account_change_log 等审批、变更和审计证据始终保留;软删除账户必须追加脱敏 DELETE 变更留痕","默认查询必须同时过滤 supplier_main.deleted_at IS NULL 以及每个参与 JOIN 的子表 deleted_at IS NULL;根据已软删除 ID 的普通详情查询按不存在处理","对已软删除目标重放按幂等成功处理;从未存在的 supplierId 返回 SUPPLIER_NOT_FOUND"],
["SUPPLIER_NOT_FOUND","SUPPLIER_STATUS_TRANSITION_INVALID"],"详细设计 §3.2/§5.2/§5.11;数据模型 supplier_main 及全部 supplier 业务子表","已实现 · 可联调"),
E("SUP-ADM-012","GET","/admin/supplier/items/{supplierId}/approval-records/page","查询供应商审批记录","供应商审批记录","平台 supplier:approval:read 权限",
"Path supplierId;approvalLogId?、operationType?、targetType?、fieldName?、status?、from?、to? + PageQuery","Result<PageResult<SupplierApprovalRecordVO>>",
["按 supplierId 分页查询供应商审批记录,唯一事实来源为 supplier_change_log;不得改查 supplier_approval_log,不得跨表拼装企微审批详情","一条返回记录严格对应一条 supplier_change_log.change_log_id;approvalLogId 可为空,为空表示草稿保存、直接维护等无需审批的留痕,不得为补齐展示而伪造审批流水","支持按 approvalLogId、operationType、targetType、fieldName、status 和 createTime 闭区间筛选;from/to 必须同时传或同时不传,from 不得晚于 to","operationType 仅允许 CREATE、UPDATE、ENABLE、DISABLE、DELETE;status 是该条留痕产生时的供应商生命周期快照,前端按 supplier_lifecycle_status 翻译","默认按 createTime DESC、changeLogId DESC 稳定排序;sortBy 仅允许 createTime/changeLogId,pageSize 最大 100","oldValueMasked/newValueMasked 只返回脱敏差异;taxNo、电话、证件号、账号及候选快照不得返回明文或密文;本接口不返回审批意见正文、企微节点、spNo 或 provider 字段","supplier_change_log 是追加式审批/变更证据表,本接口只读,不修改、不软删、不物理删除;归档供应商的历史记录仍可查询","先校验 supplierId 数据范围和 supplier:approval:read 权限;权限、参数或依赖校验失败时不得降级返回未裁剪记录"],
["SUPPLIER_NOT_FOUND"],"详细设计 §5.2/§5.11;数据模型 supplier_change_log","已实现 · 可联调"),
E("SUP-ADM-034","GET","/admin/supplier/items/{supplierId}/account-info/list","根据供应商ID查询账户信息","收款账户","平台菜单/按钮权限",
"Path supplierId","Result<SupplierAccountInfoVO>",
["supplier_main 与 supplier_account 为 1:N;根据路径参数 supplierId 查询该供应商名下全部 deleted_at IS NULL 的 bankAccounts 和 updateTime,不得只取第一条账户","同一供应商可同时拥有多个 PENDING、ACTIVE 或历史 DISABLED 收款账户;PENDING 仅表示审批或结果应用处理中且只读,每个账户独立返回 accountId、账号 mask、状态及当前生效的 settleMode/accountPeriod/invoiceType/taxRate","默认账户在列表中优先,其余按 createTime DESC、accountId DESC 稳定排序;同一供应商最多一个默认账户","注册后新增账户成功后不提供删除或停用入口;ACTIVE 账户永久保留,历史 DISABLED 账户只读。只有删除未提交供应商 DRAFT 时,符合未审批状态门禁的初始账户才可随主体软删除","supplierId 必须对应有效且未软删除的供应商;不返回账号明文、密文、候选变更值或往来数据"],
["SUPPLIER_NOT_FOUND"],"详细设计 §5.5/§5.11;supplier_account","已实现 · 可联调"),
E("SUP-ADM-035","POST","/admin/supplier/items/{supplierId}/bank-accounts/add","批量新增并提交收款账户","收款账户","平台菜单/按钮权限",
"SupplierBankAccountBatchCreateRequest","Result<List<BankAccountSubmitResultVO>>",
["supplier_main 与 supplier_account 为 1:N;仅主体 ACTIVE 时允许新增。请求 accounts 必须包含 1..50 项,一个供应商可在一次请求中新增多个收款账户","批内每项分别预分配 Snowflake accountId、approvalLogId 和 requestNo;PREPARE_TX 为每项写 supplier_account.status=PENDING、isDefault=NO 及加密候选快照,PENDING 仅表示审批或结果应用处理中,不是可编辑草稿","服务端先规范化全部 accountNo;批内规范化账号必须互不重复,并逐项执行 C-19 全库唯一校验。任一字段、附件、主体状态、批内重复或全库重复校验失败时,整个批次零写入","每个账户自己的 proofFileUrls 保留为 JSON 数组,最多 20 项;每项必须是授权文件地址并归属于当前操作者可访问范围,不得记录到普通日志","每项户名均由同一供应商主体全称派生;逐项校验 MONTHLY/accountPeriod 与 invoiceType/taxRate 条件一致性","PREPARE_TX 一次性写入本批全部账户和独立审批事实后提交;随后按请求顺序逐项调用 SupplierApprovalProvider。本期 LOCAL_AUTO 返回标准化 APPROVED,再由统一结果应用器分别将 accountId 从 PENDING 更新为 ACTIVE","响应为 List<BankAccountSubmitResultVO> 且与请求 accounts 顺序一致;每个结果返回各自 accountId、approvalLogId、requestNo、审批结果和 accountStatus。正常 LOCAL_AUTO 结果均为 ACTIVE","所有新账户固定 isDefault=NO;同一供应商最多一个默认账户,需要时通过 2.4 独立接口原子切换。新增成功后不提供删除或停用入口,只能查询和切换默认;历史 DISABLED 账户仅允许查询","短窗防重键绑定 trusted adminId + supplierId + 有序账号摘要集合 + requestDigest;相同整批请求重放返回原结果列表,不重复创建。不得仅按 supplierId 防重","分布式锁使用 supplier:aggregate:{supplierId};事务内按规范化账号摘要升序执行重复检查和写入,避免批量死锁。跨供应商请求可并发,数据库 C-19 唯一约束作为最终兜底","Provider 结果不确定或某项结果应用失败时,仅该项保留只读 PENDING 和审批证据供原 requestNo 恢复/对账;不得删除、编辑或用新批次重新创建同一规范化账号,其他项按各自标准化结果独立应用","客户端不得传 provider、模板、审批人、isDefault 或企微字段;每个 accountId 具有独立审批和应用状态"],
["SUPPLIER_PROFILE_NOT_ACTIVE","SUPPLIER_ACCOUNT_APPROVAL_IN_PROGRESS","SUPPLIER_ACCOUNT_DUPLICATE"],"详细设计 §5.5/§5.11/§6;数据模型 supplier_account/supplier_account_approval_log","已实现 · 可联调"),
E("SUP-ADM-036","GET","/admin/supplier/bank-accounts/{accountId}/view","查询收款账户详情","收款账户","平台菜单/按钮权限",
"Path accountId","Result<SupplierBankAccountDetailVO>",
["返回脱敏详情、账户级 settleMode/accountPeriod/invoiceType/taxRate、默认标识和最近审批摘要(本期 LOCAL_AUTO);proofFileUrls 仅具备 supplier:account:proof:read 时返回,否则省略","没有电话回拨历史,不返回账号明文、密文或候选值"],
["SUPPLIER_NOT_FOUND"],"详细设计 §5.5/§5.11","已实现 · 可联调"),
E("SUP-ADM-041","PUT","/admin/supplier/bank-accounts/{accountId}/default/update","设置默认收款账户","收款账户","平台菜单/按钮权限",
"Path accountId","Result<SupplierBankAccountVO>",
["目标账户必须属于当前供应商、deleted_at IS NULL 且 status=ACTIVE;每个供应商的未删除账户中最多一个默认账户","先由 accountId 只读解析 supplierId,再取得 supplier:aggregate:{supplierId} 分布式锁;事务内锁定 supplier_main 和按 account_id 升序排列的全部未删除 supplier_account","同一事务先将该供应商其他账户的 is_default 置为 NO,再将目标账户置为 YES;提交后必须且只能由目标账户作为默认账户","目标账户已经是唯一默认账户时按幂等成功返回,不更新数据且不制造空变更日志","数据库使用可空生成列 default_supplier_id 和唯一索引 uk_supplier_account_one_default 兜底;唯一约束冲突按并发冲突失败关闭","返回掩码、isDefault=YES 及该账户当前生效结算口径"],
["SUPPLIER_NOT_FOUND","SUPPLIER_PROFILE_NOT_ACTIVE","SUPPLIER_CONCURRENT_MODIFICATION"],"详细设计 §5.5/§5.11","已实现 · 可联调"),
E("SUP-ADM-048","GET","/admin/supplier/resource-relations/{resourceModule}/{resourceId}/view","查询资源当前供应商","资源侧供应商维护","平台菜单/按钮权限",
"Path resourceModule、resourceId","Result<SupplierResourceRelationVO>",
["供资源管理页面查询当前有效供应商关系;不存在有效关系时返回 SUPPLIER_RESOURCE_RELATION_NOT_FOUND","返回供应商与 requiredTypeCode 摘要,不返回账户、资质附件或其他供应商详情","resourceModule 必须是本文冻结的 10 个模块之一;VEHICLE 的菜单原文为“车队管理-车队管理”"],
["SUPPLIER_RESOURCE_MODULE_INVALID","SUPPLIER_RESOURCE_RELATION_NOT_FOUND"],"数据模型 v1.9.1 supplier_resource_rel","已实现 · 可联调"),
E("SUP-ADM-049","PUT","/admin/supplier/resource-relations/{resourceModule}/{resourceId}/update","设置或改绑资源供应商","资源侧供应商维护","平台菜单/按钮权限",
"SupplierResourceReassignRequest","Result<SupplierResourceRelationVO>",
["仅由景区、餐厅、备品、组合配品、游玩项目、酒店、服务、额外成本、服务人员和车队等资源管理页面调用;供应商详情不提供本操作","资源未关联时创建关系,已关联时只允许通过本接口显式改绑;目标 supplierId 必须 ACTIVE 并满足类型与必备资质","资源页面选择供应商时复用 SUP-ADM-001 分页接口或 SUP-ADM-043 有界列表,并按 status=ACTIVE、typeCode=requiredTypeCode 筛选;最终资格仍由本接口服务端复核","已有关系时必须携带 expectedCurrentSupplierId 与 expectedRelationUpdateTime;当前关系与预期不一致时按并发冲突失败关闭","VEHICLE 存在性和摘要通过 Fleet 内部只读能力在事务外校验;不得跨 schema 查询或写 Fleet","本地事务按 supplierId 升序锁定新旧供应商并锁定当前关系,软删除旧关系、创建新关系,双方 supplier_change_log 同步留痕","同一资源已关联目标供应商且业务字段无变化时按幂等成功返回,不制造空变更日志"],
["SUPPLIER_NOT_FOUND","SUPPLIER_PROFILE_NOT_ACTIVE","SUPPLIER_RESOURCE_MODULE_INVALID","SUPPLIER_RESOURCE_NOT_FOUND","SUPPLIER_RESOURCE_ALREADY_BOUND","SUPPLIER_RESOURCE_TYPE_MISMATCH","SUPPLIER_QUALIFICATION_REQUIRED","SUPPLIER_QUALIFICATION_EXPIRED","SUPPLIER_RESOURCE_DEPENDENCY_UNAVAILABLE","SUPPLIER_CONCURRENT_MODIFICATION"],"数据模型 v1.9.1 supplier_resource_rel","已实现 · 可联调"),
E("SUP-ADM-050","POST","/admin/supplier/resource-relations/{resourceModule}/{resourceId}/unbind","解除资源供应商","资源侧供应商维护","平台菜单/按钮权限",
"SupplierResourceUnbindRequest","Result<Void>",
["仅由资源管理页面解除当前供应商;供应商详情不提供解绑入口","请求必须携带 expectedCurrentSupplierId、expectedRelationUpdateTime 和 changeReason;当前关系不存在、已解绑或预期不一致时失败关闭","同一事务将当前 supplier_resource_rel 写 deleted_at=当前时间并追加 supplier_change_log,禁止物理删除","解除不要求资源仍处于启用状态;关系软删除后该资源允许重新设置供应商"],
["SUPPLIER_RESOURCE_MODULE_INVALID","SUPPLIER_RESOURCE_RELATION_NOT_FOUND","SUPPLIER_CONCURRENT_MODIFICATION"],"数据模型 v1.9.1 supplier_resource_rel","已实现 · 可联调"),
];
const pathNamingKinds={
page:["SUP-ADM-001","SUP-ADM-012"],
list:["SUP-ADM-043","SUP-ADM-034"],
add:["SUP-ADM-003","SUP-ADM-035"],
update:["SUP-ADM-004","SUP-ADM-041","SUP-ADM-049"],
del:["SUP-ADM-011"],
view:["SUP-ADM-002","SUP-ADM-036","SUP-ADM-048"]
};
function operationIdFor(endpoint){
return endpoint.id.toLowerCase().replace(/-/g,"_");
}
const apiSections = [
{
title:"基本信息",
detailTab:true,
groups:["基本信息"],
description:"对应供应商详情页“基本信息”:SUP-ADM-002 独立返回主体档案、类型、资质、联系人、信用和生命周期;账号由另一个详情页签返回。供应商详情不展示或维护资源关系。",
ids:["SUP-ADM-001","SUP-ADM-043","SUP-ADM-002","SUP-ADM-003","SUP-ADM-004","SUP-ADM-007","SUP-ADM-010","SUP-ADM-011","SUP-ADM-012"]
},
{
title:"账号信息",
detailTab:true,
groups:["账号信息"],
description:"对应详情页“账号信息”:SUP-ADM-034 根据路径参数 supplierId 返回该供应商名下全部收款账户及账户结算/开票口径;SUP-ADM-035 一次接收 1..50 个账户并为每项创建独立账户和审批,不生成账户草稿;默认账户使用独立写接口切换且每个供应商最多一个。账户新增成功后不提供删除或停用入口。",
ids:["SUP-ADM-034","SUP-ADM-035","SUP-ADM-036","SUP-ADM-041"]
},
{
title:"资源侧供应商维护",
detailTab:false,
groups:["资源侧供应商维护"],
description:"非供应商详情页签。景区、餐厅、备品、组合配品、游玩项目、酒店、服务、额外成本、服务人员和车队模块在各自资源页面查询、设置、改绑或解除供应商。",
ids:["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"]
}
];
const supplierDetailSections=apiSections.filter(function(section){return section.detailTab;});
const apiSectionById = {};
apiSections.forEach(function(section){
section.ids.forEach(function(endpointId){
if(apiSectionById[endpointId]) throw new Error("接口重复归类:"+endpointId);
apiSectionById[endpointId]=section.title;
});
});
endpoints.forEach(function(endpoint){
if(!apiSectionById[endpoint.id]) throw new Error("接口未归入接口分组:"+endpoint.id);
endpoint.group=apiSectionById[endpoint.id];
});
const errorRows = [
["SUPPLIER_NOT_FOUND",395001,"聚合根不存在或已软删","供应商不存在"],
["SUPPLIER_WRITE_FORBIDDEN",395002,"管理员未取得当前写接口的必需平台权限","无权执行该供应商写操作"],
["SUPPLIER_APPROVAL_OPINION_FORBIDDEN",395003,"无 supplier:approval:opinion 读取意见","无权查看审批意见"],
["SUPPLIER_STATUS_FORBIDDEN",395004,"状态动作未通过 supplier:status:manage 等必需权限或业务 Guard","无权执行该供应商状态操作"],
["SUPPLIER_STATUS_TRANSITION_INVALID",395005,"迁移不在状态图中","当前状态不允许执行该操作"],
["SUPPLIER_IDENTITY_DUPLICATE",395006,"C-02","该主体证件号已建档"],
["SUPPLIER_DUPLICATE_CONFIRM_REQUIRED",395007,"C-01 未确认","存在近似供应商,请确认后继续"],
["SUPPLIER_TYPE_PRIMARY_INVALID",395008,"无类型、多个主类型或主类型不在集合","请至少选择一个类型并设置唯一主类型"],
["SUPPLIER_INITIAL_ACCOUNT_INVALID",395009,"初始账户字段、归属、重复或户名派生失败","请修正随单收款账户后重新提交注册"],
["SUPPLIER_PROFILE_NOT_ACTIVE",395010,"注册未完成即调用注册后账户接口","请先完成供应商注册"],
["SUPPLIER_ACCOUNT_APPROVAL_IN_PROGRESS",395011,"同一账户已有在途申请","该账户正在审批中,请勿重复提交"],
["SUPPLIER_QUALIFICATION_REQUIRED",395012,"C-09 必备资质缺失","必备资质缺失"],
["SUPPLIER_QUALIFICATION_EXPIRED",395013,"C-09 必备资质已过期","必备资质已过期"],
["SUPPLIER_CONCURRENT_MODIFICATION",395014,"expectedUpdateTime 不一致","数据已被他人修改,请刷新后重试"],
["SUPPLIER_IMPORT_FILE_INVALID",395015,"导入文件归属、摘要、扩展名、大小或内容不合法","导入文件无效,请重新上传(非本期)"],
["SUPPLIER_IMPORT_TEMPLATE_UNSUPPORTED",395016,"模板版本、表头或必填列不匹配","导入模板版本不支持(非本期)"],
["SUPPLIER_IMPORT_LIMIT_EXCEEDED",395017,"导入行数超过服务端限制","导入行数超过限制,请拆分后重试(非本期)"],
["SUPPLIER_IMPORT_BATCH_STATE_INVALID",395018,"导入批次状态非法","当前导入批次状态不允许执行该操作(非本期)"],
["SUPPLIER_WECOM_NOT_CONFIGURED",395019,"企微密钥、模板或控件缺失","企业微信审批未配置,无法提交"],
["SUPPLIER_WECOM_USER_UNBOUND",395020,"发起管理员未绑定企微","当前账号未绑定企业微信"],
["SUPPLIER_WECOM_SUBMIT_FAILED",395021,"企微明确失败","企业微信申请提交失败,请稍后重试"],
["SUPPLIER_WECOM_RESULT_UNCERTAIN",395022,"超时或响应丢失","企微申请状态待对账,请勿重复提交"],
["SUPPLIER_WECOM_TEMPLATE_MISMATCH",395023,"模板不属于供应商","审批模板不匹配,已拒绝处理"],
["SUPPLIER_WECOM_DETAIL_INCOMPLETE",395024,"审批详情不完整","企业微信审批记录尚未同步完整"],
["SUPPLIER_WECOM_SELF_APPROVAL_DETECTED",395025,"发起人在任一级形成通过动作","检测到发起人自审,结果已冻结"],
["SUPPLIER_WECOM_DETAIL_REVISION_STALE",395026,"旧详情覆盖新详情","审批详情版本已过期"],
["SUPPLIER_ACCOUNT_DUPLICATE",395027,"C-19","该收款账号已被占用,请联系财务核实"],
["SUPPLIER_ACCOUNT_CHANGE_IN_PROGRESS",395028,"账户已有在途变更","该账户正在变更审批中,请勿重复提交"],
["SUPPLIER_ARCHIVED_IMMUTABLE",395031,"修改或恢复 ARCHIVED","已归档供应商仅允许查看"],
["SUPPLIER_CLEARANCE_CHECK_UNAVAILABLE",395032,"清账检查失败","暂无法确认财务已清账,不能归档"],
["SUPPLIER_IDENTITY_IMMUTABLE",395033,"供应商已进入注册审批或已经完成审批,fullName/taxNo 发生实际变化","审批中及审批完成后,供应商全称和统一社会信用代码不允许修改"],
["SUPPLIER_RESOURCE_MODULE_INVALID",395034,"resourceModule 不在冻结的 10 个模块中","不支持的资源模块"],
["SUPPLIER_RESOURCE_NOT_FOUND",395035,"指定模块下的资源不存在或已软删除","资源不存在或已删除"],
["SUPPLIER_RESOURCE_ALREADY_BOUND",395036,"资源已经存在其他有效供应商关系","该资源已关联其他供应商,请使用改绑"],
["SUPPLIER_RESOURCE_TYPE_MISMATCH",395037,"目标供应商缺少 requiredTypeCode 对应的有效类型,或删除了仍被关系引用的类型","供应商类型不满足资源关联要求"],
["SUPPLIER_RESOURCE_RELATION_NOT_FOUND",395038,"关联关系不存在、已解绑或不属于指定供应商","供应商资源关联不存在"],
["SUPPLIER_RESOURCE_DEPENDENCY_UNAVAILABLE",395039,"VEHICLE 等资源摘要只读依赖超时、异常或返回非成功结果","暂时无法校验资源,请稍后重试"],
["SUPPLIER_QUALIFICATION_VALIDITY_INVALID",395042,"permanentValid 与 expiryDate 组合矛盾","永久有效标记与到期日期不一致"]
];
function esc(value){
return String(value == null ? "" : value).replace(/[&<>"']/g,function(ch){
return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[ch];
});
}
const groupIds = {
"基本信息":"group-basic",
"账号信息":"group-account",
"资源侧供应商维护":"group-resource"
};
function anchor(id){ return id.toLowerCase().replace(/[^a-z0-9]+/g,"-"); }
function statusClass(status){
if(status==="本期不做") return "out";
if(status==="二期"||status==="后续 WECOM 对接") return "p2";
if(status.indexOf("复用")===0) return "extend";
return "current";
}
function methodClass(method){ return method.toLowerCase(); }
function list(items){
if(!items || !items.length) return "<span class='small'>无接口专属项,适用通用契约</span>";
return "<ul>"+items.map(function(x){return "<li>"+esc(x)+"</li>";}).join("")+"</ul>";
}
function renderEndpoints(){
const groups = Array.from(new Set(endpoints.map(function(x){return x.group;})));
const statuses = Array.from(new Set(endpoints.map(function(x){return x.delivery;})));
const groupFilter = document.getElementById("groupFilter");
const statusFilter = document.getElementById("statusFilter");
groups.forEach(function(g){groupFilter.insertAdjacentHTML("beforeend","<option>"+esc(g)+"</option>");});
statuses.forEach(function(s){statusFilter.insertAdjacentHTML("beforeend","<option>"+esc(s)+"</option>");});
const html = groups.map(function(group){
const groupId = groupIds[group] || "group-"+anchor(group);
return "<div class='endpoint-group' data-group='"+esc(group)+"'><h3 id='"+groupId+"'>"+esc(group)+"</h3>"+
endpoints.filter(function(e){return e.group===group;}).map(function(e){
const searchable = [e.id,operationIdFor(e),e.method,e.path,e.name,e.group,e.access,e.request,e.response,e.rules.join(" "),e.errors.join(" ")].join(" ").toLowerCase();
return "<article class='endpoint-card' id='"+anchor(e.id)+"' data-group='"+esc(e.group)+"' data-status='"+esc(e.delivery)+"' data-search='"+esc(searchable)+"'>"+
"<div class='endpoint-head'><span class='badge method "+methodClass(e.method)+"'>"+esc(e.method)+"</span>"+
"<h3>"+esc(e.id)+" · "+esc(e.name)+"</h3><span class='badge "+statusClass(e.delivery)+"'>"+esc(e.delivery)+"</span></div>"+
"<div class='path'>"+esc(e.path)+"</div>"+
"<div class='meta-grid'><div class='meta'><b>授权方式</b>"+esc(e.access)+"</div>"+
"<div class='meta'><b>提供方</b>"+(e.id.indexOf("SUP-WECOM")===0?"hl-user-service / 现有 InternalApprovalController":"hl-resource-service")+"</div>"+
"<div class='meta'><b>消费方</b>"+esc(e.consumer)+"</div>"+
"<div class='meta'><b>涉及模型</b>"+esc(e.tables||"见来源章节")+"</div></div>"+
"<div class='contract-grid'><div><h4>请求</h4><p>"+esc(e.request)+"</p></div>"+
"<div><h4>响应</h4><p>"+esc(e.response)+"</p></div>"+
"<div><h4>业务、状态与副作用</h4>"+list(e.rules)+"</div>"+
"<div><h4>接口专属错误</h4>"+list(e.errors)+"</div></div>"+
"<p class='source'><b>双来源追踪:</b>"+esc(e.source)+"</p>"+
"</article>";
}).join("")+"</div>";
}).join("");
document.getElementById("endpointList").innerHTML = html;
}
function applyFilter(){
const q = document.getElementById("search").value.trim().toLowerCase();
const g = document.getElementById("groupFilter").value;
const s = document.getElementById("statusFilter").value;
let visible=0;
document.querySelectorAll(".endpoint-card[data-search]").forEach(function(card){
const ok=(!q||card.dataset.search.indexOf(q)>=0)&&(!g||card.dataset.group===g)&&(!s||card.dataset.status===s);
card.classList.toggle("hidden",!ok); if(ok) visible++;
});
document.querySelectorAll(".endpoint-group").forEach(function(group){
group.classList.toggle("hidden",group.querySelectorAll(".endpoint-card:not(.hidden)").length===0);
});
document.getElementById("filterResult").textContent="当前显示 "+visible+" / "+endpoints.length+" 个 HTTP 契约。";
}
function renderMetrics(){
const current=endpoints.filter(function(e){return e.delivery==="已实现 · 可联调"||e.delivery.indexOf("复用")===0;}).length;
const out=endpoints.filter(function(e){return e.delivery==="本期不做";}).length;
const p2=endpoints.filter(function(e){return e.delivery==="二期";}).length;
const futureWecom=endpoints.filter(function(e){return e.delivery==="后续 WECOM 对接";}).length;
const metrics=[["HTTP 总契约",endpoints.length],["本期 LOCAL_AUTO",current],["非本期",out],["二期",p2],["后续 WECOM",futureWecom]];
document.getElementById("metrics").innerHTML=metrics.map(function(m){return "<div class='metric'><strong>"+m[1]+"</strong>"+m[0]+"</div>";}).join("");
}
function renderErrors(){
document.querySelector("#errorTable tbody").innerHTML=errorRows.map(function(r){
return "<tr><td><code>"+esc(r[0])+"</code></td><td><code>"+esc(r[1])+"</code></td><td>"+esc(r[2])+"</td><td>"+esc(r[3])+"</td></tr>";
}).join("");
}
function textSchema(options){
const schema={type:"string"};
Object.keys(options||{}).forEach(function(k){schema[k]=options[k];});
return schema;
}
function ref(name){return {$ref:"#/components/schemas/"+name};}
function idSchema(description){return textSchema({pattern:"^[0-9]+$",description:description||"Snowflake Long 的 JSON String",example:"1900000000000000001"});}
function localDateSchema(opts){return Object.assign({type:"string",format:"date",example:"2026-08-18"},opts||{});}
function localDateTimeSchema(options){
const schema={type:"string",pattern:"^[0-9]{4}-(0[1-9]|1[0-2])-([0-2][0-9]|3[01]) ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$",example:"2026-08-18 15:30:00","x-java-type":"java.time.LocalDateTime"};
Object.keys(options||{}).forEach(function(k){schema[k]=options[k];}); return schema;
}
function stringEnum(values,description){return {type:"string",enum:values,description:description};}
const lifecycleStatusDescription="供应商生命周期:DRAFT=草稿,VETTING=注册审核中,ACTIVE=合作中,SUSPENDED=暂停,FROZEN=冻结,BLACKLIST=黑名单,ARCHIVED=清账归档;前端展示字典 supplier_lifecycle_status";
const typeCodeDescription="供应商类型编码:SCENIC=景区,RESTAURANT=餐厅,SUPPLIES=备品,ACTIVITY=游玩项目,HOTEL=酒店,SERVICE=服务,FLEET=车队,RENTAL=租车,PERFORMANCE=演艺,INSURANCE=保险,CHANNEL=OTA/渠道,PROFESSIONAL_SERVICE=专业服务,PROPERTY=物业/房租,TICKET=票务,OTHER=其他;前端选项字典 supplier_type";
const resourceModuleValues=["SCENIC","RESTAURANT","SUPPLIES","SUPPLIES_COMBO","ACTIVITY","HOTEL","SERVICE","COST_ITEM","STAFF","VEHICLE"];
const resourceModuleDescription="资源模块:SCENIC=景区管理,RESTAURANT=餐厅管理,SUPPLIES=备品管理,SUPPLIES_COMBO=组合配品,ACTIVITY=游玩项目管理,HOTEL=酒店管理,SERVICE=服务管理,COST_ITEM=额外成本,STAFF=服务人员管理,VEHICLE=车队管理-车队管理;该枚举不是平台字典";
function lifecycleStatusSchema(values){return Object.assign(stringEnum(values,lifecycleStatusDescription),{"x-hl-dict-type":"supplier_lifecycle_status"});}
function typeCodeSchema(options){return textSchema(Object.assign({minLength:1,maxLength:32,description:typeCodeDescription,"x-hl-dict-type":"supplier_type"},options||{}));}
function requiredTypeCodeSchema(){return textSchema({minLength:1,maxLength:64,description:"资源关联要求的供应商类型编码;取 supplier_type 启用值并校验供应商有效类型关系","x-hl-dict-type":"supplier_type"});}
function resourceModuleSchema(){return stringEnum(resourceModuleValues,resourceModuleDescription);}
function arraySchema(items,options){const schema={type:"array",items:items};Object.keys(options||{}).forEach(function(k){schema[k]=options[k];});return schema;}
function objectSchema(properties,required,options){
const schema={type:"object",additionalProperties:false,properties:properties||{}};
if(required&&required.length) schema.required=required;
Object.keys(options||{}).forEach(function(k){schema[k]=options[k];}); return schema;
}
function nullable(schema){const copy=Object.assign({},schema);copy.nullable=true;return copy;}
function resultSchema(dataSchema){
return objectSchema({code:{type:"integer",format:"int32",enum:[200],example:200},message:{type:"string",example:"成功"},success:{type:"boolean",enum:[true],example:true},data:dataSchema},["code","message","success","data"],{description:"Result<T> 成功包络"});
}
function pageSchema(itemSchema){
return objectSchema({records:arraySchema(itemSchema),total:{type:"integer",format:"int32",minimum:0,maximum:2147483647},page:{type:"integer",format:"int32",minimum:1},pageSize:{type:"integer",format:"int32",minimum:1,maximum:100}},["records","total","page","pageSize"],{description:"与 hl-common PageResult<T> 的 int 字段对齐"});
}
function buildOpenApiSchemas(){
const statusEnum=["DRAFT","VETTING","ACTIVE","SUSPENDED","FROZEN","BLACKLIST","ARCHIVED"];
const accountStatusEnum=["PENDING","ACTIVE","DISABLED"];
const accountTypeEnum=["CORPORATE","PERSONAL"];
const onboardingStageEnum=["PROFILE_DRAFT","PROFILE_APPROVING","COMPLETED"];
const syncStatusEnum=["REQUESTING","SUBMITTED","SYNCED","APPLY_PENDING","APPLIED","APPLY_FAILED","FAILED"];
const approvalStatusEnum=["PENDING","APPROVED","REJECTED","CANCELLED","FAILED"];
const applyStatusEnum=["NOT_APPLIED","APPLY_PENDING","APPLIED","APPLY_FAILED"];
const approvalProviderEnum=["LOCAL_AUTO","WECOM"];
const detailSyncStatusEnum=["NOT_APPLICABLE","NOT_SYNCED","COMPLETE","INCOMPLETE","FAILED"];
const approvalBizTypes=["PROFILE_CREATE","ACCOUNT_CREATE","ACCOUNT_CHANGE","STATUS_CHANGE"];
const statusSubTypes=["STATUS_SUSPEND","STATUS_RESUME","STATUS_FREEZE","STATUS_UNFREEZE","STATUS_BLACKLIST","STATUS_UNBLACKLIST"];
function rawSpStatusSchema(options){return textSchema(Object.assign({minLength:1,maxLength:64,description:"企业微信原始状态码字符串;已知 1/2/3/4/6/7,未知值原样保留并失败关闭"},options||{}));}
const schemas={};
schemas.ErrorEnvelope=objectSchema({code:{type:"integer",format:"int32",example:395001},message:{type:"string"},success:{type:"boolean",enum:[false]},data:nullable({type:"object",additionalProperties:true,description:"业务错误通常为 null;需要结构化错误上下文时允许对象"})},["code","message","success","data"],{description:"Result<T> 错误包络;业务错误通常使用 HTTP 200,data 允许 null"});
schemas.SupplierRiskFlag={type:"string",enum:["MANDATORY_QUALIFICATION_INVALID","CREDIT_LEVEL_D","PENDING_BLACKLIST"],description:"本期固定风险标记;服务端实时派生,客户端不得手填"};
schemas.SupplierContactInput=objectSchema({contactId:idSchema(),contactName:textSchema({minLength:1,maxLength:500}),contactPhone:textSchema({minLength:1,maxLength:20,writeOnly:true,"x-db-type":"TEXT","x-max-utf8-bytes":20}),contactRole:textSchema({minLength:1,maxLength:32}),remark:textSchema({maxLength:200})},["contactName","contactPhone","contactRole"]);
schemas.SupplierQualificationInput=objectSchema({qualificationId:idSchema(),qualType:textSchema({minLength:1,maxLength:64}),certNo:textSchema({maxLength:128,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":128}),imageUrl:textSchema({maxLength:1000,"x-db-type":"TEXT"}),permanentValid:{type:"boolean",description:"是否永久有效;历史完整请求省略时按 expiryDate 是否为空推导"},expiryDate:localDateSchema({nullable:true})},["qualType"],{description:"permanentValid=true 时 expiryDate 必须为空,false 时 expiryDate 必填;矛盾组合返回 395042。isRequired 由服务端类型规则派生,客户端不得传入;certNo 以 AES-GCM 密文保存,普通返回仅 certNoMask"});
schemas.SupplierBankAccountInput=objectSchema({accountType:stringEnum(["CORPORATE","PERSONAL"]),bankName:textSchema({minLength:1,maxLength:500}),bankBranch:textSchema({maxLength:500}),accountNo:textSchema({minLength:1,maxLength:128,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":128}),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema({maxLength:50}),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema({maxLength:10})},["accountType","bankName","accountNo"],{description:"结算字段按当前账户保存:仅 MONTHLY 允许 accountPeriod;SPECIAL/NORMAL 必填 taxRate,NONE 时 taxRate 必须为空。accountName、accountNoMask、isPersonal 由服务端派生;accountNo 规范化后按 UTF-8 bytes 校验;proofFileUrls 保存为受控 JSON,只用于审批表单和授权详情"});
schemas.SupplierDraftUpsertRequest=objectSchema({fullName:textSchema({minLength:1,maxLength:500}),shortName:textSchema({maxLength:300}),taxNo:textSchema({minLength:1,maxLength:64,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":64}),types:arraySchema(objectSchema({typeCode:typeCodeSchema()},["typeCode"]),{minItems:1,maxItems:15,description:"供应商类型输入列表;前端从 supplier_type 取选项并将 dictValue 写入 typeCode;请求不接收 typeName,服务端校验编码并生成显示名称",example:[{typeCode:"HOTEL"},{typeCode:"RESTAURANT"},{typeCode:"SCENIC"}]}),legalRepresentative:textSchema({maxLength:500}),contactPhone:textSchema({title:"法人电话/公司电话",maxLength:20,writeOnly:true,"x-db-type":"TEXT","x-max-utf8-bytes":20}),establishDate:localDateSchema(),registeredCapital:textSchema({maxLength:50}),businessScope:textSchema({maxLength:500}),address:textSchema({maxLength:500}),staffScale:stringEnum(["LT50","R50_200","R200_500","GT500"]),mainCooperation:textSchema({minLength:1,"x-db-type":"TEXT","x-max-utf8-bytes":65535}),licenseImageUrl:textSchema({maxLength:500}),approveNote:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215,description:"对应 supplier_main.approve_note,MEDIUMTEXT;UTF-8 ≤ 16,777,215 bytes"}),remark:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215,description:"对应 supplier_main.remark,MEDIUMTEXT;UTF-8 ≤ 16,777,215 bytes"}),contacts:arraySchema(ref("SupplierContactInput"),{maxItems:100}),qualifications:arraySchema(ref("SupplierQualificationInput"),{maxItems:100}),initialAccounts:arraySchema(ref("SupplierBankAccountInput"),{maxItems:50}),duplicateConfirmToken:textSchema({maxLength:256}),expectedUpdateTime:localDateTimeSchema()},["fullName","taxNo","types","mainCooperation"],{description:"信用等级不属于建档输入,服务端固定初始化为 B;taxNo 规范化后 UTF-8 ≤64 bytes,数据库以 TEXT/ascii_bin 保存确定性密文;mainCooperation UTF-8 ≤65535 bytes"});
schemas.SupplierTypeMergeInput=objectSchema({typeCode:typeCodeSchema(),isPrimary:{type:"boolean"}},["typeCode"],{description:"types 集合传入后按 (supplierId,typeCode) 对账;已存在则更新主类型标识,不存在则新增,未出现在完整快照中的有效关联软删除。"});
schemas.SupplierContactMergeInput=objectSchema({contactId:idSchema(),contactName:textSchema({minLength:1,maxLength:500}),contactPhone:textSchema({minLength:1,maxLength:20,writeOnly:true,"x-db-type":"TEXT","x-max-utf8-bytes":20}),contactRole:textSchema({minLength:1,maxLength:32}),remark:textSchema({maxLength:200}),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"联系人集合快照项;已有记录携带 contactId 和 expectedUpdateTime,无 contactId 时新增;数据库有效联系人中未出现在快照内的记录软删除。","x-hl-existing-record-requires":["contactId","expectedUpdateTime"],"x-hl-create-omits":["contactId","expectedUpdateTime"],"x-hl-create-required":["contactName","contactPhone","contactRole"]});
schemas.SupplierQualificationMergeInput=objectSchema({qualificationId:idSchema(),qualType:textSchema({minLength:1,maxLength:64}),certNo:textSchema({maxLength:128,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":128}),imageUrl:textSchema({maxLength:1000,"x-db-type":"TEXT"}),permanentValid:{type:"boolean",description:"是否永久有效;既有项与 expiryDate 同时省略时保持原有效期"},expiryDate:localDateSchema({nullable:true}),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"资质集合快照项;已有记录携带 qualificationId 和 expectedUpdateTime,无 qualificationId 时新增;数据库有效资质中未出现在快照内的记录软删除。permanentValid=true 时 expiryDate 必须为空,false 时 expiryDate 必填。","x-hl-existing-record-requires":["qualificationId","expectedUpdateTime"],"x-hl-create-omits":["qualificationId","expectedUpdateTime"],"x-hl-create-required":["qualType"]});
schemas.SupplierContractMergeInput=objectSchema({contractId:idSchema(),contractName:textSchema({minLength:1,maxLength:500}),contractNo:textSchema({maxLength:100}),contractType:stringEnum(["FRAME","SINGLE_TRIP","PURCHASE"]),signDate:localDateSchema(),startDate:localDateSchema(),endDate:localDateSchema(),amount:{type:"number",minimum:0},pricingMode:textSchema({maxLength:100}),settleCycle:textSchema({maxLength:32}),status:stringEnum(["DRAFT","ACTIVE","EXPIRED"]),scanFileUrl:textSchema({maxLength:1000,"x-db-type":"TEXT"}),remark:textSchema({maxLength:500}),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"合同集合快照项;已有记录携带 contractId 和 expectedUpdateTime,无 contractId 时新增;数据库有效合同中未出现在快照内的记录软删除。","x-hl-existing-record-requires":["contractId","expectedUpdateTime"],"x-hl-create-omits":["contractId","expectedUpdateTime"],"x-hl-create-required":["contractName","contractType","startDate","endDate","status"]});
schemas.SupplierEvaluationMergeInput=objectSchema({evaluationId:idSchema(),orderId:idSchema(),resourceId:idSchema(),tripDate:localDateSchema(),dimension:textSchema({minLength:1,maxLength:32}),score:{type:"number",minimum:0,maximum:5},content:textSchema({"x-db-type":"TEXT","x-max-utf8-bytes":65535}),evaluator:idSchema(),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"评价集合快照项;已有记录携带 evaluationId 和 expectedUpdateTime,无 evaluationId 时新增;数据库有效评价中未出现在快照内的记录软删除。","x-hl-existing-record-requires":["evaluationId","expectedUpdateTime"],"x-hl-create-omits":["evaluationId","expectedUpdateTime"],"x-hl-create-required":["dimension","score"]});
schemas.SupplierUpdateRequest=objectSchema({fullName:textSchema({minLength:1,maxLength:500}),shortName:textSchema({maxLength:300}),taxNo:textSchema({minLength:1,maxLength:64,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":64}),legalRepresentative:textSchema({maxLength:500}),contactPhone:textSchema({title:"法人电话/公司电话",maxLength:20,writeOnly:true,"x-db-type":"TEXT","x-max-utf8-bytes":20}),establishDate:localDateSchema(),registeredCapital:textSchema({maxLength:50}),businessScope:textSchema({maxLength:500}),address:textSchema({maxLength:500}),staffScale:stringEnum(["LT50","R50_200","R200_500","GT500"]),mainCooperation:textSchema({minLength:1,"x-db-type":"TEXT","x-max-utf8-bytes":65535}),licenseImageUrl:textSchema({maxLength:500}),approveNote:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215}),remark:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215}),types:arraySchema(ref("SupplierTypeMergeInput"),{minItems:1,maxItems:15,description:"未传表示不处理;传入表示完整快照,缺失的有效类型关联软删除;至少保留一个类型"}),contacts:arraySchema(ref("SupplierContactMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效联系人"}),qualifications:arraySchema(ref("SupplierQualificationMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效资质"}),contracts:arraySchema(ref("SupplierContractMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效合同"}),evaluations:arraySchema(ref("SupplierEvaluationMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效评价"}),changeReason:textSchema({minLength:1,maxLength:500}),expectedUpdateTime:localDateTimeSchema()},["changeReason","expectedUpdateTime"],{minProperties:3,description:"供应商及关联表增量补全请求;主体标量按 PATCH 更新。集合字段未传时保持不变,一旦传入即为完整当前快照:同 ID 更新、无 ID 新增、存量缺失项写 deleted_at 软删除,禁止物理删除。","x-hl-update-mode":"PATCH_SCALAR_AND_RECONCILE_COLLECTIONS","x-hl-collection-semantics":"OMITTED_UNCHANGED_PROVIDED_FULL_SNAPSHOT","x-hl-missing-existing-item":"SOFT_DELETE","x-hl-empty-collection":{"types":"REJECT_MIN_ONE","contacts":"SOFT_DELETE_ALL","qualifications":"SOFT_DELETE_ALL","contracts":"SOFT_DELETE_ALL","evaluations":"SOFT_DELETE_ALL"},"x-hl-physical-delete":"FORBIDDEN","x-hl-at-least-one-change-excluding":["changeReason","expectedUpdateTime"],"x-hl-immutable-when-approving-or-approved":["fullName","taxNo"],"x-hl-account-fields":"FORBIDDEN_USE_ACCOUNT_APIS"});
schemas.SupplierSubmitRequest=objectSchema(Object.assign({},schemas.SupplierDraftUpsertRequest.properties,{submitNote:textSchema({maxLength:500})}),["fullName","taxNo","types","mainCooperation","licenseImageUrl","expectedUpdateTime"],{description:"提交注册审批使用完整供应商表单;qualifications 按 types 对应的必备资质规则条件校验;在同一事务中保存表单、写 supplier_change_log 和 supplier_approval_log,并将 supplier_main.status 更新为 ACTIVE"});
schemas.SupplierStatusRequest=objectSchema({targetStatus:stringEnum(["ACTIVE","SUSPENDED","FROZEN","BLACKLIST"]),changeReason:textSchema({minLength:1,maxLength:500}),expectedUpdateTime:localDateTimeSchema()},["targetStatus","changeReason","expectedUpdateTime"]);
schemas.QualificationRuleInput=objectSchema({qualType:textSchema({minLength:1,maxLength:64}),isRequired:{type:"boolean"},warningDays:{type:"integer",minimum:0},sortOrder:{type:"integer",minimum:1}},["qualType","isRequired","sortOrder"]);
schemas.QualificationRuleReplaceRequest=objectSchema({rules:arraySchema(ref("QualificationRuleInput"),{maxItems:200})},["rules"],{description:"typeCode 由路径提供,必须是平台字典 supplier_type 的启用 dictValue;服务端按 typeCode 加锁并全量替换规则"});
schemas.ContactReplaceRequest=objectSchema({contacts:arraySchema(ref("SupplierContactInput"),{maxItems:100}),changeReason:textSchema({maxLength:500}),expectedUpdateTime:localDateTimeSchema()},["contacts","expectedUpdateTime"]);
schemas.SupplierBankAccountBatchCreateRequest=objectSchema({accounts:arraySchema(ref("SupplierBankAccountInput"),{minItems:1,maxItems:50,example:[{accountType:"CORPORATE",bankName:"示例银行",bankBranch:"呼和浩特分行",accountNo:"6222020000001234",proofFileUrls:["https://files.example.test/supplier/account-1.pdf"],settleMode:"MONTHLY",accountPeriod:"月结30天",invoiceType:"SPECIAL",taxRate:"6%"},{accountType:"CORPORATE",bankName:"备用示例银行",bankBranch:"呼和浩特营业部",accountNo:"6222020000005678",proofFileUrls:["https://files.example.test/supplier/account-2.pdf"],settleMode:"PREPAY",invoiceType:"NORMAL",taxRate:"3%"}]})},["accounts"],{description:"一次请求批量新增并提交 1..50 个收款账户;服务端先规范化全部 accountNo,批内不得重复,并逐项执行 C-19 全库唯一校验。任一请求校验失败时整批零写入;每项独立生成 accountId、approvalLogId 和 requestNo。","x-hl-batch-size":{"min":1,"max":50},"x-hl-batch-duplicate-key":"normalizedAccountNo","x-hl-validation-failure":"ROLLBACK_ALL_ZERO_WRITE","x-hl-client-forbidden-fields":["accountId","accountName","accountNoMask","isDefault","provider","templateId","approver","spNo","spStatus"]});
schemas.BankAccountChangeRequest=objectSchema({bankName:textSchema({maxLength:500}),bankBranch:textSchema({maxLength:500}),accountNo:textSchema({maxLength:128,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":128}),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema({maxLength:50}),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema({maxLength:10}),changeReason:textSchema({minLength:1,maxLength:500}),expectedUpdateTime:localDateTimeSchema()},["changeReason","expectedUpdateTime"],{minProperties:3,description:"除 changeReason/expectedUpdateTime 外至少一个账户候选字段;结算字段属于目标账户,执行 MONTHLY/accountPeriod、invoiceType/taxRate 条件一致性校验。候选规范化后先以版本化 AES-GCM 密文写 candidate_snapshot_ciphertext;候选字段永不通过普通接口返回;accountNo UTF-8 ≤128 bytes"});
schemas.AccountChangeCandidateSnapshotPayload=objectSchema({bankName:textSchema({maxLength:500}),bankBranch:textSchema({maxLength:500}),accountNo:textSchema({maxLength:128,writeOnly:true,"x-sensitive":true,"x-max-utf8-bytes":128}),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema({maxLength:50}),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema({maxLength:10})},[],{minProperties:1,description:"只用于 Supplier 本地 ACCOUNT_CHANGE 加密候选快照;银行与账户级结算字段都不得进入普通响应、日志、change_log、MQ 或企微详情镜像"});
function candidateSnapshotBranch(type,payload){return objectSchema({schemaVersion:{type:"integer",enum:[1]},bizType:{type:"string",enum:[type]},changeReason:textSchema({minLength:1,maxLength:500}),payload:ref(payload)},["schemaVersion","bizType","changeReason","payload"]);}
schemas.AccountChangeCandidateSnapshot=candidateSnapshotBranch("ACCOUNT_CHANGE","AccountChangeCandidateSnapshotPayload");
schemas.StatusChangeCandidateSnapshotPayload=objectSchema({currentStatus:lifecycleStatusSchema(statusEnum),targetStatus:lifecycleStatusSchema(statusEnum)},["currentStatus","targetStatus"]);
schemas.StatusChangeCandidateSnapshot=candidateSnapshotBranch("STATUS_CHANGE","StatusChangeCandidateSnapshotPayload");
schemas.ApprovalCandidateSnapshot={oneOf:[ref("AccountChangeCandidateSnapshot"),ref("StatusChangeCandidateSnapshot")],discriminator:{propertyName:"bizType",mapping:{ACCOUNT_CHANGE:"#/components/schemas/AccountChangeCandidateSnapshot",STATUS_CHANGE:"#/components/schemas/StatusChangeCandidateSnapshot"}},"x-hl-persistence-only":true,description:"序列化、规范化、计算 SHA-256 candidate_digest 后,以 AES-GCM 写 supplier_approval_log.candidate_snapshot_ciphertext;审批通过应用器校验摘要后解密,任何普通 API 均不得返回。REQUESTING/SUBMITTED/APPLY_PENDING/APPLY_FAILED 时保留,成功应用或明确驳回/撤销后清空"};
schemas.ControlledAccountFormFact=objectSchema({accountId:idSchema(),accountName:textSchema({minLength:1,maxLength:500}),accountType:stringEnum(["CORPORATE","PERSONAL"]),bankName:textSchema({minLength:1,maxLength:500}),bankBranch:textSchema({maxLength:500}),accountNo:textSchema({minLength:1,maxLength:128,writeOnly:true,"x-sensitive":true,"x-max-utf8-bytes":128,description:"内部请求中仅短生命周期内存和受控企微表单可见;本地只有 candidate_snapshot_ciphertext 可以保存该候选值的 AES-GCM 密文,detail_snapshot_ciphertext 必须使用掩码"}),isDefault:stringEnum(["YES","NO"],"YES=是,NO=否"),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema({maxLength:50}),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema({maxLength:10})},["accountId","accountName","accountType","bankName","accountNo","isDefault"],{description:"受控账户事实;包含该账户结算口径;不得写普通日志、异常、change_log、DOMAIN_EVENT、MQ 或通用审批日志;proofFileUrls 来源于 supplier_account.proof_file_urls"});
schemas.ProfileCreateApprovalFormFacts=objectSchema({factType:{type:"string",enum:["PROFILE_CREATE"]},supplierNo:textSchema({maxLength:150,nullable:true}),supplierName:textSchema({minLength:1,maxLength:500}),shortName:textSchema({maxLength:300}),taxNoMask:textSchema({minLength:1,maxLength:128}),legalRepresentative:textSchema({maxLength:500}),supplierTypeNames:arraySchema(textSchema({minLength:1,maxLength:500,"x-max-utf8-bytes":500}),{minItems:1,maxItems:15}),primaryTypeName:textSchema({minLength:1,maxLength:500,"x-max-utf8-bytes":500}),mainCooperation:textSchema({minLength:1,"x-max-utf8-bytes":65535}),isRelatedParty:{type:"boolean"},licenseImageUrl:textSchema({minLength:1,maxLength:500}),qualificationSummary:textSchema({minLength:1,maxLength:2000}),qualificationFileUrls:arraySchema(textSchema({maxLength:1000}),{maxItems:100}),initialAccounts:arraySchema(ref("ControlledAccountFormFact"),{maxItems:50}),riskFlags:arraySchema(ref("SupplierRiskFlag"),{maxItems:4,uniqueItems:true}),submitNote:textSchema({maxLength:500}),detailUrl:textSchema({minLength:1,maxLength:1000})},["factType","supplierName","taxNoMask","supplierTypeNames","primaryTypeName","mainCooperation","isRelatedParty","licenseImageUrl","qualificationSummary","qualificationFileUrls","initialAccounts","riskFlags","detailUrl"]);
schemas.AccountCreateApprovalFormFacts=objectSchema({factType:{type:"string",enum:["ACCOUNT_CREATE"]},supplierNo:textSchema({minLength:1,maxLength:150}),supplierName:textSchema({minLength:1,maxLength:500}),account:ref("ControlledAccountFormFact"),detailUrl:textSchema({minLength:1,maxLength:1000})},["factType","supplierNo","supplierName","account","detailUrl"]);
schemas.AccountChangeApprovalFormFacts=objectSchema({factType:{type:"string",enum:["ACCOUNT_CHANGE"]},supplierNo:textSchema({minLength:1,maxLength:150}),supplierName:textSchema({minLength:1,maxLength:500}),accountId:idSchema(),currentAccountNoMask:textSchema({minLength:1,maxLength:256}),candidateAccount:ref("ControlledAccountFormFact"),changeReason:textSchema({minLength:1,maxLength:500}),detailUrl:textSchema({minLength:1,maxLength:1000})},["factType","supplierNo","supplierName","accountId","currentAccountNoMask","candidateAccount","changeReason","detailUrl"]);
schemas.StatusChangeApprovalFormFacts=objectSchema({factType:{type:"string",enum:["STATUS_CHANGE"]},supplierNo:textSchema({minLength:1,maxLength:150}),supplierName:textSchema({minLength:1,maxLength:500}),currentStatus:lifecycleStatusSchema(statusEnum),targetStatus:lifecycleStatusSchema(statusEnum),changeReason:textSchema({minLength:1,maxLength:500}),riskFlags:arraySchema(ref("SupplierRiskFlag"),{maxItems:4,uniqueItems:true}),qualificationValiditySummary:textSchema({maxLength:2000}),detailUrl:textSchema({minLength:1,maxLength:1000})},["factType","supplierNo","supplierName","currentStatus","targetStatus","changeReason","riskFlags","detailUrl"]);
schemas.WeComApprovalFormFacts={oneOf:[ref("ProfileCreateApprovalFormFacts"),ref("AccountCreateApprovalFormFacts"),ref("AccountChangeApprovalFormFacts"),ref("StatusChangeApprovalFormFacts")],discriminator:{propertyName:"factType",mapping:{PROFILE_CREATE:"#/components/schemas/ProfileCreateApprovalFormFacts",ACCOUNT_CREATE:"#/components/schemas/AccountCreateApprovalFormFacts",ACCOUNT_CHANGE:"#/components/schemas/AccountChangeApprovalFormFacts",STATUS_CHANGE:"#/components/schemas/StatusChangeApprovalFormFacts"}},description:"factType 必须与外层 bizType 完全一致;每次请求只能匹配一个分支;账户结算口径随 ACCOUNT_CREATE/ACCOUNT_CHANGE 账户事实传递"};
schemas.WeComApprovalSubmitRequest=objectSchema({requestNo:textSchema({minLength:1,maxLength:128}),applicantAdminId:idSchema("Supplier 从 Gateway 可信 X-Admin-Id 取得;仅内部 DTO 可见"),supplierId:idSchema(),approvalLogId:idSchema(),bizType:stringEnum(approvalBizTypes),subType:stringEnum(statusSubTypes,"仅 bizType=STATUS_CHANGE 时必填;其他业务必须省略"),formFacts:ref("WeComApprovalFormFacts")},["requestNo","applicantAdminId","supplierId","approvalLogId","bizType","formFacts"],{description:"仅 SupplierApprovalPort 调用。requestNo 是唯一端到端幂等号,不再定义 clientRequestId;bizType 必须等于 formFacts.factType;STATUS_CHANGE 必须携带匹配迁移的 subType,其他业务不得携带 subType。applicantAdminId 来自管理端入口可信头;templateId/controlId/approver 由外部受控配置决定。该 Schema 不得复用于管理端请求"});
schemas.ApprovalDepartmentSnapshot=objectSchema({departmentId:textSchema({minLength:1,maxLength:64,description:"企业微信审批表单部门 ID 按字符串保存"}),departmentName:textSchema({minLength:1,maxLength:500}),departmentPath:textSchema({maxLength:1000}),primary:{type:"boolean"},source:stringEnum(["WECOM_FORM","WECOM_APPROVAL","UNKNOWN"],"部门快照来源;模板受控字段正常解析使用 WECOM_FORM,UNKNOWN 只能用于 INCOMPLETE 中间态")},["departmentId","departmentName","primary","source"],{description:"从企业微信审批表单/详情解析的不可变组织快照;支持一人多部门,不得用当前通讯录补写历史"});
schemas.ApprovalActionSnapshot=objectSchema({sourceItemNo:{type:"integer",minimum:1},approverUserId:textSchema({minLength:1,maxLength:128}),approverName:textSchema({maxLength:500}),approverDepartments:arraySchema(ref("ApprovalDepartmentSnapshot")),actionResult:stringEnum(["APPROVED","REJECTED","CANCELLED","PENDING","UNKNOWN"],"转交、加签和同意并加签本期不建模,出现时使用 UNKNOWN 并保持 INCOMPLETE"),rawAction:textSchema({maxLength:64}),hasOpinion:{type:"boolean"},opinion:textSchema({maxLength:500,description:"仅具备 supplier:approval:opinion 时返回正文;无权限只返回 hasOpinion,服务端按权限裁剪并写 SENSITIVE_READ 审计"}),actionAt:localDateTimeSchema({nullable:true})},["sourceItemNo","approverUserId","approverDepartments","actionResult","rawAction","hasOpinion"]);
schemas.ApprovalLevelSnapshot=objectSchema({levelNo:{type:"integer",minimum:1},levelName:textSchema({maxLength:200}),roleCodeSnapshot:textSchema({maxLength:64}),roleNameSnapshot:textSchema({maxLength:200}),roleSource:textSchema({maxLength:64}),approvalDepartments:arraySchema(ref("ApprovalDepartmentSnapshot")),approvalMode:textSchema({maxLength:32}),levelStatus:textSchema({maxLength:32}),startedAt:localDateTimeSchema({nullable:true}),finishedAt:localDateTimeSchema({nullable:true}),actions:arraySchema(ref("ApprovalActionSnapshot"),{minItems:1}),sourceNodeNo:textSchema({maxLength:128})},["levelNo","approvalDepartments","actions"]);
schemas.SupplierApprovalSnapshotDTO=objectSchema({schemaVersion:{type:"integer",enum:[1],description:"详情快照明文结构版本;未知版本拒绝同步和终态应用"},spNo:textSchema({minLength:1,maxLength:64}),templateId:textSchema({minLength:1,maxLength:128}),supplierId:idSchema(),approvalLogId:idSchema(),requestNo:textSchema({minLength:1,maxLength:128}),bizType:stringEnum(approvalBizTypes),subType:stringEnum(statusSubTypes),spStatus:rawSpStatusSchema(),applicantUserId:textSchema({minLength:1,maxLength:128}),applicantName:textSchema({maxLength:500}),applicantDepartments:arraySchema(ref("ApprovalDepartmentSnapshot")),applyTime:localDateTimeSchema(),finishedAt:localDateTimeSchema({nullable:true}),currentLevelNo:{type:"integer",minimum:1,nullable:true},sourceRevision:{type:"integer",format:"int64",minimum:1},detailDigest:textSchema({minLength:64,maxLength:64,pattern:"^[a-fA-F0-9]{64}$"}),detailSyncStatus:stringEnum(["COMPLETE","INCOMPLETE","FAILED"]),receivedSource:stringEnum(["CALLBACK","POLL","MANUAL_RECONCILE"]),levels:arraySchema(ref("ApprovalLevelSnapshot")),sourceEventKey:textSchema({minLength:1,maxLength:128})},["schemaVersion","spNo","templateId","supplierId","approvalLogId","requestNo","bizType","spStatus","applicantUserId","applicantDepartments","applyTime","sourceRevision","detailDigest","detailSyncStatus","receivedSource","levels","sourceEventKey"],{description:"COMPLETE 时企业微信表单中的申请人、必需节点和每位实际审批人部门字段均必须非空且主部门标记合法;字段缺失、出现本期未建模动作或未知 schemaVersion 时保持 INCOMPLETE,不查询当前通讯录补写"});
schemas.ApprovalSummaryVO=objectSchema({approvalLogId:idSchema(),bizType:stringEnum(approvalBizTypes),subType:stringEnum(statusSubTypes),provider:stringEnum(approvalProviderEnum,"本期固定 LOCAL_AUTO;WECOM 仅为后续预留"),approvalStatus:stringEnum(approvalStatusEnum),applyStatus:stringEnum(applyStatusEnum),systemDecision:{type:"boolean",description:"LOCAL_AUTO=true;不得伪装为人工审批"},spNo:textSchema({nullable:true,description:"仅 provider=WECOM 可有值"}),spStatus:rawSpStatusSchema({nullable:true}),currentLevel:{type:"integer",nullable:true},detailSyncStatus:stringEnum(detailSyncStatusEnum),submittedAt:localDateTimeSchema({nullable:true}),finishedAt:localDateTimeSchema({nullable:true})},["approvalLogId","bizType","provider","approvalStatus","applyStatus","systemDecision","detailSyncStatus"]);
schemas.SupplierQualificationVO=objectSchema({qualificationId:idSchema(),qualType:textSchema(),qualTypeName:textSchema({description:"资质类型中文名称;历史或停用值未命中字典时回退 qualType"}),certNoMask:textSchema(),imageUrl:textSchema(),expiryDate:localDateSchema({nullable:true}),permanentValid:{type:"boolean",description:"由 expiryDate 是否为空动态推导,固定返回非空布尔值"},daysUntilExpiry:{type:"integer",format:"int32",nullable:true,description:"按 Asia/Shanghai 当前自然日计算的有符号剩余天数;永久有效时为 null"},validityStatus:stringEnum(["VALID","INVALID"],"资质有效状态稳定编码"),validityStatusName:textSchema({description:"资质有效状态中文名称:有效或无效"}),isRequired:{type:"boolean",description:"历史兼容的类型必备规则派生字段,不表示永久有效"},expired:{type:"boolean",description:"仅 daysUntilExpiry<0 时为 true"},updateTime:localDateTimeSchema()},["qualificationId","qualType","qualTypeName","permanentValid","daysUntilExpiry","validityStatus","validityStatusName","isRequired","expired","updateTime"]);
schemas.SupplierContactVO=objectSchema({contactId:idSchema(),contactName:textSchema(),contactPhoneMask:textSchema(),contactRole:textSchema(),remark:textSchema(),updateTime:localDateTimeSchema()},["contactId","contactName","contactPhoneMask","contactRole","updateTime"]);
schemas.SupplierBankAccountVO=objectSchema({accountId:idSchema(),accountName:textSchema(),accountType:stringEnum(accountTypeEnum),bankName:textSchema(),bankBranch:textSchema(),accountNoMask:textSchema(),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema(),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema(),status:stringEnum(accountStatusEnum),isDefault:stringEnum(["YES","NO"],"YES=是,NO=否"),updateTime:localDateTimeSchema()},["accountId","accountName","accountType","bankName","accountNoMask","status","isDefault","updateTime"],{description:"supplier_main 与 supplier_account 为 1:N;结算字段均来自当前 supplier_account;同一供应商的未删除账户中最多一个 isDefault=YES,由原子切换事务和数据库唯一约束共同保证;不返回候选值"});
schemas.SupplierBankAccountDetailVO=objectSchema({accountId:idSchema(),accountName:textSchema(),accountType:stringEnum(accountTypeEnum),bankName:textSchema(),bankBranch:textSchema(),accountNoMask:textSchema(),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema(),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema(),status:stringEnum(accountStatusEnum),isDefault:stringEnum(["YES","NO"],"YES=是,NO=否"),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),updateTime:localDateTimeSchema()},["accountId","accountName","accountType","bankName","accountNoMask","status","isDefault","updateTime"],{description:"单项详情独立严格 Schema;结算字段来自当前账户;proofFileUrls 仅具备 supplier:account:proof:read 时返回,否则省略"});
schemas.InitialAccountSummaryVO=objectSchema({accountId:idSchema(),accountNoMask:textSchema(),status:stringEnum(["DRAFT","PENDING","ACTIVE","DISABLED"])},["accountId","accountNoMask","status"]);
schemas.SupplierWriteResultVO=objectSchema({supplierId:idSchema(),supplierNo:textSchema({maxLength:150,nullable:true}),status:lifecycleStatusSchema(statusEnum),onboardingStage:stringEnum(onboardingStageEnum),initialAccounts:arraySchema(ref("InitialAccountSummaryVO")),updateTime:localDateTimeSchema()},["supplierId","status","onboardingStage","initialAccounts","updateTime"]);
schemas.SupplierListItemVO=objectSchema({supplierId:idSchema(),supplierNo:textSchema({maxLength:150,nullable:true}),fullName:textSchema(),shortName:textSchema(),types:arraySchema(objectSchema({typeCode:typeCodeSchema(),typeName:textSchema()},["typeCode","typeName"]),{description:"从 supplier_type_rel 按 supplier_id 查询出的供应商类型集合;前端按 supplier_type 翻译 typeCode",example:[{typeCode:"HOTEL",typeName:"酒店"},{typeCode:"RESTAURANT",typeName:"餐厅"},{typeCode:"SCENIC",typeName:"景区"}]}),status:lifecycleStatusSchema(statusEnum),creditLevel:stringEnum(["A","B","C","D"]),totalScore:{type:"number",nullable:true},activeAccountCount:{type:"integer",minimum:0},createTime:localDateTimeSchema(),updateTime:localDateTimeSchema()},["supplierId","fullName","types","status","creditLevel","totalScore","activeAccountCount","createTime","updateTime"],{description:"列表返回供应商基础信息、类型、状态、信用及账户统计;前端按 supplier_lifecycle_status 翻译 status"});
schemas.SupplierBasicInfoVO=objectSchema({supplierId:idSchema(),supplierNo:textSchema({maxLength:150,nullable:true}),fullName:textSchema(),shortName:textSchema(),tax_no:textSchema({description:"统一社会信用代码脱敏值"}),legalRepresentative:textSchema(),contactPhoneMask:textSchema(),establishDate:localDateSchema(),registeredCapital:textSchema(),businessScope:textSchema(),staffScale:stringEnum(["LT50","R50_200","R200_500","GT500"]),mainCooperation:textSchema(),status:lifecycleStatusSchema(statusEnum),creditLevel:stringEnum(["A","B","C","D"]),totalScore:{type:"number",nullable:true},types:arraySchema(objectSchema({typeCode:typeCodeSchema(),typeName:textSchema()},["typeCode","typeName"]),{description:"从 supplier_type_rel 按 supplier_id 查询出的供应商全部类型;前端按 supplier_type 翻译 typeCode",example:[{typeCode:"HOTEL",typeName:"酒店"},{typeCode:"RESTAURANT",typeName:"餐厅"},{typeCode:"SCENIC",typeName:"景区"}]}),contacts:arraySchema(ref("SupplierContactVO")),qualifications:arraySchema(ref("SupplierQualificationVO")),updateTime:localDateTimeSchema()},["supplierId","fullName","status","creditLevel","types","contacts","qualifications","updateTime"],{description:"基本信息独立返回;不包含账号、isRelatedParty 或 approveNote;前端按 supplier_lifecycle_status 翻译 status"});
schemas.SupplierAccountInfoVO=objectSchema({supplierId:idSchema(),bankAccounts:arraySchema(ref("SupplierBankAccountVO"),{description:"账号仅返回 mask;结算与开票口径随具体账户返回"}),updateTime:localDateTimeSchema()},["supplierId","bankAccounts","updateTime"],{description:"账号信息独立返回;不包含基本信息或往来数据"});
schemas.SupplierResourceRelationVO=objectSchema({relationId:idSchema(),supplierId:idSchema(),supplierNo:textSchema({maxLength:150,nullable:true}),supplierName:textSchema(),resourceModule:resourceModuleSchema(),moduleName:textSchema(),resourceId:idSchema(),resourceName:textSchema(),requiredTypeCode:requiredTypeCodeSchema(),requiredTypeName:textSchema(),remark:textSchema({maxLength:500,nullable:true}),available:{type:"boolean"},unavailableReasons:arraySchema(textSchema()),createTime:localDateTimeSchema(),updateTime:localDateTimeSchema()},["relationId","supplierId","supplierName","resourceModule","moduleName","resourceId","resourceName","requiredTypeCode","requiredTypeName","available","unavailableReasons","createTime","updateTime"],{description:"supplier_resource_rel 查询模型;名称均在查询时组装,不在关联表冗余保存。VEHICLE 的 moduleName 固定为车队管理-车队管理"});
schemas.SupplierResourceReassignRequest=objectSchema({supplierId:idSchema(),requiredTypeCode:requiredTypeCodeSchema(),remark:textSchema({maxLength:500}),expectedCurrentSupplierId:idSchema("当前关系的供应商 ID;资源未关联时不传"),expectedRelationUpdateTime:localDateTimeSchema({description:"当前关系更新时间;资源未关联时不传"}),changeReason:textSchema({minLength:1,maxLength:500})},["supplierId","changeReason"],{description:"资源侧设置或显式改绑供应商;已有关系时 expectedCurrentSupplierId 与 expectedRelationUpdateTime 必填"});
schemas.SupplierResourceUnbindRequest=objectSchema({expectedCurrentSupplierId:idSchema("当前有效关系的供应商 ID"),expectedRelationUpdateTime:localDateTimeSchema({description:"当前有效关系更新时间"}),changeReason:textSchema({minLength:1,maxLength:500})},["expectedCurrentSupplierId","expectedRelationUpdateTime","changeReason"],{description:"资源侧解除供应商;服务端按 resourceModule + resourceId 定位关系并校验预期值"});
schemas.ApprovalCommandResultVO=objectSchema({approvalLogId:idSchema(),requestNo:textSchema({minLength:1,maxLength:128}),provider:stringEnum(approvalProviderEnum,"本期固定 LOCAL_AUTO;由服务端选择,客户端不可传"),approvalStatus:stringEnum(approvalStatusEnum),spNo:textSchema({nullable:true,description:"LOCAL_AUTO 必须为空;后续 WECOM 才可返回"}),spStatus:rawSpStatusSchema({nullable:true}),syncStatus:stringEnum(syncStatusEnum,"编排状态;LOCAL_AUTO 正常完成为 APPLIED"),submittedAt:localDateTimeSchema({nullable:true}),finishedAt:localDateTimeSchema({nullable:true})},["approvalLogId","requestNo","provider","approvalStatus","syncStatus"]);
schemas.BankAccountSubmitResultVO=objectSchema({accountId:idSchema(),approvalLogId:idSchema(),requestNo:textSchema({minLength:1,maxLength:128}),provider:stringEnum(approvalProviderEnum,"本期固定 LOCAL_AUTO;由服务端选择,客户端不可传"),approvalStatus:stringEnum(approvalStatusEnum),spNo:textSchema({nullable:true,description:"LOCAL_AUTO 必须为空;后续 WECOM 才可返回"}),spStatus:rawSpStatusSchema({nullable:true}),syncStatus:stringEnum(syncStatusEnum,"编排状态;LOCAL_AUTO 正常完成为 APPLIED"),accountStatus:stringEnum(["PENDING","ACTIVE"],"PENDING 仅表示审批或结果应用处理中,不是可编辑草稿;LOCAL_AUTO 正常完成为 ACTIVE"),isDefault:stringEnum(["NO"],"NO=否;新账户固定非默认"),submittedAt:localDateTimeSchema({nullable:true}),finishedAt:localDateTimeSchema({nullable:true})},["accountId","approvalLogId","requestNo","provider","approvalStatus","syncStatus","accountStatus","isDefault"],{description:"批量新增收款账户的单项结果;响应列表与请求 accounts 顺序一致,每个 accountId 独立审批,新账户固定非默认"});
schemas.ArchiveResultVO=objectSchema({supplierId:idSchema(),status:{type:"string",enum:["ARCHIVED"]},clearanceRequestId:textSchema(),checkedAt:localDateTimeSchema(),ledgerRevision:textSchema(),updateTime:localDateTimeSchema()},["supplierId","status","clearanceRequestId","checkedAt","ledgerRevision","updateTime"]);
schemas.SupplierApprovalRecordVO=objectSchema({changeLogId:idSchema("supplier_change_log.change_log_id;一条返回记录对应一条变更留痕"),supplierId:idSchema(),approvalLogId:Object.assign(idSchema("关联审批日志 ID;无需审批的直接留痕为空"),{nullable:true}),operationType:stringEnum(["CREATE","UPDATE","ENABLE","DISABLE","DELETE"]),targetType:textSchema({minLength:1,maxLength:32}),targetId:Object.assign(idSchema("变更对象 ID;主体级记录可为空"),{nullable:true}),fieldName:textSchema({minLength:1,maxLength:64}),oldValueMasked:textSchema({maxLength:500,nullable:true}),newValueMasked:textSchema({maxLength:500,nullable:true}),changeReason:textSchema({maxLength:500,nullable:true}),status:Object.assign(lifecycleStatusSchema(statusEnum),{nullable:true,description:"该条 supplier_change_log 产生时的供应商生命周期快照;前端按 supplier_lifecycle_status 翻译"}),operatorId:Object.assign(idSchema("记录产生时的可信管理员 ID;系统动作可为空"),{nullable:true}),createTime:localDateTimeSchema()},["changeLogId","supplierId","operationType","targetType","fieldName","createTime"],{description:"供应商审批记录分页项;唯一事实来源为 supplier_change_log。不得联表补充审批意见、企微节点、spNo/provider,也不得返回敏感明文、密文或候选快照。"});
schemas.QualificationRuleVO=objectSchema({ruleId:idSchema(),typeCode:textSchema(),qualType:textSchema(),isRequired:{type:"boolean"},warningDays:{type:"integer",minimum:0,nullable:true},sortOrder:{type:"integer"},updateTime:localDateTimeSchema()},["ruleId","typeCode","qualType","isRequired","sortOrder","updateTime"]);
schemas.WeComApprovalSubmitResultVO=objectSchema({spNo:textSchema({minLength:1,maxLength:64}),applyUserName:textSchema({maxLength:500}),acceptedAt:localDateTimeSchema()},["spNo","acceptedAt"]);
schemas.ExistingApprovalStatusVO=objectSchema({spNo:textSchema({minLength:1,maxLength:64}),spStatus:{type:"integer",format:"int32"},spName:textSchema(),templateId:{nullable:true},applyTime:{nullable:true},applyUserId:textSchema(),applyUserName:textSchema(),applyData:textSchema()},["spNo","spStatus"],{description:"现有 InternalApprovalController#getApprovalStatus 线上 wire 结构;ApprovalFeignClient 当前以 Result<Map<String,Object>> 接收,Supplier 适配器不得修改旧方法签名,须将其显式映射为内部 WeComApprovalStatusVO(spStatus 转 String)"});
schemas.WeComApprovalStatusVO=objectSchema({spNo:textSchema(),templateId:textSchema(),spStatus:rawSpStatusSchema(),applicantUserId:textSchema(),applicantName:textSchema(),applyTime:localDateTimeSchema(),finishedAt:localDateTimeSchema({nullable:true}),currentLevelNo:{type:"integer",nullable:true}},["spNo","templateId","spStatus","applicantUserId","applyTime"]);
schemas.ApprovalSnapshotAcceptVO=objectSchema({accepted:{type:"boolean"},processStatus:stringEnum(["RECEIVED","APPLIED","IGNORED","FAILED"]),sourceRevision:{type:"integer",format:"int64"},detailDigest:textSchema(),businessApplied:{type:"boolean"},message:textSchema()},["accepted","processStatus","sourceRevision","detailDigest","businessApplied"]);
return schemas;
}
function requestSchemaForEndpoint(endpoint){
const mapping={
"SUP-ADM-003":"SupplierDraftUpsertRequest","SUP-ADM-004":"SupplierUpdateRequest","SUP-ADM-007":"SupplierSubmitRequest","SUP-ADM-035":"SupplierBankAccountBatchCreateRequest","SUP-ADM-049":"SupplierResourceReassignRequest","SUP-ADM-050":"SupplierResourceUnbindRequest"
};
return mapping[endpoint.id]?ref(mapping[endpoint.id]):null;
}
const bodylessWriteOperationIds=new Set(["SUP-ADM-010","SUP-ADM-041"]);
function queryParameter(name,schema,required,description){return {name:name,in:"query",required:!!required,schema:schema,description:description};}
function operationParameters(endpoint){
const params=[]; let match; const matcher=/\{([^}]+)\}/g;
while((match=matcher.exec(endpoint.path))!==null){
const pathName=match[1];
const pathSchema=pathName==="spNo"?textSchema({minLength:1,maxLength:64}):(pathName==="typeCode"?typeCodeSchema():(pathName==="resourceModule"?resourceModuleSchema():idSchema()));
const pathDescription=pathName==="spNo"?"企业微信审批单号":(pathName==="typeCode"?"平台字典 supplier_type 的启用 dictValue":(pathName==="resourceModule"?resourceModuleDescription:"Snowflake Long 的 JSON String"));
params.push({name:pathName,in:"path",required:true,schema:pathSchema,description:pathDescription});
}
const page=[queryParameter("page",{type:"integer",minimum:1,default:1},false),queryParameter("pageSize",{type:"integer",minimum:1,maximum:100,default:20},false),queryParameter("sortBy",textSchema({maxLength:64}),false,"仅服务端白名单"),queryParameter("sortDirection",stringEnum(["ASC","DESC"]),false)];
const add=function(items){items.forEach(function(item){params.push(item);});};
if(endpoint.id==="SUP-ADM-001") add([queryParameter("keyword",textSchema({maxLength:500,description:"包含式模糊查询 supplierNo/fullName/shortName;服务端 trim 并转义 LIKE 元字符;taxNo 仅规范化后精确匹配"}),false),queryParameter("status",lifecycleStatusSchema(["DRAFT","VETTING","ACTIVE","SUSPENDED","FROZEN","BLACKLIST","ARCHIVED"]),false,"前端筛选项来自 supplier_lifecycle_status,提交 dictValue"),queryParameter("typeCode",typeCodeSchema(),false,"按供应商类型编码精确筛选;筛选 types 集合中包含该 typeCode 的供应商,关联关系来自 supplier_type_rel;返回的 types 包含供应商的全部类型;前端筛选项来自 supplier_type,提交 dictValue;"+typeCodeDescription),queryParameter("creditLevel",stringEnum(["A","B","C","D"]),false),queryParameter("creatorId",idSchema("创建人管理员 ID;按 supplier_main.created_by 精确筛选"),false)].concat(page.slice(0,2)));
if(endpoint.id==="SUP-ADM-043") add([queryParameter("keyword",textSchema({maxLength:500,description:"包含式模糊查询 supplierNo/fullName/shortName;服务端 trim 并转义 LIKE 元字符"}),false),queryParameter("status",lifecycleStatusSchema(["DRAFT","VETTING","ACTIVE","SUSPENDED","FROZEN","BLACKLIST","ARCHIVED"]),false,"平台字典 supplier_lifecycle_status 的启用 dictValue"),queryParameter("typeCode",typeCodeSchema(),false,"平台字典 supplier_type 的启用 dictValue"),queryParameter("limit",{type:"integer",minimum:1,maximum:200,default:50},false,"最大返回条数;禁止无界全量查询")]);
if(endpoint.id==="SUP-ADM-012") add([queryParameter("approvalLogId",idSchema("关联审批日志 ID"),false),queryParameter("operationType",stringEnum(["CREATE","UPDATE","ENABLE","DISABLE","DELETE"]),false),queryParameter("targetType",textSchema({minLength:1,maxLength:32}),false),queryParameter("fieldName",textSchema({minLength:1,maxLength:64}),false),queryParameter("status",lifecycleStatusSchema(["DRAFT","VETTING","ACTIVE","SUSPENDED","FROZEN","BLACKLIST","ARCHIVED"]),false,"supplier_change_log.status 快照;前端按 supplier_lifecycle_status 翻译"),queryParameter("from",localDateTimeSchema(),false,"createTime 起点;必须与 to 同时传入"),queryParameter("to",localDateTimeSchema(),false,"createTime 终点;必须与 from 同时传入"),page[0],page[1],queryParameter("sortBy",stringEnum(["createTime","changeLogId"]),false,"仅允许 createTime/changeLogId;默认 createTime"),queryParameter("sortDirection",stringEnum(["ASC","DESC"]),false,"默认 DESC")]);
return params;
}
function dataSchemaForResponse(response){
let match=response.match(/^Result<PageResult<([A-Za-z0-9]+)>>$/); if(match) return pageSchema(ref(match[1]));
match=response.match(/^Result<List<([A-Za-z0-9]+)>>$/); if(match) return arraySchema(ref(match[1]));
match=response.match(/^Result<([A-Za-z0-9]+)>$/); if(match) return match[1]==="Void"?nullable(objectSchema({},[])):ref(match[1]);
throw new Error("未结构化的响应契约: "+response);
}
const readOnlyOperationIds=new Set([
"SUP-ADM-001","SUP-ADM-043","SUP-ADM-002","SUP-ADM-012","SUP-ADM-034",
"SUP-ADM-036","SUP-ADM-048"
]);
const externalSideEffectOperationIds=new Set([
"SUP-ADM-007","SUP-ADM-010","SUP-ADM-035"
]);
const adminPermissionRules={
"SUP-ADM-001":{allOf:["supplier:list"]},
"SUP-ADM-043":{allOf:["supplier:list"]},
"SUP-ADM-002":{allOf:["supplier:view"]},
"SUP-ADM-003":{allOf:["supplier:create"]},
"SUP-ADM-004":{allOf:["supplier:update"]},
"SUP-ADM-007":{allOf:["supplier:update","supplier:approval:submit"]},
"SUP-ADM-010":{allOf:["supplier:status:manage"]},
"SUP-ADM-011":{allOf:["supplier:delete"]},
"SUP-ADM-012":{allOf:["supplier:approval:read"]},
"SUP-ADM-034":{allOf:["supplier:view"]},
"SUP-ADM-035":{allOf:["supplier:account:manage","supplier:approval:submit"]},
"SUP-ADM-036":{allOf:["supplier:view"],conditional:[{when:"response includes proofFileUrls",allOf:["supplier:account:proof:read"],audit:"SENSITIVE_READ/READ_ACCOUNT_PROOF"}]},
"SUP-ADM-041":{allOf:["supplier:account:manage"]},
"SUP-ADM-048":{allOf:["supplier:view"]},
"SUP-ADM-049":{allOf:["supplier:update"]},
"SUP-ADM-050":{allOf:["supplier:update"]}
};
function permissionPolicyFor(endpoint){
if(endpoint.path.indexOf("/admin/")!==0) return {mode:"INTERNAL_TOKEN",header:"X-Internal-Token",adminPermissions:[]};
const rule=adminPermissionRules[endpoint.id];
if(!rule) throw new Error("管理端接口缺少权限映射: "+endpoint.id);
return Object.assign({mode:"USER_FEIGN_FAIL_CLOSED",provider:"com.hulalv.common.feign.UserFeignClient#hasPermission(Long,String)",identitySource:"trusted X-Admin-Id",authorizationSource:"PLATFORM_MENU_BUTTON",fixedRoleCheck:"FORBIDDEN",failureMode:"DENY on false/non-success/timeout/exception"},rule);
}
function guardNamesFor(endpoint){
if(endpoint.id.indexOf("SUP-WECOM-")===0) return [];
const guards=endpoint.path.indexOf("/admin/")===0?["SupplierPermissionGuard"]:["SupplierInternalAuthGuard"];
if(/^SUP-ADM-00[1-9]$/.test(endpoint.id)||["SUP-ADM-010","SUP-ADM-011","SUP-ADM-012"].indexOf(endpoint.id)>=0) guards.push("SupplierProfileGuard");
if(/^SUP-ADM-03[4-9]$/.test(endpoint.id)||/^SUP-ADM-04[0-2]$/.test(endpoint.id)) guards.push("SupplierAccountGuard");
if(["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"].indexOf(endpoint.id)>=0) guards.push("SupplierResourceGuard");
if(endpoint.id==="SUP-ADM-010") guards.push("SupplierClearanceGuard");
return guards;
}
function stateMachinePolicyFor(endpoint){
const mapping={
"SUP-ADM-003":[{machine:"Supplier",event:"CREATE_DRAFT",from:[null],to:"DRAFT"},{machine:"Account",event:"CREATE_PENDING",from:[null],to:"PENDING",cardinality:"0..N initialAccounts"}],
"SUP-ADM-007":[{machine:"Supplier",event:"SUBMIT_PROFILE",from:["DRAFT"],to:"VETTING"}],
"SUP-ADM-010":[{machine:"Supplier",event:"ARCHIVE_CLEARED",from:["SUSPENDED","BLACKLIST"],to:"ARCHIVED"}],
"SUP-ADM-035":[{machine:"Account",event:"CREATE_SUBMITTED",from:[null],to:"PENDING",phase:"PREPARE_TX",cardinality:"1..50 accounts per request"},{machine:"Account",event:"CREATE_APPROVED",from:["PENDING"],to:"ACTIVE",phase:"RESULT_TX",application:"EACH_ACCOUNT_INDEPENDENT"}]
};
return mapping[endpoint.id]||[];
}
function lockPolicyFor(endpoint,transactionKind){
if(transactionKind==="READ_ONLY"||transactionKind==="CROSS_SERVICE_FEIGN") return {mode:"NONE"};
if(endpoint.id==="SUP-ADM-035") return {mode:"LOCK4J_AND_DB",key:"supplier:aggregate:{supplierId}",resolve:"normalize every accountNo before locking; plaintext must not enter lock keys or logs",expireMs:30000,acquireTimeoutMs:3000,rowLocks:["supplier_main","existing supplier_account by normalized account digest ascending","new supplier_account by account_id ascending","new supplier_account_approval_log by approval_log_id ascending"],batchLockOrder:"normalized account digest ASC then generated accountId ASC",concurrency:"different suppliers may submit batches concurrently; batches for the same supplier serialize; C-19 database uniqueness protects cross-supplier collisions"};
if(endpoint.id==="SUP-ADM-041") return {mode:"LOCK4J_AND_DB",key:"supplier:aggregate:{supplierId}",resolve:"accountId -> non-locking supplierId lookup before aggregate lock",expireMs:30000,acquireTimeoutMs:3000,rowLocks:["supplier_main","all non-deleted supplier_account ordered by account_id ASC"],postCondition:"exactly target account is_default=YES; every other non-deleted account is_default=NO",databaseBackstop:"uk_supplier_account_one_default(default_supplier_id)"};
if(endpoint.id==="SUP-ADM-049"||endpoint.id==="SUP-ADM-050") return {mode:"LOCK4J_AND_DB",key:"supplier:resource:{resourceModule}:{resourceId}",expireMs:30000,acquireTimeoutMs:3000,rowLocks:["old and new supplier_main ordered by supplier_id ASC when applicable","current supplier_resource_rel"],databaseBackstop:"uk_resource_alive(resource_module,resource_id,active_flag)"};
if(/^SUP-ADM-03[4-9]$/.test(endpoint.id)||/^SUP-ADM-04[0-2]$/.test(endpoint.id)) return {mode:"LOCK4J_AND_DB",key:"supplier:account:{accountId}",expireMs:30000,acquireTimeoutMs:3000,rowLocks:["supplier_main","supplier_approval_log when present","supplier_account ascending"]};
return {mode:"LOCK4J_AND_DB",key:"supplier:aggregate:{supplierId}",resolve:"qualification target -> non-locking supplierId lookup when path lacks supplierId",expireMs:30000,acquireTimeoutMs:3000,rowLocks:["supplier_main","supplier_approval_log when present","child rows ascending"]};
}
function idempotencyPolicyFor(endpoint){
if(endpoint.id.indexOf("SUP-WECOM-")===0) return {mode:"NONE",reason:"read-only Feign lookup by spNo"};
if(readOnlyOperationIds.has(endpoint.id)) return {mode:"NONE",reason:"read-only"};
if(endpoint.id==="SUP-ADM-035") return {mode:"REDIS_SHORT_WINDOW",annotation:"@Idempotent",implementation:"hl-starter-protection/IdempotentAspect",key:"trusted adminId + supplierId + sha256(canonical ordered normalizedAccountNo digest list) + requestDigest",sameRequestReplay:"RETURN_OR_RECOVER_ORIGINAL_ORDERED_RESULT_LIST",differentBatchForSameSupplier:"ALLOWED_AFTER_CURRENT_LOCK_RELEASE",supplierOnlyKey:"FORBIDDEN",sensitivePlaintextInKey:"FORBIDDEN",correctnessFallback:["supplier aggregate Lock4j + deterministic batch row-lock order","batch-internal normalized account uniqueness","C-19 database unique constraint","per-account approval requestNo","per-account result-applier CAS"]};
return {mode:"REDIS_SHORT_WINDOW",annotation:"@Idempotent",implementation:"hl-starter-protection/IdempotentAspect",key:"operation + trusted actor + business object/request parameter digest",sensitivePlaintextInKey:"FORBIDDEN",correctnessFallback:["aggregate Lock4j + row lock","state machine","expectedUpdateTime/CAS","database unique constraints","approval requestNo/spNo"]};
}
function operationCodegenPolicy(endpoint){
const clientOnly=endpoint.id.indexOf("SUP-WECOM-")===0;
const readOnly=readOnlyOperationIds.has(endpoint.id);
const crossServiceRead=endpoint.id==="SUP-ADM-048"||endpoint.id==="SUP-ADM-049";
const callback=false;
const externalSideEffect=externalSideEffectOperationIds.has(endpoint.id);
const transactionKind=clientOnly?"CROSS_SERVICE_FEIGN":readOnly?"READ_ONLY":callback?"CALLBACK_ATOMIC":externalSideEffect?"EXTERNAL_THREE_PHASE":"LOCAL_ATOMIC";
const transaction=transactionKind==="READ_ONLY"
?{kind:transactionKind,annotation:"@Transactional(readOnly = true)",externalCalls:crossServiceRead?"READ_ONLY_FEIGN_ALLOWED_AFTER_LOCAL_SUPPLIER_CHECK; NO CROSS_SCHEMA SQL":"FORBIDDEN"}
:transactionKind==="LOCAL_ATOMIC"
?{kind:transactionKind,annotation:"@Transactional(rollbackFor = Exception.class)",writes:"business + BUSINESS_CHANGE + DOMAIN_EVENT atomically",externalCalls:crossServiceRead?"VEHICLE read preflight must finish before local transaction; no cross-schema SQL":"FORBIDDEN"}
:transactionKind==="EXTERNAL_THREE_PHASE"
?{kind:transactionKind,phases:["PREPARE_TX","NO_TX_EXTERNAL","RESULT_TX"],prepare:"FOR UPDATE + Guard + approval/requestNo/business in-flight state",external:"Coordinator without @Transactional and without DB row lock",result:"public proxy transaction CAS approval/business result",externalCalls:"AFTER_PREPARE_COMMIT_ONLY"}
:transactionKind==="CALLBACK_ATOMIC"
?{kind:transactionKind,annotation:"@Transactional(rollbackFor = Exception.class)",writes:"APPROVAL_SYNC + normalized detail + complete result apply + BUSINESS_CHANGE + DOMAIN_EVENT",externalCalls:"detail fetch must complete before transaction"}
:{kind:transactionKind,generate:"Supplier Port + existing ApprovalFeignClient/common DTO/fallback + InternalApprovalController provider delegation + caller/provider contract tests; reuse status/revoke paths"};
return {
guard:{classes:guardNamesFor(endpoint),order:["trusted actor identity","x-hl-permissions via UserFeignClient fail-closed","ownership and soft-delete","state transition","expectedUpdateTime","field and uniqueness rules","external prerequisite fail-closed"],failureSideEffects:"ZERO"},
stateMachine:stateMachinePolicyFor(endpoint),
transaction:transaction,
lock:lockPolicyFor(endpoint,transactionKind),
idempotency:idempotencyPolicyFor(endpoint)
};
}
function rootCodegenPolicy(){
return {
version:"supplier-frontend-v2.1-20260825",
failOnMissingWritePolicy:true,
failOnTodoOrNoopImplementation:true,
serverOperationCount:16,
outboundClientOperationCount:0,
wecomProviderExtensionOperationCount:0,
wecomReusedOperationCount:0,
referencePatterns:[
"refund/guard/RefundStateGuard: explicit transition whitelist",
"house|assignment/statemachine/*Helper: COLA same-state invalid detection, explicit self-loop, CAS persistence",
"material/service/MaterialRefService and fleet/dispatch/GroupDispatchService: proxy @Lock4j + transaction + lock-time reread",
"hl-starter-protection/IdempotentAspect: Redis short-window duplicate request rejection",
"TransactionalEventListener(AFTER_COMMIT) and Resource approval polling: external effects after commit with requestNo/spNo recovery"
],
packages:{controllerAdmin:"resource.supplier.controller.admin",controllerInternal:"resource.supplier.controller.internal",dto:"resource.supplier.dto",vo:"resource.supplier.vo",entity:"resource.supplier.entity",mapper:"resource.supplier.mapper",service:"resource.supplier.service",guard:"resource.supplier.service.guard",stateMachine:"resource.supplier.service.statemachine",approval:"resource.supplier.service.approval",account:"resource.supplier.service.account",lock:"resource.supplier.service.lock",integration:"resource.supplier.integration",job:"resource.supplier.job",enums:"resource.supplier.enums",constant:"resource.supplier.constant",errorcode:"resource.supplier.errorcode"},
softDelete:{scope:"ALL_SUPPLIER_BUSINESS_DELETE_OPERATIONS",column:"deleted_at",deleteValue:"CURRENT_TIMESTAMP",activePredicate:"deleted_at IS NULL",physicalDelete:"FORBIDDEN",collectionReconciliation:{operationId:"SUP-ADM-004",omittedCollection:"UNCHANGED",providedCollection:"FULL_CURRENT_SNAPSHOT",missingExistingItem:"SOFT_DELETE",emptyCollection:{types:"REJECT_MIN_ONE",contacts:"SOFT_DELETE_ALL",qualifications:"SOFT_DELETE_ALL",contracts:"SOFT_DELETE_ALL",evaluations:"SOFT_DELETE_ALL"},tables:["supplier_type_rel","supplier_contact","supplier_qualification","supplier_contract","supplier_evaluation"]},queryRules:["filter supplier_main and every joined supplier child table independently","MyBatis-Plus @TableLogic for standard wrappers","explicit deleted_at IS NULL for XML, native SQL, JOIN, count, existence, eligibility and internal queries"],evidenceTables:["supplier_approval_log","supplier_change_log","supplier_account_approval_log","supplier_account_change_log"],evidenceDeletion:"FORBIDDEN",statusOperationsNotDelete:["ARCHIVED","DISABLED","TERMINATED"]},
approvalRecords:{operationId:"SUP-ADM-012",sourceTable:"supplier_change_log",readMode:"ONE_RESULT_PER_CHANGE_LOG_ID",crossTableEnrichment:"FORBIDDEN",appendOnly:true,approvalLogIdNullableMeaning:"DIRECT_OR_DRAFT_CHANGE_WITHOUT_APPROVAL",filters:["supplierId","approvalLogId","operationType","targetType","fieldName","status","from+to"],defaultSort:["create_time DESC","change_log_id DESC"],sortWhitelist:["createTime","changeLogId"],sensitiveOutput:"MASKED_ONLY",forbiddenOutput:["opinion","spNo","provider","candidateSnapshot","plaintext","ciphertext"]},
accountCardinality:{relation:"supplier_main 1:N supplier_account",createOperationId:"SUP-ADM-035",oneRequestCreatesAndSubmits:"BATCH_1_TO_50_NEW_ACCOUNTS",requestField:"accounts",draftPhase:"FORBIDDEN",separateSubmitOperation:"FORBIDDEN",batchValidationFailure:"ROLLBACK_ALL_ZERO_WRITE",batchInternalDuplicateKey:"normalizedAccountNo",responseOrder:"SAME_AS_REQUEST_ACCOUNTS",existingAccountOverwrite:"FORBIDDEN",allowedConcurrentStatuses:["PENDING","ACTIVE","DISABLED"],pendingMeaning:"APPROVAL_OR_RESULT_APPLICATION_IN_PROGRESS_READ_ONLY",normalLocalAutoResult:"ACTIVE",globalAccountNoUniqueness:"C-19",defaultAccountPerSupplier:"AT_MOST_ONE_NON_DELETED",defaultSwitchOperationId:"SUP-ADM-041",defaultSwitch:"CLEAR_OTHERS_THEN_SET_TARGET_IN_ONE_TRANSACTION",defaultDatabaseBackstop:{strategy:"MYSQL_GENERATED_NULLABLE_COLUMN_UNIQUE_INDEX",generatedColumn:"default_supplier_id = CASE WHEN is_default = 'YES' AND deleted_at IS NULL THEN supplier_id ELSE NULL END",uniqueIndex:"uk_supplier_account_one_default(default_supplier_id)"},newAccountDefault:"NO",proofFileUrls:{retained:true,maxItemsPerAccount:20},independentApprovalPerAccount:true,postActivationDeleteOperation:"NONE",postActivationDisableOperation:"NONE",historicalDisabledReadOnly:true},
resourceRelationship:{table:"supplier_resource_rel",cardinality:"supplier 1:N resources; resource 0..1 active supplier",ownership:"RESOURCE_AND_FLEET_MODULES_MAINTAIN_SUPPLIER",supplierDetailTab:"NONE",supplierSideResourceOperations:"NONE",operations:["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"],supplierSelectionOperations:["SUP-ADM-001","SUP-ADM-043"],resourceModules:resourceModuleValues,vehicleMenuName:"车队管理-车队管理",vehicleRead:"FLEET_INTERNAL_READ_ONLY_AUDIT_BEFORE_IMPLEMENTATION",crossSchemaSql:"FORBIDDEN",silentOverwrite:"FORBIDDEN",databaseBackstop:"uk_resource_alive(resource_module,resource_id,active_flag)",audit:"supplier_change_log field_name=resourceRelation"},
approvalProvider:{spi:"SupplierApprovalProvider",currentProvider:"LOCAL_AUTO",currentImplementation:"LocalAutoApprovalProvider",selection:"SERVER_CONFIGURATION_ONLY",normalizedStatuses:["PENDING","APPROVED","REJECTED","CANCELLED","FAILED"],resultApplier:"SupplierApprovalResultApplier",localAutoRules:["persist approval row and immutable candidate before decision","return explicit systemDecision=true APPROVED","never fabricate spNo/templateId/WeCom user/department/level/opinion/raw status/callback","apply only through the common result applier"],futureProvider:"WECOM"},
wecomIntegration:{delivery:"FUTURE_WECOM_ONLY_EXCLUDED_FROM_CURRENT_CODEGEN",feign:"com.hulalv.common.feign.ApprovalFeignClient",fallback:"com.hulalv.common.feign.ApprovalFeignFallbackFactory",provider:"hl-user-service InternalApprovalController",apiClient:"hl-user-service WechatApiClient",commonDtos:"hl-common/hl-common-core",addMethods:["submitSupplierApproval","getSupplierApprovalDetail"],reuseMethods:["getApprovalStatus","revokeApproval"],callback:"hl-user-service -> POST /internal/resource/supplier/approval-snapshots",compatibility:"reuse current approval table, state machines and SupplierApprovalResultApplier",autoApprove:"LOCAL_AUTO is an explicit current provider, not fake WECOM"},
authorization:{provider:"com.hulalv.common.feign.UserFeignClient#hasPermission(Long,String)",source:"hl-common/hl-common-feign",requiredFor:"every /admin/supplier operation",configuration:"platform menu/button permission codes",fixedRoleCheck:"FORBIDDEN",failureMode:"DENY on false, non-success Result, timeout or exception",identity:"adminId only from trusted X-Admin-Id"},
executionOrder:["authenticate and authorize","enter @Idempotent Redis short-window guard for management writes","PREPARE_TX: lock, Guard, state machine, requestNo, approval row and immutable candidate","commit local facts","call configured SupplierApprovalProvider without transaction; current LOCAL_AUTO returns explicit system approval","RESULT_TX: common result applier locks and revalidates","apply candidate + approval/application status + BUSINESS_CHANGE/DOMAIN_EVENT atomically","future WECOM callback/poll/reconcile normalize into the same result applier"],
databaseLockOrder:["supplier_main","supplier_approval_log","supplier_account ascending","qualification/contact/contract/type_rel ascending","supplier_resource_rel by module/resource/id ascending","append-only supplier_change_log"],
stateMachines:{
Supplier:[
{from:["DRAFT"],event:"SUBMIT_PROFILE",to:"VETTING"},
{from:["VETTING"],event:"PROFILE_APPROVED",to:"ACTIVE"},
{from:["VETTING"],event:"PROFILE_REJECTED",to:"DRAFT"},
{from:["ACTIVE"],event:"SUSPEND_APPROVED",to:"SUSPENDED"},
{from:["SUSPENDED"],event:"RESUME_APPROVED",to:"ACTIVE"},
{from:["ACTIVE"],event:"FREEZE_APPROVED",to:"FROZEN"},
{from:["FROZEN"],event:"UNFREEZE_APPROVED",to:"ACTIVE"},
{from:["ACTIVE","SUSPENDED","FROZEN"],event:"BLACKLIST_APPROVED",to:"BLACKLIST"},
{from:["BLACKLIST"],event:"UNBLACKLIST_APPROVED",to:"SUSPENDED"},
{from:["SUSPENDED","BLACKLIST"],event:"ARCHIVE_CLEARED",to:"ARCHIVED"}
],
Account:[
{from:[null],event:"CREATE_SUBMITTED",to:"PENDING"},
{from:["PENDING"],event:"CREATE_APPROVED",to:"ACTIVE"}
]
},
proxyRules:{transactionAndLockMethods:"public method on separate Spring bean",forbidThisSelfInvocation:true,controllerMayInjectMapper:false,transactionExecutorMayCallExternalPort:false},
correctnessBackstops:["status/update_time CAS","tax_no unique","account_no_enc unique","request_no unique and nullable sp_no unique","provider + normalized approval_status + apply_status always present","LOCAL_AUTO has no fabricated WECOM fields","source_event_key unique when external events exist","operation_type audit for CREATE/UPDATE/ENABLE/DISABLE/DELETE and READ_APPROVAL_OPINION","future WECOM COMPLETE requires controlled organization snapshots"]
};
}
const wecomImplementationRules={};
function buildOpenApiSpec(){
const included=endpoints.filter(function(e){return e.delivery!=="本期不做"&&e.delivery!=="二期"&&e.delivery!=="后续 WECOM 对接";});
const pathNamingErrors=[];
const includedOperationIds=included.map(function(endpoint){return operationIdFor(endpoint);});
if(new Set(includedOperationIds).size!==includedOperationIds.length) pathNamingErrors.push("operationId 必须全局唯一");
Object.keys(pathNamingKinds).forEach(function(kind){
pathNamingKinds[kind].forEach(function(id){
const endpoint=included.find(function(item){return item.id===id;});
if(!endpoint||!endpoint.path.endsWith("/"+kind)) pathNamingErrors.push(id+" 的路径必须以 /"+kind+" 结尾");
});
});
if(pathNamingErrors.length) throw new Error("Path naming validation failed: "+pathNamingErrors.join("; "));
const currentSchemas=buildOpenApiSchemas();
["ControlledAccountFormFact","ProfileCreateApprovalFormFacts","AccountCreateApprovalFormFacts","AccountChangeApprovalFormFacts","StatusChangeApprovalFormFacts","WeComApprovalFormFacts","WeComApprovalSubmitRequest","SupplierApprovalSnapshotDTO","WeComApprovalSubmitResultVO","ExistingApprovalStatusVO","WeComApprovalStatusVO","ApprovalSnapshotAcceptVO"].forEach(function(name){delete currentSchemas[name];});
const numericErrors={};
errorRows.forEach(function(row){numericErrors[row[0]]=row[1];});
const tagDescriptions={};
apiSections.forEach(function(section){tagDescriptions[section.title]=section.description;});
const spec={
openapi:"3.0.3",
info:{title:"HL 供应商模块 API",version:"2.1-frontend-20260825",description:"前端联调版运行时契约;列表、分页、新增、修改、删除和详情类接口路径使用固定动作后缀。所有 Supplier 业务删除统一写 deleted_at,禁止物理 DELETE;默认查询、JOIN、统计、存在性、资格和 internal 查询均逐表过滤 deleted_at IS NULL。本期生成完整审批表、LOCAL_AUTO Provider、标准化结果、候选快照、统一结果应用器、审计、权限 Guard、状态机、事务、锁和短窗防重。供应商更新使用独立请求:主体标量增量补全,类型关联、联系人、资质、合同和评价集合未传时保持不变,传入时按完整快照对账并将缺失项软删除;审批中及审批后锁定 fullName/taxNo。供应商审批记录接口只分页读取 supplier_change_log,一条结果对应一条 change_log_id,不联表拼装企微审批详情。账户信息按 supplierId 返回全部有效账户集合,路径固定为 /admin/supplier/items/{supplierId}/account-info/list。supplier_main 与 supplier_account 为 1:N;新增接口一次接收 accounts 1..50 项,批量创建账户并为每项生成独立 accountId、approvalLogId 和 requestNo 后直接提交 ACCOUNT_CREATE 审批;批内规范化账号不得重复,任一请求校验失败整批零写入。PENDING 仅表示审批或结果应用处理中,不是可编辑草稿。每个账户自己的 proofFileUrls 保留 JSON 数组且最多 20 项,所有新账户固定非默认。每个供应商的未删除账户中最多一个默认账户;默认切换在锁定主体和全部未删除账户后,原子清除旧默认并设置目标默认,由 MySQL 可空生成列唯一索引兜底。账户新增成功后不提供删除或停用入口,只能查询和切换默认;历史 DISABLED 账户只读。清账归档无请求体,由服务端锁行重读并生成审计上下文;不生成供应商注册审批撤销接口、账户草稿编辑接口、账户独立提交接口、账户删除接口、账户停用审批接口和重复主体预检接口;供应商类型统一使用平台字典 supplier_type。资源关联独立使用 supplier_resource_rel,不并入供应商详情或基本信息更新;资源与车队模块在各自资源页面复用供应商列表并通过 SUP-ADM-048~050 查询、设置/改绑或解除供应商。VEHICLE 显示为车队管理-车队管理,只允许 Fleet 内部只读校验。WECOM 出站/回调/手工对账只作后续契约保留,不进入本期 paths/components,且不得伪造企微字段。",license:{name:"HL Internal Proprietary",url:"urn:hl:license:internal-proprietary"},"x-hl-delivery":"前端联调版 · 15 个可联调,1 个失败关闭"},
servers:[{url:"/",description:"统一经 Gateway;Internal 仅服务间调用"}],
tags:apiSections.filter(function(section){return included.some(function(endpoint){return endpoint.group===section.title;});}).map(function(section){return {name:section.title,description:tagDescriptions[section.title]||"供应商模块接口契约。"};}),
paths:{},
components:{
securitySchemes:{adminBearer:{type:"http",scheme:"bearer",bearerFormat:"JWT"},internalToken:{type:"apiKey",in:"header",name:"X-Internal-Token"}},
schemas:currentSchemas
},
"x-hl-dictionaries":{
loadMode:"ON_DEMAND",
endpoints:{supplierType:"GET /admin/dict/data/supplier_type",supplierLifecycleStatus:"GET /admin/dict/data/supplier_lifecycle_status"},
bindings:[
{dictType:"supplier_type",fields:["typeCode","types[].typeCode"],labelField:"dictLabel",valueField:"dictValue"},
{dictType:"supplier_lifecycle_status",fields:["status"],labelField:"dictLabel",valueField:"dictValue"}
],
rules:["supplier pages load the two dictionaries on demand","frontend options and labels come from active dictionary data","business requests and responses use dictValue only","never submit dictLabel or dictDataId","status transition targets remain constrained by the supplier state machine"]
},
"x-hl-excluded-operations":endpoints.filter(function(e){return e.delivery==="本期不做"||e.delivery==="二期"||e.delivery==="后续 WECOM 对接";}).map(function(e){return {id:e.id,method:e.method,path:e.path,delivery:e.delivery};}),
"x-hl-path-naming-convention":{suffixes:{list:"查询列表",page:"分页查询",add:"添加/创建/新增",update:"修改/更改/编辑/设置",del:"删除/软删除",view:"详情"},rule:"适用接口的 HTTP 路径必须以对应动作词结尾;其他提交、归档和校验接口保留专属动作路径"},
"x-hl-soft-delete-policy":{scope:"所有 Supplier 业务删除操作,包括更新接口集合差异产生的删除",column:"deleted_at",write:"CURRENT_TIMESTAMP",defaultPredicate:"deleted_at IS NULL",physicalDelete:"FORBIDDEN",collectionRule:"SUP-ADM-004 集合字段未传不处理;传入即为完整快照,存量缺失项软删除;联系人、资质、合同、评价空数组软删除全部,types 空数组因至少保留一个类型而拒绝",collectionTables:["supplier_type_rel","supplier_contact","supplier_qualification","supplier_contract","supplier_evaluation"],joinRule:"supplier_main 与每个参与查询的子表分别过滤 deleted_at IS NULL",rawSqlRule:"XML/原生 SQL/JOIN/统计/存在性/资格查询必须显式过滤",evidenceRule:"审批、变更和审计证据永不删除",notDeleteActions:["ARCHIVED","DISABLED","TERMINATED"]},
"x-hl-cross-service-codegen":{vehicleRead:{required:"CONDITIONAL_WHEN_RESOURCE_MODULE_VEHICLE",delivery:"AUDIT_BEFORE_IMPLEMENTATION",modules:["hl-resource-service","hl-fleet-service","hl-common-feign when needed"],rules:["audit existing Fleet internal read contract first","do not freeze a new Fleet path in this document","X-Internal-Token required","fail closed on timeout, exception or non-success Result","no cross-schema SQL","complete read preflight before local relation write transaction"]},futureWecom:{required:false,delivery:"FUTURE_WECOM",modules:["hl-common-core","hl-common-feign","hl-user-service","hl-resource-service"],rules:["do not generate in current phase","future adapter implements SupplierApprovalProvider","reuse existing ApprovalFeignClient/fallback/status/revoke paths","normalize callbacks into SupplierApprovalResultApplier","keep old methods and callers compatible"]}},
"x-hl-required-independent-backend-tasks":[{module:"hl-gateway",change:"add /admin/supplier/** -> hl-resource-service",verification:"GatewayRouteAuditTest plus authenticated gateway acceptance for authorized and unauthorized platform permission sets"}],
"x-hl-codegen-policy":rootCodegenPolicy()
};
included.forEach(function(e){
if(!spec.paths[e.path]) spec.paths[e.path]={};
const pathParameters=operationParameters(e);
const dataSchema=dataSchemaForResponse(e.response);
const codegenPolicy=operationCodegenPolicy(e);
const operationErrorNames=e.errors.slice();
const operation={
tags:[e.group],operationId:operationIdFor(e),summary:e.name,description:e.source,
security:e.path.indexOf("/admin/")===0?[{adminBearer:[]}]:[{internalToken:[]}],parameters:pathParameters,
responses:{
"200":{description:"Result<T>:成功包络或业务错误包络;HTTP 200 仍必须检查 code/message/success,错误 data 允许 null",content:{"application/json":{schema:{oneOf:[resultSchema(dataSchema),ref("ErrorEnvelope")]}}}},
"400":{description:"请求结构或参数错误",content:{"application/json":{schema:ref("ErrorEnvelope")}}},
"401":{description:"未认证或内部 Token 无效",content:{"application/json":{schema:ref("ErrorEnvelope")}}},
"403":{description:"平台权限、Supplier 业务 Guard 或受控端口策略拒绝",content:{"application/json":{schema:ref("ErrorEnvelope")}}},
"409":{description:"并发或业务状态冲突",content:{"application/json":{schema:ref("ErrorEnvelope")}}},
"500":{description:"依赖不可用或未知错误;资金/状态判断失败关闭",content:{"application/json":{schema:ref("ErrorEnvelope")}}}
},
"x-hl-id":e.id,"x-hl-access":e.access,"x-hl-permissions":permissionPolicyFor(e),"x-hl-delivery":e.delivery,"x-hl-consumer":e.consumer,"x-hl-provider":e.id.indexOf("SUP-WECOM")===0?"hl-user-service/InternalApprovalController":"hl-resource-service","x-hl-codegen-direction":e.id.indexOf("SUP-WECOM")===0?"supplier-client-user-provider":"supplier-provider",
"x-hl-request-contract":e.request,"x-hl-response-contract":e.response,"x-hl-rules":e.rules,
"x-hl-errors":operationErrorNames.map(function(name){return {name:name,code:numericErrors[name]||null};}),
"x-hl-guard":codegenPolicy.guard,
"x-hl-state-machine":codegenPolicy.stateMachine,
"x-hl-transaction":codegenPolicy.transaction,
"x-hl-lock":codegenPolicy.lock,
"x-hl-idempotency":codegenPolicy.idempotency
};
if(e.id==="SUP-ADM-004") operation["x-hl-soft-delete"]={enabled:true,trigger:"MISSING_FROM_PROVIDED_FULL_COLLECTION_SNAPSHOT",omittedCollection:"UNCHANGED",emptyCollection:{types:"REJECT_MIN_ONE",contacts:"SOFT_DELETE_ALL",qualifications:"SOFT_DELETE_ALL",contracts:"SOFT_DELETE_ALL",evaluations:"SOFT_DELETE_ALL"},physicalDelete:"FORBIDDEN",column:"deleted_at",value:"CURRENT_TIMESTAMP",tables:["supplier_type_rel","supplier_contact","supplier_qualification","supplier_contract","supplier_evaluation"],audit:"MASKED_BUSINESS_CHANGE_REQUIRED",transaction:"ATOMIC_WITH_CREATE_AND_UPDATE"};
if(e.id==="SUP-ADM-011") operation["x-hl-soft-delete"]={enabled:true,physicalDelete:"FORBIDDEN",column:"deleted_at",value:"CURRENT_TIMESTAMP",supplierGuard:{allowedStatus:["DRAFT"],profileCreateApproval:"NEVER_SUBMITTED",externalReferences:"NONE"},initialAccountGuard:{allowedStatus:["DRAFT","PENDING"],pendingRequires:"no APPROVED approval_status and no APPLIED apply_status",forbiddenStatus:["ACTIVE"],approvedOrApplied:"REJECT_ENTIRE_REQUEST_ZERO_WRITE"},tables:["supplier_main","supplier_type_rel","supplier_contact","supplier_qualification","supplier_contract","supplier_evaluation","eligible unapproved supplier_account"],retain:["supplier_approval_log","supplier_change_log","supplier_account_approval_log","supplier_account_change_log"],accountAudit:"MASKED DELETE change log required",queryPredicate:"every participating table.deleted_at IS NULL",replay:"IDEMPOTENT_SUCCESS"};
if(e.id==="SUP-ADM-012") operation["x-hl-read-model"]={sourceTable:"supplier_change_log",rowMapping:"ONE_CHANGE_LOG_ROW_TO_ONE_RESULT",joins:"FORBIDDEN",appendOnly:true,approvalLogIdNullable:true,filters:["supplier_id","approval_log_id","operation_type","target_type","field_name","status","create_time"],pairedParameters:[["from","to"]],defaultOrder:["create_time DESC","change_log_id DESC"],sortWhitelist:["createTime","changeLogId"],sensitiveFields:"MASKED_ONLY",forbiddenFields:["opinion","spNo","provider","candidateSnapshot","plaintext","ciphertext"]};
if(e.id==="SUP-ADM-034") operation["x-hl-account-cardinality"]={relation:"supplier_main 1:N supplier_account",returnAllNonDeletedAccounts:true,defaultOrder:["is_default DESC","create_time DESC","account_id DESC"],defaultAccountPerSupplier:"AT_MOST_ONE_NON_DELETED",databaseBackstop:"uk_supplier_account_one_default(default_supplier_id)"};
if(e.id==="SUP-ADM-035") operation["x-hl-account-cardinality"]={relation:"supplier_main 1:N supplier_account",oneRequestCreatesAndSubmits:"BATCH_1_TO_50_NEW_ACCOUNTS",requestField:"accounts",batchInternalDuplicateKey:"normalizedAccountNo",batchValidationFailure:"ROLLBACK_ALL_ZERO_WRITE",response:"ORDERED_LIST_SAME_AS_REQUEST",draftPhase:"FORBIDDEN",separateSubmitOperation:"FORBIDDEN",existingAccountOverwrite:"FORBIDDEN",prepareStatus:"PENDING",pendingMeaning:"APPROVAL_OR_RESULT_APPLICATION_IN_PROGRESS_READ_ONLY",normalLocalAutoStatus:"ACTIVE",newAccountDefault:false,globalDuplicateRule:"C-19",proofFileUrls:{retained:true,maxItemsPerAccount:20},approvalScope:"EACH_ACCOUNT_ID_INDEPENDENT",idempotencyKey:"trusted adminId + supplierId + sha256(canonical ordered normalizedAccountNo digest list) + requestDigest",postActivationDeleteOperation:"NONE",postActivationDisableOperation:"NONE"};
if(e.id==="SUP-ADM-041") operation["x-hl-default-account"]={cardinality:"AT_MOST_ONE_PER_SUPPLIER_NON_DELETED",target:{ownership:"REQUIRED",deletedAt:"NULL",status:["ACTIVE"]},lock:"supplier aggregate plus every non-deleted supplier_account ordered by account_id",mutationOrder:["set is_default=NO on every other non-deleted account","set target is_default=YES"],postCondition:"target is the only non-deleted default account",idempotentWhenAlreadySoleDefault:true,databaseBackstop:{strategy:"MYSQL_GENERATED_NULLABLE_COLUMN_UNIQUE_INDEX",generatedColumn:"default_supplier_id = CASE WHEN is_default = 'YES' AND deleted_at IS NULL THEN supplier_id ELSE NULL END",uniqueIndex:"uk_supplier_account_one_default(default_supplier_id)"},constraintConflict:"409 SUPPLIER_CONCURRENT_MODIFICATION",responseTargetIsDefault:"YES"};
if(["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"].indexOf(e.id)>=0) operation["x-hl-resource-relationship"]={table:"supplier_resource_rel",ownership:"RESOURCE_AND_FLEET_MODULES_MAINTAIN_SUPPLIER",supplierDetailTab:"NONE",cardinality:"one resource has at most one active supplier",resourceModules:resourceModuleValues,vehicleMenuName:"车队管理-车队管理",vehicleRead:"FLEET_INTERNAL_READ_ONLY_AUDIT_BEFORE_IMPLEMENTATION",crossSchemaSql:"FORBIDDEN",softDelete:true,silentOverwrite:"FORBIDDEN",databaseBackstop:"uk_resource_alive(resource_module,resource_id,active_flag)",audit:"supplier_change_log field_name=resourceRelation"};
if(wecomImplementationRules[e.id]) operation["x-hl-wecom-implementation"]=wecomImplementationRules[e.id];
if(["POST","PUT","PATCH"].indexOf(e.method)>=0&&!bodylessWriteOperationIds.has(e.id)){
const bodySchema=requestSchemaForEndpoint(e);
if(!bodySchema) throw new Error("写接口缺少严格请求 Schema: "+e.id);
operation.requestBody={required:true,description:e.request,content:{"application/json":{schema:bodySchema}}};
}
spec.paths[e.path][e.method.toLowerCase()]=operation;
});
const requiredPolicyExtensions=["x-hl-permissions","x-hl-guard","x-hl-state-machine","x-hl-transaction","x-hl-lock","x-hl-idempotency"];
const policyErrors=[];
let serverCount=0,clientCount=0,shortWindowCount=0,wecomProviderExtensionCount=0,wecomReusedCount=0;
Object.keys(spec.paths).forEach(function(path){
Object.keys(spec.paths[path]).forEach(function(method){
if(["get","post","put","patch","delete"].indexOf(method)<0) return;
const op=spec.paths[path][method];
requiredPolicyExtensions.forEach(function(key){if(op[key]===undefined||op[key]===null) policyErrors.push(op.operationId+" missing "+key);});
if(path.indexOf("/admin/")===0){
if(op["x-hl-permissions"].mode!=="USER_FEIGN_FAIL_CLOSED") policyErrors.push(op.operationId+" admin permission mode is not fail-closed UserFeign");
if(!op["x-hl-permissions"].allOf||op["x-hl-permissions"].allOf.length===0) policyErrors.push(op.operationId+" missing admin permission code");
}
if(op["x-hl-codegen-direction"].indexOf("supplier-client")===0) clientCount++; else serverCount++;
if(op["x-hl-idempotency"]&&op["x-hl-idempotency"].mode==="REDIS_SHORT_WINDOW") shortWindowCount++;
const transactionKind=op["x-hl-transaction"]&&op["x-hl-transaction"].kind;
const mutatingProvider=op["x-hl-codegen-direction"].indexOf("supplier-client")!==0&&transactionKind!=="READ_ONLY";
if(mutatingProvider){
if(!op["x-hl-guard"].classes||op["x-hl-guard"].classes.length===0) policyErrors.push(op.operationId+" missing Guard class");
if(op["x-hl-lock"].mode==="NONE") policyErrors.push(op.operationId+" mutating provider has no lock policy");
if(transactionKind!=="LOCAL_ATOMIC"&&transactionKind!=="EXTERNAL_THREE_PHASE"&&transactionKind!=="CALLBACK_ATOMIC") policyErrors.push(op.operationId+" invalid mutating transaction kind "+transactionKind);
if(op["x-hl-idempotency"].mode!=="REDIS_SHORT_WINDOW") policyErrors.push(op.operationId+" mutating management operation has no Redis short-window guard");
}
(op["x-hl-errors"]||[]).forEach(function(error){if(error.code===null) policyErrors.push(op.operationId+" unresolved error "+error.name);});
});
});
if(serverCount!==spec["x-hl-codegen-policy"].serverOperationCount) policyErrors.push("server operation count mismatch "+serverCount);
if(clientCount!==spec["x-hl-codegen-policy"].outboundClientOperationCount) policyErrors.push("client operation count mismatch "+clientCount);
if(wecomProviderExtensionCount!==spec["x-hl-codegen-policy"].wecomProviderExtensionOperationCount) policyErrors.push("wecom provider extension count mismatch "+wecomProviderExtensionCount);
if(wecomReusedCount!==spec["x-hl-codegen-policy"].wecomReusedOperationCount) policyErrors.push("wecom reused operation count mismatch "+wecomReusedCount);
if(policyErrors.length) throw new Error("Codegen policy validation failed: "+policyErrors.join("; "));
spec["x-hl-codegen-validation"]={status:"PASS",requiredPolicyExtensions:requiredPolicyExtensions,serverOperationCount:serverCount,outboundClientOperationCount:clientCount,wecomProviderExtensionOperationCount:wecomProviderExtensionCount,wecomReusedOperationCount:wecomReusedCount,redisShortWindowOperationCount:shortWindowCount,unresolvedErrors:0};
return spec;
}
function initOpenApi(){
const spec=buildOpenApiSpec();
const json=JSON.stringify(spec,null,2);
const operationCount=Object.keys(spec.paths).reduce(function(total,path){return total+Object.keys(spec.paths[path]).filter(function(method){return ["get","post","put","patch","delete"].indexOf(method)>=0;}).length;},0);
document.getElementById("openApiPreview").textContent=json;
document.getElementById("openApiStatus").textContent="OpenAPI 3.0.3 已生成:"+operationCount+" 个本期 LOCAL_AUTO operation(服务端 "+spec["x-hl-codegen-validation"].serverOperationCount+"、企微出站 "+spec["x-hl-codegen-validation"].outboundClientOperationCount+"),Redis 短窗防重写操作 "+spec["x-hl-codegen-validation"].redisShortWindowOperationCount+" 个;Guard/状态机/事务/锁/幂等策略校验 PASS;排除 "+spec["x-hl-excluded-operations"].length+" 个非本期/二期/后续 WECOM operation,核心 Schema "+Object.keys(spec.components.schemas).length+" 个。";
document.getElementById("downloadOpenApiBtn").addEventListener("click",function(){
const url=URL.createObjectURL(new Blob([json],{type:"application/json;charset=utf-8"}));
const link=document.createElement("a");link.href=url;link.download="supplier-api-openapi-v2.0.json";document.body.appendChild(link);link.click();link.remove();setTimeout(function(){URL.revokeObjectURL(url);},1000);
});
document.getElementById("copyOpenApiBtn").addEventListener("click",function(){
const status=document.getElementById("openApiStatus");
const fallback=function(){const area=document.createElement("textarea");area.value=json;area.style.position="fixed";area.style.opacity="0";document.body.appendChild(area);area.select();document.execCommand("copy");area.remove();status.textContent="已复制 OpenAPI JSON("+operationCount+" 个 operation)。";};
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(json).then(function(){status.textContent="已复制 OpenAPI JSON("+operationCount+" 个 operation)。";}).catch(fallback);}else{fallback();}
});
}
function initNavHighlight(){
const aside=document.querySelector("aside");
const links=Array.from(document.querySelectorAll("aside a[href^='#']"));
const pairs=links.map(function(link){
const id=decodeURIComponent(link.hash.slice(1));
return {link:link,id:id,target:document.getElementById(id)};
}).filter(function(item){return item.target;});
const catalogLink=links.find(function(link){return link.hash==="#catalog";});
let frame=0;
function setActive(id){
let activeLink=null;
links.forEach(function(link){
const active=decodeURIComponent(link.hash.slice(1))===id;
link.classList.toggle("active",active);
if(active){link.setAttribute("aria-current","location");activeLink=link;}
else{link.removeAttribute("aria-current");}
link.classList.remove("parent-active");
});
if(activeLink&&activeLink.classList.contains("sub")&&catalogLink){catalogLink.classList.add("parent-active");}
if(activeLink&&aside){
const top=activeLink.offsetTop, bottom=top+activeLink.offsetHeight;
if(top<aside.scrollTop+12) aside.scrollTop=Math.max(0,top-12);
else if(bottom>aside.scrollTop+aside.clientHeight-12) aside.scrollTop=bottom-aside.clientHeight+12;
}
}
function update(){
frame=0;
const cutoff=110;
let current=pairs[0];
pairs.forEach(function(item){if(item.target.getBoundingClientRect().top<=cutoff) current=item;});
if(window.scrollY+window.innerHeight>=document.documentElement.scrollHeight-2) current=pairs[pairs.length-1];
if(current) setActive(current.id);
}
function schedule(){if(!frame) frame=window.requestAnimationFrame(update);}
links.forEach(function(link){link.addEventListener("click",function(){setActive(decodeURIComponent(link.hash.slice(1)));});});
window.addEventListener("scroll",schedule,{passive:true});
window.addEventListener("resize",schedule,{passive:true});
window.addEventListener("hashchange",schedule);
window.addEventListener("load",schedule);
update();
}
function renderFleetStyleDocument(){
const spec=buildOpenApiSpec();
const specJson=JSON.stringify(spec,null,2);
const activeCount=Object.keys(spec.paths).reduce(function(total,path){
return total+Object.keys(spec.paths[path]).filter(function(method){return ["get","post","put","patch","delete"].indexOf(method)>=0;}).length;
},0);
const adminActiveCount=endpoints.filter(function(endpoint){return endpoint.path.indexOf("/admin/supplier/")===0&&endpoint.delivery!=="本期不做"&&endpoint.delivery!=="二期"&&endpoint.delivery!=="后续 WECOM 对接";}).length;
const schemas=spec.components.schemas;
const errorMap={};
errorRows.forEach(function(row){errorMap[row[0]]={code:row[1],condition:row[2],message:row[3]};});
const categories=apiSections;
function resolveSchema(schema,seen){
if(!schema) return {};
seen=seen||new Set();
if(schema.$ref){
const name=schema.$ref.split("/").pop();
if(seen.has(name)) return {type:"object",description:"循环引用 "+name};
seen.add(name);
const resolved=resolveSchema(schemas[name]||{},seen);
seen.delete(name);
return resolved;
}
if(schema.oneOf&&schema.oneOf.length) return resolveSchema(schema.oneOf[0],seen);
if(schema.allOf&&schema.allOf.length){
const merged={type:"object",properties:{},required:[]};
schema.allOf.forEach(function(part){
const item=resolveSchema(part,seen);
Object.assign(merged.properties,item.properties||{});
(item.required||[]).forEach(function(name){if(merged.required.indexOf(name)<0) merged.required.push(name);});
if(item.description) merged.description=item.description;
});
Object.keys(schema).forEach(function(key){if(key!=="allOf") merged[key]=schema[key];});
return merged;
}
return schema;
}
function friendlyValue(name,schema,depth,seen){
if(depth>8) return {};
schema=schema||{};
if(schema.$ref){
const refName=schema.$ref.split("/").pop();
seen=seen||new Set();
if(seen.has(refName)) return {};
seen.add(refName);
const value=friendlyValue(name,schemas[refName]||{},depth+1,seen);
seen.delete(refName);
return value;
}
if(schema.oneOf&&schema.oneOf.length) return friendlyValue(name,schema.oneOf[0],depth+1,seen);
if(schema.allOf&&schema.allOf.length) return friendlyValue(name,resolveSchema(schema),depth+1,seen);
if(schema.example!==undefined) return schema.example;
if(schema.nullable&&["spNo","spStatus","templateId","applicantWecomUserId","currentLevel","currentLevelNo","opinion"].indexOf(String(name))>=0) return null;
if(schema.enum&&schema.enum.length) return schema.enum[0];
if(schema.nullable&&schema.type==="object"&&!schema.properties) return null;
if(schema.type==="array"){
const arrayKey=String(name||"").toLowerCase();
if(arrayKey==="typecodes") return ["HOTEL"];
return [friendlyValue(arrayKey.endsWith("s")?String(name).slice(0,-1):name,schema.items||{},depth+1,seen)];
}
if(schema.type==="object"||schema.properties){
const result={};
Object.keys(schema.properties||{}).forEach(function(key){result[key]=friendlyValue(key,schema.properties[key],depth+1,seen);});
if(!Object.keys(result).length&&schema.additionalProperties) return {key:"示例值"};
return result;
}
if(schema.type==="integer"){
if(/pageSize/i.test(name)) return 20;
if(/page|sortOrder|warningDays|currentLevel|levelNo|actionSeq/i.test(name)) return 1;
if(/total|count/i.test(name)) return 1;
return schema.minimum!==undefined?schema.minimum:1;
}
if(schema.type==="number") return 1000;
if(schema.type==="boolean") return true;
const key=String(name||"").toLowerCase();
if(/(^|_)id$/.test(key)||/id$/.test(key)) return "1900000000000000001";
if(key==="fullname") return "呼和浩特示例供应商有限公司";
if(key==="shortname") return "示例供应商";
if(key==="supplierno") return "SUP202608190001";
if(key==="taxno"||key==="tax_no") return "91150100XXXXXXXXXX";
if(key==="typecode"||key==="type_code"||key==="primarytypecode"||key==="requiredtypecode") return "HOTEL";
if(key==="typename"||key==="primarytypename") return "酒店";
if(key==="typecodes"||key==="types") return ["HOTEL"];
if(key.indexOf("maincooperation")>=0) return "酒店住宿及团队餐饮服务";
if(key.indexOf("reason")>=0||key.indexOf("note")>=0||key==="remark") return "业务调整说明";
if(key.indexOf("phone")>=0) return "13800000000";
if(key.indexOf("accountno")>=0) return "6222020000001234";
if(key.indexOf("bankname")>=0) return "示例银行";
if(key.indexOf("bankbranch")>=0) return "呼和浩特分行";
if(key.indexOf("accountname")>=0) return "呼和浩特示例供应商有限公司";
if(key.indexOf("url")>=0) return "https://files.example.test/supplier/example.pdf";
if(key.indexOf("date")>=0&&!key.endsWith("time")) return "2026-08-19";
if(key.indexOf("time")>=0||key.indexOf("at")>=0) return "2026-08-19 10:30:00";
if(key==="keyword") return "示例供应商";
if(key==="scene") return "HOTEL";
if(key==="spno") return "202608190001";
if(key==="requestno") return "SUP-REQ-20260819-0001";
return "示例值";
}
function schemaType(schema){
schema=schema||{};
if(schema.$ref) return schema.$ref.split("/").pop();
if(schema.oneOf) return "oneOf";
if(schema.allOf) return "组合对象";
if(schema.type==="array") return "array<"+schemaType(schema.items||{})+">";
if(schema.format) return (schema.type||"string")+"("+schema.format+")";
return schema.type||"object";
}
const enumChinese={
"1":"版本1","200":"成功","true":"是","false":"否",
A:"A级",B:"B级",C:"C级",D:"D级",
ACCOUNT_CHANGE:"账户变更",ACCOUNT_CREATE:"账户新增",
ACTIVE:"启用",ACTIVITY:"游玩项目",APPLIED:"已应用",APPLY_FAILED:"应用失败",APPLY_PENDING:"待应用",APPROVAL_SYNC:"审批同步",APPROVED:"已通过",ARCHIVED:"已归档",ASC:"升序",
BLACKLIST:"黑名单",BIZ:"业务对接人",BUSINESS_CHANGE:"业务变更",BUSINESS_DOCUMENT:"业务单据",
CALLBACK:"回调",CANCELLED:"已取消",CHANNEL:"OTA/渠道",COMPLETE:"完整",COMPLETED:"已完成",CORPORATE:"对公账户",COST_ITEM:"成本项目",CREATE:"创建",CREDIT_LEVEL_D:"信用等级D",
DELETE:"删除",DESC:"降序",DISABLE:"停用",DISABLED:"已停用",DOMAIN_EVENT:"领域事件",DRAFT:"草稿",
ENABLE:"启用",EXPIRED:"已过期",FAILED:"失败",FLEET:"车队",FRAME:"框架结构",FROZEN:"已冻结",
GT500:"500人以上",HOTEL:"酒店",IGNORED:"已忽略",INCOMPLETE:"不完整",INSURANCE:"保险",LT50:"50人以下",MANDATORY_QUALIFICATION_INVALID:"必备资质无效",MANUAL_RECONCILE:"人工对账",MONTHLY:"月结",
NONE:"无",NORMAL:"普通发票",NOT_FROZEN:"尚未冻结",NOT_SYNCED:"未同步",OTHER:"其他",PENDING:"待处理",PENDING_BLACKLIST:"黑名单审批中",PERFORMANCE:"演艺",PERSONAL:"个人账户",POLL:"轮询",PREPAY:"预付",PROFESSIONAL_SERVICE:"专业服务",PROPERTY:"物业/房租",
PROFILE_APPROVING:"主档审批中",PROFILE_CREATE:"供应商注册",PROFILE_DRAFT:"主档草稿",PURCHASE:"采购",
R200_500:"200至500人",R50_200:"50至200人",READ_APPROVAL_OPINION:"读取审批意见",RECEIVED:"已接收",REJECTED:"已拒绝",REQUESTING:"请求中",
RENTAL:"租车",RESTAURANT:"餐厅",SCENIC:"景区",SENSITIVE_READ:"敏感读取",SERVICE:"服务",SINGLE:"单次结算",SINGLE_TRIP:"单团结算",SPECIAL:"增值税专用发票",STAFF:"服务人员",
STATUS_BLACKLIST:"加入黑名单",STATUS_CHANGE:"状态变更",STATUS_FREEZE:"冻结",STATUS_RESUME:"恢复",STATUS_SUSPEND:"暂停",STATUS_UNBLACKLIST:"移出黑名单",STATUS_UNFREEZE:"解除冻结",
SUBMITTED:"已提交",SUPPLIES:"备品",SUPPLIES_COMBO:"组合配品",SUSPENDED:"已暂停",SYNCED:"已同步",TERMINATED:"已终止",TICKET:"票务",UNKNOWN:"未知",UPDATE:"更新",VEHICLE:"车队管理-车队管理",VETTING:"审核中",
WECOM_APPROVAL:"企业微信审批",WECOM_FORM:"企业微信表单"
,LOCAL_AUTO:"本地系统自动",WECOM:"企业微信",NOT_APPLICABLE:"不适用",NOT_APPLIED:"未应用"
};
const fieldLabelOverrides={
code:"业务码",message:"提示消息",success:"是否成功",data:"数据体",traceId:"链路追踪ID",
supplierId:"供应商ID",supplierIds:"供应商ID列表",supplierNo:"供应商编号",supplierName:"供应商名称",fullName:"供应商全称",shortName:"供应商简称",
relationId:"关联ID",relationIds:"关联ID列表",resourceModule:"资源模块",moduleName:"模块名称",resourceId:"资源ID",resourceName:"资源名称",requiredTypeCode:"要求的供应商类型编码",requiredTypeName:"要求的供应商类型名称",associationStatus:"关联状态",currentSupplierId:"当前供应商ID",currentSupplierName:"当前供应商名称",associable:"是否可关联",unavailableReasons:"不可用原因",reasons:"不可关联原因",
taxNo:"统一社会信用代码",taxNoMask:"统一社会信用代码掩码",tax_no:"统一社会信用代码掩码",typeId:"类型ID",typeCode:"类型编码",type_code:"类型编码",typeCodes:"类型编码列表",typeName:"类型名称",types:"供应商类型列表",primaryTypeCode:"主类型编码",primaryTypeName:"主类型名称",
mainCooperation:"主要合作内容",businessScope:"经营范围",address:"地址",legalRepresentative:"法定代表人",registeredCapital:"注册资本",staffScale:"人员规模",establishDate:"成立日期",isRelatedParty:"是否关联方",
expectedUpdateTime:"期望数据更新时间",createTime:"创建时间",updateTime:"更新时间",startDate:"开始日期",endDate:"结束日期",expiryDate:"到期日期",finishedAt:"完成时间",submittedAt:"提交时间",acceptedAt:"受理时间",checkedAt:"检查时间",
page:"页码",pageSize:"每页条数",total:"总条数",records:"记录列表",keyword:"搜索关键字",creatorId:"创建人ID",scene:"业务场景",sortOrder:"排序号",sortBy:"排序字段",sortDirection:"排序方向",includeDeleted:"是否包含已删除数据",
contactId:"联系人ID",contactName:"联系人姓名",contactPhone:"联系人电话",contactPhoneMask:"联系人电话掩码",contactRole:"联系人角色",contacts:"联系人列表",businessContacts:"业务联系人列表",financialContacts:"财务联系人列表",
qualificationId:"资质ID",qualType:"资质类型",qualTypeName:"资质类型名称",certNo:"证照编号",certNoMask:"证照编号掩码",imageUrl:"图片地址",licenseImageUrl:"营业执照图片地址",qualificationFileUrls:"资质文件地址列表",permanentValid:"是否永久有效",daysUntilExpiry:"距到期自然日数",validityStatus:"资质有效状态",validityStatusName:"资质有效状态名称",isRequired:"是否必备",expired:"是否过期",warningDays:"预警天数",
accountId:"账户ID",accountName:"账户名称",accountNo:"收款账号",accountNoMask:"收款账号掩码",accountType:"账户类型",bankName:"开户银行",bankBranch:"开户支行",accounts:"新增收款账户列表",bankAccounts:"收款账户列表",isDefault:"是否默认账户",proofFileUrls:"凭证文件地址列表",
amount:"金额",settleMode:"结算方式",accountPeriod:"账期",invoiceType:"发票类型",taxRate:"税率",
changeType:"变更类型",changeReason:"变更原因",candidate:"候选变更数据",targetStatus:"目标状态",currentStatus:"当前状态",creditLevel:"信用等级",totalScore:"综合评分",riskFlags:"风险标记列表",reason:"原因",reasons:"原因列表",warning:"预警提示",warnings:"预警提示列表",
approvalLogId:"审批日志ID",bizType:"审批业务类型",subType:"审批子类型",provider:"审批提供方",approvalStatus:"标准化审批状态",applyStatus:"业务应用状态",systemDecision:"是否系统自动决策",spNo:"企业微信审批单号",spStatus:"企业微信原始审批状态",syncStatus:"同步状态",detailSyncStatus:"详情同步状态",currentLevel:"当前审批级次",currentLevelNo:"当前审批级次序号",hasOpinion:"是否存在审批意见",opinion:"审批意见",levels:"审批级次列表",actions:"审批动作列表",events:"同步事件列表",
requestNo:"请求编号",templateId:"审批模板ID",schemaVersion:"结构版本",sourceRevision:"来源版本",detailDigest:"详情摘要",sourceEventKey:"来源事件唯一键",processStatus:"处理状态",businessApplied:"业务结果是否已应用",accepted:"是否已受理",
applicantId:"申请人ID",applicantAdminId:"申请管理员ID",applicantUserId:"申请人企微用户ID",applicantWecomUserId:"申请人企微用户ID",applicantName:"申请人姓名",applicantDepartments:"申请人部门快照",approverUserId:"审批人企微用户ID",approverName:"审批人姓名",approverDepartments:"审批人部门快照",approvalDepartments:"审批部门快照",
departmentId:"部门ID",departmentName:"部门名称",departmentPath:"部门路径",primary:"是否主部门",roleCodeSnapshot:"角色编码快照",roleNameSnapshot:"角色名称快照",roleSource:"角色来源",
_status:"契约状态",_description:"规划说明"
};
const fieldTokenChinese={
id:"ID",ids:"ID列表",name:"名称",names:"名称列表",no:"编号",mask:"掩码",type:"类型",types:"类型列表",status:"状态",time:"时间",at:"时间",date:"日期",data:"数据",
accepted:"受理",account:"账户",action:"动作",actions:"动作列表",active:"生效",applicant:"申请人",admin:"管理员",approval:"审批",approve:"审批",approved:"通过",approver:"审批人",apply:"申请",associable:"可关联",bank:"银行",biz:"业务",business:"业务",candidate:"候选",candidates:"候选列表",cert:"证照",change:"变更",changes:"变更列表",checked:"检查",clearance:"清账",code:"代码",completed:"完成",confirm:"确认",conflict:"冲突",contact:"联系人",create:"创建",credit:"信用",current:"当前",department:"部门",departments:"部门列表",detail:"详情",digest:"摘要",duplicate:"重复",eligible:"资格",enabled:"启用",end:"结束",establish:"成立",event:"事件",events:"事件列表",exact:"精确",exclude:"排除",expected:"期望",expired:"过期",expiry:"到期",extra:"扩展",fact:"事实",facts:"事实列表",field:"字段",fields:"字段",financial:"财务",finished:"完成",form:"表单",from:"原",full:"完整",has:"是否有",image:"图片",initial:"初始",instance:"实例",invoice:"发票",item:"项目",items:"项目列表",latest:"最近",ledger:"台账",legal:"法定",level:"级次",levels:"级次列表",license:"执照",log:"日志",main:"主要",match:"匹配",matched:"匹配到",missing:"缺失",new:"新",old:"旧",onboarding:"注册",operation:"操作",operator:"操作人",opinion:"意见",order:"顺序",page:"页",payable:"可付款",payment:"付款",payload:"负载",period:"周期",pricing:"计价",primary:"主要",process:"处理",profile:"主档",proof:"凭证",qual:"资质",qualification:"资质",qualifications:"资质列表",raw:"原始",received:"接收",record:"记录",registered:"注册",remark:"备注",replacement:"替代",request:"请求",required:"必需",resource:"资源",result:"结果",revision:"版本",risk:"风险",role:"角色",rule:"规则",rules:"规则列表",scan:"扫描件",schema:"结构",settle:"结算",short:"简称",sign:"签署",snapshot:"快照",sort:"排序",source:"来源",sp:"企业微信审批",staff:"人员",start:"开始",started:"开始",sub:"子",submit:"提交",submitted:"提交",supplier:"供应商",sync:"同步",target:"目标",tax:"税",template:"模板",to:"目标",total:"总",trace:"链路追踪",update:"更新",usable:"可用",user:"用户",value:"值",version:"版本",warning:"预警",days:"天数",url:"地址",urls:"地址列表",count:"数量",mode:"模式",cycle:"周期",rate:"税率",scope:"范围",capital:"资本",cooperation:"合作内容",related:"关联",party:"方",summary:"摘要",file:"文件",files:"文件列表",flags:"标记列表",key:"键",description:"说明",wecom:"企业微信"
};
function fieldMeaning(name){
if(fieldLabelOverrides[name]) return fieldLabelOverrides[name];
const tokens=String(name).replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/[_-]+/g," ").toLowerCase().split(/\s+/).filter(Boolean);
const translated=tokens.map(function(token){return fieldTokenChinese[token]||token;}).join("");
return translated||String(name);
}
const narrativeEnumKeys=Object.keys(enumChinese).filter(function(key){return key.length>1&&!/^\d+$/.test(key);}).sort(function(a,b){return b.length-a.length;});
const narrativeEnumPattern=new RegExp("(^|[^A-Z0-9_])("+narrativeEnumKeys.join("|")+")(?![A-Z0-9_(])","g");
function annotateEnumText(value){
return String(value==null?"":value).replace(narrativeEnumPattern,function(match,prefix,code){return prefix+code+"("+enumChinese[code]+")";});
}
function annotatedRuleList(items){
if(!items||!items.length) return "<span class='small'>无接口专属项,适用通用契约</span>";
return "<ul>"+items.map(function(item){return "<li>"+esc(annotateEnumText(item))+"</li>";}).join("")+"</ul>";
}
function constraintText(schema){
schema=schema||{};
const parts=[];
if(schema.enum) parts.push("枚举:"+schema.enum.map(function(value){const key=String(value);return key+"="+(enumChinese[key]||"对应业务值");}).join(" / "));
if(schema.minLength!==undefined) parts.push("最少 "+schema.minLength+" 字符");
if(schema.maxLength!==undefined) parts.push("最多 "+schema.maxLength+" 字符");
if(schema.minItems!==undefined) parts.push("至少 "+schema.minItems+" 项");
if(schema.maxItems!==undefined) parts.push("最多 "+schema.maxItems+" 项");
if(schema.minimum!==undefined) parts.push("≥ "+schema.minimum);
if(schema.maximum!==undefined) parts.push("≤ "+schema.maximum);
if(schema.pattern) parts.push("格式校验");
if(schema.writeOnly) parts.push("仅写入;响应不回显");
if(schema.description) parts.push(schema.description);
return parts.join(";")||"—";
}
function collectHints(schema,hints,seen){
hints=hints||{}; seen=seen||new Set();
if(!schema) return hints;
if(schema.$ref){
const name=schema.$ref.split("/").pop();
if(seen.has(name)) return hints;
seen.add(name); collectHints(schemas[name],hints,seen); seen.delete(name); return hints;
}
if(schema.oneOf&&schema.oneOf.length) return collectHints(schema.oneOf[0],hints,seen);
if(schema.allOf){schema.allOf.forEach(function(part){collectHints(part,hints,seen);});return hints;}
const required=schema.required||[];
Object.keys(schema.properties||{}).forEach(function(name){
const property=schema.properties[name];
const details=[property.title||fieldMeaning(name)];
const constraints=constraintText(property);
if(constraints!=="—") details.push(constraints);
if(!hints[name]) hints[name]=details.join(",");
collectHints(property,hints,seen);
});
if(schema.items) collectHints(schema.items,hints,seen);
return hints;
}
function jsonc(schema,value){
const lines=JSON.stringify(value,null,2).split("\n");
const contexts=[{schema:schema||{},value:value,nextIndex:0}];
const annotated=lines.map(function(line,index){
if(index===0) return {line:line,hint:""};
const indent=(line.match(/^\s*/)||[""])[0].length;
const depth=Math.floor(indent/2);
const match=line.match(/^\s*\"([^\"]+)\"\s*:/);
const parent=depth>0?contexts[depth-1]:null;
if(!match||!parent){
if(parent){
const parentSchema=resolveSchema(parent.schema||{});
const trimmed=line.trim();
if(parentSchema.type==="array"&&trimmed&&trimmed.charAt(0)!=="]"){
const itemIndex=parent.nextIndex++;
const itemValue=Array.isArray(parent.value)?parent.value[itemIndex]:undefined;
if(/^[\[{]/.test(trimmed)&&!/^(?:\{\}|\[\])/.test(trimmed)){
contexts[depth]={schema:parentSchema.items||{},value:itemValue,nextIndex:0};
contexts.length=depth+1;
}
}
}
return {line:line,hint:""};
}
const name=match[1];
const parentSchema=resolveSchema(parent.schema||{});
const property=(parentSchema.properties||{})[name]||{};
const propertyValue=parent.value&&typeof parent.value==="object"?parent.value[name]:undefined;
const details=[fieldMeaning(name)];
const constraints=constraintText(property);
if(constraints!=="—") details.push(constraints);
if(/:\s*[\[{]\s*,?\s*$/.test(line)&&!/[:]\s*(?:\{\}|\[\])\s*,?\s*$/.test(line)){
contexts[depth]={schema:property,value:propertyValue,nextIndex:0};
contexts.length=depth+1;
}
return {line:line,hint:details.join(",")};
});
const targetColumn=Math.max(40,Math.min(80,annotated.reduce(function(max,item){return item.hint?Math.max(max,item.line.length+4):max;},0)));
return annotated.map(function(item){
if(!item.hint) return item.line;
const spaces=" ".repeat(Math.max(4,targetColumn-item.line.length));
return item.line+spaces+"// "+item.hint;
}).join("\n");
}
function propertyTable(schema){
const raw=schema&&schema.$ref?schemas[schema.$ref.split("/").pop()]||{}:schema||{};
if(raw.oneOf&&raw.oneOf.length){
return raw.oneOf.map(function(branch,index){
const label=branch.$ref?branch.$ref.split("/").pop():"分支 "+(index+1);
return "<p class='branch-label'>分支 "+(index+1)+":<code>"+esc(label)+"</code></p>"+propertyTable(branch);
}).join("");
}
const resolved=resolveSchema(schema);
const properties=resolved.properties||{};
const required=resolved.required||[];
if(!Object.keys(properties).length) return "";
return "<div class='table-wrap'><table><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>约束 / 说明</th></tr></thead><tbody>"+
Object.keys(properties).map(function(name){
return "<tr><td><code>"+esc(name)+"</code></td><td>"+esc(schemaType(properties[name]))+"</td><td>"+(required.indexOf(name)>=0?"是":"否")+"</td><td>"+esc(constraintText(properties[name]))+"</td></tr>";
}).join("")+"</tbody></table></div>";
}
function parameterExample(parameter){
return friendlyValue(parameter.name,parameter.schema||{},0,new Set());
}
function parameterTable(parameters){
if(!parameters||!parameters.length) return "";
return "<div class='table-wrap'><table><thead><tr><th>参数</th><th>位置</th><th>类型</th><th>必填</th><th>约束 / 说明</th></tr></thead><tbody>"+
parameters.map(function(parameter){
const description=[parameter.description,constraintText(parameter.schema)].filter(function(x){return x&&x!=="—";}).join(";")||"—";
return "<tr><td><code>"+esc(parameter.name)+"</code></td><td>"+esc(parameter.in)+"</td><td>"+esc(schemaType(parameter.schema))+"</td><td>"+(parameter.required?"是":"否")+"</td><td>"+esc(description)+"</td></tr>";
}).join("")+"</tbody></table></div>";
}
function requestExample(endpoint,operation){
const parameters=operation?operation.parameters||[]:[];
const pathParameters=parameters.filter(function(p){return p.in==="path";});
const queryParameters=parameters.filter(function(p){return p.in==="query";});
const headerParameters=parameters.filter(function(p){return p.in==="header";});
let path=endpoint.path;
pathParameters.forEach(function(p){path=path.replace("{"+p.name+"}",String(parameterExample(p)));});
const query=queryParameters.map(function(p){return p.name+"="+String(parameterExample(p));});
const requestLines=[endpoint.method+" "+path];
if(query.length) requestLines.push("Query:",query.map(function(item){return " "+item;}).join("\n"));
let html="<p><strong>请求示例</strong>:</p><pre><code>"+esc(requestLines.join("\n"))+"</code></pre>";
if(headerParameters.length){
html+="<p class='sub-label'>Header:</p><pre><code>"+esc(headerParameters.map(function(p){return p.name+": "+parameterExample(p);}).join("\n"))+"</code></pre>";
}
return html;
}
function parameterSections(parameters,endpointId){
const groups=[
{key:"path",label:"path"},
{key:"query",label:"query"},
{key:"header",label:"header"}
];
return groups.map(function(group){
const items=(parameters||[]).filter(function(parameter){return parameter.in===group.key;});
if(!items.length) return "";
const rows=items.map(function(parameter){
const description=[fieldMeaning(parameter.name),parameter.description,constraintText(parameter.schema)].filter(function(item,index,array){return item&&item!=="—"&&array.indexOf(item)===index;}).join(";");
return "<tr><td><code>"+esc(parameter.name)+"</code></td><td>"+esc(schemaType(parameter.schema))+"</td><td>"+(parameter.required?"是":"否")+"</td><td>"+esc(description)+"</td></tr>";
}).join("");
const tableClass=["SUP-ADM-001","SUP-ADM-043","SUP-ADM-012"].includes(endpointId)&&group.key==="query"
?" class='supplier-list-query-params'"
:["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"].includes(endpointId)
?" class='resource-relation-params'"
:"";
return "<p><strong>入参</strong>("+group.label+"):</p><div class='table-wrap'><table"+tableClass+"><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody>"+rows+"</tbody></table></div>";
}).join("");
}
function responseExample(endpoint,operation){
if(!operation) return "// 非本期或二期:响应结构未冻结,禁止据此生成代码";
const data=endpoint.response==="Result<Void>"?null:friendlyValue("data",dataSchemaForResponse(endpoint.response),0,new Set());
if(data&&endpoint.id==="SUP-ADM-003") Object.assign(data,{supplierNo:null,status:"DRAFT",onboardingStage:"PROFILE_DRAFT"});
if(data&&endpoint.id==="SUP-ADM-004") Object.assign(data,{supplierNo:"SUP202608190001",status:"ACTIVE",onboardingStage:"COMPLETED"});
if(Array.isArray(data)&&endpoint.id==="SUP-ADM-035"){
data.forEach(function(item){Object.assign(item,{provider:"LOCAL_AUTO",approvalStatus:"APPROVED",spNo:null,spStatus:null,syncStatus:"APPLIED",accountStatus:"ACTIVE",isDefault:"NO"});});
if(data.length===1){const second=JSON.parse(JSON.stringify(data[0]));Object.assign(second,{accountId:"1900000000000000002",approvalLogId:"1900000000000000002",requestNo:"SUP-REQ-20260821-0002"});data.push(second);}
}
if(data&&endpoint.id==="SUP-ADM-041") Object.assign(data,{status:"ACTIVE",isDefault:"YES"});
if(data&&endpoint.response.indexOf("ApprovalCommandResultVO")>=0) Object.assign(data,{provider:"LOCAL_AUTO",approvalStatus:"APPROVED",spNo:null,spStatus:null,syncStatus:"APPLIED"});
const envelope={code:200,message:"成功",success:true,data:data};
return jsonc(resultSchema(dataSchemaForResponse(endpoint.response)),envelope);
}
function errorBlock(endpoint,operation){
const common=[
{value:"400",meaning:"请求结构、格式或参数校验失败"},
{value:"401",meaning:endpoint.path.indexOf("/internal/")===0?"X-Internal-Token 缺失或无效":"未登录或 Gateway 认证失败"},
{value:"403",meaning:"角色、功能权限或数据范围拒绝"},
{value:"409",meaning:"并发、幂等摘要或业务状态冲突"}
];
let names=endpoint.errors.slice();
if(operation&&operation["x-hl-errors"]) names=operation["x-hl-errors"].map(function(item){return item.name;});
const rows=common.map(function(item){return "<tr><td><code>"+item.value+"</code></td><td>HTTP / 通用</td><td>"+esc(item.meaning)+"</td></tr>";});
names.forEach(function(name){
const item=errorMap[name];
rows.push("<tr><td><code>"+esc(item?item.code:name)+"</code></td><td>"+esc(name)+"</td><td>"+esc(annotateEnumText(item?(item.condition+";"+item.message):"接口专属业务错误"))+"</td></tr>");
});
return "<p><strong>错误码</strong>:400 参数校验 / 401 未认证 / 403 无权限 / 409 并发或状态冲突;业务错误可能仍返回 HTTP 200,必须检查 <code>code/message/success</code>。</p>"+
"<div class='table-wrap'><table><thead><tr><th>代码</th><th>名称</th><th>触发条件 / 提示</th></tr></thead><tbody>"+rows.join("")+"</tbody></table></div>";
}
function bodyExamples(bodySchema){
const raw=bodySchema&&bodySchema.$ref?schemas[bodySchema.$ref.split("/").pop()]||{}:bodySchema||{};
if(raw.oneOf&&raw.oneOf.length){
return raw.oneOf.map(function(branch,index){
const label=branch.$ref?branch.$ref.split("/").pop():"请求分支 "+(index+1);
const value=friendlyValue("body",branch,0,new Set());
return "<p class='branch-label'>"+(index+1)+")<code>"+esc(label)+"</code></p><pre><code class='language-json'>"+esc(jsonc(branch,value))+"</code></pre>";
}).join("");
}
const bodyValue=friendlyValue("body",bodySchema,0,new Set());
return "<pre><code class='language-json'>"+esc(jsonc(bodySchema,bodyValue))+"</code></pre>";
}
function detailSplitDiagram(){
const detailInterfaces=[
{name:"基本信息",path:"/admin/supplier/items/{supplierId}/basic-info/view",response:"Result<SupplierBasicInfoVO>"},
{name:"账户信息",path:"/admin/supplier/items/{supplierId}/account-info/list",response:"Result<SupplierAccountInfoVO>"}
];
return "<section class='detail-split-map' aria-label='供应商详情两个接口拆分图'>"+
"<h3>供应商详情两个接口拆分图</h3>"+
"<div class='detail-split-root'><strong>供应商详情</strong><span>使用同一 <code>supplierId</code>,按页签分别调用 2 个独立 GET 接口</span></div>"+
"<div class='detail-split-branches'>"+detailInterfaces.map(function(item,index){
return "<div class='detail-split-item'><span class='detail-split-index'>"+(index+1)+"</span><strong>"+esc(item.name)+"</strong><code>GET "+esc(item.path)+"</code><span class='detail-split-response'>返回 <code>"+esc(item.response)+"</code></span></div>";
}).join("")+"</div>"+
"<p class='detail-split-note'>两个接口独立返回所属页签数据,不混装其他页签字段。</p>"+
"</section>";
}
function endpointHtml(endpoint,number){
const operation=spec.paths[endpoint.path]&&spec.paths[endpoint.path][endpoint.method.toLowerCase()];
const status=operation?endpoint.delivery:"不生成代码 · "+endpoint.delivery;
const phaseOneDesignHref="./供应商模块一期实施拆分详细设计-v1.0.html#design-"+anchor(endpoint.id);
const searchable=[number,endpoint.id,operationIdFor(endpoint),endpoint.name,endpoint.path,endpoint.group,endpoint.request,endpoint.response,endpoint.rules.join(" "),endpoint.errors.join(" ")].join(" ").toLowerCase();
let inputHtml="";
if(operation){
inputHtml+=parameterSections(operation.parameters||[],endpoint.id);
if(operation.requestBody){
const bodySchema=operation.requestBody.content["application/json"].schema;
const rawBodySchema=bodySchema.$ref?schemas[bodySchema.$ref.split("/").pop()]||{}:bodySchema;
inputHtml+="<p><strong>入参</strong>(body):</p>"+(rawBodySchema.oneOf?"<p class='sub-label'>该请求按业务类型分为以下 "+rawBodySchema.oneOf.length+" 个互斥 body;实际请求只提交其中一个:</p>":"")+bodyExamples(bodySchema);
}else if(!(operation.parameters||[]).length){
inputHtml+="<p><strong>入参</strong>:</p><pre><code class='language-json'>{}</code></pre><p class='empty'>无业务入参。</p>";
}
}else{
const plannedSchema={type:"object",additionalProperties:false,properties:{_status:{type:"string",enum:["NOT_FROZEN"],description:"仅用于文档说明,不是实际请求字段"},_description:{type:"string",description:"当前规划中的请求说明,不是实际请求字段"}},required:["_status","_description"]};
inputHtml="<p><strong>入参</strong>(未冻结):</p><pre><code class='language-json'>"+esc(jsonc(plannedSchema,{_status:"NOT_FROZEN",_description:endpoint.request}))+"</code></pre><div class='warning'><strong>规划占位:</strong>字段、约束和响应尚未冻结,本期禁止生成 Controller、DTO、Service、Job 或占位响应。</div>";
}
const responseHtml=endpoint.id==="SUP-ADM-002"
?detailSplitDiagram()+"<p><strong><code>GET /admin/supplier/items/{supplierId}/basic-info/view</code> 出参 JSON</strong>:</p><pre><code class='language-json'>"+esc(responseExample(endpoint,operation))+"</code></pre>"
:"<p><strong>出参</strong>:<code>"+esc(endpoint.response)+"</code></p><pre><code class='language-json'>"+esc(responseExample(endpoint,operation))+"</code></pre>";
return "<article class='api-section' id='"+anchor(endpoint.id)+"' data-search='"+esc(searchable)+"'>"+
"<h2>"+esc(number+" "+endpoint.name)+"</h2>"+
"<div class='endpoint-line'><span class='method-badge method-"+esc(endpoint.method.toLowerCase())+"'>"+esc(endpoint.method)+"</span><code>"+esc(endpoint.path)+"</code><span class='status-badge "+statusClass(endpoint.delivery)+"'>"+esc(status)+"</span></div>"+
"<p><strong>用途</strong>:"+esc(endpoint.name)+"。"+esc(annotateEnumText(endpoint.rules[0]||"适用供应商模块通用业务契约。"))+"</p>"+
inputHtml+
(endpoint.rules.length>1?"<div class='contract-note'><strong>关键口径</strong>:"+esc(annotateEnumText(endpoint.rules[1]))+"</div>":"")+
responseHtml+
"<p><strong>业务规则</strong>:</p>"+annotatedRuleList(endpoint.rules)+
errorBlock(endpoint,operation)+
"<p class='trace'><strong>对应一期业务需求</strong>:<a href='"+phaseOneDesignHref+"'>"+(operation?"查看 "+esc(endpoint.id)+" 主要业务 SRS":"查看非一期业务边界")+"</a><br><strong>权限 / 认证</strong>:"+esc(endpoint.access)+";"+(endpoint.path.indexOf("/internal/")===0?"仅服务间 X-Internal-Token 调用。":"统一经过 Gateway,并由 UserFeignClient 按平台权限码失败关闭校验。")+"<br><strong>契约编号</strong>:"+esc(endpoint.id)+" <strong>提供方</strong>:"+(endpoint.id.indexOf("SUP-WECOM")===0?"hl-user-service / 现有 InternalApprovalController":"hl-resource-service")+" <strong>消费方</strong>:"+esc(endpoint.consumer)+"<br><strong>来源</strong>:"+esc(endpoint.source)+"</p><hr></article>";
}
const categoryMenuGroups=[
[
{title:"供应商档案",ids:["SUP-ADM-001","SUP-ADM-043","SUP-ADM-002","SUP-ADM-003","SUP-ADM-004","SUP-ADM-011"]},
{title:"注册与生命周期",ids:["SUP-ADM-007","SUP-ADM-010"]},
{title:"供应商审批记录",ids:["SUP-ADM-012"]}
],
[
{title:"收款账户",ids:["SUP-ADM-034","SUP-ADM-035","SUP-ADM-036","SUP-ADM-041"]}
],
[
{title:"资源侧供应商维护",ids:["SUP-ADM-048","SUP-ADM-049","SUP-ADM-050"]}
]
];
const categoryEndpoints=categories.map(function(category,index){
const items=endpoints.filter(function(endpoint){return category.groups.indexOf(endpoint.group)>=0;});
const menuIds=(categoryMenuGroups[index]||[]).reduce(function(result,group){return result.concat(group.ids);},[]);
const menuIdSet=new Set(menuIds);
return menuIds.map(function(id){return items.find(function(endpoint){return endpoint.id===id;});}).filter(Boolean)
.concat(items.filter(function(endpoint){return !menuIdSet.has(endpoint.id);}));
});
const sidebarGeneral="<a class='nav-h1' href='#overview'>0. 文档总则</a>"+[
["positioning","文档定位"],["catalog","供应商详情两个页签"],["auth","菜单与权限配置"],["common-contract","通用请求与响应"],["frontend-dicts-contract","前端字典使用约定"],["error-convention","错误码约定"],["codegen","代码生成契约"]
].map(function(item){return "<a class='nav-h2' href='#"+item[0]+"'>"+item[1]+"</a>";}).join("");
const sidebarCategories=categories.map(function(category,index){
const items=categoryEndpoints[index];
const endpointLink=function(endpoint){
const itemIndex=items.indexOf(endpoint);
return "<a class='nav-h2 endpoint-nav' data-search='"+esc([endpoint.id,endpoint.name,endpoint.path].join(" ").toLowerCase())+"' href='#"+anchor(endpoint.id)+"'>"+(index+1)+"."+(itemIndex+1)+" "+esc(endpoint.name)+"</a>";
};
const categoryLink="<a class='nav-h1' href='#category-"+(index+1)+"'>"+(index+1)+". "+esc(category.title)+"</a>";
const menuGroups=(categoryMenuGroups[index]||[]).map(function(group){
return {title:group.title,items:group.ids.map(function(id){return items.find(function(endpoint){return endpoint.id===id;});}).filter(Boolean)};
}).filter(function(group){return group.items.length;});
const groupedIds=new Set(menuGroups.reduce(function(result,group){return result.concat(group.items.map(function(endpoint){return endpoint.id;}));},[]));
const remainingItems=items.filter(function(endpoint){return !groupedIds.has(endpoint.id);});
if(remainingItems.length) menuGroups.push({title:"其他接口",items:remainingItems});
return categoryLink+menuGroups.map(function(group){
return "<div class='nav-submenu'><div class='nav-submenu-title'><span>"+esc(group.title)+"</span><span class='nav-submenu-count'>"+group.items.length+"</span></div>"+group.items.map(endpointLink).join("")+"</div>";
}).join("");
}).join("");
const categoryContent=categories.map(function(category,index){
const items=categoryEndpoints[index];
return "<section class='category-block' id='category-"+(index+1)+"'><h1>"+(index+1)+". "+esc(category.title)+"</h1><p class='category-description'>"+esc(category.description)+" 共 "+items.length+" 个接口。</p>"+
items.map(function(endpoint,itemIndex){return endpointHtml(endpoint,(index+1)+"."+(itemIndex+1));}).join("")+"</section>";
}).join("");
const detailTabRows=supplierDetailSections.map(function(category,index){
const categoryIndex=categories.indexOf(category);
const items=categoryEndpoints[categoryIndex];
const generated=items.filter(function(endpoint){return spec.paths[endpoint.path]&&spec.paths[endpoint.path][endpoint.method.toLowerCase()];}).length;
return "<tr><td>"+(index+1)+"</td><td><a href='#category-"+(categoryIndex+1)+"'>"+esc(category.title)+"</a></td><td>"+items.length+"</td><td>"+generated+"</td><td>"+esc(category.description)+"</td></tr>";
}).join("");
document.body.className="supplier-api-page";
document.body.innerHTML=`
<style>
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:24px}
body.supplier-api-page{margin:0;font-family:-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#f7f8fa;color:#1f2933;line-height:1.7;font-size:14px}
.supplier-header{background:linear-gradient(135deg,#1e3a8a,#3b82f6);color:#fff;padding:32px 40px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.1)}
.supplier-header h1{margin:0 0 8px;font-size:24px}.supplier-header p{margin:4px 0;color:#fff;opacity:.9;font-size:13px}
.supplier-layout{display:flex;align-items:flex-start;gap:24px;max-width:1600px;margin:0 auto;padding:24px 32px}
.supplier-sidebar{position:sticky;top:24px;width:280px;flex:0 0 280px;max-height:calc(100vh - 48px);overflow-y:auto;background:#fff;border:1px solid #e4e7eb;border-radius:8px;padding:16px 12px;box-shadow:0 1px 3px rgba(0,0,0,.04);font-size:13px}
.supplier-sidebar a{display:block;margin:1px 0;color:#475569;text-decoration:none;padding:5px 10px;border-radius:4px;border-left:3px solid transparent;transition:all .15s;word-break:break-all;cursor:pointer}
.supplier-sidebar a:hover,.supplier-sidebar a.active{background:#dbeafe;color:#1d4ed8}.supplier-sidebar a.nav-h1{font-weight:700;font-size:14px;margin-top:10px;color:#1e3a8a;border-left-color:#1e40af}.supplier-sidebar a.nav-h1:first-child{margin-top:0}.supplier-sidebar a.nav-h2{font-weight:600;padding-left:14px}.supplier-sidebar a.endpoint-nav{padding-left:24px;font-size:12px;color:#64748b}.supplier-sidebar .nav-submenu{margin:3px 0 7px 10px;padding:0;border-left:1px solid #cbd5e1}.supplier-sidebar .nav-submenu-title{display:flex;align-items:center;gap:4px;padding:6px 8px;color:#334155;font-weight:700;font-size:12px}.supplier-sidebar .nav-submenu-count{margin-left:auto;min-width:20px;padding:0 6px;border-radius:10px;background:#eef2f7;color:#64748b;font-size:10px;line-height:18px;text-align:center}.supplier-sidebar .nav-submenu a.endpoint-nav{padding-left:18px}
.supplier-main{flex:1;min-width:0;background:#fff;border:1px solid #e4e7eb;border-radius:8px;padding:32px 40px;margin-bottom:60px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.intro h1,.category-block>h1{font-size:28px;border-bottom:3px solid #1e40af;padding:0 0 10px;margin:64px 0 0;color:#1e3a8a;font-weight:700;background:none}.intro:first-child h1{margin-top:0}.category-description{margin:12px 0;color:#475569}
.intro h2,.api-section h2{font-size:22px;background:linear-gradient(90deg,#dbeafe 0%,transparent 100%);border-left:4px solid #1e40af;padding:8px 12px;margin:40px 0 0;color:#1e3a8a;font-weight:600}
.api-section{scroll-margin-top:24px}.endpoint-line{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:12px 0}.endpoint-line>code{background:#dbeafe;color:#1d4ed8;padding:2px 6px;border-radius:3px;font-size:13px;font-family:"SF Mono",Consolas,monospace}
.supplier-main p{margin:12px 0}.supplier-main code{background:#dbeafe;color:#1d4ed8;padding:2px 6px;border-radius:3px;font-size:13px;font-family:"SF Mono",Consolas,monospace}
.supplier-main pre{background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;overflow-x:auto;font-size:13px;line-height:1.5}
.supplier-main pre code{display:block!important;background:none!important;color:#e2e8f0!important;padding:0!important;border:0!important;border-radius:0!important;font-size:13px!important;white-space:pre!important;font-weight:400!important}
.table-wrap{overflow-x:auto;margin:12px 0}.supplier-main table{border-collapse:collapse;margin:0;width:100%;font-size:13px}.supplier-main th,.supplier-main td{border:1px solid #e4e7eb;padding:6px 10px;text-align:left;vertical-align:top}.supplier-main th{background:#f1f5f9;font-weight:600;white-space:nowrap}.supplier-main tr:nth-child(even) td{background:#fafbfc}.supplier-list-query-params th:first-child,.supplier-list-query-params td:first-child{width:180px;min-width:180px}.resource-relation-params th:first-child,.resource-relation-params td:first-child{width:200px;min-width:200px}
.supplier-main ul,.supplier-main ol{padding-left:24px}.supplier-main li{margin:3px 0}.supplier-main a{color:#1e40af;text-decoration:none}.supplier-main a:hover{border-bottom:1px solid #1e40af}.supplier-main strong{color:#1e3a8a}.supplier-main hr{border:none;border-top:1px solid #e4e7eb;margin:24px 0}
.method-badge{display:inline-block;min-width:auto;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:600;font-family:"SF Mono",monospace;margin-right:6px;vertical-align:middle}.method-get{background:#dcfce7;color:#15803d}.method-post{background:#dbeafe;color:#1d4ed8}.method-put{background:#fef3c7;color:#a35d00}.method-patch{background:#ede9fe;color:#6d28d9}.method-delete{background:#fee2e2;color:#b91c1c}
.status-badge{font-size:11px;border-radius:3px;padding:2px 7px;margin-left:auto;font-weight:600}.status-badge.current{background:#dcfce7;color:#15803d}.status-badge.extend{background:#dbeafe;color:#1d4ed8}.status-badge.out,.status-badge.p2{background:#fef3c7;color:#a35d00}
.notice,.warning,.trace,.contract-note{border-left:4px solid #1e40af;background:#dbeafe;margin:16px 0;padding:12px 16px;color:#475569;border-radius:0 6px 6px 0}.warning{border-left-color:#d97706;background:#fef3c7;color:#92400e}.trace{font-size:12px;background:#f1f5f9;border-left-color:#94a3b8}.branch-label{margin:16px 0 6px!important;color:#475569}.empty,.sub-label{color:#64748b}
.summary-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:16px 0}.summary-card{border:1px solid #e4e7eb;border-radius:6px;padding:12px;background:#fafbfc}.summary-card strong{display:block;font-size:20px}.detail-tabs{display:flex;flex-wrap:wrap;margin:20px 0 12px;border-bottom:1px solid #94a3b8}.detail-tabs a{display:block;padding:10px 18px;border:1px solid #94a3b8;border-bottom:0;background:#f8fafc;color:#1f2933!important;font-weight:600}.detail-tabs a:first-child{background:#0e7490;color:#fff!important}.detail-tabs a:hover{background:#dbeafe;border-bottom:0!important}.detail-split-map{margin:18px 0;padding:16px;border:1px solid #bfdbfe;border-radius:8px;background:#f8fbff}.detail-split-map h3{margin:0 0 12px;color:#1e3a8a;font-size:16px}.detail-split-root{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;padding:12px;border-radius:6px;background:#1e40af;color:#fff;text-align:center}.detail-split-root strong{color:#fff}.detail-split-root code{background:rgba(255,255,255,.18);color:#fff}.detail-split-branches{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:18px}.detail-split-item{position:relative;display:flex;min-width:0;flex-direction:column;gap:6px;padding:12px 10px;border:1px solid #bfdbfe;border-radius:6px;background:#fff;text-align:center}.detail-split-item:before{content:"";position:absolute;top:-19px;left:50%;height:18px;border-left:2px solid #93c5fd}.detail-split-index{align-self:center;width:24px;height:24px;border-radius:50%;background:#dbeafe;color:#1d4ed8;font-weight:700;line-height:24px}.detail-split-item code{display:block;overflow-wrap:anywhere;font-size:11px}.detail-split-note{margin:12px 0 0!important;color:#475569;text-align:center}.toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.toolbar button{border:0;border-radius:4px;padding:7px 12px;background:#1d4ed8;color:#fff;font-size:13px;cursor:pointer}.toolbar button.secondary{background:#475569}details{border:1px solid #e4e7eb;border-radius:6px;padding:10px 12px;margin-top:12px}details summary{cursor:pointer;color:#1e40af;font-weight:600}.footer{margin-top:40px;padding-top:16px;border-top:1px solid #e4e7eb;text-align:center;color:#64748b;font-size:12px}.search-hidden{display:none!important}
.detail-split-branches{grid-template-columns:repeat(2,minmax(0,1fr))}
@media(max-width:1024px){.supplier-layout{flex-direction:column;padding:16px}.supplier-sidebar{position:static;width:100%;max-height:320px;flex:none}.supplier-main{width:100%;padding:24px 20px}.status-badge{margin-left:0}.summary-grid{grid-template-columns:repeat(2,1fr)}.detail-split-branches{grid-template-columns:1fr}.detail-split-item:before{display:none}}
@media print{body.supplier-api-page{background:#fff}.supplier-header{background:#fff!important;color:#111;padding:15px 0;box-shadow:none}.supplier-header p{color:#333}.supplier-layout{display:block;margin:0;padding:0}.supplier-sidebar,.toolbar{display:none!important}.supplier-main{border:0;box-shadow:none;padding:0}.api-section{break-inside:avoid}.supplier-main pre{white-space:pre-wrap;background:#f5f5f5;color:#111;border:1px solid #ddd}.supplier-main pre code{color:#111!important;white-space:pre-wrap!important}}
</style>
<header class="supplier-header">
<h1>供应商模块 API 接口规范</h1>
<p>v2.1 前端联调版 · 2026-08-25 · ${activeCount} 个 HTTP 契约 · OpenAPI 3.0.3 · TEST 验收状态</p>
</header>
<div class="supplier-layout">
<aside class="supplier-sidebar" aria-label="接口大纲">
${sidebarGeneral}${sidebarCategories}
</aside>
<main class="supplier-main">
<section class="intro" id="overview"><h1>0. 文档总则</h1><h2 id="positioning">0.1 文档定位</h2>
<div class="notice"><strong>前端联调版:</strong>本文基于 v2.0 契约并标注当前 TEST 实现状态。${activeCount} 个接口进入本期 OpenAPI;SUP-ADM-010 仅失败关闭,不能据此推断 Finance 清账成功链路已开放。</div>
<div class="notice"><strong>审批分期:</strong>本期实现审批表、<code>SupplierApprovalProvider</code>、<code>LocalAutoApprovalProvider</code>、标准化结果和统一结果应用器;LOCAL_AUTO 明确标记为系统自动决策,企微专属字段保持空。后续 WECOM 只增加 Provider 适配、Feign、回调和对账。</div>
<p><strong>一期主要业务需求:</strong><a href="./供应商模块一期实施拆分详细设计-v1.0.html#overview">打开《供应商模块一期主要业务规格说明书 SRS v1.0》</a>;SRS 保留原需求来源,供应商详情仅保留基本信息、账号信息两个页签,以本文当前契约为准。</p>
<div class="summary-grid"><div class="summary-card"><strong>${endpoints.length}</strong>HTTP 契约总数</div><div class="summary-card"><strong>${activeCount}</strong>本期 OpenAPI</div><div class="summary-card"><strong>${spec["x-hl-codegen-validation"].serverOperationCount}</strong>服务端 operation</div><div class="summary-card"><strong>${spec["x-hl-codegen-validation"].outboundClientOperationCount}</strong>企微出站 operation</div></div>
<nav class="detail-tabs" aria-label="供应商详情两个页签">${supplierDetailSections.map(function(category){const categoryIndex=categories.indexOf(category);return "<a href='#category-"+(categoryIndex+1)+"'>"+esc(category.title)+"</a>";}).join("")}</nav>
<div class="notice"><strong>两页签口径:</strong>供应商详情只包含基本信息和账号信息;不展示资源信息,也不提供资源绑定或解绑入口。资源关系由资源与车队模块在各自资源页面维护。</div>
<p>所有管理端接口统一使用 <code>/admin/supplier/**</code>,需另行补充 Gateway 路由到 <code>hl-resource-service</code> 并通过 <code>GatewayRouteAuditTest</code>。Internal 接口不允许外部访问。</p>
<div class="notice"><strong>平台字典:</strong>供应商页面统一使用平台字典并按需加载。供应商类型读取 <code>supplier_type</code>,生命周期读取 <code>supplier_lifecycle_status</code>;业务接口只传编码,不传中文标签或字典数据 ID。</div>
</section>
<section class="intro" id="catalog"><h2>0.2 供应商详情两个页签</h2><div class="table-wrap"><table><thead><tr><th>序号</th><th>详情页页签</th><th>接口数</th><th>可生成</th><th>用途</th></tr></thead><tbody>${detailTabRows}</tbody></table></div><div class="notice"><strong>资源维护入口:</strong>“资源侧供应商维护”是独立接口分组,不是供应商详情第三个页签。</div></section>
<section class="intro" id="auth"><h2>0.3 菜单与权限配置</h2>
<ul><li>管理端请求先由 Gateway 认证,资源服务只从可信请求属性读取 <code>X-Admin-Id</code>,不接收客户端传入的管理员身份或权限。</li><li>供应商菜单、页面和按钮由平台配置 <code>supplier:*</code> 权限码;接口文档不定义固定角色,服务端只按当前 operation 的权限码授权。</li><li>${adminActiveCount} 个本期 <code>/admin/supplier/**</code> operation 复用 <code>hl-common-feign/.../UserFeignClient.java</code> 逐项校验 <code>x-hl-permissions</code>;返回 false、非成功 Result、超时或异常一律拒绝。</li><li>平台权限不能替代数据范围、状态机、归属、并发、敏感字段裁剪与业务 Guard;隐藏按钮也不能替代服务端授权。</li><li><code>/internal/**</code> 仅接受 <code>X-Internal-Token</code>,不得由 Gateway 暴露。</li></ul>
</section>
<section class="intro" id="common-contract"><h2>0.4 通用请求与响应</h2>
<p><strong>时间与 ID</strong>:Snowflake <code>Long</code> 在 JSON 中使用字符串;<code>LocalDateTime</code> 为 <code>yyyy-MM-dd HH:mm:ss</code>,<code>LocalDate</code> 为 <code>yyyy-MM-dd</code>。</p>
<p><strong>成功包络</strong>:</p><pre><code class="language-json">{
"code": 200,
"message": "成功",
"success": true,
"data": {}
}</code></pre>
<p><strong>分页 data</strong>:</p><pre><code class="language-json">{
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
}</code></pre>
<div class="notice"><strong>出参类型与 Controller 返回语句:</strong>接口条目中的 <code>Result&lt;T&gt;</code>、<code>Result&lt;PageResult&lt;T&gt;&gt;</code> 是 Java 方法签名及 HTTP 响应包络类型;<code>Result.success(service.xxx())</code> 是 Controller 方法体中的返回语句。两者同时存在,不能用返回语句替换文档中的出参类型。<code>Result.success(T data)</code> 会根据 Service 返回值自动推导泛型,最终 JSON 仍按上面的统一包络输出。</div>
<div class="notice"><strong>接口路径命名规则</strong>:查询列表路径以 <code>/list</code> 结尾,分页查询以 <code>/page</code> 结尾,添加/创建/新增以 <code>/add</code> 结尾,修改/更改/编辑/设置以 <code>/update</code> 结尾,删除/软删除以 <code>/del</code> 结尾,详情以 <code>/view</code> 结尾。提交、归档和校验接口保留各自专属动作路径。</div>
<p><strong>分页接口标准写法</strong>:</p><pre><code class="language-java">@GetMapping("/page")
public Result&lt;PageResult&lt;SupplierListItemVO&gt;&gt; page(
@Valid SupplierListQuery query) {
return Result.success(supplierService.page(query));
}</code></pre>
<p><strong>详情接口标准写法(基本信息)</strong>:</p><pre><code class="language-java">@GetMapping("/{supplierId}/basic-info/view")
public Result&lt;SupplierBasicInfoVO&gt; detail(
@PathVariable Long supplierId) {
return Result.success(supplierService.detail(supplierId));
}</code></pre>
<p><strong>文档标注规则</strong>:接口卡片直接展示 HTTP 方法与完整路径,不额外展示方法命名;分页接口使用对应的 <code>Result&lt;PageResult&lt;...&gt;&gt;</code>;三个详情页签分别使用独立 VO,资源关系使用 <code>SupplierResourceRelationVO</code>。Controller 统一使用 <code>return Result.success(serviceMethod(...));</code> 包装 Service 返回值。</p>
</section>
<section class="intro" id="frontend-dicts-contract"><h2>0.5 前端字典使用约定</h2>
<div class="notice"><strong>前端实现规则:</strong>下拉框、筛选项、表格状态和详情标签从平台字典读取;不得在供应商页面组件内另写一套中文枚举。业务请求与响应始终使用 <code>dictValue</code>。</div>
<div class="table-wrap"><table><thead><tr><th>业务字段</th><th>字典类型</th><th>前端用途</th><th>提交值</th></tr></thead><tbody>
<tr><td><code>typeCode</code> / <code>types[].typeCode</code></td><td><code>supplier_type</code></td><td>建档多选、列表筛选、列表与详情翻译</td><td><code>dictValue</code></td></tr>
<tr><td><code>status</code></td><td><code>supplier_lifecycle_status</code></td><td>列表筛选、列表与详情生命周期翻译</td><td><code>dictValue</code></td></tr>
<tr><td><code>resourceModule</code></td><td>后端固定枚举,非平台字典</td><td>资源页面路由校验和菜单原文展示</td><td>10 个冻结编码之一</td></tr>
</tbody></table></div>
<p><strong>按需加载:</strong></p>
<ul><li>供应商类型:<code>GET /admin/dict/data/supplier_type</code></li><li>供应商生命周期:<code>GET /admin/dict/data/supplier_lifecycle_status</code></li></ul>
<p>两个接口均经 Gateway 访问并需要管理员认证;页面进入相关筛选、表单或详情时按需请求,不调用全量字典接口。</p>
<p><strong>组件映射:</strong><code>label = dictLabel</code>,<code>value = dictValue</code>。按需接口只返回启用项,并按 <code>sortOrder ASC</code>、<code>dictDataId ASC</code> 排序。提交时不得发送中文、<code>dictDataId</code> 或整条字典对象。字典数据项自身的 <code>status=ACTIVE</code> 仅表示“字典项启用”,供应商生命周期必须读取 <code>dictValue</code>。</p>
<pre><code class="language-javascript">const options = dictData.map(item =&gt; ({
label: item.dictLabel,
value: item.dictValue
}))</code></pre>
<p><strong>建档提交示例:</strong><code>types: [{ typeCode: "HOTEL" }, { typeCode: "RESTAURANT" }]</code>。请求不发送 <code>typeName</code>;列表和详情响应中的 <code>typeName</code> 由服务端返回,仅作显示快照。</p>
<p><strong>当前供应商类型:</strong><code>SCENIC</code>=景区,<code>RESTAURANT</code>=餐厅,<code>SUPPLIES</code>=备品,<code>ACTIVITY</code>=游玩项目,<code>HOTEL</code>=酒店,<code>SERVICE</code>=服务,<code>FLEET</code>=车队,<code>RENTAL</code>=租车,<code>PERFORMANCE</code>=演艺,<code>INSURANCE</code>=保险,<code>CHANNEL</code>=OTA/渠道,<code>PROFESSIONAL_SERVICE</code>=专业服务,<code>PROPERTY</code>=物业/房租,<code>TICKET</code>=票务,<code>OTHER</code>=其他。</p>
<p><strong>当前生命周期:</strong><code>DRAFT</code>=草稿,<code>VETTING</code>=注册审核中,<code>ACTIVE</code>=合作中,<code>SUSPENDED</code>=暂停,<code>FROZEN</code>=冻结,<code>BLACKLIST</code>=黑名单,<code>ARCHIVED</code>=清账归档。</p>
<p><strong>资源模块:</strong><code>SCENIC</code>、<code>RESTAURANT</code>、<code>SUPPLIES</code>、<code>SUPPLIES_COMBO</code>、<code>ACTIVITY</code>、<code>HOTEL</code>、<code>SERVICE</code>、<code>COST_ITEM</code>、<code>STAFF</code>、<code>VEHICLE</code>;它不是平台字典,VEHICLE 的菜单原文为“车队管理-车队管理”。<code>FLEET</code> 仍可作为供应商类型/资格编码,但供应商侧不新增车队或挂接车辆。</p>
<div class="warning"><strong>状态按钮不是普通字典下拉:</strong>状态筛选和展示使用完整生命周期字典;状态变更按钮必须按第 4 节状态机和服务端返回能力收窄目标,不能让用户任意选择 7 个状态。</div>
<div class="notice"><strong>单一维护口径:</strong><code>supplier_type</code> 是供应商类型的唯一维护来源,本文不提供供应商类型增删改查接口。</div>
</section>
<section class="intro" id="error-convention"><h2>0.6 错误码约定</h2><p>参数与认证错误可使用对应 HTTP 状态;业务失败可能仍返回 HTTP 200。调用方必须同时检查 <code>code</code>、<code>message</code>、<code>success</code> 和 <code>data</code>,不能只看 HTTP 状态。</p></section>
<section class="intro" id="codegen"><h2>0.7 代码生成契约</h2>
<div class="notice" id="supplierOpenApiStatus">OpenAPI 校验 PASS:${activeCount} 个 operation,${Object.keys(schemas).length} 个核心 Schema,${spec["x-hl-codegen-validation"].redisShortWindowOperationCount} 个 Redis 短窗防重写操作;Guard / 状态机 / 事务 / 锁 / 幂等扩展齐全。</div>
<div class="toolbar"><button id="supplierDownloadOpenApi">下载 OpenAPI JSON</button><button id="supplierCopyOpenApi" class="secondary">复制 OpenAPI JSON</button><button id="supplierPrint" class="secondary">打印 / 导出 PDF</button></div>
<details><summary>查看完整 OpenAPI 3.0.3</summary><pre id="supplierOpenApiPreview"><code>${esc(specJson)}</code></pre></details>
</section>
${categoryContent}
<p class="footer">供应商模块 API 接口规范 v2.1 前端联调版 · 发行日期 2026-08-25 · 单文件离线文档</p>
</main>
</div>`;
document.getElementById("supplierDownloadOpenApi").addEventListener("click",function(){
const url=URL.createObjectURL(new Blob([specJson],{type:"application/json;charset=utf-8"}));
const link=document.createElement("a"); link.href=url; link.download="supplier-api-openapi-v2.0.json"; document.body.appendChild(link); link.click(); link.remove(); setTimeout(function(){URL.revokeObjectURL(url);},1000);
});
document.getElementById("supplierCopyOpenApi").addEventListener("click",function(){
const status=document.getElementById("supplierOpenApiStatus");
const fallback=function(){const area=document.createElement("textarea");area.value=specJson;area.style.position="fixed";area.style.opacity="0";document.body.appendChild(area);area.select();document.execCommand("copy");area.remove();status.textContent="已复制 OpenAPI JSON("+activeCount+" 个 operation)。";};
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(specJson).then(function(){status.textContent="已复制 OpenAPI JSON("+activeCount+" 个 operation)。";}).catch(fallback);}else fallback();
});
document.getElementById("supplierPrint").addEventListener("click",function(){window.print();});
const navLinks=Array.from(document.querySelectorAll(".supplier-sidebar a[href^='#']"));
const navPairs=navLinks.map(function(link){return {link:link,target:document.getElementById(link.getAttribute("href").slice(1))};}).filter(function(item){return item.target;});
const supplierSidebar=document.querySelector(".supplier-sidebar");
let navFrame=0;
function keepSupplierNavVisible(link){
if(!supplierSidebar||!link) return;
const sidebarRect=supplierSidebar.getBoundingClientRect();
const linkRect=link.getBoundingClientRect();
const inset=12;
if(linkRect.top<sidebarRect.top+inset) supplierSidebar.scrollTop-=sidebarRect.top+inset-linkRect.top;
else if(linkRect.bottom>sidebarRect.bottom-inset) supplierSidebar.scrollTop+=linkRect.bottom-(sidebarRect.bottom-inset);
}
function updateSupplierNav(){
navFrame=0; let current=navPairs[0];
const scrollPaddingTop=parseFloat(window.getComputedStyle(document.documentElement).scrollPaddingTop)||0;
navPairs.forEach(function(item){
const scrollMarginTop=parseFloat(window.getComputedStyle(item.target).scrollMarginTop)||0;
if(item.target.getBoundingClientRect().top<=scrollPaddingTop+scrollMarginTop+1) current=item;
});
navLinks.forEach(function(link){link.classList.toggle("active",!!current&&link===current.link);});
if(current){
keepSupplierNavVisible(current.link);
}
}
window.addEventListener("scroll",function(){if(!navFrame) navFrame=window.requestAnimationFrame(updateSupplierNav);},{passive:true});
window.addEventListener("hashchange",function(){if(!navFrame) navFrame=window.requestAnimationFrame(updateSupplierNav);});
navLinks.forEach(function(link){link.addEventListener("click",function(){
navLinks.forEach(function(item){item.classList.toggle("active",item===link);});
keepSupplierNavVisible(link);
});});
updateSupplierNav();
if(location.hash){
const initialLink=navLinks.find(function(link){return link.getAttribute("href")===location.hash;});
const initialTarget=document.getElementById(location.hash.slice(1));
if(initialLink&&initialTarget){
window.requestAnimationFrame(function(){initialTarget.scrollIntoView();updateSupplierNav();});
}
}
}
renderEndpoints(); renderMetrics(); renderErrors(); applyFilter(); initOpenApi(); initNavHighlight();
["search","groupFilter","statusFilter"].forEach(function(id){
document.getElementById(id).addEventListener(id==="search"?"input":"change",applyFilter);
});
document.getElementById("printBtn").addEventListener("click",function(){window.print();});
renderFleetStyleDocument();
</script>
</body>
</html>