feat: expose coin seller transfer amounts

This commit is contained in:
zhx 2026-07-01 15:17:30 +08:00
parent cb8cfd2167
commit d7570d78c0
8 changed files with 1609 additions and 1507 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1485,6 +1485,8 @@ message WalletTransaction {
int64 counterparty_user_id = 9; int64 counterparty_user_id = 9;
string room_id = 10; string room_id = 10;
int64 created_at_ms = 11; int64 created_at_ms = 11;
int64 transfer_usd_minor = 12;
string transfer_currency_code = 13;
} }
message ListWalletTransactionsRequest { message ListWalletTransactionsRequest {

View File

@ -7994,6 +7994,8 @@ func TestListWalletTransactionsIncludesCounterpartyProfile(t *testing.T) {
AvailableAfter: 1_011, AvailableAfter: 1_011,
CounterpartyUserId: 9001, CounterpartyUserId: 9001,
CreatedAtMs: 1_780_000_000_000, CreatedAtMs: 1_780_000_000_000,
TransferUsdMinor: 1250,
TransferCurrencyCode: "USD",
}}, }},
}} }}
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{ profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
@ -8034,7 +8036,7 @@ func TestListWalletTransactionsIncludesCounterpartyProfile(t *testing.T) {
t.Fatalf("wallet transaction response mismatch: %+v", response) t.Fatalf("wallet transaction response mismatch: %+v", response)
} }
first, ok := items[0].(map[string]any) 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) t.Fatalf("counterparty profile fields mismatch: %+v", first)
} }
} }

View File

@ -65,6 +65,8 @@ type walletTransactionData struct {
CounterpartyAvatar string `json:"counterparty_avatar,omitempty"` CounterpartyAvatar string `json:"counterparty_avatar,omitempty"`
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
CreatedAtMS int64 `json:"created_at_ms"` CreatedAtMS int64 `json:"created_at_ms"`
TransferUSDMinor int64 `json:"transfer_usd_minor,omitempty"`
TransferCurrencyCode string `json:"transfer_currency_code,omitempty"`
} }
type giftWallData struct { type giftWallData struct {
@ -519,6 +521,8 @@ func walletTransactionFromProto(item *walletv1.WalletTransaction, profiles map[i
CounterpartyUserID: item.GetCounterpartyUserId(), CounterpartyUserID: item.GetCounterpartyUserId(),
RoomID: item.GetRoomId(), RoomID: item.GetRoomId(),
CreatedAtMS: item.GetCreatedAtMs(), CreatedAtMS: item.GetCreatedAtMs(),
TransferUSDMinor: item.GetTransferUsdMinor(),
TransferCurrencyCode: item.GetTransferCurrencyCode(),
} }
if profile := profiles[item.GetCounterpartyUserId()]; profile != nil { if profile := profiles[item.GetCounterpartyUserId()]; profile != nil {
data.CounterpartyDisplayUserID = profile.GetDisplayUserId() data.CounterpartyDisplayUserID = profile.GetDisplayUserId()

View File

@ -140,6 +140,8 @@ type WalletTransaction struct {
CounterpartyUserID int64 CounterpartyUserID int64
RoomID string RoomID string
CreatedAtMS int64 CreatedAtMS int64
TransferUSDMinor int64
TransferCurrencyCode string
} }
// ListWalletTransactionsQuery 描述 App 钱包流水分页条件。 // ListWalletTransactionsQuery 描述 App 钱包流水分页条件。

View File

@ -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 { 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) 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 验证别人主播把工资转给币商时, // TestTransferHostSalaryToCoinSellerCreditsTargetSellerOnly 验证别人主播把工资转给币商时,

View File

@ -153,7 +153,8 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
rows, err := r.db.QueryContext(ctx, ` rows, err := r.db.QueryContext(ctx, `
SELECT e.entry_id, e.transaction_id, wt.biz_type, e.asset_type, 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.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 FROM wallet_entries e
JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id
`+where+` `+where+`
@ -169,6 +170,7 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
items := make([]ledger.WalletTransaction, 0, query.PageSize) items := make([]ledger.WalletTransaction, 0, query.PageSize)
for rows.Next() { for rows.Next() {
var item ledger.WalletTransaction var item ledger.WalletTransaction
var metadataJSON string
if err := rows.Scan( if err := rows.Scan(
&item.EntryID, &item.EntryID,
&item.TransactionID, &item.TransactionID,
@ -181,10 +183,62 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
&item.CounterpartyUserID, &item.CounterpartyUserID,
&item.RoomID, &item.RoomID,
&item.CreatedAtMS, &item.CreatedAtMS,
&metadataJSON,
); err != nil { ); err != nil {
return nil, 0, err return nil, 0, err
} }
// H5 只需要展示用户实际转给币商的金额;这里统一从交易 metadata 提取安全字段,避免 gateway 或前端理解各类内部 JSON。
item.TransferUSDMinor, item.TransferCurrencyCode = walletTransactionTransferAmount(item.BizType, metadataJSON)
items = append(items, item) items = append(items, item)
} }
return items, total, rows.Err() 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
}

View File

@ -128,6 +128,8 @@ func (s *Server) ListWalletTransactions(ctx context.Context, req *walletv1.ListW
CounterpartyUserId: item.CounterpartyUserID, CounterpartyUserId: item.CounterpartyUserID,
RoomId: item.RoomID, RoomId: item.RoomID,
CreatedAtMs: item.CreatedAtMS, CreatedAtMs: item.CreatedAtMS,
TransferUsdMinor: item.TransferUSDMinor,
TransferCurrencyCode: item.TransferCurrencyCode,
}) })
} }
return resp, nil return resp, nil