Javascript scope 导致 opensocial payment API 调用失败
结论
这通常不是 JavaScript 的 scope 或 this 变了。
sendPaymentRequest 在哪个页面环境中定义,函数体内的 opensocial、mixi 和 $ 就会从哪个环境中解析。调用方式不会改变这些变量的词法作用域,所以 var that = this 解决不了这个问题。
更可能有两种原因:
opensocial.requestPayment()没有在浏览器认可的直接用户操作链中执行。代码先发起异步$.post(),等回调执行时,点击产生的 user activation 已经失效,容器可能因此拒绝创建支付窗口。- 游戏位于另一个
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();
如果无法预取签名,可以改成两阶段交互:
- 第一次点击时获取签名并准备订单;
- 准备完成后显示“确认支付”按钮;
- 用户第二次点击时,直接同步调用
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 是否允许弹窗,以及支付参数和签名是否完整一致。
备注:内容仅供参考。
