295 lines
10 KiB
Go
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
|
|
}
|