--- schema: "hl-changelog/v2" ticket: "7377" title: "团期人员配置 staffList 改必填 + 备品候选关键字转义 + 加备品行改定点查" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "2026-09-09" status_note: "not_required 2026-09-09 mmg:三端点行为变更,前端零改动。①保存人员 saveGroupBatchStaff(orderV2GroupBatch.js:186) 请求体恒为 {staffList},省略字段路径不存在,清空即传 [](GroupBatchStaffConfigModal handleSave 过滤后数组恒在)。②备品候选 keyword 由 SuppliesPickerModal 原样透传(仅 trim),无前端通配依赖。③加备品行 addGroupBatchSupplies 只传 {suppliesResourceId,quantity},500 上限解除对调用方透明,589518 已正确区分库挂/无备品。" updated_at: "2026-09-09" base: "dev-v3" --- # 团期人员配置必填与备品库口径收口 ## 一、给前端的一句话 两处**行为变更**要注意:保存团期人员配置**必须带 `staffList` 字段**(要清空就传 `[]`,别省略);备品候选列表搜 `%` / `_` 不再返回全库。另有一处纯修复不影响调用方——加备品行不再受「备品库超 500 条」的隐性上限。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/:productBatchId/staff` | 修改接口 | `staffList` 由可省略改为**必填**,缺失返 400;显式传 `[]` 仍是清空 | | 2 | 团期物资候选列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/supplies/candidates` | 修改接口 | `keyword` 转义 LIKE 通配符,`%` / `_` 不再当通配符 | | 3 | 团期物资新增行 | POST | `/v3/admin/order/group-batch/:groupBatchId/supplies` | 修改接口 | 取备品库快照改走定点查,解除「备品库超 500 条后加不进」的隐性上限 | ## 三、接口详情 ### 1. 保存团期人员配置 `PUT /v3/admin/group-batch/:productBatchId/staff` **VO**: `BatchStaffConfigReqVO / BatchStaffConfigRespVO` #### 使用场景 团期详情的「导游 / 摄影」配置,全量覆盖式保存。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `productBatchId` | Path | String | 是 | 正整数 | 产品侧班期 ID | | `staffList` | Body | Array | **是(本次变更)** | 可为空数组 | 全量覆盖列表;**显式传 `[]` 即清空**,字段缺失一律 400 | | `staffList[].staffId` | Body | String | 是 | 正整数 | 资源域人员 ID | | `staffList[].staffRole` | Body | String | 是 | `LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER`,区分大小写 | 越界值返 400;与该人在资源域的实际类型不符返 582114 | | `staffList[].sortOrder` | Body | Integer | 否 | 默认 0 | 展示排序 | | `staffList[].remark` | Body | String | 否 | 最长 500 | 备注 | #### 出参 | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data.staffList` | Array | 保存后的配置快照,含 `staffId` / `staffName` / `staffPhone` / `staffRole` 等 | | `data.affectedOrderCount` | Integer | 已触发异步扇出的活跃订单数,仅作提示 | #### 请求示例 ```json { "staffList": [ { "staffId": "1002", "staffRole": "GUIDE", "sortOrder": 0 } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "staffList": [ { "staffId": "1002", "staffName": "李雪梅", "staffPhone": "138****1002", "staffRole": "GUIDE", "sortOrder": 0 } ], "affectedOrderCount": 2 } } ``` #### 空数据 / 降级响应 显式传 `[]` 是合法请求:清空该团期全部人员配置并返回空 `staffList`,`code` 仍为 200。 #### 错误响应 ```json { "code": 400, "message": "staff 配置列表不能缺失;确要清空请显式传空数组 []", "data": null, "success": false } ``` #### 业务边界 - **字段缺失或字段名写错一律 400,且现有配置零变动、不触发扇出**——这正是本次要堵的洞。 - 团期未创建返 589553、未成团返 589552;人员角色与资源域类型不符返 582114,整批拒绝不落库。 - 「显式传 `[]` 即清空」这条既有语义**没有改变**。 ### 2. 团期物资候选列表 `GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates` **VO**: `SuppliesCandidateRespVO` #### 使用场景 团期物资清单的「从备品库选择」弹窗,支持按分类与关键字筛选。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `groupBatchId` | Path | String | 是 | 正整数 | 团期主订单 ID | | `categoryCode` | Query | String | 否 | 字典 `supplies_category` | 分类筛选 | | `keyword` | Query | String | 否 | 无长度限制 | 匹配名称或副标题;**本次起 LIKE 通配符被转义** | #### 出参 | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data[].suppliesResourceId` | String | 备品资源 ID | | `data[].suppliesName` / `subtitle` / `category` | String | 备品名称、副标题、分类 | | `data[].billingType` / `hasCost` / `basePrice` | String / Boolean / String | 计费方式、是否收费、库价 | | `data[].added` / `batchSuppliesId` | Boolean / String | 是否已加入本团期清单及对应行 ID | #### 请求示例 ```http GET /v3/admin/order/group-batch/2097500233511362561/supplies/candidates?keyword=%E5%B8%BD ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": [ { "suppliesResourceId": "2023438566624866305", "suppliesName": "定制遮阳帽", "subtitle": "呼籁旅行专属防晒鸭舌帽", "category": "personal_gear", "billingType": "PER_PERSON", "hasCost": true, "basePrice": "15.00", "added": false, "batchSuppliesId": null } ] } ``` #### 空数据 / 降级响应 无命中返回 `data: []`,`code` 仍为 200。 #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null, "success": false } ``` #### 业务边界 - `keyword` 里的 `%` / `_` / `\` / `!` 一律按**字面字符**匹配,不再当通配符。 - 普通关键字的搜索结果**不受影响**(实测「帽」仍命中「定制遮阳帽」)。 - 只返上架备品,排序与分页口径未变。 ### 3. 团期物资新增行 `POST /v3/admin/order/group-batch/:groupBatchId/supplies` **VO**: `AddSuppliesReqVO` #### 使用场景 团期物资清单新增一行,可从备品库选择(传 `suppliesResourceId`)或纯手填(传 `suppliesName`)。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `groupBatchId` | Path | String | 是 | 正整数 | 团期主订单 ID | | `suppliesResourceId` | Body | String | 与 `suppliesName` 二选一 | 正整数 | 备品库资源 ID;传了则名称、分类、计费方式以库为准覆盖入参 | | `suppliesName` | Body | String | 与 `suppliesResourceId` 二选一 | 最长 100 | 纯手填备品名 | | `quantity` | Body | Integer | 是 | 大于 0 | 数量 | | `unitPrice` | Body | String | 否 | 金额字符串 | 单价,**入参优先**;不传取库价 | #### 出参 | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data` | String | 新建行主键 `batchSuppliesId` | #### 请求示例 ```json { "suppliesResourceId": "2023438566624866305", "quantity": 1 } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": "2097600714757783553" } ``` #### 空数据 / 降级响应 无空成功结果;任一校验失败返回 `data: null`、`success: false`。 #### 错误响应 ```json { "code": 589523, "message": "该备品已在本团期物资清单中,请直接调整数量", "data": null, "success": false } ``` #### 业务边界 - 备品库不可用(资源域故障)与备品本身不可用(不存在 / 已下架)**分开报**:前者 589522,后者 589519。 - 同一 `suppliesResourceId` 重复加入返 589523;纯手填行不参与判重。 - 团期不在「物料准备中」返 589520,团期不存在返 589500。 ## 四、契约约束与正确调用方式 - 保存人员配置**永远显式带上 `staffList`**:要清空传 `[]`,不要省略字段。省略在改前会静音清空整团配置,现在会 400。 - 备品搜索框不需要前端做任何转义,直接把用户输入原样传 `keyword` 即可。 - 加备品行时 `unitPrice` 不传即取库价;传了以入参为准(同团期可议价不同)。 ## 五、数据库行为 - 人员配置:全量覆盖式保存——软删旧行 + 插入新行,随后异步扇出到该班期全部活跃订单的 `order_staff_assignment`。校验失败时**一行都不写**。 - 物资新增:向 `order_batch_supplies` 插一行;重复 `supplies_resource_id` 在插入前被拒。 - 本次**无表结构变更、无 Flyway 脚本**。 ## 六、边界行为 - 人员配置越界角色值区分大小写,`guide` 不等于 `GUIDE`。 - 备品候选列表只返上架备品;已下架的既有行不受影响,仍留在清单里。 - 加备品行的库快照覆盖入参的字段是名称、分类、计费方式;单价例外,入参优先。 ## 六.6、修改前后对比 | 场景 | 改前 | 改后 | |---|---|---| | 保存人员配置时不带 `staffList`(或字段名写错) | **静音软删整团人员配置 + 触发全团扇出,返 200** | 返 400,现有配置零变动 | | 保存人员配置时显式传 `[]` | 清空 | 清空(**不变**) | | 备品候选搜 `%` 或 `_` | 返回全库 | 返回零结果 | | 备品候选搜普通关键字 | 正常命中 | 正常命中(**不变**) | | 备品库超过 500 条时加第 501 条之后的备品 | 一律报「所选备品不存在或已下架」,实际是被 500 条上限截断 | 正常加入 | | 资源域故障时加备品行 | 报「不存在或已下架」,与备品真下架无法区分 | 报 589522「备品库查询失败」,与 589519 分开 | ## 六.7、影响评估 - **需要前端配合的只有一条**:保存人员配置必须带 `staffList`。经排查管理后台现有调用是带该字段的,理论上不受影响;但字段名一旦写错,改前是静默清空、改后是 400,属"报错优于静默毁数据"。 - 备品搜索语义变化只影响把 `%` / `_` 当通配符用的用法,业务上没有这种用法。 - 加备品行的改动对调用方**完全透明**,契约与错误码不变,只是不再有隐性上限。 ## 七、不影响范围 - 团期看板、详情、子订单、合同保险、财务、流团与退单等其余端点未改动。 - 备品清单的查询、改数量、删除、确认物资四个端点契约不变。 - 无表结构变更、无网关路由变更、无新增错误码(589519 / 589522 / 589523 / 582114 均为既有)。 ## 八、测试环境已验证 真实网关(`api.test.1814.love`)逐条实测,`dev-v3` 分支已部署 `hl-resource-service` 与 `hl-order-service-v3`: | 用例 | 结果 | |---|---| | 保存人员配置缺 `staffList`(模拟字段名写成 `items`) | 400「staff 配置列表不能缺失;确要清空请显式传空数组 []」 | | 保存人员配置显式传 `[]` | 200,配置清空为 0 条 | | GUIDE 类型的人配成 PHOTOGRAPHER | 582114,配置数仍为 0(零写入) | | GUIDE 类型的人配成 GUIDE | 200,回显 `staffId` / `staffName` / `staffPhone` / `staffRole` 全字段 | | 配完后看导游位候选 | `assigned=true`、`assignedRole=GUIDE` | | 配完后看摄影位候选 | 该人不在候选内(勾选态按配置位收敛) | | 备品候选 `keyword=%` / `keyword=_` | 均返 0 条(改前返回全库 15 条) | | 备品候选 `keyword=帽` | 返 1 条「定制遮阳帽」 | | 从备品库加行 | 200,库快照生效(`billingType=PER_PERSON`、`hasCost=true`、`unitPrice=15.00`) | | 同一备品再加一次 | 589523,清单无重复行 | | 三个写端点传不存在的 ID | 589500 / 589521 / 589553,一律零写入 | 单元测试:`hl-order-service-v3` 全量 9303 例、`hl-resource-service` 全量 2707 例,均 0 失败 0 错误。 ## 九、相关历史 PR - PR #7028(本次合入,含 2026-09-03 审计的 F-01 / F-02 / F-04 三项中危修复) - 前序 #6950(团期人员配置候选列表)、#6986(团期物资备品库选择) ## 十、相关文档 - 审计记录:`dev-records/records/2026-09-03-local-group-tour-6950-6986-post-merge-audit.md` - 实施单:`docs/group/实施单/07-人员-导游摄影司机.html`、`docs/group/实施单/10-物资清单.html` ## 关联 / 联系人 - 工单:#7377(收口审计余项)、#7028(PR) - 后端:jw - 前端:mmg —— 只需确认保存人员配置时始终带 `staffList` 字段