فهرست منبع

Merge branch 'refund'

# Conflicts:
#	netflix-dao/src/main/java/com/cyksj/model/entity/OrderDon.java
#	netflix-service/src/main/java/com/cyksj/service/order/OrderDonService.java
#	netflix-web/src/main/java/com/cyksj/web/controller/payment/OrderController.java
zoujiajian 3 سال پیش
والد
کامیت
47fc007a82
20فایلهای تغییر یافته به همراه896 افزوده شده و 46 حذف شده
  1. BIN
      cert/apiclient_cert.p12
  2. 14 1
      netflix-common/src/main/java/com/cyksj/common/util/J11HttpC.java
  3. 141 0
      netflix-common/src/main/java/com/cyksj/common/util/NSslKit.java
  4. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/RefundOrderDonMapper.java
  5. 100 0
      netflix-dao/src/main/java/com/cyksj/model/dto/WxRefundOrder.java
  6. 14 2
      netflix-dao/src/main/java/com/cyksj/model/entity/OrderDon.java
  7. 55 0
      netflix-dao/src/main/java/com/cyksj/model/entity/RefundOrderDon.java
  8. 6 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/OrderDonView.java
  9. 26 0
      netflix-dao/src/main/java/com/cyksj/model/request/WxOrderRefundReq.java
  10. 26 0
      netflix-dao/src/main/java/com/cyksj/model/request/ZfbOrderRefundReq.java
  11. 67 0
      netflix-dao/src/main/java/com/cyksj/model/response/AliPayRefundRep.java
  12. 10 0
      netflix-dao/src/main/java/com/cyksj/model/views/BackgroundDataView.java
  13. 37 0
      netflix-service/src/main/java/com/cyksj/common/AliPayCommon.java
  14. 7 0
      netflix-service/src/main/java/com/cyksj/redis/RedisService.java
  15. 6 0
      netflix-service/src/main/java/com/cyksj/service/mange/statistics/impl/StatisticsDataServiceImpl.java
  16. 18 0
      netflix-service/src/main/java/com/cyksj/service/order/OrderDonService.java
  17. 228 12
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
  18. 9 1
      netflix-service/src/main/java/com/cyksj/service/wechat/WeChatService.java
  19. 56 1
      netflix-service/src/main/java/com/cyksj/service/wechat/impl/WeChatServiceImpl.java
  20. 63 29
      netflix-web/src/main/java/com/cyksj/web/controller/payment/OrderController.java

BIN
cert/apiclient_cert.p12


+ 14 - 1
netflix-common/src/main/java/com/cyksj/common/util/J11HttpC.java

@@ -57,11 +57,24 @@ public class J11HttpC {
                 .build();
                 .build();
     }
     }
 
 
+    /**
+     * 微信退款 专用 ssl http client
+     */
+    private static final HttpClient wx_refund;
+    static {
+        wx_refund = HttpClient.newBuilder()
+                .connectTimeout(CONNECT_TIMEOUT)
+                .sslContext(NSslKit.KeyCert.wechat_refund.getSslContext())
+                .build();
+    }
+
     /**
     /**
      * http clients
      * http clients
      */
      */
     public enum Client {
     public enum Client {
-        BASE(defaultClient);
+        BASE(defaultClient),
+        WX_REFUND(wx_refund),
+        ;
 
 
         private HttpClient symbol;
         private HttpClient symbol;
 
 

+ 141 - 0
netflix-common/src/main/java/com/cyksj/common/util/NSslKit.java

@@ -0,0 +1,141 @@
+package com.cyksj.common.util;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.net.ssl.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+
+/**
+ * @author valor.
+ * @date 2019/11/18 16:24
+ */
+@Slf4j
+public class NSslKit {
+
+    private NSslKit() { }
+
+    private static class Wechat {
+
+        private static final String certPath = "cert/apiclient_cert.p12";
+        private static final String password = "1624498111";
+        private static final String certType = "PKCS12";
+
+        private static final SSLContext sslContext;
+        static {
+            sslContext = wechatRefundSSL();
+        }
+
+        private static final HostnameVerifier verifier;
+        static {
+            verifier = ((s, sslSession) -> true);
+        }
+
+        private static SSLContext wechatRefundSSL() {
+            try {
+                char[] pass = Wechat.password.toCharArray();
+
+                // 读取客户端证书
+                KeyStore keyStore = getKeyStore(Wechat.certPath, pass, Wechat.certType);
+                // 双向认证  客户端需要向服务端提交证书
+                KeyManager[] keyManagers = keyManagers(keyStore, pass);
+                // 由于微信退款证书 是自签名证书 不通过ca认证 则忽略证书校验
+                TrustManager[] trustManagers = trustManagers();
+
+                SSLContext ctx = SSLContext.getInstance("TLS");
+                ctx.init(keyManagers, trustManagers, null);
+
+                return ctx;
+            } catch (Throwable e) {
+                log.info("微信退款证书读取错误: \n{}", e.getMessage());
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public enum KeyCert {
+        wechat_refund(Wechat.sslContext, Wechat.verifier),
+        ;
+
+        private SSLContext sslContext;
+        private HostnameVerifier verifier;
+
+        KeyCert(SSLContext ssl, HostnameVerifier verifier) {
+            this.sslContext = ssl;
+            this.verifier = verifier;
+        }
+
+        public SSLContext getSslContext() {
+            return this.sslContext;
+        }
+
+        public HostnameVerifier getVerifier() {
+            return this.verifier;
+        }
+    }
+
+    /**
+     * 读取本地客户端证书
+     */
+    private static KeyStore getKeyStore(String filepath, char[] password, String type) throws Exception {
+        if (StringUtil.isBlank(filepath)) {
+            throw new IllegalArgumentException("Unable to recognize key-store file.");
+        }
+        File file = new File(filepath);
+        if (!file.exists()) {
+            throw new IllegalArgumentException("Unable to recognize key-store file.");
+        }
+
+        KeyStore keyStore = KeyStore.getInstance(type);
+        InputStream in = new FileInputStream(file);
+        keyStore.load(in, password);
+        IoKit.close(in);
+        return keyStore;
+    }
+
+    /**
+     * 双向认证  客户端需要向服务端提交证书
+     */
+    private static KeyManager[] keyManagers(KeyStore keyStore, char[] password) throws Exception {
+        // 用于上传客户端证书
+        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+        kmf.init(keyStore, password);
+        return kmf.getKeyManagers();
+    }
+
+    /**
+     * 客户端不需要向服务端提交证书
+     */
+    private static KeyManager[] keyManagers() {
+        return null;
+    }
+
+    /**
+     * 忽略证书校验
+     */
+    private static TrustManager[] trustManagers() throws Exception {
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+        tmf.init((KeyStore) null);
+        return tmf.getTrustManagers();
+    }
+
+    /**
+     * 信任指定证书
+     */
+    private static TrustManager[] trustManagers(KeyStore keyStore) throws Exception {
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+        tmf.init(keyStore);
+        return tmf.getTrustManagers();
+    }
+
+    /**
+     * 验证系统默认证书
+     */
+    private static TrustManager[] sysDefaultTrusts() {
+        return null;
+    }
+
+
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/RefundOrderDonMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.RefundOrderDon;
+
+/*
+ *项目名: netflix
+ *文件名: RefundOrderDonMapper
+ *创建者: JavaZou
+ *创建时间:2022/11/9 9:59
+ */
+public interface RefundOrderDonMapper extends BaseMapper<RefundOrderDon> {
+}

+ 100 - 0
netflix-dao/src/main/java/com/cyksj/model/dto/WxRefundOrder.java

@@ -0,0 +1,100 @@
+package com.cyksj.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 微信H5支付 申请退款
+ * https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_4&index=4
+ *
+ * @author zzj
+ * @date 2019/3/24 15:02
+ */
+@Getter
+@Setter
+@JacksonXmlRootElement(localName = "xml")
+public class WxRefundOrder {
+
+    /**
+     * 公众号appid
+     */
+    @JacksonXmlCData
+    private String appid;
+
+    /**
+     * 商户号
+     */
+    @JacksonXmlCData
+    @JsonProperty("mch_id")
+    private String mchId;
+
+    /**
+     * 随机字符串
+     */
+    @JacksonXmlCData
+    @JsonProperty("nonce_str")
+    private String nonceStr;
+
+    /**
+     * 签名
+     */
+    @JacksonXmlCData
+    private String sign;
+
+    /**
+     * 微信订单号 在支付通知中有返回
+     */
+    @JacksonXmlCData
+    @JsonProperty("transaction_id")
+    private String transactionId;
+
+    /**
+     * 商户退款单号 同一退款单号多次请求只退一笔
+     */
+    @JacksonXmlCData
+    @JsonProperty("out_refund_no")
+    private String outRefundNo;
+
+    /**
+     * 订单总金额 单位为分
+     */
+    @JacksonXmlCData
+    @JsonProperty("total_fee")
+    private Integer totalFee;
+
+    /**
+     * 退款金额 单位为分 可以做部分退款
+     */
+    @JacksonXmlCData
+    @JsonProperty("refund_fee")
+    private Integer refundFee;
+
+    /**
+     * 退款原因
+     */
+    @JacksonXmlCData
+    @JsonProperty("refund_desc")
+    private String refundDesc;
+
+    /**
+     * 通知地址
+     */
+    @JacksonXmlCData
+    @JsonProperty("notify_url")
+    private String notifyUrl;
+
+    public String println(String shopKey) {
+        return "appid=" + this.appid +
+                "&mch_id=" + this.mchId +
+                "&nonce_str=" + this.nonceStr +
+                "&notify_url=" + this.notifyUrl +
+                "&out_refund_no=" + this.outRefundNo +
+                "&refund_fee=" + this.refundFee +
+                "&total_fee=" + this.totalFee +
+                "&transaction_id=" + this.transactionId +
+                "&key=" + shopKey;
+    }
+}

+ 14 - 2
netflix-dao/src/main/java/com/cyksj/model/entity/OrderDon.java

@@ -47,6 +47,11 @@ public class OrderDon extends BaseEntity implements Serializable {
     /** 金额 */
     /** 金额 */
     private BigDecimal money ;
     private BigDecimal money ;
 
 
+    /**
+     * 退款金额
+     */
+    private BigDecimal refundMoney;
+
     /**
     /**
      * 购买数量
      * 购买数量
      */
      */
@@ -60,6 +65,12 @@ public class OrderDon extends BaseEntity implements Serializable {
      */
      */
     private Type type;
     private Type type;
 
 
+    /**
+     * 订单类型 1普通订单、2续费订单
+     * 默认值为1
+     */
+    private Integer orderType;
+
     /**
     /**
      * 支付时间
      * 支付时间
      */
      */
@@ -74,7 +85,8 @@ public class OrderDon extends BaseEntity implements Serializable {
         hasPayment("已付款"),
         hasPayment("已付款"),
         transport("已发货"),
         transport("已发货"),
         received("已签收"),
         received("已签收"),
-        complete("已完成");
+        complete("已完成"),
+        refund("已退款"),
         ;
         ;
 
 
         private String desc;
         private String desc;
@@ -93,7 +105,7 @@ public class OrderDon extends BaseEntity implements Serializable {
     public enum Type{
     public enum Type{
         WeChat("wechat","微信"),
         WeChat("wechat","微信"),
         ALI_PAY("zfb","支付宝"),
         ALI_PAY("zfb","支付宝"),
-        EX_CODE("exchange_code","兑换码")
+        EX_CODE("exchange_code","兑换码"),
         ;
         ;
         String type;
         String type;
         String desc;
         String desc;

+ 55 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/RefundOrderDon.java

@@ -0,0 +1,55 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/*
+ *项目名: netflix
+ *文件名: RefundOrderDon
+ *创建者: JavaZou
+ *创建时间:2022/11/8 18:16
+ */
+@Getter
+@Setter
+public class RefundOrderDon extends BaseEntity{
+	private Long userId;
+
+	private Long orderId;
+
+	/**
+	 * 商品类型(0商品被删除,1虚物/2实物)
+	 */
+	private Integer goodsType;
+
+	/**
+	 * 退款订单号
+	 */
+	private String outRefundNo;
+
+	private Long goodsId;
+
+	private Long  skuId;
+
+	private String goodsTitle;
+
+	/**
+	 * 平台规格(金额)
+	 */
+	private String specVal;
+
+	/**
+	 * 订单总金额
+	 */
+	private BigDecimal totalFee;
+
+	private BigDecimal refundFee;
+
+	private String refundDesc;
+
+	/**
+	 * 退款方式
+	 */
+	private String refundMethod;
+}

+ 6 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/OrderDonView.java

@@ -63,6 +63,12 @@ public class OrderDonView {
     @DbField("o.status")
     @DbField("o.status")
     private String status;
     private String status;
 
 
+    @DbField("o.type")
+    private String type;
+
+    @DbField("o.order_type")
+    private Integer orderType;
+
     @DbField("o.num")
     @DbField("o.num")
     private Integer num;
     private Integer num;
 
 

+ 26 - 0
netflix-dao/src/main/java/com/cyksj/model/request/WxOrderRefundReq.java

@@ -0,0 +1,26 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/*
+ *项目名: netflix
+ *文件名: WxOrderRefundReq
+ *创建者: JavaZou
+ *创建时间:2022/11/8 16:51
+ */
+@Getter
+@Setter
+public class WxOrderRefundReq {
+	@NotNull(message = "订单id不为空")
+	private Long orderId;
+
+	/**
+	 * 退款金额
+	 */
+	@NotNull(message = "退款金额不为空")
+	private BigDecimal refundMoney;
+}

+ 26 - 0
netflix-dao/src/main/java/com/cyksj/model/request/ZfbOrderRefundReq.java

@@ -0,0 +1,26 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/*
+ *项目名: netflix
+ *文件名: ZfbOrderRefundReq
+ *创建者: JavaZou
+ *创建时间:2022/11/8 16:51
+ */
+@Getter
+@Setter
+public class ZfbOrderRefundReq {
+	@NotNull(message = "订单id不为空")
+	private Long orderId;
+
+	/**
+	 * 退款金额
+	 */
+	@NotNull(message = "退款金额不为空")
+	private BigDecimal refundMoney;
+}

+ 67 - 0
netflix-dao/src/main/java/com/cyksj/model/response/AliPayRefundRep.java

@@ -0,0 +1,67 @@
+package com.cyksj.model.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/*
+ *项目名: netflix
+ *文件名: AlipayTradeRefundResponse
+ *创建者: JavaZou
+ *创建时间:2022/11/9 14:27
+ */
+@Getter
+@Setter
+public class AliPayRefundRep {
+
+	/**
+	 * 签名
+	 */
+	private String sign;
+
+	@JsonProperty("alipay_trade_refund_response")
+	private AlipayTradeRefundResponse response;
+
+	@Getter
+	@Setter
+	public static class AlipayTradeRefundResponse{
+		private String code;
+
+		private String msg;
+
+		/**
+		 * 支付宝交易号
+		 */
+		@JsonProperty("trade_no")
+		private String tradeNo;
+
+		/**
+		 * 商家订单号
+		 */
+		@JsonProperty("out_trade_no")
+		private String outTradeNo;
+
+		@JsonProperty("open_id")
+		private String openId;
+
+		/**
+		 * 本次退款是否发生了资金变化 Y
+		 */
+		@JsonProperty("fund_change")
+		private String fundChange;
+
+		/**
+		 * 退款时间
+		 */
+		@JsonProperty("gmt_refund_pay")
+		private String gmtRefundPay;
+
+		/**
+		 * 退款金额
+		 */
+		@JsonProperty("refund_fee")
+		private BigDecimal refundFee;
+	}
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/views/BackgroundDataView.java

@@ -34,6 +34,16 @@ public class BackgroundDataView {
 	 */
 	 */
 	private BigDecimal totalOrderAmount;
 	private BigDecimal totalOrderAmount;
 
 
+	/**
+	 * 退款单数
+	 */
+	private Integer totalNumOfRefundOrder;
+
+	/**
+	 * 退款金额
+	 */
+	private BigDecimal totalRefundAmount;
+
 	/**
 	/**
 	 * 近5天将要到期的主账号总数
 	 * 近5天将要到期的主账号总数
 	 */
 	 */

+ 37 - 0
netflix-service/src/main/java/com/cyksj/common/AliPayCommon.java

@@ -0,0 +1,37 @@
+package com.cyksj.common;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.cyksj.config.zfb.BaseZfbConfig;
+import com.cyksj.config.zfb.ZfbConfig;
+
+/*
+ *项目名: netflix
+ *文件名: AliPayCommon
+ *创建者: JavaZou
+ *创建时间:2022/11/9 14:07
+ */
+public class AliPayCommon {
+
+	/**
+	 * 获取ALI支付客户端
+	 * @param zfbConfig
+	 * @return
+	 * @throws AlipayApiException
+	 */
+	public static AlipayClient getAlipayClient(ZfbConfig zfbConfig) throws AlipayApiException {
+		AlipayConfig alipayConfig = new AlipayConfig();
+		//网关地址
+		alipayConfig.setServerUrl(BaseZfbConfig.url);
+		alipayConfig.setAppId(zfbConfig.getAppId());
+		alipayConfig.setPrivateKey(zfbConfig.getPrivateKey());
+		alipayConfig.setFormat(BaseZfbConfig.format);
+		alipayConfig.setCharset(BaseZfbConfig.charset);
+		alipayConfig.setSignType(BaseZfbConfig.signType);
+		alipayConfig.setAlipayPublicKey(zfbConfig.getAlipayPublicKey());
+		AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
+		return alipayClient;
+	}
+}

+ 7 - 0
netflix-service/src/main/java/com/cyksj/redis/RedisService.java

@@ -574,6 +574,13 @@ public class RedisService {
         }
         }
     }
     }
 
 
+    public boolean setNx(String key, Object value, Long time) {
+        if (time > 0) {
+            return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
+        }
+        return false;
+    }
+
     @AllArgsConstructor
     @AllArgsConstructor
     @Getter
     @Getter
     public enum key {
     public enum key {

+ 6 - 0
netflix-service/src/main/java/com/cyksj/service/mange/statistics/impl/StatisticsDataServiceImpl.java

@@ -153,6 +153,12 @@ public class StatisticsDataServiceImpl implements StatisticsDataService {
 		Optional<BigDecimal> totalMoney = orderDons.stream().map(OrderDon::getMoney).reduce(BigDecimal::add);
 		Optional<BigDecimal> totalMoney = orderDons.stream().map(OrderDon::getMoney).reduce(BigDecimal::add);
 		//总订单金额
 		//总订单金额
 		backgroundDataView.setTotalOrderAmount(totalMoney.isPresent() ? totalMoney.get().divide(BigDecimal.valueOf(100)) : BigDecimal.ZERO);
 		backgroundDataView.setTotalOrderAmount(totalMoney.isPresent() ? totalMoney.get().divide(BigDecimal.valueOf(100)) : BigDecimal.ZERO);
+		//退款列表
+		List<OrderDon> refundOrders = Optional.ofNullable(orderDonService.list(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getStatus, OrderDon.Status.refund).select(OrderDon::getId, OrderDon::getRefundMoney))).orElse(new ArrayList<>());
+		backgroundDataView.setTotalNumOfRefundOrder(refundOrders.size());
+		//退款金额
+		Optional<BigDecimal> totalRefundAmount = refundOrders.stream().map(OrderDon::getRefundMoney).reduce(BigDecimal::add);
+		backgroundDataView.setTotalRefundAmount(totalRefundAmount.isPresent() ? totalRefundAmount.get().divide(BigDecimal.valueOf(100)) : BigDecimal.ZERO);
 		//近5天将要到期的主账号总数
 		//近5天将要到期的主账号总数
 		DateTime now = DateTime.now();
 		DateTime now = DateTime.now();
 		DateTime zero = DateUtil.beginOfDay(now);
 		DateTime zero = DateUtil.beginOfDay(now);

+ 18 - 0
netflix-service/src/main/java/com/cyksj/service/order/OrderDonService.java

@@ -1,6 +1,7 @@
 package com.cyksj.service.order;
 package com.cyksj.service.order;
 
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.dto.Result;
 import com.cyksj.model.dto.AliPayParams;
 import com.cyksj.model.dto.AliPayParams;
 import com.cyksj.model.dto.H5JsPayParams;
 import com.cyksj.model.dto.H5JsPayParams;
 import com.cyksj.model.entity.GoodsDon;
 import com.cyksj.model.entity.GoodsDon;
@@ -9,6 +10,8 @@ import com.cyksj.model.entity.OrderDon;
 import com.cyksj.model.entity.User;
 import com.cyksj.model.entity.User;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.request.OrderPayRequest;
 import com.cyksj.model.request.OrderPayRequest;
+import com.cyksj.model.request.WxOrderRefundReq;
+import com.cyksj.model.request.ZfbOrderRefundReq;
 
 
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
@@ -58,6 +61,21 @@ public interface OrderDonService extends IService<OrderDon> {
      */
      */
     void aliPayNotify(Map<String, String> paramsMap) throws Exception;
     void aliPayNotify(Map<String, String> paramsMap) throws Exception;
 
 
+    /**
+     * 微信退款
+     */
+    Result<String> wxRefund(WxOrderRefundReq wxOrderRefundReq) throws Exception;
+
+    /**
+     * 微信退款回调
+     */
+    void wxRefundNotify(Map<String, String> map) throws Exception;
+
+    /**
+     * 支付宝退款
+     */
+    Result<String> zfbRefund(ZfbOrderRefundReq zfbOrderRefundReq) throws Exception;
+
     /**
     /**
      * 发送新订单模板消息
      * 发送新订单模板消息
      */
      */

+ 228 - 12
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -3,14 +3,21 @@ package com.cyksj.service.order.impl;
 import cn.hutool.core.date.DateField;
 import cn.hutool.core.date.DateField;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.date.DateUtil;
-import com.alipay.api.*;
+import cn.hutool.core.util.StrUtil;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayRequest;
 import com.alipay.api.domain.AlipayTradePagePayModel;
 import com.alipay.api.domain.AlipayTradePagePayModel;
+import com.alipay.api.domain.AlipayTradeRefundModel;
 import com.alipay.api.domain.AlipayTradeWapPayModel;
 import com.alipay.api.domain.AlipayTradeWapPayModel;
 import com.alipay.api.internal.util.AlipaySignature;
 import com.alipay.api.internal.util.AlipaySignature;
 import com.alipay.api.request.AlipayTradePagePayRequest;
 import com.alipay.api.request.AlipayTradePagePayRequest;
+import com.alipay.api.request.AlipayTradeRefundRequest;
 import com.alipay.api.request.AlipayTradeWapPayRequest;
 import com.alipay.api.request.AlipayTradeWapPayRequest;
+import com.alipay.api.response.AlipayTradeRefundResponse;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.common.AliPayCommon;
 import com.cyksj.common.EnvCommonService;
 import com.cyksj.common.EnvCommonService;
 import com.cyksj.common.constant.TemplateEnum;
 import com.cyksj.common.constant.TemplateEnum;
 import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.exception.BusinessRuntimeException;
@@ -18,11 +25,14 @@ import com.cyksj.common.snowflake.Sequence;
 import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
 import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
 import com.cyksj.common.util.Codec;
 import com.cyksj.common.util.Codec;
 import com.cyksj.common.util.Jsons;
 import com.cyksj.common.util.Jsons;
+import com.cyksj.common.util.Xmls;
 import com.cyksj.config.WeChatConfig;
 import com.cyksj.config.WeChatConfig;
 import com.cyksj.config.zfb.BaseZfbConfig;
 import com.cyksj.config.zfb.BaseZfbConfig;
 import com.cyksj.config.zfb.ZfbConfig;
 import com.cyksj.config.zfb.ZfbConfig;
 import com.cyksj.config.zfb.ZfbProductCode;
 import com.cyksj.config.zfb.ZfbProductCode;
 import com.cyksj.dto.RedisKey;
 import com.cyksj.dto.RedisKey;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
 import com.cyksj.mapper.*;
 import com.cyksj.mapper.*;
 import com.cyksj.model.dto.AliPayParams;
 import com.cyksj.model.dto.AliPayParams;
 import com.cyksj.model.dto.H5JsPayParams;
 import com.cyksj.model.dto.H5JsPayParams;
@@ -32,6 +42,9 @@ import com.cyksj.model.entity.*;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.request.OrderAttach;
 import com.cyksj.model.request.OrderAttach;
 import com.cyksj.model.request.OrderPayRequest;
 import com.cyksj.model.request.OrderPayRequest;
+import com.cyksj.model.request.WxOrderRefundReq;
+import com.cyksj.model.request.ZfbOrderRefundReq;
+import com.cyksj.model.response.AliPayRefundRep;
 import com.cyksj.redis.RedisService;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.order.OrderDonService;
 import com.cyksj.service.order.OrderDonService;
 import com.cyksj.service.wechat.WeChatService;
 import com.cyksj.service.wechat.WeChatService;
@@ -105,6 +118,8 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 
 	private final CustomerServiceMapper customerServiceMapper;
 	private final CustomerServiceMapper customerServiceMapper;
 
 
+	private final RefundOrderDonMapper refundOrderDonMapper;
+
 
 
 	@Transactional(rollbackFor = Throwable.class)
 	@Transactional(rollbackFor = Throwable.class)
 	@Override
 	@Override
@@ -447,7 +462,8 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 		orderDon.setMoney(sku.getPrice().multiply(BigDecimal.valueOf(payRequest.getNum())));
 		orderDon.setMoney(sku.getPrice().multiply(BigDecimal.valueOf(payRequest.getNum())));
 		orderDon.setSkuId(payRequest.getSkuId());
 		orderDon.setSkuId(payRequest.getSkuId());
 		orderDon.setRelationId(relation.getId());
 		orderDon.setRelationId(relation.getId());
-
+		//续费订单
+		orderDon.setOrderType(2);
 		orderDon.setStatus(OrderDon.Status.noPayment);
 		orderDon.setStatus(OrderDon.Status.noPayment);
 		orderDon.setUserId(user.getId());
 		orderDon.setUserId(user.getId());
 		orderDon.setGoodsId(goodsDon.getId());
 		orderDon.setGoodsId(goodsDon.getId());
@@ -522,6 +538,215 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 		this.updateById(orderDon);
 		this.updateById(orderDon);
 	}
 	}
 
 
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public Result<String> wxRefund(WxOrderRefundReq wxOrderRefundReq) throws Exception {
+		OrderDon orderDon = orderDonMapper.selectById(wxOrderRefundReq.getOrderId());
+		GoodsDonSku sku = goodsDonSkuMapper.selectById(orderDon.getSkuId());
+		GoodsDon goodsDon = goodsDonMapper.selectById(sku.getGoodsId());
+		if (orderDon.getType() != OrderDon.Type.WeChat) {
+			throw BusinessRuntimeException.getInstance("该订单不是微信支付的订单");
+		}
+		//校验订单是否符合退款
+		checkOrderRefund(orderDon, goodsDon);
+		if (wxOrderRefundReq.getRefundMoney().compareTo(orderDon.getMoney()) > 0) {
+			throw BusinessRuntimeException.getInstance("退款金额大于订单金额");
+		}
+		if (!redisService.setNx(String.format("%s", wxOrderRefundReq.getOrderId()), wxOrderRefundReq.getOrderId(), 10l)) {
+			throw BusinessRuntimeException.getInstance("请勿重复点击,正在退款中..");
+		}
+
+		long time = System.currentTimeMillis();
+		//退款订单号
+		String outRefundNo = Codec.DoDigest.custom()
+				.setAlgorithm(Codec.DoDigest.Algorithm.MD5)
+				.setStringData(time + String.valueOf(orderDon.getId()))
+				.toHexString();
+		weChatService.refundWxOrder(weChatConfig.getAppid(),
+				orderDon.getTransactionId(),
+				outRefundNo,
+				orderDon.getMoney(),
+				wxOrderRefundReq.getRefundMoney(),
+				weChatConfig.getDomain() + "wx/refund/notify");
+		//修改订单状态
+		orderDon.setStatus(OrderDon.Status.refund);
+		orderDon.setRefundMoney(wxOrderRefundReq.getRefundMoney());
+		orderDonMapper.updateById(orderDon);
+		//记录退款信息
+		THREAD_POOL_TASK_EXECUTOR.execute(()->{
+			refundRecord(orderDon, goodsDon, sku, outRefundNo);
+		});
+		//虚物订单退款,清出车队,车票变为已过期
+		clearUnrealGoodsDetail(orderDon, goodsDon);
+		return GatewayResponse.SUCCESS.newBuilder().toResult("退款成功");
+	}
+
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public Result<String> zfbRefund(ZfbOrderRefundReq zfbOrderRefundReq) throws Exception {
+		OrderDon orderDon = orderDonMapper.selectById(zfbOrderRefundReq.getOrderId());
+		GoodsDonSku sku = goodsDonSkuMapper.selectById(orderDon.getSkuId());
+		GoodsDon goodsDon = goodsDonMapper.selectById(sku.getGoodsId());
+		if (orderDon.getType() != OrderDon.Type.ALI_PAY) {
+			throw BusinessRuntimeException.getInstance("该订单不是支付宝支付的订单");
+		}
+		//校验订单是否符合退款
+		checkOrderRefund(orderDon, goodsDon);
+		if (zfbOrderRefundReq.getRefundMoney().compareTo(orderDon.getMoney()) > 0) {
+			throw BusinessRuntimeException.getInstance("退款金额大于订单金额");
+		}
+		if (!redisService.setNx(String.format("%s", zfbOrderRefundReq.getOrderId()), zfbOrderRefundReq.getOrderId(), 10l)) {
+			throw BusinessRuntimeException.getInstance("请勿重复点击,正在退款中..");
+		}
+		//支付宝退款
+		AlipayClient alipayClient = AliPayCommon.getAlipayClient(ZfbConfig.YHLX_ZfB);
+		AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+		AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+		//支付宝交易号
+		model.setTradeNo(orderDon.getTransactionId());
+		model.setRefundAmount(zfbOrderRefundReq.getRefundMoney().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN).toString());
+		model.setQueryOptions(List.of("gmt_refund_pay"));
+		long time = System.currentTimeMillis();
+		//退款订单号
+		String outRefundNo = Codec.DoDigest.custom()
+				.setAlgorithm(Codec.DoDigest.Algorithm.MD5)
+				.setStringData(time + String.valueOf(orderDon.getId()))
+				.toHexString();
+		//退款请求号
+		model.setOutRequestNo(outRefundNo);
+		request.setBizModel(model);
+		AlipayTradeRefundResponse response = alipayClient.execute(request);
+		if (response.isSuccess()) {
+			AliPayRefundRep aliPayRefundRep = Jsons.parseObject(response.getBody(), AliPayRefundRep.class);
+			AliPayRefundRep.AlipayTradeRefundResponse res = aliPayRefundRep.getResponse();
+			//修改订单状态
+			orderDon.setStatus(OrderDon.Status.refund);
+			orderDon.setRefundMoney(res.getRefundFee().multiply(BigDecimal.valueOf(100)));
+			orderDonMapper.updateById(orderDon);
+			//记录退款信息
+			THREAD_POOL_TASK_EXECUTOR.execute(()->{
+				refundRecord(orderDon, goodsDon, sku, outRefundNo);
+			});
+			//虚物订单退款,清出车队,车票变为已过期
+			clearUnrealGoodsDetail(orderDon, goodsDon);
+			return GatewayResponse.SUCCESS.newBuilder().toResult("退款成功");
+		}
+		throw BusinessRuntimeException.getInstance("退款失败,e:{0}", response.getBody());
+	}
+
+	/**
+	 * 虚物订单退款,清出车队,车票变成已过期
+	 * @param orderDon
+	 * @param goodsDon
+	 */
+	private void clearUnrealGoodsDetail(OrderDon orderDon, GoodsDon goodsDon) {
+		if (goodsDon != null && goodsDon.getType() == 1) {
+			//清出车队,车位变为已过期
+			Long relationId = orderDon.getRelationId();
+			GroupsRelation relation = groupsRelationMapper.selectById(relationId);
+			if (relation != null) {
+				relation.setUserId(0l);
+				relation.setStatus(GroupsRelation.Status.none);
+				groupsRelationMapper.updateById(relation);
+				delRelation(relation.getId());
+			}
+		}
+	}
+
+	/**
+	 * 校验订单是否符合退款操作
+	 * @param orderDon
+	 */
+	private void checkOrderRefund(OrderDon orderDon, GoodsDon goodsDon) {
+		if (orderDon == null) {
+			throw BusinessRuntimeException.getInstance("订单不存在");
+		}
+		if (orderDon.getType() == OrderDon.Type.EX_CODE) {
+			throw BusinessRuntimeException.getInstance("兑换码订单不支持退款");
+		}
+		if (OrderDon.Status.refund == orderDon.getStatus()) {
+			throw BusinessRuntimeException.getInstance("该订单已退款");
+		}
+		if (OrderDon.Status.noPayment == orderDon.getStatus() || OrderDon.Status.close == orderDon.getStatus()) {
+			throw BusinessRuntimeException.getInstance("订单{0}状态不支持退款", orderDon.getStatus());
+		}
+		//续费订单
+		if (orderDon.getOrderType() == 2) {
+			throw BusinessRuntimeException.getInstance("续费订单不支持退款");
+		}
+		if (goodsDon != null) {
+			//虚物订单 7天内可退款
+			if (goodsDon.getType() == 1 && DateTime.now().isAfter(DateUtil.offsetDay(orderDon.getPayTime(), 7))) {
+				throw BusinessRuntimeException.getInstance("该虚物订单已过退款时间");
+			}
+		}
+	}
+
+	/**
+	 * 记录退款信息
+	 */
+	private void refundRecord(OrderDon orderDon, GoodsDon goodsDon, GoodsDonSku sku, String outRefundNo) {
+		RefundOrderDon refundOrderDon = refundOrderDonMapper.selectOne(Wrappers.lambdaQuery(RefundOrderDon.class).eq(RefundOrderDon::getOrderId, orderDon.getId()).eq(RefundOrderDon::getOutRefundNo, outRefundNo).last("limit 1"));
+		if (refundOrderDon == null) {
+			RefundOrderDon refund = new RefundOrderDon();
+			refund.setOrderId(orderDon.getId());
+			refund.setOutRefundNo(outRefundNo);
+			refund.setTotalFee(orderDon.getMoney());
+			refund.setRefundDesc("订单退款");
+			//退款金额
+			refund.setRefundFee(orderDon.getRefundMoney());
+			refund.setUserId(orderDon.getUserId());
+			refund.setGoodsId(orderDon.getGoodsId());
+			refund.setGoodsTitle(goodsDon == null ? null : goodsDon.getTitle());
+			refund.setSkuId(orderDon.getSkuId());
+			refund.setSpecVal(sku == null ? null : String.format("%s%s", sku.getSpecVal(), sku.getPrice().divide(BigDecimal.valueOf(100))));
+			refund.setRefundMethod(orderDon.getType().name());
+			refund.setGoodsType(goodsDon == null ? 0 : goodsDon.getType());
+			refundOrderDonMapper.insert(refund);
+		}
+	}
+
+	@Override
+	public void wxRefundNotify(Map<String, String> map) throws Exception {
+		boolean f = weChatService.checkSignWithMd5(map, weChatConfig.getShopKey());
+		if (!f) {
+			log.error("回调数据:{}", map);
+			throw BusinessRuntimeException.getInstance("支付成功回调, 校验签名失败.");
+		}
+		// 对退款数据 解密
+		String md5Key = Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.MD5).setStringData(weChatConfig.getShopKey()).toHexString();
+		String reqInfoXml = Codec.DoCipher.custom().setAlgorithm(Codec.DoCipher.Trans.AES_ECB_PKCS7Padding).setBase64Data(map.get("req_info")).setStringKey(md5Key).ofDecrypt().toText();
+		log.info("=== 退款数据解密: \n {}", reqInfoXml);
+		Map<String, String> reqInfoMap = Xmls.toMap(reqInfoXml);
+		// 退款失败
+		if (StrUtil.equals("CHANGE", reqInfoMap.get("refund_status"))) {
+			throw BusinessRuntimeException.getInstance("退款异常.");
+		} else if (StrUtil.equals("REFUNDCLOSE", reqInfoMap.get("refund_status"))) {
+			throw BusinessRuntimeException.getInstance("退款关闭.");
+		}
+		/* 此处处理业务逻辑 */
+		String outRefundNo = reqInfoMap.get("out_refund_no");
+		String transactionId = reqInfoMap.get("transaction_id");
+		String refundFee = reqInfoMap.get("refund_fee");
+		OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getTransactionId, transactionId).last("limit 1"));
+		if (orderDon == null) {
+			log.error("退款回调,transactionId:{}订单不存在", transactionId);
+			return;
+		}
+		BigDecimal refundMoney = new BigDecimal(refundFee);
+		if (orderDon.getStatus() == OrderDon.Status.refund) {
+			if (!orderDon.getRefundMoney().equals(refundMoney)) {
+				orderDon.setRefundMoney(refundMoney);
+				orderDonMapper.updateById(orderDon);
+			}
+		} else {
+			orderDon.setStatus(OrderDon.Status.refund);
+			orderDon.setRefundMoney(refundMoney);
+			orderDonMapper.updateById(orderDon);
+		}
+		log.info("微信退款: 退款单号[{}] 退款成功.", outRefundNo);
+	}
+
 	public void setRelation(Long relationId, Long userId, Long goodsId) {
 	public void setRelation(Long relationId, Long userId, Long goodsId) {
         int noPayCount = count(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getGoodsId, goodsId).eq(OrderDon::getStatus, OrderDon.Status.noPayment.toString()));
         int noPayCount = count(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getGoodsId, goodsId).eq(OrderDon::getStatus, OrderDon.Status.noPayment.toString()));
 
 
@@ -572,16 +797,7 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 	}
 	}
 
 
 	public AliPayParams commonAliPay(Long orderId, String productCode, ZfbConfig zfbConfig) throws Exception {
 	public AliPayParams commonAliPay(Long orderId, String productCode, ZfbConfig zfbConfig) throws Exception {
-		AlipayConfig alipayConfig = new AlipayConfig();
-		//网关地址
-		alipayConfig.setServerUrl(BaseZfbConfig.url);
-		alipayConfig.setAppId(zfbConfig.getAppId());
-		alipayConfig.setPrivateKey(zfbConfig.getPrivateKey());
-		alipayConfig.setFormat(BaseZfbConfig.format);
-		alipayConfig.setCharset(BaseZfbConfig.charset);
-		alipayConfig.setSignType(BaseZfbConfig.signType);
-		alipayConfig.setAlipayPublicKey(zfbConfig.getAlipayPublicKey());
-		AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
+		AlipayClient alipayClient = AliPayCommon.getAlipayClient(zfbConfig);
 		// 创建API对应的request
 		// 创建API对应的request
 		AlipayRequest alipayRequest = getAliAayRequest(orderId, productCode);
 		AlipayRequest alipayRequest = getAliAayRequest(orderId, productCode);
 		if (alipayRequest == null) {
 		if (alipayRequest == null) {

+ 9 - 1
netflix-service/src/main/java/com/cyksj/service/wechat/WeChatService.java

@@ -3,6 +3,7 @@ package com.cyksj.service.wechat;
 
 
 import com.cyksj.model.dto.*;
 import com.cyksj.model.dto.*;
 
 
+import java.math.BigDecimal;
 import java.util.Map;
 import java.util.Map;
 
 
 public interface WeChatService {
 public interface WeChatService {
@@ -61,5 +62,12 @@ public interface WeChatService {
      */
      */
     void sendTemplateMessage(String accessToken, String json) throws Exception;
     void sendTemplateMessage(String accessToken, String json) throws Exception;
 
 
-
+    /**
+     * @param appId 应用id
+     * @param transactionId 微信支付订单号
+     * @param outRefundNo 商户退款单号
+     * @param totalFee  订单金额 单位为分,只能为整数
+     * @param refundMoney  退款金额
+     */
+    void refundWxOrder(String appId, String transactionId, String outRefundNo, BigDecimal totalFee, BigDecimal refundMoney, String notifyUrl) throws Exception;
 }
 }

+ 56 - 1
netflix-service/src/main/java/com/cyksj/service/wechat/impl/WeChatServiceImpl.java

@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
 
 
 import javax.crypto.Mac;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import javax.crypto.spec.SecretKeySpec;
+import java.math.BigDecimal;
 import java.net.URLEncoder;
 import java.net.URLEncoder;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse;
@@ -391,7 +392,61 @@ public class WeChatServiceImpl implements WeChatService {
         log.info("发送模板消息结束,返回值:{}", send != null ? send.body() : null);
         log.info("发送模板消息结束,返回值:{}", send != null ? send.body() : null);
     }
     }
 
 
-    
+    @Override
+    public void refundWxOrder(String appId, String transactionId, String outRefundNo, BigDecimal totalFee, BigDecimal refundMoney, String notifyUrl) throws Exception {
+        BaseWeChatConfig weChatConfigStrategy = WeChatFactory.getWeChatConfigStrategy(appId);
+        WxRefundOrder wxRefundOrder = new WxRefundOrder();
+        wxRefundOrder.setAppid(appId);
+        //商户号
+        wxRefundOrder.setMchId(weChatConfigStrategy.getShopid());
+        wxRefundOrder.setOutRefundNo(outRefundNo);
+        wxRefundOrder.setNonceStr("chan123");
+        wxRefundOrder.setTotalFee(totalFee.intValue());
+        //退款金额
+        wxRefundOrder.setRefundFee(refundMoney.intValue());
+        wxRefundOrder.setTransactionId(transactionId);
+//        wxRefundOrder.setRefundDesc("订单退款");
+        wxRefundOrder.setNotifyUrl(notifyUrl);
+        String sign = Codec.DoDigest.custom()
+                .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
+                .setStringData(wxRefundOrder.println(weChatConfigStrategy.getShopKey()))
+                .toHexString()
+                .toUpperCase();
+        wxRefundOrder.setSign(sign);
+
+        String xml = Xmls.toXml(wxRefundOrder);
+        HttpResponse<byte[]> res = J11HttpC.custom()
+                .ofPost()
+                .url("https://api.mch.weixin.qq.com/secapi/pay/refund")
+                .headers(J11HttpC.ReqType.raw_xml)
+                .cacheURI()
+                .body(HttpRequest.BodyPublishers.ofString(xml, IoKit.Charsets.UTF_8.getCharset()))
+                // 指定携带ssl证书的httpClient 进行请求
+                .send(J11HttpC.Client.WX_REFUND, HttpResponse.BodyHandlers.ofByteArray());
+
+        if (200 != res.statusCode()) {
+            throw BusinessRuntimeException.getInstance("请求wx_refund失败: " + IoKit.toString(res.body()));
+        }
+
+        Map<String, String> wxRefund = Xmls.toMap(res.body());
+        log.info("微信返回值 xml 转 map: \n{}", wxRefund);
+
+        if (!StringUtil.equals(wxRefund.get("return_code"), "SUCCESS") || !StringUtil.equals(wxRefund.get("result_code"), "SUCCESS")) {
+            throw BusinessRuntimeException.getInstance("调用微信申请退款失败. 微信返回: " + wxRefund);
+        }
+        if (StringUtil.equals(wxRefund.get("return_code"), "SUCCESS") && !StringUtil.equals(wxRefund.get("result_code"), "SUCCESS")) {
+            throw BusinessRuntimeException.getInstance("订单异常: " + wxRefund.get("err_code") + "[" + wxRefund.get("err_code_des") + "]");
+        }
+
+        // 校验签名
+        boolean f = this.checkSignWithMd5(wxRefund, weChatConfigStrategy.getShopKey());
+        if (!f) {
+            throw BusinessRuntimeException.getInstance("调用微信h5申请退款, 微信返回值的签名验证失败.");
+        }
+
+        log.info("------------------- 调用微信申请退款 结束 -------------------");
+    }
+
 
 
     /**
     /**
      * sha256_HMAC加密
      * sha256_HMAC加密

+ 63 - 29
netflix-web/src/main/java/com/cyksj/web/controller/payment/OrderController.java

@@ -1,11 +1,14 @@
 package com.cyksj.web.controller.payment;
 package com.cyksj.web.controller.payment;
 
 
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.qrcode.QrCodeUtil;
 import cn.hutool.extra.qrcode.QrCodeUtil;
 import cn.hutool.extra.qrcode.QrConfig;
 import cn.hutool.extra.qrcode.QrConfig;
 import com.cyksj.common.EnvCommonService;
 import com.cyksj.common.EnvCommonService;
 import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.util.IoKit;
 import com.cyksj.common.util.IoKit;
 import com.cyksj.common.util.Xmls;
 import com.cyksj.common.util.Xmls;
+import com.cyksj.config.WeChatConfig;
+import com.cyksj.config.zfb.BaseZfbConfig;
 import com.cyksj.config.zfb.ZfbProductCode;
 import com.cyksj.config.zfb.ZfbProductCode;
 import com.cyksj.dto.Result;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
 import com.cyksj.enums.GatewayResponse;
@@ -14,6 +17,8 @@ import com.cyksj.model.dto.H5JsPayParams;
 import com.cyksj.model.entity.OrderDon;
 import com.cyksj.model.entity.OrderDon;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.request.OrderPayRequest;
 import com.cyksj.model.request.OrderPayRequest;
+import com.cyksj.model.request.WxOrderRefundReq;
+import com.cyksj.model.request.ZfbOrderRefundReq;
 import com.cyksj.model.response.AliPayTradeStatus;
 import com.cyksj.model.response.AliPayTradeStatus;
 import com.cyksj.service.order.OrderDonService;
 import com.cyksj.service.order.OrderDonService;
 import com.cyksj.web.util.StpUserUtil;
 import com.cyksj.web.util.StpUserUtil;
@@ -55,6 +60,9 @@ public class OrderController {
     @Autowired
     @Autowired
     private EnvCommonService envCommonService;
     private EnvCommonService envCommonService;
 
 
+    @Autowired
+    private WeChatConfig weChatConfig;
+
     /**
     /**
      * 支付
      * 支付
      * @author chan
      * @author chan
@@ -81,13 +89,7 @@ public class OrderController {
     public Result<H5JsPayParams> pay(@PathVariable Long orderId) throws Exception {
     public Result<H5JsPayParams> pay(@PathVariable Long orderId) throws Exception {
         final String methodName = "吊起微信支付";
         final String methodName = "吊起微信支付";
         log.info("{}[start] orderId:{}",methodName,orderId);
         log.info("{}[start] orderId:{}",methodName,orderId);
-        OrderDon orderDon = orderDonService.getById(orderId);
-        if (orderDon == null) {
-            throw BusinessRuntimeException.getInstance("订单不存在");
-        }
-        if (orderDon.getStatus() == OrderDon.Status.close) {
-            throw BusinessRuntimeException.getInstance("订单已关闭");
-        }
+
         H5JsPayParams payParams = orderDonService.pay(orderId, "JSAPI");
         H5JsPayParams payParams = orderDonService.pay(orderId, "JSAPI");
 
 
         return GatewayResponse.SUCCESS.newBuilder().toResult(payParams);
         return GatewayResponse.SUCCESS.newBuilder().toResult(payParams);
@@ -99,13 +101,6 @@ public class OrderController {
      */
      */
     @PostMapping("/get/wx/orderQrCode/{orderId}")
     @PostMapping("/get/wx/orderQrCode/{orderId}")
     public Result<String> createUnifiedOrderQr(@PathVariable Long orderId) throws Exception {
     public Result<String> createUnifiedOrderQr(@PathVariable Long orderId) throws Exception {
-        OrderDon orderDon = orderDonService.getById(orderId);
-        if (orderDon == null) {
-            throw BusinessRuntimeException.getInstance("订单不存在");
-        }
-        if (orderDon.getStatus() == OrderDon.Status.close) {
-            throw BusinessRuntimeException.getInstance("订单已关闭");
-        }
         //预下单
         //预下单
         H5JsPayParams payParams = orderDonService.wxPcWebPay(orderId, "NATIVE");
         H5JsPayParams payParams = orderDonService.wxPcWebPay(orderId, "NATIVE");
         //获取二维码链接code_url
         //获取二维码链接code_url
@@ -114,6 +109,44 @@ public class OrderController {
         return GatewayResponse.SUCCESS.newBuilder().toResult(QrCodeUtil.generateAsBase64(codeUrl, qrConfig, "png"));
         return GatewayResponse.SUCCESS.newBuilder().toResult(QrCodeUtil.generateAsBase64(codeUrl, qrConfig, "png"));
     }
     }
 
 
+    /**
+     * 微信订单退款
+     */
+    @PostMapping("/wx/refund")
+    private Result<String> wxRefund(@RequestBody @Validated WxOrderRefundReq wxOrderRefundReq) throws Exception {
+        return orderDonService.wxRefund(wxOrderRefundReq);
+    }
+
+    /**
+     * 微信退款回调
+     */
+    @PostMapping("/wx/refund/notify")
+    public void wxRefundNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        String xml = IoKit.toString(request.getInputStream());
+        log.info("=== 退款成功的回调: \n {}", xml);
+
+        // 解析xml
+        Map<String, String> map = Xmls.toMap(xml);
+
+        // 退款失败
+        if (!StrUtil.equals("SUCCESS", map.get("return_code"))) {
+            throw BusinessRuntimeException.getInstance("退款失败.");
+        }
+
+        orderDonService.wxRefundNotify(map);
+
+        String toWechatXmlResponse = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
+
+        OutputStream out = null;
+        try {
+            out = response.getOutputStream();
+            IoKit.write(toWechatXmlResponse, out);
+        } catch (Throwable e) {
+            throw BusinessRuntimeException.getInstance(e == null ? null : e.getMessage());
+        } finally {
+            IoKit.close(out);
+        }
+    }
     /**
     /**
      * 微信支付回调
      * 微信支付回调
      * @author chan
      * @author chan
@@ -187,21 +220,14 @@ public class OrderController {
 	 */
 	 */
 	@PostMapping("/post/pay/zfb/{orderId}")
 	@PostMapping("/post/pay/zfb/{orderId}")
     @Deprecated
     @Deprecated
-	public Result<String> aliPay(@PathVariable Long orderId,HttpServletResponse response) throws Exception {
-        OrderDon orderDon = orderDonService.getById(orderId);
-        if (orderDon == null) {
-            throw BusinessRuntimeException.getInstance("订单不存在");
-        }
-        if (orderDon.getStatus() == OrderDon.Status.close) {
-            throw BusinessRuntimeException.getInstance("订单已关闭");
-        }
+	public void aliPay(@PathVariable Long orderId,HttpServletResponse response) throws Exception {
         AliPayParams aliPayParams = orderDonService.aliPay(orderId, ZfbProductCode.MOBILE_WEB.getProductCode());
         AliPayParams aliPayParams = orderDonService.aliPay(orderId, ZfbProductCode.MOBILE_WEB.getProductCode());
-//        response.setContentType("text/html;charset=" + BaseZfbConfig.charset);
-//        //直接将完整的表单html输出到页面
-//        response.getWriter().write(aliPayParams.getBody());
-//        response.getWriter().flush();
-//        response.getWriter().close();
-        return GatewayResponse.SUCCESS.newBuilder().toResult(aliPayParams.getBody());
+        response.setContentType("text/html;charset=" + BaseZfbConfig.charset);
+        //直接将完整的表单html输出到页面
+        response.getWriter().write(aliPayParams.getBody());
+        response.getWriter().flush();
+        response.getWriter().close();
+//        return GatewayResponse.SUCCESS.newBuilder().toResult();
 	}
 	}
 
 
     /**
     /**
@@ -219,7 +245,15 @@ public class OrderController {
         return GatewayResponse.SUCCESS.newBuilder().toResult(aliPayParams.getBody());
         return GatewayResponse.SUCCESS.newBuilder().toResult(aliPayParams.getBody());
     }
     }
 
 
-	/**
+    /**
+     * 支付宝退款
+     */
+    @PostMapping("/zfb/refund")
+    public Result<String> zfbOrderRefund(@RequestBody @Validated ZfbOrderRefundReq zfbOrderRefundReq) throws Exception {
+        return orderDonService.zfbRefund(zfbOrderRefundReq);
+    }
+
+    /**
 	 * 支付宝回调
 	 * 支付宝回调
 	 */
 	 */
 	@PostMapping("/prepay/zfb/notify")
 	@PostMapping("/prepay/zfb/notify")