# wxpaydemo **Repository Path**: old_white_java/wxpaydemo ## Basic Information - **Project Name**: wxpaydemo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2023-11-10 - **Last Updated**: 2023-11-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot+Vue 微信支付 ## 1.微信支付产品介绍 微信支付主要包括`付款码支付`,`JSAPI支付`,`小程序支付`,`Native支付`,`APP支付`,`刷脸支付`等场景,本次主要学习`Native`支付,适用于pc网站,常见的微信扫一扫支付方式. ## 2.前置工作 ### (1)获取微信商户号 微信商户平台:https://pay.weixin.qq.com/ 场景:Native支付 步骤:提交资料 => 签署协议 => 获取商户号 ### (2)在微信开放平台获取APPID 微信公众平台:https://mp.weixin.qq.com/ 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号 ### (3)获取API密钥 APIv2版本的接口需要此秘钥 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥 ### (4)获取APIV3密钥 APIv3版本的接口需要此秘钥 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥 随机密码生成工具:https://suijimimashengcheng.bmcx.com/ ### (5)申请商户API证书 APIv3版本的所有接口都需要 APIv2版本的高级接口需要(如:退款、企业红包、企业付款等) 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 ### (6)获取微信怕平台证书 可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。 ## 3.创建Springboot微信支付案例 ### (1)创建sprigboot项目 略 ### (2)添加依赖 ```xml org.springframework.boot spring-boot-starter-web ``` ### (3)创建application.yml配置文件 ```yaml server: port: 8090 spring: application: name: payment-demo ``` ### (4)创建controller包 创建controller包,创建ProductController类 ```java @Api(tags = "微信支付") @RestController @RequestMapping("/api/product") @CrossOrigin //跨域 public class ProductController { @ApiOperation(value = "test") @GetMapping("/test") public String test() { return "hello"; } } ``` ### (5)引入swagger3依赖 ```xml io.springfox springfox-boot-starter 3.0.0 com.github.xiaoymin knife4j-spring-ui 3.0.3 ``` ### (6)swagger3配置类 ```java @Configuration @EnableOpenApi //@EnableSwaggerBootstrapUI public class SwaggerConfig { @Value("${swagger.enabled}") private boolean enable; /** * 创建API * http:IP:端口号/swagger-ui/index.html 原生地址 * http:IP:端口号/doc.html bootStrap-UI地址 */ @Bean public Docket createRestApi() { return new Docket(DocumentationType.OAS_30).pathMapping("/") // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息) /*.enable(enable)*/ .apiInfo(apiInfo()) // 设置哪些接口暴露给Swagger展示 .select() // 扫描所有有注解的api,用这种方式更灵活 .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) // 扫描指定包中的swagger注解 .apis(RequestHandlerSelectors.basePackage("com.acerola.paymentdemo.controller")) // 扫描所有 .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.regex("(?!/ApiError.*).*")) .paths(PathSelectors.any()) .build() // 支持的通讯协议集合 .protocols(newHashSet("https", "http")) .securitySchemes(securitySchemes()) .securityContexts(securityContexts()); } /** * 支持的通讯协议集合 * @param type1 * @param type2 * @return */ private Set newHashSet(String type1, String type2){ Set set = new HashSet<>(); set.add(type1); set.add(type2); return set; } /** * 认证的安全上下文 */ private List securitySchemes() { List securitySchemes = new ArrayList<>(); securitySchemes.add((SecurityScheme) new ApiKey("token", "token", "header")); return securitySchemes; } /** * 授权信息全局应用 */ private List securityContexts() { List securityContexts = new ArrayList<>(); securityContexts.add(SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.any()).build()); return securityContexts; } private List defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; List securityReferences = new ArrayList<>(); securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); return securityReferences; } /** * 添加摘要信息 */ private ApiInfo apiInfo() { // 用ApiInfoBuilder进行定制 return new ApiInfoBuilder() // 设置标题 .title("微信支付") // 描述 .description("微信支付") // 作者信息 .contact(new Contact("doctorCloud", null, null)) // 版本 .version("版本号:V.1") //协议 .license("The Apache License") //协议url .licenseUrl("http://www.baidu.com") .build(); } } ``` ### (7)测试swagger文档 http://localhost:8090/doc.html#/ ![image-20220326104137315](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326104137315.png) ### (8)引入lombok,简化实体开发 ```xml org.projectlombok lombok true ``` ### (9)创建R类,统一返回结果 ```java @Data //生成set、get等方法 public class R { private Integer code; private String message; private Map data = new HashMap<>(); public static R ok() { R r = new R(); r.setCode(0); r.setMessage("成功"); return r; } public static R error() { R r = new R(); r.setCode(-1); r.setMessage("失败"); return r; } public R data(String key, Object value) { this.data.put(key, value); return this; } } ``` ### (10)配置json时间格式 ```yaml spring: jackson: #json时间格式 date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 ``` ### (11)创建mysql数据库 创建payment_demo数据库,并执行以下sql ```sql USE `payment_demo`; /*Table structure for table `t_order_info` */ CREATE TABLE `t_order_info` ( `id` bigint(11) 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 CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; /*Table structure for table `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`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; /*Table structure for table `t_product` */ CREATE TABLE `t_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id', `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`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; /*Data for the table `t_product` */ insert into `t_product`(`title`,`price`) values ('Java课程',1); insert into `t_product`(`title`,`price`) values ('大数据课程',1); insert into `t_product`(`title`,`price`) values ('前端课程',1); insert into `t_product`(`title`,`price`) values ('UI课程',1); /*Table structure for table `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`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; ``` ### (12) 集成mybatis-plus 引入依赖 ```xml mysql mysql-connector-java com.baomidou mybatis-plus-boot-starter 3.3.1 ``` ### (13) 配置数据库连接 ```yaml datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root password: 123456 ``` ### (14)定义实体类 BaseEntity: ```java @Data public class BaseEntity { //定义主键策略:跟随数据库的主键自增 @TableId(value = "id", type = IdType.AUTO) private String id; //主键 private Date createTime;//创建时间 private Date updateTime;//更新时间 } ``` OrderInfo: ```java @Data @TableName("t_order_info") public class OrderInfo extends BaseEntity{ private String title;//订单标题 private String orderNo;//商户订单编号 private Long userId;//用户id private Long productId;//支付产品id private Integer totalFee;//订单金额(分) private String codeUrl;//订单二维码连接 private String orderStatus;//订单状态 } ``` PaymentInfo: ```java @Data @TableName("t_payment_info") public class PaymentInfo extends BaseEntity{ private String orderNo;//商品订单编号 private String transactionId;//支付系统交易编号 private String paymentType;//支付类型 private String tradeType;//交易类型 private String tradeState;//交易状态 private Integer payerTotal;//支付金额(分) private String content;//通知参数 } ``` Product: ```java @Data @TableName("t_product") public class Product extends BaseEntity{ private String title; //商品名称 private Integer price; //价格(分) } ``` RefundInfo: ```java @Data @TableName("t_refund_info") public class RefundInfo extends BaseEntity{ private String orderNo;//商品订单编号 private String refundNo;//退款单编号 private String refundId;//支付系统退款单号 private Integer totalFee;//原订单金额(分) private Integer refund;//退款金额(分) private String reason;//退款原因 private String refundStatus;//退款单状态 private String contentReturn;//申请退款返回参数 private String contentNotify;//退款结果通知参数 } ``` ### (15)定义持久层 ![image-20220326150017853](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150017853.png) ### (16)定义mybatis-plus的yml文件配置 ```yaml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: - classpath: src/main/resources/mapper/*.xml ``` ### (17) 定义业务层 定义业务层接口继承 IService<> 定义业务层接口的实现类,并继承 ServiceImpl<,> ![image-20220326150316641](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150316641.png) ### (18) 查询所有商品测试 ```java @ApiOperation("商品列表") @GetMapping("/list") public R list() { List list = productService.list(); return R.ok().data("productList", list); } ``` swagger测试: ![image-20220326151342914](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326151342914.png) ## 4. 搭建前端环境 此处为vue基础,略过 ## 5. 基础支付API V3 ### (1) 引入支付参数 #### 1.1定义微信支付相关参数 将资料文件夹中的 wxpay.properties 复制到resources目录中 这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等 wxpay.properties: ```properties # 微信支付相关参数 # 商户号 wxpay.mch-id=1558950191 # 商户API证书序列号 wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F # 商户私钥文件 wxpay.private-key-path=apiclient_key.pem # APIv3密钥 wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # APPID wxpay.appid=wx74862e0dfcf69954 # 微信服务器地址 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io ``` #### 1.2 读取支付参数 WxPayConfig.java: ```java @Configuration @PropertySource("classpath:wxpay.properties") //读取配置文件 @ConfigurationProperties(prefix="wxpay") //读取wxpay节点 @Data //使用set方法将wxpay节点中的值填充到当前类的属性中 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; } ``` #### 1.3 测试支付参数的获取 在controller包建立TestController类 ```java @Api(tags = "测试控制器") @RestController @RequestMapping("/api/test") public class TestController { @Autowired private WxPayConfig wxPayConfig; @ApiOperation(value = "测试支付参数的获取") @GetMapping("/get-wx-pay-config") public R getWxPayConfig() { String mchId = wxPayConfig.getMchId(); return R.ok().data("mchId", mchId); } } ``` #### 1.4 配置 Annotation Processor 可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发。 ```xml org.springframework.boot spring-boot-configuration-processor true ``` #### 1.5 设置wxpay.properties为springboot配置文件 让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示 File -> Project Structure -> Modules -> 选择小叶子 ![image-20220326163120508](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326163120508.png) ### (2) 加载商户私钥 #### 2.1复制商户私钥 #### 2.2 引入SDK ```xml com.github.wechatpay-apiv3 wechatpay-apache-httpclient 0.3.0 ``` #### 2.3测试商户私钥的获取 在WxPayConfig.java中添加获取商户私钥文件的方法 ```java /** * 获取商户私钥文件 * * @param filename * @return * @throws FileNotFoundException */ public PrivateKey getPrivateKey(String filename){ try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私钥不存在",e); } } ``` 在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。 ```java @Test public void testGetPrivateKey() { //获取私钥路径 String privateKeyPath = wxPayConfig.getPrivateKeyPath(); //获取商户私钥 PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath); System.out.println(privateKey); } ``` ### (3)获取签名验证器和HttpClient #### 3.1 证书密钥使用说明 ![](https://pay.weixin.qq.com/wiki/doc/apiv3/assets/img/common/ico-guide/chapter1_5_1.png) #### 3.2 获取签名验证器 https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) 平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。 签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。 ```java @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; } ``` #### 3.3 获取 HttpClient 对象 https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。 ```java /** * 获取HttpClient对象 * * @param verifier * @return */ @Bean public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) { //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于构造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); return httpClient; } ``` ### (4)API字典和相关工具 #### 4.1 API列表 | 模块名称 | 功能列表 | 描述 | | :----------------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------- | | Native支付 | [Native下单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml) | 通过本接口提交微信支付Native支付订单 | | [查询订单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml) | 通过此接口查询订单状态 | | | [关闭订单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml) | 通过此接口关闭待支付订单 | | | [Native调起支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml) | 商户后台系统先调用微信支付的Native支付接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。 | | | [支付结果通知](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml) | 微信支付通过支付通知接口将用户支付成功消息通知给商户 | | | [申请退款](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml) | 商户可以通过该接口将支付金额退还给买家 | | | [查询单笔退款](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml) | 提交退款申请后,通过调用该接口查询退款状态 | | | [退款结果通知](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml) | 微信支付通过退款通知接口将用户退款成功消息通知给商户 | | | [申请交易账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_6.shtml) | 商户可以通过该接口获取交易账单文件的下载地址 | | | [申请资金账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml) | 商户可以通过该接口获取资金账单文件的下载地址 | | | [下载账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml) | 通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。 | | #### 4.2 接口规则 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml 微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。 我们使用谷歌的json处理 ```xml com.google.code.gson gson ``` #### 4.3定义枚举 为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。 ![image-20220326174737976](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174737976.png) #### 4.4添加工具类 ![image-20220326174822867](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174822867.png) ### (5)Native下单API #### 5.1Native下单流程 [微信支付-开发者文档 (qq.com)](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml) ![](https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/assets/img/common/pay/chapter3_1_2.png) #### 5.2 Native下单API [微信支付-开发者文档 (qq.com)](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml) ##### 5.2.1 创建WxPayController ```java package com.acerola.paymentdemo.controller; import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @program: payment-demo * @description: * @author: Acerola * @create: 2022-03-26 17:53 **/ @CrossOrigin @RestController @RequestMapping("/api/wx-pay") @Api(tags = "网站微信支付") @Slf4j public class WxPayController { } ``` ##### 5.2.2 创建service层 接口: ```java package com.acerola.paymentdemo.service; public interface WxPayService { } ``` 实现: ```java package com.acerola.paymentdemo.service.impl; import com.acerola.paymentdemo.service.WxPayService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** * @program: payment-demo * @description: * @author: Acerola * @create: 2022-03-26 17:55 **/ @Service @Slf4j public class WxPayServiceImpl implements WxPayService { } ``` ##### 5.2.3 定义WxPayController方法 R对象中添加 @Accessors(chain = true),使其可以链式操作 ```java /** * Native下单 * * @param productId * @return * @throws Exception */ @ApiOperation("调用统一下单API,生成支付二维码") @PostMapping("/native/{productId}") public R nativePay(@PathVariable Long productId) throws Exception { log.info("发起支付请求"); //返回支付二维码连接和订单号 Map map = wxPayService.nativePay(productId); return R.ok().setData(map); } ``` ##### 5.2.4 定义WxPayService方法 实现 ```java @Autowired private WxPayConfig wxPayConfig; @Autowired private CloseableHttpClient wxPayClient; /** * 创建订单,调用Native支付接口 * * @param productId * @return code_url 和 订单号 */ @SneakyThrows @Override public Map nativePay(Long productId) { log.info("生成订单"); //生成订单 OrderInfo orderInfo = new OrderInfo(); orderInfo.setTitle("test"); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号 orderInfo.setProductId(productId); orderInfo.setTotalFee(1); //分 orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //TODO:存入数据库 log.info("调用统一下单API"); //调用统一下单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); log.info("请求参数:" + jsonParams); 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 resultMap = gson.fromJson(bodyAsString, HashMap.class); //二维码 String codeUrl = resultMap.get("code_url"); Map map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", orderInfo.getOrderNo()); return map; } finally { response.close(); } } ``` ##### 5.2.5 测试调用统一下单API,生成支付二维码 ![image-20220326181237729](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326181237729.png) #### 5.3 创建课程订单 ##### 5.3.1 保存订单 OrderInfoService 接口: ```java OrderInfo createOrderByProductId(Long productId); ``` 实现: ```java @Resource private ProductMapper productMapper; @Override public OrderInfo createOrderByProductId(Long productId) { //查找已存在但未支付的订单 OrderInfo orderInfo = this.getNoPayOrderByProductId(productId); if( orderInfo != null){ return orderInfo; } //获取商品信息 Product product = productMapper.selectById(productId); //生成订单 orderInfo = new OrderInfo(); orderInfo.setTitle(product.getTitle()); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号 orderInfo.setProductId(productId); orderInfo.setTotalFee(product.getPrice()); //分 orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); baseMapper.insert(orderInfo); return orderInfo; } ``` 查找未支付订单:OrderInfoService中添加辅助方法 ```java /** * 根据商品id查询未支付订单 * 防止重复创建订单对象 * * @param productId * @return */ private OrderInfo getNoPayOrderByProductId(Long productId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("product_id", productId); queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType()); // queryWrapper.eq("user_id", userId); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); return orderInfo; } ``` ##### 5.3.2 缓存二维码 OrderInfoService 接口: ```java OrderInfo createOrderByProductId(Long productId); ``` 实现: ```java /** * 存储订单二维码 * @param orderNo * @param codeUrl */ @Override public void saveCodeUrl(String orderNo, String codeUrl) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = new OrderInfo(); orderInfo.setCodeUrl(codeUrl); baseMapper.update(orderInfo, queryWrapper); } ``` ##### 5.3.3 修改WxPayServiceImpl 的 nativePay 方法 ```java @Autowired private OrderInfoService orderInfoService; /** * 创建订单,调用Native支付接口 * * @param productId * @return code_url 和 订单号 */ @SneakyThrows @Override public Map nativePay(Long productId) { log.info("生成订单"); //生成订单 OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId); String codeUrl = orderInfo.getCodeUrl(); if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){ log.info("订单已存在,二维码已保存"); //返回二维码 Map map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", orderInfo.getOrderNo()); return map; } log.info("调用统一下单API"); //调用统一下单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); log.info("请求参数 ===> {}" + jsonParams); 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 resultMap = gson.fromJson(bodyAsString, HashMap.class); //二维码 codeUrl = resultMap.get("code_url"); //保存二维码 String orderNo = orderInfo.getOrderNo(); orderInfoService.saveCodeUrl(orderNo, codeUrl); //返回二维码 Map map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", orderInfo.getOrderNo()); return map; } finally { response.close(); } } ``` #### 5.4 显示订单列表 在我的订单页面按时间倒序显示订单列表 ##### 5.4.1 创建OrderInfoController ```java package com.acerola.paymentdemo.controller; import com.acerola.paymentdemo.common.R; import com.acerola.paymentdemo.entity.OrderInfo; import com.acerola.paymentdemo.service.OrderInfoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @program: payment-demo * @description: * @author: Acerola * @create: 2022-03-27 17:57 **/ @CrossOrigin //开放前端的跨域访问 @Api(tags = "商品订单管理") @RestController @RequestMapping("/api/order-info") public class OrderInfoController { @Autowired private OrderInfoService orderInfoService; @ApiOperation("订单列表") @GetMapping("/list") public R list() { List list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list", list); } } ``` ##### 5.4.2 定义 OrderInfoService 方法 接口: ```java List listOrderByCreateTimeDesc(); ``` 实现: ```java /** * 查询订单列表,并倒序查询 * @return */ @Override public List listOrderByCreateTimeDesc() { QueryWrapper queryWrapper = new QueryWrapper ().orderByDesc("create_time"); return baseMapper.selectList(queryWrapper); } ``` ### (6)支付通知API #### 6.1 内网穿透 ##### 6.1.1下载内网穿透工具 [下载ngrok ](https://ngrok.com/) ##### 6.1.2 设置authtoken ```bash ngrok authtoken 26y23wgoy8AEsyn4CC9qfLbEvK0_2A4okRhSWNU6GprLQjyHZ ``` ##### 6.1.3 启动内网穿透 ```bash ngrok http 8090 ``` ![image-20220327201945354](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327201945354.png) ##### 6.1.4 测试外网访问 ```bash http://922c-117-158-127-32.ngrok.io/api/order-info/list ``` ![image-20220327232105872](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327232105872.png) #### 6.2 接收通知和返回应答 支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml ##### 6.2.1启动ngrok ```bash ngrok http 8090 ``` ##### 6.2.2设置通知地址 wxpay.properties `注意:每次重新启动ngrok,都需要根据实际情况修改这个配置` ```properties wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io ``` ##### 6.2.3创建通知接口 `通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理 该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认 为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保 证通知最终能成功。(通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)` ```java /** * 支付通知 * 微信支付通过支付通知接口将用户支付成功消息通知给商户 */ @ApiOperation("支付通知") @PostMapping("/native/notify") public String nativeNotify(HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); log.info("支付通知的id ===> {}", bodyMap.get("id")); log.info("支付通知的完整数据 ===> {}", body); //TODO : 签名的验证 //TODO : 处理订单 //成功应答:成功应答必须为200或204,否则就是失败应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } ``` ##### 6.2.4测试失败应答 用失败应答替换成功应答 ```java @PostMapping("/native/notify") public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { Gson gson = new Gson(); Map map = new HashMap<>(); try { } catch (Exception e) { e.printStackTrace(); // 测试错误应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "系统错误"); return gson.toJson(map); } } ``` #### 6.3验签 ##### 6.3.1 工具类 参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest ##### 6.3.2 验签 ```java @Resource private Verifier verifier; ``` 更新WxPayController中nativeNotify方法 ```java /** * 支付通知 * 微信支付通过支付通知接口将用户支付成功消息通知给商户 */ @ApiOperation("支付通知") @PostMapping("/native/notify") public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); log.info("支付通知的id ===> {}", bodyMap.get("id")); log.info("支付通知的完整数据 ===> {}", body); //TODO : 签名的验证 //签名的验证 WechatPay2ValidatorForRequest validator = new WechatPay2ValidatorForRequest(verifier, body, requestId); if (!validator.validate(request)) { log.error("通知验签失败"); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } log.info("通知验签成功"); //TODO : 处理订单 //成功应答:成功应答必须为200或204,否则就是失败应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } ``` #### 6.4解密 ##### 6.4.1 WxPayController nativeNotify 方法中添加处理订单的代码 ```java //处理订单 wxPayService.processOrder(bodyMap); ``` ##### 6.4.2 WxPayService 接口: ```java void processOrder(Map bodyMap) throws GeneralSecurityException; ``` 实现: ```java /** * 对称解密 * * @param bodyMap * @return */ private String decryptFromResource(Map bodyMap) throws GeneralSecurityException { log.info("密文解密"); //通知数据 Map resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); log.info("密文 ===> {}", ciphertext); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}", plainText); return plainText; } ``` #### 6.5处理订单 ##### 6.5.1 完善processOrder方法 ```java @Autowired private PaymentInfoService paymentInfoService; @Override public void processOrder(Map bodyMap) throws GeneralSecurityException { log.info("处理订单"); String plainText = decryptFromResource(bodyMap); //转换明文 Gson gson = new Gson(); Map plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no"); //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(plainText); } ``` ##### 6.5.2更新订单状态 OrderInfoService 接口: ```java void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus); ``` 实现: ```java /** * 根据订单编号更新订单状态 * * @param orderNo * @param orderStatus */ @Override public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) { log.info("更新订单状态 ===> {}", orderStatus.getType()); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = new OrderInfo(); orderInfo.setOrderStatus(orderStatus.getType()); baseMapper.update(orderInfo, queryWrapper); } ``` ##### 6.5.3处理支付日志 PaymentInfoService 接口: ```java void createPaymentInfo(String plainText); ``` 实现: ```java /** * 记录支付日志 * * @param plainText */ @Override public void createPaymentInfo(String plainText) { log.info("记录支付日志"); Gson gson = new Gson(); Map plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); String transactionId = (String) plainTextMap.get("transaction_id"); String tradeType = (String) plainTextMap.get("trade_type"); String tradeState = (String) plainTextMap.get("trade_state"); Map amount = (Map) plainTextMap.get("amount"); Integer payerTotal = ((Double) amount.get("payer_total")).intValue(); PaymentInfo paymentInfo = new PaymentInfo(); paymentInfo.setOrderNo(orderNo); paymentInfo.setPaymentType(PayType.WXPAY.getType()); paymentInfo.setTransactionId(transactionId); paymentInfo.setTradeType(tradeType); paymentInfo.setTradeState(tradeState); paymentInfo.setPayerTotal(payerTotal); paymentInfo.setContent(plainText); baseMapper.insert(paymentInfo); } ``` #### 6.6处理重复通知 ![image-20220328104343532](C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220328104343532.png) 在 processOrder 方法中,更新订单状态之前,添加如下代码 OrderInfoService 接口: ```java String getOrderStatus(String orderNo); ``` 实现: ```java /** * 根据订单号获取订单状态 * * @param orderNo * @return */ @Override public String getOrderStatus(String orderNo) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); //防止被删除的订单的回调通知的调用 if (orderInfo == null) { return null; } return orderInfo.getOrderStatus(); } ``` 在 processOrder 方法中,更新订单状态之前,添加如下代码 ```java //处理重复通知 //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的 String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return; } ``` #### 6.7数据锁 ##### 6.7.1定义ReentrantLock 定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。 ```java private final ReentrantLock lock = new ReentrantLock(); ``` ```java @Override public void processOrder(Map bodyMap) throws GeneralSecurityException { log.info("处理订单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); /*在对业务数据进行状态检查和处理之前, 要采用数据锁进行并发控制, 以避免函数重入造成的数据混乱*/ //尝试获取锁: // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放 if (lock.tryLock()) { try { //处理重复的通知 //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return; } //模拟通知并发 try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(plainText); } finally { //要主动释放锁 lock.unlock(); } } } ``` ### (7)商户定时查询本地订单 #### 7.1 后端定义商户查单接口 支付成功后,商户侧查询本地数据库,订单是否支付成功 ```java /** * 查询本地订单状态 */ @ApiOperation("查询本地订单状态") @GetMapping("/query-order-status/{orderNo}") public R queryOrderStatus(@PathVariable String orderNo) { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (OrderStatus.SUCCESS.getType().equals(orderStatus)) {//支付成功 return R.ok(); } return R.ok().setCode(101).setMessage("支付中..."); } ``` #### 7.2 前端定时轮询查单 在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面 ##### 7.2.1定义定时器 ```html //启动定时器 this.timer = setInterval(() => { //查询订单是否支付成功 this.queryOrderStatus() }, 3000) ``` ##### 7.2.2 查询订单 ```html // 查询订单状态 queryOrderStatus() { orderInfoApi.queryOrderStatus(this.orderNo).then(response => { console.log('查询订单状态:' + response.code) // 支付成功后的页面跳转 if (response.code === 0) { console.log('清除定时器') clearInterval(this.timer) // 三秒后跳转到订单列表 setTimeout(() => { this.$router.push({ path: '/success' }) }, 3000) } }) } ``` ### (8)用户取消订单API 实现用户主动取消订单的功能 #### 8.1定义取消订单接口 WxPayController中添加接口方法 ```java /** * 用户取消订单 * * @param orderNo * @return * @throws Exception */ @ApiOperation("用户取消订单") @PostMapping("/cancel/{orderNo}") public R cancel(@PathVariable String orderNo) throws Exception { log.info("取消订单"); wxPayService.cancelOrder(orderNo); return R.ok().setMessage("订单已取消"); } ``` #### 8.2WxPayService 接口: ```java void cancelOrder(String orderNo) throws Exception; ``` 实现: ```java /** * 用户取消订单 * * @param orderNo */ @Override public void cancelOrder(String orderNo) throws Exception { //调用微信支付的关单接口 this.closeOrder(orderNo); //更新商户端的订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); } ``` 关单方法 ```java /** * 关单接口的调用 * * @param orderNo */ private void closeOrder(String orderNo) throws Exception { log.info("关单接口的调用,订单号 ===> {}", orderNo); //创建远程请求对象 String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url); HttpPost httpPost = new HttpPost(url); //组装json请求体 Gson gson = new Gson(); Map paramsMap = new HashMap<>(); paramsMap.put("mchid", wxPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}", jsonParams); //将请求参数设置到请求对象中 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 { int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功200"); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功204"); } else { log.info("Native下单失败,响应码 = " + statusCode); throw new IOException("request failed"); } } finally { response.close(); } } ``` ### (9)微信支付查单API #### 9.1 查单接口的调用 商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态 ##### 9.1.1 WxPayController ```java /** * 查询订单 * * @param orderNo * @return * @throws URISyntaxException * @throws IOException */ @ApiOperation("查询订单:测试订单状态用") @GetMapping("query/{orderNo}") public R queryOrder(@PathVariable String orderNo) throws Exception { log.info("查询订单"); String bodyAsString = wxPayService.queryOrder(orderNo); return R.ok().setMessage("查询成功").data("bodyAsString", bodyAsString); } ``` ##### 9.1.2 WxPayService 接口: ```java String queryOrder(String orderNo) throws Exception; ``` 实现: ```java /** * 查单接口调用 */ @SneakyThrows @Override public String queryOrder(String orderNo) { log.info("查单接口调用 ===> {}", orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("? mchid=").concat(wxPayConfig.getMchId()); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpGet); 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"); } return bodyAsString; } finally { response.close(); } } ``` #### 9.2 集成Spring Task Spring 3.0后提供Spring Task实现任务调度 ##### 9.2.1 启动类添加注解 ```java @EnableScheduling ``` ##### 9.2.2 测试定时任务 创建 task 包,创建 WxPayTask.java ```java package com.acerola.paymentdemo.task; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * @program: payment-demo * @description: * @author: Acerola * @create: 2022-03-28 17:12 **/ @Slf4j @Component public class WxPayTask { /** * 测试 * (cron="秒 分 时 日 月 周") * *:每隔一秒执行 * 0/3:从第0秒开始,每隔3秒执行一次 * 1-3: 从第1秒开始执行,到第3秒结束执行 * 1,2,3:第1、2、3秒执行 * ?:不指定,若指定日期,则不指定周,反之同理 */ @Scheduled(cron = "0/3 * * * * ?") public void task1() { log.info("task1 执行"); } } ``` #### 9.3定时查找超时订单 ##### 9.3.1WxPayTask ```java @Autowired private OrderInfoService orderInfoService; @Autowired private WxPayService wxPayService; /** * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单 */ @Scheduled(cron = "0/30 * * * * ?") public void orderConfirm() throws Exception { log.info("orderConfirm 被执行......"); List orderInfoList = orderInfoService.getNoPayOrderByDuration(5); for (OrderInfo orderInfo : orderInfoList) { String orderNo = orderInfo.getOrderNo(); log.warn("超时订单 ===> {}", orderNo); //核实订单状态:调用微信支付查单接口 wxPayService.checkOrderStatus(orderNo); } } ``` ##### 9.3.2 OrderInfoService 接口: ```java List getNoPayOrderByDuration(int i); ``` 实现: ```java /** * 找出创建超过minutes分钟并且未支付的订单 * * @param minutes * @return */ @Override public List getNoPayOrderByDuration(int minutes) { //minutes分钟之前的时间 Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType()); queryWrapper.le("create_time", instant); List orderInfoList = baseMapper.selectList(queryWrapper); return orderInfoList; } ``` #### 9.4 处理超时订单 WxPayService 接口: ```java void checkOrderStatus(String orderNo); ``` 实现: ```java /** * 根据订单号查询微信支付查单接口,核实订单状态 * 如果订单已支付,则更新商户端订单状态,并记录支付日志 * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态 * * @param orderNo */ @SneakyThrows @Override public void checkOrderStatus(String orderNo) { log.warn("根据订单号核实订单状态 ===> {}", orderNo); //调用微信支付查单接口 String result = this.queryOrder(orderNo); Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端的订单状态 Object tradeState = resultMap.get("trade_state"); //判断订单状态 if (WxTradeState.SUCCESS.getType().equals(tradeState)) { log.warn("核实订单已支付 ===> {}", orderNo); //如果确认订单已支付则更新本地订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(result); } if (WxTradeState.NOTPAY.getType().equals(tradeState)) { log.warn("核实订单未支付 ===> {}", orderNo); //如果订单未支付,则调用关单接口 this.closeOrder(orderNo); //更新本地订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); } } ``` ##### 9.4.1 根据订单号获取订单 接口: ```java OrderInfo getOrderByOrderNo(String orderNo); ``` 实现: ```java /** * 根据订单号获取订单 * @param orderNo * @return */ @Override public OrderInfo getOrderByOrderNo(String orderNo) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); return orderInfo; } ``` ### (10) 申请退款API 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml #### 10.1 创建退款单 RefundsInfoService 接口: ```java RefundInfo createRefundByOrderNo(String orderNo, String reason); ``` 实现: ```java @Autowired private OrderInfoService orderInfoService; /** * 根据订单号创建退款订单 * * @param orderNo * @return */ @Override public RefundInfo createRefundByOrderNo(String orderNo, String reason) { //根据订单号获取订单信息 OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); //根据订单号生成退款订单 RefundInfo refundInfo = new RefundInfo(); refundInfo.setOrderNo(orderNo);//订单编号 refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号 refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分) refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分) refundInfo.setReason(reason);//退款原因 //保存退款订单 baseMapper.insert(refundInfo); return refundInfo; } ``` #### 10.2 更新退款单 RefundInfoService 接口: ```java void updateRefund(String content); ``` 实现: ```java /** * 记录退款记录 * * @param content */ @Override public void updateRefund(String content) { //将json字符串转换成Map Gson gson = new Gson(); Map resultMap = gson.fromJson(content, HashMap.class); //根据退款单编号修改退款单 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_no", resultMap.get("out_refund_no")); //设置要修改的字段 RefundInfo refundInfo = new RefundInfo(); refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号 //查询退款和申请退款中的返回参数 if (resultMap.get("status") != null) { refundInfo.setRefundStatus(resultMap.get("status"));//退款状态 refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段 } //退款回调中的回调参数 if (resultMap.get("refund_status") != null) { refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态 refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段 } //更新退款单 baseMapper.update(refundInfo, queryWrapper); } ``` #### 10.3 申请退款 ##### 10.3.1 WxPayController ```java @ApiOperation("申请退款") @PostMapping("/refunds/{orderNo}/{reason}") public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception { log.info("申请退款"); wxPayService.refund(orderNo, reason); return R.ok(); } ``` ##### 10.3.2 WxPayService 接口: ```java void refund(String orderNo, String reason); ``` 实现: ```java @Autowired private RefundInfoService refundsInfoService; /** * 退款 * * @param orderNo * @param reason * @throws IOException */ @SneakyThrows @Transactional(rollbackFor = Exception.class) @Override public void refund(String orderNo, String reason) { log.info("创建退款单记录"); //根据订单编号创建退款单 RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason); log.info("调用退款API"); //调用统一下单API String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()); HttpPost httpPost = new HttpPost(url); // 请求body参数 Gson gson = new Gson(); Map paramsMap = new HashMap(); paramsMap.put("out_trade_no", orderNo);//订单编号 paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号 paramsMap.put("reason", reason);//退款原因 paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址 Map amountMap = new HashMap(); amountMap.put("refund", refundsInfo.getRefund());//退款金额 amountMap.put("total", refundsInfo.getTotalFee());//原订单金额 amountMap.put("currency", "CNY");//退款币种 paramsMap.put("amount", amountMap); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" + jsonParams); 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) { log.info("成功"); } else { throw new RuntimeException("退款异常, 响应码 = " + statusCode + ", 退款返回结果 = " + bodyAsString); } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING); //更新退款单 refundsInfoService.updateRefund(bodyAsString); } finally { response.close(); } } ``` ### (11)查询退款API 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml #### 11.1 查单接口的调用 ##### 11.1.1 WxPayController ```java /** * 查询退款 * * @param refundNo * @return * @throws Exception */ @ApiOperation("查询退款:测试用") @GetMapping("/query-refund/{refundNo}") public R queryRefund(@PathVariable String refundNo) throws Exception { log.info("查询退款"); String result = wxPayService.queryRefund(refundNo); return R.ok().setMessage("查询成功").data("result", result); } ``` ##### 11.1.2 WxPayService 接口: ```java String queryRefund(String refundNo); ``` 实现: ```java @SneakyThrows @Override public String queryRefund(String refundNo) { log.info("查询退款接口调用 ===> {}", refundNo); String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo); url = wxPayConfig.getDomain().concat(url); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 查询退款返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("查询退款异常, 响应码 = " + statusCode + ", 查询退款返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } } ``` #### 11.2 定时查找退款中的订单 ##### 11.2.1 WxPayTask ```java /** * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单 */ @Scheduled(cron = "0/30 * * * * ?") public void refundConfirm() throws Exception { log.info("refundConfirm 被执行......"); //找出申请退款超过5分钟并且未成功的退款单 List refundInfoList = refundInfoService.getNoRefundOrderByDuration(5); for (RefundInfo refundInfo : refundInfoList) { String refundNo = refundInfo.getRefundNo(); log.warn("超时未退款的退款单号 ===> {}", refundNo); //核实订单状态:调用微信支付查询退款接口 wxPayService.checkRefundStatus(refundNo); } } ``` ##### 11.2.2 RefundInfoService 接口: ```java List getNoRefundOrderByDuration(int minutes); ``` 实现: ```java /** * 找出申请退款超过minutes分钟并且未成功的退款单 * * @param minutes * @return */ @Override public List getNoRefundOrderByDuration(int minutes) { //minutes分钟之前的时间 Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType()); queryWrapper.le("create_time", instant); List refundInfoList = baseMapper.selectList(queryWrapper); return refundInfoList; } ``` #### 11.3 处理超时未退款订单 WxPayService 核实订单状态 接口: ```java void checkRefundStatus(String refundNo); ``` 实现: ```java /** * 根据退款单号核实退款单状态 * * @param refundNo * @return */ @Transactional(rollbackFor = Exception.class) @Override public void checkRefundStatus(String refundNo) { log.warn("根据退款单号核实退款单状态 ===> {}", refundNo); //调用查询退款单接口 String result = this.queryRefund(refundNo); //组装json请求体字符串 Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端退款状态 String status = resultMap.get("status"); String orderNo = resultMap.get("out_trade_no"); if (WxRefundStatus.SUCCESS.getType().equals(status)) { log.warn("核实订单已退款成功 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 refundsInfoService.updateRefund(result); } if (WxRefundStatus.ABNORMAL.getType().equals(status)) { log.warn("核实订单退款异常 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); //更新退款单 refundsInfoService.updateRefund(result); } } ``` ### (12) 退款结果通知API 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml #### 12.1 接收退款通知 WxPayController ```java /** * 退款结果通知 * 退款状态改变后,微信会把相关退款结果发送给商户。 */ @PostMapping("/refunds/notify") public String refundsNotify(HttpServletRequest request, HttpServletResponse response) { log.info("退款通知执行"); Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 try { //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); log.info("支付通知的id ===> {}", requestId); //签名的验证 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("通知验签成功"); //处理退款单 wxPayService.processRefund(bodyMap); //成功应答 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); } } ``` #### 12.2 处理订单和退款单 WxPayService 接口: ```java void processRefund(Map bodyMap); ``` 实现: ```java /** * 处理退款单 */ @SneakyThrows @Transactional(rollbackFor = Exception.class) @Override public void processRefund(Map bodyMap) { log.info("退款单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); if (lock.tryLock()) { try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { return; } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 refundsInfoService.updateRefund(plainText); } finally { //要主动释放锁 lock.unlock(); } } } ``` ### (13) 账单 #### 13.1 申请交易账单和资金账单 ##### 13.1.1 WxPayController ```java @ApiOperation("获取账单url:测试用") @GetMapping("/querybill/{billDate}/{type}") public R queryTradeBill( @PathVariable String billDate, @PathVariable String type) throws Exception { log.info("获取账单url"); String downloadUrl = wxPayService.queryBill(billDate, type); return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl); } ``` ##### 13.1.2 WxPayService 接口: ```java String queryBill(String billDate, String type); ``` 实现: ```java /** * 申请账单 * * @param billDate * @param type * @return * @throws Exception */ @SneakyThrows @Override public String queryBill(String billDate, String type) { log.warn("申请账单接口调用 {}", billDate); String url = ""; if ("tradebill".equals(type)) { url = WxApiType.TRADE_BILLS.getType(); } else if ("fundflowbill".equals(type)) { url = WxApiType.FUND_FLOW_BILLS.getType(); } else { throw new RuntimeException("不支持的账单类型"); } url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(url); httpGet.addHeader("Accept", "application/json"); //使用wxPayClient发送请求得到响应 CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 申请账单返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("申请账单异常, 响应码 = " + statusCode + ", 申请账单返回结果 = " + bodyAsString); } //获取账单下载地址 Gson gson = new Gson(); Map resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap.get("download_url"); } finally { response.close(); } } ``` #### 13.2 下载账单 ##### 13.2.1 WxPayController ```java @ApiOperation("下载账单") @GetMapping("/downloadbill/{billDate}/{type}") public R downloadBill( @PathVariable String billDate, @PathVariable String type) throws Exception { log.info("下载账单"); String result = wxPayService.downloadBill(billDate, type); return R.ok().data("result", result); } ``` ##### 13.2.2 WxPayService 接口: ```java String downloadBill(String billDate, String type); ``` 实现: ```java @Autowired private CloseableHttpClient wxPayNoSignClient; /** * 下载账单 * * @param billDate * @param type * @return * @throws Exception */ @SneakyThrows @Override public String downloadBill(String billDate, String type) { log.warn("下载账单接口调用 {}, {}", billDate, type); //获取账单url地址 String downloadUrl = this.queryBill(billDate, type); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(downloadUrl); httpGet.addHeader("Accept", "application/json"); //使用wxPayClient发送请求得到响应 CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 下载账单返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } } ```