--- schema: "hl-changelog/v2" ticket: "6117" title: "导游/摄影核单去 EXCLUDED + 全量替换,确认状态统一 settlementConfirmStatus" consumer: "admin" change_type: "修改接口" author: "yst" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "ec7f2a9a" target_release: "" verified_at: "2026-08-21" status_note: "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。" updated_at: "2026-08-21" base: "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`) 响应级字段: | 字段 | 类型 | 说明 | |------|------|------| | 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} (无请求体) ``` 响应: ```json { "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)。 响应: ```json { "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 行为一致)。 **示例(典型成功:两行全量保存,一行直接置已确认)** 请求: ```json 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`。 请求: ```json PUT /v3/admin/order/12345/settlement/guide-fees { "items": [ { "candidateKey": "2026-08-03#11", "name": "导游甲", "amount": "500.00" } ] } ``` 响应: ```json { "code": 584128, "data": null, "msg": "导游或摄影费用请求字段不合法:导游费用明细不支持字段: candidateKey", "success": false } ``` **示例(业务失败:已确认行被全量替换遗漏)** 场景说明:库中行 9001 已 CONFIRMED,本次 items 只传了行 9002,相当于要删掉 9001。 响应: ```json { "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 的响应结构(确认后的最新全量)。 **示例(典型成功)** 请求: ```json POST /v3/admin/order/12345/settlement/guide-fees/confirm Authorization: Bearer {admin-token} { "itemIds": ["9002"] } ``` 响应: ```json { "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`):响应级字段与 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} (无请求体) ``` 响应: ```json { "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。) **示例(典型成功)** 请求: ```json 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 响应示例)。 **示例(业务失败:金额非法)** 请求: ```json PUT /v3/admin/order/12345/settlement/photographer-fees { "items": [ { "photographerName": "摄影甲", "feeType": "FOLLOW_SHOOT", "amount": "-50.00" } ] } ``` 响应: ```json { "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 的响应结构。 **示例(典型成功)** 请求: ```json 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](https://git.1814.love:8443/wx/HL/issues/6117) - **PR**: [#6119](https://git.1814.love:8443/wx/HL/pulls/6119) - **Merge commit**: [27ac77b500](https://git.1814.love:8443/wx/HL/commit/27ac77b500ff53496432a0e6e869dc96669e8f24) ### 13.2 联系人 - **后端负责人**: @yst(腰苏图)