从零玩转系列之微信支付实战基础框架搭建

一、前言

halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序进行关联,至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)

在此之前已经更新了微信支付开篇微信支付安全、本次更新为项目实战篇,实战篇分为几个章节因为代码量确实有点多哈.

本次项目使用技术栈

后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus

前端: Vue3、Vite、ElementPlus

小程序: Uniapp、Uview

二、项目创建

1. 创建SpringBoot项目(wx-play-demo) 3.x (这是我的版本) 使用JDK 17 该博文案例已上传到 gitee 仓库

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.yby6</groupId>
    <artifactId>wx-play-demo-blog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>wx-play-demo-blog</name>
    <description>wx-play-demo-blog</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- sql性能分析插件 -->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>

        <!--   可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

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

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

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <!-- 微信 APIv3 SDK 最新版本 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.3.0</version>
        </dependency>


        <!--微信支付 APIv2 SDK-->
        <dependency>
            <groupId>com.github.wxpay</groupId>
            <artifactId>wxpay-sdk</artifactId>
            <version>0.0.3</version>
        </dependency>

        <!--  让支付更加轻盈 -->
        <dependency>
            <groupId>com.github.javen205</groupId>
            <artifactId>IJPay-WxPay</artifactId>
            <version>2.9.6</version>
        </dependency>

        <!-- 支付宝支付 -->
        <dependency>
            <groupId>com.github.javen205</groupId>
            <artifactId>IJPay-AliPay</artifactId>
            <version>2.9.6</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.15</version>
        </dependency>

        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.5.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2. 创建数据库 payment_demo

/*
 杨不易呀: https://yby6.com
 Date: 21/05/2023 22:28:20
*/
DROP DATABASE payment_demo_blog;

create database payment_demo_blog;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
use payment_demo_blog;
-- ----------------------------
-- Table structure for t_order_info
-- ----------------------------
DROP TABLE IF EXISTS `t_order_info`;
CREATE TABLE `t_order_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
  `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
  `code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
  `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=172 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for t_payment_info
-- ----------------------------
DROP TABLE IF EXISTS `t_payment_info`;
CREATE TABLE `t_payment_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
  `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
  `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
  `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
  `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
  `content` text COMMENT '通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for t_product
-- ----------------------------
DROP TABLE IF EXISTS `t_product`;
CREATE TABLE `t_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商Bid',
  `title` varchar(20) DEFAULT NULL COMMENT '商品名称',
  `price` int(11) DEFAULT NULL COMMENT '价格(分)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for t_refund_info
-- ----------------------------
DROP TABLE IF EXISTS `t_refund_info`;
CREATE TABLE `t_refund_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '款单id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
  `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
  `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
  `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
  `content_return` text COMMENT '申请退款返回参数',
  `content_notify` text COMMENT '退款结果通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (1, 'Java课程', 1, '2022-11-13 14:31:00', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (2, '大数据课程', 1, '2022-11-13 14:31:00', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (3, '前端课程', 1, '2022-11-13 14:31:00', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (4, 'UI程', 1, '2022-11-13 14:31:00', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (5, '随机课程', 1, '2022-11-17 04:22:35', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (6, '自定义课程', 1, '2022-11-17 04:23:54', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (7, '全网课程', 1, '2022-11-18 02:52:30', '2023-05-19 10:52:49');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (8, '测试支付', 1, '2023-05-19 10:52:05', '2023-05-19 10:52:05');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (9, 'Java课程', 10, '2023-05-19 10:52:56', '2023-05-19 10:53:09');
INSERT INTO `payment_demo_blog`.`t_product` (`id`, `title`, `price`, `create_time`, `update_time`) VALUES (10, '大数据课程', 20, '2023-05-19 10:52:56', '2023-05-19 10:53:11');

image-20230610001216554

3.创建yml配置

server:
  port: 9080

spring:
  application:
    name: wx-play-demo-blog
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://127.0.0.1:3306/payment_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
    username: xxxxxxxxxxxxx
    password: xxxxxxxxx
  servlet:
    multipart:
      max-file-size: 200MB  #设置单个文件的大小  因为springboot内置tomact的的文件传输默认为10MB
      max-request-size: 500MB   #设置单次请求的文件总大小
      enabled: true    #千万注意要设置该参数,否则不生效
  mvc:
    format:
      date-time: yyyy-MM-dd HH:mm:ss
  #jackson配置
  jackson:
    # 日期格式化
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
      # 格式化输出
      INDENT_OUTPUT: false
      # 忽略无法转换的对象
      fail_on_empty_beans: false
    deserialization:
      # 允许对象忽略json中不存在的属性
      fail_on_unknown_properties: false

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/*.xml

logging:
  level:
    root: info

三、创建配置文件

1.项目当中创建config目录存放配置文件

创建数据填充配置

package com.yby6.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 自动填充
 * @author Yang Shuai
 **/
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "createTime", Date::new, Date.class); // 起始版本 3.3.3(推荐)
        this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class); // 起始版本 3.3.3(推荐)
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class); // 起始版本 3.3.3(推荐)
    }
}

创建MyBatisPlusConfig配置

package com.yby6.config;

import cn.hutool.core.net.NetUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan("com.yby6.mapper")
@EnableTransactionManagement(proxyTargetClass = true)
public class MyBatisPlusConfig {


    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

    /**
     * 分页插件,自动识别数据库类型
     */
    public PaginationInnerInterceptor paginationInnerInterceptor() {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInnerInterceptor.setMaxLimit(-1L);
        // 分页合理化
        paginationInnerInterceptor.setOverflow(true);
        return paginationInnerInterceptor;
    }

    /**
     * 乐观锁插件
     */
    public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
        return new OptimisticLockerInnerInterceptor();
    }

    /**
     * 元对象字段填充控制器
     */
    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MyMetaObjectHandler();
    }

    /**
     * 使用网卡信息绑定雪花生成器
     * 防止集群雪花ID重复
     */
    @Primary
    @Bean
    public IdentifierGenerator idGenerator() {
        return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
    }


}

创建 jackson配置序列化时间

package com.yby6.config;


import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

/**
 * jackson 配置
 *
 * @author Yang Shuai
 */
@Slf4j
@AutoConfiguration(before = JacksonAutoConfiguration.class)
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            // 全局配置序列化返回 JSON 处理
            JavaTimeModule javaTimeModule = new JavaTimeModule();
            javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
            javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
            javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
            javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
            builder.modules(javaTimeModule);
            builder.timeZone(TimeZone.getDefault());
            log.info("初始化 jackson 配置");
        };
    }

}

创建 JS精度配置

package com.yby6.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;

import java.io.IOException;

/**
 * 超出 JS 最大最小值 处理
 *
 * @author Yang Shuai
 */
@JacksonStdImpl
public class BigNumberSerializer extends NumberSerializer {

    /**
     * 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来
     */
    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
    private static final long MIN_SAFE_INTEGER = -9007199254740991L;

    /**
     * 提供实例
     */
    public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class);

    public BigNumberSerializer(Class<? extends Number> rawType) {
        super(rawType);
    }

    @Override
    public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        // 超出范围 序列化位字符串
        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
            super.serialize(value, gen, provider);
        } else {
            gen.writeString(value.toString());
        }
    }
}

创建枚举参数前往仓库下载吧我不想贴了累了兄弟们

https://gitee.com/yangbuyi/wxDemo/tree/master/src/main/java/com/yby6/enums

创建返回对象

package com.yby6.reponse;

import java.io.Serial;
import java.io.Serializable;

/**
 * 响应信息主体
 *
 * @author Yang Shuai
 */
public class R<T> implements Serializable {
    /**
     * 成功
     */
    public static final int SUCCESS = 200;
    /**
     * 失败
     */
    public static final int FAIL = 500;
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * 消息状态码
     */
    private int code;

    /**
     * 消息内容
     */
    private String msg;

    /**
     * 数据对象
     */
    private T data;

    public static <T> R<T> ok() {
        return restResult(null, SUCCESS, "操作成功");
    }

    public static <T> R<T> ok(T data) {
        return restResult(data, SUCCESS, "操作成功");
    }

    public static <T> R<T> ok(String msg) {
        return restResult(null, SUCCESS, msg);
    }

    public static <T> R<T> ok(String msg, T data) {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> fail() {
        return restResult(null, FAIL, "操作失败");
    }

    public static <T> R<T> fail(String msg) {
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> fail(T data) {
        return restResult(data, FAIL, "操作失败");
    }

    public static <T> R<T> fail(String msg, T data) {
        return restResult(data, FAIL, msg);
    }

    public static <T> R<T> fail(int code, String msg) {
        return restResult(null, code, msg);
    }

    public static <T> R<T> check(int row) {
        return row > 0 ? ok() : fail();
    }

    public static <T> R<T> check(boolean isTure) {
        return isTure ? ok() : fail();
    }


    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static <T> R<T> warn(String msg) {
        return restResult(null, 601, msg);
    }

    /**
     * 返回警告消息
     *
     * @param msg  返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static <T> R<T> warn(String msg, T data) {
        return restResult(data, 601, msg);
    }

    private static <T> R<T> restResult(T data, int code, String msg) {
        R<T> r = new R<>();
        r.setCode(code);
        r.setData(data);
        r.setMsg(msg);
        return r;
    }

    public static <T> Boolean isError(R<T> ret) {
        return !isSuccess(ret);
    }

    public static <T> Boolean isSuccess(R<T> ret) {
        return R.SUCCESS == ret.getCode();
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

创建MybatisExceptionHandler

package com.yby6.handler;

import com.yby6.reponse.R;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


/**
 * Mybatis异常处理器
 *
 * @author Yang Shuai
 */
@Slf4j
@RestControllerAdvice
public class MybatisExceptionHandler {

    /**
     * 主键或UNIQUE索引,数据重复异常
     */
    @ExceptionHandler(DuplicateKeyException.class)
    public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage());
        return R.fail("数据库中已存在该记录,请联系管理员确认");
    }

    /**
     * Mybatis系统异常 通用处理
     */
    @ExceptionHandler(MyBatisSystemException.class)
    public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        String message = e.getMessage();
        if (message.contains("CannotFindDataSourceException")) {
            log.error("请求地址'{}', 未找到数据源", requestURI);
            return R.fail("未找到数据源,请联系管理员确认");
        }
        log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
        return R.fail(message);
    }

}

创建 spy.properties

p6spy 性能分析插件配置文件 存放在resources目录下

# p6spy 性能分析插件配置文件
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
#deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# SQL语句打印时间格式
databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
# 是否过滤 Log
filter=true
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
exclude=SELECT 1

最终目录结构 微信配置暂时忽略

image-20230610011824379

4.idea链接mysql

image-20230610001615805

⚠️ 推荐一个插件一键生成CRUD

💰 MyBatisCodeHelperPro (Marketplace Edition)

image-20230610001807923

🔗 打开链接好的数据库管理选择到对应的表右击

image-20230610001932497

image-20230610002117688

🦐 下面的选项卡里面的配置

  1. lombok 勾选 @Data
  2. 定制默认方法可以全部去掉
  3. mybatisPlus配置勾选 mybatisPlus3 和 idType选择为 ASSIGN_ID
  4. 其他的你看着来吧....

✅ 直接确定生成了一个CRUD 接下来将后面的三个表也一样的操作吧

image-20230610002458491

👌 最终如此

image-20230610002613295

5. 微信配置-基础支付API V3

1.引入支付参数配置

  • 在resources当中创建wechat文件夹
  • 创建 wxpay.properties 复制到wechat当中
# 商户ID
wxpay.mch-id=xxxxx
# 商户平台号
wxpay.mch-serial-no=xxxxx
# 商户证书
wxpay.private-key-path=wechat/apiclient_key.pem

# 微信平台证书
wxpay.platformCertPath=wechat/wechatpay_2694B2E0B2719A25EE1063EE02CFC78EE035E8CF.pem
# 下载地址参考: https://github.com/wechatpay-apiv3/CertificateDownloader

# 商户api v3
wxpay.api-v3-key=xxxxx

# 小程序appid
wxpay.appid=xxxxx

# 小程序密钥
wxpay.secret=xxxxx

wxpay.domain=https://api.mch.weixin.qq.com

# 本地使用内网穿透代理回调
wxpay.notify-domain=https://x x x x x x x x x x x x/wx-api

# 商户apiv2
wxpay.partner-key=xxxxxxxxxxxxx
下载微信平台证书
java -jar CertificateDownloader.jar -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}

示例:

java -jar CertificateDownloader-1.2.0-jar-with-dependencies.jar 
-k=商户v3key 
-m=商户ID 
-f=/Users/yangbuyi/Documents/workPath/workPath/wx-play-demo/src/main/resources/apiclient_key.pem 
-s=商户证书序列号 
-o=/Users/yangbuyi/Documents/workPath/workPath/wx-play-demo/src/main/resources

解释:

java -jar CertificateDownloader-1.2.0-jar-with-dependencies.jar 
-k=这是对应配置文件当中的 APIV3Key(wxpay.api-v3-key)商户APIV3Key
-m=这是对应配置文件当中的 mchID (wxpay.mch-id) 商户ID 
-f=这是对应配置文件当中到 商户证书 apiclient_key.pem  (绝对路径地址)
-s=这是对应配置文件当中的 mchSerialNo(wxpay.mch-serial-no)商户平台证书序列号
-o=这是将生成的文件输出到哪个目录下面 (绝对路径地址)

这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等

完整

image-20230610005104021

创建 WxPayConfig

存放在config目录
package com.yby6.config;

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.ScheduledUpdateCertificatesVerifier;
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.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;


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

    // 商户号
    private String mchId;

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

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

    // 微信平台证书
    private String platformCertPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // app secret
    private String secret;

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

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

    // APIv2密钥
    private String partnerKey;


    /**
     * 获取商户的私钥文件
     */
    public PrivateKey getPrivateKey(String filename) {

        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filename);
            if (inputStream == null) {
                throw new FileNotFoundException("私钥文件不存在");
            }
            return PemUtil.loadPrivateKey(inputStream);
        } catch (Exception e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {

        log.info("构建签名验证器");
        log.info("mchId: {}", mchId);
        log.info("mchSerialNo: {}", mchSerialNo);
        log.info("privateKeyPath: {}", privateKeyPath);
        log.info("apiV3Key: {} , {}", apiV3Key , apiV3Key.length());
        log.info("appid: {}", appid);
        log.info("domain: {}", domain);
        log.info("notifyDomain: {}", notifyDomain);
        log.info("partnerKey: {} , {}", partnerKey , partnerKey.length());

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 使用定时更新的签名验证器,不需要传入证书 -> 它会帮助我们下载最新的证书不会进行过期
        return new ScheduledUpdateCertificatesVerifier(wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
    }


    /**
     * 获取http请求对象
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {

        log.info("获取httpClient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

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

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

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

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

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

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

        log.info("== getWxPayNoSignClient END ==");

        return httpClient;
    }

}
最终如此

image-20230610012032854

四、测试项目

🥬 创建Controller

package com.yby6.controller;

import cn.hutool.json.JSONUtil;
import com.yby6.config.WxPayConfig;
import com.yby6.service.WxPayService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Yang Shuai
 * Create By 2023/6/8
 */
@RestController
@RequestMapping
@RequiredArgsConstructor
public class WechatController {
    private final WxPayConfig wxPayConfig;


    @GetMapping("/index")
    public String index() {
        return "ok";
    }


    @PostMapping("getMchId")
    public Object getMchId() {
        return wxPayConfig.getMchId();
    }

}

🔥 启动项目

image-20230610012500248

🐑 IDEA插件对标PostMan

Restful Fast Request

image-20230610013733096

🚗 测试

image-20230610013551997

👋 下一章项目实战之微信支付接口搭建......

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据库—mysql、数据库编程(API)

1. Linux平台准备 &#xff08;1&#xff09;安装SDK开发包的命令 sudo apt-get install libmysqlclient-dev &#xff08;2&#xff09;编译时需要链接的库:-lmysqlclient 2. mysql 的初始化和清理 #include <mysql/mysql.h> MYSQL mysql1; //创建句柄 mysql_init(&…

宝塔面板搭建Discuz论坛并发布互联网访问【无需云服务器】

文章目录 前言1.安装基础环境2.一键部署Discuz3.安装cpolar工具4.配置域名访问Discuz5.固定域名公网地址6.配置Discuz论坛 转载自cpolar极点云的文章&#xff1a;Linux宝塔面板搭建Discuz论坛&#xff0c;并公网远程访问【内网穿透】 前言 Crossday Discuz! Board&#xff08;以…

MapReduce共享单车练习

MapReduce 本机运行 文章目录 MapReduce 本机运行✅前置工作1. 配置JDK2. 创建Java项目3. 导入所需JAR包 编程实现以下题目1. 统计各个月份共享单车使用的总数2. 统计不同天气情况下共享单车使用的总数3. 统计每个季度共享单车使用的总数4. 统计每个月份的注册数量5. 统计每天1…

【V4L2】v4l2框架分析之video_device

文章目录 &#x1f53a;一、video_device分析&#xff08;1-1&#xff09;struct video_device结构&#xff08;1-2&#xff09;struct v4l2_ioctl_ops结构&#xff08;1-3&#xff09;v4l2_file_operations结构 &#x1f53a;二、注册video设备&#x1f53a;三、卸载清除video…

WPF开发txt阅读器6:用树形图管理书籍

txt阅读器系列&#xff1a; 需求分析和文件读写目录提取类&#x1f48e;列表控件与目录字体控件绑定书籍管理系统 TreeView控件 TreeView可以通过可折叠的节点来显示层次结构中的信息&#xff0c;是表达文件系统从属关系的不二选择&#xff0c;其最终效果如下 为了构建这个树…

KISS复盘法

KISS复盘法 KISS复盘法是一种科学的项目复盘方法&#xff0c;能够把过往经验转化为实践能力&#xff0c;以促进下一次活动更好地展开&#xff0c;从而不断提升个人和团队的能力&#xff01; 模型介绍 【复盘】原是围棋术语&#xff0c;本意是对弈者在下完一盘棋之后&#xff0…

uniapp 横向滑动list(不同内容)分部问题:scroll-view,swiper overflow-x

横向滑动list,可使用标签&#xff1a; 1&#xff1a;scroll-view 2&#xff1a;swiper 3&#xff1a;overflow-x 正常来讲横向滑动的话 &#xff0c;需要特殊设置&#xff0c;只用view 设置display&#xff1a;flex&#xff0c;的话 无法横向滑动&#xff0c;右边的就会隐…

git hook

hook hook 翻译为钩子&#xff0c;简单说就是监听某个事件&#xff08;操作&#xff09;&#xff0c;然后触发自定义逻辑 在 git 中可以监听 commit&#xff0c;push 等操作&#xff0c;在操作之前或之后触发对应的 hook&#xff0c;在 hook 中写自定义的逻辑&#xff0c;比如…

Java Web开发实战经典学习过程笔记

Java Web开发实战经典学习简单笔记 第一章 Java Web 开发简介 1.胖客户端程序指的是&#xff0c;当一个程序运行时需要一个单独的客户端程序支持(如&#xff1a;QQ)。瘦客户端程序在操作时不需要任何其他程序的安装(如&#xff1a;登录网上论坛&#xff0c;只需浏览器即可)。 2…

算法刷题-链表-反转链表

反转链表 206.反转链表思路C代码双指针法递归法其他语言版本使用虚拟头结点解决链表翻转使用栈解决反转链表的问题 反转链表的写法很简单&#xff0c;一些同学甚至可以背下来但过一阵就忘了该咋写&#xff0c;主要是因为没有理解真正的反转过程。 206.反转链表 力扣题目链接 …

4.使用Express跨域资源共享(继上一章)

4.5、CORS跨域资源共享 1.接口的跨域问题 刚才编写的GET和POST接口&#xff0c;存在一个很严重的问题&#xff1a;不支持跨域请求。 解决接口跨域问题的方案主要有两种&#xff1a; ①CORS&#xff08;主流的解决方案&#xff0c;推荐使用&#xff09; ②JSONP&#xff08…

6.11下周黄金行情分析及开盘多空交易策略

近期有哪些消息面影响黄金走势&#xff1f;下周黄金多空该如何研判&#xff1f; ​黄金消息面解析&#xff1a;金价周五(6月8日)收低&#xff0c;但在美元整体走软的支撑下&#xff0c;本周录得连续第二周上升。美市尾盘&#xff0c;现货黄金收报1960.83美元/盎司&#xff0c;…

Seata介绍、原理、配置

目录 介绍&#xff1a; 核心组件&#xff1a; 原理&#xff1a; Seata 会有 4 种分布式事务解决方案&#xff0c;分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式 AT模式原理&#xff1a; 一阶段&#xff1a; 二阶段提交&#xff1a; 二阶段回滚&#xff1a; Seata配置…

华为OD机试真题 JavaScript 实现【数字涂色】【2022Q4 100分】,附详细解题思路

一、题目描述 疫情过后&#xff0c;希望小学终于又重新开学了&#xff0c;三年二班开学第一天的任务是将后面的黑板报重新制作。 黑板上已经写上了N个正整数&#xff0c;同学们需要给这每个数分别上一种颜色。 为了让黑板报既美观又有学习意义&#xff0c;老师要求同种颜色的…

每个程序员都必须知道的8种通用数据结构

8种常用数据结构 数据结构是一种特殊的组织和存储数据的方式&#xff0c;可以使我们可以更高效地对存储的数据执行操作。数据结构在计算机科学和软件工程领域具有广泛而多样的用途。 几乎所有已开发的程序或软件系统都使用数据结构。此外&#xff0c;数据结构属于计算机科学和…

微服务eureka和nacos

服务远程调用 /*** 创建RestTemplate并注入Spring容器* return*/Beanpublic RestTemplate restTemplate(){return new RestTemplate();} Autowiredprivate RestTemplate restTemplate;public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.fin…

JMeter 测试 ActiveMq

JMeter 测试 ActiveMq 的资料非常少&#xff0c; 我花了大量的时间才研究出来 关于ActiveMq 的文章请参考我另外的文章。 版本号: ActiveMq 版本号: 5.91 Jmeter 版本号: 1.13 添加ActiveMq 的jar包 将 ActiveMq 下的 "activemq-all-5.9.1.jar" 复制…

【2023】Redis cluster集群模式搭建

目录 1.cluster集群介绍2.搭建cluster集群2.1.架构图2.2.搭建集群2.2.1.创建所需配置文件2.2.2.创建集群所需容器2.2.3.创建集群&#xff1a;master1节点连接其他节点2.2.4.配置从节点&#xff0c;完成三主三从 3.在cluster集群内读写数据 1.cluster集群介绍 Redis Cluster是R…

[C++]异常笔记

我不怕练过一万种腿法的对手,就怕将一种腿法 练一万次的对手。 什么是C的异常 在C中&#xff0c;异常处理通常使用try-catch块来实现。try块用于包含可能会抛出异常的代码&#xff0c;而catch块用于捕获并处理异常。当异常被抛出时&#xff0c;程序会跳过try块中未执行…

【ABAP】数据类型(二)「预定义数据类型」

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后端的开发语言A…