180 lines
7.0 KiB
Go
180 lines
7.0 KiB
Go
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
|
||
}
|