- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
614 lines
17 KiB
Rust
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,
|
|
}
|
|
|
|
|