- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
359 lines
12 KiB
Rust
359 lines
12 KiB
Rust
//! 对账集成测试
|
||
|
||
use chrono::Utc;
|
||
use rust_decimal::Decimal;
|
||
use rust_decimal_macros::dec;
|
||
|
||
use rustjr::domain::ledger::entity::{AccountBalance, ThreeAccountResult};
|
||
use rustjr::domain::account::AccountType;
|
||
use rustjr::infrastructure::bank_integration::mock_bank::MockBankClient;
|
||
use rustjr::infrastructure::bank_integration::BankClient;
|
||
|
||
// ==================== 测试辅助 ====================
|
||
|
||
fn create_balance(personal: Decimal, labor: Decimal, frozen: Decimal, transit: Decimal) -> AccountBalance {
|
||
AccountBalance {
|
||
id: 1,
|
||
account_id: 1001,
|
||
account_type: AccountType::Virtual,
|
||
personal_balance: personal,
|
||
labor_balance: labor,
|
||
frozen_balance: frozen,
|
||
bank_balance: personal + labor + frozen,
|
||
transit_amount: transit,
|
||
system_balance: personal + labor,
|
||
available_balance: personal + labor,
|
||
frozen_amount: frozen,
|
||
version: 1,
|
||
updated_at: Utc::now(),
|
||
}
|
||
}
|
||
|
||
/// 执行三账校验
|
||
fn verify_three_accounts(balance: &AccountBalance, bank_balance: Decimal) -> ThreeAccountResult {
|
||
let ledger_total = balance.personal_balance + balance.labor_balance + balance.frozen_balance;
|
||
let transit_net = balance.transit_amount;
|
||
|
||
// 公式:总账 + 在途 = 银行
|
||
let expected_bank = ledger_total + transit_net;
|
||
let difference = bank_balance - expected_bank;
|
||
let is_balanced = difference.abs() < dec!(0.01); // 允许 1 分钱误差
|
||
|
||
ThreeAccountResult {
|
||
bank_balance,
|
||
transit_net,
|
||
ledger_total,
|
||
is_balanced,
|
||
difference,
|
||
}
|
||
}
|
||
|
||
// ==================== 三账平衡测试 ====================
|
||
|
||
#[test]
|
||
fn test_three_account_balanced_no_transit() {
|
||
// 没有在途时的三账平衡
|
||
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
let bank = balance.bank_balance; // 8000
|
||
|
||
let result = verify_three_accounts(&balance, bank);
|
||
|
||
assert!(result.is_balanced);
|
||
assert_eq!(result.difference, dec!(0.00));
|
||
assert_eq!(result.ledger_total, dec!(8000.00));
|
||
assert_eq!(result.transit_net, dec!(0.00));
|
||
}
|
||
|
||
#[test]
|
||
fn test_three_account_balanced_with_transit() {
|
||
// 有在途时的三账平衡
|
||
// 假设:扣款 1000 进入在途,银行尚未确认
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
balance.deduct_with_priority(dec!(1000.00)).unwrap();
|
||
balance.add_transit(dec!(1000.00));
|
||
|
||
// 此时:
|
||
// 总账 = 4000 + 3000 + 0 = 7000
|
||
// 在途 = 1000
|
||
// 银行余额应该还是 8000(因为银行还没确认)
|
||
|
||
// 但由于我们的 deduct_with_priority 同时减少了 bank_balance
|
||
// 在实际系统中,bank_balance 应该是从银行同步的
|
||
// 这里我们手动设置正确的银行余额来测试
|
||
let actual_bank = dec!(8000.00); // 银行还没扣
|
||
|
||
let result = verify_three_accounts(&balance, actual_bank);
|
||
|
||
assert!(result.is_balanced);
|
||
// 7000 + 1000 = 8000
|
||
}
|
||
|
||
#[test]
|
||
fn test_three_account_balanced_with_frozen() {
|
||
let balance = create_balance(dec!(5000.00), dec!(2000.00), dec!(1000.00), dec!(0.00));
|
||
let bank = balance.bank_balance; // 8000
|
||
|
||
let result = verify_three_accounts(&balance, bank);
|
||
|
||
assert!(result.is_balanced);
|
||
assert_eq!(result.ledger_total, dec!(8000.00)); // 5000 + 2000 + 1000
|
||
}
|
||
|
||
// ==================== 三账不平衡测试 ====================
|
||
|
||
#[test]
|
||
fn test_three_account_short() {
|
||
// 短款:银行少于预期
|
||
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
let bank = dec!(7500.00); // 银行少了 500
|
||
|
||
let result = verify_three_accounts(&balance, bank);
|
||
|
||
assert!(!result.is_balanced);
|
||
assert_eq!(result.difference, dec!(-500.00)); // 银行少 500
|
||
}
|
||
|
||
#[test]
|
||
fn test_three_account_long() {
|
||
// 长款:银行多于预期
|
||
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
let bank = dec!(8500.00); // 银行多了 500
|
||
|
||
let result = verify_three_accounts(&balance, bank);
|
||
|
||
assert!(!result.is_balanced);
|
||
assert_eq!(result.difference, dec!(500.00)); // 银行多 500
|
||
}
|
||
|
||
#[test]
|
||
fn test_three_account_transit_mismatch() {
|
||
// 在途不匹配
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
balance.deduct_with_priority(dec!(1000.00)).unwrap();
|
||
balance.add_transit(dec!(1000.00));
|
||
|
||
// 银行已经扣款(不应该这么快)
|
||
let bank = dec!(7000.00); // 银行已扣款
|
||
|
||
let result = verify_three_accounts(&balance, bank);
|
||
|
||
// 总账 7000 + 在途 1000 = 8000,但银行只有 7000
|
||
assert!(!result.is_balanced);
|
||
assert_eq!(result.difference, dec!(-1000.00));
|
||
}
|
||
|
||
// ==================== 对账流水比对测试 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_reconciliation_with_bank_statements() {
|
||
let client = MockBankClient::new();
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
client.create_account("EXT001", "外部账户", dec!(50000.00)).unwrap();
|
||
|
||
// 模拟几笔交易
|
||
use rustjr::infrastructure::bank_integration::BankTransferRequest;
|
||
|
||
// 出账
|
||
let request = 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: Some("提现".to_string()),
|
||
business_no: "TXN001".to_string(),
|
||
};
|
||
client.transfer(request).await.unwrap();
|
||
|
||
// 外部入账
|
||
client.simulate_external_deposit(
|
||
"PRISON001",
|
||
"FAMILY001",
|
||
"家属",
|
||
dec!(2000.00),
|
||
Some("家属充值".to_string()),
|
||
).unwrap();
|
||
|
||
// 查询银行流水
|
||
let today = Utc::now().date_naive();
|
||
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
|
||
|
||
// 验证流水
|
||
assert_eq!(statements.len(), 2);
|
||
|
||
// 找出入账
|
||
let inbound = statements.iter().filter(|s| s.direction == "in").collect::<Vec<_>>();
|
||
assert_eq!(inbound.len(), 1);
|
||
assert_eq!(inbound[0].amount, dec!(2000.00));
|
||
|
||
// 找出账
|
||
let outbound = statements.iter().filter(|s| s.direction == "out").collect::<Vec<_>>();
|
||
assert_eq!(outbound.len(), 1);
|
||
assert_eq!(outbound[0].amount, dec!(5000.00));
|
||
|
||
// 验证最终余额
|
||
let balance = client.query_balance("PRISON001").await.unwrap();
|
||
assert_eq!(balance.balance, dec!(97000.00)); // 100000 - 5000 + 2000
|
||
}
|
||
|
||
// ==================== 外部入账识别测试 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_external_deposit_recognition() {
|
||
let client = MockBankClient::new();
|
||
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
|
||
|
||
// 系统不知道的外部入账
|
||
let bank_ref = client.simulate_external_deposit(
|
||
"PRISON001",
|
||
"UNKNOWN_FAMILY",
|
||
"未知家属",
|
||
dec!(3000.00),
|
||
Some("不明来源充值".to_string()),
|
||
).unwrap();
|
||
|
||
// 查询流水会发现这笔入账
|
||
let today = Utc::now().date_naive();
|
||
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
|
||
|
||
let external = statements.iter()
|
||
.find(|s| s.bank_ref_no == bank_ref)
|
||
.unwrap();
|
||
|
||
assert_eq!(external.direction, "in");
|
||
assert_eq!(external.amount, dec!(3000.00));
|
||
|
||
// 在对账时,这笔交易应该标记为 "外部入账待确认"
|
||
// 需要人工或自动匹配到对应的罪犯账户
|
||
}
|
||
|
||
// ==================== 超时交易对账测试 ====================
|
||
|
||
#[test]
|
||
fn test_timeout_transaction_reconciliation() {
|
||
// 模拟超时交易场景
|
||
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
|
||
|
||
// 扣款并建立在途
|
||
balance.deduct_with_priority(dec!(1000.00)).unwrap();
|
||
balance.add_transit(dec!(1000.00));
|
||
|
||
// 此时:
|
||
// 系统认为:银行余额 7000,在途 1000
|
||
// 如果银行实际已成功:银行余额 7000(正确)
|
||
// 如果银行实际未成功:银行余额 8000(在途应回退)
|
||
|
||
// 场景1:银行实际已成功
|
||
let actual_bank_success = dec!(7000.00);
|
||
let result = verify_three_accounts(&balance, actual_bank_success);
|
||
// 7000 + 1000 = 8000,但银行只有 7000
|
||
assert!(!result.is_balanced);
|
||
// 这说明需要结转在途
|
||
|
||
// 场景2:银行实际未成功
|
||
let actual_bank_failed = dec!(8000.00);
|
||
let result = verify_three_accounts(&balance, actual_bank_failed);
|
||
// 7000 + 1000 = 8000,银行也是 8000
|
||
assert!(result.is_balanced);
|
||
// 但这意味着银行没扣款,我们需要回退在途
|
||
}
|
||
|
||
// ==================== 重复交易检测测试 ====================
|
||
|
||
#[tokio::test]
|
||
async fn test_duplicate_transaction_detection() {
|
||
let client = MockBankClient::new();
|
||
client.create_account("FROM001", "转出账户", dec!(10000.00)).unwrap();
|
||
client.create_account("TO001", "转入账户", dec!(0.00)).unwrap();
|
||
|
||
use rustjr::infrastructure::bank_integration::BankTransferRequest;
|
||
|
||
let request = BankTransferRequest {
|
||
from_account: "FROM001".to_string(),
|
||
to_account: "TO001".to_string(),
|
||
to_account_name: "转入账户".to_string(),
|
||
to_bank_code: "MOCK".to_string(),
|
||
amount: dec!(1000.00),
|
||
remark: None,
|
||
business_no: "TXN_DUP_001".to_string(),
|
||
};
|
||
|
||
// 第一次转账
|
||
let response1 = client.transfer(request.clone()).await.unwrap();
|
||
assert!(response1.success);
|
||
|
||
// 用相同的 business_no 查询,应该能找到
|
||
let status = client.query_transaction_status("TXN_DUP_001").await.unwrap();
|
||
assert!(status.success);
|
||
assert_eq!(status.bank_ref_no, response1.bank_ref_no);
|
||
}
|
||
|
||
// ==================== 三账校验公式验证 ====================
|
||
|
||
#[test]
|
||
fn test_three_account_formula() {
|
||
// 验证公式:总账余额 = 银行余额 + 在途净额
|
||
// 即:personal + labor + frozen = bank - transit_out + transit_in
|
||
// 简化为:total = bank (当没有在途时)
|
||
|
||
struct TestCase {
|
||
personal: Decimal,
|
||
labor: Decimal,
|
||
frozen: Decimal,
|
||
transit: Decimal,
|
||
bank: Decimal,
|
||
expected_balanced: bool,
|
||
}
|
||
|
||
let cases = vec![
|
||
// 平衡:无在途
|
||
TestCase {
|
||
personal: dec!(5000.00),
|
||
labor: dec!(3000.00),
|
||
frozen: dec!(0.00),
|
||
transit: dec!(0.00),
|
||
bank: dec!(8000.00),
|
||
expected_balanced: true,
|
||
},
|
||
// 平衡:有在途(银行未确认)
|
||
TestCase {
|
||
personal: dec!(4000.00), // 扣了1000
|
||
labor: dec!(3000.00),
|
||
frozen: dec!(0.00),
|
||
transit: dec!(1000.00), // 在途1000
|
||
bank: dec!(8000.00), // 银行还没扣
|
||
expected_balanced: true,
|
||
},
|
||
// 不平衡:银行已扣但在途未清
|
||
TestCase {
|
||
personal: dec!(4000.00),
|
||
labor: dec!(3000.00),
|
||
frozen: dec!(0.00),
|
||
transit: dec!(1000.00),
|
||
bank: dec!(7000.00), // 银行已扣
|
||
expected_balanced: false,
|
||
},
|
||
// 平衡:有冻结
|
||
TestCase {
|
||
personal: dec!(4000.00),
|
||
labor: dec!(3000.00),
|
||
frozen: dec!(1000.00),
|
||
transit: dec!(0.00),
|
||
bank: dec!(8000.00),
|
||
expected_balanced: true,
|
||
},
|
||
];
|
||
|
||
for (i, case) in cases.iter().enumerate() {
|
||
let balance = create_balance(case.personal, case.labor, case.frozen, case.transit);
|
||
let result = verify_three_accounts(&balance, case.bank);
|
||
|
||
assert_eq!(
|
||
result.is_balanced, case.expected_balanced,
|
||
"Case {} failed: expected balanced={}, got balanced={}",
|
||
i, case.expected_balanced, result.is_balanced
|
||
);
|
||
}
|
||
}
|
||
|