Skip to content

Cell - Alipay Component ​

Add @celljs/alipay to the dependencies to take payments with Alipay (支付宝): an injectable AlipayClient signs gateway requests (RSA2), verifies the signatures of the answers and builds the page, WAP and App payments, and a notification endpoint verifies asynchronous notifications and hands them to your handlers. Public key mode and certificate mode are supported. It uses Node's crypto and fetch, with no third-party dependency.

yaml
# cell.yml: configuration is server-only unless a schema marks it public; take credentials from the environment
alipay:
  appId: ${env.ALIPAY_APP_ID}
  privateKey: ${env.ALIPAY_PRIVATE_KEY}             # 应用私钥: PEM, base64 (PKCS#8 or PKCS#1) or a file path
  alipayPublicKey: ${env.ALIPAY_PUBLIC_KEY}         # 支付宝公钥 (public key mode)
  notifyUrl: https://shop.example.com/alipay/notify
  # gateway: https://openapi-sandbox.dl.alipaydev.com/gateway.do   # the sandbox
ts
import { Autowired } from '@celljs/core';
import { Body, Controller, Post } from '@celljs/mvc/lib/node';
import { AlipayClient, AlipayNotification, AlipayNotificationHandler } from '@celljs/alipay';

@Controller('orders')
export class OrderController {
    @Autowired(AlipayClient)
    protected readonly alipay: AlipayClient;

    @Post('page')        // 电脑网站支付: redirect the browser to the URL
    page(@Body() order: { id: string }) {
        return { url: this.alipay.pagePay({ out_trade_no: order.id, total_amount: '88.88', subject: 'Cell T-shirt' }, { return_url: 'https://shop.example.com/paid' }) };
    }

    @Post('qr')          // 当面付扫码: show qr_code as a QR code
    async qr(@Body() order: { id: string }) {
        const { qr_code } = await this.alipay.precreate({ out_trade_no: order.id, total_amount: '88.88', subject: 'Cell T-shirt' });
        return { qr_code };
    }
}

@AlipayNotificationHandler(['TRADE_SUCCESS', 'TRADE_FINISHED'])
export class PaidHandler implements AlipayNotificationHandler {
    async handle(notification: AlipayNotification) {
        // out_trade_no, trade_no, total_amount... as Alipay sent them, signature verified.
        // Mark the order paid once: the same notification can come again.
    }
}

Client ​

biz_content uses the field names of Alipay's documentation (snake_case); amounts are strings in yuan ('88.88'). Payment methods default notify_url to alipay.notifyUrl; the second argument sets other common parameters (return_url, app_auth_token...).

MethodAPI
pagePay(request, { httpMethod?, return_url? })alipay.trade.page.pay (电脑网站支付): a URL to redirect to, or with httpMethod: 'POST' an HTML form that submits itself
wapPay(request, options?)alipay.trade.wap.pay (手机网站支付), same results
appPay(request)alipay.trade.app.pay (App 支付): the order string for the App SDK
precreate(request) → { qr_code }alipay.trade.precreate (当面付扫码)
create(request) → { trade_no }alipay.trade.create (JSAPI / 小程序, buyer_id or buyer_open_id); the page calls my.tradePay({ tradeNO })
query(query) / close(query)alipay.trade.query / alipay.trade.close
refund(request) / queryRefund(query)alipay.trade.refund / alipay.trade.fastpay.refund.query; give out_request_no for partial refunds
billDownloadUrl({ bill_type, bill_date })alipay.data.dataservice.bill.downloadurl.query
execute(method, biz?, options?)Any other API: resolves with <method>_response
pageExecute(method, biz?, options?) / sdkExecute(method, biz?, options?)Any other browser or App SDK API
verifyNotification(params)Verifies a notification received on a route of your own

Answers whose code is not 10000 reject with CELL_ALIPAY_API_ERROR; detail is the answer (sub_code: 'ACQ.TRADE_NOT_EXIST'...). The signature of an answer is checked over its JSON text as Alipay sent it.

Notifications ​

The endpoint is POST /alipay/notify (alipay.notification.url, under server.path); set its absolute URL as notifyUrl. It runs before the body parser and authentication, verifies the signature of the form and that app_id is this application's, then runs the @AlipayNotificationHandler components of its trade_status (WAIT_BUYER_PAY, TRADE_SUCCESS, TRADE_FINISHED, TRADE_CLOSED; the notify_type for notifications without one), then those declared without types, one after the other. It answers success when they all succeed and fail otherwise (400 for a bad signature, 500 when a handler throws), so Alipay sends the notification again later: handlers must be idempotent. A partial refund comes as a notification with refund_fee and gmt_refund.

Notifications only say what happened: check total_amount against the order before shipping it.

Certificate mode ​

Applications created with certificates (公钥证书) give the three certificates instead of alipayPublicKey (PEM text or file paths):

yaml
alipay:
  appId: ${env.ALIPAY_APP_ID}
  privateKey: ${env.ALIPAY_PRIVATE_KEY}
  appCert: certs/appCertPublicKey_2021000000000001.crt
  alipayPublicCert: certs/alipayCertPublicKey_RSA2.crt
  alipayRootCert: certs/alipayRootCert.crt

Requests carry app_cert_sn and alipay_root_cert_sn computed as Alipay's SDKs do; an answer signed with another Alipay certificate is refused, so replace alipayPublicCert when Alipay renews it.

Configuration ​

yaml
alipay:
  gateway: https://openapi.alipay.com/gateway.do
  timeout: 10000               # milliseconds per request
  notification:
    enabled: true
    url: /alipay/notify

Errors ​

Failures are AlipayErrors with a stable code and a hint:

CodeWhen
CELL_ALIPAY_NOT_CONFIGUREDappId or privateKey is missing, or neither alipayPublicKey nor all three certificates are set
CELL_ALIPAY_API_ERRORThe answer's code is not 10000: detail.sub_code says why
CELL_ALIPAY_HTTP_ERRORThe gateway answered with an HTTP error or not with JSON
CELL_ALIPAY_SIGNATURE_INVALIDAn answer or a notification is not signed by Alipay
CELL_ALIPAY_APP_ID_MISMATCHA notification is for another application

Content encryption (encrypt_key, AES) is not supported.