hl-api-changelog/changelogs-v2/2026-08/21_6117_导游摄影核单确认状态统一-修改接口-管理后台.md
Mimingguang d2eb4cb817
一些检查失败了
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): #6117 前端 verified(mmg, ref ec7f2a9a)
2026-08-21 12:00:11 +08:00

729 行
29 KiB
Markdown

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

---
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-v3merge commit 27ac77b500,测试服已验证。破坏性变化导游/摄影核单废弃「按天候选 + EXCLUDED」机制改全量替换语义;保存入参删 candidateKey/completionState/sourceResolution/excludedCandidateKeys,传旧字段一律 400584128;查询出参删 candidateKey/completionState/candidateResolution/sourceActive/pendingCandidateCount;确认状态统一 settlementConfirmStatusUNCONFIRMED/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<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 | 费用明细 IDLong 序列化为字符串);未落库的预填草稿行为 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<String> | 凭证 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<ItemVO> | ✅ | 导游费用明细**全量集合**(当前应存在的全部行) | 最多 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<String> | ❌ | 凭证 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<String> | ✅ | 待确认的费用明细 ID 列表,JSON 中每项为字符串 | 1 至 200 条;ID 不存在或不属于本订单及导游角色报 584111 |
同样走严格模式,多传字段报 584128msg 形如 `导游费用确认请求不支持字段: 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<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}
(无请求体)
```
响应:
```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 | 有,候选行标识 | **删除**,传了报 400584128 |
| completionState | 有NEEDS_INPUT/COMPLETE/EXCLUDED | **删除**,传了报 400584128 |
| sourceResolution | 有,来源处理结果 | **删除**,传了报 400584128 |
| settlementConfirmStatus | 无 | **新增**,非必填,UNCONFIRMED/CONFIRMED,缺省按 UNCONFIRMED |
保存入参PUT,请求体顶层
| 字段 | 改前 | 改后 |
|------|------|------|
| excludedCandidateKeys | 有,排除候选 key 清单 | **删除**,传了报 400584128 |
查询出参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(腰苏图)