diff --git a/changelogs-v2/2026-09/17_7396_应付款四入口接团期住宿与付款身份金额对账-修改接口-管理后台.md b/changelogs-v2/2026-09/17_7396_应付款四入口接团期住宿与付款身份金额对账-修改接口-管理后台.md new file mode 100644 index 00000000..763fba97 --- /dev/null +++ b/changelogs-v2/2026-09/17_7396_应付款四入口接团期住宿与付款身份金额对账-修改接口-管理后台.md @@ -0,0 +1,1140 @@ +--- +schema: "hl-changelog/v2" +ticket: "7396" +title: "应付款四入口接团期住宿 + 团期住宿付款身份金额对账 + 新增撤销批准" +consumer: "admin" +author: "jw(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端交付(hl-finance,随 hl-order-service-v3 部署)。4 个应付款读接口返回扩大(新 sourceType=GROUP_BATCH_STAY、sourceRefKey、金额对账字段、统计新字段与 OVERPAID 状态),3 个创建口新增入参 sourceRefKey 与 598812/598813 两个新错误码,草稿编辑口新增归属与额度守卫,新增 PUT /admin/finance/payments/{id}/revoke-approval。网关前缀 /admin/finance/payments 已存在,无需新路由。前端需接新 sourceType / 新字段 / 新状态 / 新错误码 / 撤销批准按钮。" +updated_at: "2026-09-17" +base: "dev-v3" +--- + +# finance: 应付款四入口接团期住宿 + 团期住宿付款身份金额对账 + 撤销批准 + +> **存放目录**: changelogs-v2/{YYYY-MM}/(管理后台,二期 finance) +> +> **服务**: hl-order-service-v3(hl-finance 模块同进程) +> **PR**: #7866 +> **Issue**: #7396 +> **日期**: 2026-09-17 +> **影响范围**: 应付款建议(按订单 / 按供应商)、应付款统计(按团 / 按供应商)、付款单创建(单笔 / 按团批量 / 按供应商批量)、草稿编辑;新增撤销批准 + +--- + +## 关键变化(给前端 mmg 的一句话) + +1. **团期住宿进应付款了**:团期房务(整团订房 + 系统分房到户)的酒店应付,现在在四个入口都能看到,行的 `sourceType` 为 **`GROUP_BATCH_STAY`**,并带 **`sourceRefKey`**(付款身份业务键)。勾选建单时 **`sourceRefKey` 必须原样回传**,`sourceRefId` 回传行上的 `sourceId`。 +2. **团期住宿按金额对账防重**:同一付款身份(同订单、同晚、同酒店、同房型)已建单后,建议行 `alreadyGenerated=true`;若应付金额变了,`amountChanged=true`、`diffAmount` 给出差额(正数可再建差额行,负数需先删除 / 驳回 / 撤销批准或等冲正)。 +3. **新增两个错误码**:`598812`(已付超出当前应付,需财务冲正)、`598813`(未付占用超出当前应付,提示里列出需处理的单号与状态)。 +4. **统计口径变化**:已申请 / 已付改为按付款明细聚合;新增 `refundedAmount` / `netPaidAmount` / `overpaidAmount` / `diffAmount`;`owedAmount` 不再为负;状态新增 **`OVERPAID`**;按供应商统计新增 `unattributedPaidAmount`。 +5. **新增撤销批准** `PUT /admin/finance/payments/{id}/revoke-approval`:已批准未付款的单回到待提交草稿,之后可删除或编辑重提。 +6. **编辑草稿收紧**:有业务来源的草稿禁止改供应商 / 订单(598808);多明细草稿金额必须等于明细合计(598805);团期住宿草稿改金额受额度守卫(598812 / 598813)。 + +--- + +## 一、背景 + +团期房务把住宿从「逐户配房」改成「整团按日订房 + 系统分房到户」后,应付款的三个批量入口(按供应商建议、按团统计、按供应商统计)只读旧配房表,团期住宿应付在这三处恒为 0;按订单建议也没有接入。同时团期分房行 ID 会在人工微调时换新,原「按来源主键判重」可被绕过而重复申请。本次:四入口共用同一读取层;团期住宿以业务键 `{orderId}:{stayDate}:{hotelId}:{roomTypeId}` 作付款身份,并按金额对账防重;统计改从付款明细聚合,已付款不会再算出负欠付。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 撤销批准 | PUT | `/admin/finance/payments/{id}/revoke-approval` | 新增 | APPROVED → PENDING,条件更新防并发,记审核流水 UN_APPROVE | +| 2 | 应付款建议(按订单) | GET | `/admin/finance/payments/suggestions` | 修改 | 新增团期住宿行与孤儿行;新增 sourceRefKey 与金额对账字段 | +| 3 | 应付款建议(按供应商跨团) | GET | `/admin/finance/payments/suggestions/by-supplier` | 修改 | 同上;已取消但仍有活跃团期占用的订单并入候选 | +| 4 | 应付款统计(按团) | GET | `/admin/finance/payments/stats/by-team` | 修改 | 该付含团期住宿;已申请/已付改按明细聚合;新字段与 OVERPAID | +| 5 | 应付款统计(按供应商) | GET | `/admin/finance/payments/stats/by-supplier` | 修改 | 同上;新增 unattributedPaidAmount | +| 6 | 申请付款(单笔) | POST | `/admin/finance/payments` | 修改 | 新增入参 sourceRefKey;带来源时 orderId 必填;团期住宿金额对账 | +| 7 | 勾选批量生成(按团) | POST | `/admin/finance/payments/batch-create` | 修改 | 明细新增 sourceRefKey;团期住宿金额对账 | +| 8 | 按供应商跨团合并建单 | POST | `/admin/finance/payments/batch-create-by-supplier` | 修改 | 明细新增 sourceRefKey;团期住宿金额对账 | +| 9 | 编辑付款草稿 | PUT | `/admin/finance/payments/{id}` | 修改 | 有来源草稿禁改供应商/订单;多明细金额守恒;团期住宿额度守卫 | + +--- + +## 三、接口详情 + +### 1. 撤销批准 `PUT /admin/finance/payments/{id}/revoke-approval` + +**VO**: `PaymentRevokeApprovalReqVO → Result` + +#### 使用场景 + +付款单已批准(APPROVED)、出纳尚未付款时,因应付金额减少等原因需要改单:先撤销批准回到草稿(PENDING),再删除或编辑后重新提交。出纳并发付款时两者只会有一个成功。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | 是 | - | 付款单 ID | +| reason | Body | String | 否 | ≤512 字符;请求体整体可省略 | 撤销原因,记入审核流水 opinion | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | Integer | 200 成功 | +| data | Void | 恒为 null | + +#### 请求示例 + +```json +{ + "reason": "团期减房 1 间,撤销后改单" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无查询数据;成功后 `GET /admin/finance/payments/{id}/review-logs` 追加一条 `action=UN_APPROVE`、`fromStatus=APPROVED`、`toStatus=PENDING` 的记录。 + +#### 错误响应 + +```json +{ + "code": 598802, + "message": "应付单状态非法,当前状态不允许此操作", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598801, + "message": "应付单不存在", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 仅 `APPROVED` 可撤销;`PENDING` / `SUBMITTED` / `REJECTED` / `PAID` 一律 598802。 +- 条件更新(仅当状态仍为 APPROVED 才改):与出纳付款并发时恰好一个成功,已付款的单绝不会被改回草稿。 +- 撤销后单据退出出纳待付款队列;回到 PENDING 后可删除(`DELETE /admin/finance/payments/{id}`)或编辑后重新提交。 +- 同事务写审核流水,流水写失败整体回滚。 +- 权限口径与批准接口一致。 + +--- + +### 2. 应付款建议(按订单) `GET /admin/finance/payments/suggestions` + +**VO**: `PaymentSuggestionRespVO{orderId, rows: List}` + +#### 使用场景 + +填单页按订单拉「该付给供应商」的建议行,勾选后带入创建。本次新增团期住宿行(`sourceType=GROUP_BATCH_STAY`),以及「应付已消失但仍有付款占用」的孤儿行。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Query | Long | 是 | - | 订单 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | Long | 订单 ID | +| rows[].sourceType | String | `NODE` 行程节点 / `HOTEL_ASSIGNMENT` 配房 / **`GROUP_BATCH_STAY` 团期住宿(新增取值)** | +| rows[].sourceId | Long | 来源主键;团期住宿为该付款身份下最小分房行 ID,仅定位用 | +| rows[].sourceRefKey | String | **新增**。仅团期住宿有值:`{orderId}:{yyyy-MM-dd}:{hotelId}:{roomTypeId}`,建单时原样回传 | +| rows[].resourceId | Long | 资源 ID(团期住宿为酒店 ID) | +| rows[].resourceName | String | 资源名(团期住宿为 酒店名/房型名) | +| rows[].quantity | Integer | 数量(团期住宿为该晚该房型分到本户的间数合计) | +| rows[].unitPrice | BigDecimal | 单价(团期住宿为结算价 元/间·晚;同身份单价不一致时为 null) | +| rows[].amount | BigDecimal | 当前应付(团期住宿 = Σ 结算价 × 间数;孤儿行为 0) | +| rows[].paymentMethod | String | `SIGNED` / `COMPANY_PAID` | +| rows[].paymentType | String | 付款类型字典标签预填(团期住宿同配房 = 住宿) | +| rows[].supplierId | Long | 预填供应商(按酒店反查) | +| rows[].supplierName | String | 供应商全称 | +| rows[].payeeAccountId | Long | 默认收款账户 | +| rows[].eligible | boolean | 供应商是否可付款 | +| rows[].eligibleReason | String | 不可付款原因 | +| rows[].alreadyGenerated | boolean | 是否已有活跃付款占用(团期住宿按业务键判定) | +| rows[].paidAmount | BigDecimal | **新增**。已付原值(PAID 明细合计,不扣退款);仅团期住宿有值 | +| rows[].refundedAmount | BigDecimal | **新增**。已确认退款;仅团期住宿有值 | +| rows[].netPaidAmount | BigDecimal | **新增**。净已付 = paidAmount − refundedAmount;仅团期住宿有值 | +| rows[].unpaidAmount | BigDecimal | **新增**。未付占用(PENDING/SUBMITTED/APPROVED 明细合计,含草稿);仅团期住宿有值 | +| rows[].generatedAmount | BigDecimal | **新增**。已生成金额 = netPaidAmount + unpaidAmount;仅团期住宿有值 | +| rows[].amountChanged | Boolean | **新增**。已有活跃占用且已生成金额 ≠ 当前应付时为 true(未申请过的行恒 false,此时看 diffAmount 即可申请额);仅团期住宿有值 | +| rows[].diffAmount | BigDecimal | **新增**。当前应付 − 已生成金额(有符号);仅团期住宿有值 | + +#### 请求示例 + +```http +GET /admin/finance/payments/suggestions?orderId=2100244780104425473 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "orderId": "2100244780104425473", + "rows": [ + { + "sourceType": "GROUP_BATCH_STAY", + "sourceId": "2100244786240692225", + "sourceRefKey": "2100244780104425473:2026-11-18:2029956939232808961:2029956939333472258", + "resourceId": "2029956939232808961", + "resourceName": "测试酒店/标准间", + "quantity": 1, + "unitPrice": 519.00, + "amount": 519.00, + "paymentMethod": "COMPANY_PAID", + "paymentType": "住宿", + "supplierId": null, + "supplierName": null, + "payeeAccountId": null, + "eligible": false, + "eligibleReason": "资源未关联供应商,请手选", + "alreadyGenerated": false, + "paidAmount": 0, + "refundedAmount": 0, + "netPaidAmount": 0, + "unpaidAmount": 0, + "generatedAmount": 0, + "amountChanged": false, + "diffAmount": 519.00 + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无应付行返回 `rows: []`。供应商反查 / 可付款判定 / 字典不可用时行仍返回,`eligible=false` 并给出 `eligibleReason`,不打塌整单。 + +```json +{ + "code": 200, + "message": "成功", + "data": { "orderId": "2100244780104425473", "rows": [] }, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "orderId 不能为空", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 团期住宿只取「计划行已确认 + 结算方式为签单/公司付款 + 结算价非空」且分房行有效的数据;现付(cash)不进应付。 +- 团期启用前已冻结的历史旧户只出 `HOTEL_ASSIGNMENT` 行,不出团期住宿行,同订单不会两类并存。 +- 孤儿行:某身份已有活跃付款占用但当前已无应付(如计划行被删)时,输出一行 `amount=0`、`alreadyGenerated=true`、`diffAmount` 为负。 +- `NODE` / `HOTEL_ASSIGNMENT` 行的新增金额字段为 null,判重口径不变。 +- 金额类字段均为数值(BigDecimal),ID 类字段为字符串。 + +--- + +### 3. 应付款建议(按供应商跨团) `GET /admin/finance/payments/suggestions/by-supplier` + +**VO**: `SupplierSuggestionRespVO{supplierId, rows: List}` + +#### 使用场景 + +按供应商列出跨团欠付明细,勾选后走按供应商合并建单。本次新增团期住宿行(改前恒无)与孤儿行。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| supplierId | Query | Long | 是 | - | 供应商 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| supplierId | Long | 供应商 ID | +| rows[] | SupplierSuggestionRowVO | 继承按订单建议行全部字段(含本次新增的 sourceRefKey 与金额对账字段) | +| rows[].orderId | Long | 来源订单 ID(建单时回传) | +| rows[].teamNo | String | 来源团号 | +| rows[].orderNo | String | 来源订单号 | + +#### 请求示例 + +```http +GET /admin/finance/payments/suggestions/by-supplier?supplierId=88001 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "supplierId": "88001", + "rows": [ + { + "orderId": "2100244780104425473", + "teamNo": "26-3509", + "orderNo": "HL2026091701", + "sourceType": "GROUP_BATCH_STAY", + "sourceId": "2100244786240692225", + "sourceRefKey": "2100244780104425473:2026-11-18:2029956939232808961:2029956939333472258", + "amount": 800.00, + "alreadyGenerated": true, + "paidAmount": 800.00, + "refundedAmount": 0, + "netPaidAmount": 800.00, + "unpaidAmount": 0, + "generatedAmount": 800.00, + "amountChanged": false, + "diffAmount": 0 + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "data": { "supplierId": "88001", "rows": [] }, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "supplierId 不能为空", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 候选订单 = 未核单且未取消的订单,**另并入「已取消但仍有活跃团期住宿付款占用」的订单**;已取消订单不再计应付,只呈现已付与占用(孤儿行)。 +- 只保留资源反查出的供应商等于入参的行;孤儿行按付款明细上的供应商过滤。 +- 核单已完成的订单不进候选(按订单建议入口仍可见)。 + +--- + +### 4. 应付款统计(按团) `GET /admin/finance/payments/stats/by-team` + +**VO**: `PaymentStatsByTeamReqVO → PageResult` + +#### 使用场景 + +按订单(团)看该付、已申请、已付、欠付。本次该付含团期住宿;已申请/已付改为按付款**明细**的订单归属聚合(按供应商合并付款的单能正确分摊到各团)。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| page | Query | Integer | 否 | ≥1,默认 1 | 页码 | +| pageSize | Query | Integer | 否 | 1-100,默认 20 | 每页条数 | +| keyword | Query | String | 否 | - | 团号 / 产品名模糊 | +| status | Query | String | 否 | `OWED` / `OVERPAID` / `PAID` | 状态筛选(页内过滤) | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | Long | 订单 ID | +| teamNo | String | 团号 | +| productName | String | 产品名 | +| customerName | String | 客人 | +| orderNo | String | 订单号 | +| departDate | LocalDate | 出团日期 | +| returnDate | LocalDate | 返团日期 | +| payableAmount | BigDecimal | 该付(**含团期住宿**;已取消孤儿订单为 0) | +| appliedAmount | BigDecimal | 已申请(SUBMITTED/APPROVED 明细合计,草稿不计) | +| paidAmount | BigDecimal | 已付原值(PAID 明细合计,不扣退款、永不减少) | +| owedAmount | BigDecimal | 欠付 = max(该付 − 净已付, 0),**不再为负** | +| refundedAmount | BigDecimal | **新增**。已确认退款 | +| netPaidAmount | BigDecimal | **新增**。净已付 = paidAmount − refundedAmount | +| overpaidAmount | BigDecimal | **新增**。已付超出 = max(净已付 − 该付, 0) | +| diffAmount | BigDecimal | **新增**。该付 − 净已付(有符号) | +| supplierCount | Integer | 供应商数 | +| status | String | `OWED` 有欠付 / **`OVERPAID` 已付超出待冲正(新增)** / `PAID` 已付清 | + +#### 请求示例 + +```http +GET /admin/finance/payments/stats/by-team?page=1&pageSize=20&keyword=26-3509 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "list": [ + { + "orderId": "2100244780104425473", + "teamNo": "26-3509", + "payableAmount": 800.00, + "appliedAmount": 0, + "paidAmount": 800.00, + "owedAmount": 0, + "refundedAmount": 0, + "netPaidAmount": 800.00, + "overpaidAmount": 0, + "diffAmount": 0, + "supplierCount": 1, + "status": "PAID" + } + ], + "total": 1, + "page": 1, + "pageSize": 20 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无候选订单返回空列表、`total=0`。供应商反查失败只影响 `supplierCount`,该付照常计入。 + +```json +{ + "code": 200, + "message": "成功", + "data": { "list": [], "total": 0, "page": 1, "pageSize": 20 }, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "每页条数最大为100", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 候选订单同按供应商建议(含已取消孤儿订单并入),在 SQL 层合并,`total` 与分页一致。 +- 已付 / 已申请只看付款**明细**,付款主单金额不再参与聚合;按供应商合并付款的单按明细订单分摊到各团。 +- 无订单归属的付款(如合并单的出纳差额行)不进按团统计,只进按供应商统计。 +- `status` 筛选在页内过滤(与改前一致)。 + +--- + +### 5. 应付款统计(按供应商) `GET /admin/finance/payments/stats/by-supplier` + +**VO**: `PaymentStatsBySupplierReqVO → PageResult` + +#### 使用场景 + +按供应商看名下各团合计的该付、已申请、已付、欠付。本次该付含团期住宿;已申请/已付改按明细的供应商归属;新增无团归属已付额。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| page | Query | Integer | 否 | ≥1,默认 1 | 页码 | +| pageSize | Query | Integer | 否 | 1-100,默认 20 | 每页条数 | +| keyword | Query | String | 否 | - | 供应商名称模糊 | +| status | Query | String | 否 | `OWED` / `OVERPAID` / `PAID` | 状态筛选 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| supplierId | Long | 供应商 ID | +| supplierName | String | 供应商全称 | +| category | String | 类别标签(多类别以 / 拼接) | +| payableAmount | BigDecimal | 该付(**含团期住宿**) | +| appliedAmount | BigDecimal | 已申请(SUBMITTED/APPROVED 明细合计) | +| paidAmount | BigDecimal | 已付原值(PAID 明细合计) | +| owedAmount | BigDecimal | 欠付 = max(该付 − 净已付, 0) | +| refundedAmount | BigDecimal | **新增**。已确认退款 | +| netPaidAmount | BigDecimal | **新增**。净已付 | +| overpaidAmount | BigDecimal | **新增**。已付超出 | +| diffAmount | BigDecimal | **新增**。该付 − 净已付(有符号) | +| unattributedPaidAmount | BigDecimal | **新增**。已付但明细无订单归属的金额;恒等式:Σ 该供应商各团已付 + 本值 = paidAmount | +| teamCount | Integer | 涉及团数 | +| status | String | `OWED` / **`OVERPAID`(新增)** / `PAID` | + +#### 请求示例 + +```http +GET /admin/finance/payments/stats/by-supplier?page=1&pageSize=20&keyword=呼籁 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "list": [ + { + "supplierId": "88001", + "supplierName": "呼籁酒店", + "category": "住宿", + "payableAmount": 1600.00, + "appliedAmount": 0, + "paidAmount": 1700.00, + "owedAmount": 0, + "refundedAmount": 0, + "netPaidAmount": 1700.00, + "overpaidAmount": 100.00, + "diffAmount": -100.00, + "unattributedPaidAmount": 100.00, + "teamCount": 2, + "status": "OVERPAID" + } + ], + "total": 1, + "page": 1, + "pageSize": 20 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "data": { "list": [], "total": 0, "page": 1, "pageSize": 20 }, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "页码最小为1", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 已取消孤儿订单的付款占用按明细供应商建桶后并入,已付不会被丢掉。 +- 供应商行为内存聚合后分页,`total` = 筛选后供应商行数。 +- `unattributedPaidAmount` 只来自已付款且明细无订单的行(例如按供应商合并单的存量出纳差额回填行)。 + +--- + +### 6. 申请付款(单笔) `POST /admin/finance/payments` + +**VO**: `PaymentCreateReqVO → PaymentIdRespVO` + +#### 使用场景 + +手工直填或从建议清单单条勾选建付款草稿。本次:团期住宿需回传 `sourceRefKey`;任何单笔创建都会同时写出一条付款明细(带来源写来源明细,手工直填写系统内部的 MANUAL 明细),占用即时生效。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| supplierId | Body | Long | 是 | - | 供应商 ID | +| payeeAccountId | Body | Long | 是 | 供应商生效账户 | 收款账户 | +| amount | Body | BigDecimal | 是 | >0 | 付款金额 | +| paymentType | Body | String | 是 | ≤32,字典标签 | 付款类型 | +| reason | Body | String | 是 | ≤512 | 付款事由 | +| teamNo | Body | String | 否 | ≤32 | 团号 | +| orderId | Body | Long | 条件必填 | 带来源时必填 | 关联订单 | +| resourceId | Body | Long | 否 | - | 关联资源 | +| sourceRefType | Body | String | 否 | `NODE` / `HOTEL_ASSIGNMENT` / `GROUP_BATCH_STAY`;**不可传 MANUAL** | 来源类型,须与 sourceRefId 成对 | +| sourceRefId | Body | Long | 否 | 与 sourceRefType 成对 | 来源主键(团期住宿传建议行 sourceId) | +| sourceRefKey | Body | String | 条件必填 | ≤96;`GROUP_BATCH_STAY` 必填且其中订单须等于 orderId;其余类型必须为空 | **新增**。建议行 sourceRefKey 原样回传 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| paymentId | Long | 新付款单 ID | + +#### 请求示例 + +```json +{ + "supplierId": "88001", + "payeeAccountId": "90001", + "amount": 800.00, + "paymentType": "住宿", + "reason": "团期 26-3509 11/18 住宿", + "teamNo": "26-3509", + "orderId": "2100244780104425473", + "resourceId": "2029956939232808961", + "sourceRefType": "GROUP_BATCH_STAY", + "sourceRefId": "2100244786240692225", + "sourceRefKey": "2100244780104425473:2026-11-18:2029956939232808961:2029956939333472258" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { "paymentId": "2100260000000000001" }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无查询数据。供应商可付款判定 / 账户接口不可用时失败关闭(598803 / 598804),不落任何数据。 + +#### 错误响应 + +```json +{ + "code": 598809, + "message": "该来源配置已生成付款单,请勿重复生成", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598813, + "message": "未付占用超出当前应付 800.00 元,请先删除 / 驳回 / 撤销批准:FK-202609170003(APPROVED)", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598812, + "message": "已付金额超出当前应付 400.00 元,需财务冲正", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598808, + "message": "来源配置引用非法", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 团期住宿判据(Σ = 同身份净已付 + 未付占用,A = 当前应付,x = 本次金额): + - 净已付 > A → **598812**(message 带超出额); + - Σ > A → **598813**(message 列出未付单「单号(状态)」,PENDING 删除 / SUBMITTED 驳回 / APPROVED 撤销批准即可); + - Σ = A → **598809**(A 与 Σ 均为 0,即该身份不在应付范围 → 598808); + - Σ + x > A → **598813**; + - 否则成功:Σ 为 0 建首笔,Σ > 0 建差额行。 +- `NODE` / `HOTEL_ASSIGNMENT` 仍按来源主键存在性判重(598809),行为不变。 +- 598808 触发:来源类型不在取值域或传了 MANUAL;sourceRefType 与 sourceRefId 不成对;带来源未传 orderId;团期住宿 sourceRefKey 缺失、格式错误、日期非法或其中订单 ≠ orderId;非团期住宿传了 sourceRefKey。 +- 同一订单的四个建单 / 编辑写口串行执行,抢锁超时返回 `100503 资源被占用,请稍后重试`。 + +--- + +### 7. 勾选批量生成(按团) `POST /admin/finance/payments/batch-create` + +**VO**: `PaymentBatchCreateReqVO → PaymentBatchCreateRespVO` + +#### 使用场景 + +按订单建议清单勾选多行,按供应商拆单批量生成。本次明细新增 `sourceRefKey`,团期住宿走金额对账。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | Long | 是 | - | 来源订单 | +| items | Body | List | 是 | 至少 1 条 | 勾选明细 | +| items[].sourceRefType | Body | String | 是 | `NODE` / `HOTEL_ASSIGNMENT` / `GROUP_BATCH_STAY` | 来源类型 | +| items[].sourceRefId | Body | Long | 是 | - | 来源主键(团期住宿传 sourceId) | +| items[].sourceRefKey | Body | String | 条件必填 | ≤96;团期住宿必填且订单须等于 orderId;其余类型为空 | **新增** | +| items[].supplierId | Body | Long | 是 | - | 供应商(拆单键) | +| items[].amount | Body | BigDecimal | 是 | >0 | 明细金额 | +| items[].paymentType | Body | String | 是 | ≤32 | 付款类型 | +| items[].resourceId | Body | Long | 否 | - | 资源快照 | +| items[].resourceName | Body | String | 否 | ≤128 | 资源名快照 | +| payeeAccountId | Body | Long | 否 | - | 收款账户 | +| reason | Body | String | 是 | ≤512 | 付款事由 | +| submit | Body | Boolean | 是 | - | true 直接提交 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groups[].paymentId | Long | 付款单 ID | +| groups[].paymentNo | String | 付款单号 | +| groups[].batchNo | String | 批次号 | +| groups[].supplierId | Long | 供应商 ID | +| groups[].supplierName | String | 供应商名 | +| groups[].amount | BigDecimal | 单据金额 | +| groups[].status | String | PENDING / SUBMITTED | + +#### 请求示例 + +```json +{ + "orderId": "2100244780104425473", + "reason": "团期住宿", + "submit": false, + "items": [ + { + "sourceRefType": "GROUP_BATCH_STAY", + "sourceRefId": "2100244786240692225", + "sourceRefKey": "2100244780104425473:2026-11-18:2029956939232808961:2029956939333472258", + "supplierId": "88001", + "amount": 400.00, + "paymentType": "住宿" + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groups": [ + { + "paymentId": "2100260000000000002", + "paymentNo": "FK-202609170004", + "batchNo": "BATCH-2100260000000000003", + "supplierId": "88001", + "supplierName": "呼籁酒店", + "amount": 400.00, + "status": "PENDING" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无查询数据;任一明细校验失败整批回滚,不落任何单据。 + +#### 错误响应 + +```json +{ + "code": 598809, + "message": "该来源配置已生成付款单,请勿重复生成", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598813, + "message": "未付占用超出当前应付 1200.00 元,请先删除 / 驳回 / 撤销批准:FK-202609170005(PENDING)", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 团期住宿判据同单笔创建;同一请求内重复勾选同一身份 → 598809。 +- 598812 / 598808 触发条件同单笔创建。 + +--- + +### 8. 按供应商跨团合并建单 `POST /admin/finance/payments/batch-create-by-supplier` + +**VO**: `PaymentBatchCreateBySupplierReqVO → PaymentBatchCreateRespVO` + +#### 使用场景 + +按供应商建议勾选跨团明细合并成一张付款单。本次明细新增 `sourceRefKey`,团期住宿走金额对账,多订单按订单号升序串行加锁。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| supplierId | Body | Long | 是 | 全部明细须同属该供应商 | 供应商 | +| items | Body | List | 是 | 至少 1 条 | 勾选明细 | +| items[].orderId | Body | Long | 是 | - | 来源订单 | +| items[].sourceRefType | Body | String | 是 | `NODE` / `HOTEL_ASSIGNMENT` / `GROUP_BATCH_STAY` | 来源类型 | +| items[].sourceRefId | Body | Long | 是 | - | 来源主键 | +| items[].sourceRefKey | Body | String | 条件必填 | ≤96;团期住宿必填且订单须等于 items[].orderId | **新增** | +| items[].supplierId | Body | Long | 是 | 等于入参 supplierId | 供应商 | +| items[].amount | Body | BigDecimal | 是 | >0 | 明细金额 | +| items[].paymentType | Body | String | 是 | ≤32 | 付款类型 | +| payeeAccountId | Body | Long | 否 | - | 收款账户 | +| reason | Body | String | 是 | ≤512 | 付款事由 | +| submit | Body | Boolean | 是 | - | true 直接提交 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groups[] | GroupResult | 单元素,字段同按团批量 | + +#### 请求示例 + +```json +{ + "supplierId": "88001", + "reason": "11 月团期住宿合并付款", + "submit": true, + "items": [ + { + "orderId": "2100244780104425473", + "sourceRefType": "GROUP_BATCH_STAY", + "sourceRefId": "2100244786240692225", + "sourceRefKey": "2100244780104425473:2026-11-18:2029956939232808961:2029956939333472258", + "supplierId": "88001", + "amount": 800.00, + "paymentType": "住宿" + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groups": [ + { + "paymentId": "2100260000000000006", + "paymentNo": "FK-202609170006", + "batchNo": "BATCH-2100260000000000007", + "supplierId": "88001", + "supplierName": "呼籁酒店", + "amount": 800.00, + "status": "SUBMITTED" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无查询数据;校验失败整单回滚。 + +#### 错误响应 + +```json +{ + "code": 598811, + "message": "勾选明细含其他供应商,请按供应商分批勾选", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598809, + "message": "该来源配置已生成付款单,请勿重复生成", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 合并单主单无订单归属,金额按明细订单分摊到按团统计。 +- 团期住宿判据同单笔创建(598809 / 598812 / 598813 / 598808)。 + +--- + +### 9. 编辑付款草稿 `PUT /admin/finance/payments/{id}` + +**VO**: `PaymentUpdateReqVO → Result` + +#### 使用场景 + +编辑 PENDING 草稿。本次新增:有业务来源的草稿不允许改供应商与订单;明细金额 / 归属跟随主单同步;团期住宿草稿改金额受额度守卫。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | 是 | - | 付款单 ID | +| supplierId | Body | Long | 是 | 有来源草稿须等于原值 | 供应商(**请回传原值**) | +| payeeAccountId | Body | Long | 是 | 供应商生效账户 | 收款账户 | +| amount | Body | BigDecimal | 是 | >0;多明细草稿须等于明细合计 | 付款金额 | +| paymentType | Body | String | 是 | ≤32 | 付款类型 | +| reason | Body | String | 是 | ≤512 | 付款事由 | +| teamNo | Body | String | 否 | ≤32 | 团号 | +| orderId | Body | Long | 否 | 有来源草稿须等于原值(原值为空则传空) | 关联订单 | +| resourceId | Body | Long | 否 | - | 关联资源 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | Integer | 200 成功 | +| data | Void | 恒为 null | + +#### 请求示例 + +```json +{ + "supplierId": "88001", + "payeeAccountId": "90001", + "amount": 300.00, + "paymentType": "住宿", + "reason": "差额改为 300", + "teamNo": "26-3509", + "orderId": "2100244780104425473" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无查询数据;任一校验失败主单与明细零变化。 + +#### 错误响应 + +```json +{ + "code": 598808, + "message": "来源配置引用非法", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598805, + "message": "金额无效(付款金额须大于0)", + "data": null, + "success": false +} +``` + +```json +{ + "code": 598813, + "message": "未付占用超出当前应付 1200.00 元,请先删除 / 驳回 / 撤销批准:无未付占用,本次申请金额超出可申请差额", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 有来源明细(NODE / HOTEL_ASSIGNMENT / GROUP_BATCH_STAY)的草稿:改 supplierId 或 orderId → 598808;单明细同步明细金额;多明细要求 amount 等于明细合计,否则 598805。 +- 团期住宿单明细草稿:同身份其他占用 + 新金额 ≤ 当前应付,否则按净已付是否超额报 598812 / 598813。 +- 手工草稿(无来源):可自由改供应商 / 订单 / 团号 / 资源,系统同步其内部 MANUAL 明细的归属,资源名快照置空。 +- 仅 PENDING 可编辑,否则 598802。 + +--- + +## 四、契约约束与正确调用方式 + +| 场景 | 做法 | +|------|------| +| 团期住宿勾选建单 | `sourceRefType=GROUP_BATCH_STAY`、`sourceRefId=行.sourceId`、`sourceRefKey=行.sourceRefKey` 原样回传,单笔还须带 `orderId` | +| 团期住宿应付增加 | 行上 `amountChanged=true` 且 `diffAmount>0` → 以 `diffAmount` 为金额再建一笔(差额行) | +| 团期住宿应付减少、原单未付 | 598813 提示里列出的单:PENDING 删除 / SUBMITTED 驳回 / APPROVED 先调撤销批准再删或改 | +| 团期住宿应付减少、原单已付 | 598812,需财务冲正(后续能力),前端提示即可 | +| 编辑有来源草稿 | supplierId / orderId 回传原值,不提供修改入口 | +| 统计状态筛选 | 新增 `OVERPAID` 选项;`owedAmount` 不会再出现负数 | +| 建议行新字段 | 仅 `GROUP_BATCH_STAY` 行有值,其余类型为 null,前端需判空 | + +--- + +## 五、数据库行为 + +| 操作 | 表 | 行为 | +|------|-----|------| +| 迁移 V20260917_110 | `fin_payment_item` | 加列 `source_ref_key VARCHAR(96) NULL`、`item_kind VARCHAR(16) NOT NULL DEFAULT 'FULL'`,加索引 `idx_source_ref_key(source_ref_type, source_ref_key)`;幂等 | +| 迁移 V20260917_111 | `fin_payment_item` | 存量回填:无明细的付款单补一条明细(无来源记 MANUAL);已付单明细合计与出账流水不一致时补一条 `CASHIER_ADJ`;不改付款主单与资金流水,重复执行零新增 | +| 单笔创建 | `fin_payment` + `fin_payment_item` | 同事务写主单 + 1 条明细(来源明细或 MANUAL) | +| 批量创建 | `fin_payment` + `fin_payment_item` | 团期住宿明细写 `source_ref_key`;`item_kind` 为 FULL(首笔)或 ADJUST(差额) | +| 编辑草稿 | `fin_payment` + `fin_payment_item` | 同步唯一明细金额;手工单同步明细归属 | +| 撤销批准 | `fin_payment` + `fin_payment_review_log` | 条件更新 APPROVED→PENDING + 插入 UN_APPROVE 流水 | +| 出纳付款 | `fin_payment_item` | 主单实付与明细合计不一致时追加一条 `CASHIER_ADJ` 明细(新数据正常不会触发) | +| 四个读接口 | 只读 | 新读 `group_batch_room_plan` / `group_batch_room_allocation` / `order_group_batch_house_legacy` | + +--- + +## 六、边界行为 + +- 未登录 → 401;无权限 → 403。 +- 团期计划行结算价为空 → 该行不进应付(记告警日志)。 +- 同一身份跨计划行单价不一致 → `unitPrice=null`,`amount` 按各行累加。 +- 已核单完成的订单不进三个批量入口的候选,按订单建议仍可见。 +- 抢不到同订单写锁 → 100503。 +- 撤销批准与出纳付款并发 → 只有一个成功,另一方 598802 或出纳侧状态错误。 + +--- + +## 六.5、枚举 + +### 来源类型(sourceType / sourceRefType) + +**所属字段**: PaymentSuggestionRowVO.sourceType、各创建入参 sourceRefType | **类型**: String + +| 值 | 中文 | 说明 | +|----|------|------| +| NODE | 行程节点 | 既有 | +| HOTEL_ASSIGNMENT | 配房 | 既有 | +| GROUP_BATCH_STAY | 团期住宿 | **新增**,按业务键金额对账 | +| MANUAL | 手工 | 系统内部明细类型,前端不可传、建议行不出现 | + +### 统计状态(status) + +**所属字段**: PaymentStatsByTeamRowVO.status、PaymentStatsBySupplierRowVO.status | **类型**: String + +| 值 | 中文 | 说明 | +|----|------|------| +| OWED | 有欠付 | owedAmount > 0 | +| OVERPAID | 已付超出 | **新增**,overpaidAmount > 0,待冲正 | +| PAID | 已付清 | 其余 | + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| 团期住宿应付 | 按订单建议之外三处恒为 0 | 四处一致 | +| 团期住宿判重 | 按分房行 ID,微调换 ID 可重复申请 | 按业务键金额对账 | +| 统计已付来源 | 付款主单金额,按主单订单归属 | 付款明细金额,按明细订单归属 | +| 统计欠付 | 可为负数 | 不为负,超出部分进 overpaidAmount | +| 统计状态 | OWED / PAID | OWED / OVERPAID / PAID | +| 单笔创建 | 不写明细,不参与防重 | 同事务写明细,立即占用 | +| 编辑草稿 | 可任意改供应商/订单/金额,明细不跟随 | 有来源禁改归属;明细跟随;团期住宿守额度 | +| 已批准单改单 | 无出口(驳回只收 SUBMITTED) | 可撤销批准回到草稿 | +| 错误码 | 无 598812 / 598813 | 新增 | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 部分。读接口只增字段与取值(`GROUP_BATCH_STAY`、`OVERPAID`);写接口团期住宿必须带 `sourceRefKey`;有来源草稿不能再改供应商 / 订单。 +- **前端是否必须同步上线**: 建议同步。不接也不报错,但团期住宿行无法正确建单(缺 sourceRefKey 会 598808),且看不到金额变化提示与撤销批准入口。 +- **前端需做**: 建议行展示 `GROUP_BATCH_STAY` 与金额对账字段;建单回传 `sourceRefKey`;统计加 `OVERPAID` 与新金额列;按供应商统计展示 `unattributedPaidAmount`;已批准单加「撤销批准」按钮;598812 / 598813 文案映射;有来源草稿的供应商 / 订单置为只读。 + +--- + +## 七、不影响范围 + +- 行程节点、配房两类来源的判重口径不变。 +- 出纳付款接口入参 / 出参不变(仅可能追加内部明细)。 +- 付款单分页、详情、提交、批准、驳回、删除接口契约不变。 +- C 端与小程序零影响。 + +--- + +## 八、测试环境已验证 + +TEST(`https://api.test.1814.love:9443`,`hl-order-service-v3` = **dev-v3@75d77eefe**,含合并提交 `a37bd669f`;真实网关 + 管理员 token),2026-09-17: + +| 场景 | 结果 | +|---|---| +| 迁移 | `V20260917_110` / `V20260917_111` 执行成功;存量无明细单回填 7 条(6 MANUAL + 1 带来源),重复执行第①段零新增 | +| 四入口金额一致 | 纯团期户 2 间 × 400(sign):按订单建议 / 按供应商建议 / 按团统计均 800,按供应商统计应付 +800;改前按订单建议 0 行 | +| 客户自付与旧户 | 改 cash 后四处同时归 0;旧户只出 `HOTEL_ASSIGNMENT` 行、不双计 | +| 防重 | 单笔建单写出 1 行明细;再以单笔 / `batch-create` / `batch-create-by-supplier` 各建一次均 598809;H10 拆行与核单重绑后仍 598809 | +| 金额变化 | 增额可建差额行(ADJUST);未付超额 598813(列出单号与状态);已付超额 598812;草稿编辑超额 598813、改供应商/订单 598808、多明细改金额 598805 | +| 撤销批准 | `PUT /admin/finance/payments/{id}/revoke-approval` → 200、回到 PENDING、出纳队列消失、审核流水出现 `UN_APPROVE`;再撤 598802;与出纳付款并发 12 轮均恰一方成功 | +| 统计口径 | 已付后应付消失 → owed 0 / overpaid 800 / OVERPAID;合并付款按团各归各(800+800),出纳异额合并单 `unattributedPaidAmount=100` 且 800+800+100=1700 | +| 来源锁 | 同订单并发 create 与 update 8 轮均恰一个成功,Σ 不超过应付 | + +``` +GET /admin/finance/payments/suggestions?orderId=2100421247786545153 → 200,rows[0].sourceType=GROUP_BATCH_STAY,amount=800 +POST /admin/finance/payments(同 sourceRefKey 第二次)→ 598809 +PUT /admin/finance/payments/{id}/revoke-approval → 200;GET /admin/finance/payments/{id}/review-logs → 含 UN_APPROVE +``` + +逐条验收记录见工单 #7396 验收评论。 + +--- + +## 十、相关文档 + +- Issue: [#7396](https://git.1814.love:8443/wx/HL/issues/7396) +- PR: [#7866](https://git.1814.love:8443/wx/HL/pulls/7866) +- 核查 SQL: `docs/finance/7396-payment-item-reconcile-check.sql`(HL 仓库) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7396](https://git.1814.love:8443/wx/HL/issues/7396) +- **PR**: [#7866](https://git.1814.love:8443/wx/HL/pulls/7866) +- **关联**: #7327(团期核单与月报)、#7398(退款 / 冲正,598812 的后续出口) + +### 联系人 + +- **后端负责人**: @jw +- **前端负责人**: @mmg