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

366 lines
12 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::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
}