PAYATHON 2026

如何在 Java 中获取 UPI Hyperlink 支付响应

支付阿杰

明确结论

不会。UPI URI 中的 url 参数只是交易参考 URL,并不是支付完成后的 HTTP callback 或 webhook。支付完成后,UPI 应用通常不会自动向该地址发送 GET 或 POST 请求,所以无论将 Spring Controller 改成 @PostMapping 还是 @GetMapping,都解决不了这个问题。

如果用户从短信或浏览器打开 upi://pay,也没有商户 App 接收 Android Activity 的返回结果。要可靠获取支付状态,需要接入收单银行、PSP 或支付网关提供的服务端支付查询 API 或 webhook,并以服务端核验结果为准。

为什么 Controller 没有收到请求

下面这个参数:

url=https://test.com/payment/orderId=123456

只是 UPI 支付请求中的一项交易参考信息。它不会让 BHIM 或其他 UPI App 在支付完成后自动访问:

https://test.com/payment/orderId=123456

这个 URL 本身还存在路径设计问题:

/payment/orderId=123456

这里的 orderId=123456 是路径的一部分,并不是名为 orderId 的查询参数。即使有人实际访问了该地址,下面这行代码也未必能取得值:

request.getParameter("orderId");

如果要使用查询参数,URL 应写成:

https://test.com/payment?orderId=123456

也可以将 Controller 定义为接收路径变量:

@GetMapping("/payment/orderId={orderId}")
public ModelAndView payment(@PathVariable String orderId) {
    // ...
}

这些修改只能让 URL 与 Controller 正确匹配,无法让 UPI App 自动发起 callback。

方案一:通过 Android App 发起支付并接收返回结果

如果支付由自己的 Android App 发起,可以通过 Activity Result API 启动 UPI App,并接收其返回的 Intent 数据。

public class PaymentActivity extends AppCompatActivity {

    private final ActivityResultLauncher<Intent> upiPaymentLauncher =
            registerForActivityResult(
                    new ActivityResultContracts.StartActivityForResult(),
                    result -> {
                        if (result.getResultCode() == Activity.RESULT_OK
                                && result.getData() != null) {

                            String response = result.getData().getStringExtra("response");

                            if (response == null) {
                                response = result.getData().getDataString();
                            }

                            handleUpiResponse(response);
                        } else {
                            handleUpiResponse(null);
                        }
                    });

    private void startUpiPayment() {
        Uri uri = new Uri.Builder()
                .scheme("upi")
                .authority("pay")
                .appendQueryParameter("pa", "abc@upi")
                .appendQueryParameter("pn", "payeeName")
                .appendQueryParameter("tr", "1234")
                .appendQueryParameter("tn", "Pay to payeeName")
                .appendQueryParameter("am", "1.00")
                .appendQueryParameter("cu", "INR")
                .appendQueryParameter(
                        "url",
                        "https://test.com/payment?orderId=123456")
                .build();

        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        Intent chooser = Intent.createChooser(intent, "Pay with");

        try {
            upiPaymentLauncher.launch(chooser);
        } catch (ActivityNotFoundException ex) {
            // 设备上没有可处理 UPI URI 的应用
        }
    }

    private void handleUpiResponse(String response) {
        if (response == null || response.isBlank()) {
            // 用户取消、应用未返回数据,或者返回格式不兼容
            return;
        }

        Map<String, String> values = parseResponse(response);

        String status = values.get("status");
        String txnId = values.get("txnid");
        String responseCode = values.get("responsecode");
        String approvalRefNo = values.get("approvalrefno");
        String txnRef = values.get("txnref");

        // 这里的结果只能用于界面提示。
        // 最终支付状态必须由服务端向 PSP、收单机构或支付网关核验。
    }

    private Map<String, String> parseResponse(String response) {
        Map<String, String> values = new HashMap<>();

        for (String part : response.split("&")) {
            String[] pair = part.split("=", 2);
            if (pair.length == 2) {
                String key = URLDecoder.decode(
                        pair[0], StandardCharsets.UTF_8).toLowerCase(Locale.ROOT);
                String value = URLDecoder.decode(
                        pair[1], StandardCharsets.UTF_8);
                values.put(key, value);
            }
        }

        return values;
    }
}

不同 UPI App 的返回方式以及字段大小写可能并不一致。有些应用会返回空数据,用户也可能在回到商户 App 前关闭页面。因此,不能只依赖 Activity 返回值判断支付结果。

客户端返回的 SUCCESS 也不能作为发货、充值或记账的唯一依据。客户端数据可能被伪造,支付还可能处于处理中,之后延迟成功,或者发生状态反转。

方案二:通过服务端查询或 webhook 确认支付状态

短信支付场景中没有 Android Activity 可以回传结果。通常需要按以下流程处理:

  1. 由服务端创建唯一订单号。
  2. 将订单号写入 tr,同时保存订单与金额的对应关系。
  3. 用户通过短信中的 UPI URI 完成支付。
  4. 服务端调用收单银行、PSP 或支付网关的交易状态查询 API。
  5. 如果服务商支持 webhook,则接收并验证其签名回调。
  6. 校验订单号、商户 VPA、金额、币种、交易号和最终状态。
  7. 服务端核验成功后,再更新订单并交付商品或服务。

API、签名方式和回调字段由接入的银行、PSP 或支付网关决定。仅靠通用的 upi://pay URI,无法获得服务端通知。

Spring webhook 可以采用下面的形式,但前提是支付服务商已经配置并会实际调用这个地址:

@RestController
@RequestMapping("/payments")
public class PaymentWebhookController {

    @PostMapping("/upi/webhook")
    public ResponseEntity<Void> receiveWebhook(
            @RequestHeader Map<String, String> headers,
            @RequestBody String body) {

        // 1. 按支付服务商的文档验证签名
        // 2. 解析订单号、金额、交易号和状态
        // 3. 防止重复通知,保证处理幂等
        // 4. 必要时主动调用交易查询 API 二次确认
        // 5. 更新订单状态

        return ResponseEntity.ok().build();
    }
}

不能直接相信请求中的 Status=SUCCESS。必须按照服务商的规则验证签名,必要时再调用状态查询 API 确认。

URI 的正确构造方式

不要手工拼接包含 URL 的 UPI URI。嵌套 URL 中的 ?、&、= 等字符需要正确编码。Java 中可以使用 Uri.Builder,也可以单独编码参考 URL:

String referenceUrl =
        URLEncoder.encode(
                "https://test.com/payment?orderId=123456",
                StandardCharsets.UTF_8);

String upiUri =
        "upi://pay"
        + "?pa=abc%40upi"
        + "&pn=payeeName"
        + "&tr=123456"
        + "&tn=Pay%20to%20payeeName"
        + "&am=1.00"
        + "&cu=INR"
        + "&url=" + referenceUrl;

如果通过短信发送 URI,最终字符串可能是:

upi://pay?pa=abc%40upi&pn=payeeName&tr=123456&tn=Pay%20to%20payeeName&am=1.00&cu=INR&url=https%3A%2F%2Ftest.com%2Fpayment%3ForderId%3D123456

注意事项

  • url 是参考 URL,不是 webhook 地址。
  • 从短信打开 UPI URI 后,Java/Spring 服务端不会自动收到支付结果。
  • Android Activity 返回值只能用于即时界面提示,不能作为最终支付凭证。
  • tr 应使用服务端生成的唯一交易参考号,不能使用可能重复的固定值。
  • 不能假定所有 UPI App 返回的参数名称及大小写都相同。
  • 支付状态可能是 SUCCESS、FAILURE、SUBMITTED、PENDING 等,具体取值以接入机构的文档为准。
  • webhook 处理必须验证签名、使用 HTTPS、核对金额,并保证幂等。
  • 如果银行、PSP 或支付网关没有提供交易查询能力,仅使用通用 UPI hyperlink 无法可靠地自动确认支付结果。

备注:内容仅供参考。