Files
junhong_cmp_fiber/internal/domain/wallet/wallet.go
2026-07-24 16:07:18 +08:00

251 lines
7.3 KiB
Go

package wallet
import (
"math"
"github.com/break/junhong_cmp_fiber/pkg/constants"
appErrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// AgentWallet 表示代理钱包聚合的资金状态。
//
// 当前聚合统一维护扣款、冻结、完成扣除、正向入账、退款回充与调额不变量。
type AgentWallet struct {
ID uint
ShopID uint
WalletType string
Balance int64
FrozenBalance int64
CreditEnabled bool
CreditLimit int64
Status int
Version int
}
// Debit 从代理主钱包扣减指定金额,并保证扣款后总可用金额不为负数。
func (w *AgentWallet) Debit(amount int64) error {
if err := w.validateMainWalletMutation(amount); err != nil {
return err
}
candidate := *w
balance, ok := safeSub(candidate.Balance, amount)
if !ok {
return appErrors.New(appErrors.CodeInvalidParam, "扣减钱包余额时发生整数溢出")
}
candidate.Balance = balance
if err := candidate.Validate(); err != nil {
return err
}
w.Balance = balance
return nil
}
// Credit 向代理主钱包增加账面余额,负余额会自然表现为欠款减少。
func (w *AgentWallet) Credit(amount int64) error {
if err := w.validateMainWalletMutation(amount); err != nil {
return err
}
balance, ok := safeAdd(w.Balance, amount)
if !ok {
return appErrors.New(appErrors.CodeInvalidParam, "增加钱包余额时发生整数溢出")
}
candidate := *w
candidate.Balance = balance
if err := candidate.Validate(); err != nil {
return err
}
w.Balance = balance
return nil
}
// Freeze 预占代理主钱包资金,冻结金额会占用现金和信用但不直接形成欠款。
func (w *AgentWallet) Freeze(amount int64) error {
if err := w.validateMainWalletMutation(amount); err != nil {
return err
}
candidate := *w
frozen, ok := safeAdd(candidate.FrozenBalance, amount)
if !ok {
return appErrors.New(appErrors.CodeInvalidParam, "增加钱包冻结金额时发生整数溢出")
}
candidate.FrozenBalance = frozen
if err := candidate.Validate(); err != nil {
return err
}
w.FrozenBalance = frozen
return nil
}
// Release 释放已经预占的代理主钱包资金。
func (w *AgentWallet) Release(amount int64) error {
if err := w.validateMainWalletMutation(amount); err != nil {
return err
}
if w.FrozenBalance < amount {
return appErrors.New(appErrors.CodeInsufficientBalance, "钱包冻结金额不足")
}
w.FrozenBalance -= amount
return w.Validate()
}
// CompleteReserved 完成冻结资金扣除,同时减少账面余额和冻结金额。
func (w *AgentWallet) CompleteReserved(amount int64) error {
if err := w.validateMainWalletMutation(amount); err != nil {
return err
}
if w.FrozenBalance < amount {
return appErrors.New(appErrors.CodeInsufficientBalance, "钱包冻结金额不足")
}
balance, ok := safeSub(w.Balance, amount)
if !ok {
return appErrors.New(appErrors.CodeInvalidParam, "完成冻结资金扣除时发生整数溢出")
}
candidate := *w
candidate.Balance = balance
candidate.FrozenBalance -= amount
if err := candidate.Validate(); err != nil {
return err
}
w.Balance = candidate.Balance
w.FrozenBalance = candidate.FrozenBalance
return nil
}
func (w AgentWallet) validateMainWalletMutation(amount int64) error {
if amount <= 0 {
return appErrors.New(appErrors.CodeInvalidParam, "钱包变更金额必须大于零")
}
if w.WalletType != constants.AgentWalletTypeMain {
return appErrors.New(appErrors.CodeInvalidParam, "仅代理主钱包支持该资金操作")
}
if w.Status != constants.AgentWalletStatusNormal {
return appErrors.New(appErrors.CodeInvalidStatus, "当前钱包状态不允许资金操作")
}
return nil
}
// ChangeCredit 调整主钱包实际信用额度并重新校验完整资金边界。
func (w *AgentWallet) ChangeCredit(enabled bool, limit int64) error {
candidate := *w
candidate.CreditEnabled = enabled
candidate.CreditLimit = limit
if err := candidate.Validate(); err != nil {
return err
}
w.CreditEnabled = enabled
w.CreditLimit = limit
return nil
}
// Validate 校验钱包类型、信用配置、版本与总可用金额不变量。
func (w AgentWallet) Validate() error {
if w.WalletType != constants.AgentWalletTypeMain && w.WalletType != constants.AgentWalletTypeCommission {
return appErrors.New(appErrors.CodeInvalidParam, "代理钱包类型无效")
}
if w.Version < 0 {
return appErrors.New(appErrors.CodeInvalidParam, "钱包版本不能为负数")
}
if w.FrozenBalance < 0 {
return appErrors.New(appErrors.CodeInvalidParam, "冻结金额不能为负数")
}
if err := validateCredit(w.WalletType, w.CreditEnabled, w.CreditLimit); err != nil {
return err
}
if _, err := w.CashAvailableBalance(); err != nil {
return err
}
available, err := w.AvailableBalance()
if err != nil {
return err
}
if available < 0 {
return appErrors.New(appErrors.CodeInsufficientBalance, "钱包总可用金额不能为负数")
}
if _, err := w.DebtAmount(); err != nil {
return err
}
return nil
}
// EffectiveCredit 返回当前实际生效的信用额度。
func (w AgentWallet) EffectiveCredit() int64 {
if !w.CreditEnabled {
return 0
}
return w.CreditLimit
}
// CashAvailableBalance 返回现金可用金额,即账面余额减冻结金额。
func (w AgentWallet) CashAvailableBalance() (int64, error) {
available, ok := safeSub(w.Balance, w.FrozenBalance)
if !ok {
return 0, appErrors.New(appErrors.CodeInvalidParam, "计算现金可用金额时发生整数溢出")
}
return available, nil
}
// AvailableBalance 返回总可用金额,即现金可用金额加有效信用额度。
func (w AgentWallet) AvailableBalance() (int64, error) {
cashAvailable, err := w.CashAvailableBalance()
if err != nil {
return 0, err
}
available, ok := safeAdd(cashAvailable, w.EffectiveCredit())
if !ok {
return 0, appErrors.New(appErrors.CodeInvalidParam, "计算钱包总可用金额时发生整数溢出")
}
return available, nil
}
// IsInDebt 返回账面余额是否已经形成欠款。
func (w AgentWallet) IsInDebt() bool {
return w.Balance < 0
}
// DebtAmount 返回欠款金额;冻结金额不直接计入欠款。
func (w AgentWallet) DebtAmount() (int64, error) {
if !w.IsInDebt() {
return 0, nil
}
if w.Balance == math.MinInt64 {
return 0, appErrors.New(appErrors.CodeInvalidParam, "计算钱包欠款金额时发生整数溢出")
}
return -w.Balance, nil
}
func validateCredit(walletType string, enabled bool, limit int64) error {
if limit < 0 {
return appErrors.New(appErrors.CodeInvalidParam, "信用额度不能为负数")
}
if !enabled && limit != 0 {
return appErrors.New(appErrors.CodeInvalidParam, "关闭信用时信用额度必须为零")
}
if enabled && limit == 0 {
return appErrors.New(appErrors.CodeInvalidParam, "启用信用时信用额度必须大于零")
}
if walletType != constants.AgentWalletTypeMain && (enabled || limit != 0) {
return appErrors.New(appErrors.CodeInvalidParam, "只有代理主钱包可以启用信用额度")
}
return nil
}
func safeAdd(left, right int64) (int64, bool) {
if right > 0 && left > math.MaxInt64-right {
return 0, false
}
if right < 0 && left < math.MinInt64-right {
return 0, false
}
return left + right, true
}
func safeSub(left, right int64) (int64, bool) {
if right > 0 && left < math.MinInt64+right {
return 0, false
}
if right < 0 && left > math.MaxInt64+right {
return 0, false
}
return left - right, true
}