SpringBoot 2.3.12 + JPA+ QueryDsl + mysql + 小程序原生 实现微信小程序 APIV3支付

1.准备

  1. 需要从微信公众平台先申请小程序:微信公众平台地址
    这一步会得到 小程序的 appId 、appSecret
  2. 从微信商户平台,要调通微信支付最新的v3支付,需配置提供4项内容:商户私钥、证书序列号、APIv3密钥、平台证书。
    微信商户平台地址
    小程序支付文档地址
    小程序JSApi 下单接口地址
    APiV3 接口规则介绍,证书密钥签名介绍

步骤:
登录(https://pay.weixin.qq.com)后,在“账户中心”->“API安全”。完成“申请API证书”和“设置APIv3密钥”配置。“设置API密钥”可不设置,v3支付用不到。
1、商户私钥。
在“申请API证书”,根据引导流程完成配置,你将下载获得(apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem)三个文件,其中apiclient_key.pem就是商户私钥,另外两个文件没用。
2、证书序列号。
在完成“申请API证书”配置后,右侧点击“管理证书”,可以看到证书序列号,复制保存下来。
3、设置APIv3密钥。
随机字符串,对于v3支付,该密钥很重要。
4、平台证书。
获取平台证书接口地址

签名验签:可以保证数据的完整性和真实性, 商户用自己的私钥签名,微信平台用商户公钥进行验签;微信平台用平台私钥签名,商户要用平台公钥进行验签。采用的是RSA非对称加密的方式。
证书和回调报文解密:为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密

2.流程图

微信小程序登陆整体流程图:
在这里插入图片描述

微信小程序支付支付整体流程
在这里插入图片描述

3. SpringBoot 后台配置

  1. maven 配置
    parent pom.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dechnic</groupId>
    <artifactId>wcx_pay_service</artifactId>
    <packaging>pom</packaging>
    <version>0.0.1-SNAPSHOT</version>

    <modules>
        <module>dechnic-admin</module>
        <module>dechnic-common</module>
        <module>dechnic-pay</module>
    </modules>

    <name>wcx_pay_service</name>
    <description>预付费小程序后台服务</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>2.3.12.RELEASE</spring.boot.version>
        <gson.version>2.8.9</gson.version>
        <commons.lang.version>2.6</commons.lang.version>
        <httClient.version>4.5.6</httClient.version>
    </properties>

    <!--依赖声明-->
    <dependencyManagement>
        <dependencies>

            <!--springBoot 依赖配置-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--Spring Web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring.boot.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <version>${spring.boot.version}</version>
                <scope>test</scope>
            </dependency>
            <!--gson-->
            <dependency>
                <groupId>repository.com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>${gson.version}</version>
            </dependency>

            <!--commons-lang 工具包-->
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>${commons.lang.version}</version>
            </dependency>

            <!--Http Client-->
            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpcore</artifactId>
                <version>4.4.15</version>
            </dependency>

            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
                <version>${httClient.version}</version>
            </dependency>

            <!--dechnic-common-->
            <dependency>
                <groupId>com.dechnic</groupId>
                <artifactId>dechnic-common</artifactId>
                <version>${project.version}</version>
            </dependency>

            <!--dechnic-pay-->
            <dependency>
                <groupId>com.dechnic</groupId>
                <artifactId>dechnic-pay</artifactId>
                <version>${project.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.project.lombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <compilerArguments>
                        <verbose/>
                        <bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar</bootclasspath>
                    </compilerArguments>
                </configuration>

            </plugin>
        </plugins>
    </build>

</project>


支付模块 pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wcx_pay_service</artifactId>
        <groupId>com.dechnic</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>dechnic-pay</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--微信支付API v3的Apache HttpClient扩展,实现了请求签名的生成和应答签名的验证-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--commons-lang-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>
        <!--dechnic-common-->
        <dependency>
            <groupId>com.dechnic</groupId>
            <artifactId>dechnic-common</artifactId>
        </dependency>

    </dependencies>


</project>


dechnic-admin 模块 pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wcx_pay_service</artifactId>
        <groupId>com.dechnic</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>dechnic_admin</artifactId>

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- 添加Spring-data-jpa依赖. -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--querydsl-->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
            <version>2.3.9.RELEASE</version>
        </dependency>

        <!-- spring boot devtools 依赖包. -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>provided</scope>
        </dependency>

        <!--支付依赖包-->
        <dependency>
            <groupId>com.dechnic</groupId>
            <artifactId>dechnic-pay</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk16</artifactId>
            <version>1.46</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>



    <!-- 构建节点. -->
    <build>
        <finalName>wxx_pay_service</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.project.lombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <compilerArguments>
                        <verbose />
                        <bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar</bootclasspath>
                    </compilerArguments>
                </configuration>
            </plugin>

            <!--该插件可以生成querysdl需要的查询对象,执行mvn compile即可-->
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>


dechnic-common 模块 pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wcx_pay_service</artifactId>
        <groupId>com.dechnic</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>dechnic-common</artifactId>

    <dependencies>
        <!--gson-->
        <dependency>
            <groupId>repository.com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!-- 添加Spring-data-jpa依赖. -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <!-- 图片压缩 开源 -->
        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.8</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <!--commons-lang-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.4.M1</version>
        </dependency>
    </dependencies>
</project>


  1. WxPayV3Config
package com.dechnic.pay.config;

import com.dechnic.pay.configprops.WeChatPayProperties;
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.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;

/**
 * @description:
 * @author:houqd
 * @time: 2022/7/6 16:22
 */
@Slf4j
@ConditionalOnProperty(prefix = "wx.applet",name = "chargeFlag",havingValue = "true")
@Configuration
public class WxPayV3Config {
    @Autowired
    WeChatPayProperties weChatPayProperties;

    /**
     * 获取商户的私钥文件
     * @param filename
     * @return
     */
    private PrivateKey getPrivateKey(String filename){

        try {
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }
    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix = "wx.applet",name = "chargeFlag",havingValue = "true")
    public Verifier getVerifier() throws Exception {
        log.info("获取签名验证器");
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(weChatPayProperties.getPrivateKeyPath());
        // 私钥签名对象
        PrivateKeySigner keySigner = new PrivateKeySigner(weChatPayProperties.getMchSerialNo(), privateKey);
        // 身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(weChatPayProperties.getMchId(), keySigner);

        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(weChatPayProperties.getMchId(), wechatPay2Credentials,
                weChatPayProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
        Verifier verifier = certificatesManager.getVerifier(weChatPayProperties.getMchId());

        return verifier;
    }

    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean("wxPayClient")
    @ConditionalOnProperty(prefix = "wx.applet",name = "chargeFlag",havingValue = "true")
    public CloseableHttpClient getWxPayClient(Verifier verifier){

        log.info("获取httpclient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(weChatPayProperties.getPrivateKeyPath());
        // 从证书管理器中获取verifier
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(weChatPayProperties.getMchId(), weChatPayProperties.getMchSerialNo(), privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }

    /**
     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
     */
    @Bean(name = "wxPayNoSignClient")
    @ConditionalOnProperty(prefix = "wx.applet",name = "chargeFlag",havingValue = "true")
    public CloseableHttpClient getWxPayNoSignClient(){

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(weChatPayProperties.getPrivateKeyPath());

        //用于构造HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(weChatPayProperties.getMchId(), weChatPayProperties.getMchSerialNo(), privateKey)
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        log.info("== getWxPayNoSignClient END ==");
        return httpClient;
    }




}


  1. 配置文件
    application.yml
spring:
  profiles:
    active: dev

application-dev.yml

server:
  port: 443
  servlet-path: /api/spro/*
  
spring:
  application:
    name: wcx_pay_service
  datasource:
    url: jdbc:mysql://XXXXXX:3306/jyservice_spro?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
    username: XXXX
    password: XXXXX
    driver-class-name: com.mysql.cj.jdbc.Driver
    tomcat:
      max-active: 100
      max-idle: 20
      min-idle: 20
      initial-size: 10
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5Dialect
#日志
logging:
  config: classpath:logback-config.xml
  file:
    path: ./logs
  level:
    root: info
    org.hibernate: info
    com.dechnic: debug

#常量参数
pro-server:
  pro:
    name: cloud-jyservice-spro
    flag: 1

#新版微信支付配置信息
wx:
  #微信小程序配置
  applet:
    wcxVersion: 1.0.4
    appId: XXXX
    appSecret: XXX

  #微信支付配置
  pay:
    mchId: XXXX #微信支付商户号
    mchSerialNo: XXXX#证书序列号
    apiV3Key: XXXXX  #V3密钥
    keyPass: XXXX  #证书密码 默认微信商户号
    privateKeyPath: D:/apiclient_key.pem
    notifyUrl: http://localhost:443/api/spro/wxPay/pay-notify


#预付费小程序业务服务器配置
bizserver:
  appId: XXXXX
  secret: XXXXX
  URL: http://localhost:8088  
  context-path: /openapi
  sys-customer: iot-tfdchengo


WeChatAppletProperties 小程序属性配置类

package com.dechnic.pay.configprops;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @description:
 * @author:houqd
 * @time: 2022/7/6 16:39
 */
@Component
@ConfigurationProperties(prefix = "wx.applet")
@Data
public class WeChatAppletProperties {
    private String wcxVersion;//小程序版本号
    private String appId;//小程序appId
    private String appSecret;// 小程序密钥
    private boolean chargeFlag;// 是否开启充值功能
}


WeChatPayProperties.java 小程序支付属性配置类

package com.dechnic.pay.configprops;

import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @description:
 * @author:houqd
 * @time: 2022/7/5 13:58
 */
@Component
@ConfigurationProperties(prefix = "wx.pay")
@ConditionalOnProperty(prefix = "wx.applet",name = "chargeFlag",havingValue = "true")
@Data
public class WeChatPayProperties {

    /**
     * 商户id
     */
    private String mchId;

    /**
     * 商户证书序列号
     */
    private String mchSerialNo;

    /**
     * apiV3密钥
     */
    private String apiV3Key;

    /**
     * p12证书文件位置
     */
    private String p12Path;

    /**
     * 证书密码
     */
    private String keyPass;

    /**
     * 商户私钥
     */
    private String privateKeyPath;

    /**
     * 微信平台回调地址
     */
    private String notifyUrl;



}


  1. 生成订单、查询订单状态方法
    IWChatPayService
package com.dechnic.pay.service;

import com.dechnic.pay.po.QueryResultPO;

import java.util.Map;

/**
 * @description: 小程序支付接口
 * @author:houqd
 * @time: 2022/7/8 14:33
 */

public interface IWChatPayService {

    // 创建JSApiOrder 订单
    Map<String, Object> createJSApiOrder(String orderNo, Integer amount, String openid, String goodsName) throws Exception;
    // 查询订单状态
     QueryResultPO getOrderState(String outTradeNo, String mChid) throws Exception;

}

WChatPayServiceImpl

package com.dechnic.pay.service.impl;

import com.dechnic.common.util.GsonUtll;
import com.dechnic.common.util.RandomUtil;
import com.dechnic.pay.configprops.WeChatAppletProperties;
import com.dechnic.pay.configprops.WeChatPayProperties;
import com.dechnic.pay.exception.BusinessException;
import com.dechnic.pay.po.QueryResultPO;
import com.dechnic.pay.service.IWChatPayService;
import com.dechnic.pay.util.PayUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.ClientProtocolException;
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.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class WChatPayServiceImpl implements IWChatPayService {

    /**
     * 微信小程序下单的请求地址
     */
    private static final String V_3_PAY_TRANSACTIONS_JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
    private static final String V_3_ORDER_SEARCH_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/";
    // 测试开关
    private static final boolean isTest = true;
    @Autowired
    WeChatPayProperties weChatPayProperties;
    @Autowired
    WeChatAppletProperties weChatAppletProperties;

    @Resource
    private CloseableHttpClient wxPayClient;
    @Autowired
    private PayUtils payUtils;

    /**
     * 创建微信小程序订单
     *
     * @param orderNo   订单号
     * @param amount    单位 分
     * @param openid    小程序的openid
     * @param goodsName 订单名称
     * @param
     * @throws IOException
     */
    @Override
    public Map<String, Object> createJSApiOrder(String orderNo, Integer amount, String openid, String goodsName) throws IOException {// 请求body参数
        log.info("============生成订单==========");
        HttpPost httpPost = new HttpPost(V_3_PAY_TRANSACTIONS_JSAPI);
        // 请求body参数
        Map paramsMap = new HashMap();
        paramsMap.put("appid", weChatAppletProperties.getAppId());// appid
        paramsMap.put("mchid", weChatPayProperties.getMchId());//商户号
        paramsMap.put("description", goodsName);// 商品描述
        paramsMap.put("out_trade_no", orderNo);// 商户订单号
        paramsMap.put("notify_url", weChatPayProperties.getNotifyUrl());// 通知地址

        //订单金额
        //TODO 测试为 1 分钱
        Map amountMap = new HashMap();
        amountMap.put("total", isTest ? 1 : amount);
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);

        //支付者
        Map playerMap = new HashMap();
        playerMap.put("openid", openid);
        paramsMap.put("payer", playerMap);

        //将参数转化未json字符串
        String jsonParamsMap = GsonUtll.toJSON(paramsMap);
        log.info("请求参数:" + jsonParamsMap);

        StringEntity entity = new StringEntity(jsonParamsMap, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        //完成签名并执行请求
        CloseableHttpResponse resp = wxPayClient.execute(httpPost);

        int statusCode = resp.getStatusLine().getStatusCode();
        String bodyAsString = EntityUtils.toString(resp.getEntity());
        if (statusCode == 200) { //处理成功
            log.info("成功,返回结果 = " + bodyAsString);
        } else if (statusCode == 204) { //处理成功,无返回Body
            log.info("成功");
        } else {
            System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
            throw new IOException("request failed");
        }

        Map<String, Object> resMap = new HashMap<>();
        try {
            Map<String, Object> map = GsonUtll.jsonStr2Map(bodyAsString);
            String prepayId = (String) map.get("prepay_id");
            Assert.isTrue(StringUtils.isNotBlank(prepayId), "下单获取参数失败");

            long timeStamp = System.currentTimeMillis() / 1000;
            String nonceStr = RandomUtil.getUUID32().toUpperCase();
            String packagep = "prepay_id=" + prepayId;


            resMap.put("appId", weChatAppletProperties.getAppId());
            resMap.put("timeStamp", timeStamp);
            resMap.put("nonceStr", nonceStr);
            resMap.put("package", packagep);
            resMap.put("signType", "RSA");

            //通过appid,timeStamp,nonceStr,signType,package以及商户密钥进行key=value形式进行拼接加密
            String aPackage = payUtils.buildMessageForPay(weChatAppletProperties.getAppId(), timeStamp, nonceStr, (String) resMap.get("package"));

            //获取对应的签名
            String paySign = payUtils.sign(weChatPayProperties.getPrivateKeyPath(), aPackage.getBytes("utf-8"));
            resMap.put("paySign", paySign);

            log.info("[微信支付] 支付参数:" + GsonUtll.toJSON(resMap));
        } catch (Exception e) {
            throw new BusinessException(e.getMessage());
        }

        return resMap;
    }

    /**
     * 查询订单状态
     *
     * @param outTradeNo 商户订单号
     * @param mChid      直连商户号
     * @return
     */
    @Override
    public QueryResultPO getOrderState(String outTradeNo, String mChid) {
        CloseableHttpResponse response = null;
        log.info("==================开始查询订单状态,订单号:[" + outTradeNo + "',商户id:[" + mChid + "]");
        if (StringUtils.isEmpty(outTradeNo)) {
            throw new BusinessException("查询订单状态,商户订单号不能为空");
        }
        if (StringUtils.isEmpty(mChid)) {
            throw new BusinessException("查询订单状态,直连商户号不能为空");
        }
        String queryUrl = V_3_ORDER_SEARCH_URL + outTradeNo + "?mchid=" + mChid;
        QueryResultPO queryResultPO = null;
        try {
            URIBuilder uriBuilder = new URIBuilder(queryUrl);
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.addHeader("Accept", "application/json");
            try {
                response = wxPayClient.execute(httpGet);
                int statusCode = response.getStatusLine().getStatusCode();
                String bodyAsString = EntityUtils.toString(response.getEntity());
                if (statusCode == 200) { // 查询订单成功
                    log.info("查询订单成功,返回结果:" + bodyAsString);
                } else if (statusCode == 204) { //处理成功,无返回Body
                    log.info("成功");
                } else {
                    System.out.println("查询订单状态失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                }

                Map<String, Object> resultMap = GsonUtll.jsonStr2Map(bodyAsString);
                String trade_state = (String) resultMap.get("trade_state");// 支付状态
                String trade_type = (String) resultMap.get("trade_type");// 交易类型
                String transaction_id = (String) resultMap.get("transaction_id");//微信支付订单号
                String trade_state_desc = (String) resultMap.get("trade_state_desc");//交易状态描述
                String bank_type = (String) resultMap.get("bank_type");// 付款银行
                String success_time = (String) resultMap.get("success_time");//支付完成时间
                Map<String, Object> payer = (Map<String, Object>) resultMap.get("payer");
                if (null != payer){
                    String openid = (String) payer.get("openid");
                }
                Map<String, Object> amout = (Map<String, Object>) resultMap.get("amount");
                Long total = null;
                if (null != amout){
                     total = (Long) amout.get("total");//订单总金额 单位分
                }
                queryResultPO = new QueryResultPO();
                queryResultPO.setBank_type(bank_type);
                if ("SUCCESS".equals(trade_state) || "CLOSED".equals(trade_state)) {
                    queryResultPO.setPaySuccess(true);
                } else {
                    queryResultPO.setPaySuccess(false);
                }
                queryResultPO.setSuccess_time(success_time);
                queryResultPO.setTotal(total);
                queryResultPO.setTrade_state_desc(trade_state_desc);
                queryResultPO.setTransaction_id(transaction_id);

            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }


        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        return queryResultPO;

    }
}

  1. 工具类 PayUtils.java
package com.dechnic.pay.util;

import com.dechnic.pay.configprops.WeChatPayProperties;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Map;

/**
 * @description:
 * @author:houqd
 * @time: 2022/7/8 13:46
 */
@Slf4j
@Component
public class PayUtils {
    @Autowired
    WeChatPayProperties weChatPayProperties;

    /**
     * 小程序调起支付 构造签名串
     * @param appId
     * @param timestamp
     * @param nonceStr
     * @param packag
     * @return
     */
    public  String buildMessageForPay(String appId, long timestamp, String nonceStr, String packag) {
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + packag + "\n";
    }


    /**
     * 用商户私钥证书进行签名
     * @param wxCertPath 商户私钥证书路径
     * @param message 待签名数据
     * @return
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     * @throws IOException
     * @throws InvalidKeyException
     * @throws java.security.InvalidKeyException
     */
    public  String sign(String wxCertPath,byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException, java.security.InvalidKeyException {
        Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
        sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(wxCertPath)));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 微信后台返回密文解密
     * @param bodyMap
     * @return
     * @throws GeneralSecurityException
     */
    public  String decryptFromResource(Map<String,Object> bodyMap) throws GeneralSecurityException {
        log.info("===========秘文解密============");
        //通知数据
        Map<String,String > resourceMap =(Map<String, String>) bodyMap.get("resource");
        //数据秘文
        String ciphertext = resourceMap.get("ciphertext");
        //获取随机串
        String nonce = resourceMap.get("nonce");
        String associated_data = resourceMap.get("associated_data");

        log.info("秘文===》{}",ciphertext);
        AesUtil aesUtil = new AesUtil(weChatPayProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //获取明文(解密后的数据)
        String plainText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info("明文====》{}",plainText);

        return plainText;
    }
}

HttpUtils.java

package com.dechnic.pay.util.api;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;


public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @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);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


WechatPay2ValidatorForRequest.java 从微信官方发给我的加密信息,进行验签解密

package com.dechnic.pay.util.api;

import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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.*;



/**
 * 从微信官方发给我的加密信息,进行验签解密
 */
public class WechatPay2ValidatorForRequest {

    protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    protected final Verifier verifier;
    protected final String  requestId;
    protected final String  body;

    public WechatPay2ValidatorForRequest(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) throws IOException {
        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;
    }

    protected final 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);
        }
    }

    protected final String buildMessage(HttpServletRequest request) throws IOException {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

    protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

}


OrderController.java 订单controller 里面包含创建订单、获取支付签名、支付成功回调

package com.dechnic.admin.web.controller.order;

import com.dechnic.admin.model.bean.order.OrderInfo;
import com.dechnic.admin.model.bean.user.UserInfo;
import com.dechnic.admin.model.to.order.GoodsTO;
import com.dechnic.admin.service.order.OrderService;
import com.dechnic.admin.service.user.UserSearchService;
import com.dechnic.common.dto.AjaxResult;
import com.dechnic.common.util.GsonUtll;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * @author Jahnke【1029777564@qq.com】
 * @ClassName OrderController
 * @Description:
 * @date 2020/7/17 11:34
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private UserSearchService userSearchService;
    @Autowired
    private OrderService orderService;



    // 创建订单
    @RequestMapping("get")
    public String add(String sysCustomer, String token, String goodsDatas, String orderDatas, String desc) {
        UserInfo userInfo = userSearchService.getByToken(sysCustomer, token);
        List<GoodsTO> goodsList = GsonUtll.json2ListBean(goodsDatas, GoodsTO.class);
        if (goodsList == null || goodsList.isEmpty()) {
            return AjaxResult.errorResult("购买的物品不能为空!");
        }

        OrderInfo info = orderService.add(sysCustomer, goodsList, desc, orderDatas, userInfo);

        return AjaxResult.successResult(info);
    }

    // 获取支付签名
    @RequestMapping("get-sign")
    public Map<String,Object> getSign(String sysCustomer, String token, String orderNo, String payType, String wxOpenId) {
        UserInfo userInfo = userSearchService.getByToken(sysCustomer, token);
        Assert.notNull(userInfo,"当前帐号失效,请重新登录");
        Map<String,Object> sign = null;
        try {
            sign = orderService.getSign(sysCustomer, orderNo, payType, wxOpenId);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.ok(sign);
    }

    // 自己系统支付回调
    @RequestMapping("pay-notify")
    public String payNotify(String sysCustomer, String orderNo) {
        orderService.payNotify(sysCustomer, orderNo);
        return AjaxResult.successResult();

    }

}

OrderService

package com.dechnic.admin.service.order;

import com.dechnic.admin.model.bean.order.OrderInfo;
import com.dechnic.admin.model.bean.order.QOrderInfo;
import com.dechnic.admin.model.bean.user.UserInfo;
import com.dechnic.admin.model.to.order.GoodsTO;
import com.dechnic.admin.model.vo.order.OrderRepository;
import com.dechnic.admin.web.advice.JyResultException;
import com.dechnic.common.dto.AjaxResult;
import com.dechnic.common.util.GsonUtll;
import com.dechnic.pay.configprops.WeChatPayProperties;
import com.dechnic.pay.constants.OrderStateConstant;
import com.dechnic.pay.po.QueryResultPO;
import com.dechnic.pay.service.IOrderSerivce;
import com.dechnic.pay.service.impl.WChatPayServiceImpl;
import com.querydsl.core.BooleanBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * 订单方法
 */
@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private IOrderSerivce IOrderSerivce;
    @Autowired
    private WChatPayServiceImpl wChatPayServiceImpl;
    @Autowired
    private WeChatPayProperties weChatPayProperties;

    // 创建订单
    public OrderInfo add(String sysCustomer, List<GoodsTO> goodsTOList, String desc, String orderDatas, UserInfo userInfo) {
        // 计算金额
        Double money = goodsTOList.stream().collect(Collectors.summarizingDouble(GoodsTO::getMoney)).getSum();

        OrderInfo info = new OrderInfo();
        // 商户订单号生成
        String orderNo = IOrderSerivce.generaterOrderNo("power");
        info.setSysCustomer(sysCustomer);
        info.setOrderNo(orderNo);
        info.setUser_name(userInfo.getName());
        info.setUsername(userInfo.getUsername());
        info.setOrderDatas(orderDatas);
        info.setUser_phone(userInfo.getPhone());
        info.setCreateTim(new Date());
        info.setDelFlag(0);
        info.setfVersion(1);
        info.setOrderType("power");
        info.setNoticeState(0);
        info.setOrderState(OrderStateConstant.ORDER_STATE_NEW);
        info.setPaymentMode("online");
        info.setOrderMoney(money);
        info.setGoodsDatas(GsonUtll.toJSON(goodsTOList));
        info.setOrderDesc(desc);
        orderRepository.save(info);

        return info;
    }

    // 获取支付签名
    public Map<String, Object> getSign(String sysCustomer, String orderNo, String payType, String userwx) throws IOException {
        OrderInfo dbInfo = orderRepository.findBySysCustomerAndOrderNo(sysCustomer, orderNo);
        if (dbInfo != null) {
            if (dbInfo.getOrderState() <= 0) {
                return AjaxResult.error("订单已取消,无法支付");
            }
            if (dbInfo.getOrderState() == OrderStateConstant.ORDER_STATE_FINISH) {
                return AjaxResult.error("订单已完成,无法重复支付!");
            }
            if (dbInfo.getOrderState() == OrderStateConstant.ORDER_STATE_SIGN) {
                // 判断支付是否完成
                QueryResultPO queryResultPO = wChatPayServiceImpl.getOrderState(orderNo, weChatPayProperties.getMchId());
                if (queryResultPO.isPaySuccess()) {
                    this.finishOrder(sysCustomer, orderNo);
                    throw new JyResultException("订单已支付,无法再次付款!");
                }
            }
            // 获取支付签名
            BigDecimal fenMoney = BigDecimal.valueOf(dbInfo.getOrderMoney()).setScale(2, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100));
            Map<String, Object> sign = wChatPayServiceImpl.createJSApiOrder(orderNo, Integer.valueOf(fenMoney.intValue()), userwx, dbInfo.getOrderDesc());
            dbInfo.setOrderState(OrderStateConstant.ORDER_STATE_SIGN);
            dbInfo.setSignTime(new Date());
            dbInfo.setPayType(payType);
            dbInfo.setPayMoney(dbInfo.getOrderMoney());
            dbInfo.setSignEffectTime(15);

            orderRepository.save(dbInfo);
            return sign;
        }
        return null;
    }

    // 支付回调
    public void payNotify(String sysCustomer, String orderNo) {
        // 支付完成,回调
        OrderInfo dbInfo = orderRepository.findBySysCustomerAndOrderNo(sysCustomer, orderNo);
        if (dbInfo != null) {
            // 判断支付是否完成
            QueryResultPO queryResultPO = wChatPayServiceImpl.getOrderState(orderNo, weChatPayProperties.getMchId());
            if (queryResultPO.isPaySuccess()) {
                this.finishOrder(sysCustomer, orderNo);
            }
        }
    }

    // 订单完成
    public void finishOrder(String sysCustomer, String orderNo) {
        OrderInfo dbInfo = orderRepository.findBySysCustomerAndOrderNo(sysCustomer, orderNo);
        if (dbInfo != null) {
            if (OrderStateConstant.ORDER_STATE_FINISH != dbInfo.getOrderState()) {
                dbInfo.setOrderState(OrderStateConstant.ORDER_STATE_FINISH);
                orderRepository.save(dbInfo);
            }
        }
    }

    // 跟踪订单支付完成状态
    @Scheduled(cron = "0/5 * * * * ?")
    public void autoFinish() {
        // 查询当前服务器中所有存在获取支付签名,但是尚未完成支付的订单,判断是否完成支付
        QOrderInfo qm = QOrderInfo.orderInfo;
        BooleanBuilder qBuilder = new BooleanBuilder();
        qBuilder.and(qm.orderState.eq(OrderStateConstant.ORDER_STATE_SIGN));
        Pageable pageable = new PageRequest(0, 500, new Sort(Sort.Direction.DESC, "id"));
        List<OrderInfo> list = orderRepository.findAll(qBuilder, pageable).getContent();
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                QueryResultPO queryResultPO = wChatPayServiceImpl.getOrderState(list.get(i).getOrderNo(), weChatPayProperties.getMchId());
                if (queryResultPO != null && queryResultPO.isPaySuccess()) {
                    this.finishOrder(list.get(i).getSysCustomer(), list.get(i).getOrderNo());
                    continue;
                }
                // 支付超时,设置订单过期
                if ((list.get(i).getSignTime().getTime() + list.get(i).getSignEffectTime() * 60 * 1000) < System.currentTimeMillis()) {
                    list.get(i).setOrderState(OrderStateConstant.ORDER_STATE_OUTTIME);
                    orderRepository.save(list.get(i));
                }
            }
        }
    }
}

NotifyController

package com.dechnic.admin.web.controller.order;

import com.dechnic.admin.service.order.OrderService;
import com.dechnic.pay.config.WxPayV3Config;
import com.dechnic.pay.configprops.BizServerConfigProps;
import com.dechnic.pay.util.PayUtils;
import com.dechnic.pay.util.api.HttpUtils;
import com.dechnic.pay.util.api.WechatPay2ValidatorForRequest;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;

/**
 * @description:
 * @author:houqd
 * @time: 2022/7/8 8:57
 */
@Slf4j
@RestController
@RequestMapping("/wxPay")
public class NotifyController {

    @Resource
    private PayUtils payUtils;

    @Resource
    private Verifier verifier;
    @Autowired
    private OrderService orderService;
    @Autowired
    private BizServerConfigProps bizServerConfigProps;


    /**
     * 微信通知回调地址
     *
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/notify")
    public String notify(HttpServletRequest request, HttpServletResponse response) {

        Gson gson = new Gson();
        //创建一个应答对象
        HashMap<String, String> map = new HashMap<>();
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            HashMap<String, Object> bodyMap = gson.fromJson(body, HashMap.class);

            String requestId = (String) bodyMap.get("id");

            log.info("支付通知的id=====》》》{}", bodyMap.get("id"));
            log.info("支付通知的完整数据=====》》》{}",body);

            //TODO : 签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.error("通知验签失败");
                //通知失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");
            //TODO : 处理订单
            //解密密文,获取明文
            String plaintText = payUtils.decryptFromResource(bodyMap);
            //将明文转为map
            HashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);


            //获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态
            String orderNo = (String) plaintTextMap.get("out_trade_no");
            //业务编号
            String transactionId = (String) plaintTextMap.get("transaction_id");
            //trade_type,支付类型,如果有需要的话, 你可以存储在数据库中,这里我们的数据,基本上都是JSapi支付类型
            String tradeType = (String) plaintTextMap.get("trade_type");
            //交易状态
            String tradeState = (String) plaintTextMap.get("trade_state");
            //还有很多,为这里就不一一去写了


            /**
             * 在更新你订单状态之前,可以先根据orderNo,查询数据库中是否有这个订单
             * 然后查询这个订单是否已经被处理过,也就是状态是否是已经支付的状态
             * 如果这个订单已经被处理了,那么我们可以直接return,没有被处理过我们在处理
             * 这样可以避免数据库被反复的操作
             *
             * 微信官方的解释是:
             *  同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
             *  推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,
             *  并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,
             *  则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,
             *  以避免函数重入造成的数据混乱。
             */


            //更新订单状态
            /*
             * 你的数据库操作
             * 一定要存储解密出来的transaction_id字段在数据库中
             * 如果需要跟微信官方对账的话,是需要提供这个字段进行一个查账的
             * */
            orderService.payNotify(bizServerConfigProps.getSysCustomer(), orderNo);


            //成功应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);
        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }
    }

}

4. 微信小程序端

recharge.wxml

<!--pages/recharge/recharge.wxml-->
<wxs src="../../utils/fn.wxs" module="tool" />

<view class="m-page">
    <view class="wp">
        <view class="m-balance">
            <view class="record">
                <navigator class="" target="" url="../rechargeRecord/rechargeRecord?meterCode={{meterRealInfo.meterCode}}&tenantCode={{tenantsInfo.code}}&contact={{tenantsInfo.contact}}&name={{tenantsInfo.name}}&remainderValue={{meterRealInfo.remainderValue}}" hover-class="navigator-hover" open-type="navigate">
                    充值记录
                </navigator>
            </view>
            <view class="balance">
                <view class="pic">
                    <image class="" src="{{meterRealInfo.meterType==='电表'?'/images/z-i5.png':'/images/z-i6.png'}}" mode="widthFix" lazy-load="false" binderror="" bindload=""></image>
                </view>
                <view class="tit">余量:{{tool.toFixed(meterRealInfo.remainderValue)}}</view>
            </view>
        </view>
        <view class="ul-list1">
            <view class="li">
                <view class="con">{{meterRealInfo.meterCode}}</view>
                <view class="tit">电表号</view>
            </view>
            <view class="li">
                <view class="con">{{tenantsInfo.name}}</view>
                <view class="tit">缴费单位</view>
            </view>
            <view class="li">
                <view class="con">{{tenantsInfo.code}}</view>
                <view class="tit">缴费户号</view>
            </view>
            <view class="li">
                <view class="con">{{tenantsInfo.contact}}</view>
                <view class="tit">户名</view>
            </view>
        </view>
        <view class="m-recharge">
            <view class="g-t1">本次充值</view>
            <view class="ul-list2" >
                <view wx:for="{{goodsList}}" class="li {{formData.select1==index?'on':''}}" bindtap="radioChange" data-name="select1" data-index="{{index}}">
                    {{item.goods}}{{goodsUnit}} -{{item.money}}
                </view>
            </view>
        </view>
        <view class="m-recharge">
            <view class="g-t1">支付方式</view>
            <view class="ul-list3">
                <view class="li {{formData.select2==0?'on':''}}" bindtap="radioChange" data-name="select2" data-index="0">
                    <view class="circular">
                        <image class="" src="{{config.imageUrl}}/images/z-i3.png" mode="widthFix" lazy-load="false" binderror="" bindload=""></image>
                    </view>
                    <view class="left">
                        <view class="pic">
                            <image class="" src="{{config.imageUrl}}/images/z-i2.png" mode="widthFix" lazy-load="false" binderror="" bindload=""></image>
                        </view>
                        <view class="txt">
                            <view class="h3">微信支付</view>
                            <view class="em">微信安全支付</view>
                        </view>
                    </view>
                </view>
            </view>
        </view>
    </view>
</view>
<view class="wp">
    <button bindtap="sumit" class="m-btn">
        确认支付
    </button>
</view>

recharge.js

const util = require('../../utils/util.js')
const sysConfig = require('../../config/sysConfig.js')
var app = getApp();
Page({

  /**
   * 页面的初始数据
   */
  data: {
    formData: {
      select1: 0,
      select2: 0
    },
    tenantsInfo: {}, // 商户信息
    meterRealInfo: {},  // 表具信息
    goodsList: [],  // 商品列表
    goodsUnit: 'kwh'
  },
  radioChange: function(e){
    let formData = this.data.formData;
    let index = e.currentTarget.dataset.index;
    let name = e.currentTarget.dataset.name;
    formData[name] = index;
    this.setData({
      formData: formData
    });
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    var that = this
    var tenantsInfo = {
      code: options.tenantCode,
      contact:options.contact,
      name: options.name,
      meterPrise:options.meterPrise
    }
    that.setData({
      meterCode:options.meterCode,
      tenantCode:options.tenantCode,
      tenantsInfo:tenantsInfo,
      meterPrise: util.isNotNull(options.meterPrise)?Number(options.meterPrise):0   // 单价
    })
    // 通过商户号/表具号获取信息
    that.queryMeterRealData()
  },
  // 通过商户号/表具号获取信息
  queryMeterRealData () {
    var that = this;
    var params = {
      meterCode: that.data.meterCode, // 表具编号
      tenantCode: that.data.tenantCode // 商户号
    }
    app.func.otherReq("/openapi/queryMeterRealData", params, function (result) {
      var successs = result.code == 1 ? true : false;
      if (successs) {
        var info = result.obj
        // if (util.isNull(info.remainderValue)|| util.isNull(that.data.meterPrise)) {
        //   info.balance = '0.00'
        // }else {
        //   info.balance = (info.remainderValue/that.data.meterPrise).toFixed(2)
        // }
        that.setData({
          meterRealInfo:info
        })
        // 获取商品列表
        that.getGoodsList()
      }
    })
  },
  // 获取充值列表
  getGoodsList () {
    var that = this;
    var params = {
      sysCustomer: sysConfig.app.sysCustomer
    }
    app.func.iotReq("/api/goods/list", params, function (result) {
      var successs = result.status == 1 ? true : false;
      if (successs) {
        var data = result.data
        console.log("========获取充值goods列表成功:"+result.data);
        console.log("=========meterRealInfo.meterType:"+that.data.meterRealInfo.meterType);
        console.log("======电价:"+that.data.meterPrise)
        var goodsList = []
        var goodsList2 = []
        var goodsUnit = 'kwh'
        if (util.isNotNull(data) && data.length > 0) {
          for (var i=0;i<data.length;i++) {
            console.log("====data["+i+"]:"+data[i])
              if (that.data.meterRealInfo.meterType === '电表' && data[i].type==='fee-ele') {
                goodsList = data[i].data

              } else if (that.data.meterRealInfo.meterType === '水表' && data[i].type==='fee-water') {
                goodsList = data[i].data
                goodsUnit = 'm³'
              }
            }
            if (goodsList.length> 0) {
              for (let index = 0; index < goodsList.length; index++) {
                const e = goodsList[index];
                e.goods = (util.isNotNull(that.data.meterRealInfo.meterCT) ?that.data.meterRealInfo.meterCT:1) * e.goods
                e.money =  (e.goods * that.data.meterPrise).toFixed(2)
                goodsList2.push(e)
              }
            }
        }
        console.log(goodsList2)
        that.setData({
          goodsList:goodsList2,
          goodsUnit: goodsUnit
        })
      }
    })
  },
  // 支付
  sumit() {
    var that = this
    // 创建订单
    that.orderGet()
  },
  // 创建订单
  orderGet () {
    console.log("=========开始执行创建订单=========")
    var that = this;
    var token = wx.getStorageSync('token')
    if(util.isNull(token)) {
      util.showToast('请先登录!')
      setTimeout(function(){
        wx.navigateTo({
          url: '../login/login'
        })
      },500)
      return
    }
    var select1 = that.data.formData.select1
    var goodsDatas = [{
      "goods": that.data.goodsList[select1].goods,        // 商品
      "money": that.data.goodsList[select1].money     // 花费金额
    }]
    var orderDatas = {
      "tenantCode":that.data.tenantCode,            // 商户号 必填
      "meterCode": that.data.meterCode,            // 表具编号 必填
    }
    var desc = that.data.meterRealInfo.meterType === '电表'?'电费充值':(that.data.meterRealInfo.meterType === '水表'?'水费充值':that.data.meterRealInfo.meterType + '充值')
    var params = {
      sysCustomer: sysConfig.app.sysCustomer,
        "token":token,                 // 登录标识 必填
        "goodsDatas": JSON.stringify(goodsDatas),       // json数组格式的电费信息(数组视为了适配购买多个商品的情况)
        "orderDatas": JSON.stringify(orderDatas),       // 订单的其他信息(每个客户不一样)
        "desc": desc                // 订单描述  电费充值 固定文字
    }
    app.func.iotReq("/api/order/get", params, function (result) {
      var successs = result.status == 1 ? true : false;
      if (successs) {
        var info = result.info
        console.log("=========创建订单号成功,订单号为:"+info.orderNo)
        //获取支付签名
        that.orderGetSign(info.orderNo)
      }
    })
  },
  // 获取支付签名
  orderGetSign (orderNo) {
    console.log("==========准备获取支付签名============")
    var that = this;
    var token = wx.getStorageSync('token')
    var openId = wx.getStorageSync('openId')
    that.setData({
      "orderNo": orderNo,
    })
    var params = {
      "sysCustomer": sysConfig.app.sysCustomer,
      "token":token,                 // 登录标识 必填
      "orderNo": orderNo,           // 订单号
      "payType": 'wx_xcx',          // 支付方式 wx_xcx
      "wxOpenId": openId                // 用户的openId
    }
    app.func.iotReq("/api/order/get-sign", params, function (result) {
      var successs = result.status == 1 ? true : false;
      console.log("=====result.state:"+result.status)
      console.log("=====result.info:"+result.info)
      if (successs) {
        var sign = result.info
        console.log("==============获取支付签名成功,支付签名为:"+sign)
        // 调起支付
        that.requestPayment(sign)

      }
    })
  },
  // 调起支付
  requestPayment (sign) {
    console.log("===========开始调起支付==============")
    var that= this
    let info;
    if (util.isNotNull(sign)) {
      if(typeof sign == String){
         info = JSON.parse(sign);
      }else{
         info = sign;
      }
      wx.requestPayment({
        'timeStamp': info.timeStamp + '',
        'nonceStr': info.nonceStr,
        'package': info.package,
        'signType': info.signType,
        'paySign': info.paySign,
        'success': function (res) {
          // 支付成功后回调
          that.orderPayNotify()
        },
        'fail':function (res) {
          util.showToast('支付失败!')
          setTimeout(function(){
           // 返回上一页
            wx.navigateBack({
              delta: 1
            })
          },500)
        }
      })
    }

  },
  // 支付成功后回调
  orderPayNotify () {
    var that = this;
    var params = {
      "sysCustomer":sysConfig.app.sysCustomer,
      "orderNo": that.data.orderNo,           // 订单号
    }
    app.func.iotReq("/api/order/pay-notify", params, function (result) {
      var successs = result.status == 1 ? true : false;
      if (successs) {
        util.showToast('支付成功!')
        // 通过商户号/表具号获取信息
        that.queryMeterRealData()
      }
    })
  },
  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  }
})


5.小结

1.通过 java httClient 工具包 得到的HttpClient在执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。简化开发步骤,构造专属微信支付接口的httpClient 。
2. 微信小程序的支付流程: (1)先创建业务订单号 (2)请求微信小程序JSAPI 接口生成预支付订单号 prepay_id (3) 从小程序客户端 发起请求调起支付 (4)微信平台根据商户后台预留的notify_url 回调地址,商户后台处理支付完成后的业务逻辑(修改订单状态为已完成,给微信后台返回支付状态等)(5)商户后台定时任务轮询订单支付状态,并修改业务数据

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值