PAYATHON 2026

支付宝支付完成返回小程序后页面卡死且按钮无响应

支付阿杰

结论

“支付完成返回后页面卡死”通常不能只凭 appid 判断为支付失败。支付结果已经返回,接口请求也能完成,更可能是小程序重新进入前台后,页面生命周期相关逻辑出了问题。常见原因包括:

  1. onShow 重复触发,订单查询、页面刷新或订阅消息逻辑随之重复执行。
  2. my.requestSubscribeMessage 等原生弹窗的调用时机或次数异常,留下不可见的原生遮罩层。此时页面看起来正常,却无法点击。
  3. my.showLoading、自定义弹层或透明 cover-view 没有关闭,拦截了所有触摸事件。
  4. 返回小程序后执行了同步死循环、频繁 setData,或者回调之间出现递归调用,导致页面线程阻塞。
  5. 支付回调和页面 onShow 同时处理同一笔订单,引发竞态或重复跳转。

排查时,可以先暂时关闭订阅弹窗和所有遮罩层,只保留支付结果查询。若页面恢复正常,再逐项恢复其他功能,通常可以较快找到问题所在。

为什么支付后会再次触发页面逻辑

调用 my.tradePay 唤起支付宝收银台时,小程序会进入后台。用户点击收银台右上角的“完成”并返回后,小程序重新进入前台,页面的 onShow 通常会再次执行。

因此,下面的写法存在重复调用的风险:

Page({
  onShow() {
    this.queryOrder();
    this.requestSubscribe();
  }
});

页面首次打开时会执行一次,支付返回后还会再执行一次。如果订单查询又触发页面刷新、页面跳转或订阅授权,就可能造成重复调用。订阅弹窗属于原生能力,一旦调用状态异常,开发者工具也未必会显示普通的 JavaScript 报错。

推荐的处理方式

1. 区分首次进入和支付返回

不要在每次 onShow 时都执行支付后的业务。可以设置支付状态标记,仅在确认从收银台返回时查询订单。

Page({
  data: {
    paying: false,
    checkingOrder: false
  },

  onShow() {
    if (this.data.paying) {
      this.handlePaymentReturn();
    }
  },

  startPayment() {
    if (this.data.paying) {
      return;
    }

    this.setData({
      paying: true
    });

    my.tradePay({
      tradeNO: this.data.tradeNo,

      success: (res) => {
        console.info('tradePay success:', res);
        // 客户端结果只用于更新交互状态,
        // 最终支付状态仍应由服务端查询或异步通知确认。
      },

      fail: (err) => {
        console.error('tradePay fail:', err);
        this.setData({
          paying: false
        });
      },

      complete: (res) => {
        console.info('tradePay complete:', res);
      }
    });
  },

  async handlePaymentReturn() {
    if (this.data.checkingOrder) {
      return;
    }

    this.setData({
      checkingOrder: true,
      paying: false
    });

    try {
      const order = await this.queryOrderStatus();
      this.setData({
        orderStatus: order.status
      });
    } catch (err) {
      console.error('query order status failed:', err);
    } finally {
      this.setData({
        checkingOrder: false
      });
      my.hideLoading();
    }
  },

  queryOrderStatus() {
    return new Promise((resolve, reject) => {
      my.request({
        url: 'https://example.com/api/order/status',
        method: 'GET',
        data: {
          orderNo: this.data.orderNo
        },
        success: (res) => resolve(res.data),
        fail: reject
      });
    });
  }
});

不要在 my.tradePay.success 和 onShow 中分别执行完整的订单刷新、页面跳转和订阅弹窗逻辑。应设置一个统一入口,并通过状态标记防止重复处理。

2. 暂时移除订阅消息调用

先注释掉支付返回后的 my.requestSubscribeMessage。如果页面不再卡死,问题基本可以缩小到订阅弹窗的调用时机、重复调用或回调处理。

恢复订阅功能时,需要注意:

  • 同一时刻只发起一次订阅请求。
  • 记录 success、fail 和 complete 的结果。
  • 不要在 onShow 中无条件弹出订阅窗口。
  • 不要在订阅回调中再次发起相同的订阅请求。
  • 尽量由用户点击按钮主动触发,避免支付返回后连续调起多个原生交互。

示例参数需要根据当前接入的支付宝小程序订阅消息 API 文档填写:

Page({
  data: {
    requestingSubscribe: false
  },

  requestSubscribe() {
    if (this.data.requestingSubscribe) {
      return;
    }

    this.setData({
      requestingSubscribe: true
    });

    my.requestSubscribeMessage({
      entityIds: ['实际的订阅实体 ID'],

      success: (res) => {
        console.info('subscribe success:', res);
      },

      fail: (err) => {
        console.error('subscribe fail:', err);
      },

      complete: (res) => {
        console.info('subscribe complete:', res);
        this.setData({
          requestingSubscribe: false
        });
      }
    });
  }
});

如果当前基础库的 API 参数或回调形式与示例不同,应以对应版本的官方接口定义为准,不要直接照搬。

3. 检查透明遮罩层

“页面能显示,但所有按钮都无法点击”很像是全屏元素拦截了触摸事件。排查时重点检查:

  • my.showLoading() 是否始终有对应的 my.hideLoading()。
  • 自定义弹窗关闭后,遮罩节点是否还存在。
  • opacity: 0 只会让元素不可见,不会阻止它拦截点击。
  • 全屏 view、cover-view 是否仍然设置了较高的 z-index。
  • 隐藏弹层时,是否只隐藏了内容而没有隐藏遮罩。

不要只通过透明度隐藏遮罩:

/* 存在风险:透明后仍可能拦截点击 */
.mask-hidden {
  opacity: 0;
}

应直接取消渲染:

<view a:if="{{showMask}}" class="mask"></view>

还要确保所有异常分支都会关闭遮罩和 loading:

async refreshOrder() {
  this.setData({
    showMask: true
  });

  my.showLoading({
    content: '处理中'
  });

  try {
    await this.queryOrderStatus();
  } catch (err) {
    console.error('refresh order failed:', err);
  } finally {
    my.hideLoading();
    this.setData({
      showMask: false
    });
  }
}

4. 排查主线程是否阻塞

如果按钮没有响应,定时器日志也停止输出,就需要检查同步死循环或高频更新。

可以在支付返回入口加入一个简单的心跳:

onShow() {
  console.info('page onShow:', Date.now());

  setTimeout(() => {
    console.info('event loop is still running:', Date.now());
  }, 1000);

  if (this.data.paying) {
    this.handlePaymentReturn();
  }
}

如果第二条日志始终没有出现,重点检查:

  • while 或递归函数是否缺少退出条件。
  • setData 回调中是否再次执行了同一个 setData。
  • onShow、订单查询和页面跳转是否相互触发。
  • 是否同步遍历或序列化了超大数组。
  • 失败重试是否没有次数限制。
  • Promise 回调中是否重复调用自身。

例如,下面的逻辑会持续查询,应避免这样写:

queryOrder() {
  my.request({
    url: 'https://example.com/api/order/status',
    success: () => {
      this.queryOrder();
    }
  });
}

如果确实需要轮询,应设置查询间隔和次数上限,并在页面隐藏或卸载时停止:

Page({
  pollTimer: null,
  pollCount: 0,

  startPolling() {
    this.stopPolling();
    this.pollCount = 0;

    const poll = async () => {
      if (this.pollCount >= 5) {
        this.stopPolling();
        return;
      }

      this.pollCount += 1;

      try {
        const order = await this.queryOrderStatus();

        if (order.status === 'PAID') {
          this.stopPolling();
          return;
        }
      } catch (err) {
        console.error('poll order failed:', err);
      }

      this.pollTimer = setTimeout(poll, 1500);
    };

    poll();
  },

  stopPolling() {
    if (this.pollTimer) {
      clearTimeout(this.pollTimer);
      this.pollTimer = null;
    }
  },

  onHide() {
    this.stopPolling();
  },

  onUnload() {
    this.stopPolling();
  }
});

建议的最小化排查顺序

按下面的顺序逐步恢复代码,比直接检查完整业务链路更容易定位问题:

  1. 支付返回后只打印 onShow 日志,不请求接口。
  2. 加入订单查询,但不执行 setData、跳转或弹窗。
  3. 恢复必要的 setData。
  4. 检查并恢复 loading 和自定义遮罩。
  5. 最后恢复订阅消息弹窗。
  6. 每恢复一步,都记录执行次数及 success、fail、complete 回调。

同时可以在真机日志或服务端日志中记录:

console.info('payment return trace', {
  time: Date.now(),
  orderNo: this.data.orderNo,
  paying: this.data.paying,
  checkingOrder: this.data.checkingOrder,
  requestingSubscribe: this.data.requestingSubscribe
});

不要在日志中输出支付凭证、用户身份令牌、完整手机号等敏感数据。

仍无法定位时需要提供的信息

appid 本身不足以确定具体原因。如果经过最小化处理后仍能复现,应保留以下信息并提交给支付宝小程序技术支持:

  • 问题发生的时间点和订单号,订单号应按要求脱敏。
  • 支付宝客户端版本、手机型号和操作系统版本。
  • 小程序基础库版本。
  • 问题是否只在真机出现,开发者工具能否复现。
  • onShow、tradePay、订单查询和订阅请求的完整时序日志。
  • 移除订阅调用或遮罩层后,页面是否恢复。
  • 可复现的最小代码和录屏。
  • 问题是否稳定发生,还是只出现在特定账号、设备或网络环境中。

没有错误日志和最小复现代码时,不能直接认定这是支付宝客户端缺陷。此类问题通常可以通过限制 onShow 重入、统一支付返回处理入口、确保遮罩关闭,以及避免自动重复调起订阅弹窗来解决。

备注:内容仅供参考。