暂存一下,防止丢失
This commit is contained in:
294
internal/query/shop/fund_summary.go
Normal file
294
internal/query/shop/fund_summary.go
Normal file
@@ -0,0 +1,294 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user