如何在 Adobe AIR app 中集成 ALIPAY?
结论
可以采用“服务端创建订单 + StageWebView 展示支付宝收银台”的方式接入支付宝。不过,这属于网页支付,并不是原生支付宝 SDK 集成。
建议按以下方式划分职责:
- AIR 客户端向业务服务器请求创建订单。
- PHP 服务端调用支付宝官方接口,生成支付页面 URL。
- AIR 使用
StageWebView打开该 URL。 - 支付宝通过异步通知将支付结果发送给 PHP 服务端。
- AIR 向业务服务器查询订单状态,确认支付成功后发放游戏 add-on。
签名、验签、金额校验和商品发放都必须在服务端完成。即使 StageWebView 已跳转到“支付成功”页面,也不能直接据此发货。
如果目标平台是 iOS,游戏 add-on 通常属于数字内容。能否使用支付宝,取决于当前 App Store 规则、应用地区和业务类型。很多场景必须使用 Apple 的 In-App Purchase,其他应用商店也可能有类似限制,因此开发前应先核对目标商店的最新政策。
为什么不建议直接在 AS3 中调用支付宝接口
支付宝支付需要使用商户私钥为请求签名,同时验证支付宝返回的数据。把私钥或完整的签名逻辑放进 AIR 应用后,攻击者可能通过反编译安装包提取这些信息。
客户端只应获得:
- 业务订单号;
- 支付页面 URL;
- 不含敏感凭据的订单状态。
商户私钥、支付宝公钥、应用私钥配置和异步通知验签逻辑都应保存在 PHP 服务端。
如果需要唤起支付宝 App、接收支付结果回调,并改善移动端支付体验,通常要基于支付宝原生 SDK 编写或购买对应的 AIR Native Extension(ANE)。网页支付可以不依赖 ANE,但兼容性和实际体验取决于支付宝当前支付产品是否允许嵌入 WebView。
推荐支付流程
AIR 客户端
│
├─ 1. 请求创建业务订单
▼
PHP 业务服务器
│
├─ 2. 生成唯一订单号并锁定金额
├─ 3. 调用支付宝官方 SDK/API
▼
支付宝收银台 URL
│
├─ 4. StageWebView 展示支付页面
├─ 5. 用户完成或取消支付
▼
支付宝异步通知 PHP
│
├─ 6. PHP 验签并核对订单信息
├─ 7. 原子更新订单并发放商品
▼
AIR 查询订单状态
第 5~7 步是确认支付的关键路径。网页同步跳转主要用于改善用户体验,不能作为支付成功的唯一依据。
AIR 客户端示例
下面的示例先向业务服务器请求创建订单,再将服务端返回的 checkout_url 加载到 StageWebView。接口地址和返回结构仅用于说明业务流程,需要根据实际后端调整。
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.LocationChangeEvent;
import flash.geom.Rectangle;
import flash.media.StageWebView;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
public class AlipayCheckout extends Sprite
{
private var webView:StageWebView;
private var orderId:String;
public function AlipayCheckout()
{
createOrder("game_addon_001");
}
private function createOrder(productId:String):void
{
var request:URLRequest =
new URLRequest("https://api.example.com/payments/alipay/create");
request.method = URLRequestMethod.POST;
request.requestHeaders = [
new URLRequestHeader("Content-Type", "application/json")
];
request.data = JSON.stringify({
product_id: productId
});
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onOrderCreated);
loader.addEventListener(IOErrorEvent.IO_ERROR, onRequestError);
loader.load(request);
}
private function onOrderCreated(event:Event):void
{
var loader:URLLoader = URLLoader(event.currentTarget);
var response:Object = JSON.parse(String(loader.data));
orderId = String(response.order_id);
if (!response.checkout_url)
{
throw new Error("Server did not return checkout_url");
}
openCheckout(String(response.checkout_url));
}
private function openCheckout(url:String):void
{
webView = new StageWebView();
webView.stage = stage;
webView.viewPort = new Rectangle(
0,
0,
stage.stageWidth,
stage.stageHeight
);
webView.addEventListener(
LocationChangeEvent.LOCATION_CHANGING,
onLocationChanging
);
webView.loadURL(url);
}
private function onLocationChanging(event:LocationChangeEvent):void
{
var targetUrl:String = event.location;
// 该地址仅表示支付页面已返回业务系统,
// 不能直接视为支付成功。
if (targetUrl.indexOf("https://example.com/payment/return") == 0)
{
closeCheckout();
queryOrderStatus();
}
}
private function queryOrderStatus():void
{
var request:URLRequest = new URLRequest(
"https://api.example.com/payments/orders/"
+ encodeURIComponent(orderId)
);
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onStatusLoaded);
loader.addEventListener(IOErrorEvent.IO_ERROR, onRequestError);
loader.load(request);
}
private function onStatusLoaded(event:Event):void
{
var loader:URLLoader = URLLoader(event.currentTarget);
var response:Object = JSON.parse(String(loader.data));
switch (String(response.status))
{
case "paid":
unlockAddon();
break;
case "pending":
// 异步通知可能尚未到达,可延迟后再次查询。
break;
case "closed":
case "failed":
// 展示取消或失败状态。
break;
}
}
private function unlockAddon():void
{
// 最好根据服务端返回的权益状态刷新游戏数据,
// 不要仅在本地永久记录购买结果。
}
private function closeCheckout():void
{
if (webView == null)
{
return;
}
webView.removeEventListener(
LocationChangeEvent.LOCATION_CHANGING,
onLocationChanging
);
webView.dispose();
webView = null;
}
private function onRequestError(event:IOErrorEvent):void
{
trace("Payment request failed:", event.text);
}
}
}
实际应用还需要处理窗口尺寸变化、返回键、网络超时、重复查询和应用进入后台等情况。
PHP 服务端结构
不同支付宝支付产品和 SDK 版本使用的类名、接口名称及必填参数可能不同,不应把某个版本的调用参数直接写死。以下代码只用于说明服务端的职责边界,不代表任何特定版本支付宝 SDK 的完整调用方式:
<?php
declare(strict_types=1);
$input = json_decode(file_get_contents('php://input'), true);
$productId = (string)($input['product_id'] ?? '');
// 商品名称和金额必须由服务端商品表确定,不能信任客户端报价。
$product = findProductById($productId);
if ($product === null) {
http_response_code(404);
echo json_encode(['error' => 'PRODUCT_NOT_FOUND']);
exit;
}
$order = createPendingOrder([
'product_id' => $product['id'],
'amount' => $product['price'],
'currency' => 'CNY',
]);
// 此处使用当前支付宝官方 SDK 创建适合业务场景的支付请求。
// 私钥、应用标识、notify_url 等配置只能保存在服务端。
$checkoutUrl = createAlipayCheckoutUrl([
'out_trade_no' => $order['order_id'],
'subject' => $product['name'],
'total_amount' => $order['amount'],
'notify_url' => 'https://api.example.com/payments/alipay/notify',
'return_url' => 'https://example.com/payment/return',
]);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'order_id' => $order['order_id'],
'checkout_url' => $checkoutUrl,
], JSON_UNESCAPED_SLASHES);
createAlipayCheckoutUrl() 应通过所选支付宝产品对应的官方 SDK 实现。具体使用手机网站支付、App 支付还是其他产品,要根据商户签约能力、目标平台和支付宝当前文档确定。
异步通知处理
通知接口至少需要完成以下检查:
<?php
declare(strict_types=1);
$notification = $_POST;
// 必须调用当前官方 SDK 的验签函数。
if (!verifyAlipayNotification($notification)) {
http_response_code(400);
echo 'failure';
exit;
}
$orderId = (string)($notification['out_trade_no'] ?? '');
$order = findOrderForUpdate($orderId);
if ($order === null) {
echo 'failure';
exit;
}
// 字段名称和交易状态值以所采用支付产品的当前文档为准。
if (!notificationRepresentsSuccessfulPayment($notification)) {
echo 'success';
exit;
}
if (!amountMatches($order, $notification)) {
echo 'failure';
exit;
}
if (!sellerMatchesConfiguredMerchant($notification)) {
echo 'failure';
exit;
}
markOrderPaidAndGrantEntitlementAtomically($order);
echo 'success';
支付宝可能重复发送同一笔交易的通知,因此通知处理必须具备幂等性。markOrderPaidAndGrantEntitlementAtomically() 要保证同一订单即使被处理多次,也只会发放一次权益。
StageWebView 方案的限制
支付页面可能不支持嵌入
部分支付页面可能会限制 WebView,依赖外部支付宝 App,要求特定浏览器能力,或者在登录、短信验证和跳转过程中打开新窗口。是否可用,需要结合商户实际签约的产品在真实设备上测试,不能只看桌面浏览器中的结果。
App 唤起可能失败
支付页面可能跳转到自定义 URL Scheme。StageWebView 如何处理这类跳转,会受到 AIR 版本、操作系统和支付宝页面实现的影响。如果必须稳定唤起支付宝 App,通常更适合使用原生 SDK 加 ANE。
Cookie 与登录状态相互独立
StageWebView 的 Cookie 环境不一定与系统浏览器或支付宝 App 共享,用户可能需要重新登录。支付完成后,也不能假定 WebView 一定能顺利返回应用。
用户可能中途关闭页面
用户关闭页面、返回应用,或者支付页面显示异常,都不代表交易失败。客户端应保存订单号,并允许用户重新查询订单状态。
商店审核风险
游戏 add-on 属于数字商品时,使用第三方支付可能违反发布平台的支付规则。能够在技术上打开支付宝页面,并不代表应用一定可以通过审核。
安全注意事项
- 全程使用 HTTPS,并验证服务器证书。
- 商品价格必须由服务端计算,不能接受客户端提交的最终金额。
- 每次支付都要使用唯一且不可预测的业务订单号。
- 不要把支付宝商户私钥打包进 SWF、APK、IPA 或 ANE。
- 不要信任
return_url中的查询参数,也不要仅凭页面文字判断支付成功。 - 必须验证支付宝异步通知的签名、订单号、金额、商户身份和交易状态。
- 支付通知处理和权益发放应放在同一事务或可靠的幂等流程中。
- AIR 客户端应查询服务端记录的订单或权益状态,而不是直接查询本地支付结果。
- 日志中不要记录私钥、完整签名、登录凭据或其他敏感支付信息。
PHP + StageWebView 可以用于实现网页收银台,尤其是在没有可用 ANE 的情况下。正式上线前,必须确认支付宝产品是否支持 WebView,并核对应用商店的支付政策。如果需要原生支付宝 App 支付体验,应使用官方原生 SDK,再通过 ANE 将相关能力暴露给 AS3。
备注:内容仅供参考。
