PAYATHON 2026

如何在 Adobe AIR app 中集成 ALIPAY?

支付小周

结论

可以采用“服务端创建订单 + StageWebView 展示支付宝收银台”的方式接入支付宝。不过,这属于网页支付,并不是原生支付宝 SDK 集成。

建议按以下方式划分职责:

  1. AIR 客户端向业务服务器请求创建订单。
  2. PHP 服务端调用支付宝官方接口,生成支付页面 URL。
  3. AIR 使用 StageWebView 打开该 URL。
  4. 支付宝通过异步通知将支付结果发送给 PHP 服务端。
  5. 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。

StageWebView 的 Cookie 环境不一定与系统浏览器或支付宝 App 共享,用户可能需要重新登录。支付完成后,也不能假定 WebView 一定能顺利返回应用。

用户可能中途关闭页面

用户关闭页面、返回应用,或者支付页面显示异常,都不代表交易失败。客户端应保存订单号,并允许用户重新查询订单状态。

商店审核风险

游戏 add-on 属于数字商品时,使用第三方支付可能违反发布平台的支付规则。能够在技术上打开支付宝页面,并不代表应用一定可以通过审核。

安全注意事项

  • 全程使用 HTTPS,并验证服务器证书。
  • 商品价格必须由服务端计算,不能接受客户端提交的最终金额。
  • 每次支付都要使用唯一且不可预测的业务订单号。
  • 不要把支付宝商户私钥打包进 SWF、APK、IPA 或 ANE。
  • 不要信任 return_url 中的查询参数,也不要仅凭页面文字判断支付成功。
  • 必须验证支付宝异步通知的签名、订单号、金额、商户身份和交易状态。
  • 支付通知处理和权益发放应放在同一事务或可靠的幂等流程中。
  • AIR 客户端应查询服务端记录的订单或权益状态,而不是直接查询本地支付结果。
  • 日志中不要记录私钥、完整签名、登录凭据或其他敏感支付信息。

PHP + StageWebView 可以用于实现网页收银台,尤其是在没有可用 ANE 的情况下。正式上线前,必须确认支付宝产品是否支持 WebView,并核对应用商店的支付政策。如果需要原生支付宝 App 支付体验,应使用官方原生 SDK,再通过 ANE 将相关能力暴露给 AS3。

备注:内容仅供参考。