docs(order-v3): #8150 changelog 按接口类模板骨架补齐入参/响应示例/业务边界
changelog-filename-gate / validate (push) Failing after 2s

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-22 19:30:44 +08:00
共同撰写人 Claude Opus 5
父节点 e2828a3e67
当前提交 61bfe0f9a3
@@ -8,7 +8,10 @@ change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
target_release: "v2.1"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "backend_status=deployed:测试服 hl-order-service-v3 双实例(8086 / 8186)当前运行 sha=843de6401。本单修复提交 bfc10966e 不是该 sha 本身而是它的祖先,故用两条互相独立的判据:①git merge-base --is-ancestor bfc10966e 843de6401 → 退出码 0(是祖先),且 deploy-status.sh 登记 dev-v3 / 843de6401 / BEHIND=0/N;②jar mtime 2026-09-22 18:55:47,晚于 bfc10966e 的提交时刻 2026-09-22 15:24:50 +0800。两条之外还有第三条活体判据:本单契约在 18:5x 经网关实测四组请求取证(见第八节),其中三组的返回值在改动前不可能出现。gateway_status=not_required:本单零网关改动。PUT /v3/admin/group-batch/{productBatchId}/staff 是存量端点、路径未变,命中既有通配路由 Path=/v3/admin/**,未引入任何新路径段,也未改 hl-gateway 的路由或 Nacos 配置。frontend_status=not_required:本单收紧的是一个前端目前不会构造的取值。判据——对 mmg/hl-ui origin/v2.1 的调用方 grep,scopeRoles 一律由 roleMeta.memberRoles 这类字面量常量数组提供(GroupBatchStaffConfigModal.vue:153-154 写死 ['GUIDE','LEADER'] 与 ['PHOTOGRAPHER']),无任何一处会产出 null / 空串 / 纯空白元素;对 [\"\"] 与 [\" \"] 本单只改 message 文案长度、不改 code。⚠️ 覆盖边界:这是「现有实现不会命中」,不是「将来也不会」——若前端改为由下拉或接口回填拼 scopeRoles,未选中的空值会直接命中本单的 400,请按第四节的契约处理。"
updated_at: "2026-09-22"
base: "dev-v3"
@@ -79,9 +82,9 @@ base: "dev-v3"
## 二、变更接口清单
| 方法 | 路径 | 变更类型 | 本单改了什么 |
|---|---|---|---|
| `PUT` | `/v3/admin/group-batch/{productBatchId}/staff` | 入参校验收紧 | `scopeRoles` 增加元素级 `@NotBlank`;`code`/响应结构/业务语义均未变 |
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 保存团期 staff 配置(含扇出) | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改接口 | `scopeRoles` 增加元素级 `@NotBlank`(入参校验收紧);`code`、响应结构、业务语义均未变 |
**未新增、未删除、未改名任何端点。**
@@ -91,32 +94,27 @@ base: "dev-v3"
### 1. 保存团期 staff 配置(含扇出) `PUT /v3/admin/group-batch/{productBatchId}/staff`
**请求 VO**:`BatchStaffConfigReqVO`
**响应 VO**:`Result<BatchStaffConfigRespVO>`
**权限**:`GroupBatchPermissionGuard.PERMISSION_MANAGE`
**VO**: `BatchStaffConfigReqVO` → `Result<BatchStaffConfigRespVO>`
**权限**: `GroupBatchPermissionGuard.PERMISSION_MANAGE`(`hl-gateway` 侧命中既有通配路由 `Path=/v3/admin/**`)
#### 使用场景
团期详情页「配导游 / 配摄影」弹窗保存。传入列表即为**覆盖范围内**的最终状态,保存成功后异步扇出到团内所有活跃订单的 `order_staff_assignment`(`source=GROUP_BATCH`)。
#### 路径参数
#### 入参
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `productBatchId` | Long | 是 | **产品侧排期 ID**(`group_tour_batch.batch_id`),不是运营团期主键 `order_group_batch.group_batch_id`。两者 1:1 但值不同,传错不会报错、只会走不到团期 |
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `productBatchId` | path | Long | 是 | — | **产品侧排期 ID**(`group_tour_batch.batch_id`),不是运营团期主键 `order_group_batch.group_batch_id`。两者 1:1 但值不同,传错不会报错、只会走不到团期 |
| `scopeRoles` | body | `List<String>` | 否 | 数组 `@Size(min = 1)`;🆕 元素 `@NotBlank` + 元素 `@Pattern` | 本次保存覆盖的角色范围。**不传 = 整期全量覆盖**(历史行为);传了则只在这些角色内覆盖。取值域见第六.5 节。**数组不能是空数组**(清空语义只由 `staffList` 表达);🆕 **元素不能是 `null` / 空串 / 纯空白**——本单唯一的行为变化就在这里 |
| `staffList` | body | `List<Item>` | **是** | `@NotNull` + `@Valid` | 覆盖范围内的最终状态。**显式传 `[]` 即清空该范围**。缺字段或字段名拼错一律 400(审计 F-03,Refs #6950 / #7377)。本单未改动 |
| `staffList[].staffId` | body | Long | 是 | `@NotNull` | 用户域员工 ID。本单未改动 |
| `staffList[].staffRole` | body | String | 是 | `@NotBlank` + `@Pattern` | 员工角色,取值域见第六.5 节。本单未改动 |
| `staffList[].sortOrder` | body | Integer | 否 | — | 展示排序,默认 0。本单未改动 |
| `staffList[].remark` | body | String | 否 | `@Size(max = 500)` | 备注,≤500 字。本单未改动 |
#### 请求参数
| 字段 | 类型 | 必填 | 本单是否改动 | 说明 |
|---|---|---|---|---|
| `scopeRoles` | `List<String>` | 否 | **是(元素级校验)** | 本次保存覆盖的角色范围。**不传 = 整期全量覆盖**(历史行为);传了则只在这些角色内覆盖。取值域见第六.5 节。**数组不能为空数组**(`@Size(min = 1)`,清空语义只由 `staffList` 表达);🆕 **元素不能是 `null` / 空串 / 纯空白** |
| `staffList` | `List<Item>` | **是** | 否 | 覆盖范围内的最终状态。**显式传 `[]` 即清空该范围**。缺字段或字段名拼错一律 400(审计 F-03,Refs #6950 / #7377) |
| `staffList[].staffId` | Long | 是 | 否 | 用户域员工 ID |
| `staffList[].staffRole` | String | 是 | 否 | 员工角色,取值域见第六.5 节 |
| `staffList[].sortOrder` | Integer | 否 | 否 | 展示排序,默认 0 |
| `staffList[].remark` | String | 否 | 否 | 备注,≤500 字 |
#### 响应参数(本单未改动,列出供对照)
#### 出参(本单未改动,列出供对照)
| 字段 | 类型 | 说明 |
|---|---|---|
@@ -150,8 +148,61 @@ PUT /v3/admin/group-batch/80001/staff
}
```
#### 响应示例
保存成功(`code` 为 `200`,本单未改变响应结构):
```json
{
"code": 200,
"message": "操作成功",
"data": {
"productBatchId": "80001",
"groupBatchId": "90211",
"staffList": [
{
"id": 770001,
"staffId": 40001,
"staffRole": "LEADER",
"staffRoleName": "领队",
"staffName": "刘领队",
"staffPhone": "138****6677",
"avatarUrl": null,
"sortOrder": 0,
"remark": "首席领队",
"reporterRank": "NONE",
"reporterRankName": "非报账人"
}
],
"affectedOrderCount": 3
},
"traceId": null,
"success": true
}
```
🔎 本示例按 `BatchStaffConfigRespVO` 的字段逐个列出,用于说明结构;本单未改动响应结构中的任何一个字段。成功路径的活体读数不在本单的取证范围内(本单取证用的是不存在的团期,见第八节),需要成功响应的真实样本请见第十节 #8006 的交接件。
#### 空数据 / 降级响应
本单不涉及。`staffList: []` 的「清空该范围」语义未变,该情形下 `data.staffList` 返回空数组(不是 `null`),`affectedOrderCount` 照常返回扇出订单数。
#### 错误响应
🆕 `scopeRoles` 元素为 `null`(**本单新增的拒绝**):
```json
{"code":400,"message":"scopeRoles 的元素不能为空","data":null,"traceId":null,"success":false}
```
`scopeRoles` 元素为空串 / 纯空白(改前已是 400,本单只让 `message` 多带一条):
```json
{"code":400,"message":"scopeRoles 取值不在员工角色取值域内; scopeRoles 的元素不能为空","data":null,"traceId":null,"success":false}
```
全部错误码:
| code | 触发条件 | 本单是否改动 |
|---|---|---|
| `400` | `scopeRoles` 元素为 `null` | 🆕 **本单新增**(改前是 200 静默空操作) |
@@ -165,9 +216,14 @@ PUT /v3/admin/group-batch/80001/staff
🔴 **HTTP 状态行恒为 200,`body.code` 才是真相**。本服务的业务失败与入参校验一律返 HTTP 200(`GlobalExceptionHandler` 带 `@ResponseStatus(HttpStatus.OK)`),成功码是 `200` 不是 `0`。判成败请读 `body.code`,不要读 HTTP 状态码。
#### 空数据降级
#### 业务边界
本单不涉及。`staffList: []` 的"清空该范围"语义未变。
- **本单唯一的行为变化在 `scopeRoles` 的元素上**:`[null]` 由「HTTP 200 + 静默空操作」改为 `code: 400`。`scopeRoles` 不传、传合法值、以及 `staffList` 侧的一切语义都与改前逐字相同。
- **`scopeRoles` 不传 = 整期全量覆盖**,传了 = 只在这些角色内覆盖;`[]` 一直被拒(`@Size(min = 1)`),清空语义只由 `staffList: []` 表达。这三条改前改后一致。
- **`[null]` 改前不报错,但它做的事情是「什么都没做」**:`scopeRoles` 含 `null` 时下游范围计算得到空范围,接口返 200、`affectedOrderCount` 为 0、库里零写入。调用方若据 200 判定「已保存」,实际保存从未发生 —— 这正是本单要关掉的静默失败。
- **`[""]` / `[" "]` 改前已是 400**,本单只让 `message` 多带一条。**不要对这两个值的 `message` 写全等比较**:它是 `@Pattern` 与 `@NotBlank` 两条 violation 由 `GlobalExceptionHandler` 以 `"; "` 拼接而成,且 **拼接顺序不属于契约**(Bean Validation 不保证多个约束的执行顺序)。要判就判 `code == 400`,或判 `message` **包含**你关心的那个子串。
- **校验发生在 Spring `@Valid` 绑定层,在进 Service 之前**:因此 `productBatchId` 是否真实存在不影响这三种 400 的出现。反过来,拿到 `589552` / `589553` 说明请求已经穿过绑定层、是团期状态问题,不是参数格式问题。
- **HTTP 状态行恒为 200**,成功码是 `200` 不是 `0`,判成败一律读 `body.code`。
---