PAYATHON 2026

鸿蒙支付第二笔订单跳转至上次已完成页面,无法支付

支付老李

结论

不要复用第一笔订单的支付参数或支付页面实例。每次发起支付,都应由服务端针对当前业务订单重新创建支付宝支付请求。客户端收到返回结果后,还要核对其中的订单号。

如果用户支付后仍停留在支付宝完成页,再次拉起支付宝时可能只会恢复这个页面。遇到这种情况,需要先提示用户返回商家应用,再重新调用支付宝支付入口。商家应用无法直接关闭支付宝页面,也不应模拟点击“完成”按钮。

原因

出现这种情况,通常不代表第二笔订单已经支付,而是系统恢复了支付宝上次保留的页面。常见原因有:

  • 两笔业务订单使用了同一份 orderStr、支付 URL、预下单结果或支付宝交易号。
  • 客户端缓存了第一次支付的 Promise、回调或页面状态。
  • 第二次操作只是将支付宝应用切到前台,并未真正提交新的支付请求。
  • 使用 H5 收银台时沿用了原来的 Web 组件或 URL,触发了页面历史记录或前进后退缓存。
  • 商家应用没有区分支付结果对应的订单,误将上一笔订单的结果用于第二笔订单。

用户是否点击支付宝的“完成”按钮,只会影响页面是否退出,不能用于判断商家订单是否支付成功。最终支付状态必须以服务端查询结果或支付宝异步通知为准。

解决步骤

1. 每次支付都重新获取支付参数

第二笔订单必须有新的商家订单号,服务端也要重新调用支付宝下单接口。第一笔订单返回的支付参数不能缓存后继续使用。

第一笔业务订单 -> 新的 out_trade_no -> 新的支付参数
第二笔业务订单 -> 另一个 out_trade_no -> 重新生成支付参数

如果两次请求使用相同的 out_trade_no,支付宝可能会将它们识别为同一笔交易,并直接显示原交易结果。

2. 客户端建立独立的支付会话

下面是 ArkTS 风格的示意代码。请将 requestPayInfo 和 invokeAlipay 替换为项目实际使用的服务端接口和支付宝 SDK 调用方式。

interface PayInfo {
  orderId: string
  orderStr: string
}

interface PayResult {
  orderId?: string
  resultStatus?: string
}

let paying = false
let currentOrderId: string | undefined

async function startPayment(orderId: string): Promise<void> {
  if (paying) {
    throw new Error('当前已有支付请求正在处理')
  }

  paying = true
  currentOrderId = orderId

  try {
    // 每次都向服务端获取当前订单专属的支付参数
    const payInfo: PayInfo = await requestPayInfo(orderId)

    if (payInfo.orderId !== currentOrderId) {
      throw new Error('服务端返回的支付订单与当前订单不一致')
    }

    const result: PayResult = await invokeAlipay(payInfo.orderStr)

    // 防止上一笔支付的延迟回调污染当前订单
    if (currentOrderId !== orderId) {
      return
    }

    await refreshOrderStatus(orderId)
  } finally {
    if (currentOrderId === orderId) {
      paying = false
      currentOrderId = undefined
    }
  }
}

不要仅凭 resultStatus 或“已完成”页面判断支付成功。客户端返回值可以用于调整交互,但最终结果仍应由服务端确认。

3. 服务端必须按订单查询支付状态

客户端从支付宝返回后,应查询当前业务订单的状态,不能直接将订单改为已支付。

async function refreshOrderStatus(orderId: string): Promise<void> {
  const order = await queryOrderFromServer(orderId)

  switch (order.status) {
    case 'PAID':
      showPaySuccess()
      break
    case 'UNPAID':
      showPayRetry()
      break
    case 'PROCESSING':
      showPayProcessing()
      break
    default:
      showPayError()
  }
}

服务端处理支付宝异步通知时,至少还要校验以下内容:

  • out_trade_no 是否属于本系统。
  • 通知金额是否与业务订单一致。
  • 商户身份、应用身份等信息是否匹配。
  • 同一通知重复到达时,处理逻辑能否保持幂等。

4. 清理上一笔订单的客户端状态

支付结束、取消或发生异常后,应清除与该订单有关的临时状态,包括:

  • 上一次的 orderStr 或支付 URL。
  • 支付状态标记和回调引用。
  • Web 组件的历史记录。
  • 页面路由中残留的订单号。
  • 尚未结束的轮询任务和定时器。

如果使用 H5 收银台,第二笔订单应加载新的支付 URL。必要时可以销毁并重新创建 Web 组件,不要通过 back()、forward() 或恢复旧页面的方式进入收银台。

function resetPayPage(): void {
  currentOrderId = undefined
  paying = false

  // 如果项目使用 Web 收银台,可在这里销毁旧页面或创建新的 Web 实例。
  // 具体 API 取决于当前使用的 HarmonyOS Web 组件版本。
}

不要擅自在支付宝生成的签名支付 URL 后追加随机参数。修改已签名的内容可能导致验签失败。需要避免缓存时,应重新下单,并使用服务端返回的完整新地址。

5. 处理支付宝仍停留在完成页的情况

如果已经确认第二笔订单使用了全新的支付参数,但拉起支付宝后仍只显示上一笔订单的完成页,可以这样处理:

  1. 提示用户先返回商家应用。
  2. 用户返回商家应用后,重新调用当前订单的支付入口。
  3. 支付调用返回后,立即查询当前订单状态。
  4. 如果仍然无法进入新交易,记录本次调用参数的摘要、当前订单号和客户端返回状态,再交由支付宝技术支持确认。

能否通过特定 SDK 参数覆盖支付宝已有页面,取决于实际接入的支付宝 HarmonyOS SDK 版本和支付产品类型。在没有官方接口依据的情况下,不应自行构造参数,也不能假定系统可以关闭支付宝结果页。

排查重点

排查时应优先对比两次支付请求,确认以下字段是否发生变化:

业务订单号
out_trade_no
支付宝预下单结果或 orderStr
支付 URL
客户端当前订单 ID
服务端查询到的交易状态

日志中不要记录完整签名、访问令牌或包含敏感信息的完整支付参数。可以记录订单号、请求时间、是否执行了支付调用,以及脱敏后的参数摘要。

为每笔订单重新下单,每次都真实调用支付入口,按订单隔离支付结果,并由服务端确认最终状态。这样才能避免将上一笔订单的完成页面误判为第二笔订单的支付结果。

备注:内容仅供参考。