如何在 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 可以回传结果。通常需要按以下流程处理:
- 由服务端创建唯一订单号。
- 将订单号写入
tr,同时保存订单与金额的对应关系。 - 用户通过短信中的 UPI URI 完成支付。
- 服务端调用收单银行、PSP 或支付网关的交易状态查询 API。
- 如果服务商支持 webhook,则接收并验证其签名回调。
- 校验订单号、商户 VPA、金额、币种、交易号和最终状态。
- 服务端核验成功后,再更新订单并交付商品或服务。
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 无法可靠地自动确认支付结果。
备注:内容仅供参考。
