hyapp-server/server/admin/internal/modules/payment/recharge_bill_stats.go
2026-07-03 13:09:01 +08:00

197 lines
6.8 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 (
"context"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/response"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/gin-gonic/gin"
)
type rechargeBillSummaryBucketDTO struct {
BillCount int64 `json:"billCount"`
CoinAmount int64 `json:"coinAmount"`
USDMinorAmount int64 `json:"usdMinorAmount"`
}
type rechargeBillSummaryDTO struct {
Total rechargeBillSummaryBucketDTO `json:"total"`
GooglePlay rechargeBillSummaryBucketDTO `json:"googlePlay"`
ThirdParty rechargeBillSummaryBucketDTO `json:"thirdParty"`
}
type rechargeBillRegionDTO struct {
RegionID int64 `json:"regionId"`
RegionCode string `json:"regionCode"`
Name string `json:"name"`
}
type googleRechargePaidRefreshRequest struct {
TransactionIDs []string `json:"transactionIds"`
}
type googleRechargePaidDTO struct {
TransactionID string `json:"transactionId"`
CurrencyCode string `json:"currencyCode"`
PaidAmountMicro int64 `json:"paidAmountMicro"`
TaxMicro int64 `json:"taxMicro"`
NetMicro int64 `json:"netMicro"`
SyncedAtMS int64 `json:"syncedAtMs"`
Error string `json:"error,omitempty"`
}
// GetRechargeBillSummary 返回与账单列表同一筛选口径的充值总和、Google 充值与三方充值聚合。
func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
options := shared.ListOptions(c)
appCode := appctx.FromContext(c.Request.Context())
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, rechargeBillSummaryDTO{})
return
}
resp, err := h.wallet.GetRechargeBillSummary(c.Request.Context(), &walletv1.GetRechargeBillSummaryRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appCode,
UserId: userID,
SellerUserId: sellerUserID,
RegionId: queryInt64(c, "region_id", "regionId"),
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
Status: options.Status,
Keyword: options.Keyword,
StartAtMs: queryInt64(c, "start_at_ms", "startAtMs"),
EndAtMs: queryInt64(c, "end_at_ms", "endAtMs"),
})
if err != nil {
writeWalletError(c, err, "获取充值汇总失败")
return
}
response.OK(c, rechargeBillSummaryDTO{
Total: rechargeBillSummaryBucketFromProto(resp.GetTotal()),
GooglePlay: rechargeBillSummaryBucketFromProto(resp.GetGooglePlay()),
ThirdParty: rechargeBillSummaryBucketFromProto(resp.GetThirdParty()),
})
}
func rechargeBillSummaryBucketFromProto(bucket *walletv1.RechargeBillSummaryBucket) rechargeBillSummaryBucketDTO {
if bucket == nil {
return rechargeBillSummaryBucketDTO{}
}
return rechargeBillSummaryBucketDTO{
BillCount: bucket.GetBillCount(),
CoinAmount: bucket.GetCoinAmount(),
USDMinorAmount: bucket.GetUsdMinorAmount(),
}
}
// RefreshGoogleRechargePaidDetails 对指定账单触发 Google Orders API 实付明细同步,供财务补齐历史订单实付。
func (h *Handler) RefreshGoogleRechargePaidDetails(c *gin.Context) {
var request googleRechargePaidRefreshRequest
if err := c.ShouldBindJSON(&request); err != nil || len(request.TransactionIDs) == 0 {
response.BadRequest(c, "交易号参数不正确")
return
}
if len(request.TransactionIDs) > 100 {
response.BadRequest(c, "单次最多刷新 100 笔账单")
return
}
appCode := appctx.FromContext(c.Request.Context())
resp, err := h.wallet.RefreshGooglePaymentPrices(c.Request.Context(), &walletv1.RefreshGooglePaymentPricesRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appCode,
TransactionIds: request.TransactionIDs,
})
if err != nil {
writeWalletError(c, err, "查询谷歌实付明细失败")
return
}
items := make([]googleRechargePaidDTO, 0, len(resp.GetPrices()))
for _, price := range resp.GetPrices() {
items = append(items, googleRechargePaidDTO{
TransactionID: price.GetTransactionId(),
CurrencyCode: price.GetCurrencyCode(),
PaidAmountMicro: price.GetPaidAmountMicro(),
TaxMicro: price.GetTaxMicro(),
NetMicro: price.GetNetMicro(),
SyncedAtMS: price.GetSyncedAtMs(),
Error: price.GetError(),
})
}
shared.OperationLog(c, h.audit, "refresh-google-recharge-paid", "payment_orders", "success", appCode)
response.OK(c, gin.H{"items": items})
}
// ListRechargeBillRegions 返回当前 App 的区域目录,用于充值详情按区域筛选;不受财务范围授权限制。
func (h *Handler) ListRechargeBillRegions(c *gin.Context) {
appCode := appctx.FromContext(c.Request.Context())
regions, err := h.listRechargeBillRegions(c.Request.Context(), appCode)
if err != nil {
response.ServerError(c, "获取区域列表失败")
return
}
response.OK(c, gin.H{"items": regions, "total": len(regions)})
}
func (h *Handler) listRechargeBillRegions(ctx context.Context, appCode string) ([]rechargeBillRegionDTO, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
items := []rechargeBillRegionDTO{}
seen := map[int64]struct{}{}
if h.userDB != nil {
rows, err := h.userDB.QueryContext(ctx, `
SELECT region_id, region_code, name
FROM regions
WHERE app_code = ? AND status = 'active'
ORDER BY sort_order ASC, name ASC`, appCode)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item rechargeBillRegionDTO
if err := rows.Scan(&item.RegionID, &item.RegionCode, &item.Name); err != nil {
return nil, err
}
seen[item.RegionID] = struct{}{}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
}
// Yumi/Aslan 这类 legacy App 的区域目录在线上 likei Mongo这里只取目录展示账单筛选仍以 wallet 的 target_region_id 为准。
for _, source := range h.moneyRegionSources {
_, sourceRegions, _, err := source.ListMoneyMasterData(ctx, repository.MoneyAccess{All: true})
if err != nil {
return nil, err
}
for _, region := range sourceRegions {
if appctx.Normalize(region.AppCode) != appCode {
continue
}
if _, ok := seen[region.RegionID]; ok {
continue
}
seen[region.RegionID] = struct{}{}
items = append(items, rechargeBillRegionDTO{RegionID: region.RegionID, RegionCode: region.RegionCode, Name: region.Name})
}
}
return items, nil
}