docs(changelog): #6986 团期物资从备品库选择 + suppliesName 二选一校验
changelog-filename-gate / validate (push) Successful in 2s
changelog-filename-gate / validate (push) Successful in 2s
新增 GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates, 从资源域备品库拉候选并标记本团期已加入项;资源域新增内部接口 GET /internal/supplies/list-available,两个服务需同时部署。 记录本次行为变更:POST .../supplies 的 suppliesName 由 @NotBlank 改为与 suppliesResourceId 二选一,传 resourceId 时名称从库取并覆盖入参。 新增错误码 589518 备品库查询失败、589519 所选备品不存在或已下架。 2026-09-03 于测试环境网关实测 10 条正负向用例全部通过, 写入用例已删除还原;backend_status=deployed / gateway_status=verified。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,374 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "6986"
|
||||||
|
title: "团期物资支持从备品库选择配置"
|
||||||
|
consumer: "admin"
|
||||||
|
author: "jw(GIT)"
|
||||||
|
change_type: "新增接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "pending"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-09-03"
|
||||||
|
status_note: "PR #6987 已合入 dev-v3;2026-09-03 经测试环境网关实测,10 条正负向用例全部通过。前端尚未接入"
|
||||||
|
updated_at: "2026-09-03"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 团期物资: 新增备品库候选列表接口,并放宽 suppliesName 必填约束
|
||||||
|
|
||||||
|
> **服务**: hl-order-service-v3 + hl-resource-service(**跨两个服务**)
|
||||||
|
> **PR**: #6987
|
||||||
|
> **Issue**: #6986
|
||||||
|
> **影响范围**: 管理后台「团期详情 → 物资清单 → 从备品库选择」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 关键变化
|
||||||
|
|
||||||
|
**新增物资行 `POST /v3/admin/order/group-batch/:groupBatchId/supplies` 的 `suppliesName` 不再无条件必填。**
|
||||||
|
|
||||||
|
- 前端以前以为:`suppliesName` 是 `@NotBlank`,任何情况下都得填。
|
||||||
|
- 实际现在是:改为 `@AssertTrue` **二选一**——传了 `suppliesResourceId` 就可以不填名称
|
||||||
|
(名称、分类、计费方式从备品库取,**入参传了也会被库值覆盖**);不传 `suppliesResourceId` 时名称仍必填。
|
||||||
|
- 这解决的是原先「从备品库选一条加进来,却仍被迫手填名字」与「名称以库为准」自相矛盾的问题。
|
||||||
|
|
||||||
|
**部署要求**:本次跨 order-v3 与 resource-service 两个服务,**必须同时部署**。
|
||||||
|
只发 order-v3 会调不到资源域新增的 `/internal/supplies/list-available`,候选列表直接报 589518。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
物资清单原先只能手工逐条录入,名称、分类、计费方式、单价全靠人填,既慢又容易与备品库口径不一致。
|
||||||
|
本次接入资源域备品库:团期管理员从库里勾选,基础信息由库带出。
|
||||||
|
|
||||||
|
**资源域改动**:新增 `GET /internal/supplies/list-available`(内部接口,不对外)。
|
||||||
|
order-v3 侧通过 Feign 调用,并带降级工厂。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、变更接口清单
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|------|------|------|----------|------|
|
||||||
|
| 1 | 团期物资候选列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/supplies/candidates` | **新增接口** | 从备品库拉可选备品,标记本团期已加入项 |
|
||||||
|
| 2 | 新增团期物资行 | POST | `/v3/admin/order/group-batch/:groupBatchId/supplies` | **请求体校验放宽** | `suppliesName` 由必填改为与 `suppliesResourceId` 二选一 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、接口详情
|
||||||
|
|
||||||
|
### 1. 团期物资候选列表 `GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates`
|
||||||
|
|
||||||
|
**VO**: `SuppliesCandidateRespVO`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
「物资清单 → 从备品库选择」弹窗打开时调用,渲染可选备品列表。
|
||||||
|
`added=true` 的项应显示为已勾选,其 `batchSuppliesId` 即清单中对应行的 ID,可直接用于删除。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|------|------|------|------|------|------|
|
||||||
|
| `groupBatchId` | Path | Long | ✅ | — | 团期主订单 ID |
|
||||||
|
| `categoryCode` | Query | String | ❌ | 字典 `supplies_category` | 分类筛选。传不存在的分类返回空数组,不报错 |
|
||||||
|
| `keyword` | Query | String | ❌ | — | 关键字,匹配备品**名称或副标题**。中文需 URL 编码 |
|
||||||
|
|
||||||
|
#### 出参 `Result<List<SuppliesCandidateRespVO>>`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `suppliesResourceId` | Long | 备品资源 ID。**加入清单时回传本值** |
|
||||||
|
| `suppliesName` | String | 备品名称(来自库) |
|
||||||
|
| `subtitle` | String | 副标题 |
|
||||||
|
| `category` | String | 分类编码 |
|
||||||
|
| `billingType` | String | 计费方式,**已归一为产品域词表** `PER_PERSON` / `PER_QUANTITY` |
|
||||||
|
| `hasCost` | Boolean | 是否计费(资源域 `isCharged=1` 时为 true) |
|
||||||
|
| `unitPrice` | BigDecimal | 基础价,加入清单时作为 `unitPrice` 默认值 |
|
||||||
|
| `unit` | String | 计量单位 |
|
||||||
|
| `added` | Boolean | 是否已加入本团期清单,**true 时前端应显示为已勾选** |
|
||||||
|
| `batchSuppliesId` | Long | 已加入时对应的清单行 ID;未加入为 `null` |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/group-batch/1/supplies/candidates?categoryCode=personal_gear
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"suppliesResourceId": "2023438566624866305",
|
||||||
|
"suppliesName": "定制遮阳帽",
|
||||||
|
"subtitle": "呼籁旅行专属防晒鸭舌帽",
|
||||||
|
"category": "personal_gear",
|
||||||
|
"billingType": "PER_PERSON",
|
||||||
|
"hasCost": true,
|
||||||
|
"unitPrice": 15.00,
|
||||||
|
"unit": "顶",
|
||||||
|
"added": false,
|
||||||
|
"batchSuppliesId": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
备品库无匹配项(含分类不存在、关键字无命中)时返回空数组,不报错:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"success": true,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
资源域不可达时**不静默降级为空**,而是显式报 589518,避免前端误判为「库里没有备品」。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
备品库查询失败(资源域未部署或不可达):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 589518,
|
||||||
|
"message": "备品库查询失败,请稍后重试",
|
||||||
|
"success": false,
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- 只读接口,**不产生任何写入**,可安全重复调用
|
||||||
|
- 资源域只返回可用备品,已下架的不在候选池
|
||||||
|
- `billingType` 在 order-v3 侧做过归一,前端**不必**再兼容资源域原始词表
|
||||||
|
- `added` 依据 `suppliesResourceId` 匹配;纯手填(无 resourceId)的清单行不会影响任何候选项的勾选态
|
||||||
|
- 同一备品在清单中重复存在时,`batchSuppliesId` 取首条
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 新增团期物资行 `POST /v3/admin/order/group-batch/:groupBatchId/supplies`
|
||||||
|
|
||||||
|
**VO**: `AddSuppliesReqVO`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
两种来源共用本接口:从备品库勾选加入(传 `suppliesResourceId`),或手工录入临时物资(传 `suppliesName`)。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|------|------|------|------|------|------|
|
||||||
|
| `groupBatchId` | Path | Long | ✅ | — | 团期主订单 ID |
|
||||||
|
| `suppliesResourceId` | Body | Long | ❌ | 与 `suppliesName` **二选一** | 备品资源 ID。传了则名称/分类/计费方式从库取 |
|
||||||
|
| `suppliesName` | Body | String | ❌ | 与 `suppliesResourceId` **二选一** | **变更前** `@NotBlank` 无条件必填;**变更后**传了 resourceId 时可空,且**传了也会被库值覆盖** |
|
||||||
|
| `quantity` | Body | Integer | ✅ | `@NotNull` `@Min(1)` | 数量 |
|
||||||
|
| `category` | Body | String | ❌ | — | 分类,传 resourceId 时以库为准 |
|
||||||
|
| `hasCost` | Body | Boolean | ❌ | — | 是否计费 |
|
||||||
|
| `billingType` | Body | String | ❌ | — | 计费方式 |
|
||||||
|
| `unitPrice` | Body | BigDecimal | ❌ | — | 单价,缺省取库基础价 |
|
||||||
|
| `sortOrder` | Body | Integer | ❌ | — | 排序 |
|
||||||
|
| `remark` | Body | String | ❌ | — | 备注 |
|
||||||
|
|
||||||
|
#### 出参 `Result<Long>`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `data` | Long | 新建清单行 ID(`batchSuppliesId`),可直接用于删除或调整数量 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"suppliesResourceId": 2023438566624866305,
|
||||||
|
"quantity": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"success": true,
|
||||||
|
"data": "2095411540751519745"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
本接口必然返回行 ID 或错误,无空数据形态。校验失败时 `data` 为 `null` 且 `success=false`。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
名称与备品库 ID 都没传(**本次新增的二选一校验**):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "备品名称不能为空(未指定备品库 ID 时必填)",
|
||||||
|
"success": false,
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
所选备品不存在或已下架:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 589519,
|
||||||
|
"message": "所选备品不存在或已下架,请重新选择",
|
||||||
|
"success": false,
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- 二选一校验在 `@AssertTrue` 阶段完成,**失败时数据库无任何变更**
|
||||||
|
- 传了 `suppliesResourceId` 时,入参里的 `suppliesName` / `category` / `billingType` **会被库值覆盖**,不是「以入参优先」
|
||||||
|
- `quantity` 最小为 1,传 0 或负数返回 400
|
||||||
|
- 同一备品可重复加入,接口不做去重
|
||||||
|
- 纯手填行(无 `suppliesResourceId`)不会出现在候选列表的 `added` 判定中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、契约约束与正确调用方式
|
||||||
|
|
||||||
|
- **加入备品库物资时,只需传 `suppliesResourceId` 和 `quantity`**。不要再从候选列表把名称回填进请求体——
|
||||||
|
传了也会被库值覆盖,徒增不一致风险。
|
||||||
|
- **手工录入临时物资时,`suppliesName` 仍然必填**。二选一不等于两个都可以不传。
|
||||||
|
- **勾选态用 `added` 判断,删除用 `batchSuppliesId`**。候选列表已经把行 ID 回填好,无需再查一次清单。
|
||||||
|
- **`keyword` 含中文必须 URL 编码**,否则查询条件丢失(实测未编码时请求异常)。
|
||||||
|
- **589518 与「空数组」含义不同**:前者是资源域挂了,后者是库里确实没有匹配项。前端不应把 589518 展示成「暂无备品」。
|
||||||
|
- 589518 / 589519 是业务码,HTTP 状态仍为 200,判断成败要读响应体的 `code`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、数据库行为
|
||||||
|
|
||||||
|
仅描述外部可观察行为:
|
||||||
|
|
||||||
|
- 接口 1(GET candidates)**只读,不产生任何写入**。
|
||||||
|
- 接口 2(POST supplies)新增一条团期物资清单行并返回其 ID;传 `suppliesResourceId` 时,
|
||||||
|
名称、分类、计费方式、单价默认值取自备品库快照,落库后不再随备品库变动。
|
||||||
|
- 二选一校验与数量校验均发生在写入之前,**校验失败时数据库无任何变更**(已实测复核)。
|
||||||
|
- 本次变更**不涉及表结构调整**,也不对存量物资行做迁移或回填。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、边界行为
|
||||||
|
|
||||||
|
- 资源域未部署 / 不可达 → 589518,**不降级为空数组**
|
||||||
|
- 备品库无匹配 → 返回 `[]`,不报错
|
||||||
|
- `categoryCode` 传不存在的分类 → 返回 `[]`
|
||||||
|
- 备品库 ID 不存在或已下架 → 589519
|
||||||
|
- 既无 `suppliesName` 又无 `suppliesResourceId` → 400
|
||||||
|
- `quantity` 为 0、负数或缺失 → 400
|
||||||
|
- 未登录 → 401(网关拦截)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六.5、枚举 / 数据字典
|
||||||
|
|
||||||
|
### `billingType`(已归一为产品域词表)
|
||||||
|
|
||||||
|
| 取值 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `PER_PERSON` | 按人计费 |
|
||||||
|
| `PER_QUANTITY` | 按数量计费 |
|
||||||
|
|
||||||
|
> order-v3 侧已对资源域原始词表做过归一,前端直接消费这两个值即可。
|
||||||
|
|
||||||
|
### `category`(字典 `supplies_category`)
|
||||||
|
|
||||||
|
分类编码由字典维护,如 `personal_gear`(个人装备)、`camping_equipment`(露营装备)等。
|
||||||
|
本接口不校验分类是否存在,传未知值返回空数组。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六.6、错误码
|
||||||
|
|
||||||
|
段位 `589500-589599`,owner `hl-order-service-v3`(`GroupBatchErrorCode`)。
|
||||||
|
|
||||||
|
| 码 | 符号 | 消息 | 触发 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `589518` | `SUPPLIES_LIBRARY_FETCH_FAILED` | 备品库查询失败,请稍后重试 | **本次新增**。资源域 `list-available` 返回失败或空结果对象 |
|
||||||
|
| `589519` | `SUPPLIES_RESOURCE_NOT_AVAILABLE` | 所选备品不存在或已下架,请重新选择 | **本次新增**。`suppliesResourceId` 在库中查不到 |
|
||||||
|
| `400` | — | 备品名称不能为空(未指定备品库 ID 时必填) | **本次新增**。二选一校验未通过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、不影响范围
|
||||||
|
|
||||||
|
- **仅影响**:管理后台团期详情的「物资清单」模块
|
||||||
|
- **零影响**:
|
||||||
|
- 已有的物资清单查询、调整数量、删除行、确认物料四个接口的出参
|
||||||
|
- 存量团期物资行数据(不迁移,新校验只在下次新增时触发)
|
||||||
|
- 资源域备品库自身的管理接口(本次只新增一个内部只读端点)
|
||||||
|
- 团期状态机与物料确认闸门
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、测试环境已验证
|
||||||
|
|
||||||
|
✅ **已验证。** 2026-09-03 于测试环境网关实测,真实鉴权(管理端 admin 账号)。
|
||||||
|
|
||||||
|
- 网关:`https://api.test.1814.love:9443`,分支 `dev-v3`
|
||||||
|
- 前置确认:order-v3 与 resource-service **两侧均已部署**(候选列表能返回资源域真实数据即为证)
|
||||||
|
|
||||||
|
| # | 用例 | 期望 | 实测 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | GET candidates 无筛选 | 200,返回候选 | ✅ 200,15 条 |
|
||||||
|
| 2 | `?categoryCode=personal_gear` | 200,按分类收窄 | ✅ 200,5 条 |
|
||||||
|
| 3 | `?keyword=帽`(URL 编码) | 200,按名称匹配 | ✅ 200,1 条「定制遮阳帽」 |
|
||||||
|
| 4 | `?categoryCode` 传不存在分类 | 200 空数组 | ✅ 200,0 条 |
|
||||||
|
| 5 | GET candidates 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` |
|
||||||
|
| 6 | POST 既无名称也无 resourceId | 400 | ✅ `400 备品名称不能为空(未指定备品库 ID 时必填)` |
|
||||||
|
| 7 | POST `quantity=0` | 400 | ✅ `400 数量至少为 1` |
|
||||||
|
| 8 | POST 缺 `quantity` | 400 | ✅ `400 数量不能为空` |
|
||||||
|
| 9 | POST `suppliesResourceId` 不存在 | 589519 | ✅ `589519 所选备品不存在或已下架,请重新选择` |
|
||||||
|
| 10 | POST 只传 resourceId 不传名称 | 放行,名称取库值 | ✅ 200,落库 `suppliesName="定制遮阳帽"` 与库一致,`unitPrice=15.00`、`billingType=PER_PERSON` |
|
||||||
|
|
||||||
|
**行为变更核对**:用例 10 是本次核心——入参未传 `suppliesName` 仍成功建行,名称由备品库带出。
|
||||||
|
|
||||||
|
**联动核对**:加入后重查候选列表,该项 `added=true`、`batchSuppliesId` 回填为新建行 ID;删除后回到 `added=false`。
|
||||||
|
|
||||||
|
**未落库核对**:用例 6–9 执行后复查清单仍为 0 条,证明校验失败不产生写入。
|
||||||
|
|
||||||
|
**测试数据还原**:用例 10 建的行已 `DELETE` 删除,清单由测试前 0 条恢复为 0 条。
|
||||||
|
|
||||||
|
本地单元测试(提交信息记载):`GroupBatchSuppliesServiceTest` 新增 208 行用例。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、相关文档
|
||||||
|
|
||||||
|
- 团期需求文档:`docs/group/`(dev-v3 分支)
|
||||||
|
- 实施单 AC-TD-13(`ad747212d` 已标记 #6986 落地并勘误)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联 / 联系人
|
||||||
|
|
||||||
|
- **Issue**: #6986
|
||||||
|
- **PR**: #6987(合并提交 `bcf9332bd`,落入 `dev-v3`)
|
||||||
|
- **后续修正**: `8806315e3`(备品去重,同时关联 #6950)
|
||||||
|
- **服务**: hl-order-service-v3 + hl-resource-service
|
||||||
|
- **后端**: jw
|
||||||
|
- **前端**: 待认领(`frontend_status: pending`)
|
||||||
在新工单中引用
屏蔽一个用户