--- schema: "hl-changelog/v2" ticket: "6275" title: "供应商联系人默认规则与角色字典校验" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "f8a0d72d" target_release: "" verified_at: "" status_note: "PR #6276 已合并 dev-v3,合并提交 f7b442b67 已由部署任务 #785ce882 发布到 TEST 并经 Gateway 验证。非空联系人快照现在始终归一为且仅为一个默认联系人;联系人角色按 sup_content_role 当前 ACTIVE 字典值校验。管理端待将角色/职务改为动态下拉并适配新的默认联系人规则。" updated_at: "2026-08-24" base: "dev-v3" --- # 供应商联系人默认规则与角色字典校验 供应商创建、草稿更新和注册提交统一收紧联系人契约:联系人列表非空时,服务端保证最终恰好一个默认联系人;联系人角色必须使用字典 `sup_content_role` 当前启用项的 `dictValue`。本记录覆盖 #6258 中“允许没有默认联系人、取消或删除后不自动选举”的旧语义。 ## 变更接口 | 方法 | 路径 | 行为变化 | |---|---|---| | POST | `/admin/supplier/items/add` | 非空 `contacts` 未显式选择默认项时,服务端将请求第一项设为默认;联系人角色按生效字典校验 | | PUT | `/admin/supplier/items/{supplierId}/update` | 非空联系人快照最终未保留或选中默认项时,请求第一项自动成为默认;显式提交的角色按生效字典校验 | | POST | `/admin/supplier/items/{supplierId}/submit` | 完整联系人快照执行相同的唯一默认归一和角色字典校验;失败发生在主体、联系人和审批写入前 | | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 返回服务端归一后的 `contacts[].isPrimary` 和已保存的 `contacts[].contactRole`;电话仍仅返回 `contactPhoneMask` | 管理端角色下拉继续使用既有通用字典接口,该接口本工单未修改: | 方法 | 路径 | 用途 | |---|---|---| | GET | `/admin/dict/data/sup_content_role` | 获取联系人角色选项;展示 `dictLabel`,提交 `dictValue`,仅使用 `status=ACTIVE` 的项 | 业务失败可能仍返回 HTTP 200,调用方必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。 ## 默认联系人规则 - `contacts` 为非空列表时,保存后必须且只会有一个 `isPrimary=true`。 - 只有一个联系人时,即使请求传 `false`、`null` 或省略 `isPrimary`,该联系人也会成为默认联系人。 - 多个联系人且没有任何项传 `true` 时,按请求数组顺序将第一项设为默认联系人。 - 恰好一个联系人传 `true` 时保留该选择。 - 两个及以上联系人传 `true` 时返回业务码 `400`、消息 `同一供应商只能设置一个默认联系人`,整次请求零写入。 - 草稿增量更新中,已有联系人省略 `isPrimary` 时优先保留原默认状态;显式传 `false` 表示取消。若删除或取消原默认联系人后没有其他默认项,则请求列表第一项自动成为默认。 - 增量更新的 `contacts=null` 或省略字段表示联系人不变;`contacts=[]` 表示清空联系人,空列表允许零默认联系人。 - 客户端应在提交后重新读取详情,以服务端返回的 `isPrimary` 为最终状态。 创建或完整提交示例(两个联系人均未选择默认): ```json { "contacts": [ { "contactName": "联系人甲", "contactPhone": "13800000001", "contactRole": "contentBus", "isPrimary": false }, { "contactName": "联系人乙", "contactPhone": "13800000002", "contactRole": "contentMoney", "isPrimary": false } ] } ``` 保存后详情中的第一项为默认联系人: ```json { "contacts": [ { "contactName": "联系人甲", "contactPhoneMask": "138****0001", "contactRole": "contentBus", "isPrimary": true }, { "contactName": "联系人乙", "contactPhoneMask": "138****0002", "contactRole": "contentMoney", "isPrimary": false } ] } ``` `Long` 类型的 `contactId` 继续按字符串传输;更新已有联系人仍需携带详情返回的 `expectedUpdateTime`。 ## 联系人角色字典规则 - 创建和注册提交的每个联系人都必须提供非空 `contactRole`,值必须精确匹配 `sup_content_role` 当前 `status=ACTIVE` 项的 `dictValue`。 - 草稿增量更新已有联系人时,省略 `contactRole` 表示保持原值,不会因为本次无关更新而额外依赖字典;新增联系人或显式修改角色时必须提交生效的 `dictValue`。 - 空白、未知或已停用值返回业务码 `400`、消息 `联系人角色不合法或已停用`,失败请求不修改供应商或联系人。 - 字典服务异常、响应失败、空数据或没有启用项时失败关闭,返回“联系人角色字典暂不可用,请稍后重试”,不降级为硬编码选项。 - TEST 当前可见项为 `contentBus`(业务人员)和 `contentMoney`(财务人员),仅用于联调示例;客户端必须动态读取字典,不能将这些值固化在代码中。 字典响应示例: ```json { "code": 200, "success": true, "data": [ { "dictType": "sup_content_role", "dictLabel": "业务人员", "dictValue": "contentBus", "sortOrder": 1, "status": "ACTIVE" } ] } ``` ## 管理端接入事项 1. “角色/职务”字段改为下拉框,进入联系人页面时请求 `/admin/dict/data/sup_content_role`,展示 `dictLabel`、提交 `dictValue`,过滤非 `ACTIVE` 项。 2. 单联系人场景将该联系人呈现为默认;多联系人可选择一个默认联系人,但不可同时选择多个。未选择时服务端会把请求第一项设为默认。 3. 新增、删除、拖动或调整联系人顺序后,提交数组顺序应与界面顺序一致;保存成功后重新读取详情并以服务端返回值刷新默认标识。 4. 更新已有联系人时,未修改角色可省略 `contactRole`;新增联系人必须提交从字典选择的角色值。 ## 未变化范围 - 不新增接口、Gateway 路由、数据库 migration、字典数据、菜单权限、角色、状态机或审批节点。 - 不改变联系人电话加密存储、脱敏输出、软删除、审计、聚合锁、幂等与乐观版本语义。 - 不修改配置、Redis Key 或 MQ 契约,也不回填历史联系人数据。 - 本工单只交付后端;管理端代码未在后端仓库修改,前端状态保持 `pending` 直至完成上述接入并提供验证提交。 ## 验证证据 - 自动化:默认联系人写入 16 项、应用服务 14 项、角色字典 11 项、聚合校验 6 项,共 47 项定向测试零失败;Resource 全量 1966 项零失败,38 项仓库既有条件跳过。 - 合并后独立审计:在目标提交上重新执行 47 项定向测试,零失败且未发现确定性缺口。 - TEST 部署:部署任务 #785ce882 成功,`hl-resource-service` 的 8082、8182 实例滚动重启并恢复健康;部署服务器 HEAD `1f2e0ac6d` 包含目标合并提交 `f7b442b67`。 - TEST Gateway:使用真实管理员会话验证单联系人自动默认、多联系人无选择时第一项默认、删除默认后重新选举、角色省略保持原值、清空联系人、重复默认与非法角色零写入、提交失败零审批副作用;未认证请求返回业务码 `401`。 - 清理:合成供应商草稿已通过既有删除接口清理,精确查询结果为 0;未产生外部文件、Redis、MQ 或配置副作用。 ## 撤回 1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit f7b442b67f4d989fac6b29a771331ca6d66148e3`,经独立 PR 合入。 2. 重新构建并滚动部署 `hl-resource-service`;本次没有数据库、字典数据、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。 3. 回退后恢复 #6258 的旧语义:允许联系人列表非空但没有默认联系人,角色不再由后端按 `sup_content_role` 动态校验;接口路径和字段结构不变。 4. 经 Gateway 复测创建、更新、提交和详情接口,确认回退后的默认联系人语义、角色传值、电话脱敏、权限和失败零写入符合目标版本。 5. 若管理端已上线本次交互,应同步停止依赖“服务端必定选出一个默认联系人”的保证;字典下拉仍可继续使用既有通用字典接口,但需按回退版本确认提交兼容性。 ## 关联 / 联系人 - **Issue**: [#6275](https://git.1814.love:8443/wx/HL/issues/6275) - **PR**: [#6276](https://git.1814.love:8443/wx/HL/pulls/6276) - **合并提交**: [f7b442b67](https://git.1814.love:8443/wx/HL/commit/f7b442b67f4d989fac6b29a771331ca6d66148e3) - **后端负责人**: @lc