Cell - 支付宝组件
在依赖中添加 @celljs/alipay 以使用 支付宝 接收支付:一个可注入的 AlipayClient 对网关请求进行签名(RSA2)、验证响应的签名并构建页面、WAP 和 App 支付,而通知端点则验证异步通知并交给你的处理器处理。支持公钥模式和证书模式。它使用 Node 的 crypto 和 fetch,没有第三方依赖。
# cell.yml: 配置默认仅限服务端;从环境变量获取凭证
alipay:
appId: ${env.ALIPAY_APP_ID}
privateKey: ${env.ALIPAY_PRIVATE_KEY} # 应用私钥: PEM、base64(PKCS#8 或 PKCS#1)或文件路径
alipayPublicKey: ${env.ALIPAY_PUBLIC_KEY} # 支付宝公钥(公钥模式)
notifyUrl: https://shop.example.com/alipay/notify
# gateway: https://openapi-sandbox.dl.alipaydev.com/gateway.do # 沙箱环境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') // 电脑网站支付: 重定向浏览器到该 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') // 当面付扫码: 显示 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... 如支付宝发送,签名已验证
// 仅标记一次订单已支付:同一通知可能多次到达
}
}客户端
biz_content 使用支付宝文档中的字段名(蛇形命名);金额是元的字符串('88.88')。支付方法默认 notify_url 为 alipay.notifyUrl;第二个参数设置其他常见参数(return_url、app_auth_token 等)。
| 方法 | API |
|---|---|
pagePay(request, { httpMethod?, return_url? }) | alipay.trade.page.pay(电脑网站支付):重定向到的 URL,或带 httpMethod: 'POST' 时是自提交的 HTML 表单 |
wapPay(request, options?) | alipay.trade.wap.pay(手机网站支付),结果相同 |
appPay(request) | alipay.trade.app.pay(App 支付):App SDK 的订单字符串 |
precreate(request) → { qr_code } | alipay.trade.precreate(当面付扫码) |
create(request) → { trade_no } | alipay.trade.create(JSAPI / 小程序,buyer_id 或 buyer_open_id);页面调用 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;部分退款时给 out_request_no |
billDownloadUrl({ bill_type, bill_date }) | alipay.data.dataservice.bill.downloadurl.query |
execute(method, biz?, options?) | 其他任何 API:返回 <method>_response |
pageExecute(method, biz?, options?) / sdkExecute(method, biz?, options?) | 其他任何浏览器或 App SDK API |
verifyNotification(params) | 验证在你自己的路由上收到的通知 |
响应的 code 不是 10000 时会用 CELL_ALIPAY_API_ERROR 拒绝;detail 是响应(sub_code: 'ACQ.TRADE_NOT_EXIST' 等)。响应的签名在其 JSON 文本上检查,如支付宝发送的。
通知
端点是 POST /alipay/notify(alipay.notification.url,在 server.path 下);将其绝对 URL 设置为 notifyUrl。它在请求体解析和认证之前运行,验证表单的签名和 app_id 是否是该应用的,然后按 trade_status(WAIT_BUYER_PAY、TRADE_SUCCESS、TRADE_FINISHED、TRADE_CLOSED;没有时用 notify_type)运行对应的 @AlipayNotificationHandler 组件,再运行没有指定类型的处理器(依次执行)。当所有处理器都成功时返回 success,否则返回 fail(签名错误时 400,处理器抛出异常时 500),所以支付宝会在稍后重新发送通知:处理器必须是幂等的。部分退款作为通知到达,带有 refund_fee 和 gmt_refund。
通知仅说明发生的事情:发货前检查 total_amount 是否与订单相符。
证书模式
用证书创建的应用(公钥证书)给三个证书而不是 alipayPublicKey(PEM 文本或文件路径):
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请求携带 app_cert_sn 和 alipay_root_cert_sn,如支付宝 SDK 计算的;用另一支付宝证书签名的响应被拒,所以当支付宝更新时替换 alipayPublicCert。
配置
alipay:
gateway: https://openapi.alipay.com/gateway.do
timeout: 10000 # 毫秒,每个请求的超时
notification:
enabled: true
url: /alipay/notify错误
失败是 AlipayError,有一个稳定的 code 和 hint:
| 代码 | 何时 |
|---|---|
CELL_ALIPAY_NOT_CONFIGURED | appId 或 privateKey 缺失,或既没有 alipayPublicKey 也没有全部三个证书 |
CELL_ALIPAY_API_ERROR | 响应的 code 不是 10000:detail.sub_code 说明原因 |
CELL_ALIPAY_HTTP_ERROR | 网关返回 HTTP 错误或不是 JSON |
CELL_ALIPAY_SIGNATURE_INVALID | 响应或通知未由支付宝签名 |
CELL_ALIPAY_APP_ID_MISMATCH | 通知是给另一应用的 |
不支持内容加密(encrypt_key、AES)。