docs(changelog): #7916 团期物资列表与候选补分类中文名 categoryName(修改接口)
changelog-filename-gate / validate (push) Failing after 2s

这个提交包含在:
jw
2026-09-18 11:02:39 +08:00
父节点 4cdf3606e9
当前提交 4c8f93f978
@@ -0,0 +1,370 @@
---
schema: "hl-changelog/v2"
ticket: "7916"
title: "团期物资列表与候选列表出参新增分类中文名 categoryName(字典外取值按原值显示)"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
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。"
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<List<GroupBatchSuppliesRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| 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<List<SuppliesCandidateRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| 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