diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 0000000..1f5d8a5 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,572 @@ +# API 参考文档 + +## 一、API 概览 + +RustJR 账户管理系统提供 RESTful API 接口,支持账户管理、交易处理、账务查询、对账管理和积分操作等功能。 + +## 二、基础信息 + +### 2.1 基础 URL + +| 环境 | URL | +|------|-----| +| 开发 | `http://localhost:8080/api/v1` | +| 测试 | `http://test-api.example.com/api/v1` | +| 生产 | `https://api.example.com/api/v1` | + +### 2.2 认证方式 + +目前系统内部使用,暂未启用认证。生产环境建议添加 JWT 认证: + +```http +Authorization: Bearer +``` + +### 2.3 请求/响应格式 + +**请求头**: +```http +Content-Type: application/json +Accept: application/json +``` + +**统一响应格式**: +```json +{ + "code": 200, + "message": "success", + "data": { ... } +} +``` + +**错误响应格式**: +```json +{ + "code": 400, + "message": "参数错误", + "error": "具体错误描述" +} +``` + +### 2.4 HTTP 状态码 + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功 | +| 201 | 创建成功 | +| 400 | 请求参数错误 | +| 404 | 资源不存在 | +| 409 | 资源冲突 | +| 500 | 服务器内部错误 | + +## 三、API 端点汇总 + +### 3.1 账户 API (11个端点) + +| 方法 | 端点 | 说明 | 前端对接 | +|------|------|------|----------| +| POST | /physical-accounts | 创建实体账户 | ✅ | +| GET | /physical-accounts | 获取实体账户列表 | ✅ | +| GET | /physical-accounts/:id | 获取实体账户详情 | ✅ | +| POST | /physical-accounts/:id/freeze | 冻结实体账户资金 | ✅ | +| POST | /physical-accounts/:id/unfreeze | 解冻实体账户资金 | ✅ | +| POST | /sub-accounts | 创建虚拟子账户 | ✅ | +| GET | /sub-accounts/:id | 获取子账户详情 | ✅ | +| GET | /sub-accounts/:id/balance | 获取子账户余额 | ✅ | +| POST | /sub-accounts/:id/freeze | 冻结子账户资金 | ✅ | +| POST | /sub-accounts/:id/unfreeze | 解冻子账户资金 | ✅ | +| POST | /sub-accounts/:id/close | 关闭子账户 | ✅ | + +### 3.2 交易 API (5个端点) + +| 方法 | 端点 | 说明 | 前端对接 | +|------|------|------|----------| +| POST | /transactions/transfer | 发起转账 | ⚠️ 部分 | +| POST | /transactions/deposit | 发起充值 | ❌ | +| POST | /transactions/withdraw | 发起提现 | ❌ | +| GET | /transactions/:id | 获取交易详情 | ✅ | +| GET | /transactions | 获取交易列表 | ✅ | + +### 3.3 账务 API (3个端点) + +| 方法 | 端点 | 说明 | 前端对接 | +|------|------|------|----------| +| GET | /ledger/subjects | 获取会计科目列表 | ❌ | +| GET | /ledger/entries/:id | 获取分录详情 | ❌ | +| GET | /ledger/accounts/:id/entries | 获取账户分录列表 | ❌ | + +### 3.4 对账 API (8个端点) + +| 方法 | 端点 | 说明 | 前端对接 | +|------|------|------|----------| +| POST | /reconciliation/run | 执行对账 | ⚠️ 部分 | +| GET | /reconciliation/batches/:id | 获取对账批次 | ✅ | +| GET | /reconciliation/batches/:id/items | 获取对账明细 | ✅ | +| GET | /reconciliation/three-account/:id | 三账校验 | ✅ | +| POST | /reconciliation/adjustments | 创建手工补录 | ✅ | +| POST | /reconciliation/adjustments/:id/approve | 审批补录 | ❌ | +| POST | /reconciliation/adjustments/:id/reject | 拒绝补录 | ❌ | +| GET | /reconciliation/adjustments/pending | 获取待审批补录 | ❌ | + +### 3.5 积分 API (5个端点) + +| 方法 | 端点 | 说明 | 前端对接 | +|------|------|------|----------| +| GET | /points/accounts/:sub_account_id | 获取积分账户 | ❌ | +| POST | /points/earn | 获取积分 | ❌ | +| POST | /points/spend | 消费积分 | ❌ | +| POST | /points/transfer | 转移积分 | ❌ | +| GET | /points/transactions | 查询积分交易 | ❌ | + +### 3.6 健康检查 + +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /health | 服务健康状态 | + +## 四、账户 API 详细文档 + +### 4.1 创建实体账户 + +**请求**: +```http +POST /api/v1/physical-accounts +Content-Type: application/json + +{ + "account_no": "6222021234567890123", + "account_name": "测试账户", + "bank_code": "ICBC", + "bank_name": "中国工商银行", + "consistency_mode": "eventual", + "outbound_control": "online_bank" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "account_no": "6222021234567890123", + "account_name": "测试账户", + "bank_code": "ICBC", + "bank_name": "中国工商银行", + "consistency_mode": "eventual", + "outbound_control": "online_bank", + "status": "active", + "personal_balance": "0.00", + "labor_balance": "0.00", + "frozen_balance": "0.00", + "bank_balance": "0.00", + "transit_amount": "0.00", + "available_balance": "0.00", + "version": 0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } +} +``` + +### 4.2 获取实体账户列表 + +**请求**: +```http +GET /api/v1/physical-accounts?page=1&page_size=10&status=active&keyword=测试 +``` + +**查询参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| page | int | 否 | 页码,默认1 | +| page_size | int | 否 | 每页数量,默认10 | +| status | string | 否 | 状态筛选 | +| keyword | string | 否 | 关键词搜索 | + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "data": [ + { + "id": 1, + "account_no": "6222021234567890123", + "account_name": "测试账户", + ... + } + ], + "total": 100, + "page": 1, + "page_size": 10 + } +} +``` + +### 4.3 冻结账户资金 + +**请求**: +```http +POST /api/v1/physical-accounts/1/freeze +Content-Type: application/json + +{ + "amount": "100.00" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": "账户资金已冻结" +} +``` + +### 4.4 解冻账户资金 + +**请求**: +```http +POST /api/v1/physical-accounts/1/unfreeze +Content-Type: application/json + +{ + "amount": "50.00" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": "账户资金已解冻" +} +``` + +## 五、交易 API 详细文档 + +### 5.1 发起转账 + +**请求**: +```http +POST /api/v1/transactions/transfer +Content-Type: application/json + +{ + "from_account_id": 1, + "to_account_id": 2, + "amount": "1000.00", + "remark": "转账测试" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "txn_no": "TXN20240101000001", + "txn_type": "transfer", + "from_account_id": 1, + "to_account_id": 2, + "amount": "1000.00", + "status": "pending", + "created_at": "2024-01-01T00:00:00Z" + } +} +``` + +### 5.2 获取交易详情 + +**请求**: +```http +GET /api/v1/transactions/1 +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "txn_no": "TXN20240101000001", + "txn_type": "transfer", + "from_account_id": 1, + "to_account_id": 2, + "amount": "1000.00", + "status": "success", + "bank_ref_no": "BANK123456", + "created_at": "2024-01-01T00:00:00Z", + "confirmed_at": "2024-01-01T00:01:00Z" + } +} +``` + +### 5.3 获取交易列表 + +**请求**: +```http +GET /api/v1/transactions?page=1&page_size=10&status=success&from_account_id=1 +``` + +**查询参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| page | int | 否 | 页码 | +| page_size | int | 否 | 每页数量 | +| status | string | 否 | 状态筛选 | +| from_account_id | int | 否 | 转出账户 | +| to_account_id | int | 否 | 转入账户 | +| start_date | date | 否 | 开始日期 | +| end_date | date | 否 | 结束日期 | + +## 六、对账 API 详细文档 + +### 6.1 执行对账 + +**请求**: +```http +POST /api/v1/reconciliation/run +Content-Type: application/json + +{ + "physical_account_id": 1, + "recon_date": "2024-01-01" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "batch_no": "RECON20240101001", + "physical_account_id": 1, + "recon_date": "2024-01-01", + "total_count": 100, + "matched_count": 98, + "mismatch_count": 2, + "status": "completed" + } +} +``` + +### 6.2 三账校验 + +**请求**: +```http +GET /api/v1/reconciliation/three-account/1 +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "physical_account_id": 1, + "bank_balance": "100000.00", + "transit_net": "5000.00", + "ledger_total": "105000.00", + "expected_total": "105000.00", + "difference": "0.00", + "is_balanced": true, + "verified_at": "2024-01-01T00:00:00Z" + } +} +``` + +### 6.3 创建手工补录 + +**请求**: +```http +POST /api/v1/reconciliation/adjustments +Content-Type: application/json + +{ + "related_txn_no": "TXN20240101000001", + "adjustment_type": "add", + "account_id": 1, + "amount": "100.00", + "reason": "银行入账补录" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "adjustment_no": "ADJ20240101001", + "status": "pending" + } +} +``` + +## 七、积分 API 详细文档 + +### 7.1 获取积分账户 + +**请求**: +```http +GET /api/v1/points/accounts/1 +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "sub_account_id": 1, + "points_type": "production", + "balance": "1000.00", + "total_earned": "5000.00", + "total_spent": "4000.00", + "total_expired": "0.00" + } + ] +} +``` + +### 7.2 获取积分 + +**请求**: +```http +POST /api/v1/points/earn +Content-Type: application/json + +{ + "points_account_id": 1, + "amount": "100.00", + "related_business_id": "WORK20240101001", + "remark": "生产任务完成奖励" +} +``` + +**响应**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "txn_no": "PTS20240101001", + "txn_type": "earn", + "amount": "100.00", + "balance_after": "1100.00" + } +} +``` + +### 7.3 消费积分 + +**请求**: +```http +POST /api/v1/points/spend +Content-Type: application/json + +{ + "points_account_id": 1, + "amount": "50.00", + "related_business_id": "ORDER20240101001", + "remark": "商品兑换" +} +``` + +### 7.4 转移积分 + +**请求**: +```http +POST /api/v1/points/transfer +Content-Type: application/json + +{ + "from_account_id": 1, + "to_account_id": 2, + "amount": "100.00", + "remark": "积分转赠" +} +``` + +## 八、错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 400001 | 参数验证失败 | +| 400002 | 余额不足 | +| 400003 | 账户状态异常 | +| 404001 | 账户不存在 | +| 404002 | 交易不存在 | +| 409001 | 资源已存在 | +| 500001 | 数据库错误 | +| 500002 | 银行接口错误 | + +## 九、数据类型说明 + +### 9.1 金额类型 + +- 使用字符串格式的十进制数 +- 保留2位小数 +- 示例:`"1000.00"`, `"0.50"` + +### 9.2 时间类型 + +- 使用 ISO 8601 格式 +- 时区为 UTC +- 示例:`"2024-01-01T00:00:00Z"` + +### 9.3 日期类型 + +- 使用 `YYYY-MM-DD` 格式 +- 示例:`"2024-01-01"` + +## 十、SDK 使用示例 + +### 10.1 JavaScript/TypeScript + +```typescript +import { AccountAPI, TransactionAPI } from '@/api'; + +// 创建账户 +const account = await AccountAPI.createPhysicalAccount({ + account_no: '6222021234567890123', + bank_code: 'ICBC', +}); + +// 发起转账 +const transaction = await TransactionAPI.createTransfer({ + from_account_id: 1, + to_account_id: 2, + amount: '1000.00', +}); +``` + +### 10.2 cURL + +```bash +# 创建账户 +curl -X POST http://localhost:8080/api/v1/physical-accounts \ + -H "Content-Type: application/json" \ + -d '{"account_no":"6222021234567890123","bank_code":"ICBC"}' + +# 获取账户列表 +curl http://localhost:8080/api/v1/physical-accounts?page=1&page_size=10 +``` + diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..d234cf7 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,258 @@ +# 系统架构文档 + +## 一、系统概述 + +RustJR 是一个基于 Rust 构建的银行账户管理平台,采用领域驱动设计(DDD)架构,支持实体账户、虚拟子账户、复式记账、对账补录、积分管理等核心功能。 + +## 二、技术栈 + +| 层次 | 技术 | 说明 | +|------|------|------| +| Web框架 | Axum | 高性能异步 Web 框架 | +| 数据库 | MySQL 8.0 | 关系型数据库 | +| ORM | SQLx | 编译时检查的 SQL 查询 | +| 序列化 | Serde | JSON/YAML 序列化 | +| 日志 | Tracing | 结构化日志 | +| 数值计算 | rust_decimal | 高精度十进制计算 | +| 时间处理 | Chrono | 日期时间处理 | +| UUID | uuid | 唯一标识符生成 | + +## 三、系统架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ API 层 │ +│ (HTTP REST API - axum) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │账户API │ │交易API │ │账务API │ │对账API │ │积分API │ │ +│ │11端点 │ │5端点 │ │3端点 │ │8端点 │ │5端点 │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ 应用层 │ +│ (Commands / Queries / DTOs) │ +├─────────────────────────────────────────────────────────────────┤ +│ 领域层 │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 账户域 │ │ 账务域 │ │ 交易域 │ │ 对账域 │ │ 积分域 │ │ +│ │Account │ │ Ledger │ │ Txn │ │ Recon │ │ Points │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ ┌─────────┐ │ +│ │ 补偿域 │ (后台服务) │ +│ │Compensa │ │ +│ └─────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ 基础设施层 │ +│ ┌─────────────────┐ ┌─────────────────────────────────────┐ │ +│ │ MySQL 持久化 │ │ 银行对接 (直连/第三方) │ │ +│ │ (SQLx) │ │ (MockBank / DirectConnect) │ │ +│ └─────────────────┘ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 四、项目目录结构 + +``` +rustjr/ +├── src/ +│ ├── main.rs # 应用入口 +│ ├── lib.rs # 库入口 +│ ├── config.rs # 配置管理 +│ ├── error.rs # 错误定义 +│ │ +│ ├── api/ # API 层 +│ │ ├── mod.rs # 路由注册 +│ │ ├── state.rs # 应用状态 +│ │ └── handlers/ # 请求处理器 +│ │ ├── account.rs # 账户处理器 +│ │ ├── transaction.rs # 交易处理器 +│ │ ├── ledger.rs # 账务处理器 +│ │ ├── reconciliation.rs # 对账处理器 +│ │ └── points.rs # 积分处理器 +│ │ +│ ├── application/ # 应用层 +│ │ ├── commands/ # 命令 +│ │ ├── queries/ # 查询 +│ │ └── dto/ # 数据传输对象 +│ │ +│ ├── domain/ # 领域层 +│ │ ├── account/ # 账户域 +│ │ │ ├── entity.rs # 实体 +│ │ │ ├── repository.rs # 仓储接口 +│ │ │ └── service.rs # 领域服务 +│ │ ├── ledger/ # 账务域 +│ │ ├── transaction/ # 交易域 +│ │ ├── reconciliation/ # 对账域 +│ │ ├── compensation/ # 补偿域 +│ │ └── points/ # 积分域 +│ │ +│ └── infrastructure/ # 基础设施层 +│ ├── persistence/ # 持久化 +│ │ └── mysql/ # MySQL 实现 +│ └── bank_integration/ # 银行对接 +│ ├── mock_bank.rs # 模拟银行 +│ ├── direct_connect.rs # 直连银行 +│ └── third_party.rs # 第三方支付 +│ +├── migrations/ # 数据库迁移 +│ ├── 001_init_schema.sql +│ └── 002_account_model_extension.sql +│ +├── tests/ # 测试 +│ ├── common/ # 测试公共代码 +│ ├── unit/ # 单元测试 +│ ├── integration/ # 集成测试 +│ └── scenarios/ # 场景测试 +│ +└── docs/ # 文档 + ├── architecture/ # 架构文档 + ├── domains/ # 领域文档 + ├── api/ # API 文档 + └── integration/ # 集成文档 +``` + +## 五、领域划分 + +系统采用领域驱动设计,划分为 6 个核心领域: + +| 领域 | 职责 | 核心实体 | +|------|------|----------| +| Account | 账户管理 | PhysicalAccount, VirtualSubAccount | +| Ledger | 账务管理 | AccountBalance, LedgerEntry | +| Transaction | 交易处理 | SystemTransaction, BankTransaction | +| Reconciliation | 对账管理 | ReconciliationBatch, ManualAdjustment | +| Compensation | 补偿处理 | CompensationTask | +| Points | 积分管理 | PointsAccount, PointsTransaction | + +## 六、核心设计理念 + +### 6.1 三科目余额模型 + +``` +personal_balance + labor_balance + frozen_balance = bank_balance + (个人) (劳动) (冻结) (银行) +``` + +**不变量约束**:三科目之和必须等于银行余额,确保资金一致性。 + +### 6.2 交易状态机 + +``` +Created → Pending → BankSubmitted → Success/Failed/Timeout → Reversed + ↑ ↓ + └─────────────────── Retry ───────────────┘ +``` + +### 6.3 三键幂等体系 + +| 键名 | 说明 | 用途 | +|------|------|------| +| JZTxId | 狱政交易号 (txn_no) | 系统内唯一标识 | +| BankTxId | 银行交易号 (bank_ref_no) | 银行回执关联 | +| SourceKey | 来源幂等键 | 外部入账去重 | + +### 6.4 三账对账闭环 + +``` +总账余额 = 银行账余额 + 在途净额 +``` + +通过三账校验确保资金安全: +- 银行账:实际银行余额 +- 在途账:已扣未确认的资金 +- 总账:系统记账余额 + +## 七、数据流 + +### 7.1 出金流程 + +```mermaid +sequenceDiagram + participant Client + participant API + participant TxnService + participant LedgerService + participant Bank + + Client->>API: 发起转账请求 + API->>TxnService: 创建交易 (Created) + TxnService->>LedgerService: 优先级扣款 (个人→劳动) + LedgerService->>LedgerService: 建立在途 + TxnService->>TxnService: 更新状态 (Pending) + TxnService->>Bank: 提交银行 + TxnService->>TxnService: 更新状态 (BankSubmitted) + Bank-->>TxnService: 银行回执 + alt 成功 + TxnService->>LedgerService: 结转在途 + TxnService->>TxnService: 更新状态 (Success) + else 失败 + TxnService->>LedgerService: 回退在途 + TxnService->>TxnService: 更新状态 (Failed) + end + API-->>Client: 返回结果 +``` + +### 7.2 对账流程 + +```mermaid +sequenceDiagram + participant Scheduler + participant ReconService + participant LedgerService + participant BankRepo + + Scheduler->>ReconService: 触发对账 + ReconService->>BankRepo: 获取银行流水 + ReconService->>ReconService: 匹配系统交易 + ReconService->>LedgerService: 三账校验 + alt 平衡 + ReconService->>ReconService: 标记完成 + else 不平衡 + ReconService->>ReconService: 生成差异报告 + ReconService->>ReconService: 需要人工审核 + end +``` + +## 八、部署架构 + +### 8.1 Docker 部署 + +```yaml +services: + mysql: + image: mysql:8.0 + ports: ["3306:3306"] + + backend: + build: ./rustjr + ports: ["8080:8080"] + depends_on: [mysql] + + frontend: + build: ./rustjr-vue-frontend + ports: ["3001:80"] + depends_on: [backend] +``` + +### 8.2 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| DATABASE_URL | 数据库连接串 | - | +| SERVER_HOST | 服务地址 | 0.0.0.0 | +| SERVER_PORT | 服务端口 | 8080 | +| RUST_LOG | 日志级别 | info | + +## 九、安全考虑 + +1. **资金安全**:三科目不变量约束,确保资金不会凭空消失或增加 +2. **幂等性**:三键体系确保交易不重复 +3. **并发控制**:乐观锁 + 版本号防止并发更新冲突 +4. **审计追踪**:所有操作记录完整日志 + +## 十、性能优化 + +1. **异步处理**:基于 Tokio 的异步运行时 +2. **连接池**:SQLx 数据库连接池 +3. **批量操作**:批量创建账户、批量对账 +4. **索引优化**:关键查询字段建立索引 + diff --git a/docs/domains/01-account.md b/docs/domains/01-account.md new file mode 100644 index 0000000..db52d5c --- /dev/null +++ b/docs/domains/01-account.md @@ -0,0 +1,355 @@ +# 账户域 (Account Domain) + +## 一、领域概述 + +账户域负责管理银行账户的完整生命周期,包括实体账户(对应真实银行账户)和虚拟子账户(在实体账户下创建的逻辑账户)。 + +## 二、核心实体 + +### 2.1 实体账户 (PhysicalAccount) + +实体账户对应真实的银行账户,是资金管理的基础单元。 + +```rust +pub struct PhysicalAccount { + pub id: i64, // 账户ID + pub account_no: String, // 银行账号 + pub bank_code: String, // 银行代码 + pub bank_name: Option, // 银行名称 + pub consistency_mode: ConsistencyMode, // 一致性模式 + pub outbound_control: OutboundControl, // 出金管控模式 + pub status: AccountStatus, // 账户状态 + pub created_at: DateTime, // 创建时间 + pub updated_at: DateTime, // 更新时间 +} +``` + +**核心方法**: +- `is_active()` - 检查账户是否可用 +- `can_outbound()` - 检查是否允许出金 + +### 2.2 虚拟子账户 (VirtualSubAccount) + +虚拟子账户是在实体账户下创建的逻辑账户,用于资金隔离和管理。 + +```rust +pub struct VirtualSubAccount { + pub id: i64, // 子账户ID + pub physical_account_id: i64, // 所属实体账户ID + pub account_code: String, // 子账户编号 + pub account_type: SubAccountType, // 子账户类型 + pub valid_from: Option>, // 有效期开始 + pub valid_to: Option>, // 有效期结束 + pub status: AccountStatus, // 账户状态 + pub created_at: DateTime, // 创建时间 + pub updated_at: DateTime, // 更新时间 +} +``` + +**核心方法**: +- `is_active()` - 检查账户是否可用(考虑有效期) +- `is_temporary()` - 是否为临时账户 +- `is_expired()` - 临时账户是否过期 + +### 2.3 账户控制 (AccountControl) + +账户控制配置,用于设置对账频率和银行对接配置。 + +```rust +pub struct AccountControl { + pub id: i64, + pub physical_account_id: i64, + pub reconciliation_interval: i32, // 对账频率(分钟) + pub direct_connect_config: Option, // 银企直连配置 + pub third_party_config: Option, // 第三方支付配置 +} +``` + +### 2.4 临时账户池 (SubAccountPool) + +管理临时子账户的池,支持批量创建和自动销户。 + +```rust +pub struct SubAccountPool { + pub id: i64, + pub physical_account_id: i64, + pub name: String, + pub valid_from: DateTime, + pub valid_to: DateTime, + pub auto_close_rule: Option, // 自动销户规则 +} +``` + +## 三、枚举类型 + +### 3.1 账户状态 (AccountStatus) + +```rust +pub enum AccountStatus { + Active, // 正常 + Frozen, // 冻结 + Closed, // 已关闭 +} +``` + +**状态转移规则**: +``` +Active ──► Frozen ──► Active + │ │ + └──────► Closed ◄────┘ +``` + +### 3.2 一致性模式 (ConsistencyMode) + +```rust +pub enum ConsistencyMode { + Strong, // 强一致性 - 交易需等待银行确认 + Eventual, // 最终一致性 - 先记账后对账 +} +``` + +| 模式 | 特点 | 适用场景 | +|------|------|----------| +| Strong | 实时确认,响应慢 | 大额转账 | +| Eventual | 异步确认,响应快 | 小额高频 | + +### 3.3 出金管控模式 (OutboundControl) + +```rust +pub enum OutboundControl { + ReceiveOnly, // 只收不付 + OnlineBank, // 网银控制 + Token, // 令牌控制 +} +``` + +| 模式 | 说明 | 使用场景 | +|------|------|----------| +| ReceiveOnly | 禁止出金 | 归集账户 | +| OnlineBank | 网银审批出金 | 常规账户 | +| Token | 令牌验证出金 | 高安全账户 | + +### 3.4 账户类型 (AccountType) + +```rust +pub enum AccountType { + Physical, // 实体账户 + Virtual, // 虚拟账户 +} +``` + +### 3.5 子账户类型 (SubAccountType) + +```rust +pub enum SubAccountType { + Settlement, // 结算子账户 + Management, // 管理子账户 + Temporary, // 临时子账户 +} +``` + +| 类型 | 说明 | 特点 | +|------|------|------| +| Settlement | 结算账户 | 长期使用,用于日常结算 | +| Management | 管理账户 | 内部管理,资金归集 | +| Temporary | 临时账户 | 有有效期,自动销户 | + +## 四、领域服务 + +### 4.1 AccountService + +账户域的核心服务,提供账户管理功能。 + +```rust +impl AccountService { + // 创建实体账户 + pub async fn create_physical_account(&self, req: CreatePhysicalAccountRequest) -> Result; + + // 获取实体账户 + pub async fn get_physical_account(&self, id: i64) -> Result; + + // 获取实体账户列表(分页) + pub async fn list_physical_accounts_paginated( + &self, + status: Option, + keyword: Option, + page: i32, + page_size: i32 + ) -> Result<(Vec, i64)>; + + // 冻结实体账户 + pub async fn freeze_physical_account(&self, id: i64) -> Result<()>; + + // 解冻实体账户 + pub async fn unfreeze_physical_account(&self, id: i64) -> Result<()>; + + // 创建虚拟子账户 + pub async fn create_sub_account(&self, req: CreateVirtualSubAccountRequest) -> Result; + + // 批量创建子账户 + pub async fn batch_create_sub_accounts(&self, req: BatchCreateSubAccountRequest) -> Result>; + + // 关闭子账户 + pub async fn close_sub_account(&self, id: i64) -> Result<()>; + + // 获取实体账户下的子账户列表 + pub async fn list_sub_accounts(&self, physical_account_id: i64) -> Result>; +} +``` + +## 五、仓储接口 + +### 5.1 PhysicalAccountRepository + +```rust +#[async_trait] +pub trait PhysicalAccountRepository: Send + Sync { + async fn create(&self, account: &PhysicalAccount) -> Result; + async fn find_by_id(&self, id: i64) -> Result>; + async fn find_by_account_no(&self, account_no: &str) -> Result>; + async fn find_all(&self) -> Result>; + async fn find_paginated(&self, status: Option, keyword: Option, offset: i64, limit: i64) -> Result>; + async fn count(&self, status: Option, keyword: Option) -> Result; + async fn update_status(&self, id: i64, status: AccountStatus) -> Result<()>; +} +``` + +### 5.2 VirtualSubAccountRepository + +```rust +#[async_trait] +pub trait VirtualSubAccountRepository: Send + Sync { + async fn create(&self, account: &VirtualSubAccount) -> Result; + async fn find_by_id(&self, id: i64) -> Result>; + async fn find_by_physical_account(&self, physical_account_id: i64) -> Result>; + async fn find_expired_temporary(&self) -> Result>; + async fn update_status(&self, id: i64, status: AccountStatus) -> Result<()>; + async fn batch_create(&self, accounts: &[VirtualSubAccount]) -> Result>; +} +``` + +## 六、业务规则 + +### 6.1 账户创建规则 + +1. 银行账号必须唯一 +2. 实体账户必须关联有效的银行代码 +3. 子账户编号在同一实体账户下必须唯一 +4. 临时账户必须设置有效期 + +### 6.2 账户状态规则 + +1. 只有 `Active` 状态的账户可以进行交易 +2. `Frozen` 状态只影响出金,入金正常 +3. `Closed` 状态不可进行任何交易 +4. 账户关闭前必须余额为零 + +### 6.3 出金管控规则 + +1. `ReceiveOnly` 模式禁止任何出金操作 +2. `OnlineBank` 模式需要通过网银审批 +3. `Token` 模式需要提供有效令牌 + +## 七、API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/v1/physical-accounts | 创建实体账户 | +| GET | /api/v1/physical-accounts | 获取实体账户列表 | +| GET | /api/v1/physical-accounts/:id | 获取实体账户详情 | +| POST | /api/v1/physical-accounts/:id/freeze | 冻结实体账户资金 | +| POST | /api/v1/physical-accounts/:id/unfreeze | 解冻实体账户资金 | +| POST | /api/v1/sub-accounts | 创建虚拟子账户 | +| GET | /api/v1/sub-accounts/:id | 获取子账户详情 | +| GET | /api/v1/sub-accounts/:id/balance | 获取子账户余额 | +| POST | /api/v1/sub-accounts/:id/freeze | 冻结子账户资金 | +| POST | /api/v1/sub-accounts/:id/unfreeze | 解冻子账户资金 | +| POST | /api/v1/sub-accounts/:id/close | 关闭子账户 | +| GET | /api/v1/physical-accounts/:id/sub-accounts | 获取实体账户下的子账户列表 | + +## 八、数据库表结构 + +### 8.1 physical_account 表 + +```sql +CREATE TABLE physical_account ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + account_no VARCHAR(64) NOT NULL UNIQUE, + account_name VARCHAR(128), + bank_code VARCHAR(32) NOT NULL, + bank_name VARCHAR(128), + consistency_mode VARCHAR(32) DEFAULT 'eventual', + outbound_control VARCHAR(32) DEFAULT 'online_bank', + status VARCHAR(32) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_bank_code (bank_code), + INDEX idx_status (status) +); +``` + +### 8.2 virtual_sub_account 表 + +```sql +CREATE TABLE virtual_sub_account ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + physical_account_id BIGINT NOT NULL, + account_code VARCHAR(64) NOT NULL, + account_type VARCHAR(32) DEFAULT 'settlement', + valid_from TIMESTAMP NULL, + valid_to TIMESTAMP NULL, + status VARCHAR(32) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (physical_account_id) REFERENCES physical_account(id), + UNIQUE INDEX idx_account_code (physical_account_id, account_code), + INDEX idx_status (status), + INDEX idx_valid_to (valid_to) +); +``` + +## 九、使用示例 + +### 9.1 创建实体账户 + +```rust +let req = CreatePhysicalAccountRequest { + account_no: "6222021234567890123".to_string(), + bank_code: "ICBC".to_string(), + bank_name: Some("中国工商银行".to_string()), + consistency_mode: Some(ConsistencyMode::Eventual), + outbound_control: Some(OutboundControl::OnlineBank), +}; + +let account = account_service.create_physical_account(req).await?; +``` + +### 9.2 创建子账户 + +```rust +let req = CreateVirtualSubAccountRequest { + physical_account_id: 1, + account_code: "SUB001".to_string(), + account_type: SubAccountType::Settlement, + valid_from: None, + valid_to: None, +}; + +let sub_account = account_service.create_sub_account(req).await?; +``` + +### 9.3 批量创建临时账户 + +```rust +let req = BatchCreateSubAccountRequest { + physical_account_id: 1, + account_type: SubAccountType::Temporary, + prefix: "TEMP".to_string(), + count: 100, + valid_from: Some(Utc::now()), + valid_to: Some(Utc::now() + Duration::days(30)), +}; + +let accounts = account_service.batch_create_sub_accounts(req).await?; +``` + diff --git a/docs/domains/02-ledger.md b/docs/domains/02-ledger.md new file mode 100644 index 0000000..967dd0e --- /dev/null +++ b/docs/domains/02-ledger.md @@ -0,0 +1,488 @@ +# 账务域 (Ledger Domain) + +## 一、领域概述 + +账务域是系统的核心财务引擎,负责余额管理、复式记账、三科目约束校验。该域实现了银行级别的资金安全保障机制。 + +## 二、核心概念:三科目余额模型 + +### 2.1 模型定义 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 银行余额 (bank_balance) │ +│ = 1000.00 │ +├─────────────────────────────────────────────────────────┤ +│ 个人余额 │ 劳动报酬 │ 冻结余额 │ +│ personal │ labor │ frozen │ +│ = 500.00 │ = 300.00 │ = 200.00 │ +│ (可用) │ (可用) │ (不可用) │ +└─────────────────────────────────────────────────────────┘ + +不变量约束: personal + labor + frozen = bank_balance +``` + +### 2.2 余额类型说明 + +| 余额类型 | 字段 | 说明 | 可用性 | +|----------|------|------|--------| +| 个人余额 | personal_balance | 个人可支配资金 | 可用 | +| 劳动报酬 | labor_balance | 劳动所得报酬 | 可用 | +| 冻结余额 | frozen_balance | 被冻结的资金 | 不可用 | +| 银行余额 | bank_balance | 银行账面余额 | 对照 | +| 在途金额 | transit_amount | 已扣未确认 | 过渡 | + +### 2.3 可用余额计算 + +```rust +// 总可用余额 = 个人 + 劳动 +fn total_available(&self) -> Decimal { + self.personal_balance + self.labor_balance +} + +// 可支配余额 = 总可用 - 在途 +fn calculate_available(&self) -> Decimal { + self.total_available() - self.transit_amount +} +``` + +## 三、核心实体 + +### 3.1 账户余额 (AccountBalance) + +```rust +pub struct AccountBalance { + pub id: i64, + pub account_id: i64, + pub account_type: AccountType, + + // 三科目余额 + pub personal_balance: Decimal, // 个人余额 + pub labor_balance: Decimal, // 劳动报酬 + pub frozen_balance: Decimal, // 冻结余额 + + // 银行对照 + pub bank_balance: Decimal, // 银行余额 + + // 在途管理 + pub transit_amount: Decimal, // 在途金额 + + // 版本控制 + pub version: i32, // 乐观锁版本 + pub updated_at: DateTime, +} +``` + +### 3.2 会计科目 (AccountingSubject) + +```rust +pub struct AccountingSubject { + pub code: String, // 科目代码 + pub name: String, // 科目名称 + pub category: SubjectCategory, // 科目类别 + pub direction_default: i8, // 默认增加方向 + pub parent_code: Option,// 父科目代码 + pub level: i32, // 科目级别 +} +``` + +**预定义科目**: + +| 代码 | 名称 | 类别 | +|------|------|------| +| 1002 | 银行存款 | 资产 | +| 1003 | 在途资金 | 资产 | +| 2001 | 客户存款 | 负债 | +| 2002 | 待清算款项 | 负债 | +| 3001 | 手续费收入 | 收入 | +| 4001 | 利息支出 | 支出 | + +### 3.3 记账分录 (LedgerEntry) + +```rust +pub struct LedgerEntry { + pub id: i64, + pub entry_no: String, // 分录编号 + pub txn_no: String, // 关联交易号 + pub post_date: NaiveDate, // 记账日期 + pub post_time: DateTime, // 记账时间 + pub description: Option,// 摘要描述 + pub status: EntryStatus, // 状态 + pub created_at: DateTime, +} +``` + +### 3.4 分录明细 (LedgerLine) + +```rust +pub struct LedgerLine { + pub id: i64, + pub entry_id: i64, + pub account_id: i64, + pub account_type: AccountType, + pub subject_code: String, // 科目代码 + pub direction: Direction, // 借贷方向 + pub amount: Decimal, // 金额 +} +``` + +## 四、枚举类型 + +### 4.1 借贷方向 (Direction) + +```rust +pub enum Direction { + Debit, // 借方 + Credit, // 贷方 +} +``` + +### 4.2 科目类别 (SubjectCategory) + +```rust +pub enum SubjectCategory { + Asset, // 资产类 - 借方增加 + Liability, // 负债类 - 贷方增加 + Income, // 收入类 - 贷方增加 + Expense, // 支出类 - 借方增加 +} +``` + +### 4.3 分录状态 (EntryStatus) + +```rust +pub enum EntryStatus { + Pending, // 待确认 + Posted, // 已过账 + Reversed, // 已冲销 +} +``` + +## 五、核心业务逻辑 + +### 5.1 优先级扣款 + +出金时按优先级从余额中扣减:**先个人,后劳动**。 + +```rust +pub fn deduct_with_priority(&mut self, amount: Decimal) -> Result { + let available = self.available_balance(); + if available < amount { + return Err(AppError::InsufficientBalance { available, required: amount }); + } + + let mut remaining = amount; + + // 1. 先扣个人余额 + let from_personal = remaining.min(self.personal_balance); + self.personal_balance -= from_personal; + remaining -= from_personal; + + // 2. 再扣劳动报酬 + let from_labor = remaining.min(self.labor_balance); + self.labor_balance -= from_labor; + + // 3. 同步银行余额 + self.bank_balance -= amount; + + Ok(DeductionResult { from_personal, from_labor, total: amount }) +} +``` + +**扣款示例**: + +| 场景 | 扣款金额 | 个人余额 | 劳动余额 | 扣款来源 | +|------|----------|----------|----------|----------| +| 1 | 300 | 500 | 200 | 个人300 | +| 2 | 600 | 500 | 200 | 个人500 + 劳动100 | +| 3 | 800 | 500 | 200 | 失败(余额不足) | + +### 5.2 冻结/解冻 + +冻结操作将可用余额转移到冻结余额,按优先级从个人和劳动中扣减。 + +```rust +// 冻结 +pub fn freeze(&mut self, amount: Decimal) { + let mut remaining = amount; + + // 先从个人扣 + let from_personal = remaining.min(self.personal_balance); + self.personal_balance -= from_personal; + remaining -= from_personal; + + // 再从劳动扣 + if remaining > Decimal::ZERO { + let from_labor = remaining.min(self.labor_balance); + self.labor_balance -= from_labor; + } + + // 增加冻结余额 + self.frozen_balance += amount; +} + +// 解冻(默认返回到个人余额) +pub fn unfreeze(&mut self, amount: Decimal) { + let unfreeze_amount = amount.min(self.frozen_balance); + self.frozen_balance -= unfreeze_amount; + self.personal_balance += unfreeze_amount; +} +``` + +### 5.3 在途管理 + +在途金额表示已从可用余额扣减但尚未得到银行确认的资金。 + +```rust +// 建立在途(出金时) +pub fn add_transit(&mut self, amount: Decimal) { + self.transit_amount += amount; +} + +// 结转在途(银行确认成功) +pub fn settle_transit(&mut self, amount: Decimal) -> Result<()> { + if self.transit_amount < amount { + return Err(AppError::BusinessRule("在途金额不足".into())); + } + self.transit_amount -= amount; + Ok(()) +} + +// 回退在途(银行失败) +pub fn rollback_transit(&mut self, amount: Decimal) { + let rollback = amount.min(self.transit_amount); + self.transit_amount -= rollback; + self.personal_balance += rollback; // 返回到个人余额 + self.bank_balance += rollback; // 恢复银行余额 +} +``` + +### 5.4 不变量校验 + +确保三科目之和等于银行余额。 + +```rust +pub fn validate_invariant(&self) -> Result<()> { + let sum = self.personal_balance + self.labor_balance + self.frozen_balance; + if sum == self.bank_balance { + Ok(()) + } else { + Err(AppError::InvariantViolation { + account_id: self.account_id, + expected: self.bank_balance, + actual: sum, + }) + } +} +``` + +## 六、领域服务 + +### 6.1 LedgerService + +```rust +impl LedgerService { + // ========== 余额操作 ========== + + // 获取账户余额 + pub async fn get_balance(&self, account_id: i64, account_type: AccountType) -> Result; + + // 冻结金额 + pub async fn freeze_amount(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result<()>; + + // 解冻金额 + pub async fn unfreeze_amount(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result<()>; + + // 优先级扣款 + pub async fn deduct_with_priority(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result; + + // 建立在途 + pub async fn add_transit(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result<()>; + + // 结转在途 + pub async fn settle_transit(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result<()>; + + // 回退在途 + pub async fn rollback_transit(&self, account_id: i64, account_type: AccountType, amount: Decimal) -> Result<()>; + + // ========== 记账操作 ========== + + // 创建分录 + pub async fn create_entry(&self, req: CreateEntryRequest) -> Result; + + // 获取账户分录 + pub async fn get_account_entries(&self, account_id: i64, account_type: AccountType) -> Result>; + + // ========== 科目操作 ========== + + // 获取所有科目 + pub async fn list_subjects(&self) -> Result>; + + // 初始化预定义科目 + pub async fn initialize_subjects(&self) -> Result<()>; + + // ========== 校验操作 ========== + + // 校验不变量 + pub async fn validate_invariant(&self, account_id: i64, account_type: AccountType) -> Result<()>; +} +``` + +## 七、复式记账 + +### 7.1 记账原则 + +1. **借贷必相等**:每笔分录的借方金额总和必须等于贷方金额总和 +2. **有借必有贷**:每笔分录至少包含一个借方和一个贷方 +3. **科目对应**:资产/费用借增贷减,负债/收入贷增借减 + +### 7.2 记账示例 + +**存款入账**: +``` +借: 银行存款 1002 1000.00 +贷: 客户存款 2001 1000.00 +``` + +**转账出金**: +``` +借: 客户存款 2001 500.00 +贷: 银行存款 1002 500.00 +``` + +**收取手续费**: +``` +借: 客户存款 2001 10.00 +贷: 手续费收入 3001 10.00 +``` + +### 7.3 分录验证 + +```rust +impl CreateEntryRequest { + pub fn validate_balance(&self) -> Result<(), (Decimal, Decimal)> { + let mut total_debit = Decimal::ZERO; + let mut total_credit = Decimal::ZERO; + + for line in &self.lines { + match line.direction { + Direction::Debit => total_debit += line.amount, + Direction::Credit => total_credit += line.amount, + } + } + + if total_debit == total_credit { + Ok(()) + } else { + Err((total_debit, total_credit)) + } + } +} +``` + +## 八、API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/v1/ledger/subjects | 获取会计科目列表 | +| GET | /api/v1/ledger/entries/:id | 获取分录详情 | +| GET | /api/v1/ledger/accounts/:id/entries | 获取账户分录列表 | + +## 九、数据库表结构 + +### 9.1 account_balance 表 + +```sql +CREATE TABLE account_balance ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + account_id BIGINT NOT NULL, + account_type VARCHAR(32) NOT NULL, + + -- 三科目余额 + personal_balance DECIMAL(20, 2) DEFAULT 0.00, + labor_balance DECIMAL(20, 2) DEFAULT 0.00, + frozen_balance DECIMAL(20, 2) DEFAULT 0.00, + + -- 银行对照 + bank_balance DECIMAL(20, 2) DEFAULT 0.00, + + -- 在途管理 + transit_amount DECIMAL(20, 2) DEFAULT 0.00, + + -- 兼容字段 + system_balance DECIMAL(20, 2) DEFAULT 0.00, + available_balance DECIMAL(20, 2) DEFAULT 0.00, + frozen_amount DECIMAL(20, 2) DEFAULT 0.00, + + version INT DEFAULT 0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + UNIQUE INDEX idx_account (account_id, account_type) +); +``` + +### 9.2 ledger_entry 表 + +```sql +CREATE TABLE ledger_entry ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + entry_no VARCHAR(64) NOT NULL UNIQUE, + txn_no VARCHAR(64) NOT NULL, + post_date DATE NOT NULL, + post_time TIMESTAMP NOT NULL, + description TEXT, + status VARCHAR(32) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_txn_no (txn_no), + INDEX idx_post_date (post_date) +); +``` + +### 9.3 ledger_line 表 + +```sql +CREATE TABLE ledger_line ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + entry_id BIGINT NOT NULL, + account_id BIGINT NOT NULL, + account_type VARCHAR(32) NOT NULL, + subject_code VARCHAR(32) NOT NULL, + direction VARCHAR(16) NOT NULL, + amount DECIMAL(20, 2) NOT NULL, + + FOREIGN KEY (entry_id) REFERENCES ledger_entry(id), + INDEX idx_account (account_id, account_type), + INDEX idx_subject (subject_code) +); +``` + +## 十、三账校验 + +### 10.1 校验公式 + +``` +总账余额 = 银行账余额 + 在途净额 + +即: ledger_total = bank_balance + transit_net +``` + +### 10.2 校验结果 + +```rust +pub struct ThreeAccountResult { + pub bank_balance: Decimal, // 银行账余额 + pub transit_net: Decimal, // 在途净额 + pub ledger_total: Decimal, // 总账余额 + pub is_balanced: bool, // 是否平衡 + pub difference: Decimal, // 差异金额 +} +``` + +### 10.3 不平衡处理 + +1. **记录差异**:生成差异报告 +2. **人工审核**:提交人工对账 +3. **自动调整**:小额差异自动调平 +4. **告警通知**:大额差异触发告警 + diff --git a/docs/domains/03-transaction.md b/docs/domains/03-transaction.md new file mode 100644 index 0000000..32205fa --- /dev/null +++ b/docs/domains/03-transaction.md @@ -0,0 +1,481 @@ +# 交易域 (Transaction Domain) + +## 一、领域概述 + +交易域负责处理系统内的所有资金流转,包括转账、充值、提现等操作。该域实现了完整的交易状态机和三键幂等体系,确保交易的安全性和一致性。 + +## 二、核心概念 + +### 2.1 三键幂等体系 + +| 键名 | 字段 | 说明 | 唯一性 | +|------|------|------|--------| +| JZTxId | txn_no | 狱政交易号 | 系统全局唯一 | +| BankTxId | bank_ref_no | 银行交易号 | 银行返回,用于对账 | +| SourceKey | source_key | 来源幂等键 | 外部入账去重 | + +**SourceKey 格式**: +``` +{银行流水号}_{金额}_{记账日}_{对方户名归一化} +``` + +### 2.2 交易状态机 + +``` + ┌──────────────────────────────────────┐ + │ │ + ▼ │ +┌─────────┐ ┌─────────┐ ┌──────────────┐ ┌───────┴───┐ +│ Created │───►│ Pending │───►│ BankSubmitted│───►│ Success │ +└─────────┘ └─────────┘ └──────────────┘ └───────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌──────────┐ + │ Failed │ │ Timeout │──────►│ Reversed │ + └─────────┘ └─────────┘ └──────────┘ +``` + +**状态说明**: + +| 状态 | 说明 | 后续操作 | +|------|------|----------| +| Created | 已创建(初始状态) | 继续处理或取消 | +| Pending | 待处理(已建立在途) | 提交银行 | +| BankSubmitted | 已提交银行 | 等待回执 | +| Success | 成功(银行确认) | 结转在途 | +| Failed | 失败(银行拒绝) | 回退在途 | +| Timeout | 超时(无回执) | 等待对账或重试 | +| Reversed | 已冲正 | 终态 | + +## 三、核心实体 + +### 3.1 系统交易 (SystemTransaction) + +```rust +pub struct SystemTransaction { + pub id: i64, + pub txn_no: String, // 狱政交易号 (JZTxId) + pub txn_type: TransactionType, // 交易类型 + pub from_account_id: Option, // 转出账户ID + pub to_account_id: Option, // 转入账户ID + pub amount: Decimal, // 金额 + pub status: TransactionStatus, // 状态 + pub bank_ref_no: Option, // 银行交易号 (BankTxId) + pub source_key: Option, // 来源幂等键 (SourceKey) + pub remark: Option, // 备注 + pub created_at: DateTime, // 创建时间 + pub confirmed_at: Option>, // 确认时间 + pub submitted_at: Option>, // 提交银行时间 + pub version: i32, // 乐观锁版本 +} +``` + +**核心方法**: + +```rust +impl SystemTransaction { + // 检查是否可以提交到银行 + pub fn can_submit(&self) -> bool { + matches!(self.status, TransactionStatus::Pending | TransactionStatus::Created) + } + + // 检查是否需要对账 + pub fn needs_reconciliation(&self) -> bool { + matches!(self.status, + TransactionStatus::BankSubmitted | + TransactionStatus::Timeout | + TransactionStatus::Processing + ) + } + + // 检查是否为终态 + pub fn is_terminal(&self) -> bool { + self.status.is_terminal() + } + + // 检查是否超时 + pub fn is_timeout(&self, timeout_seconds: i64) -> bool { + if self.status != TransactionStatus::BankSubmitted { + return false; + } + if let Some(submitted_at) = self.submitted_at { + let elapsed = Utc::now().signed_duration_since(submitted_at); + return elapsed.num_seconds() > timeout_seconds; + } + false + } + + // 尝试状态转移 + pub fn try_transition(&mut self, target: TransactionStatus) -> Result<(), String> { + if self.status.can_transition_to(target) { + self.status = target; + self.version += 1; + Ok(()) + } else { + Err(format!("无效的状态转移: {:?} -> {:?}", self.status, target)) + } + } +} +``` + +### 3.2 银行交易 (BankTransaction) + +从银行同步的交易流水。 + +```rust +pub struct BankTransaction { + pub id: i64, + pub bank_ref_no: String, // 银行参考号 + pub physical_account_id: i64, // 实体账户ID + pub txn_type: String, // 交易类型 + pub direction: TransactionDirection, // 交易方向 + pub amount: Decimal, // 金额 + pub counterparty_name: Option,// 对手方名称 + pub counterparty_account: Option,// 对手方账号 + pub txn_time: DateTime, // 交易时间 + pub sync_time: DateTime, // 同步时间 + pub match_status: MatchStatus, // 匹配状态 + pub matched_txn_no: Option, // 匹配的系统交易号 + pub remark: Option, // 摘要 +} +``` + +## 四、枚举类型 + +### 4.1 交易状态 (TransactionStatus) + +```rust +pub enum TransactionStatus { + Created, // 已创建 + Pending, // 待处理 + BankSubmitted, // 已提交银行 + Success, // 成功 + Failed, // 失败 + Timeout, // 超时 + Reversed, // 已冲正 + // 兼容旧状态 + Processing, // 处理中 → BankSubmitted + Confirmed, // 已确认 → Success + Mismatch, // 不匹配 +} +``` + +**状态转移规则**: + +```rust +impl TransactionStatus { + pub fn can_transition_to(&self, target: Self) -> bool { + match (self, target) { + // Created -> Pending + (Self::Created, Self::Pending) => true, + // Pending -> BankSubmitted | Failed + (Self::Pending, Self::BankSubmitted) => true, + (Self::Pending, Self::Failed) => true, + // BankSubmitted -> Success | Failed | Timeout + (Self::BankSubmitted, Self::Success) => true, + (Self::BankSubmitted, Self::Failed) => true, + (Self::BankSubmitted, Self::Timeout) => true, + // Timeout -> Success | Failed (对账确认) + (Self::Timeout, Self::Success) => true, + (Self::Timeout, Self::Failed) => true, + // Success -> Reversed (冲正) + (Self::Success, Self::Reversed) => true, + _ => false, + } + } +} +``` + +### 4.2 交易类型 (TransactionType) + +```rust +pub enum TransactionType { + Transfer, // 转账 + Deposit, // 充值 + Withdrawal, // 提现 + Fee, // 手续费 + Interest, // 利息 + Adjustment, // 调整 + Other(String), // 其他 +} +``` + +### 4.3 交易方向 (TransactionDirection) + +```rust +pub enum TransactionDirection { + Inbound, // 入账 + Outbound, // 出账 +} +``` + +### 4.4 匹配状态 (MatchStatus) + +```rust +pub enum MatchStatus { + Unmatched, // 未匹配 + Matched, // 已匹配 + Mismatch, // 不匹配 +} +``` + +## 五、交易处理流程 + +### 5.1 出金流程 + +```mermaid +sequenceDiagram + participant Client + participant TxnService + participant LedgerService + participant BankClient + participant CompService + + Client->>TxnService: 1. 发起转账 + TxnService->>TxnService: 2. 创建交易 (Created) + TxnService->>LedgerService: 3. 优先级扣款 + LedgerService-->>TxnService: 扣款结果 + TxnService->>LedgerService: 4. 建立在途 + TxnService->>TxnService: 5. 更新状态 (Pending) + TxnService->>BankClient: 6. 提交银行 + TxnService->>TxnService: 7. 更新状态 (BankSubmitted) + + alt 银行成功 + BankClient-->>TxnService: 成功回执 + TxnService->>LedgerService: 8a. 结转在途 + TxnService->>TxnService: 9a. 更新状态 (Success) + else 银行失败 + BankClient-->>TxnService: 失败回执 + TxnService->>LedgerService: 8b. 回退在途 + TxnService->>TxnService: 9b. 更新状态 (Failed) + else 超时 + Note over TxnService: 等待超时 + TxnService->>CompService: 8c. 创建补偿任务 + TxnService->>TxnService: 9c. 更新状态 (Timeout) + end + + TxnService-->>Client: 返回结果 +``` + +### 5.2 入金流程 + +```mermaid +sequenceDiagram + participant Bank + participant TxnService + participant LedgerService + + Bank->>TxnService: 1. 银行入账通知 + TxnService->>TxnService: 2. 检查 SourceKey 幂等 + + alt 重复入账 + TxnService-->>Bank: 返回已处理 + else 新入账 + TxnService->>TxnService: 3. 创建交易 (Success) + TxnService->>LedgerService: 4. 增加个人余额 + TxnService->>LedgerService: 5. 同步银行余额 + TxnService-->>Bank: 处理成功 + end +``` + +### 5.3 冲正流程 + +```mermaid +sequenceDiagram + participant Operator + participant TxnService + participant LedgerService + + Operator->>TxnService: 1. 发起冲正 + TxnService->>TxnService: 2. 检查是否可冲正 + + alt 可以冲正 + TxnService->>LedgerService: 3. 反向记账 + TxnService->>TxnService: 4. 更新状态 (Reversed) + TxnService-->>Operator: 冲正成功 + else 不可冲正 + TxnService-->>Operator: 冲正失败 + end +``` + +## 六、领域服务 + +### 6.1 TransactionService + +```rust +impl TransactionService { + // ========== 交易创建 ========== + + // 创建转账交易 + pub async fn transfer(&self, req: TransferRequest) -> Result; + + // 创建充值交易 + pub async fn deposit(&self, req: DepositRequest) -> Result; + + // 创建提现交易 + pub async fn withdraw(&self, req: WithdrawRequest) -> Result; + + // ========== 交易查询 ========== + + // 获取交易详情 + pub async fn get_transaction(&self, id: i64) -> Result; + + // 根据交易号查询 + pub async fn get_by_txn_no(&self, txn_no: &str) -> Result; + + // 查询交易列表 + pub async fn list_transactions(&self, query: TransactionQuery) -> Result>; + + // ========== 状态管理 ========== + + // 提交到银行 + pub async fn submit_to_bank(&self, id: i64) -> Result; + + // 确认交易 + pub async fn confirm_transaction(&self, id: i64, bank_ref_no: &str) -> Result<()>; + + // 标记失败 + pub async fn mark_failed(&self, id: i64, reason: &str) -> Result<()>; + + // 标记超时 + pub async fn mark_timeout(&self, id: i64) -> Result<()>; + + // 冲正交易 + pub async fn reverse_transaction(&self, id: i64) -> Result<()>; + + // ========== 幂等检查 ========== + + // 检查 SourceKey 是否存在 + pub async fn check_source_key(&self, source_key: &str) -> Result>; +} +``` + +## 七、仓储接口 + +### 7.1 SystemTransactionRepository + +```rust +#[async_trait] +pub trait SystemTransactionRepository: Send + Sync { + async fn create(&self, req: &CreateSystemTransactionRequest) -> Result; + async fn find_by_id(&self, id: i64) -> Result>; + async fn find_by_txn_no(&self, txn_no: &str) -> Result>; + async fn find_by_bank_ref_no(&self, bank_ref_no: &str) -> Result>; + async fn find_by_source_key(&self, source_key: &str) -> Result>; + async fn find_by_status(&self, status: TransactionStatus) -> Result>; + async fn find_pending(&self) -> Result>; + async fn find_needs_reconciliation(&self) -> Result>; + async fn query(&self, query: &TransactionQuery) -> Result>; + async fn update_status(&self, id: i64, status: TransactionStatus) -> Result<()>; + async fn set_bank_ref_no(&self, id: i64, bank_ref_no: &str) -> Result<()>; + async fn set_submitted_at(&self, id: i64) -> Result<()>; + async fn confirm(&self, id: i64, bank_ref_no: &str) -> Result<()>; +} +``` + +### 7.2 BankTransactionRepository + +```rust +#[async_trait] +pub trait BankTransactionRepository: Send + Sync { + async fn create(&self, txn: &BankTransaction) -> Result; + async fn find_by_bank_ref_no(&self, bank_ref_no: &str) -> Result>; + async fn find_unmatched(&self) -> Result>; + async fn update_match_status(&self, id: i64, status: MatchStatus, matched_txn_no: Option<&str>) -> Result<()>; +} +``` + +## 八、API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/v1/transactions/transfer | 发起转账 | +| POST | /api/v1/transactions/deposit | 发起充值 | +| POST | /api/v1/transactions/withdraw | 发起提现 | +| GET | /api/v1/transactions/:id | 获取交易详情 | +| GET | /api/v1/transactions | 获取交易列表 | + +## 九、数据库表结构 + +### 9.1 system_transaction 表 + +```sql +CREATE TABLE system_transaction ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + txn_no VARCHAR(64) NOT NULL UNIQUE, + txn_type VARCHAR(32) NOT NULL, + from_account_id BIGINT, + to_account_id BIGINT, + amount DECIMAL(20, 2) NOT NULL, + status VARCHAR(32) DEFAULT 'created', + bank_ref_no VARCHAR(64), + source_key VARCHAR(256), + remark TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + confirmed_at TIMESTAMP NULL, + submitted_at TIMESTAMP NULL, + version INT DEFAULT 0, + + INDEX idx_status (status), + INDEX idx_from_account (from_account_id), + INDEX idx_to_account (to_account_id), + INDEX idx_bank_ref_no (bank_ref_no), + INDEX idx_source_key (source_key), + INDEX idx_created_at (created_at) +); +``` + +### 9.2 bank_transaction 表 + +```sql +CREATE TABLE bank_transaction ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + bank_ref_no VARCHAR(64) NOT NULL UNIQUE, + physical_account_id BIGINT NOT NULL, + txn_type VARCHAR(32) NOT NULL, + direction VARCHAR(16) NOT NULL, + amount DECIMAL(20, 2) NOT NULL, + counterparty_name VARCHAR(128), + counterparty_account VARCHAR(64), + txn_time TIMESTAMP NOT NULL, + sync_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + match_status VARCHAR(32) DEFAULT 'unmatched', + matched_txn_no VARCHAR(64), + remark TEXT, + + INDEX idx_physical_account (physical_account_id), + INDEX idx_match_status (match_status), + INDEX idx_txn_time (txn_time) +); +``` + +## 十、并发控制 + +### 10.1 乐观锁 + +使用版本号防止并发更新冲突: + +```rust +// 更新时检查版本 +UPDATE system_transaction +SET status = ?, version = version + 1 +WHERE id = ? AND version = ? +``` + +### 10.2 幂等性保证 + +1. **txn_no 唯一**:系统交易号全局唯一 +2. **source_key 去重**:外部入账通过 SourceKey 去重 +3. **状态机约束**:只允许合法的状态转移 + +## 十一、错误处理 + +| 错误类型 | 说明 | 处理方式 | +|----------|------|----------| +| InsufficientBalance | 余额不足 | 拒绝交易 | +| InvalidStateTransition | 非法状态转移 | 返回错误 | +| DuplicateTransaction | 重复交易 | 返回已有交易 | +| BankTimeout | 银行超时 | 创建补偿任务 | +| OptimisticLockFailed | 乐观锁冲突 | 重试 | + diff --git a/docs/domains/04-reconciliation.md b/docs/domains/04-reconciliation.md new file mode 100644 index 0000000..0771386 --- /dev/null +++ b/docs/domains/04-reconciliation.md @@ -0,0 +1,454 @@ +# 对账域 (Reconciliation Domain) + +## 一、领域概述 + +对账域负责确保系统账务与银行账务的一致性,包括交易匹配、差异处理、手工补录、三账校验等功能。 + +## 二、核心概念 + +### 2.1 三账对账闭环 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 三账校验公式 │ +│ │ +│ 总账余额 = 银行账余额 + 在途净额 │ +│ │ +│ ledger_total = bank_balance + transit_net │ +└─────────────────────────────────────────────────────────┘ + +银行账: 银行实际余额(通过银行接口查询) +在途账: 已扣未确认的资金(transit_amount) +总账: 系统记账余额(personal + labor + frozen) +``` + +### 2.2 对账流程 + +```mermaid +graph TD + A[获取银行流水] --> B[获取系统交易] + B --> C[交易匹配] + C --> D{匹配结果} + D -->|匹配| E[标记已匹配] + D -->|系统未达| F[创建系统未达项] + D -->|银行未达| G[创建银行未达项] + D -->|金额不匹配| H[创建差异项] + E --> I[三账校验] + F --> I + G --> I + H --> I + I --> J{是否平衡} + J -->|是| K[对账完成] + J -->|否| L[需要审核] +``` + +## 三、核心实体 + +### 3.1 对账批次 (ReconciliationBatch) + +```rust +pub struct ReconciliationBatch { + pub id: i64, + pub batch_no: String, // 批次编号 + pub physical_account_id: i64, // 实体账户ID + pub recon_date: NaiveDate, // 对账日期 + pub total_count: i32, // 总记录数 + pub matched_count: i32, // 匹配数 + pub mismatch_count: i32, // 不匹配数 + pub status: ReconciliationStatus, // 状态 + pub created_at: DateTime, // 创建时间 + pub completed_at: Option>, // 完成时间 + + // 三账对账结果 + pub bank_total: Option, // 银行账汇总 + pub transit_net: Option, // 在途净额 + pub ledger_total: Option, // 总账汇总 + pub three_account_balanced: Option, // 三账是否平衡 +} +``` + +**核心方法**: + +```rust +impl ReconciliationBatch { + // 计算匹配率 + pub fn match_rate(&self) -> f64 { + if self.total_count == 0 { + return 100.0; + } + (self.matched_count as f64 / self.total_count as f64) * 100.0 + } + + // 是否全部匹配 + pub fn is_all_matched(&self) -> bool { + self.mismatch_count == 0 + } + + // 是否已完成三账对账 + pub fn has_three_account_result(&self) -> bool { + self.three_account_balanced.is_some() + } + + // 三账差异金额 + pub fn three_account_difference(&self) -> Option { + match (self.ledger_total, self.bank_total, self.transit_net) { + (Some(ledger), Some(bank), Some(transit)) => { + Some(ledger - (bank + transit)) + } + _ => None, + } + } +} +``` + +### 3.2 对账明细项 (ReconciliationItem) + +```rust +pub struct ReconciliationItem { + pub id: i64, + pub batch_id: i64, // 批次ID + pub system_txn_no: Option, // 系统交易号 + pub bank_ref_no: Option, // 银行参考号 + pub system_amount: Option, // 系统金额 + pub bank_amount: Option, // 银行金额 + pub diff_amount: Decimal, // 差异金额 + pub status: ReconciliationItemStatus, // 状态 + pub remark: Option, // 处理备注 + pub created_at: DateTime, +} +``` + +**核心方法**: + +```rust +impl ReconciliationItem { + // 检查是否需要手工处理 + pub fn needs_manual_handling(&self) -> bool { + matches!( + self.status, + ReconciliationItemStatus::SystemUnreached + | ReconciliationItemStatus::BankUnreached + | ReconciliationItemStatus::AmountMismatch + ) + } +} +``` + +### 3.3 手工补录 (ManualAdjustment) + +```rust +pub struct ManualAdjustment { + pub id: i64, + pub adjustment_no: String, // 补录编号 + pub related_txn_no: Option, // 关联交易号 + pub reconciliation_item_id: Option,// 关联对账项ID + pub adjustment_type: AdjustmentType, // 补录类型 + pub account_id: i64, // 账户ID + pub amount: Decimal, // 金额 + pub reason: String, // 原因说明 + pub operator: String, // 操作人 + pub approver: Option, // 审批人 + pub status: AdjustmentStatus, // 状态 + pub created_at: DateTime, + pub approved_at: Option>, // 审批时间 +} +``` + +## 四、枚举类型 + +### 4.1 对账批次状态 (ReconciliationStatus) + +```rust +pub enum ReconciliationStatus { + Processing, // 处理中 + Completed, // 已完成 + NeedReview, // 需要审核 +} +``` + +### 4.2 对账项状态 (ReconciliationItemStatus) + +```rust +pub enum ReconciliationItemStatus { + Matched, // 已匹配 + SystemUnreached, // 系统未达(系统有银行无) + BankUnreached, // 银行未达(银行有系统无) + AmountMismatch, // 金额不匹配 + Adjusted, // 已调整 +} +``` + +| 状态 | 说明 | 处理方式 | +|------|------|----------| +| Matched | 系统与银行完全匹配 | 无需处理 | +| SystemUnreached | 系统有记录但银行无 | 核实后补录或冲销 | +| BankUnreached | 银行有记录但系统无 | 核实后补录 | +| AmountMismatch | 金额不一致 | 核实后调整 | +| Adjusted | 已手工调整 | 已处理 | + +### 4.3 补录类型 (AdjustmentType) + +```rust +pub enum AdjustmentType { + Add, // 补录(新增) + Reverse, // 冲销 + Modify, // 修改 +} +``` + +### 4.4 补录状态 (AdjustmentStatus) + +```rust +pub enum AdjustmentStatus { + Pending, // 待审批 + Approved, // 已审批 + Rejected, // 已拒绝 +} +``` + +## 五、对账匹配规则 + +### 5.1 匹配策略 + +1. **精确匹配**:银行交易号 = 系统银行参考号 +2. **金额匹配**:金额完全一致 +3. **时间匹配**:交易时间在允许范围内 +4. **方向匹配**:入账/出账方向一致 + +### 5.2 匹配优先级 + +``` +1. 按银行交易号精确匹配 +2. 按金额 + 时间 + 方向模糊匹配 +3. 按 SourceKey 匹配(入账) +4. 无法匹配则标记为未达 +``` + +### 5.3 差异处理 + +| 差异类型 | 处理方式 | +|----------|----------| +| 金额差异 < 1分 | 自动调平 | +| 金额差异 < 100元 | 人工审核 | +| 金额差异 >= 100元 | 升级处理 | + +## 六、三账校验 + +### 6.1 校验结果 + +```rust +pub struct ThreeAccountResult { + pub bank_balance: Decimal, // 银行账余额 + pub transit_net: Decimal, // 在途净额 + pub ledger_total: Decimal, // 总账余额 + pub is_balanced: bool, // 是否平衡 + pub difference: Decimal, // 差异金额 +} +``` + +### 6.2 校验公式 + +``` +期望总账 = 银行余额 + 在途净额 +差异 = 实际总账 - 期望总账 +平衡 = (差异 == 0) +``` + +### 6.3 不平衡原因分析 + +| 原因 | 说明 | 处理 | +|------|------|------| +| 银行延迟 | 银行入账延迟 | 等待银行同步 | +| 系统错误 | 记账错误 | 查找并修正 | +| 在途异常 | 在途状态不正确 | 核实在途交易 | +| 并发问题 | 对账期间有新交易 | 锁定后重新对账 | + +## 七、领域服务 + +### 7.1 ReconciliationService + +```rust +impl ReconciliationService { + // ========== 对账批次管理 ========== + + // 执行对账 + pub async fn run_reconciliation(&self, req: CreateReconciliationBatchRequest) -> Result; + + // 获取对账批次 + pub async fn get_batch(&self, id: i64) -> Result; + + // 获取对账明细 + pub async fn get_batch_items(&self, batch_id: i64) -> Result>; + + // ========== 三账校验 ========== + + // 执行三账校验 + pub async fn verify_three_accounts(&self, physical_account_id: i64) -> Result; + + // 更新批次三账结果 + pub async fn update_three_account_result(&self, batch_id: i64, result: &ThreeAccountResult) -> Result<()>; + + // ========== 手工补录 ========== + + // 创建手工补录 + pub async fn create_adjustment(&self, req: CreateManualAdjustmentRequest) -> Result; + + // 审批补录 + pub async fn approve_adjustment(&self, id: i64, approver: &str) -> Result<()>; + + // 拒绝补录 + pub async fn reject_adjustment(&self, id: i64, approver: &str, reason: &str) -> Result<()>; + + // 获取待审批补录 + pub async fn list_pending_adjustments(&self) -> Result>; + + // ========== 统计查询 ========== + + // 获取对账统计 + pub async fn get_stats(&self, batch_id: i64) -> Result; +} +``` + +### 7.2 三账校验实现 + +```rust +pub async fn verify_three_accounts(&self, physical_account_id: i64) -> Result { + // 1. 获取银行余额 + let bank_balance = self.bank_client.query_balance(physical_account_id).await?; + + // 2. 计算在途净额 + let transit_net = self.ledger_service.get_transit_total(physical_account_id).await?; + + // 3. 计算总账余额 + let ledger_total = self.ledger_service.get_ledger_total(physical_account_id).await?; + + // 4. 校验 + let expected_total = bank_balance + transit_net; + let difference = ledger_total - expected_total; + let is_balanced = difference.abs() < Decimal::new(1, 2); // 允许1分误差 + + Ok(ThreeAccountVerificationResult { + physical_account_id, + bank_balance, + transit_net, + ledger_total, + expected_total, + difference, + is_balanced, + verified_at: Utc::now(), + }) +} +``` + +## 八、API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/v1/reconciliation/run | 执行对账 | +| GET | /api/v1/reconciliation/batches/:id | 获取对账批次 | +| GET | /api/v1/reconciliation/batches/:id/items | 获取对账明细 | +| GET | /api/v1/reconciliation/three-account/:id | 三账校验 | +| POST | /api/v1/reconciliation/adjustments | 创建手工补录 | +| POST | /api/v1/reconciliation/adjustments/:id/approve | 审批补录 | +| POST | /api/v1/reconciliation/adjustments/:id/reject | 拒绝补录 | +| GET | /api/v1/reconciliation/adjustments/pending | 获取待审批补录 | + +## 九、数据库表结构 + +### 9.1 reconciliation_batch 表 + +```sql +CREATE TABLE reconciliation_batch ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + batch_no VARCHAR(64) NOT NULL UNIQUE, + physical_account_id BIGINT NOT NULL, + recon_date DATE NOT NULL, + total_count INT DEFAULT 0, + matched_count INT DEFAULT 0, + mismatch_count INT DEFAULT 0, + status VARCHAR(32) DEFAULT 'processing', + bank_total DECIMAL(20, 2), + transit_net DECIMAL(20, 2), + ledger_total DECIMAL(20, 2), + three_account_balanced BOOLEAN, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + + INDEX idx_physical_account (physical_account_id), + INDEX idx_recon_date (recon_date), + INDEX idx_status (status) +); +``` + +### 9.2 reconciliation_item 表 + +```sql +CREATE TABLE reconciliation_item ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + batch_id BIGINT NOT NULL, + system_txn_no VARCHAR(64), + bank_ref_no VARCHAR(64), + system_amount DECIMAL(20, 2), + bank_amount DECIMAL(20, 2), + diff_amount DECIMAL(20, 2) DEFAULT 0.00, + status VARCHAR(32) NOT NULL, + remark TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (batch_id) REFERENCES reconciliation_batch(id), + INDEX idx_batch (batch_id), + INDEX idx_status (status) +); +``` + +### 9.3 manual_adjustment 表 + +```sql +CREATE TABLE manual_adjustment ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + adjustment_no VARCHAR(64) NOT NULL UNIQUE, + related_txn_no VARCHAR(64), + reconciliation_item_id BIGINT, + adjustment_type VARCHAR(32) NOT NULL, + account_id BIGINT NOT NULL, + amount DECIMAL(20, 2) NOT NULL, + reason TEXT NOT NULL, + operator VARCHAR(64) NOT NULL, + approver VARCHAR(64), + status VARCHAR(32) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + approved_at TIMESTAMP NULL, + + INDEX idx_status (status), + INDEX idx_operator (operator) +); +``` + +## 十、对账时机 + +### 10.1 自动对账 + +- 每日定时对账(T+1) +- 配置对账频率(按账户) +- 批量处理 + +### 10.2 手动对账 + +- 运营人员触发 +- 指定日期范围 +- 单账户或多账户 + +### 10.3 实时对账 + +- 大额交易实时校验 +- 异常交易即时告警 +- 关键节点检查 + +## 十一、告警机制 + +| 告警级别 | 触发条件 | 处理方式 | +|----------|----------|----------| +| 警告 | 匹配率 < 99% | 记录日志 | +| 严重 | 三账不平衡 | 通知运营 | +| 紧急 | 大额差异 | 升级处理 | + diff --git a/docs/domains/05-compensation.md b/docs/domains/05-compensation.md new file mode 100644 index 0000000..714634f --- /dev/null +++ b/docs/domains/05-compensation.md @@ -0,0 +1,488 @@ +# 补偿域 (Compensation Domain) + +## 一、领域概述 + +补偿域负责处理交易过程中的异常情况,包括超时检测、失败重试、死信队列处理等。该域是保障交易最终一致性的关键组件。 + +## 二、核心概念 + +### 2.1 补偿机制 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 补偿流程 │ +│ │ +│ 交易超时 ──► 创建补偿任务 ──► 重试处理 ──► 成功/死信 │ +│ │ │ +│ └──► 指数退避 ──► 最大重试次数 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 指数退避策略 + +``` +重试间隔 = 基础间隔 × 2^重试次数 + +示例(基础间隔30秒): +- 第1次重试: 30秒后 +- 第2次重试: 60秒后 +- 第3次重试: 120秒后 +- 第4次重试: 240秒后 +``` + +### 2.3 死信队列 + +超过最大重试次数的任务进入死信队列,需要人工处理。 + +``` +正常队列 ──► 重试失败 ──► 死信队列 ──► 人工处理 +``` + +## 三、核心实体 + +### 3.1 补偿任务 (CompensationTask) + +```rust +pub struct CompensationTask { + pub id: i64, + pub txn_no: String, // 关联交易号 + pub task_type: CompensationTaskType, // 任务类型 + pub status: CompensationTaskStatus, // 任务状态 + pub retry_count: i32, // 重试次数 + pub max_retries: i32, // 最大重试次数 + pub next_retry_at: Option>,// 下次重试时间 + pub error_message: Option, // 错误信息 + pub created_at: DateTime, // 创建时间 + pub updated_at: DateTime, // 更新时间 + pub completed_at: Option>, // 完成时间 +} +``` + +**核心方法**: + +```rust +impl CompensationTask { + // 检查是否可重试 + pub fn can_retry(&self) -> bool { + self.status == CompensationTaskStatus::Failed + && self.retry_count < self.max_retries + } + + // 检查是否应该进入死信队列 + pub fn should_dead_letter(&self) -> bool { + self.status == CompensationTaskStatus::Failed + && self.retry_count >= self.max_retries + } + + // 检查是否已到重试时间 + pub fn is_ready_for_retry(&self) -> bool { + if let Some(next_retry) = self.next_retry_at { + Utc::now() >= next_retry + } else { + true + } + } +} +``` + +### 3.2 超时配置 (TimeoutConfig) + +```rust +pub struct TimeoutConfig { + pub bank_timeout_seconds: i64, // 银行交易超时秒数 + pub check_interval_seconds: i64, // 检查间隔秒数 + pub retry_base_interval_seconds: i64, // 重试间隔基数 + pub max_retries: i32, // 最大重试次数 +} + +impl Default for TimeoutConfig { + fn default() -> Self { + Self { + bank_timeout_seconds: 300, // 5分钟 + check_interval_seconds: 60, // 1分钟检查一次 + retry_base_interval_seconds: 30, // 30秒基础重试间隔 + max_retries: 3, + } + } +} +``` + +**计算下次重试时间**: + +```rust +impl TimeoutConfig { + pub fn calculate_next_retry(&self, retry_count: i32) -> DateTime { + let delay_seconds = self.retry_base_interval_seconds * (2_i64.pow(retry_count as u32)); + Utc::now() + chrono::Duration::seconds(delay_seconds) + } +} +``` + +## 四、枚举类型 + +### 4.1 补偿任务类型 (CompensationTaskType) + +```rust +pub enum CompensationTaskType { + TimeoutCheck, // 超时检查 + Reconcile, // 对账补偿 + Reverse, // 冲正处理 + Retry, // 重试 +} +``` + +| 类型 | 触发条件 | 处理方式 | +|------|----------|----------| +| TimeoutCheck | 交易提交银行后超时 | 查询银行状态 | +| Reconcile | 对账发现差异 | 调整账务 | +| Reverse | 需要冲正 | 执行冲正 | +| Retry | 失败需重试 | 重新提交 | + +### 4.2 补偿任务状态 (CompensationTaskStatus) + +```rust +pub enum CompensationTaskStatus { + Pending, // 待处理 + Processing, // 处理中 + Completed, // 已完成 + Failed, // 失败 + DeadLetter, // 死信 +} +``` + +**状态转移图**: + +``` +┌─────────┐ ┌────────────┐ ┌───────────┐ +│ Pending │───►│ Processing │───►│ Completed │ +└─────────┘ └────────────┘ └───────────┘ + │ + ▼ + ┌──────────┐ ┌────────────┐ + │ Failed │───►│ DeadLetter │ + └──────────┘ └────────────┘ + │ + ▼ + ┌──────────┐ + │ Retry │ (返回 Pending) + └──────────┘ +``` + +## 五、处理结果 + +### 5.1 补偿处理结果 (CompensationResult) + +```rust +pub struct CompensationResult { + pub task_id: i64, + pub success: bool, + pub message: String, + pub needs_retry: bool, +} +``` + +### 5.2 超时检测结果 (TimeoutDetectionResult) + +```rust +pub struct TimeoutDetectionResult { + pub timeout_count: i32, // 检测到的超时交易数 + pub task_created: i32, // 创建的补偿任务数 + pub failed_count: i32, // 处理失败数 +} +``` + +## 六、领域服务 + +### 6.1 CompensationService + +```rust +impl CompensationService { + // ========== 超时检测 ========== + + // 检测超时交易 + pub async fn detect_timeout_transactions(&self) -> Result; + + // ========== 任务管理 ========== + + // 创建补偿任务 + pub async fn create_compensation_task(&self, txn_no: &str, task_type: CompensationTaskType) -> Result; + + // 处理单个补偿任务 + pub async fn process_task(&self, task_id: i64) -> Result; + + // 批量处理待处理任务 + pub async fn process_pending_tasks(&self) -> Result>; + + // ========== 重试管理 ========== + + // 处理待重试任务 + pub async fn process_ready_for_retry(&self) -> Result>; + + // 标记任务完成 + pub async fn mark_completed(&self, task_id: i64) -> Result<()>; + + // 标记任务失败并计划重试 + pub async fn mark_failed_with_retry(&self, task_id: i64, error: &str) -> Result<()>; + + // ========== 死信处理 ========== + + // 处理死信任务 + pub async fn process_dead_letter_tasks(&self) -> Result>; + + // 移入死信队列 + pub async fn move_to_dead_letter(&self, task_id: i64) -> Result<()>; + + // 手动重试死信任务 + pub async fn retry_dead_letter(&self, task_id: i64) -> Result<()>; +} +``` + +### 6.2 超时检测实现 + +```rust +pub async fn detect_timeout_transactions(&self) -> Result { + // 查找需要检查超时的交易 + let bank_submitted_txns = self.txn_repo + .find_by_status(TransactionStatus::BankSubmitted) + .await?; + + let mut timeout_count = 0; + let mut task_created = 0; + let mut failed_count = 0; + + for txn in bank_submitted_txns { + // 检查是否超时 + if txn.is_timeout(self.config.bank_timeout_seconds) { + timeout_count += 1; + + // 检查是否已有补偿任务 + if self.task_repo.has_pending_task(&txn.txn_no, CompensationTaskType::TimeoutCheck).await? { + continue; + } + + // 更新交易状态为 Timeout + match self.txn_repo.update_status(txn.id, TransactionStatus::Timeout).await { + Ok(_) => { + // 创建补偿任务 + match self.create_compensation_task(&txn.txn_no, CompensationTaskType::TimeoutCheck).await { + Ok(_) => task_created += 1, + Err(e) => { + warn!("创建补偿任务失败: {}", e); + failed_count += 1; + } + } + } + Err(e) => { + warn!("更新交易状态失败: {}", e); + failed_count += 1; + } + } + } + } + + Ok(TimeoutDetectionResult { timeout_count, task_created, failed_count }) +} +``` + +### 6.3 任务处理实现 + +```rust +pub async fn process_task(&self, task_id: i64) -> Result { + let task = self.task_repo.find_by_id(task_id).await? + .ok_or_else(|| AppError::NotFound("补偿任务不存在".into()))?; + + // 更新状态为处理中 + self.task_repo.update_status(task_id, CompensationTaskStatus::Processing, None).await?; + + match task.task_type { + CompensationTaskType::TimeoutCheck => { + self.handle_timeout_check(&task).await + } + CompensationTaskType::Reconcile => { + self.handle_reconcile(&task).await + } + CompensationTaskType::Reverse => { + self.handle_reverse(&task).await + } + CompensationTaskType::Retry => { + self.handle_retry(&task).await + } + } +} + +async fn handle_timeout_check(&self, task: &CompensationTask) -> Result { + // 1. 获取关联交易 + let txn = self.txn_repo.find_by_txn_no(&task.txn_no).await? + .ok_or_else(|| AppError::NotFound("交易不存在".into()))?; + + // 2. 查询银行状态 + let bank_status = self.bank_client.query_transaction_status(&txn.bank_ref_no).await; + + match bank_status { + Ok(status) if status.is_success() => { + // 银行确认成功,结转在途 + self.ledger_service.settle_transit( + txn.from_account_id.unwrap(), + AccountType::Physical, + txn.amount + ).await?; + + self.txn_repo.update_status(txn.id, TransactionStatus::Success).await?; + self.task_repo.mark_completed(task.id).await?; + + Ok(CompensationResult { + task_id: task.id, + success: true, + message: "银行确认成功".to_string(), + needs_retry: false, + }) + } + Ok(status) if status.is_failed() => { + // 银行确认失败,回退在途 + self.ledger_service.rollback_transit( + txn.from_account_id.unwrap(), + AccountType::Physical, + txn.amount + ).await?; + + self.txn_repo.update_status(txn.id, TransactionStatus::Failed).await?; + self.task_repo.mark_completed(task.id).await?; + + Ok(CompensationResult { + task_id: task.id, + success: true, + message: "银行确认失败,已回退".to_string(), + needs_retry: false, + }) + } + _ => { + // 仍然无法确认,需要重试 + Ok(CompensationResult { + task_id: task.id, + success: false, + message: "无法确认银行状态".to_string(), + needs_retry: true, + }) + } + } +} +``` + +## 七、仓储接口 + +### 7.1 CompensationTaskRepository + +```rust +#[async_trait] +pub trait CompensationTaskRepository: Send + Sync { + // 创建任务 + async fn create(&self, task: &CompensationTask) -> Result; + + // 查询 + async fn find_by_id(&self, id: i64) -> Result>; + async fn find_by_txn_no(&self, txn_no: &str) -> Result>; + async fn find_pending(&self) -> Result>; + async fn find_ready_for_retry(&self) -> Result>; + async fn find_dead_letter(&self) -> Result>; + + // 状态更新 + async fn update_status(&self, id: i64, status: CompensationTaskStatus, error_message: Option<&str>) -> Result<()>; + async fn increment_retry(&self, id: i64, next_retry_at: DateTime) -> Result<()>; + async fn mark_completed(&self, id: i64) -> Result<()>; + async fn mark_dead_letter(&self, id: i64) -> Result<()>; + + // 检查 + async fn has_pending_task(&self, txn_no: &str, task_type: CompensationTaskType) -> Result; +} +``` + +## 八、数据库表结构 + +### 8.1 compensation_task 表 + +```sql +CREATE TABLE compensation_task ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + txn_no VARCHAR(64) NOT NULL, + task_type VARCHAR(32) NOT NULL, + status VARCHAR(32) DEFAULT 'pending', + retry_count INT DEFAULT 0, + max_retries INT DEFAULT 3, + next_retry_at TIMESTAMP NULL, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + + INDEX idx_txn_no (txn_no), + INDEX idx_status (status), + INDEX idx_next_retry (next_retry_at), + INDEX idx_task_type (task_type) +); +``` + +## 九、调度机制 + +### 9.1 定时任务 + +| 任务 | 频率 | 说明 | +|------|------|------| +| 超时检测 | 每分钟 | 扫描超时交易 | +| 待处理任务 | 每30秒 | 处理待处理任务 | +| 重试任务 | 每分钟 | 处理到期重试任务 | +| 死信巡检 | 每小时 | 检查死信队列 | + +### 9.2 并发控制 + +- 分布式锁防止重复处理 +- 乐观锁防止并发更新 +- 任务状态机保证正确流转 + +## 十、监控告警 + +### 10.1 监控指标 + +| 指标 | 说明 | 阈值 | +|------|------|------| +| pending_tasks | 待处理任务数 | > 100 告警 | +| failed_tasks | 失败任务数 | > 10 告警 | +| dead_letter_count | 死信数量 | > 0 告警 | +| avg_retry_count | 平均重试次数 | > 2 关注 | + +### 10.2 告警规则 + +```yaml +alerts: + - name: compensation_task_backlog + condition: pending_tasks > 100 + severity: warning + + - name: dead_letter_alert + condition: dead_letter_count > 0 + severity: critical + + - name: high_failure_rate + condition: failed_tasks / total_tasks > 0.1 + severity: error +``` + +## 十一、最佳实践 + +### 11.1 任务设计 + +1. 任务幂等:同一任务多次执行结果相同 +2. 原子操作:任务内操作要么全成功要么全失败 +3. 超时设置:合理设置任务超时时间 + +### 11.2 重试策略 + +1. 指数退避:避免系统过载 +2. 最大重试:防止无限重试 +3. 死信处理:异常任务人工介入 + +### 11.3 监控运维 + +1. 任务可追溯:完整记录任务生命周期 +2. 告警及时:异常情况及时通知 +3. 手动干预:提供手动处理入口 + diff --git a/docs/domains/06-points.md b/docs/domains/06-points.md new file mode 100644 index 0000000..b88d0f9 --- /dev/null +++ b/docs/domains/06-points.md @@ -0,0 +1,503 @@ +# 积分域 (Points Domain) + +## 一、领域概述 + +积分域负责管理用户积分的完整生命周期,包括积分获取、消费、转移和过期处理。支持多种积分类型和灵活的积分规则配置。 + +## 二、核心实体 + +### 2.1 积分账户 (PointsAccount) + +```rust +pub struct PointsAccount { + pub id: i64, + pub sub_account_id: i64, // 关联子账户ID + pub points_type: PointsType, // 积分类型 + pub balance: Decimal, // 积分余额 + pub total_earned: Decimal, // 累计获得 + pub total_spent: Decimal, // 累计消费 + pub total_expired: Decimal, // 累计过期 + pub created_at: DateTime, + pub updated_at: DateTime, +} +``` + +**核心方法**: + +```rust +impl PointsAccount { + // 检查是否有足够积分 + pub fn has_sufficient_points(&self, amount: Decimal) -> bool { + self.balance >= amount + } + + // 增加积分 + pub fn add_points(&mut self, amount: Decimal) { + self.balance += amount; + self.total_earned += amount; + self.updated_at = Utc::now(); + } + + // 减少积分 + pub fn subtract_points(&mut self, amount: Decimal) { + self.balance -= amount; + self.total_spent += amount; + self.updated_at = Utc::now(); + } + + // 过期积分 + pub fn expire_points(&mut self, amount: Decimal) { + self.balance -= amount; + self.total_expired += amount; + self.updated_at = Utc::now(); + } +} +``` + +### 2.2 积分交易 (PointsTransaction) + +```rust +pub struct PointsTransaction { + pub id: i64, + pub txn_no: String, // 交易编号 + pub points_account_id: i64, // 积分账户ID + pub txn_type: PointsTransactionType, // 交易类型 + pub amount: Decimal, // 积分数量 + pub balance_before: Decimal, // 交易前余额 + pub balance_after: Decimal, // 交易后余额 + pub related_business_id: Option, // 关联业务ID + pub remark: Option, // 备注 + pub created_at: DateTime, +} +``` + +### 2.3 积分规则 (PointsRule) + +```rust +pub struct PointsRule { + pub id: i64, + pub name: String, // 规则名称 + pub points_type: PointsType, // 积分类型 + pub rule_type: String, // 规则类型 + pub config: serde_json::Value, // 规则配置 + pub enabled: bool, // 是否启用 + pub created_at: DateTime, +} +``` + +**规则配置示例**: + +```json +{ + "earn_rule": { + "base_points": 100, + "multiplier": 1.5, + "max_daily": 1000, + "valid_days": 365 + }, + "spend_rule": { + "min_points": 100, + "exchange_rate": 0.01 + } +} +``` + +## 三、枚举类型 + +### 3.1 积分类型 (PointsType) + +```rust +pub enum PointsType { + Production, // 生产积分 + Management, // 管理积分 + Other, // 其他积分 +} +``` + +| 类型 | 说明 | 获取方式 | +|------|------|----------| +| Production | 生产积分 | 完成生产任务 | +| Management | 管理积分 | 管理工作奖励 | +| Other | 其他积分 | 活动奖励等 | + +### 3.2 积分交易类型 (PointsTransactionType) + +```rust +pub enum PointsTransactionType { + Earn, // 获取 + Spend, // 消费 + Transfer, // 转移 + Expire, // 过期 + Adjust, // 调整 +} +``` + +## 四、业务流程 + +### 4.1 积分获取流程 + +```mermaid +sequenceDiagram + participant Business + participant PointsService + participant PointsRepo + + Business->>PointsService: 1. 发起积分获取 + PointsService->>PointsService: 2. 校验规则 + PointsService->>PointsRepo: 3. 获取积分账户 + PointsService->>PointsService: 4. 计算积分 + PointsService->>PointsRepo: 5. 更新余额 + PointsService->>PointsRepo: 6. 记录交易 + PointsService-->>Business: 7. 返回结果 +``` + +### 4.2 积分消费流程 + +```mermaid +sequenceDiagram + participant User + participant PointsService + participant PointsRepo + + User->>PointsService: 1. 发起积分消费 + PointsService->>PointsRepo: 2. 获取积分账户 + PointsService->>PointsService: 3. 检查余额 + alt 余额充足 + PointsService->>PointsRepo: 4a. 扣减余额 + PointsService->>PointsRepo: 5a. 记录交易 + PointsService-->>User: 6a. 消费成功 + else 余额不足 + PointsService-->>User: 4b. 返回余额不足 + end +``` + +### 4.3 积分转移流程 + +```mermaid +sequenceDiagram + participant UserA + participant PointsService + participant PointsRepo + + UserA->>PointsService: 1. 发起转移 + PointsService->>PointsRepo: 2. 获取转出账户 + PointsService->>PointsService: 3. 检查余额 + PointsService->>PointsRepo: 4. 获取转入账户 + PointsService->>PointsRepo: 5. 扣减转出账户 + PointsService->>PointsRepo: 6. 增加转入账户 + PointsService->>PointsRepo: 7. 记录两笔交易 + PointsService-->>UserA: 8. 转移成功 +``` + +## 五、领域服务 + +### 5.1 PointsService + +```rust +impl PointsService { + // ========== 账户管理 ========== + + // 获取积分账户 + pub async fn get_accounts(&self, sub_account_id: i64) -> Result>; + + // 创建积分账户 + pub async fn create_account(&self, req: CreatePointsAccountRequest) -> Result; + + // ========== 积分操作 ========== + + // 获取积分 + pub async fn earn_points(&self, req: PointsTransactionRequest) -> Result; + + // 消费积分 + pub async fn spend_points(&self, req: PointsTransactionRequest) -> Result; + + // 转移积分 + pub async fn transfer_points(&self, req: PointsTransferRequest) -> Result<(PointsTransaction, PointsTransaction)>; + + // 调整积分 + pub async fn adjust_points(&self, req: PointsTransactionRequest) -> Result; + + // ========== 过期处理 ========== + + // 处理过期积分 + pub async fn process_expired_points(&self) -> Result; + + // ========== 查询 ========== + + // 查询积分交易 + pub async fn list_transactions(&self, query: PointsTransactionQuery) -> Result>; + + // 获取积分统计 + pub async fn get_statistics(&self, account_id: i64) -> Result; +} +``` + +### 5.2 积分获取实现 + +```rust +pub async fn earn_points(&self, req: PointsTransactionRequest) -> Result { + // 1. 获取积分账户 + let mut account = self.account_repo + .find_by_id(req.points_account_id) + .await? + .ok_or_else(|| AppError::NotFound("积分账户不存在".into()))?; + + // 2. 校验规则(可选) + if let Some(rule) = self.get_earn_rule(&account.points_type).await? { + self.validate_earn_rule(&rule, req.amount)?; + } + + // 3. 记录交易前余额 + let balance_before = account.balance; + + // 4. 增加积分 + account.add_points(req.amount); + + // 5. 更新账户 + self.account_repo.update(&account).await?; + + // 6. 创建交易记录 + let txn = PointsTransaction { + id: 0, + txn_no: self.generate_txn_no(), + points_account_id: account.id, + txn_type: PointsTransactionType::Earn, + amount: req.amount, + balance_before, + balance_after: account.balance, + related_business_id: req.related_business_id, + remark: req.remark, + created_at: Utc::now(), + }; + + let txn_id = self.txn_repo.create(&txn).await?; + + Ok(PointsTransaction { id: txn_id, ..txn }) +} +``` + +### 5.3 积分转移实现 + +```rust +pub async fn transfer_points(&self, req: PointsTransferRequest) -> Result<(PointsTransaction, PointsTransaction)> { + // 1. 获取转出账户 + let mut from_account = self.account_repo + .find_by_id(req.from_account_id) + .await? + .ok_or_else(|| AppError::NotFound("转出账户不存在".into()))?; + + // 2. 检查余额 + if !from_account.has_sufficient_points(req.amount) { + return Err(AppError::InsufficientBalance { + available: from_account.balance, + required: req.amount, + }); + } + + // 3. 获取转入账户 + let mut to_account = self.account_repo + .find_by_id(req.to_account_id) + .await? + .ok_or_else(|| AppError::NotFound("转入账户不存在".into()))?; + + // 4. 执行转移 + let from_before = from_account.balance; + let to_before = to_account.balance; + + from_account.subtract_points(req.amount); + to_account.add_points(req.amount); + + // 5. 更新账户 + self.account_repo.update(&from_account).await?; + self.account_repo.update(&to_account).await?; + + // 6. 创建交易记录 + let txn_no = self.generate_txn_no(); + + let from_txn = PointsTransaction { + id: 0, + txn_no: format!("{}-OUT", txn_no), + points_account_id: from_account.id, + txn_type: PointsTransactionType::Transfer, + amount: -req.amount, + balance_before: from_before, + balance_after: from_account.balance, + related_business_id: Some(format!("TRANSFER_TO_{}", to_account.id)), + remark: req.remark.clone(), + created_at: Utc::now(), + }; + + let to_txn = PointsTransaction { + id: 0, + txn_no: format!("{}-IN", txn_no), + points_account_id: to_account.id, + txn_type: PointsTransactionType::Transfer, + amount: req.amount, + balance_before: to_before, + balance_after: to_account.balance, + related_business_id: Some(format!("TRANSFER_FROM_{}", from_account.id)), + remark: req.remark, + created_at: Utc::now(), + }; + + let from_id = self.txn_repo.create(&from_txn).await?; + let to_id = self.txn_repo.create(&to_txn).await?; + + Ok(( + PointsTransaction { id: from_id, ..from_txn }, + PointsTransaction { id: to_id, ..to_txn }, + )) +} +``` + +## 六、仓储接口 + +### 6.1 PointsAccountRepository + +```rust +#[async_trait] +pub trait PointsAccountRepository: Send + Sync { + async fn create(&self, account: &PointsAccount) -> Result; + async fn find_by_id(&self, id: i64) -> Result>; + async fn find_by_sub_account(&self, sub_account_id: i64) -> Result>; + async fn update(&self, account: &PointsAccount) -> Result<()>; +} +``` + +### 6.2 PointsTransactionRepository + +```rust +#[async_trait] +pub trait PointsTransactionRepository: Send + Sync { + async fn create(&self, txn: &PointsTransaction) -> Result; + async fn find_by_account(&self, account_id: i64, query: &PointsTransactionQuery) -> Result>; + async fn find_by_txn_no(&self, txn_no: &str) -> Result>; +} +``` + +## 七、API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/v1/points/accounts/:sub_account_id | 获取积分账户 | +| POST | /api/v1/points/earn | 获取积分 | +| POST | /api/v1/points/spend | 消费积分 | +| POST | /api/v1/points/transfer | 转移积分 | +| GET | /api/v1/points/transactions | 查询积分交易 | + +## 八、数据库表结构 + +### 8.1 points_account 表 + +```sql +CREATE TABLE points_account ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + sub_account_id BIGINT NOT NULL, + points_type VARCHAR(32) NOT NULL, + balance DECIMAL(20, 2) DEFAULT 0.00, + total_earned DECIMAL(20, 2) DEFAULT 0.00, + total_spent DECIMAL(20, 2) DEFAULT 0.00, + total_expired DECIMAL(20, 2) DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + UNIQUE INDEX idx_sub_account_type (sub_account_id, points_type), + INDEX idx_points_type (points_type) +); +``` + +### 8.2 points_transaction 表 + +```sql +CREATE TABLE points_transaction ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + txn_no VARCHAR(64) NOT NULL UNIQUE, + points_account_id BIGINT NOT NULL, + txn_type VARCHAR(32) NOT NULL, + amount DECIMAL(20, 2) NOT NULL, + balance_before DECIMAL(20, 2) NOT NULL, + balance_after DECIMAL(20, 2) NOT NULL, + related_business_id VARCHAR(128), + remark TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (points_account_id) REFERENCES points_account(id), + INDEX idx_account (points_account_id), + INDEX idx_txn_type (txn_type), + INDEX idx_created_at (created_at) +); +``` + +### 8.3 points_rule 表 + +```sql +CREATE TABLE points_rule ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(128) NOT NULL, + points_type VARCHAR(32) NOT NULL, + rule_type VARCHAR(32) NOT NULL, + config JSON NOT NULL, + enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_points_type (points_type), + INDEX idx_enabled (enabled) +); +``` + +## 九、积分规则 + +### 9.1 获取规则 + +| 参数 | 说明 | 示例 | +|------|------|------| +| base_points | 基础积分 | 100 | +| multiplier | 倍率 | 1.5 | +| max_daily | 每日上限 | 1000 | +| valid_days | 有效期(天) | 365 | + +### 9.2 消费规则 + +| 参数 | 说明 | 示例 | +|------|------|------| +| min_points | 最低消费 | 100 | +| exchange_rate | 兑换比例 | 0.01 | + +### 9.3 过期规则 + +| 参数 | 说明 | 示例 | +|------|------|------| +| expire_days | 过期天数 | 365 | +| notify_before | 提前通知天数 | 30 | + +## 十、统计报表 + +### 10.1 账户统计 + +```rust +pub struct PointsStatistics { + pub account_id: i64, + pub balance: Decimal, + pub total_earned: Decimal, + pub total_spent: Decimal, + pub total_expired: Decimal, + pub earn_count: i64, + pub spend_count: i64, + pub transfer_in_count: i64, + pub transfer_out_count: i64, +} +``` + +### 10.2 周期统计 + +- 日统计:每日积分变动 +- 月统计:月度积分汇总 +- 年统计:年度积分报表 + +## 十一、注意事项 + +1. **积分精度**:使用 Decimal 类型,保留2位小数 +2. **并发控制**:积分操作需加锁,防止超发 +3. **审计追踪**:所有积分变动必须有交易记录 +4. **过期处理**:定期扫描处理过期积分 + diff --git a/docs/integration/frontend-backend.md b/docs/integration/frontend-backend.md new file mode 100644 index 0000000..14e340a --- /dev/null +++ b/docs/integration/frontend-backend.md @@ -0,0 +1,356 @@ +# 前后端对接清单 + +## 一、对接状态总览 + +### 1.1 后端 API 统计 + +| 领域 | 端点总数 | 已对接 | 未对接 | +|------|----------|--------|--------| +| Account | 11 | 11 | 0 | +| Transaction | 5 | 3 | 2 | +| Ledger | 3 | 0 | 3 | +| Reconciliation | 8 | 4 | 4 | +| Points | 5 | 0 | 5 | +| **总计** | **32** | **18** | **14** | + +### 1.2 前端多余 API + +前端定义了以下 API 但后端尚未实现: + +| 前端方法 | 预期端点 | 状态 | +|----------|----------|------| +| submitToBank | POST /transactions/:id/submit | 待补充 | +| cancelTransaction | POST /transactions/:id/cancel | 待补充 | +| retryTransaction | POST /transactions/:id/retry | 待补充 | +| getTransactionStatus | GET /transactions/:id/status | 待补充 | +| getBankStatements | GET /bank-statements | 待补充 | +| getTransactionStats | GET /transactions/stats | 待补充 | +| executeBatch | POST /reconciliation/batch | 待补充 | +| getAdjustments | GET /reconciliation/adjustments | 待补充 | +| getStats | GET /reconciliation/stats | 待补充 | +| exportReport | GET /reconciliation/export | 待补充 | + +## 二、详细对接清单 + +### 2.1 账户 API 对接 + +| 后端端点 | 前端方法 | 状态 | 说明 | +|----------|----------|------|------| +| POST /physical-accounts | AccountAPI.createPhysicalAccount | ✅ 已对接 | | +| GET /physical-accounts | AccountAPI.getPhysicalAccounts | ✅ 已对接 | | +| GET /physical-accounts/:id | AccountAPI.getPhysicalAccount | ✅ 已对接 | | +| POST /physical-accounts/:id/freeze | AccountAPI.freezeAccount | ✅ 已对接 | | +| POST /physical-accounts/:id/unfreeze | AccountAPI.unfreezeAccount | ✅ 已对接 | | +| POST /sub-accounts | AccountAPI.createSubAccount | ✅ 已对接 | | +| GET /sub-accounts/:id | AccountAPI.getSubAccount | ✅ 已对接 | | +| GET /sub-accounts/:id/balance | AccountAPI.getSubAccountBalance | ✅ 已对接 | | +| POST /sub-accounts/:id/freeze | AccountAPI.freezeSubAccount | ✅ 已对接 | | +| POST /sub-accounts/:id/unfreeze | AccountAPI.unfreezeSubAccount | ✅ 已对接 | | +| POST /sub-accounts/:id/close | AccountAPI.closeSubAccount | ✅ 已对接 | | + +**前端文件**: `src/api/account.ts` + +### 2.2 交易 API 对接 + +| 后端端点 | 前端方法 | 状态 | 说明 | +|----------|----------|------|------| +| POST /transactions/transfer | TransactionAPI.createTransfer | ✅ 已对接 | | +| POST /transactions/deposit | - | ❌ 未对接 | 需添加 createDeposit | +| POST /transactions/withdraw | - | ❌ 未对接 | 需添加 createWithdraw | +| GET /transactions/:id | TransactionAPI.getTransaction | ✅ 已对接 | | +| GET /transactions | TransactionAPI.getTransactions | ✅ 已对接 | | + +**前端文件**: `src/api/transaction.ts` + +**需要补充的前端方法**: +```typescript +// 充值 +static async createDeposit(data: DepositRequest): Promise { + return apiClient.post('/transactions/deposit', data) +} + +// 提现 +static async createWithdraw(data: WithdrawRequest): Promise { + return apiClient.post('/transactions/withdraw', data) +} +``` + +### 2.3 账务 API 对接 + +| 后端端点 | 前端方法 | 状态 | 说明 | +|----------|----------|------|------| +| GET /ledger/subjects | - | ❌ 未对接 | 需创建 ledger.ts | +| GET /ledger/entries/:id | - | ❌ 未对接 | 需创建 ledger.ts | +| GET /ledger/accounts/:id/entries | - | ❌ 未对接 | 需创建 ledger.ts | + +**需要创建的前端文件**: `src/api/ledger.ts` + +### 2.4 对账 API 对接 + +| 后端端点 | 前端方法 | 状态 | 说明 | +|----------|----------|------|------| +| POST /reconciliation/run | ReconciliationAPI.runReconciliation | ✅ 已对接 | | +| GET /reconciliation/batches/:id | ReconciliationAPI.getBatch | ✅ 已对接 | | +| GET /reconciliation/batches/:id/items | ReconciliationAPI.getBatchItems | ✅ 已对接 | | +| GET /reconciliation/three-account/:id | ReconciliationAPI.verifyThreeAccounts | ✅ 已对接 | | +| POST /reconciliation/adjustments | ReconciliationAPI.createAdjustment | ✅ 已对接 | | +| POST /reconciliation/adjustments/:id/approve | - | ❌ 未对接 | 需添加 approveAdjustment | +| POST /reconciliation/adjustments/:id/reject | - | ❌ 未对接 | 需添加 rejectAdjustment | +| GET /reconciliation/adjustments/pending | - | ❌ 未对接 | 需添加 getPendingAdjustments | + +**前端文件**: `src/api/reconciliation.ts` + +**需要补充的前端方法**: +```typescript +// 审批补录 +static async approveAdjustment(id: number, approver: string): Promise { + return apiClient.post(`/reconciliation/adjustments/${id}/approve`, { approver }) +} + +// 拒绝补录 +static async rejectAdjustment(id: number, approver: string, reason: string): Promise { + return apiClient.post(`/reconciliation/adjustments/${id}/reject`, { approver, reason }) +} + +// 获取待审批补录 +static async getPendingAdjustments(): Promise { + return apiClient.get('/reconciliation/adjustments/pending') +} +``` + +### 2.5 积分 API 对接 + +| 后端端点 | 前端方法 | 状态 | 说明 | +|----------|----------|------|------| +| GET /points/accounts/:id | - | ❌ 未对接 | 需创建 points.ts | +| POST /points/earn | - | ❌ 未对接 | 需创建 points.ts | +| POST /points/spend | - | ❌ 未对接 | 需创建 points.ts | +| POST /points/transfer | - | ❌ 未对接 | 需创建 points.ts | +| GET /points/transactions | - | ❌ 未对接 | 需创建 points.ts | + +**需要创建的前端文件**: `src/api/points.ts` + +## 三、类型定义对照 + +### 3.1 账户相关类型 + +| 后端类型 | 前端类型 | 位置 | +|----------|----------|------| +| PhysicalAccount | PhysicalAccount | types/account.ts | +| VirtualSubAccount | SubAccount | types/account.ts | +| AccountStatus | AccountStatus | types/account.ts | +| ConsistencyMode | ConsistencyMode | types/account.ts | +| OutboundControl | OutboundControl | types/account.ts | + +### 3.2 交易相关类型 + +| 后端类型 | 前端类型 | 位置 | +|----------|----------|------| +| SystemTransaction | Transaction | types/transaction.ts | +| TransactionStatus | TransactionStatus | types/transaction.ts | +| TransactionType | TransactionType | types/transaction.ts | +| TransactionDirection | TransactionDirection | types/transaction.ts | + +### 3.3 对账相关类型 + +| 后端类型 | 前端类型 | 位置 | +|----------|----------|------| +| ReconciliationBatch | ReconciliationBatch | types/reconciliation.ts | +| ReconciliationItem | ReconciliationItem | types/reconciliation.ts | +| ManualAdjustment | ManualAdjustment | types/reconciliation.ts | +| ThreeAccountResult | ThreeAccountResult | types/reconciliation.ts | + +### 3.4 积分相关类型 (需添加) + +```typescript +// types/points.ts + +export interface PointsAccount { + id: number + sub_account_id: number + points_type: PointsType + balance: string + total_earned: string + total_spent: string + total_expired: string + created_at: string + updated_at: string +} + +export interface PointsTransaction { + id: number + txn_no: string + points_account_id: number + txn_type: PointsTransactionType + amount: string + balance_before: string + balance_after: string + related_business_id?: string + remark?: string + created_at: string +} + +export type PointsType = 'production' | 'management' | 'other' +export type PointsTransactionType = 'earn' | 'spend' | 'transfer' | 'expire' | 'adjust' +``` + +### 3.5 账务相关类型 (需添加) + +```typescript +// types/ledger.ts + +export interface AccountingSubject { + code: string + name: string + category: SubjectCategory + direction_default: number + parent_code?: string + level: number +} + +export interface LedgerEntry { + id: number + entry_no: string + txn_no: string + post_date: string + post_time: string + description?: string + status: EntryStatus + created_at: string + lines: LedgerLine[] +} + +export interface LedgerLine { + id: number + entry_id: number + account_id: number + account_type: string + subject_code: string + direction: Direction + amount: string +} + +export type SubjectCategory = 'asset' | 'liability' | 'income' | 'expense' +export type Direction = 'debit' | 'credit' +export type EntryStatus = 'pending' | 'posted' | 'reversed' +``` + +## 四、待办事项 + +### 4.1 高优先级 + +1. ✅ 创建 `src/api/points.ts` - 积分 API 客户端 +2. ✅ 创建 `src/api/ledger.ts` - 账务 API 客户端 +3. 补充 `src/api/transaction.ts` 中的 deposit 和 withdraw 方法 +4. 补充 `src/api/reconciliation.ts` 中的审批相关方法 + +### 4.2 中优先级 + +1. 添加 `src/types/points.ts` - 积分类型定义 +2. 添加 `src/types/ledger.ts` - 账务类型定义 +3. 更新前端界面支持新的 API + +### 4.3 低优先级 + +1. 后端补充前端多余 API 的实现 +2. 添加 API 版本管理 +3. 添加 API 文档自动生成(OpenAPI/Swagger) + +## 五、接口规范 + +### 5.1 请求规范 + +```typescript +// 统一请求配置 +const apiClient = axios.create({ + baseURL: '/api/v1', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}) +``` + +### 5.2 响应处理 + +```typescript +// 响应拦截器 +apiClient.interceptors.response.use( + (response) => response.data.data, + (error) => { + const message = error.response?.data?.message || '请求失败' + ElMessage.error(message) + return Promise.reject(error) + } +) +``` + +### 5.3 错误处理 + +```typescript +// 统一错误处理 +interface ApiError { + code: number + message: string + error?: string +} + +// 使用示例 +try { + await AccountAPI.createPhysicalAccount(data) +} catch (error) { + if (axios.isAxiosError(error)) { + const apiError = error.response?.data as ApiError + console.error('API错误:', apiError.message) + } +} +``` + +## 六、Mock 数据 + +开发环境使用 MSW (Mock Service Worker) 进行 API 模拟: + +**配置文件**: `src/mocks/handlers.ts` + +```typescript +// MSW handlers 示例 +export const handlers = [ + rest.get('/api/v1/physical-accounts', (req, res, ctx) => { + return res(ctx.json({ + code: 200, + message: 'success', + data: mockPhysicalAccounts + })) + }), + // ... 其他 handlers +] +``` + +## 七、环境配置 + +### 7.1 开发环境 (.env.development) + +```env +VITE_API_BASE_URL=/api/v1 +VITE_USE_MOCK=true +``` + +### 7.2 生产环境 (.env.production) + +```env +VITE_API_BASE_URL=https://api.example.com/api/v1 +VITE_USE_MOCK=false +``` + +## 八、对接检查清单 + +在进行前后端对接时,请确认以下事项: + +- [ ] 接口路径是否正确 +- [ ] 请求方法是否匹配 (GET/POST/PUT/DELETE) +- [ ] 请求参数格式是否正确 (Query/Body/Path) +- [ ] 响应数据结构是否匹配 +- [ ] 错误码处理是否完整 +- [ ] 类型定义是否同步 +- [ ] Mock 数据是否更新 +