docs: 添加完整技术文档体系
- 系统架构文档 (architecture/README.md) - 6个领域文档: - 账户域 (01-account.md) - 账务域 (02-ledger.md) - 交易域 (03-transaction.md) - 对账域 (04-reconciliation.md) - 补偿域 (05-compensation.md) - 积分域 (06-points.md) - API 参考文档 (api/README.md) - 前后端对接清单 (integration/frontend-backend.md)
This commit is contained in:
parent
94f72ca2a8
commit
1460187516
572
docs/api/README.md
Normal file
572
docs/api/README.md
Normal file
@ -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 <token>
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
258
docs/architecture/README.md
Normal file
258
docs/architecture/README.md
Normal file
@ -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. **索引优化**:关键查询字段建立索引
|
||||
|
||||
355
docs/domains/01-account.md
Normal file
355
docs/domains/01-account.md
Normal file
@ -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<String>, // 银行名称
|
||||
pub consistency_mode: ConsistencyMode, // 一致性模式
|
||||
pub outbound_control: OutboundControl, // 出金管控模式
|
||||
pub status: AccountStatus, // 账户状态
|
||||
pub created_at: DateTime<Utc>, // 创建时间
|
||||
pub updated_at: DateTime<Utc>, // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
- `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<DateTime<Utc>>, // 有效期开始
|
||||
pub valid_to: Option<DateTime<Utc>>, // 有效期结束
|
||||
pub status: AccountStatus, // 账户状态
|
||||
pub created_at: DateTime<Utc>, // 创建时间
|
||||
pub updated_at: DateTime<Utc>, // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
- `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<Value>, // 银企直连配置
|
||||
pub third_party_config: Option<Value>, // 第三方支付配置
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 临时账户池 (SubAccountPool)
|
||||
|
||||
管理临时子账户的池,支持批量创建和自动销户。
|
||||
|
||||
```rust
|
||||
pub struct SubAccountPool {
|
||||
pub id: i64,
|
||||
pub physical_account_id: i64,
|
||||
pub name: String,
|
||||
pub valid_from: DateTime<Utc>,
|
||||
pub valid_to: DateTime<Utc>,
|
||||
pub auto_close_rule: Option<Value>, // 自动销户规则
|
||||
}
|
||||
```
|
||||
|
||||
## 三、枚举类型
|
||||
|
||||
### 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<PhysicalAccount>;
|
||||
|
||||
// 获取实体账户
|
||||
pub async fn get_physical_account(&self, id: i64) -> Result<PhysicalAccount>;
|
||||
|
||||
// 获取实体账户列表(分页)
|
||||
pub async fn list_physical_accounts_paginated(
|
||||
&self,
|
||||
status: Option<AccountStatus>,
|
||||
keyword: Option<String>,
|
||||
page: i32,
|
||||
page_size: i32
|
||||
) -> Result<(Vec<PhysicalAccount>, 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<VirtualSubAccount>;
|
||||
|
||||
// 批量创建子账户
|
||||
pub async fn batch_create_sub_accounts(&self, req: BatchCreateSubAccountRequest) -> Result<Vec<VirtualSubAccount>>;
|
||||
|
||||
// 关闭子账户
|
||||
pub async fn close_sub_account(&self, id: i64) -> Result<()>;
|
||||
|
||||
// 获取实体账户下的子账户列表
|
||||
pub async fn list_sub_accounts(&self, physical_account_id: i64) -> Result<Vec<VirtualSubAccount>>;
|
||||
}
|
||||
```
|
||||
|
||||
## 五、仓储接口
|
||||
|
||||
### 5.1 PhysicalAccountRepository
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait PhysicalAccountRepository: Send + Sync {
|
||||
async fn create(&self, account: &PhysicalAccount) -> Result<i64>;
|
||||
async fn find_by_id(&self, id: i64) -> Result<Option<PhysicalAccount>>;
|
||||
async fn find_by_account_no(&self, account_no: &str) -> Result<Option<PhysicalAccount>>;
|
||||
async fn find_all(&self) -> Result<Vec<PhysicalAccount>>;
|
||||
async fn find_paginated(&self, status: Option<AccountStatus>, keyword: Option<String>, offset: i64, limit: i64) -> Result<Vec<PhysicalAccount>>;
|
||||
async fn count(&self, status: Option<AccountStatus>, keyword: Option<String>) -> Result<i64>;
|
||||
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<i64>;
|
||||
async fn find_by_id(&self, id: i64) -> Result<Option<VirtualSubAccount>>;
|
||||
async fn find_by_physical_account(&self, physical_account_id: i64) -> Result<Vec<VirtualSubAccount>>;
|
||||
async fn find_expired_temporary(&self) -> Result<Vec<VirtualSubAccount>>;
|
||||
async fn update_status(&self, id: i64, status: AccountStatus) -> Result<()>;
|
||||
async fn batch_create(&self, accounts: &[VirtualSubAccount]) -> Result<Vec<i64>>;
|
||||
}
|
||||
```
|
||||
|
||||
## 六、业务规则
|
||||
|
||||
### 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?;
|
||||
```
|
||||
|
||||
488
docs/domains/02-ledger.md
Normal file
488
docs/domains/02-ledger.md
Normal file
@ -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<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 会计科目 (AccountingSubject)
|
||||
|
||||
```rust
|
||||
pub struct AccountingSubject {
|
||||
pub code: String, // 科目代码
|
||||
pub name: String, // 科目名称
|
||||
pub category: SubjectCategory, // 科目类别
|
||||
pub direction_default: i8, // 默认增加方向
|
||||
pub parent_code: Option<String>,// 父科目代码
|
||||
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<Utc>, // 记账时间
|
||||
pub description: Option<String>,// 摘要描述
|
||||
pub status: EntryStatus, // 状态
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
### 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<DeductionResult> {
|
||||
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<AccountBalance>;
|
||||
|
||||
// 冻结金额
|
||||
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<DeductionResult>;
|
||||
|
||||
// 建立在途
|
||||
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<LedgerEntry>;
|
||||
|
||||
// 获取账户分录
|
||||
pub async fn get_account_entries(&self, account_id: i64, account_type: AccountType) -> Result<Vec<LedgerEntry>>;
|
||||
|
||||
// ========== 科目操作 ==========
|
||||
|
||||
// 获取所有科目
|
||||
pub async fn list_subjects(&self) -> Result<Vec<AccountingSubject>>;
|
||||
|
||||
// 初始化预定义科目
|
||||
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. **告警通知**:大额差异触发告警
|
||||
|
||||
481
docs/domains/03-transaction.md
Normal file
481
docs/domains/03-transaction.md
Normal file
@ -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<i64>, // 转出账户ID
|
||||
pub to_account_id: Option<i64>, // 转入账户ID
|
||||
pub amount: Decimal, // 金额
|
||||
pub status: TransactionStatus, // 状态
|
||||
pub bank_ref_no: Option<String>, // 银行交易号 (BankTxId)
|
||||
pub source_key: Option<String>, // 来源幂等键 (SourceKey)
|
||||
pub remark: Option<String>, // 备注
|
||||
pub created_at: DateTime<Utc>, // 创建时间
|
||||
pub confirmed_at: Option<DateTime<Utc>>, // 确认时间
|
||||
pub submitted_at: Option<DateTime<Utc>>, // 提交银行时间
|
||||
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<String>,// 对手方名称
|
||||
pub counterparty_account: Option<String>,// 对手方账号
|
||||
pub txn_time: DateTime<Utc>, // 交易时间
|
||||
pub sync_time: DateTime<Utc>, // 同步时间
|
||||
pub match_status: MatchStatus, // 匹配状态
|
||||
pub matched_txn_no: Option<String>, // 匹配的系统交易号
|
||||
pub remark: Option<String>, // 摘要
|
||||
}
|
||||
```
|
||||
|
||||
## 四、枚举类型
|
||||
|
||||
### 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<SystemTransaction>;
|
||||
|
||||
// 创建充值交易
|
||||
pub async fn deposit(&self, req: DepositRequest) -> Result<SystemTransaction>;
|
||||
|
||||
// 创建提现交易
|
||||
pub async fn withdraw(&self, req: WithdrawRequest) -> Result<SystemTransaction>;
|
||||
|
||||
// ========== 交易查询 ==========
|
||||
|
||||
// 获取交易详情
|
||||
pub async fn get_transaction(&self, id: i64) -> Result<SystemTransaction>;
|
||||
|
||||
// 根据交易号查询
|
||||
pub async fn get_by_txn_no(&self, txn_no: &str) -> Result<SystemTransaction>;
|
||||
|
||||
// 查询交易列表
|
||||
pub async fn list_transactions(&self, query: TransactionQuery) -> Result<Vec<SystemTransaction>>;
|
||||
|
||||
// ========== 状态管理 ==========
|
||||
|
||||
// 提交到银行
|
||||
pub async fn submit_to_bank(&self, id: i64) -> Result<String>;
|
||||
|
||||
// 确认交易
|
||||
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<Option<SystemTransaction>>;
|
||||
}
|
||||
```
|
||||
|
||||
## 七、仓储接口
|
||||
|
||||
### 7.1 SystemTransactionRepository
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait SystemTransactionRepository: Send + Sync {
|
||||
async fn create(&self, req: &CreateSystemTransactionRequest) -> Result<SystemTransaction>;
|
||||
async fn find_by_id(&self, id: i64) -> Result<Option<SystemTransaction>>;
|
||||
async fn find_by_txn_no(&self, txn_no: &str) -> Result<Option<SystemTransaction>>;
|
||||
async fn find_by_bank_ref_no(&self, bank_ref_no: &str) -> Result<Option<SystemTransaction>>;
|
||||
async fn find_by_source_key(&self, source_key: &str) -> Result<Option<SystemTransaction>>;
|
||||
async fn find_by_status(&self, status: TransactionStatus) -> Result<Vec<SystemTransaction>>;
|
||||
async fn find_pending(&self) -> Result<Vec<SystemTransaction>>;
|
||||
async fn find_needs_reconciliation(&self) -> Result<Vec<SystemTransaction>>;
|
||||
async fn query(&self, query: &TransactionQuery) -> Result<Vec<SystemTransaction>>;
|
||||
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<i64>;
|
||||
async fn find_by_bank_ref_no(&self, bank_ref_no: &str) -> Result<Option<BankTransaction>>;
|
||||
async fn find_unmatched(&self) -> Result<Vec<BankTransaction>>;
|
||||
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 | 乐观锁冲突 | 重试 |
|
||||
|
||||
454
docs/domains/04-reconciliation.md
Normal file
454
docs/domains/04-reconciliation.md
Normal file
@ -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<Utc>, // 创建时间
|
||||
pub completed_at: Option<DateTime<Utc>>, // 完成时间
|
||||
|
||||
// 三账对账结果
|
||||
pub bank_total: Option<Decimal>, // 银行账汇总
|
||||
pub transit_net: Option<Decimal>, // 在途净额
|
||||
pub ledger_total: Option<Decimal>, // 总账汇总
|
||||
pub three_account_balanced: Option<bool>, // 三账是否平衡
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
|
||||
```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<Decimal> {
|
||||
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<String>, // 系统交易号
|
||||
pub bank_ref_no: Option<String>, // 银行参考号
|
||||
pub system_amount: Option<Decimal>, // 系统金额
|
||||
pub bank_amount: Option<Decimal>, // 银行金额
|
||||
pub diff_amount: Decimal, // 差异金额
|
||||
pub status: ReconciliationItemStatus, // 状态
|
||||
pub remark: Option<String>, // 处理备注
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
|
||||
```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<String>, // 关联交易号
|
||||
pub reconciliation_item_id: Option<i64>,// 关联对账项ID
|
||||
pub adjustment_type: AdjustmentType, // 补录类型
|
||||
pub account_id: i64, // 账户ID
|
||||
pub amount: Decimal, // 金额
|
||||
pub reason: String, // 原因说明
|
||||
pub operator: String, // 操作人
|
||||
pub approver: Option<String>, // 审批人
|
||||
pub status: AdjustmentStatus, // 状态
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub approved_at: Option<DateTime<Utc>>, // 审批时间
|
||||
}
|
||||
```
|
||||
|
||||
## 四、枚举类型
|
||||
|
||||
### 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<ReconciliationBatch>;
|
||||
|
||||
// 获取对账批次
|
||||
pub async fn get_batch(&self, id: i64) -> Result<ReconciliationBatch>;
|
||||
|
||||
// 获取对账明细
|
||||
pub async fn get_batch_items(&self, batch_id: i64) -> Result<Vec<ReconciliationItem>>;
|
||||
|
||||
// ========== 三账校验 ==========
|
||||
|
||||
// 执行三账校验
|
||||
pub async fn verify_three_accounts(&self, physical_account_id: i64) -> Result<ThreeAccountVerificationResult>;
|
||||
|
||||
// 更新批次三账结果
|
||||
pub async fn update_three_account_result(&self, batch_id: i64, result: &ThreeAccountResult) -> Result<()>;
|
||||
|
||||
// ========== 手工补录 ==========
|
||||
|
||||
// 创建手工补录
|
||||
pub async fn create_adjustment(&self, req: CreateManualAdjustmentRequest) -> Result<ManualAdjustment>;
|
||||
|
||||
// 审批补录
|
||||
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<Vec<ManualAdjustment>>;
|
||||
|
||||
// ========== 统计查询 ==========
|
||||
|
||||
// 获取对账统计
|
||||
pub async fn get_stats(&self, batch_id: i64) -> Result<ReconciliationStats>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 三账校验实现
|
||||
|
||||
```rust
|
||||
pub async fn verify_three_accounts(&self, physical_account_id: i64) -> Result<ThreeAccountVerificationResult> {
|
||||
// 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% | 记录日志 |
|
||||
| 严重 | 三账不平衡 | 通知运营 |
|
||||
| 紧急 | 大额差异 | 升级处理 |
|
||||
|
||||
488
docs/domains/05-compensation.md
Normal file
488
docs/domains/05-compensation.md
Normal file
@ -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<DateTime<Utc>>,// 下次重试时间
|
||||
pub error_message: Option<String>, // 错误信息
|
||||
pub created_at: DateTime<Utc>, // 创建时间
|
||||
pub updated_at: DateTime<Utc>, // 更新时间
|
||||
pub completed_at: Option<DateTime<Utc>>, // 完成时间
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
|
||||
```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<Utc> {
|
||||
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<TimeoutDetectionResult>;
|
||||
|
||||
// ========== 任务管理 ==========
|
||||
|
||||
// 创建补偿任务
|
||||
pub async fn create_compensation_task(&self, txn_no: &str, task_type: CompensationTaskType) -> Result<CompensationTask>;
|
||||
|
||||
// 处理单个补偿任务
|
||||
pub async fn process_task(&self, task_id: i64) -> Result<CompensationResult>;
|
||||
|
||||
// 批量处理待处理任务
|
||||
pub async fn process_pending_tasks(&self) -> Result<Vec<CompensationResult>>;
|
||||
|
||||
// ========== 重试管理 ==========
|
||||
|
||||
// 处理待重试任务
|
||||
pub async fn process_ready_for_retry(&self) -> Result<Vec<CompensationResult>>;
|
||||
|
||||
// 标记任务完成
|
||||
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<Vec<CompensationResult>>;
|
||||
|
||||
// 移入死信队列
|
||||
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<TimeoutDetectionResult> {
|
||||
// 查找需要检查超时的交易
|
||||
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<CompensationResult> {
|
||||
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<CompensationResult> {
|
||||
// 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<i64>;
|
||||
|
||||
// 查询
|
||||
async fn find_by_id(&self, id: i64) -> Result<Option<CompensationTask>>;
|
||||
async fn find_by_txn_no(&self, txn_no: &str) -> Result<Vec<CompensationTask>>;
|
||||
async fn find_pending(&self) -> Result<Vec<CompensationTask>>;
|
||||
async fn find_ready_for_retry(&self) -> Result<Vec<CompensationTask>>;
|
||||
async fn find_dead_letter(&self) -> Result<Vec<CompensationTask>>;
|
||||
|
||||
// 状态更新
|
||||
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<Utc>) -> 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<bool>;
|
||||
}
|
||||
```
|
||||
|
||||
## 八、数据库表结构
|
||||
|
||||
### 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. 手动干预:提供手动处理入口
|
||||
|
||||
503
docs/domains/06-points.md
Normal file
503
docs/domains/06-points.md
Normal file
@ -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<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
|
||||
```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<String>, // 关联业务ID
|
||||
pub remark: Option<String>, // 备注
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
### 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<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
**规则配置示例**:
|
||||
|
||||
```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<Vec<PointsAccount>>;
|
||||
|
||||
// 创建积分账户
|
||||
pub async fn create_account(&self, req: CreatePointsAccountRequest) -> Result<PointsAccount>;
|
||||
|
||||
// ========== 积分操作 ==========
|
||||
|
||||
// 获取积分
|
||||
pub async fn earn_points(&self, req: PointsTransactionRequest) -> Result<PointsTransaction>;
|
||||
|
||||
// 消费积分
|
||||
pub async fn spend_points(&self, req: PointsTransactionRequest) -> Result<PointsTransaction>;
|
||||
|
||||
// 转移积分
|
||||
pub async fn transfer_points(&self, req: PointsTransferRequest) -> Result<(PointsTransaction, PointsTransaction)>;
|
||||
|
||||
// 调整积分
|
||||
pub async fn adjust_points(&self, req: PointsTransactionRequest) -> Result<PointsTransaction>;
|
||||
|
||||
// ========== 过期处理 ==========
|
||||
|
||||
// 处理过期积分
|
||||
pub async fn process_expired_points(&self) -> Result<i32>;
|
||||
|
||||
// ========== 查询 ==========
|
||||
|
||||
// 查询积分交易
|
||||
pub async fn list_transactions(&self, query: PointsTransactionQuery) -> Result<Vec<PointsTransaction>>;
|
||||
|
||||
// 获取积分统计
|
||||
pub async fn get_statistics(&self, account_id: i64) -> Result<PointsStatistics>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 积分获取实现
|
||||
|
||||
```rust
|
||||
pub async fn earn_points(&self, req: PointsTransactionRequest) -> Result<PointsTransaction> {
|
||||
// 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<i64>;
|
||||
async fn find_by_id(&self, id: i64) -> Result<Option<PointsAccount>>;
|
||||
async fn find_by_sub_account(&self, sub_account_id: i64) -> Result<Vec<PointsAccount>>;
|
||||
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<i64>;
|
||||
async fn find_by_account(&self, account_id: i64, query: &PointsTransactionQuery) -> Result<Vec<PointsTransaction>>;
|
||||
async fn find_by_txn_no(&self, txn_no: &str) -> Result<Option<PointsTransaction>>;
|
||||
}
|
||||
```
|
||||
|
||||
## 七、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. **过期处理**:定期扫描处理过期积分
|
||||
|
||||
356
docs/integration/frontend-backend.md
Normal file
356
docs/integration/frontend-backend.md
Normal file
@ -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<Transaction> {
|
||||
return apiClient.post('/transactions/deposit', data)
|
||||
}
|
||||
|
||||
// 提现
|
||||
static async createWithdraw(data: WithdrawRequest): Promise<Transaction> {
|
||||
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<void> {
|
||||
return apiClient.post(`/reconciliation/adjustments/${id}/approve`, { approver })
|
||||
}
|
||||
|
||||
// 拒绝补录
|
||||
static async rejectAdjustment(id: number, approver: string, reason: string): Promise<void> {
|
||||
return apiClient.post(`/reconciliation/adjustments/${id}/reject`, { approver, reason })
|
||||
}
|
||||
|
||||
// 获取待审批补录
|
||||
static async getPendingAdjustments(): Promise<ManualAdjustment[]> {
|
||||
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 数据是否更新
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user