比较提交
32
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
3d6f6245a0 | ||
|
|
ff16cfd94c | ||
|
|
2e18de27e1 | ||
|
|
6461e1ce39 | ||
|
|
48b1d069c8 | ||
|
|
36a85b8669 | ||
|
|
43b9c16a36 | ||
|
|
fd76eea701 | ||
|
|
49f67134fd | ||
|
|
314c228654 | ||
|
|
3e8ea93caf | ||
|
|
33c0aa2c08 | ||
|
|
6831ec3128 | ||
|
|
42a16b78c6 | ||
|
|
257ecb1233 | ||
|
|
e9b52319c9 | ||
|
|
4060c65261 | ||
|
|
0fcf0801cf | ||
|
|
e1b0a667cd | ||
|
|
c3acb6e822 | ||
|
|
8bdd0d9057 | ||
|
|
1eafac1a59 | ||
|
|
ded9289d98 | ||
|
|
119c459808 | ||
|
|
06fa788882 | ||
|
|
a74320185f | ||
|
|
e2b36e1b60 | ||
|
|
3ae7d06158 | ||
|
|
0c3b9056e6 | ||
|
|
6b3d84ba7b | ||
|
|
491adfc28e | ||
|
|
2cbdcfb13a |
+3
-3
@@ -2,7 +2,7 @@
|
||||
# changelog 发布门禁(推送前强制校验)
|
||||
# 启用(每台机一次): git config core.hooksPath .githooks
|
||||
# 拦截目标: 接口类 changelog 未部署测试服(backend_status != deployed)就推送给前端,
|
||||
# 以及文件名/frontmatter 结构违规。规则实现见 scripts/validate-changelog-*.mjs。
|
||||
# 以及文件名/frontmatter/CHANGELOG_TEMPLATE.md 结构违规。规则实现见 scripts/validate-changelog-*.mjs。
|
||||
zero=0000000000000000000000000000000000000000
|
||||
status=0
|
||||
while read local_ref local_sha remote_ref remote_sha; do
|
||||
@@ -19,7 +19,7 @@ while read local_ref local_sha remote_ref remote_sha; do
|
||||
done
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "" >&2
|
||||
echo "推送被 changelog 发布门禁拦截:接口类条目必须测试服已部署+实测(backend_status=deployed)后才能推送给前端。" >&2
|
||||
echo "修正文件后重试;规则详见 BACKEND_CHANGELOG_DELIVERY_GUIDE.md §2.1。" >&2
|
||||
echo "推送被 changelog 发布门禁拦截:接口类条目必须已部署实测,并完整仿照 CHANGELOG_TEMPLATE.md。" >&2
|
||||
echo "每个接口需自含入参、出参、请求/响应、错误和业务边界;修正规则详见 BACKEND_CHANGELOG_DELIVERY_GUIDE.md。" >&2
|
||||
fi
|
||||
exit $status
|
||||
|
||||
@@ -25,7 +25,8 @@ changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理
|
||||
|
||||
## 2. 写什么
|
||||
|
||||
可以复制仓库根目录的 `CHANGELOG_TEMPLATE.md`,至少写清:
|
||||
必须复制并按仓库根目录的 `CHANGELOG_TEMPLATE.md` 组织接口文档。接口类 Changelog 不能只写
|
||||
路径和变更摘要,至少写清:
|
||||
|
||||
- 关联的 Issue 和后端 PR;
|
||||
- 接口路径和 HTTP 方法;
|
||||
@@ -34,6 +35,11 @@ changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理
|
||||
- 前端需要做什么;
|
||||
- 后端测试、部署和网关验证结果。
|
||||
|
||||
多接口条目必须让每个接口小节独立包含入参、出参、请求示例、响应示例、错误响应和业务边界;
|
||||
“变更接口清单”的 `METHOD /path` 必须与逐接口详情一一对应。该结构由
|
||||
`check:frontmatter` 机器校验,缺失时分别报 `E_API_TEMPLATE`、`E_API_ENDPOINTS` 或
|
||||
`E_API_DETAIL`。存量接口文档一旦修改,也必须升级到当前模板标准。
|
||||
|
||||
元数据中:
|
||||
|
||||
```yaml
|
||||
|
||||
+12
-2
@@ -3,7 +3,8 @@ schema: "hl-changelog/v2"
|
||||
ticket: "{issue-no}"
|
||||
title: "{一句话概括变化}"
|
||||
consumer: "{admin|mp|internal|multiple}"
|
||||
author: "{推送者登录名}(GIT)" # 如 wx(GIT)/yst(GIT),谁 push 到 main 就写谁
|
||||
# author 示例: wx(GIT) / yst(GIT);谁 push 到 main 就写谁
|
||||
author: "{推送者登录名}(GIT)"
|
||||
change_type: "{新增接口|修改接口|删除接口}"
|
||||
backend_status: "pending"
|
||||
gateway_status: "pending"
|
||||
@@ -67,6 +68,10 @@ base: "{dev|dev-v3}"
|
||||
|
||||
**VO**: `{VO 类名}`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
说明前端在什么页面、什么时机调用;多接口条目中每个接口都必须自包含,不依赖其他章节补参数或错误语义。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
@@ -113,9 +118,14 @@ base: "{dev|dev-v3}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 写清本接口自己的鉴权、空值、状态、幂等/并发、失败零写入和兼容规则。
|
||||
- 不要只在全局章节写一次;消费方应能单独阅读本接口小节完成联调。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(选填,字段互斥/联动/切换场景必写)
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
|
||||
+18
@@ -75,6 +75,24 @@ pending → claimed → implemented → released → verified
|
||||
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
|
||||
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
|
||||
|
||||
## 接口文档模板门禁
|
||||
|
||||
新增、修改或删除接口的 Changelog 必须以仓库根目录
|
||||
[`CHANGELOG_TEMPLATE.md`](CHANGELOG_TEMPLATE.md) 为结构基线,不能只写接口路径和一段变更摘要。
|
||||
|
||||
机器校验要求:
|
||||
|
||||
- 使用“二、变更接口清单”的标准六列表格;
|
||||
- 清单中的每个 `METHOD /path` 都必须在“三、接口详情”中有且只有一个对应小节;
|
||||
- 每个接口小节必须自含入参字段表、出参字段表、请求示例、响应示例、错误响应和业务边界;
|
||||
- 含写接口时必须说明外部可观察的数据库行为,但不得泄露物理表结构;
|
||||
- 修改或删除接口必须补“修改前后对比”和“影响评估”;
|
||||
- 必须保留契约约束、边界行为、不影响范围、TEST 验证、相关文档及联系人章节。
|
||||
|
||||
缺少上述内容时 `check:frontmatter` 返回 `E_API_TEMPLATE`、`E_API_ENDPOINTS` 或
|
||||
`E_API_DETAIL`,PR/push 检测失败。存量文件不全量追责,但任何接口类文件一旦新增或修改,
|
||||
就必须补到当前模板标准。
|
||||
|
||||
已下发路径是消费契约的一部分,不通过重命名表达状态。历史路径已发生迁移时:
|
||||
|
||||
- `changelog-path-aliases.json` 是机器可识别的唯一映射源;
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<main>
|
||||
<header class="hero">
|
||||
<h1>供应商模块 API 接口规范</h1>
|
||||
<p>发行版 v2.1 · 基线 v2.0 / 前端联调版 2026-08-23 · 已部署 TEST</p>
|
||||
<p>发行版 v2.1 · 基线 v2.0 / 前端联调版 2026-08-25 · 已部署 TEST</p>
|
||||
<p>供前端联调使用;契约基线来自 v2.0,运行状态以当前 TEST 验收结论为准。</p>
|
||||
<div class="chips">
|
||||
<span class="chip">后端 only</span>
|
||||
@@ -584,24 +584,25 @@
|
||||
<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<SupplierContactInput></td><td>选填</td><td>逐条校验</td></tr>
|
||||
<tr><td>qualifications</td><td>List<SupplierQualificationInput></td><td>选填</td><td>提交时按所有类型必备规则并集执行 C-09</td></tr>
|
||||
<tr><td>qualifications</td><td>List<SupplierQualificationInput></td><td>选填</td><td>提交时按所有类型必备规则并集执行 C-09;每项以 permanentValid + expiryDate 表达有效期</td></tr>
|
||||
<tr><td>initialAccounts</td><td>List<SupplierBankAccountInput>(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)、expiryDate?</td><td>isRequired 由服务端规则派生;客户端不得传 isRequired</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?、资质字段?、expectedUpdateTime?</td><td>已有项必须同时携带 qualificationId/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>
|
||||
@@ -612,7 +613,7 @@
|
||||
<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、信用、状态、updateTime</td><td>isRelatedParty、approveNote、完整账号</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>
|
||||
@@ -735,7 +736,7 @@
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<p class="footer">发行日期:2026-08-23 · 基线:v2.0 契约基线 · 前端联调版;归档成功依赖尚未接入。</p>
|
||||
<p class="footer">发行日期:2026-08-25 · 基线:v2.0 契约基线 · 前端联调版;归档成功依赖尚未接入。</p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -757,23 +758,23 @@
|
||||
[],"详细设计 §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>",
|
||||
["仅返回主体档案、多类型、联系人、资质、信用和生命周期状态","前端按 supplier_type 翻译 types[].typeCode,按 supplier_lifecycle_status 翻译 status;不得在详情组件写死中文","不返回账号或资源字段;三个详情页签接口互不混装","SupplierBasicInfoVO 不返回 isRelatedParty 和 approveNote;两字段仅保留在写入、候选快照和服务端业务处理中"],
|
||||
["仅返回主体档案、多类型、联系人、资质、信用和生命周期状态","资质同时返回 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 提交注册审批使用","创建 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"],
|
||||
["保存供应商注册表单草稿,供后续 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 未传时保持不变,一旦传入即代表该集合的完整当前快照","集合对账规则固定为:已有记录携带稳定 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"],
|
||||
["用于补全或更新供应商资料;供应商注册草稿可能只录入最低必填信息,除 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>",
|
||||
["提交供应商注册表单,生成注册审批流水,并使供应商进入合作中状态","仅 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"],"详细设计 §5.2/§5.4/§5.11/§6;数据模型 supplier_main/supplier_approval_log/supplier_change_log","已实现 · 可联调"),
|
||||
["提交供应商注册表单,生成注册审批流水,并使供应商进入合作中状态","提交表单的资质 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 不可恢复,历史引用和审批流水不得删除"],
|
||||
@@ -901,7 +902,8 @@
|
||||
["SUPPLIER_RESOURCE_ALREADY_BOUND",395036,"资源已经存在其他有效供应商关系","该资源已关联其他供应商,请使用改绑"],
|
||||
["SUPPLIER_RESOURCE_TYPE_MISMATCH",395037,"目标供应商缺少 requiredTypeCode 对应的有效类型,或删除了仍被关系引用的类型","供应商类型不满足资源关联要求"],
|
||||
["SUPPLIER_RESOURCE_RELATION_NOT_FOUND",395038,"关联关系不存在、已解绑或不属于指定供应商","供应商资源关联不存在"],
|
||||
["SUPPLIER_RESOURCE_DEPENDENCY_UNAVAILABLE",395039,"VEHICLE 等资源摘要只读依赖超时、异常或返回非成功结果","暂时无法校验资源,请稍后重试"]
|
||||
["SUPPLIER_RESOURCE_DEPENDENCY_UNAVAILABLE",395039,"VEHICLE 等资源摘要只读依赖超时、异常或返回非成功结果","暂时无法校验资源,请稍后重试"],
|
||||
["SUPPLIER_QUALIFICATION_VALIDITY_INVALID",395042,"permanentValid 与 expiryDate 组合矛盾","永久有效标记与到期日期不一致"]
|
||||
];
|
||||
|
||||
function esc(value){
|
||||
@@ -990,7 +992,7 @@
|
||||
}
|
||||
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(){return {type:"string",format:"date",example:"2026-08-18"};}
|
||||
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;
|
||||
@@ -1034,12 +1036,12 @@
|
||||
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"}),expiryDate:localDateSchema()},["qualType"],{description:"isRequired 由服务端类型规则派生,客户端不得传入;certNo 以 AES-GCM 密文保存,普通返回仅 certNoMask"});
|
||||
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"}),expiryDate:localDateSchema(),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"资质集合快照项;已有记录携带 qualificationId 和 expectedUpdateTime,无 qualificationId 时新增;数据库有效资质中未出现在快照内的记录软删除。","x-hl-existing-record-requires":["qualificationId","expectedUpdateTime"],"x-hl-create-omits":["qualificationId","expectedUpdateTime"],"x-hl-create-required":["qualType"]});
|
||||
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"});
|
||||
@@ -1069,7 +1071,7 @@
|
||||
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(),certNoMask:textSchema(),imageUrl:textSchema(),expiryDate:localDateSchema(),isRequired:{type:"boolean"},expired:{type:"boolean"},updateTime:localDateTimeSchema()},["qualificationId","qualType","isRequired","expired","updateTime"]);
|
||||
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 时返回,否则省略"});
|
||||
@@ -1210,7 +1212,7 @@
|
||||
}
|
||||
function rootCodegenPolicy(){
|
||||
return {
|
||||
version:"supplier-frontend-v2.1-20260823",
|
||||
version:"supplier-frontend-v2.1-20260825",
|
||||
failOnMissingWritePolicy:true,
|
||||
failOnTodoOrNoopImplementation:true,
|
||||
serverOperationCount:16,
|
||||
@@ -1277,7 +1279,7 @@
|
||||
apiSections.forEach(function(section){tagDescriptions[section.title]=section.description;});
|
||||
const spec={
|
||||
openapi:"3.0.3",
|
||||
info:{title:"HL 供应商模块 API",version:"2.1-frontend-20260823",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 个失败关闭"},
|
||||
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:{},
|
||||
@@ -1567,7 +1569,7 @@
|
||||
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:"资质类型",certNo:"证照编号",certNoMask:"证照编号掩码",imageUrl:"图片地址",licenseImageUrl:"营业执照图片地址",qualificationFileUrls:"资质文件地址列表",isRequired:"是否必备",expired:"是否过期",warningDays:"预警天数",
|
||||
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:"预警提示列表",
|
||||
@@ -1913,7 +1915,7 @@
|
||||
</style>
|
||||
<header class="supplier-header">
|
||||
<h1>供应商模块 API 接口规范</h1>
|
||||
<p>v2.1 前端联调版 · 2026-08-23 · ${activeCount} 个 HTTP 契约 · OpenAPI 3.0.3 · TEST 验收状态</p>
|
||||
<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="接口大纲">
|
||||
@@ -1991,7 +1993,7 @@ public Result<SupplierBasicInfoVO> detail(
|
||||
<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-23 · 单文件离线文档</p>
|
||||
<p class="footer">供应商模块 API 接口规范 v2.1 前端联调版 · 发行日期 2026-08-25 · 单文件离线文档</p>
|
||||
</main>
|
||||
</div>`;
|
||||
document.getElementById("supplierDownloadOpenApi").addEventListener("click",function(){
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>供应商详情资源信息分页 API 接口规范 · v1.0</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #172033;
|
||||
--muted: #667085;
|
||||
--line: #dfe5ef;
|
||||
--soft: #f6f8fb;
|
||||
--blue: #1677ff;
|
||||
--blue-soft: #eaf3ff;
|
||||
--green: #15803d;
|
||||
--green-soft: #eaf8ef;
|
||||
--amber: #a15c00;
|
||||
--amber-soft: #fff7e6;
|
||||
--red: #b42318;
|
||||
--code: #101828;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background: #eef2f7;
|
||||
font: 14px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.page {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
margin: 28px auto 64px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 48px rgba(16, 24, 40, .08);
|
||||
overflow: hidden;
|
||||
}
|
||||
header {
|
||||
padding: 42px 48px 34px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #123d78, #1677ff 68%, #39a0ff);
|
||||
}
|
||||
header h1 { margin: 10px 0 8px; font-size: 30px; line-height: 1.3; }
|
||||
header p { margin: 0; opacity: .9; }
|
||||
.eyebrow { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; opacity: .78; }
|
||||
.badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 3px 10px;
|
||||
border: 1px solid rgba(255,255,255,.35);
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.14);
|
||||
font-size: 12px;
|
||||
}
|
||||
main { padding: 12px 48px 54px; }
|
||||
nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin: 0 -48px 24px;
|
||||
padding: 13px 48px;
|
||||
overflow-x: auto;
|
||||
background: rgba(255,255,255,.96);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
nav a { color: #344054; text-decoration: none; white-space: nowrap; font-size: 13px; }
|
||||
nav a:hover { color: var(--blue); }
|
||||
section { scroll-margin-top: 62px; padding-top: 22px; }
|
||||
h2 { margin: 0 0 14px; font-size: 22px; }
|
||||
h3 { margin: 24px 0 10px; font-size: 16px; }
|
||||
p { margin: 8px 0; }
|
||||
code {
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
background: var(--soft);
|
||||
color: #175cd3;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: .93em;
|
||||
}
|
||||
pre {
|
||||
margin: 12px 0;
|
||||
padding: 18px 20px;
|
||||
overflow: auto;
|
||||
border-radius: 10px;
|
||||
background: var(--code);
|
||||
color: #d1e9ff;
|
||||
font: 12px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
pre code { padding: 0; color: inherit; background: transparent; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 18px; }
|
||||
th, td { padding: 10px 12px; border: 1px solid var(--line); text-align: left; vertical-align: top; }
|
||||
th { background: var(--soft); color: #344054; font-weight: 600; }
|
||||
td:first-child code { white-space: nowrap; }
|
||||
.endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 12px 0 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #b9d8ff;
|
||||
border-radius: 10px;
|
||||
background: var(--blue-soft);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.method { padding: 4px 9px; border-radius: 6px; color: #fff; background: var(--blue); font-weight: 700; }
|
||||
.path { font: 600 14px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
|
||||
.callout {
|
||||
margin: 14px 0;
|
||||
padding: 13px 15px;
|
||||
border-left: 4px solid var(--blue);
|
||||
border-radius: 6px;
|
||||
background: var(--blue-soft);
|
||||
}
|
||||
.callout.ok { border-color: var(--green); background: var(--green-soft); }
|
||||
.callout.warn { border-color: #f59e0b; background: var(--amber-soft); }
|
||||
.callout strong { display: block; margin-bottom: 2px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 16px 0; }
|
||||
.card { padding: 15px; border: 1px solid var(--line); border-radius: 10px; background: #fff; }
|
||||
.card b { display: block; margin-bottom: 4px; color: #344054; }
|
||||
.card span { color: var(--muted); }
|
||||
.status-ok { color: var(--green); font-weight: 700; }
|
||||
.status-pending { color: var(--amber); font-weight: 700; }
|
||||
ul, ol { padding-left: 22px; }
|
||||
li + li { margin-top: 5px; }
|
||||
footer { padding: 24px 48px; border-top: 1px solid var(--line); background: var(--soft); color: var(--muted); }
|
||||
@media (max-width: 760px) {
|
||||
.page { width: 100%; margin: 0; border: 0; border-radius: 0; }
|
||||
header, main, footer { padding-left: 20px; padding-right: 20px; }
|
||||
nav { margin-left: -20px; margin-right: -20px; padding-left: 20px; padding-right: 20px; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
table { display: block; overflow-x: auto; white-space: nowrap; }
|
||||
}
|
||||
@media print {
|
||||
@page { size: A4; margin: 14mm; }
|
||||
body { background: #fff; }
|
||||
.page { width: 100%; margin: 0; border: 0; box-shadow: none; }
|
||||
header { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
||||
nav { display: none; }
|
||||
main { padding: 10px 0 20px; }
|
||||
footer { padding: 16px 0 0; }
|
||||
section { break-inside: avoid-page; }
|
||||
pre { white-space: pre-wrap; word-break: break-word; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="page">
|
||||
<header>
|
||||
<div class="eyebrow">HL Supplier API · Admin</div>
|
||||
<h1>供应商详情资源信息分页 API 接口规范</h1>
|
||||
<p>v1.0 · Issue #6316 · 2026-08-25</p>
|
||||
<div class="badges">
|
||||
<span class="badge">已实现 · 可联调</span>
|
||||
<span class="badge">后端 deployed</span>
|
||||
<span class="badge">Gateway verified</span>
|
||||
<span class="badge">前端 pending</span>
|
||||
<span class="badge">只读接口</span>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<nav aria-label="文档目录">
|
||||
<a href="#overview">概览</a>
|
||||
<a href="#request">请求</a>
|
||||
<a href="#response">响应</a>
|
||||
<a href="#fields">字段</a>
|
||||
<a href="#semantics">业务口径</a>
|
||||
<a href="#errors">错误码</a>
|
||||
<a href="#frontend">前端接入</a>
|
||||
<a href="#verification">验证</a>
|
||||
<a href="#rollback">撤回</a>
|
||||
</nav>
|
||||
|
||||
<section id="overview">
|
||||
<h2>1. 接口概览</h2>
|
||||
<div class="endpoint"><span class="method">GET</span><span class="path">/admin/supplier/items/{supplierId}/resource-info/page</span></div>
|
||||
<p>用于供应商管理详情页“资源信息”页签,分页展示该供应商当前有效关系对应的资源权威信息。关系来源为本系统 <code>supplier_resource_rel</code>;Resource 提供九类本地资源,Fleet 提供车辆资源。</p>
|
||||
<div class="grid">
|
||||
<div class="card"><b>实现状态</b><span class="status-ok">已实现 · 可联调</span></div>
|
||||
<div class="card"><b>调用方</b><span>管理后台;前端接入待完成</span></div>
|
||||
<div class="card"><b>数据副作用</b><span>无写库、Redis、MQ、配置或审计副作用</span></div>
|
||||
</div>
|
||||
<div class="callout ok"><strong>契约真相</strong>本文以合并提交 <code>4f032cd6f</code> 的 Controller、请求/响应 VO、Service 权限门禁及 TEST Gateway 验收为准。</div>
|
||||
</section>
|
||||
|
||||
<section id="request">
|
||||
<h2>2. 请求契约</h2>
|
||||
<h3>2.1 路径与查询参数</h3>
|
||||
<table>
|
||||
<thead><tr><th>参数</th><th>位置</th><th>类型</th><th>必填</th><th>约束与默认值</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>supplierId</code></td><td>path</td><td>string</td><td>是</td><td>正整数;Snowflake ID 必须按字符串传递</td></tr>
|
||||
<tr><td><code>page</code></td><td>query</td><td>integer</td><td>否</td><td>默认 1,最小 1;公共分页兼容 <code>pageNo</code></td></tr>
|
||||
<tr><td><code>pageSize</code></td><td>query</td><td>integer</td><td>否</td><td>默认 20,范围 1..100</td></tr>
|
||||
<tr><td><code>resourceModule</code></td><td>query</td><td>string</td><td>否</td><td>为空查询全部;非空按关系冻结模块精确筛选</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>2.2 支持的资源模块</h3>
|
||||
<table>
|
||||
<thead><tr><th>编码</th><th>moduleName</th><th>数据归属</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>SCENIC</code></td><td>景区管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>RESTAURANT</code></td><td>餐厅管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>SUPPLIES</code></td><td>备品管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>SUPPLIES_COMBO</code></td><td>组合配品</td><td>Resource</td></tr>
|
||||
<tr><td><code>ACTIVITY</code></td><td>游玩项目管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>HOTEL</code></td><td>酒店管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>SERVICE</code></td><td>服务管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>COST_ITEM</code></td><td>额外成本</td><td>Resource</td></tr>
|
||||
<tr><td><code>STAFF</code></td><td>服务人员管理</td><td>Resource</td></tr>
|
||||
<tr><td><code>VEHICLE</code></td><td>车队管理-车队管理</td><td>Fleet(内部批量聚合)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>2.3 调用示例</h3>
|
||||
<pre><code>GET /admin/supplier/items/2091715622923657217/resource-info/page?page=1&pageSize=20&resourceModule=SCENIC
|
||||
Authorization: Bearer <有效管理端访问令牌></code></pre>
|
||||
<div class="callout warn"><strong>权限是双门禁</strong>服务端仅允许 ADMIN、FINANCE、SUPER_ADMIN,并同时要求 <code>supplier:view</code> 与 <code>supplier:resource:view</code>。不能只通过隐藏页签代替服务端授权。</div>
|
||||
</section>
|
||||
|
||||
<section id="response">
|
||||
<h2>3. 响应结构</h2>
|
||||
<p>统一返回 <code>Result<PageResult<SupplierResourceInfoRespVO>></code>。业务失败通常仍是 HTTP 200,调用方必须检查 <code>code</code>、<code>success</code> 与 <code>message</code>。</p>
|
||||
<pre><code>{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"relationId": "2091715622923657218",
|
||||
"resourceModule": "SCENIC",
|
||||
"moduleName": "景区管理",
|
||||
"resourceId": "2091715622923657001",
|
||||
"resourceName": "示例景区",
|
||||
"coverUrl": null,
|
||||
"city": "海拉尔",
|
||||
"isCharged": true,
|
||||
"isChargedName": "是",
|
||||
"settleTypeCode": "CASH",
|
||||
"settleTypeName": "现付",
|
||||
"tags": [
|
||||
{ "tagId": "2084636804090089473", "tagName": "自然风光", "tagColor": "#52C41A" }
|
||||
],
|
||||
"seasons": [
|
||||
{ "seasonCode": "spring", "seasonName": "春" }
|
||||
],
|
||||
"statusCode": "ENABLED",
|
||||
"statusName": "启用",
|
||||
"enabled": true,
|
||||
"resourceAvailable": true,
|
||||
"updateTime": "2026-08-25 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="fields">
|
||||
<h2>4. records[] 字段</h2>
|
||||
<table>
|
||||
<thead><tr><th>字段</th><th>类型</th><th>可空</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>relationId</code></td><td>string</td><td>否</td><td>供应商资源关系 ID</td></tr>
|
||||
<tr><td><code>resourceModule</code></td><td>string</td><td>否</td><td>关系冻结的资源模块编码</td></tr>
|
||||
<tr><td><code>moduleName</code></td><td>string</td><td>否</td><td>模块中文名</td></tr>
|
||||
<tr><td><code>resourceId</code></td><td>string</td><td>否</td><td>资源 ID;资源缺失仍保留关系原值</td></tr>
|
||||
<tr><td><code>resourceName</code></td><td>string</td><td>是</td><td>当前资源名称</td></tr>
|
||||
<tr><td><code>coverUrl</code></td><td>string</td><td>是</td><td>当前有效封面 URL</td></tr>
|
||||
<tr><td><code>city</code></td><td>string</td><td>是</td><td>城市展示值</td></tr>
|
||||
<tr><td><code>isCharged</code></td><td>boolean</td><td>是</td><td>是否收费;不适用/未知为 null</td></tr>
|
||||
<tr><td><code>isChargedName</code></td><td>string</td><td>是</td><td>是否收费中文名</td></tr>
|
||||
<tr><td><code>settleTypeCode</code></td><td>string</td><td>是</td><td>结算方式编码</td></tr>
|
||||
<tr><td><code>settleTypeName</code></td><td>string</td><td>是</td><td>结算方式中文名</td></tr>
|
||||
<tr><td><code>tags</code></td><td>array</td><td>否</td><td>标签列表;无数据为 []</td></tr>
|
||||
<tr><td><code>seasons</code></td><td>array</td><td>否</td><td>标准季节列表;无数据为 []</td></tr>
|
||||
<tr><td><code>statusCode</code></td><td>string</td><td>是</td><td>资源当前权威状态编码</td></tr>
|
||||
<tr><td><code>statusName</code></td><td>string</td><td>是</td><td>归一化状态中文名</td></tr>
|
||||
<tr><td><code>enabled</code></td><td>boolean</td><td>是</td><td>归一化启用标记</td></tr>
|
||||
<tr><td><code>resourceAvailable</code></td><td>boolean</td><td>否</td><td>false 表示关系保留但资源已删除/缺失</td></tr>
|
||||
<tr><td><code>updateTime</code></td><td>string</td><td>是</td><td>资源本身更新时间;yyyy-MM-dd HH:mm:ss</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>4.1 子结构</h3>
|
||||
<table>
|
||||
<thead><tr><th>数组</th><th>字段</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>tags[]</code></td><td><code>tagId</code>、<code>tagName</code>、<code>tagColor</code></td><td>标签 ID 沿用各资源模块的字符串表达</td></tr>
|
||||
<tr><td><code>seasons[]</code></td><td><code>seasonCode</code>、<code>seasonName</code></td><td>标准季节编码与中文名</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="semantics">
|
||||
<h2>5. 业务口径</h2>
|
||||
<ul>
|
||||
<li>只读取当前有效的供应商资源关系,按关系 <code>update_time DESC, rel_id DESC</code> 稳定排序。</li>
|
||||
<li>响应中的 <code>updateTime</code> 是资源主数据更新时间,不是关系更新时间。</li>
|
||||
<li>标量字段不适用、未知或资源缺失时返回 <code>null</code>;<code>tags</code> 与 <code>seasons</code> 永远返回数组。</li>
|
||||
<li>资源被软删除或不存在时不丢弃关系行:<code>resourceAvailable=false</code>,名称、状态、更新时间等当前资源字段为 null。</li>
|
||||
<li>十类资源以本系统权威数据为准;展示形式可参考现有资源列表,但不要从参考图硬编码字段值或状态。</li>
|
||||
<li>供应商处于草稿、审批中、合作中、暂停、黑名单、归档等任意生命周期状态时均可只读查询。</li>
|
||||
<li>Fleet/字典依赖出现空响应、非成功、重复、缺失、额外或非法数据时返回 395039,不降级为部分成功。</li>
|
||||
</ul>
|
||||
<div class="callout"><strong>内部依赖说明</strong>Resource 通过内部 Token 调用 <code>POST /internal/fleet/vehicles/supplier-resource-info/batch</code> 聚合车辆信息。该路径不面向管理端,前端不得调用、转发或持有内部 Token。</div>
|
||||
</section>
|
||||
|
||||
<section id="errors">
|
||||
<h2>6. 错误码</h2>
|
||||
<table>
|
||||
<thead><tr><th>业务码</th><th>场景</th><th>管理端建议</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>401</code></td><td>未认证或登录态失效</td><td>按统一登录续期/退出逻辑处理</td></tr>
|
||||
<tr><td><code>400</code></td><td>supplierId、page 或 pageSize 等参数不合法</td><td>修正请求,不自动重试</td></tr>
|
||||
<tr><td><code>395001</code></td><td>供应商不存在</td><td>关闭失效详情或刷新列表</td></tr>
|
||||
<tr><td><code>395034</code></td><td>resourceModule 不受支持</td><td>仅使用本文十个稳定编码</td></tr>
|
||||
<tr><td><code>395039</code></td><td>Fleet、字典或资源必要依赖不可用/响应不完整</td><td>提示稍后重试,不展示旧数据冒充成功</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<pre><code>{
|
||||
"code": 395034,
|
||||
"message": "不支持的资源模块",
|
||||
"data": null,
|
||||
"success": false
|
||||
}</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="frontend">
|
||||
<h2>7. 管理端接入清单</h2>
|
||||
<ol>
|
||||
<li>在供应商详情“账号信息”页签后新增“资源信息”页签;仅在页签打开时加载数据。</li>
|
||||
<li>建议列:资源名称(封面 + 名称)、资源模块、城市、是否收费、结算方式、标签、季节、状态、更新时间。</li>
|
||||
<li>所有 ID 按字符串保存、传参和比较,禁止转换为 JavaScript number。</li>
|
||||
<li>优先展示服务端中文字段;未知字段显示“—”,不得前端猜测状态或字典名称。</li>
|
||||
<li><code>resourceAvailable=false</code> 时保留行并明确显示“资源已删除/不可用”。</li>
|
||||
<li>模块筛选或页容量改变时回到第 1 页;pageSize 最大 100。</li>
|
||||
<li>按 <code>code/success</code> 判断业务结果;不要仅依据 HTTP 200。</li>
|
||||
<li>页签为只读展示,不增加绑定、改绑、解绑、启停或删除按钮。</li>
|
||||
</ol>
|
||||
<h3>7.1 建议验收场景</h3>
|
||||
<table>
|
||||
<thead><tr><th>场景</th><th>预期</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>有资源关系</td><td>按分页显示,中文字段、数组、字符串 ID 正常</td></tr>
|
||||
<tr><td>无资源关系</td><td>records=[]、total=0,不显示错误空态</td></tr>
|
||||
<tr><td>按模块筛选</td><td>返回行的 resourceModule 全部等于筛选值</td></tr>
|
||||
<tr><td>关系存在但资源缺失</td><td>保留关系行,resourceAvailable=false</td></tr>
|
||||
<tr><td>无双权限/未登录</td><td>服务端拒绝,页面不泄露数据</td></tr>
|
||||
<tr><td>依赖暂不可用</td><td>展示 395039 对应提示,不展示不完整成功页</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="verification">
|
||||
<h2>8. 实现与验收状态</h2>
|
||||
<ul>
|
||||
<li><strong>代码:</strong>PR #6330 已合并 <code>dev-v3</code>,合并提交 <code>4f032cd6f6698607a2f1524437533d595975fc9a</code>。</li>
|
||||
<li><strong>自动化:</strong>Resource 全量 2043 项零失败;Fleet 可运行全量 3853 项零失败;GatewayRouteAuditTest 3 项零失败;独立审计套件再次通过。</li>
|
||||
<li><strong>TEST:</strong>任务 <code>cc1fe248</code>、<code>2697d374</code> 精确部署同一合并提交,Resource/Fleet 双实例与 Nacos 各 2 实例健康。</li>
|
||||
<li><strong>真实 Gateway:</strong>有效管理端会话只读查询 3 个供应商,实际取得 1 条 SCENIC 关系;18 字段、字符串 ID、数组、时间和模块筛选均通过。</li>
|
||||
<li><strong>负向:</strong>395001、395034、page/pageSize 参数 400 与未认证 401 均实测通过。</li>
|
||||
<li><strong>车辆样本:</strong>TEST 当时无 VEHICLE 供应商关系;未伪造数据,跨服务路径由双服务真实部署健康与内部契约/批量/失败关闭测试覆盖。</li>
|
||||
<li><strong>环境恢复:</strong>任务 <code>45e843e5</code>、<code>c05eda6e</code> 已回切包含本次合并的 <code>dev-v3</code>;Resource 8082/8182、Fleet 8087/8187 及 Nacos 各 2 个实例健康,临时部署分支已删除。</li>
|
||||
</ul>
|
||||
<div class="callout ok"><strong>数据清理</strong>TEST 验收全部为 GET 只读请求,没有创建或修改供应商、资源、数据库、Redis、MQ 或配置数据。</div>
|
||||
</section>
|
||||
|
||||
<section id="rollback">
|
||||
<h2>9. 撤回方案</h2>
|
||||
<ol>
|
||||
<li>从最新 <code>dev-v3</code> 创建回退分支,执行 <code>git revert -m 1 --no-edit 4f032cd6f6698607a2f1524437533d595975fc9a</code>,经独立 PR 合入。</li>
|
||||
<li>依次重新构建并滚动部署 Resource、Fleet,分别保持双实例可用。</li>
|
||||
<li>本次无数据库、Redis、MQ、Nacos 或其他配置变更,无需 DDL、DML、缓存清理、消息补偿或配置恢复,也无不可逆影响。</li>
|
||||
<li>管理端停止调用新增 GET 和读取本次字段;既有供应商详情、账号和资源管理接口不受影响。</li>
|
||||
<li>经 Gateway 复测新增路径撤回、既有供应商详情正常,并确认两服务双实例和 Nacos 健康。</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
关联:Issue #6316 · PR #6330 · 后端负责人 @lc · 本文件为可离线、可打印单页接口规范。
|
||||
</footer>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6273"
|
||||
title: "供应商审批记录补充供应商编号"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "a46da334"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-24"
|
||||
status_note: "PR #6274 已合并 dev-v3,合并提交 588573ae4 已随 Resource 双实例滚动部署到 TEST。审批记录分页项新增当前供应商编号 supplierNo;既有目标字段、脱敏字段、权限、分页和闭区间时间筛选保持不变。管理端列展示和自然日边界组装仍待前端交付。"
|
||||
updated_at: "2026-08-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商审批记录补充供应商编号
|
||||
|
||||
供应商详情的审批记录分页项新增当前供应商编号 `supplierNo`,供管理端直接展示。此次是向后兼容的响应扩展,不删除或重命名既有字段,也不改变查询、权限和脱敏语义。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 权限 | 行为变化 |
|
||||
|---|---|---|---|
|
||||
| GET | `/admin/supplier/items/{supplierId}/approval-records/page` | `supplier:approval:read` | 每条分页记录新增 `supplierNo` |
|
||||
|
||||
请求参数、分页外壳和排序规则均未变化。
|
||||
|
||||
## 新增响应字段
|
||||
|
||||
| 字段 | 类型 | 空值语义 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `supplierNo` | String / null | 尚未生成编号的草稿返回 `null` | 当前供应商编号,直接来自供应商主体档案;不能从供应商 ID、审计 JSON 或展示摘要推导 |
|
||||
|
||||
典型分页项如下;示例值仅说明字段结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"supplierId": "2091715622923657217",
|
||||
"supplierNo": "SUP2091715622923657217",
|
||||
"supplierName": "示例供应商",
|
||||
"supplierTypes": [],
|
||||
"operationType": "UPDATE",
|
||||
"targetType": "SUPPLIER",
|
||||
"targetId": null,
|
||||
"oldValueMasked": "统一社会信用代码:****",
|
||||
"newValueMasked": "统一社会信用代码:****",
|
||||
"changeReason": "提交注册前保存完整表单",
|
||||
"status": "草稿",
|
||||
"operatorName": "示例管理员",
|
||||
"operatorRole": "超级管理员",
|
||||
"createTime": "2026-08-24 10:34:34"
|
||||
}
|
||||
```
|
||||
|
||||
## 保持兼容的字段与行为
|
||||
|
||||
- `targetType` 和 `targetId` 继续返回;管理端可以隐藏“目标类型”列,但不能把前端隐藏等同于删除接口字段。
|
||||
- `oldValueMasked` 和 `newValueMasked` 继续返回脱敏后的业务摘要。管理端表头可简化为“变更前”“变更后”,但不得改为读取或展示未脱敏数据。
|
||||
- `page`、`pageSize`、`operationType`、`status`、`from`、`to`、`sortBy` 和 `sortDirection` 的请求约束不变。
|
||||
- 查询仍使用 `createTime >= from AND createTime <= to` 的闭区间语义;服务端不替客户端推导自然日边界。
|
||||
- 认证方式、`supplier:approval:read` 权限、管理员数据范围、排序、错误码、Gateway 路由和统一 `Result<T>` 外壳均未变化。
|
||||
- 不新增数据库 migration,不修改配置、Redis、MQ、审批状态机、审计写入或跨服务契约。
|
||||
|
||||
## 管理端待办
|
||||
|
||||
1. 在审批记录表格最前面增加“供应商编号”列,绑定 `supplierNo`;空值显示 `—`。
|
||||
2. 隐藏“目标类型”列,但保留接口兼容字段,不要求后端删除 `targetType`。
|
||||
3. 将“变更前(脱敏)”“变更后(脱敏)”表头改为“变更前”“变更后”;单元格仍绑定 `oldValueMasked`、`newValueMasked`。
|
||||
4. 日期查询开始值组装为当天 `00:00:00`,结束值组装为当天 `23:59:59`,并在查询条件变化后重置到第 1 页。
|
||||
|
||||
若选择同一天,不能把开始和结束都提交为 `00:00:00`。例如查询 2026-08-24 应提交:
|
||||
|
||||
```text
|
||||
from=2026-08-24 00:00:00
|
||||
to=2026-08-24 23:59:59
|
||||
```
|
||||
|
||||
## 错误与边界语义
|
||||
|
||||
| 场景 | 结果 |
|
||||
|---|---|
|
||||
| `from` 晚于 `to` | 参数校验失败,消息为 `from不能晚于to` |
|
||||
| 只传 `from` 或只传 `to` | 参数校验失败,时间边界必须成对提交 |
|
||||
| 未认证 | 统一响应 `code=401`、`success=false`;HTTP 状态可能仍为 200 |
|
||||
| 无读取权限或超出数据范围 | 服务端拒绝查询,不返回审批记录 |
|
||||
| 筛选结果为空 | 返回空分页,`records=[]`,不返回 `null` |
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:供应商审批记录 Controller、投影和查询聚焦测试 16 项零失败;Resource 全量 1954 项零失败,38 项仓库既有条件跳过。
|
||||
- 部署:TEST 部署任务 `32288c37` 成功、退出码 0;`hl-resource-service` 的 8182、8082 双实例依次启动并健康。部署服务器 HEAD `435ae0a86` 包含目标合并提交 `588573ae4` 和功能提交 `9cf77177c`。
|
||||
- 真实 Gateway:使用现有有效超级管理员登录态查询 4 条审批记录,每条均返回当前 `supplierNo`;`targetType`、`targetId`、`oldValueMasked` 和 `newValueMasked` 同时保留,脱敏摘要仍包含掩码。
|
||||
- 分页兼容:请求 `page=1&pageSize=2` 返回 2 条记录、总数 4,新增字段不改变分页外壳。
|
||||
- 时间边界:同一自然日 `00:00:00` 至 `23:59:59` 返回 4 条;旧前端把结束值提交为 `00:00:00` 时返回 0 条,证明日期问题来自客户端结束边界组装。
|
||||
- 参数门禁:反向时间范围被拒绝并返回 `from不能晚于to`。
|
||||
- 认证门禁:不携带 Authorization 经 TEST Gateway 查询,返回 HTTP 200、业务码 `401`、`success=false`;该结果仅作为负向门禁验证,不计作正向验收。
|
||||
- 清理:本次 TEST 验收只执行只读分页查询,未产生数据库、Redis、MQ、文件或外部业务副作用,无需测试数据清理。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 先停止发布或回退依赖 `supplierNo` 的管理端版本,避免后端字段回退后出现空列。
|
||||
2. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 588573ae4ae016829d90dbb365f5c8ec3df981f0`,经独立 PR 合入。
|
||||
3. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ,没有不可逆数据影响。
|
||||
4. 回退后接口不再保证返回 `supplierNo`;复测原有分页、权限、数据范围、脱敏字段、目标字段、排序和闭区间时间筛选。
|
||||
5. 撤回本 Changelog 时,以新的文档提交删除本文件或明确标记撤回,并通知管理端停止消费新增字段;不要重写已发布提交历史。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6273](https://git.1814.love:8443/wx/HL/issues/6273)
|
||||
- **PR**: [#6274](https://git.1814.love:8443/wx/HL/pulls/6274)
|
||||
- **合并提交**: [588573ae4](https://git.1814.love:8443/wx/HL/commit/588573ae4ae016829d90dbb365f5c8ec3df981f0)
|
||||
- **后端负责人**: @lc
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6275"
|
||||
title: "供应商联系人默认规则与角色字典校验"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f8a0d72d"
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "PR #6276 已合并 dev-v3,合并提交 f7b442b67 已由部署任务 #785ce882 发布到 TEST 并经 Gateway 验证。非空联系人快照现在始终归一为且仅为一个默认联系人;联系人角色按 sup_content_role 当前 ACTIVE 字典值校验。管理端待将角色/职务改为动态下拉并适配新的默认联系人规则。"
|
||||
updated_at: "2026-08-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商联系人默认规则与角色字典校验
|
||||
|
||||
供应商创建、草稿更新和注册提交统一收紧联系人契约:联系人列表非空时,服务端保证最终恰好一个默认联系人;联系人角色必须使用字典 `sup_content_role` 当前启用项的 `dictValue`。本记录覆盖 #6258 中“允许没有默认联系人、取消或删除后不自动选举”的旧语义。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 行为变化 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/supplier/items/add` | 非空 `contacts` 未显式选择默认项时,服务端将请求第一项设为默认;联系人角色按生效字典校验 |
|
||||
| PUT | `/admin/supplier/items/{supplierId}/update` | 非空联系人快照最终未保留或选中默认项时,请求第一项自动成为默认;显式提交的角色按生效字典校验 |
|
||||
| POST | `/admin/supplier/items/{supplierId}/submit` | 完整联系人快照执行相同的唯一默认归一和角色字典校验;失败发生在主体、联系人和审批写入前 |
|
||||
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 返回服务端归一后的 `contacts[].isPrimary` 和已保存的 `contacts[].contactRole`;电话仍仅返回 `contactPhoneMask` |
|
||||
|
||||
管理端角色下拉继续使用既有通用字典接口,该接口本工单未修改:
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/dict/data/sup_content_role` | 获取联系人角色选项;展示 `dictLabel`,提交 `dictValue`,仅使用 `status=ACTIVE` 的项 |
|
||||
|
||||
业务失败可能仍返回 HTTP 200,调用方必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 默认联系人规则
|
||||
|
||||
- `contacts` 为非空列表时,保存后必须且只会有一个 `isPrimary=true`。
|
||||
- 只有一个联系人时,即使请求传 `false`、`null` 或省略 `isPrimary`,该联系人也会成为默认联系人。
|
||||
- 多个联系人且没有任何项传 `true` 时,按请求数组顺序将第一项设为默认联系人。
|
||||
- 恰好一个联系人传 `true` 时保留该选择。
|
||||
- 两个及以上联系人传 `true` 时返回业务码 `400`、消息 `同一供应商只能设置一个默认联系人`,整次请求零写入。
|
||||
- 草稿增量更新中,已有联系人省略 `isPrimary` 时优先保留原默认状态;显式传 `false` 表示取消。若删除或取消原默认联系人后没有其他默认项,则请求列表第一项自动成为默认。
|
||||
- 增量更新的 `contacts=null` 或省略字段表示联系人不变;`contacts=[]` 表示清空联系人,空列表允许零默认联系人。
|
||||
- 客户端应在提交后重新读取详情,以服务端返回的 `isPrimary` 为最终状态。
|
||||
|
||||
创建或完整提交示例(两个联系人均未选择默认):
|
||||
|
||||
```json
|
||||
{
|
||||
"contacts": [
|
||||
{
|
||||
"contactName": "联系人甲",
|
||||
"contactPhone": "13800000001",
|
||||
"contactRole": "contentBus",
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"contactName": "联系人乙",
|
||||
"contactPhone": "13800000002",
|
||||
"contactRole": "contentMoney",
|
||||
"isPrimary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
保存后详情中的第一项为默认联系人:
|
||||
|
||||
```json
|
||||
{
|
||||
"contacts": [
|
||||
{
|
||||
"contactName": "联系人甲",
|
||||
"contactPhoneMask": "138****0001",
|
||||
"contactRole": "contentBus",
|
||||
"isPrimary": true
|
||||
},
|
||||
{
|
||||
"contactName": "联系人乙",
|
||||
"contactPhoneMask": "138****0002",
|
||||
"contactRole": "contentMoney",
|
||||
"isPrimary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Long` 类型的 `contactId` 继续按字符串传输;更新已有联系人仍需携带详情返回的 `expectedUpdateTime`。
|
||||
|
||||
## 联系人角色字典规则
|
||||
|
||||
- 创建和注册提交的每个联系人都必须提供非空 `contactRole`,值必须精确匹配 `sup_content_role` 当前 `status=ACTIVE` 项的 `dictValue`。
|
||||
- 草稿增量更新已有联系人时,省略 `contactRole` 表示保持原值,不会因为本次无关更新而额外依赖字典;新增联系人或显式修改角色时必须提交生效的 `dictValue`。
|
||||
- 空白、未知或已停用值返回业务码 `400`、消息 `联系人角色不合法或已停用`,失败请求不修改供应商或联系人。
|
||||
- 字典服务异常、响应失败、空数据或没有启用项时失败关闭,返回“联系人角色字典暂不可用,请稍后重试”,不降级为硬编码选项。
|
||||
- TEST 当前可见项为 `contentBus`(业务人员)和 `contentMoney`(财务人员),仅用于联调示例;客户端必须动态读取字典,不能将这些值固化在代码中。
|
||||
|
||||
字典响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"dictType": "sup_content_role",
|
||||
"dictLabel": "业务人员",
|
||||
"dictValue": "contentBus",
|
||||
"sortOrder": 1,
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 管理端接入事项
|
||||
|
||||
1. “角色/职务”字段改为下拉框,进入联系人页面时请求 `/admin/dict/data/sup_content_role`,展示 `dictLabel`、提交 `dictValue`,过滤非 `ACTIVE` 项。
|
||||
2. 单联系人场景将该联系人呈现为默认;多联系人可选择一个默认联系人,但不可同时选择多个。未选择时服务端会把请求第一项设为默认。
|
||||
3. 新增、删除、拖动或调整联系人顺序后,提交数组顺序应与界面顺序一致;保存成功后重新读取详情并以服务端返回值刷新默认标识。
|
||||
4. 更新已有联系人时,未修改角色可省略 `contactRole`;新增联系人必须提交从字典选择的角色值。
|
||||
|
||||
## 未变化范围
|
||||
|
||||
- 不新增接口、Gateway 路由、数据库 migration、字典数据、菜单权限、角色、状态机或审批节点。
|
||||
- 不改变联系人电话加密存储、脱敏输出、软删除、审计、聚合锁、幂等与乐观版本语义。
|
||||
- 不修改配置、Redis Key 或 MQ 契约,也不回填历史联系人数据。
|
||||
- 本工单只交付后端;管理端代码未在后端仓库修改,前端状态保持 `pending` 直至完成上述接入并提供验证提交。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:默认联系人写入 16 项、应用服务 14 项、角色字典 11 项、聚合校验 6 项,共 47 项定向测试零失败;Resource 全量 1966 项零失败,38 项仓库既有条件跳过。
|
||||
- 合并后独立审计:在目标提交上重新执行 47 项定向测试,零失败且未发现确定性缺口。
|
||||
- TEST 部署:部署任务 #785ce882 成功,`hl-resource-service` 的 8082、8182 实例滚动重启并恢复健康;部署服务器 HEAD `1f2e0ac6d` 包含目标合并提交 `f7b442b67`。
|
||||
- TEST Gateway:使用真实管理员会话验证单联系人自动默认、多联系人无选择时第一项默认、删除默认后重新选举、角色省略保持原值、清空联系人、重复默认与非法角色零写入、提交失败零审批副作用;未认证请求返回业务码 `401`。
|
||||
- 清理:合成供应商草稿已通过既有删除接口清理,精确查询结果为 0;未产生外部文件、Redis、MQ 或配置副作用。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit f7b442b67f4d989fac6b29a771331ca6d66148e3`,经独立 PR 合入。
|
||||
2. 重新构建并滚动部署 `hl-resource-service`;本次没有数据库、字典数据、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
|
||||
3. 回退后恢复 #6258 的旧语义:允许联系人列表非空但没有默认联系人,角色不再由后端按 `sup_content_role` 动态校验;接口路径和字段结构不变。
|
||||
4. 经 Gateway 复测创建、更新、提交和详情接口,确认回退后的默认联系人语义、角色传值、电话脱敏、权限和失败零写入符合目标版本。
|
||||
5. 若管理端已上线本次交互,应同步停止依赖“服务端必定选出一个默认联系人”的保证;字典下拉仍可继续使用既有通用字典接口,但需按回退版本确认提交兼容性。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6275](https://git.1814.love:8443/wx/HL/issues/6275)
|
||||
- **PR**: [#6276](https://git.1814.love:8443/wx/HL/pulls/6276)
|
||||
- **合并提交**: [f7b442b67](https://git.1814.love:8443/wx/HL/commit/f7b442b67f4d989fac6b29a771331ca6d66148e3)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,419 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6304"
|
||||
title: "供应商资质增加永久有效与到期剩余天数"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "25fb4b27"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-25"
|
||||
status_note: "PR #6342 与补充修复 PR #6349 均已合并 dev-v3,TEST 最终部署提交为 af3a7bae5。真实 SUPER_ADMIN 经 Gateway 已验证显式永久有效、395042 冲突零写入、历史省略兼容和数据清理;未认证请求仍返回标准 401 业务响应。管理端尚待适配,frontend_status 保持 pending。"
|
||||
updated_at: "2026-08-25"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商资质增加永久有效与到期剩余天数
|
||||
|
||||
供应商资质的有效期输入改为显式的“永久有效”契约;非永久有效资质必须提交到期日期。详情按上海时区自然日返回有符号剩余天数,便于页面直接展示“剩余 N 天”“今日到期”或“已过期 N 天”。
|
||||
|
||||
> **服务**: `hl-resource-service`
|
||||
> **PR**: #6342、补充修复 #6349
|
||||
> **Issue**: #6304、补充修复 #6347
|
||||
> **日期**: 2026-08-25
|
||||
> **影响范围**: 管理端供应商新建、编辑、提交审批和基本信息详情的资质证照区域
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 资质写入字段新增 `permanentValid`;`true` 时不得提交 `expiryDate`,`false` 时必须提交 `expiryDate`。
|
||||
- 详情资质项新增 `daysUntilExpiry`;永久有效时返回 `null`,有限期按 `Asia/Shanghai` 自然日返回正数、零或负数。
|
||||
- 既有只读字段 `isRequired` 仍表示“该供应商类型是否要求此资质”,没有改义,也不能作为永久有效标识。
|
||||
|
||||
## 一、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变化 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | `qualifications[].permanentValid` 可写 |
|
||||
| 2 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | `qualifications[].permanentValid` 可写,并支持增量保留语义 |
|
||||
| 3 | 提交供应商注册审批 | POST | `/admin/supplier/items/{supplierId}/submit` | `qualifications[].permanentValid` 可写 |
|
||||
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `qualifications[]` 新增 `daysUntilExpiry` |
|
||||
|
||||
统一响应仍为 `Result<T>`。业务校验失败可能仍返回 HTTP 200,调用方必须同时判断 `code`、`success` 和 `data`。
|
||||
|
||||
## 二、字段契约
|
||||
|
||||
### 2.1 写入字段
|
||||
|
||||
| 字段 | 位置 | JSON 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `permanentValid` | `qualifications[]` | Boolean | 条件必填 | `true` 表示永久有效,`false` 表示存在到期日;历史调用方可省略并触发兼容推导 |
|
||||
| `expiryDate` | `qualifications[]` | String / null | 条件必填 | 格式 `yyyy-MM-dd`;仅 `permanentValid=false` 时必须有值 |
|
||||
|
||||
合法组合:
|
||||
|
||||
| 场景 | `permanentValid` | `expiryDate` | 结果 |
|
||||
|---|---:|---|---|
|
||||
| 永久有效 | `true` | 省略或 `null` | 接受,到期日为空 |
|
||||
| 有到期日 | `false` | `yyyy-MM-dd` | 接受,保存该到期日 |
|
||||
| 历史永久请求 | 省略 | 省略或 `null` | 接受,兼容推导为 `true` |
|
||||
| 历史有限期请求 | 省略 | `yyyy-MM-dd` | 接受,兼容推导为 `false` |
|
||||
| 矛盾组合 | `true` | `yyyy-MM-dd` | 拒绝,业务码 `395042` |
|
||||
| 缺少到期日 | `false` | 省略或 `null` | 拒绝,业务码 `395042` |
|
||||
|
||||
更新既有资质时还有以下增量规则:
|
||||
|
||||
- `permanentValid` 与 `expiryDate` 均省略:保留当前有效期,不做修改。
|
||||
- 仅提交非空 `expiryDate`:兼容推导 `permanentValid=false`。
|
||||
- 显式提交 `permanentValid=true` 且不提交到期日:切换为永久有效并清空原到期日。
|
||||
- 新增资质且两字段均省略:按历史兼容口径创建为永久有效。
|
||||
|
||||
### 2.2 详情响应字段
|
||||
|
||||
| 字段 | JSON 类型 | 空值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `permanentValid` | Boolean | 不为空 | 由 `expiryDate` 是否为空动态推导 |
|
||||
| `daysUntilExpiry` | Integer / null | 永久有效时为 `null` | 到期日相对上海业务当日的有符号自然日差 |
|
||||
| `expired` | Boolean | 不为空 | 仅当 `daysUntilExpiry < 0` 时为 `true` |
|
||||
| `isRequired` | Boolean | 不为空 | 历史只读兼容字段;继续表示供应商类型规则是否要求该资质 |
|
||||
|
||||
以 2026-08-25(上海日期)为例:
|
||||
|
||||
| `expiryDate` | `permanentValid` | `daysUntilExpiry` | `expired` |
|
||||
|---|---:|---:|---:|
|
||||
| `null` | `true` | `null` | `false` |
|
||||
| `2026-08-26` | `false` | `1` | `false` |
|
||||
| `2026-08-25` | `false` | `0` | `false` |
|
||||
| `2026-08-24` | `false` | `-1` | `true` |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 3.1 创建供应商注册草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
使用场景:新建供应商草稿时保存资质证照。`permanentValid` 位于每个 `qualifications[]` 项内。
|
||||
|
||||
典型请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例酒店管理有限公司",
|
||||
"shortName": "示例酒店",
|
||||
"taxNo": "91110108MA00000001",
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "HOTEL"
|
||||
}
|
||||
],
|
||||
"mainCooperation": "酒店住宿服务",
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-0001",
|
||||
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
|
||||
"permanentValid": false,
|
||||
"expiryDate": "2027-08-25"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092152294748352514",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-25 15:21:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
永久有效请求只需将资质项改为:
|
||||
|
||||
```json
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-PERM",
|
||||
"imageUrl": "https://files.example.test/supplier/license-perm.jpg",
|
||||
"permanentValid": true,
|
||||
"expiryDate": null
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
使用场景:编辑既有资质或切换永久有效状态。`qualifications` 一旦传入,仍代表资质集合的完整当前快照;既有项必须携带 `qualificationId` 和该项的 `expectedUpdateTime`。
|
||||
|
||||
将有限期资质切换为永久有效:
|
||||
|
||||
```json
|
||||
{
|
||||
"qualifications": [
|
||||
{
|
||||
"qualificationId": "2092152300000000001",
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-0001",
|
||||
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
|
||||
"permanentValid": true,
|
||||
"expiryDate": null,
|
||||
"expectedUpdateTime": "2026-08-25 15:21:00"
|
||||
}
|
||||
],
|
||||
"changeReason": "营业执照变更为永久有效",
|
||||
"expectedUpdateTime": "2026-08-25 15:21:00"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092152294748352514",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-25 15:35:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
若本次不修改既有资质的有效期,可同时省略该项的 `permanentValid` 和 `expiryDate`;不要回传未经确认的默认值。
|
||||
|
||||
### 3.3 提交供应商注册审批 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
使用场景:提交完整供应商表单。资质有效期组合规则与创建草稿完全一致。
|
||||
|
||||
典型请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例酒店管理有限公司",
|
||||
"shortName": "示例酒店",
|
||||
"taxNo": "91110108MA00000001",
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "HOTEL"
|
||||
}
|
||||
],
|
||||
"mainCooperation": "酒店住宿服务",
|
||||
"licenseImageUrl": "https://files.example.test/supplier/license-1.jpg",
|
||||
"qualifications": [
|
||||
{
|
||||
"qualificationId": "2092152300000000001",
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-0001",
|
||||
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
|
||||
"permanentValid": false,
|
||||
"expiryDate": "2027-08-25"
|
||||
}
|
||||
],
|
||||
"expectedUpdateTime": "2026-08-25 15:35:00"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"approvalLogId": "2092152400000000001",
|
||||
"requestNo": "SUP-PROFILE-20260825-0001",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"spNo": null,
|
||||
"spStatus": null,
|
||||
"syncStatus": "APPLIED",
|
||||
"submittedAt": "2026-08-25 15:36:00",
|
||||
"finishedAt": "2026-08-25 15:36:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
使用场景:展示供应商详情的资质证照。请求需要有效的管理端 `Authorization`,无请求体。
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2092152294748352514/basic-info/view
|
||||
Authorization: Bearer <有效管理端令牌>
|
||||
```
|
||||
|
||||
有限期资质成功响应(假设上海日期为 2026-08-25):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092152294748352514",
|
||||
"supplierNo": null,
|
||||
"fullName": "示例酒店管理有限公司",
|
||||
"shortName": "示例酒店",
|
||||
"tax_no": "9111********0001",
|
||||
"legalRepresentative": null,
|
||||
"contactPhoneMask": null,
|
||||
"establishDate": null,
|
||||
"registeredCapital": null,
|
||||
"businessScope": null,
|
||||
"staffScale": null,
|
||||
"mainCooperation": "酒店住宿服务",
|
||||
"status": "DRAFT",
|
||||
"creditLevel": "B",
|
||||
"totalScore": null,
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "HOTEL",
|
||||
"typeName": "酒店"
|
||||
}
|
||||
],
|
||||
"contacts": [],
|
||||
"qualifications": [
|
||||
{
|
||||
"qualificationId": "2092152300000000001",
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"qualTypeName": "营业执照",
|
||||
"certNoMask": "LIC****0001",
|
||||
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
|
||||
"expiryDate": "2026-08-26",
|
||||
"permanentValid": false,
|
||||
"daysUntilExpiry": 1,
|
||||
"validityStatus": "VALID",
|
||||
"validityStatusName": "有效",
|
||||
"isRequired": true,
|
||||
"expired": false,
|
||||
"updateTime": "2026-08-25 15:21:00"
|
||||
}
|
||||
],
|
||||
"updateTime": "2026-08-25 15:21:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
永久有效资质的差异字段为:
|
||||
|
||||
```json
|
||||
{
|
||||
"expiryDate": null,
|
||||
"permanentValid": true,
|
||||
"daysUntilExpiry": null,
|
||||
"validityStatus": "VALID",
|
||||
"validityStatusName": "有效",
|
||||
"expired": false
|
||||
}
|
||||
```
|
||||
|
||||
## 四、错误响应
|
||||
|
||||
新增业务错误码:
|
||||
|
||||
| 业务码 | 消息 | 触发条件 | 写入结果 |
|
||||
|---:|---|---|---|
|
||||
| `395042` | 永久有效标记与到期日期不一致 | `permanentValid=true` 且到期日非空,或 `permanentValid=false` 且到期日为空 | 整笔请求失败,零写入 |
|
||||
|
||||
异常请求片段:
|
||||
|
||||
```json
|
||||
{
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"permanentValid": true,
|
||||
"expiryDate": "2027-08-25"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
错误响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 395042,
|
||||
"message": "永久有效标记与到期日期不一致",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
既有错误码继续有效,包括资质类型无效、证照编号无效、必备资质缺失、资质过期和并发版本冲突。
|
||||
|
||||
## 五、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 写入 `qualifications[].permanentValid` | 无显式写入契约,只能由到期日是否为空隐式表达 | 可显式提交;省略时仍兼容历史请求 |
|
||||
| 响应 `qualifications[].daysUntilExpiry` | 无 | 有限期返回有符号整数,永久有效返回 `null` |
|
||||
| 响应 `qualifications[].isRequired` | 供应商类型是否要求此资质 | 语义不变,继续只读返回 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 永久有效输入 | 仅通过不传到期日表达 | 优先显式提交 `permanentValid=true`;旧请求仍兼容 |
|
||||
| 非永久有效输入 | 提交到期日 | 提交 `permanentValid=false` 与到期日 |
|
||||
| 到期提示 | 调用方自行按日期计算 | 直接读取 `daysUntilExpiry`,无需自行处理时区和跨日 |
|
||||
|
||||
## 六、管理端接入事项
|
||||
|
||||
1. 将资质证照区域的“是否必备”编辑控件改为“永久有效”;提交字段必须使用 `permanentValid`,不得复用或改写 `isRequired`。
|
||||
2. `permanentValid=true` 时清空并禁用 `expiryDate`;`false` 时要求用户选择到期日。
|
||||
3. 非永久有效时在到期日期附近显示后端返回的 `daysUntilExpiry`:正数表示剩余天数,零表示今日到期,负数表示已过期天数。
|
||||
4. 永久有效时不显示到期天数。
|
||||
5. 编辑既有数据以详情返回的 `permanentValid` 回显;不要从 `isRequired` 推导。
|
||||
|
||||
## 七、兼容性与影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。旧客户端省略 `permanentValid` 时仍按 `expiryDate` 兼容推导。
|
||||
- **前端是否必须同步上线**: 是。当前界面尚未提交新字段,也未展示剩余天数。
|
||||
- **前端 workaround 清理点**: 若页面存在本地到期天数计算,改为直接消费 `daysUntilExpiry`。
|
||||
- 不新增接口、Gateway 路由、权限点、数据库迁移、配置、Redis 或 MQ 契约。
|
||||
- 不改变供应商状态机、审批语义、资质必备规则、证照编号脱敏、软删除和数据范围。
|
||||
|
||||
## 八、TEST 验证证据
|
||||
|
||||
- 原实现 PR #6342 与补充修复 PR #6349 均已合并;远端精确部署分支 `deploy/6347-supplier-qualification-null-expiry` 的 HEAD 与最终 `dev-v3` 合并提交 `af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 完全一致,TEST 双实例健康。
|
||||
- 真实 `SUPER_ADMIN` 经 Gateway 将既有有限期资质切换为永久有效后,更新成功;重新查询返回 `expiryDate=null`、`permanentValid=true`、`daysUntilExpiry=null`、`expired=false`、`validityStatus=VALID`。
|
||||
- 同一资质提交 `permanentValid=true` 与非空 `expiryDate` 时返回业务码 `395042`、消息“永久有效标记与到期日期不一致”;失败前后供应商和资质的 `updateTime` 均保持 `2026-08-25 16:19:20`,证明零写入。
|
||||
- 历史客户端省略 `permanentValid`、仅提交 `expiryDate=2026-08-26` 时更新成功;详情兼容返回 `permanentValid=false`、`daysUntilExpiry=1`、`expired=false`。
|
||||
- 本工单创建的 DRAFT 测试供应商已通过业务删除接口清理;按精确测试名称分页查询 `total=0`,按原 ID 查询返回 `395001 供应商不存在`。
|
||||
- 未认证访问供应商入口返回业务码 `401`、`success=false`、`data=null`,证明请求仍经 Gateway 认证门禁。
|
||||
- 原实现定向 80 项零失败、Resource 全量 2058 项零失败(38 项条件跳过);补充修复定向 46 项零失败、Resource 全量 2059 项零失败(38 项条件跳过)。离线 HTML 自检通过,文档仓库 `npm test` 46 项零失败。
|
||||
- 管理端适配不属于本后端工单,`frontend_status` 保持 `pending`,不把后端契约验证误记为前端已完成。
|
||||
|
||||
## 九、撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,先执行 `git revert -m 1 --no-edit af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 撤回补充修复,再执行 `git revert -m 1 --no-edit c5b1a84aea6b1c7beb7a626131376355008e662c` 撤回原实现;分别审查并经独立 PR 合入。
|
||||
2. 重新构建并滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
|
||||
3. 回退前确认管理端不再提交 `permanentValid` 或读取 `daysUntilExpiry`;旧字段 `expiryDate`、`expired` 和 `isRequired` 以及存量数据无需迁移。
|
||||
4. 回退后经 Gateway 复测草稿创建、更新、提交、详情、未认证和永久/有限期边界,并确认资质必备规则与证照脱敏保持原行为。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6304](https://git.1814.love:8443/wx/HL/issues/6304)
|
||||
- **PR**: [#6342](https://git.1814.love:8443/wx/HL/pulls/6342)
|
||||
- **合并提交**: [c5b1a84ae](https://git.1814.love:8443/wx/HL/commit/c5b1a84aea6b1c7beb7a626131376355008e662c)
|
||||
- **补充 Issue**: [#6347](https://git.1814.love:8443/wx/HL/issues/6347)
|
||||
- **补充 PR**: [#6349](https://git.1814.love:8443/wx/HL/pulls/6349)
|
||||
- **最终部署提交**: [af3a7bae5](https://git.1814.love:8443/wx/HL/commit/af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,454 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6312"
|
||||
title: "供应商敏感字段与数值格式校验收紧"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "187b7c3b"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-25"
|
||||
status_note: "PR #6322 已合并 dev-v3,合并提交 52a61a88 已由部署任务 b5d27d2b 发布到 TEST。供应商新增、修改、注册提交和独立新增收款账户现统一校验税号、电话、证照编号、银行账号、税率、金额、评分、枚举及业务 ID;接口路径、字段名和成功响应结构不变。"
|
||||
updated_at: "2026-08-25"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商敏感字段与数值格式校验收紧
|
||||
|
||||
供应商新增、修改、注册提交和独立新增收款账户的输入契约现统一收紧。管理端必须在提交前按本文校验税号、电话、证照编号、银行账号、税率、金额、评分和枚举,并将供应商子表的 `Long` ID 作为规范正整数字符串发送。
|
||||
|
||||
本次没有新增、删除或重命名字段,也没有改变成功响应结构;但以前可能被接受的错误格式现在会返回参数错误,因此属于请求兼容性收紧。
|
||||
|
||||
## 🔧 关键变化
|
||||
|
||||
| 项目 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 敏感字段格式 | 部分入口仅校验非空或宽松长度,Controller 与 Service 口径可能不一致 | Controller 与 Service 使用同一业务口径,绕过 Controller 也不能写入非法值 |
|
||||
| 银行账号 | 展示格式和规范化后的数字长度缺少完整门禁 | 去除空格、连字符后必须为 8 至 32 位数字 |
|
||||
| 税率、金额、评分 | 个别入口可进入后续流程后才失败 | 请求入口即校验范围和小数位 |
|
||||
| 子表业务 ID | `null`、数字令牌、零或溢出值可能被错误解释为新增或类型错误 | 只有字段省略表示新增;显式 ID 必须是规范正整数字符串 |
|
||||
| 枚举 | DTO 与直接 Service 调用的口径可能不一致 | 人员规模、账户、结算、发票、合同类型和合同状态均双层校验 |
|
||||
|
||||
业务失败可能仍返回 HTTP 200。管理端必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 变更接口清单
|
||||
|
||||
| # | 方法 | 路径 | 权限 | 变化 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | POST | `/admin/supplier/items/add` | `supplier:create` | 收紧主体、联系人、资质和初始账户输入 |
|
||||
| 2 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 收紧主体、子表 ID、合同和评价输入 |
|
||||
| 3 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` | 完整注册表单复用与新增一致的字段规则 |
|
||||
| 4 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` | 独立账户复用初始账户的账号、结算和开票规则 |
|
||||
|
||||
## 通用字段规则
|
||||
|
||||
### 主体、联系人和资质
|
||||
|
||||
| 字段 | JSON 类型 | 空值语义 | 当前约束 |
|
||||
|---|---|---|---|
|
||||
| `taxNo` | String | 新增、提交必填;修改时省略表示不改 | 原始文本 6 至 64 位,只允许字母、数字、空格和连字符;服务端移除空格、连字符并转大写后仍须为 6 至 64 位字母或数字;可识别的统一社会信用代码、居民身份证同时校验日期或校验位 |
|
||||
| `contactPhone` | String / null | 公司电话可省略 | 原始文本 7 至 20 位;去除空格、括号、连字符及 `+86`/`0086` 后,须为中国大陆手机号、带 `0` 的座机或 400/800 服务号 |
|
||||
| `contacts[].contactPhone` | String | 新增、完整提交的联系人必填;修改既有联系人时省略表示不改 | 与公司电话使用同一规范化和号码类型规则 |
|
||||
| `qualifications[].certNo` | String / null | 草稿可空;提交是否必填继续按资质规则判断 | 最大 128 个 UTF-8 字节;超过长度直接拒绝,读取仍只返回脱敏值 |
|
||||
| `staffScale` | String / null | 省略表示不改或不设置 | 仅允许 `LT50`、`R50_200`、`R200_500`、`GT500`,区分大小写 |
|
||||
|
||||
电话示例:
|
||||
|
||||
| 输入 | 结果 |
|
||||
|---|---|
|
||||
| `+86 138-0013-8000` | 接受,按境内手机号规范化 |
|
||||
| `010-12345678` | 接受,按境内座机规范化 |
|
||||
| `1234567` | 拒绝,消息为 `公司联系电话格式不合法` 或 `联系人电话格式不合法` |
|
||||
| `13800ABC000` | 拒绝,电话不能包含字母 |
|
||||
|
||||
### 收款账户、结算和开票
|
||||
|
||||
`initialAccounts[]` 与独立账户接口 `accounts[]` 使用同一规则:
|
||||
|
||||
| 字段 | 必填 | 当前约束 |
|
||||
|---|---|---|
|
||||
| `accountType` | 是 | `CORPORATE`(对公)或 `PERSONAL`(对私) |
|
||||
| `bankName` | 是 | 非空,最大 500 字符 |
|
||||
| `bankBranch` | 否 | 最大 500 字符 |
|
||||
| `accountNo` | 是 | 只允许数字、空格和连字符;去除展示分隔符后必须为 8 至 32 位数字 |
|
||||
| `settleMode` | 否 | `PREPAY`、`MONTHLY`、`SINGLE` |
|
||||
| `accountPeriod` | 条件必填 | 只有 `MONTHLY` 必须填写;其他结算方式必须为空 |
|
||||
| `invoiceType` | 否 | `SPECIAL`、`NORMAL`、`NONE` |
|
||||
| `taxRate` | 条件必填 | `SPECIAL`/`NORMAL` 必填,`NONE` 必须为空;格式为 `0%` 至 `100%`,最多两位小数 |
|
||||
|
||||
合法税率包括 `0%`、`6%`、`6.5%`、`6.50%`、`100%`、`100.00%`。`5.123%`、`100.01%`、缺少 `%` 或负数均拒绝。
|
||||
|
||||
### 合同、评价和枚举
|
||||
|
||||
| 字段 | 当前约束 |
|
||||
|---|---|
|
||||
| `contracts[].amount` | `0` 至 `9999999999.99`,最多两位小数 |
|
||||
| `contracts[].contractType` | `FRAME`、`SINGLE_TRIP`、`PURCHASE` |
|
||||
| `contracts[].status` | `DRAFT`、`ACTIVE`、`EXPIRED` |
|
||||
| `evaluations[].score` | `0` 至 `5`,最多两位小数 |
|
||||
|
||||
枚举均区分大小写。空值是否表示“不修改”继续由对应修改字段语义决定;显式提交未知值一律拒绝。
|
||||
|
||||
### 路径和子表业务 ID
|
||||
|
||||
路径中的 `{supplierId}` 必须大于 0。JSON 中以下可选子表 ID 使用同一严格规则:
|
||||
|
||||
- `contacts[].contactId`
|
||||
- `qualifications[].qualificationId`
|
||||
- `contracts[].contractId`
|
||||
- `evaluations[].evaluationId`
|
||||
- `evaluations[].orderId`
|
||||
- `evaluations[].resourceId`
|
||||
- `evaluations[].evaluator`
|
||||
|
||||
| 发送值 | 结果 |
|
||||
|---|---|
|
||||
| 字段省略 | 接受,表示新增子项或未建立可选关联 |
|
||||
| `"1"` 至 `"9223372036854775807"` | 接受,必须是 1 至 19 位且在 Java `Long` 正整数范围内 |
|
||||
| `null`、`""`、`"0"`、`"-1"` | 拒绝 |
|
||||
| `"01"`、`" 1"` | 拒绝,不允许前导零或空白 |
|
||||
| `1` | 拒绝,数字 JSON 令牌不能替代字符串 |
|
||||
| `"9223372036854775808"` 或 20 位以上数字 | 拒绝,超出范围或长度 |
|
||||
|
||||
管理端更新已有子项时,应原样回传详情接口返回的字符串 ID;新增子项必须省略 ID 字段,不能传 `null`。
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 1. 创建供应商草稿 `POST /admin/supplier/items/add`
|
||||
|
||||
请求头:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <accessToken>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
典型合法请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"shortName": "示例旅行",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"contactPhone": "010-12345678",
|
||||
"staffScale": "R50_200",
|
||||
"mainCooperation": "住宿与地接服务",
|
||||
"types": [
|
||||
{ "typeCode": "HOTEL" }
|
||||
],
|
||||
"contacts": [
|
||||
{
|
||||
"contactName": "示例联系人",
|
||||
"contactPhone": "+86 138-0013-8000",
|
||||
"contactRole": "contentBus",
|
||||
"isPrimary": true
|
||||
}
|
||||
],
|
||||
"initialAccounts": [
|
||||
{
|
||||
"accountType": "CORPORATE",
|
||||
"bankName": "示例银行",
|
||||
"accountNo": "6222 0212 3456 7890",
|
||||
"settleMode": "SINGLE",
|
||||
"invoiceType": "SPECIAL",
|
||||
"taxRate": "6.00%"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
成功响应结构不变:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092000000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [
|
||||
{
|
||||
"accountId": "2092000000000000002",
|
||||
"accountNoMask": "6222********7890",
|
||||
"status": "DRAFT"
|
||||
}
|
||||
],
|
||||
"updateTime": "2026-08-25 12:35:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
无效税号、电话、证照编号或初始账户会在供应商、联系人、账户和审批写入前失败。
|
||||
|
||||
### 2. 修改供应商 `PUT /admin/supplier/items/{supplierId}/update`
|
||||
|
||||
请求头:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <accessToken>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
更新已有联系人时,`contactId` 必须使用字符串;新增联系人则省略该字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"contactPhone": "+86 138-0013-8000",
|
||||
"staffScale": "R200_500",
|
||||
"contacts": [
|
||||
{
|
||||
"contactId": "2092000000000000010",
|
||||
"contactPhone": "010-87654321",
|
||||
"expectedUpdateTime": "2026-08-25 12:35:00"
|
||||
},
|
||||
{
|
||||
"contactName": "新增联系人",
|
||||
"contactPhone": "13900139000",
|
||||
"contactRole": "contentMoney",
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"contractName": "年度合作框架",
|
||||
"contractType": "FRAME",
|
||||
"amount": 1000000.50,
|
||||
"status": "DRAFT"
|
||||
}
|
||||
],
|
||||
"evaluations": [
|
||||
{
|
||||
"orderId": "2092000000000000020",
|
||||
"score": 4.75,
|
||||
"content": "履约正常"
|
||||
}
|
||||
],
|
||||
"changeReason": "更新联系方式与合作资料",
|
||||
"expectedUpdateTime": "2026-08-25 12:35:00"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应仍为 `Result<SupplierWriteRespVO>`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "2092000000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-25 12:40:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
任一子项 ID、金额、评分或枚举非法时,整个更新失败,既有主体与子表快照不变。
|
||||
|
||||
### 3. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
请求头:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <accessToken>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
提交接口接收完整注册表单,主体、联系人、资质和 `initialAccounts` 复用创建接口的全部规则,并要求 `expectedUpdateTime`:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例旅行服务有限公司",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"contactPhone": "010-12345678",
|
||||
"mainCooperation": "住宿与地接服务",
|
||||
"types": [
|
||||
{ "typeCode": "HOTEL" }
|
||||
],
|
||||
"contacts": [
|
||||
{
|
||||
"contactName": "示例联系人",
|
||||
"contactPhone": "13800138000",
|
||||
"contactRole": "contentBus",
|
||||
"isPrimary": true
|
||||
}
|
||||
],
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-000001",
|
||||
"imageUrl": "https://files.example.com/supplier/license.png"
|
||||
}
|
||||
],
|
||||
"expectedUpdateTime": "2026-08-25 12:40:00"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应结构不变:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"approvalLogId": "2092000000000000030",
|
||||
"requestNo": "SUPPLIER-PROFILE-EXAMPLE",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"spNo": null,
|
||||
"spStatus": null,
|
||||
"syncStatus": "APPLIED",
|
||||
"submittedAt": "2026-08-25 12:41:00",
|
||||
"finishedAt": "2026-08-25 12:41:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
格式错误会在创建审批事实前失败;必备资质、并发版本、重复主体和状态机规则保持既有语义。
|
||||
|
||||
### 4. 独立新增收款账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
|
||||
|
||||
请求头:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <accessToken>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"accountType": "CORPORATE",
|
||||
"bankName": "示例银行",
|
||||
"bankBranch": "示例支行",
|
||||
"accountNo": "6222-0212-3456-7890",
|
||||
"settleMode": "MONTHLY",
|
||||
"accountPeriod": "月结30天",
|
||||
"invoiceType": "NORMAL",
|
||||
"taxRate": "6%",
|
||||
"proofFileUrls": [
|
||||
"https://files.example.com/supplier/account-proof.png"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
成功响应仍为按请求顺序返回的数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"accountId": "2092000000000000040",
|
||||
"approvalLogId": "2092000000000000041",
|
||||
"requestNo": "SUPPLIER-ACCOUNT-EXAMPLE",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"spNo": null,
|
||||
"spStatus": null,
|
||||
"syncStatus": "APPLIED",
|
||||
"accountStatus": "ACTIVE",
|
||||
"isDefault": "NO",
|
||||
"submittedAt": "2026-08-25 12:45:00",
|
||||
"finishedAt": "2026-08-25 12:45:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
账号、结算或开票组合非法时整批在账户和审批写入前失败。供应商尚未完成注册时,合法格式仍返回既有业务错误 `395010`(`请先完成供应商注册`)。
|
||||
|
||||
## 典型错误响应
|
||||
|
||||
字段格式失败统一使用业务码 `400`,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "收款账号规范化后必须为8到32位数字",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| 场景 | `code` | 典型消息 |
|
||||
|---|---:|---|
|
||||
| `{supplierId}` 为 0 | `400` | `供应商ID必须为正数` |
|
||||
| 路径 ID 超出 `Long` 范围 | `400` | `参数 supplierId 格式错误,请检查后重试` |
|
||||
| 可识别的统一社会信用代码校验位错误 | `400` | `主体证件号的统一社会信用代码校验位不合法` |
|
||||
| 公司或联系人电话不符合境内号码规则 | `400` | `公司联系电话格式不合法` / `联系人电话格式不合法` |
|
||||
| 子表 ID 显式为 `null`、空、数字令牌、零或溢出 | `400` | `请求数据格式错误:字段 [...] 格式错误` |
|
||||
| 账号规范化后少于 8 位或超过 32 位 | `400` | `收款账号规范化后必须为8到32位数字` |
|
||||
| 税率超过 100% 或多于两位小数 | `400` | `税率必须为0%至100%且最多两位小数` |
|
||||
| 合同金额超过范围或多于两位小数 | `400` | `合同金额最多10位整数和2位小数` |
|
||||
| 评价分数超过 5 或多于两位小数 | `400` | `评分不能大于5` / `评分最多1位整数和2位小数` |
|
||||
| 未认证 | `401` | 统一认证失败;HTTP 状态可能仍为 200 |
|
||||
|
||||
本次没有新增业务错误码;既有权限、供应商不存在、未注册、必备资质、乐观锁和审批状态错误继续使用原错误码。
|
||||
|
||||
## 管理端接入事项
|
||||
|
||||
1. 新建、编辑和提交表单按本文在前端同步限制长度、数字格式、范围、小数位和枚举,避免用户提交后才看到后端错误。
|
||||
2. 银行账号输入可保留空格或连字符用于展示,但提交前应确认去除分隔符后为 8 至 32 位数字;不要使用 JavaScript `Number` 保存账号。
|
||||
3. 税率必须保留 `%`;根据 `invoiceType` 联动必填和清空,根据 `settleMode` 联动 `accountPeriod`。
|
||||
4. 所有子表业务 ID 使用字符串保存和提交。新增项省略 ID,不能传 `null`;禁止用 `Number(...)` 转换雪花 ID。
|
||||
5. 合同金额和评分使用十进制定点输入,分别限制两位小数和对应范围。
|
||||
6. 接口失败时保留用户表单,展示后端 `message`;不能只判断 HTTP 状态。
|
||||
|
||||
## 影响评估与未变化范围
|
||||
|
||||
- **是否破坏向后兼容**:对合法请求兼容;对以前可能被接受的非法格式不兼容。
|
||||
- **前端是否需要同步**:需要。请求字段和响应结构未变,但表单校验、ID 序列化及结算/开票联动应对齐。
|
||||
- **历史数据**:不迁移、不批量重写;存量数据读取不受影响,下次显式修改相关字段时按新规则校验。
|
||||
- **权限与状态机**:不变,仍使用既有管理端角色、平台权限、乐观版本、聚合锁、幂等、审批、审计和软删除规则。
|
||||
- **基础设施**:无数据库 migration、配置、Redis Key、MQ Topic、Feign 或 Gateway 路由变化。
|
||||
- **响应与脱敏**:成功响应结构不变;税号、电话、证照号和账号读取仍只返回脱敏值。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:12 个供应商聚焦测试类全部通过;Resource 及依赖模块全量 2018 项测试零失败、零错误,38 项仓库既有条件跳过,13 个 Maven 模块构建成功。
|
||||
- 独立审查:合并提交的补丁等价、字段边界、Controller/Service 入口一致性和失败零副作用测试均通过,未发现确定性缺口。
|
||||
- TEST 部署:部署任务 `b5d27d2b` 成功,`hl-resource-service` 的 8182、8082 双实例依次启动;服务器运行提交确认包含合并提交 `52a61a88c`,Nacos 两实例均为 healthy、enabled。
|
||||
- 真实 Gateway:使用真实有效 SUPER_ADMIN 登录完成 35 项验收,覆盖未认证、路径 ID、税号、公司电话、联系人电话、证照长度、初始/独立账户、税率、子表 ID、合同金额、评价分数、枚举、合法创建、脱敏读取、合法更新和业务状态失败。
|
||||
- 零写入:合法草稿建立后的数据库快照为 `1` 个主体、`1` 个类型、`1` 个联系人、`1` 个初始账户、`0` 个审批;全部失败用例执行后快照完全一致。
|
||||
- 清理:合成草稿通过既有删除接口成功清理,活动主体、类型、联系人、账户、评价和审批均为 0;按审计要求保留 3 条主体变更事实和 2 条账户变更事实,不含真实个人或企业数据。
|
||||
- 运行健康:验收后 8082、8182 仍健康,Gateway 可达,两实例本次启动日志 `ERROR=0`;未触发文件、配置或 MQ 副作用,未生成需要清理的重复确认令牌。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 52a61a88c906395d5cafd72cbda2390ad31a02a3`,经新的独立 PR 合入。
|
||||
2. 重新构建并滚动部署 `hl-resource-service`,确认部署提交不再包含本工单变更。
|
||||
3. 无数据库 migration、数据改写、配置、Redis、MQ 或跨服务契约变更,不需要执行 DDL、DML、缓存清理、消息补偿或配置恢复;现有业务和审计数据保持不变。
|
||||
4. 回退后接口路径、字段名和成功响应结构仍不变,但输入接受范围恢复为本工单前的宽松口径;管理端若已上线严格校验可继续保持,避免重新接受明显非法值。
|
||||
5. 经 Gateway 重跑合法新增、修改、提交、独立账户、未认证、失败零写入和脱敏读取用例,并确认 Nacos 双实例健康、服务日志无新增错误。
|
||||
6. 若撤回本 Changelog,以新的文档提交删除本文件或标记撤回,不重写已发布历史。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6312](https://git.1814.love:8443/wx/HL/issues/6312)
|
||||
- **PR**: [#6322](https://git.1814.love:8443/wx/HL/pulls/6322)
|
||||
- **合并提交**: [52a61a88c](https://git.1814.love:8443/wx/HL/commit/52a61a88c906395d5cafd72cbda2390ad31a02a3)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6313"
|
||||
title: "景区资源列表增加供应商全称字段"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "3c0edfd7"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-25"
|
||||
status_note: "PR #6315 已合并 dev-v3,合并提交 ae76a3407 已随 Resource 双实例滚动部署到 TEST。管理端景区分页列表现固定返回 supplierFullName;有效关联返回供应商当前全称,未关联或关系主体缺失返回 null。管理端列表列展示仍待前端接入。"
|
||||
updated_at: "2026-08-25"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 景区资源列表增加供应商全称字段
|
||||
|
||||
管理端景区分页列表新增供应商全称响应字段,供资源列表直接展示当前有效关联的供应商名称。字段只扩展管理端列表响应,不改变查询参数、分页、排序、权限或既有业务错误。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 新增字段 | 类型 | 语义 |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/admin/scenic/spots` | `data.records[].supplierFullName` | `String \| null` | 当前有效供应商关系对应的供应商全称;无有效关系或主体缺失时为 `null` |
|
||||
|
||||
`supplierFullName` 在每条管理端景区记录中固定存在。客户端可按可空字符串处理,不需要根据字段是否存在做兼容分支。
|
||||
|
||||
## 取值规则
|
||||
|
||||
- 只匹配当前未删除、`resourceModule=SCENIC` 且 `activeFlag=1` 的供应商资源关系。
|
||||
- 返回供应商主体当前的 `fullName`,不保存名称快照;供应商改名后列表随当前值更新。
|
||||
- 供应商处于暂停合作等非删除状态时仍显示全称。
|
||||
- 景区未绑定供应商时返回 `null`。
|
||||
- 有效关系指向已删除或缺失主体时记录服务端告警并返回 `null`,不会中断整页查询。
|
||||
|
||||
## 性能、安全与兼容性
|
||||
|
||||
- 空页不追加供应商查询;非空页最多一次批量查询关系、一次批量查询供应商主体,不产生逐行查询。
|
||||
- 供应商查询只投影标识和全称,不读取或返回税号、账户、联系人、资质等敏感字段。
|
||||
- `/internal/mp/scenic/list`、景区详情及其他资源列表不新增该字段。
|
||||
- 不新增筛选或排序能力,不修改请求参数、统一响应结构、权限、错误码、Gateway 路由或跨服务契约。
|
||||
- 不涉及数据库 migration、配置、Redis、MQ 或数据回写。
|
||||
- 管理端当前页面尚未增加“供应商全称”列;前端接入时直接读取 `supplierFullName`,并将 `null` 展示为空值占位即可。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:定向测试 `24/24` 通过;Resource 全量 `1977` 项零失败、零错误(38 项仓库既有条件跳过);结构检查和独立合并后审计通过。
|
||||
- 部署:TEST 任务 `447a06bc` 退出码 0,`hl-resource-service` 的 8182、8082 双实例依次健康;服务器 HEAD `29d7c011e` 明确包含合并提交 `ae76a3407`。
|
||||
- 真实 Gateway:首屏 10 条记录全部包含 `supplierFullName`,其中同时覆盖非空名称与 `null`;无匹配条件返回 `total=0`、空 `records`;统一响应 `code=200`、`success=true`。
|
||||
- 认证:不带 `Authorization` 的请求返回业务码 `401`、`success=false`、`data=null`。
|
||||
- 清理:验收只执行只读 GET,查询条件已重置;未写入数据库、缓存、MQ、审批、派单或会话数据。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit ae76a3407b5f1b2d50b0f347eb4420aaa7649435`,经独立 PR 合入。
|
||||
2. 重新构建并滚动部署 `hl-resource-service`;无需恢复数据库、配置、Redis 或 MQ。
|
||||
3. 撤回前调用方停止依赖 `supplierFullName`;撤回不会修改景区、供应商或关联数据。
|
||||
4. 撤回后经 Gateway 复测 `/admin/scenic/spots` 恢复旧响应,并确认 `/internal/mp/scenic/list`、分页和筛选保持正常。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6313](https://git.1814.love:8443/wx/HL/issues/6313)
|
||||
- **PR**: [#6315](https://git.1814.love:8443/wx/HL/pulls/6315)
|
||||
- **合并提交**: [ae76a3407](https://git.1814.love:8443/wx/HL/commit/ae76a3407b5f1b2d50b0f347eb4420aaa7649435)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6316"
|
||||
title: "供应商详情新增资源信息分页"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "de665364"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-25"
|
||||
status_note: "PR #6330 已合并 dev-v3,合并提交 4f032cd6f 已由任务 cc1fe248、2697d374 精确发布 Resource/Fleet 至 TEST 并经真实 Gateway 只读验收;验收后任务 45e843e5、c05eda6e 已回切包含该提交的 dev-v3,两个服务均保持双实例健康。"
|
||||
updated_at: "2026-08-25"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商详情新增资源信息分页
|
||||
|
||||
供应商详情新增只读“资源信息”分页接口。数据以本系统 `supplier_resource_rel` 当前有效关系为入口,展示 Resource 本地九类资源及 Fleet 车辆的当前权威信息;供应商关系已保留但资源已删除时返回缺失占位,避免详情悄然丢行。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/supplier/items/{supplierId}/resource-info/page` | 分页查询供应商当前有效关系对应的资源信息 |
|
||||
|
||||
查询参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认/约束 | 说明 |
|
||||
|---|---|---:|---|---|
|
||||
| `supplierId` | string | 是 | 正整数 | 路径参数,Snowflake ID 按字符串处理 |
|
||||
| `page` | integer | 否 | 默认 `1`,最小 `1` | 页码;兼容公共分页参数 `pageNo` |
|
||||
| `pageSize` | integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 |
|
||||
| `resourceModule` | string | 否 | 十选一 | 按关系冻结的资源模块精确筛选 |
|
||||
|
||||
支持模块:`SCENIC`、`RESTAURANT`、`SUPPLIES`、`SUPPLIES_COMBO`、`ACTIVITY`、`HOTEL`、`SERVICE`、`COST_ITEM`、`STAFF`、`VEHICLE`。
|
||||
|
||||
## 返回字段
|
||||
|
||||
分页 `data` 使用统一 `PageResult`:`records`、`total`、`page`、`pageSize`。`records[]` 字段如下:
|
||||
|
||||
| 字段 | JSON 类型 | 语义 |
|
||||
|---|---|---|
|
||||
| `relationId` | string | 供应商资源关系 ID |
|
||||
| `resourceModule` | string | 冻结模块编码 |
|
||||
| `moduleName` | string | 模块中文名 |
|
||||
| `resourceId` | string | 资源 ID;资源缺失时仍保留关系原值 |
|
||||
| `resourceName` | string/null | 当前资源名称 |
|
||||
| `coverUrl` | string/null | 当前有效封面 URL |
|
||||
| `city` | string/null | 城市展示值 |
|
||||
| `isCharged` | boolean/null | 是否收费 |
|
||||
| `isChargedName` | string/null | 是否收费中文名 |
|
||||
| `settleTypeCode` | string/null | 结算方式编码 |
|
||||
| `settleTypeName` | string/null | 结算方式中文名 |
|
||||
| `tags` | array | 标签数组,永不返回 `null` |
|
||||
| `seasons` | array | 标准季节数组,永不返回 `null` |
|
||||
| `statusCode` | string/null | 资源当前权威状态编码 |
|
||||
| `statusName` | string/null | 归一化状态中文名 |
|
||||
| `enabled` | boolean/null | 归一化启用状态 |
|
||||
| `resourceAvailable` | boolean | 资源当前是否存在;`false` 为关系保留、资源缺失 |
|
||||
| `updateTime` | string/null | 资源本身更新时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
`tags[]` 返回 `tagId`、`tagName`、`tagColor`;`seasons[]` 返回 `seasonCode`、`seasonName`。标量不适用或未知时返回 `null`,集合无数据时返回 `[]`。结果按关系 `update_time DESC, rel_id DESC` 排序,展示的 `updateTime` 则来自资源主数据。
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"relationId": "2091715622923657218",
|
||||
"resourceModule": "SCENIC",
|
||||
"moduleName": "景区管理",
|
||||
"resourceId": "2091715622923657001",
|
||||
"resourceName": "示例景区",
|
||||
"coverUrl": null,
|
||||
"city": "海拉尔",
|
||||
"isCharged": true,
|
||||
"isChargedName": "是",
|
||||
"settleTypeCode": "CASH",
|
||||
"settleTypeName": "现付",
|
||||
"tags": [
|
||||
{ "tagId": "2084636804090089473", "tagName": "自然风光", "tagColor": "#52C41A" }
|
||||
],
|
||||
"seasons": [
|
||||
{ "seasonCode": "spring", "seasonName": "春" }
|
||||
],
|
||||
"statusCode": "ENABLED",
|
||||
"statusName": "启用",
|
||||
"enabled": true,
|
||||
"resourceAvailable": true,
|
||||
"updateTime": "2026-08-25 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 权限、依赖与错误码
|
||||
|
||||
- Gateway 强制认证;服务端仅允许 `ADMIN`、`FINANCE`、`SUPER_ADMIN`,并同时要求 `supplier:view` 与 `supplier:resource:view`。
|
||||
- 任意供应商生命周期状态均可查询;接口没有写入、缓存、MQ 或操作审计副作用。
|
||||
- 本地九类资源批量查询 Resource 数据;`VEHICLE` 通过内部 Token 调用 Fleet 批量接口,管理端不得直接调用内部接口。
|
||||
- Resource、Fleet 或必要字典依赖异常时失败关闭,不返回半真半假的成功数据。
|
||||
|
||||
| 业务码 | 场景 |
|
||||
|---:|---|
|
||||
| `401` | 未认证或登录态失效 |
|
||||
| `400` | 路径/分页参数不合法 |
|
||||
| `395001` | 供应商不存在 |
|
||||
| `395034` | 不支持的 `resourceModule` |
|
||||
| `395039` | Fleet、字典或资源必要依赖不可用/响应不完整 |
|
||||
|
||||
## 管理端接入事项
|
||||
|
||||
1. 在供应商详情“账号信息”页签后新增“资源信息”页签,进入页签后按需请求本接口。
|
||||
2. 建议列为资源名称(含封面)、模块/类型、城市、是否收费、结算方式、标签、季节、状态、更新时间;字段与用户提供的资源列表示意保持一致,但以本接口实际数据为准。
|
||||
3. ID 全程按字符串传递和比较,禁止转为 JavaScript `number`。
|
||||
4. 展示名称优先使用 `moduleName`、`isChargedName`、`settleTypeName`、`statusName`;未知值显示占位符,不自行推导业务码。
|
||||
5. `tags`、`seasons` 直接按数组渲染;`resourceAvailable=false` 时保留该行并展示“资源已删除/不可用”,不要过滤。
|
||||
6. 分页筛选变化时回到第 1 页;页容量不得超过 100。管理端不提供资源关系的新增、编辑、删除或状态切换操作。
|
||||
|
||||
## 兼容性与未变化范围
|
||||
|
||||
- 新增只读 GET,不修改既有供应商详情、账号、审批、资源管理写接口及响应字段。
|
||||
- 不新增或修改数据库 migration;沿用既有 `supplier_resource_rel`、Resource 主表与 Fleet 车辆数据。
|
||||
- 不变更 Gateway 顶级路由、Nacos 配置、Redis、MQ、状态机、审批、数据范围或软删除语义。
|
||||
- 后端仓库未修改任何管理端源码;管理端接入状态保持 `pending`。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:供应商相关测试 282 项零失败(1 项条件跳过);Fleet 车辆包 228 项零失败;Resource 全量 2043 项零失败(38 项条件跳过);Fleet 可运行全量 3853 项零失败(7 项条件跳过);Gateway 路由审计 3 项零失败。
|
||||
- 独立审计:在合并提交 `4f032cd6f` 的 detached worktree 重新核对权限、错误码、软删除、缺失资源占位、Fleet 批量与失败关闭;供应商审计套件和 Fleet 审计套件 249 项均零失败。
|
||||
- TEST 精确部署:Resource 任务 `cc1fe248`(13:02:08–13:02:42)和 Fleet 任务 `2697d374`(13:03:42–13:04:18)均退出码 0;部署端 HEAD 为 `4f032cd6f`,Resource 8082/8182、Fleet 8087/8187 滚动恢复健康,Nacos `test` 中各有 2 个实例。
|
||||
- 真实 Gateway:使用现有有效管理端会话只读查询全部 3 个 TEST 供应商,实际取得 1 条 `SCENIC` 资源关系;18 个字段齐全,Snowflake ID 为字符串,`tags`/`seasons` 为数组,时间格式及模块筛选通过。不存在供应商、未知模块、`page=0`、`pageSize=101` 分别返回 `395001`、`395034`、`400`、`400`;未认证请求为 HTTP 200、业务码 `401`、`success=false`。
|
||||
- TEST 当前没有 `VEHICLE` 供应商关系样本,因此未伪造数据;跨服务车辆路径由真实双服务部署健康、内部 Token 控制器契约、批量查询、重复/缺失/额外响应拒绝及 tombstone 测试覆盖。
|
||||
- 环境恢复:验收后任务 `45e843e5`(Resource)与 `c05eda6e`(Fleet)依次回切包含本次合并的 `dev-v3`,退出码均为 0;8082/8182、8087/8187 及 Nacos 各 2 个实例保持健康,临时精确部署分支已删除。
|
||||
- 清理:验收全程只读,无供应商、资源、数据库、Redis、MQ 或配置数据需要清理。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 4f032cd6f6698607a2f1524437533d595975fc9a`,经独立 PR 合入。
|
||||
2. 依次重新构建并滚动部署 `hl-resource-service`、`hl-fleet-service`;回退期间两个服务分别维持双实例滚动策略。
|
||||
3. 本次无数据库、Redis、MQ、Nacos 或其他配置变更,不需要 DDL、DML、缓存清理、消息补偿或配置恢复;不存在不可逆数据影响。
|
||||
4. 回退后管理端停止调用新增 GET 和读取本次字段;既有供应商详情、账号及资源管理接口继续兼容。
|
||||
5. 经 Gateway 复测新增路径不可用/已撤回、既有供应商详情正常、两个服务双实例与 Nacos 健康,并核对数据库、缓存和 MQ 无副作用。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6316](https://git.1814.love:8443/wx/HL/issues/6316)
|
||||
- **PR**: [#6330](https://git.1814.love:8443/wx/HL/pulls/6330)
|
||||
- **合并提交**: [4f032cd6f](https://git.1814.love:8443/wx/HL/commit/4f032cd6f6698607a2f1524437533d595975fc9a)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6317"
|
||||
title: "供应商详情补充中文名称与资质有效状态"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "33fe59ac"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-25"
|
||||
status_note: "PR #6323 已合并 dev-v3,合并提交 e9e523733 已由部署任务 318eb344 精确发布到 TEST 并经真实 Gateway 验证;验收后任务 faa1df6e 已将 Resource 恢复到包含该提交的最新 dev-v3。供应商详情新增联系人角色、资质类型中文名,以及永久使用和有效状态字段;isRequired 保持原必备规则并集语义,仅为兼容保留,详情业务不展示。"
|
||||
updated_at: "2026-08-25"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商详情补充中文名称与资质有效状态
|
||||
|
||||
供应商基本信息详情补充面向管理端展示的中文名称和资质有效性字段。联系人角色与资质类型继续保留原始字典值,同时返回中文标签;资质有效性由有效期截止日按上海自然日动态推导。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 行为变化 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `contacts[]` 新增 `contactRoleName`;`qualifications[]` 新增 `qualTypeName`、`permanentValid`、`validityStatus` 和 `validityStatusName` |
|
||||
|
||||
原字段 `contactRole`、`qualType`、`expiryDate`、`isRequired` 和 `expired` 均继续返回,字段名与 JSON 类型不变。
|
||||
|
||||
响应片段示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"contacts": [
|
||||
{
|
||||
"contactRole": "contentBus",
|
||||
"contactRoleName": "业务人员"
|
||||
}
|
||||
],
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"qualTypeName": "营业执照",
|
||||
"expiryDate": "2027-08-25",
|
||||
"permanentValid": false,
|
||||
"validityStatus": "VALID",
|
||||
"validityStatusName": "有效",
|
||||
"isRequired": true,
|
||||
"expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 字段与状态语义
|
||||
|
||||
- `contactRoleName` 来自 `sup_content_role` 生效字典项的中文标签;`qualTypeName` 来自 `company_qualification_type` 生效字典项的中文标签。
|
||||
- 历史值、自由文本值或已停用值未命中当前生效字典时,中文名称字段回退为原始值,避免详情空白。
|
||||
- 字典依赖异常时详情查询失败关闭,不伪造硬编码中文标签。
|
||||
- `expiryDate=null` 表示永久使用,返回 `permanentValid=true`、`validityStatus=VALID`、`validityStatusName=有效`。
|
||||
- `expiryDate` 早于上海时区当天时返回 `INVALID`、`无效`;等于当天或晚于当天时返回 `VALID`、`有效`。
|
||||
- `isRequired` 继续按供应商全部类型对应必备资质规则的并集计算,不改义为“是否永久”,也不参与有效状态推导。该字段仅为历史调用方兼容保留,供应商详情业务不展示。
|
||||
- 既有 `expired` 字段继续与“截止日早于上海当天”保持一致;调用方可迁移到新的状态字段,但无需立即停止兼容读取。
|
||||
|
||||
## 管理端接入事项
|
||||
|
||||
1. 联系人角色展示使用 `contactRoleName`,提交和稳定判断仍使用 `contactRole`。
|
||||
2. 资质类型展示使用 `qualTypeName`,提交和稳定判断仍使用 `qualType`。
|
||||
3. 资质区域展示固定为“有效期至”“永久使用”“状态”;“永久使用”按 `permanentValid` 显示“是/否”,“状态”按 `validityStatusName` 显示“有效/无效”。
|
||||
4. 详情页隐藏 `isRequired`,不要将其当作永久标识;`expiryDate` 为空时可显示占位符,永久语义以 `permanentValid=true` 为准。
|
||||
|
||||
## 兼容性与未变化范围
|
||||
|
||||
- 本次是既有 GET 响应的向后兼容扩展,不新增接口、请求参数、业务错误码、Gateway 路由或权限点。
|
||||
- 不修改供应商联系人、资质、类型规则等数据库数据,不新增 migration,也不回填历史值。
|
||||
- 不改变联系人电话和证件编号脱敏、软删除、数据范围、`supplier:view` 权限或统一响应结构。
|
||||
- 不修改配置、Redis、MQ、审批、状态机或跨服务写契约。
|
||||
- 本工单只交付后端;管理端源码未在后端仓库修改,前端状态保持 `pending`,直至完成上述展示适配并提供验证提交。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:新增详情展示 4 项测试,联合供应商字典 11 项定向测试共 15 项零失败;Resource 全量 2022 项零失败,38 项仓库既有条件跳过。
|
||||
- 合并后独立审计:在合并提交 `e9e523733` 上重新执行 15 项定向测试,零失败;同时核对 Gateway 强制认证、`supplier:view` 权限、脱敏、软删除和 `isRequired` 并集逻辑均未被绕过。
|
||||
- TEST 部署:任务 `318eb344` 将合并提交 `e9e523733` 精确发布,12:07:42 至 12:08:23、退出码 0;`hl-resource-service` 的 8182、8082 依次恢复健康,部署服务器 HEAD 与合并提交一致,Nacos `test` 命名空间实例数为 2。
|
||||
- 真实 Gateway:使用现有 `SUPER_ADMIN` 会话只读查询三条有效供应商;详情实际返回 `contactRoleName=业务人员`、`qualTypeName=营业执照`、`permanentValid=true`、`validityStatus=VALID` 和 `validityStatusName=有效`;同一资质的 `isRequired=false`,证明该字段未被改义为永久标识。未认证请求返回 HTTP 200、业务码 `401`、`success=false`。
|
||||
- 环境恢复:验收后任务 `faa1df6e` 将 Resource 恢复到包含 `e9e523733` 的最新 `dev-v3`,退出码 0,8082/8182 与 Nacos 两实例保持健康;临时精确提交部署分支已删除。
|
||||
- 清理:本次验收只读查询,不创建或修改供应商、文件、数据库、Redis、MQ 或配置数据,无测试数据需要清理。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit e9e523733c51dda3e5d5cd9a69cc20972cd8b741`,经独立 PR 合入。
|
||||
2. 重新构建并滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
|
||||
3. 回退后详情不再返回本次新增的四类展示字段;历史字段和数据保持兼容,管理端应同步停止读取 `contactRoleName`、`qualTypeName`、`permanentValid`、`validityStatus` 和 `validityStatusName`。
|
||||
4. 经 Gateway 复测详情成功、未认证、无权限、历史字典值回退和资质永久/过期/今日到期/未来到期边界,并确认电话与证件号仍脱敏。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6317](https://git.1814.love:8443/wx/HL/issues/6317)
|
||||
- **PR**: [#6323](https://git.1814.love:8443/wx/HL/pulls/6323)
|
||||
- **合并提交**: [e9e523733](https://git.1814.love:8443/wx/HL/commit/e9e523733c51dda3e5d5cd9a69cc20972cd8b741)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6343"
|
||||
title: "供应商新建修改允许供应商类型为空"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "d84c6c25"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6385 已合并 dev-v3,合并提交 b8d99f58 已部署到 TEST。供应商创建草稿的 types 可省略、传 null 或空数组;更新省略 types 保持原关系,显式空数组清空无资源占用的类型;注册提交仍至少需要一个类型。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商新建修改允许供应商类型为空
|
||||
|
||||
供应商草稿阶段不再强制选择供应商类型。创建草稿可不传类型,编辑草稿也可显式清空全部未被资源占用的类型;注册提交的完整性门禁保持不变。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 行为变化 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/supplier/items/add` | `types` 省略、为 `null` 或空数组时均可创建无类型草稿;非空时继续校验最多 15 项、生效字典值和唯一性 |
|
||||
| PUT | `/admin/supplier/items/{supplierId}/update` | 省略 `types` 时保持原类型关系;显式传空数组时清空全部无资源占用的类型;非空快照继续执行原校验 |
|
||||
| POST | `/admin/supplier/items/{supplierId}/submit` | 行为不变:没有供应商类型时仍拒绝提交注册 |
|
||||
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 无类型供应商的 `types` 返回空数组 |
|
||||
|
||||
字段名和 JSON 类型没有变化。`types` 非空时仍提交对象数组,类型值来自供应商类型生效字典。
|
||||
|
||||
## 校验与错误语义
|
||||
|
||||
- 创建无类型草稿仅放宽草稿保存,不放宽注册提交、审批或状态机。
|
||||
- 更新显式清空前仍检查资源关系占用;存在有效资源关系的类型不能被移除,失败时主体、类型关系和审计保持零变化。
|
||||
- 无类型草稿提交继续返回既有业务错误码 `395008`,草稿状态和并发版本不变。
|
||||
- 非空类型继续校验最多 15 项、生效字典值、重复值和主类型约束。
|
||||
- 业务失败可能仍使用 HTTP 200,客户端必须同时检查统一响应的 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 兼容性与前端事项
|
||||
|
||||
- 管理端请求字段和响应结构不变,现有非空类型流程无需调整,因此无前端源码变更。
|
||||
- 无类型供应商仍可被列表和详情读取;客户端应兼容 `types: []`。
|
||||
- 不新增数据库 migration,不修改配置、Gateway、Redis、MQ、Nacos、Feign 或跨服务写入。
|
||||
- 本次不自动修改历史数据,也不绕过既有权限、数据范围、软删除、资源占用和审计规则。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 合并后独立审计:#6343 相关跨层定向 153 项零失败;Resource 最新目标分支全量 2111 项零失败、38 项仓库既有条件跳过;`git diff --check` 通过。
|
||||
- TEST 运行态:服务器后端仓库为 `dev-v3` 的 `1a16a5aec`,包含合并提交 `b8d99f58c`;`hl-resource-service` 的 8082、8182 双实例均监听。
|
||||
- 真实 Gateway 正向:`types` 省略、`null`、空数组分别成功创建三个 `DRAFT`,详情均返回空类型数组;带一个生效类型的草稿创建成功。
|
||||
- 更新语义:省略 `types` 后原一项类型保持不变,显式 `types: []` 后详情返回空数组。
|
||||
- 状态门禁:无类型草稿提交返回 `395008`,状态仍为 `DRAFT`、版本不变,确认零写入。
|
||||
- 权限门禁:未登录请求返回业务码 401,真实 `CUSTOMIZER` 返回 `395002`,两者均通过列表回读确认零写入;正向使用真实 `SUPER_ADMIN` 身份。
|
||||
- 清理:四条成功创建的 TEST 草稿全部删除;逐条详情返回 `395001`,按随机全名查询均为零条,没有留下测试业务数据。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit b8d99f58c84daa12ef50d68bcc3090f38f57a22d`,经独立 PR 合入。
|
||||
2. 通过 Deploy Panel API 重新构建并滚动部署 `hl-resource-service`,复核 8082、8182 双实例和 Nacos 健康状态。
|
||||
3. 无数据库、配置、Redis 或 MQ 变更,不执行 DDL、DML、缓存清理或消息补偿;不要回退后续无关工单的提交或 migration。
|
||||
4. 回退前让调用方恢复创建时至少提交一个生效类型,并停止更新时发送 `types: []`;更新不改类型时继续省略该字段。
|
||||
5. 回退后经 Gateway 复测非空类型创建、更新省略保持、无类型创建拒绝、无类型提交拒绝、资源占用移除保护和越权零写入。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6343](https://git.1814.love:8443/wx/HL/issues/6343)
|
||||
- **PR**: [#6385](https://git.1814.love:8443/wx/HL/pulls/6385)
|
||||
- **功能提交**: [d625f0448](https://git.1814.love:8443/wx/HL/commit/d625f04489655e11982b9baeeb45d74ba4f7c0e1)
|
||||
- **合并提交**: [b8d99f58c](https://git.1814.love:8443/wx/HL/commit/b8d99f58c84daa12ef50d68bcc3090f38f57a22d)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,849 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6350"
|
||||
title: "行政区划主数据与三级联动"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# User: 行政区划主数据与三级联动
|
||||
|
||||
> **服务**: `hl-user-service`(统一经 Gateway 访问)
|
||||
>
|
||||
> **PR**: #6359、#6387、#6393、#6398、#6402、#6412
|
||||
>
|
||||
> **Issue**: #6350、#6384、#6389、#6395、#6399、#6409
|
||||
>
|
||||
> **日期**: 2026-08-26
|
||||
>
|
||||
> **影响范围**: 管理后台行政区划维护、省/市/县三级选择器和历史值父链回显
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 本次补文档不改变 TEST 上已经部署的接口行为;它把原 Changelog 中分散的说明整理为可直接联调的完整契约。
|
||||
- 前端不得再把行政区划 ID 当 JavaScript number:响应中的所有行政区划 ID 均为 JSON string,缺失层级保持真正的 `null`。
|
||||
- 三级联动不允许通过行政区编码截位推断父子关系;必须逐级调用 `children`,编辑回显先调用 `path`。
|
||||
- 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
管理后台需要一套统一的行政区划主数据能力,同时支持:
|
||||
|
||||
- 省、市、县三级选择器逐级加载;
|
||||
- 编辑历史业务数据时,从任意节点恢复完整父链;
|
||||
- 具有专用权限的管理员创建自定义行政区或导航聚合节点;
|
||||
- 使用 `rowVersion` 防止两位管理员并发更新时互相覆盖。
|
||||
|
||||
接口当前只开放省、市、县三级写入,层级代码依次为 `PROVINCE`、`PREFECTURE`、`COUNTY`。接口返回的示例 ID 仅用于展示字符串类型,前端不得硬编码。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 单级联动列表 | GET | `/admin/region/children` | 新增接口 | 加载根层省级或指定父节点的当前有效直接子节点 |
|
||||
| 2 | 父链回显 | GET | `/admin/region/{id}/path` | 新增接口 | 从任意省/市/县节点恢复省级根到目标节点的完整路径 |
|
||||
| 3 | 创建行政区划节点 | POST | `/admin/region` | 新增接口 | 创建自定义行政区或不可选择的导航聚合节点 |
|
||||
| 4 | 更新行政区划节点 | PUT | `/admin/region/{id}` | 新增接口 | 使用完整可变字段和 `rowVersion` 执行 CAS 更新 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 单级联动列表 `GET /admin/region/children`
|
||||
|
||||
**VO**: `AdministrativeRegionChildrenVO / AdministrativeRegionItemVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
- 新建页面首次加载省级列表:省略 `parentId`。
|
||||
- 选择省后加载市、选择市后加载县:把当前选中节点 ID 作为下一次请求的 `parentId`。
|
||||
- 编辑页面逐层恢复默认值:把本层历史 ID 作为 `selectedId`;它仍在当前 `items` 中时会成为 `defaultId`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` | 真实管理端登录令牌 |
|
||||
| `parentId` | Query | String | ❌ | 十进制正整数;根层省级列表必须省略 | 指定要查询直接子节点的父节点 ID |
|
||||
| `selectedId` | Query | String | ❌ | 十进制正整数 | 编辑回显时希望优先选中的本级节点 ID |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `Result<AdministrativeRegionChildrenVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示成功;业务失败读取具体业务码 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 仅当 `code=200` 时为 `true` |
|
||||
| `traceId` | String / null | 链路追踪 ID;报错排查时提供给后端 |
|
||||
| `data.parentId` | String / null | 本次查询父节点;根层查询为 `null` |
|
||||
| `data.defaultId` | String / null | 合法 `selectedId`,否则为排序后首项 ID;空列表为 `null` |
|
||||
| `data.items` | Array | 当前有效且状态为 `ACTIVE` 的直接子节点,稳定排序 |
|
||||
| `data.items[].id` | String | 节点 ID,始终为字符串 |
|
||||
| `data.items[].parentId` | String / null | 父节点 ID;省级根节点为 `null` |
|
||||
| `data.items[].codeStandard` | String | 编码标准,例如 `GB/T 2260`、`HL_CUSTOM`、`HL_GROUP` |
|
||||
| `data.items[].regionCode` | String | 编码标准内的地区代码 |
|
||||
| `data.items[].levelCode` | String | `PROVINCE`、`PREFECTURE` 或 `COUNTY` |
|
||||
| `data.items[].depth` | Integer | 树深度,省/市/县分别为 1/2/3 |
|
||||
| `data.items[].name` | String | 行政区划完整名称 |
|
||||
| `data.items[].shortName` | String / null | 简称 |
|
||||
| `data.items[].nodeKind` | String | `ADMINISTRATIVE` 或 `AGGREGATION` |
|
||||
| `data.items[].navigable` | Boolean | 是否允许继续查询下一层 |
|
||||
| `data.items[].selectable` | Boolean | 是否允许作为最终业务行政区值 |
|
||||
| `data.items[].hasChildren` | Boolean | 是否存在当前有效、启用的直接子节点 |
|
||||
| `data.items[].currentlyEffective` | Boolean | 中国业务当日是否位于有效期且状态启用 |
|
||||
| `data.items[].status` | String | `ACTIVE` 或 `INACTIVE` |
|
||||
| `data.items[].validFrom` | String | 生效日期,格式 `yyyy-MM-dd` |
|
||||
| `data.items[].validTo` | String / null | 失效日期;`null` 表示持续有效 |
|
||||
| `data.items[].sortOrder` | Integer | 同级排序号 |
|
||||
| `data.items[].rowVersion` | Integer | 当前 CAS 版本,更新时原样提交 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/children?parentId=2090000000000000001&selectedId=2090000000000000035 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
根层省级列表:
|
||||
|
||||
```http
|
||||
GET /admin/region/children HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"parentId": "2090000000000000001",
|
||||
"items": [
|
||||
{
|
||||
"id": "2090000000000000035",
|
||||
"parentId": "2090000000000000001",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110100",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"rowVersion": 0
|
||||
}
|
||||
],
|
||||
"defaultId": "2090000000000000035"
|
||||
},
|
||||
"traceId": "a1b2c3d4-e5f6-7890",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
父节点存在但没有当前有效直接子节点时,返回成功空数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"parentId": "2090000000000000035",
|
||||
"items": [],
|
||||
"defaultId": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
父节点不存在或已删除:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210801,
|
||||
"message": "行政区划节点不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只返回 `status=ACTIVE` 且当前有效的直接子节点,停用或历史节点不会混入当前选择项。
|
||||
- `selectedId` 不在本次 `items` 中时不会报错,而是稳定回退到排序后的第一项;空列表回退为 `null`。
|
||||
- 排序固定为 `sortOrder`、`regionCode`、`id`,前端不得另用行政区编码推断顺序或父子关系。
|
||||
- `navigable` 控制能否继续向下加载,`selectable` 控制能否作为最终值;两者不能相互替代。
|
||||
- 未登录返回业务码 `401`;`parentId=0` 等非正整数参数返回业务码 `400`。
|
||||
|
||||
---
|
||||
|
||||
### 2. 父链回显 `GET /admin/region/{id}/path`
|
||||
|
||||
**VO**: `AdministrativeRegionPathVO / AdministrativeRegionItemVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整省/市/县父链,再按父链逐层调用 `children` 加载每一级可选项。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` | 真实管理端登录令牌 |
|
||||
| `id` | Path | String | ✅ | 十进制正整数 | 要回显的省、市或县节点 ID |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参 `Result<AdministrativeRegionPathVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示成功;业务失败读取具体业务码 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 仅当 `code=200` 时为 `true` |
|
||||
| `data.selectedId` | String | 调用方传入并成功解析的目标节点 ID |
|
||||
| `data.provinceId` | String | 父链中的省级 ID |
|
||||
| `data.prefectureId` | String / null | 父链中的地级 ID;目标为省级时为 `null` |
|
||||
| `data.countyId` | String / null | 父链中的县级 ID;目标高于县级时为 `null` |
|
||||
| `data.path` | Array | 从省级根到目标节点的完整有序父链 |
|
||||
| `data.path[].id` | String | 节点 ID,始终为字符串 |
|
||||
| `data.path[].parentId` | String / null | 父节点 ID;首个省级节点为 `null` |
|
||||
| `data.path[].codeStandard` | String | 编码标准 |
|
||||
| `data.path[].regionCode` | String | 地区代码 |
|
||||
| `data.path[].levelCode` | String | 省/市/县层级代码 |
|
||||
| `data.path[].depth` | Integer | 从 1 开始的连续深度 |
|
||||
| `data.path[].name` | String | 完整名称 |
|
||||
| `data.path[].shortName` | String / null | 简称 |
|
||||
| `data.path[].nodeKind` | String | 节点类型 |
|
||||
| `data.path[].navigable` | Boolean | 是否允许逐级导航 |
|
||||
| `data.path[].selectable` | Boolean | 是否允许作为最终值 |
|
||||
| `data.path[].hasChildren` | Boolean | 是否存在当前有效直接子节点 |
|
||||
| `data.path[].currentlyEffective` | Boolean | 当前是否有效;历史节点可能为 `false` |
|
||||
| `data.path[].status` | String | `ACTIVE` 或 `INACTIVE` |
|
||||
| `data.path[].validFrom` | String | 生效日期 |
|
||||
| `data.path[].validTo` | String / null | 失效日期 |
|
||||
| `data.path[].sortOrder` | Integer | 同级排序号 |
|
||||
| `data.path[].rowVersion` | Integer | 当前 CAS 版本 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/region/2090000000000000376/path HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"selectedId": "2090000000000000376",
|
||||
"provinceId": "2090000000000000001",
|
||||
"prefectureId": "2090000000000000035",
|
||||
"countyId": "2090000000000000376",
|
||||
"path": [
|
||||
{
|
||||
"id": "2090000000000000001",
|
||||
"parentId": null,
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110000",
|
||||
"levelCode": "PROVINCE",
|
||||
"depth": 1,
|
||||
"name": "北京市",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"rowVersion": 0
|
||||
},
|
||||
{
|
||||
"id": "2090000000000000035",
|
||||
"parentId": "2090000000000000001",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110100",
|
||||
"levelCode": "PREFECTURE",
|
||||
"depth": 2,
|
||||
"name": "北京市",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"hasChildren": true,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"rowVersion": 0
|
||||
},
|
||||
{
|
||||
"id": "2090000000000000376",
|
||||
"parentId": "2090000000000000035",
|
||||
"codeStandard": "GB/T 2260",
|
||||
"regionCode": "110101",
|
||||
"levelCode": "COUNTY",
|
||||
"depth": 3,
|
||||
"name": "东城区",
|
||||
"shortName": null,
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"hasChildren": false,
|
||||
"currentlyEffective": true,
|
||||
"status": "ACTIVE",
|
||||
"validFrom": "2025-12-31",
|
||||
"validTo": null,
|
||||
"sortOrder": 100,
|
||||
"rowVersion": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口不返回“成功但空父链”。目标不存在、父链不完整或超过安全深度时使用错误响应,不返回部分路径。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210801,
|
||||
"message": "行政区划节点不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
父链存在孤儿、越级、循环或异常终止:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210811,
|
||||
"message": "行政区划父链数据不完整",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `path` 必须从省级根开始,`parentId`、`depth` 和 `levelCode` 逐级连续,最后一个节点必须等于 `selectedId`。
|
||||
- 目标为省级时 `prefectureId`、`countyId` 均为 `null`;目标为地级时只有 `countyId=null`,绝不会返回字符串 `"null"`。
|
||||
- 历史停用节点允许回显,节点自身 `currentlyEffective=false`;前端只能展示历史值,不能把它重新加入当前可选列表。
|
||||
- 父链异常时整体失败,不返回可被误用的部分路径。
|
||||
- 未登录返回业务码 `401`;不存在节点返回 `210801`。
|
||||
|
||||
---
|
||||
|
||||
### 3. 创建行政区划节点 `POST /admin/region`
|
||||
|
||||
**VO**: `AdministrativeRegionCreateReqVO / String`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:create` 权限的管理员创建自定义业务行政区,或创建只用于层级导航、不可作为最终业务值的聚合节点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` 且具有 `system:region:create` | 管理端登录令牌 |
|
||||
| `codeStandard` | Body | String | ✅ | 1~32 字符;聚合节点必须为 `HL_GROUP` | 编码命名空间,自定义行政区推荐 `HL_CUSTOM` |
|
||||
| `regionCode` | Body | String | ✅ | 1~64 字符;首字符为字母或数字;仅含字母、数字、`.`、`_`、`:`、`-` | 编码标准内的唯一地区代码;服务端转大写 |
|
||||
| `parentId` | Body | String / null | ❌ | 十进制正整数;创建省级根节点时为 `null` | 父节点 ID |
|
||||
| `levelCode` | Body | String | ✅ | 1~32 字符;省/市/县必须匹配派生深度 | `PROVINCE`、`PREFECTURE`、`COUNTY` |
|
||||
| `nodeKind` | Body | String | ✅ | `ADMINISTRATIVE` 或 `AGGREGATION` | 节点业务类型 |
|
||||
| `name` | Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
|
||||
| `shortName` | Body | String / null | ❌ | 最多 64 字符;空白归一为 `null` | 简称 |
|
||||
| `navigable` | Body | Boolean | ✅ | 聚合节点固定为 `true` | 是否允许继续向下导航 |
|
||||
| `selectable` | Body | Boolean | ✅ | 聚合节点固定为 `false` | 是否允许作为最终业务值 |
|
||||
| `validFrom` | Body | String | ✅ | `yyyy-MM-dd` | 生效日期 |
|
||||
| `validTo` | Body | String / null | ❌ | `yyyy-MM-dd`,必须严格晚于 `validFrom` | 失效日期;`null` 表示持续有效 |
|
||||
| `status` | Body | String | ✅ | `ACTIVE` 或 `INACTIVE` | 节点状态 |
|
||||
| `sortOrder` | Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
|
||||
| `sourceVersion` | Body | String | ✅ | 非空,最多 64 字符 | 可审计的数据来源版本 |
|
||||
| `sourceRef` | Body | String | ✅ | 非空,最多 512 字符 | 来源 URL、文件定位或管理批次标识 |
|
||||
| `extension` | Body | Object / null | ❌ | 必须是 JSON object,序列化后最多 4000 字符 | 结构化扩展;旧入参别名 `extJson` 仍兼容 |
|
||||
|
||||
`depth`、操作人和来源校验值由服务端派生,禁止由前端提交。
|
||||
|
||||
#### 出参 `Result<String>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示创建成功 |
|
||||
| `message` | String | 响应说明 |
|
||||
| `success` | Boolean | 创建成功为 `true` |
|
||||
| `data` | String | 新节点 ID,始终为 JSON string |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/region HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <具有 system:region:create 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"codeStandard": "HL_CUSTOM",
|
||||
"regionCode": "DEMO-COUNTY-001",
|
||||
"parentId": "2090000000000000035",
|
||||
"levelCode": "COUNTY",
|
||||
"nodeKind": "ADMINISTRATIVE",
|
||||
"name": "示例业务区",
|
||||
"shortName": "示例区",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"validFrom": "2026-08-26",
|
||||
"validTo": null,
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 10000,
|
||||
"sourceVersion": "ADMIN-20260826",
|
||||
"sourceRef": "工单或数据批次定位",
|
||||
"extension": {
|
||||
"businessNote": "示例扩展信息"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": "2090000000000001001",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口成功时一定返回字符串 ID,不存在 `data=null` 的成功语义。失败时不创建节点。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
层级代码与父链派生深度不一致:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210813,
|
||||
"message": "行政区划层级代码与父链深度不一致",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 当前最大写入深度为 3;省、市、县层级必须分别使用 `PROVINCE`、`PREFECTURE`、`COUNTY`。
|
||||
- 父节点必须存在、可导航、有效期完整覆盖子节点,并且不会让写入深度超过三级。
|
||||
- `AGGREGATION` 必须同时满足 `codeStandard=HL_GROUP`、`navigable=true`、`selectable=false`;它只能导航,不能作为最终业务值。
|
||||
- `ADMINISTRATIVE` 不得占用 `HL_GROUP`;旧命名空间 `HL_INTERNAL` 不允许产生新写入。
|
||||
- 同一 `codeStandard + regionCode` 的版本有效期不得重叠;有效期采用左闭右开 `[validFrom, validTo)`。
|
||||
- 缺少权限返回 `210802`;空请求或格式错误返回 `400`;全部失败路径零业务写入。
|
||||
|
||||
---
|
||||
|
||||
### 4. 更新行政区划节点 `PUT /admin/region/{id}`
|
||||
|
||||
**VO**: `AdministrativeRegionUpdateReqVO / Void`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
具有 `system:region:update` 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带最近读取到的完整可变字段和 `rowVersion`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` 且具有 `system:region:update` | 管理端登录令牌 |
|
||||
| `id` | Path | String | ✅ | 十进制正整数 | 要更新的节点 ID |
|
||||
| `parentId` | Body | String / null | ❌ | 十进制正整数;省级根节点为 `null` | 更新后的父节点 ID |
|
||||
| `levelCode` | Body | String | ✅ | 1~32 字符;必须匹配更新后父链深度 | 省/市/县层级代码 |
|
||||
| `name` | Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
|
||||
| `shortName` | Body | String / null | ❌ | 最多 64 字符;空白归一为 `null` | 简称 |
|
||||
| `navigable` | Body | Boolean | ✅ | 聚合节点必须保持 `true` | 是否允许继续向下导航 |
|
||||
| `selectable` | Body | Boolean | ✅ | 聚合节点必须保持 `false` | 是否允许作为最终业务值 |
|
||||
| `validFrom` | Body | String | ✅ | `yyyy-MM-dd` | 生效日期 |
|
||||
| `validTo` | Body | String / null | ❌ | 必须严格晚于 `validFrom` | 失效日期 |
|
||||
| `status` | Body | String | ✅ | `ACTIVE` 或 `INACTIVE` | 节点状态 |
|
||||
| `sortOrder` | Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
|
||||
| `extension` | Body | Object / null | ❌ | 必须是 JSON object,最多 4000 字符 | 结构化扩展;旧别名 `extJson` 仍兼容 |
|
||||
| `rowVersion` | Body | Integer | ✅ | 大于等于 0,必须等于当前版本 | CAS 并发版本,成功后自动加 1 |
|
||||
|
||||
`codeStandard`、`regionCode`、`nodeKind`、`sourceVersion` 和 `sourceRef` 不属于更新接口,不能通过请求改写。
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | Integer | `200` 表示更新成功 |
|
||||
| `message` | String | 成功时为“行政区划更新成功” |
|
||||
| `success` | Boolean | 更新成功为 `true` |
|
||||
| `data` | null | 更新接口不返回业务数据 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /admin/region/2090000000000001001 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <具有 system:region:update 的管理员令牌>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentId": "2090000000000000035",
|
||||
"levelCode": "COUNTY",
|
||||
"name": "示例业务区(更新)",
|
||||
"shortName": "示例区",
|
||||
"navigable": true,
|
||||
"selectable": true,
|
||||
"validFrom": "2026-08-26",
|
||||
"validTo": null,
|
||||
"status": "ACTIVE",
|
||||
"sortOrder": 10001,
|
||||
"extension": {
|
||||
"businessNote": "名称已更新"
|
||||
},
|
||||
"rowVersion": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "行政区划更新成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口成功时 `data=null` 是固定契约,调用方以 `code=200 && success=true` 判断成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
提交了已经过期的 `rowVersion`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 210807,
|
||||
"message": "行政区划已被其他操作更新,请刷新后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 更新采用完整可变字段快照,不是局部 PATCH;前端必须先读取最新节点信息再提交。
|
||||
- `rowVersion` 冲突返回 `210807` 且零写入;前端应重新加载,而不是自动覆盖重试。
|
||||
- 有子节点的分支禁止移动父级或改变层级,返回 `210810`。
|
||||
- 父节点提前停用、取消导航或缩短有效期,导致现有子节点失去合法父级时返回 `210814`。
|
||||
- 已到生效日的 `GB/T 2260` 法定节点不能原位改写历史属性;应先关闭旧版本,再创建有效期不重叠的新版本。
|
||||
- 成功后 `rowVersion` 增加 1;再次编辑必须使用新版本号。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 三级联动新建流程
|
||||
|
||||
```text
|
||||
GET /admin/region/children
|
||||
→ 用户选择 provinceId
|
||||
GET /admin/region/children?parentId={provinceId}
|
||||
→ 用户选择 prefectureId
|
||||
GET /admin/region/children?parentId={prefectureId}
|
||||
→ 用户选择 countyId
|
||||
→ 业务表单最终保存 countyId(或业务允许的上级 selectable 节点)
|
||||
```
|
||||
|
||||
### 编辑历史值回显流程
|
||||
|
||||
```text
|
||||
GET /admin/region/{savedRegionId}/path
|
||||
→ 读取 provinceId / prefectureId / countyId
|
||||
→ 按 path 顺序逐层调用 children
|
||||
→ 每层把对应历史 ID 作为 selectedId
|
||||
→ 历史节点 currentlyEffective=false 时只展示历史值,不重新作为当前选项提交
|
||||
```
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 调用或 payload |
|
||||
|---|---|
|
||||
| ✅ 加载省级 | `GET /admin/region/children`,省略 `parentId` |
|
||||
| ✅ 加载市级 | `GET /admin/region/children?parentId={provinceId}` |
|
||||
| ✅ 编辑回显 | 先 `GET /admin/region/{savedId}/path`,再逐层加载 `children` |
|
||||
| ✅ 比较 ID | 全程以 string 比较,例如 `item.id === selectedId` |
|
||||
| ❌ 根层传 0 | `GET /admin/region/children?parentId=0` → 400 |
|
||||
| ❌ 截行政区编码推父级 | 根据 `110101` 截取 `110100`;父子关系只能以接口返回的 ID 为准 |
|
||||
| ❌ 把聚合节点当最终值 | `nodeKind=AGGREGATION && selectable=false` 时仍提交为业务行政区 |
|
||||
| ❌ 强转 number | `Number("2090000000000000376")` 会丢失精度 |
|
||||
|
||||
### 统一响应判断
|
||||
|
||||
```javascript
|
||||
if (response.code !== 200 || response.success !== true) {
|
||||
throw new Error(response.message || '行政区划接口调用失败');
|
||||
}
|
||||
```
|
||||
|
||||
禁止只判断 HTTP status;业务错误可能使用 HTTP 200 返回。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端操作 | 外部可观察的持久化行为 | 失败行为 |
|
||||
|---|---|---|
|
||||
| 查询 `children` | 只读,不改变节点、版本或审计 | 不写入 |
|
||||
| 查询 `path` | 只读,不改变节点、版本或审计 | 不写入,不返回部分父链 |
|
||||
| 创建节点 | 成功新增一个节点,初始 `rowVersion=0`;事务提交后联动和父链读取可见 | 参数、权限、父链、有效期或唯一性失败时不新增节点 |
|
||||
| 更新节点 | 成功更新一个节点并令 `rowVersion+1`;事务提交后联动和父链返回新值 | 版本冲突或业务 guard 失败时原节点保持不变 |
|
||||
|
||||
前端无需理解或操作物理表、缓存键和审计表,也不得通过其他接口绕过创建/更新权限和版本检查。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录:业务码 `401`。
|
||||
- 已登录但缺少创建/更新专用权限:`210802`。
|
||||
- Query/Path ID 为 0、负数或非十进制正整数:`400`。
|
||||
- 父节点或目标节点不存在:`210801`。
|
||||
- 父级不可导航、有效期不能覆盖子级:`210803`。
|
||||
- 写入超过当前三级边界:`210804`。
|
||||
- 自引用或父链成环:`210805`。
|
||||
- 当前编码或版本冲突:`210806` / `210815`。
|
||||
- `rowVersion` 过期:`210807`,零写入。
|
||||
- `validTo` 不晚于 `validFrom`:`210808`。
|
||||
- 父链异常:`210811`,不返回部分路径。
|
||||
- `extension` 不是 JSON object 或过长:`210812`。
|
||||
- 层级代码与父链深度不一致:`210813`。
|
||||
- 老数据兼容:停用历史节点可由 `path` 回显;旧缓存中的数字 ID 可由后端读取,但当前 API 响应始终输出 string ID。
|
||||
|
||||
### 业务错误码
|
||||
|
||||
| code | message | 典型接口与场景 |
|
||||
|---:|---|---|
|
||||
| `400` | 参数校验失败文案 | 任一接口的 ID 非正整数、Body 缺少必填字段或格式不合法 |
|
||||
| `401` | 未认证文案 | 任一接口未携带有效管理端身份 |
|
||||
| `210801` | 行政区划节点不存在 | `children` 父节点或 `path`/`update` 目标不存在 |
|
||||
| `210802` | 无行政区划维护权限 | `create` / `update` 缺少专用写权限 |
|
||||
| `210803` | 行政区划父节点不合法 | 父级不可导航、已失效或有效期不能覆盖子级 |
|
||||
| `210804` | 行政区划层级超过当前允许深度 | `create` / `update` 试图写入第四级 |
|
||||
| `210805` | 行政区划父链存在循环 | `update` 自引用或移动后形成祖先环 |
|
||||
| `210806` | 行政区划当前编码已存在 | `create` 唯一编码冲突 |
|
||||
| `210807` | 行政区划已被其他操作更新,请刷新后重试 | `update.rowVersion` 已过期 |
|
||||
| `210808` | 行政区划有效期不合法 | `validTo` 不晚于 `validFrom` |
|
||||
| `210809` | 该行政区划编码标准为系统保留值 | 新写入继续使用旧 `HL_INTERNAL` |
|
||||
| `210810` | 存在子节点的行政区划不能移动或变更层级 | `update` 移动分支或改变分支层级 |
|
||||
| `210811` | 行政区划父链数据不完整 | `path` 遇到孤儿、越级、循环或异常终止 |
|
||||
| `210812` | 行政区划扩展字段必须是合法 JSON | `extension` 不是 JSON object 或内容过长 |
|
||||
| `210813` | 行政区划层级代码与父链深度不一致 | 省/市/县代码与派生深度不匹配 |
|
||||
| `210814` | 行政区划更新与现有子节点状态或有效期冲突 | 父级提前停用、取消导航或缩短有效期 |
|
||||
| `210815` | 行政区划编码的版本有效期与既有数据重叠 | 同编码的两个版本时间窗重叠 |
|
||||
| `210816` | 行政区划节点类型或编码命名空间不合法 | `nodeKind`、`codeStandard`、可导航/可选组合错误 |
|
||||
| `210817` | 已生效法定行政区划只能关闭旧版本后新增 | 原位改写生效中的 `GB/T 2260` 节点 |
|
||||
| `210818` | 行政区划数据来源信息不完整 | `sourceVersion` 或 `sourceRef` 缺失/过长 |
|
||||
| `210819` | 法定行政区划代码必须为六位数字 | `GB/T 2260` 的 `regionCode` 格式错误 |
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `levelCode`(行政区划层级)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.levelCode`、`AdministrativeRegionUpdateReqVO.levelCode`、`AdministrativeRegionItemVO.levelCode` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `PROVINCE` | 省级 | 深度 1,父节点必须为 `null` |
|
||||
| `PREFECTURE` | 地级 | 深度 2,父节点必须是省级 |
|
||||
| `COUNTY` | 县级 | 深度 3,父节点必须是地级 |
|
||||
|
||||
### `nodeKind`(节点类型)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.nodeKind`、`AdministrativeRegionItemVO.nodeKind` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `ADMINISTRATIVE` | 行政区节点 | 可按 `selectable` 决定是否作为最终业务值;不得使用 `HL_GROUP` |
|
||||
| `AGGREGATION` | 导航聚合节点 | 必须使用 `HL_GROUP`、`navigable=true`、`selectable=false` |
|
||||
|
||||
### `status`(节点状态)
|
||||
|
||||
**所属字段**: 创建/更新请求及节点响应的 `status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `ACTIVE` | 启用 | 位于有效期时可进入当前联动列表 |
|
||||
| `INACTIVE` | 停用 | 不进入当前联动列表,但历史父链仍可回显 |
|
||||
|
||||
### `codeStandard`(编码命名空间)
|
||||
|
||||
**所属字段**: `AdministrativeRegionCreateReqVO.codeStandard`、`AdministrativeRegionItemVO.codeStandard` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `GB/T 2260` | 法定行政区编码 | `regionCode` 必须为六位数字;生效历史受不可变保护 |
|
||||
| `HL_CUSTOM` | 自定义行政区编码 | 管理端创建普通自定义行政区的推荐命名空间 |
|
||||
| `HL_GROUP` | 导航聚合编码 | 只能与 `AGGREGATION` 搭配 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
本条为新增接口,没有需要兼容的旧 `/admin/region/**` 公开接口。补充工单 #6409 在正式前端接入前固定了 ID 输出类型:
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 初始实现风险 | 当前已部署契约 |
|
||||
|---|---|---|
|
||||
| 所有行政区划 Long ID | 小数值可能被全局序列化规则输出为 JSON number | 一律输出 JSON string |
|
||||
| 缺失父级/层级 | 可能被调用方误当字符串处理 | 保持真正的 JSON `null` |
|
||||
| `extension` 历史入参 | 旧调用方可能使用 `extJson` | 请求继续兼容 `extJson`,当前文档统一使用 `extension` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 接入前 | 当前已部署行为 |
|
||||
|---|---|---|
|
||||
| 三级联动 | 无统一公开接口,容易截行政区编码推断 | 逐级调用 `children`,以后端父子关系为准 |
|
||||
| 编辑回显 | 调用方自行拼装父链 | 调用 `path` 返回完整有序父链和三级快捷 ID |
|
||||
| 并发更新 | 无本接口旧语义 | 必须提交 `rowVersion`,冲突返回 `210807` 且零写入 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否;这是新增接口,且正式前端接入前已经固定字符串 ID 契约。
|
||||
- **前端是否必须同步上线**: 是;`frontend_status=pending`,管理端需接入三级选择器、历史回显和主数据维护。
|
||||
- **前端 workaround 清理点**: 不得保留行政区编码截位、Long ID 转 number、只判断 HTTP status 或把 `hasChildren` 等同于 `selectable` 的临时逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台行政区划维护、省/市/县三级选择器及历史行政区值回显。
|
||||
- **零影响**:
|
||||
- 小程序端现有行政区接口;
|
||||
- Product、Order、Resource 和 Fleet 现有业务接口;
|
||||
- 已保存业务数据中的行政区 ID;
|
||||
- 登录、权限菜单和非行政区划缓存;
|
||||
- 生产环境(本 Changelog 只证明 TEST 已部署和验收)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
```text
|
||||
GET /admin/region/children → 根级/子级列表、defaultId、string/null ID ✓
|
||||
GET /admin/region/{id}/path → 省市县完整父链、缺失层级 null、string ID ✓
|
||||
POST /admin/region → 创建成功返回 string ID,数据库与缓存读取一致 ✓
|
||||
PUT /admin/region/{id} → CAS 更新成功,rowVersion 0→1,联动与父链同步 ✓
|
||||
GET /admin/region/children(未认证) → 401 ✓
|
||||
POST /admin/region(缺参/非法节点类型) → 400 且零业务写入 ✓
|
||||
GET /admin/region/{missingId}/path → 210801 ✓
|
||||
```
|
||||
|
||||
- TEST 精确提交:`b62054035a139e3c689179ef75e2d052d08dde52`,包含六个相关 PR 的合并提交。
|
||||
- Deploy Panel API 任务:`52771074`(`hl-user-service`)和 `b1db24b2`(`hl-gateway`),终态均为 `success`,双实例、Nacos、滚动采样及部署窗口日志检查通过。
|
||||
- 真实 TEST 管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、持久化结果和缓存一致性验收。
|
||||
- 验收创建的临时节点已精确清理;数据库数量与测试命名空间恢复基线,四个缓存快照按原值和绝对过期时间恢复,登录会话已注销;操作审计按系统设计保留。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|---|---|---|---|
|
||||
| #6359 | #6350 | 行政区划主数据、三级写入与读接口基础实现 | ✅ 有效 |
|
||||
| #6387 | #6384 | 权限、来源和节点类型契约补强 | ✅ 有效 |
|
||||
| #6393 | #6389 | 有效期、父链和历史版本保护补强 | ✅ 有效 |
|
||||
| #6398 | #6395 | Gateway 路由与认证策略补齐 | ✅ 有效 |
|
||||
| #6402 | #6399 | 缓存一致性与滚动兼容补强 | ✅ 有效 |
|
||||
| #6412 | #6409 | 所有行政区划响应 ID 固定为 JSON string | ✅ 有效 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 主 Issue: [wx/HL#6350](https://git.1814.love:8443/wx/HL/issues/6350)
|
||||
- ID 字符串补充 Issue: [wx/HL#6409](https://git.1814.love:8443/wx/HL/issues/6409)
|
||||
- 主 PR: [wx/HL#6359](https://git.1814.love:8443/wx/HL/pulls/6359)
|
||||
- ID 字符串 PR: [wx/HL#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
|
||||
- 本次文档补充 Issue: [wx/HL#6422](https://git.1814.love:8443/wx/HL/issues/6422)
|
||||
|
||||
---
|
||||
|
||||
## 撤回
|
||||
|
||||
本次 #6422 仅补充 Changelog 和模板校验,不改变后端制品。若文档补充需要撤回,从最新 `hl-api-changelog/main` 创建独立分支,revert #6422 对应合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据、配置、Redis 和 MQ 均不需要回退。撤回后前端暂以当前 Controller/VO 契约为准,并重新提交一份通过模板门禁的自包含文档。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6350](https://git.1814.love:8443/wx/HL/issues/6350)
|
||||
- **补充文档 Issue**: [#6422](https://git.1814.love:8443/wx/HL/issues/6422)
|
||||
- **PR**: [#6359](https://git.1814.love:8443/wx/HL/pulls/6359)
|
||||
- **ID 契约 PR**: [#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
|
||||
- **最终后端提交**: [b62054035](https://git.1814.love:8443/wx/HL/commit/b62054035a139e3c689179ef75e2d052d08dde52)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,369 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6391"
|
||||
title: "供应商法人证件号与身份证正反面"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "74b3d8bf"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6401 已合并 dev-v3 并部署 TEST;供应商创建、更新、提交新增法人身份证号及正反面永久地址,详情仅返回脱敏证件号。旧客户端省略三字段时保持兼容,前端待接入输入与双面上传。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商法人证件号与身份证正反面
|
||||
|
||||
供应商“基本信息-法定代表人”新增身份证号、人像面和国徽面三个字段。身份证图片继续使用既有文件上传能力,本次接口只接收上传完成后的永久 HTTPS 地址,不新增上传或 OCR 接口。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 权限 | 变化 |
|
||||
|---|---|---|---|
|
||||
| POST | `/admin/supplier/items/add` | `supplier:create` | 创建草稿可保存三个法人证件字段 |
|
||||
| PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 增量更新三个法人证件字段 |
|
||||
| POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update`、`supplier:approval:submit` | 完整注册表单可提交三个法人证件字段 |
|
||||
| GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `supplier:view` | 返回脱敏证件号及身份证正反面地址 |
|
||||
|
||||
## 通用字段与校验
|
||||
|
||||
| 字段 | 类型 | 必填 | 规则 |
|
||||
|---|---|---|---|
|
||||
| `legalRepresentativeIdNo` | string | 条件必填 | 18 位中国大陆居民身份证号;校验长度、出生日期、顺序码及校验位;末位小写 `x` 会规范为大写 `X` |
|
||||
| `legalRepresentativeIdCardFrontUrl` | string | 条件必填 | 身份证人像面永久地址,最长 1000 字符;必须为公网 HTTPS 地址,且不能含账号密码、查询串或片段 |
|
||||
| `legalRepresentativeIdCardBackUrl` | string | 条件必填 | 身份证国徽面永久地址,规则同人像面 |
|
||||
|
||||
三个字段必须“全部省略”或“同时提供”:
|
||||
|
||||
- 全部省略:兼容旧客户端和没有法人证件数据的存量供应商。
|
||||
- 任意一个有值:三个字段必须同时有值,不能只保存证件号或单面图片。
|
||||
- 法定代表人可能对应多个供应商,证件号不作为供应商之间的唯一键。
|
||||
- 写接口的成功响应不回传证件号;需要展示时调用详情接口。
|
||||
- 统一响应可能以 HTTP 200 承载业务失败,必须同时判断 `code`、`success` 和 `data`。
|
||||
|
||||
## 1. 创建供应商草稿
|
||||
|
||||
### 使用场景
|
||||
|
||||
在新建供应商基本信息时,同时保存法定代表人身份证号及正反面扫描件地址。
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/add
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
本次新增的三个字段遵循上方通用规则;创建草稿的既有最低必填字段仍为 `fullName`、`taxNo`、`mainCooperation`。
|
||||
|
||||
典型成功请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "法人证件联调示例供应商",
|
||||
"taxNo": "L6391EXAMPLE001",
|
||||
"legalRepresentative": "示例法人",
|
||||
"legalRepresentativeIdNo": "11010519491231002x",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
|
||||
"mainCooperation": "旅游资源供应"
|
||||
}
|
||||
```
|
||||
|
||||
典型成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "1900000000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-26 11:40:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
兼容边界:旧客户端可完全省略三个新字段,其余请求保持原样。
|
||||
|
||||
异常请求(仅传人像面):
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "法人证件联调示例供应商",
|
||||
"taxNo": "L6391EXAMPLE001",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
|
||||
"mainCooperation": "旅游资源供应"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "法定代表人证件号、人像面和国徽面必须同时提供",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
失败不会创建供应商草稿。
|
||||
|
||||
## 2. 更新供应商
|
||||
|
||||
### 使用场景
|
||||
|
||||
在供应商基本信息编辑页补录或替换完整的法人证件三字段。
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
PUT /admin/supplier/items/1900000000000000001/update
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
`changeReason` 和 `expectedUpdateTime` 沿用原接口必填约束;三个法人证件字段作为一组增量字段处理。典型成功请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"legalRepresentative": "示例法人",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front-v2.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back-v2.jpg",
|
||||
"changeReason": "补录法人身份证扫描件",
|
||||
"expectedUpdateTime": "2026-08-26 11:40:00"
|
||||
}
|
||||
```
|
||||
|
||||
典型成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "1900000000000000001",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-26 11:42:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
兼容边界:三个新字段全部省略时,不修改现有法人证件数据。若请求中出现任一新字段,服务端会与当前值合并后再次检查三字段是否完整。
|
||||
|
||||
异常请求(身份证校验位错误):
|
||||
|
||||
```json
|
||||
{
|
||||
"legalRepresentativeIdNo": "110105194912310021",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front-v2.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back-v2.jpg",
|
||||
"changeReason": "补录法人身份证扫描件",
|
||||
"expectedUpdateTime": "2026-08-26 11:40:00"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "法定代表人证件号的日期或校验位不合法",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
失败时主体版本、法人证件数据和变更记录均保持原状。
|
||||
|
||||
## 3. 提交供应商注册审批
|
||||
|
||||
### 使用场景
|
||||
|
||||
提交草稿的完整注册表单时,将法人证件三字段一并纳入审批内容。
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/1900000000000000001/submit
|
||||
Authorization: Bearer <管理员令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
本接口继续要求完整注册表单和当前 `expectedUpdateTime`。典型成功请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "法人证件联调示例供应商",
|
||||
"taxNo": "L6391EXAMPLE001",
|
||||
"legalRepresentative": "示例法人",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
|
||||
"mainCooperation": "旅游资源供应",
|
||||
"expectedUpdateTime": "2026-08-26 11:42:00"
|
||||
}
|
||||
```
|
||||
|
||||
典型成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"approvalLogId": "1900000000000000101",
|
||||
"requestNo": "SUP-REQ-20260826-0001",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"spNo": null,
|
||||
"spStatus": null,
|
||||
"syncStatus": "APPLIED",
|
||||
"submittedAt": "2026-08-26 11:43:00",
|
||||
"finishedAt": "2026-08-26 11:43:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
兼容边界:没有法人证件数据的旧草稿仍可按旧请求提交;若提交法人证件,则三字段必须完整。
|
||||
|
||||
异常请求(图片地址带查询串):
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "法人证件联调示例供应商",
|
||||
"taxNo": "L6391EXAMPLE001",
|
||||
"legalRepresentativeIdNo": "11010519491231002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg?token=temporary",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
|
||||
"mainCooperation": "旅游资源供应",
|
||||
"expectedUpdateTime": "2026-08-26 11:42:00"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "法人证件人像面地址不在允许范围",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
失败时不会创建审批或改变供应商状态。
|
||||
|
||||
## 4. 查询供应商基本信息
|
||||
|
||||
### 使用场景
|
||||
|
||||
编辑页回显法人证件信息。证件号只返回掩码,不能用于恢复原文或再次提交;图片地址可用于有权限页面的预览。
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/1900000000000000001/basic-info/view
|
||||
Authorization: Bearer <管理员令牌>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
典型成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "1900000000000000001",
|
||||
"supplierNo": null,
|
||||
"fullName": "法人证件联调示例供应商",
|
||||
"shortName": null,
|
||||
"tax_no": "L639****E001",
|
||||
"legalRepresentative": "示例法人",
|
||||
"legalRepresentativeIdNoMask": "110105********002X",
|
||||
"legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id/front.jpg",
|
||||
"legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id/back.jpg",
|
||||
"contactPhoneMask": null,
|
||||
"establishDate": null,
|
||||
"registeredCapital": null,
|
||||
"businessScope": null,
|
||||
"staffScale": null,
|
||||
"mainCooperation": "旅游资源供应",
|
||||
"status": "DRAFT",
|
||||
"creditLevel": "B",
|
||||
"totalScore": null,
|
||||
"types": [],
|
||||
"contacts": [],
|
||||
"qualifications": [],
|
||||
"updateTime": "2026-08-26 11:42:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
兼容边界:存量供应商没有法人证件数据时,三个响应字段均为 `null`。响应中不存在 `legalRepresentativeIdNo` 明文字段。
|
||||
|
||||
未认证示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未认证或登录已失效",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 错误与前端处理
|
||||
|
||||
| 响应码 | 触发条件 | 前端处理 |
|
||||
|---:|---|---|
|
||||
| `400` | 身份证不是 18 位、出生日期/顺序码/校验位错误、三字段不完整、图片地址不符合规则 | 保留表单并定位到法人证件区域;不要自动重试 |
|
||||
| `401` | 未登录或 Gateway 认证失效 | 进入统一重新登录流程 |
|
||||
| `403` | 角色、功能权限或数据范围不足 | 隐藏无权操作并展示统一无权限提示 |
|
||||
| `395014` | `expectedUpdateTime` 已过期 | 重新读取详情,提示用户确认后再提交 |
|
||||
|
||||
本次不新增业务错误码;参数失败继续使用统一 `400`。
|
||||
|
||||
## 前端改造清单
|
||||
|
||||
- 在“新建/编辑供应商-基本信息-法定代表人”后增加身份证号输入框、人像面上传和国徽面上传。
|
||||
- 上传完成后提交永久 HTTPS 地址;不要提交临时签名 URL、查询参数 URL、Base64 或文件二进制。
|
||||
- 前端可做 18 位长度和末位 `X/x` 预校验,但最终以服务端日期及校验位结果为准。
|
||||
- 三字段联动必填;旧数据三个字段均为空时允许继续按原流程操作。
|
||||
- 详情只展示 `legalRepresentativeIdNoMask`;不得寻找或缓存身份证号明文。
|
||||
- 写成功后如需回显,重新调用详情接口,不要从写响应读取新字段。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化:最新 `dev-v3` 的 Resource 全量测试 2111 项通过、0 失败、0 错误,38 项既有条件跳过;法人证件聚焦测试 89 项通过。
|
||||
- TEST:Deploy Panel 任务 `118fcff1` 将目标提交 `de615b49e3ecef4be13bd6bc78b3100d08ef0bd2` 部署到双实例;该提交包含 #6391 合并提交 `b909c8f7dfd73712886a33b574c72030dde89d4f`,服务与 Nacos 健康检查通过。
|
||||
- 真实 Gateway:7 组场景通过,覆盖合法创建、错误校验位零写入、详情脱敏与图片回显、未认证拒绝、非法更新零写入、旧客户端省略字段兼容和迁移状态。
|
||||
- 清理:本轮验收草稿已通过业务删除接口软删除并保留正常删除审计,不遗留可用测试供应商。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,revert #6401 合并提交并经独立 PR 合入。
|
||||
2. 重新部署 `hl-resource-service`;新增的可空数据结构保留,不执行破坏性删除。
|
||||
3. 旧客户端、存量供应商和已保存的安全数据保持兼容;无需恢复配置、Redis 或 MQ。
|
||||
4. 经 Gateway 重跑旧请求、合法/非法证件号、三字段完整性、详情脱敏和失败零写入检查。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6391](https://git.1814.love:8443/wx/HL/issues/6391)
|
||||
- **PR**: [#6401](https://git.1814.love:8443/wx/HL/pulls/6401)
|
||||
- **合并提交**: [b909c8f7d](https://git.1814.love:8443/wx/HL/commit/b909c8f7dfd73712886a33b574c72030dde89d4f)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6392"
|
||||
title: "供应商暂停合作与拉黑原因必填接口"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "49b071b0"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6403 已合并 dev-v3;两个状态命令已部署 TEST,ACTIVE→SUSPENDED、ACTIVE/SUSPENDED→BLACKLIST、原因审计、权限、并发、幂等和失败零写入均经真实 Gateway 验证。前端待接入原因弹窗与并发版本。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 供应商暂停合作与拉黑原因必填接口
|
||||
|
||||
供应商管理新增两个窄状态命令:暂停合作与列入黑名单。服务端强制要求业务原因和调用方读取到的并发版本,并沿用可信管理员身份、专用平台权限、聚合锁、短窗幂等、行锁、状态机及同事务审计。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 允许来源状态 | 目标状态 |
|
||||
|---|---|---|---|
|
||||
| POST | `/admin/supplier/items/{supplierId}/suspend` | `ACTIVE` | `SUSPENDED` |
|
||||
| POST | `/admin/supplier/items/{supplierId}/blacklist` | `ACTIVE`、`SUSPENDED` | `BLACKLIST` |
|
||||
|
||||
两个接口都要求真实 `SUPER_ADMIN` 身份且拥有 `supplier:status:manage` 平台权限。前端按钮可按角色和状态控制展示,但不能替代服务端门禁。
|
||||
|
||||
## 公共请求
|
||||
|
||||
```json
|
||||
{
|
||||
"reason": "供应商连续违约,暂停合作复核",
|
||||
"expectedUpdateTime": "2026-08-26 15:40:00"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 规则 |
|
||||
|---|---|---|---|
|
||||
| `reason` | string | 是 | 去除首尾空白后必须非空,最长 500 字符;规范化后的原因为状态审计内容 |
|
||||
| `expectedUpdateTime` | string | 是 | 调用方最近一次读取到的供应商 `updateTime`,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
路径参数 `supplierId` 必须为正 Long。请求体不接受角色、操作者或目标状态,身份仅来自 Gateway 验证后的可信属性。
|
||||
|
||||
## 成功响应
|
||||
|
||||
暂停合作成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"supplierId": "1900000000000000001",
|
||||
"status": "SUSPENDED",
|
||||
"updateTime": "2026-08-26 15:40:01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
拉黑成功时结构相同,`status` 为 `BLACKLIST`。`supplierId` 固定为 JSON string;后续状态命令必须使用响应或详情中的新 `updateTime`,不能继续提交旧版本。
|
||||
|
||||
每次成功迁移与一条 `supplier_change_log` 状态审计在同一本地事务内提交。审计记录包含 `fromStatus`、`toStatus`、操作者、角色、Trace ID 和规范化后的 `reason`;审计写入失败时状态整体回滚。
|
||||
|
||||
## 失败语义
|
||||
|
||||
| 场景 | 业务码 | 结果 |
|
||||
|---|---:|---|
|
||||
| 未登录或 Token 无效 | `401` | Gateway/服务认证拒绝,零业务写入 |
|
||||
| `reason` 缺失、空白、纯空格、超过 500 字符,或缺少版本 | `400` | 参数/服务双层拒绝,零业务写入 |
|
||||
| 供应商不存在或已软删除 | `395001` | 零业务写入 |
|
||||
| 非 `SUPER_ADMIN` 或缺少 `supplier:status:manage` | `395004` | 专用状态权限失败关闭,零业务写入 |
|
||||
| suspend 来源不是 `ACTIVE`;blacklist 来源不是 `ACTIVE/SUSPENDED` | `395005` | 状态机拒绝,零业务写入 |
|
||||
| `expectedUpdateTime` 与数据库秒级版本不一致 | `395014` | 状态和审计均不写入,调用方应刷新后重试 |
|
||||
| 5 秒内同操作者、供应商和相同请求重复提交 | `100502` | 重复请求被拒绝,不产生第二条状态审计 |
|
||||
|
||||
统一响应可能以 HTTP 200 承载业务失败,客户端必须同时检查 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 前端联调事项
|
||||
|
||||
- 在合作中供应商上提供“暂停合作”和“列入黑名单”;暂停合作供应商仅提供“列入黑名单”。其他状态不展示这两个动作。
|
||||
- 点击动作后弹出必填原因输入框,前端限制 500 字符;提交详情或列表中最近一次读取到的 `updateTime`。
|
||||
- `395014` 应提示数据已变化并刷新详情;`395005` 应刷新当前状态;`100502` 应提示不要重复提交。
|
||||
- 成功后使用响应中的 `status` 和 `updateTime` 更新页面或重新拉取详情/列表。
|
||||
- 前端显示控制不构成授权;不得从请求体传入操作者、角色或目标状态。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 代码:PR #6403 合并提交为 `3f3a0bf734737ca0e84ed1cfcad1801835cd46d5`;供应商目标测试 62 项通过,Resource 全量 2100 项通过、0 失败、0 错误(38 项既有条件跳过),Gateway 路由/认证 9 项通过,`hl-verify` 全部通过。
|
||||
- 部署:原规范任务 `d08c52f6` 成功,目标/实际提交均为 `4b31612aeba5c0d3ac690cc6af15b66290bd8b48`;只读恢复绑定原任务期样本,5 个可验证样本均健康、3 个本地不可验证样本、0 个故障样本,没有重复部署。
|
||||
- 当前制品:后续同服务规范任务 `da90f211` 将 TEST 更新为 `dev-v3@692547a5989ec1f8fa1b1495fc8dd501cddc0fe0`,该提交包含本工单合并提交;6/6 个任务期样本均为双进程、Nacos 双 healthy/enabled 且端口匹配,0 不可验证、0 故障样本。本工单最终业务验收在该当前制品上完成。
|
||||
- 真实 Gateway:验证 `ACTIVE→SUSPENDED→BLACKLIST` 和 `ACTIVE→BLACKLIST`,成功响应、详情状态、并发版本与同事务审计一致;原因首尾空白按规范化值入审计。
|
||||
- 失败分支:未认证、真实 `CUSTOMIZER` 越权、原因缺失/空白/超长、缺版本、旧版本、DRAFT 来源、不存在对象、重复状态与短窗重复请求全部返回预期错误,主体版本、状态和审计计数均保持不变。
|
||||
- 清理:仅创建 3 个随机 TEST 草稿,并以 ID、随机全名、当前状态三重条件设置 2 个 ACTIVE 夹具;未触碰既有供应商。验收后精确恢复 2 行为 DRAFT,再通过业务删除接口软删除全部 3 个夹具;有效供应商总量恢复为 4,`HL6392-` 活跃命名空间为 0。9 条脱敏审计按系统约定保留;两枚验收会话已注销,幂等键等待 6 秒自然过期,未手工修改 Redis。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建独立回退分支,执行 `git revert -m 1 --no-edit 3f3a0bf734737ca0e84ed1cfcad1801835cd46d5`,经评审 PR 合入;不要回退后续无关提交。
|
||||
2. 通过 Deploy Panel API 对精确回退目标执行新鲜预检和显式部署 `hl-resource-service`,复核双实例、Nacos、日志以及既有供应商查询、建档、资料更新和审批链路。
|
||||
3. 本次无 DDL、配置、Redis、MQ 或跨服务写入变更,不执行 schema、缓存或消息回退。
|
||||
4. 已实际发生的 `SUSPENDED`、`BLACKLIST` 状态和审计不会随代码回退自动反转;禁止直接改库,只能由后续合法恢复合作/解除黑名单业务流程处理。
|
||||
5. 回退后两个新增 POST 接口恢复不可用;前端在回退部署前隐藏/停用对应动作,并继续兼容既有供应商接口。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6392](https://git.1814.love:8443/wx/HL/issues/6392)
|
||||
- **PR**: [#6403](https://git.1814.love:8443/wx/HL/pulls/6403)
|
||||
- **合并提交**: [3f3a0bf73](https://git.1814.love:8443/wx/HL/commit/3f3a0bf734737ca0e84ed1cfcad1801835cd46d5)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,392 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6397"
|
||||
title: "供应商注册合同聚合信息"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f2a4200d"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6405 已合并 dev-v3;Deploy Panel 任务 af3f205b 成功发布 Resource 双实例,合同接口已完成 23 项真实 Gateway 验收。TEST 工作副本存在服务器本地提交导致精确提交回读仍被阻塞,后端工单保持开启;本条只交接已经实测存在的接口契约。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔧 供应商注册合同聚合信息
|
||||
|
||||
供应商创建草稿、提交注册和基础信息详情现统一支持合同完整快照。管理端应把“合同信息”放在“资质证照”之后、现有账户区域之前,并把原“初始账户”展示标题改为“结算信息”。
|
||||
|
||||
展示名调整不改变接口字段:原请求字段仍为 `initialAccounts`,不得改成 `settlementInfo` 或其他名称。
|
||||
|
||||
## 变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变化 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 请求可选增加 `contracts` 完整集合 |
|
||||
| 2 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 请求可选增加 `contracts` 完整快照 |
|
||||
| 3 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应增加 `contracts` 列表 |
|
||||
|
||||
统一响应均为 `Result<T>`。业务失败可能仍为 HTTP 200,调用方必须同时判断 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## 公共合同字段
|
||||
|
||||
### 请求字段 `contracts[]`
|
||||
|
||||
| 字段 | 类型 | 创建必填 | 提交既有项必填 | 约束与说明 |
|
||||
|---|---|---:|---:|---|
|
||||
| `contractId` | String | 否,且禁止传入 | 是 | 正整数 ID 字符串;必须属于当前供应商,同一快照不得重复 |
|
||||
| `contractName` | String | 是 | 是 | 非空白,最长 500 字符 |
|
||||
| `contractNo` | String | 否 | 否 | 最长 100 字符 |
|
||||
| `contractType` | String | 是 | 是 | `FRAME`、`SINGLE_TRIP`、`PURCHASE` |
|
||||
| `signDate` | String | 否 | 否 | `yyyy-MM-dd` |
|
||||
| `startDate` | String | 是 | 是 | `yyyy-MM-dd` |
|
||||
| `endDate` | String | 是 | 是 | `yyyy-MM-dd`,不得早于 `startDate` |
|
||||
| `amount` | Number | 否 | 否 | 大于等于 0,最多 10 位整数和 2 位小数 |
|
||||
| `pricingMode` | String | 否 | 否 | 计价方式说明,最长 100 字符 |
|
||||
| `settleCycle` | String | 否 | 否 | 结算周期说明,最长 32 字符 |
|
||||
| `status` | String | 是 | 是 | 写接口允许 `DRAFT`、`ACTIVE`、`EXPIRED` |
|
||||
| `scanFileUrl` | String | 否 | 否 | 合同扫描件永久地址,最长 1000 字符 |
|
||||
| `remark` | String | 否 | 否 | 最长 500 字符 |
|
||||
|
||||
### 响应字段 `contracts[]`
|
||||
|
||||
详情返回上述全部业务字段,并额外返回:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `contractId` | String | 合同 ID,始终按字符串处理,不得转 JavaScript Number |
|
||||
| `updateTime` | String | 合同当前版本,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
历史数据可能返回只读状态 `TERMINATED`;创建和提交请求不得发送该状态。
|
||||
|
||||
## 1. 创建供应商注册草稿
|
||||
|
||||
`POST /admin/supplier/items/add`
|
||||
|
||||
### 使用场景与边界
|
||||
|
||||
- `contracts` 可省略、为 `null` 或空数组,旧客户端行为不变。
|
||||
- 非空时最多 100 项,合同与供应商主体、资质和 `initialAccounts` 一起成功或一起失败。
|
||||
- 创建请求中的每个合同都是新合同,禁止携带 `contractId`。
|
||||
- 写入仍要求现有供应商创建权限;仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有对应平台权限的身份可执行。
|
||||
|
||||
### 典型请求
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/add
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"shortName": "示例供应商",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "SCENIC"
|
||||
}
|
||||
],
|
||||
"mainCooperation": "景区资源合作",
|
||||
"licenseImageUrl": "https://files.example.com/license.png",
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-001",
|
||||
"imageUrl": "https://files.example.com/license.png",
|
||||
"permanentValid": true
|
||||
}
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"contractName": "2026 年度框架合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-26",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": 1200.50,
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "DRAFT",
|
||||
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
|
||||
"remark": "年度合作"
|
||||
}
|
||||
],
|
||||
"initialAccounts": []
|
||||
}
|
||||
```
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2090300000000063970",
|
||||
"supplierNo": null,
|
||||
"status": "DRAFT",
|
||||
"onboardingStage": "PROFILE_DRAFT",
|
||||
"initialAccounts": [],
|
||||
"updateTime": "2026-08-26 11:10:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 失败响应:创建携带合同 ID
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "创建草稿不能携带合同ID",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
失败时不会留下供应商主体或部分合同。
|
||||
|
||||
## 2. 提交供应商注册
|
||||
|
||||
`POST /admin/supplier/items/{supplierId}/submit`
|
||||
|
||||
### 使用场景与快照语义
|
||||
|
||||
- `contracts` 省略或为 `null`:本次不处理合同,保留草稿当前合同。
|
||||
- `contracts: []`:明确清空当前全部合同。
|
||||
- 非空数组:作为完整快照;带 `contractId` 的项覆盖当前合同,不带 ID 的项新增,当前已有但数组中遗漏的合同删除。
|
||||
- 带 ID 的合同必须属于路径中的供应商;不属于当前供应商、重复 ID、非法枚举、负金额或日期逆序均失败。
|
||||
- `expectedUpdateTime` 仍是供应商聚合并发版本;发生并发修改时调用方应刷新详情后重新组装完整表单。
|
||||
- 合同快照会进入本次审批资料,但提交注册不会自动改写合同自身的 `status`。
|
||||
|
||||
### 典型请求
|
||||
|
||||
```http
|
||||
POST /admin/supplier/items/2090300000000063970/submit
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "示例供应商有限公司",
|
||||
"shortName": "示例供应商",
|
||||
"taxNo": "91350211M000100Y46",
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "SCENIC"
|
||||
}
|
||||
],
|
||||
"mainCooperation": "景区资源合作",
|
||||
"licenseImageUrl": "https://files.example.com/license.png",
|
||||
"qualifications": [
|
||||
{
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"certNo": "LIC-2026-001",
|
||||
"imageUrl": "https://files.example.com/license.png",
|
||||
"permanentValid": true
|
||||
}
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"contractId": "2090300000000063971",
|
||||
"contractName": "2026 年度框架合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-26",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": 1200.50,
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "ACTIVE",
|
||||
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
|
||||
"remark": "提交审批"
|
||||
}
|
||||
],
|
||||
"initialAccounts": [],
|
||||
"expectedUpdateTime": "2026-08-26 11:10:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"approvalLogId": "2090300000000063972",
|
||||
"requestNo": "SUP-REQ-7b8c9d00112233445566778899aabbccddeeff00112233445566778899aabbcc",
|
||||
"provider": "LOCAL_AUTO",
|
||||
"approvalStatus": "APPROVED",
|
||||
"spNo": null,
|
||||
"spStatus": null,
|
||||
"syncStatus": "APPLIED",
|
||||
"submittedAt": "2026-08-26 11:11:00",
|
||||
"finishedAt": "2026-08-26 11:11:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 失败响应:合同不属于当前供应商
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "合同不属于当前供应商",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
该失败会回滚本次提交表单中的主体、资质、合同和审计变化,供应商仍保持原状态和原版本。
|
||||
|
||||
## 3. 查询供应商基本信息
|
||||
|
||||
`GET /admin/supplier/items/{supplierId}/basic-info/view`
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
GET /admin/supplier/items/2090300000000063970/basic-info/view
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
无请求体。读取继续要求可信读角色和 `supplier:view` 平台权限。
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"supplierId": "2090300000000063970",
|
||||
"supplierNo": null,
|
||||
"fullName": "示例供应商有限公司",
|
||||
"shortName": "示例供应商",
|
||||
"tax_no": "9135**********0Y46",
|
||||
"legalRepresentative": null,
|
||||
"legalRepresentativeIdNoMask": null,
|
||||
"legalRepresentativeIdCardFrontUrl": null,
|
||||
"legalRepresentativeIdCardBackUrl": null,
|
||||
"contactPhoneMask": null,
|
||||
"establishDate": null,
|
||||
"registeredCapital": null,
|
||||
"businessScope": null,
|
||||
"staffScale": null,
|
||||
"mainCooperation": "景区资源合作",
|
||||
"status": "DRAFT",
|
||||
"creditLevel": "B",
|
||||
"totalScore": null,
|
||||
"types": [
|
||||
{
|
||||
"typeCode": "SCENIC",
|
||||
"typeName": "景区"
|
||||
}
|
||||
],
|
||||
"contacts": [],
|
||||
"qualifications": [
|
||||
{
|
||||
"qualificationId": "2090300000000063973",
|
||||
"qualType": "BUSINESS_LICENSE",
|
||||
"qualTypeName": "营业执照",
|
||||
"certNoMask": "LI*********01",
|
||||
"imageUrl": "https://files.example.com/license.png",
|
||||
"expiryDate": null,
|
||||
"permanentValid": true,
|
||||
"daysUntilExpiry": null,
|
||||
"validityStatus": "VALID",
|
||||
"validityStatusName": "有效",
|
||||
"isRequired": false,
|
||||
"expired": false,
|
||||
"updateTime": "2026-08-26 11:10:00"
|
||||
}
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"contractId": "2090300000000063971",
|
||||
"contractName": "2026 年度框架合同",
|
||||
"contractNo": "HT-2026-001",
|
||||
"contractType": "FRAME",
|
||||
"signDate": "2026-08-26",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"amount": 1200.50,
|
||||
"pricingMode": "按团结算",
|
||||
"settleCycle": "MONTHLY",
|
||||
"status": "DRAFT",
|
||||
"scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
|
||||
"remark": "年度合作",
|
||||
"updateTime": "2026-08-26 11:10:00"
|
||||
}
|
||||
],
|
||||
"updateTime": "2026-08-26 11:10:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
合同按 `contractId` 升序返回,只包含当前有效合同。没有合同时返回空数组 `[]`,不返回 `null`。
|
||||
|
||||
### 常见失败
|
||||
|
||||
| 场景 | `code` | 前端处理 |
|
||||
|---|---:|---|
|
||||
| 未登录或 Token 失效 | `401` | 跳转登录,不展示空详情 |
|
||||
| 可信角色或 `supplier:view` 平台权限不足 | `403` | 展示无权限状态 |
|
||||
| 供应商不存在或已删除 | `395001` | 返回列表并刷新 |
|
||||
|
||||
## 修改前后对比
|
||||
|
||||
| 场景 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 创建草稿 | 请求不能携带合同 | 可选携带完整 `contracts`,与草稿一起成功或失败 |
|
||||
| 提交注册 | 提交表单不能维护合同 | 可省略保留、空数组清空或提交完整合同快照 |
|
||||
| 基础信息 | 不返回合同列表 | 返回完整 `contracts[]` 及字符串 ID、版本 |
|
||||
| 账户区域标题 | 页面显示“初始账户” | 页面应显示“结算信息”,接口字段仍为 `initialAccounts` |
|
||||
| 页面区块顺序 | 资质后直接进入账户区域 | 资质证照 → 合同信息 → 结算信息 |
|
||||
|
||||
## 兼容性与管理端接入事项
|
||||
|
||||
1. 在“资质证照”区域之后新增“合同信息”表格,在合同之后显示原账户表格。
|
||||
2. 原账户表格标题改为“结算信息”;所有请求和响应继续使用 `initialAccounts`,不要改字段名。
|
||||
3. 创建草稿时合同为完整新项,不发送 `contractId`;编辑后提交时,既有合同必须原样带回字符串 `contractId`。
|
||||
4. 提交表单是完整快照。用户明确删除全部合同时发送 `contracts: []`;未加载合同或不处理合同时省略字段,不要误发空数组。
|
||||
5. 所有 ID 均作为字符串保存、比较和回传,不经过 Number 转换。
|
||||
6. 本次不新增接口路径、权限点或业务错误码;旧客户端省略 `contracts` 时继续可用。
|
||||
7. 管理端源码不在本后端工单中修改,前端状态保持 `pending`,直至完成页签、标题和表格接入并提供前端引用。
|
||||
|
||||
## TEST 验证证据
|
||||
|
||||
- 自动化:供应商定向测试 115 项通过;`hl-resource-service` 全量 2,111 项,0 失败、0 错误,38 项条件跳过;`hl-verify` 与差异检查通过。
|
||||
- 部署:Deploy Panel API 任务 `af3f205b` 终态 `success`、退出码 0、`has_build_error=false`,未发现 Maven、编译或滚动发布错误。
|
||||
- 健康:`hl-resource-service` 的 8082、8182 双实例运行;Nacos `test` 命名空间两实例均 `healthy=true`、`enabled=true`。
|
||||
- 真实 Gateway:23 项断言通过,覆盖合同随草稿创建、详情完整回显、字符串 ID、创建携带 ID 失败、普通 ADMIN 越权、提交外部合同 ID 完整回滚、日期逆序零写入和旧客户端省略 `contracts`。
|
||||
- 清理:两个临时 DRAFT 均通过业务删除接口软删除并回读为不存在;仅保留不可逆的 CREATE/DELETE 操作审计。
|
||||
- 环境限制:部署前锁定 `origin/dev-v3=1a16a5aec7d0b95ec87e6fb222581060a3135984`,但面板 Git API 回读到服务器本地短提交 `6f7d3ca78`,Gitea 无法解析该对象。接口行为已真实验证,后端工单仍等待测试环境恢复精确远端提交后复验,不能据此宣称最终交付完成。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 1a16a5aec7d0b95ec87e6fb222581060a3135984`,经独立 PR 合入。
|
||||
2. 重新滚动部署 `hl-resource-service`;无需执行数据库结构、配置、Redis 或 MQ 恢复。
|
||||
3. 管理端停止发送和读取 `contracts`,恢复原页面结构;`initialAccounts` 契约始终不变。
|
||||
4. 已保存的合同资料保留,不做破坏性批量清理;回退后旧客户端继续按省略 `contracts` 的路径工作。
|
||||
5. 经 Gateway 复测创建、提交、基础信息、未认证、越权、非法合同 ID、失败零写入和旧客户端兼容,并确认双实例与 Nacos 健康。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6397](https://git.1814.love:8443/wx/HL/issues/6397)
|
||||
- **PR**: [#6405](https://git.1814.love:8443/wx/HL/pulls/6405)
|
||||
- **合并提交**: [1a16a5aec](https://git.1814.love:8443/wx/HL/commit/1a16a5aec7d0b95ec87e6fb222581060a3135984)
|
||||
- **后端负责人**: @lc
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6406"
|
||||
title: "开票申请按税号自动回填企业工商信息"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "080ea8df"
|
||||
target_release: ""
|
||||
verified_at: "2026-08-26"
|
||||
status_note: "PR #6415 已合并 dev-v3 并部署测试服,经网关 API 实测 companyInfo 与 lastInvoiceTitle 两段往返一致,非法税号返回 581526"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 🔍 开票申请:按税号自动回填企业工商信息
|
||||
|
||||
开票申请填写页面现支持按税号自动回填企业工商信息。后端对接第三方企业信息查询服务(元典开放平台),并同时返回本系统同税号最近一条历史开票抬头,两段数据并列返回、由前端自行决定展示与回填优先级,后端不做合并取舍。
|
||||
|
||||
> **PR**: [#6415](https://git.1814.love:8443/wx/HL/pulls/6415) | **服务**: hl-order-service-v3 | **作者**: wx | **更新时间**: 2026-08-26
|
||||
|
||||
---
|
||||
|
||||
## 一、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 开票企业信息自动回填查询 | GET | `/v3/admin/invoice/company-info` | 新增 | 按税号查两段回填数据 |
|
||||
|
||||
---
|
||||
|
||||
## 二、接口详情
|
||||
|
||||
### 1. 开票企业信息自动回填查询 `GET /v3/admin/invoice/company-info`
|
||||
|
||||
**入参**: `taxNo`(Query 参数)
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| taxNo | Query | String | ✅ | 非空;去空格后长度 15 或 18 | 统一社会信用代码(或旧 15 位注册号) |
|
||||
|
||||
**出参 `Result<CompanyInfoRespVO>`**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| companyInfo | Object 或 null | 元典第三方工商照面;查不到/第三方异常/全 key 耗尽时为 null |
|
||||
| lastInvoiceTitle | Object 或 null | 本系统同税号最近一条非作废发票的历史抬头;无历史时为 null |
|
||||
|
||||
**companyInfo 字段**(元典工商照面):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| titleName | String | 企业名称 |
|
||||
| taxNo | String | 统一社会信用代码 |
|
||||
| legalPersonName | String | 法定代表人 |
|
||||
| registAddress | String | 注册地址 |
|
||||
| regStatus | String | 经营状态(存续/注销/吊销等,由第三方返回) |
|
||||
|
||||
**lastInvoiceTitle 字段**(本系统历史抬头):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| titleName | String | 开票抬头 |
|
||||
| bankName | String | 开户行 |
|
||||
| bankAccount | String | 银行账号 |
|
||||
| registAddress | String | 注册地址 |
|
||||
| registPhone | String | 注册电话 |
|
||||
| email | String | 邮箱 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```
|
||||
GET /v3/admin/invoice/company-info?taxNo=91110000802100433B
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"companyInfo": {
|
||||
"titleName": "北京百度网讯科技有限公司",
|
||||
"taxNo": "91110000802100433B",
|
||||
"registAddress": "北京市海淀区上地十街10号百度大厦2层",
|
||||
"legalPersonName": "梁志祥",
|
||||
"regStatus": "存续"
|
||||
},
|
||||
"lastInvoiceTitle": {
|
||||
"titleName": "北京百度网讯科技有限公司",
|
||||
"bankName": "招商银行北京分行",
|
||||
"bankAccount": "110900100011110",
|
||||
"registAddress": "北京市海淀区上地十街10号百度大厦2层",
|
||||
"registPhone": "010-59928888",
|
||||
"email": "invoice@baidu.com"
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 降级响应示例(任一段查不到为 null,互不阻塞)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"companyInfo": null,
|
||||
"lastInvoiceTitle": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 非法税号错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581526,
|
||||
"message": "税号格式不正确(统一社会信用代码须为 15 或 18 位)",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 税号为空 / 去空格后非 15 或 18 位 → `581526` 参数错误
|
||||
- 元典查无该企业 → `companyInfo = null`,`lastInvoiceTitle` 照常返回
|
||||
- 元典接口超时 / 异常 / 全部 key 不可用 → 降级 `companyInfo = null`,不影响 `lastInvoiceTitle`
|
||||
- 本系统无同税号历史发票 → `lastInvoiceTitle = null`
|
||||
- 税号含空格 → 后端自动去空格后再查询,不报错
|
||||
|
||||
---
|
||||
|
||||
## 四、不影响范围(显式声明)
|
||||
|
||||
- **仅新增**:本查询接口为纯只读旁路查询
|
||||
- **零影响**:
|
||||
- 开票申请提交接口(`/v3/admin/order/{orderId}/invoice/apply`)行为不变
|
||||
- 开票流程、开票记录写入逻辑
|
||||
- 既有发票列表/详情查询
|
||||
- 订单、订单核心、房务、车务模块
|
||||
|
||||
---
|
||||
|
||||
## 五、测试环境已验证
|
||||
|
||||
网关实测(`https://api.test.1814.love:9443`):
|
||||
|
||||
```
|
||||
GET /v3/admin/invoice/company-info?taxNo=91110000802100433B
|
||||
→ 200 + companyInfo(北京百度网讯科技有限公司/梁志祥/存续) ✓
|
||||
→ 200 + lastInvoiceTitle(招商银行北京分行/110900100011110/invoice@baidu.com) ✓
|
||||
GET /v3/admin/invoice/company-info?taxNo=123
|
||||
→ 581526 税号格式不正确 ✓
|
||||
未带 Authorization 头
|
||||
→ 401 缺少有效的 Authorization 头 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#6406](https://git.1814.love:8443/wx/HL/issues/6406)
|
||||
- 关联 PR: [wx/HL#6415](https://git.1814.love:8443/wx/HL/pulls/6415)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#6406](https://git.1814.love:8443/wx/HL/issues/6406)
|
||||
- **PR**: [#6415](https://git.1814.love:8443/wx/HL/pulls/6415)
|
||||
- **Merge commit**: [f2c6e9fef53315a458b79f71747ee3494c420694](https://git.1814.love:8443/wx/HL/commit/f2c6e9fef53315a458b79f71747ee3494c420694)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "6409"
|
||||
title: "行政区划响应 ID 固定为字符串"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "PR #6412 已合并 dev-v3;hl-user-service 已以提交 4b31612a 部署 TEST,create、children、path 经真实 Gateway 验证全部 Long ID 固定为 JSON string,空层级保持 null。"
|
||||
updated_at: "2026-08-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 行政区划响应 ID 固定为字符串
|
||||
|
||||
行政区划创建、单级联动和父链回显接口中的 Long ID 现在始终按 JSON 字符串返回,避免小整数因全局安全整数规则被输出为 JSON number。请求参数、权限、业务校验和错误码不变。
|
||||
|
||||
## 变更接口
|
||||
|
||||
| 方法 | 路径 | 响应变化 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/region` | 成功响应 `data` 从 JSON number 固定为 string |
|
||||
| GET | `/admin/region/children` | `parentId`、`defaultId`、`items[].id`、`items[].parentId` 固定为 string;空值保持 `null` |
|
||||
| GET | `/admin/region/{id}/path` | `selectedId`、`provinceId`、`prefectureId`、`countyId` 及 `path` 节点 ID 固定为 string;缺失层级保持 `null` |
|
||||
|
||||
更新接口 `PUT /admin/region/{id}` 的请求和响应不变。所有请求路径、Query/Path 参数仍按原 Long 语义解析。
|
||||
|
||||
## 兼容性与错误语义
|
||||
|
||||
- 这是已发布接口 JSON 类型的契约修正,不新增字段,也不改变字段名称。
|
||||
- 行政区划 Redis 缓存可同时反序列化旧 JSON number 与新 JSON string,滚动部署期间兼容。
|
||||
- 根层 `parentId`、无默认节点的 `defaultId` 和父链中不存在的层级继续返回 `null`,不会变成字符串 `"null"`。
|
||||
- 未认证请求继续返回业务码 `401`;非法正整数参数、缺失节点和非法节点类型继续使用既有错误语义,失败路径零业务写入。
|
||||
- 无数据库 migration、配置、Gateway、MQ、缓存键或跨服务契约变更;无需修改前端源码,调用方按既定字符串 ID 契约消费即可。
|
||||
- 统一响应可能以 HTTP 200 承载业务失败,客户端必须同时检查 `code`、`success`、`message` 和 `data`。
|
||||
|
||||
## TEST 验证证据
|
||||
|
||||
- PR #6412 合并提交为 `8494028aa2dfbb4e39147c09695898d247045d4c`;TEST 目标提交 `4b31612aeba5c0d3ac690cc6af15b66290bd8b48` 包含该合并提交。
|
||||
- Deploy Panel API 任务 `b10dd2ed` 成功执行 `/opt/hulalv/scripts/deploy-backend.sh`;User 的 8081、8181 双实例和 Nacos 两个 healthy/enabled 实例通过,任务期 9 个可验证采样均有可用实例,0 个故障采样。
|
||||
- 真实 TEST 管理员经 Gateway 验证 create、children、path:创建 ID、联动列表 ID、父链 ID 和对应 Redis 缓存 ID 均为字符串,根节点和缺失层级空值保持 `null`。
|
||||
- 未认证请求返回 `401`;非法父节点、缺失节点、空创建和旧非法节点类型均按既有错误返回,并确认失败路径零写入。
|
||||
- 成功创建和 CAS 更新后,精确删除本次创建的 1 行;数据库总量恢复且测试命名空间为 0。4 个相关 Redis key 按基线值及绝对过期时间恢复,验收会话已注销;操作审计按系统约定保留。
|
||||
- 本地验证:Controller/Cache 定向 22 项通过;User 全量 3644 项通过、0 失败、0 错误,8 项条件跳过;`hl-verify` 与 `git diff --check` 通过。
|
||||
|
||||
## 撤回
|
||||
|
||||
1. 从最新 `dev-v3` 创建独立回退分支,执行 `git revert -m 1 --no-edit 8494028aa2dfbb4e39147c09695898d247045d4c`,经评审 PR 合入;不要回退后续无关提交。
|
||||
2. 通过 Deploy Panel API 对回退目标执行新鲜预检和显式部署 `hl-user-service`,复核双实例、Nacos、日志和精确提交。
|
||||
3. 本修复无数据库、配置或 MQ 变更,不执行 DDL、DML 或消息补偿。缓存新旧表示均可读取,通常无需清理;确需强制恢复旧表示时,仅在停止行政区划刷新后精确处理 `hl:user:region:v1:` 命名空间并由回退版本预热。
|
||||
4. 撤回前确认调用方可重新接受 JSON number ID;撤回后经 Gateway 复测 create、children、path、空层级、未认证和失败零写入。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: [#6409](https://git.1814.love:8443/wx/HL/issues/6409)
|
||||
- **PR**: [#6412](https://git.1814.love:8443/wx/HL/pulls/6412)
|
||||
- **合并提交**: [8494028aa](https://git.1814.love:8443/wx/HL/commit/8494028aa2dfbb4e39147c09695898d247045d4c)
|
||||
- **后端负责人**: @lc
|
||||
@@ -30,6 +30,7 @@ const REQUIRED_KEYS = [
|
||||
'ticket',
|
||||
'title',
|
||||
'consumer',
|
||||
'author',
|
||||
'change_type',
|
||||
'backend_status',
|
||||
'gateway_status',
|
||||
@@ -38,9 +39,31 @@ const REQUIRED_KEYS = [
|
||||
'frontend_ref',
|
||||
'target_release',
|
||||
'verified_at',
|
||||
'status_note',
|
||||
'updated_at',
|
||||
'base',
|
||||
];
|
||||
const HTTP_METHOD_PATTERN = '(?:GET|POST|PUT|PATCH|DELETE)';
|
||||
const API_TEMPLATE_SECTION_PREFIXES = [
|
||||
'二、变更接口清单',
|
||||
'三、接口详情',
|
||||
'四、契约约束与正确调用方式',
|
||||
'六、边界行为',
|
||||
'七、不影响范围',
|
||||
'八、测试环境已验证',
|
||||
'十、相关文档',
|
||||
'关联 / 联系人',
|
||||
];
|
||||
const API_DETAIL_SUBSECTIONS = [
|
||||
'使用场景',
|
||||
'入参',
|
||||
'出参',
|
||||
'请求示例',
|
||||
'响应示例',
|
||||
'空数据 / 降级响应',
|
||||
'错误响应',
|
||||
'业务边界',
|
||||
];
|
||||
|
||||
function ruleError(code, file, message) {
|
||||
return { code, path: file, message };
|
||||
@@ -68,6 +91,181 @@ function isIsoDateOrTime(value) {
|
||||
&& Number.isFinite(Date.parse(value));
|
||||
}
|
||||
|
||||
function sectionByPrefix(body, prefix, level = 2) {
|
||||
const marker = `${'#'.repeat(level)} ${prefix}`;
|
||||
const lines = String(body ?? '').replaceAll('\r\n', '\n').split('\n');
|
||||
const start = lines.findIndex((line) => line.trim().startsWith(marker));
|
||||
if (start < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const nextMarker = '#'.repeat(level);
|
||||
let end = lines.length;
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const value = lines[index].trim();
|
||||
if (value.startsWith(`${nextMarker} `) && !value.startsWith(`${nextMarker}#`)) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines.slice(start + 1, end).join('\n');
|
||||
}
|
||||
|
||||
function apiEndpointKey(method, endpointPath) {
|
||||
return `${method.trim().toUpperCase()} ${endpointPath.trim()}`;
|
||||
}
|
||||
|
||||
function validateApiTemplate(file, metadata, body) {
|
||||
const errors = [];
|
||||
const missingSections = API_TEMPLATE_SECTION_PREFIXES.filter(
|
||||
(prefix) => sectionByPrefix(body, prefix) === undefined,
|
||||
);
|
||||
if (missingSections.length > 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
`接口类正文必须仿照 CHANGELOG_TEMPLATE.md,缺少章节: ${missingSections.join('、')}`,
|
||||
));
|
||||
}
|
||||
|
||||
const listSection = sectionByPrefix(body, '二、变更接口清单');
|
||||
const detailSection = sectionByPrefix(body, '三、接口详情');
|
||||
if (listSection === undefined || detailSection === undefined) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const requiredHeader = /^\|\s*#\s*\|\s*接口\s*\|\s*方法\s*\|\s*路径\s*\|\s*变更类型\s*\|\s*说明\s*\|\s*$/m;
|
||||
if (!requiredHeader.test(listSection)) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“二、变更接口清单”必须使用模板列: #、接口、方法、路径、变更类型、说明',
|
||||
));
|
||||
}
|
||||
|
||||
const listPattern = new RegExp(
|
||||
'^\\|\\s*\\d+\\s*\\|\\s*[^|]+\\|\\s*('
|
||||
+ HTTP_METHOD_PATTERN
|
||||
+ ')\\s*\\|\\s*`([^`]+)`\\s*\\|\\s*[^|]+\\|\\s*[^|]+\\|\\s*$',
|
||||
'gm',
|
||||
);
|
||||
const listed = [...listSection.matchAll(listPattern)]
|
||||
.map((match) => apiEndpointKey(match[1], match[2]));
|
||||
if (listed.length === 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“二、变更接口清单”至少需要一条带 METHOD 和反引号路径的接口记录',
|
||||
));
|
||||
}
|
||||
|
||||
const detailPattern = new RegExp(
|
||||
'^###\\s+\\d+\\.\\s+.+?\\s+`('
|
||||
+ HTTP_METHOD_PATTERN
|
||||
+ ')\\s+([^`]+)`\\s*$',
|
||||
'gm',
|
||||
);
|
||||
const detailMatches = [...detailSection.matchAll(detailPattern)];
|
||||
const detailed = detailMatches.map((match) => apiEndpointKey(match[1], match[2]));
|
||||
if (detailMatches.length === 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'“三、接口详情”至少需要一个“### N. 接口名 `METHOD /path`”子节',
|
||||
));
|
||||
}
|
||||
|
||||
const listedSet = [...new Set(listed)].sort();
|
||||
const detailedSet = [...new Set(detailed)].sort();
|
||||
if (
|
||||
listed.length !== listedSet.length
|
||||
|| detailed.length !== detailedSet.length
|
||||
|| listedSet.join('\n') !== detailedSet.join('\n')
|
||||
) {
|
||||
errors.push(ruleError(
|
||||
'E_API_ENDPOINTS',
|
||||
file,
|
||||
'接口清单与逐接口详情的 METHOD/path 必须去重且一一对应',
|
||||
));
|
||||
}
|
||||
|
||||
for (let index = 0; index < detailMatches.length; index += 1) {
|
||||
const match = detailMatches[index];
|
||||
const next = detailMatches[index + 1];
|
||||
const block = detailSection.slice(
|
||||
match.index + match[0].length,
|
||||
next ? next.index : detailSection.length,
|
||||
);
|
||||
const missing = API_DETAIL_SUBSECTIONS.filter(
|
||||
(prefix) => sectionByPrefix(block, prefix, 4) === undefined,
|
||||
);
|
||||
const usage = sectionByPrefix(block, '使用场景', 4) ?? '';
|
||||
const input = sectionByPrefix(block, '入参', 4) ?? '';
|
||||
const output = sectionByPrefix(block, '出参', 4) ?? '';
|
||||
const requestExample = sectionByPrefix(block, '请求示例', 4) ?? '';
|
||||
const responseExample = sectionByPrefix(block, '响应示例', 4) ?? '';
|
||||
const emptyResponse = sectionByPrefix(block, '空数据 / 降级响应', 4) ?? '';
|
||||
const errorExample = sectionByPrefix(block, '错误响应', 4) ?? '';
|
||||
const boundary = sectionByPrefix(block, '业务边界', 4) ?? '';
|
||||
if (!/^\*\*VO\*\*:\s*`[^`]+`/m.test(block)) {
|
||||
missing.push('VO 契约');
|
||||
}
|
||||
if (!usage.trim()) {
|
||||
missing.push('使用场景说明');
|
||||
}
|
||||
if (!/^\|\s*字段\s*\|\s*位置\s*\|\s*类型\s*\|\s*必填\s*\|\s*约束\s*\|\s*说明\s*\|\s*$/m.test(input)) {
|
||||
missing.push('入参字段表');
|
||||
}
|
||||
if (!/^\|\s*字段\s*\|\s*类型\s*\|\s*说明\s*\|\s*$/m.test(output)) {
|
||||
missing.push('出参字段表');
|
||||
}
|
||||
if (!/```(?:json|http)\s*\n[\s\S]+?\n```/.test(requestExample)) {
|
||||
missing.push('请求示例代码块');
|
||||
}
|
||||
if (!/```json\s*\n[\s\S]+?\n```/.test(responseExample)) {
|
||||
missing.push('响应示例 JSON');
|
||||
}
|
||||
if (!emptyResponse.trim()) {
|
||||
missing.push('空数据 / 降级说明');
|
||||
}
|
||||
if (!/```json\s*\n[\s\S]+?\n```/.test(errorExample)) {
|
||||
missing.push('错误响应 JSON');
|
||||
}
|
||||
if (!/^\s*[-*]\s+\S/m.test(boundary)) {
|
||||
missing.push('业务边界条目');
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
errors.push(ruleError(
|
||||
'E_API_DETAIL',
|
||||
file,
|
||||
`${detailed[index]} 缺少逐接口自包含内容: ${[...new Set(missing)].join('、')}`,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const writeMethods = listed.some((entry) => /^(?:POST|PUT|PATCH|DELETE) /.test(entry));
|
||||
if (writeMethods && sectionByPrefix(body, '五、数据库行为') === undefined) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'包含写接口时必须按模板提供“五、数据库行为”章节(只写外部可观察行为,不泄露表结构)',
|
||||
));
|
||||
}
|
||||
if (
|
||||
['修改接口', '删除接口'].includes(metadata.change_type)
|
||||
&& (
|
||||
sectionByPrefix(body, '六.6、修改前后对比') === undefined
|
||||
|| sectionByPrefix(body, '六.7、影响评估') === undefined
|
||||
)
|
||||
) {
|
||||
errors.push(ruleError(
|
||||
'E_API_TEMPLATE',
|
||||
file,
|
||||
'修改/删除接口必须提供“六.6、修改前后对比”和“六.7、影响评估”章节',
|
||||
));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function parseFrontmatter(text) {
|
||||
const value = String(text ?? '').replaceAll('\r\n', '\n');
|
||||
if (!value.startsWith('---\n')) {
|
||||
@@ -165,11 +363,14 @@ export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
errors.push(ruleError('E_REQUIRED', file, `frontmatter 缺少 ${key}`));
|
||||
}
|
||||
}
|
||||
for (const key of ['ticket', 'title', 'consumer', 'change_type', 'backend_status', 'gateway_status', 'frontend_status', 'updated_at', 'base']) {
|
||||
for (const key of ['ticket', 'title', 'consumer', 'author', 'change_type', 'backend_status', 'gateway_status', 'frontend_status', 'updated_at', 'base']) {
|
||||
if (!metadata[key]?.trim()) {
|
||||
errors.push(ruleError('E_REQUIRED', file, `${key} 不能为空`));
|
||||
}
|
||||
}
|
||||
if (metadata.author && !/^\S+\(GIT\)$/.test(metadata.author)) {
|
||||
errors.push(ruleError('E_AUTHOR', file, 'author 必须使用“登录名(GIT)”格式'));
|
||||
}
|
||||
if (!CHANGE_TYPES.has(metadata.change_type)) {
|
||||
errors.push(ruleError('E_CHANGE_TYPE', file, `change_type 非法: ${metadata.change_type || '(空)'}`));
|
||||
}
|
||||
@@ -213,13 +414,9 @@ export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
if (/\{\{[^{}\n]+\}\}|\bTODO\b|待补充/i.test(body)) {
|
||||
errors.push(ruleError('E_PLACEHOLDER', file, '正文仍有 TODO、待补充或模板占位符'));
|
||||
}
|
||||
// 接口类条目必须有接口清单章节与验证章节;修复/前端类条目结构自由,不强制
|
||||
// 接口类条目必须逐项遵循根模板,保证前端不依赖 Swagger 或口头补充也能联调。
|
||||
if (API_CHANGE_TYPES.has(metadata.change_type)) {
|
||||
const hasApiSection = /^##[^\n]*(变更接口|变更清单|变更内容|接口详情|变更点|接口变化|行为变化)/m.test(body);
|
||||
const hasEvidenceSection = /^##[^\n]*(验证|测试|复现|证据)/m.test(body);
|
||||
if (!hasApiSection || !hasEvidenceSection) {
|
||||
errors.push(ruleError('E_SECTIONS', file, '接口类正文缺少接口清单(变更接口/变更清单)或验证(验证证据/测试)章节'));
|
||||
}
|
||||
errors.push(...validateApiTemplate(file, metadata, body));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
@@ -21,6 +21,7 @@ function metadata(overrides = {}) {
|
||||
ticket: '5218',
|
||||
title: '工作流治理',
|
||||
consumer: 'admin',
|
||||
author: 'lc(GIT)',
|
||||
change_type: '修改接口',
|
||||
backend_status: 'deployed',
|
||||
gateway_status: 'verified',
|
||||
@@ -41,7 +42,15 @@ function document(overrides = {}) {
|
||||
const frontmatter = Object.entries(fields)
|
||||
.map(([key, value]) => `${key}: "${value}"`)
|
||||
.join('\n');
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 二、变更接口清单\n\n| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |\n|---|---|---|---|---|---|\n| 1 | 保存配置 | POST | \`/admin/workflow/config\` | 修改请求 | 保存工作流配置 |\n\n## 三、接口详情\n\n### 1. 保存配置 \`POST /admin/workflow/config\`\n\n**VO**: \`WorkflowConfigReqVO / String\`\n\n#### 使用场景\n\n- 管理员保存工作流配置。\n\n#### 入参\n\n| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |\n|---|---|---|---|---|---|\n| name | Body | String | ✅ | 非空 | 配置名称 |\n\n#### 出参 \`Result<String>\`\n\n| 字段 | 类型 | 说明 |\n|---|---|---|\n| data | String | 配置 ID |\n\n#### 请求示例\n\n\`\`\`json\n{ "name": "审批流" }\n\`\`\`\n\n#### 响应示例\n\n\`\`\`json\n{ "code": 200, "message": "成功", "data": "1", "success": true }\n\`\`\`\n\n#### 空数据 / 降级响应\n\n成功时一定返回字符串 ID。\n\n#### 错误响应\n\n\`\`\`json\n{ "code": 400, "message": "name 不能为空", "data": null, "success": false }\n\`\`\`\n\n#### 业务边界\n\n- 业务失败也必须检查响应体 code。\n\n## 四、契约约束与正确调用方式\n\n- 请求体必须使用 JSON。\n\n## 五、数据库行为\n\n- 成功请求保存一份配置,失败请求不产生业务写入。\n\n## 六、边界行为\n\n- 未登录返回 401。\n\n## 六.6、修改前后对比\n\n| 字段 | 改前 | 改后 |\n|---|---|---|\n| name | 可为空 | 必填 |\n\n## 六.7、影响评估\n\n- **是否破坏向后兼容**: 否\n- **前端是否必须同步上线**: 是\n- **前端 workaround 清理点**: 无\n\n## 七、不影响范围\n\n- 查询接口不变。\n\n## 八、测试环境已验证\n\n- POST /admin/workflow/config → 200 ✓\n\n## 十、相关文档\n\n- Issue: #5218\n\n## 关联 / 联系人\n\n- **后端负责人**: @lc\n`;
|
||||
}
|
||||
|
||||
function incompleteApiDocument() {
|
||||
const fields = metadata();
|
||||
const frontmatter = Object.entries(fields)
|
||||
.map(([key, value]) => `${key}: "${value}"`)
|
||||
.join('\n');
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- POST /admin/workflow/config。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
|
||||
}
|
||||
|
||||
test('parses quoted flat YAML frontmatter', () => {
|
||||
@@ -50,10 +59,89 @@ test('parses quoted flat YAML frontmatter', () => {
|
||||
assert.equal(parsed.metadata.frontend_status, 'pending');
|
||||
});
|
||||
|
||||
test('CHANGELOG_TEMPLATE.md contains every section enforced for API handoffs', () => {
|
||||
const template = readFileSync(
|
||||
new URL('../CHANGELOG_TEMPLATE.md', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
for (const value of [
|
||||
'author: "{推送者登录名}(GIT)"',
|
||||
'## 二、变更接口清单',
|
||||
'## 三、接口详情',
|
||||
'#### 使用场景',
|
||||
'#### 入参',
|
||||
'#### 出参',
|
||||
'#### 请求示例',
|
||||
'#### 响应示例',
|
||||
'#### 空数据 / 降级响应',
|
||||
'#### 错误响应',
|
||||
'#### 业务边界',
|
||||
'## 四、契约约束与正确调用方式',
|
||||
'## 五、数据库行为',
|
||||
'## 六、边界行为',
|
||||
'## 六.6、修改前后对比',
|
||||
'## 六.7、影响评估',
|
||||
'## 七、不影响范围',
|
||||
'## 八、测试环境已验证',
|
||||
'## 十、相关文档',
|
||||
'## 关联 / 联系人',
|
||||
]) {
|
||||
assert.ok(template.includes(value), `template missing ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('accepts a complete v2 handoff with pending frontend consumption', () => {
|
||||
assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []);
|
||||
});
|
||||
|
||||
test('rejects an API handoff that does not follow CHANGELOG_TEMPLATE.md', () => {
|
||||
const errors = validateV2Document(FILE, incompleteApiDocument(), { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
|
||||
});
|
||||
|
||||
test('rejects an endpoint detail without a required self-contained contract block', () => {
|
||||
const value = document().replace('#### 错误响应', '#### 异常说明');
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_DETAIL'));
|
||||
});
|
||||
|
||||
test('requires a use case and explicit empty/degraded behavior for every endpoint', () => {
|
||||
const value = document()
|
||||
.replace('#### 使用场景', '#### 调用时机')
|
||||
.replace('#### 空数据 / 降级响应', '#### 无数据');
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code, message }) => (
|
||||
code === 'E_API_DETAIL'
|
||||
&& message.includes('使用场景')
|
||||
&& message.includes('空数据 / 降级响应')
|
||||
)));
|
||||
});
|
||||
|
||||
test('rejects a mismatch between the API list and endpoint detail headings', () => {
|
||||
const value = document().replace(
|
||||
'### 1. 保存配置 `POST /admin/workflow/config`',
|
||||
'### 1. 保存配置 `POST /admin/workflow/other`',
|
||||
);
|
||||
const errors = validateV2Document(FILE, value, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_ENDPOINTS'));
|
||||
});
|
||||
|
||||
test('requires the template author format and write-operation database section', () => {
|
||||
const invalidAuthor = validateV2Document(
|
||||
FILE,
|
||||
document({ author: 'lc' }),
|
||||
{ requireV2: true },
|
||||
);
|
||||
assert.ok(invalidAuthor.some(({ code }) => code === 'E_AUTHOR'));
|
||||
|
||||
const withoutDatabaseBehavior = document().replace(
|
||||
'## 五、数据库行为',
|
||||
'## 五、持久化说明',
|
||||
);
|
||||
const errors = validateV2Document(FILE, withoutDatabaseBehavior, { requireV2: true });
|
||||
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
|
||||
});
|
||||
|
||||
test('rejects backend and gateway pending at publication', () => {
|
||||
const errors = validateV2Document(
|
||||
FILE,
|
||||
|
||||
在新工单中引用
屏蔽一个用户