目录
微信支付通过支付通知接口将用户支付成功消息通知给商户
定时任务处理特殊订单
(这些要根据直接项目的情况按需来安排)
(只展示部分 这个设计到项目内容 不好全部展示)
- @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);
- }
- /**
- *回调验证
- * @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;
- }
- 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::";
- }
- /**
- *验证用户支付金额 场景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;
- }
- @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;
- }
- /**
- *构造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());
- }
- /**
- * 查询订单
- * 使用场景:
- * 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;
- }
- /**
- * 关闭订单
- * 使用场景:
- * 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;
- }
- /**
- *验证用户支付金额 场景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;
- }
-
- /**
- * 清除过去票 恢复库存
- *
- * @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());
- }
- }
- }
- });
- }
-
-
- }