PAYATHON 2026

Javascript scope 导致 opensocial payment API 调用失败

支付小周

结论

这通常不是 JavaScript 的 scope 或 this 变了。

sendPaymentRequest 在哪个页面环境中定义,函数体内的 opensocial、mixi 和 $ 就会从哪个环境中解析。调用方式不会改变这些变量的词法作用域,所以 var that = this 解决不了这个问题。

更可能有两种原因:

  1. opensocial.requestPayment() 没有在浏览器认可的直接用户操作链中执行。代码先发起异步 $.post(),等回调执行时,点击产生的 user activation 已经失效,容器可能因此拒绝创建支付窗口。
  2. 游戏位于另一个 iframe、Flash/Canvas 运行时或独立的 window 中,导致调用发生在错误的浏览上下文。Mixi API 必须由加载了 OpenSocial 环境的 gadget 页面调用,不能把另一个 frame 中的同名函数或对象当成本地 API 使用。

已经出现 rpc.json 请求,说明 opensocial.requestPayment() 确实执行了。没有后续的 win_create.pl,更可能是容器拒绝开窗、请求参数校验失败,或者调用环境不符合要求,而不是函数访问不到 opensocial。

为什么不是普通的 scope 问题

无论以下函数以什么方式调用,函数体中的 opensocial 都会从定义该函数的全局环境中查找:

function sendPaymentRequest() {
  opensocial.requestPayment(/* ... */);
}

即使调用时的 this 不同:

sendPaymentRequest();
game.sendPaymentRequest();
sendPaymentRequest.call(otherObject);

自由变量 opensocial 也不会因此变成 this.opensocial。

只有以下情况才可能让执行环境真正发生变化:

  • 游戏运行在另一个 iframe 中;
  • 函数代码被复制或重新执行到另一个 window;
  • 游戏使用了 eval、new Function() 或类似机制;
  • 游戏通过 ExternalInterface 等桥接机制调用另一个 frame;
  • opensocial 被局部变量或参数遮蔽。

不要依赖保存顶层 this 的做法:

var that = this;

顶层 this 的含义会受到脚本类型、frame 和加载方式影响,而且保存它也无法恢复支付接口所需的用户手势。

先确认实际调用环境

在函数入口和异步回调中记录当前环境与参数:

function sendPaymentRequest(id, price, count, name, imageUrl) {
  console.log("entry", {
    id: id,
    price: price,
    count: count,
    name: name,
    amount: Number(price) * Number(count),
    sameTopWindow: window === window.top,
    hasOpenSocial: typeof window.opensocial !== "undefined",
    hasMixi: typeof window.mixi !== "undefined",
    userActivation: navigator.userActivation
      ? navigator.userActivation.isActive
      : "unsupported"
  });

  $.post(
    "https://url.com/payment.php",
    {
      item_id: id,
      item_price: Number(price) * Number(count),
      item_name: name
    }
  ).done(function (data) {
    console.log("post callback", {
      hasOpenSocial: typeof window.opensocial !== "undefined",
      userActivation: navigator.userActivation
        ? navigator.userActivation.isActive
        : "unsupported"
    });
  }).fail(function (xhr, status, error) {
    console.error("payment.php failed", {
      status: xhr.status,
      textStatus: status,
      error: error,
      responseText: xhr.responseText
    });
  });
}

如果入口处的 isActive 是 true,进入 $.post() 回调后却变成了 false,基本可以确定点击产生的直接用户操作链已经中断。

还要检查以下内容:

  • 游戏是否正确传入了 id、price 和 count;
  • amount 是否为有效数字,而不是 NaN;
  • responseJSON.signature 是否存在;
  • 签名使用的金额、商品 ID 和商品名称,是否与提交给 Mixi 的值完全一致;
  • 页面使用 HTTPS 时,请求是否仍指向 http://,因而被浏览器作为 mixed content 拦截;
  • 控制台中是否出现 popup、sandbox、cross-origin 或 RPC 错误。

推荐做法:分开准备签名和发起支付

当前流程是:

点击游戏按钮
→ 调用 JavaScript
→ 发起 $.post()
→ 等待服务器响应
→ 调用 opensocial.requestPayment()

异步请求会切断直接用户操作链。更可靠的方案是提前取得支付所需的信息,等用户点击时再同步调用支付 API。

let preparedPayment = null;

function preparePayment(id, price, count, name) {
  const amount = Number(price) * Number(count);

  if (!Number.isFinite(amount) || amount <= 0) {
    throw new Error("Invalid payment amount");
  }

  return $.ajax({
    url: "https://url.com/payment.php",
    method: "POST",
    dataType: "json",
    data: {
      item_id: id,
      item_price: amount,
      item_name: name
    }
  }).then(function (responseJSON) {
    if (!responseJSON.signature) {
      throw new Error("Payment signature is missing");
    }

    preparedPayment = {
      id: id,
      amount: amount,
      name: name,
      signature: responseJSON.signature
    };
  });
}

function openPreparedPayment() {
  if (!preparedPayment) {
    alert("支付信息尚未准备完成,请稍后重试。");
    return;
  }

  const params = {};
  params[opensocial.Payment.Field.AMOUNT] = preparedPayment.amount;
  params[mixi.Payment.Field.ITEM_NAME] = preparedPayment.name;
  params[mixi.Payment.Field.SIGNATURE] = preparedPayment.signature;
  params[mixi.Payment.Field.ITEM_ID] = preparedPayment.id;
  params[mixi.Payment.Field.IS_TEST] = "true";
  params[mixi.Payment.Field.INVENTORY_CODE] = 123;
  params[opensocial.Payment.Field.PAYMENT_TYPE] =
    opensocial.Payment.PaymentType.PAYMENT;

  const payment = opensocial.newPayment(params);

  opensocial.requestPayment(
    payment,
    "https://url.com/payment-callback.php",
    function (response) {
      if (response.hadError()) {
        console.error("Payment failed", {
          code: response.getErrorCode(),
          message: response.getErrorMessage()
        });
        return;
      }

      console.log("Payment response", response.getData());
    }
  );
}

游戏中的最终购买按钮应直接调用:

openPreparedPayment();

如果无法预取签名,可以改成两阶段交互:

  1. 第一次点击时获取签名并准备订单;
  2. 准备完成后显示“确认支付”按钮;
  3. 用户第二次点击时,直接同步调用 opensocial.requestPayment()。

最终点击和支付 API 之间不要插入 $.post()、setTimeout()、Promise 回调或其他异步步骤。

如果游戏运行在 iframe 中

支付函数应放在已经加载 Mixi/OpenSocial API 的 gadget 页面中,再由游戏向这个页面发送请求。

同源时,可以明确调用父页面暴露的方法:

// Gadget 页面
window.mixiPaymentBridge = {
  openPayment: function () {
    openPreparedPayment();
  }
};

游戏 frame 中:

window.parent.mixiPaymentBridge.openPayment();

跨域时不能直接访问 window.parent 中的对象,需要使用 postMessage:

// 游戏 frame
window.parent.postMessage(
  {
    type: "MIXI_PAYMENT_REQUEST",
    orderId: "order-123"
  },
  "https://gadget.example.com"
);

Gadget 页面接收消息并验证来源:

window.addEventListener("message", function (event) {
  if (event.origin !== "https://game.example.com") {
    return;
  }

  if (event.data && event.data.type === "MIXI_PAYMENT_REQUEST") {
    openPreparedPayment();
  }
});

不过,某些浏览器或容器不会把跨 frame 的 postMessage 回调视为直接用户操作。如果 Mixi 支付窗口明确要求 user activation,最好把最终确认按钮放在有权调用 OpenSocial API 的 gadget 页面中。

如果 iframe 使用了 sandbox,还需要确认它是否允许弹窗,例如:

<iframe
  sandbox="allow-scripts allow-same-origin allow-popups"
  src="https://game.example.com/">
</iframe>

实际需要哪些权限取决于 Mixi 容器和目标浏览器,仅凭这段代码无法确定。

完善原函数的错误处理

原代码没有处理 $.post() 失败、JSON 解析失败和参数无效等情况,容易把服务器错误误判为 API scope 问题。至少应该补上这些检查:

function sendPaymentRequest(id, price, count, name, imageUrl) {
  const amount = Number(price) * Number(count);

  if (!id || !Number.isFinite(amount) || amount <= 0) {
    console.error("Invalid payment arguments", {
      id: id,
      price: price,
      count: count,
      amount: amount
    });
    return;
  }

  $.ajax({
    url: "https://url.com/payment.php",
    method: "POST",
    dataType: "json",
    data: {
      item_id: id,
      item_price: amount,
      item_name: name
    }
  }).done(function (responseJSON) {
    if (!responseJSON || !responseJSON.signature) {
      console.error("Invalid signature response", responseJSON);
      return;
    }

    try {
      const params = {};
      params[opensocial.Payment.Field.AMOUNT] = amount;
      params[mixi.Payment.Field.ITEM_NAME] = name;
      params[mixi.Payment.Field.SIGNATURE] = responseJSON.signature;
      params[mixi.Payment.Field.ITEM_ID] = id;
      params[mixi.Payment.Field.IS_TEST] = "true";
      params[mixi.Payment.Field.INVENTORY_CODE] = 123;
      params[opensocial.Payment.Field.PAYMENT_TYPE] =
        opensocial.Payment.PaymentType.PAYMENT;

      const payment = opensocial.newPayment(params);

      opensocial.requestPayment(
        payment,
        "https://url.com/payment-callback.php",
        function (response) {
          if (response.hadError()) {
            console.error(
              "Mixi payment error:",
              response.getErrorCode(),
              response.getErrorMessage()
            );
            return;
          }

          console.log("Mixi payment succeeded", response.getData());
        }
      );
    } catch (error) {
      console.error("Unable to create payment request", error);
    }
  }).fail(function (xhr, status, error) {
    console.error("Signature request failed", {
      httpStatus: xhr.status,
      status: status,
      error: error,
      responseText: xhr.responseText
    });
  });
}

这段代码可以帮助诊断问题,但无法避免异步回调造成的 user activation 丢失。如果容器要求支付调用必须直接来自点击事件,仍然应该使用“预取签名,再点击确认支付”的流程。

注意事项

  • alert() 能显示,只能说明 JavaScript 函数执行了,不能说明支付参数、用户手势和容器上下文都有效。
  • console.log(payment) 能输出对象,也不能说明签名或字段已经通过容器校验。
  • 不要把 opensocial 改成 this.opensocial,否则代码会开始依赖具体的调用方式。
  • 不要为了跨 frame 访问而随意使用 window.top.opensocial。跨域访问会被同源策略阻止,而且顶层窗口不一定是 API 所属的环境。
  • 应显式转换并校验支付金额,避免游戏传入字符串、空值或非法数量。
  • 服务端签名必须绑定实际提交的订单字段。已经签名的金额或商品信息不能在客户端随意修改。
  • IS_TEST、INVENTORY_CODE、回调 URL 和签名字段的具体要求取决于所使用的 Mixi Payment API 规范,应以对应容器版本的文档为准。

排查时,应把重点从 this 和普通 JavaScript scope 转到以下几项:调用是否仍处于直接用户操作中,游戏与 gadget 是否属于同一个 window,iframe 是否允许弹窗,以及支付参数和签名是否完整一致。

备注:内容仅供参考。