---
schema: "hl-changelog/v2"
ticket: "5320"
title: "核单其他收支确认状态与项目类别"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "pi:019fadb7-dac9-74bf-9581-058835251208"
frontend_ref: "1444fc7f0bf34efaec0ee9f775f7d529b609b847"
target_release: "v2.1"
verified_at: "2026-07-29T20:43:00+08:00"
status_note: "管理后台已接入其他收入、其他支出新增编辑，显式提交确认状态，并按项目类别与票种规格启用字典提交 dictValue；pnpm checkpoint 全部通过。"
updated_at: "2026-07-29"
base: "dev-v3"
generated: "2026-07-29T09:19:00+08:00"
---

# 【⚠️ 修改接口·管理后台】核单其他收支确认状态与项目类别（#5320）

> **PR**: #5330 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-29 09:19

## 1. 接口背景

核单「其他收入」「其他支出」需要在保存时明确记录当前确认状态，不能再由后端统一改成已确认。四个保存接口新增必填字段 `settlementConfirmStatus`，调用方传 `UNCONFIRMED` 或 `CONFIRMED`，响应返回实际保存值。

其他收入同时收紧项目类别和票种/规格：

- `projectCategory` 必须提交 `settlement_other_income_project_category` 字典的启用 `dictValue`；
- `specification` 可空，非空时必须提交 `settlement_ticket_spec` 字典的启用 `dictValue`；
- 响应 `projectCategoryName` 返回项目类别字典的中文 `dictLabel`。

> **关键变化**：#5310 的“保存即确认”规则已被本次变更替代。保存接口不再强制写 `CONFIRMED`，调用方必须明确提交确认状态。此前删除的两个独立确认接口不恢复。

路径中的 `:orderId`、`:incomeId`、`:settlementId` 表示对应的路径参数。

## 变更接口

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 新增其他收入并原子创建订单增费 | POST | `/v3/admin/order/:orderId/settlement/other-incomes` | 修改 | `settlementConfirmStatus` 改为必填；项目类别和非空规格按启用字典校验 |
| 2 | 修改其他收入 | PUT | `/v3/admin/order/:orderId/settlement/other-incomes/:incomeId` | 修改 | `settlementConfirmStatus` 改为必填并保存传入值；项目类别和非空规格按启用字典校验 |
| 3 | 新增其他支出 | POST | `/v3/admin/order/:orderId/settlement/other-expenses` | 修改 | `settlementConfirmStatus` 改为必填并保存传入值 |
| 4 | 修改其他支出 | PUT | `/v3/admin/order/:orderId/settlement/other-expenses/:settlementId` | 修改 | `settlementConfirmStatus` 改为必填并保存传入值 |

## 3. 接口详情

### 3.1 新增其他收入

- **方法 + 路径**：`POST /v3/admin/order/:orderId/settlement/other-incomes`
- **接口名**：新增其他收入并原子创建订单增费
- **使用场景**：在核单其他收入页新增一条记录，并明确该记录当前是未确认还是已确认。
- **认证**：需要管理后台登录态；需要资金写入权限。
- **幂等性**：是；同一订单内 `requestId` 永久唯一。相同 `requestId` 且完整请求载荷一致时返回同一条记录；确认状态也是幂等载荷的一部分。
- **限流**：无接口专属限流。

#### 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID，必须大于 0 |

#### 请求体

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `requestId` | String | 是 | 客户端生成的稳定幂等请求 ID，同订单内永久唯一 | 非空，最长 64 字符 |
| `incomeDate` | String(date) | 是 | 收入日期，格式 `YYYY-MM-DD` | 非空 |
| `projectName` | String | 是 | 项目名称 | 非空，最长 100 字符 |
| `projectCategory` | String | 是 | 项目类别编码 | 必须是 `settlement_other_income_project_category` 的启用 `dictValue` |
| `specification` | String | 否 | 票种/规格 | 最长 100 字符；非空时必须是 `settlement_ticket_spec` 的启用 `dictValue` |
| `quantity` | Decimal | 是 | 数量 | >= 0，最多 8 位整数、4 位小数 |
| `unitPrice` | Decimal | 是 | 核算单价 | >= 0，最多 8 位整数、2 位小数 |
| `settlementAmount` | Decimal | 是 | 核算金额 | > 0，最多 8 位整数、2 位小数；必须等于 `quantity * unitPrice` 四舍五入到 2 位 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `settlementConfirmStatus` | String | 是 | 本次保存的确认状态 | `UNCONFIRMED` / `CONFIRMED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项；每项最长 1024 字符；必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 500 字符 |

#### 响应 `Result<SettlementOtherIncomeItemRespVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码，成功为 `200` |
| `message` | String | 响应消息，成功为“成功” |
| `data.id` | String | 其他收入 ID |
| `data.requestId` | String / null | 手工新增幂等请求 ID；自动投影记录为空 |
| `data.incomeDate` | String(date) | 收入日期 |
| `data.projectName` | String | 项目名称 |
| `data.projectCategory` | String | 项目类别 `dictValue` |
| `data.projectCategoryName` | String | 项目类别 `dictLabel`；标签缺失时回退为 `projectCategory` |
| `data.specification` | String / null | 票种/规格 `dictValue` |
| `data.quantity` | Decimal | 数量 |
| `data.unitPrice` | Decimal | 核算单价 |
| `data.settlementAmount` | Decimal | 核算金额 |
| `data.paymentMethod` | String | 付款类型 |
| `data.paymentMethodName` | String | 付款类型名称 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 实际保存的确认状态，与请求一致 |
| `data.settlementConfirmStatusName` | String | `未确认` / `已确认` |
| `data.remark` | String / null | 备注 |
| `data.sourceType` | String | 固定为 `ORDER_SURCHARGE` |
| `data.sourceTypeName` | String | 固定为“订单增费” |
| `data.sourceId` | String | 来源附加费 ID |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |

#### 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、确认状态/付款类型格式非法、凭证 URL 非法、金额不等于数量乘单价 |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不允许写入 |
| `584075` | 其他收入关联的附加费来源无效 | 无法得到有效来源记录 |
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | 金额关系不一致 |
| `584086` | 无权修改核单资金数据 | 当前账号不是允许写入资金数据的角色 |
| `584087` | requestId 已用于另一笔其他收入 | 同订单重复使用 `requestId`，但请求载荷不同 |
| `584089` | 核单或结算已完成，资金数据不可再修改 | 订单资金数据已锁定 |
| `584103` | 其他收入项目类别不在启用字典范围内 | `projectCategory` 不是启用 `dictValue` |
| `584104` | 其他收入票种/规格不在启用字典范围内 | 非空 `specification` 不是启用 `dictValue` |
| `584105` | 结算字典暂时不可用，请稍后重试 | 字典服务失败、空响应或无启用项 |
| `584106` | 确认状态非法 | 服务层收到非 `UNCONFIRMED` / `CONFIRMED`；公开接口通常先由参数校验返回 `400` |

#### 业务边界

- `settlementConfirmStatus` 必须显式提交；不传不会默认成 `CONFIRMED`。
- 传 `UNCONFIRMED` 时记录保存成功并返回未确认；提交核单前仍需改为 `CONFIRMED`。
- `projectCategory` 提交英文 `dictValue`，不能提交中文 `dictLabel`。
- `specification` 可省略或传 `null`；非空时必须是票种规格启用值。
- `requestId` 的重复请求必须连确认状态在内保持完整载荷一致，否则返回 `584087`。

#### 示例

##### 典型成功：保存为已确认

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-incomes
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "requestId": "oi-20260729-0001",
  "incomeDate": "2026-07-29",
  "projectName": "临时加收门票",
  "projectCategory": "TICKET",
  "specification": "成人票",
  "quantity": 2,
  "unitPrice": 120.00,
  "settlementAmount": 240.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "CONFIRMED",
  "voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
  "remark": "现场补收"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "99001",
    "requestId": "oi-20260729-0001",
    "incomeDate": "2026-07-29",
    "projectName": "临时加收门票",
    "projectCategory": "TICKET",
    "projectCategoryName": "门票/游玩项目",
    "specification": "成人票",
    "quantity": 2,
    "unitPrice": 120.00,
    "settlementAmount": 240.00,
    "paymentMethod": "CASH_PAID",
    "paymentMethodName": "现付",
    "voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
    "settlementConfirmStatus": "CONFIRMED",
    "settlementConfirmStatusName": "已确认",
    "remark": "现场补收",
    "sourceType": "ORDER_SURCHARGE",
    "sourceTypeName": "订单增费",
    "sourceId": "88001"
  },
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}
```

##### 边界成功：无规格、保存为未确认

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-incomes
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "requestId": "oi-20260729-0002",
  "incomeDate": "2026-07-29",
  "projectName": "其他收入",
  "projectCategory": "OTHER",
  "specification": null,
  "quantity": 0.0001,
  "unitPrice": 100.00,
  "settlementAmount": 0.01,
  "paymentMethod": "SIGNED",
  "settlementConfirmStatus": "UNCONFIRMED",
  "voucherUrls": [],
  "remark": null
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "99002",
    "requestId": "oi-20260729-0002",
    "incomeDate": "2026-07-29",
    "projectName": "其他收入",
    "projectCategory": "OTHER",
    "projectCategoryName": "其他",
    "specification": null,
    "quantity": 0.0001,
    "unitPrice": 100.00,
    "settlementAmount": 0.01,
    "paymentMethod": "SIGNED",
    "paymentMethodName": "签单",
    "voucherUrls": [],
    "settlementConfirmStatus": "UNCONFIRMED",
    "settlementConfirmStatusName": "未确认",
    "remark": null,
    "sourceType": "ORDER_SURCHARGE",
    "sourceTypeName": "订单增费",
    "sourceId": "88002"
  },
  "traceId": "b2c3d4e5-f6a7-8901",
  "success": true
}
```

##### 业务失败：项目类别不是启用字典值

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-incomes
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "requestId": "oi-20260729-0003",
  "incomeDate": "2026-07-29",
  "projectName": "自由文本类别",
  "projectCategory": "房差",
  "quantity": 1,
  "unitPrice": 100.00,
  "settlementAmount": 100.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "CONFIRMED"
}
```

**响应**：

```json
{
  "code": 584103,
  "message": "其他收入项目类别不在启用字典范围内",
  "data": null,
  "traceId": "c3d4e5f6-a7b8-9012",
  "success": false
}
```

### 3.2 修改其他收入

- **方法 + 路径**：`PUT /v3/admin/order/:orderId/settlement/other-incomes/:incomeId`
- **接口名**：修改其他收入；金额或项目名变化时原子冲销并重建订单增费
- **使用场景**：修改已有其他收入，或把记录的确认状态在 `UNCONFIRMED` 与 `CONFIRMED` 之间明确切换。
- **认证**：需要管理后台登录态；需要资金写入权限。
- **幂等性**：否。
- **限流**：无接口专属限流。

#### 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID，必须大于 0 |
| `incomeId` | Long | 是 | 其他收入 ID，必须大于 0 |

#### 请求体

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `incomeDate` | String(date) | 是 | 收入日期，格式 `YYYY-MM-DD` | 非空 |
| `projectName` | String | 是 | 项目名称 | 非空，最长 100 字符 |
| `projectCategory` | String | 是 | 项目类别编码 | 必须是 `settlement_other_income_project_category` 的启用 `dictValue` |
| `specification` | String | 否 | 票种/规格 | 最长 100 字符；非空时必须是 `settlement_ticket_spec` 的启用 `dictValue` |
| `quantity` | Decimal | 是 | 数量 | >= 0，最多 8 位整数、4 位小数 |
| `unitPrice` | Decimal | 是 | 核算单价 | >= 0，最多 8 位整数、2 位小数 |
| `settlementAmount` | Decimal | 是 | 核算金额 | > 0，最多 8 位整数、2 位小数；必须等于 `quantity * unitPrice` 四舍五入到 2 位 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `settlementConfirmStatus` | String | 是 | 本次保存的确认状态 | `UNCONFIRMED` / `CONFIRMED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项；每项最长 1024 字符；必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 500 字符 |

#### 响应 `Result<SettlementOtherIncomeItemRespVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码，成功为 `200` |
| `message` | String | 响应消息，成功为“成功” |
| `data.id` | String | 其他收入 ID |
| `data.requestId` | String / null | 原记录的幂等请求 ID |
| `data.incomeDate` | String(date) | 收入日期 |
| `data.projectName` | String | 项目名称 |
| `data.projectCategory` | String | 项目类别 `dictValue` |
| `data.projectCategoryName` | String | 项目类别 `dictLabel`；标签缺失时回退为 `projectCategory` |
| `data.specification` | String / null | 票种/规格 `dictValue` |
| `data.quantity` | Decimal | 数量 |
| `data.unitPrice` | Decimal | 核算单价 |
| `data.settlementAmount` | Decimal | 核算金额 |
| `data.paymentMethod` | String | 付款类型 |
| `data.paymentMethodName` | String | 付款类型名称 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 实际保存的确认状态，与请求一致 |
| `data.settlementConfirmStatusName` | String | `未确认` / `已确认` |
| `data.remark` | String / null | 备注 |
| `data.sourceType` | String | 固定为 `ORDER_SURCHARGE` |
| `data.sourceTypeName` | String | 固定为“订单增费” |
| `data.sourceId` | String | 来源附加费 ID |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |

#### 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、确认状态/付款类型格式非法、凭证 URL 非法、金额不一致 |
| `584073` | 其他收入不存在或不属于当前订单 | `incomeId` 不存在或不属于 `orderId` |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不允许写入 |
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | 金额关系不一致 |
| `584086` | 无权修改核单资金数据 | 当前账号不是允许写入资金数据的角色 |
| `584089` | 核单或结算已完成，资金数据不可再修改 | 订单资金数据已锁定 |
| `584103` | 其他收入项目类别不在启用字典范围内 | `projectCategory` 不是启用 `dictValue` |
| `584104` | 其他收入票种/规格不在启用字典范围内 | 非空 `specification` 不是启用 `dictValue` |
| `584105` | 结算字典暂时不可用，请稍后重试 | 字典服务失败、空响应或无启用项 |
| `584106` | 确认状态非法 | 服务层收到非 `UNCONFIRMED` / `CONFIRMED`；公开接口通常先由参数校验返回 `400` |

#### 业务边界

- PUT 是完整字段保存，不能只传 `settlementConfirmStatus`；请求体中的其他必填字段必须一起提交。
- 传 `UNCONFIRMED` 会把该记录保存为未确认，不会被后端改回 `CONFIRMED`。
- 编辑旧自由文本项目类别时，必须改为 7 个启用 `dictValue` 之一。
- 修改项目名称或金额会同步更新关联订单增费，但不改变确认状态的“按请求保存”规则。

#### 示例

##### 典型成功：从未确认改为已确认

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-incomes/99002
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "incomeDate": "2026-07-29",
  "projectName": "其他收入",
  "projectCategory": "OTHER",
  "specification": null,
  "quantity": 1,
  "unitPrice": 88.00,
  "settlementAmount": 88.00,
  "paymentMethod": "COMPANY_PAID",
  "settlementConfirmStatus": "CONFIRMED",
  "voucherUrls": [],
  "remark": "已复核"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "99002",
    "requestId": "oi-20260729-0002",
    "incomeDate": "2026-07-29",
    "projectName": "其他收入",
    "projectCategory": "OTHER",
    "projectCategoryName": "其他",
    "specification": null,
    "quantity": 1,
    "unitPrice": 88.00,
    "settlementAmount": 88.00,
    "paymentMethod": "COMPANY_PAID",
    "paymentMethodName": "公司付款",
    "voucherUrls": [],
    "settlementConfirmStatus": "CONFIRMED",
    "settlementConfirmStatusName": "已确认",
    "remark": "已复核",
    "sourceType": "ORDER_SURCHARGE",
    "sourceTypeName": "订单增费",
    "sourceId": "88002"
  },
  "traceId": "d4e5f6a7-b8c9-0123",
  "success": true
}
```

##### 边界成功：主动改回未确认

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-incomes/99001
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "incomeDate": "2026-07-29",
  "projectName": "临时加收门票",
  "projectCategory": "TICKET",
  "specification": "成人票",
  "quantity": 2,
  "unitPrice": 120.00,
  "settlementAmount": 240.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "UNCONFIRMED",
  "voucherUrls": [],
  "remark": "金额待复核"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "99001",
    "requestId": "oi-20260729-0001",
    "incomeDate": "2026-07-29",
    "projectName": "临时加收门票",
    "projectCategory": "TICKET",
    "projectCategoryName": "门票/游玩项目",
    "specification": "成人票",
    "quantity": 2,
    "unitPrice": 120.00,
    "settlementAmount": 240.00,
    "paymentMethod": "CASH_PAID",
    "paymentMethodName": "现付",
    "voucherUrls": [],
    "settlementConfirmStatus": "UNCONFIRMED",
    "settlementConfirmStatusName": "未确认",
    "remark": "金额待复核",
    "sourceType": "ORDER_SURCHARGE",
    "sourceTypeName": "订单增费",
    "sourceId": "88001"
  },
  "traceId": "e5f6a7b8-c9d0-1234",
  "success": true
}
```

##### 业务失败：规格不是启用字典值

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-incomes/99001
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "incomeDate": "2026-07-29",
  "projectName": "临时加收门票",
  "projectCategory": "TICKET",
  "specification": "儿童票",
  "quantity": 1,
  "unitPrice": 80.00,
  "settlementAmount": 80.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "CONFIRMED"
}
```

**响应**：

```json
{
  "code": 584104,
  "message": "其他收入票种/规格不在启用字典范围内",
  "data": null,
  "traceId": "f6a7b8c9-d0e1-2345",
  "success": false
}
```

### 3.3 新增其他支出

- **方法 + 路径**：`POST /v3/admin/order/:orderId/settlement/other-expenses`
- **接口名**：新增其他支出
- **使用场景**：在核单其他支出页新增一条记录，并明确保存为未确认或已确认。
- **认证**：需要管理后台登录态；需要资金写入权限。
- **幂等性**：否。
- **限流**：无接口专属限流。

#### 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID，必须大于 0 |

#### 请求体

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expenseType` | String | 是 | 支出类型 | `FUEL` / `TOLL` / `PARKING` / `RENTAL` / `MAINTENANCE` / `OTHER` |
| `projectName` | String | 是 | 项目名称 | 非空，最长 200 字符 |
| `expenseDate` | String(date) | 否 | 发生日期，格式 `YYYY-MM-DD` | 可空 |
| `actualAmount` | Decimal | 是 | 实际金额 | >= 0，最多 8 位整数、2 位小数 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `settlementConfirmStatus` | String | 是 | 本次保存的确认状态 | `UNCONFIRMED` / `CONFIRMED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项；每项非空、最长 1024 字符；必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 512 字符 |

#### 响应 `Result<SettlementOtherExpenseRespVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码，成功为 `200` |
| `message` | String | 响应消息，成功为“成功” |
| `data.id` | String | 其他支出 ID |
| `data.expenseType` | String | 支出类型 |
| `data.projectName` | String | 项目名称 |
| `data.expenseDate` | String(date) / null | 发生日期 |
| `data.actualAmount` | String | 实际金额，按字符串返回 |
| `data.paymentMethod` | String | 付款类型 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 实际保存的确认状态，与请求一致 |
| `data.remark` | String / null | 备注 |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |

#### 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、确认状态/支出类型/付款类型格式非法、金额或凭证格式非法 |
| `584086` | 无权修改核单资金数据 | 当前账号不是允许写入资金数据的角色 |
| `584089` | 核单或结算已完成，资金数据不可再修改 | 订单资金数据已锁定 |
| `584095` | 其他支出字段超出允许范围 | 支出类型、项目名、金额或备注超出业务允许范围 |
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量、协议或单项长度不合法 |
| `584106` | 确认状态非法 | 服务层收到非 `UNCONFIRMED` / `CONFIRMED`；公开接口通常先由参数校验返回 `400` |
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不允许写入 |

#### 业务边界

- `settlementConfirmStatus` 必须显式提交；不传不会默认成 `CONFIRMED`。
- `actualAmount=0.00` 允许保存。
- `expenseDate` 可为 `null`。
- 传 `UNCONFIRMED` 时记录保存成功，但提交核单前仍需通过 PUT 改为 `CONFIRMED`。

#### 示例

##### 典型成功：保存为未确认

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-expenses
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "TOLL",
  "projectName": "过路费",
  "expenseDate": "2026-07-29",
  "actualAmount": 50.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "UNCONFIRMED",
  "voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
  "remark": "金额待复核"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "801",
    "expenseType": "TOLL",
    "projectName": "过路费",
    "expenseDate": "2026-07-29",
    "actualAmount": "50.00",
    "paymentMethod": "CASH_PAID",
    "voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
    "settlementConfirmStatus": "UNCONFIRMED",
    "remark": "金额待复核"
  },
  "traceId": "07b8c9d0-e1f2-3456",
  "success": true
}
```

##### 边界成功：0 元、无日期、保存为已确认

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-expenses
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "OTHER",
  "projectName": "0元备注支出",
  "expenseDate": null,
  "actualAmount": 0.00,
  "paymentMethod": "SIGNED",
  "settlementConfirmStatus": "CONFIRMED",
  "voucherUrls": [],
  "remark": null
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "802",
    "expenseType": "OTHER",
    "projectName": "0元备注支出",
    "expenseDate": null,
    "actualAmount": "0.00",
    "paymentMethod": "SIGNED",
    "voucherUrls": [],
    "settlementConfirmStatus": "CONFIRMED",
    "remark": null
  },
  "traceId": "18c9d0e1-f2a3-4567",
  "success": true
}
```

##### 参数失败：未提交确认状态

**请求**：

```http
POST /v3/admin/order/60001/settlement/other-expenses
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "PARKING",
  "projectName": "停车费",
  "actualAmount": 30.00,
  "paymentMethod": "CASH_PAID"
}
```

**响应**：

```json
{
  "code": 400,
  "message": "确认状态不能为空",
  "data": null,
  "traceId": "29d0e1f2-a3b4-5678",
  "success": false
}
```

### 3.4 修改其他支出

- **方法 + 路径**：`PUT /v3/admin/order/:orderId/settlement/other-expenses/:settlementId`
- **接口名**：修改其他支出
- **使用场景**：修改已有其他支出，或明确切换该记录的确认状态。
- **认证**：需要管理后台登录态；需要资金写入权限。
- **幂等性**：否。
- **限流**：无接口专属限流。

#### 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID，必须大于 0 |
| `settlementId` | Long | 是 | 其他支出 ID，必须大于 0 |

#### 请求体

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expenseType` | String | 是 | 支出类型 | `FUEL` / `TOLL` / `PARKING` / `RENTAL` / `MAINTENANCE` / `OTHER` |
| `projectName` | String | 是 | 项目名称 | 非空，最长 200 字符 |
| `expenseDate` | String(date) | 否 | 发生日期，格式 `YYYY-MM-DD` | 可空 |
| `actualAmount` | Decimal | 是 | 实际金额 | >= 0，最多 8 位整数、2 位小数 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `settlementConfirmStatus` | String | 是 | 本次保存的确认状态 | `UNCONFIRMED` / `CONFIRMED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项；每项非空、最长 1024 字符；必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 512 字符 |

#### 响应 `Result<SettlementOtherExpenseRespVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码，成功为 `200` |
| `message` | String | 响应消息，成功为“成功” |
| `data.id` | String | 其他支出 ID |
| `data.expenseType` | String | 支出类型 |
| `data.projectName` | String | 项目名称 |
| `data.expenseDate` | String(date) / null | 发生日期 |
| `data.actualAmount` | String | 实际金额，按字符串返回 |
| `data.paymentMethod` | String | 付款类型 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 实际保存的确认状态，与请求一致 |
| `data.remark` | String / null | 备注 |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |

#### 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、确认状态/支出类型/付款类型格式非法、金额或凭证格式非法 |
| `584086` | 无权修改核单资金数据 | 当前账号不是允许写入资金数据的角色 |
| `584089` | 核单或结算已完成，资金数据不可再修改 | 订单资金数据已锁定 |
| `584091` | 其他支出不存在或不属于当前订单 | `settlementId` 不存在或不属于 `orderId` |
| `584095` | 其他支出字段超出允许范围 | 支出类型、项目名、金额或备注超出业务允许范围 |
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量、协议或单项长度不合法 |
| `584106` | 确认状态非法 | 服务层收到非 `UNCONFIRMED` / `CONFIRMED`；公开接口通常先由参数校验返回 `400` |
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不允许写入 |

#### 业务边界

- PUT 是完整字段保存，不能只传 `settlementConfirmStatus`。
- 请求传 `UNCONFIRMED` 时，原先已确认的记录也会按本次请求保存为未确认。
- `actualAmount` 在响应中是字符串，不是 JSON 数字。
- 不存在或不属于当前订单的 `settlementId` 返回 `584091`。

#### 示例

##### 典型成功：修改并确认

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-expenses/801
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "PARKING",
  "projectName": "停车费",
  "expenseDate": "2026-07-29",
  "actualAmount": 35.00,
  "paymentMethod": "COMPANY_PAID",
  "settlementConfirmStatus": "CONFIRMED",
  "voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
  "remark": "已复核"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "801",
    "expenseType": "PARKING",
    "projectName": "停车费",
    "expenseDate": "2026-07-29",
    "actualAmount": "35.00",
    "paymentMethod": "COMPANY_PAID",
    "voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
    "settlementConfirmStatus": "CONFIRMED",
    "remark": "已复核"
  },
  "traceId": "3ae1f2a3-b4c5-6789",
  "success": true
}
```

##### 边界成功：改回未确认

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-expenses/801
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "PARKING",
  "projectName": "停车费",
  "expenseDate": "2026-07-29",
  "actualAmount": 35.00,
  "paymentMethod": "COMPANY_PAID",
  "settlementConfirmStatus": "UNCONFIRMED",
  "voucherUrls": [],
  "remark": "凭证待补"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "id": "801",
    "expenseType": "PARKING",
    "projectName": "停车费",
    "expenseDate": "2026-07-29",
    "actualAmount": "35.00",
    "paymentMethod": "COMPANY_PAID",
    "voucherUrls": [],
    "settlementConfirmStatus": "UNCONFIRMED",
    "remark": "凭证待补"
  },
  "traceId": "4bf2a3b4-c5d6-7890",
  "success": true
}
```

##### 业务失败：其他支出不存在

**请求**：

```http
PUT /v3/admin/order/60001/settlement/other-expenses/99999
Authorization: Bearer <admin-token>
Content-Type: application/json
```

```json
{
  "expenseType": "OTHER",
  "projectName": "不存在记录",
  "actualAmount": 10.00,
  "paymentMethod": "CASH_PAID",
  "settlementConfirmStatus": "CONFIRMED"
}
```

**响应**：

```json
{
  "code": 584091,
  "message": "其他支出不存在或不属于当前订单",
  "data": null,
  "traceId": "5ca3b4c5-d6e7-8901",
  "success": false
}
```

## 6. 枚举 / 数据字典

### 6.1 `settlementConfirmStatus`

**所属字段**：四个接口请求和响应的 `settlementConfirmStatus` | **类型**：String | **必填**：是

| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 保存成功但仍阻塞核单提交，需要后续 PUT 改为已确认 |
| `CONFIRMED` | 已确认 | 保存成功且该记录确认状态为已确认 |

### 6.2 `settlement_other_income_project_category`

**所属字段**：其他收入请求 `projectCategory`、响应 `projectCategory/projectCategoryName`

| `dictValue` | `dictLabel` | 说明 |
|-------------|-------------|------|
| `HOTEL` | 住宿 | 住宿类其他收入 |
| `TICKET` | 门票/游玩项目 | 门票或游玩项目类其他收入 |
| `MEAL` | 餐食 | 餐食类其他收入 |
| `VEHICLE` | 车辆 | 车辆类其他收入 |
| `GUIDE` | 导游 | 导游类其他收入 |
| `PHOTOGRAPHER` | 摄影 | 摄影类其他收入 |
| `OTHER` | 其他 | 以上类别以外的其他收入 |

加载接口：

`GET /admin/dict/data/settlement_other_income_project_category`

展示 `dictLabel`，保存提交 `dictValue`。

### 6.3 `settlement_ticket_spec`

**所属字段**：其他收入请求/响应 `specification` | **类型**：String | **必填**：否

| `dictValue` | `dictLabel` | 说明 |
|-------------|-------------|------|
| `成人票` | 成人票 | 当前启用票种/规格 |

加载接口：

`GET /admin/dict/data/settlement_ticket_spec`

后续增加启用项时，接口会返回新项；保存时提交选中项的 `dictValue`。

### 6.4 `paymentMethod`

**所属字段**：四个接口请求/响应 `paymentMethod`

| 值 | 中文 |
|----|------|
| `CASH_PAID` | 现付 |
| `COMPANY_PAID` | 公司付款 |
| `SIGNED` | 签单 |

### 6.5 `expenseType`

**所属字段**：其他支出请求/响应 `expenseType`

| 值 | 中文 |
|----|------|
| `FUEL` | 油费 |
| `TOLL` | 过路费 |
| `PARKING` | 停车费 |
| `RENTAL` | 租赁费 |
| `MAINTENANCE` | 维修保养费 |
| `OTHER` | 其他 |

### 6.6 `sourceType`

**所属字段**：其他收入响应 `sourceType`

| 值 | 中文 |
|----|------|
| `ORDER_SURCHARGE` | 订单增费 |

## 验证证据

- merge commit `7477b03963` 中的 `SettlementControllerTest` 覆盖四个保存接口显式接收
  `UNCONFIRMED` / `CONFIRMED`，以及缺失或非法确认状态返回参数错误。
- `SettlementDictValueValidatorTest` 覆盖项目类别、可选票种规格、确认状态、禁用字典项、
  空字典响应及 Feign 异常。
- hl-user-service 的迁移结构测试与 MySQL 迁移测试覆盖 7 个项目类别值、固定 ID、
  重复执行和冲突失败。
- changelog consumer gate、文件名校验、frontmatter 校验及仓库 46 项自动化测试均已通过。
- 本次没有新增或修改 Gateway 路由，`gateway_status=not_required`。

## 9. 业务边界

- 四个接口都必须显式提交 `settlementConfirmStatus`，且后端按请求值保存。
- `UNCONFIRMED` 记录可以保存，但提交核单时仍会被未确认门禁拦截。
- #5310 删除的
  `POST /v3/admin/order/:orderId/settlement/other-incomes/confirm`
  和
  `POST /v3/admin/order/:orderId/settlement/other-expenses/confirm`
  不恢复；确认状态通过对应 PUT 完整保存。
- 其他收入 `projectCategory` 只接受 7 个启用项目类别值。
- 其他收入 `specification` 可空；非空时只接受 `settlement_ticket_spec` 启用值。
- 字典不可用时其他收入 POST/PUT 失败，不会绕过校验写入。

## 10. 修改前后对比

### 10.1 字段级对比

| 字段 | 改前 | 改后 |
|------|------|------|
| `settlementConfirmStatus`（四个请求） | 无该请求字段；保存后强制 `CONFIRMED` | 必填，`UNCONFIRMED` / `CONFIRMED`，按请求保存 |
| `projectCategory`（其他收入） | 非空自由文本，最长 64 字符 | 必须是 `settlement_other_income_project_category` 启用 `dictValue` |
| `projectCategoryName`（其他收入响应） | 通常与 `projectCategory` 原值相同 | 返回项目类别 `dictLabel`，缺失时回退原值 |
| `specification`（其他收入） | 可选自由文本，最长 100 字符 | 可空；非空时必须是 `settlement_ticket_spec` 启用 `dictValue` |

### 10.2 行为级对比

| 行为 | 改前 | 改后 |
|------|------|------|
| 新增/修改其他收入 | 保存即确认 | 调用方决定保存为未确认或已确认 |
| 新增/修改其他支出 | 保存即确认 | 调用方决定保存为未确认或已确认 |
| 切换确认状态 | 保存接口总是写 `CONFIRMED` | PUT 完整保存时传目标状态 |
| 其他收入项目类别 | 可提交自由文本 | 按启用字典强校验 |
| 其他收入规格 | 可提交自由文本 | 非空时按票种规格启用字典强校验 |
| 独立确认接口 | #5310 已删除 | 仍保持删除，不恢复 |

## 11. 影响评估

- **是否破坏向后兼容**：是。四个请求新增必填字段；其他收入自由文本项目类别及非字典规格不再接受。
- **前端是否必须同步上线**：是。调用四个接口时必须提交 `settlementConfirmStatus`；其他收入必须改用项目类别和票种规格字典值。
- **前端读取兼容**：响应结构不新增字段，但 `projectCategoryName` 从原始值改为字典中文标签。

## 12. 注意事项

- 不能沿用“保存成功就一定已确认”的前端判断；以响应 `settlementConfirmStatus` 为准。
- PUT 是完整保存，不是确认状态局部更新；切换状态时必须提交该接口全部必填字段。
- 其他收入项目类别展示中文 `dictLabel`，请求提交英文 `dictValue`。
- 票种/规格继续使用 `settlement_ticket_spec`，不要使用项目类别字典填充。
- 旧记录如果保存了自由文本 `projectCategory` 或非字典 `specification`，再次编辑时必须先转换为当前启用字典值。

## 13. 关联 / 联系人

### 13.1 链接

- **Issue**: [#5320](https://git.1814.love:8443/wx/HL/issues/5320)
- **PR**: [#5330](https://git.1814.love:8443/wx/HL/pulls/5330)
- **Merge commit**: [7477b03963](https://git.1814.love:8443/wx/HL/commit/7477b0396352e7591e78a5d5ab9b080a640187d7)
- **前序变更**: [#5310](https://git.1814.love:8443/wx/HL/issues/5310) / [#5313](https://git.1814.love:8443/wx/HL/pulls/5313)

### 13.2 联系人

- **后端负责人**: @yst
