目录
第二弹:支付回调
微信支付通过支付通知接口将用户支付成功消息通知给商户
定时任务处理特殊订单
(这些要根据直接项目的情况按需来安排)
成果展示
(只展示部分 这个设计到项目内容 不好全部展示)
1 订单设计思路
1.1设计思路
2.回调接口
2.1controller
@PostMapping("/wechatPayCallback")
@ApiOperation("支付回调给微信确认")
@ApiIgnore
public String wechatCallback(HttpServletRequest request) {
ToolWxConfig wxConfig = iToolWxConfigService.find();
log.info("微信退款回调通知调用=============================");
Gson gson = new Gson();
Map<String,String> result = new HashMap(SystemConstant.NUM_16);
result.put("code", "FAIL");
result.put("message","失败");
try {
//微信回调信息校验
// 构建request,传入必要参数
Notification notification = WxPayUtil.verifyBack(request, wxConfig);
log.info("=================微信验证签名成功=======成功时间=={}=====",notification.getCreateTime());
// 思路: 验证订单 订单号是否存在 订单状态 通过缓存来做到 一回调验证多订单的类型
// 生成订单的时候 把订单信息放入缓存中 order:key key为订单号 30min 通过获取 订单消息做到 快速验证 插入操作 用if 进行
if (iToolWxConfigService.verifyCreateOrder(notification.getDecryptData())) {
log.info("==============================微信退款成功订单=====================================");
result.put("code", WXOrderConstant.WX_BACK_OK);
result.put("message", "支付回调成功");
}
} catch (ValidationException | ParseException | IOException e) {
log.error("微信支付回调失败验证" + e);
}
log.info("微信返回结果"+result);
return gson.toJson(result);
}
2.2验证静态方法
/**
*回调验证
* @param request 微信回调请求
* @param wxConfig 微信基本配置信息
* @return String
* @author zhangjunrong
* @date 2022/4/21 15:02
*/
public static Notification verifyBack(HttpServletRequest request, ToolWxConfig wxConfig) throws IOException, ValidationException, ParseException {
//应答报文主体
BufferedReader br = request.getReader();
String str;
StringBuilder builder = new StringBuilder();
while ((str = br.readLine()) != null) {
builder.append(str);
}
// 构建request,传入必要参数
//参数 1.微信序列号 2.应答随机串 3.应答时间戳 4.应答签名 5.应答报文主体
NotificationRequest notificationRequest = new NotificationRequest.Builder()
.withSerialNumber(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SERIAL))
.withNonce(request.getHeader(WechatPayHttpHeaders.WECHATPAY_NONCE))
.withTimestamp(request.getHeader(WechatPayHttpHeaders.WECHATPAY_TIMESTAMP))
.withSignature(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SIGNATURE))
.withBody(builder.toString())
.build();
NotificationHandler handler = new NotificationHandler(WxPayUtil.getVerifier(wxConfig), wxConfig.getApiV3key().getBytes(StandardCharsets.UTF_8));
// 验签和解析请求体
log.info("验签和解析请求体==============================开始验证==============================");
Notification notification = handler.parse(notificationRequest);
Assert.assertNotNull(notification);
return notification;
}
2.3缓存参数 常量
package com.yqs.constant.wechatPay;
/**
* @Description 微信定时任务
* @Author 小乌龟
* @Date 2022/5/13 9:01
*/
public class WxRedisKey {
/**
*微信支付订单 list<对象>=>(订单详细信息key+订单类型+下单时间+支付状态) 有效时间6分钟
*/
public static final String WX_PAY_ORDER = "wxPayOrderList";
/**
*微信退款订单 存微信基础信息 有效时间3分钟 wxRefundOrder::
*/
public static final String WX_REFUND_ORDER = "wxRefundOrder::";
}
2.4 service 校验金额
2.4.1金额静态方法校验
/**
*验证用户支付金额 场景1.微信回调验证 2.定时任务核对订单
* @param node 微信回调json 返回参数
* @param redisTotal redis记录金额
* @return Boolean
* @author zhangjunrong
* @date 2022/5/16 8:39
*/
public static Boolean verifyMoney(JsonNode node,Integer redisTotal){
//总金额计数值 用户支付计算
int userPayTotal = SystemConstant.NUM_ZERO;
//1.验证订单金额
//用户支付金额
int payerTotal = node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_PAYER_TOTAL).asInt();
userPayTotal = userPayTotal + payerTotal;
//CASH充值型代金券 要加上优惠金额 银行优惠 获取
//排空 如果没有优惠则跳过
if (!ObjectUtil.isEmpty(node.get(WXOrderConstant.PROMOTION_DETAIL))) {
for (JsonNode objNode : node.get(WXOrderConstant.PROMOTION_DETAIL)) {
//如果优惠类型为CASH 则要和 用户支付金额 累加
if (WXOrderConstant.WX_DISCOUNT_TYPE.equals(objNode.get(WXOrderConstant.PROMOTION_DETAIL_TYPE).textValue())) {
userPayTotal = userPayTotal + objNode.get(WXOrderConstant.AMOUNT).asInt();
}
}
}
//2.总金额 预支付时的 金额 与 total 用户支付金额
//微信端返回的支付总金额
int wxTotal = node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_TOTAL).asInt();
//redis缓存中的金额
//校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
//缓存中存入的用户支付金额 totalRedisRO.getTicketOrder().getTotalMoney().movePointRight(SystemConstant.NUM_TWO).intValue()
log.info("微信回调金额===比较=== "+"微信端返回的支付总金额"+node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_TOTAL).asInt()+"==========redis缓存中的金额"+redisTotal+"================用户支付总金额计算"+userPayTotal);
//只要缓存的金额小于用户实际付款金额 判定成功
return wxTotal == userPayTotal && redisTotal == wxTotal;
}
2.4.2总体校验
@Override
public Boolean verifyCreateOrder(String decryptOrder) {
//在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
//实现: 加入一把可重入锁
if (reentrantLock.tryLock()) {
try {
log.info("===================================进入微信支付回调核对订单中========================================");
ObjectMapper objectMapper = new ObjectMapper();
//微信回调 解密后 信息
JsonNode node = objectMapper.readTree(decryptOrder);
//获取订单商户号
String orderNo = node.get(WXOrderConstant.OUT_TRADE_NO).textValue();
//1.获取redis中的订单信息
OrderTotalRedisRO totalRedisRO = (OrderTotalRedisRO) redisUtil.get(SystemConstant.ORDER_TOTAL + orderNo);
//1.1微信 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
//实现方法: 通过订单状态来 判定是否要进行判定 出未支付以外的 都返回 结果 通过缓存获取到支付状态 防止微信重复调用该方法
//如果回调 缓存中记录清除说明 入库判定等等成功 直接返回true
if (ObjectUtil.isEmpty(totalRedisRO)) {
return true;
}
log.info(node.get(WXOrderConstant.OUT_TRADE_NO) + "订单回调信息记录:订单状态:" + orderNo);
//2.如果回调 支付类型为成功 核对金额 入数据库
//获取支付状态
String tradeState = node.get(WXOrderConstant.TRADE_STATE).textValue();
if (StrUtil.equals(WXOrderConstant.WX_BACK_OK, tradeState)) {
//redis缓存中的金额
int redisTotal = totalRedisRO.getTicketOrder().getPayMoney().movePointRight(SystemConstant.NUM_TWO).intValue();
//校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
//缓存中存入的用户支付金额 totalRedisRO.getTicketOrder().getTotalMoney().movePointRight(SystemConstant.NUM_TWO).intValue()
if (WxPayUtil.verifyMoney(node, redisTotal)) {
//3.对应的数据入库
log.info("redis入数据库信息======================" + totalRedisRO);
if (!ObjectUtil.isEmpty(totalRedisRO)) {
//缓存放入一个状态 表明已操作该订单 存放200秒
// 支付成功就把redis中缓存记录清除
totalRedisRO.getTicketOrder().setOrderStatus(SystemConstant.NUM_ONE);
redisUtil.del(SystemConstant.ORDER_TOTAL + orderNo);
//订单入库
iTicketOrderService.createAllTicket(totalRedisRO, node.get(WXOrderConstant.TRANSACTION_ID).textValue());
}
}
//为什么没有插入成功也返回true?
//因为就算数据库没有入成功 但是金额 订单校验等等的都通过
//说明数据库入库失败
//如果入库失败 让用户联系客服接入管理 [钱一定要收下来]
return true;
}
} catch (Exception e) {
log.error("订单支付异常===>订单回调信息记录:订单状态:" + decryptOrder);
}finally {
//释放锁
reentrantLock.unlock();
}
}
return false;
}
3. 定时任务 查询处理特殊订单
3.1 get请求 统一配置
/**
*构造HttpClient 实现 微信申请接口 调用功能
* @param wxConfig 微信支付数据库参数
* @param verifier 微信验签器
* @return CloseableHttpClient
* @author zhangjunrong
* @date 2022/5/16 8:54
*/
public static CloseableHttpClient getHttpClient(ToolWxConfig wxConfig, Verifier verifier) {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(wxConfig.getPrivateKey().getBytes(StandardCharsets.UTF_8)));
//通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(wxConfig.getMchId(), wxConfig.getMchSerialNo(), merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
return builder.build();
}
/**
* 调用微信支付 get请求 统一配置
* 要微信签名认证
* @param url
* @return String
* @author zhangjunrong
* @date 2022/5/9 20:39
*/
private String getHttpGet(ToolWxConfig wxConfig, String url) throws URISyntaxException, IOException {
//1.构造httpGet请求
URIBuilder uriBuilder = null;
uriBuilder = new URIBuilder(url);
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON);
//2.调起微信查询订单接口
CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpGet);
//3.返回结果信息
return EntityUtils.toString(response.getEntity());
}
3.2 查询订单service
/**
* 查询订单
* 使用场景:
* 1.当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
* 2.调用支付接口后,返回系统错误或未知交易状态情况。
* 3.调用付款码支付API,返回USERPAYING(用户付费)的状态。
* 4.调用关单或撤销接口API之前,需确认支付状态。
* @param wxConfig 微信配置
* @param outTradeNo 商户订单号 系统生成
* @return String 订单交易状态
* @author zhangjunrong
* @date 2022/4/14 16:14
*
*/
@Override
public JsonNode queryCreateOrder(ToolWxConfig wxConfig, String outTradeNo) {
try {
//1.查单 微信接口 编辑 微信订单号 + 商户号
String url = StrFormatter.format(WxApiType.QUERY_CREATE_ORDER.getValue(), outTradeNo, wxConfig.getMchId());
//2.调用微信接口
String bodyAsString = getHttpGet(wxConfig, url);
log.info("支付查单信息" + bodyAsString);
//返回查单结果信息
if (ObjectUtil.isNotEmpty(bodyAsString)){
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(bodyAsString);
}
} catch (Exception e) {
log.error("支付查单失败" + outTradeNo);
}
return null;
}
3.3关闭订单
/**
* 关闭订单
* 使用场景:
* 1、商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
* 2、系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
* 注意:关单没有时间限制,建议在订单生成后间隔几分钟(最短5分钟)再调用关单接口,避免出现订单状态同步不及时导致关单失败
*
* @param wxConfig
* @param outTradeNo
* @return String
* @author zhangjunrong
* @date 2022/4/14 17:02
*/
@Override
public String closeOrder(ToolWxConfig wxConfig, String outTradeNo) {
try {
//1.微信接口编辑
String url = StrFormatter.format(WxApiType.CLOSE_ORDER.getValue(), outTradeNo);
HttpPost httpPost = new HttpPost(url);
//格式配置
httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON);
httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//2.添加商户id
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put(WXOrderConstant.MCHID, wxConfig.getMchId());
objectMapper.writeValue(bos, rootNode);
//3.调起微信关单接口
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost);
//无数据(Http状态码为204) 微信返回结果无数据 状态码为204 成功
if (response.getStatusLine().getStatusCode() == MessageEnum.NO_CONTENT.getCode()) {
return "关单成功";
}
} catch (Exception e) {
log.error("关单失败" + outTradeNo + e);
}
return null;
}
3.4 定时任务 处理订单
3.4.1 金额校验 (和微信回调处理金额校验相同)
/**
*验证用户支付金额 场景1.微信回调验证 2.定时任务核对订单
* @param node 微信回调json 返回参数
* @param redisTotal redis记录金额
* @return Boolean
* @author zhangjunrong
* @date 2022/5/16 8:39
*/
public static Boolean verifyMoney(JsonNode node,Integer redisTotal){
//总金额计数值 用户支付计算
int userPayTotal = SystemConstant.NUM_ZERO;
//1.验证订单金额
//用户支付金额
int payerTotal = node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_PAYER_TOTAL).asInt();
userPayTotal = userPayTotal + payerTotal;
//CASH充值型代金券 要加上优惠金额 银行优惠 获取
//排空 如果没有优惠则跳过
if (!ObjectUtil.isEmpty(node.get(WXOrderConstant.PROMOTION_DETAIL))) {
for (JsonNode objNode : node.get(WXOrderConstant.PROMOTION_DETAIL)) {
//如果优惠类型为CASH 则要和 用户支付金额 累加
if (WXOrderConstant.WX_DISCOUNT_TYPE.equals(objNode.get(WXOrderConstant.PROMOTION_DETAIL_TYPE).textValue())) {
userPayTotal = userPayTotal + objNode.get(WXOrderConstant.AMOUNT).asInt();
}
}
}
//2.总金额 预支付时的 金额 与 total 用户支付金额
//微信端返回的支付总金额
int wxTotal = node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_TOTAL).asInt();
//redis缓存中的金额
//校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
//缓存中存入的用户支付金额 totalRedisRO.getTicketOrder().getTotalMoney().movePointRight(SystemConstant.NUM_TWO).intValue()
log.info("微信回调金额===比较=== "+"微信端返回的支付总金额"+node.get(WXOrderConstant.AMOUNT).get(WXOrderConstant.AMOUNT_TOTAL).asInt()+"==========redis缓存中的金额"+redisTotal+"================用户支付总金额计算"+userPayTotal);
//只要缓存的金额小于用户实际付款金额 判定成功
return wxTotal == userPayTotal && redisTotal == wxTotal;
}
3.4.2 定时任务处理
/**
* 清除过去票 恢复库存
*
* @param order 子订单信息
* @return void
* @author zhangjunrong
* @date 2022/5/16 19:44
*/
private void dealVoidTicket(WxPayListRO order) {
//微信订单redis2 订单详情
OrderTotalRedisRO orderTotalRedisRO = (OrderTotalRedisRO) redisUtil.get(SystemConstant.ORDER_TOTAL + order.getOutTradeNo());
orderTotalRedisRO.getTicketOrderItems().stream()
.filter(Objects::nonNull)
.forEach(ticketOrderItem -> {
//恢复票相应的库存
Boolean timeOrder = iTicketOrderService.updateTimeOrder(ticketOrderItem.getTicketId(), ticketOrderItem.getUseStart(), ticketOrderItem.getBranchStart(), ticketOrderItem.getBuyCount());
log.info("订单恢复库存{}======订单信息{}===============", timeOrder, ticketOrderItem);
}
);
//清除缓存 redis1
long lRemove = redisUtil.lRemove(WxRedisKey.WX_PAY_ORDER, SystemConstant.NUM_ONE, order);
log.info("清除redis1缓存" + lRemove);
//清除缓存 redis2
redisUtil.del(SystemConstant.ORDER_TOTAL + order.getOutTradeNo());
}
/**
* 对于下单定时任务核对 每间隔50秒查询5分钟前的订单
* 1.防止微信付款成功 订单数据未入库 2.实现定期关单功能
*
* @return void
* @author zhangjunrong
* @date 2022/5/13 9:59
*/
@Async
@Scheduled(cron = "0/50 * * * * ? ")
public void orderTimeCheck() {
//redis1 key=>wxPay 设置7分钟有效 一直有效(4分钟更新一次)(redis2key+订单类型+下单时间+支付状态)list<对象>数组redis2 10分钟有效期订单详细信息
//redis2 订单详细信息 8分钟有效时间
//1.获取出redis1 微信支付订单list
List<Object> orderList = redisUtil.lGet(WxRedisKey.WX_PAY_ORDER, SystemConstant.NUM_ZERO, SystemConstant.NUM_NEGATIVE_ONE);
if (ObjectUtil.isNotEmpty(orderList)) {
log.info("定时任务核销单====处理特殊订单开启====================");
List<WxPayListRO> wxPayListROS = redisUtil.castList(orderList, WxPayListRO.class);
wxPayListROS.stream()
//2.获取 前5分钟的订单信息
.filter(order -> DateUtil.between(order.getCreateOrderTime(), DateUtil.date(), DateUnit.MINUTE) >= SystemConstant.NUM_FIVE)
//3.过滤掉已处理的订单 redis2中没有的订单 支付成功会删除redis2记录
.filter(order ->
{
//如果redis2中没有该记录 则删除掉redis1对于该记录
if (!redisUtil.hasKey(SystemConstant.ORDER_TOTAL + order.getOutTradeNo())) {
log.info("定时任务判定是redis否拥有订单====" + order.getOutTradeNo());
long lRemove = redisUtil.lRemove(WxRedisKey.WX_PAY_ORDER, SystemConstant.NUM_ONE, order);
log.info("清除redis1记录" + lRemove);
return false;
}
return true;
})
.forEach(order -> {
//4.调微信接口判定
log.info("定时任务核销单====微信定时任务开始核对订单未支付成功订单====" + order);
ToolWxConfig wxConfig = iToolWxConfigService.find();
//4.1查询微信那的订单状态
JsonNode queryCreateOrder = iToolWxConfigService.queryCreateOrder(wxConfig, order.getOutTradeNo());
//4.2如果查询结果为null 说明用户没有调启微信支付 直接清除订单redis
if (ObjectUtil.isEmpty(queryCreateOrder.get(WXOrderConstant.TRADE_STATE))) {
log.info("定时任务核销单====微信订单信息不存在==============");
//恢复票务库存 清除库存
dealVoidTicket(order);
} else {
//交易状态
String tradeState = queryCreateOrder.get(WXOrderConstant.TRADE_STATE).textValue();
String transactionId = queryCreateOrder.get(WXOrderConstant.TRANSACTION_ID).textValue();
log.info("定时任务核销单====微信订单状态: 微信接口 对象" + queryCreateOrder + "redis缓存" + order.getOrderStatus());
// 4.3核对支付 状态 如果未支付 关单 redis1和redis2清除缓存
if (WxPayStatusEnum.NOTPAY.getValue().equals(tradeState)) {
log.info("定时任务核销单====微信订单信息未支付==========");
//关闭订单
String closeOrder = iToolWxConfigService.closeOrder(wxConfig, order.getOutTradeNo());
log.info("定时任务核销单====返回结果" + closeOrder);
//恢复票务库存 清除库存
dealVoidTicket(order);
}
// 4.4核对支付 状态 如果已支付 redis1未支付 数据库查单再次判定 如果不存在 则入数据库
if (WxPayStatusEnum.SUCCESS.getValue().equals(tradeState)) {
//数据库查单判定
if (ObjectUtil.isEmpty(iTicketOrderService.queryOrderByTranId(transactionId))) {
log.info("定时任务核销单====微信订单信息支付==========数据库未有数据");
//获取redis2
OrderTotalRedisRO orderTotalRedisRO = (OrderTotalRedisRO) redisUtil.get(SystemConstant.ORDER_TOTAL + order.getOutTradeNo());
log.info("微信redis2获取信息===" + orderTotalRedisRO.toString());
//订单金额验证 等等 插入数据库
//redis缓存中的金额
int redisTotal = orderTotalRedisRO.getTicketOrder().getPayMoney().movePointRight(SystemConstant.NUM_TWO).intValue();
//只有金额对等 方可入数据库
if (WxPayUtil.verifyMoney(queryCreateOrder, redisTotal)) {
iTicketOrderService.createAllTicket(orderTotalRedisRO, transactionId);
}
//清除缓存 redis1
long lRemove = redisUtil.lRemove(WxRedisKey.WX_PAY_ORDER, SystemConstant.NUM_ONE, order);
log.info("清除redis1缓存" + lRemove);
//清除缓存 redis2
redisUtil.del(SystemConstant.ORDER_TOTAL + order.getOutTradeNo());
}
}
}
});
}
}