hyapp-server/server/admin/internal/modules/payment/recharge_bill_overview.go
2026-07-09 19:57:39 +08:00

219 lines
8.9 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")
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
}