文件
hl-api-changelog/changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md
T
Mimingguang 2f6a989fcf
changelog-filename-gate / validate (push) Failing after 2s
chore(changelog): 补齐 17 条消费闭环 frontmatter 回写
11 条有业务交付改判 verified(#6397/6903/6904/6905/6950/6979/6986/7013/7029/7036/7066,owner=mmg+对应业务 commit ref+交付日 verified_at);
6 条实证零改动改判 not_required(#6014/6016/6140/6938/6842/7087,仅翻 frontend_status 不填 owner/ref)。
#5935 挂起待后端补字段,保持 pending 不动。sync-log 均已记账。
2026-09-06 10:43:20 +08:00

375 行
14 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "6986"
title: "团期物资支持从备品库选择配置"
consumer: "admin"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "4d82bdb5"
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`)