rustjr-account-management/tests/scenarios/compensation_scenarios.rs
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

326 lines
9.2 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::{Duration, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use rustjr::domain::ledger::entity::AccountBalance;
use rustjr::domain::account::AccountType;
use rustjr::domain::compensation::{
CompensationTask, CompensationTaskStatus, CompensationTaskType,
};
// ==================== 测试辅助 ====================
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(),
}
}
fn create_compensation_task(txn_no: &str, task_type: CompensationTaskType) -> CompensationTask {
CompensationTask {
id: 0,
txn_no: txn_no.to_string(),
task_type,
status: CompensationTaskStatus::Pending,
retry_count: 0,
max_retries: 3,
next_retry_at: None,
error_message: None,
created_at: Utc::now(),
updated_at: Utc::now(),
completed_at: None,
}
}
// ==================== 场景1超时检测创建补偿任务 ====================
#[test]
fn test_timeout_detection_creates_task() {
// 模拟交易超时检测
// 交易已提交银行超过阈值时间
let submitted_at = Utc::now() - Duration::minutes(10);
let timeout_threshold = Duration::minutes(5);
let is_timeout = Utc::now() - submitted_at > timeout_threshold;
assert!(is_timeout);
// 创建补偿任务
let task = create_compensation_task(
"TXN_TIMEOUT_001",
CompensationTaskType::TimeoutCheck,
);
assert_eq!(task.status, CompensationTaskStatus::Pending);
assert_eq!(task.retry_count, 0);
assert_eq!(task.task_type, CompensationTaskType::TimeoutCheck);
}
// ==================== 场景2补偿任务成功处理 ====================
#[test]
fn test_compensation_task_success() {
let mut task = create_compensation_task(
"TXN_TIMEOUT_002",
CompensationTaskType::TimeoutCheck,
);
// 处理中
task.status = CompensationTaskStatus::Processing;
// 模拟对账查询成功
// 银行确认交易已成功
// 任务完成
task.status = CompensationTaskStatus::Completed;
task.completed_at = Some(Utc::now());
assert_eq!(task.status, CompensationTaskStatus::Completed);
}
// ==================== 场景3补偿任务重试 ====================
#[test]
fn test_compensation_task_retry() {
let mut task = create_compensation_task(
"TXN_TIMEOUT_003",
CompensationTaskType::TimeoutCheck,
);
// 第一次尝试失败
task.status = CompensationTaskStatus::Processing;
task.retry_count = 1;
task.error_message = Some("银行查询超时".to_string());
task.status = CompensationTaskStatus::Failed;
// 计算下次重试时间(指数退避)
let delay = Duration::minutes(2_i64.pow(task.retry_count as u32)); // 2^1 = 2分钟
task.next_retry_at = Some(Utc::now() + delay);
assert_eq!(task.status, CompensationTaskStatus::Failed);
assert_eq!(task.retry_count, 1);
assert!(task.next_retry_at.is_some());
}
// ==================== 场景4达到最大重试进入死信 ====================
#[test]
fn test_compensation_task_dead_letter() {
let mut task = create_compensation_task(
"TXN_TIMEOUT_004",
CompensationTaskType::TimeoutCheck,
);
// 模拟多次重试失败
for i in 0..task.max_retries {
task.retry_count = i + 1;
task.error_message = Some(format!("{}次尝试失败", i + 1));
}
// 达到最大重试次数
assert!(task.retry_count >= task.max_retries);
task.status = CompensationTaskStatus::DeadLetter;
assert_eq!(task.status, CompensationTaskStatus::DeadLetter);
}
// ==================== 场景5手动重试死信任务 ====================
#[test]
fn test_manual_retry_dead_letter() {
let mut task = create_compensation_task(
"TXN_TIMEOUT_005",
CompensationTaskType::TimeoutCheck,
);
// 设置为死信状态
task.retry_count = 3;
task.status = CompensationTaskStatus::DeadLetter;
task.error_message = Some("多次重试失败".to_string());
// 手动重试
task.status = CompensationTaskStatus::Pending;
task.retry_count = 0; // 重置重试次数
task.error_message = None;
task.next_retry_at = None;
assert_eq!(task.status, CompensationTaskStatus::Pending);
assert_eq!(task.retry_count, 0);
}
// ==================== 场景6补偿与余额更新 ====================
#[test]
fn test_compensation_with_balance_update() {
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
// 假设交易超时,在途还在
balance.deduct_with_priority(dec!(2000.00)).unwrap();
balance.add_transit(dec!(2000.00));
assert_eq!(balance.transit_amount, dec!(2000.00));
assert_eq!(balance.bank_balance, dec!(6000.00));
// 补偿任务执行,发现银行实际成功
let bank_success = true;
if bank_success {
// 结转在途
balance.settle_transit(dec!(2000.00)).unwrap();
} else {
// 回退在途
balance.rollback_transit(dec!(2000.00));
}
assert_eq!(balance.transit_amount, dec!(0.00));
assert!(balance.validate_invariant().is_ok());
}
// ==================== 场景7多任务并行处理 ====================
#[test]
fn test_parallel_compensation_tasks() {
let tasks: Vec<CompensationTask> = (0..5)
.map(|i| {
create_compensation_task(
&format!("TXN_PAR_{}", i),
CompensationTaskType::TimeoutCheck,
)
})
.collect();
assert_eq!(tasks.len(), 5);
// 所有任务都是 Pending
assert!(tasks.iter().all(|t| t.status == CompensationTaskStatus::Pending));
// 模拟并行处理
let mut processed = 0;
for _task in &tasks {
// 处理逻辑
processed += 1;
}
assert_eq!(processed, 5);
}
// ==================== 场景8补偿任务类型 ====================
#[test]
fn test_compensation_task_types() {
// 不同类型的补偿任务
let timeout_task = create_compensation_task(
"TXN_T1",
CompensationTaskType::TimeoutCheck,
);
let reversal_task = create_compensation_task(
"TXN_R1",
CompensationTaskType::Reverse,
);
let reconcile_task = create_compensation_task(
"TXN_RC1",
CompensationTaskType::Reconcile,
);
assert_eq!(timeout_task.task_type, CompensationTaskType::TimeoutCheck);
assert_eq!(reversal_task.task_type, CompensationTaskType::Reverse);
assert_eq!(reconcile_task.task_type, CompensationTaskType::Reconcile);
}
// ==================== 场景9补偿任务幂等性 ====================
#[test]
fn test_compensation_idempotency() {
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
// 建立在途
balance.deduct_with_priority(dec!(2000.00)).unwrap();
balance.add_transit(dec!(2000.00));
let initial_bank = balance.bank_balance;
// 第一次结转
balance.settle_transit(dec!(2000.00)).unwrap();
// 模拟重复调用结转(应该失败或无效果)
let result = balance.settle_transit(dec!(2000.00));
// 在途已经为 0再次结转会失败
assert!(result.is_err());
// 余额只扣减了一次
assert_eq!(balance.bank_balance, initial_bank);
}
// ==================== 场景10补偿任务与状态机一致性 ====================
#[test]
fn test_compensation_state_consistency() {
// 确保补偿后交易状态与余额状态一致
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum TxnStatus {
BankSubmitted,
Timeout,
Success,
Failed,
}
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
let withdrawal = dec!(2000.00);
balance.deduct_with_priority(withdrawal).unwrap();
balance.add_transit(withdrawal);
let mut txn_status = TxnStatus::BankSubmitted;
// 超时
txn_status = TxnStatus::Timeout;
let _ = txn_status; // 避免 unused 警告
// 补偿:银行成功
let bank_result = true;
if bank_result {
txn_status = TxnStatus::Success;
balance.settle_transit(withdrawal).unwrap();
} else {
txn_status = TxnStatus::Failed;
balance.rollback_transit(withdrawal);
}
// 一致性验证
match txn_status {
TxnStatus::Success => {
// 成功:在途为 0银行余额已扣
assert_eq!(balance.transit_amount, dec!(0.00));
assert_eq!(balance.bank_balance, dec!(6000.00));
}
TxnStatus::Failed => {
// 失败:在途为 0银行余额恢复
assert_eq!(balance.transit_amount, dec!(0.00));
assert_eq!(balance.bank_balance, dec!(8000.00));
}
_ => panic!("Invalid final state"),
}
assert!(balance.validate_invariant().is_ok());
}