274 lines
10 KiB
Go
274 lines
10 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||
)
|
||
|
||
// ListRechargeBills 查询 wallet-service 的充值账单投影,后台不能直接读写业务库内部语义。
|
||
func (r *Repository) ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error) {
|
||
if r == nil || r.db == nil {
|
||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
ctx = contextWithCommandApp(ctx, query.AppCode)
|
||
query = normalizeRechargeBillsQuery(query)
|
||
query.AppCode = appcode.FromContext(ctx)
|
||
|
||
where, args := rechargeBillsWhereSQL(query)
|
||
total, err := r.countRechargeBillRows(ctx, where, args...)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
// 充值事实仍以 wallet_recharge_records 为主表;external_recharge_orders 补 H5 三方订单的实付币种和金额,
|
||
// payment_orders 补 Google 订单经 Orders API 同步的实付明细,两个 LEFT JOIN 都走 wallet_transaction_id 索引。
|
||
rows, err := r.db.QueryContext(ctx, `
|
||
SELECT rr.app_code, rr.transaction_id, wt.command_id, wt.biz_type, wt.status, wt.external_ref,
|
||
rr.user_id, rr.seller_user_id, rr.seller_region_id, rr.target_region_id,
|
||
rr.policy_id, rr.policy_version, rr.currency_code,
|
||
rr.coin_amount, rr.usd_minor_amount, rr.exchange_coin_amount, rr.exchange_usd_minor_amount,
|
||
COALESCE(eo.provider_amount_minor, 0), rr.created_at_ms,
|
||
COALESCE(eo.provider_code, ''), COALESCE(eo.currency_code, ''), COALESCE(CAST(eo.provider_payload AS CHAR), ''),
|
||
COALESCE(po.paid_currency_code, ''), COALESCE(po.paid_amount_micro, 0), COALESCE(po.paid_tax_micro, 0),
|
||
COALESCE(po.paid_net_micro, 0), COALESCE(po.paid_synced_at_ms, 0)
|
||
FROM wallet_recharge_records rr
|
||
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
|
||
LEFT JOIN external_recharge_orders eo ON eo.app_code = rr.app_code AND eo.wallet_transaction_id = rr.transaction_id
|
||
LEFT JOIN payment_orders po ON po.app_code = rr.app_code AND po.wallet_transaction_id = rr.transaction_id
|
||
`+where+`
|
||
ORDER BY rr.created_at_ms DESC, rr.transaction_id DESC
|
||
LIMIT ? OFFSET ?`,
|
||
append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
|
||
)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
items := make([]ledger.RechargeBill, 0, query.PageSize)
|
||
for rows.Next() {
|
||
var item ledger.RechargeBill
|
||
var externalProviderCode, externalCurrencyCode, externalPayloadJSON string
|
||
var paidCurrencyCode string
|
||
var paidAmountMicro, paidTaxMicro, paidNetMicro, paidSyncedAtMS int64
|
||
if err := rows.Scan(
|
||
&item.AppCode,
|
||
&item.TransactionID,
|
||
&item.CommandID,
|
||
&item.RechargeType,
|
||
&item.Status,
|
||
&item.ExternalRef,
|
||
&item.UserID,
|
||
&item.SellerUserID,
|
||
&item.SellerRegionID,
|
||
&item.TargetRegionID,
|
||
&item.PolicyID,
|
||
&item.PolicyVersion,
|
||
&item.CurrencyCode,
|
||
&item.CoinAmount,
|
||
&item.USDMinorAmount,
|
||
&item.ExchangeCoinAmount,
|
||
&item.ExchangeUSDMinorAmount,
|
||
&item.ProviderAmountMinor,
|
||
&item.CreatedAtMS,
|
||
&externalProviderCode,
|
||
&externalCurrencyCode,
|
||
&externalPayloadJSON,
|
||
&paidCurrencyCode,
|
||
&paidAmountMicro,
|
||
&paidTaxMicro,
|
||
&paidNetMicro,
|
||
&paidSyncedAtMS,
|
||
); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
fillRechargeBillPaidFacts(&item, externalProviderCode, externalCurrencyCode, externalPayloadJSON,
|
||
paidCurrencyCode, paidAmountMicro, paidTaxMicro, paidNetMicro, paidSyncedAtMS)
|
||
items = append(items, item)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return items, total, nil
|
||
}
|
||
|
||
// fillRechargeBillPaidFacts 把三方订单快照和 Google 实付明细收敛成账单统一的实付字段。
|
||
func fillRechargeBillPaidFacts(item *ledger.RechargeBill, externalProviderCode string, externalCurrencyCode string, externalPayloadJSON string,
|
||
paidCurrencyCode string, paidAmountMicro int64, paidTaxMicro int64, paidNetMicro int64, paidSyncedAtMS int64) {
|
||
if item.RechargeType == bizTypeGooglePlayRecharge {
|
||
item.ProviderCode = "google_play"
|
||
item.UserPaidCurrencyCode = paidCurrencyCode
|
||
item.UserPaidAmountMicro = paidAmountMicro
|
||
item.ProviderTaxMicro = paidTaxMicro
|
||
item.ProviderNetMicro = paidNetMicro
|
||
item.PaidSyncedAtMS = paidSyncedAtMS
|
||
// Google 不单独返回服务费,只能由 实付总额 - 净收入 - 税 推出;任一缺失则保持 0。
|
||
if paidAmountMicro > 0 && paidNetMicro > 0 {
|
||
if fee := paidAmountMicro - paidNetMicro - paidTaxMicro; fee > 0 {
|
||
item.ProviderFeeMicro = fee
|
||
}
|
||
}
|
||
return
|
||
}
|
||
item.ProviderCode = externalProviderCode
|
||
item.UserPaidCurrencyCode = externalCurrencyCode
|
||
if item.ProviderAmountMinor > 0 {
|
||
item.UserPaidAmountMicro = item.ProviderAmountMinor * 10_000
|
||
}
|
||
// V5Pay 的回调和查单快照带 fee/tax/accAmount;MiFaPay 与 USDT 没有该数据,解析不到就保持 0。
|
||
fee, tax, net := externalRechargeSettlementFromPayload(externalPayloadJSON)
|
||
item.ProviderFeeMicro = fee
|
||
item.ProviderTaxMicro = tax
|
||
item.ProviderNetMicro = net
|
||
}
|
||
|
||
// externalRechargeSettlementFromPayload 从 provider_payload 快照解析三方结算字段(实付币种微单位)。
|
||
func externalRechargeSettlementFromPayload(payloadJSON string) (feeMicro int64, taxMicro int64, netMicro int64) {
|
||
payloadJSON = strings.TrimSpace(payloadJSON)
|
||
if payloadJSON == "" || !strings.Contains(payloadJSON, "accAmount") && !strings.Contains(payloadJSON, "\"fee\"") {
|
||
return 0, 0, 0
|
||
}
|
||
var payload struct {
|
||
Fee string `json:"fee"`
|
||
Tax string `json:"tax"`
|
||
AccAmount string `json:"accAmount"`
|
||
}
|
||
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
||
return 0, 0, 0
|
||
}
|
||
return decimalAmountToMicro(payload.Fee), decimalAmountToMicro(payload.Tax), decimalAmountToMicro(payload.AccAmount)
|
||
}
|
||
|
||
// decimalAmountToMicro 把三方返回的十进制金额字符串(如 "10.50")转成微单位;非法或负数一律按 0 处理。
|
||
func decimalAmountToMicro(value string) int64 {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return 0
|
||
}
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed <= 0 {
|
||
return 0
|
||
}
|
||
return int64(math.Round(parsed * 1_000_000))
|
||
}
|
||
|
||
// SummarizeRechargeBills 按与列表一致的筛选口径聚合充值账单,拆分 Google 与三方两个渠道桶。
|
||
func (r *Repository) SummarizeRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) (ledger.RechargeBillSummary, error) {
|
||
if r == nil || r.db == nil {
|
||
return ledger.RechargeBillSummary{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
ctx = contextWithCommandApp(ctx, query.AppCode)
|
||
query = normalizeRechargeBillsQuery(query)
|
||
query.AppCode = appcode.FromContext(ctx)
|
||
|
||
where, args := rechargeBillsWhereSQL(query)
|
||
rows, err := r.db.QueryContext(ctx, `
|
||
SELECT wt.biz_type, COUNT(*), COALESCE(SUM(rr.coin_amount), 0), COALESCE(SUM(rr.usd_minor_amount), 0)
|
||
FROM wallet_recharge_records rr
|
||
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
|
||
`+where+`
|
||
GROUP BY wt.biz_type`,
|
||
args...,
|
||
)
|
||
if err != nil {
|
||
return ledger.RechargeBillSummary{}, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var summary ledger.RechargeBillSummary
|
||
for rows.Next() {
|
||
var bizType string
|
||
var bucket ledger.RechargeBillSummaryBucket
|
||
if err := rows.Scan(&bizType, &bucket.BillCount, &bucket.CoinAmount, &bucket.USDMinorAmount); err != nil {
|
||
return ledger.RechargeBillSummary{}, err
|
||
}
|
||
summary.Total.BillCount += bucket.BillCount
|
||
summary.Total.CoinAmount += bucket.CoinAmount
|
||
summary.Total.USDMinorAmount += bucket.USDMinorAmount
|
||
if bizType == bizTypeGooglePlayRecharge {
|
||
summary.GooglePlay.BillCount += bucket.BillCount
|
||
summary.GooglePlay.CoinAmount += bucket.CoinAmount
|
||
summary.GooglePlay.USDMinorAmount += bucket.USDMinorAmount
|
||
} else {
|
||
summary.ThirdParty.BillCount += bucket.BillCount
|
||
summary.ThirdParty.CoinAmount += bucket.CoinAmount
|
||
summary.ThirdParty.USDMinorAmount += bucket.USDMinorAmount
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return ledger.RechargeBillSummary{}, err
|
||
}
|
||
return summary, nil
|
||
}
|
||
|
||
func (r *Repository) countRechargeBillRows(ctx context.Context, where string, args ...any) (int64, error) {
|
||
var total int64
|
||
err := r.db.QueryRowContext(ctx, `
|
||
SELECT COUNT(*)
|
||
FROM wallet_recharge_records rr
|
||
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
|
||
`+where,
|
||
args...,
|
||
).Scan(&total)
|
||
return total, err
|
||
}
|
||
|
||
func normalizeRechargeBillsQuery(query ledger.ListRechargeBillsQuery) ledger.ListRechargeBillsQuery {
|
||
query.AppCode = appcode.Normalize(query.AppCode)
|
||
query.RechargeType = strings.ToLower(strings.TrimSpace(query.RechargeType))
|
||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||
query.Keyword = strings.TrimSpace(query.Keyword)
|
||
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
|
||
if query.EndAtMS > 0 && query.StartAtMS > query.EndAtMS {
|
||
query.StartAtMS, query.EndAtMS = query.EndAtMS, query.StartAtMS
|
||
}
|
||
return query
|
||
}
|
||
|
||
func rechargeBillsWhereSQL(query ledger.ListRechargeBillsQuery) (string, []any) {
|
||
// 财务系统的 APP 充值详情只展示用户主动向平台充值的账单;币商给用户转普通金币虽然会落充值事实供活动/统计消费,
|
||
// 但它本质是币商出货,不应混入财务充值明细和分页总数。
|
||
where := `WHERE rr.app_code = ? AND wt.biz_type <> ?`
|
||
args := []any{query.AppCode, bizTypeCoinSellerTransfer}
|
||
if query.UserID > 0 {
|
||
where += ` AND rr.user_id = ?`
|
||
args = append(args, query.UserID)
|
||
}
|
||
if query.SellerUserID > 0 {
|
||
where += ` AND rr.seller_user_id = ?`
|
||
args = append(args, query.SellerUserID)
|
||
}
|
||
if query.RegionID > 0 {
|
||
where += ` AND rr.target_region_id = ?`
|
||
args = append(args, query.RegionID)
|
||
}
|
||
if query.RechargeType != "" {
|
||
where += ` AND wt.biz_type = ?`
|
||
args = append(args, query.RechargeType)
|
||
}
|
||
if query.Status != "" {
|
||
where += ` AND wt.status = ?`
|
||
args = append(args, query.Status)
|
||
}
|
||
if query.StartAtMS > 0 {
|
||
where += ` AND rr.created_at_ms >= ?`
|
||
args = append(args, query.StartAtMS)
|
||
}
|
||
if query.EndAtMS > 0 {
|
||
where += ` AND rr.created_at_ms < ?`
|
||
args = append(args, query.EndAtMS)
|
||
}
|
||
if query.Keyword != "" {
|
||
like := "%" + query.Keyword + "%"
|
||
where += ` AND (rr.transaction_id LIKE ? OR wt.command_id LIKE ? OR wt.external_ref LIKE ? OR rr.policy_version LIKE ?)`
|
||
args = append(args, like, like, like, like)
|
||
}
|
||
return where, args
|
||
}
|