tangweijie d7f81893c5 Initial commit: 完整的 Rust 账户管理系统
- 实现账户管理改进设计文档中的所有核心功能
- 三科目余额管理 (个人余额、劳动报酬、冻结余额)
- 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed)
- 三键幂等体系 (JZTxId/BankTxId/SourceKey)
- 优先级扣款规则 (先个人后劳动)
- 在途资金管理 (可用→在途→结转/回退)
- 三账对账闭环 (总账 = 银行账 + 在途净额)
- 补偿服务域 (超时检测、重试、死信队列)
- 虚拟银行模拟器用于业务测试
- 完整的集成测试套件 (133 个测试全部通过)
- Docker 容器化部署配置
- 前端 Vue3 + TypeScript 项目结构
2026-01-05 17:56:01 +08:00

307 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 在途流转集成测试
//!
//! 测试场景:
//! - 正常在途流转(可用 → 在途 → 结转)
//! - 在途回退(银行失败)
//! - 在途超时处理
use chrono::Utc;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use rustjr::domain::ledger::entity::AccountBalance;
use rustjr::domain::account::AccountType;
use rustjr::infrastructure::bank_integration::mock_bank::{MockBankClient, FailureConfig};
use rustjr::infrastructure::bank_integration::{BankClient, BankTransferRequest};
// ==================== 测试辅助 ====================
/// 创建测试余额
fn create_balance(personal: Decimal, labor: Decimal) -> AccountBalance {
AccountBalance {
id: 1,
account_id: 1001,
account_type: AccountType::Virtual,
personal_balance: personal,
labor_balance: labor,
frozen_balance: Decimal::ZERO,
bank_balance: personal + labor,
transit_amount: Decimal::ZERO,
system_balance: personal + labor,
available_balance: personal + labor,
frozen_amount: Decimal::ZERO,
version: 1,
updated_at: Utc::now(),
}
}
// ==================== 正常在途流转测试 ====================
#[tokio::test]
async fn test_transit_normal_flow_success() {
// 1. 初始化余额
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
assert!(balance.validate_invariant().is_ok());
// 2. 按优先级扣款(模拟提现前扣款)
let deduction = balance.deduct_with_priority(dec!(300.00)).unwrap();
assert_eq!(deduction.from_personal, dec!(300.00));
assert_eq!(deduction.from_labor, dec!(0.00));
assert_eq!(balance.personal_balance, dec!(700.00));
assert_eq!(balance.bank_balance, dec!(1200.00));
assert!(balance.validate_invariant().is_ok());
// 3. 划转到在途
balance.add_transit(dec!(300.00));
assert_eq!(balance.transit_amount, dec!(300.00));
// 4. 模拟银行成功
let client = MockBankClient::new();
client.create_account("FROM001", "转出账户", dec!(10000.00)).unwrap();
client.create_account("TO001", "转入账户", dec!(0.00)).unwrap();
let request = BankTransferRequest {
from_account: "FROM001".to_string(),
to_account: "TO001".to_string(),
to_account_name: "转入账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(300.00),
remark: Some("提现".to_string()),
business_no: "TXN001".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(response.success);
// 5. 结转在途
balance.settle_transit(dec!(300.00)).unwrap();
assert_eq!(balance.transit_amount, dec!(0.00));
// 6. 验证最终状态
assert_eq!(balance.personal_balance, dec!(700.00));
assert_eq!(balance.labor_balance, dec!(500.00));
assert_eq!(balance.bank_balance, dec!(1200.00));
assert!(balance.validate_invariant().is_ok());
}
#[tokio::test]
async fn test_transit_deduction_from_both() {
// 测试扣款跨越个人和劳动余额
let mut balance = create_balance(dec!(200.00), dec!(500.00));
// 扣款 400应该从个人扣 200从劳动扣 200
let deduction = balance.deduct_with_priority(dec!(400.00)).unwrap();
assert_eq!(deduction.from_personal, dec!(200.00));
assert_eq!(deduction.from_labor, dec!(200.00));
assert_eq!(balance.personal_balance, dec!(0.00));
assert_eq!(balance.labor_balance, dec!(300.00));
assert!(balance.validate_invariant().is_ok());
}
// ==================== 在途回退测试 ====================
#[tokio::test]
async fn test_transit_rollback_on_bank_failure() {
// 1. 初始化
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
// 2. 扣款并建立在途
balance.deduct_with_priority(dec!(300.00)).unwrap();
balance.add_transit(dec!(300.00));
// 3. 模拟银行失败
let client = MockBankClient::with_failure_config(FailureConfig::force_failure());
client.create_account("FROM001", "转出账户", dec!(10000.00)).unwrap();
let request = BankTransferRequest {
from_account: "FROM001".to_string(),
to_account: "TO001".to_string(),
to_account_name: "转入账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(300.00),
remark: None,
business_no: "TXN002".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(!response.success);
// 4. 回退在途
balance.rollback_transit(dec!(300.00));
// 5. 验证:余额恢复
assert_eq!(balance.personal_balance, dec!(1000.00));
assert_eq!(balance.labor_balance, dec!(500.00));
assert_eq!(balance.transit_amount, dec!(0.00));
assert_eq!(balance.bank_balance, dec!(1500.00));
assert!(balance.validate_invariant().is_ok());
}
#[tokio::test]
async fn test_transit_partial_rollback() {
// 测试部分回退
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
// 建立多笔在途
balance.deduct_with_priority(dec!(300.00)).unwrap();
balance.add_transit(dec!(300.00));
balance.deduct_with_priority(dec!(200.00)).unwrap();
balance.add_transit(dec!(200.00));
assert_eq!(balance.transit_amount, dec!(500.00));
// 部分回退
balance.rollback_transit(dec!(200.00));
assert_eq!(balance.transit_amount, dec!(300.00));
assert_eq!(balance.personal_balance, dec!(700.00)); // 500 + 200 回退
assert!(balance.validate_invariant().is_ok());
}
// ==================== 在途不足测试 ====================
#[tokio::test]
async fn test_settle_transit_insufficient() {
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
balance.transit_amount = dec!(100.00);
// 尝试结转超过在途金额
let result = balance.settle_transit(dec!(200.00));
assert!(result.is_err());
// 在途金额不变
assert_eq!(balance.transit_amount, dec!(100.00));
}
// ==================== 并发场景测试 ====================
#[tokio::test]
async fn test_transit_multiple_transactions() {
// 模拟多笔交易的在途管理
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
// 交易1: 扣款并建立在途
balance.deduct_with_priority(dec!(1000.00)).unwrap();
balance.add_transit(dec!(1000.00));
assert!(balance.validate_invariant().is_ok());
// 交易2: 扣款并建立在途
balance.deduct_with_priority(dec!(500.00)).unwrap();
balance.add_transit(dec!(500.00));
assert!(balance.validate_invariant().is_ok());
// 交易1 成功
balance.settle_transit(dec!(1000.00)).unwrap();
assert!(balance.validate_invariant().is_ok());
// 交易2 失败,回退
balance.rollback_transit(dec!(500.00));
assert!(balance.validate_invariant().is_ok());
// 最终验证
assert_eq!(balance.transit_amount, dec!(0.00));
// 初始 8000扣 1000 成功,扣 500 回退 = 7000
assert_eq!(balance.bank_balance, dec!(7000.00));
}
// ==================== 边界条件测试 ====================
#[tokio::test]
async fn test_transit_zero_amount() {
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
// 零金额在途
balance.add_transit(dec!(0.00));
assert_eq!(balance.transit_amount, dec!(0.00));
// 零金额结转
let result = balance.settle_transit(dec!(0.00));
assert!(result.is_ok());
}
#[tokio::test]
async fn test_transit_exact_balance() {
let mut balance = create_balance(dec!(300.00), dec!(200.00));
// 扣光所有余额
let deduction = balance.deduct_with_priority(dec!(500.00)).unwrap();
assert_eq!(deduction.from_personal, dec!(300.00));
assert_eq!(deduction.from_labor, dec!(200.00));
assert_eq!(balance.available_balance(), dec!(0.00));
assert!(balance.validate_invariant().is_ok());
}
#[tokio::test]
async fn test_transit_insufficient_balance() {
let mut balance = create_balance(dec!(300.00), dec!(200.00));
// 尝试扣款超过余额
let result = balance.deduct_with_priority(dec!(600.00));
assert!(result.is_err());
// 余额不变
assert_eq!(balance.personal_balance, dec!(300.00));
assert_eq!(balance.labor_balance, dec!(200.00));
}
// ==================== 与虚拟银行交互测试 ====================
#[tokio::test]
async fn test_transit_with_mock_bank_query() {
let client = MockBankClient::new();
client.create_account("ACC001", "测试账户", dec!(10000.00)).unwrap();
// 查询初始余额
let balance = client.query_balance("ACC001").await.unwrap();
assert_eq!(balance.balance, dec!(10000.00));
// 执行转账
client.create_account("ACC002", "对手账户", dec!(0.00)).unwrap();
let request = BankTransferRequest {
from_account: "ACC001".to_string(),
to_account: "ACC002".to_string(),
to_account_name: "对手账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(3000.00),
remark: None,
business_no: "TXN003".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(response.success);
// 验证余额变化
let balance = client.query_balance("ACC001").await.unwrap();
assert_eq!(balance.balance, dec!(7000.00));
let balance = client.query_balance("ACC002").await.unwrap();
assert_eq!(balance.balance, dec!(3000.00));
}
#[tokio::test]
async fn test_transit_bank_ref_tracking() {
let client = MockBankClient::new();
client.create_account("ACC001", "测试账户", dec!(10000.00)).unwrap();
client.create_account("ACC002", "对手账户", dec!(0.00)).unwrap();
let request = BankTransferRequest {
from_account: "ACC001".to_string(),
to_account: "ACC002".to_string(),
to_account_name: "对手账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(1000.00),
remark: None,
business_no: "TXN004".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(response.success);
// 可以通过业务流水号查询交易状态
let status = client.query_transaction_status("TXN004").await.unwrap();
assert!(status.success);
assert_eq!(status.bank_ref_no, response.bank_ref_no);
}