Files
hfb_sys/backend/internal/modules/withdrawal/review_context.go
T

369 lines
14 KiB
Go

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(&current, 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
}