• 微信支付 APP端 第三弹 申请退款


    目录

    第三弹 申请退款

    成果展示:

    1.退款设计思路

     2.申请退款

    3.微信退款回调

    3.1controller

    3.1.1 微信退款controller

    3.1.2验证回调类 (和微信支付回调验证一样 如果看了之前的可以不用写)

    3.1.3 service 退款回调验证


    第三弹 申请退款

    当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。

    (这些要根据直接项目的情况按需来安排)

    成果展示:

    1.退款设计思路

    微信官方请求示例:

    1. {
    2. "transaction_id": "1217752501201407033233368018",
    3. "out_refund_no": "1217752501201407033233368018",
    4. "reason": "商品已售完",
    5. "notify_url": "https://weixin.qq.com",
    6. "funds_account": "AVAILABLE",
    7. "amount": {
    8. "refund": 888,
    9. "from": [
    10. {
    11. "account": "AVAILABLE",
    12. "amount": 444
    13. }
    14. ],
    15. "total": 888,
    16. "currency": "CNY"
    17. },
    18. "goods_detail": [
    19. {
    20. "merchant_goods_id": "1217752501201407033233368018",
    21. "wechatpay_goods_id": "1001",
    22. "goods_name": "iPhone6s 16G",
    23. "unit_price": 528800,
    24. "refund_amount": 528800,
    25. "refund_quantity": 1
    26. }
    27. ]
    28. }

     2.申请退款

    1. /**
    2. * 微信退款
    3. * 注意:1、交易时间超过一年的订单无法提交退款
    4. * 2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
    5. * 3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
    6. * 4、每个支付订单的部分退款次数不能超过50次
    7. * 5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
    8. * 6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
    9. * 7、一个月之前的订单申请退款频率限制为:5000/min
    10. * 8、同一笔订单多次退款的请求需相隔1分钟
    11. * @param wxConfig 微信商家配置
    12. * @param orderId 订单id
    13. * @return String
    14. * @author zhangjunrong
    15. * @date 2022/4/21 15:11
    16. */
    17. @Override
    18. @Transactional(rollbackFor = Exception.class)
    19. public String refundOrder(ToolWxConfig wxConfig, Long orderId) {
    20. TicketOrderReturn refundOrder = (TicketOrderReturn) redisUtil.get(WxRedisKey.WX_REFUND_ORDER + orderId);
    21. try {
    22. //通过redis 中获取订单相应的信息
    23. if (ObjectUtil.isNotEmpty(refundOrder)) {
    24. log.info("微信退款=====redis记录信息========={}==",refundOrder.toString());
    25. //1.请求配置参数
    26. HttpPost httpPost = new HttpPost(WxApiType.REFUND_ORDER.getValue());
    27. //格式配置
    28. //格式配置
    29. httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON);
    30. httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF);
    31. ByteArrayOutputStream bos = new ByteArrayOutputStream();
    32. ObjectMapper objectMapper = new ObjectMapper();
    33. ObjectNode rootNode = objectMapper.createObjectNode();
    34. //2.配置参数 订单商户号 退款订单号 微信订单号(微信生成) 退款回调地址(与下单回调地址不一样) 金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分) 退款币种 CNY 人民币
    35. //商户订单号 下单时生成
    36. rootNode.put(WXOrderConstant.OUT_TRADE_NO, refundOrder.getOrderSn())
    37. //微信支付系统生成的订单号(微信生成) 下单时生成
    38. .put(WXOrderConstant.TRANSACTION_ID, refundOrder.getTransactionId())
    39. //微信支付系统生成的订单号 下单时生成(系统生成)
    40. .put(WXOrderConstant.OUT_REFUND_NO, refundOrder.getOrderRefundSn())
    41. //退款回调地址
    42. .put(WXOrderConstant.NOTIFY_URL, wxConfig.getRefNotifyUrl());
    43. //金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分)
    44. rootNode.putObject(WXOrderConstant.AMOUNT)
    45. //现阶段 total==refund 不支持部分退款
    46. //原订单金额 total
    47. .put(WXOrderConstant.AMOUNT_TOTAL, refundOrder.getRefundMoney().movePointRight(SystemConstant.NUM_TWO).intValue())
    48. //退款金额 refund
    49. .put(WXOrderConstant.AMOUNT_REFUND, refundOrder.getRefundMoney().movePointRight(SystemConstant.NUM_TWO).intValue())
    50. //退款币种 CNY 人民币
    51. .put(WXOrderConstant.AMOUNT_CURRENCY, wxConfig.getCurrency());
    52. objectMapper.writeValue(bos, rootNode);
    53. //3.调用微信退款接口
    54. httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
    55. //接口返回值
    56. CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost);
    57. String bodyAsString = EntityUtils.toString(response.getEntity());
    58. log.info("微信申请退款返回结果" + "response:" + bodyAsString);
    59. JsonNode refundNode = objectMapper.readTree(bodyAsString);
    60. // 修改订单退款状态 微信退款入数据库 库存恢复
    61. //1.修改订单退款状态
    62. String transactionId = refundNode.get(WXOrderConstant.TRANSACTION_ID).textValue();
    63. String status = refundNode.get(WXOrderConstant.STATUS).textValue();
    64. Boolean flag = iTicketOrderService.updateOrderStatus(transactionId, status);
    65. log.info("微信退款======母订单状态修改=={}===",flag);
    66. //2.微信退款入数据库
    67. Boolean insertRefund = iTicketOrderReturnService.insertRefund(refundNode, refundOrder);
    68. log.info("微信退款======退款订单入数据库=={}===",insertRefund);
    69. //3.库存恢复 实现 通过微信支付id(微信生成) 查询出母单id 根据母单id查询出所有子单 让子单库存恢复
    70. iTicketOrderService.restoreStock(transactionId);
    71. return status;
    72. }
    73. } catch (Exception e) {
    74. log.info("微信退款" + refundOrder.getOrderSn() + "失败");
    75. throw new YqsException(MessageEnum.NOT_REFUND.getCode(),"退款失败,请联系客服解决");
    76. }
    77. return null;
    78. }

    3.微信退款回调

    3.1controller

    3.1.1 微信退款controller

    1. @PostMapping("/wechatPayCallback")
    2. @ApiOperation("支付回调给微信确认")
    3. @ApiIgnore
    4. public String wechatCallback(HttpServletRequest request) {
    5. ToolWxConfig wxConfig = iToolWxConfigService.find();
    6. log.info("微信退款回调通知调用=============================");
    7. Gson gson = new Gson();
    8. Map<String,String> result = new HashMap(SystemConstant.NUM_16);
    9. result.put("code", "FAIL");
    10. result.put("message","失败");
    11. try {
    12. //微信回调信息校验
    13. // 构建request,传入必要参数
    14. Notification notification = WxPayUtil.verifyBack(request, wxConfig);
    15. log.info("=================微信验证签名成功=======成功时间=={}=====",notification.getCreateTime());
    16. // 思路: 验证订单 订单号是否存在 订单状态 通过缓存来做到 一回调验证多订单的类型
    17. // 生成订单的时候 把订单信息放入缓存中 order:key key为订单号 30min 通过获取 订单消息做到 快速验证 插入操作 用if 进行
    18. if (iToolWxConfigService.verifyCreateOrder(notification.getDecryptData())) {
    19. log.info("==============================微信退款成功订单=====================================");
    20. result.put("code", WXOrderConstant.WX_BACK_OK);
    21. result.put("message", "支付回调成功");
    22. }
    23. } catch (ValidationException | ParseException | IOException e) {
    24. log.error("微信支付回调失败验证" + e);
    25. }
    26. log.info("微信返回结果"+result);
    27. return gson.toJson(result);
    28. }

    3.1.2验证回调类 (和微信支付回调验证一样 如果看了之前的可以不用写)

    1. /**
    2. *回调验证
    3. * @param request 微信回调请求
    4. * @param wxConfig 微信基本配置信息
    5. * @return String
    6. * @author zhangjunrong
    7. * @date 2022/4/21 15:02
    8. */
    9. public static Notification verifyBack(HttpServletRequest request, ToolWxConfig wxConfig) throws IOException, ValidationException, ParseException {
    10. //应答报文主体
    11. BufferedReader br = request.getReader();
    12. String str;
    13. StringBuilder builder = new StringBuilder();
    14. while ((str = br.readLine()) != null) {
    15. builder.append(str);
    16. }
    17. // 构建request,传入必要参数
    18. //参数 1.微信序列号 2.应答随机串 3.应答时间戳 4.应答签名 5.应答报文主体
    19. NotificationRequest notificationRequest = new NotificationRequest.Builder()
    20. .withSerialNumber(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SERIAL))
    21. .withNonce(request.getHeader(WechatPayHttpHeaders.WECHATPAY_NONCE))
    22. .withTimestamp(request.getHeader(WechatPayHttpHeaders.WECHATPAY_TIMESTAMP))
    23. .withSignature(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SIGNATURE))
    24. .withBody(builder.toString())
    25. .build();
    26. NotificationHandler handler = new NotificationHandler(WxPayUtil.getVerifier(wxConfig), wxConfig.getApiV3key().getBytes(StandardCharsets.UTF_8));
    27. // 验签和解析请求体
    28. log.info("验签和解析请求体==============================开始验证==============================");
    29. Notification notification = handler.parse(notificationRequest);
    30. Assert.assertNotNull(notification);
    31. return notification;
    32. }

    3.1.3 service 退款回调验证

    1. /**
    2. *微信支付回调验证判定 核对成功 数据异步入库
    3. * @param
    4. * @param decryptOrder
    5. * @return Boolean
    6. * @author zhangjunrong
    7. * @date 2022/5/3 8:23
    8. */
    9. @Override
    10. public Boolean verifyCreateOrder(String decryptOrder) {
    11. //在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
    12. //实现: 加入一把可重入锁
    13. if (reentrantLock.tryLock()) {
    14. try {
    15. log.info("===================================进入微信支付回调核对订单中========================================");
    16. ObjectMapper objectMapper = new ObjectMapper();
    17. //微信回调 解密后 信息
    18. JsonNode node = objectMapper.readTree(decryptOrder);
    19. //获取订单商户号
    20. String orderNo = node.get(WXOrderConstant.OUT_TRADE_NO).textValue();
    21. //1.获取redis中的订单信息
    22. OrderTotalRedisRO totalRedisRO = (OrderTotalRedisRO) redisUtil.get(SystemConstant.ORDER_TOTAL + orderNo);
    23. //1.1微信 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
    24. //实现方法: 通过订单状态来 判定是否要进行判定 出未支付以外的 都返回 结果 通过缓存获取到支付状态 防止微信重复调用该方法
    25. //如果回调 缓存中记录清除说明 入库判定等等成功 直接返回true
    26. if (ObjectUtil.isEmpty(totalRedisRO)) {
    27. return true;
    28. }
    29. log.info(node.get(WXOrderConstant.OUT_TRADE_NO) + "订单回调信息记录:订单状态:" + orderNo);
    30. //2.如果回调 支付类型为成功 核对金额 入数据库
    31. //获取支付状态
    32. String tradeState = node.get(WXOrderConstant.TRADE_STATE).textValue();
    33. if (StrUtil.equals(WXOrderConstant.WX_BACK_OK, tradeState)) {
    34. //redis缓存中的金额
    35. int redisTotal = totalRedisRO.getTicketOrder().getPayMoney().movePointRight(SystemConstant.NUM_TWO).intValue();
    36. //校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
    37. //缓存中存入的用户支付金额 totalRedisRO.getTicketOrder().getTotalMoney().movePointRight(SystemConstant.NUM_TWO).intValue()
    38. if (WxPayUtil.verifyMoney(node, redisTotal)) {
    39. //3.对应的数据入库
    40. log.info("redis入数据库信息======================" + totalRedisRO);
    41. if (!ObjectUtil.isEmpty(totalRedisRO)) {
    42. //缓存放入一个状态 表明已操作该订单 存放200秒
    43. // 支付成功就把redis中缓存记录清除
    44. totalRedisRO.getTicketOrder().setOrderStatus(SystemConstant.NUM_ONE);
    45. redisUtil.del(SystemConstant.ORDER_TOTAL + orderNo);
    46. //订单入库
    47. iTicketOrderService.createAllTicket(totalRedisRO, node.get(WXOrderConstant.TRANSACTION_ID).textValue());
    48. }
    49. }
    50. //为什么没有插入成功也返回true?
    51. //因为就算数据库没有入成功 但是金额 订单校验等等的都通过
    52. //说明数据库入库失败
    53. //如果入库失败 让用户联系客服接入管理 [钱一定要收下来]
    54. return true;
    55. }
    56. } catch (Exception e) {
    57. log.error("订单支付异常===>订单回调信息记录:订单状态:" + decryptOrder);
    58. }finally {
    59. //释放锁
    60. reentrantLock.unlock();
    61. }
    62. }
    63. return false;
    64. }

  • 相关阅读:
    九、【React基础】组件的生命周期
    GD32f103系列教程—(时钟篇)
    有意思的鼠标指针交互探究
    【AI视野·今日NLP 自然语言处理论文速览 第四十四期】Fri, 29 Sep 2023
    SpringBoot中_JAVA利用国密算法_实现内容的加密_解密--SpringCloud工作笔记180
    Spring(18) @Order注解介绍、使用、底层原理
    【NOI模拟赛】刮痧(动态规划)
    Linux主机连接腾讯云服务器
    java.awt.HeadlessException
    win11自带矩形块截屏、录屏、视频编辑等功能
  • 原文地址:https://blog.csdn.net/Little___Turtle/article/details/125527970