29 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 | 6117 | 导游/摄影核单去 EXCLUDED + 全量替换,确认状态统一 settlementConfirmStatus | admin | 修改接口 | yst | deployed | verified | verified | mmg | ec7f2a9a | 2026-08-21 | PR #6119 已合并 dev-v3(merge commit 27ac77b500),测试服已验证。破坏性变化:导游/摄影核单废弃「按天候选 + EXCLUDED」机制改全量替换语义;保存入参删 candidateKey/completionState/sourceResolution/excludedCandidateKeys,传旧字段一律 400(584128);查询出参删 candidateKey/completionState/candidateResolution/sourceActive/pendingCandidateCount;确认状态统一 settlementConfirmStatus(UNCONFIRMED/CONFIRMED 二值);blockReasonCode 删 SOURCE_INACTIVE/CANDIDATES_UNRESOLVED 两值。无 DDL。 | 2026-08-21 | dev-v3 |
【修改接口·管理后台】导游/摄影核单确认状态统一——去 EXCLUDED 改全量替换(#6117)
PR: #6119 | 服务: hl-order-service-v3 | 更新时间: 2026-08-21
1. 接口背景
订单核单页「导游」「摄影」两个费用 tab 此前使用「按天候选 + EXCLUDED」机制:后端按订单行程天数预生成候选行,前端要在「纳入(INCLUDED)/ 排除(EXCLUDED)/ 未处理(UNRESOLVED)」三种候选处理结果之间来回切换,还要单独维护一份 excludedCandidateKeys 排除清单。这套机制字段多、状态绕,和酒店/门票 tab 的「全量保存 + 确认状态」模型完全不一致,前端两套交互逻辑要分别维护。
本次变更把导游/摄影 tab 拉齐到酒店/门票同款模型:
- 废弃候选机制,保存接口改全量替换语义——传当前应存在的全部行,没传的未确认行即删除;
- 确认状态统一为每行一个
settlementConfirmStatus(UNCONFIRMED/CONFIRMED二值),保存时可直接把行置为已确认; - 已确认行受保护:不能被全量替换顺手删掉,编辑业务字段会自动退回未确认、需重新确认。
涉及导游(guide-fees)与摄影(photographer-fees)两组共 6 个接口,两组结构完全同构,仅字段名有差异(导游用 name/serviceType,摄影用 photographerName/feeType)。
2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询导游单层费用明细 | GET | /v3/admin/order/{orderId}/settlement/guide-fees |
修改接口 | 出参删 candidateKey/completionState/candidateResolution/sourceActive/pendingCandidateCount |
| 2 | 全量保存导游单层费用明细 | PUT | /v3/admin/order/{orderId}/settlement/guide-fees |
修改接口 | 入参删 candidateKey/completionState/sourceResolution/excludedCandidateKeys,新增 settlementConfirmStatus;改全量替换语义 |
| 3 | 确认导游单层费用明细 | POST | /v3/admin/order/{orderId}/settlement/guide-fees/confirm |
修改接口 | 签名不变(itemIds),确认语义对齐新模型 |
| 4 | 查询摄影单层费用明细 | GET | /v3/admin/order/{orderId}/settlement/photographer-fees |
修改接口 | 同 #1 |
| 5 | 全量保存摄影单层费用明细 | PUT | /v3/admin/order/{orderId}/settlement/photographer-fees |
修改接口 | 同 #2 |
| 6 | 确认摄影单层费用明细 | POST | /v3/admin/order/{orderId}/settlement/photographer-fees/confirm |
修改接口 | 同 #3 |
3. 接口详情
3.1 查询导游单层费用明细(GET guide-fees)
- 使用场景:打开核单页「导游」tab 时加载费用明细列表与分类汇总
- 认证:管理后台 JWT(房务角色只读拦截,返回 403)
- 幂等性:是(只读)
- 限流:无
入参
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderId | Long(路径参数) | ✅ | 订单 ID,必须大于 0 |
出参(Result<SettlementGuideFeesRespVO>)
响应级字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| category | String | 核算分类,恒为 GUIDE |
| totalAmount | String | 纳入核算的费用合计,金额字符串,如 500.00 |
| cashPaidAmount | String | 现付费用合计,金额字符串 |
| unconfirmedCount | Integer | 未确认明细数(全行口径,见 §10.2) |
| settlementReady | Boolean | 是否满足本分类提交核单条件 |
| blockReasonCode | String | 阻断原因码;无阻断时为 null,取值见 §6.4 |
| items | Array | 费用明细数组;无数据返回空数组 |
| editable | Boolean | 当前订单是否允许编辑本分类 |
| readOnlyReasonCode | String | 只读原因码;可编辑时为 null |
items[] 行字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 费用明细 ID(Long 序列化为字符串);未落库的预填草稿行为 null |
| staffAssignmentId | String | 人员分配 ID;手工新增行为 null |
| serviceDate | String | 服务日期 YYYY-MM-DD;预填草稿行为 null |
| name | String | 导游姓名 |
| serviceType | String | 服务类型,取值见 §6.1 |
| serviceTypeName | String | 服务类型中文名;serviceType 为 null 时为 null |
| paymentMethod | String | 付款方式,取值见 §6.3 |
| paymentMethodName | String | 付款方式中文名;paymentMethod 为 null 时为 null |
| amount | String | 金额字符串;预填草稿行为 null |
| settlementConfirmStatus | String | 核单确认状态,取值见 §6.5 |
| settlementConfirmStatusName | String | 核单确认状态中文名 |
| remark | String | 备注;无备注为 null |
| sourceType | String | 来源类型:STAFF_ASSIGNMENT 人员安排 / MANUAL 手工 / SYSTEM 系统 |
| sourceTypeName | String | 来源类型中文名 |
| voucherUrls | Array | 凭证 URL;无凭证返回空数组 |
错误码:见 §7 全组共用错误码表。
业务边界:首次查询(订单尚无该角色核单行)时,返回按订单人员分配预填的草稿行(不落库,行特征:id 为 null、settlementConfirmStatus=UNCONFIRMED);这些预填行会被计入 unconfirmedCount,影响 settlementReady 展示口径。
示例(典型成功)
请求:
GET /v3/admin/order/12345/settlement/guide-fees
Authorization: Bearer {admin-token}
(无请求体)
响应:
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "800.00",
"cashPaidAmount": "300.00",
"unconfirmedCount": 1,
"settlementReady": false,
"blockReasonCode": "ITEMS_UNCONFIRMED",
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9001",
"staffAssignmentId": "11",
"serviceDate": "2026-08-03",
"name": "导游甲",
"serviceType": "FULL_COURSE_GUIDE",
"serviceTypeName": "全陪导游",
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司支付",
"amount": "500.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
},
{
"id": "9002",
"staffAssignmentId": null,
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"serviceTypeName": "地接导游",
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"amount": "300.00",
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": "现场临时请的地陪",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
},
"msg": "",
"success": true
}
示例(边界:首次查询返回预填草稿行)
场景说明:订单分配了 1 名导游但从未保存过核单行,GET 返回不落库的预填草稿(id/serviceDate/amount 为 null)。
响应:
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "0.00",
"cashPaidAmount": "0.00",
"unconfirmedCount": 1,
"settlementReady": false,
"blockReasonCode": "ITEMS_UNCONFIRMED",
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": null,
"staffAssignmentId": "11",
"serviceDate": null,
"name": "导游甲",
"serviceType": null,
"serviceTypeName": null,
"paymentMethod": null,
"paymentMethodName": null,
"amount": null,
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
}
]
},
"msg": "",
"success": true
}
3.2 全量保存导游单层费用明细(PUT guide-fees)
- 使用场景:在「导游」tab 编辑完费用明细后整体保存(新增 / 修改 / 删除行都通过本接口一次性提交)
- 认证:管理后台 JWT
- 幂等性:是(全量替换语义,同一 body 重放结果一致)
- 限流:无
入参
路径参数:orderId(Long,必填,订单 ID)。
请求体字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| items | Array | ✅ | 导游费用明细全量集合(当前应存在的全部行) | 最多 200 条,超出报 584121 |
ItemVO 字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| id | String | ❌ | 费用明细 ID;已存在行必传,新增行不传 | 必须传字符串形式 |
| staffAssignmentId | String | ❌ | 人员分配 ID;关联人员分配的行传 | 非空时必须属本单且角色为导游,否则报 584115 |
| serviceDate | String | ❌ | 服务日期 YYYY-MM-DD |
必须在订单行程范围内,否则报 584112;订单出发/返程日期缺失时报 584123 |
| name | String | ❌ | 导游姓名 | 最长 64 字符 |
| serviceType | String | ❌ | 服务类型 | 取值见 §6.1,非法值报 584113 |
| paymentMethod | String | ❌ | 付款方式 | 取值见 §6.3 |
| amount | String | ❌ | 金额字符串 | 0 至 99999999.99 且最多两位小数,否则报 584114 |
| remark | String | ❌ | 备注;无备注传 null | 最长 500 字符 |
| voucherUrls | Array | ❌ | 凭证 URL | 最多 9 个,单条最长 1024;重复值去重并保持首次出现顺序 |
| settlementConfirmStatus | String | ❌ | 核单确认状态;缺省/null 按 UNCONFIRMED 处理,保存时可直接置 CONFIRMED | 仅允许 UNCONFIRMED / CONFIRMED,其他值报 584128 |
严格模式:请求体(顶层或行内)出现任何未定义字段一律 400。改前旧字段 candidateKey / completionState / sourceResolution / excludedCandidateKeys 现已删除,传了同样 400(错误码 584128,msg 形如 导游或摄影费用请求字段不合法:导游费用明细不支持字段: candidateKey)。
出参:同 3.1 的响应结构(保存成功后返回最新全量)。
错误码:见 §7。
业务边界(全量替换语义):
- 本次请求传入的
items即保存后的全部行;库里存在但未传入的未确认行会被删除; - 已 CONFIRMED 行不可删除——若库里某行已确认但本次未传,报 584120「已确认的有效费用不能直接删除,请先进入编辑状态」,整单保存失败;
- 已 CONFIRMED 行若本次修改了业务字段(姓名/金额/日期/类型/付款方式等),保存后自动重置回
UNCONFIRMED,需重新确认; - 新增行可在保存时直接置
CONFIRMED(与酒店/门票 tab 行为一致)。
示例(典型成功:两行全量保存,一行直接置已确认)
请求:
PUT /v3/admin/order/12345/settlement/guide-fees
Authorization: Bearer {admin-token}
{
"items": [
{
"id": "9001",
"staffAssignmentId": "11",
"serviceDate": "2026-08-03",
"name": "导游甲",
"serviceType": "FULL_COURSE_GUIDE",
"paymentMethod": "COMPANY_PAID",
"amount": "500.00",
"remark": null,
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED"
},
{
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"paymentMethod": "CASH_PAID",
"amount": "300.00",
"remark": "现场临时请的地陪",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
}
响应:code=200,data 为保存后的最新全量(结构同 3.1 响应示例)。
示例(业务失败:误传已删除的旧字段)
场景说明:前端未清理旧逻辑,行内仍带 candidateKey。
请求:
PUT /v3/admin/order/12345/settlement/guide-fees
{
"items": [
{
"candidateKey": "2026-08-03#11",
"name": "导游甲",
"amount": "500.00"
}
]
}
响应:
{
"code": 584128,
"data": null,
"msg": "导游或摄影费用请求字段不合法:导游费用明细不支持字段: candidateKey",
"success": false
}
示例(业务失败:已确认行被全量替换遗漏)
场景说明:库中行 9001 已 CONFIRMED,本次 items 只传了行 9002,相当于要删掉 9001。
响应:
{
"code": 584120,
"data": null,
"msg": "已确认的有效费用不能直接删除,请先进入编辑状态",
"success": false
}
3.3 确认导游单层费用明细(POST guide-fees/confirm)
- 使用场景:勾选若干未确认行后点「确认」,将这批行批量置为已确认
- 认证:管理后台 JWT
- 幂等性:是(对已确认行重复确认无副作用)
- 限流:无
入参
路径参数:orderId(Long,必填)。
请求体字段:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| itemIds | Array | ✅ | 待确认的费用明细 ID 列表,JSON 中每项为字符串 | 1 至 200 条;ID 不存在或不属于本订单及导游角色报 584111 |
同样走严格模式,多传字段报 584128(msg 形如 导游费用确认请求不支持字段: xxx)。
出参:同 3.1 的响应结构(确认后的最新全量)。
示例(典型成功)
请求:
POST /v3/admin/order/12345/settlement/guide-fees/confirm
Authorization: Bearer {admin-token}
{
"itemIds": ["9002"]
}
响应:
{
"code": 200,
"data": {
"category": "GUIDE",
"totalAmount": "800.00",
"cashPaidAmount": "300.00",
"unconfirmedCount": 0,
"settlementReady": true,
"blockReasonCode": null,
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9002",
"staffAssignmentId": null,
"serviceDate": "2026-08-04",
"name": "导游乙",
"serviceType": "LOCAL_GUIDE",
"serviceTypeName": "地接导游",
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "现场临时请的地陪",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"voucherUrls": ["https://oss.example.com/voucher/a.jpg"]
}
]
},
"msg": "",
"success": true
}
3.4 查询摄影单层费用明细(GET photographer-fees)
- 使用场景:打开核单页「摄影」tab 时加载费用明细列表与分类汇总
- 认证 / 幂等 / 限流:同 3.1
入参:同 3.1(orderId 路径参数)。
出参(Result<SettlementPhotographerFeesRespVO>):响应级字段与 3.1 完全一致(category 恒为 PHOTOGRAPHER),items[] 行字段仅以下差异,其余字段同 3.1:
| 字段 | 类型 | 说明 |
|---|---|---|
| photographerName | String | 摄影姓名(对应导游组的 name) |
| feeType | String | 摄影费用类型,取值见 §6.2 |
| feeTypeName | String | 摄影费用类型中文名;feeType 为 null 时为 null |
(行内不再有 name / serviceType / serviceTypeName。)
示例(典型成功)
请求:
GET /v3/admin/order/12345/settlement/photographer-fees
Authorization: Bearer {admin-token}
(无请求体)
响应:
{
"code": 200,
"data": {
"category": "PHOTOGRAPHER",
"totalAmount": "300.00",
"cashPaidAmount": "0.00",
"unconfirmedCount": 0,
"settlementReady": true,
"blockReasonCode": null,
"editable": true,
"readOnlyReasonCode": null,
"items": [
{
"id": "9101",
"staffAssignmentId": "12",
"serviceDate": "2026-08-03",
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"feeTypeName": "跟拍",
"paymentMethod": "SIGNED",
"paymentMethodName": "签单",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": null,
"sourceType": "STAFF_ASSIGNMENT",
"sourceTypeName": "人员安排",
"voucherUrls": []
}
]
},
"msg": "",
"success": true
}
首次查询同样返回按人员分配预填的草稿行(不落库,UNCONFIRMED),行为同 3.1 边界示例。
3.5 全量保存摄影单层费用明细(PUT photographer-fees)
- 使用场景 / 认证 / 幂等 / 限流:同 3.2
入参:结构同 3.2,ItemVO 字段仅以下差异,其余(含 settlementConfirmStatus、严格模式、全量替换语义、584120 保护、已确认行编辑自动退回未确认)完全一致:
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| photographerName | String | ❌ | 摄影姓名 | 最长 64 字符 |
| feeType | String | ❌ | 摄影费用类型 | 取值见 §6.2,非法值报 584113 |
(不再有 name / serviceType;staffAssignmentId 非空时角色须为摄影,否则报 584115。)
示例(典型成功)
请求:
PUT /v3/admin/order/12345/settlement/photographer-fees
Authorization: Bearer {admin-token}
{
"items": [
{
"id": "9101",
"staffAssignmentId": "12",
"serviceDate": "2026-08-03",
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"paymentMethod": "SIGNED",
"amount": "300.00",
"settlementConfirmStatus": "CONFIRMED"
}
]
}
响应:code=200,data 为保存后的最新全量(结构同 3.4 响应示例)。
示例(业务失败:金额非法)
请求:
PUT /v3/admin/order/12345/settlement/photographer-fees
{
"items": [
{
"photographerName": "摄影甲",
"feeType": "FOLLOW_SHOOT",
"amount": "-50.00"
}
]
}
响应:
{
"code": 584114,
"data": null,
"msg": "金额必须为 0 至 99999999.99 且最多两位小数",
"success": false
}
3.6 确认摄影单层费用明细(POST photographer-fees/confirm)
- 使用场景 / 认证 / 幂等 / 限流:同 3.3
入参:同 3.3(itemIds 字符串数组,1-200 条;ID 不存在或不属于本订单及摄影角色报 584111;多传字段报 584128,msg 形如 摄影费用确认请求不支持字段: xxx)。
出参:同 3.4 的响应结构。
示例(典型成功)
请求:
POST /v3/admin/order/12345/settlement/photographer-fees/confirm
Authorization: Bearer {admin-token}
{
"itemIds": ["9101"]
}
响应:code=200,data 中该行 settlementConfirmStatus 变为 CONFIRMED、unconfirmedCount 相应减少(结构同 3.4 响应示例)。
4. 接口入参
已按接口分散在 §3.1 ~ §3.6 各自小节内(本组为多接口 changelog,入参不单独集中成节)。
5. 出参(响应)
已按接口分散在 §3.1 ~ §3.6 各自小节内。
6. 枚举 / 数据字典
6.1 serviceType(导游服务类型)
所属字段:导游组 ItemVO 的 serviceType | 类型:String | 必填:❌
| 值 | 中文 | 说明 |
|---|---|---|
FULL_COURSE_GUIDE |
全陪导游 | — |
LOCAL_GUIDE |
地接导游 | — |
COMMENTARY_SERVICE |
讲解服务 | — |
TEMPORARY_SUPPLEMENT |
临时补录 | — |
6.2 feeType(摄影费用类型)
所属字段:摄影组 ItemVO 的 feeType | 类型:String | 必填:❌
| 值 | 中文 | 说明 |
|---|---|---|
FOLLOW_SHOOT |
跟拍 | — |
PORTRAIT |
写真 | — |
AERIAL_SHOOT |
航拍 | — |
EDITING_DELIVERY |
剪辑出片 | — |
CAMERA_DRONE |
相机/无人机 | — |
OTHER |
其他 | — |
6.3 paymentMethod(付款方式)
所属字段:两组 ItemVO 的 paymentMethod | 类型:String | 必填:❌
| 值 | 中文 | 说明 |
|---|---|---|
COMPANY_PAID |
公司支付 | — |
CASH_PAID |
现付 | 计入 cashPaidAmount |
SIGNED |
签单 | — |
6.4 blockReasonCode(阻断原因码)
所属字段:响应级 blockReasonCode | 类型:String | 必填:❌(无阻断时为 null)
| 值 | 中文 | 说明 |
|---|---|---|
ORDER_DATE_INCOMPLETE |
订单日期不完整 | 订单出发或返程日期缺失,无法校验服务日期 |
ITEMS_UNCONFIRMED |
存在未确认明细 | 存在 UNCONFIRMED 行(含预填草稿行),不满足提交核单条件 |
⚠️ 本次变更删除两个旧值:
SOURCE_INACTIVE、CANDIDATES_UNRESOLVED,后端不再返回。
6.5 settlementConfirmStatus(核单确认状态)
所属字段:两组 ItemVO 的 settlementConfirmStatus(入参 + 出参) | 类型:String | 必填:❌(入参缺省按 UNCONFIRMED)
| 值 | 中文 | 说明 |
|---|---|---|
UNCONFIRMED |
未确认 | 可被全量替换删除 |
CONFIRMED |
已确认 | 不可删除;编辑业务字段自动退回 UNCONFIRMED |
7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| 584111 | 费用明细不存在或不属于当前订单及角色 | confirm 的 itemIds 含不存在/他单/角色不符的行 |
| 584112 | 服务日期不在订单行程范围内 | 保存时 serviceDate 超出订单出发~返程日期 |
| 584113 | 服务类型或摄影费用类型不合法 | serviceType / feeType 传了枚举外值 |
| 584114 | 金额必须为 0 至 99999999.99 且最多两位小数 | amount 为负数、超限或小数位超过 2 位 |
| 584115 | 人员分配不存在、角色不匹配或来源已失效 | staffAssignmentId 非空但不属本单,或角色不匹配(导游组传了摄影人员等) |
| 584120 | 已确认的有效费用不能直接删除,请先进入编辑状态 | 全量保存时遗漏了库里已 CONFIRMED 的行 |
| 584121 | 单个导游或摄影分类最多 200 条明细 | items 超过 200 条 |
| 584123 | 订单出发或返程日期缺失,无法校验服务日期 | 保存时订单日期不完整且传了 serviceDate |
| 584125 | 导游或摄影费用请求字段不合法 | 请求体缺失或整体解析失败(无具体字段原因时) |
| 584128 | 导游或摄影费用请求字段不合法:{0} | 请求体(顶层或行内)出现未定义字段(含已删除的旧字段 candidateKey / completionState / sourceResolution / excludedCandidateKeys);msg 带具体字段名 |
8. 示例(典型 / 边界 / 异常)
已按接口分散在 §3.1 ~ §3.6 各自小节内,每组示例均含请求 + 响应:
- 典型成功:§3.1 / §3.2 / §3.3 / §3.4 / §3.5 / §3.6
- 边界(首次查询预填草稿行,id/serviceDate/amount 为 null):§3.1 第二组示例
- 业务失败(584128 旧字段误传 / 584120 已确认行误删 / 584114 金额非法):§3.2 / §3.5
9. 业务边界
- ✅ 适用场景:订单核单进行中(editable=true)时可调保存/确认接口;查询接口在核单各阶段均可调(只读阶段 editable=false + readOnlyReasonCode 指明原因)
- ❌ 不适用场景:订单已提交核单终态后,保存/确认接口被拒(editable=false 时调用按只读规则拦截);房务角色(house)调本组接口返回 403
- ⚠️ 特殊边界 1(预填草稿行):首次 GET 返回的草稿行不落库(id 为 null),但计入
unconfirmedCount影响settlementReady展示;而「完成核单」的提交门禁只查已落库行,两个口径有意分离(展示口径 ≠ 提交口径) - ⚠️ 特殊边界 2(已确认行保护):已 CONFIRMED 行想删除,必须先修改该行业务字段使其退回 UNCONFIRMED(或走反确认流程),再在下一轮全量保存中不传该行
- ⚠️ 特殊边界 3(金额 0):amount 允许
0.00,不被非负校验拦截
10. 修改前后对比
10.1 字段级对比
保存入参(PUT,ItemVO 行内):
| 字段 | 改前 | 改后 |
|---|---|---|
| candidateKey | 有,候选行标识 | 删除,传了报 400(584128) |
| completionState | 有(NEEDS_INPUT/COMPLETE/EXCLUDED) | 删除,传了报 400(584128) |
| sourceResolution | 有,来源处理结果 | 删除,传了报 400(584128) |
| settlementConfirmStatus | 无 | 新增,非必填,UNCONFIRMED/CONFIRMED,缺省按 UNCONFIRMED |
保存入参(PUT,请求体顶层):
| 字段 | 改前 | 改后 |
|---|---|---|
| excludedCandidateKeys | 有,排除候选 key 清单 | 删除,传了报 400(584128) |
查询出参(GET,ItemVO 行内):
| 字段 | 改前 | 改后 |
|---|---|---|
| candidateKey | 有 | 删除,不再下发 |
| completionState | 有 | 删除,不再下发 |
| candidateResolution | 有(INCLUDED/EXCLUDED/UNRESOLVED) | 删除,不再下发 |
| sourceActive | 有 | 删除,不再下发 |
| settlementConfirmStatus / settlementConfirmStatusName | 有(仅展示) | 保留,语义不变 |
查询出参(GET,响应级):
| 字段 | 改前 | 改后 |
|---|---|---|
| pendingCandidateCount | 有,未处理候选数 | 删除,不再下发 |
| blockReasonCode 值域 | 含 SOURCE_INACTIVE、CANDIDATES_UNRESOLVED |
删除这两个值,只保留 ORDER_DATE_INCOMPLETE、ITEMS_UNCONFIRMED |
10.2 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 保存语义 | 候选机制:行按 candidateKey 对齐候选,配合 excludedCandidateKeys 声明排除 | 全量替换:items 即保存后的全部行,未传的未确认行删除 |
| 确认入口 | 只能通过 confirm 接口把 INCLUDED 行置已确认 | confirm 接口保留;保存时也可直接置 CONFIRMED(settlementConfirmStatus 入参) |
| 已确认行删除 | 通过 excludedCandidateKeys 排除 | 不可删:全量保存遗漏已确认行报 584120 |
| 已确认行编辑 | 编辑后走候选重算 | 编辑业务字段自动退回 UNCONFIRMED,需重新确认 |
| unconfirmedCount 口径 | 未确认的 INCLUDED 明细数 | 未确认明细数(全行口径,含预填草稿行) |
| 未处理候选 | 前端要处理 UNRESOLVED 候选(pendingCandidateCount > 0 阻断提交) | 机制删除,无此概念 |
| 旧字段容错 | 未知字段按 Jackson 默认处理 | 严格模式:任何未定义字段(含全部旧字段)一律 400 |
11. 影响评估 / 回滚
11.1 影响评估
- 是否破坏向后兼容:是。保存接口传旧字段(candidateKey / completionState / sourceResolution / excludedCandidateKeys)从「生效」变为「一律 400」;查询出参删 5 个字段;blockReasonCode 删 2 个枚举值。
- 前端是否必须同步上线:是。导游/摄影 tab 的保存请求必须清掉旧字段,否则保存全部 400;出参侧引用 candidateKey / completionState / candidateResolution / sourceActive / pendingCandidateCount 的渲染与逻辑必须同步删除。
11.2 回滚方案
- 回滚方式:后端 revert PR #6119 即可恢复旧候选机制契约;无 DDL、无数据迁移,回滚不涉及数据清理
- 前端配合:前端若已按新契约上线,后端回滚时需同步回退前端版本(新旧契约互不兼容)
12. 注意事项
- 前端 workaround 清理点:此前为候选机制写的 workaround 可全部删除——按 candidateKey 对齐行的本地映射、excludedCandidateKeys 的收集逻辑、对 candidateResolution=UNRESOLVED 行的特殊渲染、等待 pendingCandidateCount 归零的轮询/重试逻辑
- 行内确认状态直接用 settlementConfirmStatus:新模型下「行是否已确认」只看
settlementConfirmStatus,不要再拼接 completionState + candidateResolution 推断 - 保存即全量:局部更新场景也必须先 GET 拿全量、改完整体 PUT,缺行等于删行(未确认行)
- ID 一律字符串:入参 id / staffAssignmentId / itemIds 均传字符串形式;出参 id / staffAssignmentId 也是字符串,不要按 Number 解析
13. 关联 / 联系人
13.1 链接
- Issue: #6117
- PR: #6119
- Merge commit: 27ac77b500
13.2 联系人
- 后端负责人: @yst(腰苏图)