From e1b0a667cdb4bff82c3bcb91f53e9b1154d6e108 Mon Sep 17 00:00:00 2001 From: lc Date: Tue, 25 Aug 2026 16:23:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(api):=20=E5=8F=91=E5=B8=83=E4=BE=9B?= =?UTF-8?q?=E5=BA=94=E5=95=86=E8=B5=84=E8=B4=A8=E6=9C=89=E6=95=88=E6=9C=9F?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...›应商模块 API 接口规范-v2.1-前端联调版.html | 48 +- ...增加永久有效与到期剩余天数-修改接口-管理后台.md | 419 ++++++++++++++++++ 2 files changed, 444 insertions(+), 23 deletions(-) create mode 100644 changelogs-v2/2026-08/25_6304_供应商资质增加永久有效与到期剩余天数-修改接口-管理后台.md diff --git a/api-docs/supplier/供应商模块 API 接口规范-v2.1-前端联调版.html b/api-docs/supplier/供应商模块 API 接口规范-v2.1-前端联调版.html index c688f7f3..83c9782e 100644 --- a/api-docs/supplier/供应商模块 API 接口规范-v2.1-前端联调版.html +++ b/api-docs/supplier/供应商模块 API 接口规范-v2.1-前端联调版.html @@ -163,7 +163,7 @@

供应商模块 API 接口规范

-

发行版 v2.1 · 基线 v2.0 / 前端联调版 2026-08-23 · 已部署 TEST

+

发行版 v2.1 · 基线 v2.0 / 前端联调版 2026-08-25 · 已部署 TEST

供前端联调使用;契约基线来自 v2.0,运行状态以当前 TEST 验收结论为准。

后端 only @@ -584,24 +584,25 @@ approveNoteString(MEDIUMTEXT,UTF-8 ≤ 16,777,215 bytes)选填审批说明;对应 approve_note MEDIUMTEXT remarkString(MEDIUMTEXT,UTF-8 ≤ 16,777,215 bytes)选填备注;对应 remark MEDIUMTEXT contactsList<SupplierContactInput>选填逐条校验 - qualificationsList<SupplierQualificationInput>选填提交时按所有类型必备规则并集执行 C-09 + qualificationsList<SupplierQualificationInput>选填提交时按所有类型必备规则并集执行 C-09;每项以 permanentValid + expiryDate 表达有效期 initialAccountsList<SupplierBankAccountInput>(0..N)选填仅注册草稿;随 PROFILE_CREATE 共审 duplicateConfirmTokenString条件必填存在近似候选时使用;不接受 force=true expectedUpdateTimeLocalDateTime更新供应商必填格式 yyyy-MM-dd HH:mm:ss;创建草稿不传
更新边界:SupplierUpdateRequest 是独立的增量补全请求,不再继承全量建档 DTO;主体标量字段按 PATCH 语义更新。types、contacts、qualifications、contracts、evaluations 未传时保持不变,一旦传入则代表该集合的完整当前快照:同 ID 项更新、无 ID 项新增、数据库有效记录中未出现在请求内的项写 deleted_at 软删除。联系人、资质、合同、评价传空数组表示软删除该集合全部有效记录;类型至少保留一个,types=[] 参数校验失败。除 changeReason、expectedUpdateTime 外至少提交一个实际变化字段。未提交草稿允许修改 fullName、taxNo;进入审批中或审批完成后,两字段只允许原值回传,任何实际变化均拒绝。请求不接收 creditLevel 或账户字段。
+
资质有效期契约:permanentValid=true 时 expiryDate 必须为空,permanentValid=false 时 expiryDate 必填,矛盾组合返回 SUPPLIER_QUALIFICATION_VALIDITY_INVALID(395042) 且零写入。历史完整请求未传 permanentValid 时按 expiryDate 是否为空推导;增量更新的既有资质同时省略两字段时保持原有效期。详情固定返回 permanentValid,并按 Asia/Shanghai 当前自然日动态返回有符号 daysUntilExpiry;永久有效时天数为 null。

嵌套输入

- + - + @@ -612,7 +613,7 @@ - + @@ -735,7 +736,7 @@
DTO字段严格规则
SupplierContactInputcontactId?、contactName(1..500)、contactPhone(1..20)、contactRole、remark(0..200)既有 ID 必须属于当前供应商;电话不回显明文
SupplierQualificationInputqualificationId?、qualType(1..64)、certNo(0..128)、imageUrl(TEXT,0..65,535 UTF-8 bytes)、expiryDate?isRequired 由服务端规则派生;客户端不得传 isRequired
SupplierQualificationInputqualificationId?、qualType(1..64)、certNo(0..128)、imageUrl(TEXT,0..65,535 UTF-8 bytes)、permanentValid?、expiryDate?permanentValid=true 时 expiryDate 必须为空,false 时 expiryDate 必填;未传时按 expiryDate 是否为空兼容推导。isRequired 由服务端规则派生,客户端不得传 isRequired
SupplierBankAccountInputaccountType、bankName(1..500)、bankBranch(0..500)、accountNo(UTF-8 1..128 bytes)、proofFileUrls?(0..20,每项 1..1000)、settleMode?、accountPeriod?、invoiceType?、taxRate?CORPORATE/PERSONAL;结算字段按账户保存;MONTHLY 才允许 accountPeriod,SPECIAL/NORMAL 必填 taxRate,NONE 时 taxRate 为空;accountName 由主体全称派生;无 confirmAccountNo/accountNoMask/isPersonal
SupplierTypeMergeInputtypeCode、isPrimary?集合传入后按 (supplierId,typeCode) 对账;已存在则更新,不存在则新增,未出现在本次完整快照中的有效关联写 deleted_at
SupplierContactMergeInputcontactId?、联系人字段?、expectedUpdateTime?已有项必须同时携带 contactId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有联系人软删除
SupplierQualificationMergeInputqualificationId?、资质字段?、expectedUpdateTime?已有项必须同时携带 qualificationId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有资质软删除
SupplierQualificationMergeInputqualificationId?、资质字段?、permanentValid?、expiryDate?、expectedUpdateTime?已有项必须同时携带 qualificationId/expectedUpdateTime;既有项同时省略 permanentValid/expiryDate 时保持当前有效期;新增项不传 ID/版本且双省略时兼容为永久有效;集合快照中缺失的既有资质软删除
SupplierContractMergeInputcontractId?、合同字段?、expectedUpdateTime?已有项必须同时携带 contractId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有合同软删除
SupplierEvaluationMergeInputevaluationId?、评价字段?、expectedUpdateTime?已有项必须同时携带 evaluationId/expectedUpdateTime;新增项不传两字段;集合快照中缺失的既有评价软删除
VO核心字段禁止字段
SupplierListItemVOsupplierId/no、full/shortName、types、status、creditLevel/totalScore、activeAccountCount、create/updateTime税号/证件/电话/账号明文
SupplierBasicInfoVO主体公开业务字段、证件 mask、types、contacts mask、qualifications mask、信用、状态、updateTimeisRelatedParty、approveNote、完整账号
SupplierBasicInfoVO主体公开业务字段、证件 mask、types、contacts mask、qualifications mask(含类型中文名、permanentValid、daysUntilExpiry、有效状态及只读 isRequired)、信用、状态、updateTimeisRelatedParty、approveNote、完整账号
SupplierAccountInfoVOsupplierId、bankAccounts(含当前生效结算口径及账号 mask)、updateTime完整账号、密文、候选账号与往来数据
SupplierResourceRelationVOrelationId、supplierId/no/name、resourceModule/moduleName、resourceId/name、requiredTypeCode/name、remark、available/reasons、create/updateTime资源主表名、跨服务内部字段、供应商账户与资质附件
SupplierWriteResultVOsupplierId、supplierNo?、status、onboardingStage、initialAccounts[{accountId,accountNoMask,status}]、updateTime账号明文
- +
@@ -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", - ["仅返回主体档案、多类型、联系人、资质、信用和生命周期状态","前端按 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", - ["保存供应商注册表单草稿,供后续 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", - ["用于补全或更新供应商资料;供应商注册草稿可能只录入最低必填信息,除 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", - ["提交供应商注册表单,生成注册审批流水,并使供应商进入合作中状态","仅 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", ["无 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 错误包络;业务错误通常使用 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 @@

供应商模块 API 接口规范

-

v2.1 前端联调版 · 2026-08-23 · ${activeCount} 个 HTTP 契约 · OpenAPI 3.0.3 · TEST 验收状态

+

v2.1 前端联调版 · 2026-08-25 · ${activeCount} 个 HTTP 契约 · OpenAPI 3.0.3 · TEST 验收状态

`; document.getElementById("supplierDownloadOpenApi").addEventListener("click",function(){ diff --git a/changelogs-v2/2026-08/25_6304_供应商资质增加永久有效与到期剩余天数-修改接口-管理后台.md b/changelogs-v2/2026-08/25_6304_供应商资质增加永久有效与到期剩余天数-修改接口-管理后台.md new file mode 100644 index 00000000..09d95fae --- /dev/null +++ b/changelogs-v2/2026-08/25_6304_供应商资质增加永久有效与到期剩余天数-修改接口-管理后台.md @@ -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`。业务校验失败可能仍返回 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