682 lines
22 KiB
Go
682 lines
22 KiB
Go
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 ""
|
||
}
|