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

239 lines
8.4 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::account::AccountType;
use rustjr::domain::ledger::AccountBalance;
use rustjr::infrastructure::bank_integration::mock_bank::{MockBankClient, MockBankTestEnv, FailureConfig};
use rustjr::infrastructure::bank_integration::{BankClient, BankTransferRequest};
// ==================== 端到端测试入口 ====================
/// 完整的业务流程测试
///
/// 测试覆盖:
/// 1. 账户初始化
/// 2. 充值(个人/劳动)
/// 3. 提现(正常/失败/超时)
/// 4. 冻结解冻
/// 5. 三账对账
/// 6. 补偿处理
#[tokio::test]
async fn test_full_business_cycle() {
use chrono::Utc;
use rust_decimal::Decimal;
// ========== 阶段1初始化 ==========
let client = MockBankClient::new();
client.create_account("PRISON_MAIN", "监狱主账户", dec!(1000000.00)).unwrap();
client.create_account("FAMILY_001", "家属账户1", dec!(50000.00)).unwrap();
client.create_account("FAMILY_002", "家属账户2", dec!(30000.00)).unwrap();
let mut inmate_balance = AccountBalance {
id: 1,
account_id: 1001,
account_type: AccountType::Virtual,
personal_balance: Decimal::ZERO,
labor_balance: Decimal::ZERO,
frozen_balance: Decimal::ZERO,
bank_balance: Decimal::ZERO,
transit_amount: Decimal::ZERO,
system_balance: Decimal::ZERO,
available_balance: Decimal::ZERO,
frozen_amount: Decimal::ZERO,
version: 1,
updated_at: Utc::now(),
};
// ========== 阶段2充值 ==========
// 家属充值 5000 元
client.simulate_external_deposit(
"PRISON_MAIN",
"FAMILY_001",
"家属1",
dec!(5000.00),
Some("给罪犯1001充值".to_string()),
).unwrap();
inmate_balance.add_personal(dec!(5000.00));
assert!(inmate_balance.validate_invariant().is_ok());
assert_eq!(inmate_balance.personal_balance, dec!(5000.00));
// 发放劳动报酬 2000 元
inmate_balance.add_labor(dec!(2000.00));
assert!(inmate_balance.validate_invariant().is_ok());
assert_eq!(inmate_balance.labor_balance, dec!(2000.00));
// ========== 阶段3冻结部分资金 ==========
inmate_balance.freeze(dec!(1000.00));
assert_eq!(inmate_balance.frozen_balance, dec!(1000.00));
assert_eq!(inmate_balance.available_balance(), dec!(6000.00)); // 5000-1000+2000
assert!(inmate_balance.validate_invariant().is_ok());
// ========== 阶段4正常提现 ==========
let withdrawal_amount = dec!(3000.00);
let deduction = inmate_balance.deduct_with_priority(withdrawal_amount).unwrap();
assert_eq!(deduction.from_personal, dec!(3000.00));
assert_eq!(deduction.from_labor, dec!(0.00));
inmate_balance.add_transit(withdrawal_amount);
let request = BankTransferRequest {
from_account: "PRISON_MAIN".to_string(),
to_account: "FAMILY_002".to_string(),
to_account_name: "家属2".to_string(),
to_bank_code: "MOCK".to_string(),
amount: withdrawal_amount,
remark: Some("罪犯1001提现".to_string()),
business_no: "E2E_WD_001".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(response.success);
inmate_balance.settle_transit(withdrawal_amount).unwrap();
assert!(inmate_balance.validate_invariant().is_ok());
assert_eq!(inmate_balance.personal_balance, dec!(1000.00)); // 5000-1000-3000
assert_eq!(inmate_balance.bank_balance, dec!(4000.00)); // 7000-3000
// ========== 阶段5验证银行余额 ==========
let prison_balance = client.query_balance("PRISON_MAIN").await.unwrap();
// 初始 1000000 + 5000(入账) - 3000(出账) = 1002000
assert_eq!(prison_balance.balance, dec!(1002000.00));
let family2_balance = client.query_balance("FAMILY_002").await.unwrap();
// 初始 30000 + 3000 = 33000
assert_eq!(family2_balance.balance, dec!(33000.00));
// ========== 阶段6解冻并提现全部 ==========
inmate_balance.unfreeze(dec!(1000.00));
assert_eq!(inmate_balance.frozen_balance, dec!(0.00));
assert_eq!(inmate_balance.available_balance(), dec!(4000.00));
// 提现全部余额
let final_withdrawal = dec!(4000.00);
let deduction2 = inmate_balance.deduct_with_priority(final_withdrawal).unwrap();
assert_eq!(deduction2.from_personal, dec!(2000.00)); // 1000+1000(解冻)
assert_eq!(deduction2.from_labor, dec!(2000.00));
inmate_balance.add_transit(final_withdrawal);
let request2 = BankTransferRequest {
from_account: "PRISON_MAIN".to_string(),
to_account: "FAMILY_001".to_string(),
to_account_name: "家属1".to_string(),
to_bank_code: "MOCK".to_string(),
amount: final_withdrawal,
remark: Some("罪犯1001结清".to_string()),
business_no: "E2E_WD_002".to_string(),
};
let response2 = client.transfer(request2).await.unwrap();
assert!(response2.success);
inmate_balance.settle_transit(final_withdrawal).unwrap();
// ========== 阶段7验证最终状态 ==========
assert_eq!(inmate_balance.personal_balance, dec!(0.00));
assert_eq!(inmate_balance.labor_balance, dec!(0.00));
assert_eq!(inmate_balance.frozen_balance, dec!(0.00));
assert_eq!(inmate_balance.bank_balance, dec!(0.00));
assert_eq!(inmate_balance.transit_amount, dec!(0.00));
assert!(inmate_balance.validate_invariant().is_ok());
// ========== 阶段8验证银行对账流水 ==========
let today = Utc::now().date_naive();
let statements = client.query_statements("PRISON_MAIN", today, today).await.unwrap();
// 应有 3 条流水1入2出
assert_eq!(statements.len(), 3);
let inbound_count = statements.iter().filter(|s| s.direction == "in").count();
let outbound_count = statements.iter().filter(|s| s.direction == "out").count();
assert_eq!(inbound_count, 1);
assert_eq!(outbound_count, 2);
let total_in: Decimal = statements.iter()
.filter(|s| s.direction == "in")
.map(|s| s.amount)
.sum();
let total_out: Decimal = statements.iter()
.filter(|s| s.direction == "out")
.map(|s| s.amount)
.sum();
assert_eq!(total_in, dec!(5000.00));
assert_eq!(total_out, dec!(7000.00));
}
/// 测试失败恢复流程
#[tokio::test]
async fn test_failure_recovery_cycle() {
use chrono::Utc;
use rust_decimal::Decimal;
// 配置银行强制失败
let client = MockBankClient::with_failure_config(FailureConfig::force_failure());
client.create_account("PRISON_MAIN", "监狱主账户", dec!(100000.00)).unwrap();
let mut balance = AccountBalance {
id: 1,
account_id: 1001,
account_type: AccountType::Virtual,
personal_balance: dec!(5000.00),
labor_balance: dec!(3000.00),
frozen_balance: Decimal::ZERO,
bank_balance: dec!(8000.00),
transit_amount: Decimal::ZERO,
system_balance: dec!(8000.00),
available_balance: dec!(8000.00),
frozen_amount: Decimal::ZERO,
version: 1,
updated_at: Utc::now(),
};
let original_bank = balance.bank_balance;
// 尝试提现(会失败)
balance.deduct_with_priority(dec!(2000.00)).unwrap();
balance.add_transit(dec!(2000.00));
let request = BankTransferRequest {
from_account: "PRISON_MAIN".to_string(),
to_account: "FAMILY001".to_string(),
to_account_name: "家属".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(2000.00),
remark: None,
business_no: "FAIL_001".to_string(),
};
let response = client.transfer(request).await.unwrap();
assert!(!response.success);
// 回退
balance.rollback_transit(dec!(2000.00));
// 验证余额恢复
assert_eq!(balance.bank_balance, original_bank);
assert_eq!(balance.transit_amount, dec!(0.00));
assert!(balance.validate_invariant().is_ok());
}
/// 测试使用 MockBankTestEnv 快捷环境
#[tokio::test]
async fn test_with_standard_env() {
let env = MockBankTestEnv::new();
// 验证标准环境已创建
let balance = env.client.query_balance("MAIN001").await.unwrap();
assert_eq!(balance.balance, dec!(100000.00));
let ext_balance = env.client.query_balance("EXT001").await.unwrap();
assert_eq!(ext_balance.balance, dec!(50000.00));
}