领券事件回调通知(JSON)
领券完成后,微信会把相关领券结果和用户信息发送给商户,商户需要接收处理,并按照文档规范返回应答。出于安全的考虑,我们对支付结果数据进行了加密,商户需要先对通知数据进行解密,才能得到支付结果数据。
注意:
- 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
- 特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
- 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为60s/次 - 总计11次 )
请求参数 | 类型 | 描述 |
---|---|---|
headers | object | 通知的头参数 |
Content-Type | string | application/json |
Request-ID | string | 通知的唯一标识 |
Wechatpay-Nonce | string | 数据签名使用的随机串 |
Wechatpay-Serial | string | 平台证书序列号/平台公钥ID |
Wechatpay-Signature | string | 签名串 |
Wechatpay-Signature-Type | string | 签名算法WECHATPAY2-SHA256-RSA2048 枚举值 |
Wechatpay-Timestamp | string | 时间戳 |
body | object | 通知的JSON 数据结构 |
id | string | 通知的唯一ID |
create_time | string | 通知创建的时间 |
event_type | string | 通知的类型COUPON.SEND 枚举值 |
resource_type | string | 通知的资源数据类型 |
summary | string | 回调摘要 |
resource | object | 通知资源数据 |
algorithm | string | 对数据进行加密的加密算法AEAD_AES_256_GCM 枚举值 |
associated_data | string | 数据加密的附加数据 |
nonce | string | 加密使用的随机串 |
ciphertext | string | 加密后的密文数据 |
original_type | string | 原始回调类型 |
event_type | string | 业务细分事件类型EVENT_TYPE_BUSICOUPON_SEND 枚举值 |
coupon_code | string | 券的唯一标识。 |
stock_id | string | 批次号 |
send_time | string | 发放时间 |
openid | string | 微信用户在AppID下的唯一标识。 |
unionid | string | 微信用户在同一个微信开放平台账号下的唯一用户标识 |
send_channel | string | 发放渠道BUSICOUPON_SEND_CHANNEL_MINIAPP | BUSICOUPON_SEND_CHANNEL_API | BUSICOUPON_SEND_CHANNEL_PAYGIFT | BUSICOUPON_SEND_CHANNEL_H5 | BUSICOUPON_SEND_CHANNEL_FTOF | BUSICOUPON_SEND_CHANNEL_MEMBERCARD_ACT | BUSICOUPON_SEND_CHANNEL_HALL | BUSICOUPON_SEND_CHANNEL_JSAPI | BUSICOUPON_SEND_CHANNEL_MINI_APP_LIVE | BUSICOUPON_SEND_CHANNEL_WECHAT_SEARCH | BUSICOUPON_SEND_CHANNEL_PAY_HAS_DISCOUNT | BUSICOUPON_SEND_CHANNEL_WECHAT_AD | BUSICOUPON_SEND_CHANNEL_RIGHTS_PLATFORM | BUSICOUPON_SEND_CHANNEL_RECEIVE_MONEY_GIFT | BUSICOUPON_SEND_CHANNEL_MEMBER_PAY_RIGHT | BUSICOUPON_SEND_CHANNEL_BUSI_SMART_RETAIL | BUSICOUPON_SEND_CHANNEL_FINDER_LIVEROOM 枚举值之一 |
send_merchant | string | 发券商户号 |
attach_info | object | 仅在支付有礼、扫码领券(营销馆)、会员有礼发放渠道,才有该信息 |
transaction_id | string | 仅在支付有礼渠道,才有该信息,对应支付有礼曝光支付订单编号信息 |
act_code | string | 仅在扫码领券(营销馆)渠道,才有该信息,对应领券的营销馆领券活动ID信息信息。该字段并不会和支付有礼活动编码(act_code)同时出现。 |
hall_code | string | 仅在扫码领券(营销馆)渠道,才有该信息,对应领券的营销馆ID信息 |
hall_belong_mch_id | number | 仅在扫码领券(营销馆)渠道,才有该信息,对应领券的营销馆所属商户号信息 |
card_id | string | 仅在会员卡活动渠道,才有该信息,对应会员卡Card_ID信息 |
code | string | 仅在会员卡活动渠道,才有该信息,对应用户卡包会员卡Code信息 |
activity_id | string | 仅在会员卡活动渠道,才有该信息,对应会员有礼活动ID信息 |
php
function webhookProcessor(\Psr\Http\Message\RequestInterface $request,
array $platformPublicKeyMap, string $apiv3Key): array {
if (!\count($platformPublicKeyMap)) {
throw new \WeChatPay\Exception\InvalidArgumentException('平台证书或者平台公钥数组不能为空');
}
if (\strlen($apiv3Key) !== 32) {
throw new \WeChatPay\Exception\InvalidArgumentException('APIV3密钥为32字节,长度不对');
}
if (!($request->hasHeader(\WeChatPay\WechatpayNonce)
&& $request->hasHeader(\WeChatPay\WechatpaySerial)
&& $request->hasHeader(\WeChatPay\WechatpaySignature)
&& $request->hasHeader(\WeChatPay\WechatpayTimestamp))) {
throw new \WeChatPay\Exception\InvalidArgumentException('通知的头参数缺失必要参数');
}
// 检查通知的时间偏移量,允许5分钟之内的偏移
[$inWechatpayTimestamp] = $request->getHeader(\WeChatPay\WechatpayTimestamp);
if (\WeChatPay\MAXIMUM_CLOCK_OFFSET < \abs(
\WeChatPay\Formatter::timestamp() - (int)$inWechatpayTimestamp)) {
throw new \WeChatPay\Exception\InvalidArgumentException('通知头参数的时间偏移量超过可信阈值');
}
// 检查通知的平台证书/平台公钥实例是否存在
[$inWechatpaySerial] = $request->getHeader(\WeChatPay\WechatpaySerial);
if (!\array_key_exists($inWechatpaySerial, $platformPublicKeyMap)) {
throw new \WeChatPay\Exception\InvalidArgumentException('通知头参数的证书序列号/公钥ID本地不存在');
}
// 验证通知的数据签名
[$inWechatpaySignature] = $request->getHeader(\WeChatPay\WechatpaySignature);
[$inWechatpayNonce] = $request->getHeader(\WeChatPay\WechatpayNonce);
$inBody = (string)$request->getBody();
if (!\WeChatPay\Crypto\Rsa::verify(
\WeChatPay\Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature, $platformPublicKeyMap[$inWechatpaySerial]
)) {
throw new \WeChatPay\Exception\InvalidArgumentException('通知头参数的数据签名校验未通过');
}
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)\json_decode($inBody, true);
// 使用PHP7+的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密,有可能解密异常(eg: 平台探测流量)会抛 \UnexpectedValueException
$inBodyResource = \WeChatPay\Crypto\AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)\json_decode($inBodyResource, true);
// 把解密后的数组定义为'original_data'函数返回
$inBodyArray['resource']['original_data'] = $inBodyResourceArray;
return [
'headers' => $request->getHeaders(),
'body' => $inBodyArray
];
}
// do your business
// ...
// ...
$json = \json_encode([
'code' => 'SUCCESS',
'message' => 'OK'
]);
应答规范 | 类型 | 描述 |
---|---|---|
status | number | HTTP 状态码20X 4XX 5XX 枚举值之一 |
body | object | 应答的JSON 数据结构 |
code | string | 业务处理状态码SUCCESS | FAIL 枚举值之一 |
message | string | 业务处理附加信息 |
参阅