11 KiB
schema, ticket, title, consumer, change_type, author, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | change_type | author | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 5674 | 核单导游/摄影费用保存接口入参字段白名单(响应只读派生字段不得回传) | admin | 修改接口 | yaosutu(GIT) | deployed | verified | implemented | mmg | 9694bb68 | 契约零变更的澄清说明:#5674 后保存请求 VO 对未声明字段显式拒绝(@JsonAnySetter),前端若把 GET 响应 item 原样回传会带出只读派生字段(如 sourceType)被 400 拦截。本文档给出保存入参字段白名单与必须剥掉的只读字段清单。 | 2026-08-08 | dev-v3 |
【⚠️ 修改接口·管理后台】核单导游/摄影费用保存接口入参字段白名单(#5674 澄清)
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)
PUT /v3/admin/order/2084000000000002978/settlement/guide-fees
Authorization: Bearer <token>
Content-Type: application/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": []
}
{"code": 200, "message": "success", "data": {"saved": true}, "success": true}
8.2 典型成功 —— 摄影(不含 expectedSourceFingerprint)
PUT /v3/admin/order/2084000000000002978/settlement/photographer-fees
Authorization: Bearer <token>
Content-Type: application/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": []
}
{"code": 200, "message": "success", "data": {"saved": true}, "success": true}
8.3 业务失败 —— 多传只读字段 sourceType(400)
{
"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": []
}
{"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 白名单字段:
// 伪代码示意(仅说明映射思路,非前端代码)
toSaveItem(respItem) => 只保留白名单 12 个字段,其余丢弃
适用 / 不适用
- ✅ 适用:核单页「导游费用」「摄影费用」页签的保存按钮。
- ❌ 不适用:确认接口(
/confirm)与 GET 查询接口不在本文档范围。
10. 修改前后对比
本次为澄清说明,接口签名与字段零变化,无修改前后对比。关键行为澄清:
| 场景 | 行为 |
|---|---|
| 保存请求只含白名单字段 | 正常保存(200) |
| 保存请求夹带响应只读字段(sourceType 等) | 400,584128 指出不支持字段名 |
11. 影响评估 / 回滚
- 是否破坏向后兼容:否。契约零变化。
- 前端是否必须同步上线:建议尽快。若当前前端存在原样回传响应对象的路径,必然触发 400,需按白名单重建 payload。
- 回滚方案:无需回滚(无代码变更)。
12. 注意事项
- 导游接口必传
expectedSourceFingerprint(64 位 hex),摄影接口不收该字段,传了会被当未知字段拒绝。 candidateKey是候选行标识,手工新增行必须为 null。- 失败 toast 直接展示后端
message,已含具体字段原因。
13. 关联 / 联系人
13.1 链接
13.2 联系人
- 后端负责人: @yaosutu (yst)
前端实证确认(2026-08-08 mmg,hl-admin@9694bb68)
前端保存非原样回传响应对象,而是 returnDetailAdapter.js buildStaffFeeTabSaveRequest 逐项显式构造——但与 §4.2 白名单有真实出入,已按白名单重建修复:
| 字段 | 前端改动前 | 改动后 |
|---|---|---|
sourceType |
传 source.sourceType || 'MANUAL' |
删除(只读派生,传了 400) |
settlementConfirmStatus |
传 UNCONFIRMED/CONFIRMED | 删除(确认状态后端算) |
expectedSourceFingerprint |
导游+摄影都传 | 仅导游传,摄影省略(摄影不收,传了被当未知字段拒) |
| 手工/候选来源区分 | 靠 sourceType=MANUAL | 改由 candidateKey/staffAssignmentId 是否为 null 区分(changelog §4.3 口径) |
| 姓名/类型字段 | name+serviceType(导游)/photographerName+feeType(摄影) | 不变(本就不混用) |
修复前导游保存必带 sourceType+settlementConfirmStatus → 必触发 584128 400;摄影另多传 expectedSourceFingerprint → 同样 400。修复后严格白名单。改动文件:returnDetailAdapter.js + 两个 spec 同步断言(新增 not.toHaveProperty('sourceType'/'settlementConfirmStatus')、摄影 not.objectContaining expectedSourceFingerprint 守卫)。step1/step2/meal/vehicle/otherIncome/otherExpense 的 settlementConfirmStatus 不在本白名单范围、契约未变,保持不动。验证:settlement 全量定向 vitest 92/92 通过;checkpoint 全绿。