# F1 人工账单、收款、成本接口契约 > 版本:V1.0 > 日期:2026-08-11 > 状态:目标契约;以《F1人工财务契约审查与门禁》为开发/联调准入依据 > OpenAPI:`openapi/finance-f1-manual-v1.yaml` ## 1. 首期边界 - 只支持 RMB/CNY,不做汇率、月结、钱包、发票、银行流水和自动认款。 - 平台财务人工填写账单、收款、成本表单并上传凭证。 - 客户只读查看本客户账单、收款记录和未收余额。 - 出账前账单可改;出账后冻结,只能由平台财务调整或作废重开。 - 客户账期按客户配置,默认 30 个自然日;账单日期和到期日由服务端按 `billDate + creditDays` 计算。 - 一笔收款可分配到多张账单;财务逐张填写分配金额。首期不处理预收、多收、手续费、退款。 - 单票成本归属单票;整柜清关、拖车等公共成本按柜内有效票体积分摊。 ## 2. 统一接口约定 - 路径分为平台财务 `/api/platform/finance/v1` 和客户只读 `/api/client/finance/v1`。 - 普通业务返回 HTTP 200,业务结果在 `code` 字段;成功 `code="200"`。认证/网关拒绝可返回 401/403。 - 所有 ID 以字符串传输;金额为十进制定点数,首期 `currency=CNY`。 - 写请求必须带 `Idempotency-Key`;更新/动作请求必须带 `expectedVersion`。 - 客户范围从登录授权上下文取得,服务端忽略请求体中的客户切换字段。 - 文件先由 File 服务上传,Finance 接收 `fileId` 引用,不直接接收二进制。 - 平台账单列表必须带 `customerId`;收款和成本列表的 `customerId` 为可选筛选。首期接口返回非分页数组,前端分页只能在适配层完成;不得把数组伪装成服务端分页对象。 - Finance 对外 DTO 应稳定使用 camelCase;数据库 JDBC `snake_case` 仅限适配层,不能作为长期前后端契约。 - 所有成功和失败响应固定包含 `requestId`、`traceId`、`timestamp`;`timestamp` 使用 UTC ISO-8601。账单、收款、成本和账期分别使用 OpenAPI 中的固定 DTO,不允许返回 JDBC `Map` 或未声明字段。 ### 2.1 幂等处理 - 幂等范围为 `customerId + operatorId + operation + Idempotency-Key`,请求哈希由 HTTP 方法、规范化路径、客户范围和规范化 JSON 请求体计算,不得只哈希幂等键。 - 同键同参且原请求已成功:返回第一次创建的资源和业务结果,不重复写库;本次响应生成新的 `requestId/traceId/timestamp`。 - 同键异参:返回 HTTP 409、`FIN-IDEMPOTENCY-CONFLICT`,不得覆盖原记录。 - 同键同参但原请求为 `PROCESSING`:返回 HTTP 409、`FIN-IDEMPOTENCY-PROCESSING` 和 `Retry-After: 2`;调用方只能用原键重试,不得自动换键重复创建。 - `PROCESSING` 超过 30 秒由服务端恢复任务核对业务资源和事务结果,恢复为 `SUCCEEDED` 或可重试失败;客户端不负责判断原事务是否落库。 ## 3. 对象与状态 ### 3.1 账单 字段:`billId`、`billNo`、`customerId`、`billDate`、`dueDate`、`currency`、`lines[]`、`totalAmount`、`receivedAmount`、`outstandingAmount`、`status`、`version`。 账单明细至少保存 `sourceFeeId`、`sourceFeeVersion`、`orderId`/`inboundId`、费用项目快照、数量、单价和金额。多笔订单分别出账;同一账单可包含多个费用项。 状态:`DRAFT → ISSUED → PARTIALLY_PAID → PAID`;未发生收款时允许 `DRAFT/ISSUED → VOIDED`。`DRAFT` 可编辑,已出账账单不得原地无痕覆盖金额;调整必须形成可追溯的新版本/修订记录,或走作废后重开。 ### 3.2 收款 字段:`receiptId`、`receiptNo`、`customerId`、`receivedDate`、`amount`、`currency`、`paymentMethod`、`voucherFileIds[]`、`remark`、`allocations[]`、`operatorSnapshot`。 `allocations[]` 每项为 `billId + allocatedAmount`。首期要求分配金额之和等于收款金额;每项不得超过对应账单未收余额。收款凭证可被同一笔收款的多张账单共享。 收款只允许分配到同一客户且状态为 `ISSUED/PARTIALLY_PAID` 的账单;已收清、草稿、作废和跨客户账单均拒绝。服务端必须在同一事务内锁定所有账单、写入收款与分配并更新余额,不允许部分成功。 ### 3.3 成本 字段:`costEntryId`、`costNo`、`costType`(`SINGLE_ORDER`/`CONTAINER_SHARED`)、`orderId`/`containerId`、费用项目、供应商/成本对象快照、金额、币种、凭证文件、备注、操作人。 单票成本必须提供 `orderId`,订单必须存在并属于表单中的客户。整柜公共成本必须提供 `containerId`,柜次及柜内参与分摊的有效票必须属于同一客户;混入其他客户、已作废票或不属于该柜的票时拒绝。 整柜分摊固定规则: 1. 使用成本创建时冻结的订单有效体积快照,单位 CBM、精度 6 位;有效票按 `orderId` 升序形成确定顺序。 2. `ratio = orderVolume / totalVolume`;除最后一票外,`allocatedAmount = costAmount × ratio`,按 6 位小数 `HALF_UP`。 3. 最后一票金额为 `costAmount - 前序分摊金额合计`,承接全部尾差;保存 `roundingAdjustment`,保证分摊金额合计严格等于成本金额。 4. 单票体积为零时该票自动分摊为 0;柜内有效票总体积为零时返回 `FIN-COST-002`,随后仅允许 `MANUAL_ZERO_VOLUME` 人工指定同柜订单金额,且合计必须等于成本金额。 5. 已落库的体积、比例、分摊金额和尾差均为不可变快照;来源体积后续变化不追改历史成本。 ### 3.4 财务凭证上传与关联 1. Admin 先调用 File 的 `POST /api/platform/v1/files/finance-vouchers`,提交 `customerId + purpose(RECEIPT/COST) + file`,取得临时 `fileId`。 2. File 只接受 JPEG、PNG、WebP、PDF,单文件不超过 10 MiB;记录客户、用途、上传人、SHA-256、状态 `TEMPORARY`。临时文件不得直接对客户开放。 3. Finance 创建收款/成本前,使用内部服务令牌调用 `POST /internal/file/v1/finance-vouchers/validate` 批量校验:文件存在、`ACTIVE/TEMPORARY`、未删除、客户一致、用途一致、类型/大小合规、尚未关联其他资源。任一失败则整个财务表单不落库。 4. Finance 落单成功后调用 `POST /internal/file/v1/finance-vouchers/associate`,把文件幂等关联到 `FINANCE_RECEIPT/FINANCE_COST + resourceId`;失败由 Finance Outbox 使用原幂等键重试,不重复创建收款或成本。 5. 相同文件关联同一资源视为同参重放并返回原结果;关联其他资源返回 `FIN-FILE-004`。Finance 保存 fileId 关联,文件内容和元数据由 File 管理。 6. 客户侧收款 DTO 不返回 `voucherFileIds`、内部备注或下载地址;首期客户不能查看平台收款/成本凭证。 ## 4. 接口清单 ### 平台财务 | 方法 | 路径 | 用途 | |---|---|---| | GET | `/api/platform/finance/v1/bills` | 查询账单 | | POST | `/api/platform/finance/v1/bills` | 保存账单草稿 | | GET | `/api/platform/finance/v1/bills/{billId}` | 查询平台账单详情 | | PUT | `/api/platform/finance/v1/bills/{billId}` | 更新账单草稿 | | POST | `/api/platform/finance/v1/bills/{billId}/issue` | 出账并冻结 | | POST | `/api/platform/finance/v1/bills/{billId}/adjust` | 出账后调整/生成新版本 | | POST | `/api/platform/finance/v1/bills/{billId}/void` | 作废账单 | | POST | `/api/platform/finance/v1/receipts` | 人工登记收款并分配账单 | | GET | `/api/platform/finance/v1/receipts` | 查询收款记录 | | POST | `/api/platform/finance/v1/cost-entries` | 人工登记单票/整柜成本 | | GET | `/api/platform/finance/v1/cost-entries` | 查询成本记录 | | GET | `/api/platform/finance/v1/customers/{tenantId}/terms` | 查询客户账期(`tenantId` 为兼容字段,产品语义为客户) | | PUT | `/api/platform/finance/v1/customers/{tenantId}/terms` | 保存客户账期,默认 30 天 | ### 客户只读 | 方法 | 路径 | 用途 | |---|---|---| | GET | `/api/client/finance/v1/bills` | 查询本客户账单 | | GET | `/api/client/finance/v1/bills/{billId}` | 查看账单详情 | | GET | `/api/client/finance/v1/receipts` | 查看本客户收款记录 | 客户端禁止 POST、PUT、PATCH、DELETE 财务写操作。 客户列表和详情只能返回当前客户且状态为 `ISSUED/PARTIALLY_PAID/PAID` 的账单。`DRAFT/VOIDED`、跨客户和不存在的账单详情统一返回 `FIN-AR-001`,不得通过错误差异泄露账单存在性;客户收款列表只返回分配到上述可见账单的记录。 ## 5. 最小错误码 | 错误码 | 场景 | |---|---| | `FIN-AR-001` | 账单不存在或不属于当前客户 | | `FIN-AR-002` | 账单状态不允许当前操作 | | `FIN-AR-003` | 出账后尝试直接修改金额 | | `FIN-AR-004` | 账期配置无效 | | `FIN-AR-005` | 首期仅允许 CNY | | `FIN-AR-006` | 收款分配金额合计不等于收款金额 | | `FIN-AR-007` | 分配金额超过账单未收余额 | | `FIN-AR-008` | 凭证文件不存在、无权引用或类型不允许 | | `FIN-AR-009` | 版本冲突,请刷新后重试 | | `FIN-AR-010` | 幂等键重复但请求内容不同 | | `FIN-COST-001` | 成本归属对象缺失 | | `FIN-COST-002` | 体积为零,无法按体积分摊 | | `FIN-COST-003` | 订单、柜次或柜内票不属于指定客户 | | `FIN-COST-004` | 人工分摊金额合计不等于成本金额 | | `FIN-FILE-001` | 凭证不存在、已删除或状态无效 | | `FIN-FILE-002` | 凭证客户或用途不匹配 | | `FIN-FILE-003` | 凭证类型或大小不允许 | | `FIN-FILE-004` | 凭证已关联其他财务资源 | | `FIN-IDEMPOTENCY-CONFLICT` | 同一幂等键对应不同请求内容 | | `FIN-IDEMPOTENCY-PROCESSING` | 同参请求仍在处理中,可按 Retry-After 使用原键重试 | | `FIN-AUTH-001` | 当前角色无平台财务写权限 | ## 6. 验收用例 1. 平台财务创建 CNY 草稿账单,保存后可继续编辑。 2. 账单出账生成账单号和到期日(出账日+客户账期,默认 30 天)。 3. 出账后修改金额被拒绝;通过调整或作废重开保留原版本。 4. 登记 300 元收款,上传一张凭证并分配到 3 张 100 元账单,三张账单均更新余额。 5. 一张账单分两次收款后显示 `PARTIALLY_PAID`,余额归零后显示 `PAID`。 6. 收款分配超过任一账单未收余额或合计不等于收款金额时拒绝且不落部分数据。 7. 客户只读可见本客户账单和收款记录,不可写入、不跨客户查询。 8. 客户子用户停用后不能访问财务页面,但历史操作人和客户财务归属不变。 9. 登记单票成本成功;整柜公共成本按体积分摊,零体积场景要求人工指定。 10. 重复 `Idempotency-Key` 且同参返回原结果;同键不同请求返回 `FIN-IDEMPOTENCY-CONFLICT`。 11. 同键同参处于 `PROCESSING` 时返回 409、`FIN-IDEMPOTENCY-PROCESSING` 和 `Retry-After`,不会生成第二张单。 12. File 凭证客户、用途、状态、类型或大小任一不匹配时,收款/成本和分配均不落库;关联失败可由 Outbox 幂等补偿。 13. 整柜自动分摊按 6 位 `HALF_UP` 计算,最后一票承接尾差且合计严格等于成本金额;总体积为零只允许同柜同客户订单人工分摊。 14. 客户列表、详情和收款记录均看不到 `DRAFT/VOIDED` 账单,也看不到平台凭证 fileId 和内部备注。 ## 7. 权限矩阵 平台写入由 `platform:finance:*` 权限控制:账单 `view/create/update/issue/adjust/void`,收款 `view/create`,成本 `view/create`,客户账期 `view/update`。客户仅有 `merchant:finance:statement:list/view`,服务端从登录上下文确定客户范围。 ## 8. 实现依赖 - F0:Finance 服务、客户范围、权限码、编号器、审计、文件引用和幂等表。 - Firstleg:已发布费用来源快照和来源版本查询;Finance 不跨库读取。 - Admin:平台财务路由和三个写表单。 - Client:账单/收款只读查询。 本契约不要求修改 Firstleg 或 Finance Java;后端实现时以此文档、OpenAPI 和门禁审查结论为准。当前 Java 返回的 JDBC `snake_case Map`、非分页数组属于过渡实现,联调前必须由 API 适配层稳定化。