This commit is contained in:
2026-08-10 17:48:45 +08:00
parent 80f71e57f1
commit 9689522041
10 changed files with 2113 additions and 4 deletions
@@ -0,0 +1,122 @@
# Firstleg-Finance 跨服务契约(技术草案)
> 版本:V0.1(开发前冻结草案)
> 日期:2026-08-10
> 状态:技术边界已确认;业务口径以《财务管理产品确认表》为准,未将产品待确认项擅自固化。
本文是 `firstleg-service` 与规划中的 `finance-service` 之间的技术契约草案。它只规定服务边界、传输可靠性和来源快照,不替代财务账单、收款、核销、汇率或收入确认规则。
## 1. 事实依据与当前实现差异
当前 Firstleg 已有 `fl_inbound_outbox`(与入库事务同库写入)和发布状态/重试字段;`FbaPricingService` 已发布 `FIRSTLEG_FEE_SENT`,调整已发布费用时发布 `FIRSTLEG_FEE_ADJUSTED`。现有 payload 主要只有 `inboundId`、`feeId`、`feeVersion`,Finance 不得把它当作完整财务凭证,必须再调用受保护的来源快照接口。
当前代码没有独立 Finance 消费者、`FIRSTLEG_FEE_VOIDED` 事件或 Finance 数据库;下列事件版本、信封、消费表和接口均为实施时新增/补齐的契约,不代表已上线。
## 2. 事件目录
| 事件名 | 生产时机 | Finance 处理 | 备注 |
|---|---|---|---|
| `FIRSTLEG_FEE_PUBLISHED` | 费用确认并发送、来源明细发布成功 | 建立/更新来源池候选,待规则校验 | 首版可由现有 `FIRSTLEG_FEE_SENT` 兼容映射;新契约统一使用 PUBLISHED |
| `FIRSTLEG_FEE_ADJUSTED` | 已发布费用被调整,新版本替代旧版本 | 原来源保留,新增调整来源;不得覆盖已出账明细 | 只有来源版本递增且哈希改变才接受 |
| `FIRSTLEG_FEE_VOIDED` | 费用来源作废 | 标记来源作废;若已入账,生成冲销/补差任务,不自动删除应收 | 需要 Firstleg 补充明确作废动作及原因 |
事件不是“已生成应收”通知;Finance 仍按产品确认的入池条件(例如 `settlementIncluded`、审核状态和作废状态)决定是否进入可出账池。
## 3. 统一事件信封
```json
{
"eventId": "fl-01J...", "eventType": "FIRSTLEG_FEE_PUBLISHED",
"eventVersion": 1, "producer": "firstleg-service", "occurredAt": "2026-08-10T10:20:30.123+08:00",
"tenantId": "123", "aggregateType": "INBOUND", "aggregateId": "456",
"sequence": 3, "idempotencyKey": "FIRSTLEG_FEE:456:789:2",
"payloadHash": "sha256...", "traceId": "...", "payload": { "inboundId": "456", "feeId": "789", "feeVersion": 2 }
}
```
约束:ID 以字符串传输;`eventId` 全局唯一且不可复用;`eventVersion` 为整数并只向后兼容增加字段;金额/数量不放在短 payload 中,统一从快照查询获取;`payloadHash` 对规范化 JSON 计算 SHA-256;`tenantId` 必须与来源快照客户一致。`sequence` 按同一 `aggregateId` 单调递增,用于识别乱序,但不能替代版本校验。
## 4. 来源快照内部接口
Finance 使用服务间令牌调用 Firstleg(不经过客户端权限,不共享 Entity):
`GET /internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/source-snapshot`
请求头:`X-Service-Name: finance-service`、`X-Service-Token`、`X-Request-Id`。令牌由环境密钥管理,按服务名、路径和方法授予最小权限;拒绝客户端 JWT 伪造内部调用。
响应 DTO 至少包含:
```text
tenantId, inboundId, inboundCode, customerId, customerName,
feeId, feeNo, feeVersion, sourceType, direction, businessDate,
currency, subtotalAmount, additionalAmount, totalAmount,
feeStatus, publicationStatus, settlementIncluded, publishedAt,
chargeUnit, chargeableQuantity, actualGrossWeightKg, actualVolumeCbm,
originWarehouseId/name, channelCode/name, destinationCode/name,
items[{feeItemId, feeCode, name, quantity, unit, unitPrice, amount}],
sourceHash, snapshotVersion
```
响应必须是当前 Firstleg 已发布版本的不可变业务快照;Finance 将完整 DTO 和 `sourceHash` 存入自有来源池。若上游只返回 `SUPERSEDED`/`VOID` 版本,Finance 不得将其作为新应收来源。
建议附加只读校验接口:`GET /internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/publication`,用于消费重试时快速判断发布状态;该接口不能返回跨客户数据。
## 5. 消费幂等、乱序与冲突
- Finance 在本地 `fin_event_inbox` 以 `eventId` 唯一;以 `sourceType + sourceFeeId + sourceFeeVersion` 建唯一业务键。
- 首次消费在同一事务内写入 Inbox、来源快照和处理结果;重复事件返回原结果,不重复生成来源/账单。
- 同一业务键的 `payloadHash` 不一致,标记 `CONFLICT`,告警并人工处理,禁止覆盖原快照。
- `feeVersion` 小于已处理版本:标记 `STALE`,保留审计,不回退财务来源。
- 高版本先到:先保存为 `PENDING_SOURCE`,查询快照成功且版本链完整后转 `ACCEPTED`;缺少前序版本不阻塞新版本,但需记录 gap,产品规则决定是否暂停出账。
- `VOIDED` 只能作用于同一来源版本;已出账时生成待处理冲销事件/任务,绝不物理删除。
## 6. 重试、超时与降级
Firstleg Outbox 发布采用至少一次投递:指数退避(建议 10s、30s、2m、10m、30m,最多 12 次),每次带 `traceId`;超过次数进入 `FAILED/DLQ` 并告警。Finance 消费失败分为:
| 类型 | 处理 |
|---|---|
| 连接/HTTP 408、429、5xx | 可重试;不确认消息 |
| 401/403、签名/租户不匹配 | 不重试,标记 `REJECTED` 并告警 |
| 来源 404/尚未提交 | 重试查询,超过保留期转 `SOURCE_UNAVAILABLE` |
| DTO 校验、哈希或版本冲突 | 不自动重试,进入人工队列 |
快照接口超时建议 3 秒连接、5 秒读取;消费端总处理超时 10 秒。Finance 不因 Firstleg 暂时不可用而伪造金额、跳过来源校验或直接生成应收;仅允许已保存且校验通过的来源继续后续财务流程。
## 7. 统一错误码(内部契约)
| 错误码 | 含义 | 是否重试 |
|---|---|---|
| `FIN-SRC-001` | 来源不存在/不可见 | 查询型可重试,最终人工 |
| `FIN-SRC-002` | 来源状态不满足入池 | 否 |
| `FIN-SRC-003` | 客户/主体范围不一致 | 否,安全告警 |
| `FIN-SRC-004` | 来源哈希或版本冲突 | 否,人工 |
| `FIN-SRC-005` | 快照字段不完整 | 否,修复上游契约后重放 |
| `FIN-EVT-001` | 事件重复且内容一致 | 否,返回原结果 |
| `FIN-EVT-002` | 事件重复但内容不同 | 否,隔离冲突 |
| `FIN-EVT-003` | 事件版本不支持 | 否,进入 DLQ |
| `FIN-AUTH-001` | 服务令牌无效/过期 | 否 |
| `FIN-TIME-001` | 财务期间已关闭 | 否,走调整/冲销 |
业务 API 继续遵循后端统一约定:HTTP 200 + body `code`(成功为字符串 `"200"`);内部认证/基础设施拒绝可使用 401/403/503。
## 8. 可观测性与审计
所有事件和查询日志必须带 `eventId`、`traceId`、`tenantId`、`aggregateId`、`sourceFeeId/version`、`attempt`、`consumerStatus`。Finance 至少提供 Inbox/来源池指标:待处理数、重试数、DLQ 数、冲突数、快照延迟、事件到财务入池延迟。拒绝、冲突、作废、重放和人工补偿写入财务审计;日志不得记录服务令牌和完整客户敏感凭证。
## 9. 发布顺序与契约测试
1. 先冻结本文和产品确认中 F0-08/F1-09 的业务口径,补齐 Firstleg 事件版本与快照 OpenAPI。
2. 先上线兼容生产者(保留 `FIRSTLEG_FEE_SENT` 映射),再部署 Finance 消费者并回放历史 Outbox;确认无冲突后切换新事件名。
3. 最后启用调整/作废新事件及 Finance 出账入口;任何一方不支持的 `eventVersion` 必须安全拒绝。
契约测试至少覆盖:信封字段和 JSON Schema、签名/服务令牌、租户隔离、重复事件同结果、同键不同哈希冲突、乱序版本、Outbox 重试/DLQ、快照 404/超时、作废已出账保护、金额精度和三位币种、跨币种核销拒绝。测试可由 Firstleg producer contract test 与 Finance consumer contract test 共享固定 JSON fixtures,但不得共享 Java Entity。
## 10. 确认边界与风险
技术已确认:独立服务/数据库、DTO/事件通信、Outbox 至少一次、Finance Inbox 幂等、不可变来源快照、无跨库 Join、服务令牌、错误隔离和审计要求。
仍依赖产品确认:何时 `settlementIncluded`、何种来源进入应收;客户/供应商账单聚合和确认节点;调整/作废对已出账单的补差或冲销策略;汇率、财务期间和收入/成本确认口径;客户账户是否抵扣应收。上述未确认项不得由事件消费者自行推断。
主要风险:现有 `FIRSTLEG_FEE_SENT` payload 过小且 Outbox 未带 `eventVersion/payloadHash/sequence`;当前没有明确的作废 API;`fl_consumed_event` 属 Firstleg 内部消费表,不能直接复用为 Finance Inbox。实施时需新增兼容字段/映射和回放工具,并在切换前验证历史费用版本的唯一性与哈希完整性。
+17 -4
View File
@@ -5,6 +5,18 @@
> 盘点范围:二期入库计费现状、平台后台财务原型、Admin/Client/Firstleg 当前实现 > 盘点范围:二期入库计费现状、平台后台财务原型、Admin/Client/Firstleg 当前实现
> 结论口径:本文区分“代码已实现”“原型展示”和“建议范围”;原型功能不自动成为开发范围。 > 结论口径:本文区分“代码已实现”“原型展示”和“建议范围”;原型功能不自动成为开发范围。
产品/业务逐项确认请使用:[财务管理产品确认表](财务管理产品确认表.md)。
技术边界已确认:[财务服务边界设计](财务服务边界设计.md)。Finance 采用独立服务和独立数据库,Firstleg 仅提供业务费用来源。
开发前总控门禁与 F0/F1 任务清单:[财务开工门禁与F0-F1任务清单](财务开工门禁与F0-F1任务清单.md)。
详细设计材料:
- [财务领域模型与数据字典](财务领域模型与数据字典.md)
- [Firstleg-Finance跨服务契约](Firstleg-Finance跨服务契约.md)
- [财务历史迁移与上线验收方案](财务历史迁移与上线验收方案.md)
## 一、结论 ## 一、结论
当前已经实现了从入库测量数据到对客头程费用发布的**业务计费闭环**: 当前已经实现了从入库测量数据到对客头程费用发布的**业务计费闭环**:
@@ -144,7 +156,7 @@ Firstleg 已有 `fl_inbound_fee_detail`,包含:
| 阶段 | 交付范围 | 关键闭环 | 前置依赖 | | 阶段 | 交付范围 | 关键闭环 | 前置依赖 |
|---|---|---|---| |---|---|---|---|
| F0 财务底座 | 财务主体、客户/供应商往来单位、币种、费用科目、期间、权限、审计、幂等、金额精度 | 所有后续对象使用同一基础口径 | 产品确认主体、币种、权限和编码 | | F0 财务底座 | 独立 Finance 服务/数据库、财务主体、客户/供应商往来单位、币种、费用科目、期间、权限、审计、幂等、金额精度 | 所有后续对象使用同一基础口径 | 服务边界已技术确认;产品确认主体、币种、权限和编码 |
| F1 应收与客户对账 | 应收费用池、客户账单、账单确认/争议、结算单应收、收款登记、核销、反核销 | 费用 → 账单 → 应收 → 收款 → 结清 | 当前已发布对客费用来源 | | F1 应收与客户对账 | 应收费用池、客户账单、账单确认/争议、结算单应收、收款登记、核销、反核销 | 费用 → 账单 → 应收 → 收款 → 结清 | 当前已发布对客费用来源 |
| F2 应付与供应商结算 | 成本费用池、供应商账单、结算单应付、付款登记、付款核销、应付调整 | 成本 → 供应商账单 → 应付 → 付款 → 结清 | 供应商主数据和真实成本来源 | | F2 应付与供应商结算 | 成本费用池、供应商账单、结算单应付、付款登记、付款核销、应付调整 | 成本 → 供应商账单 → 应付 → 付款 → 结清 | 供应商主数据和真实成本来源 |
| F3 客户账户与发票 | 充值、冻结、扣款、退款、账户流水;开票申请、开票登记、红冲/作废 | 账户余额与应收核销可选联动;应收与发票可追溯 | 资金/税务主体、开票平台和合规口径 | | F3 客户账户与发票 | 充值、冻结、扣款、退款、账户流水;开票申请、开票登记、红冲/作废 | 账户余额与应收核销可选联动;应收与发票可追溯 | 资金/税务主体、开票平台和合规口径 |
@@ -305,6 +317,7 @@ DRAFT -> REVIEWED -> SENT -> CONFIRMED -> CLOSED
1. 由产品/业务填写 F1-F12,冻结首期范围。 1. 由产品/业务填写 F1-F12,冻结首期范围。
2. 形成《客户应收闭环需求确认表》,明确页面、角色、状态和异常处理。 2. 形成《客户应收闭环需求确认表》,明确页面、角色、状态和异常处理。
3. 形成《财务领域模型与服务边界设计》,决定财务数据是否进入独立服务,并定义与 Firstleg 的事件/接口契约。 3. 使用《财务管理产品确认表》完成 S1-S8、F0-F4 的产品确认。
4. 形成《客户应收接口与权限契约》,包括错误码、幂等、数据范围和审计事件。 4. 服务边界已经技术确认;下一步形成《财务领域模型与跨服务契约》,细化 Finance 与 Firstleg 的事件、内部查询、版本和错误处理,不再重复讨论是否拆服务。
5. 形成《历史费用迁移与上线方案》,避免已有线下结算数据被重复出账。 5. 形成《客户应收接口与权限契约》,包括错误码、幂等、数据范围和审计事件。
6. 形成《历史费用迁移与上线方案》,避免已有线下结算数据被重复出账。
+190
View File
@@ -0,0 +1,190 @@
# 财务 F0 实施任务拆分与验收门禁
> 本文只拆分技术实施任务,不定义新的业务规则,也不修改现有规划。范围依据《财务服务边界设计》《财务管理产品确认表》和后端仓库现行 Maven、Nacos、Gateway、Docker Compose、Jenkins 约定。
>
> F0 目标:建立可独立构建、部署、回滚的 `finance-service` 与 `baiyue_finance`,冻结后续财务共同底座(主体/账套、往来单位快照、币种与精度、期间、编号、来源入池、幂等、权限和审计)的技术承载方式。F0 不交付应收、应付、收付款、核销、发票或利润业务闭环。
## 1. 实施边界与不变约束
- 根 Maven 聚合新增模块目录 `finance/`,其 `artifactId` 固定为 `finance-service`;Nacos 服务名固定为 `finance-service`,默认端口 `9203`,数据库固定为 `baiyue_finance`。
- Finance 只访问自己的数据库,不建跨库外键、不做跨库 Join、不共享其他服务 Entity;跨服务信息通过 DTO、内部服务令牌和事件协作。
- Firstleg 仍是业务费用来源的唯一生产者;Finance 接收 APPROVED、PUBLISHED、`settlementIncluded=true` 且未作废/未被有效账单使用的不可变费用版本。
- 产品层仍只有“平台—客户”两级:`tenant_id=0` 为平台技术作用域,客户数据使用 `tenant_id>0`;不新增租户选择器或租户级 RBAC。
- 所有财务写操作必须具备幂等键、乐观锁、服务端客户范围校验、权限校验和审计留痕;金额使用定点精度,原币金额不可覆盖。
## 2. 分阶段任务、依赖与验收命令
### F0-1:Maven `finance-service` 骨架
**任务**
1. 在根 `pom.xml` 增加 `<module>finance</module>`;创建与现有服务一致的 `finance/pom.xml`、Spring Boot 入口、`application.yml`、测试目录和 OpenAPI 目录。
2. 复用 `common-core`、`common-web`、`common-security`、`common-openapi`、`common-audit`、Nacos Discovery/Config、MyBatis-Plus、Actuator 等既有依赖;不得复制公共安全或审计实现。
3. 配置 `FINANCE_DB_URL/USERNAME/PASSWORD`、`FINANCE_PORT`、`INTERNAL_SERVICE_TOKEN`、Nacos 连接和日志变量;健康检查可在无业务表时启动。
4. 建立 `foundation/`、`source/`、`idempotency/`、`audit/` 等包边界,先落 DTO/接口占位和错误码约定,不实现 F1-F4 业务。
**依赖**:现有根聚合及公共模块可构建;产品确认表 F0-02 至 F0-10 的字段、状态和权限结论至少形成技术草案。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -DskipTests validate
mvn -pl finance -am test
mvn -pl finance -am package
```
必须生成 `finance/target/finance-service-1.0.0-SNAPSHOT.jar`,应用名和端口默认值分别为 `finance-service`、`9203`;禁止通过直接依赖 Firstleg Entity 通过编译。
### F0-2:独立数据库与迁移
**任务**
1. 在 `finance/src/main/resources/db/` 建立可溯源的 Finance 基线/兼容迁移:主体/账套、往来单位引用与名称快照、币种/精度、汇率版本预留、财务期间、编号器、来源费用池、Inbox/消费结果、乐观锁/审计所需表和索引。
2. 将 Finance 数据库创建及迁移区块纳入 `deploy/sql/头程数据库统一增量-MySQL8.sql`;遵守现有入口的 `information_schema` 判断、幂等写入、只增不删和末尾 `PASS` 总体验证规则。不得另建日期发布 SQL。
3. 明确所有表的 `tenant_id`/主体隔离键、金额/币种字段、来源版本/哈希/幂等键唯一约束;不得出现指向其他数据库的外键。
4. 提供只读结构断言脚本或测试资源,覆盖表、索引、精度、唯一键、期间状态和数据库归属。
**依赖**:F0-1;产品确认表 F0-02 至 F0-08 的未决项需在进入对应业务写入前冻结,未冻结时只允许迁移预留字段。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
# 模块 SQL/结构测试
mvn -pl finance -am test
# 在获授权的 MySQL 8 测试库执行唯一发布入口(不得在本地自行启动数据库)
mysql --default-character-set=utf8mb4 < deploy/sql/头程数据库统一增量-MySQL8.sql
```
数据库执行结果必须为 `migration_status = PASS`;只读核验 `baiyue_finance` 存在、Finance 表全部属于该库、无跨库外键,且脚本可重复执行不重复造数。测试环境若不可用,记录阻塞,不以本地模拟成功替代。
### F0-3:Gateway 与 OpenAPI 聚合
**任务**
1. 在 `gateway/src/main/resources/application.yml` 增加 Finance 显式路由,分别预留平台端 `/api/platform/v1/finance/**`、客户端 `/api/merchant/v1/finance/**`(具体资源路径以契约冻结为准);如需环境开关,再同步 `config/nacos/{local,cn}/gateway.yml`,该文件不作为路由定义来源。
2. 在 Gateway 应用配置增加 `/finance-service/v3/api-docs/**` 聚合路由与 Swagger UI 条目;同步 App Scope、CORS、JWT 公开路径检查。
3. 不暴露 Finance 内部事件消费、服务令牌快照或管理端点;下游 Finance 仍必须再次校验客户范围和权限。
**依赖**:F0-1;Finance Controller/OpenAPI 路径和平台/客户访问域已书面冻结。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -pl gateway -am test
```
用已部署测试环境只读验证:登录后平台/客户路由分别可达,未登录返回 401,错误 App Scope 返回 403,内部路径经 Gateway 不可达,Swagger 聚合返回 Finance 文档;若契约路径未冻结,路由任务保持待办而不提前开放泛匹配。
### F0-4:部署、配置与回滚
**任务**
1. 更新 `deploy/service-map.yml`(artifact、JAR、9203)和 `deploy/docker-compose.yml`(镜像、容器、端口、日志、Finance 数据源环境变量)。
2. 更新 `Jenkinsfile`、`Jenkinsfile.service` 的服务白名单、Maven 模块/JAR 映射和部署参数,使 `finance` 可单服务 `up`、重启和回滚;`deploy/scripts/deploy-services.sh` 继续接收 Compose 服务名并执行部署前置校验。补充 `config/nacos/{local,cn}/finance-service.yml`,敏感值全部使用环境变量。
3. 验证镜像构建上下文只包含 Finance JAR 与必要运行文件;健康检查、日志目录和网络与现有服务一致。
**依赖**:F0-1、F0-2;测试环境具备 `baiyue_finance` 连接凭据和镜像构建权限。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -pl finance -am -DskipTests package
sh deploy/scripts/deploy-services.sh test <image-repository> <image-tag> finance <env-file>
docker compose --project-name firstleg-test --env-file <env-file> -f deploy/docker-compose.yml ps finance
```
容器注册为 `finance-service`、监听 9203、Actuator health 为 UP;仅重部署 Finance 不重启其他服务;使用上一镜像标签可回滚,迁移脚本不删除已存在财务数据。生产/预发布部署须另获当前对话授权。
### F0-5:IAM 权限与审计接入
**任务**
1. 在 IAM 迁移中加入 Finance 菜单/API/按钮权限资源的技术占位和角色授权映射,权限码、父菜单、平台/客户 app scope、风险等级在契约冻结后一次性确定;不自行增加租户层级。
2. Finance 接入公共 JWT/授权快照校验和审计 Outbox/审计客户端;为制单、复核、确认、期间开关、来源入池等 F0 写操作定义独立审计事件,记录操作者、客户、主体、幂等键、前后版本和 request/trace id。
3. 服务令牌仅用于受保护的 Firstleg 内部快照/事件协作,禁止作为浏览器权限替代。
**依赖**:F0-1、F0-2;产品确认表 F0-10 高风险权限结论及 Finance/Firstleg 事件与内部查询契约冻结。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -pl iam,finance -am test
```
验收正常、无权限、跨客户、跨主体、关闭期间写入均分别得到预期业务拒绝;每次成功或拒绝的财务写操作都有可查询审计记录。权限未冻结时不得把占位权限授予生产角色,作为该能力的阻塞项记录。
### F0-6:CI/CD 与受影响服务识别
**任务**
1. 在 `Jenkinsfile` 的 `serviceModules`、`serviceCheckboxes` 和参数 checkbox 中加入 `finance`;在 `Jenkinsfile.service` 的 `serviceModules`、`serviceJars`、允许服务校验、Maven 模块解析、镜像/部署参数中加入 `finance`。
2. 保持现有规则:Finance 目录变更只触发 Finance;根 POM、公共模块、`deploy/` 或总控流水线变更触发全服务;Finance 与 Gateway/IAM/Firstleg 契约变更按显式勾选或公共路径规则联动。
3. 更新 `.gitlab-ci.yml`(如继续使用)或等价 CI 配置,确保全量 verify 与 Finance 单服务 package/test 可执行。
**依赖**:F0-1、F0-4;Jenkins 子 Job/凭据/测试环境部署目标已配置。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -DskipTests validate
mvn -pl finance -am clean test
mvn clean verify
```
通过 Jenkins dry-run/测试 Job 证明:Finance 单独变更不构建无关服务;公共模块变更包含 Finance;构建成功后可独立推镜像并按参数部署。任何缺少子 Job、凭据或环境变量的情况必须在 CI 阻塞项中明确,不得静默跳过。
### F0-7:契约、集成与回归测试
**任务**
1. 冻结 Firstleg 费用 Outbox 事件、来源版本/哈希、内部快照查询、服务令牌和错误码;Finance 建立 Inbox、消费结果、失败重试/DLQ、乱序与冲突处理。
2. 编写单元、Repository/迁移、Controller 安全、契约和端到端回归用例:首次入池、重复事件、重复请求、并发版本冲突、重试恢复、跨客户/币种、已关闭期间和审计完整性。
3. 仅以真实来源快照和财务库台账断言结果;不得用前端静态数字或修改 Firstleg 费用状态冒充财务入池。
**依赖**:F0-1 至 F0-6;Firstleg 事件生产者和测试数据/测试环境可用。
**验收门禁**
```bash
# 以下命令均在 baiyue-firstleg/ 目录执行
mvn -pl finance -am test
mvn -pl gateway,iam,firstleg,finance -am test
mvn test
```
所有 F0 正常与异常案例通过;同一来源版本最多生成一条有效来源记录,重试返回原结果;越权、跨币种直接合并、关闭期间直接改账均被服务端拒绝;事件失败可观察、可重试且最终审计完整。
## 3. 总体验收闸门与阻塞项
F0 只有在 F0-1 至 F0-7 全部通过后才能标记完成,并允许 F1/F2 进入生产数据链路。总体验收至少包含:
1. `mvn -DskipTests validate`、`mvn -pl finance -am test`、受影响模块测试和全量 `mvn test` 均通过。
2. 统一 SQL 返回 `migration_status = PASS`,Finance 数据库独立、可重复迁移、无跨库外键/Join。
3. Finance JAR、镜像、容器、Gateway 路由、Swagger、Nacos 配置和单服务回滚均可验证。
4. IAM 权限、客户范围、主体范围、期间状态、幂等/乐观锁、审计和事件可靠性均有自动化或测试环境证据。
5. 形成版本化的 Finance/Firstleg 事件与内部查询契约,明确 requestId/traceId、重试、DLQ 和兼容策略。
以下任一项未解决,只阻塞相应能力,不得通过“先做页面/静态数据”绕过:
| 阻塞项 | 影响范围 | 解除条件 |
|---|---|---|
| F0-02/F0-03/F0-06/F0-07 产品字段或编码未确认 | 对应主体、币种、期间、编号、入池写入及后续 F1-F4 | 产品确认表形成结论或明确暂缓和默认值;技术骨架可继续 |
| Finance/Firstleg 事件、快照、服务令牌未冻结 | 来源入池、幂等消费和集成回归 | 双方 DTO/OpenAPI/事件版本及错误处理书面确认 |
| MySQL 测试库、镜像仓库、Nacos 或 Jenkins 凭据不可用 | 迁移、部署、CI 环节 | 测试环境只读核验/凭据配置完成;不得改用本地后端伪造通过 |
| IAM 权限码/高风险角色未审核 | 受保护写操作和生产发布 | 权限资源、app scope、角色授权与审计事件审核通过 |
| 受影响模块测试或契约测试失败 | F0 总体验收及进入 F1/F2 | 修复并保留回归用例;不得以跳过测试作为完成证据 |
## 4. 交付物清单
- `finance/` Maven 服务骨架及单元/契约测试。
- Finance 独立迁移目录、统一增量 SQL 区块及只读结构断言。
- Finance local/cn Nacos 配置、Gateway 路由/OpenAPI 聚合、Compose/service-map/部署脚本变更。
- Jenkins 总控与单服务 Job 的 Finance 构建、镜像、部署映射。
- IAM 权限与审计接入、Firstleg↔Finance 事件/内部查询契约及 F0 验收证据。
@@ -0,0 +1,560 @@
# 财务历史迁移与上线验收方案
> 版本:V1.0
> 编制日期:2026-08-10
> 状态:开发前设计;技术方案已定,业务数据口径待产品确认
> 适用范围:Finance F0 财务底座、F1 客户应收与对账首发上线
> 关联文档:[财务管理闭环盘点与范围建议](README.md)、[财务服务边界设计](财务服务边界设计.md)、[财务管理产品确认表](财务管理产品确认表.md)
## 一、目标与基本结论
本方案用于把 Finance 上线前已经存在于 Firstleg 的历史对客费用,安全地接入独立 `finance-service` 和 `baiyue_finance`,并定义 F0/F1 从测试、灰度到正式启用的验收门禁。
本方案不把“费用已发送/已发布”推断为“客户尚欠款”,也不把二期计费状态推断为财务结算状态。历史迁移分成三个明确步骤:
```text
Firstleg 历史费用候选集
-> 迁移暂存与规则校验
-> Finance 财务来源池(不自动生成应收)
-> 经业务确认的可出账来源(才允许生成对账单/应收)
```
正式上线采用以下技术原则:
- Firstleg 是历史业务费用事实来源,Finance 不跨库 Join、不直接写 Firstleg 数据库;
- 迁移通过受保护的导出/查询契约或离线只读导出文件完成,导入由 Finance 执行;
- 每一条历史来源保留来源服务、来源主键、业务编号、版本、金额、币种和内容哈希;
- 迁移批次可重跑、单条记录幂等,同一来源版本不会重复入池;
- 线下已结算、已作废、已被替代或无法证明归属的记录默认排除,不自动形成应收;
- 历史数据迁移完成且对账通过后,才开启实时费用事件消费;
- Finance 上线前不回写或批量修改 Firstleg 历史费用状态;
- 已生成财务单据后不物理删除,错误通过冲销、反核销或补偿记录修正。
## 二、确认状态
### 2.1 技术已定事项
| 事项 | 结论 |
|---|---|
| 服务与数据库 | 独立 `finance-service`、独立 `baiyue_finance` |
| 数据读取 | 受保护 DTO 查询或只读导出;禁止跨库 Join |
| 迁移层次 | 暂存、财务来源池、出账三层分离 |
| 幂等键 | `sourceService + sourceType + sourceId + sourceVersion`;批次另有唯一 `migrationBatchNo` |
| 内容冲突 | 同一幂等键且哈希不同必须拒绝,进入人工冲突队列 |
| 历史留痕 | 保存原始来源快照、迁移规则版本、批次号和校验结果 |
| 上线切换 | 先历史迁移水位,再实时事件追平;用时间水位与事件 ID 防止漏单/重单 |
| 回滚原则 | 未激活暂存记录可整批废弃;已入账记录只补偿/冲销,不物理删除 |
| 发布策略 | 测试环境验证 → 生产影子接入 → 小范围灰度 → 全量启用 |
### 2.2 待产品/业务确认事项
以下事项影响哪些历史记录成为客户欠款,不能由技术自行推断:
| 编号 | 待确认业务口径 | 本方案安全默认值 |
|---|---|---|
| M-BIZ-01 | 历史费用纳入起始日期/截止日期 | 不预设;首次执行前按财务主体、客户和业务日期书面冻结 |
| M-BIZ-02 | 历史候选费用的最终状态组合 | 仅考虑已审核、已发布/发送、参与结算且未作废的应收方向费用 |
| M-BIZ-03 | 线下已结算的判定依据 | 以产品确认的线下结算台账、到账凭证或财务签字清单为准;无法判断的先隔离 |
| M-BIZ-04 | 部分线下结算如何处理 | 默认整条隔离,不自动拆分余额;确认后按“原金额 + 期初已结金额/余额”专门迁移 |
| M-BIZ-05 | 历史费用是否直接生成对账单/应收 | 默认只进入来源池,财务人工选择后生成 |
| M-BIZ-06 | 历史到期日、账期和逾期计算 | 未确认时不补算逾期;待客户结算条款和基准日确认 |
| M-BIZ-07 | 历史费用调整链如何取版本 | 默认仅取每条业务来源当前最终有效版本,调整/冲销链必须成组校验 |
| M-BIZ-08 | 已线下结算数据是否需要作为期初档案展示 | 默认不进 F1 应收;如需要,只读迁入“历史已结算档案”,不得产生可收余额 |
| M-BIZ-09 | 首批灰度客户及财务主体 | 上线前由产品、财务、运营共同给出白名单 |
| M-BIZ-10 | 迁移差异允许阈值 | 金额与条数默认要求零差异;确需容差必须逐币种、逐类型确认,且不得自动抹平差额 |
上述事项未确认不阻断 Finance 骨架、暂存区、校验器、批次和报表开发,但阻断生产历史数据激活及 F1 对客户开放。
## 三、历史数据范围
### 3.1 候选来源
首批只处理能够追溯到 Firstleg 已发布对客费用版本的 `RECEIVABLE` 数据。当前二期费用用于证明业务计费结果,不直接证明应收、收款或核销事实。
每条候选记录至少应能提供:
- 客户 ID、客户名称快照;
- 入库单 ID、入仓码/业务单号;
- 来源费用 ID、来源费用单号、来源版本;
- 费用项目 ID/编码/名称快照、费用方向;
- 业务发生日、发布时间、审核时间;
- 币种、原币金额、计费量及计费快照摘要;
- 审核状态、发布状态、参与结算标记、作废/调整状态;
- 来源更新时间、内容哈希;
- 可用于判断调整链的前序/后续来源引用(如已有)。
### 3.2 纳入规则
历史候选记录只有同时满足以下规则,才能进入“待激活财务来源池”:
1. 费用方向为客户应收 `RECEIVABLE`;
2. 已审核通过;
3. 已发布/已发送给客户;
4. `settlementIncluded=true`,或经产品确认的等价历史标志;
5. 来源版本有效,未作废,未被后续有效版本完全替代;
6. 客户、币种、金额、业务日期和费用项目完整且合法;
7. 能唯一映射到一个客户和一个财务主体;
8. 不在“线下已结算/不再追收”排除清单中;
9. 未在 Finance 以相同来源版本入池;
10. 通过金额、版本链和哈希校验。
其中第 4、7、8 项的最终业务映射表必须在生产执行前由产品/财务确认。
### 3.3 默认排除与隔离
| 排除/隔离原因码 | 场景 | 处理方式 |
|---|---|---|
| `OFFLINE_SETTLED` | 已在线下收款并结清 | 排除,不生成可收余额;按 M-BIZ-08 决定是否迁入只读档案 |
| `OFFLINE_PARTIAL_UNKNOWN` | 已部分结算但剩余金额不可靠 | 隔离,禁止自动拆分或生成应收 |
| `VOIDED` | 费用已作废 | 排除,保留统计和证据 |
| `SUPERSEDED_VERSION` | 已被后续有效版本替代 | 排除旧版本,保留版本链核验 |
| `NOT_APPROVED` | 未审核或审核驳回 | 排除 |
| `NOT_PUBLISHED` | 仅试算/确认但未发布 | 排除 |
| `NOT_SETTLEMENT_INCLUDED` | 不参与结算 | 排除 |
| `NON_RECEIVABLE` | 应付或方向未知 | 不进入 F1;后续 F2 单独评估 |
| `CUSTOMER_UNMAPPED` | 客户缺失、重复或无法映射 | 隔离,禁止用默认客户兜底 |
| `ENTITY_UNMAPPED` | 财务主体无法确定 | 隔离 |
| `CURRENCY_INVALID` | 币种缺失或非 ISO 三位码 | 隔离,不自动假定 CNY |
| `AMOUNT_INVALID` | 金额为空、精度不合法或业务不允许的负数 | 隔离 |
| `VERSION_CONFLICT` | 同一来源版本内容哈希不同 | 冲突队列,停止该来源链激活 |
| `DUPLICATE_BUSINESS_FACT` | 不同 ID 疑似同一业务费用 | 隔离,人工确认,不自动去重 |
| `OUT_OF_RANGE` | 不在确认的迁移日期/客户范围 | 排除本批次,可在后续批次纳入 |
每条排除或隔离记录必须保存原因码、规则版本、原始摘要和处理人;不得只记录一条汇总数量。
## 四、线下已结算防重复机制
### 4.1 结算排除台账
上线前由业务/财务提供结构化“线下结算排除台账”,禁止仅以聊天记录或口头说明作为迁移依据。至少包含:
```text
客户标识
财务主体
币种
关联业务单号/入库单号/费用单号(至少一种可唯一定位)
费用项目(按需)
线下结算金额
结算日期
结算凭证号或证明附件引用
结算状态(全额/部分/不再追收)
确认人
确认批次
备注
```
排除台账导入后也要版本化,计算文件哈希,不允许覆盖旧版本。每一次迁移批次明确引用一个排除台账版本。
### 4.2 匹配优先级
匹配按确定性从高到低执行:
1. 来源费用 ID + 来源版本;
2. 费用单号 + 费用明细 ID;
3. 入库单号 + 费用项目 + 币种 + 金额;
4. 客户 + 币种 + 业务日期范围 + 金额。
只有前两级可自动排除。第 3、4 级产生“疑似匹配”,必须人工复核;禁止仅因金额相等就自动认定已结算。
### 4.3 上线保护
- 历史迁移来源默认 `activationStatus=PENDING_REVIEW`;
- 只有排除台账匹配完成、差异报告签字后才能变为 `ACTIVE`;
- 历史来源激活不自动发送客户通知,不自动生成账单;
- 客户看到历史账单前必须经过财务制单和复核;
- 对已经线下结算却误生成应收的情况,立即停止对应客户灰度,使用冲销/作废流程修正,禁止伪造收款核销来“做平”。
## 五、迁移批次与幂等设计
### 5.1 批次状态
```text
CREATED
-> EXTRACTED
-> VALIDATED
-> IMPORTED_PENDING_REVIEW
-> RECONCILED
-> APPROVED
-> ACTIVATED
任一执行中状态 -> FAILED
未激活批次 -> ABANDONED
已激活批次 -> COMPENSATING -> COMPENSATED/PARTIALLY_COMPENSATED
```
状态迁移由服务端校验。`ACTIVATED` 后不得回到编辑态;批次重跑必须沿用原批次重试号或新建补偿批次。
### 5.2 批次元数据
每批至少记录:
- `migrationBatchNo`、环境、规则版本、排除台账版本;
- 提取范围:客户、财务主体、业务时间、来源更新时间水位;
- 提取开始/结束时间、操作者、审批者;
- 来源总数、导入数、排除数、隔离数、冲突数;
- 按客户和币种汇总的来源金额、导入金额和差异;
- 输入文件/响应摘要哈希、执行程序版本或提交 SHA;
- 各阶段开始/结束时间、错误摘要和重试次数;
- 对账报告、审批记录和激活时间。
### 5.3 单条幂等与冲突
Finance 财务来源唯一键:
```text
(source_service, source_type, source_id, source_version)
```
处理规则:
- 唯一键不存在:写入来源快照和批次关联;
- 唯一键已存在且 `sourceHash` 相同:返回第一次结果,记为重复但成功;
- 唯一键已存在且 `sourceHash` 不同:拒绝写入,记 `VERSION_CONFLICT`;
- 同一批次重复上传:不重复增加条数和金额;
- 同一来源在后续批次出现:只增加批次检测关系,不新建第二条有效来源;
- 同一业务操作的超时重试必须返回可查询的原处理结果。
### 5.4 历史与实时切换水位
为避免迁移期间新费用漏入,按以下顺序切换:
1. 记录历史提取截止水位 `T0`(来源更新时间 + 稳定排序键);
2. 导入并对账 `<= T0` 的历史数据;
3. Finance 实时消费者先以影子模式接收事件并写 Inbox,不激活来源;
4. 历史批次对账通过后,重放/处理 `> T0` 的 Inbox 事件;
5. 核对 `T0` 边界前后窗口,确认无缺号、无重复、无哈希冲突;
6. 开启实时来源激活,再开放制单入口。
时间戳不能单独作为游标;同一时间戳必须带稳定主键/事件 ID 排序,边界查询应采用重叠窗口并依赖幂等去重。
## 六、迁移执行流程
### 6.1 预演
1. 冻结产品业务口径、首批客户和日期范围;
2. 从 Firstleg 只读提取候选记录,生成不可修改的输入快照;
3. 导入线下结算排除台账;
4. 执行 `dry-run`,只生成纳入、排除、隔离和冲突报告;
5. 财务逐客户、逐币种核对金额和异常;
6. 修正映射或排除台账后新建规则/台账版本重新预演;
7. 产品、财务、技术共同签署预演报告。
### 6.2 正式迁移
1. 校验目标环境、批次号、规则版本、输入哈希和审批记录;
2. 写入迁移暂存区,不直接写对账单、应收单或收款单;
3. 完成字段、引用、版本链、重复和金额校验;
4. 将通过的数据幂等写入待复核来源池;
5. 生成逐客户/逐币种对账报告;
6. 对账通过并审批后激活来源;
7. 追平 `T0` 之后的实时事件;
8. 仅对灰度客户开放 Finance 制单与客户查看。
### 6.3 数据映射约束
- Firstleg 历史 `tenantId` 仅作为当前客户隔离兼容字段使用,Finance 产品层仍使用“客户/企业”,不得迁移出可见的租户层;
- 平台公共仓 `tenant_id=0` 不能被误映射成客户;
- 客户、费用项目、财务主体映射必须版本化,禁止运行时按名称模糊匹配;
- 金额按原币定点小数迁移,不在迁移时自动折算本位币;
- 原始金额和币种不得被清洗规则覆盖;修正必须形成明确映射或补偿记录;
- 历史缺失的财务字段使用 `UNKNOWN/PENDING` 类显式状态(若模型允许),不得伪造默认凭证号、结算日或操作人;
- 文件附件只保存 File 服务引用和当时元数据快照,不复制二进制文件。
## 七、校验与对账
### 7.1 四级校验
| 级别 | 校验内容 | 失败处理 |
|---|---|---|
| 结构校验 | 必填字段、类型、精度、币种、日期、枚举 | 单条隔离 |
| 引用校验 | 客户、财务主体、费用项目和业务单据能唯一映射 | 单条隔离 |
| 业务校验 | 状态组合、方向、结算标记、作废和版本链 | 排除或隔离 |
| 财务校验 | 按客户/币种的条数、原币金额、排除金额和激活金额平衡 | 批次不得审批 |
### 7.2 对账恒等式
每个批次必须逐客户、逐币种满足:
```text
候选条数 = 纳入条数 + 排除条数 + 隔离条数
候选原币金额 = 纳入原币金额 + 排除原币金额 + 隔离原币金额
Finance 待复核来源条数 = 本批纳入唯一来源条数
Finance 待复核来源金额 = 本批纳入原币金额
```
重复幂等命中作为独立统计项展示,但不得重复进入金额恒等式。负数、零金额和冲销记录须按记录性质单列,禁止用正负汇总抵消后隐藏异常。
### 7.3 必须生成的报告
- 批次总览报告;
- 逐客户、逐币种、逐费用项目的条数/金额报告;
- 线下已结算自动匹配、疑似匹配和未匹配报告;
- 排除/隔离原因明细;
- 重复与哈希冲突明细;
- 调整/作废版本链完整性报告;
- 历史水位与实时 Inbox 衔接报告;
- Finance 来源到 Firstleg 来源的抽样追溯报告。
技术验收要求条数与原币金额零差异。业务决定的排除不是“差异”,但必须在等式中有明细、有金额、有审批。
### 7.4 抽样规则
自动全量校验之外,人工抽样至少覆盖:
- 每个灰度客户;
- 每种币种和主要费用项目;
- 最大金额、最小非零金额、零金额/负数(如业务允许);
- 多版本调整、已作废、线下已结算和疑似重复记录;
- 跨月边界、迁移截止水位前后记录;
- 金额合计前 80% 的高金额样本,以及不少于 30 条随机样本。
## 八、回滚与补偿
### 8.1 未激活阶段
`CREATED` 至 `APPROVED` 的批次不对 F1 制单开放。失败时可以:
- 标记批次 `FAILED` 或 `ABANDONED`;
- 清理可重建的暂存数据,保留批次元数据、输入哈希、日志和差异报告;
- 修正规则或映射后,以新规则版本重跑;
- 不修改 Firstleg 原始数据。
### 8.2 已激活但未出账
发现问题时先冻结批次和相关客户的制单入口,再执行补偿:
- 将错误来源标记为不可出账/已补偿;
- 记录补偿批次、原因和原来源关联;
- 重新迁移正确版本;
- 已产生但仍为草稿的账单按受控作废流程处理,不物理删除审计记录。
### 8.3 已出账、收款或核销
不得通过删表、改余额或回写旧快照回滚。应按对象状态使用:
- 账单作废或重开;
- 应收冲销/补差;
- 核销反核销;
- 收款撤销(仅在真实到账事实允许时);
- 新补偿来源和新单据。
补偿执行前必须生成影响清单:客户、币种、账单、应收、收款、核销、余额和客户可见内容。若已经向客户发送账单,业务负责人与财务必须确认沟通方案。
### 8.4 服务发布回滚
- Finance 服务、Gateway 路由和前端可独立回滚;
- 数据库迁移采用向前兼容的增量脚本,服务回滚版本必须能读取已存在结构;
- 不以回退数据库结构作为常规回滚手段;
- 关闭 Finance 功能开关或客户白名单时,实时事件仍进入 Inbox/Outbox 重试链,不得丢弃;
- 回滚后再次上线必须从已记录的消费水位恢复,并先完成积压对账。
## 九、灰度上线方案
### 9.1 阶段划分
| 阶段 | 开放范围 | 退出条件 |
|---|---|---|
| G0 测试环境 | 测试数据与脱敏历史样本 | F0/F1 自动化及业务回归通过 |
| G1 生产影子 | 全量或限定来源只入 Inbox/待复核池;用户不可见、不可制单 | 连续观察期无漏单、重复或冲突;观察时长待上线评审确定 |
| G2 内部灰度 | 白名单客户;仅平台财务可见和制单 | 财务逐日对账通过,无 P0/P1 缺陷 |
| G3 客户灰度 | 白名单客户可查看、确认/争议 | 客户侧金额和状态一致,通知无重复 |
| G4 全量 | 全部已确认客户 | 灰度总结批准,监控、值守和补偿预案就绪 |
### 9.2 灰度维度
灰度至少同时受以下开关控制:
- 环境;
- 财务主体;
- 客户白名单;
- 功能(来源接入、制单、发送、客户确认、收款、核销、反核销);
- 历史批次号与实时事件起始水位。
开关由服务端执行,前端隐藏按钮不能代替权限或灰度校验。
## 十、监控与告警
### 10.1 必须监控的指标
| 类别 | 指标 |
|---|---|
| 事件接入 | Firstleg Outbox 待发送数/最老年龄、Finance Inbox 积压数/最老年龄、消费成功率、重试数 |
| 幂等与冲突 | 重复命中数、哈希冲突数、乱序等待数、死信数 |
| 迁移 | 各批次状态、处理速率、排除/隔离比例、长时间无进展批次 |
| 对账 | 逐客户/币种条数差、金额差、历史与实时水位差 |
| F1 台账 | 来源入池数、账单生成数、应收余额变化、收款未核销余额、超额/跨币种拒绝数 |
| 稳定性 | API 错误率、延迟、数据库连接池、锁等待、乐观锁冲突、定时任务失败 |
| 安全审计 | 越权拒绝、跨客户访问、反核销/作废/关账等高风险操作 |
告警阈值在上线评审时结合基线确定;以下事件不等待阈值,首次发生即告警:哈希冲突、跨客户数据命中、超额核销成功、同一来源生成两条有效来源、金额对账非零差异、审计写入失败。
### 10.2 可观测性要求
- 跨服务请求保留 `requestId`、`traceId`、`eventId`、`migrationBatchNo`;
- 日志不得输出银行账号全量、凭证附件内容或客户敏感信息;
- 每个批次和事件可查询最后处理状态、失败原因、重试次数和下一步动作;
- Dashboard 按环境、财务主体、客户和币种筛选;
- 审计、迁移和业务日志的保留期限待合规要求确认,但不得短于上线问题追溯周期。
## 十一、F0 上线验收用例
### 11.1 正常用例
| 编号 | 场景 | 预期结果 |
|---|---|---|
| F0-N01 | Finance 独立启动、注册并连接财务库 | 健康检查通过,只访问 `baiyue_finance` |
| F0-N02 | 合法历史来源首次导入 | 生成一条待复核来源,快照、哈希、批次和审计完整 |
| F0-N03 | 相同批次重复提交 | 返回原处理结果,不重复条数/金额 |
| F0-N04 | 相同来源由后续批次再次导入 | 幂等命中,不生成第二条有效来源 |
| F0-N05 | Firstleg 发布实时费用事件 | Inbox 记录并生成来源;可追溯至事件和上游快照 |
| F0-N06 | 历史水位后事件追平 | 边界窗口无漏单,重复事件被幂等吸收 |
| F0-N07 | 按客户/币种生成迁移对账 | 条数、金额和原因分类满足恒等式 |
| F0-N08 | 有权限人员查看迁移批次/冲突 | 数据范围正确,查询和处理均有审计 |
### 11.2 异常用例
| 编号 | 场景 | 预期结果 |
|---|---|---|
| F0-E01 | 同一幂等键内容哈希不同 | 拒绝并告警,原来源不被覆盖 |
| F0-E02 | 事件重复、乱序、超时重试 | 不重复入池;乱序进入等待/核验,不错误激活 |
| F0-E03 | 客户或财务主体无法映射 | 单条隔离,不使用默认归属 |
| F0-E04 | 币种缺失、金额精度非法 | 单条隔离并显示明确原因 |
| F0-E05 | 已作废/未审核/未发布来源 | 排除,不进入可出账来源池 |
| F0-E06 | 线下已结算精确匹配 | 排除并关联排除台账证据 |
| F0-E07 | 仅金额相同的疑似线下结算 | 不自动排除,进入人工复核 |
| F0-E08 | 跨客户查询或激活 | 服务端拒绝并产生安全审计 |
| F0-E09 | 批次中途失败后重跑 | 已成功记录不重复,失败记录可续跑,汇总一致 |
| F0-E10 | 审计落库失败 | 财务写操作整体失败,不出现无审计的成功记录 |
| F0-E11 | Finance 暂时不可用 | Firstleg 业务费用事实不回滚;Outbox 保留并重试 |
| F0-E12 | 数据库/服务发布回滚 | 来源与 Inbox 不丢失,恢复后从原水位继续 |
## 十二、F1 上线验收用例
### 12.1 正常主链路
| 编号 | 场景 | 预期结果 |
|---|---|---|
| F1-N01 | 财务从激活来源池按客户+币种+账期生成账单 | 来源只进入一张有效账单,金额合计正确 |
| F1-N02 | 账单内部复核、发送、客户确认 | 状态按确认模型流转,平台与客户显示一致 |
| F1-N03 | 客户提出明细争议 | 记录原因和争议明细,原快照不被覆盖,可重开再发送 |
| F1-N04 | 确认账单形成应收 | 应收金额、余额、到期日和来源关系正确 |
| F1-N05 | 登记一笔收款核销多张应收 | 每条分配关系可追溯,收款和应收余额正确 |
| F1-N06 | 一张应收被多笔收款部分核销 | 状态依次为 OPEN/PARTIALLY_SETTLED/SETTLED,累计不超额 |
| F1-N07 | 反核销 | 原核销变为 REVERSED,应收及收款可用余额恢复,审计完整 |
| F1-N08 | 已出账费用发生调整 | 原账单不覆盖,通过补差/冲销来源进入后续流程 |
### 12.2 异常和并发
| 编号 | 场景 | 预期结果 |
|---|---|---|
| F1-E01 | 同一来源被并发生成账单 | 只有一个请求成功,另一请求返回明确冲突 |
| F1-E02 | 跨币种生成账单或核销 | 服务端拒绝 |
| F1-E03 | 收款核销金额超过应收余额 | 服务端拒绝,所有余额不变 |
| F1-E04 | 多请求并发核销同一余额 | 乐观锁/原子约束保证不超额,不产生部分脏写 |
| F1-E05 | 重复提交收款或核销 | 返回首次结果,不重复记账 |
| F1-E06 | 无权限执行收款、核销、反核销或作废 | 服务端拒绝并审计 |
| F1-E07 | 客户访问其他客户账单或内部字段 | 服务端拒绝;供应商成本、内部备注和凭证不可见 |
| F1-E08 | 已关闭期间直接改账 | 拒绝,提示使用后续期间调整/冲销 |
| F1-E09 | 已确认账单原地删除或改金额 | 拒绝,原记录保持不变 |
| F1-E10 | 附件服务失败 | 财务单据不伪造附件成功;按业务动作原子性返回失败或保留可重试状态 |
| F1-E11 | 客户通知失败 | 财务单据状态不回滚,通知独立重试且不重复发送业务动作 |
| F1-E12 | 线下已结算历史来源误入池 | 停止客户灰度,禁止继续制单,按补偿流程处理并出具影响报告 |
## 十三、上线门禁
### 13.1 开发开工门禁
- Finance 服务边界和 Firstleg 事件/内部查询契约书面冻结;
- F0/F1 对象、状态机、金额精度、权限码、错误码和审计事件冻结;
- M-BIZ 未确认项保持显式配置或阻断,不硬编码业务假设;
- 迁移暂存、来源池、账单、应收、收款、核销是独立对象;
- 数据库脚本、回滚兼容策略和部署映射纳入评审。
### 13.2 测试环境部署门禁
- 仅部署受影响单元,执行目标模块声明的 Maven 测试;
- Finance 纳入 Maven、Nacos、Gateway、OpenAPI、Docker Compose、服务映射和 Jenkins 增量部署;
- 数据库初始化/迁移必须获得当前对话或发布审批的明确授权,不能借普通服务部署自动执行;
- 不得跳过测试,不得在范围不明时执行后端全量部署;
- 测试数据不使用真实客户敏感信息;
- F0/F1 正常、异常、并发、越权用例全部有证据。
### 13.3 生产迁移门禁
- M-BIZ-01 至 M-BIZ-10 已确认或有书面暂缓方案;
- 首批客户、财务主体、时间范围、规则版本和线下排除台账冻结;
- 至少完成一次等规模 dry-run,条数与金额零差异;
- 所有隔离项有责任人和处理结论,高风险冲突为零;
- 回滚/补偿演练通过,明确停止开关和联系人;
- 监控、告警、Dashboard、日志和审计查询可用;
- 发布版本、完整 commit SHA、数据库脚本、配置和镜像可追溯;
- 生产数据库变更和发布已获得对应授权;
- 财务、产品、技术共同签署上线检查单。
### 13.4 客户开放门禁
- 影子接入和内部灰度观察完成;
- 对应客户历史来源及实时来源逐币种对账通过;
- 该客户线下已结算排除清单已确认;
- 客户侧权限、脱敏、空态、错误态和争议入口验收通过;
- 不发送历史自动账单;首张账单由财务人工复核后发送;
- 客服/运营已获得争议、误账和通知失败处理说明。
## 十四、上线后检查与退出条件
### 14.1 上线后立即检查
1. 核对服务注册、健康状态、Gateway 路由和数据库连接;
2. 核对历史批次状态、实时 Inbox 水位和 Firstleg Outbox 积压;
3. 抽取首批来源追溯到 Firstleg 原始快照;
4. 逐灰度客户、逐币种核对来源条数和金额;
5. 完成一条真实但受控的“来源 → 账单 → 应收 → 收款 → 核销 → 反核销”回归;
6. 检查 Admin/Client 金额、状态、权限和客户隔离一致;
7. 记录构建号、commit、配置版本、批次号、测试证据、`requestId/traceId`。
### 14.2 扩大灰度条件
- 当前灰度范围内无金额差异、无重复有效来源、无跨客户问题;
- 无 P0/P1 缺陷,未发生未解释的哈希冲突;
- 事件积压和失败重试在既定服务水平内;
- 财务日结核对通过;
- 已发现问题均有补偿结果并复核。
### 14.3 立即停止扩大灰度的条件
- 任何客户金额串用或越权可见;
- 来源、账单、应收、收款或核销出现重复记账;
- 对账出现无法解释的非零差异;
- 超额核销、跨币种核销或关闭期间改账成功;
- 审计缺失、迁移输入不可追溯或补偿路径不可用;
- Firstleg 与 Finance 水位持续扩大且无法追平。
停止扩大灰度不等于删除数据。应先关闭相关功能/客户白名单、保留事件接收和证据,再按影响范围决定补偿或服务版本回滚。
## 十五、交付证据清单
每次预演、迁移或上线至少归档:
- 已确认的业务口径和灰度白名单;
- 迁移批次元数据、输入哈希、规则版本、排除台账版本;
- 条数/金额对账报告及排除、隔离、冲突明细;
- 自动化测试、并发测试、权限测试和业务回归结果;
- 数据库迁移审批与执行结果;
- Jenkins Job、构建号、分支、commit SHA、镜像版本和下游结果;
- 健康检查、真实 API/浏览器验收证据;
- 告警和 Dashboard 截图/导出;
- 回滚/补偿演练记录;
- 产品、财务、技术的上线与灰度扩大结论。
## 十六、当前阻断项
截至 2026-08-10,本方案技术设计无架构阻断,可以进入 F0/F1 详细设计与开发准备;以下事项阻断生产历史迁移激活或客户开放:
1. M-BIZ-01 至 M-BIZ-10 尚未由产品/财务确认;
2. 线下已结算排除台账尚未提供,无法证明历史费用不会重复追收;
3. Firstleg 历史费用版本链和等价状态映射需要在迁移开发前以真实数据抽样确认;
4. 首批灰度客户、财务主体和时间范围尚未确定;
5. 财务服务尚未实施,Jenkins/Nacos/Gateway/数据库部署单元尚未建立;
6. 生产数据库迁移、生产发布和任何真实数据写入均需独立授权与上线审批。
这些阻断项不应通过默认 CNY、默认客户、默认未结算或直接导入应收来绕过。
@@ -0,0 +1,96 @@
# 财务开工门禁与 F0/F1 任务清单
> 版本:V1.0
> 日期:2026-08-10
> 当前阶段:开发前详细设计
> 结论:技术方案可继续;业务开发须按门禁逐项放行
## 一、总控结论
Finance 的服务拆分、独立数据库、服务内部模块边界和 Firstleg 协作方式已经技术确认,可以继续做详细设计和契约测试准备。
当前不能直接进入全量业务开发的原因,是产品确认表仍有业务决策待填写,尤其是财务主体/账套、期间、来源入池、账单确认、收款核销、历史迁移和 F1 收入口径。未确认部分只能使用可逆的草案模型,不能生成生产数据或把建议默认值当成正式口径。
## 二、文档交付状态
| 材料 | 状态 | 作用 |
|---|---|---|
| 财务服务边界设计 | 已完成 | 确认 `finance-service`、`baiyue_finance`、9203 预留和部署改造清单 |
| 财务管理产品确认表/XLSX | 待产品填写 | 仅保留需要业务确认的 26 项;技术结论单独列示 |
| 财务领域模型与数据字典 | 草案完成 | F0/F1 模型、表和约束;未确认口径显式标记 |
| Firstleg-Finance 跨服务契约 | 草案完成 | 事件、快照查询、幂等、重试、权限和契约测试 |
| 财务历史迁移与上线验收方案 | 草案完成 | 历史费用迁移、防重复、灰度和 F0/F1 验收 |
## 三、分阶段开工门禁
### G0:详细设计门禁
可立即进行。条件已满足:
- 独立服务/独立库结论已记录;
- Finance 与 Firstleg 的数据归属和禁止事项已记录;
- 领域模型、跨服务契约、迁移验收方案已有草案;
- 产品待确认项与技术结论已分离。
G0 允许:完善 API/OpenAPI、数据库迁移草案、契约测试样例、页面线框和任务拆分;不允许写入生产财务数据。
### G1:F0 财务底座开发门禁
进入 F0 开发前必须满足:
1. 产品确认表中的 F0 主体/账套、往来单位、币种精度、汇率预留、期间、编号、来源入池、权限审计和历史迁移口径已填写,或明确书面暂缓方案。
2. `finance-service` 的模块目录、端口、数据库名、Nacos 配置和 Gateway 路由完成技术评审。
3. 事件信封、来源版本、Inbox/Outbox、幂等冲突和内部快照查询契约冻结。
4. Finance 权限码、客户范围校验、平台员工操作范围和审计事件清单冻结。
### G2:F1 客户应收开发门禁
进入 F1 前必须满足:
- 账单聚合维度和账期计算规则冻结;
- 账单确认、争议、重开和关闭状态冻结;
- 应收生成节点、到期日、收款登记和多对多核销规则冻结;
- 预收、多收、手续费、退款、反核销的处理方式冻结;
- 至少准备一组真实或脱敏费用来源样本,能够完成费用→账单→应收→收款→核销回归。
### G3/G4:F2-F4 门禁
供应商应付、客户账户、发票、费用分析和利润必须分别满足产品确认表中的阶段门禁。没有供应商成本凭证、资金性质、税务主体、汇率和分摊口径时,只能保留预留模型和页面草案。
## 四、F0/F1 任务清单
| 编号 | 任务 | 产出 | 依赖 | 放行标准 |
|---|---|---|---|---|
| F0-T01 | 创建 Finance 服务骨架 | `finance/`、Maven 模块、启动类、健康检查 | G1-2 | 独立构建/启动,不改 Firstleg 数据库 |
| F0-T02 | 建立 Finance 数据库与迁移 | `baiyue_finance` 表结构、迁移脚本、版本记录 | F0-T01 | 无跨库外键/Join;迁移可重复执行 |
| F0-T03 | 财务基础主数据 | 主体、账套、币种、期间、编号、往来单位快照 | 产品 F0 | 关闭期间、币种和编号规则服务端生效 |
| F0-T04 | 来源池与事件消费 | Inbox、来源池、快照、事件结果查询 | 契约冻结 | 重复/乱序/重试/冲突均有可验收结果 |
| F0-T05 | 财务权限与审计 | 权限码、客户范围、操作审计和高风险操作保护 | G1-3 | 越权和反核销等操作被服务端拒绝并留痕 |
| F1-T01 | 应收费用池 | 跨入库单筛选、排除原因、来源版本锁定 | F0-T04 | 一条有效来源只能进入一张有效账单 |
| F1-T02 | 客户对账单 | 生成、复核、发送、确认、争议、重开、关闭 | 产品 F1 | 状态迁移和版本并发校验完整 |
| F1-T03 | 应收单 | 应收金额、已核销额、余额、到期/逾期 | F1-T02 | 账单与应收金额可追溯且不可覆盖历史 |
| F1-T04 | 收款与核销 | 收款登记、多对多核销、反核销 | 产品 F1 | 禁止跨币种、超额和重复核销 |
| F1-T05 | 客户侧账单 | 客户查看、确认/争议、余额和收款记录 | F1-T02/F1-T04 | 客户不可见内部备注、供应商成本和内部附件 |
| F1-T06 | F1 回归验收 | 正常、重复、并发、越权、冲销、迁移案例 | 以上任务 | 通过迁移与上线验收方案中的 F0/F1 用例 |
## 五、当前可立即执行与必须等待
### 现在可以继续
- 把草案契约转换为 OpenAPI/事件契约测试样例;
- 完善 F0/F1 数据字典、状态转移表和权限矩阵;
- 设计 Finance Admin/Client 页面和空态、错误态、争议态;
- 准备脱敏历史费用样本与迁移校验脚本设计;
- 按 F0-T01~F0-T05 拆解开发任务,但暂不合入业务代码。
### 必须等待产品确认
- 生产账单、应收、收款、核销的正式业务规则;
- 客户账户、发票、供应商应付和利润的正式口径;
- 历史费用是否迁移、线下已结算排除台账和灰度客户范围。
## 六、总控放行结论
当前状态为:**G0 已通过,G1/G2 待产品确认后放行。**
这意味着可以继续做开发前详细设计和可逆的契约/测试准备;产品回传 XLSX 并冻结 F0/F1 后,即可进入 `finance-service` 骨架、独立数据库和客户应收闭环开发。
+205
View File
@@ -0,0 +1,205 @@
# 财务服务边界设计
> 决策日期:2026-08-10
> 决策状态:技术已确认
> 决策结论:Finance 采用独立服务和独立数据库
> 适用范围:财务 F0-F4 全量建设
## 一、正式结论
财务域新增独立 `finance-service`,拥有独立财务数据库。不得把应收、应付、对账单、收付款、核销、客户账户、发票、费用分析或利润继续放入 `firstleg-service`。
`firstleg-service` 保持现有业务边界,只负责:
- 入库单、测量、称重和审核;
- FBA/头程计费规则匹配与费用计算;
- 业务费用快照、费用版本、发布、调整和作废;
- 向 Finance 发布不可变费用来源事件;
- 提供 Finance 核验完整费用来源的内部查询接口。
`finance-service` 负责:
- 财务主体、往来单位引用、币种、汇率、财务期间和单据编号;
- 财务费用来源池;
- 客户对账单、应收单、收款单、核销和反核销;
- 供应商对账单、应付单、付款单、付款核销和反核销;
- 客户账户、充值、冻结、解冻、扣款、退款和账户流水;
- 发票申请、开票登记、红冲、作废和附件关联;
- 成本台账、费用分析、利润分析和期间快照;
- 财务权限、幂等、Outbox、版本和审计记录。
## 二、为什么拆服务
拆库本身不强制拆服务,但当前财务范围已经形成独立业务域,具备独立服务的必要条件:
1. 财务拥有独立状态机。计费状态不能代替账单、应收、收款和核销状态。
2. 财务拥有独立事务边界。入库完成不能与收款、核销或开票放在同一个事务中。
3. 财务需要更严格的权限和审计。反核销、付款、关账和红冲不应复用入库费用编辑权限。
4. 财务数据生命周期更长。已出账、已核销和已开票记录不能随着业务订单调整被覆盖。
5. 财务同时消费 Firstleg、Order、Warehouse、IAM、File 等业务信息,不属于 Firstleg 的单一履约职责。
6. 后续应付、客户账户、发票和利润会持续扩展,放入 Firstleg 会形成不可控耦合。
## 三、目标拓扑
```text
Admin / Client
|
v
Gateway
|
+-----------------------+
| |
v v
firstleg-service finance-service
firstleg 数据库 finance 数据库
| |
| 费用事件 / 内部查询 | 财务单据与台账
+---------------------->|
|
+--> file-service(凭证/发票附件)
+--> iam-service(身份、客户与权限核验)
+--> warehouse-service(供应商/往来单位核验)
```
禁止:
- Finance 跨库 Join Firstleg、Warehouse、IAM 或 Order 数据库;
- Finance 直接引用其他服务 Entity;
- Firstleg 直接写 Finance 数据库;
- 前端传入客户 ID 后绕过服务端客户范围校验;
- 通过修改 Firstleg 费用状态代替财务调整、冲销或反核销。
### 3.1 实现标识预留
经现有 Maven 聚合工程、服务端口表、Nacos 配置和部署清单核对,Finance 实施时统一使用以下标识:
| 项目 | 规划值 | 当前状态 |
|---|---|---|
| 后端模块目录 | `baiyue-firstleg/finance/` | 规划,尚未创建 |
| Maven artifactId | `finance-service` | 规划,尚未创建 |
| Nacos 注册服务名 | `finance-service` | 规划,尚未注册 |
| 服务端口 | `9203` | 规划预留;现有 11 个服务未占用 |
| 独立数据库 | `baiyue_finance` | 规划,尚未创建 |
`9203` 仅作为当前架构下的默认预留端口;若部署环境端口占用情况变化,可由配置调整,但仓库内默认值、部署映射和文档必须保持一致。
## 四、数据归属
| 数据 | 归属服务 | 说明 |
|---|---|---|
| 入库单、测量、重量、体积 | Firstleg | 业务履约事实 |
| 计费规则、价格版本、计费快照 | Firstleg | 业务费用计算依据 |
| 已发布费用来源版本 | Firstleg | Finance 的不可变上游来源 |
| 财务来源池 | Finance | 保存来源引用、版本、哈希和财务入池状态 |
| 客户/供应商对账单 | Finance | 不回写业务费用表 |
| 应收、应付、收款、付款 | Finance | 财务余额和状态唯一来源 |
| 核销、反核销 | Finance | 不允许物理删除 |
| 客户账户和流水 | Finance | 与应收台账分开,抵扣必须产生核销记录 |
| 发票台账 | Finance | 文件内容仍由 File 保存 |
| 成本、分摊、利润和期间快照 | Finance | 只能基于财务台账计算 |
| 客户身份和权限资源 | IAM | Finance 只引用和核验 |
| 公共往来单位/供应商资料 | Warehouse | Finance 保存引用和历史名称快照 |
## 五、Firstleg 到 Finance 的契约
### 5.1 事件
Firstleg 通过本地 Outbox 发布:
- `FIRSTLEG_FEE_PUBLISHED`
- `FIRSTLEG_FEE_ADJUSTED`
- `FIRSTLEG_FEE_VOIDED`
事件至少包含:
```text
eventId
eventType
occurredAt
customerId
inboundId
sourceFeeId
sourceFeeVersion
sourceFeeNo
feeDetailId
direction
businessDate
currency
amount
sourceHash
```
Finance 使用 `eventId + sourceType + sourceId + sourceVersion` 幂等消费。重复相同事件返回第一次处理结果;同一幂等键对应不同内容时拒绝处理并记录冲突。
### 5.2 内部查询
Finance 接收事件后,通过服务令牌调用 Firstleg 内部接口核验完整来源快照。内部接口返回 DTO,不返回或共享 Entity。
Finance 保存来源快照后,不依赖 Firstleg 当前表数据实时计算历史账单。已出账来源发生调整时,Finance 生成补差或冲销来源,不覆盖原账单明细。
## 六、Finance 服务内部模块
建议 `finance-service` 内部先按业务域分包,不在首期继续拆成多个微服务:
```text
finance-service/
foundation/ 财务主体、币种、汇率、期间、编号
source/ 费用来源接入与校验
receivable/ 对账单、应收、收款、核销
payable/ 供应商对账、应付、付款、核销
account/ 客户账户和流水
invoice/ 发票生命周期
costing/ 成本和分摊
reporting/ 费用分析和利润
audit/ 财务操作日志
```
当前只拆一个 Finance 服务,不把应收、应付、钱包、发票和报表各自拆成独立微服务。只有将来出现独立团队、独立扩缩容或明显不同的可用性需求时,才重新评估二次拆分。
## 七、开发顺序
```text
F0:finance-service 骨架、独立数据库、主体/币种/期间/编号、来源接入、幂等和审计
-> F1:客户对账、应收、收款和核销
-> F2:供应商对账、应付、付款和核销
-> F3:客户账户和发票
-> F4:成本、费用分析和利润
```
F1 与 F2 可以在 F0 契约冻结后并行开发;F3/F4 可以并行做页面和契约设计,但生产数据链路必须依赖前序财务台账。
## 八、技术验收标准
- `finance-service` 可以独立构建、部署和回滚;
- Finance 数据库不存在对其他服务数据库的跨库外键或 Join;
- Firstleg 发布费用后,Finance 能幂等生成一条财务来源;
- 重复事件、乱序事件和失败重试不会重复入账;
- 已出账费用调整不会覆盖历史账单;
- 所有财务写操作有幂等键、乐观锁、权限和审计;
- 长整型 ID 以前端字符串传输,金额使用定点小数和 ISO 三位币种;
- 跨客户、跨主体、跨币种和关闭期间操作由服务端拒绝;
- Admin/Client 使用正式 Finance API,不依赖 `/prototype/finance/*` 页面或模拟数据。
## 九、进入实现时的仓库改造清单
以下是 Finance 开发阶段必须完成的技术工作,不属于产品待确认项。本轮只形成规划,不创建服务、不建库、不修改部署环境。
| 范围 | 实施时需要新增或修改 | 验证重点 |
|---|---|---|
| Maven 聚合 | 根 `pom.xml` 增加 `finance` module;新增 `finance/pom.xml` | `mvn -pl finance -am test` 可独立构建 |
| 服务配置 | 新增 `config/nacos/local/finance-service.yml`、`config/nacos/cn/finance-service.yml` | 服务名、端口、数据源、内部令牌与环境变量约定一致 |
| 数据库 | 新增 Finance 自有迁移目录,并同步统一增量 SQL 入口 | 只创建/访问 `baiyue_finance`,无跨库外键或 Join |
| 部署映射 | 更新 `deploy/service-map.yml`、`deploy/docker-compose.yml` 和部署脚本 | 生成独立 JAR、镜像、容器,可单独部署/回滚 |
| Gateway | 增加 Finance 平台端/客户端路由和 Swagger/OpenAPI 聚合 | 前端仍只通过 `/api` 访问;平台端与客户端访问域隔离 |
| CI/CD | 更新 Jenkins 增量构建、受影响服务识别和部署映射 | 修改 Finance 只构建相关单元;公共模块变更包含 Finance |
| 服务契约 | Firstleg 增加费用 Outbox 事件及受保护内部快照查询;Finance 增加 Inbox/消费结果 | 重复、乱序、重试、冲突事件均可验证 |
| 权限与审计 | IAM 增加 Finance 权限资源;Finance 接入公共鉴权与审计能力 | 服务端校验客户范围、高风险权限和操作留痕 |
| 架构文档 | 更新服务总览、端口/数据库表和调用拓扑 | 可运行服务口径由 11 个变为 12 个 |
实施时必须先冻结 Finance 与 Firstleg 的事件/查询契约,再并行建设 Finance 内部 F1/F2 模块;不得先由前端或数据库表反推跨服务契约。
## 十、复核结论
当前仓库采用 Maven 多模块单仓、服务独立 JAR/镜像/容器、服务独立数据库、DTO/事件协作的架构。Finance 独立服务与这一模式一致,不需要因为代码处于同一 Git 仓库而并回 `firstleg-service`。
因此本技术决策保持不变:**新增一个 `finance-service` 和一个 `baiyue_finance` 数据库,服务内部按财务子域分包,当前不继续拆成多个财务微服务。**
+408
View File
@@ -0,0 +1,408 @@
# 财务来源接入现状
> 盘点日期:2026-08-10
> 盘点范围:`baiyue-firstleg/`、`apps/admin-web/`
> 盘点方式:只读核对 Controller、DTO、Service、Entity、SQL、OpenAPI、前端 API/类型/页面和测试
> 用途:为 Firstleg → Finance F0 的来源接入设计提供代码现状基线,不代表 Finance 已实现
## 一、结论摘要
当前仓库已经具备可作为 Finance 上游的入库费用事实,但尚未形成可直接接入 Finance 的完整生产链路。
已经具备:
- 入库费用从试算、确认、发布到发布后调价的版本化快照;
- 自动发布的对客应收入库运费明细,以及人工应收/应付费用明细的录入与审核基础;
- 平台公共费用项目主数据;
- 费用写操作幂等、业务审计和 Firstleg 本地 Outbox 落库;
- Admin 的费用项目、入库费用快照、已发布费用调整和统一订单费用明细查询;
- Order 的可运行 Outbox 调度、重试模式,以及 Event Bridge 的事件信封和 Inbox 幂等模式,可作为实现参考。
尚未具备:
- `finance-service`、Finance 数据库、Finance Inbox 和财务来源池;
- Firstleg 面向 Finance 的冻结事件契约和内部来源快照查询接口;
- Firstleg Outbox 的实际扫描、投递、重试、失败/死信处理;
- 费用作废事件,以及已出账后补差/冲销语义;
- 跨入库单的待结算费用池、账单占用/冻结和财务状态回执;
- Finance 独立权限、财务单据、收付款及核销能力。
因此,F0 可以复用当前费用快照、费用明细和本地事务 Outbox 的写入位置,但不能直接把 `fl_inbound_fee_detail` 当作 Finance 台账,也不能把现有 Event Bridge 当作 Firstleg → Finance 的现成消息通道。
## 二、现有费用来源模型
### 2.1 入库计费快照
`fl_inbound_fee` 保存版本化费用头,`fl_inbound_fee_item` 保存费用组成。核心能力如下:
| 能力 | 当前实现 | 可复用价值 | 现有限制 |
|---|---|---|---|
| 试算 | `POST /api/platform/v1/inbounds/{inboundId}/fees/calculate` | 保存计费输入、规则/价格版本、计费量和金额快照 | 只面向单个入库单,不是财务来源查询接口 |
| 确认 | `POST .../fees/confirm` | `CALCULATED -> CONFIRMED`,写操作带 `Idempotency-Key` | 确认是业务计费状态,不等于财务审核 |
| 发布 | `POST .../fees/send` | `CONFIRMED -> SENT`,生成对客费用明细并写 Outbox | 方法和事件仍使用 `send/SENT` 旧命名;事件负载过少 |
| 查询最新快照 | `GET .../fees/latest` | Admin 可读取最新业务快照 | 只返回最新版本,不适合 Finance 核验指定历史版本 |
| 发布后调价 | `PUT .../fees/published` | 旧 `SENT` 版本转 `SUPERSEDED`,生成新 `SENT` 版本,保留调整原因并写 Outbox | 没有已出账判断;Finance 上线后不能继续无条件覆盖来源有效版本 |
`FeeSnapshotResponse` 已包含可复用的主要快照字段:
- 标识:`feeId`、`feeNo`、`feeVersion`、`inboundId`、`inboundCode`;
- 状态:`feeStatus`、`nextProcessStatus`;
- 客户与业务:`customerId`、`customerName`、`businessType`;
- 规则与价格:规则/价格 ID、编码、版本、价格表版本及有效期;
- 路由与目的地:起运仓、渠道、承运商、目的地类型/ID/名称/编码;
- 计费依据:实际重量、实际体积、换算量、计费量、单位、换算/舍入/税模式;
- 金额:单价、小计、附加费、总额、币种;
- 时间:试算、确认、发布时间。
费用快照的数据库约束已经提供 `(tenant_id, inbound_id, fee_version)` 唯一键,金额使用定点小数,币种为三位码。这些可继续作为 Firstleg 来源版本的事实基础。
### 2.2 已发布费用明细
`fl_inbound_fee_detail` 是当前最接近财务来源的业务表。自动发布链路会把 `SENT` 费用快照转换为一条入库应收运费明细:
| 字段组 | 已有字段/状态 |
|---|---|
| 来源定位 | `sourceType=FBA_PRICING`、`sourceFeeId`、`sourceFeeVersion`、`sourceFeeNo` |
| 业务定位 | `tenantId`、`inboundId`、`inboundCode`、`businessDate` |
| 客户快照 | `customerId`、`customerName`、`invoiceTitle` |
| 费用快照 | 费用分类、贸易范围、费用名称、方向、币种、单价、金额 |
| 计费依据 | 箱数、重量、体积、体积重、计费量、计费单位 |
| 审核 | `PENDING / APPROVED / REJECTED`,审核人、时间、原因 |
| 发布 | `DRAFT / PUBLISHED / SUPERSEDED`,发布人、时间 |
| 结算候选标识 | `settlementIncluded` |
自动发布的 FBA 头程运费固定为:
- `direction=RECEIVABLE`;
- `reviewStatus=APPROVED`;
- `publicationStatus=PUBLISHED`;
- `settlementIncluded=true`;
- 来源费用项目编码 `FBA_HEAD_FREIGHT`,不存在时由服务端以平台公共费用项目自动创建。
发布新费用版本时,旧的已发布运费明细会被标记为 `SUPERSEDED`,而不是物理删除。`(tenant_id, source_type, source_fee_id)` 的来源唯一性和并发重复键兜底,已经能支撑来源版本幂等的第一层防护。
人工费用明细支持 `RECEIVABLE | PAYABLE`,但创建后为 `PENDING + DRAFT + settlementIncluded=false`。现有审核通过只改变审核状态,没有发布/纳入结算动作,因此人工明细尚不能按统一规则自动进入 Finance。
### 2.3 费用项目
平台费用项目接口已完整实现:
| 操作 | 接口 | 权限 |
|---|---|---|
| 分页查询 | `GET /api/platform/v1/fee-items` | `firstleg:fee-item:view` |
| 详情 | `GET /api/platform/v1/fee-items/{id}` | `firstleg:fee-item:view` |
| 新增 | `POST /api/platform/v1/fee-items` | `firstleg:fee-item:create` |
| 编辑 | `PUT /api/platform/v1/fee-items/{id}` | `firstleg:fee-item:edit` |
| 启停 | `PATCH /api/platform/v1/fee-items/{id}/status` | `firstleg:fee-item:edit` |
| 删除 | `DELETE /api/platform/v1/fee-items/{id}` | `firstleg:fee-item:delete` |
现有 DTO/实体字段包括 `feeCode`、分类、`DOMESTIC | INTERNATIONAL`、提货/仓库应收应付适用性、是否分摊费、说明和 `ACTIVE | DISABLED`。
F0 可引用稳定 `feeCode` 并保存名称/分类快照,但不应直接把 Firstleg 费用项目表作为 Finance 自有会计科目表。Finance 费用科目与业务费用项目需要显式映射和独立版本管理。
## 三、现有接口、DTO、权限与状态
### 3.1 可复用平台接口
| 范围 | 接口 | DTO/返回 | 权限 | F0 复用判断 |
|---|---|---|---|---|
| 费用快照 | `GET .../inbounds/{inboundId}/fees/latest` | `FeeSnapshotResponse` | `firstleg:fba-pricing:view` | 可供 Admin 查看,不满足 Finance 按来源版本核验 |
| 费用调整 | `PUT .../inbounds/{inboundId}/fees/published` | `AdjustFeeRequest` → `FeeSnapshotResponse` | `firstleg:fba-pricing:adjust` | 保留业务入口,但需增加已出账约束或改走补差/冲销 |
| 费用明细查询 | `GET .../inbounds/{inboundId}/fee-details?accountId=` | `List<InboundFeeDetailEntity>` | `firstleg:inbound:fee-detail:view` | 仅单入库单平台查询,不作为跨服务契约 |
| 人工费用新增/编辑 | `POST/PUT .../fee-details` | `SaveRequest` → `InboundFeeDetailEntity` | `firstleg:inbound:fee-detail:edit` | 可继续维护业务费用,不能代替财务制单 |
| 人工费用删除 | `DELETE .../fee-details/{detailId}` | 空 | `firstleg:inbound:fee-detail:delete` | 仅未审核明细可删;Finance 入池后必须禁止物理语义 |
| 人工费用审核 | `POST .../{detailId}/approve|reject` | `RejectRequest` | `firstleg:inbound:fee-detail:review` | 可作为业务来源前置条件 |
| 客户已发布费用 | `GET /api/merchant/v1/first-leg/orders/{orderId}/inbound-billing` | `MerchantInboundBillingResponse` | `merchant:order:view` | 客户只读展示,不是 Finance 内部来源接口 |
费用试算、确认、发布还分别使用:
- `firstleg:fba-pricing:calculate`;
- `firstleg:fba-pricing:confirm`;
- `firstleg:inbound:send-fee`。
上述权限都是 Firstleg 业务权限。F0 需要新增 Finance 独立权限资源,不能复用它们授权财务入池、冲突处理、来源重放或来源作废。
### 3.2 建议冻结的来源状态映射
| Firstleg 状态组合 | 当前含义 | F0 建议 |
|---|---|---|
| `feeStatus=CALCULATED` | 已试算 | 不入池 |
| `feeStatus=CONFIRMED` | 业务已确认 | 不入池 |
| `feeStatus=SENT` + 明细 `APPROVED/PUBLISHED/settlementIncluded=true` | 当前有效对客费用 | 允许生成 Finance 来源 |
| 旧费用 `SUPERSEDED` + 旧明细 `SUPERSEDED` | 已有更新版本 | Finance 已消费时保留原来源,新增调整来源,不覆盖历史 |
| 人工明细 `PENDING/DRAFT/false` | 待业务审核 | 不入池 |
| 人工明细 `APPROVED/DRAFT/false` | 已审核但未发布 | 当前仍不入池;先补发布动作与事件 |
| `REJECTED` | 业务驳回 | 不入池 |
`SENT` 是当前代码中的“已发布给客户”状态,F0 契约宜对外统一为业务语义清晰的 `PUBLISHED`,同时保留内部兼容映射,避免直接修改存量状态造成迁移风险。
## 四、Outbox 与事件能力现状
### 4.1 Firstleg 本地 Outbox
`fl_inbound_outbox` 已包含:事件 ID、客户隔离键、聚合类型/ID、事件类型、JSON 负载、发布状态、重试次数、下次重试时间、发布时间和创建时间。费用发布/调整与业务表在同一本地事务内写入 Outbox,写入位置可复用。
当前实际费用事件只有:
| 事件 | 触发点 | 当前负载 |
|---|---|---|
| `FIRSTLEG_FEE_SENT` | 已确认费用发布 | `inboundId`、`feeId` |
| `FIRSTLEG_FEE_ADJUSTED` | 已发布费用生成新版本 | `inboundId`、`feeId`、`feeVersion` |
当前 Firstleg 模块没有 `@Scheduled` Outbox 扫描器、消息生产者、发布成功/重试更新逻辑,也没有费用作废事件。因此 `PENDING` 记录目前只落库,不会自动送达 Finance。
### 4.2 可参考的 Order Outbox
Order 已有可运行的 `OrderInboundOutboxPublisher`:
- 定时分页扫描待发布事件;
- 校验聚合类型、事件类型、所有权和负载完整性;
- 通过受保护内部客户端投递 Firstleg;
- 成功标记已发布,失败设置下次重试,不支持的事件永久失败;
- 对应测试覆盖成功、无效事件、负载错误、所有权冲突和重试。
F0 可以复用这一可靠投递结构和测试思路,但费用事件应使用专门发布器/消费者和冻结的事件信封,不应扩展 `OrderInboundOutboxPublisher` 承载 Finance 逻辑。
### 4.3 Event Bridge 与公共事件契约
`common-event-contract` 已提供版本化 `EventEnvelope`、区域、事件 ID、负载规范化、SHA-256 哈希和基础校验;ID 使用字符串,适合跨语言和长整型安全传输。
`event-bridge-service` 已提供:
- `POST /internal/event-bridge/events`,使用 `X-Internal-Service-Token`;
- 目标区域和 `payloadHash` 校验;
- 以 `eventId` 幂等入库;
- 相同事件 ID 但负载哈希、客户或信封身份不同则拒绝覆盖;
- Inbox 状态初始为 `RECEIVED`。
但它明确是跨区域接收骨架:状态接口返回发送能力未就绪,尚无真实跨区发送、重试调度、死信补投和领域消费分发。公共 `EventTypes` 也没有费用事件常量。Firstleg → Finance 是同国内业务域内的服务协作,F0 应优先建立独立的服务内事件投递/Inbox 契约;只有架构明确要求统一事件总线时,才将公共信封的通用字段复用进该链路。
### 4.4 审计 Outbox 不是领域 Outbox
Firstleg 还包含通用 `sys_audit_outbox`,用于 `@BusinessAudit` 业务操作审计。它与 `fl_inbound_outbox` 的领域事件用途不同,不能把审计事件当作 Finance 来源事件。F0 需要同时保留“来源领域事件”和“财务操作审计”两条记录。
## 五、Admin 已有能力
### 5.1 正式页面与接口
| 页面/能力 | 当前状态 | 使用契约 |
|---|---|---|
| 费用项目 | 正式路由 `/master-data/fee-items`,支持查询、新增、编辑、启停、删除和权限态 | `/platform/v1/fee-items` |
| 船期价格表 | 正式路由 `/first-leg-pricing/carrier-rates`,支持价格表上传、查询和发布 | FBA pricing price-book API |
| 入库费用快照 | 入库详情工作区已嵌入 `InboundFeePanel`,展示计算依据、金额、规则/价格版本和时间线 | `GET .../fees/latest` |
| 已发布费用调整 | 入库费用面板支持填写小计、附加费和原因,带客户端幂等键 | `PUT .../fees/published` |
| 统一订单费用明细 | 订单详情费用页签按入库单加载应收/应付明细并展示审核、计费、结算标识 | `GET .../fee-details?accountId=` |
Admin 当前没有:
- 财务来源池页面;
- 跨客户/跨入库单费用筛选、入池状态、冲突和重放操作;
- 账单占用、冻结、补差/冲销、作废展示;
- Finance 权限和正式 Finance API 适配。
财务管理导航仍整体 `hidden: true`,指向 `/prototype/finance/*`。这些原型页不属于正式生产能力,不应作为 F0 接入基础。
### 5.2 前后端契约注意点
- Admin 对费用快照和入库 ID 使用字符串类型,符合长整型传输要求;
- 费用项目资源兼容 `number | string`,但新增/编辑请求没有暴露后端可选 `feeCode`,F0 不应从页面名称反推稳定来源科目;
- 统一订单费用明细前端预留了 `billNo`、结算起止日等字段,后端当前实体并未提供这些财务字段;这些 UI 预留不能视为已实现账单能力;
- `InboundFeePanel` 只接入查询和发布后调整,没有接入试算、确认、发布按钮,相关动作仍由后端流程/其他编排承担。
## 六、F0 缺口清单
| 优先级 | 缺口 | 风险/影响 |
|---|---|---|
| P0 | 缺少 Finance 服务、数据库、Inbox 和来源表 | 无法建立财务所有权和独立事务边界 |
| P0 | Firstleg 无费用 Outbox 发布器 | 事件只停留在 `PENDING`,Finance 无法接收 |
| P0 | 费用事件名称、版本和负载未冻结 | 当前 `FIRSTLEG_FEE_SENT` 与目标 `PUBLISHED` 语义不一致,负载无法独立幂等核验 |
| P0 | 缺少按 `sourceFeeId + sourceFeeVersion` 查询的内部快照 API | Finance 只能依赖事件少量字段或平台 API,均不合适 |
| P0 | 缺少来源哈希 | 无法检测同一来源版本内容被覆盖或事件冲突 |
| P0 | 缺少作废事件和原因 | 错误费用无法以不可变方式退出财务来源池 |
| P0 | 发布后调价未校验财务占用 | 已出账后仍可能直接把旧来源标记为 `SUPERSEDED` |
| P1 | 人工费用审核后无发布动作 | 人工应收/应付无法可靠进入统一来源链路 |
| P1 | 无跨单费用池查询和游标/时间增量接口 | 补偿扫描、历史迁移和运营核对困难 |
| P1 | 无 Outbox 失败查询、人工重放、退避和死信策略 | 故障不可运营化处理 |
| P1 | 无 Finance 独立权限与审计事件 | 来源冲突、重放、作废等高风险操作无法分权 |
| P1 | 应付来源字段不足 | 人工 `PAYABLE` 缺供应商、付款主体、凭证和成本归属,不能直接进入 F2 |
| P2 | OpenAPI 未提供完整 `FeeSnapshotResponse` schema | 跨服务调用方难以通过契约测试防止字段漂移 |
| P2 | Admin 无来源接入状态和错误可观测性 | 上游已发布但 Finance 未入池时无法定位 |
## 七、Firstleg → Finance F0 接入建议
### 7.1 接入主链路
```text
Firstleg 费用发布/调整/作废事务
-> 更新 Firstleg 业务费用与不可变版本
-> 同事务写 fl_inbound_outbox
-> Firstleg Outbox Publisher 投递版本化事件
-> Finance Inbox 以 eventId 去重并校验来源身份
-> Finance 用内部令牌查询指定来源版本完整快照
-> 校验 sourceHash
-> Finance 保存不可变来源快照和处理结果
-> 后续 F1/F2 从 Finance 来源池制单
```
Finance 不跨库读取 Firstleg,不共享 Entity,不以 Admin 平台接口作为服务间接口。
### 7.2 建议冻结的事件
建议对外统一:
- `FIRSTLEG_FEE_PUBLISHED`;
- `FIRSTLEG_FEE_ADJUSTED`;
- `FIRSTLEG_FEE_VOIDED`。
兼容策略:存量 `FIRSTLEG_FEE_SENT` 可由发布器映射为 `FIRSTLEG_FEE_PUBLISHED` V1,或通过一次受控迁移重建待发布事件;不要让 Finance 同时长期理解两套同义事件。
事件信封至少包含:
- `eventId`、`eventType`、`eventVersion`、`occurredAt`;
- `customerId`(技术字段仍可映射现有 `tenantId`);
- `aggregateType=FIRSTLEG_FEE`、`aggregateId=sourceFeeId`、`aggregateVersion=sourceFeeVersion`;
- `inboundId`、`sourceFeeId`、`sourceFeeVersion`、`sourceFeeNo`;
- `feeDetailId`、`direction`、`businessDate`、`currency`、`amount`;
- `reviewStatus`、`publicationStatus`、`settlementIncluded`;
- `sourceHash`、`traceId`、`correlationId`、`causationId`;
- 调整/作废事件的 `previousSourceFeeId`、`reason`。
事件只用于通知和快速路由,Finance 仍应通过内部接口读取完整快照。
### 7.3 建议新增的 Firstleg 内部接口
建议提供受内部服务令牌保护的版本查询,而不是复用 `latest`:
```text
GET /internal/firstleg/v1/finance/fee-sources/{sourceFeeId}?version={sourceFeeVersion}
```
返回专用 `FirstlegFeeSourceSnapshot`,至少覆盖:来源身份、客户、入库业务键、费用项目快照、方向、业务日、币种金额、计费依据、审核/发布/结算候选状态、发布时间、调整链和 `sourceHash`。不存在、版本不符、未达到入池状态时返回明确业务错误码。
可补充一个只用于补偿和迁移的分页接口,按 `publishedAt + sourceFeeId` 稳定游标扫描;它不能替代实时事件。
### 7.4 幂等与乱序
- Outbox 事件 ID 全局唯一;
- Finance Inbox 先按 `eventId` 去重;
- 财务来源再以 `sourceType + sourceId + sourceVersion` 唯一;
- 同一唯一键但 `sourceHash` 不同必须进入冲突状态,禁止覆盖;
- 调整 V2 早于发布 V1 到达时,先保存待依赖状态并重试核验,不能用 V2 覆盖 V1;
- Finance 已出账的 V1 永久保留,V2 形成补差/冲销候选,不修改原账单明细;
- 发布器成功、重试、永久失败和人工重放均记录时间、次数、错误摘要和审计人。
### 7.5 Firstleg 调整门禁
F0 上线前应冻结以下规则:
1. 未进入 Finance 或仍在待对账池的来源,可由新版本替代,但 Finance 保留版本历史;
2. 已被账单占用的来源不能直接调整原金额,必须发布补差或冲销来源;
3. 作废不物理删除原费用和事件;
4. Firstleg 不保存财务余额或账单状态,只保存必要的只读占用/处理回执或通过 Finance 内部校验门禁;
5. Finance 不回写 `fl_inbound_fee_detail` 伪造 `billNo`,账单关系归 Finance 自有表。
### 7.6 权限建议
保留现有 Firstleg 业务权限,并在 IAM 中为 Finance 新建独立权限,例如:
- `finance:source:view`;
- `finance:source:retry`;
- `finance:source:resolve-conflict`;
- `finance:source:void`;
- `finance:source:export`。
内部事件投递和快照查询使用服务身份,不使用平台员工权限。人工重放、冲突处理和作废属于高风险操作,必须填写原因并记录业务审计。
## 八、建议实施顺序与验收
### 8.1 实施顺序
1. 冻结 V1 事件、内部快照 DTO、来源哈希算法和状态映射;
2. 在 Firstleg 增加指定版本内部查询及契约测试;
3. 增加 Firstleg Outbox 发布器、重试/失败状态和运维查询;
4. 建立 Finance Inbox、来源表、唯一约束和冲突状态;
5. 联通发布、重复、乱序、失败重试、调整和作废链路;
6. 增加 Admin 来源池和接入状态页面;
7. 最后迁移满足条件的历史已发布费用。
### 8.2 F0 最小验收案例
- 一条 `APPROVED + PUBLISHED + settlementIncluded=true` 的应收费用只生成一个 Finance 来源;
- 同一事件重复投递不重复入池;
- 同一来源版本不同哈希被拒绝并可运营处理;
- Finance 暂停后恢复,Firstleg Outbox 能重试并最终成功;
- V2 调整不会覆盖 Finance 中 V1 来源;
- V2 先于 V1 到达不会破坏版本顺序;
- 已出账 V1 的后续调整形成补差/冲销候选;
- 作废保留原来源和审计链;
- 跨客户查询、事件或快照调用被服务端拒绝;
- 长整型 ID 按字符串传输,金额/币种精度不丢失;
- Admin 能看到待处理、成功、重试、冲突状态以及 `requestId/traceId`。
## 九、证据路径
### 9.1 Firstleg 费用与计费
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFbaPricingController.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/dto/FbaPricingDtos.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundFeeEntity.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundFeeItemEntity.java`
- `baiyue-firstleg/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml`
- `baiyue-firstleg/firstleg/src/main/resources/db/phase2-inbound-schema.sql`
- `baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java`
- `baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java`
### 9.2 费用明细与费用项目
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundFeeDetailController.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundFeeDetailDtos.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundFeeDetailEntity.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFeeItemController.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/entity/FeeItemEntity.java`
- `baiyue-firstleg/firstleg/src/main/resources/openapi/fee-item-v1.yaml`
- `baiyue-firstleg/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql`
- `baiyue-firstleg/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql`
- `baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java`
- `baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java`
### 9.3 客户费用查询
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java`
- `baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java`
### 9.4 Outbox、事件与权限
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundOutboxEntity.java`
- `baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOutboxMapper.java`
- `baiyue-firstleg/order/src/main/java/com/baiyue/order/entity/OutboxEventEntity.java`
- `baiyue-firstleg/order/src/main/java/com/baiyue/order/service/OrderInboundOutboxPublisher.java`
- `baiyue-firstleg/order/src/test/java/com/baiyue/order/service/OrderInboundOutboxPublisherTest.java`
- `baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/EventEnvelope.java`
- `baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java`
- `baiyue-firstleg/event-bridge-service/src/main/java/com/baiyue/eventbridge/controller/EventBridgeController.java`
- `baiyue-firstleg/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/EventInboxService.java`
- `baiyue-firstleg/event-bridge-service/src/test/java/com/baiyue/eventbridge/service/EventInboxServiceTest.java`
- `baiyue-firstleg/iam/src/main/resources/db/phase2-fba-pricing-permissions.sql`
- `baiyue-firstleg/iam/src/main/resources/db/phase2-fee-item-permissions.sql`
- `baiyue-firstleg/deploy/sql/头程数据库统一增量-MySQL8.sql`
### 9.5 Admin
- `apps/admin-web/src/modules/inbounds/fee/api.ts`
- `apps/admin-web/src/modules/inbounds/fee/types.ts`
- `apps/admin-web/src/modules/inbounds/fee/InboundFeePanel.vue`
- `apps/admin-web/src/modules/inbounds/components/InboundWorkflowWorkspace.vue`
- `apps/admin-web/src/modules/fee-items/api/platformFeeItems.ts`
- `apps/admin-web/src/modules/fee-items/pages/FeeItemsView.vue`
- `apps/admin-web/src/modules/unified-orders/api/unifiedOrders.ts`
- `apps/admin-web/src/modules/unified-orders/pages/UnifiedOrdersView.vue`
- `apps/admin-web/src/core/navigation/adminNavigation.ts`
## 十、盘点边界
本文只证明当前代码已经存在的能力和 F0 接入缺口。没有修改业务代码、数据库脚本或既有文档,也没有验证测试环境消息中间件、数据库实值或实际事件投递。Finance 的财务主体、期间、账套、币种、权限和编号等产品口径仍以财务管理产品确认结果为准。
+144
View File
@@ -0,0 +1,144 @@
# 财务管理产品确认表
> 版本:V1.0
> 创建日期:2026-08-10
> 状态:待产品与业务确认
> 适用范围:客户账户、客户应收应付、客户对账单、结算单应收/应付、收付款核销、发票、费用分析和利润
> 技术方案说明:服务边界、跨服务访问、事件幂等、金额存储、状态留痕和正式模块发布方式已按当前仓库架构直接确认,不再要求产品确认;以 XLSX 的“技术结论”工作表为准。
正式技术决策见:[财务服务边界设计](财务服务边界设计.md)。
## 一、确认说明
本表用于把财务原型中的全量能力转换为可开发的产品结论。原型只作为页面和字段参考,不能代替状态、权限、金额口径和数据来源确认。
- “建议结论”是可直接评审的默认方案,不是已确认需求。
- “最终结论”由产品/业务负责人填写;确认后状态改为“已确认”。
- 暂时无法确认的事项改为“暂缓”,同时填写本阶段临时处理方式。
- F0 是全量财务共同底座;F1/F2 可在 F0 契约冻结后并行;F3/F4 依赖前序台账。
## 二、全量范围确认
| 编号 | 范围 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| S1 | 客户账户 | 做预充值、冻结、解冻、扣款、退款和账户流水;账户余额与应收核销分开,任何抵扣都产生核销记录 | 待填写 | 待确认 |
| S2 | 客户应收 | 做客户费用池、对账单、应收单、到期/逾期、收款、部分核销、结清、反核销 | 待填写 | 待确认 |
| S3 | 客户应付 | 仅在确有平台向客户付款的业务场景时启用;否则不开放菜单和数据方向 | 待填写 | 待确认 |
| S4 | 客户对账单 | 按客户 + 币种 + 账期生成,支持内部复核、发送、客户确认、争议、重开和关闭 | 待填写 | 待确认 |
| S5 | 供应商应付 | 做供应商成本池、供应商对账、应付单、付款、付款核销和反核销 | 待填写 | 待确认 |
| S6 | 发票 | 做发票申请、开票登记、票号/文件关联、红冲和作废;不直接假定外部开票平台 | 待填写 | 待确认 |
| S7 | 费用分析 | 按费用项目、客户、供应商、期间统计发生额、已结算额和余额,支持钻取来源单据 | 待填写 | 待确认 |
| S8 | 利润 | 支持客户、货柜、单票、月份维度;收入、成本、分摊、汇率和利润率必须可追溯 | 待填写 | 待确认 |
## 三、F0 财务底座确认
| 编号 | 需要确认的事项 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| F0-01 | 财务服务边界 | 新增独立 Finance 服务和财务数据库;Firstleg 只提供不可变业务费用来源,不承载应收应付余额 | 按当前架构采用独立 Finance 服务/财务库 | 技术已确认 |
| F0-02 | 财务主体/账套 | 至少支持平台收款主体、付款主体;主体、账套和客户隔离键分开建模 | 待填写 | 待确认 |
| F0-03 | 往来单位 | Finance 引用客户/供应商主数据并保存名称快照;不复制业务实体,不跨库 Join | 待填写 | 待确认 |
| F0-04 | 币种与精度 | 使用 ISO 4217 三位码;原币金额不可覆盖;不同币种不直接合并或核销 | 待填写 | 待确认 |
| F0-05 | 汇率 | F0 预留汇率来源、生效时间、版本和快照;未确认前不做自动折算 | 待填写 | 待确认 |
| F0-06 | 财务期间 | 支持 OPEN、CLOSING、CLOSED;关闭期间禁止直接改账,错误走调整/冲销单 | 待填写 | 待确认 |
| F0-07 | 编号 | AR/AP/对账单/收款/付款/核销/发票/钱包分别由服务端发号,重试返回原单号 | 待填写 | 待确认 |
| F0-08 | 来源费用入池 | 仅接收 APPROVED、PUBLISHED、settlementIncluded=true 且未作废/未被有效账单使用的来源版本 | 待填写 | 待确认 |
| F0-09 | 事件可靠性 | Firstleg Outbox 发布费用事件,Finance 消费幂等并保存来源快照;失败可重试、可查结果 | 按现有 Outbox 与幂等模式实现 | 技术已确认 |
| F0-10 | 审计与权限 | 制单、复核、确认、收付款、核销、反核销、关账、开票、钱包操作使用独立权限和审计事件 | 待填写 | 待确认 |
## 四、F1 客户应收与对账确认
| 编号 | 需要确认的事项 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| F1-01 | 账单聚合维度 | 客户 + 币种 + 账期;不同币种不合并 | 待填写 | 待确认 |
| F1-02 | 生成方式 | 支持按筛选结果人工生成;后续可增加定时生成,但不得重复出账 | 待填写 | 待确认 |
| F1-03 | 账单状态 | DRAFT → INTERNAL_REVIEW → SENT → CUSTOMER_CONFIRMED → CLOSED;SENT 可 DISPUTED 后重开 | 待填写 | 待确认 |
| F1-04 | 应收状态 | OPEN → PARTIALLY_SETTLED → SETTLED;到期后增加 OVERDUE 标记,不改变余额 | 待填写 | 待确认 |
| F1-05 | 客户确认 | 客户可确认或提出争议;争议必须填写原因和明细,处理后重新发送 | 待填写 | 待确认 |
| F1-06 | 收款登记 | 首版人工登记到账日、金额、币种、方式、凭证号、附件和登记人 | 待填写 | 待确认 |
| F1-07 | 核销关系 | 支持一笔收款核销多张应收、一张应收被多笔收款核销;禁止跨币种和超额 | 待填写 | 待确认 |
| F1-08 | 反核销/作废 | 独立高风险权限,填写原因并保留原记录;不物理删除 | 待填写 | 待确认 |
| F1-09 | 出账后调整 | 不直接覆盖原费用;通过补差、冲销或调整单形成新来源 | 待填写 | 待确认 |
| F1-10 | 预收/多收/手续费/退款 | 单独建模为预收、其他应付款或退款,不直接冲减应收金额 | 待填写 | 待确认 |
## 五、F2 供应商应付与结算确认
| 编号 | 需要确认的事项 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| F2-01 | 成本来源 | 只有具备供应商、付款主体、成本凭证、币种金额和成本归属的来源才能进入应付池 | 待填写 | 待确认 |
| F2-02 | 成本确认节点 | 以供应商账单/业务完成/内部审核中确定一个正式节点;确认后冻结成本快照 | 待填写 | 待确认 |
| F2-03 | 供应商账单 | DRAFT → INTERNAL_REVIEW → SENT → CONFIRMED → CLOSED;差异走争议/补差/冲销 | 待填写 | 待确认 |
| F2-04 | 应付状态 | OPEN → PARTIALLY_PAID → PAID;到期增加 OVERDUE 标记 | 待填写 | 待确认 |
| F2-05 | 付款与核销 | 付款单独登记,支持多对多付款核销;禁止跨币种和超额 | 待填写 | 待确认 |
| F2-06 | 审批 | 应付确认和付款前复核使用独立权限;是否需要金额分级审批待确认 | 待填写 | 待确认 |
| F2-07 | 预付/押金/代付/退款 | 不混入普通应付余额,单独建模并保留关联单据 | 待填写 | 待确认 |
| F2-08 | 当前人工 PAYABLE 明细 | 仅作为待补充来源,不得直接视为供应商应付闭环 | 待填写 | 待确认 |
## 六、F3 客户账户与发票确认
| 编号 | 需要确认的事项 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| F3-01 | 账户余额性质 | 明确是客户预付款余额还是平台内部授信;充值必须有到账凭证,余额变更原子落流水 | 待填写 | 待确认 |
| F3-02 | 账户与应收关系 | 账户扣款可以核销应收,但不得直接修改应收原金额;余额、扣款、核销三条记录均保留 | 待填写 | 待确认 |
| F3-03 | 冻结与解冻 | 明确冻结触发、解冻人、原因、部分冻结和失败补偿 | 待填写 | 待确认 |
| F3-04 | 退款 | 退款必须关联原充值/收款,记录审批、到账方式、退款凭证和余额恢复规则 | 待填写 | 待确认 |
| F3-05 | 发票主体 | 明确开票主体、税率、含税/未税、发票类型和客户抬头校验 | 待填写 | 待确认 |
| F3-06 | 开票流程 | 申请 → 审核 → 开票登记 → 票据/文件关联;红冲、作废不可删除原票 | 待填写 | 待确认 |
| F3-07 | 外部平台 | 首版可人工登记,外部开票平台和银行流水接口另定接入契约 | 待填写 | 待确认 |
## 七、F4 费用分析与利润确认
| 编号 | 需要确认的事项 | 建议结论 | 最终结论 | 状态 |
|---|---|---|---|---|
| F4-01 | 收入口径 | 按已确认/已发布应收还是已收款确认收入,二者报表分开显示 | 待填写 | 待确认 |
| F4-02 | 成本口径 | 按已确认应付还是已付款确认成本,二者报表分开显示 | 待填写 | 待确认 |
| F4-03 | 分摊维度 | 支持客户、订单、单票、柜次、费用项目;明确公共成本分摊算法 | 待填写 | 待确认 |
| F4-04 | 汇率 | 明确交易日、账单日或月末汇率,以及本位币和汇兑损益 | 待填写 | 待确认 |
| F4-05 | 期间锁定 | 月结后报表冻结;调整通过新期间补差/冲销反映 | 待填写 | 待确认 |
| F4-06 | 利润维度 | 客户、货柜、单票、月份均支持;每个结果可钻取到收入、成本、分摊和原始单据 | 待填写 | 待确认 |
| F4-07 | 导出与权限 | 导出按财务数据权限控制,客户不可见供应商成本和内部利润 | 待填写 | 待确认 |
## 八、全量验收主链路
### 8.1 客户应收
```text
Firstleg 已发布费用
→ Finance 来源池
→ 客户对账单
→ 内部复核/客户确认
→ 应收单
→ 部分收款
→ 多对多核销
→ 结清
→ 反核销恢复余额
```
### 8.2 供应商应付
```text
已审核供应商成本
→ 供应商对账单
→ 应付审批/确认
→ 付款登记
→ 付款核销
→ 结清
→ 反核销/付款撤销
```
### 8.3 账户、发票和利润
```text
充值/收款 → 账户流水 → 应收核销
应收确认 → 开票申请 → 开票登记/红冲
应收收入 + 应付成本 + 分摊 + 汇率 → 利润分析/利润表
```
## 九、进入设计与开发的门槛
- S1-S8、F0-01 至 F0-10 全部形成结论,或明确暂缓处理方式。
- F1/F2 的账单、应收应付、收付款和核销对象关系冻结。
- F2 供应商成本来源不能继续使用没有供应商和凭证的人工 PAYABLE 明细替代。
- 财务服务与 Firstleg 的事件、查询接口、来源版本和幂等契约完成书面确认。
- F3 钱包资金性质、发票税务主体和 F4 收入成本/汇率/分摊口径完成确认后,才能进入对应开发阶段。
- 每阶段至少准备正常、重复提交、并发、越权、跨币种、超额核销、已关账调整和反核销验收案例。
@@ -0,0 +1,195 @@
# 财务领域模型与数据字典(F0/F1 首版)
> 版本:V0.1(开发前设计稿)
> 日期:2026-08-10
> 范围:F0 财务底座、F1 客户应收与对账;F2-F4 预留
> 关联:[财务服务边界设计](财务服务边界设计.md)、[财务管理产品确认表](财务管理产品确认表.md)、[二期费用项目数据归属决策记录](../二期/二期费用项目数据归属决策记录.md)
## 1. 阅读和决策标记
| 标记 | 含义 |
|---|---|
| 技术已确认 | 已由当前架构或总控决策冻结,开发必须遵守;不再要求产品选择技术方案。 |
| 建议默认值 | 为保证模型可实施而提出的默认值,产品未确认前可调整。 |
| 产品待确认 | 会改变业务口径、页面或验收结果,不能当作已确定需求。 |
本稿只冻结逻辑归属、关系和不可逆数据保护规则。账期天数、账套/主体、收入确认日、汇率及分摊算法等业务口径仍以产品确认表为准。
## 2. 已确认的公共约束
### 2.1 服务与数据库
- **技术已确认**:Finance 为独立 `finance-service`,数据库为 `baiyue_finance`;Finance 不跨库 Join,不引用其他服务 Entity,不接受 Firstleg 直接写库。
- **技术已确认**:Firstleg 只提供已发布的业务费用版本;Finance 入池后保存来源快照,历史账单不依赖 Firstleg 当前行回算。
- **技术已确认**:跨服务只使用 DTO、受保护内部查询和 Outbox/Inbox 事件;事件消费必须幂等、可重试、可查结果。
### 2.2 主体、客户和 ID
- **技术已确认**:产品层级为“平台—客户”,`tenant_id=0` 仅为平台技术作用域,客户业务数据使用 `tenant_id>0`。Finance 表按客户隔离;平台公共主数据不因客户筛选改变候选范围。
- **建议默认值**:所有主键使用 `BIGINT UNSIGNED`(雪花/号段 ID);API、JSON 和前端 TypeScript 均按字符串传输。
- **技术已确认**:往来单位只保存 `party_id` 引用、`party_type` 和当时的名称/编码快照,不复制客户或供应商实体。
### 2.3 金额、币种和精度
- **技术已确认**:币种保存 ISO 4217 三位码;原币金额不可覆盖;不同币种不能直接合并或核销。
- **建议默认值**:金额 `DECIMAL(20,6)` 保存原始计算值,展示/结算精度和舍入模式另设字段;数量/重量/体积 `DECIMAL(20,6)`。具体精度需与产品确认的计费规则复核。
- 所有金额字段成对保存 `*_amount` 与 `currency_code`(或由单据头统一币种且明细校验一致),不得用浮点数。
### 2.4 通用字段与审计
所有业务表建议具备:`id`、`tenant_id`、`version`(乐观锁)、`created_at`、`created_by`、`updated_at`、`updated_by`、`deleted`(仅允许非财务事实草稿使用)。已出账、收款、核销、冲销、开票和期间记录禁止物理删除;更正使用反向/调整记录。
## 3. 聚合边界与关系
```text
Firstleg FeeVersion --(Outbox)--> FinanceFeeSource
FinanceFeeSource --(可选多条)--> ARStatementLine --(一条有效来源只入一张有效账单)
ARStatement --(确认后生成)--> ARReceivable
ARReceipt <--> ARAllocation <--> ARReceivable
ARAllocation --(反核销)--> ARAllocationReversal
所有写操作 --> FinanceAuditLog / FinanceOutbox
```
聚合根建议:`FinanceFeeSource`(来源池)、`ARStatement`(客户对账单)、`ARReceivable`(应收单)、`ARReceipt`(收款单)。核销是独立不可变事实,不把余额直接写成“收款成功”。
## 4. F0 财务底座数据字典
以下为建议物理表名;字段类型是实现建议,不代表已经执行建表。
### 4.1 `fin_subject`:财务主体(产品待确认)
| 字段 | 类型/约束 | 说明 | 状态 |
|---|---|---|---|
| `id` | BIGINT PK | 主体 ID | 技术已确认(ID 规则) |
| `subject_code` | VARCHAR(64) UNIQUE | 收款/付款法律或业务主体编码 | 产品待确认:主体数量和编码 |
| `subject_name` | VARCHAR(200) | 主体名称快照 | 产品待确认 |
| `subject_type` | VARCHAR(32) | `RECEIVING`/`PAYING`/`BOTH` 建议 | 建议默认值 |
| `status` | VARCHAR(20) | `ACTIVE`/`INACTIVE` | 建议默认值 |
### 4.2 `fin_ledger`:账套与期间归属(账套待确认)
`ledger` 关联 `subject_id`,不把客户 ID 当作账套。建议字段:`ledger_code`(唯一)、`ledger_name`、`subject_id`、`base_currency_code`、`status`、审计字段。期间表 `fin_period` 使用 `period_code`(如 `2026-08`)和 `status=OPEN|CLOSING|CLOSED`;关闭期间禁止原地改账。主体、账套数量、本位币和是否月结均为**产品待确认**。
### 4.3 `fin_counterparty_ref`:往来单位引用快照
| 字段 | 类型/约束 | 说明 |
|---|---|---|
| `id` | BIGINT PK | Finance 内部引用 ID |
| `tenant_id` | BIGINT UNSIGNED NOT NULL | 客户隔离键;供应商为平台公共或其所属业务范围需按确认结果定 |
| `party_type` | VARCHAR(20) | `CUSTOMER`/`SUPPLIER` |
| `party_id` | BIGINT UNSIGNED NOT NULL | IAM/Warehouse 的外部 ID,仅引用 |
| `party_code`,`party_name` | VARCHAR | 获取时快照,保障历史可读 |
| 唯一约束 | `(tenant_id, party_type, party_id)` | 禁止同一客户重复引用 |
往来单位来源服务和供应商归属规则为**产品待确认**;Finance 不跨库外键。
### 4.4 `fin_currency` / `fin_exchange_rate`(F0 预留)
`fin_currency`:`currency_code CHAR(3) PK`、名称、精度、启用状态。
`fin_exchange_rate`:`id`、`from_currency`、`to_currency`、`rate DECIMAL(20,10)`、`rate_source`、`effective_at`、`version_no`、`snapshot_hash`、审计字段;唯一建议为 `(from_currency,to_currency,effective_at,version_no)`。汇率来源、交易日/账单日/月末取值和本位币折算为**产品待确认**,F0 仅建扩展位,未确认前不自动折算。
### 4.5 `fin_number_sequence`:单据编号
按 `sequence_type`(`AR`、`AP`、`STATEMENT`、`RECEIPT`、`PAYMENT`、`ALLOCATION`、`INVOICE`、`ACCOUNT`)和日期/主体分段发号。建议唯一 `(tenant_id, sequence_type, period_key)`,数据库锁或原子更新保证并发安全;重试使用幂等键返回原单号。编号格式和是否按主体分段为**产品待确认**。
### 4.6 `fin_fee_source`:财务费用来源池
| 字段 | 类型/约束 | 说明 |
|---|---|---|
| `id` | BIGINT PK | Finance 来源记录 ID |
| `tenant_id` | BIGINT UNSIGNED NOT NULL, CHECK >0 | 客户隔离键 |
| `source_type` | VARCHAR(32) | `FIRSTLEG_FEE` 等 |
| `source_id`,`source_version` | BIGINT/INT NOT NULL | Firstleg 费用 ID 与不可变版本 |
| `source_no` | VARCHAR(64) | 业务费用单号快照 |
| `inbound_id`,`order_id` | BIGINT NULL | 业务关联 ID 快照,不建跨库 FK |
| `fee_item_id`,`fee_item_code`,`fee_item_name` | ID + 快照 | 引用平台公共费用项目;名称保留快照 |
| `direction` | VARCHAR(16) | `RECEIVABLE`/`PAYABLE`,供应商方向待 F2 确认 |
| `amount`,`currency_code` | DECIMAL + CHAR(3) | 原币金额 |
| `tax_amount`,`tax_rate` | DECIMAL | 税务字段,税口径待确认 |
| `business_date` | DATE | 来源业务日期 |
| `eligibility_status` | VARCHAR(20) | `PENDING/ELIGIBLE/EXCLUDED/BILLED/VOIDED` |
| `source_hash` | CHAR(64) | 来源内容哈希 |
| `event_id` | VARCHAR(100) | Outbox 事件 ID |
| 唯一约束 | `(source_type, source_id, source_version)`;`(event_id)` | 防重复入池 |
**建议默认值**:仅 `APPROVED + PUBLISHED + settlementIncluded=true`、未作废版本进入 `ELIGIBLE`。该门槛、是否自动入池及历史迁移是**产品待确认(F0-08/F1 相关)**。
### 4.7 `fin_inbox_event` / `fin_audit_log`
`fin_inbox_event` 保存 `event_id`、事件类型、来源服务、幂等键、payload_hash、处理状态、重试次数、最后错误和处理时间;唯一 `(source_service,event_id)`,同键不同哈希必须拒绝并报警。
`fin_audit_log` 保存聚合类型/ID、动作、前后状态、变更摘要、操作者、请求/幂等键、trace_id、时间和原因。审计留存期、是否需要不可篡改存储为**产品待确认**,但财务事实不得删除是技术底线。
## 5. F1 客户应收与对账数据字典
### 5.1 `ar_statement`:客户对账单聚合根
核心字段:`id`、`tenant_id`、`statement_no`、`ledger_id`、`subject_id`、`counterparty_ref_id`、`currency_code`、`period_start/end`、`due_date`、`status`、`subtotal_amount`、`tax_amount`、`total_amount`、`confirmed_at/by`、`dispute_reason`、`version`。
建议唯一 `(tenant_id, statement_no)`;防重复出账的业务唯一键建议 `(tenant_id,counterparty_ref_id,currency_code,period_start,period_end,generation_key)`。账期边界、到期日和是否允许同一期间多张账单为**产品待确认**。
状态建议:`DRAFT → INTERNAL_REVIEW → SENT → CUSTOMER_CONFIRMED → CLOSED`;`SENT → DISPUTED → DRAFT`(重开)或 `VOIDED`。状态值来自产品确认表建议,最终流转和客户确认是否必需仍待确认。
### 5.2 `ar_statement_line`:对账单明细快照
字段:`id`、`statement_id`、`tenant_id`、`fee_source_id`、`source_version`、费用项目/来源单号快照、`quantity`、`unit_price`、`amount`、`currency_code`、税额、争议标记、排除原因、审计字段。
唯一建议 `(statement_id, fee_source_id)`;另以唯一条件约束同一有效 `fee_source_id` 只能关联一张未作废账单。明细不直接回写 Firstleg。
### 5.3 `ar_receivable`:应收单
字段:`id`、`tenant_id`、`receivable_no`、`statement_id`、`counterparty_ref_id`、`subject_id`、`currency_code`、`original_amount`、`allocated_amount`、`outstanding_amount`、`due_date`、`status`、`overdue_flag`、`recognized_at`、版本与审计字段。
唯一建议 `(tenant_id, statement_id)`(若产品允许一张账单拆多应收,改为 `(statement_id, receivable_no)`)。余额由核销事实汇总并在事务内更新,禁止前端传余额。
状态建议:`OPEN → PARTIALLY_SETTLED → SETTLED`;到期只置 `overdue_flag`。是否账单确认即生成应收、是否允许账单拆分,为**产品待确认**。
### 5.4 `ar_receipt`:收款单
字段:`id`、`tenant_id`、`receipt_no`、`subject_id`、`counterparty_ref_id`、`currency_code`、`received_amount`、`allocated_amount`、`available_amount`、`received_at`、`method`、`voucher_no`、`file_id`、`status`、登记/复核人、版本与审计字段。
建议状态 `DRAFT → CONFIRMED → PARTIALLY_ALLOCATED → FULLY_ALLOCATED → VOIDED`。到账方式、是否需复核和银行流水接入均为**产品待确认**;首版默认人工登记。
### 5.5 `ar_allocation` 与 `ar_allocation_reversal`
核销记录字段:`id`、`tenant_id`、`allocation_no`、`receipt_id`、`receivable_id`、`currency_code`、`allocated_amount`、`allocated_at`、`status=ACTIVE|REVERSED`、幂等键、操作者、原因和版本。
反核销字段:`id`、`allocation_id`、`reversal_no`、`reversed_amount`、`reason`、审批/操作人、时间、幂等键。唯一建议 `(tenant_id, idempotency_key)`;服务端校验同币种、不可超出收款可用额和应收未结额,反核销不删除原核销。
### 5.6 `ar_dispute`(建议预留)
保存 `statement_id`、提出方、争议明细/原因、状态 `OPEN|PROCESSING|RESOLVED|REJECTED`、处理结论和审计字段。客户是否在线提出争议、是否超期自动确认是**产品待确认**;若 F1 暂不做,可先在对账单保留原因字段而不开放独立菜单。
## 6. F2-F4 预留模型(不代表本期开发)
| 阶段 | 建议聚合/表 | 预留关系 | 产品待确认 |
|---|---|---|---|
| F2 应付 | `ap_statement`、`ap_statement_line`、`ap_payable`、`ap_payment`、`ap_allocation`、`ap_allocation_reversal` | 成本来源池 → 供应商账单 → 应付 → 付款核销 | 供应商主数据、成本确认节点、付款主体、审批分级 |
| F3 账户 | `fin_customer_account`、`fin_account_entry`、`fin_account_hold`、`fin_account_refund` | 充值/冻结/扣款/退款与应收核销分开,任何抵扣关联 `ar_allocation` | 账户是预付款还是授信、冻结/退款规则 |
| F3 发票 | `fin_invoice_application`、`fin_invoice`、`fin_invoice_action` | 应收可选关联发票;红冲/作废新增动作,不覆盖原票 | 开票主体、税率、含税口径、外部平台 |
| F4 分析 | `fin_cost_entry`、`fin_allocation_rule`、`fin_profit_snapshot`、`fin_report_snapshot` | 收入/成本来源、分摊、汇率快照 → 可钻取利润 | 收入成本确认日、汇率日、分摊维度、本位币和月结 |
F2 的来源必须具备供应商、付款主体、成本凭证、币种金额和成本归属;当前人工 `PAYABLE` 明细不自动视为应付闭环。
## 7. 跨服务来源契约(逻辑字段)
Firstleg Outbox 事件建议包含:`eventId`、`eventType`、`occurredAt`、`customerId`、`inboundId`、`orderId`、`sourceFeeId`、`sourceFeeVersion`、`sourceFeeNo`、`feeDetailId`、`direction`、`businessDate`、`currency`、`amount`、`sourceHash`、`settlementIncluded`、`isVoided`。Finance 收到后通过内部 DTO 查询完整费用快照并写入 `fin_fee_source`。
事件顺序可乱序:以 `(source_id, source_version)` 和版本状态机处理,旧版本不得覆盖新版本。相同幂等键不同 `source_hash` 必须进入冲突状态并告警。费用调整/作废不改写已出账明细,而是形成新的补差或冲销来源(具体业务单据待 F1/F2 确认)。
## 8. 数据访问、索引和隔离验收
- 业务表查询索引必须以 `tenant_id` 开头,并覆盖状态、日期、客户和单号等常用筛选;单据号在客户内唯一。
- Finance 不建立跨库外键;所有外部 ID 仅作引用和快照,失联时显示“来源不可用”而不删除财务事实。
- 平台员工按授权客户范围查询;客户端从登录态取 `tenant_id`,禁止信任请求体客户 ID。供应商成本、内部备注、审计详情和内部附件不向客户返回。
- 金额变更必须通过服务方法在同一财务期间事务内完成,使用幂等键和乐观锁;期间 `CLOSING/CLOSED` 禁止直接编辑。
- 关键唯一约束:来源 `(source_type,source_id,source_version)`;有效账单来源占用;单据号;核销幂等键;主体/账套/币种编码。
## 9. 产品待确认清单(本稿不替代确认表)
1. F0:财务主体与账套数量、往来单位/供应商归属、账期与关账、单据编号格式、来源入池和历史迁移。
2. F1:账单生成维度和周期、客户确认/争议、应收生成节点、收款登记/复核、到期日、预收/多收/手续费/退款处理。
3. F2:供应商成本确认节点、付款主体、应付审批和付款方式。
4. F3:账户资金性质、冻结/解冻/退款、发票税务主体和外部平台。
5. F4:收入/成本确认日、汇率来源和日期、公共成本分摊、期间快照及报表权限。
## 10. 实施前检查
- 产品确认表中 S1-S8、F0、F1 形成结论或“暂缓+临时口径”后,冻结本稿对应字段和状态。
- 冻结后补充 API/权限/错误码/事件契约,并将表名建议转为迁移脚本;不得先从页面或跨库查询反推模型。
- 迁移前只读核对历史已发布费用和线下结算事实,避免重复入账;任何冲突先停在预检,不直接改写历史。
- 本文件是设计文档,不代表已创建 `finance-service`、`baiyue_finance` 或任何数据库表。
@@ -0,0 +1,176 @@
# 财务领域模型与跨服务契约
> 版本:F0-V1.0(设计冻结稿)
> 日期:2026-08-10
> 状态:技术契约已确认;产品门禁项见第十节
> 适用:`finance-service` F0,以及 Firstleg → Finance 的费用来源接入
本文只定义财务 F0 的领域对象、来源快照和服务间契约,不创建服务、数据库或业务代码。范围和服务边界以[财务服务边界设计](财务服务边界设计.md)为准;财务全量产品项仍以[财务管理产品确认表](财务管理产品确认表.md)为准。
## 1. 设计原则与现状基线
- Finance 是独立 `finance-service`,使用独立 `baiyue_finance` 数据库;不得跨库 Join、共享 Entity 或让 Firstleg 直接写 Finance。
- Firstleg 是费用事实的所有者:负责测量、计费快照、版本、发布、调整和作废;Finance 只保存来源引用、不可变快照及财务状态。
- 当前 Firstleg 已有 `fl_inbound_fee_detail` 和 `fl_inbound_outbox`。费用发布会产生 `sourceType=FBA_PRICING`、`sourceFeeId/sourceFeeVersion`、`direction=RECEIVABLE`、币种和金额等字段;自动发布的 `settlementIncluded=true`。
- 当前 Outbox 已写入 `FIRSTLEG_FEE_SENT`、`FIRSTLEG_FEE_ADJUSTED`,负载仅含 `inboundId/feeId(/feeVersion)`,仓库中尚未形成 Finance 专用发布器或内部来源查询接口。它们是迁移基础,不是本契约的完整 v1 事件。
- Firstleg 现有业务错误遵循 HTTP 200 + body `code`(成功为字符串 `"200"`);认证、网关和基础设施拒绝可使用真实 401/403/503。内部接口沿用该业务响应封装,服务令牌失败使用 401/403。
## 2. F0 核心对象
Finance 只在自己的库保存以下对象;所有长整型 ID 在 JSON 中以字符串传输,金额以定点十进制字符串传输,币种使用 ISO 4217 三位大写码。
| 对象 | 必要字段/约束 | 生命周期与归属 |
|---|---|---|
| `FinancialEntity` 财务主体 | `entityId`、主体名称快照、主体类型、默认币种、状态、版本 | Finance 维护;主体、账套、客户隔离键分开建模。主体是否至少平台收款/付款两个方向为产品门禁 F0-02 |
| `CounterpartyRef` 往来单位引用 | `counterpartyType=CUSTOMER/SUPPLIER`、外部 ID、名称快照、来源服务、快照时间 | 不复制 IAM/Warehouse 实体;引用变更不改历史快照。供应商主数据来源及字段待 F0-03/F2-01 确认 |
| `CurrencyRef` 币种 | ISO 码、精度、启用状态 | F0 固定三位码和精度;汇率来源、生效时间、版本只预留,自动折算待 F0-05/F4-04 |
| `AccountingPeriod` 财务期间 | `periodId`、起止日期、状态 `OPEN/CLOSING/CLOSED`、关闭人/时间、版本 | Finance 唯一维护;CLOSED 禁止直接改账,只能补差/冲销 |
| `DocumentSequence` 单据号 | 业务类型(AR/AP/账单/收付款/核销/发票/钱包)、期间、序号、版本 | 服务端发号;同一幂等重试返回原单号,不由客户端生成 |
| `FeeSource` 财务费用来源 | `sourceType`、`sourceId=feeDetailId`、`sourceFeeId`、`sourceVersion`、来源单号、客户、方向、业务日、币种、金额、字段快照、`sourceHash`、入池状态 | Finance 的不可变接入记录;`RECEIVABLE` 已发布费用可进入来源池,`PAYABLE` 需供应商/主体/凭证等产品条件确认 |
| `FeeSourceLine` 来源明细 | 费用编码/名称快照、数量、单位、单价、金额、业务维度和来源明细 ID | 与 FeeSource 同版本冻结;不能用当前 Firstleg 表实时重算历史 |
| `SourceInbox` 事件接收记录 | `eventId`、事件类型、契约版本、幂等键、payloadHash、状态 `RECEIVED/PROCESSED/RETRY/CONFLICT/DLQ`、重试信息、错误码 | Finance 本地 Inbox;唯一键至少 `(eventId)` 和 `(sourceType,sourceId,sourceVersion)` |
| `FinanceAuditLog` 财务审计 | 事件/操作编码、主体/客户范围、操作者、请求/链路 ID、前后快照、结果、错误码、时间 | 追加写、不可物理删除;敏感字段脱敏 |
F1-F4 的账单、应收、付款、钱包、发票和利润对象引用 `FeeSource`,但不改变 F0 来源快照。一个有效来源版本只能进入一张未作废账单明细;出账后调整产生新来源或补差/冲销记录。
## 3. 来源入池与快照规则
### 3.1 入池门禁
Finance 仅接受满足以下技术条件的来源:
1. 事件声明来源版本处于 Firstleg 的有效发布版本,来源明细 `reviewStatus=APPROVED`、`publicationStatus=PUBLISHED`;
2. `settlementIncluded=true`,方向为 `RECEIVABLE`;来源未作废、未被有效账单使用;
3. 客户、入库单、费用明细、来源版本、币种、金额和业务日期齐全,金额非负;
4. `sourceHash` 可由事件内容与查询返回的规范化快照校验;校验失败不得入账;
5. Finance 当前期间允许入池。期间关闭不阻止接收事件,但应转为待处理并返回期间错误。
是否允许客户应付、供应商成本、预收、税额、汇率折算及历史费用迁移,不由本技术契约推断,分别受 F0/F2/F3/F4 门禁约束。
### 3.2 快照内容
Firstleg 内部查询至少返回:来源 ID/版本/单号、入库单 ID/编号、客户 ID/名称/发票抬头、方向、业务日、费用项目编码/名称/分类快照、渠道和目的地、包裹数、实重/体积/计费量/计费单位、单价、币种、金额、审核/发布时间、`settlementIncluded`、作废/替代关系及 `sourceHash`。可选的计费规则、价格表和税模式字段作为追溯扩展。
Finance 持久化查询返回的完整快照及接收时的事件元数据。历史账单只引用此快照;Firstleg 后续 `SUPERSEDED` 不会覆盖已出账明细。
## 4. Firstleg → Finance 事件契约 v1
### 4.1 Envelope
事件使用可靠本地 Outbox 发布,v1 消息主题固定为 `finance.firstleg.fee.v1`,事件体为 JSON:
```json
{
"eventId": "190000000000000001",
"eventType": "FIRSTLEG_FEE_PUBLISHED",
"schemaVersion": 1,
"occurredAt": "2026-08-10T08:00:00Z",
"producer": "firstleg-service",
"tenantId": "10001",
"customerId": "10001",
"sourceType": "INBOUND_FEE_DETAIL",
"sourceId": "200000000000000001",
"sourceFeeId": "300000000000000001",
"sourceVersion": 2,
"sourceFeeNo": "FBAF-778-2",
"inboundId": "778",
"feeDetailId": "200000000000000001",
"direction": "RECEIVABLE",
"businessDate": "2026-08-09",
"currency": "CNY",
"amount": "1280.00",
"sourceHash": "sha256:...",
"traceId": "trace-...",
"requestId": "req-..."
}
```
必填字段是上例中除 `traceId/requestId` 外的全部字段;金额必须是字符串,ID 必须是十进制字符串。事件类型固定为:
- `FIRSTLEG_FEE_PUBLISHED`:产生/重新发布一个有效来源版本;
- `FIRSTLEG_FEE_ADJUSTED`:替代旧发布版本,必须带 `supersedesSourceId/supersedesSourceVersion`;
- `FIRSTLEG_FEE_VOIDED`:作废来源,必须带作废原因和操作者信息。
事件只声明来源索引,不承载可被截断的完整账单字段;Finance 必须随后调用内部查询获取快照。Outbox 记录的 `id` 作为 `eventId`,事件入库与 Firstleg 费用状态变更必须在同一事务内完成。
### 4.2 当前事件兼容与迁移
现有 `FIRSTLEG_FEE_SENT` 仅允许作为迁移期别名:消费者可解析其 `feeId`,调用内部查询后映射为 `FIRSTLEG_FEE_PUBLISHED`,但不得把最小 payload 直接入财务来源。Firstleg 发布正式 v1 后停止新增 `FEE_SENT`;旧事件重放必须保留原 `eventId`,不得重新生成导致重复来源。`FIRSTLEG_FEE_ADJUSTED` 旧 payload 缺少完整替代关系时同样必须查询补齐,否则进入 `CONFLICT`/DLQ。
## 5. Finance → Firstleg 内部查询契约
调用方式:Finance 使用服务间令牌,禁止前端访问。v1 接口固定为:
```text
GET /internal/firstleg/v1/finance/fee-sources/{sourceType}/{sourceId}/versions/{sourceVersion}
Authorization: Bearer <service-token>
X-Request-Id: <id>
```
返回统一业务封装 `{code:"200", message, data:{...}}`;`data` 为 DTO,不返回 Entity。查询必须按 `tenantId` 服务端上下文隔离,且校验版本的客户、发布状态、作废/替代关系和 `sourceHash`。业务成功/失败使用 HTTP 200,由 body `code` 区分成功、来源不存在和版本/状态冲突;服务身份失败使用 HTTP 401/403,网关或基础设施暂时不可用可使用 HTTP 503。
Finance 不以当前查询结果覆盖已保存快照。查询超时、5xx 或网络错误可重试;明确的 404/状态冲突不可无限重试,应记录错误并进入人工处理。
## 6. 幂等、乱序、重试与死信
1. Inbox 先以 `eventId` 去重,再以 `(sourceType,sourceId,sourceVersion)` 防止不同事件重复入池。相同键且 payloadHash 相同,返回首次 `PROCESSED` 结果;相同键内容不同,标记 `CONFLICT`,错误码 `FIN_SOURCE_IDEMPOTENCY_CONFLICT`,不得覆盖。
2. `ADJUSTED` 可能先于 `PUBLISHED` 到达。Finance 先保存事件为 `RETRY`,查询完整快照成功后再按来源版本建立替代关系;不能按到达顺序直接入账。旧版本已出账时只生成待处理补差/冲销来源。
3. v1 默认采用 1m、5m、30m、2h、12h 的指数退避,最多 5 次且允许配置覆盖;每次保存 `retryCount/nextRetryAt/lastErrorCode`。超过上限进入 DLQ,支持受权限保护的重放;重放沿用原事件 ID 和幂等键。
4. 只有来源快照落库、校验通过、Inbox 标记 `PROCESSED` 后,才允许进入费用池;消费失败不得部分写入财务单据。并发消费使用行锁/版本号,避免双写。
## 7. 错误码与响应语义
错误码是跨服务稳定字符串,message 可本地化且不能作为程序判断依据:
| 错误码 | 语义 | 是否重试 |
|---|---|---|
| `FIN_SOURCE_NOT_FOUND` | 来源或指定版本不存在 | 否 |
| `FIN_SOURCE_NOT_PUBLISHED` | 来源未达到有效发布门禁 | 否 |
| `FIN_SOURCE_VOIDED` | 来源已作废 | 否 |
| `FIN_SOURCE_HASH_MISMATCH` | 事件与快照哈希不一致 | 否,人工处理 |
| `FIN_SOURCE_IDEMPOTENCY_CONFLICT` | 幂等键对应不同内容 | 否,人工处理 |
| `FIN_SOURCE_VERSION_CONFLICT` | 乱序/替代关系冲突 | 否或待人工 |
| `FIN_PERIOD_CLOSED` | 期间已关闭,禁止直接入账/改账 | 否 |
| `FIN_CURRENCY_MISMATCH` | 跨币种合并或核销 | 否 |
| `FIN_TENANT_SCOPE_DENIED` | 客户范围不匹配 | 否 |
| `FIN_SERVICE_UNAVAILABLE` | Firstleg/消息基础设施暂不可用 | 是 |
| `FIN_CONSUME_RETRY_EXHAUSTED` | 重试耗尽,已进入 DLQ | 仅授权重放 |
## 8. 权限、客户隔离与审计
Finance 服务端从登录态/服务令牌取得客户范围,绝不信任客户端传入的客户 ID。平台操作 `tenantId=0` 仅表示平台作用域,访问具体客户仍需目标客户权限;客户用户只能读写自身客户范围。跨客户、跨主体、跨币种和关闭期间操作必须硬拒绝。
v1 权限资源编码固定为(角色如何分配仍待 F0-10):`finance:source:view`、`finance:source:retry`、`finance:period:manage`、`finance:document:create`、`finance:document:review`、`finance:payment:create`、`finance:allocation:create`、`finance:allocation:reverse`、`finance:invoice:operate`、`finance:report:export`。反核销、付款、关账、红冲/作废和来源重放属于高风险权限,需独立授权,不得复用 Firstleg 费用编辑权限。
每次接收、重试、冲突、重放及后续财务写操作均追加审计:`eventId/requestId/traceId`、业务客户/主体、操作者及权限域、资源和版本、前后脱敏快照、结果/错误码、时间和服务名。审计写入失败不得回滚已完成的核心交易,但必须告警并可补偿;Token、凭证和敏感附件不得进入日志。
## 9. 版本与兼容规则
- 本契约当前为 `schemaVersion=1`。新增可选字段向后兼容;不得复用字段含义、改变枚举语义或把可选字段改为必填。
- 破坏性变更递增主版本并使用新主题/事件类型(例如 `...fee.v2`),Finance 至少保留 v1 消费窗口;不得静默重写历史事件。
- 事件和来源都以 `(sourceType,sourceId,sourceVersion)` 标识业务版本;`sourceHash` 是对内部查询完整快照计算的 SHA-256,计算时排除 `sourceHash` 本身及请求/链路元数据,JSON 字段按字典序、无空白、金额按约定精度、时间统一 UTC,结果前缀为 `sha256:`。同版本哈希不可变。
- Firstleg 新版本替代旧版本时,旧来源仍可查询;Finance 通过新事件的替代字段建立有向版本链。已出账、已核销或已开票记录只能新增补差/冲销/红冲,不更新原金额。
- 合同测试必须覆盖:事件字段完整性、版本链、重复/冲突、乱序、重试耗尽、查询 DTO 脱敏及权限范围。
## 10. 产品门禁(不得由本技术文档代定)
以下事项在[财务管理产品确认表](财务管理产品确认表.md)仍为“待确认”,本契约只预留扩展,不视为已决定:
- F0-02 财务主体/账套的实际数量和收付款方向;
- F0-03 往来单位字段和供应商主数据责任;
- F0-05/F4-04 汇率来源、折算日、本位币和汇兑损益;
- F0-08/F2-01 供应商成本、客户应付、PAYABLE 入池条件;
- F0-10 角色分配、制单/复核是否必须分离及金额分级审批;
- F1/F2 账期、账单确认、收付款方式、核销关系和反核销流程;
- F3 钱包资金性质、冻结/退款、税务主体和外部开票平台;
- F4 收入/成本确认时点、分摊算法、利润维度和导出范围。
在上述门禁未关闭前,只能实现可逆的契约校验、来源接收和审计骨架;不得以原型静态数据或当前人工 `PAYABLE` 明细推导完整应付、钱包、发票或利润业务。
## 11. 验收清单
- 发布、调整、作废均可追溯到不可变来源版本;Finance 接收后保存完整快照。
- 重复事件不重复生成来源,内容冲突可发现;乱序可重试,失败可进入 DLQ 并授权重放。
- 查询使用服务令牌和 DTO;跨客户/越权/跨币种/关闭期间操作被拒绝。
- ID、金额、币种、哈希和版本符合本契约;所有接收与写操作有审计记录。
- 事件/查询契约冻结后,才进入 F1/F2 财务单据实现;本轮不修改任何代码或其他文档。