hyapp-server/server/admin/internal/modules/payment/recharge_bill_overview.go
2026-07-06 11:10:19 +08:00

180 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}