财务口径
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) {
|
func (h *Handler) ListRechargeBills(c *gin.Context) {
|
||||||
options := shared.ListOptions(c)
|
options := shared.ListOptions(c)
|
||||||
appCode := appctx.FromContext(c.Request.Context())
|
appCode := appctx.FromContext(c.Request.Context())
|
||||||
if source, ok := h.billSources[appCode]; ok {
|
rechargeType := strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType"))
|
||||||
h.listLegacyRechargeBills(c, source)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
||||||
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
|
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
|
||||||
if !ok {
|
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})
|
response.OK(c, response.Page{Items: []rechargeBillDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||||||
return
|
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{
|
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
|
||||||
RequestId: middleware.CurrentRequestID(c),
|
RequestId: middleware.CurrentRequestID(c),
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
SellerUserId: sellerUserID,
|
SellerUserId: sellerUserID,
|
||||||
RegionId: queryInt64(c, "region_id", "regionId"),
|
RegionId: queryInt64(c, "region_id", "regionId"),
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
RechargeType: rechargeType,
|
||||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||||
Status: options.Status,
|
Status: options.Status,
|
||||||
Keyword: options.Keyword,
|
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) {
|
func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) {
|
||||||
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
|
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
|
||||||
Channels: []*walletv1.ThirdPartyPaymentChannel{{
|
Channels: []*walletv1.ThirdPartyPaymentChannel{{
|
||||||
@ -609,6 +637,7 @@ func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
router.GET("/admin/payment/recharge-bills", handler.ListRechargeBills)
|
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/third-party-channels", handler.ListThirdPartyPaymentChannels)
|
||||||
router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks)
|
router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks)
|
||||||
router.POST("/admin/payment/temporary-links", handler.CreateTemporaryPaymentLink)
|
router.POST("/admin/payment/temporary-links", handler.CreateTemporaryPaymentLink)
|
||||||
@ -755,6 +784,7 @@ type mockPaymentWallet struct {
|
|||||||
|
|
||||||
lastRechargeBills *walletv1.ListRechargeBillsRequest
|
lastRechargeBills *walletv1.ListRechargeBillsRequest
|
||||||
rechargeBillsResp *walletv1.ListRechargeBillsResponse
|
rechargeBillsResp *walletv1.ListRechargeBillsResponse
|
||||||
|
rechargeSummaryResp *walletv1.GetRechargeBillSummaryResponse
|
||||||
lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest
|
lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest
|
||||||
thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
|
thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
|
||||||
lastTemporaryOrders *walletv1.ListTemporaryRechargeOrdersRequest
|
lastTemporaryOrders *walletv1.ListTemporaryRechargeOrdersRequest
|
||||||
@ -779,6 +809,13 @@ func (m *mockPaymentWallet) ListRechargeBills(_ context.Context, req *walletv1.L
|
|||||||
return &walletv1.ListRechargeBillsResponse{}, nil
|
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) {
|
func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
|
||||||
m.lastThirdPartyChannels = req
|
m.lastThirdPartyChannels = req
|
||||||
if m.thirdPartyChannelsResp != nil {
|
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")
|
return nil, 0, fmt.Errorf("legacy bill source is not configured")
|
||||||
}
|
}
|
||||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||||
return s.listCoinSellerRechargeBills(ctx, query)
|
// 财务工作台币商充值已切到后台币商订单 + H5 币商身份订单,legacy dashboard 进货聚合不再作为账单来源。
|
||||||
|
return []rechargeBillDTO{}, 0, nil
|
||||||
}
|
}
|
||||||
if s.collection == nil {
|
if s.collection == nil {
|
||||||
return nil, 0, fmt.Errorf("legacy bill source is not configured")
|
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")
|
return rechargeBillSummaryDTO{}, fmt.Errorf("legacy bill source is not configured")
|
||||||
}
|
}
|
||||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||||
summary := rechargeBillSummaryDTO{}
|
return rechargeBillSummaryDTO{}, nil
|
||||||
if err := s.mergeCoinSellerSummary(ctx, query, legacySummaryTZOffset(query), &summary); err != nil {
|
|
||||||
return rechargeBillSummaryDTO{}, err
|
|
||||||
}
|
|
||||||
summary.Total = summary.CoinSeller
|
|
||||||
return summary, nil
|
|
||||||
}
|
}
|
||||||
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
|
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -635,11 +631,6 @@ func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, qu
|
|||||||
if err := cursor.Err(); err != nil {
|
if err := cursor.Err(); err != nil {
|
||||||
return rechargeBillSummaryDTO{}, fmt.Errorf("iterate legacy recharge summary for %s: %w", s.config.AppCode, err)
|
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)
|
legacyRecomputeSummaryTotal(&summary)
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
@ -907,9 +898,6 @@ func (s *MongoRechargeBillSource) Overview(ctx context.Context, query legacyRech
|
|||||||
return overview, err
|
return overview, err
|
||||||
}
|
}
|
||||||
if legacyQueryRechargeType(query) == "coin_seller" {
|
if legacyQueryRechargeType(query) == "coin_seller" {
|
||||||
if err := s.mergeCoinSellerOverview(ctx, query, tzOffsetMinutes, &overview); err != nil {
|
|
||||||
return overview, err
|
|
||||||
}
|
|
||||||
return overview, nil
|
return overview, nil
|
||||||
}
|
}
|
||||||
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
|
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 {
|
if err := s.overviewGooglePaid(ctx, filter, query, &overview); err != nil {
|
||||||
return overview, err
|
return overview, err
|
||||||
}
|
}
|
||||||
if legacyQueryRechargeType(query) == "" {
|
|
||||||
if err := s.mergeCoinSellerOverview(ctx, query, tzOffsetMinutes, &overview); err != nil {
|
|
||||||
return overview, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return overview, nil
|
return overview, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -234,7 +234,7 @@ func (s *fakeDashboardRechargeSource) ListCoinSellerRechargeBills(_ context.Cont
|
|||||||
return s.bills, s.billTotal, nil
|
return s.bills, s.billTotal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLegacyCoinSellerListUsesDashboardBillSource(t *testing.T) {
|
func TestLegacyCoinSellerListDoesNotUseDashboardBillSource(t *testing.T) {
|
||||||
dashboardSource := &fakeDashboardRechargeSource{
|
dashboardSource := &fakeDashboardRechargeSource{
|
||||||
appCode: "aslan",
|
appCode: "aslan",
|
||||||
billTotal: 2,
|
billTotal: 2,
|
||||||
@ -283,37 +283,11 @@ func TestLegacyCoinSellerListUsesDashboardBillSource(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListRechargeBills: %v", err)
|
t.Fatalf("ListRechargeBills: %v", err)
|
||||||
}
|
}
|
||||||
if total != 2 || len(items) != 2 {
|
if total != 0 || len(items) != 0 {
|
||||||
t.Fatalf("unexpected result total=%d items=%+v", total, items)
|
t.Fatalf("legacy dashboard coin seller bills should be disabled: total=%d items=%+v", total, items)
|
||||||
}
|
}
|
||||||
item := items[0]
|
if dashboardSource.billCalls != 0 {
|
||||||
if item.RechargeType != "coin_seller_stock_purchase" || item.ProviderCode != "coin_seller" {
|
t.Fatalf("dashboard bill source should not be called: calls=%d query=%+v", dashboardSource.billCalls, dashboardSource.billQuery)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -84,12 +84,61 @@ func (h *Handler) collectRechargeBillsForExport(c *gin.Context, appCode string)
|
|||||||
// legacy 币商充值导出必须和页面列表使用同一个自然日解释方式。
|
// legacy 币商充值导出必须和页面列表使用同一个自然日解释方式。
|
||||||
tzOffsetMinutes = defaultLegacyFinanceTZOffsetMinutes
|
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 {
|
if source, ok := h.billSources[appCode]; ok {
|
||||||
bills := make([]rechargeBillDTO, 0, rechargeBillExportPageSize)
|
bills := make([]rechargeBillDTO, 0, rechargeBillExportPageSize)
|
||||||
for page := 1; len(bills) < rechargeBillExportMaxRows; page++ {
|
for page := 1; len(bills) < rechargeBillExportMaxRows; page++ {
|
||||||
items, total, err := source.ListRechargeBills(c.Request.Context(), legacyRechargeBillQuery{
|
items, total, err := source.ListRechargeBills(c.Request.Context(), legacyRechargeBillQuery{
|
||||||
Keyword: options.Keyword,
|
Keyword: options.Keyword,
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
RechargeType: rechargeType,
|
||||||
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||||
@ -230,6 +279,8 @@ func rechargeSourceLabel(rechargeType string) string {
|
|||||||
switch rechargeType {
|
switch rechargeType {
|
||||||
case "google_play_recharge":
|
case "google_play_recharge":
|
||||||
return "谷歌充值"
|
return "谷歌充值"
|
||||||
|
case "coin_seller":
|
||||||
|
return "币商充值"
|
||||||
case "coin_seller_stock_purchase":
|
case "coin_seller_stock_purchase":
|
||||||
return "币商进货"
|
return "币商进货"
|
||||||
case "coin_seller_stock_deduction":
|
case "coin_seller_stock_deduction":
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
package payment
|
package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/middleware"
|
"hyapp-admin-server/internal/middleware"
|
||||||
"hyapp-admin-server/internal/modules/shared"
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
"hyapp-admin-server/internal/repository"
|
|
||||||
"hyapp-admin-server/internal/response"
|
"hyapp-admin-server/internal/response"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
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")
|
startAtMS := queryInt64(c, "start_at_ms", "startAtMs")
|
||||||
endAtMS := queryInt64(c, "end_at_ms", "endAtMs")
|
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 {
|
if source, ok := h.billSources[appCode]; ok {
|
||||||
query := legacyRechargeBillQuery{
|
query := legacyRechargeBillQuery{
|
||||||
Keyword: options.Keyword,
|
Keyword: options.Keyword,
|
||||||
@ -78,22 +89,26 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
|||||||
StartAtMS: startAtMS,
|
StartAtMS: startAtMS,
|
||||||
EndAtMS: endAtMS,
|
EndAtMS: endAtMS,
|
||||||
}
|
}
|
||||||
if !h.requireLegacyCoinSellerDayRange(c, query, tzOffsetMinutes) {
|
overview := rechargeBillOverviewDTO{Daily: []rechargeBillDailyBucketDTO{}, Regions: []rechargeBillRegionBucketDTO{}}
|
||||||
return
|
if userFilter.IsEmpty() && sellerFilter.IsEmpty() {
|
||||||
|
var err error
|
||||||
|
overview, err = source.Overview(c.Request.Context(), query, tzOffsetMinutes)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取财务概览失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
overview, err := source.Overview(c.Request.Context(), query, tzOffsetMinutes)
|
if err := h.applyFinanceCoinSellerOverview(c.Request.Context(), appCode, financeCoinSellerRechargeQuery{
|
||||||
if err != nil {
|
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||||
response.ServerError(c, "获取财务概览失败")
|
Status: options.Status,
|
||||||
return
|
PaidState: query.PaidState,
|
||||||
}
|
Keyword: options.Keyword,
|
||||||
if err := h.applyFinanceThirdPartyOverview(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
RegionID: query.RegionID,
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
StartAtMS: startAtMS,
|
||||||
Status: options.Status,
|
EndAtMS: endAtMS,
|
||||||
Keyword: options.Keyword,
|
TzOffsetMinutes: tzOffsetMinutes,
|
||||||
RegionID: query.RegionID,
|
TargetUserID: sellerUserID,
|
||||||
StartAtMS: startAtMS,
|
HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
|
||||||
EndAtMS: endAtMS,
|
|
||||||
TzOffsetMinutes: tzOffsetMinutes,
|
|
||||||
}, &overview); err != nil {
|
}, &overview); err != nil {
|
||||||
response.ServerError(c, "获取财务概览失败")
|
response.ServerError(c, "获取财务概览失败")
|
||||||
return
|
return
|
||||||
@ -103,79 +118,76 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
|||||||
return
|
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{
|
overview := rechargeBillOverviewDTO{
|
||||||
Daily: make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily())),
|
Daily: []rechargeBillDailyBucketDTO{},
|
||||||
Regions: make([]rechargeBillRegionBucketDTO, 0, len(resp.GetRegions())),
|
Regions: []rechargeBillRegionBucketDTO{},
|
||||||
}
|
}
|
||||||
for _, bucket := range resp.GetDaily() {
|
financeQuery := financeCoinSellerRechargeQuery{
|
||||||
overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{
|
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||||
Date: bucket.GetDate(),
|
Status: options.Status,
|
||||||
GoogleUsdMinor: bucket.GetGoogleUsdMinor(),
|
PaidState: strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
|
||||||
ThirdPartyUsdMinor: bucket.GetThirdPartyUsdMinor(),
|
Keyword: options.Keyword,
|
||||||
CoinSellerUsdMinor: bucket.GetCoinSellerUsdMinor(),
|
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||||
GoogleCoinAmount: bucket.GetGoogleCoinAmount(),
|
StartAtMS: startAtMS,
|
||||||
ThirdPartyCoinAmount: bucket.GetThirdPartyCoinAmount(),
|
EndAtMS: endAtMS,
|
||||||
CoinSellerCoinAmount: bucket.GetCoinSellerCoinAmount(),
|
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 {
|
||||||
// 区域名从后台区域目录补齐;目录缺失时回退区域 ID 文本,避免概览页出现空白区块。
|
writeWalletError(c, err, "获取财务概览失败")
|
||||||
regionNames := map[int64]string{}
|
return
|
||||||
if regions, err := h.listRechargeBillRegions(c.Request.Context(), appCode); err == nil {
|
}
|
||||||
for _, region := range regions {
|
overview.Daily = make([]rechargeBillDailyBucketDTO, 0, len(resp.GetDaily()))
|
||||||
name := region.Name
|
for _, bucket := range resp.GetDaily() {
|
||||||
if name == "" {
|
overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{
|
||||||
name = region.RegionCode
|
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(),
|
||||||
}
|
}
|
||||||
regionNames[region.RegionID] = name
|
}
|
||||||
|
if transfer := resp.GetSalaryTransfer(); transfer != nil {
|
||||||
|
overview.Withdrawal.TransferCount = transfer.GetTransferCount()
|
||||||
|
overview.Withdrawal.TransferUsdMinor = transfer.GetTransferUsdMinor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, bucket := range resp.GetRegions() {
|
if normalizedRechargeType(financeQuery.RechargeType) == "" {
|
||||||
overview.Regions = append(overview.Regions, rechargeBillRegionBucketDTO{
|
ordinaryRegions, err := h.walletOrdinaryRechargeOverviewRegions(c.Request.Context(), appCode, financeQuery)
|
||||||
RegionID: bucket.GetRegionId(),
|
if err != nil {
|
||||||
Name: regionNames[bucket.GetRegionId()],
|
writeWalletError(c, err, "获取财务概览失败")
|
||||||
BillCount: bucket.GetBillCount(),
|
return
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
|
overview.Regions = h.fillFinanceCoinSellerRegionNames(c.Request.Context(), appCode, ordinaryRegions)
|
||||||
}
|
}
|
||||||
if transfer := resp.GetSalaryTransfer(); transfer != nil {
|
if err := h.applyFinanceCoinSellerOverview(c.Request.Context(), appCode, financeQuery, &overview); err != nil {
|
||||||
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 {
|
|
||||||
response.ServerError(c, "获取财务概览失败")
|
response.ServerError(c, "获取财务概览失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -183,73 +195,6 @@ func (h *Handler) GetRechargeBillOverview(c *gin.Context) {
|
|||||||
response.OK(c, overview)
|
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 {
|
func (h *Handler) requireLegacyCoinSellerDayRange(c *gin.Context, query legacyRechargeBillQuery, tzOffsetMinutes int32) bool {
|
||||||
if !legacyCoinSellerAggregateRequested(query) || !legacyCoinSellerAggregateFilterSupported(query) {
|
if !legacyCoinSellerAggregateRequested(query) || !legacyCoinSellerAggregateFilterSupported(query) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -49,26 +49,6 @@ type googleRechargePaidDTO struct {
|
|||||||
Error string `json:"error,omitempty"`
|
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 账单列表。
|
// listLegacyRechargeBills 处理 legacy App 的充值明细;分页与展示口径对齐 wallet-service 账单列表。
|
||||||
func (h *Handler) listLegacyRechargeBills(c *gin.Context, source RechargeBillSource) {
|
func (h *Handler) listLegacyRechargeBills(c *gin.Context, source RechargeBillSource) {
|
||||||
options := shared.ListOptions(c)
|
options := shared.ListOptions(c)
|
||||||
@ -105,6 +85,20 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
|||||||
tzOffsetMinutes = defaultLegacyFinanceTZOffsetMinutes
|
tzOffsetMinutes = defaultLegacyFinanceTZOffsetMinutes
|
||||||
}
|
}
|
||||||
if source, ok := h.billSources[appCode]; ok {
|
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{
|
query := legacyRechargeBillQuery{
|
||||||
Keyword: options.Keyword,
|
Keyword: options.Keyword,
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
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"),
|
EndAtMS: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||||
TzOffsetMinutes: tzOffsetMinutes,
|
TzOffsetMinutes: tzOffsetMinutes,
|
||||||
}
|
}
|
||||||
if !h.requireLegacyCoinSellerDayRange(c, query, tzOffsetMinutes) {
|
summary := rechargeBillSummaryDTO{}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
summary, err := source.SummarizeRechargeBills(c.Request.Context(), query)
|
if err := h.applyFinanceCoinSellerSummary(c.Request.Context(), appCode, financeCoinSellerRechargeQuery{
|
||||||
if err != nil {
|
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||||
response.ServerError(c, "获取充值汇总失败")
|
Status: options.Status,
|
||||||
return
|
PaidState: query.PaidState,
|
||||||
}
|
Keyword: options.Keyword,
|
||||||
if err := h.applyFinanceThirdPartySummary(c.Request.Context(), appCode, financeThirdPartyRechargeQuery{
|
RegionID: query.RegionID,
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
StartAtMS: query.StartAtMS,
|
||||||
Status: options.Status,
|
EndAtMS: query.EndAtMS,
|
||||||
Keyword: options.Keyword,
|
TzOffsetMinutes: tzOffsetMinutes,
|
||||||
RegionID: query.RegionID,
|
TargetUserID: sellerUserID,
|
||||||
StartAtMS: query.StartAtMS,
|
HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
|
||||||
EndAtMS: query.EndAtMS,
|
|
||||||
TzOffsetMinutes: tzOffsetMinutes,
|
|
||||||
}, &summary); err != nil {
|
}, &summary); err != nil {
|
||||||
response.ServerError(c, "获取充值汇总失败")
|
response.ServerError(c, "获取充值汇总失败")
|
||||||
return
|
return
|
||||||
@ -151,31 +151,36 @@ func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
|||||||
response.OK(c, rechargeBillSummaryDTO{})
|
response.OK(c, rechargeBillSummaryDTO{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.wallet.GetRechargeBillSummary(c.Request.Context(), &walletv1.GetRechargeBillSummaryRequest{
|
paidState := strings.TrimSpace(firstQuery(c, "paid_state", "paidState"))
|
||||||
RequestId: middleware.CurrentRequestID(c),
|
var summary rechargeBillSummaryDTO
|
||||||
AppCode: appCode,
|
if paidState == "" {
|
||||||
UserId: userID,
|
resp, err := h.wallet.GetRechargeBillSummary(c.Request.Context(), &walletv1.GetRechargeBillSummaryRequest{
|
||||||
SellerUserId: sellerUserID,
|
RequestId: middleware.CurrentRequestID(c),
|
||||||
RegionId: queryInt64(c, "region_id", "regionId"),
|
AppCode: appCode,
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
UserId: userID,
|
||||||
Status: options.Status,
|
SellerUserId: sellerUserID,
|
||||||
Keyword: options.Keyword,
|
RegionId: queryInt64(c, "region_id", "regionId"),
|
||||||
StartAtMs: queryInt64(c, "start_at_ms", "startAtMs"),
|
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||||
EndAtMs: queryInt64(c, "end_at_ms", "endAtMs"),
|
Status: options.Status,
|
||||||
})
|
Keyword: options.Keyword,
|
||||||
if err != nil {
|
StartAtMs: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||||
writeWalletError(c, err, "获取充值汇总失败")
|
EndAtMs: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||||
return
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeWalletError(c, err, "获取充值汇总失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
summary = rechargeBillSummaryDTO{
|
||||||
|
Total: rechargeBillSummaryBucketFromProto(resp.GetTotal()),
|
||||||
|
GooglePlay: rechargeBillSummaryBucketFromProto(resp.GetGooglePlay()),
|
||||||
|
ThirdParty: rechargeBillSummaryBucketFromProto(resp.GetThirdParty()),
|
||||||
|
CoinSeller: rechargeBillSummaryBucketFromProto(resp.GetCoinSeller()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
summary := rechargeBillSummaryDTO{
|
if err := h.applyFinanceCoinSellerSummary(c.Request.Context(), appCode, financeCoinSellerRechargeQuery{
|
||||||
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{
|
|
||||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||||
Status: options.Status,
|
Status: options.Status,
|
||||||
|
PaidState: paidState,
|
||||||
Keyword: options.Keyword,
|
Keyword: options.Keyword,
|
||||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||||
StartAtMS: queryInt64(c, "start_at_ms", "startAtMs"),
|
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 {
|
func normalizedRechargeType(value string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(value))
|
return strings.ToLower(strings.TrimSpace(value))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ var (
|
|||||||
type CoinSellerRechargeOrderListOptions struct {
|
type CoinSellerRechargeOrderListOptions struct {
|
||||||
Page int
|
Page int
|
||||||
PageSize int
|
PageSize int
|
||||||
|
PageSizeLimit int
|
||||||
AppCode string
|
AppCode string
|
||||||
Status string
|
Status string
|
||||||
VerifyStatus string
|
VerifyStatus string
|
||||||
@ -26,6 +27,7 @@ type CoinSellerRechargeOrderListOptions struct {
|
|||||||
ProviderCode string
|
ProviderCode string
|
||||||
Keyword string
|
Keyword string
|
||||||
TargetUserID int64
|
TargetUserID int64
|
||||||
|
RegionID int64
|
||||||
OperatorID uint
|
OperatorID uint
|
||||||
CreatedFromMS int64
|
CreatedFromMS int64
|
||||||
CreatedToMS 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) {
|
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)
|
query := applyCoinSellerRechargeOrderFilters(s.db.Model(&model.CoinSellerRechargeOrder{}), options)
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
@ -126,6 +128,22 @@ func (s *Store) ListCoinSellerRechargeOrders(options CoinSellerRechargeOrderList
|
|||||||
return orders, total, err
|
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) {
|
func (s *Store) CoinSellerRechargeOrderStats(options CoinSellerRechargeOrderStatsOptions) (CoinSellerRechargeOrderStats, error) {
|
||||||
stats := CoinSellerRechargeOrderStats{
|
stats := CoinSellerRechargeOrderStats{
|
||||||
VerifiedDaily: map[string]CoinSellerRechargeOrderStatsBucket{},
|
VerifiedDaily: map[string]CoinSellerRechargeOrderStatsBucket{},
|
||||||
@ -350,6 +368,9 @@ func applyCoinSellerRechargeOrderFilters(query *gorm.DB, options CoinSellerRecha
|
|||||||
if options.TargetUserID > 0 {
|
if options.TargetUserID > 0 {
|
||||||
query = query.Where("target_user_id = ?", options.TargetUserID)
|
query = query.Where("target_user_id = ?", options.TargetUserID)
|
||||||
}
|
}
|
||||||
|
if options.RegionID > 0 {
|
||||||
|
query = query.Where("target_region_id = ?", options.RegionID)
|
||||||
|
}
|
||||||
if options.OperatorID > 0 {
|
if options.OperatorID > 0 {
|
||||||
query = query.Where("operator_user_id = ?", options.OperatorID)
|
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 != "" {
|
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||||||
like := "%" + 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
|
return query
|
||||||
}
|
}
|
||||||
@ -392,7 +413,7 @@ func applyCoinSellerRechargeOrderStatsFilters(query *gorm.DB, options CoinSeller
|
|||||||
}
|
}
|
||||||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||||||
like := "%" + 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
|
return query
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user