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 }