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

313 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::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());
}