--- schema: "hl-changelog/v2" ticket: "7916" title: "团期物资列表与候选列表出参新增分类中文名 categoryName(字典外取值按原值显示)" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "08d13de33b27948c31cb1e511ad59f342420e88c" target_release: "" verified_at: "2026-09-18" status_note: "团期物资列表 GET /v3/admin/order/group-batch/{groupBatchId}/supplies 与候选列表 .../supplies/candidates 出参各纯新增一个 categoryName;既有 category 原值一字不改,入参与错误码零变化。翻译三段回落:命中字典 supplies_category 的码译中文;值本身已是中文原样返回;字典外取值(TEST 存量的 EQUIPMENT / PROTECTION,共 140 行)按原值显示,不编造也不置空。字典 Feign 不可达时整批回落原值且接口仍 200(TEST 实测注入过)。后端已合并 dev-v3 并部署 TEST。[mmg 2026-09-18 已实现并验证] SuppliesPanel/SuppliesPickerModal 分类列由自查字典(getDictLabel||code)改显后端 categoryName(categoryName||category||'—'),SuppliesPanel 随删闲置 dictStore;筛选仍传 categoryCode 码、提交仍用 category 原值,categoryName 仅展示不分组不回传;SuppliesPanel.spec 加 categoryName 优先/回落断言,定向 7/7+checkpoint 全绿。" updated_at: "2026-09-18" base: "dev-v3" --- # 团期物资: 列表与候选列表出参新增分类中文名 `categoryName` > **服务**: hl-order-service-v3 (端口 8086) > **PR**: #7920 > **Issue**: #7916 > **日期**: 2026-09-18 > **影响范围**: 管理后台团期详情「物资清单」与「从备品库选择」候选弹窗的分类列 --- ## ⚠️ 关键变化 - 本次两个接口的出参**各纯新增一个 `categoryName`**,既有 `category` 原值**一字不改**。 - 前端以前可能以为:拿到 `category` 自己查字典 `supplies_category` 就能译出中文。**这个做法译不出线上多数行**——TEST 实测 `order_batch_supplies.category` 存量 442 行里有 140 行是字典里根本没有的 `EQUIPMENT` / `PROTECTION`,另有 301 行**存的本来就是中文**。 - 实际现在是:后端按三段回落给出 `categoryName`,**字典外的取值原样回传**(显示出来仍是 `EQUIPMENT`),不是后端漏译,是该列的存量脏值,归一另列工单。 --- ## 一、背景 `order_batch_supplies.category` 是**下单时固化的快照**,写入来源有三个且口径各异(产品备品中文与大写码混杂、备品库基本是字典码、管理员手工新增无校验),因此同一列并存三套词表。 | 维度 | TEST 实测(2026-09-18,`hl_order_service_v3`) | |------|--------------------------------------------| | 中文标签(如 `个人装备`) | 301 行 | | 字典码(如 `safety_protection`) | 1 行 | | 字典外大写码 `EQUIPMENT` | 70 行 | | 字典外大写码 `PROTECTION` | 70 行 | | 字典 `supplies_category` 取值 | 8 项,**全是小写码**,无 `EQUIPMENT` / `PROTECTION` | --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 查询团期备品列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/supplies` | 出参新增字段 | 每行新增 `categoryName`,`category` 不变 | | 2 | 团期物资候选列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/supplies/candidates` | 出参新增字段 | 每项新增 `categoryName`,与列表同一套翻译规则 | --- ## 三、接口详情 ### 1. 查询团期备品列表 `GET /v3/admin/order/group-batch/{groupBatchId}/supplies` **VO**: `GroupBatchSuppliesRespVO` #### 使用场景 管理后台团期详情页「物资清单」区域加载已固化/已手工追加的备品行;分类列改为显示 `categoryName`,排序、筛选、提交仍用 `category` 原值。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID | 团期主订单 ID | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | id | String | 备品行 ID(19 位雪花,字符串输出) | | groupBatchId | String | 团期 ID | | suppliesName | String | 备品名称 | | category | String | **分类原值(本次不变)**:可能是字典码、中文标签或字典外取值,可为 null | | categoryName | String | **本次新增**:分类展示名;`category` 为 null/空白时为 null | | hasCost | Boolean | 是否计费 | | billingType | String | 计费方式 PER_PERSON / PER_QUANTITY | | unitPrice | String | 单价 | | quantity | Integer | 数量 | | sortOrder | Integer | 排序号 | | remark | String | 备注 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2100509904627191810/supplies Authorization: Bearer {admin-jwt} ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": [ { "id": "2100509905000000001", "suppliesName": "团队识别手环1", "category": "个人装备", "categoryName": "个人装备", "quantity": 1 }, { "id": "2100509905000000002", "suppliesName": "户外急救箱", "category": "safety_protection", "categoryName": "安全防护", "quantity": 1 } ], "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 200, "message": "成功", "data": [], "success": true } ``` 字典服务不可达时**不降级整个接口**,只是 `categoryName` 全批等于 `category`: ```json { "code": 200, "message": "成功", "data": [ { "id": "2100509905000000002", "suppliesName": "户外急救箱", "category": "safety_protection", "categoryName": "safety_protection" } ], "success": true } ``` #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null, "success": false } ``` #### 业务边界 - 鉴权:管理后台 JWT + 团期查看权限(`GroupBatchPermissionGuard` VIEW),未登录 401。 - `category` 为 null 或纯空白 → `categoryName` 为 **null**,不是空串、不是字符串 "null"。 - `category` 命中字典 → 中文标签;已是中文标签 → 原样返回;字典外取值 → **原样返回**,后端不编造分类。 - 只读接口,天然幂等;整页只加载一次字典,不按行调用。 --- ### 2. 团期物资候选列表 `GET /v3/admin/order/group-batch/{groupBatchId}/supplies/candidates` **VO**: `SuppliesCandidateRespVO` #### 使用场景 管理后台团期物资「从备品库选择」弹窗,列出资源域备品库中上架未软删的备品,并标出哪些已加入本团清单;分类列同样改用 `categoryName`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID | 团期主订单 ID | | categoryCode | Query | String | ❌ | 字典 `supplies_category` 的值 | 按分类筛选,**筛选仍用码,不用中文名** | | keyword | Query | String | ❌ | - | 匹配名称或副标题 | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | suppliesId | String | 备品库 ID | | suppliesName | String | 备品名称 | | subtitle | String | 副标题 | | category | String | 分类码原值(本次不变),来自备品库 `category_code` | | categoryName | String | **本次新增**:分类展示名,规则与列表接口完全一致 | | billingType | String | 计费方式(已归一为 PER_PERSON / PER_QUANTITY) | | hasCost | Boolean | 是否计费 | | unitPrice | String | 单价 | | selected | Boolean | 是否已加入本团清单 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2099919772194836482/supplies/candidates?categoryCode=camping_equipment Authorization: Bearer {admin-jwt} ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": [ { "suppliesId": "2074063318837792770", "suppliesName": "折叠桌椅套装", "category": "camping_equipment", "categoryName": "露营设备" }, { "suppliesId": "2074063318837792771", "suppliesName": "对讲机", "category": "electronics", "categoryName": "电子设备" } ], "success": true } ``` #### 空数据 / 降级响应 资源域备品库不可达或无候选时返回空数组,不 500: ```json { "code": 200, "message": "成功", "data": [], "success": true } ``` #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null, "success": false } ``` #### 业务边界 - 鉴权同列表接口(VIEW 权限)。 - `categoryCode` 筛选**只认字典码**,传中文名筛不出结果。 - 备品库里存在字典外的 `category_code`(TEST 现存 1 条 `TENT`,已下架)→ `categoryName` 回传 `TENT`。 - 只读接口,整页只加载一次字典。 --- ## 四、契约约束与正确调用方式(接口类必写) > 本节只写后端返回值的契约,不写 UI 渲染建议。 | 场景 | `category` | `categoryName` | |------|-----------|----------------| | ✅ 字典码 | `safety_protection` | `安全防护` | | ✅ 存量中文 | `个人装备` | `个人装备` | | ✅ 字典外取值 | `EQUIPMENT` | `EQUIPMENT`(原值,非空、非 null) | | ✅ 空分类 | `null` | `null` | | ✅ 字典不可达 | `safety_protection` | `safety_protection`(整批回落) | ### 调用方必须注意 - **写入与筛选一律用 `category` / `categoryCode` 原值**,`categoryName` 只用于展示,不要拿它回传给任何写接口。 - **不要假设 `categoryName` 一定是中文**:字典外取值原样回传,这是刻意的(编造分类比显示英文码更有害)。 - **不要用 `categoryName` 做分组键**:同一分类可能因存量三套词表而出现 `个人装备` 与 `personal_gear` 两个不同的 `category`,却都展示成「个人装备」。分组仍应按 `category`。 --- ## 五、数据库行为 **零变更**:本次无 Flyway 迁移,不新增/修改任何表与列。`categoryName` 是读取时按数据字典翻译出来的**派生展示字段,不落库**;`order_batch_supplies.category` 只读不写,存量脏值不回填。 --- ## 六、边界行为 - 未登录 → 401(网关拦截) - 无团期查看权限 → 权限守卫拒绝 - 团期不存在 → 589500 - 字典服务降级 → `categoryName` 全批等于 `category`,接口仍 200,不阻断页面 - 老数据兼容 → `category` 为 null 的历史行 → `categoryName` 为 null,不异常 --- ## 六.5、枚举 / 数据字典 ### categoryName 的取值来源(数据字典 `supplies_category`) **所属字段**: `GroupBatchSuppliesRespVO.categoryName` / `SuppliesCandidateRespVO.categoryName` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `personal_gear` | 个人装备 | 字典项 | | `camping_equipment` | 露营设备 | 字典项 | | `riding_gear` | 骑行装备 | 字典项 | | `electronics` | 电子设备 | 字典项 | | `safety_protection` | 安全防护 | 字典项 | | `entertainment` | 娱乐器材 | 字典项 | | `warmth_gear` | 保暖装备 | 字典项 | | `vehicle_accessories` | 车载装备 | 字典项 | | `EQUIPMENT` / `PROTECTION` / `TENT` | (无) | **不在字典内的存量取值,原样回传** | ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `GroupBatchSuppliesRespVO.category` | 分类原值 | **不变** | | `GroupBatchSuppliesRespVO.categoryName` | 不存在 | 新增,三段回落的展示名,可为 null | | `SuppliesCandidateRespVO.category` | 分类码原值 | **不变** | | `SuppliesCandidateRespVO.categoryName` | 不存在 | 新增,同一套规则 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 前端显示分类中文 | 需自查字典,且线上多数行查不到 | 直接用 `categoryName` | | 字典服务不可达 | 与本字段无关 | 接口仍 200,`categoryName` 整批等于 `category` | | 字典查询次数 | — | 每次请求整页一次,不按行调用 | | 入参 / 错误码 | — | **零变化** | ## 六.7、影响评估 - **是否破坏向后兼容**: 否(纯新增出参字段,既有字段与错误码零变化) - **前端是否必须同步上线**: 否(不接入则与现状一致) - **前端 workaround 清理点**: 前端若已有「按 `category` 自查字典译中文」的逻辑,可改用 `categoryName`;但**不要顺手把字典外取值改成显示空白**,那是存量脏值,另有工单归一。 --- ## 七、不影响范围 - **仅影响**: 管理后台团期物资清单、团期物资候选弹窗两个只读接口的出参 - **零影响**: - 小程序端备品清单与其分组接口(本次未改) - 物资新增 / 改数量 / 软删 / 确认物资等写接口(取值域未收紧,手工新增仍可自由填) - 终止退款链路取备品行的内部调用(刻意不走翻译,避免在写事务内发跨服务字典调用) - 数据库(零 Flyway、存量不回填) --- ## 八、测试环境已验证 部署:`dev-v3@a18569162`(合并 PR #7920 后)部署至 TEST,`hl-order-service-v3` 双实例滚动完成。 ``` GET /v3/admin/order/group-batch/2100509904627191810/supplies → 200;category=个人装备 → categoryName=个人装备 ✓(存量中文原样) → 200;category=safety_protection → categoryName=安全防护 ✓(字典码译中文) GET /v3/admin/order/group-batch/2099463556951941122/supplies → 200;category=EQUIPMENT → categoryName=EQUIPMENT ✓(字典外原值) → 200;category=PROTECTION → categoryName=PROTECTION ✓ GET /v3/admin/order/group-batch/2099919772194836482/supplies/candidates → 200;15 项全部带 categoryName,personal_gear→个人装备、camping_equipment→露营设备 ✓ category 为空的行(临时夹具,取证后已删) → 200;category=null → categoryName=null ✓(非空串、非 "null") 字典不可达注入(临时改 sys_dict_type.dict_type 使 supplies_category 查不到,重启后冷缓存) → 列表 200;safety_protection → safety_protection ✓(整批回落原值) → 候选 200;personal_gear → personal_gear、camping_equipment → camping_equipment ✓ → 还原字典后翻译恢复 ✓ ``` 验证团期: `2100509904627191810`(三段全覆盖)、`2099463556951941122`(字典外取值)、`2099919772194836482`(候选列表) --- ## 十、相关文档 - 关联 Issue: [wx/HL#7916](https://git.1814.love:8443/wx/HL/issues/7916) - 关联 PR: [wx/HL#7920](https://git.1814.love:8443/wx/HL/pulls/7920) - 同类先例(子订单 `roomTypeName`): 见 `changelogs-v2/2026-09/14_7536_*` ## 关联 / 联系人 ### 链接 - **Issue**: [#7916](https://git.1814.love:8443/wx/HL/issues/7916) - **PR**: [#7920](https://git.1814.love:8443/wx/HL/pulls/7920) - **Merge commit**: [a18569162](https://git.1814.love:8443/wx/HL/commit/a18569162cb97e54b63f6b442e75292db4ad88d7) ### 联系人 - **后端负责人**: @jw