package app import ( "context" "fmt" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" ) type appBalanceAccount struct { AccountId int64 CreatedDept int64 Balance float64 AccountLabel string Status string } func (s *sAppCard) deductAppBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, now *gtime.Time) (*appBalanceAccount, error) { return s.deductAppBalanceForDept(ctx, tx, user, orderSn, amount, remark, 0, now) } func (s *sAppCard) deductAppBalanceForDept(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, payerDeptId int64, now *gtime.Time) (*appBalanceAccount, error) { amount = appRoundAmount(amount) if amount <= 0 { return nil, gerror.New("扣款金额必须大于0") } var account *appBalanceAccount var err error if s.isTerminalUser(ctx, user) { account, err = s.lockAppBalanceAccount(ctx, tx, user) } else { account, err = s.lockAppAgentBalanceAccount(ctx, tx, user, payerDeptId) } if err != nil { return nil, err } if err = s.ensureAppBalanceAccountConsumable(account); err != nil { return nil, err } if account.Balance < amount { return nil, gerror.Newf("账户余额不足,当前余额 %.2f 元,应付 %.2f 元", account.Balance, amount) } afterBalance := appRoundAmount(account.Balance - amount) if s.isTerminalUser(ctx, user) { result, err := dao.TTerminal.Ctx(ctx).TX(tx). Where(dao.TTerminal.Columns().TenantId, user.TenantId). Where(dao.TTerminal.Columns().TerminalId, user.UserId). Data(do.TTerminal{ Balance: afterBalance, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "扣减终端账户余额失败") } if affected, err := result.RowsAffected(); err != nil { return nil, gerror.Wrap(err, "读取终端账户余额扣减数量失败") } else if affected != 1 { return nil, gerror.New("终端账户余额已变化,请刷新后重试") } } else { result, err := dao.TAgentBalance.Ctx(ctx).TX(tx). Where(dao.TAgentBalance.Columns().TenantId, user.TenantId). Where(dao.TAgentBalance.Columns().DeptId, account.AccountId). Data(do.TAgentBalance{ Balance: afterBalance, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "扣减企业账户余额失败") } if affected, err := result.RowsAffected(); err != nil { return nil, gerror.Wrap(err, "读取企业账户余额扣减数量失败") } else if affected != 1 { return nil, gerror.New("企业账户余额已变化,请刷新后重试") } } account.Balance = afterBalance if err = s.insertAppBalanceDetail(ctx, tx, user, account, orderSn, amount, account.Balance+amount, afterBalance, consts.BalanceExpense, consts.BalanceTransactionTypeConsume, remark, now); err != nil { return nil, err } return account, nil } func (s *sAppCard) creditAppBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, channelSerialNo string, now *gtime.Time) (*appBalanceAccount, error) { return s.creditAppBalanceWithChannel(ctx, tx, user, orderSn, amount, remark, consts.BalanceTransactionChannelWechat, channelSerialNo, now) } func (s *sAppCard) creditAppBalanceWithChannel(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, transactionChannel int, channelSerialNo string, now *gtime.Time) (*appBalanceAccount, error) { amount = appRoundAmount(amount) if amount <= 0 { return nil, gerror.New("入账金额必须大于0") } account, err := s.lockAppBalanceAccount(ctx, tx, user) if err != nil { return nil, err } beforeBalance := account.Balance afterBalance := appRoundAmount(beforeBalance + amount) if s.isTerminalUser(ctx, user) { result, err := dao.TTerminal.Ctx(ctx).TX(tx). Where(dao.TTerminal.Columns().TenantId, user.TenantId). Where(dao.TTerminal.Columns().TerminalId, user.UserId). Data(do.TTerminal{ Balance: afterBalance, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "增加终端账户余额失败") } if affected, err := result.RowsAffected(); err != nil { return nil, gerror.Wrap(err, "读取终端账户余额入账数量失败") } else if affected != 1 { return nil, gerror.New("终端账户余额已变化,请刷新后重试") } } else { result, err := dao.TAgentBalance.Ctx(ctx).TX(tx). Where(dao.TAgentBalance.Columns().TenantId, user.TenantId). Where(dao.TAgentBalance.Columns().DeptId, user.DeptId). Data(do.TAgentBalance{ Balance: afterBalance, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "增加企业账户余额失败") } if affected, err := result.RowsAffected(); err != nil { return nil, gerror.Wrap(err, "读取企业账户余额入账数量失败") } else if affected != 1 { return nil, gerror.New("企业账户余额已变化,请刷新后重试") } } account.Balance = afterBalance if err = s.insertAppBalanceDetailWithChannel(ctx, tx, user, account, orderSn, amount, beforeBalance, afterBalance, consts.BalanceIncome, consts.BalanceTransactionTypeRecharge, transactionChannel, channelSerialNo, remark, now); err != nil { return nil, err } return account, nil } func (s *sAppCard) lockAppBalanceAccount(ctx context.Context, tx gdb.TX, user *entity.SysUser) (*appBalanceAccount, error) { if user == nil { return nil, gerror.New("当前用户不存在") } if s.isTerminalUser(ctx, user) { var terminal *entity.TTerminal if err := dao.TTerminal.Ctx(ctx).TX(tx). Where(dao.TTerminal.Columns().TenantId, user.TenantId). Where(dao.TTerminal.Columns().TerminalId, user.UserId). LockUpdate(). Limit(1). Scan(&terminal); err != nil { return nil, gerror.Wrap(err, "锁定终端账户余额失败") } if terminal == nil || terminal.TerminalId == 0 { return nil, gerror.New("终端账户不存在,无法余额扣款") } return &appBalanceAccount{ AccountId: user.UserId, CreatedDept: appUserDeptOrDefault(user), Balance: terminal.Balance, AccountLabel: "terminal", Status: strings.TrimSpace(terminal.Status), }, nil } return s.lockAppAgentBalanceAccount(ctx, tx, user, 0) } func (s *sAppCard) lockAppAgentBalanceAccount(ctx context.Context, tx gdb.TX, user *entity.SysUser, payerDeptId int64) (*appBalanceAccount, error) { if user == nil { return nil, gerror.New("当前用户不存在") } payerDeptId = appBalancePayerDeptId(user, payerDeptId) if payerDeptId <= 0 { return nil, gerror.New("当前用户未绑定机构,无法余额扣款") } var agentBalance *entity.TAgentBalance if err := dao.TAgentBalance.Ctx(ctx).TX(tx). Where(dao.TAgentBalance.Columns().TenantId, user.TenantId). Where(dao.TAgentBalance.Columns().DeptId, payerDeptId). LockUpdate(). Limit(1). Scan(&agentBalance); err != nil { return nil, gerror.Wrap(err, "锁定企业账户余额失败") } if agentBalance == nil || agentBalance.Id == 0 { createdBalance, createErr := s.ensureAppAgentBalanceForDept(ctx, tx, user, payerDeptId, gtime.Now()) if createErr != nil { return nil, createErr } agentBalance = createdBalance } if agentBalance.Status != 0 { return nil, gerror.New("企业账户已停用,无法余额扣款") } return &appBalanceAccount{ AccountId: payerDeptId, CreatedDept: payerDeptId, Balance: agentBalance.Balance, AccountLabel: "agent", Status: fmt.Sprintf("%d", agentBalance.Status), }, nil } func (s *sAppCard) ensureAppBalanceAccountConsumable(account *appBalanceAccount) error { if account == nil { return gerror.New("余额账户信息不完整") } switch account.AccountLabel { case "terminal": if status := strings.TrimSpace(account.Status); status != "" && status != consts.SysUserStatusNormal { return gerror.New("终端账户已停用,无法扣减余额") } case "agent": if status := strings.TrimSpace(account.Status); status != "" && status != "0" { return gerror.New("企业账户已停用,无法扣减余额") } } return nil } func (s *sAppCard) ensureAppAgentBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, now *gtime.Time) (*entity.TAgentBalance, error) { return s.ensureAppAgentBalanceForDept(ctx, tx, user, 0, now) } func (s *sAppCard) ensureAppAgentBalanceForDept(ctx context.Context, tx gdb.TX, user *entity.SysUser, payerDeptId int64, now *gtime.Time) (*entity.TAgentBalance, error) { if user == nil { return nil, gerror.New("当前用户未绑定企业,无法初始化企业账户") } payerDeptId = appBalancePayerDeptId(user, payerDeptId) if payerDeptId <= 0 { return nil, gerror.New("当前用户未绑定企业,无法初始化企业账户") } if now == nil { now = gtime.Now() } var dept *entity.SysDept deptMod := dao.SysDept.Ctx(ctx). Where(dao.SysDept.Columns().TenantId, user.TenantId). Where(dao.SysDept.Columns().DeptId, payerDeptId). Limit(1) if tx != nil { deptMod = deptMod.TX(tx).LockUpdate() } if err := deptMod.Scan(&dept); err != nil { return nil, gerror.Wrap(err, "查询企业机构失败") } if dept == nil || dept.DeptId == 0 { return nil, gerror.New("企业机构不存在,无法初始化余额账户") } balanceModel := func() *gdb.Model { mod := dao.TAgentBalance.Ctx(ctx) if tx != nil { mod = mod.TX(tx) } return mod } var existing *entity.TAgentBalance if err := balanceModel(). Where(dao.TAgentBalance.Columns().TenantId, user.TenantId). Where(dao.TAgentBalance.Columns().DeptId, payerDeptId). LockUpdate(). Limit(1). Scan(&existing); err != nil { return nil, gerror.Wrap(err, "查询企业余额账户失败") } if existing != nil && existing.Id > 0 { return existing, nil } id, err := balanceModel().Data(do.TAgentBalance{ DeptId: payerDeptId, Balance: 0, WithdrawAmount: 0, WithdrawAmountStatus: 1, InvoiceAmount: 0, CanInvoiceAmount: 0, CreditLimit: 0, UsedCredit: 0, PendingRepayment: 0, Status: 0, TenantId: user.TenantId, CreatedDept: payerDeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).InsertAndGetId() if err != nil { var created *entity.TAgentBalance if queryErr := balanceModel(). Where(dao.TAgentBalance.Columns().TenantId, user.TenantId). Where(dao.TAgentBalance.Columns().DeptId, payerDeptId). LockUpdate(). Limit(1). Scan(&created); queryErr == nil && created != nil && created.Id > 0 { return created, nil } return nil, gerror.Wrap(err, "初始化企业余额账户失败") } return &entity.TAgentBalance{ Id: id, DeptId: payerDeptId, Balance: 0, WithdrawAmountStatus: 1, Status: 0, TenantId: user.TenantId, CreatedDept: payerDeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }, nil } func appBalancePayerDeptId(user *entity.SysUser, payerDeptId int64) int64 { if payerDeptId > 0 { return payerDeptId } if user != nil { return user.DeptId } return 0 } func (s *sAppCard) insertAppBalanceDetail(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceAccount, orderSn string, amount, beforeBalance, afterBalance float64, incomeExpenseType int, transactionType int, remark string, now *gtime.Time) error { return s.insertAppBalanceDetailWithChannel(ctx, tx, user, account, orderSn, amount, beforeBalance, afterBalance, incomeExpenseType, transactionType, consts.BalanceTransactionChannelBalance, "", remark, now) } func (s *sAppCard) insertAppBalanceDetailWithChannel(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceAccount, orderSn string, amount, beforeBalance, afterBalance float64, incomeExpenseType int, transactionType int, transactionChannel int, channelSerialNo string, remark string, now *gtime.Time) error { if user == nil || account == nil { return gerror.New("余额流水账户信息不完整") } serialNo := appCardBatchNo("BAL", user.UserId, now) if _, err := dao.TBalanceDetail.Ctx(ctx).TX(tx).Data(do.TBalanceDetail{ DeptId: account.AccountId, SerialNo: serialNo, TransactionTime: now, IncomeExpenseType: incomeExpenseType, TransactionType: transactionType, TransactionChannel: transactionChannel, Amount: appRoundAmount(amount), BeforeBalance: appRoundAmount(beforeBalance), BalanceAfter: appRoundAmount(afterBalance), ChannelSerialNo: channelSerialNo, OrderSn: orderSn, Remark: fmt.Sprintf("%s:%s", account.AccountLabel, remark), TenantId: user.TenantId, CreatedDept: account.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert(); err != nil { return gerror.Wrap(err, "写入余额流水失败") } return nil }