- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
313 lines
10 KiB
Rust
313 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::domain::transaction::entity::TransactionStatus;
|
||
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(),
|
||
}
|
||
}
|
||
|
||
// ==================== 场景1:银行拒绝交易 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_bank_rejection() {
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
|
||
let original_bank = balance.bank_balance;
|
||
|
||
let client = MockBankClient::with_failure_config(FailureConfig::force_failure());
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
|
||
// 扣款并建立在途
|
||
let withdrawal = dec!(2000.00);
|
||
balance.deduct_with_priority(withdrawal).unwrap();
|
||
balance.add_transit(withdrawal);
|
||
|
||
// 银行拒绝
|
||
let request = BankTransferRequest {
|
||
from_account: "PRISON001".to_string(),
|
||
to_account: "FAMILY001".to_string(),
|
||
to_account_name: "家属".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: withdrawal,
|
||
remark: None,
|
||
business_no: "REJECT001".to_string(),
|
||
};
|
||
|
||
let response = client.transfer(request).await.unwrap();
|
||
assert!(!response.success);
|
||
assert!(response.error_code.is_some());
|
||
|
||
// 回退在途
|
||
balance.rollback_transit(withdrawal);
|
||
|
||
// 验证余额恢复
|
||
assert_eq!(balance.bank_balance, original_bank);
|
||
assert_eq!(balance.transit_amount, dec!(0.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
|
||
// ==================== 场景2:余额不足 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_insufficient_bank_balance() {
|
||
let client = MockBankClient::new();
|
||
client.create_account("FROM001", "转出账户", dec!(100.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!(1000.00), // 超过余额
|
||
remark: None,
|
||
business_no: "INSUF001".to_string(),
|
||
};
|
||
|
||
let response = client.transfer(request).await.unwrap();
|
||
assert!(!response.success);
|
||
assert_eq!(response.error_code, Some("INSUFFICIENT_BALANCE".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_insufficient_system_balance() {
|
||
let mut balance = create_balance(dec!(500.00), dec!(300.00));
|
||
|
||
// 尝试扣款超过可用余额
|
||
let result = balance.deduct_with_priority(dec!(1000.00));
|
||
assert!(result.is_err());
|
||
|
||
// 余额不变
|
||
assert_eq!(balance.personal_balance, dec!(500.00));
|
||
assert_eq!(balance.labor_balance, dec!(300.00));
|
||
}
|
||
|
||
// ==================== 场景3:账户不存在 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_account_not_found() {
|
||
let client = MockBankClient::new();
|
||
|
||
let request = BankTransferRequest {
|
||
from_account: "NONEXISTENT".to_string(),
|
||
to_account: "TO001".to_string(),
|
||
to_account_name: "转入".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: dec!(100.00),
|
||
remark: None,
|
||
business_no: "NOTFOUND001".to_string(),
|
||
};
|
||
|
||
let response = client.transfer(request).await.unwrap();
|
||
assert!(!response.success);
|
||
assert_eq!(response.error_code, Some("ACCOUNT_NOT_FOUND".to_string()));
|
||
}
|
||
|
||
// ==================== 场景4:部分失败回滚 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_partial_failure_rollback() {
|
||
// 多笔交易中部分失败的处理
|
||
// 使用余额不足来触发第三笔失败
|
||
|
||
let mut balances = vec![
|
||
create_balance(dec!(5000.00), dec!(3000.00)),
|
||
create_balance(dec!(4000.00), dec!(2000.00)),
|
||
create_balance(dec!(3000.00), dec!(1000.00)),
|
||
];
|
||
|
||
let client = MockBankClient::new();
|
||
// 监狱账户余额只够前两笔
|
||
client.create_account("PRISON001", "监狱账户", dec!(2000.00)).unwrap();
|
||
client.create_account("FAMILY001", "家属1", dec!(0.00)).unwrap();
|
||
client.create_account("FAMILY002", "家属2", dec!(0.00)).unwrap();
|
||
client.create_account("FAMILY003", "家属3", dec!(0.00)).unwrap();
|
||
|
||
let amounts = vec![dec!(1000.00), dec!(1000.00), dec!(1000.00)];
|
||
let families = vec!["FAMILY001", "FAMILY002", "FAMILY003"];
|
||
|
||
let mut results = vec![];
|
||
|
||
for i in 0..3 {
|
||
balances[i].deduct_with_priority(amounts[i]).unwrap();
|
||
balances[i].add_transit(amounts[i]);
|
||
|
||
let request = BankTransferRequest {
|
||
from_account: "PRISON001".to_string(),
|
||
to_account: families[i].to_string(),
|
||
to_account_name: format!("家属{}", i + 1),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: amounts[i],
|
||
remark: None,
|
||
business_no: format!("PARTIAL_{}", i),
|
||
};
|
||
|
||
let response = client.transfer(request).await.unwrap();
|
||
results.push(response.success);
|
||
|
||
if response.success {
|
||
balances[i].settle_transit(amounts[i]).unwrap();
|
||
} else {
|
||
balances[i].rollback_transit(amounts[i]);
|
||
}
|
||
}
|
||
|
||
// 验证:前两笔成功,第三笔失败(余额不足)
|
||
assert!(results[0]);
|
||
assert!(results[1]);
|
||
assert!(!results[2]); // 余额不足
|
||
|
||
// 验证余额
|
||
// 第一笔成功:在途清零
|
||
assert_eq!(balances[0].transit_amount, dec!(0.00));
|
||
// 第二笔成功:在途清零
|
||
assert_eq!(balances[1].transit_amount, dec!(0.00));
|
||
// 第三笔失败回滚:余额恢复
|
||
// 初始: personal=3000, labor=1000, bank=4000
|
||
// deduct_with_priority(1000): personal=2000, bank=3000
|
||
// rollback_transit(1000): personal=3000, bank=4000
|
||
assert_eq!(balances[2].transit_amount, dec!(0.00));
|
||
assert_eq!(balances[2].personal_balance, dec!(3000.00)); // 回滚后恢复到 deduct 前的状态
|
||
assert_eq!(balances[2].bank_balance, dec!(4000.00)); // bank_balance 也恢复
|
||
|
||
for balance in &balances {
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
}
|
||
|
||
// ==================== 场景5:重复交易处理 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_duplicate_transaction_handling() {
|
||
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!(1000.00),
|
||
remark: None,
|
||
business_no: "DUP001".to_string(),
|
||
};
|
||
|
||
let response1 = client.transfer(request.clone()).await.unwrap();
|
||
assert!(response1.success);
|
||
|
||
// 检查幂等性:通过 business_no 查询
|
||
let status = client.query_transaction_status("DUP001").await.unwrap();
|
||
assert!(status.success);
|
||
assert_eq!(status.bank_ref_no, response1.bank_ref_no);
|
||
|
||
// 验证只扣款一次
|
||
let balance = client.query_balance("FROM001").await.unwrap();
|
||
assert_eq!(balance.balance, dec!(9000.00));
|
||
}
|
||
|
||
// ==================== 场景6:连续失败后成功 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_retry_after_failures() {
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
|
||
|
||
// 第一次尝试(失败)
|
||
let client_fail = MockBankClient::with_failure_config(FailureConfig::force_failure());
|
||
client_fail.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
|
||
balance.deduct_with_priority(dec!(1000.00)).unwrap();
|
||
balance.add_transit(dec!(1000.00));
|
||
|
||
let request = BankTransferRequest {
|
||
from_account: "PRISON001".to_string(),
|
||
to_account: "FAMILY001".to_string(),
|
||
to_account_name: "家属".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: dec!(1000.00),
|
||
remark: None,
|
||
business_no: "RETRY001".to_string(),
|
||
};
|
||
|
||
let response1 = client_fail.transfer(request.clone()).await.unwrap();
|
||
assert!(!response1.success);
|
||
|
||
// 回滚
|
||
balance.rollback_transit(dec!(1000.00));
|
||
assert_eq!(balance.bank_balance, dec!(8000.00));
|
||
|
||
// 第二次尝试(成功)
|
||
let client_success = MockBankClient::new();
|
||
client_success.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
client_success.create_account("FAMILY001", "家属", dec!(0.00)).unwrap();
|
||
|
||
balance.deduct_with_priority(dec!(1000.00)).unwrap();
|
||
balance.add_transit(dec!(1000.00));
|
||
|
||
let request2 = BankTransferRequest {
|
||
business_no: "RETRY002".to_string(), // 新的业务号
|
||
..request
|
||
};
|
||
|
||
let response2 = client_success.transfer(request2).await.unwrap();
|
||
assert!(response2.success);
|
||
|
||
balance.settle_transit(dec!(1000.00)).unwrap();
|
||
|
||
// 验证最终状态
|
||
assert_eq!(balance.bank_balance, dec!(7000.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
|
||
// ==================== 场景7:冻结余额保护 ====================
|
||
|
||
#[test]
|
||
fn test_frozen_balance_protection() {
|
||
let mut balance = create_balance(dec!(3000.00), dec!(2000.00));
|
||
|
||
// 冻结 2500
|
||
balance.freeze(dec!(2500.00));
|
||
|
||
// 可用余额现在只有 2500
|
||
assert_eq!(balance.available_balance(), dec!(2500.00));
|
||
|
||
// 尝试扣款 3000(超过可用)
|
||
let result = balance.deduct_with_priority(dec!(3000.00));
|
||
assert!(result.is_err());
|
||
|
||
// 冻结金额不受影响
|
||
assert_eq!(balance.frozen_balance, dec!(2500.00));
|
||
|
||
// 可以扣款 2000
|
||
let result = balance.deduct_with_priority(dec!(2000.00));
|
||
assert!(result.is_ok());
|
||
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
|