支持C2C Pay的USDC入账校验

This commit is contained in:
zhx 2026-07-14 18:11:20 +08:00
parent bb5efebabd
commit 449110aa95
3 changed files with 41 additions and 9 deletions

View File

@ -89,13 +89,15 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
return ports.BinanceTransferRecord{}, err
}
if req.Network == "C2C" {
// C2C/P2P 与 Binance Pay 是两套独立账单;显式 C2C 只查法币交易历史,禁止再回退到 Pay 或链上入金。
startTimeMS, endTimeMS := payHistoryRange(req.OrderTimeMS, serverTimeMS)
records, err := c.c2cHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
// 后台的 C2C 对应 Binance Pay 账单中的 orderType=C2C而不是法币 P2P Trade History。
// 显式 C2C 只查 Pay且允许收款币种为 USDT 或 USDC不能再回退到链上或 P2P 历史。
// 运营录单时间可能是补单操作时间而非 Binance 成交时间;显式订单号查验使用有界 90 天窗口,避免错误窄窗漏单。
startTimeMS, endTimeMS := maxInt64(0, serverTimeMS-int64(defaultPayLookback/time.Millisecond)), serverTimeMS
records, err := c.payHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
return matchC2CRecord(req, records)
return matchExplicitC2CPayRecord(req, records)
}
// Binance 的 deposit history 查询窗口有限;后台凭证校验只做近期有界查询,避免一次手工校验变成无界扫描。
records, err := c.depositHistory(ctx, account, req.Coin, serverTimeMS-int64(defaultDepositLookback/time.Millisecond), serverTimeMS)
@ -118,6 +120,32 @@ func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransfer
return matchPayRecord(req, payRecords)
}
func matchExplicitC2CPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) {
for _, record := range records {
if !payExternalIDMatches(record, req.ExternalID) {
continue
}
if record.OrderType != "C2C" {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay order type is not c2c")
}
if record.Currency != "USDT" && record.Currency != "USDC" {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay c2c currency is not supported")
}
if strings.HasPrefix(strings.TrimSpace(record.Amount), "-") {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
}
amountMinor, err := parseUSDTAmountMinor(record.Amount)
if err != nil {
return ports.BinanceTransferRecord{}, err
}
if amountMinor <= 0 || amountMinor != req.AmountMinor {
return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay amount mismatch")
}
return ports.BinanceTransferRecord{Source: "pay_history", ID: record.OrderID, TxID: firstNonEmpty(record.TransactionID, record.OrderID), Coin: record.Currency, Status: 1, Amount: record.Amount, AmountMinor: amountMinor, InsertTimeMS: record.TransactionTimeMS, CompleteTimeMS: record.TransactionTimeMS, WalletType: record.WalletType, RawJSON: strings.TrimSpace(record.RawJSON)}, nil
}
return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance pay c2c transfer not found")
}
// c2cHistory 查询当前 API Key 账户作为买方完成的 C2C 订单BUY 表示账户收到 assetSELL 则是付出 asset。
func (c *Client) c2cHistory(ctx context.Context, account account, startTimeMS int64, endTimeMS int64, serverTimeMS int64) ([]c2cRecord, error) {
query := url.Values{}

View File

@ -209,14 +209,14 @@ func TestFindUSDTTransferFallsBackToIncomingPayC2C(t *testing.T) {
}
}
func TestFindUSDTTransferC2CUsesFiatTradeHistoryOnly(t *testing.T) {
func TestFindUSDTTransferC2CUsesPayAndAcceptsUSDC(t *testing.T) {
const orderID = "440537037787594752"
var sawC2C bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/time":
_, _ = w.Write([]byte(`{"serverTime":1784018000000}`))
case "/sapi/v1/c2c/orderMatch/listUserOrderHistory":
case "/sapi/v1/pay/transactions":
sawC2C = true
values := r.URL.Query()
signature := values.Get("signature")
@ -224,10 +224,10 @@ func TestFindUSDTTransferC2CUsesFiatTradeHistoryOnly(t *testing.T) {
if signature != signQueryString(values.Encode(), "secret-lalu") {
t.Fatalf("C2C signature mismatch: %s", values.Encode())
}
if values.Get("tradeType") != "BUY" || values.Get("page") != "1" || values.Get("rows") != "100" {
if values.Get("limit") != "100" {
t.Fatalf("C2C query mismatch: %s", values.Encode())
}
_, _ = w.Write([]byte(`{"code":"000000","success":true,"data":[{"orderNumber":"440537037787594752","tradeType":"BUY","asset":"USDT","amount":"200.00000000","orderStatus":"COMPLETED","createTime":1784017560000}]}`))
_, _ = w.Write([]byte(`{"code":"000000","success":true,"data":[{"orderType":"C2C","orderId":"440537037787594752","transactionId":"P_A22V8RE4HQS71114","transactionTime":1782977844437,"amount":"200","currency":"USDC","walletType":1}]}`))
default:
t.Fatalf("C2C lookup must not call another history endpoint: %s", r.URL.Path)
}
@ -238,7 +238,7 @@ func TestFindUSDTTransferC2CUsesFiatTradeHistoryOnly(t *testing.T) {
if err != nil {
t.Fatalf("C2C lookup failed: %v", err)
}
if !sawC2C || record.Source != "c2c_history" || record.ID != orderID || record.AmountMinor != 20000 {
if !sawC2C || record.Source != "pay_history" || record.ID != orderID || record.Coin != "USDC" || record.AmountMinor != 20000 {
t.Fatalf("C2C record mismatch: %+v", record)
}
}

View File

@ -392,6 +392,10 @@ func (s *Service) verifyBinanceTransferReceipt(ctx context.Context, command ledg
snapshot.Status = "confirmed"
snapshot.ProviderOrderID = record.TxID
snapshot.ProviderAmountMinor = record.AmountMinor
if snapshot.Chain == "c2c" && strings.TrimSpace(record.Coin) != "" {
// C2C Pay 实际收款可能是 USDT 或 USDC凭证必须保存 Binance 返回币种,不能继续伪装成表单默认 USDT。
snapshot.CurrencyCode = strings.ToUpper(strings.TrimSpace(record.Coin))
}
// Pay C2C 没有链上收款地址;只有 Deposit History 命中时才用 Binance 返回地址覆盖配置快照。
if snapshot.Chain != "c2c" && strings.TrimSpace(record.Address) != "" {
snapshot.ReceiveAddress = record.Address