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;
string room_id = 10;
int64 created_at_ms = 11;
int64 transfer_usd_minor = 12;
string transfer_currency_code = 13;
}
message ListWalletTransactionsRequest {

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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 钱包流水分页条件。

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

View File

@ -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
}

View File

@ -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