Android 重新下单仍显示并支付原订单
结论
重新下单后仍显示原订单,问题通常不在支付页面是否刷新,而在于应用复用了第一次支付请求的订单参数、Intent 或支付 Activity 实例。处理时需要保证:
- 每次重试都由服务端创建新订单,并重新获取支付凭证。
- 每次拉起支付时都创建新的请求对象,不复用旧的
Intent、Bundle、PayReq或订单字符串。 - 上一次支付流程尚未结束时,不要直接覆盖状态并再次拉起支付。应先查询旧订单状态,再决定继续支付还是创建新订单。
- 如果支付 Activity 使用了
singleTop、singleTask、FLAG_ACTIVITY_REORDER_TO_FRONT等复用方式,需要在onNewIntent()中读取新参数,或者取消复用。
只修改客户端订单号通常解决不了问题。支付宝的支付字符串、微信支付的 prepay_id、签名等凭证都绑定了具体订单,必须由服务端重新生成。
常见原因
复用了旧的支付参数
第一次拉起支付后,订单信息可能保存在成员变量、单例、静态变量或 ViewModel 中。重新下单虽然生成了新的业务订单,但调用支付 SDK 时仍然使用旧变量:
// 错误示例:payInfo 可能仍然是第一次请求的内容
paymentSdk.pay(payInfo)
异步请求也可能产生竞态。第二次下单已经开始,但第一次请求返回得更晚,导致旧支付凭证覆盖了当前页面中的新数据。
复用了支付 Activity
如果承载支付调用的 Activity 配置了以下启动模式或标志:
android:launchMode="singleTask"
或者:
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
再次启动时,系统可能只把原 Activity 移到前台,而不会创建新实例。此时系统只调用 onNewIntent(),原先在 onCreate() 中读取的订单参数不会自动更新。
第一次支付流程尚未结束
用户切到后台并不代表支付失败。第一次订单可能仍是“待支付”状态,第三方支付应用也可能保留着对应的支付会话。如果客户端没有处理返回结果就再次调用支付,很容易重新进入原来的流程。
服务端返回了旧支付凭证
如果下单接口使用相同的商户订单号、幂等键或缓存键,服务端可能把新请求识别为同一笔交易,并返回原订单的支付参数。
解决步骤
1. 为每笔业务订单生成唯一订单号
订单号应由服务端生成并保证唯一。用户重新下单时,不要继续使用旧订单号。
服务端还需要明确幂等策略:
- 同一次按钮重复点击可以返回同一订单。
- 用户明确要求重新下单时,应创建新订单,或者先关闭旧订单。
- 不要只按用户 ID、商品 ID 等长期不变的字段缓存支付凭证。
2. 每次重新获取完整的支付凭证
不能只替换 orderId。客户端应重新请求服务端,获取与新订单匹配的全部参数,例如:
- 支付宝:新的订单字符串和签名。
- 微信支付:新的
prepayId、nonceStr、timestamp和签名。 - 其他支付 SDK:新的 token、session 或 transaction data。
客户端可以为每次支付请求分配一个序号,避免旧接口的迟到响应覆盖新请求:
private var paymentRequestVersion = 0
fun createAndPay() {
val version = ++paymentRequestVersion
lifecycleScope.launch {
val paymentInfo = repository.createOrderAndGetPaymentInfo()
// 页面已发起更新请求时,丢弃旧请求的迟到响应
if (version != paymentRequestVersion) return@launch
launchPayment(paymentInfo)
}
}
3. 不要复用支付请求对象
每次支付都应创建新对象,并使用本次接口响应中的数据为所有字段赋值。
以微信支付请求为例:
fun launchWechatPay(data: WechatPaymentInfo) {
val request = PayReq().apply {
appId = data.appId
partnerId = data.partnerId
prepayId = data.prepayId
packageValue = data.packageValue
nonceStr = data.nonceStr
timeStamp = data.timeStamp
sign = data.sign
}
wxApi.sendReq(request)
}
不要把 PayReq 设置为单例,也不要只更新其中几个字段。
使用支付宝 SDK 时,也应传入服务端为本次订单返回的完整订单字符串。支付调用需要在非主线程中执行:
fun launchAlipay(activity: Activity, orderInfo: String) {
Thread {
val result = PayTask(activity).payV2(orderInfo, true)
activity.runOnUiThread {
handleAlipayResult(result)
}
}.start()
}
这里的 orderInfo 必须属于当前新订单,不能从上一次支付状态中读取。
4. 正确处理 Activity 复用
如果支付 Activity 没有复用需求,优先采用默认的 standard 启动模式,并移除 FLAG_ACTIVITY_REORDER_TO_FRONT 等标志。
如果业务必须使用 singleTop 或 singleTask,需要在 onCreate() 和 onNewIntent() 中处理新订单参数:
class PaymentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handlePaymentIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handlePaymentIntent(intent)
}
private fun handlePaymentIntent(intent: Intent) {
val orderId = intent.getStringExtra(EXTRA_ORDER_ID) ?: return
val paymentToken = intent.getStringExtra(EXTRA_PAYMENT_TOKEN) ?: return
launchPayment(
orderId = orderId,
paymentToken = paymentToken
)
}
companion object {
const val EXTRA_ORDER_ID = "extra_order_id"
const val EXTRA_PAYMENT_TOKEN = "extra_payment_token"
}
}
每次启动时也要创建新的 Intent:
val intent = Intent(context, PaymentActivity::class.java).apply {
putExtra(PaymentActivity.EXTRA_ORDER_ID, paymentInfo.orderId)
putExtra(PaymentActivity.EXTRA_PAYMENT_TOKEN, paymentInfo.paymentToken)
}
context.startActivity(intent)
5. 返回应用后先查询订单状态
不要把“切回 App”当作支付失败,也不要只依赖客户端回调。应用恢复到前台时,应向服务端查询订单状态:
override fun onResume() {
super.onResume()
currentOrderId?.let { orderId ->
lifecycleScope.launch {
when (repository.queryOrderStatus(orderId)) {
OrderStatus.PAID -> showPaymentSuccess()
OrderStatus.PENDING -> showContinuePaymentOrCancel()
OrderStatus.CLOSED,
OrderStatus.FAILED -> enableCreateNewOrder()
}
}
}
}
最终支付结果应以服务端收到的支付平台异步通知和主动查询结果为准。
建议的支付状态控制
支付按钮应避免连续点击,同时明确记录当前订单:
private var paymentLaunching = false
private var currentOrderId: String? = null
fun onPayClicked() {
if (paymentLaunching) return
paymentLaunching = true
lifecycleScope.launch {
try {
val paymentInfo = repository.createOrderAndGetPaymentInfo()
currentOrderId = paymentInfo.orderId
launchPayment(paymentInfo)
} finally {
paymentLaunching = false
}
}
}
用户要求重新下单时,可以按以下顺序处理:
- 查询旧订单是否已经支付。
- 如果已经支付,直接展示成功结果,避免重复下单。
- 如果尚未支付,根据业务规则关闭旧订单,或将其保留为待支付订单。
- 创建新订单,并获取全新的支付凭证。
- 使用新的请求对象拉起支付。
- 服务端校验支付通知中的商户订单号、金额和支付平台交易号。
排查时应记录的字段
在客户端发起支付、切到后台、返回应用及接收支付回调时,记录以下信息:
businessOrderId
merchantOrderNo
paymentRequestVersion
prepayId / paymentToken
Activity instance hashCode
Intent extras
SDK result status
服务端同时记录:
merchantOrderNo
idempotencyKey
payment credential creation time
payment platform transaction id
order status
如果第二次拉起支付时,日志中的商户订单号已经变化,但 prepayId、支付字符串或 token 没有变化,通常需要检查服务端生成支付凭证和缓存的逻辑。如果接口返回了新凭证,但 SDK 收到的仍是旧值,则通常需要检查客户端对象、异步响应或 Activity 复用逻辑。
注意事项
- 不要试图通过清理支付应用缓存或强制结束第三方支付应用来解决问题,这些操作无法保证交易一致性。
- 不要在客户端自行拼接或修改已经签名的订单参数。
- 支付页面显示的订单信息通常取决于支付凭证,而不是客户端当前页面中的订单对象。
- 是否关闭旧订单取决于支付平台和业务规则。关闭前必须确认订单尚未支付。
- 不同支付 SDK 对重复调用和未完成会话的处理方式不同。如果完成上述检查后仍能复现,应结合具体支付渠道、SDK 版本、Activity 启动模式以及第二次调用时传入的实际参数继续定位。
备注:内容仅供参考。
