fix: 支持H5币安站内转账查单
This commit is contained in:
parent
ca15f09850
commit
52264c2990
@ -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")
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 API;service 只关心入金记录是否匹配后台订单。
|
||||
// BinanceClient 隔离 Binance signed API;service 只关心入金记录是否匹配当前 App 订单。
|
||||
type BinanceClient interface {
|
||||
FindUSDTDeposit(ctx context.Context, req BinanceDepositLookupRequest) (BinanceDepositRecord, error)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user