PAYATHON 2026

Spring Boot 集成 LINE Pay API 时 sandbox 支付失败

支付阿杰

结论

Reserve Payment API 已返回 transactionId、webUrl 和 appUrl,说明服务端认证和预留请求很可能已经通过。“Unable to process your request”通常出现在 LINE Pay 支付页面,建议优先检查以下问题:

  1. Reserve API 与支付 URL 是否属于同一环境,sandbox 和 production 不能混用。
  2. 返回的 webUrl 是否被转义、截断、重复编码,或被网关改写。
  3. 预留交易是否已经过期、取消,或支付 URL 是否被重复使用。
  4. confirmUrl、币种、金额和商户 sandbox 配置是否符合当前测试环境的要求。
  5. 是否在不支持的设备或浏览器中打开了 appUrl。
  6. sandbox 服务是否暂时异常。

仅凭支付页面上的提示,无法断定问题出在 Spring Boot 集成。先保存完整的 Reserve 响应和 LINE Pay 返回码,再用原始 webUrl 做一次最小化测试。如果请求参数没有变化,同一个请求有时成功、有时失败,才更可能是 sandbox 环境问题。

首先检查支付 URL

可以把 Reserve 响应中的 URL 直接返回给浏览器,或通过 HTTP 302 跳转。不要手工拼接查询参数,也不要再次编码整个 URL。

下面的写法会破坏 URL:

String redirectUrl = URLEncoder.encode(webUrl, StandardCharsets.UTF_8);
return "redirect:" + redirectUrl;

直接重定向更稳妥:

@GetMapping("/line-pay/start")
public ResponseEntity<Void> startPayment() {
    LinePayReserveResponse response = linePayService.reservePayment();

    URI paymentUri = URI.create(response.getWebUrl());

    return ResponseEntity.status(HttpStatus.FOUND)
            .location(paymentUri)
            .build();
}

如果使用 Spring MVC 的 redirect:,也不要重新编码完整 URL:

@GetMapping("/line-pay/start")
public String startPayment() {
    LinePayReserveResponse response = linePayService.reservePayment();
    return "redirect:" + response.getWebUrl();
}

同时检查 JSON 字段映射。不同 API 版本或 SDK 的响应结构可能不同,应以实际响应和当前版本的文档为准。例如:

@JsonIgnoreProperties(ignoreUnknown = true)
public class LinePayReserveResponse {

    private String transactionId;
    private PaymentUrl paymentUrl;

    // getters and setters

    public static class PaymentUrl {
        private String web;
        private String app;

        // getters and setters
    }
}

不要假定 v2 和 v3 的字段层级完全相同,也不要把某个非官方 SDK 的字段名直接用于其他版本。

确认没有混用 sandbox 与正式环境

Reserve、支付页面和 Confirm 必须使用同一环境和同一套商户凭证。

需要核对以下配置:

linepay.base-url=<sandbox API base URL>
linepay.channel-id=<sandbox channel id>
linepay.channel-secret=<sandbox channel secret>

支付地址必须直接使用 sandbox Reserve 响应中返回的地址。以下情况都可能导致系统无法识别交易:

  • 使用 sandbox 凭证调用 production API。
  • Reserve 请求发往 sandbox,Confirm 请求却发往 production。
  • 使用以前保存的 production webUrl 测试 sandbox 交易。
  • 更换商户账号或凭证后,仍然打开旧交易的支付 URL。

具体域名和路径应以当前 LINE Pay 官方文档及商户后台的信息为准。不要照搬旧博客中的配置,因为接口地址和测试环境规则可能发生调整。

每次测试都创建新的交易

支付 URL 通常绑定一笔具体的预留交易。旧 URL 可能因超时、取消、已经支付或重复访问而失效。

每次测试可以按以下步骤进行:

  1. 生成新的商户订单号。
  2. 重新调用 Reserve Payment API。
  3. 记录完整响应,并对密钥和签名进行脱敏。
  4. 立即在浏览器中打开新返回的 webUrl。
  5. 支付返回后,使用对应的 transactionId 调用 Confirm API。

不要重复使用订单号:

String orderId = "LINE-" + UUID.randomUUID();

还需要保存订单号与 LINE Pay 交易号的对应关系:

paymentRepository.save(
        new PaymentRecord(orderId, reserveResponse.getTransactionId())
);

调用 Confirm 时,必须使用该次 Reserve 返回的 transactionId,金额和币种也要与 Reserve 请求一致。

检查回调地址

Reserve 请求中的确认跳转地址必须能被浏览器访问。下面的地址通常只能在开发者自己的电脑上访问:

http://localhost:8080/payments/line/return

如果 LINE Pay 页面运行在远程环境或移动设备上,它无法访问你电脑上的 localhost。本地联调时,可以使用受控的 HTTPS 隧道,或把服务部署到测试服务器。例如:

https://test.example.com/payments/line/return

Spring Boot 可以分别提供成功和取消入口:

@GetMapping("/payments/line/return")
public ResponseEntity<String> paymentReturn(
        @RequestParam(required = false) String transactionId) {

    // 不应只凭浏览器参数判定付款成功。
    // 应从服务端订单记录取得可信的 transactionId,
    // 再调用 Confirm API 或查询接口核验交易。
    return ResponseEntity.ok("Payment authorization returned");
}

@GetMapping("/payments/line/cancel")
public ResponseEntity<String> paymentCancel() {
    return ResponseEntity.ok("Payment cancelled");
}

即使页面成功跳回 confirmUrl,也不能直接把订单标记为已支付。最终状态应以 Confirm API 或官方提供的交易查询结果为准。

正确选择 webUrl 和 appUrl

使用桌面浏览器联调时,一般优先选择 webUrl。appUrl 可能依赖移动设备、LINE 应用、系统的深链接处理能力和用户登录状态。

可以根据设备选择 URL,但不要修改 URL 本身:

public String selectPaymentUrl(
        LinePayReserveResponse response,
        boolean mobileDevice) {

    if (mobileDevice
            && response.getAppUrl() != null
            && !response.getAppUrl().isBlank()) {
        return response.getAppUrl();
    }

    return response.getWebUrl();
}

排查时可以先绕过前端代码。从服务端日志中复制脱敏后的原始 webUrl,立即粘贴到无痕浏览器中测试。如果原始地址能够打开,而经过前端跳转后失败,通常需要检查 URL 编码、代理跳转或前端路由处理。

检查 v3 请求签名

如果使用 v3 API,必须按照对应文档生成签名。签名通常涉及以下内容:

  • Channel Secret
  • API URI
  • 请求体
  • Nonce

签名字符串的拼接顺序、请求体字节和实际发送的 HTTP 内容必须一致。下面只展示代码结构,具体拼接规则应以当前 v3 官方文档为准:

public String createHmacSignature(String secret, String message) {
    try {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
        ));

        byte[] digest = mac.doFinal(
                message.getBytes(StandardCharsets.UTF_8)
        );

        return Base64.getEncoder().encodeToString(digest);
    } catch (GeneralSecurityException ex) {
        throw new IllegalStateException("Unable to create LINE Pay signature", ex);
    }
}

不要先对一个 JSON 字符串签名,再让序列化框架生成字段顺序或内容不同的请求体。可以先生成最终请求体,然后用同一个字符串完成签名和发送:

String requestBody = objectMapper.writeValueAsString(request);
String nonce = UUID.randomUUID().toString();

String signatureSource = buildSignatureSource(
        channelSecret,
        apiUri,
        requestBody,
        nonce
);

String signature = createHmacSignature(channelSecret, signatureSource);

不过,Reserve 既然已经成功,签名通常不是支付页面报错的首要排查对象。除非程序在解析返回码时,把业务失败误判成了成功。

不要只检查 HTTP 状态码

LINE Pay API 返回 HTTP 200,并不一定表示业务预留成功。还要检查响应中的业务返回码和消息:

LinePayReserveResponse response = linePayClient.reserve(request);

if (!response.isSuccessful()) {
    throw new IllegalStateException(
            "LINE Pay reserve failed: code="
                    + response.getReturnCode()
                    + ", message="
                    + response.getReturnMessage()
    );
}

if (response.getTransactionId() == null
        || response.getWebUrl() == null) {
    throw new IllegalStateException(
            "LINE Pay reserve response does not contain payment URL"
    );
}

isSuccessful() 的判断条件应按照当前 API 版本的官方返回码定义实现,不能假定所有版本都使用完全相同的成功码。

建议保留的排查信息

出现错误时,可以记录:

  • Reserve 请求时间。
  • API 版本和完整请求路径。
  • HTTP 状态码。
  • LINE Pay 业务返回码及返回消息。
  • 商户订单号。
  • transactionId。
  • 金额和币种。
  • confirmUrl、cancelUrl。
  • 返回支付 URL 的域名和路径。
  • 打开 URL 的设备、浏览器及时间。
  • Confirm 或交易查询接口的结果。

不要记录:

  • Channel Secret。
  • 完整认证签名。
  • 用户支付凭据。
  • 未脱敏的个人信息。

如果创建新交易、直接打开原始 webUrl,并确认环境和参数一致后,仍然稳定出现相同错误,可以把上述信息脱敏后提交给 LINE Pay 技术支持。当支付页面只显示通用错误时,服务端交易状态和平台日志通常比浏览器页面更适合用来定位问题。

备注:内容仅供参考。