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

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);
}
}