Files
junhong_cmp_fiber/internal/query/shop/fund_summary.go
2026-07-24 16:07:18 +08:00

295 lines
10 KiB
Go

package shop
import (
"context"
"math"
"strings"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// FundSummaryQuery 提供数据权限范围内的店铺资金概况读取投影。
type FundSummaryQuery struct {
db *gorm.DB
}
// NewFundSummaryQuery 创建店铺资金概况 Query。
func NewFundSummaryQuery(db *gorm.DB) *FundSummaryQuery {
return &FundSummaryQuery{db: db}
}
// List 分页查询店铺,并以固定批次数投影主钱包、佣金、提现和主账号信息。
func (q *FundSummaryQuery) List(ctx context.Context, req dto.ShopFundSummaryListReq) (*dto.ShopFundSummaryPageResult, error) {
page, pageSize := normalizeFundSummaryPage(req.Page, req.PageSize)
base := middleware.ApplyShopIDFilter(ctx, q.db.WithContext(ctx).Model(&model.Shop{}))
base = applyFundSummaryFilters(base, req)
var total int64
if err := base.Count(&total).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺资金概况总数失败")
}
var shops []model.Shop
if err := base.Order("created_at DESC, id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&shops).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺资金概况列表失败")
}
if len(shops) == 0 {
return &dto.ShopFundSummaryPageResult{Items: []dto.ShopFundSummaryItem{}, Total: total, Page: page, Size: pageSize}, nil
}
shopIDs := make([]uint, 0, len(shops))
for index := range shops {
shopIDs = append(shopIDs, shops[index].ID)
}
wallets, err := q.loadWallets(ctx, shopIDs)
if err != nil {
return nil, err
}
withdrawals, err := q.loadWithdrawals(ctx, shopIDs)
if err != nil {
return nil, err
}
accounts, err := q.loadPrimaryAccounts(ctx, shopIDs)
if err != nil {
return nil, err
}
items := make([]dto.ShopFundSummaryItem, 0, len(shops))
for index := range shops {
item, err := projectFundSummary(shops[index], wallets[shops[index].ID], withdrawals[shops[index].ID], accounts[shops[index].ID])
if err != nil {
return nil, err
}
items = append(items, item)
}
return &dto.ShopFundSummaryPageResult{Items: items, Total: total, Page: page, Size: pageSize}, nil
}
type fundWallets struct {
main *model.AgentWallet
commission *model.AgentWallet
}
type withdrawalAmounts struct {
approved int64
pending int64
}
type withdrawalAggregate struct {
ShopID uint
Status int
Amount int64
}
func normalizeFundSummaryPage(page, pageSize int) (int, int) {
if page <= 0 {
page = constants.DefaultPage
}
if pageSize <= 0 {
pageSize = constants.DefaultPageSize
}
if pageSize > constants.MaxPageSize {
pageSize = constants.MaxPageSize
}
return page, pageSize
}
func applyFundSummaryFilters(db *gorm.DB, req dto.ShopFundSummaryListReq) *gorm.DB {
if shopName := strings.TrimSpace(req.ShopName); shopName != "" {
db = db.Where("shop_name ILIKE ?", "%"+shopName+"%")
}
if username := strings.TrimSpace(req.Username); username != "" {
db = db.Where(`EXISTS (
SELECT 1 FROM tb_account AS account_filter
WHERE account_filter.shop_id = tb_shop.id
AND account_filter.is_primary = TRUE
AND account_filter.deleted_at IS NULL
AND account_filter.username ILIKE ?
)`, "%"+username+"%")
}
return db
}
func (q *FundSummaryQuery) loadWallets(ctx context.Context, shopIDs []uint) (map[uint]fundWallets, error) {
var records []model.AgentWallet
if err := q.db.WithContext(ctx).
Where("shop_id IN ? AND wallet_type IN ?", shopIDs, []string{constants.AgentWalletTypeMain, constants.AgentWalletTypeCommission}).
Order("id ASC").Find(&records).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询店铺钱包失败")
}
result := make(map[uint]fundWallets, len(shopIDs))
for index := range records {
wallet := &records[index]
pair := result[wallet.ShopID]
switch wallet.WalletType {
case constants.AgentWalletTypeMain:
if pair.main == nil {
pair.main = wallet
}
case constants.AgentWalletTypeCommission:
if pair.commission == nil {
pair.commission = wallet
}
}
result[wallet.ShopID] = pair
}
return result, nil
}
func (q *FundSummaryQuery) loadWithdrawals(ctx context.Context, shopIDs []uint) (map[uint]withdrawalAmounts, error) {
var records []withdrawalAggregate
if err := q.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}).
Select("shop_id, status, COALESCE(SUM(amount), 0) AS amount").
Where("shop_id IN ? AND status IN ?", shopIDs, []int{constants.WithdrawalStatusApproved, constants.WithdrawalStatusPending}).
Group("shop_id, status").Scan(&records).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询店铺提现汇总失败")
}
result := make(map[uint]withdrawalAmounts, len(shopIDs))
for _, record := range records {
amounts := result[record.ShopID]
if record.Status == constants.WithdrawalStatusApproved {
amounts.approved = record.Amount
} else if record.Status == constants.WithdrawalStatusPending {
amounts.pending = record.Amount
}
result[record.ShopID] = amounts
}
return result, nil
}
func (q *FundSummaryQuery) loadPrimaryAccounts(ctx context.Context, shopIDs []uint) (map[uint]*model.Account, error) {
var records []model.Account
if err := q.db.WithContext(ctx).
Where("shop_id IN ? AND is_primary = ? AND deleted_at IS NULL", shopIDs, true).
Order("id ASC").Find(&records).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询店铺主账号失败")
}
result := make(map[uint]*model.Account, len(shopIDs))
for index := range records {
account := &records[index]
if account.ShopID != nil && result[*account.ShopID] == nil {
result[*account.ShopID] = account
}
}
return result, nil
}
func projectFundSummary(shop model.Shop, wallets fundWallets, withdrawals withdrawalAmounts, account *model.Account) (dto.ShopFundSummaryItem, error) {
mainProjection, err := projectMainWallet(wallets.main)
if err != nil {
return dto.ShopFundSummaryItem{}, err
}
commissionProjection, err := projectCommissionWallet(wallets.commission, withdrawals)
if err != nil {
return dto.ShopFundSummaryItem{}, err
}
item := dto.ShopFundSummaryItem{
ShopID: shop.ID, ShopName: shop.ShopName, ShopCode: shop.ShopCode,
MainBalance: mainProjection.balance, MainFrozenBalance: mainProjection.frozen,
CashAvailableBalance: mainProjection.cashAvailable, CreditEnabled: mainProjection.creditEnabled,
CreditLimit: mainProjection.creditLimit, AvailableBalance: mainProjection.available,
IsInDebt: mainProjection.isInDebt, DebtAmount: mainProjection.debtAmount, Version: mainProjection.version,
TotalCommission: commissionProjection.total, WithdrawnCommission: withdrawals.approved,
UnwithdrawCommission: commissionProjection.unwithdrawn, FrozenCommission: commissionProjection.frozen,
WithdrawingCommission: withdrawals.pending, AvailableCommission: commissionProjection.available,
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
}
if account != nil {
item.Username = account.Username
item.Phone = account.Phone
}
return item, nil
}
type mainWalletProjection struct {
balance int64
frozen int64
cashAvailable int64
creditEnabled bool
creditLimit int64
available int64
isInDebt bool
debtAmount int64
version int
}
func projectMainWallet(wallet *model.AgentWallet) (mainWalletProjection, error) {
if wallet == nil {
return mainWalletProjection{}, nil
}
cashAvailable, ok := safeFundSub(wallet.Balance, wallet.FrozenBalance)
if !ok {
return mainWalletProjection{}, errors.New(errors.CodeInternalError, "计算主钱包现金可用金额时发生整数溢出")
}
effectiveCredit := int64(0)
if wallet.CreditEnabled {
effectiveCredit = wallet.CreditLimit
}
available, ok := safeFundAdd(cashAvailable, effectiveCredit)
if !ok {
return mainWalletProjection{}, errors.New(errors.CodeInternalError, "计算主钱包总可用金额时发生整数溢出")
}
debtAmount := int64(0)
if wallet.Balance < 0 {
if wallet.Balance == math.MinInt64 {
return mainWalletProjection{}, errors.New(errors.CodeInternalError, "计算主钱包欠款金额时发生整数溢出")
}
debtAmount = -wallet.Balance
}
return mainWalletProjection{
balance: wallet.Balance, frozen: wallet.FrozenBalance, cashAvailable: cashAvailable,
creditEnabled: wallet.CreditEnabled, creditLimit: wallet.CreditLimit, available: available,
isInDebt: wallet.Balance < 0, debtAmount: debtAmount, version: wallet.Version,
}, nil
}
type commissionProjection struct {
total int64
unwithdrawn int64
frozen int64
available int64
}
func projectCommissionWallet(wallet *model.AgentWallet, withdrawals withdrawalAmounts) (commissionProjection, error) {
balance, frozen := int64(0), int64(0)
if wallet != nil {
balance = wallet.Balance
frozen = wallet.FrozenBalance
}
unwithdrawn, ok := safeFundAdd(balance, frozen)
if !ok {
return commissionProjection{}, errors.New(errors.CodeInternalError, "计算未提现佣金时发生整数溢出")
}
total, ok := safeFundAdd(unwithdrawn, withdrawals.approved)
if !ok {
return commissionProjection{}, errors.New(errors.CodeInternalError, "计算累计佣金时发生整数溢出")
}
available, ok := safeFundSub(balance, withdrawals.pending)
if !ok {
return commissionProjection{}, errors.New(errors.CodeInternalError, "计算可提现佣金时发生整数溢出")
}
if available < 0 {
available = 0
}
return commissionProjection{total: total, unwithdrawn: unwithdrawn, frozen: frozen, available: available}, nil
}
func safeFundAdd(left, right int64) (int64, bool) {
if (right > 0 && left > math.MaxInt64-right) || (right < 0 && left < math.MinInt64-right) {
return 0, false
}
return left + right, true
}
func safeFundSub(left, right int64) (int64, bool) {
if (right > 0 && left < math.MinInt64+right) || (right < 0 && left > math.MaxInt64+right) {
return 0, false
}
return left - right, true
}