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

614 lines
17 KiB
Rust

//! 账务域实体定义
use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::domain::account::AccountType;
/// 借贷方向
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
/// 借方
Debit,
/// 贷方
Credit,
}
impl Direction {
/// 获取相反方向
pub fn opposite(&self) -> Self {
match self {
Self::Debit => Self::Credit,
Self::Credit => Self::Debit,
}
}
}
impl std::fmt::Display for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Debit => write!(f, "debit"),
Self::Credit => write!(f, "credit"),
}
}
}
/// 会计科目类别
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubjectCategory {
/// 资产类
Asset,
/// 负债类
Liability,
/// 收入类
Income,
/// 支出类
Expense,
}
impl SubjectCategory {
/// 获取默认增加方向
pub fn default_direction(&self) -> Direction {
match self {
Self::Asset | Self::Expense => Direction::Debit,
Self::Liability | Self::Income => Direction::Credit,
}
}
}
impl std::fmt::Display for SubjectCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Asset => write!(f, "asset"),
Self::Liability => write!(f, "liability"),
Self::Income => write!(f, "income"),
Self::Expense => write!(f, "expense"),
}
}
}
/// 分录状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryStatus {
/// 待确认
Pending,
/// 已过账
Posted,
/// 已冲销
Reversed,
}
impl Default for EntryStatus {
fn default() -> Self {
Self::Pending
}
}
impl std::fmt::Display for EntryStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => write!(f, "pending"),
Self::Posted => write!(f, "posted"),
Self::Reversed => write!(f, "reversed"),
}
}
}
/// 会计科目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountingSubject {
/// 科目代码
pub code: String,
/// 科目名称
pub name: String,
/// 科目类别
pub category: SubjectCategory,
/// 默认增加方向 (1=借方增加, -1=贷方增加)
pub direction_default: i8,
/// 父科目代码
pub parent_code: Option<String>,
/// 科目级别
pub level: i32,
}
impl AccountingSubject {
/// 预定义科目:银行存款
pub const BANK_DEPOSIT: &'static str = "1002";
/// 预定义科目:在途资金
pub const IN_TRANSIT: &'static str = "1003";
/// 预定义科目:客户存款
pub const CUSTOMER_DEPOSIT: &'static str = "2001";
/// 预定义科目:待清算款项
pub const PENDING_SETTLEMENT: &'static str = "2002";
/// 预定义科目:手续费收入
pub const FEE_INCOME: &'static str = "3001";
/// 预定义科目:利息支出
pub const INTEREST_EXPENSE: &'static str = "4001";
}
/// 账户余额 - 三科目模型
///
/// 不变量约束: personal_balance + labor_balance + frozen_balance = bank_balance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountBalance {
/// 余额ID
pub id: i64,
/// 账户ID
pub account_id: i64,
/// 账户类型
pub account_type: AccountType,
// ========== 三科目余额 ==========
/// 个人余额(可用)
pub personal_balance: Decimal,
/// 劳动报酬(可用)
pub labor_balance: Decimal,
/// 冻结余额(不可用)
pub frozen_balance: Decimal,
// ========== 银行对照 ==========
/// 银行余额(对照账面)
pub bank_balance: Decimal,
// ========== 在途管理 ==========
/// 在途金额(已从可用划转,等待银行确认)
pub transit_amount: Decimal,
// ========== 兼容字段(逐步废弃)==========
/// 系统余额(内部记账)- 兼容旧代码
#[serde(default)]
pub system_balance: Decimal,
/// 可支配余额 - 兼容旧代码
#[serde(default)]
pub available_balance: Decimal,
/// 冻结金额 - 兼容旧代码,映射到 frozen_balance
#[serde(default)]
pub frozen_amount: Decimal,
/// 乐观锁版本
pub version: i32,
/// 更新时间
pub updated_at: DateTime<Utc>,
}
impl AccountBalance {
/// 创建新余额记录
pub fn new(account_id: i64, account_type: AccountType) -> Self {
Self {
id: 0,
account_id,
account_type,
// 三科目余额
personal_balance: Decimal::ZERO,
labor_balance: Decimal::ZERO,
frozen_balance: Decimal::ZERO,
// 银行对照
bank_balance: Decimal::ZERO,
// 在途
transit_amount: Decimal::ZERO,
// 兼容字段
system_balance: Decimal::ZERO,
available_balance: Decimal::ZERO,
frozen_amount: Decimal::ZERO,
version: 0,
updated_at: Utc::now(),
}
}
/// 计算总可用余额(个人 + 劳动)
pub fn total_available(&self) -> Decimal {
self.personal_balance + self.labor_balance
}
/// 计算可支配余额(兼容旧代码)
pub fn calculate_available(&self) -> Decimal {
self.total_available() - self.transit_amount
}
/// 检查是否有足够可支配余额
pub fn has_sufficient_balance(&self, amount: Decimal) -> bool {
self.calculate_available() >= amount
}
/// 校验不变量: personal + labor + frozen = bank_balance
///
/// 返回 Ok(()) 如果不变量成立,否则返回 InvariantViolation 错误
pub fn validate_invariant(&self) -> Result<(), crate::error::AppError> {
let sum = self.personal_balance + self.labor_balance + self.frozen_balance;
if sum == self.bank_balance {
Ok(())
} else {
Err(crate::error::AppError::InvariantViolation {
account_id: self.account_id,
expected: self.bank_balance,
actual: sum,
})
}
}
/// 增加个人余额
pub fn add_personal_balance(&mut self, amount: Decimal) {
self.personal_balance += amount;
self.sync_legacy_fields();
}
/// 减少个人余额
pub fn subtract_personal_balance(&mut self, amount: Decimal) {
self.personal_balance -= amount;
self.sync_legacy_fields();
}
/// 增加劳动报酬
pub fn add_labor_balance(&mut self, amount: Decimal) {
self.labor_balance += amount;
self.sync_legacy_fields();
}
/// 减少劳动报酬
pub fn subtract_labor_balance(&mut self, amount: Decimal) {
self.labor_balance -= amount;
self.sync_legacy_fields();
}
/// 增加系统余额(兼容旧代码,默认增加到个人余额)
pub fn add_system_balance(&mut self, amount: Decimal) {
self.personal_balance += amount;
self.sync_legacy_fields();
}
/// 减少系统余额(兼容旧代码,按优先级从个人/劳动扣减)
pub fn subtract_system_balance(&mut self, amount: Decimal) {
let mut remaining = amount;
// 先扣个人
let from_personal = remaining.min(self.personal_balance);
self.personal_balance -= from_personal;
remaining -= from_personal;
// 再扣劳动
if remaining > Decimal::ZERO {
let from_labor = remaining.min(self.labor_balance);
self.labor_balance -= from_labor;
}
self.sync_legacy_fields();
}
/// 冻结金额(从可用余额转移到冻结余额)
///
/// 按优先级从个人余额和劳动报酬扣减
pub fn freeze(&mut self, amount: Decimal) {
let mut remaining = amount;
// 先从个人余额扣减
let from_personal = remaining.min(self.personal_balance);
self.personal_balance -= from_personal;
remaining -= from_personal;
// 再从劳动报酬扣减
if remaining > Decimal::ZERO {
let from_labor = remaining.min(self.labor_balance);
self.labor_balance -= from_labor;
}
// 增加冻结余额
self.frozen_balance += amount;
self.sync_legacy_fields();
}
/// 解冻金额(从冻结余额转移回个人余额)
pub fn unfreeze(&mut self, amount: Decimal) {
let unfreeze_amount = amount.min(self.frozen_balance);
self.frozen_balance -= unfreeze_amount;
// 解冻默认返回到个人余额
self.personal_balance += unfreeze_amount;
self.sync_legacy_fields();
}
/// 设置在途金额
pub fn set_transit(&mut self, amount: Decimal) {
self.transit_amount = amount;
self.sync_legacy_fields();
}
/// 增加在途金额
pub fn add_transit(&mut self, amount: Decimal) {
self.transit_amount += amount;
self.sync_legacy_fields();
}
/// 减少在途金额
pub fn subtract_transit(&mut self, amount: Decimal) {
self.transit_amount -= amount;
if self.transit_amount < Decimal::ZERO {
self.transit_amount = Decimal::ZERO;
}
self.sync_legacy_fields();
}
/// 同步银行余额
pub fn sync_bank_balance(&mut self, amount: Decimal) {
self.bank_balance = amount;
}
/// 同步遗留字段(保持向后兼容)
fn sync_legacy_fields(&mut self) {
self.system_balance = self.personal_balance + self.labor_balance;
self.available_balance = self.calculate_available();
self.frozen_amount = self.frozen_balance;
}
// ========== 新增便捷方法(用于测试) ==========
/// 增加个人余额(简化版)
pub fn add_personal(&mut self, amount: Decimal) {
self.personal_balance += amount;
self.bank_balance += amount;
self.sync_legacy_fields();
}
/// 减少个人余额(带校验)
pub fn subtract_personal(&mut self, amount: Decimal) -> Result<(), crate::error::AppError> {
if self.personal_balance < amount {
return Err(crate::error::AppError::InsufficientBalance {
available: self.personal_balance,
required: amount,
});
}
self.personal_balance -= amount;
self.bank_balance -= amount;
self.sync_legacy_fields();
Ok(())
}
/// 增加劳动报酬(简化版)
pub fn add_labor(&mut self, amount: Decimal) {
self.labor_balance += amount;
self.bank_balance += amount;
self.sync_legacy_fields();
}
/// 减少劳动报酬(带校验)
pub fn subtract_labor(&mut self, amount: Decimal) -> Result<(), crate::error::AppError> {
if self.labor_balance < amount {
return Err(crate::error::AppError::InsufficientBalance {
available: self.labor_balance,
required: amount,
});
}
self.labor_balance -= amount;
self.bank_balance -= amount;
self.sync_legacy_fields();
Ok(())
}
/// 可用余额(个人 + 劳动)
pub fn available_balance(&self) -> Decimal {
self.personal_balance + self.labor_balance
}
/// 总余额(三科目之和,应等于银行余额)
pub fn total_balance(&self) -> Decimal {
self.personal_balance + self.labor_balance + self.frozen_balance
}
/// 按优先级扣款: 个人 -> 劳动
///
/// 先从个人余额扣减,不足再从劳动报酬扣减。
/// 返回扣款明细,失败时余额不变。
pub fn deduct_with_priority(&mut self, amount: Decimal) -> Result<DeductionResult, crate::error::AppError> {
if amount.is_zero() {
return Ok(DeductionResult {
from_personal: Decimal::ZERO,
from_labor: Decimal::ZERO,
total: Decimal::ZERO,
});
}
let available = self.available_balance();
if available < amount {
return Err(crate::error::AppError::InsufficientBalance {
available,
required: amount,
});
}
let mut remaining = amount;
// 先扣个人
let from_personal = remaining.min(self.personal_balance);
self.personal_balance -= from_personal;
remaining -= from_personal;
// 再扣劳动
let from_labor = remaining.min(self.labor_balance);
self.labor_balance -= from_labor;
// 同步银行余额
self.bank_balance -= amount;
self.sync_legacy_fields();
Ok(DeductionResult {
from_personal,
from_labor,
total: amount,
})
}
/// 结转在途(银行确认成功)
///
/// 银行已确认扣款,从在途中扣除金额,银行余额应该已经同步
pub fn settle_transit(&mut self, amount: Decimal) -> Result<(), crate::error::AppError> {
if self.transit_amount < amount {
return Err(crate::error::AppError::BusinessRule(
format!("在途金额不足: 在途 {}, 需要 {}", self.transit_amount, amount)
));
}
self.transit_amount -= amount;
self.sync_legacy_fields();
Ok(())
}
/// 回退在途(银行失败)
///
/// 银行失败或超时,将在途金额返回到个人余额
pub fn rollback_transit(&mut self, amount: Decimal) {
let rollback_amount = amount.min(self.transit_amount);
self.transit_amount -= rollback_amount;
// 返回到个人余额
self.personal_balance += rollback_amount;
// 恢复银行余额
self.bank_balance += rollback_amount;
self.sync_legacy_fields();
}
}
/// 扣款结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeductionResult {
/// 从个人余额扣减的金额
pub from_personal: Decimal,
/// 从劳动报酬扣减的金额
pub from_labor: Decimal,
/// 总扣减金额
pub total: Decimal,
}
/// 三账对账结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreeAccountResult {
/// 银行账余额
pub bank_balance: Decimal,
/// 在途净额
pub transit_net: Decimal,
/// 总账余额
pub ledger_total: Decimal,
/// 是否平衡
pub is_balanced: bool,
/// 差异金额
pub difference: Decimal,
}
/// 余额组成(按会计科目分类)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceComponent {
/// 组成ID
pub id: i64,
/// 余额ID
pub balance_id: i64,
/// 科目代码
pub subject_code: String,
/// 金额
pub amount: Decimal,
}
/// 记账分录(凭证头)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LedgerEntry {
/// 分录ID
pub id: i64,
/// 分录编号
pub entry_no: String,
/// 关联交易号
pub txn_no: String,
/// 记账日期
pub post_date: NaiveDate,
/// 记账时间
pub post_time: DateTime<Utc>,
/// 摘要描述
pub description: Option<String>,
/// 状态
pub status: EntryStatus,
/// 创建时间
pub created_at: DateTime<Utc>,
}
/// 分录明细(凭证行)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LedgerLine {
/// 明细ID
pub id: i64,
/// 分录ID
pub entry_id: i64,
/// 账户ID
pub account_id: i64,
/// 账户类型
pub account_type: AccountType,
/// 科目代码
pub subject_code: String,
/// 借贷方向
pub direction: Direction,
/// 金额
pub amount: Decimal,
}
/// 创建分录请求
#[derive(Debug, Clone, Deserialize)]
pub struct CreateEntryRequest {
/// 关联交易号
pub txn_no: String,
/// 摘要描述
pub description: Option<String>,
/// 分录明细
pub lines: Vec<CreateEntryLineRequest>,
}
/// 创建分录明细请求
#[derive(Debug, Clone, Deserialize)]
pub struct CreateEntryLineRequest {
/// 账户ID
pub account_id: i64,
/// 账户类型
pub account_type: AccountType,
/// 科目代码
pub subject_code: String,
/// 借贷方向
pub direction: Direction,
/// 金额
pub amount: Decimal,
}
impl CreateEntryRequest {
/// 验证借贷是否平衡
pub fn validate_balance(&self) -> Result<(), (Decimal, Decimal)> {
let mut total_debit = Decimal::ZERO;
let mut total_credit = Decimal::ZERO;
for line in &self.lines {
match line.direction {
Direction::Debit => total_debit += line.amount,
Direction::Credit => total_credit += line.amount,
}
}
if total_debit == total_credit {
Ok(())
} else {
Err((total_debit, total_credit))
}
}
}
/// 余额变动记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceChange {
/// 账户ID
pub account_id: i64,
/// 账户类型
pub account_type: AccountType,
/// 变动前余额
pub before: Decimal,
/// 变动后余额
pub after: Decimal,
/// 变动金额
pub change: Decimal,
/// 关联分录ID
pub entry_id: i64,
}