wangming 1 miesiąc temu
rodzic
commit
a0eb9b12c4

+ 43 - 0
yami-shop-api/src/main/java/com/yami/shop/api/controller/AAAController.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2018-2999 广州亚米信息科技有限公司 All rights reserved.
+ *
+ * https://www.gz-yami.com/
+ *
+ * 未经允许,不可做商业用途!
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.yami.shop.api.controller;
+
+import com.yami.shop.wx.po.JsapiPo;
+import com.yami.shop.wx.service.WxProviderService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+
+@RestController
+@RequestMapping("/wx")
+@Api(tags = "地址接口")
+@AllArgsConstructor
+public class AAAController {
+
+    @Autowired
+    private WxProviderService wxProviderService;
+
+    @PostMapping("/pay")
+    @ApiOperation(value = "支付", notes = "支付")
+    public ResponseEntity<Map<String, Object>> wx(JsapiPo po) {
+        return ResponseEntity.ok(wxProviderService.subJsapi(po));
+    }
+
+}

+ 12 - 0
yami-shop-wx/pom.xml

@@ -13,6 +13,18 @@
     <version>0.0.1-SNAPSHOT</version>
 
     <dependencies>
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-apache-httpclient</artifactId>
+            <version>0.4.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.23</version>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-core</artifactId>

+ 88 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/config/CombinePayUrlEnum.java

@@ -0,0 +1,88 @@
+package com.yami.shop.wx.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * @author kaur
+ */
+@Getter
+@AllArgsConstructor
+public enum CombinePayUrlEnum {
+
+    /**
+     * native
+     */
+    NATIVE("native"),
+
+    /**
+     * app
+     */
+    APP("app"),
+
+    /**
+     * h5
+     */
+    H5("h5"),
+
+    /**
+     * jsapi
+     */
+    JSAPI("jsapi"),
+
+    /**
+     * 小程序jsapi
+     */
+    SUB_JSAPI("sub_jsapi"),
+
+    /**
+     * 查询订单
+     */
+    ORDER_QUERY_BY_NO("/combine-transactions/out-trade-no/"),
+
+    /**
+     * 关闭订单
+     */
+    CLOSE_ORDER_BY_NO("/combine-transactions/out-trade-no/%s/close"),
+
+    /**
+     * 申请退款
+     */
+    DOMESTIC_REFUNDS("/refund/domestic/refunds"),
+
+    /**
+     * 查询单笔退款
+     */
+    DOMESTIC_REFUNDS_QUERY("/refund/domestic/refunds/"),
+
+    /**
+     * 申请交易账单
+     */
+    TRADE_BILLS("/bill/tradebill"),
+
+    /**
+     * 申请资金账单
+     */
+    FUND_FLOW_BILLS("/bill/fundflowbill"),
+
+    /**
+     * 申请单个子商户资金账单
+     */
+    SUB_MERCHANT_FUND_FLOW_BILLS("/bill/sub-merchant-fundflowbill"),
+
+    /**
+     * 下单
+     */
+    PAY_TRANSACTIONS("/pay/partner/transactions/"),
+
+    /**
+     * 合单支付APIV3
+     */
+    COMBINE_TRANSACTIONS("/combine-transactions/");
+
+    /**
+     * 类型
+     */
+    private final String type;
+}

+ 43 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/config/WxConstants.java

@@ -0,0 +1,43 @@
+package com.yami.shop.wx.config;
+
+/**
+ * 微信常量配置
+ *
+ * @author kaur
+ */
+public class WxConstants {
+
+    //特约子商户
+    public static final String SUB_MCH_ID = "1726971843";
+
+    //特约子商户APP_ID
+    public static final String SUB_APP_ID = "wxbc64403830bb13c5";
+
+    //服务商APP_ID
+    public static final String SP_APP_ID = "wx43b5b906cc30ed0b";
+
+    //服务商MCH_ID
+    public static final String SP_MCH_ID = "1725845681";
+
+    //服务商API_KEY
+    public static final String API_KEY = "4b64e17419689527b256f07cdf6bd60c";
+
+    //服务商API_V3_KEY
+    public static final String API_V3_KEY = "4b64e17419689527b256f07cdf6bd60c";
+
+    //服务商API_V3_KEY
+    public static final String KEY_PEM_PATH = "apiclient_key.pem";
+
+    //服务商SERIAL_NO
+    public static final String SERIAL_NO = "65E9559D81ADA0BDA0CD3CF484A59A8DFB5610BE";
+
+    //服务商API_V3_KEY
+    public static final String BASE_URL = "https://api.mch.weixin.qq.com/v3";
+
+    //微信回调地址
+    public static final String  NOTIFY_URL= "http://xx.xx.xx.xx:30002";
+
+    //微信退款回调地址
+    public static final String  REFUND_NOTIFY_URL= "http://xx.xx.xx.xx:30002";
+
+}

+ 39 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/po/JsapiPo.java

@@ -0,0 +1,39 @@
+package com.yami.shop.wx.po;
+
+import com.alibaba.fastjson2.JSONObject;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author kaur
+ */
+@Data
+@ApiModel(value = "小程序下单")
+public class JsapiPo {
+
+    @ApiModelProperty(value = "详情")
+    @NotNull(message = "详情不能为空...")
+    private String description;
+
+    @ApiModelProperty(value = "总价")
+    @NotNull(message = "总价不能为空...")
+    private Integer total;
+
+    @ApiModelProperty(value = "openId")
+    @NotNull(message = "openId不能为空...")
+    private String openId;
+
+    @ApiModelProperty(value = "goodsId")
+    @NotNull(message = "goodsId不能为空...")
+    private String goodsId;
+
+    @ApiModelProperty(value = "outTradeNo")
+    @NotNull(message = "outTradeNo不能为空...")
+    private String outTradeNo;
+
+    @ApiModelProperty(value = "需要微信带回的参数")
+    private JSONObject attachJson;
+}

+ 17 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/service/WxProviderService.java

@@ -0,0 +1,17 @@
+package com.yami.shop.wx.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.yami.shop.wx.po.JsapiPo;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+public interface WxProviderService {
+
+    Map<String, Object> subJsapi(JsapiPo po);
+
+    String refundOrder(String orderNo);
+
+    JSONObject jsapiNotify(HttpServletRequest request, HttpServletResponse response);
+}

+ 568 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/service/impl/WxProviderServiceImpl.java

@@ -0,0 +1,568 @@
+package com.yami.shop.wx.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.TypeReference;
+import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
+import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
+import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
+import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
+import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
+import com.yami.shop.wx.config.CombinePayUrlEnum;
+import com.yami.shop.wx.config.WxConstants;
+import com.yami.shop.wx.po.JsapiPo;
+import com.yami.shop.wx.service.WxProviderService;
+import com.yami.shop.wx.utils.HttpUtils;
+import com.yami.shop.wx.utils.OrderUtils;
+import com.yami.shop.wx.utils.WechatPayValidator;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Slf4j
+@Service
+public class WxProviderServiceImpl implements WxProviderService {
+
+    private final ReentrantLock lock = new ReentrantLock();
+
+    public synchronized Map<String, Object> subJsapi(JsapiPo po) {
+        System.out.println("微信支付传入参数===========" + po);
+        String type = "sub_jsapi";
+        Map<String, Object> params = new HashMap<>(8);
+        params.put("sp_appid", WxConstants.SP_APP_ID);
+        params.put("sp_mchid", WxConstants.SP_MCH_ID);
+        params.put("sub_appid", WxConstants.SUB_APP_ID);
+        params.put("sub_mchid", WxConstants.SUB_MCH_ID);
+        Map<String, Object> payerMap = new HashMap<>(4);
+        payerMap.put("sub_openid", po.getOpenId());
+        params.put("payer", payerMap);
+        params.put("description", po.getDescription());
+        params.put("out_trade_no", po.getOutTradeNo());
+        params.put("notify_url", WxConstants.NOTIFY_URL);
+        JSONObject attachJson = po.getAttachJson();
+        if (attachJson != null) {
+            params.put("attach", JSONObject.toJSONString(attachJson));
+        }
+        Map<String, Object> amountMap = new HashMap<>(4);
+        amountMap.put("total", po.getTotal());
+        amountMap.put("currency", "CNY");
+        params.put("amount", amountMap);
+        Map<String, Object> sceneInfoMap = new HashMap<>(4);
+        sceneInfoMap.put("payer_client_ip", "127.0.0.1");
+        sceneInfoMap.put("device_id", "127.0.0.1");
+        params.put("scene_info", sceneInfoMap);
+        String paramsStr = JSON.toJSONString(params);
+        log.info("请求参数 ===> {}" + paramsStr);
+        String[] split = type.split("_");
+        String newType = split[split.length - 1];
+        String url = WxConstants.BASE_URL.concat(CombinePayUrlEnum.PAY_TRANSACTIONS.getType().concat(newType));
+        log.info("请求地址 ===> {}" + url);
+        String resStr = wechatHttpPost(url, paramsStr);
+        Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
+        });
+        Map<String, Object> signMap = paySignMsgApplet(resMap);
+        resMap.put("type", type);
+        resMap.put("signMap", signMap);
+        return resMap;
+    }
+
+    /**
+     * 构建签名
+     *
+     * @param map 参数
+     * @return map
+     */
+    private Map<String, Object> paySignMsgApplet(Map<String, Object> map) {
+        long timeMillis = System.currentTimeMillis();
+        String timeStamp = timeMillis / 1000 + "";
+        String nonceStr = String.valueOf(timeMillis);
+        String prepayId = map.get("prepay_id").toString();
+        String packageStr = "prepay_id=" + prepayId;
+        Map<String, Object> resMap = new HashMap<>();
+        resMap.put("nonceStr", nonceStr);
+        resMap.put("timeStamp", timeStamp);
+        resMap.put("appId", WxConstants.SP_APP_ID);
+        resMap.put("package", packageStr);
+        resMap.put("paySign", getWxPayResultMap(prepayId, timeStamp, nonceStr));
+        resMap.put("signType", "RSA");
+        return resMap;
+    }
+
+    @SneakyThrows
+    public String getWxPayResultMap(String prepayId, String timestamp, String nonceStr) {
+        Signature sign = Signature.getInstance("SHA256withRSA");
+        PrivateKey privateKey = getPrivateKey(WxConstants.KEY_PEM_PATH);
+        sign.initSign(privateKey);
+        String sb = WxConstants.SUB_APP_ID + "\n" +
+                timestamp + "\n" +
+                nonceStr + "\n" +
+                "prepay_id=" + prepayId + "\n";
+        sign.update(sb.getBytes(StandardCharsets.UTF_8));
+        return Base64.getEncoder().encodeToString(sign.sign());
+    }
+
+    /**
+     * 关闭(取消)订单
+     *
+     * @param orderNo orderNo
+     */
+    public void closeOrder(String orderNo) {
+        // TODO 用于在客户下单后,不进行支付,取消订单的场景
+        log.info("根据订单号取消订单,订单号: {}", orderNo);
+        String url = String.format(CombinePayUrlEnum.CLOSE_ORDER_BY_NO.getType(), orderNo);
+        url = WxConstants.BASE_URL.concat(url);
+        Map<String, String> params = new HashMap<>(2);
+        params.put("sp_mchid", WxConstants.SP_MCH_ID);
+        params.put("sub_mchid", WxConstants.SUB_MCH_ID);
+        String paramsStr = JSON.toJSONString(params);
+        log.info("请求参数 ===> {}" + paramsStr);
+        String res = wechatHttpPost(url, paramsStr);
+        log.info(res);
+    }
+
+    /**
+     * 申请退款
+     *
+     * @param orderNo orderNo
+     * @return orderNo
+     */
+    public String refundOrder(String orderNo) {
+        Integer total = 0;
+        log.info("根据订单号申请退款,订单号: {}", orderNo);
+        String url = WxConstants.BASE_URL.concat(CombinePayUrlEnum.DOMESTIC_REFUNDS.getType());
+        Map<String, Object> params = new HashMap<>(2);
+        params.put("out_trade_no", orderNo);
+        params.put("sub_mchid", WxConstants.SUB_MCH_ID);
+        String outRefundNo = OrderUtils.getOrderNo("TK");
+        log.info("退款申请号:{}", outRefundNo);
+        params.put("out_refund_no", outRefundNo + "");
+        params.put("reason", "申请退款");
+        params.put("notify_url", WxConstants.REFUND_NOTIFY_URL);
+        Map<String, Object> amountMap = new HashMap<>();
+        //退款金额,单位:分
+        amountMap.put("refund", total);
+        //原订单金额,单位:分
+        amountMap.put("total", total);
+        amountMap.put("currency", "CNY");
+        params.put("amount", amountMap);
+        String paramsStr = JSON.toJSONString(params);
+        log.info("请求参数 ===> {}" + paramsStr);
+        String res;
+        try {
+            res = wechatHttpPost(url, paramsStr);
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+        log.info("退款结果:{}", res);
+        return res;
+    }
+
+
+    /**
+     * 查询单笔退款信息
+     *
+     * @param refundNo refundNo
+     * @return return
+     */
+    public Map<String, Object> queryRefundOrder(String refundNo) {
+        log.info("根据订单号查询退款订单,订单号: {}", refundNo);
+        String url = WxConstants.BASE_URL
+                .concat(CombinePayUrlEnum.DOMESTIC_REFUNDS_QUERY.getType().concat(refundNo))
+                .concat("?sub_mchid=").concat(WxConstants.SUB_MCH_ID);
+        String res = wechatHttpGet(url);
+        log.info("查询退款订单结果:{}", res);
+        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
+        });
+        String successTime = resMap.get("success_time").toString();
+        String refundId = resMap.get("refund_id").toString();
+        /*
+          款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
+          枚举值:
+          SUCCESS:退款成功
+          CLOSED:退款关闭
+          PROCESSING:退款处理中
+          ABNORMAL:退款异常
+         */
+        String status = resMap.get("status").toString();
+        /*
+          枚举值:
+          ORIGINAL:原路退款
+          BALANCE:退回到余额
+          OTHER_BALANCE:原账户异常退到其他余额账户
+          OTHER_BANKCARD:原银行卡异常退到其他银行卡
+         */
+        String channel = resMap.get("channel").toString();
+        String userReceivedAccount = resMap.get("user_received_account").toString();
+        log.info("successTime:" + successTime);
+        log.info("channel:" + channel);
+        log.info("refundId:" + refundId);
+        log.info("status:" + status);
+        log.info("userReceivedAccount:" + userReceivedAccount);
+        // TODO 在查询单笔退款信息时,可以再去查询一次订单的状态,保证该订单已经退款完毕了
+        return resMap;
+    }
+
+    /**
+     * 申请交易账单
+     *
+     * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请 ,如果传入日期未为当天则会出错
+     * @param billType 分为:ALL、SUCCESS、REFUND
+     *                 ALL:返回当日所有订单信息(不含充值退款订单)
+     *                 SUCCESS:返回当日成功支付的订单(不含充值退款订单)
+     *                 REFUND:返回当日退款订单(不含充值退款订单)
+     * @return 结果
+     */
+    public String tradeBill(String billDate, String billType) {
+        log.info("申请交易账单,billDate:{},billType:{}", billDate, billType);
+        String url = WxConstants.BASE_URL.concat(CombinePayUrlEnum.TRADE_BILLS.getType())
+                .concat("?bill_date=").concat(billDate).concat("&bill_type=").concat(billType);
+        // 填则默认返回服务商下的交易或退款数据,下载某个子商户下的交易或退款数据,则该字段必填
+        url = url.concat("&sub_mchid=").concat(WxConstants.SUB_MCH_ID);
+        String res = wechatHttpGet(url);
+        log.info("查询退款订单结果:{}", res);
+        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
+        });
+        return resMap.get("download_url").toString();
+    }
+
+    /**
+     * @param billDate    格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
+     * @param accountType 分为:BASIC、OPERATION、FEES
+     *                    BASIC:基本账户
+     *                    OPERATION:运营账户
+     *                    FEES:手续费账户
+     * @return 结果
+     */
+    public String fundFlowBill(String billDate, String accountType) {
+        log.info("申请交易账单,billDate:{},accountType:{}", billDate, accountType);
+        String url = WxConstants.BASE_URL.concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
+                .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType);
+        String res = wechatHttpGet(url);
+        log.info("查询退款订单结果:{}", res);
+        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
+        });
+        return resMap.get("download_url").toString();
+    }
+
+    /**
+     * @param billDate    格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
+     * @param accountType 分为:BASIC、OPERATION、FEES
+     *                    BASIC:基本账户
+     *                    OPERATION:运营账户
+     *                    FEES:手续费账户
+     * @return 结果
+     */
+    public String subMerchantFundFlowBill(String billDate, String accountType) {
+        log.info("申请单个子商户资金账单,billDate:{},accountType:{}", billDate, accountType);
+        String url = WxConstants.BASE_URL.concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
+                .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType)
+                .concat("&sub_mchid=").concat(billDate).concat("&algorithm=").concat("AEAD_AES_256_GCM");
+        String res = wechatHttpGet(url);
+        log.info("查询退款订单结果:{}", res);
+        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
+        });
+        String downloadBillCount = resMap.get("download_bill_count").toString();
+        String downloadBillList = resMap.get("download_bill_list").toString();
+        List<Map<String, Object>> billListMap = JSONObject.parseObject(downloadBillList, new TypeReference<List<Map<String, Object>>>() {
+        });
+        String downloadUrl = billListMap.get(0).get("download_url").toString();
+        log.info("downloadBillCount=" + downloadBillCount);
+        log.info("downloadUrl=" + downloadUrl);
+        return downloadUrl;
+    }
+
+    /**
+     * 下载账单
+     *
+     * @param downloadUrl downloadUrl
+     */
+    public void downloadBill(String downloadUrl) {
+        log.info("下载账单,下载地址:{}", downloadUrl);
+        HttpGet httpGet = new HttpGet(downloadUrl);
+        httpGet.addHeader("Accept", "application/json");
+        CloseableHttpResponse response = null;
+        try {
+            response = noSignHttpClient().execute(httpGet);
+            String body = EntityUtils.toString(response.getEntity());
+            int statusCode = response.getStatusLine().getStatusCode();
+            if (statusCode == 200 || statusCode == 204) {
+                log.info("下载账单,返回结果 = " + body);
+            } else {
+                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + body);
+            }
+            // TODO 将body内容转为excel存入本地或者输出到浏览器,演示存入本地
+            writeStringToFile(body);
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        } finally {
+            if (response != null) {
+                try {
+                    response.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * 文件写入
+     *
+     * @param body body
+     */
+    private void writeStringToFile(String body) {
+        FileWriter fw = null;
+        try {
+            String filePath = "C:\\Users\\lhz12\\Desktop\\wxPay.txt";
+            fw = new FileWriter(filePath, true);
+            BufferedWriter bw = new BufferedWriter(fw);
+            bw.write(body);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (fw != null) {
+                    fw.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 获取商户的私钥文件
+     *
+     * @param keyPemPath keyPemPath
+     * @return return
+     */
+    public PrivateKey getPrivateKey(String keyPemPath) {
+        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
+        if (inputStream == null) {
+            throw new RuntimeException("私钥文件不存在");
+        }
+        return PemUtil.loadPrivateKey(inputStream);
+    }
+
+    /**
+     * 获取证书管理器实例
+     *
+     * @return return
+     */
+    @SneakyThrows
+    public Verifier getVerifier() {
+        log.info("获取证书管理器实例");
+        //获取商户私钥
+        PrivateKey privateKey = getPrivateKey(WxConstants.KEY_PEM_PATH);
+        //私钥签名对象
+        PrivateKeySigner privateKeySigner = new PrivateKeySigner(WxConstants.SERIAL_NO, privateKey);
+        //身份认证对象
+        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(WxConstants.SP_MCH_ID, privateKeySigner);
+        // 使用定时更新的签名验证器,不需要传入证书
+        CertificatesManager certificatesManager = CertificatesManager.getInstance();
+        certificatesManager.putMerchant(WxConstants.SP_MCH_ID,
+                wechatPay2Credentials, WxConstants.API_V3_KEY.getBytes(StandardCharsets.UTF_8));
+        return certificatesManager.getVerifier(WxConstants.SP_MCH_ID);
+    }
+
+    /**
+     * 获取支付http请求对象
+     *
+     * @return return
+     */
+    public CloseableHttpClient httpClient() {
+        String keyPemPath = WxConstants.KEY_PEM_PATH;
+        PrivateKey privateKey = getPrivateKey(keyPemPath);
+        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
+                .withMerchant(WxConstants.SP_MCH_ID, WxConstants.SERIAL_NO, privateKey)
+                .withValidator(new WechatPay2Validator(getVerifier()));
+        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
+        return builder.build();
+    }
+
+    /**
+     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
+     */
+    public CloseableHttpClient noSignHttpClient() {
+        //获取商户私钥
+        PrivateKey privateKey = getPrivateKey(WxConstants.KEY_PEM_PATH);
+        //用于构造HttpClient
+        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
+                //设置商户信息
+                .withMerchant(WxConstants.SP_MCH_ID, WxConstants.SERIAL_NO, privateKey)
+                //无需进行签名验证、通过withValidator((response) -> true)实现
+                .withValidator((response) -> true);
+        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
+        return builder.build();
+    }
+
+    /**
+     * 发送get请求
+     *
+     * @param url 请求地址
+     * @return 结果
+     */
+    public String wechatHttpGet(String url) {
+        CloseableHttpClient wxPayClient = httpClient();
+        try {
+            HttpGet httpGet = new HttpGet(url);
+            httpGet.setHeader("Accept", "application/json");
+            CloseableHttpResponse response = wxPayClient.execute(httpGet);
+            return getResponseBody(response);
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    /**
+     * 发送post请求
+     *
+     * @param url       请求地址
+     * @param paramsStr 参数
+     * @return 结果
+     */
+    public String wechatHttpPost(String url, String paramsStr) {
+        CloseableHttpClient wxPayClient = httpClient();
+        try {
+            HttpPost httpPost = new HttpPost(url);
+            StringEntity entity = new StringEntity(paramsStr, "utf-8");
+            entity.setContentType("application/json");
+            httpPost.setEntity(entity);
+            httpPost.setHeader("Accept", "application/json");
+            CloseableHttpResponse response = wxPayClient.execute(httpPost);
+            return getResponseBody(response);
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    /**
+     * 读取响应的请求体
+     *
+     * @param response 响应
+     * @return 响应
+     * @throws IOException IOException
+     */
+    private String getResponseBody(CloseableHttpResponse response) throws IOException {
+        HttpEntity entity = response.getEntity();
+        String body = entity == null ? "" : EntityUtils.toString(entity);
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
+            log.info("成功, 返回结果 = " + body);
+        } else {
+            String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
+            log.error(msg);
+            throw new RuntimeException(msg);
+        }
+        return body;
+    }
+
+    public JSONObject jsapiNotify(HttpServletRequest request, HttpServletResponse response) {
+        JSONObject bodyJson = getNotifyBodyJson(request);
+        if (bodyJson == null) {
+            return falseMsg(response);
+        }
+        if (lock.tryLock()) {
+            try {
+                // 解密resource中的通知数据
+                String resource = bodyJson.getString("resource");
+                JSONObject resourceJson = WechatPayValidator.decryptFromResource(resource, WxConstants.API_V3_KEY, 2);
+                log.info("  =================== 服务商小程序支付回调解密resource中的通知数据 ===================\n" + resourceJson);
+                Integer trans = statusTrans(resourceJson.getString("trade_state"));
+                JSONObject attach = resourceJson.getJSONObject("attach");
+                if (trans == 1 && attach != null) {
+                    JSONObject successJson = trueMsg(response);
+                    successJson.put("attach", attach);
+                    return successJson;
+                } else {
+                    System.err.println("支付失败...");
+                }
+            } finally {
+                lock.unlock();
+            }
+        }
+        return trueMsg(response);
+    }
+
+    private JSONObject getNotifyBodyJson(HttpServletRequest request) {
+        String body = HttpUtils.readData(request);
+        log.info("===========微信回调参数===========\n" + body);
+        log.info("微信回调参数:{}", body);
+        JSONObject jsonObject = JSONObject.parseObject(body);
+        WechatPayValidator wechatPayValidator
+                = new WechatPayValidator(getVerifier(), jsonObject.getString("id"), body);
+        if (!wechatPayValidator.validate(request)) {
+            log.error("通知验签失败");
+            return null;
+        }
+        log.info("通知验签成功");
+        return jsonObject;
+    }
+
+    private JSONObject falseMsg(HttpServletResponse response) {
+        JSONObject resMap = new JSONObject();
+        response.setStatus(500);
+        resMap.put("code", "ERROR");
+        resMap.put("message", "通知验签失败");
+        return resMap;
+    }
+
+    /**
+     * 支付状态( 1-支付成功 )
+     *
+     * @param tradeState 微信返回支付状态码
+     * @return 状态
+     */
+    private Integer statusTrans(String tradeState) {
+        int payStatus;
+        if ("SUCCESS".equals(tradeState)) {
+            payStatus = 1;
+        } else if ("NOTPAY".equals(tradeState)) {
+            payStatus = 0;
+        } else if ("REVOKED".equals(tradeState)) {
+            payStatus = 4;
+        } else if ("CLOSED".equals(tradeState)) {
+            payStatus = 6;
+        } else if ("PAYERROR".equals(tradeState)) {
+            payStatus = 5;
+        } else {
+            payStatus = 8;
+        }
+        return payStatus;
+    }
+
+    private JSONObject trueMsg(HttpServletResponse response) {
+        JSONObject resMap = new JSONObject();
+        //成功应答
+        response.setStatus(200);
+        resMap.put("code", "SUCCESS");
+        resMap.put("message", "成功");
+        return resMap;
+    }
+
+}

+ 41 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/utils/HttpUtils.java

@@ -0,0 +1,41 @@
+package com.yami.shop.wx.utils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+
+/**
+ * @author kaur
+ **/
+public class HttpUtils {
+
+    /**
+     * 将通知参数转化为字符串
+     * @param request HttpServletRequest
+     * @return 字符串
+     */
+    public static String readData(HttpServletRequest request) {
+        BufferedReader br = null;
+        try {
+            StringBuilder result = new StringBuilder();
+            br = request.getReader();
+            for (String line; (line = br.readLine()) != null; ) {
+                if (result.length() > 0) {
+                    result.append("\n");
+                }
+                result.append(line);
+            }
+            return result.toString();
+        } catch (IOException e) {
+            throw new RuntimeException(e.getMessage());
+        } finally {
+            if (br != null) {
+                try {
+                    br.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+}

+ 148 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/utils/OrderUtils.java

@@ -0,0 +1,148 @@
+package com.yami.shop.wx.utils;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Random;
+
+/**
+ * * 订单编码生成器,生成32位数字编码,
+ * * @生成规则 1位单号类型+17位时间戳+14位(用户id加密&随机数)
+ */
+public class OrderUtils {
+
+    /**
+     * 订单类别头
+     */
+    private static final String ORDER_CODE = "";
+
+    /**
+     * 退货类别头
+     */
+    private static final String RETURN_ORDER = "";
+
+    /**
+     * 退款类别头
+     */
+    private static final String REFUND_ORDER = "";
+
+    /**
+     * 随即编码
+     */
+    private static final int[] r = new int[]{7, 9, 6, 2, 8, 1, 3, 0, 5, 4};
+
+    /**
+     * 用户id和随机数总长度
+     */
+    private static final int maxLength = 14;
+
+    /**
+     * 根据id进行加密+加随机数组成固定长度编码
+     */
+    private static String toCode(Long userId) {
+        String idStr = userId.toString();
+        StringBuilder idsbs = new StringBuilder();
+        for (int i = idStr.length() - 1; i >= 0; i--) {
+            idsbs.append(r[idStr.charAt(i) - '0']);
+        }
+        return idsbs.append(getRandom(maxLength - idStr.length())).toString();
+    }
+
+    /**
+     * 生成时间戳
+     */
+    private static String getDateTime() {
+        DateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
+        return sdf.format(new Date());
+    }
+
+    /**
+     * 生成固定长度随机码
+     *
+     * @param n 长度
+     */
+    public static long getRandom(long n) {
+        long min = 1, max = 9;
+        for (int i = 1; i < n; i++) {
+            min *= 10;
+            max *= 10;
+        }
+        long rangeLong = (((long) (new Random().nextDouble() * (max - min)))) + min;
+        return rangeLong;
+    }
+
+
+    /**
+     * 生成不带类别标头的编码
+     *
+     * @param userId
+     */
+    private static synchronized String getCode(Long userId) {
+        userId = userId == null ? 10000 : userId;
+        return getDateTime() + toCode(userId);
+    }
+
+
+    /**
+     * 生成订单单号编码(调用方法)
+     *
+     * @param userId 网站中该用户唯一ID 防止重复
+     */
+    public static String getOrderCode(Long userId) {
+        return ORDER_CODE + getCode(userId);
+    }
+
+
+    /**
+     * 生成退货单号编码(调用方法)
+     *
+     * @param userId 网站中该用户唯一ID 防止重复
+     */
+    public static String getReturnCode(Long userId) {
+        return RETURN_ORDER + getCode(userId);
+    }
+
+
+    /**
+     * 生成退款单号编码(调用方法)
+     *
+     * @param userId 网站中该用户唯一ID 防止重复
+     */
+    public static String getRefundCode(Long userId) {
+        return REFUND_ORDER + getCode(userId);
+    }
+
+    /**
+     * 生成对应个数的随机验证码
+     *
+     * @param num 个数
+     * @return 随机验证码
+     */
+    public static String getRandomNum(Integer num) {
+        String base = "0123456789";
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < num; i++) {
+            int number = random.nextInt(base.length());
+            sb.append(base.charAt(number));
+        }
+        return sb.toString();
+    }
+
+
+    /**
+     * 获取统一单号规则
+     * @param orderType:单号类型:订单(DD)、退款(TK)
+     * @return
+     */
+    public static String getOrderNo(String orderType){
+        StringBuffer sb = new StringBuffer();
+        sb.append(orderType);
+        sb.append(System.currentTimeMillis());
+        Random rd = new Random();
+        sb.append(rd.nextInt(10)).append(rd.nextInt(10)).append(rd.nextInt(10));
+        return sb.toString();
+    }
+
+
+}

+ 143 - 0
yami-shop-wx/src/main/java/com/yami/shop/wx/utils/WechatPayValidator.java

@@ -0,0 +1,143 @@
+package com.yami.shop.wx.utils;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.util.EntityUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+
+import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
+
+/**
+ * 回调校验器
+ *
+ * @author kaur*/
+@Slf4j
+public class WechatPayValidator {
+    /**
+     * 应答超时时间,单位为分钟
+     */
+    private static final long RESPONSE_EXPIRED_MINUTES = 5;
+    private final Verifier verifier;
+    private final String requestId;
+    private final String body;
+
+
+    public WechatPayValidator(Verifier verifier, String requestId, String body) {
+        this.verifier = verifier;
+        this.requestId = requestId;
+        this.body = body;
+    }
+
+    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) {
+        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, requestId);
+            }
+        } catch (IllegalArgumentException e) {
+            log.warn(e.getMessage());
+            return false;
+        }
+
+        return true;
+    }
+
+    private 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);
+        }
+    }
+
+    private String buildMessage(HttpServletRequest request) {
+        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
+        String nonce = request.getHeader(WECHAT_PAY_NONCE);
+        return timestamp + "\n"
+                + nonce + "\n"
+                + body + "\n";
+    }
+
+    private String getResponseBody(CloseableHttpResponse response) throws IOException {
+        HttpEntity entity = response.getEntity();
+        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
+    }
+
+    /**
+     * 对称解密,异步通知的加密数据
+     *
+     * @param resource 加密数据
+     * @param apiV3Key apiV3密钥
+     * @param type     1-支付,2-退款
+     * @return
+     */
+    public static JSONObject decryptFromResource(String resource, String apiV3Key, Integer type) {
+
+        String msg = type == 1 ? "支付成功" : "退款成功";
+        log.info(msg + ",回调通知,密文解密");
+        try {
+            //通知数据
+            JSONObject jsonObject = JSONObject.parseObject(resource);
+            //数据密文
+            String ciphertext = jsonObject.getString("ciphertext");
+            //随机串
+            String nonce = jsonObject.getString("nonce");
+            //附加数据
+            String associatedData = jsonObject.getString("associated_data");
+            log.info("密文: {}", ciphertext);
+            AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
+            String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
+                    nonce.getBytes(StandardCharsets.UTF_8),
+                    ciphertext);
+            log.info(msg + ",回调通知,解密结果 : {}", resourceStr);
+            return JSONObject.parseObject(resourceStr);
+        } catch (Exception e) {
+            throw new RuntimeException("回调参数,解密失败!");
+        }
+    }
+}

+ 28 - 0
yami-shop-wx/src/main/resources/apiclient_key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7aKXAWrNy4W9N
+Mv2Ah9zp1VQDZk68nWX3EfACgHdCXhqF2mhzUqQJHGjumqkSBKdTwDTzgXCcs7EK
+PSIeWXSajfbyn8f5cYupRYESFvrIpiHaRh3aUTNVmfRiXLN6zMSiAUkgPp3mZXzZ
+lp38wPZS1Vl+S8lesETEqnE/qFK6A2MKCjOFbtCNQIsPc4q70JzgAwNx20giHNMm
+h6TN41as1Qu6PVpamvNLgLp4U2GBInURpHLUqVJfQOezCOHUEkDxtcdY30Nb4aIF
+FzSLzGssczduovhm8b57Wy0dD5crGKZVGsix3m9CXd+BshJDYIB/EAivU+npK4Tz
+inA7K0wdAgMBAAECggEASTeFKjVFTmiMl1iIeaKayDAz9nAN6tqDb5ducUvHTAJ/
+0jOWAxgSRgz3YeDClOuEg8/f4BJ98HqzfBCON9UzHP7Br+CEvAuESMmCt5KKv8FI
+EIK8PwLiT1sqgxM8e4lseO6Ppy1PeyfDMy8I4ipxEcOQhysMrRWoiD0eqYQVbd4g
+YfOEsq8EbSHr+sDHgFJM5hEEHEWNvSUUSjifuo861SkYEtjQcg5AKflPce9Fz7Ze
+UNxHEh+gAOvDzkFTlVSnqhUR3G2EeoFSwALwNMNJW4vBiQ16InhAlsUPh5zByPBS
+HAbfiNYNUgYQM5PUy2YSamMXwUAAvNvcrbNUN6PrgQKBgQD4OtObOKB60zZYuRfa
+6lpfIxT4EeaP7aGFc+vL20ZFTEhueWAHw8opznrRwWa3zok1T+PhJHJ0BeLsU+m1
+Y4BVDHNw7rfKxMaw63LL2AGnx2wmeDDm1bliPvnQrc7KVqQsgu31+xuOKcgL5brg
+YdxfXPyHP7vAfI8RaanGHS3luQKBgQDBRm+FAELD/O0Hsn8Eyz6tehTG0AqyLdCE
+DmKjC1cQnQVDWReFRcLjFQZLd3eajNe9L1liyqnKERnJCgsyxaygmZVnt4xdwRSm
+dV76SFV6+kXs9dDILeRNhybyfAIdrijv60gF8uVfUMhbQGtpuEs9e+1UhP5TcvbS
+Y10Q3xQLhQKBgQDTcrjiVkBNoDGBLlolvs+wjt/D2Ou/2rpmRbKsemLHkQ16HaYx
+txQ0vJBesRJgrGywnxcMp9FfB8yfZ3ODcfjVeb+0iYTCExD1j/q/rYbrdrKnqmZt
+m81I5UBEBGpMbbV3vSgbCwYT+X9QsyaXEIV6LmlmhGHYu8HbIVjfS+fW8QKBgQCn
+083Ko+tV3C7G3ExHkWUfpj6cVNK03euOgB9OjO5RUsfbL2WpKGaOYRdSOK722Q4N
+DSyFCI5fFHJbjAklUVkRK8v8f/m2/4467PhIRL9VRjWqCdn9uTvhxlYfgWck9rI9
+gwTLhZt/JNqVwl6DMrDC9vWiysk9FTsWfYm2uL7jMQKBgQDy2kud6Ct2wvN6AakJ
+PrXwvW0FVrI5ieGS4eG0cUWiK/7S1TjyyuF/dJYabxVtXnYXCLTztFigPCVNWeog
+R9FQITYKducHCS/Gjz53ksEeX9LuUB78gyrqiGvq5iyKKOsYvh9j3YmLpZgeQ1CE
+be6f9A6UjSGKP4qEvgJhIy09bw==
+-----END PRIVATE KEY-----

+ 0 - 0
yami-shop-wx/src/main/resources/application.yml