- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
304 lines
7.8 KiB
Rust
304 lines
7.8 KiB
Rust
//! 测试数据夹具
|
|
//!
|
|
//! 提供预定义的测试数据,确保测试可重复和一致
|
|
|
|
use chrono::Utc;
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal_macros::dec;
|
|
|
|
// ==================== 账户相关夹具 ====================
|
|
|
|
/// 标准监狱主账户
|
|
pub const PRISON_MAIN_ACCOUNT: &str = "6225001234567890";
|
|
pub const PRISON_MAIN_ACCOUNT_NAME: &str = "XX监狱收款账户";
|
|
|
|
/// 标准罪犯账户前缀
|
|
pub const INMATE_ACCOUNT_PREFIX: &str = "ZF";
|
|
|
|
/// 外部账户(家属)
|
|
pub const FAMILY_ACCOUNT: &str = "6226009876543210";
|
|
pub const FAMILY_ACCOUNT_NAME: &str = "家属账户";
|
|
|
|
/// 生成罪犯账户号
|
|
pub fn gen_inmate_account_no(inmate_id: i64) -> String {
|
|
format!("{}{:012}", INMATE_ACCOUNT_PREFIX, inmate_id)
|
|
}
|
|
|
|
// ==================== 金额相关夹具 ====================
|
|
|
|
/// 标准测试金额
|
|
pub struct TestAmounts;
|
|
|
|
impl TestAmounts {
|
|
/// 小额 (100元)
|
|
pub fn small() -> Decimal {
|
|
dec!(100.00)
|
|
}
|
|
|
|
/// 中等 (1000元)
|
|
pub fn medium() -> Decimal {
|
|
dec!(1000.00)
|
|
}
|
|
|
|
/// 大额 (10000元)
|
|
pub fn large() -> Decimal {
|
|
dec!(10000.00)
|
|
}
|
|
|
|
/// 零
|
|
pub fn zero() -> Decimal {
|
|
Decimal::ZERO
|
|
}
|
|
|
|
/// 初始余额
|
|
pub fn initial_balance() -> Decimal {
|
|
dec!(100000.00)
|
|
}
|
|
|
|
/// 负数(用于测试错误情况)
|
|
pub fn negative() -> Decimal {
|
|
dec!(-100.00)
|
|
}
|
|
}
|
|
|
|
// ==================== 账户余额夹具 ====================
|
|
|
|
/// 三科目余额测试数据
|
|
#[derive(Debug, Clone)]
|
|
pub struct BalanceFixture {
|
|
pub personal_balance: Decimal,
|
|
pub labor_balance: Decimal,
|
|
pub frozen_balance: Decimal,
|
|
pub bank_balance: Decimal,
|
|
pub transit_amount: Decimal,
|
|
}
|
|
|
|
impl BalanceFixture {
|
|
/// 空账户
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
personal_balance: Decimal::ZERO,
|
|
labor_balance: Decimal::ZERO,
|
|
frozen_balance: Decimal::ZERO,
|
|
bank_balance: Decimal::ZERO,
|
|
transit_amount: Decimal::ZERO,
|
|
}
|
|
}
|
|
|
|
/// 只有个人余额
|
|
pub fn personal_only(amount: Decimal) -> Self {
|
|
Self {
|
|
personal_balance: amount,
|
|
labor_balance: Decimal::ZERO,
|
|
frozen_balance: Decimal::ZERO,
|
|
bank_balance: amount,
|
|
transit_amount: Decimal::ZERO,
|
|
}
|
|
}
|
|
|
|
/// 只有劳动报酬
|
|
pub fn labor_only(amount: Decimal) -> Self {
|
|
Self {
|
|
personal_balance: Decimal::ZERO,
|
|
labor_balance: amount,
|
|
frozen_balance: Decimal::ZERO,
|
|
bank_balance: amount,
|
|
transit_amount: Decimal::ZERO,
|
|
}
|
|
}
|
|
|
|
/// 混合余额(个人 + 劳动)
|
|
pub fn mixed(personal: Decimal, labor: Decimal) -> Self {
|
|
Self {
|
|
personal_balance: personal,
|
|
labor_balance: labor,
|
|
frozen_balance: Decimal::ZERO,
|
|
bank_balance: personal + labor,
|
|
transit_amount: Decimal::ZERO,
|
|
}
|
|
}
|
|
|
|
/// 部分冻结
|
|
pub fn with_frozen(personal: Decimal, labor: Decimal, frozen: Decimal) -> Self {
|
|
Self {
|
|
personal_balance: personal,
|
|
labor_balance: labor,
|
|
frozen_balance: frozen,
|
|
bank_balance: personal + labor + frozen,
|
|
transit_amount: Decimal::ZERO,
|
|
}
|
|
}
|
|
|
|
/// 有在途金额
|
|
pub fn with_transit(personal: Decimal, labor: Decimal, transit: Decimal) -> Self {
|
|
Self {
|
|
personal_balance: personal,
|
|
labor_balance: labor,
|
|
frozen_balance: Decimal::ZERO,
|
|
bank_balance: personal + labor,
|
|
transit_amount: transit,
|
|
}
|
|
}
|
|
|
|
/// 标准测试余额
|
|
pub fn standard() -> Self {
|
|
Self::mixed(dec!(5000.00), dec!(3000.00))
|
|
}
|
|
|
|
/// 不变量是否满足
|
|
pub fn invariant_holds(&self) -> bool {
|
|
self.personal_balance + self.labor_balance + self.frozen_balance == self.bank_balance
|
|
}
|
|
|
|
/// 可用余额
|
|
pub fn available(&self) -> Decimal {
|
|
self.personal_balance + self.labor_balance
|
|
}
|
|
}
|
|
|
|
// ==================== 交易夹具 ====================
|
|
|
|
/// 交易测试数据
|
|
#[derive(Debug, Clone)]
|
|
pub struct TransactionFixture {
|
|
pub txn_no: String,
|
|
pub amount: Decimal,
|
|
pub from_account: String,
|
|
pub to_account: String,
|
|
pub remark: Option<String>,
|
|
}
|
|
|
|
impl TransactionFixture {
|
|
/// 生成唯一交易号
|
|
pub fn gen_txn_no() -> String {
|
|
format!("TXN{:016}", Utc::now().timestamp_nanos_opt().unwrap_or(0))
|
|
}
|
|
|
|
/// 标准转账
|
|
pub fn standard_transfer() -> Self {
|
|
Self {
|
|
txn_no: Self::gen_txn_no(),
|
|
amount: TestAmounts::medium(),
|
|
from_account: PRISON_MAIN_ACCOUNT.to_string(),
|
|
to_account: gen_inmate_account_no(1001),
|
|
remark: Some("测试转账".to_string()),
|
|
}
|
|
}
|
|
|
|
/// 提现交易
|
|
pub fn withdrawal(inmate_id: i64, amount: Decimal) -> Self {
|
|
Self {
|
|
txn_no: Self::gen_txn_no(),
|
|
amount,
|
|
from_account: gen_inmate_account_no(inmate_id),
|
|
to_account: FAMILY_ACCOUNT.to_string(),
|
|
remark: Some("罪犯提现".to_string()),
|
|
}
|
|
}
|
|
|
|
/// 充值交易
|
|
pub fn deposit(inmate_id: i64, amount: Decimal) -> Self {
|
|
Self {
|
|
txn_no: Self::gen_txn_no(),
|
|
amount,
|
|
from_account: FAMILY_ACCOUNT.to_string(),
|
|
to_account: gen_inmate_account_no(inmate_id),
|
|
remark: Some("家属充值".to_string()),
|
|
}
|
|
}
|
|
|
|
/// 劳动报酬发放
|
|
pub fn labor_payment(inmate_id: i64, amount: Decimal) -> Self {
|
|
Self {
|
|
txn_no: Self::gen_txn_no(),
|
|
amount,
|
|
from_account: PRISON_MAIN_ACCOUNT.to_string(),
|
|
to_account: gen_inmate_account_no(inmate_id),
|
|
remark: Some("劳动报酬".to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== 对账夹具 ====================
|
|
|
|
/// 对账测试数据
|
|
pub struct ReconciliationFixture;
|
|
|
|
impl ReconciliationFixture {
|
|
/// 平衡的三账数据
|
|
pub fn balanced() -> (Decimal, Decimal, Decimal) {
|
|
(
|
|
dec!(100000.00), // 银行余额
|
|
dec!(95000.00), // 总账余额
|
|
dec!(5000.00), // 在途净额
|
|
)
|
|
// 总账 + 在途 = 银行,平衡
|
|
}
|
|
|
|
/// 短款(银行少)
|
|
pub fn short() -> (Decimal, Decimal, Decimal) {
|
|
(
|
|
dec!(100000.00), // 银行余额
|
|
dec!(100000.00), // 总账余额
|
|
dec!(5000.00), // 在途净额
|
|
)
|
|
// 银行 < 总账 + 在途,短款
|
|
}
|
|
|
|
/// 长款(银行多)
|
|
pub fn long() -> (Decimal, Decimal, Decimal) {
|
|
(
|
|
dec!(110000.00), // 银行余额
|
|
dec!(100000.00), // 总账余额
|
|
dec!(5000.00), // 在途净额
|
|
)
|
|
// 银行 > 总账 + 在途,长款
|
|
}
|
|
}
|
|
|
|
// ==================== 时间夹具 ====================
|
|
|
|
pub mod time_fixtures {
|
|
use chrono::{Duration, NaiveDate, Utc};
|
|
|
|
/// 今天
|
|
pub fn today() -> NaiveDate {
|
|
Utc::now().date_naive()
|
|
}
|
|
|
|
/// 昨天
|
|
pub fn yesterday() -> NaiveDate {
|
|
(Utc::now() - Duration::days(1)).date_naive()
|
|
}
|
|
|
|
/// 一周前
|
|
pub fn week_ago() -> NaiveDate {
|
|
(Utc::now() - Duration::days(7)).date_naive()
|
|
}
|
|
|
|
/// 一个月前
|
|
pub fn month_ago() -> NaiveDate {
|
|
(Utc::now() - Duration::days(30)).date_naive()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_balance_fixture_invariant() {
|
|
let balance = BalanceFixture::mixed(dec!(1000.00), dec!(500.00));
|
|
assert!(balance.invariant_holds());
|
|
assert_eq!(balance.available(), dec!(1500.00));
|
|
}
|
|
|
|
#[test]
|
|
fn test_gen_inmate_account() {
|
|
let account = gen_inmate_account_no(1001);
|
|
assert!(account.starts_with(INMATE_ACCOUNT_PREFIX));
|
|
assert_eq!(account.len(), 14);
|
|
}
|
|
}
|
|
|