This commit is contained in:
2026-08-11 13:55:19 +08:00
parent a5b49293ab
commit 863f9ca395
6 changed files with 114 additions and 252 deletions
@@ -1,7 +1,7 @@
# F1 人工财务契约审查与门禁
> 审查日期:2026-08-11
> 审查范围:Finance Java、Finance 建库 SQL、统一增量 SQL、Admin/Client Finance API、`openapi/finance-f1-manual-v1.yaml`
> 审查范围:Finance Java、Finance 建库 SQL、统一增量 SQL、Admin/Client Finance API、File Java/测试、`openapi/finance-f1-manual-v1.yaml`
> 结论:F0/F1 基础代码已经实现,当前处于专项修复与契约收敛阶段;不是整体未开发。修复项完成前尚未达到完整联调验收/测试放行。
## 1. 已确认且已对齐
@@ -19,7 +19,17 @@
| 权限码 | 平台 12 个按钮权限,客户 2 个按钮权限 | Controller、OpenAPI 和统一 SQL 权限种子已对齐 |
| 表结构 | 账单、修订、收款分配、成本及成本分摊 | Finance SQL 与统一增量 SQL 均存在对应表 |
当前实现包括 Finance Controller/Service、独立财务表、统一 SQL 权限种子、Admin 人工财务页面和 Client 只读账单页面。本文中的 D1-D10 是对现有实现的修复门禁,不是从零建设清单。
当前实现包括 Finance Controller/Service、独立财务表、统一 SQL 权限种子、Admin 人工财务页面和 Client 只读账单页面。本文中的 D1-D11 是对现有实现的修复门禁,不是从零建设清单。
### 1.1 第三轮复验记录(2026-08-11)
| 验证对象 | 命令/范围 | 结果 | 结论 |
|---|---|---|---|
| Finance | `mvn -pl finance -am test` | 通过;Finance 7 个测试通过(1 个源码契约检查、6 个校验单测) | 可证明现有校验和 1:2:3 体积分摊单测通过;不能证明固定 DTO、完整幂等或跨服务闭环 |
| Admin Finance API | `pnpm test -- src/modules/finance/api/finance.test.ts` | 通过;1 个测试文件、7 个测试通过 | D8 字段适配与账单动作封装有直接证据 |
| File | `mvn -pl file -am test` | 失败;`file-service` 运行 0 个测试,JUnit Jupiter 发现阶段失败 | `ProductImageControllerContractTest` 缺少 `AuthenticatedUser`;File 凭证链路无绿色全量测试证据 |
File 失败转储:`baiyue-firstleg/file/target/surefire-reports/2026-08-11T12-07-49_956-jvmRun1.dump`。由于 File 测试未进入执行阶段,Admin 创建收款/成本的真实凭证端到端闭环继续阻断。
## 2. 接口与权限基线
@@ -27,11 +37,12 @@
|---|---|---|
| `GET /api/platform/finance/v1/bills?customerId=` | `platform:finance:bill:view` | 已实现;返回非分页数组 |
| `POST /api/platform/finance/v1/bills` | `platform:finance:bill:create` | 已实现 |
| `GET/PUT /api/platform/finance/v1/bills/{billId}` | `bill:view/update` | 已实现;Admin 尚未接详情/修改 |
| `POST .../{billId}/issue|adjust|void` | `bill:issue/adjust/void` | Java 已实现;Admin 仅已接出账 |
| `GET/PUT /api/platform/finance/v1/bills/{billId}` | `bill:view/update` | Java 和 Admin 均已接入;缺修订历史查询 |
| `POST .../{billId}/issue|adjust|void` | `bill:issue/adjust/void` | Java 和 Admin 均已接入;端到端闭环未放行 |
| `GET/POST /api/platform/finance/v1/receipts` | `receipt:view/create` | 已实现 |
| `GET/POST /api/platform/finance/v1/cost-entries` | `cost:view/create` | 登记已实现;整柜分摊未实现 |
| `GET/POST /api/platform/finance/v1/cost-entries` | `cost:view/create` | 登记及分摊落库已实现;真实体积快照、归属和成本凭证校验未完成 |
| `GET/PUT /api/platform/finance/v1/customers/{tenantId}/terms` | `customer-terms:view/update` | Java 已实现;Admin 尚未接入 |
| `POST /api/platform/finance/v1/vouchers?customerId=` | `receipt:create` | 已实现图片上传;用途区分、成本上传权限和关联状态未实现 |
| `GET /api/client/finance/v1/bills` | `merchant:finance:statement:list` | 已实现 |
| `GET /api/client/finance/v1/bills/{billId}` | `merchant:finance:statement:view` | 已实现 |
| `GET /api/client/finance/v1/receipts` | `merchant:finance:statement:view` | 已实现 |
@@ -44,17 +55,17 @@
| 编号 | 阻断项 | 当前事实 | 完成标准 |
|---|---|---|---|
| D1 | 稳定响应 DTO | Finance 直接返回 JDBC `Map`/`List<Map>`,字段为 snake_case | 按 OpenAPI 1.1.0 输出账单、收款、成本、账期固定 camelCase DTO;所有成功/失败响应带 `requestId/traceId/timestamp` |
| D1 | 稳定响应 DTO | **未完成。** Finance 仍返回 JDBC `Map`/`List<Map>` 和 snake_case;`ApiResponse` 已有 `requestId/traceId/timestamp`,但 Controller 调用 `success(data)` 时成功响应的 `requestId/traceId` 仍为空 | 按 OpenAPI 1.2.0 输出固定 camelCase DTO;所有成功/失败响应的三个追踪字段均为非空有效值,并有序列化契约测试 |
| D2 | 列表形态统一 | Java 为非分页数组;Admin/Client 已做本地分页适配,但仍发送分页/筛选参数 | 首期明确维持非分页并清理无效参数,或后端真正实现统一分页;两端测试与 OpenAPI 同步 |
| D3 | 调整记录可查询 | Java 调整前写 `fin_bill_revision`,再更新当前账单及替换明细,具备数据库快照;但无修订查询接口,产品无法查看调整前后 | 明确“同账单版本修订”即首期方案,并提供调整记录查询/展示;不得无痕覆盖 |
| D4 | 幂等语义 | 重复幂等键目前直接返回冲突,`request_hash` 只哈希键本身 | 同键同参成功后返回原资源;异参返回 `FIN-IDEMPOTENCY-CONFLICT`;`PROCESSING` 返回 409 + `Retry-After` 并由超时恢复任务核对 |
| D5 | 财务凭证链路 | Admin 已主动阻止空凭证保存;Finance 未校验 File,File 也没有财务专用公开上传/内部关联接口 | 实现 OpenAPI 中临时上传、批量校验、幂等关联;严格校验客户、用途、状态、类型和大小,关联失败由 Finance Outbox 补偿 |
| D6 | 整柜体积分摊 | 有 `fin_cost_allocation` 表,但 `createCost` 只写 `fin_cost`,未计算或落分摊 | 同客户有效票按体积、6 位 `HALF_UP` 分摊,最后一票承接尾差;总体积为零才允许人工分摊且合计严格相等 |
| D7 | 成本归属校验 | Java 仅校验成本类型和金额,未强制单票 `orderId`/整柜 `containerId` | 两种类型分别强制唯一归属,且校验对象存在、属于对应客户 |
| D8 | Admin 成本字段映射 | 后端列为 `fee_item_name/cost_amount`,Admin 读取 `expense_name/amount`,列表费用名和金额会为空/0 | Admin 适配真实线协议并增加回归测试 |
| D9 | Admin 完整操作 | 已有创建、列表和出账;缺账单详情/草稿修改/调整/作废、客户账期维护 | 接齐接口、权限按钮、版本冲突和错误态;调整原因、作废原因必填 |
| D4 | 幂等语义 | **部分实现。** 已使用 SHA-256(JSON(request)),支持成功同参返回原资源、异参冲突和 PROCESSING 冲突;但哈希未统一覆盖 `method + path + customer + normalized body`,`updateBill` 的 `String.valueOf(lines)` 不稳定,且缺 `Retry-After`、超时恢复、FAILED 处理和真实回归测试 | 全部写操作使用完整规范化请求哈希;固定冲突码;补同参、异参、PROCESSING、恢复和并发测试 |
| D5 | 财务凭证链路 | **未完成。** 现有上传为 `POST /api/platform/finance/v1/vouchers`,仅图片且权限为 `receipt:create`;收款调用校验但 `fileClient == null` 时 fail-open,成本未调用校验;无关联接口/状态;File 全量测试发现失败 | 收款和成本均 fail-closed 校验文件存在、客户、业务类型、MIME 和状态;成本角色可上传;建立幂等关联或明确等价持久化关系;File 全量测试通过 |
| D6 | 整柜体积分摊 | **部分实现。** 已有 `ContainerOrderVolume`、`fin_cost_allocation` 落库及 6 位 `HALF_UP` 测试;但公开请求和 Admin 允许人工填 `volumeCbm`,没有 Firstleg 真实快照;归属由请求自证;最后一票无实际尾差也标记 `roundingAdjustment=true`,且未保存真实 `roundingDelta` | 服务端从 Firstleg 取得不可篡改体积快照并验证柜次/订单/客户关系;只有 `tailAmount != round(costAmount × tailRatio, 6, HALF_UP)` 时标记尾差并保存 delta;总和严格等于成本 |
| D7 | 成本归属校验 | **未完成。** 单票只检查 `orderId` 非空;整柜请求的 `customerId` 被直接复制到各分摊行,未验证订单、柜次及入柜关系 | 单票订单存在且属于客户;整柜存在且订单实际在柜内,并全部属于该客户;任何不一致整单拒绝 |
| D8 | Admin 成本字段映射 | **定向验证通过。** Admin 已兼容 `fee_item_name/expense_name` 与 `cost_amount/amount`;Finance API 测试 7/7 通过 | 保持回归测试;不得据此宣称成本创建端到端闭环通过 |
| D9 | Admin 完整操作 | **部分实现。** 已有详情、草稿修改、调整、作废、出账、凭证选择/上传和收款/成本创建;未接客户账期和修订历史,整柜体积仍人工填写,成本凭证后端不校验,File 全量测试失败 | 接客户账期和修订历史;改为服务端真实体积快照;File/Finance 凭证校验通过并完成真实环境创建闭环 |
| D10 | 业务错误码 | 文档定义 `FIN-AR-*`/`FIN-COST-*`;Java 当前主要返回通用 BAD_REQUEST/CONFLICT + 中文消息 | 后端建立稳定业务码并由 Admin/Client 按码处理,不依赖消息文本 |
| D11 | 客户状态可见性 | Client Controller 当前按客户隔离,但账单列表/详情未过滤状态 | 客户只可见 `ISSUED/PARTIALLY_PAID/PAID`;草稿、作废、跨客户和不存在详情统一按不存在处理 |
| D11 | 客户状态可见性 | **部分实现。** 账单列表/详情 SQL 已过滤 `ISSUED/PARTIALLY_PAID/PAID`;收款列表只按客户过滤,仍可返回 `voucher_file_ids`、`voucher_no`、内部状态等平台字段;DRAFT/VOIDED 详情异常尚未固定映射 `FIN-AR-001` | 客户账单及其收款只来源于三种可见状态;使用专用只读 DTO,去除凭证和内部字段;不可见/跨客户/不存在统一 `FIN-AR-001` |
## 4. 非阻断但需锁定的契约说明
@@ -82,6 +93,6 @@
- **当前阶段:专项修复中。** Finance、SQL、Admin 和 Client 均已有实现,不再按“未开发”管理。
- **可继续开发:是。** 路径、权限、固定 DTO、凭证、分摊、幂等和客户可见性已经冻结,不再需要新增产品决策。
- **可进行接口冒烟:有条件。** 可验证现有路径、鉴权和基础创建/查询,但结果不代表业务验收通过。
- **可进入完整联调验收:否。** 主要阻断为整柜成本分摊、凭证服务端校验、Admin 完整操作、稳定 DTO/错误码和幂等重放。
- **可进入完整联调验收:否。** 主要阻断为真实体积快照与归属校验、收款/成本凭证 fail-closed 校验、File 全量测试故障、Admin 创建闭环、稳定 DTO/错误码、完整幂等和客户收款 DTO。
本次审查只修订文档与 OpenAPI,未修改 Java、前端或 SQL。
@@ -1,6 +1,6 @@
# F1 人工账单、收款、成本接口契约
> 版本:V1.0
> 版本:V1.2
> 日期:2026-08-11
> 状态:目标契约;以《F1人工财务契约审查与门禁》为开发/联调准入依据
> OpenAPI:`openapi/finance-f1-manual-v1.yaml`
@@ -61,20 +61,20 @@
整柜分摊固定规则:
1. 使用成本创建时冻结的订单有效体积快照,单位 CBM、精度 6 位;有效票按 `orderId` 升序形成确定顺序。
1. 放行后的 Finance 公开入口仅接收 `containerId`,并由服务端向 Firstleg 读取成本创建时的真实柜次/订单有效体积快照;当前公开请求中的 `containerOrders[].volumeCbm` 是过渡实现,不可信且不得作为放行后的权威输入。快照单位 CBM、精度 6 位;有效票按 `orderId` 升序形成确定顺序。
2. `ratio = orderVolume / totalVolume`;除最后一票外,`allocatedAmount = costAmount × ratio`,按 6 位小数 `HALF_UP`。
3. 最后一票金额为 `costAmount - 前序分摊金额合计`,承接全部尾差;保存 `roundingAdjustment`,保证分摊金额合计严格等于成本金额。
4. 单票体积为零时该票自动分摊为 0;柜内有效票总体积为零时返回 `FIN-COST-002`,随后仅允许 `MANUAL_ZERO_VOLUME` 人工指定同柜订单金额,且合计必须等于成本金额。
3. 最后一票金额为 `costAmount - 前序分摊金额合计`。仅当该金额不等于 `costAmount × tailRatio` 按 6 位 `HALF_UP` 的常规舍入结果时,`roundingAdjustment=true`,并保存非零 `roundingDelta=tailAmount-roundedTailAmount`;没有实际尾差时必须为 `false/0`。
4. 单票体积为零时该票自动分摊为 0;柜内有效票总体积为零时返回 `FIN-COST-VOLUME-SNAPSHOT-INVALID`。首期不支持请求端 `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`、内部备注或下载地址;首期客户不能查看平台收款/成本凭证。
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. 放行目标要求收款和成本在落库前均 fail-closed 校验:File 客户与表单客户一致、业务类型为 `FINANCE_VOUCHER`、文件存在且有效、未删除、MIME 为允许的图片类型。File 客户端不可用或超时时返回 `FIN-FILE-VALIDATION-UNAVAILABLE`,不得跳过校验。
4. 当前 File 模型没有用途区分和关联状态;首期可由 Finance 的 `voucherFileIds` 与财务资源事务性绑定作为关联事实,但必须防止跨客户引用。若后续增加 File 关联接口,需另行更新契约,不能在本版预先承诺 Outbox 行为。
5. 上传权限必须同时覆盖收款和成本创建人员;当前入口只有 `platform:finance:receipt:create`,因此成本凭证上传仍是门禁阻断。
6. 客户侧收款 DTO 不返回 `voucherFileIds`、`voucherNo`、内部状态、内部备注或下载地址;首期客户不能查看平台收款/成本凭证。
## 4. 接口清单
@@ -93,6 +93,7 @@
| 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 天 |
@@ -123,13 +124,14 @@
| `FIN-AR-009` | 版本冲突,请刷新后重试 |
| `FIN-AR-010` | 幂等键重复但请求内容不同 |
| `FIN-COST-001` | 成本归属对象缺失 |
| `FIN-COST-002` | 体积为零,无法按体积分摊 |
| `FIN-COST-003` | 订单、柜次或柜内票不属于指定客户 |
| `FIN-COST-004` | 人工分摊金额合计不等于成本金额 |
| `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-002` | 凭证客户或用途不匹配 |
| `FIN-FILE-003` | 凭证类型或大小不允许 |
| `FIN-FILE-004` | 凭证已关联其他财务资源 |
| `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` | 当前角色无平台财务写权限 |
@@ -144,11 +146,11 @@
6. 收款分配超过任一账单未收余额或合计不等于收款金额时拒绝且不落部分数据。
7. 客户只读可见本客户账单和收款记录,不可写入、不跨客户查询。
8. 客户子用户停用后不能访问财务页面,但历史操作人和客户财务归属不变。
9. 登记单票成本成功;整柜公共成本按体积分摊,零体积场景要求人工指定。
9. 登记单票成本前校验订单存在且属于客户;整柜公共成本仅使用服务端真实快照分摊,零总体积直接拒绝。
10. 重复 `Idempotency-Key` 且同参返回原结果;同键不同请求返回 `FIN-IDEMPOTENCY-CONFLICT`。
11. 同键同参处于 `PROCESSING` 时返回 409、`FIN-IDEMPOTENCY-PROCESSING` 和 `Retry-After`,不会生成第二张单。
12. File 凭证客户、用途、状态、类型或大小任一不匹配时,收款/成本和分配均不落库;关联失败可由 Outbox 幂等补偿。
13. 整柜自动分摊按 6 位 `HALF_UP` 计算,最后一票承接尾差且合计严格等于成本金额;总体积为零只允许同柜同客户订单人工分摊。
12. File 凭证客户、业务类型、状态或 MIME 任一不匹配,或 File 校验不可用时,收款/成本和分配均不落库。
13. 整柜自动分摊按 6 位 `HALF_UP` 计算,合计严格等于成本金额;只有实际发生尾差时最后一票才标记 `roundingAdjustment=true` 并保存非零 `roundingDelta`。
14. 客户列表、详情和收款记录均看不到 `DRAFT/VOIDED` 账单,也看不到平台凭证 fileId 和内部备注。
## 7. 权限矩阵
@@ -162,4 +164,4 @@
- Admin:平台财务路由和三个写表单。
- Client:账单/收款只读查询。
本契约不要求修改 Firstleg 或 Finance Java;后端实现时以此文档、OpenAPI 和门禁审查结论为准。当前 Java 返回的 JDBC `snake_case Map`、非分页数组属于过渡实现,联调前必须由 API 适配层稳定化。
本契约记录开发目标与当前过渡接口;是否可测试以门禁审查结论为准。当前 Java 返回的 JDBC `snake_case Map`、公开请求体积和非分页数组均属于过渡实现,不能据此放行。
+7 -7
View File
@@ -54,7 +54,7 @@
-> 客户只读查询
```
当前仍需修复:固定响应 DTO、财务凭证真实上传及 File 校验/关联、整柜体积分摊落库、完整幂等重放、客户账单状态过滤,以及 Admin 调整/作废/账期等操作。完成《F1人工财务契约审查与门禁》中的测试门禁后,才能宣称 F1 人工财务闭环通过验收。
当前仍需修复:固定响应 DTO、收款/成本凭证 fail-closed 校验、Firstleg 真实体积快照与客户归属校验、完整幂等请求哈希与恢复、客户收款专用 DTO,以及 Admin 客户账期和修订历史查询。完成《F1人工财务契约审查与门禁》中的测试门禁后,才能宣称 F1 人工财务闭环通过验收。
本轮确认“全部都需要做”后,目标范围升级为覆盖原型财务菜单的完整财务域:客户账户、客户应收应付、客户对账单、结算单应收、结算单应付、收付款与核销、费用分析、发票和利润报表。仍建议按依赖关系分阶段交付,避免在应收、应付、钱包和利润各自造一套金额口径。
@@ -128,18 +128,18 @@ Firstleg 已有 `fl_inbound_fee_detail`,包含:
原型还出现了发票抬头、开票日期、汇率、支付方式、收付款凭证号等字段。它们只说明未来可能需要,不代表当前产品已经确认税务、外币折算、资金或发票边界。
当前 Admin 已有正式财务路由、人工账单/收款/成本页面和出账操作,Client 已有正式只读账单页面及线协议适配;但凭证上传仍被界面主动阻止,Admin 尚未接齐草稿修改、调整、作废和客户账期操作,因此只能视为修复中的实现,不能视为已验收交付。
当前 Admin 已有正式财务路由、账单详情/草稿修改/出账/调整/作废、人工收款和成本页面及真实凭证上传调用,Client 已有正式只读账单页面;但 Admin 未接客户账期和修订历史,成本仍允许人工填写柜内体积,File 全量测试在发现阶段失败,后端成本凭证校验也未接通,因此只能视为修复中的实现,不能视为已验收交付。
## 五、当前修复中缺口
| 序号 | 断点 | 直接影响 |
|---|---|---|
| 1 | Finance 仍直接返回 JDBC `Map/List<Map>` | 字段和响应结构未形成稳定 DTO,前端依赖临时适配 |
| 2 | 凭证上传与 File 校验/关联未接通 | 收款和成本表单不能带真实凭证完成保存 |
| 3 | 整柜成本仅登记主记录,尚未生成体积分摊明细 | 柜级清关费、拖车费不能形成可审计的单票成本 |
| 4 | 幂等当前将所有重复键作为冲突 | 同键同参不能返回第一次结果,`PROCESSING` 恢复语义缺失 |
| 5 | 客户查询未在服务端过滤可见状态 | 草稿或作废账单存在泄露风险 |
| 6 | Admin 未接齐账单详情、修改、调整、作废和客户账期 | 平台财务无法在正式页面完成全部人工处理 |
| 2 | 凭证已可上传,但成本未校验,收款校验存在 fail-open,且 File 全量测试故障 | Admin 收款/成本真实创建闭环不能放行 |
| 3 | 整柜分摊已落库,但体积由请求人工填写且归属未向 Firstleg 校验 | 分摊结果不可证明来自真实柜次快照 |
| 4 | 幂等已有 SHA-256 与同参重放,但未覆盖完整规范化请求,缺 PROCESSING 恢复测试 | 仍可能误判同参或无法恢复中间态 |
| 5 | 客户账单已过滤可见状态,收款列表仍暴露平台字段且未按可见账单过滤 | 客户只读边界仍不完整 |
| 6 | Admin 已接账单动作,但未接客户账期、修订历史,整柜仍人工填体积 | 平台财务页面仍非完整闭环 |
| 7 | 业务错误码仍以通用错误和中文消息为主 | 前端无法稳定区分版本、幂等、凭证和归属错误 |
| 8 | 修订快照已有表和写入逻辑,但缺少查询展示 | 调整历史尚不能由产品和测试人员核对 |
+47 -205
View File
@@ -1,10 +1,11 @@
openapi: 3.0.3
info:
title: Finance F1 Manual AR API
version: 1.1.0
version: 1.2.0
description: >-
F1 人工账单、收款和成本目标契约。仅 CNY;平台财务可写;客户只读且仅可见
ISSUED、PARTIALLY_PAID、PAID 账单。所有响应使用固定 DTO 和统一追踪字段。
ISSUED、PARTIALLY_PAID、PAID 账单。固定 DTO、完整追踪字段、真实体积快照和
File fail-closed 校验属于放行目标;当前实现差异见契约门禁文档。
servers:
- url: /api
paths:
@@ -203,8 +204,9 @@ paths:
post:
summary: 人工登记单票或整柜成本
description: >-
SINGLE_ORDER 直接归属同客户订单;CONTAINER_SHARED 默认按柜内同客户有效票体积分摊,
只有总体积为零时才接受 MANUAL_ZERO_VOLUME。
SINGLE_ORDER 直接归属经服务端验证的同客户订单;CONTAINER_SHARED 当前请求包含
containerOrders 过渡字段,但放行前必须改由服务端从 Firstleg 取得不可篡改体积快照。
总体积为零时拒绝,首期不支持人工分摊。
x-permission: platform:finance:cost:create
security:
- platformFinance: []
@@ -221,17 +223,20 @@ paths:
$ref: '#/components/responses/CostResponse'
'409':
$ref: '#/components/responses/IdempotencyConflict'
/platform/v1/files/finance-vouchers:
/platform/finance/v1/vouchers:
post:
summary: 上传财务凭证临时文件
summary: 上传财务凭证图片
description: >-
File 服务目标接口。文件上传后为 TEMPORARY,Finance 成功创建收款或成本后通过内部接口幂等关联。
临时文件不得直接向客户开放下载。
x-permissions:
当前 Finance Controller 的真实入口,query 参数为 customerId,multipart 字段为 file。
当前 File 仅校验图片 MIME,尚无用途区分和关联状态;收款/成本落库前的 fail-closed
校验仍是门禁阻断。
x-permission-any-of:
- platform:finance:receipt:create
- platform:finance:cost:create
security:
- platformFinance: []
parameters:
- $ref: '#/components/parameters/RequiredCustomerId'
requestBody:
required: true
content:
@@ -279,46 +284,6 @@ paths:
$ref: '#/components/responses/ClientReceiptListResponse'
'403':
$ref: '#/components/responses/BusinessError'
/internal/file/v1/finance-vouchers/validate:
post:
servers:
- url: /
summary: Finance 批量校验财务凭证
description: Finance 落单前同步调用;任一文件不合规则整批失败,Finance 不落业务数据。
security:
- internalService: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VoucherValidationRequest'
responses:
'200':
$ref: '#/components/responses/VoucherValidationResponse'
/internal/file/v1/finance-vouchers/associate:
post:
servers:
- url: /
summary: Finance 幂等关联财务凭证
description: >-
Finance 落单成功后以业务幂等键调用。相同文件、资源类型和资源 ID 重放返回原关联结果;
已关联到其他资源时拒绝。调用失败进入 Finance Outbox 重试,不重复创建财务单据。
security:
- internalService: []
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VoucherAssociationRequest'
responses:
'200':
$ref: '#/components/responses/VoucherAssociationResponse'
'409':
$ref: '#/components/responses/IdempotencyConflict'
components:
securitySchemes:
platformFinance:
@@ -327,10 +292,6 @@ components:
customerFinanceRead:
type: http
scheme: bearer
internalService:
type: apiKey
in: header
name: X-Internal-Service-Token
parameters:
BillId:
name: billId
@@ -361,6 +322,9 @@ components:
name: Idempotency-Key
in: header
required: true
description: >-
服务端以 HTTP method、规范化 path、客户范围和完整规范化 JSON body 计算请求哈希。
同键同参重放原结果;同键异参冲突;PROCESSING 返回 409 和 Retry-After。
schema:
type: string
minLength: 1
@@ -373,6 +337,10 @@ components:
type: string
pattern: '^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$'
example: '300.000000'
SignedMoney:
type: string
pattern: '^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$'
example: '-0.000001'
ResponseMeta:
type: object
required:
@@ -624,7 +592,7 @@ components:
required:
- costType
- containerId
- allocationMode
- containerOrders
properties:
costType:
type: string
@@ -632,94 +600,38 @@ components:
- CONTAINER_SHARED
containerId:
$ref: '#/components/schemas/Id'
allocationMode:
type: string
enum:
- AUTO_VOLUME
- MANUAL_ZERO_VOLUME
default: AUTO_VOLUME
manualAllocations:
containerOrders:
type: array
minItems: 1
items:
$ref: '#/components/schemas/ManualCostAllocationInput'
description: 仅柜内有效票总体积为零且 allocationMode=MANUAL_ZERO_VOLUME 时允许。
ManualCostAllocationInput:
$ref: '#/components/schemas/ContainerOrderVolumeInput'
description: >-
当前 Java 过渡请求字段。调用方不得把人工填写体积视为可信快照;放行前应由
Finance 根据 containerId 从 Firstleg 获取并校验,届时公开请求应移除此字段。
ContainerOrderVolumeInput:
type: object
required:
- orderId
- amount
- inboundId
- containerId
- volumeCbm
properties:
orderId:
$ref: '#/components/schemas/Id'
amount:
inboundId:
$ref: '#/components/schemas/Id'
containerId:
$ref: '#/components/schemas/Id'
volumeCbm:
$ref: '#/components/schemas/Money'
FinanceVoucherUploadRequest:
type: object
required:
- customerId
- purpose
- file
properties:
customerId:
$ref: '#/components/schemas/Id'
purpose:
type: string
enum:
- RECEIPT
- COST
file:
type: string
format: binary
VoucherValidationRequest:
type: object
required:
- customerId
- purpose
- fileIds
properties:
customerId:
$ref: '#/components/schemas/Id'
purpose:
type: string
enum:
- RECEIPT
- COST
fileIds:
type: array
minItems: 1
uniqueItems: true
items:
$ref: '#/components/schemas/Id'
VoucherAssociationRequest:
type: object
required:
- customerId
- purpose
- resourceType
- resourceId
- fileIds
properties:
customerId:
$ref: '#/components/schemas/Id'
purpose:
type: string
enum:
- RECEIPT
- COST
resourceType:
type: string
enum:
- FINANCE_RECEIPT
- FINANCE_COST
resourceId:
$ref: '#/components/schemas/Id'
fileIds:
type: array
minItems: 1
uniqueItems: true
items:
$ref: '#/components/schemas/Id'
BillStatus:
type: string
enum:
@@ -929,6 +841,7 @@ components:
- ratio
- allocatedAmount
- roundingAdjustment
- roundingDelta
properties:
orderId:
$ref: '#/components/schemas/Id'
@@ -942,7 +855,10 @@ components:
allocatedAmount:
$ref: '#/components/schemas/Money'
roundingAdjustment:
$ref: '#/components/schemas/Money'
type: boolean
description: 仅实际尾差非零时为 true。
roundingDelta:
$ref: '#/components/schemas/SignedMoney'
Cost:
type: object
required:
@@ -977,7 +893,6 @@ components:
enum:
- DIRECT
- AUTO_VOLUME
- MANUAL_ZERO_VOLUME
expenseName:
type: string
amount:
@@ -1025,22 +940,14 @@ components:
required:
- fileId
- customerId
- purpose
- fileName
- contentType
- sizeBytes
- sha256
- status
properties:
fileId:
$ref: '#/components/schemas/Id'
customerId:
$ref: '#/components/schemas/Id'
purpose:
type: string
enum:
- RECEIPT
- COST
fileName:
type: string
contentType:
@@ -1049,54 +956,11 @@ components:
- image/jpeg
- image/png
- image/webp
- application/pdf
sizeBytes:
type: integer
format: int64
minimum: 1
maximum: 10485760
sha256:
type: string
pattern: '^[a-f0-9]{64}$'
status:
type: string
enum:
- TEMPORARY
- ASSOCIATED
resourceType:
type: string
resourceId:
$ref: '#/components/schemas/Id'
VoucherValidationResult:
type: object
required:
- valid
- files
properties:
valid:
type: boolean
files:
type: array
items:
$ref: '#/components/schemas/VoucherFile'
VoucherAssociationResult:
type: object
required:
- resourceType
- resourceId
- files
properties:
resourceType:
type: string
enum:
- FINANCE_RECEIPT
- FINANCE_COST
resourceId:
$ref: '#/components/schemas/Id'
files:
type: array
items:
$ref: '#/components/schemas/VoucherFile'
ErrorDetail:
type: object
required:
@@ -1119,6 +983,12 @@ components:
code:
type: string
example: FIN-AR-002
description: >-
稳定业务码。体积快照/归属使用 FIN-COST-VOLUME-SNAPSHOT-REQUIRED、
FIN-COST-VOLUME-SNAPSHOT-INVALID、FIN-COST-CUSTOMER-MISMATCH、
FIN-COST-ORDER-NOT-IN-CONTAINER;凭证使用 FIN-FILE-VALIDATION-UNAVAILABLE、
FIN-FILE-CUSTOMER-MISMATCH、FIN-FILE-TYPE-INVALID;幂等使用
FIN-IDEMPOTENCY-CONFLICT、FIN-IDEMPOTENCY-PROCESSING。
message:
type: string
details:
@@ -1241,24 +1111,6 @@ components:
code: {type: string, example: '200'}
message: {type: string}
data: {$ref: '#/components/schemas/VoucherFile'}
VoucherValidationEnvelope:
allOf:
- $ref: '#/components/schemas/ResponseMeta'
- type: object
required: [code, message, data]
properties:
code: {type: string, example: '200'}
message: {type: string}
data: {$ref: '#/components/schemas/VoucherValidationResult'}
VoucherAssociationEnvelope:
allOf:
- $ref: '#/components/schemas/ResponseMeta'
- type: object
required: [code, message, data]
properties:
code: {type: string, example: '200'}
message: {type: string}
data: {$ref: '#/components/schemas/VoucherAssociationResult'}
responses:
BillListResponse:
description: 平台账单列表。
@@ -1315,16 +1167,6 @@ components:
content:
application/json:
schema: {$ref: '#/components/schemas/VoucherUploadEnvelope'}
VoucherValidationResponse:
description: 财务凭证批量校验结果。
content:
application/json:
schema: {$ref: '#/components/schemas/VoucherValidationEnvelope'}
VoucherAssociationResponse:
description: 财务凭证关联结果。
content:
application/json:
schema: {$ref: '#/components/schemas/VoucherAssociationEnvelope'}
BusinessError:
description: 业务错误。
content:
@@ -82,7 +82,7 @@ G0 允许:完善 API/OpenAPI、数据库迁移草案、契约测试样例、
- 完善 F0/F1 数据字典、状态转移表和权限矩阵;
- 设计 Finance Admin/Client 页面和空态、错误态、线下异议录入态;
- 准备脱敏历史费用样本与迁移校验脚本设计;
- 按 OpenAPI 1.1.0 修复服务端 DTO、凭证链路、体积分摊、幂等和客户可见状态,并补齐契约测试。
- 按 OpenAPI 1.2.0 修复服务端 DTO、凭证链路、真实体积快照与归属、幂等和客户可见状态,并补齐契约测试。
### 本轮不再等待产品确认