Skip to content

Cell - 微信支付组件 ​

在依赖中添加 @celljs/wechat-pay 以使用 微信支付 API v3 接收支付。一个可注入的 WechatPayClient 对请求进行签名并验证响应的签名,而通知端点则验证、解密支付和退款通知并交给你的处理器处理。它使用 Node 的 crypto 和 fetch,没有第三方依赖。

yaml
# cell.yml: 配置默认仅限服务端;从环境变量获取凭证
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: 其 PEM 文本或文件路径
  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')      // 扫码支付: 将 code_url 显示为二维码
    async native(@Body() order: { id: string }) {
        return this.wechatPay.createNativeOrder({ description: 'Cell T-shirt', out_trade_no: order.id, amount: { total: 100 } }); // 单位: 分
    }

    @Post('jsapi')       // 小程序 / 公众号: 页面调用 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 已解密: out_trade_no, transaction_id, trade_state, amount.total...
        // 仅标记一次订单已支付: 同一通知可能多次到达
    }
}

客户端 ​

接口使用微信支付文档中的字段名(蛇形命名),其文档可直接应用。appid、mchid 和 notify_url 默认取自配置。

方法API
createJsapiOrder(order) → { prepay_id }JSAPI / 小程序下单; 然后用 jsapiParams(prepayId, appId?) 获取 wx.requestPayment / getBrandWCPayRequest 的参数
createAppOrder(order) → { prepay_id }App 下单; 然后用 appParams(prepayId, appId?) 获取 App SDK 所需参数
createNativeOrder(order) → { code_url }Native 扫码下单
createH5Order(order) → { h5_url }H5 下单(scene_info.h5_info 必需)
queryOrder({ out_trade_no } | { transaction_id })查询订单
closeOrder(outTradeNo)关闭订单
refund(request) / queryRefund(outRefundNo)申请退款 / 查询单笔退款
tradeBill(query) / fundFlowBill(query),然后 download(download_url)交易账单 / 资金账单
request(method, path, body?, { query, headers })其他任何 API,已签名并已验证
parseNotification(headers, rawBody)验证并解密在你自己的路由上收到的通知

WechatPayUtil 提供基本操作:signMessage、verifyMessage、decrypt(AEAD_AES_256_GCM)、encrypt(RSA-OAEP,用于敏感字段;在 headers 中用 Wechatpay-Serial 发送密钥序列号)。

通知 ​

端点是 POST /wechat-pay/notify(wechatPay.notification.url,在 server.path 下);将其绝对 URL 设置为 notifyUrl。它在请求体解析和认证之前运行,按原样读取请求体,检查签名和时间戳是否在 tolerance 秒内(默认 300),解密资源并按事件类型(TRANSACTION.SUCCESS、REFUND.SUCCESS、REFUND.ABNORMAL、REFUND.CLOSED)运行对应的 @WechatPayNotificationHandler 组件,然后运行没有指定类型的处理器(依次执行)。返回:

  • 204:所有处理器成功执行(或没有处理器处理该事件类型:记录为警告);
  • 401 {"code":"FAIL"}:签名或时间戳错误;
  • 500 {"code":"FAIL"}:处理器抛出异常,微信支付会在稍后重新发送通知,所以处理器必须是幂等的。

通知仅说明发生的事情:对于金额重要的订单,要对比 resource.amount.total 和订单,或调用 queryOrder。

验证微信支付的密钥 ​

2024 年后创建的商户有一个微信支付公钥(publicKeyId + publicKey)。较早的商户用平台证书验证:不设置 publicKey,客户端会在签名中的序列号未知时(至多一分钟一次)从 /v3/certificates 下载,或者给 platformCertificates 提供(PEM,可单个或列表)。迁移期间两者都可设置:Wechatpay-Serial 中的序列号选择密钥。

配置 ​

yaml
wechatPay:
  baseUrl: https://api.mch.weixin.qq.com
  timeout: 10000               # 毫秒,每个请求的超时
  notification:
    enabled: true
    url: /wechat-pay/notify
    tolerance: 300             # 秒

错误 ​

失败是 WechatPayError,有一个稳定的 code 和 hint:

代码何时
CELL_WECHAT_PAY_NOT_CONFIGUREDmchId、serialNo、privateKey、apiV3Key 或 AppID 缺失
CELL_WECHAT_PAY_API_ERROR微信支付返回错误:status 和 detail.code(ORDER_NOT_EXIST、PARAM_ERROR 等)
CELL_WECHAT_PAY_SIGNATURE_INVALID响应或通知未由微信支付签名
CELL_WECHAT_PAY_TIMESTAMP_EXPIRED通知的时间戳超出 tolerance 范围
CELL_WECHAT_PAY_UNKNOWN_SERIAL没有公钥或平台证书持有签名中的序列号
CELL_WECHAT_PAY_DECRYPT_FAILED资源无法解密:apiV3Key 错误

微信支付 API v3 没有沙箱:用真实商户和小额测试,并在开发期间用隧道暴露通知端点。