package payment import ( "strings" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) type rechargeBillDailyBucketDTO struct { Date string `json:"date"` GoogleUsdMinor int64 `json:"googleUsdMinor"` ThirdPartyUsdMinor int64 `json:"thirdPartyUsdMinor"` CoinSellerUsdMinor int64 `json:"coinSellerUsdMinor"` GoogleCoinAmount int64 `json:"googleCoinAmount"` ThirdPartyCoinAmount int64 `json:"thirdPartyCoinAmount"` CoinSellerCoinAmount int64 `json:"coinSellerCoinAmount"` } type rechargeBillRegionBucketDTO struct { RegionID int64 `json:"regionId"` Name string `json:"name"` BillCount int64 `json:"billCount"` UsdMinorAmount int64 `json:"usdMinorAmount"` } type rechargeBillGooglePaidStatsDTO struct { GoogleBillCount int64 `json:"googleBillCount"` SyncedCount int64 `json:"syncedCount"` UnsyncedCount int64 `json:"unsyncedCount"` CoveredUsdMinor int64 `json:"coveredUsdMinor"` EstFeeUsdMinor int64 `json:"estFeeUsdMinor"` EstTaxUsdMinor int64 `json:"estTaxUsdMinor"` EstNetUsdMinor int64 `json:"estNetUsdMinor"` } // rechargeBillWithdrawalStatsDTO 是“用户提现”合并口径:工资转币商(钱包账本)+ 审核通过的提现申请(admin 库)。 type rechargeBillWithdrawalStatsDTO struct { TransferCount int64 `json:"transferCount"` TransferUsdMinor int64 `json:"transferUsdMinor"` ApprovedCount int64 `json:"approvedCount"` ApprovedUsdMinor int64 `json:"approvedUsdMinor"` TotalUsdMinor int64 `json:"totalUsdMinor"` } type rechargeBillOverviewDTO struct { Daily []rechargeBillDailyBucketDTO `json:"daily"` Regions []rechargeBillRegionBucketDTO `json:"regions"` GooglePaid rechargeBillGooglePaidStatsDTO `json:"googlePaid"` Withdrawal rechargeBillWithdrawalStatsDTO `json:"withdrawal"` } // GetRechargeBillOverview 返回财务概览聚合:按日趋势、区域分布与谷歌实付覆盖度;筛选口径与账单列表一致。 func (h *Handler) GetRechargeBillOverview(c *gin.Context) { options := shared.ListOptions(c) appCode := appctx.FromContext(c.Request.Context()) tzOffsetMinutes := int32(queryInt64(c, "tz_offset_minutes", "tzOffsetMinutes")) if tzOffsetMinutes == 0 { // 财务系统统一按中国时区切日边界。 tzOffsetMinutes = 480 } startAtMS := queryInt64(c, "start_at_ms", "startAtMs") endAtMS := queryInt64(c, "end_at_ms", "endAtMs") if source, ok := h.billSources[appCode]; ok { query := legacyRechargeBillQuery{ Keyword: options.Keyword, RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")), PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")), RegionID: queryInt64(c, "region_id", "regionId"), StartAtMS: startAtMS, EndAtMS: endAtMS, } if !h.requireLegacyCoinSellerDayRange(c, query, tzOffsetMinutes) { return } overview, err := source.Overview(c.Request.Context(), query, tzOffsetMinutes) if err != nil { response.ServerError(c, "获取财务概览失败") return } h.fillApprovedWithdrawalStats(&overview, appCode, startAtMS, endAtMS) response.OK(c, overview) return } resp, err := h.wallet.GetRechargeBillOverview(c.Request.Context(), &walletv1.GetRechargeBillOverviewRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appCode, RegionId: queryInt64(c, "region_id", "regionId"), RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")), Status: options.Status, StartAtMs: startAtMS, EndAtMs: endAtMS, TzOffsetMinutes: tzOffsetMinutes, }) if err != nil { writeWalletError(c, err, "获取财务概览失败") return } overview := rechargeBillOverviewDTO{ Daily: make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily())), Regions: make([]rechargeBillRegionBucketDTO, 0, len(resp.GetRegions())), } for _, bucket := range resp.GetDaily() { overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{ Date: bucket.GetDate(), GoogleUsdMinor: bucket.GetGoogleUsdMinor(), ThirdPartyUsdMinor: bucket.GetThirdPartyUsdMinor(), CoinSellerUsdMinor: bucket.GetCoinSellerUsdMinor(), GoogleCoinAmount: bucket.GetGoogleCoinAmount(), ThirdPartyCoinAmount: bucket.GetThirdPartyCoinAmount(), CoinSellerCoinAmount: bucket.GetCoinSellerCoinAmount(), }) } // 区域名从后台区域目录补齐;目录缺失时回退区域 ID 文本,避免概览页出现空白区块。 regionNames := map[int64]string{} if regions, err := h.listRechargeBillRegions(c.Request.Context(), appCode); err == nil { for _, region := range regions { name := region.Name if name == "" { name = region.RegionCode } regionNames[region.RegionID] = name } } for _, bucket := range resp.GetRegions() { overview.Regions = append(overview.Regions, rechargeBillRegionBucketDTO{ RegionID: bucket.GetRegionId(), Name: regionNames[bucket.GetRegionId()], BillCount: bucket.GetBillCount(), UsdMinorAmount: bucket.GetUsdMinorAmount(), }) } if stats := resp.GetGooglePaid(); stats != nil { overview.GooglePaid = rechargeBillGooglePaidStatsDTO{ GoogleBillCount: stats.GetGoogleBillCount(), SyncedCount: stats.GetSyncedCount(), UnsyncedCount: stats.GetUnsyncedCount(), CoveredUsdMinor: stats.GetCoveredUsdMinor(), EstFeeUsdMinor: stats.GetEstFeeUsdMinor(), EstTaxUsdMinor: stats.GetEstTaxUsdMinor(), EstNetUsdMinor: stats.GetEstNetUsdMinor(), } } if transfer := resp.GetSalaryTransfer(); transfer != nil { overview.Withdrawal.TransferCount = transfer.GetTransferCount() overview.Withdrawal.TransferUsdMinor = transfer.GetTransferUsdMinor() } h.fillApprovedWithdrawalStats(&overview, appCode, startAtMS, endAtMS) response.OK(c, overview) } func (h *Handler) requireLegacyCoinSellerDayRange(c *gin.Context, query legacyRechargeBillQuery, tzOffsetMinutes int32) bool { if !legacyCoinSellerAggregateRequested(query) || !legacyCoinSellerAggregateFilterSupported(query) { return true } if _, _, _, err := legacyDashboardDayRange(query, tzOffsetMinutes); err != nil { response.BadRequest(c, "币商统计仅支持明确整日时间范围") return false } return true } // fillApprovedWithdrawalStats 把 admin 库的“审核通过提现申请”并入用户提现口径;查询失败只降级该卡片,不阻断概览。 func (h *Handler) fillApprovedWithdrawalStats(overview *rechargeBillOverviewDTO, appCode string, startAtMS int64, endAtMS int64) { if h.store != nil { if stats, err := h.store.ApprovedWithdrawalStats(appCode, startAtMS, endAtMS); err == nil { overview.Withdrawal.ApprovedCount = stats.ApprovedCount overview.Withdrawal.ApprovedUsdMinor = stats.ApprovedUSDMinor } } overview.Withdrawal.TotalUsdMinor = overview.Withdrawal.TransferUsdMinor + overview.Withdrawal.ApprovedUsdMinor }