From 828d9ae2e44b81a9cc6657c558679eaeeed81c8e Mon Sep 17 00:00:00 2001 From: jw Date: Thu, 3 Sep 2026 15:22:10 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#6986=20=E5=9B=A2=E6=9C=9F?= =?UTF-8?q?=E7=89=A9=E8=B5=84=E4=BB=8E=E5=A4=87=E5=93=81=E5=BA=93=E9=80=89?= =?UTF-8?q?=E6=8B=A9=20+=20suppliesName=20=E4=BA=8C=E9=80=89=E4=B8=80?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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) --- ..._团期物资从备品库选择-新增接口-管理后台.md | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md b/changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md new file mode 100644 index 00000000..ca38215b --- /dev/null +++ b/changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md @@ -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>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `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`)