PAYATHON 2026

如何在 PhoneGap 应用中实现 Payment gateway?

支付阿杰

结论

PhoneGap 应用可以接入支付网关,但支付密钥、可信订单的生成和支付结果的确认都不应放在应用端处理。建议采用以下流程:

  1. PhoneGap 请求业务后端创建订单。
  2. 后端调用支付网关 API,创建支付会话或生成支付参数。
  3. 应用通过支付网关官方 SDK、PhoneGap/Cordova 插件或系统浏览器完成付款。
  4. 支付网关通过服务端回调通知后端。
  5. 后端验签并查询支付状态,确认付款成功后再更新订单。

代码和参数需要根据支付服务商调整。不同网关可能使用 Hosted Checkout、原生 SDK 或浏览器跳转等接入方式。

为什么需要服务端

PhoneGap 本质上是运行在 WebView 中的 HTML、CSS 和 JavaScript 应用。安装包内的 JavaScript 可以被查看和分析,因此不能在应用中保存商户密钥、API Secret、签名私钥等敏感信息。

例如,下面的代码并不安全:

const secretKey = "YOUR_PAYMENT_SECRET";

攻击者取得密钥后,可能伪造请求或操作商户接口。客户端提交的支付金额同样不能直接信任,后端需要根据商品编号、优惠规则和用户订单重新计算实际金额。

推荐的接入流程

1. 选择支付网关及支付方式

先确认支付服务商是否提供以下能力中的一种或多种:

  • 托管收银台,即 Hosted Checkout
  • 适用于 Cordova/PhoneGap 的官方插件
  • Android 和 iOS 原生 SDK
  • 支持移动端跳转的网页支付
  • 服务端订单查询、退款和异步通知接口

如果支付服务商没有维护 PhoneGap 插件,可以优先考虑托管收银台。这样可以减少应用直接接触银行卡数据的范围,维护成本通常也低于自行封装原生 SDK。

除非团队具备相应的支付安全和合规能力,否则不要在普通 WebView 中自行制作银行卡表单,再将卡号发送到自己的服务器。

2. 由应用请求后端创建订单

PhoneGap 只向业务后端提交商品和订单信息,不提交商户密钥。

async function createPayment(productId) {
  const response = await fetch("https://api.example.com/payments", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + getAccessToken()
    },
    body: JSON.stringify({
      productId: productId,
      quantity: 1
    })
  });

  if (!response.ok) {
    throw new Error("创建支付订单失败");
  }

  return response.json();
}

后端需要完成以下工作:

  • 验证登录用户
  • 检查商品是否有效
  • 根据服务端数据计算金额
  • 创建唯一的商户订单号
  • 调用支付网关创建支付会话
  • 保存商户订单号与网关订单号的对应关系
  • 向应用返回支付地址或客户端令牌

最终支付金额不能由应用自行决定。

3. 后端调用支付网关

下面是一个抽象的 Node.js 示例。接口路径、字段名称和签名方式仅用于说明整体结构,接入时必须替换为支付网关官方 API 中的实际配置。

import express from "express";

const app = express();
app.use(express.json());

app.post("/payments", async (req, res) => {
  const user = await authenticate(req);
  const product = await findProduct(req.body.productId);

  if (!user || !product) {
    return res.status(400).json({ error: "Invalid request" });
  }

  const order = await createLocalOrder({
    userId: user.id,
    productId: product.id,
    amount: product.price,
    currency: "CNY",
    status: "PENDING"
  });

  const gatewayResponse = await fetch(
    "https://payment-provider.example/api/checkout-sessions",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.PAYMENT_SECRET}`
      },
      body: JSON.stringify({
        orderId: order.id,
        amount: order.amount,
        currency: order.currency,
        returnUrl: "myapp://payment/return",
        notifyUrl: "https://api.example.com/payment-webhook"
      })
    }
  );

  if (!gatewayResponse.ok) {
    return res.status(502).json({ error: "Payment gateway error" });
  }

  const session = await gatewayResponse.json();

  await saveGatewayOrderId(order.id, session.id);

  res.json({
    orderId: order.id,
    checkoutUrl: session.checkoutUrl
  });
});

生产环境还要处理超时、重试、日志脱敏和幂等请求。密钥应保存在服务器的环境变量或密钥管理系统中。

4. 在应用中打开支付页面

如果支付网关提供托管收银台,可以通过系统浏览器或受支持的 Cordova 插件打开支付地址。与任意内嵌 WebView 相比,系统浏览器通常更适合处理登录、3-D Secure 和第三方应用跳转。

下面以 cordova-plugin-inappbrowser 的调用方式为例:

async function startPayment(productId) {
  try {
    const payment = await createPayment(productId);

    const browser = cordova.InAppBrowser.open(
      payment.checkoutUrl,
      "_system",
      "location=yes"
    );

    return {
      orderId: payment.orderId,
      browser: browser
    };
  } catch (error) {
    console.error(error);
    alert("暂时无法发起支付,请稍后重试");
  }
}

插件名称和可用参数可能因项目环境而异,应以所选插件和支付服务商的当前文档为准。如果支付页面禁止嵌入,或者需要跳转到外部支付应用,应使用 _system 在系统浏览器中打开。

5. 配置支付完成后的返回地址

支付结束后,可以通过自定义 URL Scheme 或 Universal Links/App Links 返回应用,例如:

myapp://payment/return?orderId=ORDER_ID

应用收到返回链接后,不能直接根据查询参数中的 status=success 判定支付成功。返回链接可能被伪造,用户关闭页面、网络中断或跳转失败也会使客户端状态变得不可靠。

应用应使用订单号向自己的后端查询:

async function queryPaymentStatus(orderId) {
  const response = await fetch(
    `https://api.example.com/payments/${encodeURIComponent(orderId)}`,
    {
      headers: {
        "Authorization": "Bearer " + getAccessToken()
      }
    }
  );

  if (!response.ok) {
    throw new Error("查询支付状态失败");
  }

  return response.json();
}

服务端回调与验签

支付网关通常会向 notifyUrl 发送异步通知。后端收到通知后,需要按照网关规则验证签名,并检查以下内容:

  • 商户订单号是否存在
  • 网关订单号是否匹配
  • 商户号是否正确
  • 支付金额和币种是否一致
  • 通知是否已经处理
  • 网关返回的交易状态是否有效

示意代码如下:

app.post("/payment-webhook", express.raw({ type: "*/*" }), async (req, res) => {
  const signature = req.headers["x-payment-signature"];

  const verified = verifyGatewaySignature(
    req.body,
    signature,
    process.env.PAYMENT_WEBHOOK_SECRET
  );

  if (!verified) {
    return res.status(400).send("invalid signature");
  }

  const event = parseGatewayEvent(req.body);

  if (event.type === "payment.succeeded") {
    await markOrderPaidOnce({
      orderId: event.orderId,
      gatewayTransactionId: event.transactionId,
      amount: event.amount,
      currency: event.currency
    });
  }

  res.status(200).send("ok");
});

verifyGatewaySignature、事件名称和请求头都要按照实际支付网关实现。部分网关还建议后端收到通知后,主动调用订单查询接口再次确认。

订单更新必须具有幂等性。同一条成功通知即使多次到达,也只能触发一次发货、充值或权益发放。

原生 SDK 的接入方式

如果支付服务商只提供 Android 和 iOS SDK,可以采用以下方式之一:

  • 使用支付服务商官方 Cordova 插件
  • 使用维护可靠的第三方 Cordova 插件
  • 编写自定义 Cordova 插件,分别封装 Android 和 iOS SDK

JavaScript 层通常只负责调用插件:

cordova.exec(
  function onSuccess(result) {
    console.log("支付流程已返回", result);
    queryPaymentStatus(currentOrderId);
  },
  function onError(error) {
    console.error("支付调用失败", error);
  },
  "PaymentPlugin",
  "pay",
  [paymentParameters]
);

即使原生 SDK 返回”成功”,应用仍要向后端查询最终状态。插件回调只能表示客户端支付流程已经返回,不能代替支付网关的服务端通知和验签。

常见问题

跨域请求失败

PhoneGap 请求业务后端时,可能受到 Content Security Policy、Cordova 白名单配置或服务端 CORS 策略的影响。配置时应只允许可信域名,并在服务端正确设置允许的请求来源、方法和请求头。

不要为了方便而允许所有来源访问带有用户凭证的接口。

支付完成后没有回到应用

需要检查以下配置:

  • 自定义 URL Scheme 是否正确注册
  • Android App Links 或 iOS Universal Links 是否配置完整
  • 支付网关是否允许所填写的返回地址
  • 应用能否正确处理冷启动和后台唤醒场景

即使没有自动返回,用户重新进入订单页后,也应该能够查询到真实的支付状态。

用户重复点击支付按钮

客户端可以暂时禁用按钮,后端则要通过幂等键或复用未支付订单,避免重复创建订单:

let paying = false;

async function handlePay(productId) {
  if (paying) {
    return;
  }

  paying = true;

  try {
    await startPayment(productId);
  } finally {
    paying = false;
  }
}

客户端防重复只能改善操作体验。真正的重复订单防护仍要由后端完成。

安全与上线注意事项

  • 应用与后端、后端与网关之间的通信都应使用 HTTPS。
  • 不要在 PhoneGap 代码、配置文件或安装包中保存支付密钥。
  • 不要信任客户端提交的金额、折扣、币种和支付状态。
  • 回调接口必须验签,同时校验订单金额和币种。
  • 日志中不要记录卡号、安全码、完整令牌或签名密钥。
  • 分别处理支付成功、失败、取消、处理中和超时状态。
  • 先在支付网关提供的沙箱环境中联调,再切换到生产配置。
  • 明确隔离沙箱与生产环境的密钥、接口地址和回调地址。
  • 退款应由后端调用网关接口,并保存退款记录。
  • 涉及银行卡数据时,需要评估 PCI DSS 等合规要求。具体义务取决于业务地区、支付方式和接入模式。

PhoneGap 负责发起支付和展示结果,业务后端负责订单、金额、密钥、验签和最终状态。守住这一边界后,接入其他支付网关时,通常只需替换后端的网关调用和客户端的支付入口。

备注:内容仅供参考。