财务口径
This commit is contained in:
parent
ccc2a8b108
commit
07302f0aff
@ -0,0 +1,681 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hyapp-admin-server/internal/model"
|
||||
"hyapp-admin-server/internal/repository"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
)
|
||||
|
||||
type financeCoinSellerRechargeQuery struct {
|
||||
RechargeType string
|
||||
Status string
|
||||
PaidState string
|
||||
Keyword string
|
||||
RegionID int64
|
||||
StartAtMS int64
|
||||
EndAtMS int64
|
||||
TzOffsetMinutes int32
|
||||
TargetUserID int64
|
||||
HasUserOnlyFilter bool
|
||||
Page int
|
||||
PageSize int
|
||||
MaxPageSize int
|
||||
RequestID string
|
||||
UserID int64
|
||||
}
|
||||
|
||||
type financeCoinSellerRechargeStats struct {
|
||||
Total rechargeBillSummaryBucketDTO
|
||||
Daily map[string]rechargeBillSummaryBucketDTO
|
||||
Regions []rechargeBillRegionBucketDTO
|
||||
}
|
||||
|
||||
const (
|
||||
financeCoinSellerRechargeMaxPageSize = 100
|
||||
financeRechargeMergeMaxPrefetch = 5000
|
||||
)
|
||||
|
||||
func (h *Handler) applyFinanceCoinSellerSummary(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, summary *rechargeBillSummaryDTO) error {
|
||||
if summary == nil {
|
||||
return nil
|
||||
}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
if rechargeType != "" && rechargeType != "coin_seller" {
|
||||
summary.CoinSeller = rechargeBillSummaryBucketDTO{}
|
||||
legacyRecomputeSummaryTotal(summary)
|
||||
return nil
|
||||
}
|
||||
stats, err := h.financeCoinSellerRechargeStats(ctx, appCode, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
summary.CoinSeller = stats.Total
|
||||
if rechargeType == "coin_seller" {
|
||||
summary.GooglePlay = rechargeBillSummaryBucketDTO{}
|
||||
summary.ThirdParty = rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
legacyRecomputeSummaryTotal(summary)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) applyFinanceCoinSellerOverview(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, overview *rechargeBillOverviewDTO) error {
|
||||
if overview == nil {
|
||||
return nil
|
||||
}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
if rechargeType != "" && rechargeType != "coin_seller" {
|
||||
for i := range overview.Daily {
|
||||
overview.Daily[i].CoinSellerUsdMinor = 0
|
||||
overview.Daily[i].CoinSellerCoinAmount = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
stats, err := h.financeCoinSellerRechargeStats(ctx, appCode, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byDate := map[string]int{}
|
||||
for i := range overview.Daily {
|
||||
byDate[overview.Daily[i].Date] = i
|
||||
// 财务工作台的币商充值只认后台币商订单 + H5 币商身份充值;wallet 库存流水和 legacy dashboard 旧聚合先清掉再重填。
|
||||
overview.Daily[i].CoinSellerUsdMinor = 0
|
||||
overview.Daily[i].CoinSellerCoinAmount = 0
|
||||
}
|
||||
for date, bucket := range stats.Daily {
|
||||
if date == "" {
|
||||
continue
|
||||
}
|
||||
index, ok := byDate[date]
|
||||
if !ok {
|
||||
overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{Date: date})
|
||||
index = len(overview.Daily) - 1
|
||||
byDate[date] = index
|
||||
}
|
||||
overview.Daily[index].CoinSellerUsdMinor = bucket.USDMinorAmount
|
||||
overview.Daily[index].CoinSellerCoinAmount = bucket.CoinAmount
|
||||
}
|
||||
sort.Slice(overview.Daily, func(i, j int) bool {
|
||||
return overview.Daily[i].Date < overview.Daily[j].Date
|
||||
})
|
||||
if rechargeType == "coin_seller" {
|
||||
overview.Regions = h.fillFinanceCoinSellerRegionNames(ctx, appCode, stats.Regions)
|
||||
return nil
|
||||
}
|
||||
if rechargeType == "" {
|
||||
overview.Regions = h.fillFinanceCoinSellerRegionNames(ctx, appCode, mergeRechargeRegionBuckets(overview.Regions, stats.Regions))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) financeCoinSellerRechargeStats(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) (financeCoinSellerRechargeStats, error) {
|
||||
stats := financeCoinSellerRechargeStats{Daily: map[string]rechargeBillSummaryBucketDTO{}}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
if rechargeType != "" && rechargeType != "coin_seller" {
|
||||
return stats, nil
|
||||
}
|
||||
if !financeCoinSellerSucceededStatus(query.Status) || strings.TrimSpace(query.PaidState) != "" || query.HasUserOnlyFilter {
|
||||
return stats, nil
|
||||
}
|
||||
if h != nil && h.store != nil {
|
||||
repoStats, err := h.store.CoinSellerRechargeOrderStats(repository.CoinSellerRechargeOrderStatsOptions{
|
||||
AppCode: appCode,
|
||||
RegionID: query.RegionID,
|
||||
TargetUserID: query.TargetUserID,
|
||||
Keyword: query.Keyword,
|
||||
CreatedFromMS: query.StartAtMS,
|
||||
CreatedToMS: query.EndAtMS,
|
||||
TzOffsetMinutes: query.TzOffsetMinutes,
|
||||
})
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
stats.addBucket(rechargeBillSummaryBucketDTO{
|
||||
BillCount: repoStats.VerifiedCount,
|
||||
CoinAmount: repoStats.VerifiedCoinAmount,
|
||||
USDMinorAmount: repoStats.VerifiedUSDMinor,
|
||||
})
|
||||
for date, bucket := range repoStats.VerifiedDaily {
|
||||
stats.addDaily(date, rechargeBillSummaryBucketDTO{BillCount: bucket.Count, CoinAmount: bucket.CoinAmount, USDMinorAmount: bucket.USDMinor})
|
||||
}
|
||||
for _, row := range repoStats.VerifiedRegionBuckets {
|
||||
stats.addRegion(row.RegionID, row.Count, row.USDMinor)
|
||||
}
|
||||
}
|
||||
if h != nil && h.walletDB != nil {
|
||||
if err := h.addH5CoinSellerRechargeStats(ctx, appCode, query, &stats); err != nil {
|
||||
return stats, err
|
||||
}
|
||||
}
|
||||
sort.Slice(stats.Regions, func(i, j int) bool {
|
||||
return stats.Regions[i].UsdMinorAmount > stats.Regions[j].UsdMinorAmount
|
||||
})
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *financeCoinSellerRechargeStats) addBucket(bucket rechargeBillSummaryBucketDTO) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.Total.BillCount += bucket.BillCount
|
||||
s.Total.CoinAmount += bucket.CoinAmount
|
||||
s.Total.USDMinorAmount += bucket.USDMinorAmount
|
||||
}
|
||||
|
||||
func (s *financeCoinSellerRechargeStats) addDaily(date string, bucket rechargeBillSummaryBucketDTO) {
|
||||
if s == nil || date == "" {
|
||||
return
|
||||
}
|
||||
if s.Daily == nil {
|
||||
s.Daily = map[string]rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
current := s.Daily[date]
|
||||
current.BillCount += bucket.BillCount
|
||||
current.CoinAmount += bucket.CoinAmount
|
||||
current.USDMinorAmount += bucket.USDMinorAmount
|
||||
s.Daily[date] = current
|
||||
}
|
||||
|
||||
func (s *financeCoinSellerRechargeStats) addRegion(regionID int64, billCount int64, usdMinor int64) {
|
||||
if s == nil || regionID <= 0 {
|
||||
return
|
||||
}
|
||||
for i := range s.Regions {
|
||||
if s.Regions[i].RegionID == regionID {
|
||||
s.Regions[i].BillCount += billCount
|
||||
s.Regions[i].UsdMinorAmount += usdMinor
|
||||
return
|
||||
}
|
||||
}
|
||||
s.Regions = append(s.Regions, rechargeBillRegionBucketDTO{RegionID: regionID, BillCount: billCount, UsdMinorAmount: usdMinor})
|
||||
}
|
||||
|
||||
func (h *Handler) addH5CoinSellerRechargeStats(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, stats *financeCoinSellerRechargeStats) error {
|
||||
where, args := h5CoinSellerRechargeWhere(appCode, query)
|
||||
var total rechargeBillSummaryBucketDTO
|
||||
if err := h.walletDB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*), COALESCE(SUM(eo.coin_amount), 0), COALESCE(SUM(eo.usd_minor_amount), 0)
|
||||
FROM external_recharge_orders eo
|
||||
JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
|
||||
`+where, args...).Scan(&total.BillCount, &total.CoinAmount, &total.USDMinorAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
stats.addBucket(total)
|
||||
|
||||
tzOffsetMS := int64(query.TzOffsetMinutes) * 60_000
|
||||
dailyArgs := append([]any{tzOffsetMS}, args...)
|
||||
rows, err := h.walletDB.QueryContext(ctx, `
|
||||
SELECT DATE_FORMAT(FROM_UNIXTIME((eo.updated_at_ms + ?) / 1000), '%Y-%m-%d') AS date,
|
||||
COUNT(*), COALESCE(SUM(eo.coin_amount), 0), COALESCE(SUM(eo.usd_minor_amount), 0)
|
||||
FROM external_recharge_orders eo
|
||||
JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
|
||||
`+where+`
|
||||
GROUP BY date
|
||||
ORDER BY date ASC`, dailyArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var date string
|
||||
var bucket rechargeBillSummaryBucketDTO
|
||||
if err := rows.Scan(&date, &bucket.BillCount, &bucket.CoinAmount, &bucket.USDMinorAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
stats.addDaily(date, bucket)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regionRows, err := h.walletDB.QueryContext(ctx, `
|
||||
SELECT eo.target_region_id, COUNT(*), COALESCE(SUM(eo.usd_minor_amount), 0)
|
||||
FROM external_recharge_orders eo
|
||||
JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
|
||||
`+where+`
|
||||
GROUP BY eo.target_region_id
|
||||
ORDER BY 3 DESC`, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer regionRows.Close()
|
||||
for regionRows.Next() {
|
||||
var regionID, billCount, usdMinor int64
|
||||
if err := regionRows.Scan(®ionID, &billCount, &usdMinor); err != nil {
|
||||
return err
|
||||
}
|
||||
stats.addRegion(regionID, billCount, usdMinor)
|
||||
}
|
||||
return regionRows.Err()
|
||||
}
|
||||
|
||||
func (h *Handler) listFinanceCoinSellerRechargeBills(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) ([]rechargeBillDTO, int64, error) {
|
||||
if !financeCoinSellerSucceededStatus(query.Status) || strings.TrimSpace(query.PaidState) != "" || query.HasUserOnlyFilter {
|
||||
return []rechargeBillDTO{}, 0, nil
|
||||
}
|
||||
page, pageSize := financeCoinSellerPage(query.Page, query.PageSize, query.MaxPageSize)
|
||||
prefetch := financeRechargePrefetch(page, pageSize, query.MaxPageSize)
|
||||
items := make([]rechargeBillDTO, 0, prefetch*2)
|
||||
var total int64
|
||||
if h != nil && h.store != nil {
|
||||
orders, orderTotal, err := h.store.ListCoinSellerRechargeOrders(repository.CoinSellerRechargeOrderListOptions{
|
||||
Page: 1,
|
||||
PageSize: prefetch,
|
||||
PageSizeLimit: prefetch,
|
||||
AppCode: appCode,
|
||||
VerifyStatus: model.CoinSellerRechargeVerifyStatusVerified,
|
||||
Keyword: query.Keyword,
|
||||
TargetUserID: query.TargetUserID,
|
||||
RegionID: query.RegionID,
|
||||
CreatedFromMS: query.StartAtMS,
|
||||
CreatedToMS: query.EndAtMS,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total += orderTotal
|
||||
for _, order := range orders {
|
||||
if query.RegionID > 0 && order.TargetRegionID != query.RegionID {
|
||||
continue
|
||||
}
|
||||
items = append(items, financeCoinSellerRechargeOrderBill(order))
|
||||
}
|
||||
}
|
||||
if h != nil && h.walletDB != nil {
|
||||
h5Items, h5Total, err := h.listH5CoinSellerRechargeBills(ctx, appCode, query, prefetch)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total += h5Total
|
||||
items = append(items, h5Items...)
|
||||
}
|
||||
sortRechargeBills(items)
|
||||
offset := (page - 1) * pageSize
|
||||
if offset >= len(items) {
|
||||
return []rechargeBillDTO{}, total, nil
|
||||
}
|
||||
end := offset + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[offset:end], total, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listFinanceAllRechargeBills(ctx context.Context, appCode string, source RechargeBillSource, query financeCoinSellerRechargeQuery) ([]rechargeBillDTO, int64, error) {
|
||||
page, pageSize := financeCoinSellerPage(query.Page, query.PageSize, query.MaxPageSize)
|
||||
prefetch := financeRechargePrefetch(page, pageSize, query.MaxPageSize)
|
||||
items := make([]rechargeBillDTO, 0, prefetch*3)
|
||||
var total int64
|
||||
|
||||
if source != nil {
|
||||
if !query.HasUserOnlyFilter && query.TargetUserID == 0 {
|
||||
// legacy 普通充值源没有 user/seller 过滤能力;有用户筛选时宁可不展示普通源,避免 all 视图返回未过滤全量。
|
||||
ordinary, ordinaryTotal, err := source.ListRechargeBills(ctx, legacyRechargeBillQuery{
|
||||
Keyword: query.Keyword,
|
||||
RechargeType: "",
|
||||
PaidState: query.PaidState,
|
||||
RegionID: query.RegionID,
|
||||
StartAtMS: query.StartAtMS,
|
||||
EndAtMS: query.EndAtMS,
|
||||
TzOffsetMinutes: query.TzOffsetMinutes,
|
||||
Page: 1,
|
||||
PageSize: prefetch,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, ordinary...)
|
||||
total += ordinaryTotal
|
||||
}
|
||||
} else if h != nil && h.wallet != nil {
|
||||
// wallet 的空 recharge_type 会并入 coin_seller_stock_records;all 视图必须拆成普通三方和 Google 两路,币商改走财务新口径。
|
||||
for _, rechargeType := range []string{"third_party", "google_play_recharge"} {
|
||||
resp, err := h.wallet.ListRechargeBills(ctx, &walletv1.ListRechargeBillsRequest{
|
||||
RequestId: query.RequestID,
|
||||
AppCode: appCode,
|
||||
UserId: query.UserID,
|
||||
SellerUserId: query.TargetUserID,
|
||||
RegionId: query.RegionID,
|
||||
RechargeType: rechargeType,
|
||||
PaidState: query.PaidState,
|
||||
Status: query.Status,
|
||||
Keyword: query.Keyword,
|
||||
StartAtMs: query.StartAtMS,
|
||||
EndAtMs: query.EndAtMS,
|
||||
Page: 1,
|
||||
PageSize: int32(prefetch),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total += resp.GetTotal()
|
||||
for _, item := range resp.GetBills() {
|
||||
items = append(items, rechargeBillFromProto(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coinItems, coinTotal, err := h.listFinanceCoinSellerRechargeBills(ctx, appCode, financeCoinSellerRechargeQuery{
|
||||
RechargeType: "coin_seller",
|
||||
Status: query.Status,
|
||||
PaidState: query.PaidState,
|
||||
Keyword: query.Keyword,
|
||||
RegionID: query.RegionID,
|
||||
StartAtMS: query.StartAtMS,
|
||||
EndAtMS: query.EndAtMS,
|
||||
TzOffsetMinutes: query.TzOffsetMinutes,
|
||||
TargetUserID: query.TargetUserID,
|
||||
HasUserOnlyFilter: query.HasUserOnlyFilter,
|
||||
Page: 1,
|
||||
PageSize: prefetch,
|
||||
MaxPageSize: prefetch,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, coinItems...)
|
||||
total += coinTotal
|
||||
|
||||
sortRechargeBills(items)
|
||||
offset := (page - 1) * pageSize
|
||||
if offset >= len(items) {
|
||||
return []rechargeBillDTO{}, total, nil
|
||||
}
|
||||
end := offset + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[offset:end], total, nil
|
||||
}
|
||||
|
||||
func (h *Handler) walletOrdinaryRechargeOverviewRegions(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) ([]rechargeBillRegionBucketDTO, error) {
|
||||
if h == nil || h.wallet == nil {
|
||||
return []rechargeBillRegionBucketDTO{}, nil
|
||||
}
|
||||
if !financeWalletOverviewSupportsOrdinaryFilters(query) {
|
||||
// wallet overview proto 没有用户、币商、关键词和 paid_state 字段;过滤条件存在时返回空区域,避免展示未过滤的全量区域。
|
||||
return []rechargeBillRegionBucketDTO{}, nil
|
||||
}
|
||||
var out []rechargeBillRegionBucketDTO
|
||||
for _, rechargeType := range []string{"third_party", "google_play_recharge"} {
|
||||
resp, err := h.wallet.GetRechargeBillOverview(ctx, &walletv1.GetRechargeBillOverviewRequest{
|
||||
RequestId: query.RequestID,
|
||||
AppCode: appCode,
|
||||
RegionId: query.RegionID,
|
||||
RechargeType: rechargeType,
|
||||
Status: query.Status,
|
||||
StartAtMs: query.StartAtMS,
|
||||
EndAtMs: query.EndAtMS,
|
||||
TzOffsetMinutes: query.TzOffsetMinutes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, bucket := range resp.GetRegions() {
|
||||
out = append(out, rechargeBillRegionBucketDTO{
|
||||
RegionID: bucket.GetRegionId(),
|
||||
BillCount: bucket.GetBillCount(),
|
||||
UsdMinorAmount: bucket.GetUsdMinorAmount(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return mergeRechargeRegionBuckets(out), nil
|
||||
}
|
||||
|
||||
func financeWalletOverviewSupportsOrdinaryFilters(query financeCoinSellerRechargeQuery) bool {
|
||||
return query.UserID == 0 && query.TargetUserID == 0 && strings.TrimSpace(query.Keyword) == "" && strings.TrimSpace(query.PaidState) == ""
|
||||
}
|
||||
|
||||
func (h *Handler) listH5CoinSellerRechargeBills(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, limit int) ([]rechargeBillDTO, int64, error) {
|
||||
where, args := h5CoinSellerRechargeWhere(appCode, query)
|
||||
var total int64
|
||||
if err := h.walletDB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM external_recharge_orders eo
|
||||
JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
|
||||
`+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit < 1 {
|
||||
return []rechargeBillDTO{}, total, nil
|
||||
}
|
||||
rows, err := h.walletDB.QueryContext(ctx, `
|
||||
SELECT eo.app_code, eo.order_id, wt.command_id, eo.target_user_id, eo.target_region_id,
|
||||
eo.product_id, eo.product_code, eo.product_name, eo.coin_amount, eo.usd_minor_amount,
|
||||
eo.provider_code, eo.currency_code, eo.provider_amount_minor, eo.provider_order_id,
|
||||
eo.wallet_transaction_id, eo.updated_at_ms, eo.created_at_ms
|
||||
FROM external_recharge_orders eo
|
||||
JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
|
||||
`+where+`
|
||||
ORDER BY eo.updated_at_ms DESC, eo.order_id DESC
|
||||
LIMIT ?`, append(args, limit)...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]rechargeBillDTO, 0, limit)
|
||||
for rows.Next() {
|
||||
var item rechargeBillDTO
|
||||
var productID int64
|
||||
var productCode, productName, providerOrderID, walletTransactionID string
|
||||
var createdAtMS int64
|
||||
if err := rows.Scan(
|
||||
&item.AppCode,
|
||||
&item.ExternalRef,
|
||||
&item.CommandID,
|
||||
&item.SellerUserID,
|
||||
&item.SellerRegionID,
|
||||
&productID,
|
||||
&productCode,
|
||||
&productName,
|
||||
&item.CoinAmount,
|
||||
&item.USDMinorAmount,
|
||||
&item.ProviderCode,
|
||||
&item.UserPaidCurrencyCode,
|
||||
&item.ProviderAmountMinor,
|
||||
&providerOrderID,
|
||||
&walletTransactionID,
|
||||
&item.CreatedAtMS,
|
||||
&createdAtMS,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
item.TransactionID = firstNonEmptyPaymentString(walletTransactionID, item.ExternalRef)
|
||||
item.RechargeType = "coin_seller"
|
||||
item.Status = "succeeded"
|
||||
item.TargetRegionID = item.SellerRegionID
|
||||
item.PolicyID = productID
|
||||
item.PolicyVersion = productCode
|
||||
item.CurrencyCode = "USD"
|
||||
item.UserPaidAmountMicro = item.ProviderAmountMinor * 10_000
|
||||
item.PaidSyncedAtMS = item.CreatedAtMS
|
||||
if item.UserPaidCurrencyCode == "" {
|
||||
item.UserPaidCurrencyCode = "USD"
|
||||
item.UserPaidAmountMicro = item.USDMinorAmount * 10_000
|
||||
}
|
||||
if providerOrderID != "" {
|
||||
item.ExternalRef = providerOrderID
|
||||
}
|
||||
if item.CreatedAtMS == 0 {
|
||||
item.CreatedAtMS = createdAtMS
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func financeCoinSellerRechargeOrderBill(order model.CoinSellerRechargeOrder) rechargeBillDTO {
|
||||
currency := firstNonEmptyPaymentString(order.ProviderCurrencyCode, "USD")
|
||||
providerAmountMinor := order.ProviderAmountMinor
|
||||
userPaidAmountMicro := providerAmountMinor * 10_000
|
||||
if providerAmountMinor == 0 {
|
||||
userPaidAmountMicro = order.USDMinorAmount * 10_000
|
||||
}
|
||||
return rechargeBillDTO{
|
||||
AppCode: order.AppCode,
|
||||
TransactionID: firstNonEmptyPaymentString(order.WalletTransactionID, fmt.Sprintf("coin-seller-order:%d", order.ID)),
|
||||
CommandID: order.WalletCommandID,
|
||||
RechargeType: "coin_seller",
|
||||
Status: "succeeded",
|
||||
ExternalRef: firstNonEmptyPaymentString(order.ProviderOrderID, order.ExternalOrderNo),
|
||||
SellerUserID: order.TargetUserID,
|
||||
SellerRegionID: order.TargetRegionID,
|
||||
TargetRegionID: order.TargetRegionID,
|
||||
CurrencyCode: "USD",
|
||||
CoinAmount: order.CoinAmount,
|
||||
USDMinorAmount: order.USDMinorAmount,
|
||||
ProviderAmountMinor: providerAmountMinor,
|
||||
CreatedAtMS: order.CreatedAtMS,
|
||||
ProviderCode: order.ProviderCode,
|
||||
UserPaidCurrencyCode: currency,
|
||||
UserPaidAmountMicro: userPaidAmountMicro,
|
||||
PaidSyncedAtMS: order.CreatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func h5CoinSellerRechargeWhere(appCode string, query financeCoinSellerRechargeQuery) (string, []any) {
|
||||
where := []string{
|
||||
"eo.app_code = ?",
|
||||
"eo.audience_type = 'coin_seller'",
|
||||
"eo.status = 'credited'",
|
||||
"eo.wallet_transaction_id <> ''",
|
||||
"wt.biz_type = 'coin_seller_recharge'",
|
||||
"wt.status = 'succeeded'",
|
||||
}
|
||||
args := []any{appCode}
|
||||
if query.TargetUserID > 0 {
|
||||
where = append(where, "eo.target_user_id = ?")
|
||||
args = append(args, query.TargetUserID)
|
||||
}
|
||||
if query.RegionID > 0 {
|
||||
where = append(where, "eo.target_region_id = ?")
|
||||
args = append(args, query.RegionID)
|
||||
}
|
||||
if query.StartAtMS > 0 {
|
||||
where = append(where, "eo.updated_at_ms >= ?")
|
||||
args = append(args, query.StartAtMS)
|
||||
}
|
||||
if query.EndAtMS > 0 {
|
||||
where = append(where, "eo.updated_at_ms < ?")
|
||||
args = append(args, query.EndAtMS)
|
||||
}
|
||||
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
where = append(where, "(eo.order_id LIKE ? OR eo.provider_order_id LIKE ? OR eo.wallet_transaction_id LIKE ? OR eo.product_code LIKE ? OR eo.product_name LIKE ? OR eo.tx_hash LIKE ? OR CAST(eo.target_user_id AS CHAR) LIKE ?)")
|
||||
args = append(args, like, like, like, like, like, like, like)
|
||||
}
|
||||
return "WHERE " + strings.Join(where, " AND "), args
|
||||
}
|
||||
|
||||
func financeCoinSellerSucceededStatus(status string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "", "all", "succeeded", "success":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func financeCoinSellerPage(page int, pageSize int, maxPageSize int) (int, int) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
if maxPageSize <= 0 {
|
||||
maxPageSize = financeCoinSellerRechargeMaxPageSize
|
||||
}
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func financeRechargePrefetch(page int, pageSize int, maxPageSize int) int {
|
||||
prefetch := page * pageSize
|
||||
limit := financeRechargeMergeMaxPrefetch
|
||||
if maxPageSize > limit {
|
||||
limit = maxPageSize
|
||||
}
|
||||
if prefetch > limit {
|
||||
return limit
|
||||
}
|
||||
return prefetch
|
||||
}
|
||||
|
||||
func sortRechargeBills(items []rechargeBillDTO) {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].CreatedAtMS == items[j].CreatedAtMS {
|
||||
return items[i].TransactionID > items[j].TransactionID
|
||||
}
|
||||
return items[i].CreatedAtMS > items[j].CreatedAtMS
|
||||
})
|
||||
}
|
||||
|
||||
func mergeRechargeRegionBuckets(groups ...[]rechargeBillRegionBucketDTO) []rechargeBillRegionBucketDTO {
|
||||
byRegion := map[int64]*rechargeBillRegionBucketDTO{}
|
||||
for _, group := range groups {
|
||||
for _, row := range group {
|
||||
if row.RegionID <= 0 {
|
||||
continue
|
||||
}
|
||||
bucket, ok := byRegion[row.RegionID]
|
||||
if !ok {
|
||||
copy := row
|
||||
byRegion[row.RegionID] = ©
|
||||
continue
|
||||
}
|
||||
bucket.BillCount += row.BillCount
|
||||
bucket.UsdMinorAmount += row.UsdMinorAmount
|
||||
if bucket.Name == "" {
|
||||
bucket.Name = row.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]rechargeBillRegionBucketDTO, 0, len(byRegion))
|
||||
for _, bucket := range byRegion {
|
||||
out = append(out, *bucket)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].UsdMinorAmount > out[j].UsdMinorAmount
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) fillFinanceCoinSellerRegionNames(ctx context.Context, appCode string, buckets []rechargeBillRegionBucketDTO) []rechargeBillRegionBucketDTO {
|
||||
if len(buckets) == 0 {
|
||||
return buckets
|
||||
}
|
||||
names := map[int64]string{}
|
||||
if h != nil {
|
||||
if regions, err := h.listRechargeBillRegions(ctx, appCode); err == nil {
|
||||
for _, region := range regions {
|
||||
name := firstNonEmptyPaymentString(region.Name, region.RegionCode, strconv.FormatInt(region.RegionID, 10))
|
||||
names[region.RegionID] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range buckets {
|
||||
if name := names[buckets[i].RegionID]; name != "" {
|
||||
buckets[i].Name = name
|
||||
continue
|
||||
}
|
||||
buckets[i].Name = strconv.FormatInt(buckets[i].RegionID, 10)
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
func firstNonEmptyPaymentString(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -77,10 +77,7 @@ func New(wallet walletclient.Client, userDB *sql.DB, walletDB *sql.DB, store *re
|
||||
func (h *Handler) ListRechargeBills(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
appCode := appctx.FromContext(c.Request.Context())
|
||||
if source, ok := h.billSources[appCode]; ok {
|
||||
h.listLegacyRechargeBills(c, source)
|
||||
return
|
||||
}
|
||||
rechargeType := strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType"))
|
||||
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
||||
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
|
||||
if !ok {
|
||||
@ -95,13 +92,58 @@ func (h *Handler) ListRechargeBills(c *gin.Context) {
|
||||
response.OK(c, response.Page{Items: []rechargeBillDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||||
return
|
||||
}
|
||||
source, hasSource := h.billSources[appCode]
|
||||
normalizedType := normalizedRechargeType(rechargeType)
|
||||
if normalizedType == "" || normalizedType == "coin_seller" {
|
||||
query := financeCoinSellerRechargeQuery{
|
||||
RechargeType: rechargeType,
|
||||
Status: options.Status,
|
||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||
Keyword: options.Keyword,
|
||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||
EndAtMS: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||
TargetUserID: sellerUserID,
|
||||
HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
RequestID: middleware.CurrentRequestID(c),
|
||||
UserID: userID,
|
||||
}
|
||||
var items []rechargeBillDTO
|
||||
var total int64
|
||||
var err error
|
||||
if normalizedType == "coin_seller" {
|
||||
items, total, err = h.listFinanceCoinSellerRechargeBills(c.Request.Context(), appCode, query)
|
||||
} else {
|
||||
var legacySource RechargeBillSource
|
||||
if hasSource {
|
||||
legacySource = source
|
||||
}
|
||||
items, total, err = h.listFinanceAllRechargeBills(c.Request.Context(), appCode, legacySource, query)
|
||||
}
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取账单列表失败")
|
||||
return
|
||||
}
|
||||
if err := h.fillRechargeBillUsers(c.Request.Context(), appCode, items); err != nil {
|
||||
response.ServerError(c, "获取账单用户资料失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
|
||||
return
|
||||
}
|
||||
if hasSource {
|
||||
h.listLegacyRechargeBills(c, source)
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appCode,
|
||||
UserId: userID,
|
||||
SellerUserId: sellerUserID,
|
||||
RegionId: queryInt64(c, "region_id", "regionId"),
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
RechargeType: rechargeType,
|
||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||
Status: options.Status,
|
||||
Keyword: options.Keyword,
|
||||
|
||||
@ -87,6 +87,34 @@ func TestListRechargeBillsReturnsProviderAmountMinor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRechargeBillSummaryKeepsOrdinaryThirdParty(t *testing.T) {
|
||||
wallet := &mockPaymentWallet{rechargeSummaryResp: &walletv1.GetRechargeBillSummaryResponse{
|
||||
Total: &walletv1.RechargeBillSummaryBucket{BillCount: 31, CoinAmount: 310000, UsdMinorAmount: 12345},
|
||||
ThirdParty: &walletv1.RechargeBillSummaryBucket{BillCount: 31, CoinAmount: 310000, UsdMinorAmount: 12345},
|
||||
}}
|
||||
router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
|
||||
request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills/summary?recharge_type=third_party&status=succeeded", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response adminPaymentTestResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
thirdParty := response.Data["thirdParty"].(map[string]any)
|
||||
total := response.Data["total"].(map[string]any)
|
||||
if thirdParty["billCount"] != float64(31) || thirdParty["usdMinorAmount"] != float64(12345) {
|
||||
t.Fatalf("ordinary third party summary should be preserved: %+v", response.Data)
|
||||
}
|
||||
if total["billCount"] != float64(31) || total["usdMinorAmount"] != float64(12345) {
|
||||
t.Fatalf("total should be recomputed from ordinary third party bucket: %+v", response.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) {
|
||||
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
|
||||
Channels: []*walletv1.ThirdPartyPaymentChannel{{
|
||||
@ -609,6 +637,7 @@ func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine {
|
||||
c.Next()
|
||||
})
|
||||
router.GET("/admin/payment/recharge-bills", handler.ListRechargeBills)
|
||||
router.GET("/admin/payment/recharge-bills/summary", handler.GetRechargeBillSummary)
|
||||
router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels)
|
||||
router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks)
|
||||
router.POST("/admin/payment/temporary-links", handler.CreateTemporaryPaymentLink)
|
||||
@ -755,6 +784,7 @@ type mockPaymentWallet struct {
|
||||
|
||||
lastRechargeBills *walletv1.ListRechargeBillsRequest
|
||||
rechargeBillsResp *walletv1.ListRechargeBillsResponse
|
||||
rechargeSummaryResp *walletv1.GetRechargeBillSummaryResponse
|
||||
lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest
|
||||
thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
|
||||
lastTemporaryOrders *walletv1.ListTemporaryRechargeOrdersRequest
|
||||
@ -779,6 +809,13 @@ func (m *mockPaymentWallet) ListRechargeBills(_ context.Context, req *walletv1.L
|
||||
return &walletv1.ListRechargeBillsResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentWallet) GetRechargeBillSummary(_ context.Context, req *walletv1.GetRechargeBillSummaryRequest) (*walletv1.GetRechargeBillSummaryResponse, error) {
|
||||
if m.rechargeSummaryResp != nil {
|
||||
return m.rechargeSummaryResp, nil
|
||||
}
|
||||
return &walletv1.GetRechargeBillSummaryResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
|
||||
m.lastThirdPartyChannels = req
|
||||
if m.thirdPartyChannelsResp != nil {
|
||||
|
||||
@ -248,7 +248,8 @@ func (s *MongoRechargeBillSource) ListRechargeBills(ctx context.Context, query l
|
||||
return nil, 0, fmt.Errorf("legacy bill source is not configured")
|
||||
}
|
||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||
return s.listCoinSellerRechargeBills(ctx, query)
|
||||
// 财务工作台币商充值已切到后台币商订单 + H5 币商身份订单,legacy dashboard 进货聚合不再作为账单来源。
|
||||
return []rechargeBillDTO{}, 0, nil
|
||||
}
|
||||
if s.collection == nil {
|
||||
return nil, 0, fmt.Errorf("legacy bill source is not configured")
|
||||
@ -565,12 +566,7 @@ func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, qu
|
||||
return rechargeBillSummaryDTO{}, fmt.Errorf("legacy bill source is not configured")
|
||||
}
|
||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||
summary := rechargeBillSummaryDTO{}
|
||||
if err := s.mergeCoinSellerSummary(ctx, query, legacySummaryTZOffset(query), &summary); err != nil {
|
||||
return rechargeBillSummaryDTO{}, err
|
||||
}
|
||||
summary.Total = summary.CoinSeller
|
||||
return summary, nil
|
||||
return rechargeBillSummaryDTO{}, nil
|
||||
}
|
||||
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
|
||||
if err != nil {
|
||||
@ -635,11 +631,6 @@ func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, qu
|
||||
if err := cursor.Err(); err != nil {
|
||||
return rechargeBillSummaryDTO{}, fmt.Errorf("iterate legacy recharge summary for %s: %w", s.config.AppCode, err)
|
||||
}
|
||||
if legacyQueryRechargeType(query) == "" {
|
||||
if err := s.mergeCoinSellerSummary(ctx, query, legacySummaryTZOffset(query), &summary); err != nil {
|
||||
return rechargeBillSummaryDTO{}, err
|
||||
}
|
||||
}
|
||||
legacyRecomputeSummaryTotal(&summary)
|
||||
return summary, nil
|
||||
}
|
||||
@ -907,9 +898,6 @@ func (s *MongoRechargeBillSource) Overview(ctx context.Context, query legacyRech
|
||||
return overview, err
|
||||
}
|
||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||
if err := s.mergeCoinSellerOverview(ctx, query, tzOffsetMinutes, &overview); err != nil {
|
||||
return overview, err
|
||||
}
|
||||
return overview, nil
|
||||
}
|
||||
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
|
||||
@ -930,11 +918,6 @@ func (s *MongoRechargeBillSource) Overview(ctx context.Context, query legacyRech
|
||||
if err := s.overviewGooglePaid(ctx, filter, query, &overview); err != nil {
|
||||
return overview, err
|
||||
}
|
||||
if legacyQueryRechargeType(query) == "" {
|
||||
if err := s.mergeCoinSellerOverview(ctx, query, tzOffsetMinutes, &overview); err != nil {
|
||||
return overview, err
|
||||
}
|
||||
}
|
||||
return overview, nil
|
||||
}
|
||||
|
||||
|
||||
@ -234,7 +234,7 @@ func (s *fakeDashboardRechargeSource) ListCoinSellerRechargeBills(_ context.Cont
|
||||
return s.bills, s.billTotal, nil
|
||||
}
|
||||
|
||||
func TestLegacyCoinSellerListUsesDashboardBillSource(t *testing.T) {
|
||||
func TestLegacyCoinSellerListDoesNotUseDashboardBillSource(t *testing.T) {
|
||||
dashboardSource := &fakeDashboardRechargeSource{
|
||||
appCode: "aslan",
|
||||
billTotal: 2,
|
||||
@ -283,37 +283,11 @@ func TestLegacyCoinSellerListUsesDashboardBillSource(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ListRechargeBills: %v", err)
|
||||
}
|
||||
if total != 2 || len(items) != 2 {
|
||||
t.Fatalf("unexpected result total=%d items=%+v", total, items)
|
||||
if total != 0 || len(items) != 0 {
|
||||
t.Fatalf("legacy dashboard coin seller bills should be disabled: total=%d items=%+v", total, items)
|
||||
}
|
||||
item := items[0]
|
||||
if item.RechargeType != "coin_seller_stock_purchase" || item.ProviderCode != "coin_seller" {
|
||||
t.Fatalf("unexpected coin seller mapping: %+v", item)
|
||||
}
|
||||
if item.TransactionID != "aslan-freight-purchase-99" || item.SellerUserID != 2069828597070528514 || item.USDMinorAmount != 132790 {
|
||||
t.Fatalf("unexpected bill fields: %+v", item)
|
||||
}
|
||||
if item.SellerRegionID != 301 || item.TargetRegionID != 301 {
|
||||
t.Fatalf("region mapping failed: %+v", item)
|
||||
}
|
||||
if item.UserPaidCurrencyCode != "USD" || item.UserPaidAmountMicro != 1_327_900_000 {
|
||||
t.Fatalf("paid facts mismatch: %+v", item)
|
||||
}
|
||||
deduction := items[1]
|
||||
if deduction.RechargeType != "coin_seller_stock_deduction" || deduction.ProviderCode != "coin_seller" {
|
||||
t.Fatalf("unexpected deduction mapping: %+v", deduction)
|
||||
}
|
||||
if deduction.TransactionID != "aslan-freight-deduction-100" || deduction.SellerUserID != 2069828597070528515 || deduction.USDMinorAmount != -100000 {
|
||||
t.Fatalf("unexpected deduction fields: %+v", deduction)
|
||||
}
|
||||
if deduction.SellerRegionID != 302 || deduction.TargetRegionID != 302 {
|
||||
t.Fatalf("deduction region mapping failed: %+v", deduction)
|
||||
}
|
||||
if deduction.UserPaidCurrencyCode != "USD" || deduction.UserPaidAmountMicro != -1_000_000_000 {
|
||||
t.Fatalf("deduction paid facts mismatch: %+v", deduction)
|
||||
}
|
||||
if dashboardSource.billCalls != 1 || dashboardSource.billQuery.PageSize != 20 || dashboardSource.billQuery.StatTZ != "Asia/Shanghai" {
|
||||
t.Fatalf("dashboard bill source not called as expected: calls=%d query=%+v", dashboardSource.billCalls, dashboardSource.billQuery)
|
||||
if dashboardSource.billCalls != 0 {
|
||||
t.Fatalf("dashboard bill source should not be called: calls=%d query=%+v", dashboardSource.billCalls, dashboardSource.billQuery)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -84,12 +84,61 @@ func (h *Handler) collectRechargeBillsForExport(c *gin.Context, appCode string)
|
||||
// legacy 币商充值导出必须和页面列表使用同一个自然日解释方式。
|
||||
tzOffsetMinutes = defaultLegacyFinanceTZOffsetMinutes
|
||||
}
|
||||
rechargeType := strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType"))
|
||||
normalizedType := normalizedRechargeType(rechargeType)
|
||||
if normalizedType == "" || normalizedType == "coin_seller" {
|
||||
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
||||
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
sellerFilter := shared.UserIdentityFilterFromQuery(c, "seller")
|
||||
sellerUserID, sellerMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, sellerFilter, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId")
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if (!userFilter.IsEmpty() && !userMatched) || (!sellerFilter.IsEmpty() && !sellerMatched) {
|
||||
return []rechargeBillDTO{}, true
|
||||
}
|
||||
query := financeCoinSellerRechargeQuery{
|
||||
RechargeType: rechargeType,
|
||||
Status: options.Status,
|
||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||
Keyword: options.Keyword,
|
||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||
EndAtMS: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||
TargetUserID: sellerUserID,
|
||||
HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
|
||||
Page: 1,
|
||||
PageSize: rechargeBillExportMaxRows,
|
||||
MaxPageSize: rechargeBillExportMaxRows,
|
||||
RequestID: middleware.CurrentRequestID(c),
|
||||
UserID: userID,
|
||||
}
|
||||
var items []rechargeBillDTO
|
||||
var err error
|
||||
if normalizedType == "coin_seller" {
|
||||
items, _, err = h.listFinanceCoinSellerRechargeBills(c.Request.Context(), appCode, query)
|
||||
} else {
|
||||
items, _, err = h.listFinanceAllRechargeBills(c.Request.Context(), appCode, h.billSources[appCode], query)
|
||||
}
|
||||
if err != nil {
|
||||
response.ServerError(c, "导出账单失败")
|
||||
return nil, false
|
||||
}
|
||||
if err := h.fillRechargeBillUsers(c.Request.Context(), appCode, items); err != nil {
|
||||
response.ServerError(c, "导出账单失败")
|
||||
return nil, false
|
||||
}
|
||||
return capRechargeBillExportRows(items), true
|
||||
}
|
||||
if source, ok := h.billSources[appCode]; ok {
|
||||
bills := make([]rechargeBillDTO, 0, rechargeBillExportPageSize)
|
||||
for page := 1; len(bills) < rechargeBillExportMaxRows; page++ {
|
||||
items, total, err := source.ListRechargeBills(c.Request.Context(), legacyRechargeBillQuery{
|
||||
Keyword: options.Keyword,
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
RechargeType: rechargeType,
|
||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||
@ -230,6 +279,8 @@ func rechargeSourceLabel(rechargeType string) string {
|
||||
switch rechargeType {
|
||||
case "google_play_recharge":
|
||||
return "谷歌充值"
|
||||
case "coin_seller":
|
||||
return "币商充值"
|
||||
case "coin_seller_stock_purchase":
|
||||
return "币商进货"
|
||||
case "coin_seller_stock_deduction":
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
|
||||
@ -69,6 +66,20 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
}
|
||||
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,
|
||||
@ -78,22 +89,26 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
StartAtMS: startAtMS,
|
||||
EndAtMS: endAtMS,
|
||||
}
|
||||
if !h.requireLegacyCoinSellerDayRange(c, query, tzOffsetMinutes) {
|
||||
return
|
||||
}
|
||||
overview, err := source.Overview(c.Request.Context(), query, tzOffsetMinutes)
|
||||
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.applyFinanceThirdPartyOverview(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
||||
}
|
||||
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
|
||||
@ -103,11 +118,30 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
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: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
RechargeType: financeQuery.RechargeType,
|
||||
Status: options.Status,
|
||||
StartAtMs: startAtMS,
|
||||
EndAtMs: endAtMS,
|
||||
@ -117,11 +151,7 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
writeWalletError(c, err, "获取财务概览失败")
|
||||
return
|
||||
}
|
||||
|
||||
overview := rechargeBillOverviewDTO{
|
||||
Daily: make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily())),
|
||||
Regions: make([]rechargeBillRegionBucketDTO, 0, len(resp.GetRegions())),
|
||||
}
|
||||
overview.Daily = make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily()))
|
||||
for _, bucket := range resp.GetDaily() {
|
||||
overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{
|
||||
Date: bucket.GetDate(),
|
||||
@ -133,25 +163,6 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
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(),
|
||||
@ -167,15 +178,16 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
overview.Withdrawal.TransferCount = transfer.GetTransferCount()
|
||||
overview.Withdrawal.TransferUsdMinor = transfer.GetTransferUsdMinor()
|
||||
}
|
||||
if err := h.applyFinanceThirdPartyOverview(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
Status: options.Status,
|
||||
Keyword: options.Keyword,
|
||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||
StartAtMS: startAtMS,
|
||||
EndAtMS: endAtMS,
|
||||
TzOffsetMinutes: tzOffsetMinutes,
|
||||
}, &overview); err != nil {
|
||||
}
|
||||
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
|
||||
}
|
||||
@ -183,73 +195,6 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
||||
response.OK(c, overview)
|
||||
}
|
||||
|
||||
func (h *Handler) applyFinanceThirdPartyOverview(ctx context.Context, appCode string, query financeThirdPartyRechargeQuery, overview *rechargeBillOverviewDTO) error {
|
||||
if overview == nil {
|
||||
return nil
|
||||
}
|
||||
stats, err := h.financeThirdPartyRechargeStats(ctx, appCode, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
byDate := map[string]int{}
|
||||
for i := range overview.Daily {
|
||||
byDate[overview.Daily[i].Date] = i
|
||||
if rechargeType == "" || rechargeType == "third_party" {
|
||||
// 覆写而不是累加:ordinary 三方支付仍可存在于钱包账单,但不能进入 finance thirdParty 统计。
|
||||
overview.Daily[i].ThirdPartyUsdMinor = 0
|
||||
overview.Daily[i].ThirdPartyCoinAmount = 0
|
||||
}
|
||||
}
|
||||
if rechargeType == "" || rechargeType == "third_party" {
|
||||
for date, bucket := range stats.VerifiedDaily {
|
||||
if date == "" {
|
||||
continue
|
||||
}
|
||||
index, ok := byDate[date]
|
||||
if !ok {
|
||||
overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{Date: date})
|
||||
index = len(overview.Daily) - 1
|
||||
byDate[date] = index
|
||||
}
|
||||
overview.Daily[index].ThirdPartyUsdMinor = bucket.USDMinorAmount
|
||||
overview.Daily[index].ThirdPartyCoinAmount = bucket.CoinAmount
|
||||
}
|
||||
}
|
||||
sort.Slice(overview.Daily, func(i, j int) bool {
|
||||
return overview.Daily[i].Date < overview.Daily[j].Date
|
||||
})
|
||||
if rechargeType == "third_party" {
|
||||
// thirdParty 区域分布来自工单 target_region_id/target_country_id 快照,不能复用普通支付账单区域。
|
||||
overview.Regions = financeThirdPartyRegionBuckets(stats.VerifiedRegions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func financeThirdPartyRegionBuckets(rows []repository.CoinSellerRechargeOrderRegionStatsBucket) []rechargeBillRegionBucketDTO {
|
||||
byRegion := map[int64]*rechargeBillRegionBucketDTO{}
|
||||
for _, row := range rows {
|
||||
if row.RegionID <= 0 {
|
||||
continue
|
||||
}
|
||||
bucket, ok := byRegion[row.RegionID]
|
||||
if !ok {
|
||||
bucket = &rechargeBillRegionBucketDTO{RegionID: row.RegionID}
|
||||
byRegion[row.RegionID] = bucket
|
||||
}
|
||||
bucket.BillCount += row.Count
|
||||
bucket.UsdMinorAmount += row.USDMinor
|
||||
}
|
||||
out := make([]rechargeBillRegionBucketDTO, 0, len(byRegion))
|
||||
for _, bucket := range byRegion {
|
||||
out = append(out, *bucket)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].UsdMinorAmount > out[j].UsdMinorAmount
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) requireLegacyCoinSellerDayRange(c *gin.Context, query legacyRechargeBillQuery, tzOffsetMinutes int32) bool {
|
||||
if !legacyCoinSellerAggregateRequested(query) || !legacyCoinSellerAggregateFilterSupported(query) {
|
||||
return true
|
||||
|
||||
@ -49,26 +49,6 @@ type googleRechargePaidDTO struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type financeThirdPartyRechargeStats struct {
|
||||
Verified rechargeBillSummaryBucketDTO
|
||||
GrantedOverlap rechargeBillSummaryBucketDTO
|
||||
VerifiedDaily map[string]rechargeBillSummaryBucketDTO
|
||||
GrantedDaily map[string]rechargeBillSummaryBucketDTO
|
||||
VerifiedRegions []repository.CoinSellerRechargeOrderRegionStatsBucket
|
||||
}
|
||||
|
||||
type financeThirdPartyRechargeQuery struct {
|
||||
RechargeType string
|
||||
Status string
|
||||
Keyword string
|
||||
RegionID int64
|
||||
StartAtMS int64
|
||||
EndAtMS int64
|
||||
TzOffsetMinutes int32
|
||||
TargetUserID int64
|
||||
HasUserOnlyFilter bool
|
||||
}
|
||||
|
||||
// listLegacyRechargeBills 处理 legacy App 的充值明细;分页与展示口径对齐 wallet-service 账单列表。
|
||||
func (h *Handler) listLegacyRechargeBills(c *gin.Context, source RechargeBillSource) {
|
||||
options := shared.ListOptions(c)
|
||||
@ -105,6 +85,20 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
||||
tzOffsetMinutes = defaultLegacyFinanceTZOffsetMinutes
|
||||
}
|
||||
if source, ok := h.billSources[appCode]; ok {
|
||||
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
|
||||
}
|
||||
query := legacyRechargeBillQuery{
|
||||
Keyword: options.Keyword,
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
@ -114,22 +108,28 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
||||
EndAtMS: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||
TzOffsetMinutes: tzOffsetMinutes,
|
||||
}
|
||||
if !h.requireLegacyCoinSellerDayRange(c, query, tzOffsetMinutes) {
|
||||
return
|
||||
}
|
||||
summary, err := source.SummarizeRechargeBills(c.Request.Context(), query)
|
||||
summary := rechargeBillSummaryDTO{}
|
||||
if !userFilter.IsEmpty() || !sellerFilter.IsEmpty() {
|
||||
// legacy 普通充值汇总没有 user/seller 过滤能力;有用户筛选时只保留后续可精确计算的财务币商桶。
|
||||
} else {
|
||||
var err error
|
||||
summary, err = source.SummarizeRechargeBills(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取充值汇总失败")
|
||||
return
|
||||
}
|
||||
if err := h.applyFinanceThirdPartySummary(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
||||
}
|
||||
if err := h.applyFinanceCoinSellerSummary(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: query.StartAtMS,
|
||||
EndAtMS: query.EndAtMS,
|
||||
TzOffsetMinutes: tzOffsetMinutes,
|
||||
TargetUserID: sellerUserID,
|
||||
HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
|
||||
}, &summary); err != nil {
|
||||
response.ServerError(c, "获取充值汇总失败")
|
||||
return
|
||||
@ -151,6 +151,9 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
||||
response.OK(c, rechargeBillSummaryDTO{})
|
||||
return
|
||||
}
|
||||
paidState := strings.TrimSpace(firstQuery(c, "paid_state", "paidState"))
|
||||
var summary rechargeBillSummaryDTO
|
||||
if paidState == "" {
|
||||
resp, err := h.wallet.GetRechargeBillSummary(c.Request.Context(), &walletv1.GetRechargeBillSummaryRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appCode,
|
||||
@ -167,15 +170,17 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
||||
writeWalletError(c, err, "获取充值汇总失败")
|
||||
return
|
||||
}
|
||||
summary := rechargeBillSummaryDTO{
|
||||
summary = rechargeBillSummaryDTO{
|
||||
Total: rechargeBillSummaryBucketFromProto(resp.GetTotal()),
|
||||
GooglePlay: rechargeBillSummaryBucketFromProto(resp.GetGooglePlay()),
|
||||
ThirdParty: rechargeBillSummaryBucketFromProto(resp.GetThirdParty()),
|
||||
CoinSeller: rechargeBillSummaryBucketFromProto(resp.GetCoinSeller()),
|
||||
}
|
||||
if err := h.applyFinanceThirdPartySummary(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
||||
}
|
||||
if err := h.applyFinanceCoinSellerSummary(c.Request.Context(), appCode, financeCoinSellerRechargeQuery{
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
Status: options.Status,
|
||||
PaidState: paidState,
|
||||
Keyword: options.Keyword,
|
||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||
@ -201,81 +206,6 @@ func rechargeBillSummaryBucketFromProto(bucket *walletv1.RechargeBillSummaryBuck
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) applyFinanceThirdPartySummary(ctx context.Context, appCode string, query financeThirdPartyRechargeQuery, summary *rechargeBillSummaryDTO) error {
|
||||
if summary == nil {
|
||||
return nil
|
||||
}
|
||||
stats, err := h.financeThirdPartyRechargeStats(ctx, appCode, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
if rechargeType == "" || rechargeType == "third_party" {
|
||||
// 财务页 thirdParty 只表示后台工单校验通过的币商充值外部订单;普通 MifaPay/V5Pay/USDT H5 支付不能进入该桶。
|
||||
summary.ThirdParty = stats.Verified
|
||||
} else {
|
||||
summary.ThirdParty = rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
if rechargeType != "" && rechargeType != "coin_seller" {
|
||||
summary.CoinSeller = rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
if rechargeType != "" && rechargeType != "google_play_recharge" {
|
||||
summary.GooglePlay = rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
if rechargeType == "" {
|
||||
// coinSeller 桶保持全量展示;total 扣掉已落账重叠部分,避免同一财务工单同时经 thirdParty 与 coinSeller 重复计入总额。
|
||||
summary.Total = nonNegativeRechargeBillBucket(rechargeBillSummaryBucketDTO{
|
||||
BillCount: summary.GooglePlay.BillCount + summary.CoinSeller.BillCount + summary.ThirdParty.BillCount - stats.GrantedOverlap.BillCount,
|
||||
CoinAmount: summary.GooglePlay.CoinAmount + summary.CoinSeller.CoinAmount + summary.ThirdParty.CoinAmount - stats.GrantedOverlap.CoinAmount,
|
||||
USDMinorAmount: summary.GooglePlay.USDMinorAmount + summary.CoinSeller.USDMinorAmount + summary.ThirdParty.USDMinorAmount - stats.GrantedOverlap.USDMinorAmount,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
legacyRecomputeSummaryTotal(summary)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) financeThirdPartyRechargeStats(ctx context.Context, appCode string, query financeThirdPartyRechargeQuery) (financeThirdPartyRechargeStats, error) {
|
||||
stats := financeThirdPartyRechargeStats{
|
||||
VerifiedDaily: map[string]rechargeBillSummaryBucketDTO{},
|
||||
GrantedDaily: map[string]rechargeBillSummaryBucketDTO{},
|
||||
}
|
||||
rechargeType := normalizedRechargeType(query.RechargeType)
|
||||
if rechargeType != "" && rechargeType != "third_party" && rechargeType != "coin_seller" {
|
||||
return stats, nil
|
||||
}
|
||||
if h == nil || h.store == nil || query.HasUserOnlyFilter {
|
||||
return stats, nil
|
||||
}
|
||||
repoStats, err := h.store.CoinSellerRechargeOrderStats(repository.CoinSellerRechargeOrderStatsOptions{
|
||||
AppCode: appCode,
|
||||
Status: query.Status,
|
||||
RegionID: query.RegionID,
|
||||
TargetUserID: query.TargetUserID,
|
||||
Keyword: query.Keyword,
|
||||
CreatedFromMS: query.StartAtMS,
|
||||
CreatedToMS: query.EndAtMS,
|
||||
TzOffsetMinutes: query.TzOffsetMinutes,
|
||||
})
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
stats.Verified = rechargeBillSummaryBucketDTO{BillCount: repoStats.VerifiedCount, CoinAmount: repoStats.VerifiedCoinAmount, USDMinorAmount: repoStats.VerifiedUSDMinor}
|
||||
stats.GrantedOverlap = rechargeBillSummaryBucketDTO{BillCount: repoStats.GrantedCount, CoinAmount: repoStats.GrantedCoinAmount, USDMinorAmount: repoStats.GrantedUSDMinor}
|
||||
stats.VerifiedDaily = financeThirdPartyDailyStats(repoStats.VerifiedDaily)
|
||||
stats.GrantedDaily = financeThirdPartyDailyStats(repoStats.GrantedDaily)
|
||||
stats.VerifiedRegions = repoStats.VerifiedRegionBuckets
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func financeThirdPartyDailyStats(in map[string]repository.CoinSellerRechargeOrderStatsBucket) map[string]rechargeBillSummaryBucketDTO {
|
||||
out := make(map[string]rechargeBillSummaryBucketDTO, len(in))
|
||||
for date, bucket := range in {
|
||||
out[date] = rechargeBillSummaryBucketDTO{BillCount: bucket.Count, CoinAmount: bucket.CoinAmount, USDMinorAmount: bucket.USDMinor}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizedRechargeType(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ var (
|
||||
type CoinSellerRechargeOrderListOptions struct {
|
||||
Page int
|
||||
PageSize int
|
||||
PageSizeLimit int
|
||||
AppCode string
|
||||
Status string
|
||||
VerifyStatus string
|
||||
@ -26,6 +27,7 @@ type CoinSellerRechargeOrderListOptions struct {
|
||||
ProviderCode string
|
||||
Keyword string
|
||||
TargetUserID int64
|
||||
RegionID int64
|
||||
OperatorID uint
|
||||
CreatedFromMS int64
|
||||
CreatedToMS int64
|
||||
@ -111,7 +113,7 @@ func (s *Store) GetCoinSellerRechargeOrder(id uint) (*model.CoinSellerRechargeOr
|
||||
}
|
||||
|
||||
func (s *Store) ListCoinSellerRechargeOrders(options CoinSellerRechargeOrderListOptions) ([]model.CoinSellerRechargeOrder, int64, error) {
|
||||
page, pageSize := normalizePage(options.Page, options.PageSize)
|
||||
page, pageSize := normalizeCoinSellerRechargeOrderPage(options.Page, options.PageSize, options.PageSizeLimit)
|
||||
query := applyCoinSellerRechargeOrderFilters(s.db.Model(&model.CoinSellerRechargeOrder{}), options)
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
@ -126,6 +128,22 @@ func (s *Store) ListCoinSellerRechargeOrders(options CoinSellerRechargeOrderList
|
||||
return orders, total, err
|
||||
}
|
||||
|
||||
func normalizeCoinSellerRechargeOrderPage(page int, pageSize int, pageSizeLimit int) (int, int) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSizeLimit <= 0 {
|
||||
pageSizeLimit = 100
|
||||
}
|
||||
if pageSize > pageSizeLimit {
|
||||
pageSize = pageSizeLimit
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func (s *Store) CoinSellerRechargeOrderStats(options CoinSellerRechargeOrderStatsOptions) (CoinSellerRechargeOrderStats, error) {
|
||||
stats := CoinSellerRechargeOrderStats{
|
||||
VerifiedDaily: map[string]CoinSellerRechargeOrderStatsBucket{},
|
||||
@ -350,6 +368,9 @@ func applyCoinSellerRechargeOrderFilters(query *gorm.DB, options CoinSellerRecha
|
||||
if options.TargetUserID > 0 {
|
||||
query = query.Where("target_user_id = ?", options.TargetUserID)
|
||||
}
|
||||
if options.RegionID > 0 {
|
||||
query = query.Where("target_region_id = ?", options.RegionID)
|
||||
}
|
||||
if options.OperatorID > 0 {
|
||||
query = query.Where("operator_user_id = ?", options.OperatorID)
|
||||
}
|
||||
@ -361,7 +382,7 @@ func applyCoinSellerRechargeOrderFilters(query *gorm.DB, options CoinSellerRecha
|
||||
}
|
||||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
query = query.Where("external_order_no LIKE ? OR target_display_user_id LIKE ? OR operator_name LIKE ? OR verified_by_name LIKE ? OR granted_by_name LIKE ? OR remark LIKE ? OR CAST(target_user_id AS CHAR) LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like, like, like, like)
|
||||
query = query.Where("external_order_no LIKE ? OR provider_order_id LIKE ? OR target_display_user_id LIKE ? OR wallet_transaction_id LIKE ? OR operator_name LIKE ? OR verified_by_name LIKE ? OR granted_by_name LIKE ? OR remark LIKE ? OR CAST(target_user_id AS CHAR) LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
return query
|
||||
}
|
||||
@ -392,7 +413,7 @@ func applyCoinSellerRechargeOrderStatsFilters(query *gorm.DB, options CoinSeller
|
||||
}
|
||||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
query = query.Where("external_order_no LIKE ? OR provider_order_id LIKE ? OR target_display_user_id LIKE ? OR wallet_transaction_id LIKE ? OR operator_name LIKE ? OR verified_by_name LIKE ? OR remark LIKE ? OR CAST(target_user_id AS CHAR) LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like, like, like, like, like)
|
||||
query = query.Where("external_order_no LIKE ? OR provider_order_id LIKE ? OR target_display_user_id LIKE ? OR wallet_transaction_id LIKE ? OR operator_name LIKE ? OR verified_by_name LIKE ? OR granted_by_name LIKE ? OR remark LIKE ? OR CAST(target_user_id AS CHAR) LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user