Files
iotlcm/server/internal/logic/app/app_balance.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

380 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}