- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
366 lines
12 KiB
Rust
366 lines
12 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;
|
||
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_pending(&mut self) {
|
||
self.status = TransactionStatus::Pending;
|
||
}
|
||
|
||
fn set_bank_submitted(&mut self) {
|
||
self.status = TransactionStatus::BankSubmitted;
|
||
}
|
||
|
||
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_scenario_withdrawal_e2e() {
|
||
// ========== 1. 初始化 ==========
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00));
|
||
let mut txn = MockTransaction::new("WD_E2E_001", dec!(2000.00));
|
||
|
||
let client = MockBankClient::new();
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
client.create_account("FAMILY001", "家属账户", dec!(0.00)).unwrap();
|
||
|
||
// 验证初始状态
|
||
assert!(balance.validate_invariant().is_ok());
|
||
assert_eq!(txn.status, TransactionStatus::Created);
|
||
|
||
// ========== 2. 创建交易 (Created → Pending) ==========
|
||
// 校验余额
|
||
let withdrawal_amount = txn.amount;
|
||
assert!(balance.available_balance() >= withdrawal_amount);
|
||
|
||
// 扣款并冻结/建立在途
|
||
let deduction = balance.deduct_with_priority(withdrawal_amount).unwrap();
|
||
balance.add_transit(withdrawal_amount);
|
||
|
||
txn.set_pending();
|
||
|
||
assert_eq!(txn.status, TransactionStatus::Pending);
|
||
assert_eq!(deduction.from_personal, dec!(2000.00));
|
||
assert_eq!(balance.personal_balance, dec!(3000.00));
|
||
assert_eq!(balance.transit_amount, dec!(2000.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
|
||
// ========== 3. 提交银行 (Pending → BankSubmitted) ==========
|
||
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: Some("罪犯提现".to_string()),
|
||
business_no: txn.txn_no.clone(),
|
||
};
|
||
|
||
let response = client.transfer(request).await.unwrap();
|
||
|
||
// ========== 4. 银行成功 (BankSubmitted → Success) ==========
|
||
assert!(response.success);
|
||
let bank_ref_no = response.bank_ref_no.unwrap();
|
||
|
||
txn.set_success(bank_ref_no.clone());
|
||
|
||
// 结转在途
|
||
balance.settle_transit(withdrawal_amount).unwrap();
|
||
|
||
// ========== 5. 验证最终状态 ==========
|
||
assert_eq!(txn.status, TransactionStatus::Success);
|
||
assert!(txn.bank_ref_no.is_some());
|
||
|
||
assert_eq!(balance.personal_balance, dec!(3000.00));
|
||
assert_eq!(balance.labor_balance, dec!(3000.00));
|
||
assert_eq!(balance.bank_balance, dec!(6000.00));
|
||
assert_eq!(balance.transit_amount, dec!(0.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
|
||
// 验证银行余额
|
||
let prison_balance = client.query_balance("PRISON001").await.unwrap();
|
||
assert_eq!(prison_balance.balance, dec!(98000.00));
|
||
|
||
let family_balance = client.query_balance("FAMILY001").await.unwrap();
|
||
assert_eq!(family_balance.balance, dec!(2000.00));
|
||
|
||
// 验证交易可查询
|
||
let status = client.query_transaction_status(&txn.txn_no).await.unwrap();
|
||
assert!(status.success);
|
||
}
|
||
|
||
// ==================== 场景2:完整充值流程 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_scenario_deposit_e2e() {
|
||
// ========== 1. 初始化 ==========
|
||
let mut balance = create_balance(dec!(1000.00), dec!(500.00));
|
||
let deposit_amount = dec!(2000.00);
|
||
|
||
let client = MockBankClient::new();
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
|
||
// ========== 2. 模拟外部入账 ==========
|
||
let bank_ref = client.simulate_external_deposit(
|
||
"PRISON001",
|
||
"FAMILY001",
|
||
"家属张三",
|
||
deposit_amount,
|
||
Some("给罪犯XXX充值".to_string()),
|
||
).unwrap();
|
||
|
||
// ========== 3. 系统接收到入账通知,更新余额 ==========
|
||
balance.add_personal(deposit_amount);
|
||
|
||
// ========== 4. 验证最终状态 ==========
|
||
assert_eq!(balance.personal_balance, dec!(3000.00));
|
||
assert_eq!(balance.labor_balance, dec!(500.00));
|
||
assert_eq!(balance.bank_balance, dec!(3500.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
|
||
// 银行流水可查
|
||
let today = Utc::now().date_naive();
|
||
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
|
||
let deposit_record = statements.iter().find(|s| s.bank_ref_no == bank_ref);
|
||
assert!(deposit_record.is_some());
|
||
}
|
||
|
||
// ==================== 场景3:劳动报酬发放 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_scenario_labor_payment_e2e() {
|
||
// ========== 1. 初始化 ==========
|
||
let mut balance = create_balance(dec!(1000.00), dec!(0.00));
|
||
let labor_amount = dec!(500.00);
|
||
|
||
// ========== 2. 发放劳动报酬 ==========
|
||
balance.add_labor(labor_amount);
|
||
|
||
// ========== 3. 验证 ==========
|
||
assert_eq!(balance.personal_balance, dec!(1000.00));
|
||
assert_eq!(balance.labor_balance, dec!(500.00));
|
||
assert_eq!(balance.bank_balance, dec!(1500.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
|
||
// 可用余额增加
|
||
assert_eq!(balance.available_balance(), dec!(1500.00));
|
||
}
|
||
|
||
// ==================== 场景4:冻结后提现 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_scenario_freeze_then_withdraw() {
|
||
// ========== 1. 初始化 ==========
|
||
let mut balance = create_balance(dec!(3000.00), dec!(2000.00));
|
||
|
||
// ========== 2. 冻结部分余额 ==========
|
||
balance.freeze(dec!(1500.00));
|
||
|
||
assert_eq!(balance.personal_balance, dec!(1500.00));
|
||
assert_eq!(balance.frozen_balance, dec!(1500.00));
|
||
assert_eq!(balance.available_balance(), dec!(3500.00)); // 1500 + 2000
|
||
|
||
// ========== 3. 尝试提现超过可用余额 ==========
|
||
let result = balance.deduct_with_priority(dec!(4000.00));
|
||
assert!(result.is_err()); // 失败,因为可用只有 3500
|
||
|
||
// ========== 4. 提现可用余额范围内 ==========
|
||
let result = balance.deduct_with_priority(dec!(3000.00));
|
||
assert!(result.is_ok());
|
||
|
||
// ========== 5. 验证 ==========
|
||
assert_eq!(balance.personal_balance, dec!(0.00));
|
||
assert_eq!(balance.labor_balance, dec!(500.00));
|
||
assert_eq!(balance.frozen_balance, dec!(1500.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
|
||
// ==================== 场景5:多笔交易混合 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_scenario_mixed_transactions() {
|
||
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 req1 = 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: "MIX001".to_string(),
|
||
};
|
||
let resp1 = client.transfer(req1).await.unwrap();
|
||
assert!(resp1.success);
|
||
balance.settle_transit(dec!(3000.00)).unwrap();
|
||
|
||
// ========== 2. 收到充值 ==========
|
||
client.simulate_external_deposit(
|
||
"PRISON001",
|
||
"EXT001",
|
||
"外部",
|
||
dec!(2000.00),
|
||
None,
|
||
).unwrap();
|
||
balance.add_personal(dec!(2000.00));
|
||
|
||
// ========== 3. 发放劳动报酬 ==========
|
||
balance.add_labor(dec!(1000.00));
|
||
|
||
// ========== 4. 第二笔提现 ==========
|
||
balance.deduct_with_priority(dec!(5000.00)).unwrap();
|
||
balance.add_transit(dec!(5000.00));
|
||
|
||
let req2 = 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!(5000.00),
|
||
remark: None,
|
||
business_no: "MIX002".to_string(),
|
||
};
|
||
let resp2 = client.transfer(req2).await.unwrap();
|
||
assert!(resp2.success);
|
||
balance.settle_transit(dec!(5000.00)).unwrap();
|
||
|
||
// ========== 5. 验证最终状态 ==========
|
||
// 初始: 10000 + 5000 = 15000
|
||
// -3000 +2000 +1000 -5000 = 10000
|
||
assert_eq!(balance.bank_balance, dec!(10000.00));
|
||
assert!(balance.validate_invariant().is_ok());
|
||
}
|
||
|
||
// ==================== 场景6:日终对账 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_scenario_daily_reconciliation() {
|
||
let client = MockBankClient::new();
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
client.create_account("EXT001", "外部账户", dec!(50000.00)).unwrap();
|
||
|
||
// 模拟一天的交易
|
||
// 出账 5000
|
||
let req = BankTransferRequest {
|
||
from_account: "PRISON001".to_string(),
|
||
to_account: "EXT001".to_string(),
|
||
to_account_name: "外部".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: dec!(5000.00),
|
||
remark: None,
|
||
business_no: "DAY001".to_string(),
|
||
};
|
||
client.transfer(req).await.unwrap();
|
||
|
||
// 入账 3000
|
||
client.simulate_external_deposit(
|
||
"PRISON001",
|
||
"FAM001",
|
||
"家属",
|
||
dec!(3000.00),
|
||
None,
|
||
).unwrap();
|
||
|
||
// 出账 2000
|
||
let req2 = BankTransferRequest {
|
||
from_account: "PRISON001".to_string(),
|
||
to_account: "EXT001".to_string(),
|
||
to_account_name: "外部".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: dec!(2000.00),
|
||
remark: None,
|
||
business_no: "DAY002".to_string(),
|
||
};
|
||
client.transfer(req2).await.unwrap();
|
||
|
||
// 日终对账
|
||
let today = Utc::now().date_naive();
|
||
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
|
||
|
||
// 统计
|
||
let total_out: Decimal = statements.iter()
|
||
.filter(|s| s.direction == "out")
|
||
.map(|s| s.amount)
|
||
.sum();
|
||
|
||
let total_in: Decimal = statements.iter()
|
||
.filter(|s| s.direction == "in")
|
||
.map(|s| s.amount)
|
||
.sum();
|
||
|
||
assert_eq!(total_out, dec!(7000.00)); // 5000 + 2000
|
||
assert_eq!(total_in, dec!(3000.00));
|
||
|
||
// 验证余额
|
||
let balance = client.query_balance("PRISON001").await.unwrap();
|
||
assert_eq!(balance.balance, dec!(96000.00)); // 100000 - 7000 + 3000
|
||
}
|
||
|