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") userFilter := shared.UserIdentityFilterFromQuery(c, "") userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId") if !ok { return } sellerFilter := shared.UserIdentityFilterFromQuery(c, "seller") sellerUserID, sellerMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, sellerFilter, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId") if !ok { return } if (!userFilter.IsEmpty() && !userMatched) || (!sellerFilter.IsEmpty() && !sellerMatched) { response.OK(c, rechargeBillOverviewDTO{Daily: []rechargeBillDailyBucketDTO{}, Regions: []rechargeBillRegionBucketDTO{}}) return } 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, } overview := rechargeBillOverviewDTO{Daily: []rechargeBillDailyBucketDTO{}, Regions: []rechargeBillRegionBucketDTO{}} if userFilter.IsEmpty() && sellerFilter.IsEmpty() { var err error overview, err = source.Overview(c.Request.Context(), query, tzOffsetMinutes) if err != nil { response.ServerError(c, "获取财务概览失败") return } } if err := h.applyFinanceCoinSellerOverview(c.Request.Context(), appCode, financeCoinSellerRechargeQuery{ RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")), Status: options.Status, PaidState: query.PaidState, Keyword: options.Keyword, RegionID: query.RegionID, StartAtMS: startAtMS, EndAtMS: endAtMS, TzOffsetMinutes: tzOffsetMinutes, TargetUserID: sellerUserID, HasUserOnlyFilter: userID > 0 && sellerUserID == 0, }, &overview); err != nil { response.ServerError(c, "获取财务概览失败") return } h.fillApprovedWithdrawalStats(&overview, appCode, startAtMS, endAtMS) response.OK(c, overview) return } overview := rechargeBillOverviewDTO{ Daily: []rechargeBillDailyBucketDTO{}, Regions: []rechargeBillRegionBucketDTO{}, } financeQuery := financeCoinSellerRechargeQuery{ RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")), Status: options.Status, PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")), Keyword: options.Keyword, RegionID: queryInt64(c, "region_id", "regionId"), StartAtMS: startAtMS, EndAtMS: endAtMS, TzOffsetMinutes: tzOffsetMinutes, TargetUserID: sellerUserID, HasUserOnlyFilter: userID > 0 && sellerUserID == 0, RequestID: middleware.CurrentRequestID(c), UserID: userID, } if financeWalletOverviewSupportsOrdinaryFilters(financeQuery) { resp, err := h.wallet.GetRechargeBillOverview(c.Request.Context(), &walletv1.GetRechargeBillOverviewRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appCode, RegionId: queryInt64(c, "region_id", "regionId"), RechargeType: financeQuery.RechargeType, Status: options.Status, StartAtMs: startAtMS, EndAtMs: endAtMS, TzOffsetMinutes: tzOffsetMinutes, }) if err != nil { writeWalletError(c, err, "获取财务概览失败") return } overview.Daily = make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily())) 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(), }) } 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() } } if normalizedRechargeType(financeQuery.RechargeType) == "" { ordinaryRegions, err := h.walletOrdinaryRechargeOverviewRegions(c.Request.Context(), appCode, financeQuery) if err != nil { writeWalletError(c, err, "获取财务概览失败") return } overview.Regions = h.fillFinanceCoinSellerRegionNames(c.Request.Context(), appCode, ordinaryRegions) } if err := h.applyFinanceCoinSellerOverview(c.Request.Context(), appCode, financeQuery, &overview); err != nil { response.ServerError(c, "获取财务概览失败") return } 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 }