fix: 支持H5币安站内转账查单

This commit is contained in:
zhx 2026-07-12 23:59:51 +08:00
parent ca15f09850
commit 52264c2990
6 changed files with 145 additions and 12 deletions

View File

@ -148,6 +148,20 @@ func TestFindUSDTDepositMissingAccountReturnsConfiguredError(t *testing.T) {
}
}
func TestAccountForAppSupportsAllProductionApps(t *testing.T) {
client := New(config.BinanceConfig{
Lalu: config.BinanceAccountConfig{APIKey: "api-lalu", APISecretKey: "secret-lalu"},
Yumi: config.BinanceAccountConfig{APIKey: "api-yumi", APISecretKey: "secret-yumi"},
Aslan: config.BinanceAccountConfig{APIKey: "api-aslan", APISecretKey: "secret-aslan"},
})
for _, appCode := range []string{"lalu", "yumi", "aslan"} {
account, ok := client.accountForApp(appCode)
if !ok || account.apiKey != "api-"+appCode || account.apiSecretKey != "secret-"+appCode {
t.Fatalf("Binance account mapping mismatch for %s: ok=%v account=%+v", appCode, ok, account)
}
}
}
func TestDepositTxIDMatchesOffchainVariants(t *testing.T) {
if !depositTxIDMatches("Off-chain transfer 388611987194", "388611987194") {
t.Fatalf("plain off-chain id should match Binance txId")

View File

@ -6,6 +6,7 @@ import (
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
"hyapp/services/wallet-service/internal/service/wallet/ports"
"strings"
)
@ -178,7 +179,7 @@ func providerPayloadJSON(err error) string {
return ""
}
// SubmitH5RechargeTx 把用户提交的 tx_hash 交给 TRON 适配器校验,通过后立即入账。
// SubmitH5RechargeTx 校验用户提交的 TRON 链上哈希或 Binance 站内转账号,通过后立即入账。
func (s *Service) SubmitH5RechargeTx(ctx context.Context, command ledger.SubmitExternalRechargeTxCommand) (ledger.ExternalRechargeOrder, error) {
command.AppCode = appcode.Normalize(command.AppCode)
command.OrderID = strings.TrimSpace(command.OrderID)
@ -200,18 +201,43 @@ func (s *Service) SubmitH5RechargeTx(ctx context.Context, command ledger.SubmitE
if order.ProviderCode != ledger.PaymentProviderUSDTTRC20 || order.Status != ledger.ExternalRechargeStatusPending {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Conflict, "external recharge order is not pending usdt order")
}
if s.tronUSDT == nil {
return s.repository.AttachExternalRechargeTx(ctx, command, "")
}
rawJSON, err := s.tronUSDT.VerifyUSDTTransfer(ctx, command.TxHash, order.ReceiveAddress, order.USDMinorAmount)
providerOrderID, rawJSON, err := s.verifyH5USDTTransfer(ctx, command, order)
if err != nil {
// 校验失败仍保留用户提交的编号和上游快照,便于后续重试与财务复核;这一步不会改变账务。
_, _ = s.repository.AttachExternalRechargeTx(ctx, command, rawJSON)
return ledger.ExternalRechargeOrder{}, err
}
if _, err := s.repository.AttachExternalRechargeTx(ctx, command, rawJSON); err != nil {
return ledger.ExternalRechargeOrder{}, err
}
return s.repository.CreditExternalRechargeOrder(ctx, command.AppCode, command.OrderID, "", command.TxHash, rawJSON)
return s.repository.CreditExternalRechargeOrder(ctx, command.AppCode, command.OrderID, providerOrderID, command.TxHash, rawJSON)
}
// verifyH5USDTTransfer 只根据凭证形态选择事实源64 位十六进制哈希查 TRON其他编号查当前 App 的 Binance 入金记录。
// Binance 路径不信任付款方截图FindUSDTDeposit 会同时核对币种、网络、收款地址、金额和成功状态。
func (s *Service) verifyH5USDTTransfer(ctx context.Context, command ledger.SubmitExternalRechargeTxCommand, order ledger.ExternalRechargeOrder) (string, string, error) {
if isLikelyChainTransactionHash(command.TxHash) {
if s.tronUSDT == nil {
return "", "", xerr.New(xerr.Unavailable, "tron grid client is not configured")
}
rawJSON, err := s.tronUSDT.VerifyUSDTTransfer(ctx, command.TxHash, order.ReceiveAddress, order.USDMinorAmount)
return "", rawJSON, err
}
if s.binance == nil {
return "", "", xerr.New(xerr.Unavailable, "binance client is not configured")
}
record, err := s.binance.FindUSDTDeposit(ctx, ports.BinanceDepositLookupRequest{
AppCode: command.AppCode,
ExternalID: command.TxHash,
Coin: ledger.PaidCurrencyUSDT,
Network: "TRX",
ToAddress: order.ReceiveAddress,
AmountMinor: order.USDMinorAmount,
})
if err != nil {
return "", "", err
}
return record.TxID, strings.TrimSpace(record.RawJSON), nil
}
func (s *Service) GetH5RechargeOrder(ctx context.Context, appCode string, orderID string, targetUserID int64) (ledger.ExternalRechargeOrder, error) {

View File

@ -592,6 +592,58 @@ func TestVerifyCoinSellerRechargeReceiptUSDTTRC20OffchainMissingBinanceAccount(t
}
}
func TestSubmitH5RechargeTxBinanceOffchainUsesAppScopedAccount(t *testing.T) {
for index, appCode := range []string{"lalu", "yumi", "aslan"} {
t.Run(appCode, func(t *testing.T) {
order := ledger.ExternalRechargeOrder{
OrderID: "order-" + appCode,
AppCode: appCode,
TargetUserID: int64(81000 + index),
USDMinorAmount: 30000,
ProviderCode: ledger.PaymentProviderUSDTTRC20,
ReceiveAddress: "TReceiveAddress-" + appCode,
Status: ledger.ExternalRechargeStatusPending,
}
repository := &fakeH5USDTRepository{order: order}
binance := &fakeBinanceClient{record: walletservice.BinanceDepositRecord{
TxID: "Off-chain transfer 390597460031",
AmountMinor: 30000,
Address: order.ReceiveAddress,
RawJSON: `{"txId":"Off-chain transfer 390597460031","status":1}`,
}}
tron := &fakeTronUSDTClient{err: xerr.New(xerr.Internal, "tron should not be called")}
svc := walletservice.New(repository)
svc.SetBinanceClient(binance)
svc.SetTronUSDTClient(tron)
credited, err := svc.SubmitH5RechargeTx(context.Background(), ledger.SubmitExternalRechargeTxCommand{
AppCode: appCode,
OrderID: order.OrderID,
TargetUserID: order.TargetUserID,
TxHash: "390597460031",
})
if err != nil {
t.Fatalf("SubmitH5RechargeTx Binance off-chain failed: %v", err)
}
if credited.Status != ledger.ExternalRechargeStatusCredited {
t.Fatalf("Binance off-chain order was not credited: %+v", credited)
}
if binance.calls != 1 || binance.req.AppCode != appCode || binance.req.ExternalID != "390597460031" || binance.req.Coin != ledger.PaidCurrencyUSDT || binance.req.Network != "TRX" || binance.req.ToAddress != order.ReceiveAddress || binance.req.AmountMinor != 30000 {
t.Fatalf("Binance app-scoped lookup mismatch: calls=%d req=%+v", binance.calls, binance.req)
}
if tron.lastTxHash != "" {
t.Fatalf("Binance off-chain transfer must not query TronGrid: %+v", tron)
}
if repository.attachCommand.TxHash != "390597460031" || repository.attachPayload != binance.record.RawJSON {
t.Fatalf("submitted Binance evidence was not attached: command=%+v payload=%s", repository.attachCommand, repository.attachPayload)
}
if repository.creditProviderOrderID != binance.record.TxID || repository.creditTxHash != "390597460031" || repository.creditPayload != binance.record.RawJSON {
t.Fatalf("Binance credit evidence mismatch: provider_order_id=%s tx_hash=%s payload=%s", repository.creditProviderOrderID, repository.creditTxHash, repository.creditPayload)
}
})
}
}
type fakeReceiptRepository struct {
walletservice.Repository
channels []ledger.ThirdPartyPaymentChannel
@ -600,6 +652,47 @@ type fakeReceiptRepository struct {
querySeen bool
}
type fakeH5USDTRepository struct {
walletservice.Repository
order ledger.ExternalRechargeOrder
attachCommand ledger.SubmitExternalRechargeTxCommand
attachPayload string
creditProviderOrderID string
creditTxHash string
creditPayload string
}
func (r *fakeH5USDTRepository) GetExternalRechargeOrder(_ context.Context, appCode string, orderID string) (ledger.ExternalRechargeOrder, error) {
if appCode != r.order.AppCode || orderID != r.order.OrderID {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
}
return r.order, nil
}
func (r *fakeH5USDTRepository) AttachExternalRechargeTx(_ context.Context, command ledger.SubmitExternalRechargeTxCommand, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
// 测试仓储保留入账前的凭证快照,用于断言上游查单结果没有在 service 层丢失。
r.attachCommand = command
r.attachPayload = payloadJSON
r.order.TxHash = command.TxHash
r.order.ProviderPayloadJSON = payloadJSON
return r.order, nil
}
func (r *fakeH5USDTRepository) CreditExternalRechargeOrder(_ context.Context, appCode string, orderID string, providerOrderID string, txHash string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
if appCode != r.order.AppCode || orderID != r.order.OrderID {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
}
// 只有校验通过才会进入此方法;真实 MySQL 仓储仍负责唯一凭证和原子入账约束。
r.creditProviderOrderID = providerOrderID
r.creditTxHash = txHash
r.creditPayload = payloadJSON
r.order.ProviderOrderID = providerOrderID
r.order.TxHash = txHash
r.order.ProviderPayloadJSON = payloadJSON
r.order.Status = ledger.ExternalRechargeStatusCredited
return r.order, nil
}
func (r *fakeReceiptRepository) ListThirdPartyPaymentChannels(_ context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) {
r.query = query
r.querySeen = true

View File

@ -147,7 +147,7 @@ type V5PayProductTypesResponse struct {
RawJSON string
}
// BinanceDepositLookupRequest 是后台 USDT 币商充值使用的 Binance 入金查单条件。
// BinanceDepositLookupRequest 是 H5 充值和后台 USDT 币商充值共用的 Binance 入金查单条件。
type BinanceDepositLookupRequest struct {
AppCode string
ExternalID string
@ -201,7 +201,7 @@ type BSCUSDTClient interface {
VerifyUSDTTransfer(ctx context.Context, txHash string, toAddress string, amountMinor int64) (string, error)
}
// BinanceClient 隔离 Binance signed APIservice 只关心入金记录是否匹配后台订单。
// BinanceClient 隔离 Binance signed APIservice 只关心入金记录是否匹配当前 App 订单。
type BinanceClient interface {
FindUSDTDeposit(ctx context.Context, req BinanceDepositLookupRequest) (BinanceDepositRecord, error)
}

View File

@ -63,7 +63,7 @@ func (s *Service) SetBSCUSDTClient(client ports.BSCUSDTClient) {
s.bscUSDT = client
}
// SetBinanceClient 注入 Binance 入金只读查询客户端;后台币商充值可用 off-chain 单号自动查账。
// SetBinanceClient 注入 Binance 入金只读查询客户端;H5 充值和后台币商充值共用按 App 隔离的 off-chain 查账。
func (s *Service) SetBinanceClient(client ports.BinanceClient) {
s.binance = client
}

View File

@ -3568,7 +3568,7 @@ func TestH5USDTRechargeCreditsNormalUserCoinWallet(t *testing.T) {
AppCode: appcode.Default,
OrderID: order.OrderID,
TargetUserID: 43001,
TxHash: "tx-normal-usdt",
TxHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
})
if err != nil {
t.Fatalf("SubmitH5RechargeTx normal failed: %v", err)
@ -3576,7 +3576,7 @@ func TestH5USDTRechargeCreditsNormalUserCoinWallet(t *testing.T) {
if credited.Status != ledger.ExternalRechargeStatusCredited || credited.TransactionID == "" {
t.Fatalf("normal usdt credited order mismatch: %+v", credited)
}
if tron.lastTxHash != "tx-normal-usdt" || tron.lastToAddress != "TPlatformReceiveAddress" || tron.lastAmountMinor != 150 {
if tron.lastTxHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" || tron.lastToAddress != "TPlatformReceiveAddress" || tron.lastAmountMinor != 150 {
t.Fatalf("tron verification input mismatch: %+v", tron)
}
assertBalance(t, svc, 43001, ledger.AssetCoin, 120000)
@ -3620,7 +3620,7 @@ func TestH5USDTRechargeCreditsCoinSellerWalletOnly(t *testing.T) {
AppCode: appcode.Default,
OrderID: order.OrderID,
TargetUserID: 44001,
TxHash: "tx-seller-usdt",
TxHash: "1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
})
if err != nil {
t.Fatalf("SubmitH5RechargeTx seller failed: %v", err)