--- 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