# F1 人工账单、收款、成本接口契约 > 版本:V1.2 > 日期:2026-08-11 > 状态:目标契约;以《F1人工财务契约审查与门禁》为开发/联调准入依据 > OpenAPI:`openapi/finance-f1-manual-v1.yaml` > 一期 5 页产品与新增接口以《财务一期五页产品与接口验收契约》和 `openapi/finance-phase1-five-pages-v1.yaml` 为准;本文保留现有人工账单、收款和成本能力的细化契约。 ## 1. 首期边界 - 领导验收页面固定为客户账户、客户应收应付、客户对账单、结算单应收、结算单应付。 - 费用分析、利润表、发票、钱包、预收和充值不做,不保留按钮、菜单或路由。 - 客户账户只读聚合账单、收款和应收应付来源,不存在可直接修改的账户余额。 - 只支持 RMB/CNY,不做汇率、月结、钱包、发票、银行流水和自动认款。 - 平台财务人工填写账单、收款、成本表单并上传凭证。 - 客户只读查看本客户账单、收款记录和未收余额。 - 出账前账单可改;出账后冻结,只能由平台财务调整或作废重开。 - 客户账期按客户配置,默认 30 个自然日;账单日期和到期日由服务端按 `billDate + creditDays` 计算。 - 一笔收款可分配到多张账单;财务逐张填写分配金额。首期不处理预收、多收、手续费、退款。 - 单票成本归属单票;整柜清关、拖车等公共成本按柜内有效票体积分摊。 - 账单/对账单不得接收 Admin 自造来源:只有已审核、已发布、参与结算、有效且为 CNY 的入库单/柜子费用版本进入 `fin_fee_source` 后才能选入。 ## 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. Finance 公开入口仅接收 `containerId`,由服务端调用 `GET /internal/firstleg/v1/finance-cost-snapshots/customers/{customerId}/containers/{containerId}` 读取真实柜次/订单有效体积快照。浏览器不得提交 `containerOrders` 或 `volumeCbm`。快照单位 CBM、精度 6 位;有效票按 Firstleg 柜次明细形成确定输入。 2. `ratio = orderVolume / totalVolume`;除最后一票外,`allocatedAmount = costAmount × ratio`,按 6 位小数 `HALF_UP`。 3. 最后一票金额为 `costAmount - 前序分摊金额合计`。仅当该金额不等于 `costAmount × tailRatio` 按 6 位 `HALF_UP` 的常规舍入结果时,`roundingAdjustment=true`;没有实际尾差时必须为 `false`。分摊金额合计严格等于成本金额。 4. 单票体积为零时该票自动分摊为 0;柜内有效票总体积为零时返回 `FIN-COST-VOLUME-SNAPSHOT-INVALID`。首期不支持请求端 `MANUAL_ZERO_VOLUME` 人工分摊。 5. 已落库的体积、比例、分摊金额和尾差均为不可变快照;来源体积后续变化不追改历史成本。 ### 3.4 财务凭证上传与关联 1. 当前公开上传入口为 Finance 的 `POST /api/platform/finance/v1/vouchers?customerId={customerId}`,multipart 字段为 `file`;当前只接受图片。OpenAPI 描述该真实入口,不再声明不存在的 `/platform/v1/files/finance-vouchers`。 2. Finance 当前通过 File 的既有 `POST /internal/file/v1/business-files/{tenantId}/FINANCE_VOUCHER/{customerId}` 上传,并以 `GET /internal/file/v1/business-files/{tenantId}/FINANCE_VOUCHER/{customerId}/{fileId}` 查询校验。仓库当前没有财务专用 `validate/associate` Controller,OpenAPI 不得将其写成已实现路径。 3. 收款和成本现已在落库前调用 File 元数据校验:File 客户与表单客户一致、业务类型为 `FINANCE_VOUCHER`、文件存在且 MIME 为允许的图片类型。当前遗留是 `fileClient == null` 时仍跳过校验;放行前必须改为 fail-closed,并返回 `FIN-FILE-VALIDATION-UNAVAILABLE`。 4. 当前 File 模型没有用途区分和关联状态;首期可由 Finance 的 `voucherFileIds` 与财务资源事务性绑定作为关联事实,但必须防止跨客户引用。若后续增加 File 关联接口,需另行更新契约,不能在本版预先承诺 Outbox 行为。 5. 上传入口使用 `hasAnyAuthority('platform:finance:receipt:create','platform:finance:cost:create')`,收款和成本创建人员均可上传。 6. 客户侧收款 DTO 不返回 `voucherFileIds`、`voucherNo`、内部状态、内部备注或下载地址;首期客户不能查看平台收款/成本凭证。 ## 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` | 作废账单 | | GET | `/api/platform/finance/v1/bills/{billId}/revisions?customerId=` | 查询账单修订历史并校验客户归属 | | 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` | 查询成本记录 | | POST | `/api/platform/finance/v1/vouchers?customerId=` | 上传财务凭证图片(当前实现) | | 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-VOLUME-SNAPSHOT-REQUIRED` | 未取得服务端真实柜次体积快照 | | `FIN-COST-VOLUME-SNAPSHOT-INVALID` | 快照缺失、总体积为零或体积数据无效 | | `FIN-COST-CUSTOMER-MISMATCH` | 订单、柜次或柜内票不属于成本客户 | | `FIN-COST-ORDER-NOT-IN-CONTAINER` | 订单不在指定柜次内 | | `FIN-FILE-001` | 凭证不存在、已删除或状态无效 | | `FIN-FILE-CUSTOMER-MISMATCH` | 凭证所属客户与表单客户不一致 | | `FIN-FILE-TYPE-INVALID` | 凭证业务类型或 MIME 不允许 | | `FIN-FILE-VALIDATION-UNAVAILABLE` | File 校验不可用,财务表单必须失败关闭 | | `FIN-IDEMPOTENCY-CONFLICT` | 同一幂等键对应不同请求内容 | | `FIN-IDEMPOTENCY-PROCESSING` | 同参请求仍在处理中,可按 Retry-After 使用原键重试 | | `FIN-AUTH-001` | 当前角色无平台财务写权限 | ## 6. 验收用例 1. 平台财务创建 CNY 草稿账单,保存后可继续编辑。 2. 账单创建时按账单日期生成到期日(`billDate + creditDays`,默认 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 凭证客户、业务类型或 MIME 任一不匹配时,收款/成本和分配均不落库;File 客户端缺失或不可用场景仍需补 fail-closed 验收。 13. 整柜自动分摊按 Firstleg 真实体积快照和 6 位 `HALF_UP` 计算,合计严格等于成本金额;只有实际发生尾差时最后一票才标记 `roundingAdjustment=true`。 14. 平台按 `customerId + billId` 查询 revision 成功;跨客户账单拒绝且不返回修订快照。 15. 客户列表、详情和收款记录均看不到 `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:账单/收款只读查询。 本契约记录开发目标与当前实现;是否可测试以门禁审查结论为准。当前 Java 返回的 JDBC `snake_case Map` 和非分页数组仍属于过渡实现。整柜成本的浏览器体积输入已从后端契约移除,但 Admin 消费端尚需同步。