# 📝【契约纠正·管理后台】对公转账不需要代收人 (#5120)

> **变更性质**：现有接口契约澄清 + 历史文档示例纠错｜**端类型**：管理后台｜**更新日期**：2026-07-21
>
> 本次没有发布新的后端字段、枚举或行为变更；下文说明接口已有的稳定契约。

## 1. 接口背景

管理后台在“登记线下收款”中选择“对公转账”后仍显示“代收人”，与当前接口契约不一致。对公转账不由某位员工代收，只需填写转账流水号；代收人仅在“报账人收款”渠道下需要选择。

2026-07-10 的历史通知曾在示例中给 `BANK_TRANSFER.collectors` 放入“公司账户”对象，该示例与实际响应不符，本次一并纠正为空数组。

## 2. 变更清单

| # | 方法 | 路径 | 通知类型 | 说明 |
|---|---|---|---|---|
| 1 | GET | `/v3/admin/order/{orderId}/payment/manual-receipt/options` | 契约澄清 | `BANK_TRANSFER.collectors` 始终为 `[]`；各渠道使用各自的候选项 |
| 2 | POST | `/v3/admin/order/{orderId}/payment/manual-receipt` | 契约澄清 | `collectorStaffId` 仅对 `DRIVER_CASH` 条件必填；`BANK_TRANSFER` 条件必填 `transferRef` |
| 3 | 文档 | `2026-07/10_4884_线下收款代收人-修改接口-管理后台.md` | 示例纠错 | 将对公转账的错误 `collectors` 对象改为 `[]` |

## 3. 接口详情

### 3.1 查询线下收款选项

- **方法与路径**：`GET /v3/admin/order/{orderId}/payment/manual-receipt/options`
- **使用场景**：打开登记线下收款表单时，查询当前订单可用的渠道、款项类型、代收人和收款方式。
- **认证**：需要管理后台 JWT。
- **幂等性**：幂等，只读查询。
- **限流**：无接口专属限流规则。

### 3.2 登记线下收款

- **方法与路径**：`POST /v3/admin/order/{orderId}/payment/manual-receipt`
- **使用场景**：按 options 当前返回的可用渠道和款项类型登记一笔线下收款。
- **认证**：需要管理后台 JWT。
- **幂等性**：非幂等，每次成功请求会新增一条收款记录。
- **限流**：无接口专属限流规则。

## 4. 接口入参

### 4.1 路径参数（两个接口通用）

| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `orderId` | Path | String(Long) | 是 | 订单 ID，按字符串处理 |

GET 接口无 Query 参数、无请求体。

### 4.2 POST 请求体

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| `channel` | String | 是 | 收款渠道 | `DRIVER_CASH` / `BANK_TRANSFER` / `CONSULTANT_COLLECTION` |
| `payType` | String | 是 | 款项类型 | 必须取 options 中当前渠道的 `allowedPayTypes` |
| `amount` | Decimal | 是 | 收款金额 | 最小 `0.01`，不能超过当前可收余额 |
| `receivedAt` | String(LocalDateTime) | 否 | 收款时间 | `yyyy-MM-dd'T'HH:mm:ss`；不传默认当前时间 |
| `transferRef` | String | 条件必填 | 对公转账流水号 | `BANK_TRANSFER` 必填，其他渠道不使用 |
| `receiptMethod` | String | 否 | 收款方式 | 取当前渠道 `receiptMethods[].value`；`BANK_TRANSFER` 为空 |
| `collectorStaffId` | String(Long) | 条件必填 | 代收人 assignmentId | **仅 `DRIVER_CASH` 必填**，且必须取当前渠道 `collectors[].collectorId` |
| `collectorType` | String | 否 | 实际代收人类型 | 不传时按 `channel` 推导；如传入，必须与渠道匹配 |
| `voucherUrls` | Array<String> | 否 | 凭证图片 URL 列表 | 可为空数组或不传 |
| `remark` | String | 否 | 备注 | 最长 500 字 |

### 4.3 渠道联动必填矩阵

| `channel` | `collectorStaffId` | `collectorType` | `transferRef` | 代收人规则 |
|---|---|---|---|---|
| `BANK_TRANSFER` | 不需要；误传也不作为员工代收人处理 | 可不传；如传只能为 `COMPANY_ACCOUNT` | **必填** | 不选择任何员工，公司账户是收款归属而非代收人候选项 |
| `CONSULTANT_COLLECTION` | 不需要 | 可不传；如传只能为 `CONSULTANT` | 不需要 | 使用订单定制师，不使用员工选择器 |
| `DRIVER_CASH` | **必填** | 可不传；如传只能为 `ORDER_STAFF` | 不需要 | 仅能选当前订单 options 返回的有效报账人 |

## 5. 出参（响应）

### 5.1 通用响应包装

| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | `200` 表示成功，其他值为业务错误码 |
| `message` | String | 结果或错误说明 |
| `data` | Object/null | 业务数据；失败时通常为 `null` |
| `success` | Boolean | 是否成功 |

### 5.2 GET options 的 `data`

| 字段 | 类型 | 说明 |
|---|---|---|
| `channels` | Array<ChannelOption> | 当前订单的线下收款渠道列表 |
| `channels[].channel` | String | 渠道枚举值 |
| `channels[].channelText` | String | 渠道展示文案 |
| `channels[].allowedPayTypes` | Array<String> | 当前订单状态下该渠道允许的款项类型；以本次响应为准 |
| `channels[].disabled` | Boolean | `true` 表示当前不可提交该渠道 |
| `channels[].disabledReason` | String/null | 禁用原因；可用时为 `null` |
| `channels[].collectors` | Array<CollectorOption> | **该渠道自己的代收人候选列表**；`BANK_TRANSFER` 为 `[]` |
| `channels[].receiptMethods` | Array<OptionItem> | 该渠道可选收款方式；`BANK_TRANSFER` 为 `[]` |
| `collectors[].collectorType` | String | 代收人类型 |
| `collectors[].collectorId` | String(Long) | `ORDER_STAFF` 为 assignmentId，`CONSULTANT` 为管理员 ID |
| `collectors[].collectorName` | String | 代收人姓名 |
| `collectors[].collectorRole` | String | 代收人角色值 |
| `collectors[].collectorRoleText` | String | 代收人角色文案 |
| `collectors[].defaultSelected` | Boolean | 是否默认选中 |
| `receiptMethods[].value` | String | 收款方式值 |
| `receiptMethods[].label` | String | 收款方式文案 |
| `receiptMethods[].defaultSelected` | Boolean | 是否默认选中 |

### 5.3 POST 的 `data`

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | String(Long) | 收款凭据 ID |
| `orderId` | String(Long) | 订单 ID |
| `channel` / `channelLabel` | String | 收款渠道值 / 文案 |
| `payType` / `payTypeLabel` | String | 款项类型值 / 文案 |
| `amount` | Decimal | 本次收款金额 |
| `receivedAt` | String(LocalDateTime) | 收款时间 |
| `collectorStaffId` / `collectorStaffName` | String(Long)/String/null | 仅 `DRIVER_CASH` 有值 |
| `collectorType` | String | 实际代收人类型 |
| `collectorAdminId` | String(Long)/null | `CONSULTANT_COLLECTION` 为定制师管理员 ID |
| `collectorName` / `collectorRole` | String | 代收归属快照名称 / 角色 |
| `transferRef` | String/null | 对公转账流水号，仅 `BANK_TRANSFER` 有值 |
| `receiptMethod` / `receiptMethodLabel` | String/null | 收款方式值 / 文案；`BANK_TRANSFER` 为空 |
| `voucherUrls` | Array<String> | 凭证图片 URL 列表 |
| `remark` | String/null | 备注 |
| `operatorName` | String | 登记人姓名 |
| `createTime` | String(LocalDateTime) | 登记时间 |
| `voided` | Boolean | 是否已撤销；新登记为 `false` |
| `voidedByName` / `voidedAt` / `voidReason` | String/null | 撤销信息；新登记时为 `null` |
| `paidAmountAfter` | Decimal | 登记后订单累计已付金额 |
| `payStatusAfter` | String | 登记后订单支付状态 |

## 6. 枚举 / 数据字典

### 6.1 `channel`

**所属字段**：`channel` / `channels[].channel`｜**类型**：String

| 值 | 中文 | 说明 |
|---|---|---|
| `BANK_TRANSFER` | 对公转账 | 无员工代收人，必须填 `transferRef` |
| `CONSULTANT_COLLECTION` | 定制师代收 | 使用订单定制师，不传 `collectorStaffId` |
| `DRIVER_CASH` | 报账人收款 | 仅允许尾款，必须从本渠道 `collectors` 选择代收人 |

### 6.2 `payType`

**所属字段**：`payType` / `channels[].allowedPayTypes[]`｜**类型**：String

| 值 | 中文 | 说明 |
|---|---|---|
| `DEPOSIT` | 订金 | 是否可登记以 options 当前返回为准 |
| `FULL` | 全款 | 是否可登记以 options 当前返回为准 |
| `BALANCE` | 尾款 | 是否可登记以 options 当前返回为准；`DRIVER_CASH` 只允许此值 |

> `BANK_TRANSFER` 的通用契约可支持 `DEPOSIT` / `FULL` / `BALANCE`，但具体订单当次能提交哪些值，必须以 options 的 `allowedPayTypes` 为准，不要将某个实例的 `BALANCE` 硬编码为全局规则。

### 6.3 `collectorType`

**所属字段**：`collectorType` / `collectors[].collectorType`｜**类型**：String

| 值 | 中文 | 匹配渠道 |
|---|---|---|
| `COMPANY_ACCOUNT` | 公司账户 | `BANK_TRANSFER` |
| `CONSULTANT` | 定制师 | `CONSULTANT_COLLECTION` |
| `ORDER_STAFF` | 订单工作人员 | `DRIVER_CASH` |

### 6.4 `receiptMethod`

**所属字段**：`receiptMethod` / `receiptMethods[].value`｜**类型**：String

| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT_TRANSFER` | 微信转账 | 人员代收渠道的当前默认字典值 |
| `CASH` | 现金收款 | 人员代收渠道的当前默认字典值 |

`BANK_TRANSFER.receiptMethods=[]`；该字典可扩展，实际可选值以 options 当次返回为准。

### 6.5 `payStatusAfter`

**所属字段**：POST 响应 `payStatusAfter`｜**类型**：String

| 值 | 中文 | 说明 |
|---|---|---|
| `UNPAID` | 未付款 | 尚未完成有效收款 |
| `DEPOSIT_PAID` | 已付订金 | 订金已收 |
| `FULLY_PAID` | 已付全款 | 应收金额已收齐 |

## 7. 错误码

| code | message / 含义 | 触发场景 |
|---|---|---|
| `520011` | 支付类型无效或与订单状态不匹配 | `payType` 不在当前 options 允许范围内 |
| `520401` | 收款渠道非法 | `channel` 不在三个渠道枚举中 |
| `520402` | 对公转账渠道必须填写转账流水号 | `BANK_TRANSFER` 未传 `transferRef` |
| `520403` | 报账人收款渠道必须指定代收人 | `DRIVER_CASH` 未传 `collectorStaffId` |
| `520404` | 代收人不属于本订单人员 | `collectorStaffId` 不是本订单有效人员 |
| `520407` | 订单已取消，不允许登记线下收款 | 已取消订单提交 POST |
| `520408` | 收款金额必须大于 0 | `amount < 0.01` |
| `520409` | 线下收款代收人类型非法 | `collectorType` 与 `channel` 不匹配 |
| `520410` | 报账人收款只能登记尾款 | `DRIVER_CASH` 提交 `DEPOSIT` 或 `FULL` |
| `520411` | 报账人收款必须选择本订单报账人 | 选中的订单人员不是报账人 |
| `520412` | 订单没有可用定制师，不能登记定制师代收 | `CONSULTANT_COLLECTION` 无可用定制师 |
| `520413` | 本次收款金额超过当前可收余额 | `amount` 大于当前可收金额 |

## 8. 示例（典型 + 边界 + 异常）

### 8.1 典型：查询选项，对公转账无代收人

**请求**：

```http
GET /v3/admin/order/2079454953641836546/payment/manual-receipt/options
Authorization: Bearer <admin-jwt>

无请求体
```

**响应**：

```json
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "channels": [
      {
        "channel": "CONSULTANT_COLLECTION",
        "channelText": "定制师代收",
        "allowedPayTypes": ["BALANCE"],
        "disabled": false,
        "disabledReason": null,
        "collectors": [
          {
            "collectorType": "CONSULTANT",
            "collectorId": "2037350531801993218",
            "collectorName": "张三",
            "collectorRole": "CONSULTANT",
            "collectorRoleText": "定制师",
            "defaultSelected": true
          }
        ],
        "receiptMethods": [
          {"value": "WECHAT_TRANSFER", "label": "微信转账", "defaultSelected": true},
          {"value": "CASH", "label": "现金收款", "defaultSelected": false}
        ]
      },
      {
        "channel": "BANK_TRANSFER",
        "channelText": "对公转账",
        "allowedPayTypes": ["BALANCE"],
        "disabled": false,
        "disabledReason": null,
        "collectors": [],
        "receiptMethods": []
      },
      {
        "channel": "DRIVER_CASH",
        "channelText": "报账人收款",
        "allowedPayTypes": [],
        "disabled": true,
        "disabledReason": "本订单暂无可代收报账人",
        "collectors": [],
        "receiptMethods": [
          {"value": "WECHAT_TRANSFER", "label": "微信转账", "defaultSelected": true},
          {"value": "CASH", "label": "现金收款", "defaultSelected": false}
        ]
      }
    ]
  },
  "success": true
}
```

### 8.2 边界：对公转账不传代收人

**场景说明**：`collectorStaffId` 和 `collectorType` 都不传；仅提交 options 当前允许的款项类型与对公转账流水号。

**请求**：

```http
POST /v3/admin/order/2079454953641836546/payment/manual-receipt
Authorization: Bearer <admin-jwt>
Content-Type: application/json

{
  "channel": "BANK_TRANSFER",
  "payType": "BALANCE",
  "amount": 100.00,
  "transferRef": "BANK202607210001",
  "voucherUrls": [],
  "remark": "客户对公转账"
}
```

**响应**：

```json
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "id": "2079600000000000001",
    "orderId": "2079454953641836546",
    "channel": "BANK_TRANSFER",
    "channelLabel": "对公转账",
    "payType": "BALANCE",
    "payTypeLabel": "尾款",
    "amount": 100.00,
    "receivedAt": "2026-07-21T15:30:00",
    "collectorStaffId": null,
    "collectorStaffName": null,
    "collectorType": "COMPANY_ACCOUNT",
    "collectorAdminId": null,
    "collectorName": "公司账户",
    "collectorRole": "COMPANY_ACCOUNT",
    "transferRef": "BANK202607210001",
    "receiptMethod": null,
    "receiptMethodLabel": null,
    "voucherUrls": [],
    "remark": "客户对公转账",
    "operatorName": "管理员",
    "createTime": "2026-07-21T15:30:00",
    "voided": false,
    "voidedByName": null,
    "voidedAt": null,
    "voidReason": null,
    "paidAmountAfter": 1600.00,
    "payStatusAfter": "DEPOSIT_PAID"
  },
  "success": true
}
```

### 8.3 异常：报账人收款未选择代收人

**请求**：

```http
POST /v3/admin/order/2079454953641836546/payment/manual-receipt
Authorization: Bearer <admin-jwt>
Content-Type: application/json

{
  "channel": "DRIVER_CASH",
  "payType": "BALANCE",
  "amount": 100.00,
  "receiptMethod": "CASH"
}
```

**响应**：

```json
{
  "code": 520403,
  "message": "报账人收款渠道必须指定代收人",
  "data": null,
  "success": false
}
```

## 9. 业务边界

- `collectors` 是每个 channel 自己的候选列表，不是所有渠道共用的必选列表。
- `BANK_TRANSFER`：`collectors=[]`，不传 `collectorStaffId`，必须传 `transferRef`；即使误传 `collectorStaffId`，响应中员工代收人 ID 仍为空。
- `CONSULTANT_COLLECTION`：不要提交 `collectorStaffId`；当订单没有可用定制师时，渠道禁用。
- `DRIVER_CASH`：仅允许 `BALANCE`，必须传当前订单有效报账人的 assignmentId。
- options 的 `allowedPayTypes` 随订单状态和可收余额变化。待支付订单中，对公转账/定制师代收可返回 `DEPOSIT`、`FULL`；非待支付且仍有可收余额时可返回 `BALANCE`。
- 渠道 `disabled=true` 或 `allowedPayTypes=[]` 时，当前不可提交该渠道。
- 已取消订单、无可收余额的订单不能登记线下收款。

## 10. 修改前后对比

> 本节对比的是“错误理解 / 错误文档示例”与“正确的现有契约”，不表示后端今日发布了新的接口变更。

| 项目 | 错误理解 / 历史错误示例 | 正确契约 |
|---|---|---|
| 对公转账的代收人候选 | `BANK_TRANSFER.collectors` 含“公司账户”对象 | `BANK_TRANSFER.collectors=[]` |
| `collectorStaffId` 字段 | 所有渠道都要选代收人，或该字段已从后端删除 | 字段仍保留，**仅 `DRIVER_CASH` 条件必填** |
| 对公转账必填项 | 代收人 | `transferRef` 转账流水号 |
| 定制师代收 | 复用员工代收人选择器并传 `collectorStaffId` | 不要传 `collectorStaffId`，使用订单定制师 |
| 对公转账款项类型 | 固定只能是某一种款项 | 以 options 当前返回的 `allowedPayTypes` 为准 |

## 11. 影响评估 / 回滚

### 11.1 影响评估

- **是否破坏向后兼容**：否。后端字段、枚举和行为没有变更。
- **前端是否必须同步上线**：是。已有页面在 `BANK_TRANSFER` 下显示代收人，需要按正确契约纠正。

### 11.2 回滚说明

- 本次仅修正通知文档，不涉及后端接口回滚。
- 若前端回滚渠道联动修正，对公转账将再次错误显示代收人。

## 12. 注意事项

- 选中 `BANK_TRANSFER` 时，隐藏代收人选择器，并清空从其他渠道切换前残留的 `collectorStaffId`。
- 选中 `BANK_TRANSFER` 时，显示并校验 `transferRef`，不要根据统一响应结构中“存在 `collectors` 字段”就认定代收人必选。
- 仅 `DRIVER_CASH` 把 `collectorStaffId` 设为必填，候选项取当前 channel 的 `collectors`。
- `CONSULTANT_COLLECTION` 不要复用 `DRIVER_CASH` 的员工代收人校验。
- 不要把测试订单中 `BANK_TRANSFER.allowedPayTypes=["BALANCE"]` 固化为全局规则；每次均以 options 返回为准。

## 13. 关联 / 联系人

### 13.1 关联

- **Issue**：[#5120](https://git.1814.love:8443/wx/HL/issues/5120)
- **后端 PR**：无（本次无后端代码变更）
- **后端 commit**：无（本次无后端代码变更）

### 13.2 联系人

- **后端负责人**：腰苏图
