369 lines
14 KiB
Go
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(¤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
|
|
}
|