PAYATHON 2026

如何在 Android app 中集成 LINE Pay

支付老李

结论

LINE Pay 通常不能像普通 Android SDK 那样直接集成到 App。支付应按以下流程完成:

  1. Android App 向业务服务器提交支付请求。
  2. 业务服务器调用 LINE Pay 的 Request API 创建交易。
  3. Android App 打开 LINE Pay 返回的付款页面或 LINE Pay App。
  4. 用户完成支付授权后,LINE Pay 跳转到预先配置的回调地址。
  5. 业务服务器调用 Confirm API 确认扣款。
  6. Android App 向业务服务器查询最终支付结果。

Channel ID、Channel Secret、签名逻辑和支付确认操作都必须放在服务器端,不能写入 APK。官方 Merchant API 文档主要面向服务端接口,因此一般不会提供完整的 Android 原生集成章节。

推荐的支付流程

Android App
    │
    │ 1. 提交订单
    ▼
业务服务器
    │
    │ 2. 调用 LINE Pay Request API
    ▼
LINE Pay
    │
    │ 3. 返回 paymentUrl 和 transactionId
    ▼
Android App
    │
    │ 4. 打开 paymentUrl,用户授权付款
    ▼
LINE Pay
    │
    │ 5. 跳转 confirmUrl 或 cancelUrl
    ▼
业务服务器
    │
    │ 6. 调用 Confirm API
    ▼
Android App
       7. 查询订单状态

Android 收到成功跳转后,不要立即把订单标记为已支付。这个跳转通常只表示用户完成了授权流程。最终是否支付成功,应以服务器调用 Confirm API 后收到的响应为准。

服务端创建支付请求

API 域名、路径、签名格式和请求字段可能随泰国商户平台、API 版本及账户配置而变化,应以客户商户账号对应的 LINE Pay Merchant 文档为准。下面的代码只用于说明各端的职责,不应直接作为生产环境的固定接口定义。

import express from "express";
import crypto from "crypto";

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

const LINE_PAY_CHANNEL_ID = process.env.LINE_PAY_CHANNEL_ID;
const LINE_PAY_CHANNEL_SECRET = process.env.LINE_PAY_CHANNEL_SECRET;
const LINE_PAY_API_BASE = process.env.LINE_PAY_API_BASE;

function createSignature(secret, message) {
  return crypto
    .createHmac("sha256", secret)
    .update(message)
    .digest("base64");
}

app.post("/api/orders/:orderId/payments/line-pay", async (req, res) => {
  const order = await loadOrderFromDatabase(req.params.orderId);

  if (!order || order.status !== "UNPAID") {
    return res.status(400).json({ message: "Order cannot be paid" });
  }

  const body = {
    amount: order.amount,
    currency: order.currency,
    orderId: order.id,
    packages: [
      {
        id: order.id,
        amount: order.amount,
        name: order.description,
        products: [
          {
            id: order.productId,
            name: order.description,
            quantity: 1,
            price: order.amount
          }
        ]
      }
    ],
    redirectUrls: {
      confirmUrl: `https://api.example.com/payments/line-pay/return?orderId=${encodeURIComponent(order.id)}`,
      cancelUrl: `https://api.example.com/payments/line-pay/cancel?orderId=${encodeURIComponent(order.id)}`
    }
  };

  const path = "/replace-with-request-api-path";
  const nonce = crypto.randomUUID();
  const requestBody = JSON.stringify(body);

  // 签名拼接顺序必须按照当前 Merchant API 文档实现。
  const signature = createSignature(
    LINE_PAY_CHANNEL_SECRET,
    `${LINE_PAY_CHANNEL_SECRET}${path}${requestBody}${nonce}`
  );

  const response = await fetch(`${LINE_PAY_API_BASE}${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
      "X-LINE-Authorization-Nonce": nonce,
      "X-LINE-Authorization": signature
    },
    body: requestBody
  });

  const result = await response.json();

  if (!response.ok) {
    return res.status(502).json({ message: "Unable to create payment" });
  }

  const transactionId = extractTransactionId(result);
  const paymentUrl = extractPaymentUrl(result);

  await savePendingPayment({
    orderId: order.id,
    transactionId,
    amount: order.amount,
    currency: order.currency
  });

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

示例中的请求路径、请求头和签名字符串仅用于说明结构。开发时尤其要核对:

  • 泰国商户使用的 API Host。
  • Sandbox 与 Production 各自的地址。
  • Request API 的实际路径。
  • 签名算法和字符串拼接顺序。
  • 返回结果中付款 URL 对应的字段名。
  • 泰国商户支持的币种和金额格式。

Android 打开付款页面

服务器返回 paymentUrl 后,Android 可以通过外部浏览器或 Custom Tabs 打开。与直接使用 WebView 相比,Custom Tabs 通常能更稳定地处理跳转、Cookie 和外部 App 唤起。

import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent

fun openLinePay(paymentUrl: String) {
    val uri = Uri.parse(paymentUrl)

    require(uri.scheme == "https") {
        "Only HTTPS payment URLs are allowed"
    }

    CustomTabsIntent.Builder()
        .setShowTitle(true)
        .build()
        .launchUrl(this, uri)
}

如果项目没有引入 Custom Tabs 依赖,也可以让系统浏览器处理:

import android.content.Intent
import android.net.Uri

fun openLinePayInBrowser(paymentUrl: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl))
    startActivity(intent)
}

付款 URL 必须由可信的业务服务器返回。建议同时校验协议和主机名,避免把任意外部 URL 直接交给 Intent。

处理返回 App 的跳转

一种较稳妥的方案是把 LINE Pay 的 confirmUrl 指向业务服务器。服务器完成支付确认后,再展示结果页面,并通过 Android App Link 返回 App,例如:

https://pay.example.com/line-pay/result?orderId=ORDER_ID

在 AndroidManifest.xml 中配置 App Link:

<activity
    android:name=".PaymentResultActivity"
    android:exported="true">

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:scheme="https"
            android:host="pay.example.com"
            android:pathPrefix="/line-pay/result" />
    </intent-filter>
</activity>

在 Activity 中读取订单编号:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val orderId = intent?.data?.getQueryParameter("orderId")
    if (orderId != null) {
        loadPaymentStatus(orderId)
    } else {
        showPaymentError()
    }
}

不要因为回调 URL 中出现 status=SUCCESS 就直接展示支付成功。App 应携带当前登录用户的凭证,向业务服务器查询订单状态:

interface PaymentApi {
    @GET("api/orders/{orderId}/payment-status")
    suspend fun getPaymentStatus(
        @Path("orderId") orderId: String
    ): PaymentStatusResponse
}

data class PaymentStatusResponse(
    val orderId: String,
    val status: String
)

服务器可以返回以下状态:

PENDING
PAID
CANCELLED
FAILED
EXPIRED

服务端确认支付

LINE Pay 跳转到 confirmUrl 后,业务服务器应从回调参数中取得交易标识,将其与创建支付请求时保存的数据进行匹配,再调用 Confirm API。

app.get("/payments/line-pay/return", async (req, res) => {
  const orderId = String(req.query.orderId || "");
  const transactionId = String(req.query.transactionId || "");

  const payment = await loadPendingPayment(orderId);

  if (!payment || payment.transactionId !== transactionId) {
    return res.status(400).send("Invalid payment callback");
  }

  try {
    const confirmResult = await confirmLinePayPayment({
      transactionId: payment.transactionId,
      amount: payment.amount,
      currency: payment.currency
    });

    if (isConfirmed(confirmResult)) {
      await markOrderPaidAtomically(orderId, payment.transactionId);
    } else {
      await markPaymentFailed(orderId, confirmResult);
    }
  } catch (error) {
    // 保持 PENDING,之后可通过查询接口或补偿任务核对交易状态。
    await recordConfirmationError(orderId, error);
  }

  res.redirect(
    `https://pay.example.com/line-pay/result?orderId=${encodeURIComponent(orderId)}`
  );
});

confirmLinePayPayment() 应按照对应版本的 Merchant API 文档发送金额、币种和签名。确认支付前,必须使用数据库中保存的金额,不能相信 Android 或回调 URL 提交的金额。

关键注意事项

不要把商户密钥放进 Android

APK 可以被反编译。混淆、NDK 或加密字符串都无法安全地保存服务端商户密钥。密钥一旦泄露,攻击者可能伪造商户请求。

Android 只能保存业务服务器返回的短期数据,例如订单编号和付款 URL。

确保确认操作幂等

回调页面可能被刷新,LINE Pay 或网络层也可能重试请求。服务器需要保证同一个 transactionId 只能让订单入账一次。

可以在数据库中为 transactionId 添加唯一约束,并通过事务更新支付记录和订单状态。

不要相信客户端金额

商品价格、优惠、税费、币种和最终应付金额都应由服务器计算。Android 只负责提交商品或订单标识,无权决定最终金额。

正确处理用户中断

支付过程中可能出现这些情况:

  • 用户取消付款。
  • 用户关闭浏览器后返回 App。
  • 用户完成付款,但没有返回 App。
  • LINE Pay 已完成授权,Confirm API 却暂时超时。
  • App 在支付期间被系统回收。

因此,App 每次恢复支付页面时,都应向服务端查询订单状态。服务端也应提供定时补偿或交易查询机制,用于处理长时间处于 PENDING 状态的记录。

谨慎使用 WebView

除非对应的商户文档明确支持,否则不建议把 LINE Pay 付款页固定加载到 WebView。支付期间可能需要唤起外部 App、完成身份验证、共享 Cookie 或进行多级重定向,普通 WebView 容易遇到兼容性问题。

核实泰国地区配置

LINE Pay 的产品能力、API Host、币种、测试环境和商户开通流程可能因地区和商户合同而异。开发前,应让客户提供:

  • 已启用 API 权限的泰国 LINE Pay 商户账号。
  • 对应地区的 Merchant API 文档。
  • Sandbox 凭据和测试账号。
  • Production 凭据的申请方式及上线审核要求。
  • 已登记的回调域名和允许使用的 Redirect URL。

如果客户只有普通 LINE 账号或收款二维码,没有 Merchant API 凭据,就无法按上述流程完成支付网关集成。客户需要先联系当地 LINE Pay 商户支持渠道,确认并开通相应产品。

备注:内容仅供参考。