Skip to content

Cell - WeChat Pay Component ​

Add @celljs/wechat-pay to the dependencies to take payments with WeChat Pay API v3 (微信支付): an injectable WechatPayClient signs the requests and verifies the signatures of the answers, and a notification endpoint verifies, decrypts and hands payment and refund notifications to your handlers. 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
wechatPay:
  mchId: ${env.WECHAT_PAY_MCH_ID}                 # 商户号
  appId: ${env.WECHAT_PAY_APP_ID}                 # 公众号 / 小程序 / App 的 AppID
  serialNo: ${env.WECHAT_PAY_SERIAL_NO}           # 商户 API 证书序列号
  privateKey: ${env.WECHAT_PAY_PRIVATE_KEY}       # apiclient_key.pem: its PEM text or its path
  apiV3Key: ${env.WECHAT_PAY_API_V3_KEY}          # APIv3 密钥
  publicKeyId: ${env.WECHAT_PAY_PUBLIC_KEY_ID}    # 微信支付公钥 ID: PUB_KEY_ID_...
  publicKey: ${env.WECHAT_PAY_PUBLIC_KEY}         # 微信支付公钥, PEM
  notifyUrl: https://shop.example.com/wechat-pay/notify
ts
import { Autowired } from '@celljs/core';
import { Body, Controller, Post } from '@celljs/mvc/lib/node';
import { WechatPayClient, WechatPayNotification, WechatPayNotificationHandler, WechatPayTransaction } from '@celljs/wechat-pay';

@Controller('orders')
export class OrderController {
    @Autowired(WechatPayClient)
    protected readonly wechatPay: WechatPayClient;

    @Post('native')      // 扫码支付: show code_url as a QR code
    async native(@Body() order: { id: string }) {
        return this.wechatPay.createNativeOrder({ description: 'Cell T-shirt', out_trade_no: order.id, amount: { total: 100 } }); // in fen: 1 yuan
    }

    @Post('jsapi')       // 小程序 / 公众号: the page calls wx.requestPayment(params)
    async jsapi(@Body() order: { id: string; openid: string }) {
        const { prepay_id } = await this.wechatPay.createJsapiOrder({ description: 'Cell T-shirt', out_trade_no: order.id, amount: { total: 100 }, payer: { openid: order.openid } });
        return this.wechatPay.jsapiParams(prepay_id);
    }
}

@WechatPayNotificationHandler('TRANSACTION.SUCCESS')
export class PaidHandler implements WechatPayNotificationHandler<WechatPayTransaction> {
    async handle({ resource }: WechatPayNotification<WechatPayTransaction>) {
        // resource is decrypted: out_trade_no, transaction_id, trade_state, amount.total...
        // Mark the order paid once: the same notification can come again.
    }
}

Client ​

The payloads use the field names of WeChat Pay's documentation (snake_case), so its pages apply as they are. appid, mchid and notify_url default to the configuration.

MethodAPI
createJsapiOrder(order) → { prepay_id }JSAPI / 小程序下单; then jsapiParams(prepayId, appId?) for wx.requestPayment / getBrandWCPayRequest
createAppOrder(order) → { prepay_id }App 下单; then appParams(prepayId, appId?) for the App SDK
createNativeOrder(order) → { code_url }Native 扫码下单
createH5Order(order) → { h5_url }H5 下单 (scene_info.h5_info required)
queryOrder({ out_trade_no } | { transaction_id })查询订单
closeOrder(outTradeNo)关闭订单
refund(request) / queryRefund(outRefundNo)申请退款 / 查询单笔退款
tradeBill(query) / fundFlowBill(query), then download(download_url)交易账单 / 资金账单
request(method, path, body?, { query, headers })Any other API, signed and verified
parseNotification(headers, rawBody)Verifies and decrypts a notification received on a route of your own

WechatPayUtil has the primitives: signMessage, verifyMessage, decrypt (AEAD_AES_256_GCM), encrypt (RSA-OAEP for sensitive fields; send the key's serial as Wechatpay-Serial in headers).

Notifications ​

The endpoint is POST /wechat-pay/notify (wechatPay.notification.url, under server.path); set its absolute URL as notifyUrl. It runs before the body parser and authentication, reads the body as sent, checks the signature and that the timestamp is within tolerance seconds (300), decrypts the resource and runs the @WechatPayNotificationHandler components of the event type (TRANSACTION.SUCCESS, REFUND.SUCCESS, REFUND.ABNORMAL, REFUND.CLOSED), then those declared without types, one after the other. It answers:

  • 204 when they all succeed (or when no handler takes the event type: logged as a warning);
  • 401 {"code":"FAIL"} when the signature or the timestamp is wrong;
  • 500 {"code":"FAIL"} when a handler throws: WeChat Pay sends the notification again later, so handlers must be idempotent.

Notifications only say what happened: for amounts that matter, check resource.amount.total against the order, or call queryOrder.

Keys that verify WeChat Pay ​

Merchants created since 2024 have a WeChat Pay public key (publicKeyId + publicKey). Older merchants verify with platform certificates (平台证书): leave publicKey out and the client downloads them from /v3/certificates when a signature names a serial it does not know (at most once a minute), or give them as platformCertificates (PEM, one or a list). Both can be set during a migration: the serial in Wechatpay-Serial selects the key.

Configuration ​

yaml
wechatPay:
  baseUrl: https://api.mch.weixin.qq.com
  timeout: 10000               # milliseconds per request
  notification:
    enabled: true
    url: /wechat-pay/notify
    tolerance: 300             # seconds

Errors ​

Failures are WechatPayErrors with a stable code and a hint:

CodeWhen
CELL_WECHAT_PAY_NOT_CONFIGUREDmchId, serialNo, privateKey, apiV3Key or the AppID is missing
CELL_WECHAT_PAY_API_ERRORWeChat Pay answered an error: status and detail.code (ORDER_NOT_EXIST, PARAM_ERROR...)
CELL_WECHAT_PAY_SIGNATURE_INVALIDAn answer or a notification is not signed by WeChat Pay
CELL_WECHAT_PAY_TIMESTAMP_EXPIREDA notification's timestamp is outside tolerance
CELL_WECHAT_PAY_UNKNOWN_SERIALNo public key or platform certificate has the serial of a signature
CELL_WECHAT_PAY_DECRYPT_FAILEDThe resource cannot be decrypted: wrong apiV3Key

WeChat Pay API v3 has no sandbox: test with a real merchant and small amounts, and expose the notification endpoint with a tunnel during development.