Cell - 微信支付组件
在依赖中添加 @celljs/wechat-pay 以使用 微信支付 API v3 接收支付。一个可注入的 WechatPayClient 对请求进行签名并验证响应的签名,而通知端点则验证、解密支付和退款通知并交给你的处理器处理。它使用 Node 的 crypto 和 fetch,没有第三方依赖。
# 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/notifyimport { 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 中的序列号选择密钥。
配置
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_CONFIGURED | mchId、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 没有沙箱:用真实商户和小额测试,并在开发期间用隧道暴露通知端点。