Appearance
微信支付通过支付通知接口将用户支付成功消息通知给商户,当用户支付成功时,一般不会让用户主动去查询是否支付成功,默认的处理方式应该是使用微信提供的接口回调来处理,即用户支付后通知我们支付结果。
TIP
• 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
• 如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。
DANGER
特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。
微信验签工具类
啥也不说了,直接复制即可,也没什么注意事项。
java
public class WechatPayValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPayValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String body;
protected final String requestId;
public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) {
this.verifier = verifier;
this.body = body;
this.requestId = requestId;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
validateParameters(request);
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, request.getHeader(REQUEST_ID));
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期应答
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
}
对称解密方法
创建 WxPayCallbackUtil 微信支付成功回调类,decryptFromResource用于解密微信
java
@Slf4j
public class WxPayCallbackUtil {
/**
* 对称解密
*/
private static String decryptFromResource(HashMap<String, Object> bodyMap, WxPayConfig wxPayConfig) {
// 通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
// 数据密文
String ciphertext = resourceMap.get("ciphertext");
// 随机串
String nonce = resourceMap.get("nonce");
// 附加数据
String associateData = resourceMap.get("associated_data");
AesUtil aesUtil = new AesUtil(wxPayConfig.getKey().getBytes(StandardCharsets.UTF_8));
try {
return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
} catch (GeneralSecurityException e) {
e.printStackTrace();
throw new DefaultException("解密失败");
}
}
}
封装微信返回的数据对象
微信返回给我们的数据是json格式的,我们需要将其转换成java对象,方便我们调用。
java
/**
* @author cv大魔王
* @version 1.0
* @description 微信支付成功回调返回的数据
* @date 2022/8/4
*/
@Data
@Slf4j
public class WxchatCallbackSuccessData {
/**
* 商户订单号
*/
private String orderId;
/**
* 微信支付系统生成的订单号
*/
private String transactionId;
/**
* 交易状态
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(付款码支付)
* USERPAYING:用户支付中(付款码支付)
* PAYERROR:支付失败(其他原因,如银行返回失败)
*/
private String tradestate;
/**
* 支付完成时间
*/
private Date successTime;
/**
* 交易类型
* JSAPI:公众号支付
* NATIVE:扫码支付
* APP:APP支付
* MICROPAY:付款码支付
* MWEB:H5支付
* FACEPAY:刷脸支付
*/
private String tradetype;
/**
* 订单总金额
*/
private BigDecimal totalMoney;
public Date getSuccessTime() {
return successTime;
}
public void setSuccessTime(String successTime) {
// Hutool工具包的方法,自动识别一些常用格式的日期字符串
this.successTime = DateUtil.parse(successTime);
}
}
支付成功回调使用方法
我们先不看回调是如何封装的,先来看使用方法。
java
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private Verifier verifier;
@ApiOperation("微信支付回调接口")
@PostMapping("/wx/callback")
public String courseNative(HttpServletRequest request, HttpServletResponse response) {
return WxPayCallbackUtil.wxPaySuccessCallback(request, response, verifier, wxPayConfig, callbackData -> {
// TODO 处理你的业务逻辑,下面说一下一般业务逻辑处理方法
log.info("微信支付返回的信息:{}", callbackData);
// 1.根据订单id获取订单信息
// 2.判断金额是否相符,如果不相符则调用退款接口,并取消该订单,通知客户支付金额不符
// 3.查询订单状态是否是未支付,如果是未支付则改为已支付,填充其他逻辑,
// 4.如果是其他状态综合你的业务逻辑来处理
// 5.如果是虚拟物品,则对应充值,等等其他逻辑
});
}
封装后就这么一句话,就获取到了微信返回给我们的数据,其中callbackData就是上面封装的WxchatCallbackSuccessData对象,我们只需要在回调方法中完成我们的业务逻辑即可。
支付成功回调方法封装
java
import java.util.function.Consumer;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.card.pay.domain.WxchatCallbackSuccessData;
import com.card.utils.HttpUtils;
import com.card.utils.WechatPayValidatorForRequest;
@Slf4j
public class WxPayCallbackUtil {
/**
* 微信支付创建订单回调方法
* @param verifier 证书
* @param wxPayConfig 微信配置
* @param businessCallback 回调方法,用于处理业务逻辑
* @return json格式的string数据,直接返回给微信
*/
public static String wxPaySuccessCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConfig wxPayConfig, Consumer<WxchatCallbackSuccessData> businessCallback) {
Gson gson = new Gson();
// 1.处理通知参数
final String body = HttpUtils.readData(request);
HashMap<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
// 2.签名验证
WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id"));
try {
if (!wechatForRequest.validate(request)) {
// 通知验签失败
response.setStatus(500);
final HashMap<String, Object> map = new HashMap<>();
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
} catch (IOException e) {
e.printStackTrace();
}
// 3.获取明文数据
String plainText = decryptFromResource(bodyMap,wxPayConfig);
HashMap<String,Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
log.info("plainTextMap:{}",plainTextMap);
// 4.封装微信返回的数据
WxchatCallbackSuccessData callbackData = new WxchatCallbackSuccessData();
callbackData.setSuccessTime(String.valueOf(plainTextMap.get("success_time")));
callbackData.setOrderId(String.valueOf(plainTextMap.get("out_trade_no")));
callbackData.setTransactionId(String.valueOf(plainTextMap.get("transaction_id")));
callbackData.setTradestate(String.valueOf(plainTextMap.get("trade_state")));
callbackData.setTradetype(String.valueOf(plainTextMap.get("trade_type")));
String amount = String.valueOf(plainTextMap.get("amount"));
HashMap<String,Object> amountMap = gson.fromJson(amount, HashMap.class);
String total = String.valueOf(amountMap.get("total"));
callbackData.setTotalMoney(new BigDecimal(total).movePointLeft(2));
log.info("callbackData:{}",callbackData);
if ("SUCCESS".equals(callbackData.getTradestate())) {
// 执行业务逻辑
businessCallback.accept(callbackData);
}
// 5.成功应答
response.setStatus(200);
final HashMap<String, Object> resultMap = new HashMap<>();
resultMap.put("code", "SUCCESS");
resultMap.put("message", "成功");
return gson.toJson(resultMap);
}
/**
* 对称解密
*/
private static String decryptFromResource(HashMap<String, Object> bodyMap, WxPayConfig wxPayConfig) {
// 通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
// 数据密文
String ciphertext = resourceMap.get("ciphertext");
// 随机串
String nonce = resourceMap.get("nonce");
// 附加数据
String associateData = resourceMap.get("associated_data");
AesUtil aesUtil = new AesUtil(wxPayConfig.getKey().getBytes(StandardCharsets.UTF_8));
try {
return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
} catch (GeneralSecurityException e) {
e.printStackTrace();
throw new DefaultException("解密失败");
}
}
}
注意看方法的最后一个参数,Consumer<WxchatCallbackSuccessData> businessCallback
,这是一个jdk自带的函数式接口,通过接口回调的方式,完善业务逻辑。函数式接口或者回调函数不理解的可以观看jdk自带函数接口以及其系列文章,当然您也可以直接使用。