- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
307 lines
10 KiB
Rust
307 lines
10 KiB
Rust
//! 在途流转集成测试
|
||
//!
|
||
//! 测试场景:
|
||
//! - 正常在途流转(可用 → 在途 → 结转)
|
||
//! - 在途回退(银行失败)
|
||
//! - 在途超时处理
|
||
|
||
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);
|
||
}
|
||
|