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.
# 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 sandboximport { 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...).
| Method | API |
|---|---|
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):
alipay:
appId: ${env.ALIPAY_APP_ID}
privateKey: ${env.ALIPAY_PRIVATE_KEY}
appCert: certs/appCertPublicKey_2021000000000001.crt
alipayPublicCert: certs/alipayCertPublicKey_RSA2.crt
alipayRootCert: certs/alipayRootCert.crtRequests 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
alipay:
gateway: https://openapi.alipay.com/gateway.do
timeout: 10000 # milliseconds per request
notification:
enabled: true
url: /alipay/notifyErrors
Failures are AlipayErrors with a stable code and a hint:
| Code | When |
|---|---|
CELL_ALIPAY_NOT_CONFIGURED | appId or privateKey is missing, or neither alipayPublicKey nor all three certificates are set |
CELL_ALIPAY_API_ERROR | The answer's code is not 10000: detail.sub_code says why |
CELL_ALIPAY_HTTP_ERROR | The gateway answered with an HTTP error or not with JSON |
CELL_ALIPAY_SIGNATURE_INVALID | An answer or a notification is not signed by Alipay |
CELL_ALIPAY_APP_ID_MISMATCH | A notification is for another application |
Content encryption (encrypt_key, AES) is not supported.