package withdrawal import ( "context" "errors" "time" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) var ownerIncomeBizTypes = []string{"owner_income", "deposit_compensation", "arbitration_owner_income"} func normalizeReviewContextQuery(query WithdrawalReviewContextQuery) WithdrawalReviewContextQuery { if query.OrderPage < 1 { query.OrderPage = 1 } if query.PickupPage < 1 { query.PickupPage = 1 } if query.WithdrawalPage < 1 { query.WithdrawalPage = 1 } if query.PageSize < 1 || query.PageSize > 50 { query.PageSize = 8 } return query } // AdminReviewContext 返回提现审核弹窗所需的用户财务汇总和最近历史记录。 // 该数据只在打开单笔详情时查询,不进入提现分页列表,避免列表产生 N+1。 func (r *Repository) AdminReviewContext(ctx context.Context, id uint64, query WithdrawalReviewContextQuery) (*WithdrawalReviewContextDTO, error) { query = normalizeReviewContextQuery(query) var current model.WithdrawalRequest if err := r.db.WithContext(ctx).First(¤t, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrWithdrawalNotFound } return nil, err } summary, err := r.withdrawalReviewSummary(ctx, current.UserID, current.DistributionSiteID) if err != nil { return nil, err } orders, err := r.withdrawalReviewOrders(ctx, current.UserID, current.DistributionSiteID, query) if err != nil { return nil, err } pickups, err := r.withdrawalReviewPickups(ctx, current.UserID, current.DistributionSiteID, query) if err != nil { return nil, err } withdrawals, err := r.withdrawalReviewHistory(ctx, current.UserID, current.DistributionSiteID, query) if err != nil { return nil, err } return &WithdrawalReviewContextDTO{ Summary: summary, Orders: orders, OrderPage: query.OrderPage, OrderPageSize: query.PageSize, Pickups: pickups, PickupPage: query.PickupPage, PickupPageSize: query.PageSize, Withdrawals: withdrawals, WithdrawalPage: query.WithdrawalPage, WithdrawalPageSize: query.PageSize, }, nil } func scopeWithdrawalSite(db *gorm.DB, column string, siteID uint64) *gorm.DB { if siteID > 0 { return db.Where(column+" = ?", siteID) } return db.Where("(" + column + " IS NULL OR " + column + " = 0)") } func (r *Repository) withdrawalReviewSummary(ctx context.Context, userID, siteID uint64) (WithdrawalReviewSummaryDTO, error) { db := r.db.WithContext(ctx) var result WithdrawalReviewSummaryDTO var orderSummary struct { OwnerOrderCount int64 FinalizedOrderCount int64 PlatformManagedIncomeCent int64 } orderQuery := scopeWithdrawalSite(db.Table("rental_orders"), "distribution_site_id", siteID) if err := orderQuery. Select(`COUNT(*) AS owner_order_count, COALESCE(SUM(CASE WHEN settlement_status IN ('settled','arbitrated') THEN 1 ELSE 0 END), 0) AS finalized_order_count, COALESCE(SUM(CASE WHEN settlement_mode = 'platform_managed' AND settlement_status IN ('settled','arbitrated') THEN COALESCE(offline_settlement_amount_cent, 0) ELSE 0 END), 0) AS platform_managed_income_cent`). Where("owner_id = ?", userID). Scan(&orderSummary).Error; err != nil { return result, err } var walletIncome int64 walletQuery := scopeWithdrawalSite(db.Table("wallet_ledger AS wl"), "wl.distribution_site_id", siteID) if err := walletQuery. Select("COALESCE(SUM(wl.amount_cent), 0)"). Joins("JOIN rental_orders AS o ON o.id = wl.order_id"). Where("wl.user_id = ? AND o.owner_id = ? AND o.settlement_mode <> ?", userID, userID, "platform_managed"). Where("wl.direction = ? AND wl.biz_type IN ?", "in", ownerIncomeBizTypes). Scan(&walletIncome).Error; err != nil { return result, err } type pickupBaseRow struct { SettlementMode string Count int64 AmountCent int64 } var pickupBases []pickupBaseRow pickupBaseQuery := scopeWithdrawalSite(db.Table("admin_pickups AS p"), "l.distribution_site_id", siteID). Select("p.settlement_mode, COUNT(*) AS count, COALESCE(SUM(p.settle_amount_cent), 0) AS amount_cent"). Joins("JOIN rental_listings AS l ON l.id = p.listing_id"). Where("p.owner_id = ? AND p.status = ?", userID, "completed"). Group("p.settlement_mode") if err := pickupBaseQuery.Scan(&pickupBases).Error; err != nil { return result, err } type pickupAdjustmentRow struct { SettlementMode string AmountCent int64 } var pickupAdjustments []pickupAdjustmentRow pickupAdjustmentQuery := scopeWithdrawalSite(db.Table("admin_pickup_financial_adjustments AS fa"), "l.distribution_site_id", siteID). Select("p.settlement_mode, COALESCE(SUM(fa.settle_delta_cent), 0) AS amount_cent"). Joins("JOIN admin_pickups AS p ON p.id = fa.pickup_id AND p.status = ?", "completed"). Joins("JOIN rental_listings AS l ON l.id = p.listing_id"). Where("p.owner_id = ?", userID). Group("p.settlement_mode") if err := pickupAdjustmentQuery.Scan(&pickupAdjustments).Error; err != nil { return result, err } pickupAmounts := make(map[string]int64, len(pickupBases)) for _, row := range pickupBases { pickupAmounts[row.SettlementMode] += row.AmountCent result.OwnerPickupCount += row.Count } for _, row := range pickupAdjustments { pickupAmounts[row.SettlementMode] += row.AmountCent } result.WalletPickupIncomeCent = pickupAmounts["owner_wallet"] result.PlatformManagedPickupIncomeCent = pickupAmounts["platform_managed"] result.PickupIncomeCent = result.WalletPickupIncomeCent + result.PlatformManagedPickupIncomeCent var withdrawalSummary struct { WithdrawalCount int64 WithdrawnAmountCent int64 PendingWithdrawalAmountCent int64 RejectedWithdrawalCount int64 } withdrawalQuery := scopeWithdrawalSite(db.Table("withdrawal_requests"), "distribution_site_id", siteID) if err := withdrawalQuery. Select(`COUNT(*) AS withdrawal_count, COALESCE(SUM(CASE WHEN status = 'completed' THEN actual_amount_cent ELSE 0 END), 0) AS withdrawn_amount_cent, COALESCE(SUM(CASE WHEN status IN ('pending','processing') THEN actual_amount_cent ELSE 0 END), 0) AS pending_withdrawal_amount_cent, COALESCE(SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END), 0) AS rejected_withdrawal_count`). Where("user_id = ?", userID). Scan(&withdrawalSummary).Error; err != nil { return result, err } result.OwnerOrderCount = orderSummary.OwnerOrderCount result.FinalizedOrderCount = orderSummary.FinalizedOrderCount result.WalletActualIncomeCent = walletIncome + result.WalletPickupIncomeCent result.PlatformManagedIncomeCent = orderSummary.PlatformManagedIncomeCent + result.PlatformManagedPickupIncomeCent result.WithdrawalCount = withdrawalSummary.WithdrawalCount result.WithdrawnAmountCent = withdrawalSummary.WithdrawnAmountCent result.PendingWithdrawalAmountCent = withdrawalSummary.PendingWithdrawalAmountCent result.RejectedWithdrawalCount = withdrawalSummary.RejectedWithdrawalCount result.ReconcileDifferenceAmountCent = result.WalletActualIncomeCent - withdrawalSummary.WithdrawnAmountCent - withdrawalSummary.PendingWithdrawalAmountCent return result, nil } func (r *Repository) withdrawalReviewOrders(ctx context.Context, userID, siteID uint64, query WithdrawalReviewContextQuery) ([]WithdrawalReviewOrderDTO, error) { type orderRow struct { ID uint64 OrderNo string ListingNo string Title string Status string SettlementStatus string SettlementMode string RentAmountCent int64 OfflineSettlementAmountCent int64 CreatedAt time.Time SettledAt *time.Time } var rows []orderRow offset := (query.OrderPage - 1) * query.PageSize orderQuery := scopeWithdrawalSite(r.db.WithContext(ctx), "o.distribution_site_id", siteID) if err := orderQuery. Table("rental_orders AS o"). Select(`o.id, o.order_no, l.listing_no, a.title, o.status, o.settlement_status, o.settlement_mode, o.rent_amount_cent, COALESCE(o.offline_settlement_amount_cent, 0) AS offline_settlement_amount_cent, o.created_at, o.settled_at`). Joins("JOIN rental_listings AS l ON l.id = o.listing_id"). Joins("JOIN game_accounts AS a ON a.id = o.account_id"). Where("o.owner_id = ?", userID). Order("o.id DESC"). Offset(offset).Limit(query.PageSize). Scan(&rows).Error; err != nil { return nil, err } if len(rows) == 0 { return []WithdrawalReviewOrderDTO{}, nil } orderIDs := make([]uint64, 0, len(rows)) for _, row := range rows { orderIDs = append(orderIDs, row.ID) } var walletRows []struct { OrderID uint64 AmountCent int64 } if err := r.db.WithContext(ctx). Table("wallet_ledger"). Select("order_id, COALESCE(SUM(amount_cent), 0) AS amount_cent"). Where("order_id IN ? AND user_id = ? AND direction = ? AND biz_type IN ?", orderIDs, userID, "in", ownerIncomeBizTypes). Group("order_id"). Scan(&walletRows).Error; err != nil { return nil, err } walletIncome := make(map[uint64]int64, len(walletRows)) for _, row := range walletRows { walletIncome[row.OrderID] = row.AmountCent } items := make([]WithdrawalReviewOrderDTO, 0, len(rows)) for _, row := range rows { var income *int64 if row.SettlementStatus == "settled" || row.SettlementStatus == "arbitrated" { amount := walletIncome[row.ID] if row.SettlementMode == "platform_managed" { amount = row.OfflineSettlementAmountCent } income = &amount } items = append(items, WithdrawalReviewOrderDTO{ ID: row.ID, OrderNo: row.OrderNo, ListingNo: row.ListingNo, Title: row.Title, Status: row.Status, SettlementStatus: row.SettlementStatus, SettlementMode: row.SettlementMode, RentAmountCent: row.RentAmountCent, OwnerActualIncomeCent: income, CreatedAt: row.CreatedAt, SettledAt: row.SettledAt, }) } return items, nil } func (r *Repository) withdrawalReviewPickups(ctx context.Context, userID, siteID uint64, query WithdrawalReviewContextQuery) ([]WithdrawalReviewPickupDTO, error) { type pickupRow struct { ID uint64 PickupNo string ListingNo string Title string Status string SettlementMode string OfflineSettlementStatus string SettleAmountCent int64 CompletedAt *time.Time CreatedAt time.Time } var rows []pickupRow offset := (query.PickupPage - 1) * query.PageSize pickupQuery := scopeWithdrawalSite(r.db.WithContext(ctx), "l.distribution_site_id", siteID) if err := pickupQuery. Table("admin_pickups AS p"). Select(`p.id, p.pickup_no, l.listing_no, a.title, p.status, p.settlement_mode, p.offline_settlement_status, p.settle_amount_cent, p.completed_at, p.created_at`). Joins("JOIN rental_listings AS l ON l.id = p.listing_id"). Joins("JOIN game_accounts AS a ON a.id = p.account_id"). Where("p.owner_id = ? AND p.status = ?", userID, "completed"). Order("p.id DESC"). Offset(offset).Limit(query.PageSize). Scan(&rows).Error; err != nil { return nil, err } if len(rows) == 0 { return []WithdrawalReviewPickupDTO{}, nil } pickupIDs := make([]uint64, 0, len(rows)) for _, row := range rows { pickupIDs = append(pickupIDs, row.ID) } var adjustmentRows []struct { PickupID uint64 AmountCent int64 } if err := r.db.WithContext(ctx). Table("admin_pickup_financial_adjustments"). Select("pickup_id, COALESCE(SUM(settle_delta_cent), 0) AS amount_cent"). Where("pickup_id IN ?", pickupIDs). Group("pickup_id"). Scan(&adjustmentRows).Error; err != nil { return nil, err } adjustments := make(map[uint64]int64, len(adjustmentRows)) for _, row := range adjustmentRows { adjustments[row.PickupID] = row.AmountCent } items := make([]WithdrawalReviewPickupDTO, 0, len(rows)) for _, row := range rows { adjustment := adjustments[row.ID] items = append(items, WithdrawalReviewPickupDTO{ ID: row.ID, PickupNo: row.PickupNo, ListingNo: row.ListingNo, Title: row.Title, Status: row.Status, SettlementMode: row.SettlementMode, OfflineSettlementStatus: row.OfflineSettlementStatus, SettleAmountCent: row.SettleAmountCent, SettleAdjustmentCent: adjustment, EffectiveSettleAmountCent: row.SettleAmountCent + adjustment, CompletedAt: row.CompletedAt, CreatedAt: row.CreatedAt, }) } return items, nil } func (r *Repository) withdrawalReviewHistory(ctx context.Context, userID, siteID uint64, query WithdrawalReviewContextQuery) ([]WithdrawalReviewHistoryDTO, error) { var rows []model.WithdrawalRequest offset := (query.WithdrawalPage - 1) * query.PageSize historyQuery := scopeWithdrawalSite(r.db.WithContext(ctx), "distribution_site_id", siteID) if err := historyQuery. Where("user_id = ?", userID). Order("created_at DESC, id DESC"). Offset(offset).Limit(query.PageSize). Find(&rows).Error; err != nil { return nil, err } items := make([]WithdrawalReviewHistoryDTO, 0, len(rows)) for _, row := range rows { items = append(items, WithdrawalReviewHistoryDTO{ ID: row.ID, WithdrawNo: row.WithdrawNo, AmountCent: row.AmountCent, FeeCent: row.FeeCent, ActualAmountCent: row.ActualAmountCent, Status: row.Status, CreatedAt: row.CreatedAt, ReviewedAt: row.ReviewedAt, PaidAt: row.PaidAt, }) } return items, nil }