cn.felord.payment.wechat.v3.WechatPayCallback Maven / Gradle / Ivy
/*
*
* Copyright 2019-2020 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
import cn.felord.payment.wechat.v3.model.*;
import cn.felord.payment.wechat.v3.model.busifavor.BusiFavorReceiveConsumeData;
import cn.felord.payment.wechat.v3.model.combine.CombineTransactionConsumeData;
import cn.felord.payment.wechat.v3.model.discountcard.DiscountCardAcceptedConsumeData;
import cn.felord.payment.wechat.v3.model.discountcard.DiscountCardAgreementEndConsumeData;
import cn.felord.payment.wechat.v3.model.discountcard.DiscountCardConsumer;
import cn.felord.payment.wechat.v3.model.discountcard.DiscountCardUserPaidConsumeData;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreConsumer;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserConfirmConsumeData;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserPaidConsumeData;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserPermissionConsumeData;
import cn.felord.payment.wechat.v3.model.profitsharing.ProfitsharingConsumeData;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
* 微信支付回调工具.
*
* 注意:开发者应该保证回调调用的幂等性
*
* 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Slf4j
public class WechatPayCallback {
/**
* The constant MAPPER.
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* The Signature provider.
*/
private final SignatureProvider signatureProvider;
/**
* The Tenant id.
*/
private final String tenantId;
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
.registerModule(new JavaTimeModule());
}
/**
* Instantiates a new Wechat pay callback.
*
* @param signatureProvider the signature provider
* @param tenantId the tenant id
*/
public WechatPayCallback(SignatureProvider signatureProvider, String tenantId) {
this.signatureProvider = signatureProvider;
Assert.hasText(tenantId, "tenantId is required");
this.tenantId = tenantId;
}
/**
* 微信支付分账V2回调.
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
* @since 1.0.10.RELEASE
*/
@SneakyThrows
public Map profitSharingCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
ProfitSharingConsumeData consumeData = MAPPER.readValue(data, ProfitSharingConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信支付代金券核销回调.
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map couponCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
String data = this.callback(params, EventType.COUPON_USE);
CouponConsumeData consumeData = MAPPER.readValue(data, CouponConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信支付成功回调,在1.0.8.RELEASE时支持服务商模式支付回调通知
*
* 无需开发者判断,只有扣款成功微信才会回调此接口
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map transactionCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
TransactionConsumeData consumeData = MAPPER.readValue(data, TransactionConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信合单支付成功回调.
*
* 无需开发者判断,只有扣款成功微信才会回调此接口
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map combineTransactionCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
CombineTransactionConsumeData consumeData = MAPPER.readValue(data, CombineTransactionConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信支付分确认订单、支付成功回调通知.
*
* 该链接是通过商户 创建支付分订单 提交notify_url参数,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例: “https://pay.weixin.qq.com/wxpay/pay.action”
*
* @param params the params
* @param payScoreConsumer the pay score consumer
* @return the map
* @since 1.0.2.RELEASE
*/
@SneakyThrows
public Map payscoreUserOrderCallback(ResponseSignVerifyParams params, PayScoreConsumer payScoreConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
if (Objects.equals(eventType, EventType.PAYSCORE_USER_CONFIRM.event)) {
String data = this.decrypt(callbackParams);
PayScoreUserConfirmConsumeData confirmConsumeData = MAPPER.readValue(data, PayScoreUserConfirmConsumeData.class);
payScoreConsumer.getConfirmConsumeDataConsumer().accept(confirmConsumeData);
} else if (Objects.equals(eventType, EventType.PAYSCORE_USER_PAID.event)) {
String data = this.decrypt(callbackParams);
PayScoreUserPaidConsumeData paidConsumeData = MAPPER.readValue(data, PayScoreUserPaidConsumeData.class);
payScoreConsumer.getPaidConsumeDataConsumer().accept(paidConsumeData);
} else {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
return response();
}
/**
* 授权/解除授权服务回调通知API.
*
* 微信支付分通过授权/解除授权服务通知接口将用户过授权/解除授权服务消息通知给商户
*
* 普通授权模式是通过[商户入驻配置申请表]提交service_notify_url设置,预授权模式是通过[商户预授权]提交的notify_url设置,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例: “https://pay.weixin.qq.com/wxpay/pay.action”
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
*/
@SneakyThrows
public Map permissionCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
boolean closed;
if (Objects.equals(eventType, EventType.PAYSCORE_USER_OPEN.event)) {
closed = false;
} else if (Objects.equals(eventType, EventType.PAYSCORE_USER_CLOSE.event)) {
closed = true;
} else {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
String data = this.decrypt(callbackParams);
PayScoreUserPermissionConsumeData consumeData = MAPPER.readValue(data, PayScoreUserPermissionConsumeData.class);
consumeData.setClosed(closed);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 用户领卡、守约状态变化、扣费状态变化通知API
*
* 用户领取优惠卡后或者用户守约状态发生变更后或扣费状态变化后,微信会把对应信息发送给商户。
*
* 该链接是通过商户预受理领卡请求中提交notify_url参数,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例: “https://pay.weixin.qq.com/wxpay/pay.action”
*
* @param params the params
* @param discountCardConsumer the discount card consumer
* @return the map
*/
@SneakyThrows
public Map discountCardCallback(ResponseSignVerifyParams params, DiscountCardConsumer discountCardConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
if (Objects.equals(eventType, EventType.DISCOUNT_CARD_AGREEMENT_ENDED.event)) {
String data = this.decrypt(callbackParams);
DiscountCardAgreementEndConsumeData agreementEndConsumeData = MAPPER.readValue(data, DiscountCardAgreementEndConsumeData.class);
discountCardConsumer.getAgreementEndConsumeDataConsumer().accept(agreementEndConsumeData);
} else if (Objects.equals(eventType, EventType.DISCOUNT_CARD_USER_ACCEPTED.event)) {
String data = this.decrypt(callbackParams);
DiscountCardAcceptedConsumeData acceptedConsumeData = MAPPER.readValue(data, DiscountCardAcceptedConsumeData.class);
discountCardConsumer.getAcceptedConsumeDataConsumer().accept(acceptedConsumeData);
} else if (Objects.equals(eventType, EventType.DISCOUNT_CARD_USER_PAID.event)) {
String data = this.decrypt(callbackParams);
DiscountCardUserPaidConsumeData paidConsumeData = MAPPER.readValue(data, DiscountCardUserPaidConsumeData.class);
discountCardConsumer.getCardUserPaidConsumeDataConsumer().accept(paidConsumeData);
} else {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
return response();
}
/**
* 商家券领券事件回调通知API
*
* 领券完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并按照文档规范返回应答。出于安全的考虑,我们对支付结果数据进行了加密,商户需要先对通知数据进行解密,才能得到支付结果数据。
*
* 该链接是通过商户设置商家券事件通知地址API中提交notify_url参数,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例: “https://pay.weixin.qq.com/wxpay/pay.action”
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
*/
@SneakyThrows
public Map busiFavorReceiveCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
if (!Objects.equals(eventType, EventType.COUPON_SEND.event)) {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
String data = this.decrypt(callbackParams);
BusiFavorReceiveConsumeData consumeData = MAPPER.readValue(data, BusiFavorReceiveConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 退款结果通知API
*
* 退款状态改变后,微信会把相关退款结果发送给商户。
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return map map
*/
@SneakyThrows
public Map refundCallback(ResponseSignVerifyParams params, Consumer consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
if (!(Objects.equals(eventType, EventType.REFUND_CLOSED.event) ||
Objects.equals(eventType, EventType.REFUND_ABNORMAL.event) ||
Objects.equals(eventType, EventType.REFUND_SUCCESS.event))) {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
String data = this.decrypt(callbackParams);
RefundConsumeData consumeData = MAPPER.readValue(data, RefundConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信支付分账V3动账通知
*
* @param params the params
* @param profitsharingConsumeDataConsumer the profitsharing consume data consumer
* @return map map
*/
@SneakyThrows
public Map profitsharingCallback(ResponseSignVerifyParams params, Consumer profitsharingConsumeDataConsumer) {
String callback = this.callback(params, EventType.TRANSACTION);
ProfitsharingConsumeData consumeData = MAPPER.readValue(callback, ProfitsharingConsumeData.class);
profitsharingConsumeDataConsumer.accept(consumeData);
return response();
}
/**
* Callback.
*
* @param params the params
* @param eventType the event type
* @return the string
*/
@SneakyThrows
private String callback(ResponseSignVerifyParams params, EventType eventType) {
CallbackParams callbackParams = this.resolve(params);
if (!Objects.equals(callbackParams.getEventType(), eventType.event)) {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
return this.decrypt(callbackParams);
}
/**
* Resolve callback params.
*
* @param params the params
* @return the callback params
* @throws JsonProcessingException the json processing exception
* @since 1.0.2.RELEASE
*/
private CallbackParams resolve(ResponseSignVerifyParams params) throws JsonProcessingException {
if (signatureProvider.responseSignVerify(params)) {
return MAPPER.readValue(params.getBody(), CallbackParams.class);
}
throw new PayException("invalid wechat pay callback");
}
/**
* Decrypt.
*
* @param callbackParams the callback params
* @return the string
* @since 1.0.2.RELEASE
*/
private String decrypt(CallbackParams callbackParams) {
CallbackParams.Resource resource = callbackParams.getResource();
String associatedData = resource.getAssociatedData();
String nonce = resource.getNonce();
String ciphertext = resource.getCiphertext();
String data = signatureProvider.decryptResponseBody(tenantId, associatedData, nonce, ciphertext);
Assert.hasText(data, "decryptData is required");
return data;
}
/**
* 回调应答
*
* @return response
*/
private Map response() {
Map responseBody = new HashMap<>(2);
responseBody.put("code", "SUCCESS");
responseBody.put("message", "SUCCESS");
return responseBody;
}
/**
* 事件类型用于处理回调.
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
enum EventType {
/**
* 微信支付分确认订单事件.
*
* @since 1.0.2.RELEASE
*/
PAYSCORE_USER_CONFIRM("PAYSCORE.USER_CONFIRM"),
/**
* 微信支付分用户支付成功订单事件.
*
* @since 1.0.2.RELEASE
*/
PAYSCORE_USER_PAID("PAYSCORE.USER_PAID"),
/**
* 微信支付分授权事件.
*
* @since 1.0.2.RELEASE
*/
PAYSCORE_USER_OPEN("PAYSCORE.USER_OPEN_SERVICE"),
/**
* 微信支付分解除授权事件.
*
* @since 1.0.2.RELEASE
*/
PAYSCORE_USER_CLOSE("PAYSCORE.USER_CLOSE_SERVICE"),
/**
* 用户领取微信先享卡事件.
*
* @since 1.0.2.RELEASE
*/
DISCOUNT_CARD_USER_ACCEPTED("DISCOUNT_CARD.USER_ACCEPTED"),
/**
* 微信先享卡守约状态变化事件.
*
* @since 1.0.2.RELEASE
*/
DISCOUNT_CARD_AGREEMENT_ENDED("DISCOUNT_CARD.AGREEMENT_ENDED"),
/**
* 微信先享卡扣费状态变化事件.
*
* @since 1.0.2.RELEASE
*/
DISCOUNT_CARD_USER_PAID("DISCOUNT_CARD.USER_PAID"),
/**
* 优惠券核销事件.
*
* 代金券
*
* @since 1.0.0.RELEASE
*/
COUPON_USE("COUPON.USE"),
/**
* 优惠券领券事件.
*
* 商家券
*
* @since 1.0.0.RELEASE
*/
COUPON_SEND("COUPON.SEND"),
/**
* 支付成功、分账、分账回退事件.
*
* @since 1.0.0.RELEASE
*/
TRANSACTION("TRANSACTION.SUCCESS"),
/**
* 退款成功事件.
*
* @since 1.0.6.RELEASE
*/
REFUND_SUCCESS("REFUND.SUCCESS"),
/**
* 退款异常事件.
*
* @since 1.0.6.RELEASE
*/
REFUND_ABNORMAL("REFUND.ABNORMAL"),
/**
* 退款关闭事件.
*
* @since 1.0.6.RELEASE
*/
REFUND_CLOSED("REFUND.CLOSED");
/**
* The Event.
*/
private final String event;
/**
* Instantiates a new Event type.
*
* @param event the event
*/
EventType(String event) {
this.event = event;
}
}
}