首页 > 其他分享 >SpringBoot集成微信支付JSAPIV3保姆教程

SpringBoot集成微信支付JSAPIV3保姆教程

时间:2023-09-11 17:02:45浏览次数:39  
标签:JSAPIV3 orderID String 微信 request new 退款 SpringBoot

前言

最近为一个公众号h5商城接入了微信支付功能,查找资料过程中踩了很多坑,以此文章记录一下和大家分享

前期准备

公众号认证

微信支付功能需要开通企业号并进行资质认证,费用一年300,且需企业营业执照等信息,对公账户打款验证

登录微信公众平台https://mp.weixin.qq.com/,创建服务号

如果已有服务号扫码登录后点击公众号头像选择认证详情菜单

商户开通

点击公众号左侧微信支付菜单,选择右侧关联商户按钮,如果没有商户按指引申请

参数获取

公众号参数

点击左侧基本配置菜单,记录右侧的应用ID(appid

商户参数

点击公众号左侧微信支付菜单,滑动到已关联商户号,点击查看按钮

进入商户后,选择产品中心,左侧开发配置,记录商户号(mchId

进入商户后,选择账户中心,左侧API安全,按照指引获取APIV3密钥(apiV3Key),API证书的序列号(merchantSerialNumber)和私钥文件apiclient_key.pem

参数配置

外网映射

在微信支付本地调试时需要用到外网映射工具,这里推荐NATAPP:https://natapp.cn/(非广)

一个月带备案域名的映射隧道12元,我们需要两个,一个映射公众号菜单页面,一个映射后端接口

公众号参数

进入公众点击左侧自定义菜单,右侧点击添加菜单,输入外网映射后的菜单地址

如果你是新手,需要进行网页授权认证获取用户openid,那你还需要进行网页授权域名的设置

点左侧接口权限菜单,修改右侧的网页授权用户信息获取

进入后设置JS接口安全域名,会需要将一个txt认证文件放置到你的静态页面目录,参照指引即可

商户参数

进入商户后,选择产品中心,左侧我的产品,进入JSAPI支付

点击产品设置,在支付配置模块,添加支付授权目录(后端接口和前端网页都添加)

支付对接

参数声明

wechartpay:
  # 公众号id
  appId: xxx
  # 公众号中微信支付绑定的商户的商户号
  mchId: xxxx
  # 商户apiV3Keyz密钥
  apiV3Key: xxxx
  #商户证书序列号
  merchantSerialNumber: xxxx
  # 支付回调地址
  v3PayNotifyUrl: http://xxxxxx/wechatpay/pay_notify
  # 退款回调地址
  v3BackNotifyUrl: http://xxxxx/wechatpay/back_notify
	@Value("${wechartpay.appId}")
    private String appId;
    @Value("${wechartpay.mchId}")
    private String mchId;
    @Value("${wechartpay.apiV3Key}")
    private String apiV3Key;
    @Value("${wechartpay.merchantSerialNumber}")
    private String merchantSerialNumber;
    @Value("${wechartpay.v3PayNotifyUrl}")
    private String v3PayNotifyUrl;
    @Value("${wechartpay.v3BackNotifyUrl}")
    private String v3BackNotifyUrl;

	public static RSAAutoCertificateConfig config = null;
    public static JsapiServiceExtension service = null;
    public static RefundService backService = null;

	private void initPayConfig() {
        initConfig();
        // 构建service
        if (service == null) {
            service = new JsapiServiceExtension.Builder().config(config).build();
        }
    }

    private void initBackConfig() {
        initConfig();
        // 构建service
        if (backService == null) {
            backService = new RefundService.Builder().config(config).build();
        }
    }

    private void initConfig() {
        String filePath = getFilePath("apiclient_key.pem");
        if (config == null) {
            config = new RSAAutoCertificateConfig.Builder()
                    .merchantId(mchId)
                    .privateKeyFromPath(filePath)
                    .merchantSerialNumber(merchantSerialNumber)
                    .apiV3Key(apiV3Key)
                    .build();
        }
    }

    public RSAAutoCertificateConfig getConfig() {
        initConfig();
        return config;
    }

    public static String getFilePath(String classFilePath) {
        String filePath = "";
        try {
            String templateFilePath = "tempfiles/classpathfile/";
            File tempDir = new File(templateFilePath);
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }
            String[] filePathList = classFilePath.split("/");
            String checkFilePath = "tempfiles/classpathfile";
            for (String item : filePathList) {
                checkFilePath += "/" + item;
            }
            File tempFile = new File(checkFilePath);
            if (tempFile.exists()) {
                filePath = checkFilePath;
            } else {
                //解析
                ClassPathResource classPathResource = new ClassPathResource(classFilePath);
                InputStream inputStream = classPathResource.getInputStream();
                checkFilePath = "tempfiles/classpathfile";
                for (int i = 0; i < filePathList.length; i++) {
                    checkFilePath += "/" + filePathList[i];
                    if (i == filePathList.length - 1) {
                        //文件
                        File file = new File(checkFilePath);
                        if (!file.exists()) {
                            FileUtils.copyInputStreamToFile(inputStream, file);
                        }
                    } else {
                        //目录
                        tempDir = new File(checkFilePath);
                        if (!tempDir.exists()) {
                            tempDir.mkdirs();
                        }
                    }
                }
                inputStream.close();
                filePath = checkFilePath;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return filePath;
    }

将apiclient_key.pem私钥文件拷贝到resources文件夹根目录

Maven引用

        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.11</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.8.0</version>
        </dependency>

用户授权

为了测试流程的完整性,这里简单描述下如何通过网页授权获取用户的openid,几个参数定义如下

var appid = "xxxx";
var appsecret = "xxxx";
redirect_uri = encodeURIComponent("http://xxxx/xxx.html");
response_type = "code";
scope = "snsapi_userinfo";
  • 发起授权请求

    function getCodeUrl() {
        var url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appid + "&redirect_uri=" + redirect_uri + "&response_type=" + response_type + "&scope=" + scope + "#wechat_redirect";
        return url
    }
    

    跳转到开始授权页面,会跳转到redirect_uri这个页面,url参数携带授权code

  • 用户同意授权

    function getAuthUrl(code) {
        var url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid + "&secret=" + appsecret + "&code=" + code + "&grant_type=authorization_code";
        return url
    }
    

    根据code生成正式授权url,需要用户手动点击同意,使用get方式请求该url成功后会返回openid

    var url = getAuthUrl(code);
    $.get(url, function (data, status) {
       var result = JSON.parse(data)
       if (!result.errcode) {
           var openid = result.openid;
        }
    });
    

不使用用户授权流程也能简单的获取到用户openid进行测试,如果该用户关注了公众号,选择公众号左侧的用户管理菜单,点击用户跳转到与该用户的聊天界面,url参数中的tofakeid就是用户的openid

支付准备

根据用户的openid,订单号,订单金额,订单说明四个参数进行支付前的参数准备,会返回如下参数

公众号ID(appId)

时间戳(timeStamp)

随机串(nonceStr)

打包值(packageVal)

微信签名方式(signType)

微信签名(paySign)

这里的orderID指业务中生成的订单号,最大32位,由数字和字母组成,支付金额最终要转转换成已分为单位

    @PostMapping("/prepay")
    public Object prepay(@RequestBody Map<String, Object> params) throws Exception {
        String openId = "xxxx";
        String orderID = String.valueOf(params.get("orderID"));
        BigDecimal payAmount = new BigDecimal(String.valueOf(params.get("payAmount")));
        String payDes = "支付测试";
        return paySDK.getPreparePayInfo(openId,orderID,payAmount,payDes);
    }
	//支付前的准备参数,供前端调用
    public PrepayWithRequestPaymentResponse getPreparePayInfo(String openid, String orderID, BigDecimal payAmount, String payDes) {
        initPayConfig();
        //元转换为分
        Integer amountInteger = (payAmount.multiply(new BigDecimal(100))).intValue();
        //组装预约支付的实体
        // request.setXxx(val)设置所需参数,具体参数可见Request定义
        PrepayRequest request = new PrepayRequest();
        //计算金额
        Amount amount = new Amount();
        amount.setTotal(amountInteger);
        amount.setCurrency("CNY");
        request.setAmount(amount);
        //公众号appId
        request.setAppid(appId);
        //商户号
        request.setMchid(mchId);
        //支付者信息
        Payer payer = new Payer();
        payer.setOpenid(openid);
        request.setPayer(payer);
        //描述
        request.setDescription(payDes);
        //微信回调地址,需要是https://开头的,必须外网可以正常访问
        //本地测试可以使用内网穿透工具,网上很多的
        request.setNotifyUrl(v3PayNotifyUrl);
        //订单号
        request.setOutTradeNo(orderID);
        // 加密
        PrepayWithRequestPaymentResponse payment = service.prepayWithRequestPayment(request);
        //默认加密类型为RSA
        payment.setSignType("MD5");
        payment.setAppId(appId);
        return payment;
    }

支付拉起

在微信环境调用支付准备接口获取参数后,使用WeixinJSBridge.invoke方法发起微信支付

function submitWeChatPay(orderID,payAmount,callback) {
    if (typeof WeixinJSBridge != "undefined") {
        var param = {
            orderID:orderID,
            payAmount:payAmount
        }
        httpPost(JSON.stringify(param),"http://xxxx/wechatpay/prepay",function (data, status) {
            var param = {
                "appId": data.appId,    
                "timeStamp": data.timeStamp,  
                "nonceStr": data.nonceStr,    
                "package": data.packageVal,
                "signType": data.signType,    
                "paySign": data.paySign
            }
            WeixinJSBridge.invoke(
                'getBrandWCPayRequest', param,callback);
        })
    } else {
        alert("非微信环境")
    }
}

支付回调

支付回调地址是在支付准备阶段传递的,在用户付款完成后会自动调用该接口,传递支付订单的相关信息

    @PostMapping("/pay_notify")
    public void pay_notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //获取报文
        String body = getRequestBody(request);
        //随机串
        String nonceStr = request.getHeader("Wechatpay-Nonce");
        //微信传递过来的签名
        String signature = request.getHeader("Wechatpay-Signature");
        //证书序列号(微信平台)
        String serialNo = request.getHeader("Wechatpay-Serial");
        //时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        InputStream is = null;
        try {
            is = request.getInputStream();
            // 构造 RequestParam
            com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
                    .serialNumber(serialNo)
                    .nonce(nonceStr)
                    .signature(signature)
                    .timestamp(timestamp)
                    .body(body)
                    .build();
            // 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用  config
            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(paySDK.getConfig());
            // 验签、解密并转换成 Transaction
            Transaction transaction = parser.parse(requestParam, Transaction.class);
            //记录日志信息
            Transaction.TradeStateEnum state = transaction.getTradeState();
            String orderNo = transaction.getOutTradeNo();
            System.out.println("订单号:" + orderNo);
            if (state == Transaction.TradeStateEnum.SUCCESS) {
                System.out.println("支付成功");
                //TODO------
                //根据自己的需求处理相应的业务逻辑,异步

                //通知微信回调成功
                response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
            } else {
                System.out.println("微信回调失败,JsapiPayController.payNotify.transaction:" + transaction.toString());
                //通知微信回调失败
                response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            is.close();
        }
    }

订单查询

除了支付回调的异步通知,我们还需要通过定时任务主动去查询支付信息来保证业务订单支付状态的正确

    @PostMapping("/pay_check")
    public Object pay_check(@RequestBody Map<String, Object> params) throws Exception {
        String orderID = String.valueOf(params.get("orderID"));
        com.wechat.pay.java.service.payments.model.Transaction transaction = paySDK.getPayOrderInfo(orderID);
        com.wechat.pay.java.service.payments.model.Transaction.TradeStateEnum state = transaction.getTradeState();
        if (state == com.wechat.pay.java.service.payments.model.Transaction.TradeStateEnum.SUCCESS) {
            return Result.okResult().add("obj", transaction);
        } else {
            return Result.errorResult().add("obj", transaction);
        }
    }
    //获取订单支付结果信息
    public com.wechat.pay.java.service.payments.model.Transaction getPayOrderInfo(String orderID) {
        initPayConfig();
        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
        request.setMchid(mchId);
        request.setOutTradeNo(orderID);
        com.wechat.pay.java.service.payments.model.Transaction transaction = service.queryOrderByOutTradeNo(request);
        return transaction;
    }

退款申请

退款申请需要业务订单号和微信支付号,所以我们这里先通过查询订单信息得到transactionId,你也可以冗余记录在表中

退款支持全款退款和部分退款,部分退款对应的场景就是同一个订单买了多个商品,只退款了其中一个

这里只发起退款申请,具体的退款处理进度通知由退款回调完成

    @PostMapping("/back")
    public Object back(@RequestBody Map<String, Object> params) throws Exception {
        String orderID = String.valueOf(params.get("orderID"));
        String backID = String.valueOf(params.get("backID"));
        BigDecimal backAmount = new BigDecimal(String.valueOf(params.get("backAmount")));
        paySDK.applyRefund(orderID,backID,backAmount);
        return Result.okResult();
    }
    //申请退款
    public void applyRefund(String orderID, String backID,BigDecimal backAmount) {
        initPayConfig();
        initBackConfig();
        QueryOrderByOutTradeNoRequest payRequest = new QueryOrderByOutTradeNoRequest();
        payRequest.setMchid(mchId);
        payRequest.setOutTradeNo(orderID);
        com.wechat.pay.java.service.payments.model.Transaction transaction = service.queryOrderByOutTradeNo(payRequest);


        CreateRequest request = new CreateRequest();
        request.setTransactionId(transaction.getTransactionId());
        request.setNotifyUrl(v3BackNotifyUrl);
        request.setOutTradeNo(transaction.getOutTradeNo());
        request.setOutRefundNo(backID);
        request.setReason("测试退款");
        AmountReq amountReq = new AmountReq();
        amountReq.setCurrency(transaction.getAmount().getCurrency());
        amountReq.setTotal(Long.parseLong((transaction.getAmount().getTotal().toString())));
        amountReq.setRefund( (backAmount.multiply(new BigDecimal(100))).longValue());
        request.setAmount(amountReq);
        backService.create(request);
    }

退款回调

退款回调在申请退款后自动调用该接口,由于退款需要一定的处理时间,所以回调通知一般显示的状态为处理中(PROCESSING)可以在此回调更新订单退款的处理状态

    @PostMapping("/back_notify")
    public void back_notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //获取报文
        String body = getRequestBody(request);
        //随机串
        String nonceStr = request.getHeader("Wechatpay-Nonce");
        //微信传递过来的签名
        String signature = request.getHeader("Wechatpay-Signature");
        //证书序列号(微信平台)
        String serialNo = request.getHeader("Wechatpay-Serial");
        //时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        InputStream is = null;
        try {
            is = request.getInputStream();
            // 构造 RequestParam
            com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
                    .serialNumber(serialNo)
                    .nonce(nonceStr)
                    .signature(signature)
                    .timestamp(timestamp)
                    .body(body)
                    .build();
            // 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用  config
            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(paySDK.getConfig());
            // 验签、解密并转换成 Transaction
            Refund refund = parser.parse(requestParam, Refund.class);
            //记录日志信息
            Status state = refund.getStatus();
            String orderID = refund.getOutTradeNo();
            String backID = refund.getOutRefundNo();
            System.out.println("订单ID:" + orderID);
            System.out.println("退款ID:" + backID);
            if (state == Status.PROCESSING) {
                //TODO------
                //根据自己的需求处理相应的业务逻辑,异步

                //通知微信回调成功
                response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
                System.out.println("退款处理中");
            } else if (state == Status.SUCCESS) {
                //TODO------
                //根据自己的需求处理相应的业务逻辑,异步

                //通知微信回调成功
                response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
                System.out.println("退款完成");
            } else {
                System.out.println("微信回调失败,JsapiPayController.Refund:" + state.toString());
                //通知微信回调失败
                response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            is.close();
        }
    }

退款查询

除了退款回调的异步通知,我们还需要通过定时任务主动去查询退款信息来保证业务订单退款状态的正确

 @PostMapping("/back_check")
    public Object back_check(@RequestBody Map<String, Object> params) throws Exception {
        String backID = String.valueOf(params.get("backID"));
        Refund refund = paySDK.getRefundOrderInfo(backID);
        if (refund.getStatus() == Status.SUCCESS) {
            return Result.okResult().add("obj", refund);
        }if (refund.getStatus() == Status.PROCESSING) {
            return Result.okResult().setCode(2).setMsg("退款处理中").add("obj", refund);
        } else {
            return Result.errorResult().add("obj", refund);
        }
    }
    //获取订单退款结果信息
    public Refund getRefundOrderInfo(String backID){
        initBackConfig();
        QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
        request.setOutRefundNo(backID);
        return backService.queryByOutRefundNo(request);
    }

标签:JSAPIV3,orderID,String,微信,request,new,退款,SpringBoot
From: https://blog.51cto.com/u_11284240/7436968

相关文章

  • SpringBoot集成微信支付JSAPIV3保姆教程
    前言最近为一个公众号h5商城接入了微信支付功能,查找资料过程中踩了很多坑,以此文章记录一下和大家分享前期准备公众号认证微信支付功能需要开通企业号并进行资质认证,费用一年300,且需企业营业执照等信息,对公账户打款验证登录微信公众平台https://mp.weixin.qq.com/,创建服务号......
  • springboot的bean的声明周期
    创建对象属性填充aware接口的对应方法init-method方法(@PostConstruct)initialingBean接口的方法BeanPostProcessor的前置方法BeanPostProcessor的后置方法DisposableBean的distory方法测试对象@ComponentpublicclassTestControllerimplementsBeanNameAware,Initial......
  • 使用Jenkins的流水线生产Allure报告发送到企业微信
    pipeline{agentanyenvironment{name="${JOB_NAME}"times="${currentBuild.duration}"task="${BUILD_ID}"logs="${ChangeLog}"}stages{stage('拉取代码'){steps{gitcred......
  • 使用gradle的方式进行Springboot3的web开发(微服务版)
    简要:最近看了很多的Springboot3的项目,但是发现很多都是用maven来进行版本管理的,很少有用gradle来管理的,通过网上查找资料,看视频,终于自己写一个gradle管理的Springboot3的项目 第一步:创建项目注意:JDK的版本必须要在17或者以上。 第二步:设置gradle仓库 第三步:创建项目......
  • SpringBoot上传文件
    application.yaml配置上传路经,其实写在哪都行无所谓,就是引入配置文件,@Values注解赋值web:#绝对路经upload-path:D://test/springservlet:multipart:enabled:true#单个文件的最大上限max-file-size:1024MB#单个请求的文件总大......
  • SpringBoot跨域访问
    没有引入SpringSecuity的情况Christopher2021.10.23CORS后端跨域CORS是一种访问机制,Cross-OriginResourceSharing,跨域资源共享,通过在服务器端设置相应头,把发起跨域的原始域名添加到Access-Control-Allow-Origin中即可。何为跨域域,即域名,跨域,即从域名A申请访......
  • 关于spring的注解作用(springboot相较于spring 的不同)
      springboot的@Bean注解作用在方法上,它会将这个方法返回的类型实例注入spring容器。  <bean>标签代表一个实例(或对象),而不是一个类型。在Spring中,<bean>标签用于声明和配置一个bean实例。当我们在XML配置文件中使用<bean>标签时,我们定义的是一个具体的b......
  • 分享一个 SpringBoot + Redis 实现「查找附近的人」的小技巧
    前言SpringDataRedis提供了十分简单的地理位置定位的功能,今天我就用一小段代码告诉大家如何实现。正文1、引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>2、更......
  • SpringBoot + 自定义注解,实现用户操作日志(支持SpEL表达式)
    背景一个成熟的系统,都会针对一些关键的操作,去创建用户操作日志。比如:XX人创建了一条订单,订单号:XXXXXXXXX因为操作人或者订单号是动态的,所以有些开发人员,不知道获取,就将这种操作日志和业务代码融在一起。我们当然要杜绝这种现象,一定会有更好的解决方案。当前项目除了......
  • SpringBoot创建Thymeleaf
    1.pom.xml导入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency> 2.thymelef的默认配置文件springboot工程默认有一个templates文件夹,所有的html页面都放这个文件夹里。......