我想在我自己的系统中加入微信支付功能,原来这么简单!!!

news2024/10/5 13:00:22

微信支付功能实现

在这里插入图片描述

一、创建SpringBoot项目

  我们首先创建一个基本的SpringBoot项目。添加相关的依赖。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <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>
            <scope>test</scope>
        </dependency>
    </dependencies>

  然后引入Swagger。目的是自动生成接口文档和测试页面

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

  然后添加Swagger的配置文件。创建对应的config包和对应的Swagger2Config配置类

@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder().title("微信支付接口文档").build());
    }
}

创建测试接口

@RestController
public class WebController {

    @ApiOperation("测试接口")
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

swagger中的两个核心注解要注意:

  • @Api(tags=‘xxx’) 作用在类上
  • @ApiOperation(‘xxxx’) 作用在方法上

启动服务后。访问:http://localhost:8080/swagger-ui.html 测试即可

image.png

引入lombok依赖。简化实体类的开发

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

二、集成MyBatisPlus

  本案例中还是有些数据需要持久化到数据库中。这块我们通过MyBatisPlus来实现处理。先添加相关的Maven依赖:

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

然后添加对应的数据库配置信息

spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/payment_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true&nullCatalogMeansCurrent=true
spring.datasource.username=root
spring.datasource.password=123456

mybatis-plus.mapper-locations=classpath:mapper/*.xml

然后通过MyBatis的自动代码生成器来生成相关的模板代码

DROP TABLE IF EXISTS `t_order_info`;

CREATE TABLE `t_order_info` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(32) DEFAULT NULL COMMENT '订单标题',
  `orderNo` varchar(64) DEFAULT NULL COMMENT '商户订单编号',
  `userId` int DEFAULT NULL COMMENT '用户id',
  `productId` int DEFAULT NULL COMMENT '支付产品id',
  `totalFee` int DEFAULT NULL COMMENT '订单金额(分)',
  `codeUrl` varchar(128) DEFAULT NULL COMMENT '订单二维码链接',
  `orderStatus` varchar(32) DEFAULT NULL COMMENT '订单状态',
  `createTime` datetime DEFAULT NULL COMMENT '创建时间',
  `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `t_order_info` */

/*Table structure for table `t_payment_info` */

DROP TABLE IF EXISTS `t_payment_info`;

CREATE TABLE `t_payment_info` (
  `id` int NOT NULL AUTO_INCREMENT,
  `orderNo` varchar(64) DEFAULT NULL COMMENT '商品订单编号',
  `transactionId` varchar(64) DEFAULT NULL COMMENT '支付系统交易编号',
  `paymentType` varchar(32) DEFAULT NULL COMMENT '支付类型',
  `tradeType` varchar(32) DEFAULT NULL COMMENT '交易类型',
  `tradeState` varchar(32) DEFAULT NULL COMMENT '交易状态',
  `payerTotal` int DEFAULT NULL COMMENT '支付金额(分)',
  `content` varchar(64) DEFAULT NULL COMMENT '通知参数',
  `createTime` datetime DEFAULT NULL COMMENT '创建时间',
  `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `t_payment_info` */

/*Table structure for table `t_product` */

DROP TABLE IF EXISTS `t_product`;

CREATE TABLE `t_product` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(32) DEFAULT NULL,
  `price` int DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;



/*Table structure for table `t_refund_info` */

DROP TABLE IF EXISTS `t_refund_info`;

CREATE TABLE `t_refund_info` (
  `id` int NOT NULL AUTO_INCREMENT,
  `orderNo` varchar(64) DEFAULT NULL COMMENT '商品订单编号',
  `refundNo` varchar(64) DEFAULT NULL COMMENT '退款单编号',
  `refundId` varchar(64) DEFAULT NULL COMMENT '支付系统退款单号',
  `totalFee` int DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(64) DEFAULT NULL COMMENT '退款原因',
  `refundStatus` varchar(32) DEFAULT NULL COMMENT '退款单状态',
  `contentReturn` varchar(64) DEFAULT NULL COMMENT '申请退款返回参数',
  `contentNotify` varchar(128) DEFAULT NULL COMMENT '退款结果通知参数',
  `createTime` datetime DEFAULT NULL COMMENT '创建时间',
  `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
        <!-- MyBatisPlus 代码生成器 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- 在MyBatisPlus的代码生成器中我们需要导入 freemarker的依赖 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

代码生成器的相关代码

public class MyFastGeneratorConfiguration {
    public static void main(String[] args) {

        FastAutoGenerator.create("jdbc:mysql://localhost:3306/payment_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true"
                , "root", "123456")
                .globalConfig(builder -> {
                    builder.author("boge") // 设置作者
                            //.enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D://pay"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.boge") // 设置父包名
                            .moduleName("pay") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "D://pay")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("t_order_info","t_payment_info","t_product","t_refund_info") // 设置需要生成的表名
                            .addTablePrefix("t_"); // 设置过滤表前缀

                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();

    }
}

image.png

最后添加扫描的路径

image.png

三、添加支付页面

  导入我们提前准备好的支付页面,具体的代码在附近中

image.png

四、定义相关枚举类型

  在支付案例中我们会涉及到各种类型的使用。所以我们会定义各种类型来应用。

支付类型:

@AllArgsConstructor
@Getter
public enum PayType {
    /**
     * 微信
     */
    WXPAY("微信"),


    /**
     * 支付宝
     */
    ALIPAY("支付宝");

    /**
     * 类型
     */
    private final String type;
}

订单状态:

@AllArgsConstructor
@Getter
public enum OrderStatus {
    /**
     * 未支付
     */
    NOTPAY("未支付"),


    /**
     * 支付成功
     */
    SUCCESS("支付成功"),

    /**
     * 已关闭
     */
    CLOSED("超时已关闭"),

    /**
     * 已取消
     */
    CANCEL("用户已取消"),

    /**
     * 退款中
     */
    REFUND_PROCESSING("退款中"),

    /**
     * 已退款
     */
    REFUND_SUCCESS("已退款"),

    /**
     * 退款异常
     */
    REFUND_ABNORMAL("退款异常");

    /**
     * 类型
     */
    private final String type;
}

微信Native下单接口

@AllArgsConstructor
@Getter
public enum WxApiType {

	/**
	 * Native下单
	 */
	NATIVE_PAY("/v3/pay/transactions/native"),

	/**
	 * Native下单
	 */
	NATIVE_PAY_V2("/pay/unifiedorder"),

	/**
	 * 查询订单
	 */
	ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

	/**
	 * 关闭订单
	 */
	CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

	/**
	 * 申请退款
	 */
	DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

	/**
	 * 查询单笔退款
	 */
	DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

	/**
	 * 申请交易账单
	 */
	TRADE_BILLS("/v3/bill/tradebill"),

	/**
	 * 申请资金账单
	 */
	FUND_FLOW_BILLS("/v3/bill/fundflowbill");


	/**
	 * 类型
	 */
	private final String type;
}

微信支付通知相关接口

@AllArgsConstructor
@Getter
public enum WxNotifyType {

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY("/api/wx-pay/native/notify"),

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),


	/**
	 * 退款结果通知
	 */
	REFUND_NOTIFY("/api/wx-pay/refunds/notify");

	/**
	 * 类型
	 */
	private final String type;
}

退款相关状态

@AllArgsConstructor
@Getter
public enum WxRefundStatus {

    /**
     * 退款成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 退款关闭
     */
    CLOSED("CLOSED"),

    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING"),

    /**
     * 退款异常
     */
    ABNORMAL("ABNORMAL");

    /**
     * 类型
     */
    private final String type;
}

支付相关的状态

@AllArgsConstructor
@Getter
public enum WxTradeState {

    /**
     * 支付成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 未支付
     */
    NOTPAY("NOTPAY"),

    /**
     * 已关闭
     */
    CLOSED("CLOSED"),

    /**
     * 转入退款
     */
    REFUND("REFUND");

    /**
     * 类型
     */
    private final String type;
}

五、基础支付API V3

1.引入相关参数

  在项目的resources目录中创建wxpay.properties文件。并在其中定义相关的支付属性.

# 微信支付相关参数
# 商户号
wxpay.mch-id=1640588*****
# 商户API证书序列号
wxpay.mch-serial-no=36E5C0892B813B5B78BC44CFB90******
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=Y7CgFTppIyzzOXLm3RU1IFS*****
# APPID
wxpay.appid=wx1d4eab9******
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://b513049e52.zicp.fun

# APIv2密钥
wxpay.partnerKey: xxxxxxxxxxxxxxx

2.读取支付参数

  我们在配置文件中配置的数据。系统启动的时候还是需要加载到内存中的。为了便于管理。我们创建一个WxPayConfig这个配置文件。来存储对应的配置信息

@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {

    // 商户号
    private String mchId;

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

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

    // APIv2密钥
    private String partnerKey;



}

对应的我们需要添加对应的依赖信息

        <!-- 生成自定义配置的元数据信息 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

然后我们简单测试下是否能够读取到对应的信息:

@Controller
public class WebController {

    @Autowired
    private WxPayConfig wxPayConfig;

    @GetMapping({"/index","/"})
    public String index(){
        System.out.println(wxPayConfig.getAppid());
        System.out.println(wxPayConfig.getMchId());
        return "index";
    }

}

重启服务后访问。可以看到获取到了相关的配置信息

image.png

3.加载商户私钥

3.1 复制商户私钥

  把我们前面下载的私钥文件复制到项目的根目录下:

image.png

3.2 引入SDK

  我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证:

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml

image.png

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.9</version>
</dependency>

3.3 获取商户私钥

    /**
     * 获取商户的私钥文件
     * @param filename
     * @return
     */
    public PrivateKey getPrivateKey(String filename){
        try {
            return PemUtil.loadPrivateKey(getClass().getResourceAsStream("/"+filename));
        } catch (RuntimeException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

3.4 测试获取私钥

  我们在测试的时候可以把上面的方法设置为public。

    @Autowired
    private WxPayConfig wxPayConfig;

    @GetMapping({"/index","/"})
    public String index(){
        System.out.println(wxPayConfig.getAppid());
        System.out.println(wxPayConfig.getMchId());
        System.out.println("------获取私钥信息------");
        PrivateKey privateKey = wxPayConfig.getPrivateKey(wxPayConfig.getPrivateKeyPath());
        System.out.println(privateKey);
        return "index";
    }

效果如下:

image.png

4.获取签名验证器和HttpClient

4.1 证书秘钥使用说明

微信支付-开发者文档 (qq.com)

4.2 获取签名验证器

签名验证器:(定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。

    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier(){
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
        // 使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));
        return verifier;
    }

4.3 获取HttpClient对象

HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。

    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

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

        return httpClient;
    }

5. API字典和相关工具

5.1 API列表

我们的项目中要实现以下所有API的功能。

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml

image.png

5.2 接口规则

基本规则-接口规则 | 微信支付商户平台文档中心 (qq.com)

微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。

image.png

5.3 添加工具类

  将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发

image.png

6. Native下单API

6.1 Native支付流程

  要完成Native下单我们需要先搞清楚Native的完整流程。这个在官网有详细的介绍:微信支付-开发者文档 (qq.com)

业务流程说明:

(1)商户后台系统根据用户选购的商品生成订单。

(2)用户确认支付后调用微信支付【Native下单API】生成预支付交易;

(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。

(4)商户后台系统根据返回的code_url生成二维码。

(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。

(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。

(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。

(8)微信支付系统根据用户授权完成支付交易。

(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。

(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。

(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。

(12)商户确认订单已支付后给用户发货。

6.2 Native下单API

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

  商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户
端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。

处理的核心代码:

    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception{
//生成订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTitle("test");
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
        orderInfo.setProductId(productId.intValue());
        orderInfo.setTotalFee(1); //分
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
        //TODO:存入数据库
        //调用统一下单API
        HttpPost httpPost = new
                HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        paramsMap.put("notify_url",
                wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
        Map amountMap = new HashMap();
        amountMap.put("total", orderInfo.getTotalFee());
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
//将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功");
            } else {
                log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " +
                        bodyAsString);
                throw new IOException("request failed");
            }
            //响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString,
                    HashMap.class);
            //二维码
            String codeUrl = resultMap.get("code_url");
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        } finally {
            response.close();
        }

    }

运行后的提示报错:

image.png

登录商户平台发下Native支付为开通。我们需要开通改服务

image.png

申请开通:

image.png

然后我们再通过PostMan来测试访问:

image.png

在控制台也可以看到成功的信息

image.png

6.3 二维码展示

  上面响应返回了对应的二维码地址。我们还需要把这个内容以图片的方式展示给客户。但是我们没有办法直接通过这个url地址来生成二维码图片,我们需要使用第三方库将 code_url 转化为二维码图片,例如 qrcode 库。

  QRCode库是一个用于生成和解析二维码的开源库,它支持多种编程语言,如Java、Python、C++等。该库提供了丰富的API,可以用于生成不同大小、颜色和格式的二维码。同时,它还支持错误校验和纠正,可以确保生成的二维码在有损情况下仍然可读。

QRCode库的主要功能包括:

  1. 生成二维码:可以生成不同大小、颜色和格式的二维码,支持自定义错误校验和纠正。
  2. 解析二维码:可以解析已有的二维码并获取其中的信息。
  3. 自定义样式:可以自定义二维码的样式,如颜色、背景图片等。
  4. 支持多种编程语言:支持多种编程语言,如Java、Python、C++等。

QRCode库的使用非常简单,只需要导入库并调用相应的API即可。由于其开源的特性,用户也可以根据自己的需要对其进行二次开发。

导入相关的依赖:

<!--        生成二维码-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.3.0</version>
        </dependency>

然后创建相关的测试代码

static final String BASEPATH = "D://weixin";
  
    public static void generateQRCodeImage(String text, int width, int height, String fileName)
            throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);

        File file = new File(BASEPATH);
        if(!file.exists()){
            file.mkdir();
        }
        Path path = FileSystems.getDefault().getPath(BASEPATH+"/"+fileName);
        MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
    }

    public static void main(String[] args) {
        try {
            generateQRCodeImage("weixin://wxpay/bizpayurl?pr=YC9gpgMzz", 350, 350, "QRTest.png");
        } catch (WriterException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

image.png

在对应的controller中调用生成即可

@ResponseBody
    @PostMapping("/native/{productId}")
    public Map<String,Object> nativePay(@PathVariable Long productId) throws Exception{
        Map<String, Object> map = wxPayService.nativePay(productId);
        // 把文件存储在 D://weixin/orderId.png
        WxPayConfig.generateQRCodeImage(map.get("codeUrl").toString(),350, 350,map.get("orderNo")+".png");
        return map;
    }

我们先提供一个下载二维码图片的功能

@GetMapping("/download")
    public void downloadImg(String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 加载需要下载的文件
        InputStream in = new FileInputStream(WxPayConfig.BASE_PATH+"/"+fileName);
        int size = in.available();
        byte[] data = new byte[size];
        in.read(data);
        in.close();
        // 把读取的数据响应给客户端
        response.setContentType("image/jpg");
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(data);
        outputStream.flush();

    }

然后在页面中点击支付弹出对应的支付二维码信息

    function goPay(){
        $.post("/api/wx-pay/native/1001",function(data) {
            console.log(data.orderNo);
            $("#payImg").attr("src","/api/wx-pay/download?fileName="+data.orderNo+".png")
            $(".mask").show();
            $("body").css({overflow: "hidden"});
            // 检查是否已经存在倒计时定时器,避免重复启动
            if (!countdownInterval) {
                startCountdown();
            }
        })
    }

具体的效果如下:

image.png

支付成功的截图

image.png

还有重复支付的截图

image.png

7.签名生成和验签

7.1 签名生成

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml

上面的生成订单展示对应的支付二维码是如下的完整的流程

image.png

那么在这个流程中设计到了签名的生成和服务端的签名验证:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

image.png

签名的流程:

  1. 构造签名串:签名是密文,那么这个签名串就是这个密文对应的明文
  2. 计算签名值:对签名串加密,通过特定的规则加密
  3. 将签名发送给微信服务器:通过在http的请求头中传递

源码层面的逻辑:SignatureExec中的executeWithSignature方法中

image.png

然后进入到getToken中

@Override
    public final String getToken(HttpRequestWrapper request) throws IOException {
        String nonceStr = generateNonceStr(); // 获取随机值
        long timestamp = generateTimestamp(); // 获取时间戳

        String message = buildMessage(nonceStr, timestamp, request); // 构建签名串
        log.debug("authorization message=[{}]", message);
		// 获取签名信息
        Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
		// 组装签名信息
        String token = "mchid=\"" + getMerchantId() + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + signature.certificateSerialNumber + "\","
                + "signature=\"" + signature.sign + "\"";
        log.debug("authorization token=[{}]", token);

        return token;
    }

7.2 签名验证

  商户可以按照下述步骤验证应答或者回调的签名。

如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。

同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

获取平台证书

微信支付API v3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载

Get方法:https://api.mch.weixin.qq.com/v3/certificates

image.png

在具体的代码中。我们在系统启动的时候需要加载微信的证书列表

image.png

image.png

设置更新的频率是60分钟

image.png

处理的核心代码

image.png

image.png

image.png

签名验证

做超时时间处理

image.png

验证签名的逻辑

image.png

image.png

image.png

Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名

image.png

image.png

image.png

8.内网穿透

  在用户支付完成后。微信服务的会调用我们本地服务来做支付的通知。这时就需要让我们本地的服务可以被微信的服务端访问到。这时需要利用内网穿透的方式来解决。 http://ngrok.com .下载对应操作系统的工具

image.png

然后下载对应的客户端段。解压缩后在对应的目录下打开cmd窗口。

image.png

然后我们利用对应的地址访问即可

9.支付通知

  用户支付成功后。微信服务器会回调我们在发起支付的时候传递的回调通知的地址

Map<String,Object> map = new HashMap<>();
        map.put("appid",wxPayConfig.getAppid());
        map.put("mchid",wxPayConfig.getMchId());
        map.put("description",orderInfo.getTitle());
        map.put("out_trade_no",orderInfo.getOrderNo());
        map.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

那么我们需要修改在配置文件中的通知地址为我们内网穿透设置的地址

# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://c65f-183-215-31-250.ngrok-free.app

然后就可以定义对应的回调接口了

 /**
     * 微信服务的通知支付结果的方法
     * @param request
     * @param response
     * @return
     */
    @ResponseBody
    @PostMapping("/native/notify")
    public String nativePayNotify(HttpServletRequest request,HttpServletResponse response){
        // 1.获取微信服务器发送的通知内容  读取的内容是一个JSON格式的字符串
        String body = HttpUtils.readData(request);
        Gson gson = new Gson();
        Map<String,Object> bodyJson = gson.fromJson(body,HashMap.class);
        System.out.println("响应数据的ID===》"+bodyJson.get("id"));
        System.out.println("响应的JSON数据===》"+body);
        // 1.1 签名验证

        // 1.2 更新订单信息

        // 2.响应成功应答给微信服务器
        response.setStatus(200);
        Map<String,String> map = new HashMap<>();
        map.put("code","SUCCESS");
        map.put("message","成功");
        return gson.toJson(map);
    }

然后当我们支付成功后。就可以看到下面的打印信息了。说明接口实现是成功。

响应数据的ID===》379de717-0f49-5f76-8fda-fd91688642e5
响应的JSON数据==={"id":"379de717-0f49-5f76-8fda-fd91688642e5","create_time":"2023-06-27T20:43:44+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"t96vhF2Z/VptyJ8m+3PUtxp5RQcDcqezxgV/K/Ik4qJtFjO80TVDDbsrGaCCmJKbN9eV997vgkxOhjeDPt4SN09JPAiK7cQitlcvY9BsSMvji+fFoigbP0sae/4dSccW12g/DCXA3MMgPItRuy2Tq84rvgPcGF/fSZ6ZFDEQ4lLQpAO3Dk7RPYFeDJEhYRtC9FFjuM4J2voSg8O3Rg+VarHne0Hou14JZoPep0Q1Zdi3j34kULHPNIHLLrxf7bWUgtM0aCv2fjscqPaIH9CVB8bih9l30xjxrlIbolziuwY8bo45oT/N6z9mM5Dxb6glga3AQOrJkFKmKOYSigHo/jqRumpnCD024wlfBYtDRrJh8/n3gVlf1JguLzWcA5Ed9EAsShCxBt9Zb4/DqQt1LaFU9bivNV1T4Jpi2+LG55zNR/0fygG5Xi/MLAcjwdt7TGfqjeHStKZnTDkw7pcrri/svKGooZ2rJCr4APmd27OO/TX6P3ks1GmzRoaJI4WL85VaiUDrix889ngRjNetoU4FolaCrulg+TxHKX+AlBDvO2ajTFhIafd7MOZ4XaFDMEXczFn5cg==","associated_data":"transaction","nonce":"mrOQHEfn99Qw"}}
响应数据的ID===》af5afe61-af37-508d-9e04-5d27094ac30d
响应的JSON数据==={"id":"af5afe61-af37-508d-9e04-5d27094ac30d","create_time":"2023-06-27T20:44:57+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"FeE/+/rD0SqNKXTCMLMoWnms0deI6VnQzm15N2jUzvJoJ4RN5DvOiXpoqEX5XQ2+o+jd5ho/h4EZvsWP4Cb+x9/6MbkRu5IVVRMbNXI+teMpv3yMi++vqoYwutwYc7K31bv6AeofH921U0kzBdFpOO9YtSogSaVHA6UlXxleyKdmXulSH1e2QzC9vAzzh/pxWgFWfu3+bJ/MmucWrVMmpCb0iWKBgUMU8V1HFTcyeofm9GDoMk1SjffiA+USL3jSxxql6pP+4j9DfcpShmj27qROO8c/Tzcgwi+Oy3jdPjWnvkHHHk2oozcLzqrc7cWHxmcXfvfvQ56RqEXQL5wJZHZe3kteL7TH/bZonnS7ZPrYLHGeQCy5N1zRX/6wH/jAl7y2IwAocIVSZgsrgTOIElM9UncqpxYZn1pL0fywEkmHHNgqHVphyuMtPI3PBkKSiHFl8jlT2WbTLe8eTHIL+6Lsp6h/KvzGjkdKJaR0MZRMUxSwLun+99u+zDAnKsgKpJV41XwXZc9k1MD0L8c2nMTI+9Z4OYwZ7T+m8V6Gf+rIe76mLtJNw1QFhTmyVFKDlZYfIHVdtw==","associated_data":"transaction","nonce":"rvBWUUlQaVLh"}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/719738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

php宝塔搭建EMLOG站长工具箱网站自适应PC手机端php源码

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。本期给大家带来一套站长工具箱网站自适应PC手机端php源码。感兴趣的朋友可以自行下载学习。 技术架构 PHP5.6 nginx mysql5.6 JS CSS HTMLcnetos7以上 宝塔面板 文字搭建教程 下载源码&#xff0c;宝塔添…

SciencePub学术 | 数据处理类重点SCIEEI征稿中

SciencePub学术 刊源推荐: 数据处理类重点SCI&EI征稿中&#xff01;信息如下&#xff0c;录满为止&#xff1a; 一、期刊概况&#xff1a; 数据处理类重点SCIE&EI 【期刊简介】IF&#xff1a;3.5-4.0&#xff0c;JCR2区&#xff0c;中科院4区&#xff1b; 【出版社】…

CSS选择器常见用法

目录 一.总体分类 二.基础选择器 &#xff08;1&#xff09;标签选择器 &#xff08;2&#xff09;类选择器 &#xff08;3&#xff09;id选择器 &#xff08;4&#xff09;通配符选择器&#xff08;特殊&#xff09; 三.复合选择器 &#xff08;1&#xff09;后代选择器 &…

注意看!!!Linux中Ubuntu22.04之SVN的三种配置方式【详解~】

注意看&#xff01;&#xff01;&#xff01;Linux中Ubuntu22.04之SVN的三种配置方式【详解&#xff5e;】 配置目录大全<1>I、 安装&#xff08;易上手&#x1f446;&#xff09;II、使用SVN&#xff08;简单易操作&#xff09;III、配置SVN&#xff08;精通&#xff09…

一个事务回滚问题的解决

同事遇见一个事务回滚的问题&#xff0c;从controller提交的时候出现&#xff0c;transaction marked as rollbackonly 这个错误。 从调用栈上来看&#xff0c;没啥可用的信息&#xff0c;另外基本没有啥用户代码&#xff0c;都是框架JPA的代码 这个事其实以前遇见过类似的&…

Spring Boot 中的健康检查是什么, 如何使用

Spring Boot 是一个非常流行的 Java Web 开发框架&#xff0c;它提供了许多方便的功能&#xff0c;其中之一就是健康检查。健康检查是一种用来确保应用程序和其所依赖的服务的状态正常的机制。在本文中&#xff0c;我们将探讨 Spring Boot 中的健康检查是什么&#xff0c;以及如…

货损、灭失、延迟配送怎么办?打破这几个点才是关键

物流行业离不开讨论的一个核心话题就是物流质损。不管是在货物的运输、存储还是配送环节&#xff0c;都有可能发生货物损坏的情况。 要降低货物质损的风险&#xff0c;有两个基本因素需要考虑&#xff1a;距离长短和环节次数。距离越短&#xff0c;货物摇晃挤压影响就越少&…

odoo 集成 Minio

将Odoo与MinIO集成的优点包括以下几点&#xff1a;1、可扩展性&#xff1a;MinIO是一个高性能、可扩展的对象存储服务&#xff0c;可以 轻松处理大规模的数据存储和访问需求。通过将Odoo与MinIO集成&#xff0c;您可以有效地扩展和管理您的数据存储。2、弹性存储&#xff1a;Mi…

【数据结构课程设计系列】图书管理系统操作演示

图书管理系统操作演示 随着计算机技术的进步和人们对系统需求的进一步提高&#xff0c;学校对于图书馆信息管理也相应的提升了。学校对于图书馆信息管理主要侧重于数据的更新快捷、准确、占用较少的人力资源&#xff0c;而达到最大的办公效率。 本次设计所解决的主要问题就是如…

ModaHub魔搭社区:Zilliz Cloud 数据迁移,数据的备份和恢复

目录 01.从 Milvus 到 Zilliz Cloud&#xff0c;轻点鼠标即可实现无缝迁移 02.掌握数据库的备份和恢复&#xff0c;让明天没有意外 01. 从 Milvus 到 Zilliz Cloud&#xff0c;轻点鼠标即可实现无缝迁移 越来越多的用户选择将数据从 Milvus 迁移至 Zilliz Cloud&#xff0c;通…

【网络安全带你练爬虫-100练】第2练:爬取指定位置数据

目录 一、思路 二、工具 三、代码处理 第一部分&#xff1a;发起请求接收响应&#xff08;不过多讲&#xff09; 第二部分&#xff1a;解析HTML页面提取数据 第三部分&#xff1a;处理数据 一、思路 分解步骤&#xff0c;化繁为简 爬虫分为五步走&#xff1a; 发起HTTP…

极智项目 | 实战TensorRT部署DETR

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多经验分享 大家好&#xff0c;我是极智视界&#xff0c;本文来介绍 实战TensorRT部署DETR。 本文介绍的实战 TensorRT 部署 DETR&#xff0c;提供完整的可以一键执行的项目工程源码&#xff0c;获取方式有两个&#xff…

淘宝APP商品详情接口(商品信息,价格销量,优惠券信息,详情图等)

淘宝APP商品详情接口&#xff08;商品信息接口&#xff0c;价格销量接口&#xff0c;优惠券信息接口&#xff0c;详情图接口等&#xff09;代码对接如下&#xff1a; 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;&#xff…

java jvm什么是记忆集,卡表?

记忆集 &#xff1a; 是一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构 。如果我们不考虑 效率和成本的话&#xff0c;最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结 构 记忆集作用 &#xff1a;解决对象跨代引用所带来的问题&a…

Rdkit|分子输出

Rdkit|分子输出 Github&#xff1a; 地址 输出SMILES/SMARTS 输出SMILES&#xff1a;MolToSmiles(mol, isomericSmiles, kekuleSmiles, canonical, …) kekuleSmiles&#xff1a;默认False&#xff0c;不使用kekule时&#xff1a;脂肪族碳用"C"表示&#xff08;大…

分布式运用——rsync远程同步

一、rsync的背景和原理 rsync&#xff08;Remote Sync&#xff0c;远程同步&#xff09;是由Andrew Tridgell于1996年开发的一款开源软件。 是一个开源的快速备份工具&#xff0c;可以在不同主机之间镜像同步整个目录树&#xff0c;支持增量备份&#xff0c;并保持链接和权限&a…

笔试强训第33天

目录 剪花布条 客似云来 剪花布条 #include <iostream> #include <string>using namespace std;int main() {string s,t;while(cin>>s>>t){int ans 0;while(s.find(t) ! string::npos){s.erase(s.find(t), t.size());ans;}cout<<ans<<en…

Codeforces Round 877 (Div. 2) A-E

题目链接&#xff1a;Dashboard - Codeforces Round 877 (Div. 2) - Codeforces A - Blackboard List 解题思路&#xff1a;因为取的是绝对值&#xff0c;所以有负数肯定取负数&#xff0c;没负数就取最大值。 #include <bits/stdc.h> using namespace std; const int…

基于PyQt5的桌面图像调试仿真平台开发(7)伽马矫正

系列文章目录 基于PyQt5的桌面图像调试仿真平台开发(1)环境搭建 基于PyQt5的桌面图像调试仿真平台开发(2)UI设计和控件绑定 基于PyQt5的桌面图像调试仿真平台开发(3)黑电平处理 基于PyQt5的桌面图像调试仿真平台开发(4)白平衡处理 基于PyQt5的桌面图像调试仿真平台开发(5)…