问题背景

最近在做一个面向印度和东南亚市场的SaaS产品,需要接入本地支付。在对接过程中踩了不少坑,从最初的单个通道接入失败率高达40%,到现在优化后成功率稳定在85%以上,积累了一些实战经验。这篇文章记录整个过程,希望能帮到有类似需求的朋友。

第一个大坑:直接对接单一支付通道

错误做法

一开始图省事,只接入了印度的一个信用卡支付网关。心想着印度人口这么多,信用卡应该够用了吧?

// 初版代码 - 只支持信用卡
async function processPayment(orderId, amount) {
  const paymentUrl = await creditCardGateway.createOrder({
    orderId,
    amount,
    currency: 'INR'
  });
  
  window.location.href = paymentUrl;
}

结果惨痛

  • 首充成功率只有 35%

  • 用户流失严重

  • 客服被投诉电话打爆

问题分析

后来调研才发现,印度信用卡普及率只有 3-4%,大部分用户使用 UPI、Paytm、PhonePe 等本地支付方式。单一通道根本无法满足用户需求。

正确做法

必须接入多种支付方式,覆盖目标用户的主流支付习惯。

// 改进版 - 支持多种支付方式
const PAYMENT_METHODS = {
  UPI: 'upi',
  PAYTM: 'paytm',
  PHONEPE: 'phonepe',
  CREDIT_CARD: 'card',
  NET_BANKING: 'netbanking'
};

async function processPayment(orderId, amount, method) {
  const gateway = getGatewayByMethod(method);
  
  try {
    const result = await gateway.createOrder({
      orderId,
      amount,
      currency: 'INR',
      method
    });
    
    return result;
  } catch (error) {
    // 失败时自动切换备用通道
    return fallbackToAlternativeGateway(orderId, amount, method);
  }
}

效果:首充成功率提升到 70%,但还不够。

第二个坑:回调处理不当导致订单状态错乱

问题现象

用户支付成功了,但系统显示支付失败;或者反过来,支付失败但订单显示已支付。最恐怖的是有些订单被重复处理,导致用户被多次扣款。

错误代码

// 危险的回调处理
app.post('/payment/callback', async (req, res) => {
  const { orderId, status, signature } = req.body;
  
  // 致命错误1:没有验签
  // 致命错误2:没有幂等性检查
  // 致命错误3:同步处理业务逻辑
  
  if (status === 'SUCCESS') {
    await updateOrderStatus(orderId, 'paid');
    await grantUserAccess(orderId);
  }
  
  res.json({ code: 200 });
});

问题清单

  1. 没有签名验证,任何人都能伪造回调

  2. 没有幂等性控制,重复回调会重复处理

  3. 同步处理业务,响应慢容易超时

  4. 没有日志记录,出问题无法追溯

正确的实现

const crypto = require('crypto');
const Redis = require('ioredis');
const redis = new Redis();

app.post('/payment/callback', async (req, res) => {
  const startTime = Date.now();
  const { orderId, status, amount, timestamp, signature } = req.body;
  
  // 立即返回200,避免支付网关重试
  res.json({ code: 200, message: 'received' });
  
  try {
    // 1. 记录原始回调数据(重要!)
    await logPaymentCallback({
      orderId,
      rawData: req.body,
      headers: req.headers,
      ip: req.ip,
      timestamp: new Date()
    });
    
    // 2. 验证签名
    if (!verifySignature(req.body, signature)) {
      console.error('Invalid signature for order:', orderId);
      return;
    }
    
    // 3. 幂等性检查(使用Redis)
    const lockKey = `payment:lock:${orderId}`;
    const acquired = await redis.set(lockKey, '1', 'EX', 300, 'NX');
    
    if (!acquired) {
      console.log('Duplicate callback for order:', orderId);
      return;
    }
    
    // 4. 二次验证(主动查询支付网关)
    const realStatus = await queryPaymentStatus(orderId);
    if (realStatus !== status) {
      console.error('Status mismatch:', { callback: status, real: realStatus });
      return;
    }
    
    // 5. 放入消息队列异步处理
    await messageQueue.publish('payment.success', {
      orderId,
      amount,
      status,
      timestamp
    });
    
    console.log(`Callback processed in ${Date.now() - startTime}ms`);
    
  } catch (error) {
    console.error('Callback processing error:', error);
    // 发送告警
    await sendAlert('Payment callback failed', { orderId, error });
  }
});

// 验证签名
function verifySignature(data, signature) {
  const { orderId, status, amount, timestamp } = data;
  const secretKey = process.env.PAYMENT_SECRET;
  
  const signString = `${orderId}${status}${amount}${timestamp}${secretKey}`;
  const expectedSign = crypto
    .createHash('sha256')
    .update(signString)
    .digest('hex');
  
  return expectedSign === signature;
}

关键改进点

  • 先记录日志,再处理业务

  • 严格的签名验证

  • Redis分布式锁保证幂等性

  • 二次验证,避免被欺诈

  • 异步处理,快速响应

  • 完善的错误处理和告警

第三个坑:没有做支付路由优化

问题描述

接入了多个支付通道后,一开始是让用户自己选择支付方式。但发现不同通道的成功率差异巨大:

  • UPI在小额支付(<500卢比)成功率90%

  • 但大额支付(>5000卢比)成功率只有60%

  • 信用卡正好相反

  • 不同时段成功率也不同(银行维护时段)

智能路由实现

class PaymentRouter {
  constructor() {
    this.redis = new Redis();
  }
  
  // 根据多维度选择最优支付通道
  async selectBestGateway(params) {
    const { amount, userId, deviceType, location } = params;
    
    // 1. 获取各通道实时成功率
    const gateways = await this.getAvailableGateways();
    const successRates = await this.getRealtimeSuccessRates(gateways);
    
    // 2. 根据金额段选择
    let candidates = gateways.filter(g => {
      if (amount < 500) return g.supportSmallAmount;
      if (amount > 10000) return g.supportLargeAmount;
      return true;
    });
    
    // 3. 根据用户历史偏好
    const userPreference = await this.getUserPreference(userId);
    if (userPreference && candidates.includes(userPreference)) {
      candidates = [userPreference, ...candidates.filter(g => g !== userPreference)];
    }
    
    // 4. 根据设备类型
    if (deviceType === 'mobile') {
      // 移动端优先使用App唤起方式
      candidates.sort((a, b) => 
        b.supportsAppIntent - a.supportsAppIntent
      );
    }
    
    // 5. 综合评分排序
    const scored = candidates.map(gateway => ({
      gateway,
      score: this.calculateScore(gateway, {
        successRate: successRates[gateway.id],
        cost: gateway.fee,
        speed: gateway.avgProcessTime
      })
    }));
    
    scored.sort((a, b) => b.score - a.score);
    
    return scored[0].gateway;
  }
  
  calculateScore(gateway, metrics) {
    // 成功率权重最高
    return (
      metrics.successRate * 0.6 +
      (1 - metrics.cost / 100) * 0.2 +
      (1 - metrics.speed / 60) * 0.2
    );
  }
  
  // 实时监控各通道成功率
  async getRealtimeSuccessRates(gateways) {
    const rates = {};
    
    for (const gateway of gateways) {
      const key = `gateway:success_rate:${gateway.id}`;
      const data = await this.redis.get(key);
      rates[gateway.id] = data ? parseFloat(data) : 0.5;
    }
    
    return rates;
  }
  
  // 更新成功率(每次支付后调用)
  async updateSuccessRate(gatewayId, isSuccess) {
    const key = `gateway:success_rate:${gatewayId}`;
    const counterKey = `gateway:counter:${gatewayId}`;
    
    // 使用滑动窗口统计(最近1小时)
    const pipeline = this.redis.pipeline();
    pipeline.incr(`${counterKey}:total`);
    if (isSuccess) pipeline.incr(`${counterKey}:success`);
    pipeline.expire(`${counterKey}:total`, 3600);
    pipeline.expire(`${counterKey}:success`, 3600);
    
    await pipeline.exec();
    
    // 计算成功率
    const total = await this.redis.get(`${counterKey}:total`);
    const success = await this.redis.get(`${counterKey}:success`);
    const rate = success / total;
    
    await this.redis.setex(key, 3600, rate);
  }
}

效果对比

  • 之前:用户自选,平均成功率 70%

  • 之后:智能路由,平均成功率 85%

  • 成本降低:自动选择费率更低的通道,节省 15% 手续费

第四个坑:没有做好异常降级

惨痛教训

某天晚上10点,主支付通道突然挂了(印度的网络和系统真的不稳定)。因为没有自动切换机制,30分钟内损失了几千美元的订单。

实现自动降级

class PaymentService {
  constructor() {
    this.primaryGateway = new UPIGateway();
    this.fallbackGateways = [
      new PaytmGateway(),
      new CreditCardGateway()
    ];
    this.circuitBreaker = new CircuitBreaker();
  }
  
  async createPayment(order) {
    // 尝试主通道
    try {
      if (this.circuitBreaker.isOpen('primary')) {
        throw new Error('Circuit breaker open');
      }
      
      const result = await this.primaryGateway.pay(order);
      this.circuitBreaker.recordSuccess('primary');
      return result;
      
    } catch (error) {
      this.circuitBreaker.recordFailure('primary');
      console.error('Primary gateway failed:', error);
      
      // 自动切换到备用通道
      return await this.fallbackToSecondary(order);
    }
  }
  
  async fallbackToSecondary(order) {
    for (const gateway of this.fallbackGateways) {
      try {
        console.log(`Falling back to ${gateway.name}`);
        const result = await gateway.pay(order);
        
        // 记录降级事件
        await this.logFallback({
          orderId: order.id,
          from: 'primary',
          to: gateway.name,
          timestamp: new Date()
        });
        
        return result;
        
      } catch (error) {
        console.error(`${gateway.name} also failed:`, error);
        continue;
      }
    }
    
    // 所有通道都失败,返回友好提示
    throw new PaymentError('支付系统暂时维护中,请稍后重试');
  }
}

// 熔断器实现
class CircuitBreaker {
  constructor() {
    this.failures = new Map();
    this.threshold = 5; // 连续失败5次触发熔断
    this.timeout = 60000; // 熔断持续1分钟
  }
  
  isOpen(gatewayId) {
    const state = this.failures.get(gatewayId);
    if (!state) return false;
    
    if (state.count >= this.threshold) {
      if (Date.now() - state.lastFailure < this.timeout) {
        return true;
      } else {
        // 超时后重置
        this.failures.delete(gatewayId);
        return false;
      }
    }
    return false;
  }
  
  recordFailure(gatewayId) {
    const state = this.failures.get(gatewayId) || { count: 0 };
    state.count++;
    state.lastFailure = Date.now();
    this.failures.set(gatewayId, state);
    
    if (state.count >= this.threshold) {
      // 发送告警
      sendAlert(`Gateway ${gatewayId} circuit breaker opened!`);
    }
  }
  
  recordSuccess(gatewayId) {
    this.failures.delete(gatewayId);
  }
}

关键经验总结

1. 一定要接入本地主流支付方式

不同市场的支付习惯完全不同,必须做功课。推荐使用专业的支付系统搭建服务,省去繁琐的对接工作。

各市场主流支付

  • 印度:UPI(必须)、Paytm、PhonePe、NetBanking

  • 印尼:OVO、Dana、GoPay

  • 泰国:PromptPay、TrueMoney

  • 菲律宾:GCash、PayMaya

  • 巴西:PIX(必须)、Boleto

2. 回调处理三原则

1. 快速响应(<1秒)
2. 异步处理业务逻辑  
3. 完整的日志记录

3. 监控指标要全面

const METRICS = {
  // 核心指标
  successRate: '支付成功率',
  avgResponseTime: '平均响应时间',
  p99ResponseTime: 'P99响应时间',
  
  // 业务指标  
  gmv: '交易金额',
  orderCount: '订单数',
  avgOrderValue: '客单价',
  
  // 异常指标
  errorRate: '错误率',
  timeoutRate: '超时率',
  callbackDelayTime: '回调延迟'
};

4. 数据库设计要合理

-- 支付订单表
CREATE TABLE payment_orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no VARCHAR(64) UNIQUE NOT NULL,
  user_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  currency VARCHAR(3) NOT NULL,
  gateway_id VARCHAR(32) NOT NULL,
  gateway_order_no VARCHAR(128),
  status VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_status (status),
  INDEX idx_created_at (created_at)
);

-- 支付日志表(关键!)
CREATE TABLE payment_logs (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no VARCHAR(64) NOT NULL,
  event_type VARCHAR(32) NOT NULL,
  request_data JSON,
  response_data JSON,
  error_msg TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_order_no (order_no),
  INDEX idx_created_at (created_at)
);

推荐的技术栈

基于实战经验,推荐的技术方案:

后端框架

  • Node.js: Express / Koa(异步处理天然优势)

  • Java: Spring Boot(企业级稳定)

  • Go: Gin(高性能需求)

数据库

  • MySQL(订单核心数据)

  • Redis(缓存、分布式锁、计数器)

  • MongoDB(日志存储)

消息队列

  • RabbitMQ(稳定可靠)

  • Kafka(大数据量)

监控告警

  • Prometheus + Grafana(指标监控)

  • ELK Stack(日志分析)

  • Sentry(错误追踪)

关于印度支付的特别建议

印度市场有其特殊性:

  1. UPI是王道:月交易量超过100亿笔,覆盖率最高

  2. 小额高频:印度用户客单价低,但复购率高

  3. 网络不稳定:必须做好超时和重试处理

  4. 合规要求严:RBI(印度央行)监管严格,数据必须本地存储

最后的建议

如果你的团队规模不大(<10人),建议直接使用第三方支付聚合服务,不要自己从零开始对接。原因:

  1. 时间成本:自己对接一个市场需要2-3个月

  2. 维护成本:支付通道经常变更,需要持续维护

  3. 合规风险:本地法规复杂,容易踩坑

  4. 优化难度:需要大量数据才能做好路由优化

专注于业务本身,把支付交给专业的团队处理,才是明智的选择。


参考资源

这篇文章记录了我在实战中踩过的坑和总结的经验,希望能帮到正在做跨境支付的朋友。有问题欢迎留言讨论!


评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!