feat: expose coin seller transfer amounts
This commit is contained in:
parent
cb8cfd2167
commit
d7570d78c0
File diff suppressed because it is too large
Load Diff
@ -1485,6 +1485,8 @@ message WalletTransaction {
|
||||
int64 counterparty_user_id = 9;
|
||||
string room_id = 10;
|
||||
int64 created_at_ms = 11;
|
||||
int64 transfer_usd_minor = 12;
|
||||
string transfer_currency_code = 13;
|
||||
}
|
||||
|
||||
message ListWalletTransactionsRequest {
|
||||
|
||||
@ -7986,14 +7986,16 @@ func TestListWalletTransactionsIncludesCounterpartyProfile(t *testing.T) {
|
||||
walletClient := &fakeWalletClient{transactionsResp: &walletv1.ListWalletTransactionsResponse{
|
||||
Total: 1,
|
||||
Transactions: []*walletv1.WalletTransaction{{
|
||||
EntryId: 9,
|
||||
TransactionId: "wtx-seller-1",
|
||||
BizType: "coin_seller_transfer",
|
||||
AssetType: "COIN_SELLER_COIN",
|
||||
AvailableDelta: -10_000,
|
||||
AvailableAfter: 1_011,
|
||||
CounterpartyUserId: 9001,
|
||||
CreatedAtMs: 1_780_000_000_000,
|
||||
EntryId: 9,
|
||||
TransactionId: "wtx-seller-1",
|
||||
BizType: "coin_seller_transfer",
|
||||
AssetType: "COIN_SELLER_COIN",
|
||||
AvailableDelta: -10_000,
|
||||
AvailableAfter: 1_011,
|
||||
CounterpartyUserId: 9001,
|
||||
CreatedAtMs: 1_780_000_000_000,
|
||||
TransferUsdMinor: 1250,
|
||||
TransferCurrencyCode: "USD",
|
||||
}},
|
||||
}}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
@ -8034,7 +8036,7 @@ func TestListWalletTransactionsIncludesCounterpartyProfile(t *testing.T) {
|
||||
t.Fatalf("wallet transaction response mismatch: %+v", response)
|
||||
}
|
||||
first, ok := items[0].(map[string]any)
|
||||
if !ok || first["counterparty_display_user_id"] != "163001" || first["counterparty_username"] != "Alice" || first["counterparty_avatar"] != "https://cdn.example/alice.png" {
|
||||
if !ok || first["counterparty_display_user_id"] != "163001" || first["counterparty_username"] != "Alice" || first["counterparty_avatar"] != "https://cdn.example/alice.png" || first["transfer_usd_minor"] != float64(1250) || first["transfer_currency_code"] != "USD" {
|
||||
t.Fatalf("counterparty profile fields mismatch: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,8 @@ type walletTransactionData struct {
|
||||
CounterpartyAvatar string `json:"counterparty_avatar,omitempty"`
|
||||
RoomID string `json:"room_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
TransferUSDMinor int64 `json:"transfer_usd_minor,omitempty"`
|
||||
TransferCurrencyCode string `json:"transfer_currency_code,omitempty"`
|
||||
}
|
||||
|
||||
type giftWallData struct {
|
||||
@ -508,17 +510,19 @@ func walletTransactionFromProto(item *walletv1.WalletTransaction, profiles map[i
|
||||
return walletTransactionData{}
|
||||
}
|
||||
data := walletTransactionData{
|
||||
EntryID: item.GetEntryId(),
|
||||
TransactionID: item.GetTransactionId(),
|
||||
BizType: item.GetBizType(),
|
||||
AssetType: item.GetAssetType(),
|
||||
AvailableDelta: item.GetAvailableDelta(),
|
||||
FrozenDelta: item.GetFrozenDelta(),
|
||||
AvailableAfter: item.GetAvailableAfter(),
|
||||
FrozenAfter: item.GetFrozenAfter(),
|
||||
CounterpartyUserID: item.GetCounterpartyUserId(),
|
||||
RoomID: item.GetRoomId(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(),
|
||||
EntryID: item.GetEntryId(),
|
||||
TransactionID: item.GetTransactionId(),
|
||||
BizType: item.GetBizType(),
|
||||
AssetType: item.GetAssetType(),
|
||||
AvailableDelta: item.GetAvailableDelta(),
|
||||
FrozenDelta: item.GetFrozenDelta(),
|
||||
AvailableAfter: item.GetAvailableAfter(),
|
||||
FrozenAfter: item.GetFrozenAfter(),
|
||||
CounterpartyUserID: item.GetCounterpartyUserId(),
|
||||
RoomID: item.GetRoomId(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(),
|
||||
TransferUSDMinor: item.GetTransferUsdMinor(),
|
||||
TransferCurrencyCode: item.GetTransferCurrencyCode(),
|
||||
}
|
||||
if profile := profiles[item.GetCounterpartyUserId()]; profile != nil {
|
||||
data.CounterpartyDisplayUserID = profile.GetDisplayUserId()
|
||||
|
||||
@ -129,17 +129,19 @@ type DiamondExchangeRule struct {
|
||||
|
||||
// WalletTransaction 是钱包流水页按用户分录查询的投影。
|
||||
type WalletTransaction struct {
|
||||
EntryID int64
|
||||
TransactionID string
|
||||
BizType string
|
||||
AssetType string
|
||||
AvailableDelta int64
|
||||
FrozenDelta int64
|
||||
AvailableAfter int64
|
||||
FrozenAfter int64
|
||||
CounterpartyUserID int64
|
||||
RoomID string
|
||||
CreatedAtMS int64
|
||||
EntryID int64
|
||||
TransactionID string
|
||||
BizType string
|
||||
AssetType string
|
||||
AvailableDelta int64
|
||||
FrozenDelta int64
|
||||
AvailableAfter int64
|
||||
FrozenAfter int64
|
||||
CounterpartyUserID int64
|
||||
RoomID string
|
||||
CreatedAtMS int64
|
||||
TransferUSDMinor int64
|
||||
TransferCurrencyCode string
|
||||
}
|
||||
|
||||
// ListWalletTransactionsQuery 描述 App 钱包流水分页条件。
|
||||
|
||||
@ -2253,6 +2253,19 @@ func TestTransferSalaryToCoinSellerUsesRegionRateTier(t *testing.T) {
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.biz_type')) = ?", first.TransactionID, "WalletBalanceChanged", "salary_transfer_to_coin_seller"); got != 2 {
|
||||
t.Fatalf("salary transfer balance events must expose biz_type for statistics, got %d", got)
|
||||
}
|
||||
transactions, total, err := svc.ListWalletTransactions(context.Background(), ledger.ListWalletTransactionsQuery{
|
||||
AppCode: appcode.Default,
|
||||
UserID: 35002,
|
||||
AssetType: ledger.AssetCoinSellerCoin,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListWalletTransactions for seller failed: %v", err)
|
||||
}
|
||||
if total != 1 || len(transactions) != 1 || transactions[0].TransferUSDMinor != 4000 || transactions[0].TransferCurrencyCode != "USD" {
|
||||
t.Fatalf("seller transfer amount history mismatch: total=%d transactions=%+v", total, transactions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransferHostSalaryToCoinSellerCreditsTargetSellerOnly 验证别人主播把工资转给币商时,
|
||||
|
||||
@ -153,7 +153,8 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT e.entry_id, e.transaction_id, wt.biz_type, e.asset_type,
|
||||
e.available_delta, e.frozen_delta, e.available_after, e.frozen_after,
|
||||
e.counterparty_user_id, e.room_id, e.created_at_ms
|
||||
e.counterparty_user_id, e.room_id, e.created_at_ms,
|
||||
COALESCE(CAST(wt.metadata_json AS CHAR), '{}')
|
||||
FROM wallet_entries e
|
||||
JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id
|
||||
`+where+`
|
||||
@ -169,6 +170,7 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
|
||||
items := make([]ledger.WalletTransaction, 0, query.PageSize)
|
||||
for rows.Next() {
|
||||
var item ledger.WalletTransaction
|
||||
var metadataJSON string
|
||||
if err := rows.Scan(
|
||||
&item.EntryID,
|
||||
&item.TransactionID,
|
||||
@ -181,10 +183,62 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
|
||||
&item.CounterpartyUserID,
|
||||
&item.RoomID,
|
||||
&item.CreatedAtMS,
|
||||
&metadataJSON,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// H5 只需要展示用户实际转给币商的金额;这里统一从交易 metadata 提取安全字段,避免 gateway 或前端理解各类内部 JSON。
|
||||
item.TransferUSDMinor, item.TransferCurrencyCode = walletTransactionTransferAmount(item.BizType, metadataJSON)
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
// walletTransactionTransferAmount 把工资转币商、H5 币商进货和后台进货的不同 metadata 统一成展示金额;解析失败时返回空值,避免流水页展示错误金额。
|
||||
func walletTransactionTransferAmount(bizType string, metadataJSON string) (int64, string) {
|
||||
var metadata struct {
|
||||
SalaryUSDMinor int64 `json:"salary_usd_minor"`
|
||||
RechargeUSDMinor int64 `json:"recharge_usd_minor"`
|
||||
PaidAmountMicro int64 `json:"paid_amount_micro"`
|
||||
PaidCurrencyCode string `json:"paid_currency_code"`
|
||||
RechargeCurrencyCode string `json:"recharge_currency_code"`
|
||||
}
|
||||
if strings.TrimSpace(metadataJSON) == "" {
|
||||
return 0, ""
|
||||
}
|
||||
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
|
||||
return 0, ""
|
||||
}
|
||||
switch strings.TrimSpace(bizType) {
|
||||
case bizTypeSalaryTransferToCoinSeller:
|
||||
if metadata.SalaryUSDMinor > 0 {
|
||||
return metadata.SalaryUSDMinor, "USD"
|
||||
}
|
||||
case "coin_seller_recharge":
|
||||
if metadata.RechargeUSDMinor > 0 {
|
||||
return metadata.RechargeUSDMinor, walletTransactionCurrency(metadata.RechargeCurrencyCode, "USD")
|
||||
}
|
||||
if metadata.PaidAmountMicro > 0 {
|
||||
return metadata.PaidAmountMicro / 10000, walletTransactionCurrency(metadata.PaidCurrencyCode, "USD")
|
||||
}
|
||||
case bizTypeCoinSellerStockPurchase, bizTypeCoinSellerStockDeduction:
|
||||
if metadata.PaidAmountMicro != 0 {
|
||||
return metadata.PaidAmountMicro / 10000, walletTransactionCurrency(metadata.PaidCurrencyCode, "USD")
|
||||
}
|
||||
}
|
||||
if metadata.SalaryUSDMinor > 0 {
|
||||
return metadata.SalaryUSDMinor, "USD"
|
||||
}
|
||||
if metadata.RechargeUSDMinor > 0 {
|
||||
return metadata.RechargeUSDMinor, walletTransactionCurrency(metadata.RechargeCurrencyCode, "USD")
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func walletTransactionCurrency(value string, fallback string) string {
|
||||
value = strings.ToUpper(strings.TrimSpace(value))
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@ -117,17 +117,19 @@ func (s *Server) ListWalletTransactions(ctx context.Context, req *walletv1.ListW
|
||||
resp := &walletv1.ListWalletTransactionsResponse{Transactions: make([]*walletv1.WalletTransaction, 0, len(items)), Total: total}
|
||||
for _, item := range items {
|
||||
resp.Transactions = append(resp.Transactions, &walletv1.WalletTransaction{
|
||||
EntryId: item.EntryID,
|
||||
TransactionId: item.TransactionID,
|
||||
BizType: item.BizType,
|
||||
AssetType: item.AssetType,
|
||||
AvailableDelta: item.AvailableDelta,
|
||||
FrozenDelta: item.FrozenDelta,
|
||||
AvailableAfter: item.AvailableAfter,
|
||||
FrozenAfter: item.FrozenAfter,
|
||||
CounterpartyUserId: item.CounterpartyUserID,
|
||||
RoomId: item.RoomID,
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
EntryId: item.EntryID,
|
||||
TransactionId: item.TransactionID,
|
||||
BizType: item.BizType,
|
||||
AssetType: item.AssetType,
|
||||
AvailableDelta: item.AvailableDelta,
|
||||
FrozenDelta: item.FrozenDelta,
|
||||
AvailableAfter: item.AvailableAfter,
|
||||
FrozenAfter: item.FrozenAfter,
|
||||
CounterpartyUserId: item.CounterpartyUserID,
|
||||
RoomId: item.RoomID,
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
TransferUsdMinor: item.TransferUSDMinor,
|
||||
TransferCurrencyCode: item.TransferCurrencyCode,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user