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