Android 集成 Instamojo SDK 提示 Transaction ID not unique
结论
问题不在 PHP 的唯一 ID 生成函数,而是 Android 端没有使用后端返回的 transaction_id。
后端已经返回了交易 ID:
$response['transaction_id'] = $this->getGUIDnoHash();
但 Android 收到响应后只读取 access_token,接着又调用本地的 createTransactionID() 生成了一个 ID:
accessToken = response.getString("access_token");
transactionID = createTransactionID();
所以,最终传给 Order 的并不是 PHP 生成的 ID。如果 createTransactionID() 返回固定值、生成了重复值,或者应用重试时沿用了旧值,Instamojo 就会返回:
Transaction ID is not unique
Android 应直接读取后端返回的 transaction_id。每次创建新订单时,都应由服务器生成并保存交易 ID,同时校验其唯一性。
修正 Android 端的参数传递
将:
accessToken = response.getString("access_token");
transactionID = createTransactionID();
Log.e("Transaction Id", transactionID);
createOrder(accessToken, transactionID);
改为:
accessToken = response.getString("access_token");
transactionID = response.getString("transaction_id");
Log.d("Transaction Id", transactionID);
createOrder(accessToken, transactionID);
完整的关键逻辑可以这样写:
@Override
public void onResponse(JSONObject response) {
try {
if (response.has("error")) {
showToast(response.getString("error"));
return;
}
String accessToken = response.getString("access_token");
String transactionID = response.getString("transaction_id");
if (transactionID.trim().isEmpty()) {
showToast("Transaction ID is empty");
return;
}
Log.d("Transaction Id", transactionID);
createOrder(accessToken, transactionID);
} catch (JSONException e) {
Log.e("Instamojo", "Invalid server response", e);
showToast("Invalid server response");
}
}
收到服务器响应后,不要再次调用客户端的 createTransactionID()。同一笔订单从创建到支付回调都应使用同一个服务端订单号。
更可靠的服务端 ID 生成方式
当前的 getGUIDnoHash() 处理得比较复杂:
$charid = md5(uniqid(rand(), true));
$c = unpack("C*", $charid);
$c = implode("", $c);
return substr($c, 0, 20);
这段代码先生成 MD5 十六进制字符串,再将其转换为字符编码并截断。这样做不一定会立即产生碰撞,但没有必要,而且仅靠随机生成也无法从业务层面保证绝对唯一。
如果字段允许使用字母和数字,可以改用密码学安全随机数:
private function createTransactionId(): string
{
return 'ORD' . strtoupper(bin2hex(random_bytes(8)));
}
生成结果类似:
ORD8F3A94D96C217BE2
如果 transaction ID 只能包含数字,可以组合时间和安全随机数。不过在使用前,需要先确认 SDK/API 对长度和字符集的限制:
private function createNumericTransactionId(): string
{
return date('ymdHis') . random_int(100000, 999999);
}
更稳妥的做法是根据数据库订单主键生成交易号:
private function buildTransactionId(int $orderId): string
{
return 'ORD' . date('Ymd') . str_pad(
(string) $orderId,
8,
'0',
STR_PAD_LEFT
);
}
还应为数据库中的 transaction_id 字段添加唯一索引。即使并发请求意外生成了相同的值,数据库也能在调用支付接口前将其拦截。
正确的订单创建流程
订单创建流程可以按以下步骤处理:
- Android 请求自己的后端创建业务订单。
- 后端生成唯一的
transaction_id。 - 后端先将订单号写入数据库,并用唯一索引防止重复。
- 后端获取 Instamojo 凭证,或直接调用相应的订单接口。
- 后端将本次订单需要的信息返回给 Android。
- Android 直接使用返回的
transaction_id,不要重新生成或修改。 - 发生网络重试时,要明确区分是重试同一订单,还是创建新订单。
例如,后端返回:
{
"access_token": "...",
"transaction_id": "ORD8F3A94D96C217BE2"
}
Android 必须使用 JSON 中的同一个值:
String transactionID = response.getString("transaction_id");
Order order = new Order(
accessToken,
transactionID,
name,
email,
phone,
amount,
description
);
还要防止用户重复点击支付按钮,导致多个 createOrder() 并发执行。可以在请求开始后暂时禁用按钮,等回调结束再恢复:
payButton.setEnabled(false);
Request request = new Request(order, new OrderRequestCallBack() {
@Override
public void onFinish(final Order order, final Exception error) {
runOnUiThread(() -> {
payButton.setEnabled(true);
dialog.dismiss();
if (error != null) {
showToast(error.getMessage());
return;
}
startPreCreatedUI(order);
});
}
});
request.execute();
如何确认实际发送的 ID
排查时,分别记录后端返回的值和 Android 最终传入 Order 的值:
String serverTransactionId = response.getString("transaction_id");
Log.d("Instamojo", "Server transaction ID: " + serverTransactionId);
createOrder(accessToken, serverTransactionId);
然后在 createOrder() 入口再次记录:
private void createOrder(String accessToken, String transactionID) {
Log.d("Instamojo", "Order transaction ID: " + transactionID);
// ...
}
两处日志中的值必须完全一致。如果值一致,但服务端仍然提示重复,常见原因包括:
- 这个 ID 之前已经成功提交,或者请求已部分成功;
- 网络重试时再次提交了同一个 ID;
- 用户重复点击,触发了多个请求;
- 服务端没有为新订单生成新的 ID;
- 测试环境中已经存在相同的交易号。
遇到 “重复” 错误后,不要直接不断生成新 ID 并重试。应先查询本地订单状态,确认之前的请求是否已经创建成功,以免同一笔业务订单被重复支付。
安全注意事项
示例中公开了 client_id 和 client_secret。如果它们是真实凭证,应立即在 Instamojo 后台撤销或轮换,不能继续使用。
还需要处理以下问题:
- 不要将
client_secret放进 Android 应用,也不要把它返回给客户端。 - 最好由后端获取令牌并创建支付订单,客户端只接收完成支付所需的数据。
- 不要关闭 TLS 证书验证:
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
生产环境应启用证书验证:
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
- 后端接口和 webhook 应使用有效的 HTTPS 地址。
- 获取 access token 失败时,应先检查响应内容和 HTTP 状态码,不能直接读取
$jsonOutput->access_token。
当前代码最直接的修正,是将 Android 端的:
transactionID = createTransactionID();
替换为:
transactionID = response.getString("transaction_id");
之后再通过数据库唯一索引、服务端订单记录和防重复提交机制,保证 transaction ID 在整个支付流程中始终唯一,并且前后一致。
备注:内容仅供参考。
