docs(api): 发布供应商资质有效期契约
这个提交包含在:
@@ -163,7 +163,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1>供应商模块 API 接口规范</h1>
|
<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>
|
<p>供前端联调使用;契约基线来自 v2.0,运行状态以当前 TEST 验收结论为准。</p>
|
||||||
<div class="chips">
|
<div class="chips">
|
||||||
<span class="chip">后端 only</span>
|
<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>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>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>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>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>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>
|
<tr><td>expectedUpdateTime</td><td>LocalDateTime</td><td>更新供应商必填</td><td>格式 yyyy-MM-dd HH:mm:ss;创建草稿不传</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>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>
|
<h3>嵌套输入</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>DTO</th><th>字段</th><th>严格规则</th></tr></thead>
|
<thead><tr><th>DTO</th><th>字段</th><th>严格规则</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>SupplierContactInput</td><td>contactId?、contactName(1..500)、contactPhone(1..20)、contactRole、remark(0..200)</td><td>既有 ID 必须属于当前供应商;电话不回显明文</td></tr>
|
<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>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>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>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>SupplierContractMergeInput</td><td>contractId?、合同字段?、expectedUpdateTime?</td><td>已有项必须同时携带 contractId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有合同软删除</td></tr>
|
||||||
<tr><td>SupplierEvaluationMergeInput</td><td>evaluationId?、评价字段?、expectedUpdateTime?</td><td>已有项必须同时携带 evaluationId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有评价软删除</td></tr>
|
<tr><td>SupplierEvaluationMergeInput</td><td>evaluationId?、评价字段?、expectedUpdateTime?</td><td>已有项必须同时携带 evaluationId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有评价软删除</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -612,7 +613,7 @@
|
|||||||
<thead><tr><th>VO</th><th>核心字段</th><th>禁止字段</th></tr></thead>
|
<thead><tr><th>VO</th><th>核心字段</th><th>禁止字段</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>SupplierListItemVO</td><td>supplierId/no、full/shortName、types、status、creditLevel/totalScore、activeAccountCount、create/updateTime</td><td>税号/证件/电话/账号明文</td></tr>
|
<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>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>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>
|
<tr><td>SupplierWriteResultVO</td><td>supplierId、supplierNo?、status、onboardingStage、initialAccounts[{accountId,accountNoMask,status}]、updateTime</td><td>账号明文</td></tr>
|
||||||
@@ -735,7 +736,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="footer">发行日期:2026-08-23 · 基线:v2.0 契约基线 · 前端联调版;归档成功依赖尚未接入。</p>
|
<p class="footer">发行日期:2026-08-25 · 基线:v2.0 契约基线 · 前端联调版;归档成功依赖尚未接入。</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -757,23 +758,23 @@
|
|||||||
[],"详细设计 §5.2/§5.11;数据模型 supplier_main/type_rel","已实现 · 可联调"),
|
[],"详细设计 §5.2/§5.11;数据模型 supplier_main/type_rel","已实现 · 可联调"),
|
||||||
E("SUP-ADM-002","GET","/admin/supplier/items/{supplierId}/basic-info/view","查询供应商详细信息","档案与生命周期","平台菜单/按钮权限",
|
E("SUP-ADM-002","GET","/admin/supplier/items/{supplierId}/basic-info/view","查询供应商详细信息","档案与生命周期","平台菜单/按钮权限",
|
||||||
"Path supplierId(String)","Result<SupplierBasicInfoVO>",
|
"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","已实现 · 可联调"),
|
["SUPPLIER_NOT_FOUND"],"财务控制台原型三页签;详细设计 §5.2/§5.11;supplier_main/type_rel/contact/qualification","已实现 · 可联调"),
|
||||||
E("SUP-ADM-003","POST","/admin/supplier/items/add","创建供应商注册草稿","档案与生命周期","平台菜单/按钮权限",
|
E("SUP-ADM-003","POST","/admin/supplier/items/add","创建供应商注册草稿","档案与生命周期","平台菜单/按钮权限",
|
||||||
"SupplierDraftUpsertRequest","Result<SupplierWriteResultVO>",
|
"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 时拒绝且零写入","主体、关联表或供应商/账户变更留痕任一步失败时整笔事务回滚,不得留下不完整草稿"],
|
["保存供应商注册表单草稿,供后续 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_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","已实现 · 可联调"),
|
"详细设计 §5.2/§5.11;数据模型 main/type/contact/qualification/account","已实现 · 可联调"),
|
||||||
E("SUP-ADM-004","PUT","/admin/supplier/items/{supplierId}/update","更新供应商","档案与生命周期","平台菜单/按钮权限",
|
E("SUP-ADM-004","PUT","/admin/supplier/items/{supplierId}/update","更新供应商","档案与生命周期","平台菜单/按钮权限",
|
||||||
"SupplierUpdateRequest","Result<SupplierWriteResultVO>",
|
"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;新增、更新、软删除及审计在同一事务内完成,任一步失败整笔回滚"],
|
["用于补全或更新供应商资料;供应商注册草稿可能只录入最低必填信息,除 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_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","已实现 · 可联调"),
|
"详细设计 §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","提交供应商注册审批","档案与生命周期","平台菜单/按钮权限",
|
E("SUP-ADM-007","POST","/admin/supplier/items/{supplierId}/submit","提交供应商注册审批","档案与生命周期","平台菜单/按钮权限",
|
||||||
"SupplierSubmitRequest:完整供应商表单字段、duplicateConfirmToken?、submitNote?、expectedUpdateTime;fullName、taxNo、types、mainCooperation、licenseImageUrl、expectedUpdateTime 必填",
|
"SupplierSubmitRequest:完整供应商表单字段、duplicateConfirmToken?、submitNote?、expectedUpdateTime;fullName、taxNo、types、mainCooperation、licenseImageUrl、expectedUpdateTime 必填",
|
||||||
"Result<ApprovalCommandResultVO>",
|
"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;表单保存、变更留痕、审批流水或状态更新任一步失败时整笔事务回滚"],
|
["提交供应商注册表单,生成注册审批流水,并使供应商进入合作中状态","提交表单的资质 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"],"详细设计 §5.2/§5.4/§5.11/§6;数据模型 supplier_main/supplier_approval_log/supplier_change_log","已实现 · 可联调"),
|
["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","清账归档","档案与生命周期","平台菜单/按钮权限",
|
E("SUP-ADM-010","POST","/admin/supplier/items/{supplierId}/archive","清账归档","档案与生命周期","平台菜单/按钮权限",
|
||||||
"Path supplierId","Result<ArchiveResultVO>",
|
"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 不可恢复,历史引用和审批流水不得删除"],
|
["无 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_ALREADY_BOUND",395036,"资源已经存在其他有效供应商关系","该资源已关联其他供应商,请使用改绑"],
|
||||||
["SUPPLIER_RESOURCE_TYPE_MISMATCH",395037,"目标供应商缺少 requiredTypeCode 对应的有效类型,或删除了仍被关系引用的类型","供应商类型不满足资源关联要求"],
|
["SUPPLIER_RESOURCE_TYPE_MISMATCH",395037,"目标供应商缺少 requiredTypeCode 对应的有效类型,或删除了仍被关系引用的类型","供应商类型不满足资源关联要求"],
|
||||||
["SUPPLIER_RESOURCE_RELATION_NOT_FOUND",395038,"关联关系不存在、已解绑或不属于指定供应商","供应商资源关联不存在"],
|
["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){
|
function esc(value){
|
||||||
@@ -990,7 +992,7 @@
|
|||||||
}
|
}
|
||||||
function ref(name){return {$ref:"#/components/schemas/"+name};}
|
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 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){
|
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"};
|
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;
|
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.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.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.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.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.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.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.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.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.SupplierEvaluationMergeInput=objectSchema({evaluationId:idSchema(),orderId:idSchema(),resourceId:idSchema(),tripDate:localDateSchema(),dimension:textSchema({minLength:1,maxLength:32}),score:{type:"number",minimum:0,maximum:5},content:textSchema({"x-db-type":"TEXT","x-max-utf8-bytes":65535}),evaluator:idSchema(),expectedUpdateTime:localDateTimeSchema()},[],{minProperties:1,description:"评价集合快照项;已有记录携带 evaluationId 和 expectedUpdateTime,无 evaluationId 时新增;数据库有效评价中未出现在快照内的记录软删除。","x-hl-existing-record-requires":["evaluationId","expectedUpdateTime"],"x-hl-create-omits":["evaluationId","expectedUpdateTime"],"x-hl-create-required":["dimension","score"]});
|
||||||
schemas.SupplierUpdateRequest=objectSchema({fullName:textSchema({minLength:1,maxLength:500}),shortName:textSchema({maxLength:300}),taxNo:textSchema({minLength:1,maxLength:64,writeOnly:true,"x-db-type":"TEXT","x-db-collation":"ascii_bin","x-max-utf8-bytes":64}),legalRepresentative:textSchema({maxLength:500}),contactPhone:textSchema({title:"法人电话/公司电话",maxLength:20,writeOnly:true,"x-db-type":"TEXT","x-max-utf8-bytes":20}),establishDate:localDateSchema(),registeredCapital:textSchema({maxLength:50}),businessScope:textSchema({maxLength:500}),address:textSchema({maxLength:500}),staffScale:stringEnum(["LT50","R50_200","R200_500","GT500"]),mainCooperation:textSchema({minLength:1,"x-db-type":"TEXT","x-max-utf8-bytes":65535}),licenseImageUrl:textSchema({maxLength:500}),approveNote:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215}),remark:textSchema({"x-db-type":"MEDIUMTEXT","x-max-utf8-bytes":16777215}),types:arraySchema(ref("SupplierTypeMergeInput"),{minItems:1,maxItems:15,description:"未传表示不处理;传入表示完整快照,缺失的有效类型关联软删除;至少保留一个类型"}),contacts:arraySchema(ref("SupplierContactMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效联系人"}),qualifications:arraySchema(ref("SupplierQualificationMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效资质"}),contracts:arraySchema(ref("SupplierContractMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效合同"}),evaluations:arraySchema(ref("SupplierEvaluationMergeInput"),{maxItems:100,description:"未传表示不处理;传入表示完整快照,缺失项软删除;空数组软删除全部有效评价"}),changeReason:textSchema({minLength:1,maxLength:500}),expectedUpdateTime:localDateTimeSchema()},["changeReason","expectedUpdateTime"],{minProperties:3,description:"供应商及关联表增量补全请求;主体标量按 PATCH 更新。集合字段未传时保持不变,一旦传入即为完整当前快照:同 ID 更新、无 ID 新增、存量缺失项写 deleted_at 软删除,禁止物理删除。","x-hl-update-mode":"PATCH_SCALAR_AND_RECONCILE_COLLECTIONS","x-hl-collection-semantics":"OMITTED_UNCHANGED_PROVIDED_FULL_SNAPSHOT","x-hl-missing-existing-item":"SOFT_DELETE","x-hl-empty-collection":{"types":"REJECT_MIN_ONE","contacts":"SOFT_DELETE_ALL","qualifications":"SOFT_DELETE_ALL","contracts":"SOFT_DELETE_ALL","evaluations":"SOFT_DELETE_ALL"},"x-hl-physical-delete":"FORBIDDEN","x-hl-at-least-one-change-excluding":["changeReason","expectedUpdateTime"],"x-hl-immutable-when-approving-or-approved":["fullName","taxNo"],"x-hl-account-fields":"FORBIDDEN_USE_ACCOUNT_APIS"});
|
schemas.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.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.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.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.SupplierBankAccountVO=objectSchema({accountId:idSchema(),accountName:textSchema(),accountType:stringEnum(accountTypeEnum),bankName:textSchema(),bankBranch:textSchema(),accountNoMask:textSchema(),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema(),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema(),status:stringEnum(accountStatusEnum),isDefault:stringEnum(["YES","NO"],"YES=是,NO=否"),updateTime:localDateTimeSchema()},["accountId","accountName","accountType","bankName","accountNoMask","status","isDefault","updateTime"],{description:"supplier_main 与 supplier_account 为 1:N;结算字段均来自当前 supplier_account;同一供应商的未删除账户中最多一个 isDefault=YES,由原子切换事务和数据库唯一约束共同保证;不返回候选值"});
|
||||||
schemas.SupplierBankAccountDetailVO=objectSchema({accountId:idSchema(),accountName:textSchema(),accountType:stringEnum(accountTypeEnum),bankName:textSchema(),bankBranch:textSchema(),accountNoMask:textSchema(),settleMode:stringEnum(["PREPAY","MONTHLY","SINGLE"]),accountPeriod:textSchema(),invoiceType:stringEnum(["SPECIAL","NORMAL","NONE"]),taxRate:textSchema(),status:stringEnum(accountStatusEnum),isDefault:stringEnum(["YES","NO"],"YES=是,NO=否"),proofFileUrls:arraySchema(textSchema({minLength:1,maxLength:1000}),{maxItems:20}),updateTime:localDateTimeSchema()},["accountId","accountName","accountType","bankName","accountNoMask","status","isDefault","updateTime"],{description:"单项详情独立严格 Schema;结算字段来自当前账户;proofFileUrls 仅具备 supplier:account:proof:read 时返回,否则省略"});
|
schemas.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(){
|
function rootCodegenPolicy(){
|
||||||
return {
|
return {
|
||||||
version:"supplier-frontend-v2.1-20260823",
|
version:"supplier-frontend-v2.1-20260825",
|
||||||
failOnMissingWritePolicy:true,
|
failOnMissingWritePolicy:true,
|
||||||
failOnTodoOrNoopImplementation:true,
|
failOnTodoOrNoopImplementation:true,
|
||||||
serverOperationCount:16,
|
serverOperationCount:16,
|
||||||
@@ -1277,7 +1279,7 @@
|
|||||||
apiSections.forEach(function(section){tagDescriptions[section.title]=section.description;});
|
apiSections.forEach(function(section){tagDescriptions[section.title]=section.description;});
|
||||||
const spec={
|
const spec={
|
||||||
openapi:"3.0.3",
|
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 仅服务间调用"}],
|
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]||"供应商模块接口契约。"};}),
|
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:{},
|
paths:{},
|
||||||
@@ -1567,7 +1569,7 @@
|
|||||||
expectedUpdateTime:"期望数据更新时间",createTime:"创建时间",updateTime:"更新时间",startDate:"开始日期",endDate:"结束日期",expiryDate:"到期日期",finishedAt:"完成时间",submittedAt:"提交时间",acceptedAt:"受理时间",checkedAt:"检查时间",
|
expectedUpdateTime:"期望数据更新时间",createTime:"创建时间",updateTime:"更新时间",startDate:"开始日期",endDate:"结束日期",expiryDate:"到期日期",finishedAt:"完成时间",submittedAt:"提交时间",acceptedAt:"受理时间",checkedAt:"检查时间",
|
||||||
page:"页码",pageSize:"每页条数",total:"总条数",records:"记录列表",keyword:"搜索关键字",creatorId:"创建人ID",scene:"业务场景",sortOrder:"排序号",sortBy:"排序字段",sortDirection:"排序方向",includeDeleted:"是否包含已删除数据",
|
page:"页码",pageSize:"每页条数",total:"总条数",records:"记录列表",keyword:"搜索关键字",creatorId:"创建人ID",scene:"业务场景",sortOrder:"排序号",sortBy:"排序字段",sortDirection:"排序方向",includeDeleted:"是否包含已删除数据",
|
||||||
contactId:"联系人ID",contactName:"联系人姓名",contactPhone:"联系人电话",contactPhoneMask:"联系人电话掩码",contactRole:"联系人角色",contacts:"联系人列表",businessContacts:"业务联系人列表",financialContacts:"财务联系人列表",
|
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:"凭证文件地址列表",
|
accountId:"账户ID",accountName:"账户名称",accountNo:"收款账号",accountNoMask:"收款账号掩码",accountType:"账户类型",bankName:"开户银行",bankBranch:"开户支行",accounts:"新增收款账户列表",bankAccounts:"收款账户列表",isDefault:"是否默认账户",proofFileUrls:"凭证文件地址列表",
|
||||||
amount:"金额",settleMode:"结算方式",accountPeriod:"账期",invoiceType:"发票类型",taxRate:"税率",
|
amount:"金额",settleMode:"结算方式",accountPeriod:"账期",invoiceType:"发票类型",taxRate:"税率",
|
||||||
changeType:"变更类型",changeReason:"变更原因",candidate:"候选变更数据",targetStatus:"目标状态",currentStatus:"当前状态",creditLevel:"信用等级",totalScore:"综合评分",riskFlags:"风险标记列表",reason:"原因",reasons:"原因列表",warning:"预警提示",warnings:"预警提示列表",
|
changeType:"变更类型",changeReason:"变更原因",candidate:"候选变更数据",targetStatus:"目标状态",currentStatus:"当前状态",creditLevel:"信用等级",totalScore:"综合评分",riskFlags:"风险标记列表",reason:"原因",reasons:"原因列表",warning:"预警提示",warnings:"预警提示列表",
|
||||||
@@ -1913,7 +1915,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<header class="supplier-header">
|
<header class="supplier-header">
|
||||||
<h1>供应商模块 API 接口规范</h1>
|
<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>
|
</header>
|
||||||
<div class="supplier-layout">
|
<div class="supplier-layout">
|
||||||
<aside class="supplier-sidebar" aria-label="接口大纲">
|
<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>
|
<details><summary>查看完整 OpenAPI 3.0.3</summary><pre id="supplierOpenApiPreview"><code>${esc(specJson)}</code></pre></details>
|
||||||
</section>
|
</section>
|
||||||
${categoryContent}
|
${categoryContent}
|
||||||
<p class="footer">供应商模块 API 接口规范 v2.1 前端联调版 · 发行日期 2026-08-23 · 单文件离线文档</p>
|
<p class="footer">供应商模块 API 接口规范 v2.1 前端联调版 · 发行日期 2026-08-25 · 单文件离线文档</p>
|
||||||
</main>
|
</main>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.getElementById("supplierDownloadOpenApi").addEventListener("click",function(){
|
document.getElementById("supplierDownloadOpenApi").addEventListener("click",function(){
|
||||||
|
|||||||
@@ -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: "pending"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
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
|
||||||
在新工单中引用
屏蔽一个用户