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