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

330 lines
11 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(),
}
}
/// 模拟交易
struct MockTransaction {
txn_no: String,
amount: Decimal,
status: TransactionStatus,
bank_ref_no: Option<String>,
}
impl MockTransaction {
fn new(txn_no: &str, amount: Decimal) -> Self {
Self {
txn_no: txn_no.to_string(),
amount,
status: TransactionStatus::Created,
bank_ref_no: None,
}
}
fn set_bank_submitted(&mut self) {
self.status = TransactionStatus::BankSubmitted;
}
fn set_timeout(&mut self) {
self.status = TransactionStatus::Timeout;
}
fn set_success(&mut self, bank_ref_no: String) {
self.status = TransactionStatus::Success;
self.bank_ref_no = Some(bank_ref_no);
}
fn set_failed(&mut self) {
self.status = TransactionStatus::Failed;
}
}
// ==================== 场景1超时后对账发现成功 ====================
#[tokio::test]
async fn test_timeout_then_reconcile_success() {
// ========== 1. 初始化 ==========
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
let mut txn = MockTransaction::new("TIMEOUT001", dec!(2000.00));
let withdrawal_amount = txn.amount;
// 使用正常银行(模拟银行实际成功但响应超时)
let client = MockBankClient::new();
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
client.create_account("FAMILY001", "家属账户", dec!(0.00)).unwrap();
// ========== 2. 创建交易并提交 ==========
balance.deduct_with_priority(withdrawal_amount).unwrap();
balance.add_transit(withdrawal_amount);
txn.set_bank_submitted();
// 实际提交银行(成功)
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_amount,
remark: None,
business_no: txn.txn_no.clone(),
};
let response = client.transfer(request).await.unwrap();
let actual_bank_ref = response.bank_ref_no.clone();
// ========== 3. 模拟系统超时(未收到响应) ==========
// 实际上银行成功了,但系统认为超时
txn.set_timeout();
assert_eq!(txn.status, TransactionStatus::Timeout);
assert_eq!(balance.transit_amount, dec!(2000.00));
// ========== 4. 对账发现银行已成功 ==========
// 查询银行流水或交易状态
let bank_status = client.query_transaction_status(&txn.txn_no).await.unwrap();
assert!(bank_status.success);
// ========== 5. 根据对账结果更新状态 ==========
// 对账确认:银行已成功,更新交易状态
txn.set_success(bank_status.bank_ref_no.unwrap());
// 结转在途
balance.settle_transit(withdrawal_amount).unwrap();
// ========== 6. 验证最终状态 ==========
assert_eq!(txn.status, TransactionStatus::Success);
assert_eq!(balance.transit_amount, dec!(0.00));
assert_eq!(balance.bank_balance, dec!(6000.00));
assert!(balance.validate_invariant().is_ok());
// 银行余额正确
let prison_balance = client.query_balance("PRISON001").await.unwrap();
assert_eq!(prison_balance.balance, dec!(98000.00));
}
// ==================== 场景2超时后对账发现失败 ====================
#[tokio::test]
async fn test_timeout_then_reconcile_failed() {
// ========== 1. 初始化 ==========
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
let original_bank = balance.bank_balance;
let mut txn = MockTransaction::new("TIMEOUT002", dec!(2000.00));
let withdrawal_amount = txn.amount;
// 银行会失败
let client = MockBankClient::with_failure_config(FailureConfig::force_failure());
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
// ========== 2. 创建交易并提交 ==========
balance.deduct_with_priority(withdrawal_amount).unwrap();
balance.add_transit(withdrawal_amount);
txn.set_bank_submitted();
// 提交银行(失败)
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_amount,
remark: None,
business_no: txn.txn_no.clone(),
};
let _response = client.transfer(request).await.unwrap();
// ========== 3. 模拟系统超时 ==========
txn.set_timeout();
// ========== 4. 对账发现银行未成功 ==========
let bank_status = client.query_transaction_status(&txn.txn_no).await.unwrap();
// 交易不存在(因为失败了没记录)或标记失败
assert!(!bank_status.success || bank_status.error_code == Some("TXN_NOT_FOUND".to_string()));
// ========== 5. 回退在途 ==========
txn.set_failed();
balance.rollback_transit(withdrawal_amount);
// ========== 6. 验证:余额恢复 ==========
assert_eq!(txn.status, TransactionStatus::Failed);
assert_eq!(balance.transit_amount, dec!(0.00));
assert_eq!(balance.bank_balance, original_bank);
assert!(balance.validate_invariant().is_ok());
// 银行余额未变
let prison_balance = client.query_balance("PRISON001").await.unwrap();
assert_eq!(prison_balance.balance, dec!(100000.00));
}
// ==================== 场景3多笔超时交易批量处理 ====================
#[tokio::test]
async fn test_batch_timeout_processing() {
// 模拟多笔超时交易的批量处理
let client = MockBankClient::new();
client.create_account("PRISON001", "监狱账户", dec!(1000000.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 mut balances = vec![
create_balance(dec!(10000.00), dec!(5000.00)),
create_balance(dec!(8000.00), dec!(4000.00)),
create_balance(dec!(6000.00), dec!(3000.00)),
];
let amounts = vec![dec!(3000.00), dec!(2000.00), dec!(1500.00)];
let family_accounts = vec!["FAMILY001", "FAMILY002", "FAMILY003"];
// 所有交易都成功但模拟超时
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: family_accounts[i].to_string(),
to_account_name: format!("家属{}", i + 1),
to_bank_code: "MOCK".to_string(),
amount: amounts[i],
remark: None,
business_no: format!("BATCH_{}", i),
};
let response = client.transfer(request).await.unwrap();
assert!(response.success);
}
// 批量对账确认
for i in 0..3 {
let status = client.query_transaction_status(&format!("BATCH_{}", i)).await.unwrap();
assert!(status.success);
// 结转在途
balances[i].settle_transit(amounts[i]).unwrap();
assert!(balances[i].validate_invariant().is_ok());
}
// 验证所有余额正确
for i in 0..3 {
assert_eq!(balances[i].transit_amount, dec!(0.00));
}
}
// ==================== 场景4超时期间新交易处理 ====================
#[tokio::test]
async fn test_new_transaction_during_timeout() {
// 当有超时交易时,新交易应该正常处理
let mut balance = create_balance(dec!(10000.00), dec!(5000.00));
let client = MockBankClient::new();
client.create_account("PRISON001", "监狱账户", dec!(1000000.00)).unwrap();
client.create_account("FAMILY001", "家属1", dec!(0.00)).unwrap();
client.create_account("FAMILY002", "家属2", dec!(0.00)).unwrap();
// ========== 1. 第一笔交易超时 ==========
balance.deduct_with_priority(dec!(3000.00)).unwrap();
balance.add_transit(dec!(3000.00));
let request1 = BankTransferRequest {
from_account: "PRISON001".to_string(),
to_account: "FAMILY001".to_string(),
to_account_name: "家属1".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(3000.00),
remark: None,
business_no: "TIMEOUT_TXN".to_string(),
};
client.transfer(request1).await.unwrap();
// 假设这笔超时了,在途还在
// ========== 2. 新交易正常处理 ==========
// 可用余额:(10000 - 3000) + 5000 = 12000
assert_eq!(balance.available_balance(), dec!(12000.00));
balance.deduct_with_priority(dec!(2000.00)).unwrap();
balance.add_transit(dec!(2000.00));
let request2 = BankTransferRequest {
from_account: "PRISON001".to_string(),
to_account: "FAMILY002".to_string(),
to_account_name: "家属2".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(2000.00),
remark: None,
business_no: "NEW_TXN".to_string(),
};
let response2 = client.transfer(request2).await.unwrap();
assert!(response2.success);
// 新交易成功,结转
balance.settle_transit(dec!(2000.00)).unwrap();
// ========== 3. 超时交易后续确认 ==========
let status1 = client.query_transaction_status("TIMEOUT_TXN").await.unwrap();
assert!(status1.success);
balance.settle_transit(dec!(3000.00)).unwrap();
// ========== 4. 验证最终状态 ==========
assert_eq!(balance.transit_amount, dec!(0.00));
// 15000 - 3000 - 2000 = 10000
assert_eq!(balance.bank_balance, dec!(10000.00));
assert!(balance.validate_invariant().is_ok());
}
// ==================== 场景5超时阈值边界 ====================
#[test]
fn test_timeout_threshold_boundary() {
// 测试超时阈值判断逻辑
use chrono::Duration;
let submitted_at = Utc::now() - Duration::seconds(300); // 5分钟前
let timeout_threshold = Duration::seconds(300); // 5分钟阈值
// 刚好超时
let is_timeout = Utc::now() - submitted_at >= timeout_threshold;
assert!(is_timeout);
// 未超时
let submitted_recent = Utc::now() - Duration::seconds(200);
let is_timeout_recent = Utc::now() - submitted_recent >= timeout_threshold;
assert!(!is_timeout_recent);
}