diff --git a/changelogs-v2/2026-08/08_5674_核单导游摄影费用保存接口字段白名单-修改接口-管理后台.md b/changelogs-v2/2026-08/08_5674_核单导游摄影费用保存接口字段白名单-修改接口-管理后台.md new file mode 100644 index 0000000..e43d079 --- /dev/null +++ b/changelogs-v2/2026-08/08_5674_核单导游摄影费用保存接口字段白名单-修改接口-管理后台.md @@ -0,0 +1,266 @@ +--- +schema: "hl-changelog/v2" +ticket: "5674" +title: "核单导游/摄影费用保存接口入参字段白名单(响应只读派生字段不得回传)" +consumer: "admin" +change_type: "修改接口" +author: "yaosutu(GIT)" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "契约零变更的澄清说明:#5674 后保存请求 VO 对未声明字段显式拒绝(@JsonAnySetter),前端若把 GET 响应 item 原样回传会带出只读派生字段(如 sourceType)被 400 拦截。本文档给出保存入参字段白名单与必须剥掉的只读字段清单。" +updated_at: "2026-08-08" +base: "dev-v3" +--- + +# 【⚠️ 修改接口·管理后台】核单导游/摄影费用保存接口入参字段白名单(#5674 澄清) + +> **Commit**: [146cc2ae4](https://git.1814.love:8443/wx/HL/commit/146cc2ae4) | **服务**: hl-order-service-v3 | **更新时间**: 2026-08-08 + +## 1. 接口背景 + +核单「导游费用」「摄影费用」两个页签的全量保存接口,因 #5674 引入了严格校验:保存请求 VO 用 `@JsonAnySetter` 对**未声明字段显式拒绝**(不是忽略)。 + +**前端风险点**:若把 GET 查询响应里的 item 对象**原样回传**给保存接口,会带出 9 个只读派生字段(如 `sourceType`),后端直接 400 报「不支持字段: sourceType」。 + +本文档目的:给前端一份**保存入参字段白名单 + 必须剥掉的只读字段清单**。 + +## 2. 变更清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 前端动作 | +|---|---|---|---|---|---| +| 1 | 保存导游费用 | PUT | `/v3/admin/order/{orderId}/settlement/guide-fees` | 契约澄清(签名零变化) | 保存 payload 按白名单重建,勿原样回传响应对象 | +| 2 | 保存摄影费用 | PUT | `/v3/admin/order/{orderId}/settlement/photographer-fees` | 契约澄清(签名零变化) | 同上 | + +> 本次为**澄清说明类 changelog**:接口签名、入参、出参、枚举均未变化;目的是防止前端因原样回传响应对象触发 400。 + +## 3. 接口详情 + +- **使用场景**:核单人员在订单核单页维护导游费用 / 摄影费用明细(全量覆盖式保存)。 +- **认证**:需要管理后台登录态(Bearer Token)。 +- **幂等性**:全量覆盖式保存,重复提交相同载荷结果一致;导游接口带 `expectedSourceFingerprint` 乐观校验。 +- **限流**:未声明接口专属限流。 + +## 4. 接口入参 + +### 4.1 顶层字段(两接口有差异,重点提示) + +| 字段 | guide-fees(导游) | photographer-fees(摄影) | +|---|---|---| +| `expectedSourceFingerprint` | ✅ 必填(64 位 hex 数据指纹) | ❌ **不收,别传** | +| `items` | ✅ 必填(全量集合,最多 200 条) | ✅ 必填 | +| `excludedCandidateKeys` | ✅ 可选(空数组 = 不新增排除) | ✅ 可选 | + +### 4.2 items[] 允许提交字段白名单 + +**导游**(guide-fees): + +``` +id / candidateKey / staffAssignmentId / serviceDate / name / serviceType +paymentMethod / amount / remark / voucherUrls / sourceResolution / completionState +``` + +**摄影**(photographer-fees): + +``` +id / candidateKey / staffAssignmentId / serviceDate / photographerName / feeType +paymentMethod / amount / remark / voucherUrls / sourceResolution / completionState +``` + +> 导游用 `name` + `serviceType`;摄影用 `photographerName` + `feeType`,**两接口字段名别混**。 + +### 4.3 字段约束(写全) + +| 字段 | 类型 | 约束 | +|---|---|---| +| `id` | Long(字符串形式) | 既有行必传;新增手工行传 null | +| `candidateKey` | String | 候选行 key;**手工行必须为 null** | +| `staffAssignmentId` | Long(字符串形式) | 关联派单人员;手工行为 null | +| `serviceDate` | String(yyyy-MM-dd) | INCLUDED 必填;EXCLUDED 为 null | +| `name` / `photographerName` | String | INCLUDED 必填,最长 64 | +| `serviceType` | Enum | `FULL_COURSE_GUIDE` / `LOCAL_GUIDE` / `COMMENTARY_SERVICE` / `TEMPORARY_SUPPLEMENT` | +| `feeType` | Enum | `FOLLOW_SHOOT` / `PORTRAIT` / `AERIAL_SHOOT` / `EDITING_DELIVERY` / `CAMERA_DRONE` / `OTHER` | +| `paymentMethod` | Enum | `COMPANY_PAID` / `CASH_PAID` / `SIGNED` | +| `amount` | String | 金额字符串,0 ~ 99999999.99,最多两位小数 | +| `remark` | String | 无备注传 null,最长 500 | +| `voucherUrls` | String[] | 最多 9 个,单条最长 1024,去重保序 | +| `sourceResolution` | Enum | 仅转换手工行时传 `CONVERT_TO_MANUAL`,其余不传 | +| `completionState` | Enum | `COMPLETE` / `EXCLUDED`;普通保存可 null | + +## 5. 出参字段 + +成功响应为统一 `Result` 结构,`code=200`。出参字段本次无变化,不在本文档范围。 + +## 6. 枚举 / 数据字典 + +枚举值见 §4.3 字段约束表(serviceType / feeType / paymentMethod / sourceResolution / completionState)。本次不涉及枚举新增或改值。 + +## 7. 错误码 + +| code | 含义 | 触发场景 | +|---:|---|---| +| `584128` | 导游或摄影费用请求字段不合法:{具体原因} | 请求体含未声明字段 / 反序列化失败 / Bean 校验失败 | +| `584125` | 导游或摄影费用请求字段不合法 | 请求体为空 / null | + +多传只读字段(如 `sourceType`)时,`584128` message 形如:「...不支持字段: sourceType」,直接展示后端 message 即可。 + +## 8. 示例 + +### 8.1 典型成功 —— 导游(含 expectedSourceFingerprint) + +```http +PUT /v3/admin/order/2084000000000002978/settlement/guide-fees +Authorization: Bearer +Content-Type: application/json +``` + +```json +{ + "expectedSourceFingerprint": "9f2c4a1b8d3e6f0a1b2c3d4e5f60718293a4b5c6d7e8f901234567890abcdef1", + "items": [ + { + "id": null, + "candidateKey": null, + "staffAssignmentId": null, + "serviceDate": "2026-08-08", + "name": "张三", + "serviceType": "LOCAL_GUIDE", + "paymentMethod": "COMPANY_PAID", + "amount": "300.00", + "remark": "半天讲解", + "voucherUrls": ["https://oss.example.com/voucher/1.png"], + "sourceResolution": null, + "completionState": "COMPLETE" + } + ], + "excludedCandidateKeys": [] +} +``` + +```json +{"code": 200, "message": "success", "data": {"saved": true}, "success": true} +``` + +### 8.2 典型成功 —— 摄影(不含 expectedSourceFingerprint) + +```http +PUT /v3/admin/order/2084000000000002978/settlement/photographer-fees +Authorization: Bearer +Content-Type: application/json +``` + +```json +{ + "items": [ + { + "id": null, + "candidateKey": null, + "staffAssignmentId": null, + "serviceDate": "2026-08-08", + "photographerName": "李四", + "feeType": "FOLLOW_SHOOT", + "paymentMethod": "CASH_PAID", + "amount": "800.00", + "remark": null, + "voucherUrls": [], + "sourceResolution": null, + "completionState": "COMPLETE" + } + ], + "excludedCandidateKeys": [] +} +``` + +```json +{"code": 200, "message": "success", "data": {"saved": true}, "success": true} +``` + +### 8.3 业务失败 —— 多传只读字段 sourceType(400) + +```json +{ + "expectedSourceFingerprint": "9f2c4a1b8d3e6f0a1b2c3d4e5f60718293a4b5c6d7e8f901234567890abcdef1", + "items": [ + { + "id": "2084000000000003001", + "candidateKey": "GUIDE#2026-08-08#张三", + "serviceDate": "2026-08-08", + "name": "张三", + "serviceType": "LOCAL_GUIDE", + "paymentMethod": "COMPANY_PAID", + "amount": "300.00", + "sourceType": "CANDIDATE", + "sourceTypeName": "候选带入", + "settlementConfirmStatus": "UNCONFIRMED" + } + ], + "excludedCandidateKeys": [] +} +``` + +```json +{"code": 584128, "message": "导游或摄影费用请求字段不合法:导游费用明细不支持字段: sourceType", "data": null, "success": false} +``` + +## 9. 业务边界 + +### ⚠️ 必须剥掉的只读派生字段(响应里有、保存不收,传了就 400) + +``` +sourceType / sourceTypeName / sourceActive +serviceTypeName / paymentMethodName / feeTypeName(摄影) +settlementConfirmStatus / settlementConfirmStatusName +candidateResolution +``` + +**通则**:所有 `*Name` 中文字段、`sourceType`/`sourceActive`、确认状态、候选处理结果,都是后端算的,前端保存时**一律别回传**。 + +### 推荐做法 + +**不要直接回传 GET 响应对象**。保存前按白名单重建 payload——维护一个 `toSaveItem` 映射函数,只挑 §4.2 白名单字段: + +```js +// 伪代码示意(仅说明映射思路,非前端代码) +toSaveItem(respItem) => 只保留白名单 12 个字段,其余丢弃 +``` + +### 适用 / 不适用 + +- ✅ 适用:核单页「导游费用」「摄影费用」页签的保存按钮。 +- ❌ 不适用:确认接口(`/confirm`)与 GET 查询接口不在本文档范围。 + +## 10. 修改前后对比 + +本次为澄清说明,接口签名与字段零变化,无修改前后对比。关键行为澄清: + +| 场景 | 行为 | +|---|---| +| 保存请求只含白名单字段 | 正常保存(200) | +| 保存请求夹带响应只读字段(sourceType 等) | 400,`584128` 指出不支持字段名 | + +## 11. 影响评估 / 回滚 + +- **是否破坏向后兼容**:否。契约零变化。 +- **前端是否必须同步上线**:建议尽快。若当前前端存在原样回传响应对象的路径,必然触发 400,需按白名单重建 payload。 +- **回滚方案**:无需回滚(无代码变更)。 + +## 12. 注意事项 + +- 导游接口必传 `expectedSourceFingerprint`(64 位 hex),摄影接口**不收**该字段,传了会被当未知字段拒绝。 +- `candidateKey` 是候选行标识,手工新增行必须为 null。 +- 失败 toast 直接展示后端 `message`,已含具体字段原因。 + +## 13. 关联 / 联系人 + +### 13.1 链接 + +- **Issue**: [#5674](https://git.1814.love:8443/wx/HL/issues/5674) +- **Commit**: [146cc2ae4](https://git.1814.love:8443/wx/HL/commit/146cc2ae4) + +### 13.2 联系人 + +- **后端负责人**: @yaosutu (yst)