PayUMoney Android 支付成功后仅获取 paymentId
结论
PayUMoney Android SDK 6.1.0 的 onActivityResult() 只返回 paymentId,通常不是代码解析出了问题。客户端回调只能用来判断支付流程是否已经结束,完整的交易信息需要由服务端获取并确认。
建议按以下流程处理:
- Android 保存自行生成的
txnid。 - PayUMoney 将支付结果 POST 到商户的
sUrl或fUrl。 - 商户服务端校验响应 Hash,并保存交易数据。
- 服务端调用 PayU 的交易查询接口,核验最终状态。
- Android 通过商户自己的订单查询接口获取完整交易结果。
不能仅凭 RESULT_OK 或客户端返回的 paymentId,就把订单标记为支付成功。
为什么只返回 paymentId
onActivityResult() 返回的是 SDK 层面的结果,不同版本的 SDK 写入 Intent 的字段并不完全相同。在 6.1.0 中,如果 Bundle 里只有 paymentId,Android 端遍历 Bundle 也拿不到其他字段。
下面的代码只能打印 SDK 已经返回的数据,不能让 SDK 生成额外的交易字段:
Bundle bundle = data.getExtras();
if (bundle != null) {
for (String key : bundle.keySet()) {
Log.d("PayU", key + ": " + bundle.get(key));
}
}
因此,不能依赖 data.getExtras() 获取完整交易详情。这些信息应该来自服务端回调和交易查询接口。
正确的处理流程
1. 创建支付前,由服务端生成交易参数
txnid、支付 Hash,以及后续调用查询接口所需的 Hash,都应该在服务端生成。Salt 不能放进 Android 应用,否则攻击者反编译 APK 后可能获得商户密钥。
Android 可以先向自己的服务端申请支付参数:
{
"txnid": "ORDER_1720000000000",
"amount": "1.00",
"key": "merchant_key",
"hash": "generated_on_server",
"surl": "https://example.com/payment/payu/success",
"furl": "https://example.com/payment/payu/failure"
}
应用拿到这些参数后,再启动 PayUMoney SDK。
2. 在 sUrl 和 fUrl 接收支付结果
PayU 通常会向 sUrl 或 fUrl 发送表单形式的 POST 请求。服务端需要读取并保存收到的字段,例如:
txnidmihpayidstatusunmappedstatusamountproductinfofirstnameemailbank_ref_numerrorerror_Messagehash
具体有哪些字段,取决于账户接入方式和 PayU 的实际返回内容,不能假定每次都会包含全部字段。
下面是一个简化的 Spring Boot 示例:
@PostMapping(
value = "/payment/payu/success",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<String> paymentSuccess(
@RequestParam Map<String, String> params) {
String txnId = params.get("txnid");
String paymentId = params.get("mihpayid");
String status = params.get("status");
String amount = params.get("amount");
String responseHash = params.get("hash");
boolean validHash = payuHashService.verifyResponseHash(params);
if (!validHash) {
return ResponseEntity.badRequest().body("Invalid response hash");
}
// 这里只记录回调,不要仅凭 status 直接完成订单。
paymentRepository.saveCallback(
txnId,
paymentId,
status,
amount,
params
);
// 随后由服务端查询 PayU,确认最终交易状态。
paymentVerificationService.verifyAsync(txnId);
return ResponseEntity.ok("Payment response received");
}
失败地址同样需要接收并校验响应:
@PostMapping(
value = "/payment/payu/failure",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<String> paymentFailure(
@RequestParam Map<String, String> params) {
if (!payuHashService.verifyResponseHash(params)) {
return ResponseEntity.badRequest().body("Invalid response hash");
}
paymentRepository.saveCallback(
params.get("txnid"),
params.get("mihpayid"),
params.get("status"),
params.get("amount"),
params
);
return ResponseEntity.ok("Failure response received");
}
响应 Hash 的拼接顺序必须遵循当前 PayUMoney 或 PayU 商户账户及 SDK 版本对应的官方规则,不能自行猜测。
3. 服务端主动查询交易状态
收到回调后,服务端应该使用原始 txnid 调用 PayU 的交易验证接口,例如当前接入版本中的 verify_payment 接口。查询接口通常需要商户 Key、命令、交易号,以及使用 Salt 计算出的 Hash。
常见的请求逻辑如下:
command = verify_payment
var1 = 原始 txnid
hash = SHA-512(key|command|var1|salt)
Java Hash 计算示例:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public final class PayUHash {
public static String sha512(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] bytes = digest.digest(
value.getBytes(StandardCharsets.UTF_8)
);
StringBuilder result = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
result.append(String.format("%02x", b & 0xff));
}
return result.toString();
} catch (Exception e) {
throw new IllegalStateException("Unable to calculate SHA-512", e);
}
}
public static String verificationHash(
String key,
String command,
String txnId,
String salt) {
return sha512(key + "|" + command + "|" + txnId + "|" + salt);
}
}
接口地址、请求格式和 Hash 规则可能会因 PayUMoney 产品线及账户接入方式而异。SDK 6.1.0 是较旧的版本,应以商户账户对应的 PayU 文档或后台提供的接口为准,尤其不能混用 PayUMoney 旧接口和新版 PayU India 接口。
Android 端如何取得最终详情
Android 收到 SDK 回调后,可以把 paymentId 作为流程标识,但仍需通过自己的服务端查询订单:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode !=
PayUmoneySdkInitilizer.PAYU_SDK_PAYMENT_REQUEST_CODE) {
return;
}
if (resultCode == RESULT_OK) {
String paymentId = null;
if (data != null) {
paymentId = data.getStringExtra("paymentId");
}
// 向自己的服务端查询 txnId 对应的最终状态。
queryOrderFromBackend(txnId, paymentId);
} else if (resultCode ==
PayUmoneySdkInitilizer.RESULT_FAILED) {
// 即使客户端显示失败,也可让服务端再次核验,
// 以处理支付已完成但客户端回调异常的情况。
queryOrderFromBackend(txnId, null);
}
}
服务端返回给 Android 的数据可以整理成以下格式:
{
"txnid": "ORDER_1720000000000",
"paymentId": "123456789",
"status": "success",
"amount": "1.00",
"bankReferenceNumber": "987654321",
"verified": true
}
只有服务端完成 Hash 校验,并通过交易查询接口确认金额、交易号和状态全部一致后,应用才能显示”支付成功”。
当前代码还需要修正的地方
status 判断条件错误
原代码是:
if (status != null || status.equals("1")) {
这里使用 || 会带来两个问题:
- 只要
status不为null,即使它不是"1",代码也会进入成功分支。 - 当
status为null时,代码仍可能执行status.equals("1"),从而触发NullPointerException。
应改为:
if ("1".equals(status)) {
或者:
if (status != null && status.equals("1")) {
第一种写法更简洁,也能避免空指针。
不要在客户端请求测试 Hash 服务
下面的地址只适用于示例或测试:
https://test.payumoney.com/payment/op/calculateHashForTest
生产环境不应依赖公开的测试 Hash 服务。支付 Hash 应由商户自己的服务端计算,Salt 也必须只保存在服务端。
测试和生产配置必须一致
以下配置必须来自同一个环境:
- Merchant Key
- Merchant ID
- Salt
- Hash 生成接口
- 支付环境
- 交易查询接口
isDebug设置
如果测试 Key 配合生产接口使用,或者生产 Key 配合测试接口使用,Hash 校验、支付请求或交易查询都可能失败。
注意事项
RESULT_OK只表示 SDK 流程成功返回,不代表订单已经通过服务端核验。- 不要信任 Android 上传的金额、支付状态或商品信息,服务端应将其与订单记录进行比较。
- 同一个
txnid不能重复用于多次独立支付。 - 回调可能重复到达,因此服务端必须进行幂等处理。
- 网络中断时,支付可能已经完成,但 Android 没有收到正常结果,所以系统必须支持按
txnid补查。 - 在部分响应中,
paymentId可能对应mihpayid,具体映射应以实际接口响应为准。 - SDK 6.1.0 已经较旧。如果准备升级 SDK,需要同时核对回调字段、Hash 规则和交易查询接口,不能只替换依赖版本。
Android 负责发起支付并展示结果。服务端负责接收回调、校验 Hash、查询交易详情,并决定订单的最终状态。即使 SDK 只返回 paymentId,按照这一流程也能可靠地获取并保存完整的 Transaction Details。
备注:内容仅供参考。
