# 微信小程序之V3微信支付
**Repository Path**: samsom/wx_pay_study
## Basic Information
- **Project Name**: 微信小程序之V3微信支付
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2022-07-21
- **Last Updated**: 2022-07-21
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 微信支付Native-V3系列
# 01、准备工作
## 01、适合人群
1、只要服务于企业开发中需要使用微信支付的小伙伴
2、必须要有一定的java基础
## 02、微信支付官网
[https://pay.weixin.qq.com](https://pay.weixin.qq.com/)
## 03、商家注册
https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal
## 04、小程序官网
https://mp.weixin.qq.com/wxamp/
## 05、小程序注册
https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token=
## 06、相关接口和文档
接口文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
Native接口规则:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
## 07、相关工具下载
微信小程序:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
hbuilderx工具:https://www.dcloud.io/hbuilderx.html
finalshell工具:http://www.hostbuf.com/t/988.html
# 02、商户注册&证书安装
官网说明:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml
## 01、接入前准备
> 商户/服务商在接入前首先要判断自己公司注册区域适用的接入模式,微信支付目前提供两种接入方式:直连模式和服务商模式。
### 直连模式:
信息、资金流:微信支付—>直连商户
直连模式,商户自行申请入驻微信支付,无需服务商协助。(商户平台申请)成为普通商户
### 服务商模式:

服务商模式,商户申请成为微信支付服务商,服务商自身无法作为一个普通商户直接发起交易,其发起交易必须传入相关特约商户商户号的参数信息。(服务商平台申请)成为普通服务商
请结合自身实际情况来选择接入模式。
## 02、接入步骤和流程
### 02-1、申请APPID
也就申请微信公众号或者微信小程序
> 注册地址:https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token=
### 02-2、微信支付注册商家-申请mchid
> 商家号注册:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal
注册成功后登录扫码登录即可、如下获取商户号:

或者

### 02-3、绑定APPID及mchid

### 02-4、配置API key
API v3密钥主要用于平台证书解密、回调信息解密。配置如下:
#### 登录微信商户平台,进入【账户中心 > API安全 > API安全】目录,点击【设置密钥】。

#### 在弹出窗口中点击“已沟通”。

#### 输入API密钥,内容为32位字符,包括数字及大小写字母。点击获取短信验证码。


#### 输入短信验证码,点击“确认”即设置成功。
它是一串32位的随机数,生成代码的随机数如下:
```java
/**
* 生成随机数
* @return
*/
public static String getNonceStr(){
return UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);
}
public static void main(String[] args) {
System.out.println(getNonceStr());
}
```
### 02-5、下载并配置商户证书
商户可登录微信商户平台,在【账户中心】->【API安全】->【API证书】目录下载证书
#### 从2018年底开始,微信支付新入驻机构及商户都将使用CA签发证书,在证书申请页面上点击“下载证书”。

#### 在弹出窗口内点击“下载证书工具”按钮下载证书工具。

#### 安装证书工具并打开,选择证书需要存储的路径后点击“申请证书”。

#### 在证书工具中,将复制的商户信息粘贴并点击“下一步”

#### 获取请求串



#### 生成证书串
步骤1 在【商户平台】-“复制证书串”环节,点击“复制证书串”按钮后;
步骤2 在【证书工具】-“复制请求串”环节,点击“下一步”按钮进入“粘贴证书串”环节;
步骤3 在【证书工具】-“粘贴证书串”环节,点击“粘贴”按钮后;
步骤4 点击“下一步”按钮,进入【证书工具】-“生成证书”环节



#### 在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件。

# 03、产品签约
## 01、目标
完成JSPAPI产品的签约
## 02、具体步骤

# 04、开发步骤及参数准备
官网:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
## 01、目标
01、完成项目的搭建
02、在项目的pom.xml中完成相关微信支付的依赖
03、定义支付相关的请求HttpUtils.java
04、定义支付WeixinNavtiveController.java完成支付逻辑
05、完成二维码和微信navtive支付地址的融合用流输入
07、定义页面支付页面和页面控制的Controller
08、使用
标签完成二维码的展示,并完成扫码支付
09、测试支付回调地址
## 02、微信支付依赖所需参数
```yaml
########################微信支付参数#######################################
#微信商户号
wechat:
mchId: 1370687602
#商家API证书序列号
mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C
#商户在微信公众平台申请服务号对应的APPID
appId: wx2f823cdc8dfba815
#回调报文解密V3密钥key
v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6
#微信获取平台证书列表地址
certificates:
url: https://api.mch.weixin.qq.com/v3/certificates
#微信统一下单Navtive的API地址,用于二维码支付
unifiedOrder:
url: https://api.mch.weixin.qq.com/v3/pay/transactions/native
#异步接收微信支付结果通知的回调地址
callback: http://api.kuangstudy.com/api/pay/callback
#商户证书私钥路径
key:
path: C:/tools/1370687602_20210504_cert/apiclient_key.pem
########################微信支付参数#######################################
```
## 02、整体流程图和架构如下

## 03、参数获取的来源
### 03-1、微信商户号
> wechat.mchId = xxxx

### 03-2、商家API证书序列号
> wechat.mchSerialNo = xxxx

### 03-3、商户在微信公众平台申请服务号对应的APPID
> wechat.appId = xxx
1、微信小程序地址:https://mp.weixin.qq.com/
2、扫码登录以后如下

### 03-4、回调报文解密V3密钥key
> wechat.v3Key = xxxx

### 03-5、微信获取平台证书列表地址用于签名验证
```
#这个值是固定的wechat.certificates.url = https://api.mch.weixin.qq.com/v3/certificates
```
参考:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml

> 关于为什么要签名验证如下有说明:
> 官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
> `微信支付API v3 key要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized。`
#### 签名代码
```java
import okhttp3.HttpUrl;
import java.security.Signature;
import java.util.Base64;
// Authorization:
// GET - getToken("GET", httpurl, "")
// POST - getToken("POST", httpurl, json)
String schema = "WECHATPAY2-SHA256-RSA2048";
HttpUrl httpurl = HttpUrl.parse(url);
String getToken(String method, HttpUrl url, String body) {
String nonceStr = "your nonce string";
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + yourMerchantId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + yourCertificateSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
String sign(byte[] message) {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(yourPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
```
### 03-6、微信统一下单Navtive的API地址,用于二维码支付
> wechat.unifiedOrder.url = https://api.mch.weixin.qq.com/v3/pay/transactions/native
来源地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

#### navtive支付请求官方代码,仅做参考
```java
public void CreateOrder() throws Exception{
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 请求body参数
String reqdata = "{"
+ "\"time_expire\":\"2018-06-08T10:34:56+08:00\","
+ "\"amount\": {"
+ "\"total\":100,"
+ "\"currency\":\"CNY\""
+ "},"
+ "\"mchid\":\"1230000109\","
+ "\"description\":\"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\":\"https://www.weixin.qq.com/wxpay/pay.php\","
+ "\"out_trade_no\":\"1217752501201407033233368018\","
+ "\"goods_tag\":\"WXG\","
+ "\"appid\":\"wxd678efh567hg6787\","
+ "\"attach\":\"自定义数据说明\","
+ "\"detail\": {"
+ "\"invoice_id\":\"wx123\","
+ "\"goods_detail\": ["
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "},"
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "}"
+ "],"
+ "\"cost_price\":608800"
+ "},"
+ "\"scene_info\": {"
+ "\"store_info\": {"
+ "\"address\":\"广东省深圳市南山区科技中一道10000号\","
+ "\"area_code\":\"440305\","
+ "\"name\":\"腾讯大厦分店\","
+ "\"id\":\"0001\""
+ "},"
+ "\"device_id\":\"013467007045764\","
+ "\"payer_client_ip\":\"14.23.150.211\""
+ "}"
+ "}";
StringEntity entity = new StringEntity(reqdata);
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
```
### 03-7、异步接收微信支付结果通知的回调地址
> wechat.callback = xxxx

### 03-8、商户证书私钥路径
> wechat.key.path = xxxxxxx
> 在windows中存储在你指定的目录下如下:

#### 生成证书串
步骤1 在【商户平台】-“复制证书串”环节,点击“复制证书串”按钮后;
步骤2 在【证书工具】-“复制请求串”环节,点击“下一步”按钮进入“粘贴证书串”环节;
步骤3 在【证书工具】-“粘贴证书串”环节,点击“粘贴”按钮后;
步骤4 点击“下一步”按钮,进入【证书工具】-“生成证书”环节


#### 在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件。


# 05、参数接口API
## 01、目录
对统一下单API的理解
## 02、概述
官网参考:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户Native支付统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
接口说明
> 适用对象: 直连商户
> 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/native
> 请求方式:POST
## 03、必填请求参数
参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
## 04、返回值是:
```
{
"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"
}
```
## 05、其实简单原理如下:


# 06、实战开发-框架的搭建
## 01、目标
完成微信支付native的框架搭建
## 02、实现步骤
1、新建一个springboot单体架构工程
2、在pom.xml中配置和整合ssm的依赖
3、配置环境隔离以及数据库连接配置
4、新建数据库weixindb和数据库表kss_courses
5、新建课程产品对应的业务entity、mapper、service和controller
6、静态资源导入(ksd.css,jquery,vue,axios)
7、新建index.html和coursedetail.html
8、在IndexController配置路由进行对应的index和coursedetail.html跳转
9、实现课程列表业务接口的对接工作
## 03、具体实现
### 03-1、新建一个springboot单体架构工程

### 03-2、在pom.xml中配置和整合ssm的依赖
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-freemarker
org.projectlombok
lombok
1.18.12
org.springframework.boot
spring-boot-starter-test
test
org.apache.httpcomponents
httpclient
4.5.3
org.apache.httpcomponents
httpmime
4.5.2
com.google.zxing
core
3.3.0
commons-io
commons-io
2.6
com.google.code.gson
gson
2.8.6
mysql
mysql-connector-java
5.1.10
com.baomidou
mybatis-plus-boot-starter
3.4.0
com.fasterxml.jackson.dataformat
jackson-dataformat-avro
org.apache.commons
commons-lang3
3.6
```
### 03-3、配置环境隔离以及数据库链接配置
#### application.yml
```yaml
spring:
profiles:
active: dev
freemarker:
suffix: .html
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
locale: zh_CN
# 解决json返回过程中long的精度丢失问题
generator:
write-numbers-as-strings: true
write-bigdecimal-as-plain: true
```
#### application-dev.yml
```yaml
server:
port: 8080
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/weixindb?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
hikari:
connection-timeout: 60000
validation-timeout: 3000
idle-timeout: 60000
login-timeout: 5
max-lifetime: 60000
maximum-pool-size: 400
minimum-idle: 100
read-only: false
logging:
level:
root: debug
```
#### application-prod.yml
```yaml
server:
port: 80
# 数据库连接
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://8.210.51.92:3306/kuangstudydb?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: kuangstudydb
password: BNJptkrCRayf6NEw
hikari:
connection-timeout: 60000
validation-timeout: 3000
idle-timeout: 60000
login-timeout: 5
max-lifetime: 60000
maximum-pool-size: 400
minimum-idle: 100
read-only: false
logging:
level:
root: info
```
### 03-4、新建数据库weixindb和数据库表kss_courses
```sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50733
Source Host : localhost:3306
Source Database : weixindb
Target Server Type : MYSQL
Target Server Version : 50733
File Encoding : 65001
Date: 2021-05-10 20:13:39
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for kss_courses
-- ----------------------------
DROP TABLE IF EXISTS `kss_courses`;
CREATE TABLE `kss_courses` (
`courseid` varchar(32) NOT NULL COMMENT '课程唯一id',
`title` varchar(100) DEFAULT NULL COMMENT '课程标题',
`intro` varchar(500) DEFAULT NULL COMMENT '课程简短介绍',
`img` varchar(300) DEFAULT NULL COMMENT '课程封面地址',
`price` decimal(10,2) DEFAULT NULL COMMENT '课程的活动价',
`status` int(1) DEFAULT NULL COMMENT '状态:已发布/未发布',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`courseid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of kss_courses
-- ----------------------------
INSERT INTO `kss_courses` VALUES ('1317503462556848129', '预科阶段', '学习编程之前你要了解的知识!', '/assert/course/c1/02.jpg', '0.01', '1', '2020-10-18 00:31:18', '2021-04-01 10:56:38');
INSERT INTO `kss_courses` VALUES ('1317503769349214209', '入门环境搭建', '工欲善其事,必先利其器!', '/assert/course/c1/03.jpg', '0.01', '1', '2020-10-18 00:32:31', '2021-04-01 10:53:10');
INSERT INTO `kss_courses` VALUES ('1317504142650658818', '基础语法学习', '基础决定你未来的高度!', '/assert/course/c1/04.jpg', '0.01', '1', '2020-10-18 00:34:00', '2021-04-01 10:54:18');
INSERT INTO `kss_courses` VALUES ('1317504447027105793', '流程控制学习', '程序的本质就是这些!', '/assert/course/c1/05.jpg', '0.01', '1', '2020-10-18 00:35:13', '2021-04-01 10:56:03');
INSERT INTO `kss_courses` VALUES ('1317504610634321921', '方法详解', '封装的思想!', '/assert/course/c1/06.jpg', '0.01', '1', '2020-10-18 00:35:52', '2021-04-01 10:55:04');
INSERT INTO `kss_courses` VALUES ('1317504817342205954', '数组详解', '最简单的数据结构!', '/assert/course/c1/07.jpg', '0.01', '1', '2020-10-18 00:35:52', '2020-10-18 00:35:52');
INSERT INTO `kss_courses` VALUES ('1317504988834713602', '面向对象编程', 'Java的精髓OOP!', '/assert/course/c1/08.jpg', '0.01', '1', '2020-10-18 00:35:52', '2020-10-18 00:35:52');
INSERT INTO `kss_courses` VALUES ('1377518279077142529', '第三方支付课程-支付宝', '第三方支付课程-支付宝', '/assert/course/c10/07.jpg', '0.01', '1', '2020-10-18 00:18:08', '2021-04-01 10:54:25');
```
### 03-5、新建课程产品对应的业务entity、mapper、service和controller
#### entity
```java
package com.kuangstudy.wxpay.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1012:39
*/
@Data
@Accessors(chain = true)
@ToString
@AllArgsConstructor
@NoArgsConstructor
@TableName("kss_courses")
public class Course {
// 课程id
@TableId(type = IdType.ID_WORKER_STR)
private String courseid;
// 课程标题
private String title;
// 课程介绍
private String intro;
// 课程封面
private String img;
// 课程价格
private String price;
// 课程状态 0未发布1发布
private Integer status;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
```
#### mapper
```java
package com.kuangstudy.wxpay.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kuangstudy.wxpay.entity.Course;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1012:44
*/
public interface CourseMapper extends BaseMapper {
}
```
#### service和serviceimpl
```java
package com.kuangstudy.wxpay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kuangstudy.wxpay.entity.Course;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1012:46
*/
public interface CourseService extends IService {
}
```
### 03-6、静态资源导入(ksd.css,jquery,vue,axios)
> 代码篇幅过长,详看视频,或者相关文件中查找对应的资源文件放入到static目录下,如下:
> 
### 03-7、新建index.html和coursedetail.html

#### index.html
```html
Document
学相伴微信支付V3-Native系列
你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }
```
#### coursedetail.html
```html
Document
学相伴微信支付V3-Native系列
你购买的课程是:${course.title},价格是:¥${course.price}
```
### 03-8、在IndexController配置路由进行对应的index和coursedetail.html跳转
```html
package com.kuangstudy.wxpay.controller;
import com.kuangstudy.wxpay.service.CourseService;
import com.kuangstudy.wxpay.vo.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
@Controller
public class IndexController {
@Autowired
CourseService courseService;
/**
* 跳转课程首页
* @return
*/
@GetMapping("index")
public String index() {
return "index";
}
/**
* 查询所有课程
* @return
*/
@ResponseBody
@GetMapping("/loadcourse")
public R loadCourse() {
return R.ok().data("courses", courseService.list());
}
/**
* 跳转课程明细
* @param id
* @param modelMap
* @return
*/
@GetMapping("coursedetail/{id}")
public String coursedetail(@PathVariable("id")String id, ModelMap modelMap){
modelMap.put("course",courseService.getById(id));
return "coursedetail";
}
}
```
### 03-9、实现课程列表业务接口的对接工作并访问
> 在浏览器访问http://localhost:8080/index

# 07、实战开发-微信支付接口定义
官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
## 01、目标
完成微信支付native的接口定义和二维码生成
> 商户Native支付统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
## 02、接口说明
- 适用对象: 直连商户
- 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/native
- 请求方式:POST
## 03、官方图解

## 04、飞哥图解

## 05、实现步骤
01、在开发环境的配置和在生成环境的配置隔离
02、定义WechatPayUtils.java获取商家私钥和平台证书
03、在配置类中实现CommandLineRunner接口完成参数的注入和初始化
04、定义微信支付接口请求类HttpUtils.java
05、定义微信支付接口WeixinNavtiveController.java类
06、完成支付接口的与zxing生成二维码
07、进行测试生成微信支付二维码
## 06、实现步骤
### 06-01、在开发环境的配置和在生成环境的配置隔离
#### 在application-dev.yml新增
```yaml
########################微信支付参数#######################################
#微信商户号
wechat:
mchId: 1370687602
#商家API证书序列号
mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C
#商户在微信公众平台申请服务号对应的APPID
appId: wx2f823cdc8dfba815
#回调报文解密V3密钥key
v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6
#微信获取平台证书列表地址
certificates:
url: https://api.mch.weixin.qq.com/v3/certificates
#微信统一下单Navtive的API地址,用于二维码支付
unifiedOrder:
url: https://api.mch.weixin.qq.com/v3/pay/transactions/native
jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
#异步接收微信支付结果通知的回调地址
callback: https://api.kuangstudy.com/api/pay/callback
#商户证书私钥路径
key:
path: D:/1370687602_20210504_cert/apiclient_key.pem
###########################################################################
```
#### 在application-prod.yml新增
```yaml
########################微信支付参数#######################################
#微信商户号
wechat:
mchId: 1370687602
#商家API证书序列号
mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C
#商户在微信公众平台申请服务号对应的APPID
appId: wx2f823cdc8dfba815
#回调报文解密V3密钥key
v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6
#微信获取平台证书列表地址
certificates:
url: https://api.mch.weixin.qq.com/v3/certificates
#微信统一下单Navtive的API地址,用于二维码支付
unifiedOrder:
url: https://api.mch.weixin.qq.com/v3/pay/transactions/native
jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
#异步接收微信支付结果通知的回调地址
callback: https://api.kuangstudy.com/api/pay/callback
#商户证书私钥路径
key:
path: /www/web/weixincert/apiclient_key.pem
###########################################################################
```
#### 注意事项1
> 区别点在于:商家证书的私钥路径不同,因为一个是linux环境一个是windows环境。
> 在部署linux环境的时候,可能会引发加密的的java异常。这个时候需要手动配置jdk和jdk-security安全相关的环境才可以校验通过。
>
> ```java
> #1:先解压jdk-8u291-linux-i586.tar.gz
> tar -zxvf jdk-8u291-linux-i586.tar.gz
> #2:然后把jce_policy-8.zip中所有local_policy.jar和US_export_policy.jar放入到 /www/jdk1.8.0_291/jre/lib
> #3:编辑/etc/profile文件如下图vim /etc/profile
> ```
>
> 
```
# 4: 然后重启即可> source /etc/profile# 5、检验jdk是否生效>java -version
```

#### 注意事项2
> 合成二维码的时候需要用的一个logo。在本机放在工程的resources即可。如果是在linux系统下,这个时候就需要放在指定的目录如下:
> 
### 06-02、定义WechatPayUtils.java获取商家私钥和平台证书
> 参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
```java
package com.kuangstudy.wxpay.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.kuangstudy.wxpay.common.KsdStaticParameter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 参考官网 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
*/
public class WechatPayUtils {
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
System.out.println("filename:"+filename);
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 生成token
* @param method
* @param url
* @param body
* @return
* @throws Exception
*/
public static String getToken(String method, URL url, String body) throws Exception {
String nonceStr = getNonceStr();
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "WECHATPAY2-SHA256-RSA2048 "+"mchid=\"" + KsdStaticParameter.mchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + KsdStaticParameter.mchSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
/**
* 生成token
* @param method
* @param url
* @param body
* @return
* @throws Exception
*/
public static Map getTokenWeixin(String method, URL url, String body,String prepay_id) throws Exception {
String nonceStr = getNonceStr();
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
Map map = new HashMap<>();
map.put("timeStamp",String.valueOf(timestamp));
map.put("nonceStr",nonceStr);
map.put("package", "prepay_id=" + prepay_id);
map.put("signType","RSA");
map.put("paySign",signature);
return map;
}
/**
* 生成签名
* @param message
* @return
* @throws Exception
*/
public static String sign(byte[] message) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(KsdStaticParameter.privateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 生成签名串
* @param method
* @param url
* @param timestamp
* @param nonceStr
* @param body
* @return
*/
public static String buildMessage(String method, URL url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.getPath();
if (url.getQuery() != null) {
canonicalUrl += "?" + url.getQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
/**
* 生成随机数
* @return
*/
public static String getNonceStr(){
return UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);
}
/**
* 获取平台证书
* @return
*/
public static Map refreshCertificate() throws Exception {
Map certificateMap = new HashMap();
// 1: 执行get请求
JsonNode jsonNode = HttpUtils.doGet(KsdStaticParameter.certificatesUrl);
// 2: 获取平台验证的相关参数信息
JsonNode data = jsonNode.get("data");
if(data!=null){
for(int i=0;i certificateMap = new ConcurrentHashMap<>();
public static void certificateMap(String serialNo) {
}
}
```
### 06-04、定义微信支付接口请求类HttpUtils.java
```java
package com.kuangstudy.wxpay.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class HttpUtils {
private static final ObjectMapper JSON=new ObjectMapper();
/**
* get方法
* @param url
* @return
*/
public static JsonNode doGet(String url){
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpGet httpget = new HttpGet(url);
httpget.addHeader("Content-Type", "application/json;charset=UTF-8");
httpget.addHeader("Accept", "application/json");
try{
String token = WechatPayUtils.getToken("GET", new URL(url), "");
httpget.addHeader("Authorization", token);
CloseableHttpResponse httpResponse = httpClient.execute(httpget);
if(httpResponse.getStatusLine().getStatusCode() == 200){
String jsonResult = EntityUtils.toString( httpResponse.getEntity());
return JSON.readTree(jsonResult);
}else{
System.err.println(EntityUtils.toString( httpResponse.getEntity()));
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
/**
* 封装post
* @return
*/
public static Map doPost(String url, String body){
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Content-Type","application/json;chartset=utf-8");
httpPost.addHeader("Accept", "application/json");
try{
String token = WechatPayUtils.getToken("POST", new URL(url), body);
httpPost.addHeader("Authorization", token);
if(body==null){
throw new IllegalArgumentException("data参数不能为空");
}
StringEntity stringEntity = new StringEntity(body,"utf-8");
httpPost.setEntity(stringEntity);
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String jsonResult = EntityUtils.toString(httpEntity);
return JSON.readValue(jsonResult,HashMap.class);
}else{
System.err.println("微信支付错误信息"+EntityUtils.toString(httpEntity));
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
```
### 06-05、定义微信支付接口WeixinNavtiveController.java类
```java
package com.kuangstudy.wxpay.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.kuangstudy.wxpay.common.KsdStaticParameter;
import com.kuangstudy.wxpay.utils.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
@RestController
@Log4j2
public class WeixinNavtiveController {
@Value("${spring.profiles.active}")
private String profiles;
/**
* 付款订单Api,根据传入的订单号 生成付款二维码
*
* @param courseid
* @param response
*/
@RequestMapping("/weixinpay")
@ResponseBody
public byte[] weixinpay(HttpServletResponse response) throws JsonProcessingException {
//封装请求参数
Map map = new HashMap();
map.put("appid", KsdStaticParameter.appId);
map.put("mchid", KsdStaticParameter.mchId);
//临时写死配置
map.put("description", "测试数据");
map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + "");
map.put("notify_url", KsdStaticParameter.notifyUrl);
Map amount = new HashMap();
//订单金额 单位分
amount.put("total", Integer.parseInt(getMoney("0.01")));
amount.put("currency", "CNY");
map.put("amount", amount);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(map);
Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body);
String codeUrl = stringObjectMap.get("code_url").toString();
//生成付款二维码
//生成二维码配置
Map hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
try {
// 7: 生成微信支付二维码
ByteArrayOutputStream output = new ByteArrayOutputStream();
String logopath = null;
if(profiles.equals("dev")) {
logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
}else{
logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath();
}
BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false);
ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
ImageIO.write(buff, "JPEG", imageOut);
imageOut.close();
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return FileCopyUtils.copyToByteArray(input);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 元转换成分
*
* @param money
* @return
*/
private String getMoney(String money) {
if (money == null || money.equalsIgnoreCase("0")) {
return "";
}
// 金额转化为分为单位
// 处理包含, ¥ 或者$的金额
String currency = money.replaceAll("\\$|\\¥|\\,", "");
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}
}
```
### 06-06、完成支付接口的与zxing生成二维码
```java
// 7: 生成微信支付二维码
ByteArrayOutputStream output = new ByteArrayOutputStream();
String logopath = null;
if(profiles.equals("dev")) {
logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
}else{
logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath();
}
BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false);
ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
ImageIO.write(buff, "JPEG", imageOut);
imageOut.close();
```
### 06-07、进行测试生成微信支付二维码
```java
学相伴微信支付V3-Native系列
你购买的课程是:预科阶段,价格是:¥0.01
```
# 08、实战开发-业务对接微信支付
## 01、目标
完成课程产品和微信支付的对接工作
## 02、实现步骤
1、点击课程完成每个产品和微信支付的生成和绑定
2、将微信支付中静态的数据修改成课程数据即可
## 03、具体操作
### 03-1、点击课程完成每个产品和微信支付的生成和绑定
```java
Document
学相伴微信支付V3-Native系列
你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }
```
### 03-2、将微信支付中静态的数据修改成课程数据即可
```java
package com.kuangstudy.wxpay.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.kuangstudy.wxpay.common.KsdStaticParameter;
import com.kuangstudy.wxpay.entity.Course;
import com.kuangstudy.wxpay.service.CourseService;
import com.kuangstudy.wxpay.utils.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
@RestController
@Log4j2
public class WeixinNavtiveController {
@Autowired
private CourseService courseService;
@Value("${spring.profiles.active}")
private String profiles;
/**
* 付款订单Api,根据传入的订单号 生成付款二维码
*
* @param courseid
* @param response
*/
@RequestMapping("/weixinpay")
@ResponseBody
public byte[] unifiedOrder(String courseid, HttpServletResponse response) throws JsonProcessingException {
Course course = courseService.getById(courseid);
if (course == null) return null;
//封装请求参数
Map map = new HashMap();
map.put("appid", KsdStaticParameter.appId);
map.put("mchid", KsdStaticParameter.mchId);
//临时写死配置
map.put("description", course.getTitle());
map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + "");
map.put("notify_url", KsdStaticParameter.notifyUrl);
Map amount = new HashMap();
//订单金额 单位分
amount.put("total", Integer.parseInt(getMoney(course.getPrice())));
amount.put("currency", "CNY");
map.put("amount", amount);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(map);
Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body);
String codeUrl = stringObjectMap.get("code_url").toString();
//生成付款二维码
//生成二维码配置
Map hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
try {
// 7: 生成微信支付二维码
ByteArrayOutputStream output = new ByteArrayOutputStream();
String logopath = null;
if(profiles.equals("dev")) {
logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
}else{
logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath();
}
BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false);
ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
ImageIO.write(buff, "JPEG", imageOut);
imageOut.close();
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return FileCopyUtils.copyToByteArray(input);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 元转换成分
*
* @param money
* @return
*/
private String getMoney(String money) {
if (money == null || money.equalsIgnoreCase("0")) {
return "";
}
// 金额转化为分为单位
// 处理包含, ¥ 或者$的金额
String currency = money.replaceAll("\\$|\\¥|\\,", "");
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}
}
```
# 09、实战开发-回调地址notify_url&解密
## 01、目标
1、配置微信支付回调地址
2、回调报文解密
3、定义回调支付逻辑,完成回调地址获取微信支付响应的参数
## 02、具体实现
### 02-1、配置支付回调地址
参考官网:[https://pay.weixin.qq.com](https://pay.weixin.qq.com/)

### 02-2、回调报文解密
参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。
```java
package com.kuangstudy.wxpay.utils;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
```
### 02-3、定义回调支付逻辑,完成回调地址获取微信支付响应的参数
参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
```java
package com.kuangstudy.wxpay.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.kuangstudy.wxpay.common.KsdStaticParameter;
import com.kuangstudy.wxpay.entity.Course;
import com.kuangstudy.wxpay.service.CourseService;
import com.kuangstudy.wxpay.utils.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@RestController
@Log4j2
@RequestMapping("/api")
public class WeixinNavtiveController {
@Autowired
private CourseService courseService;
@Value("${spring.profiles.active}")
private String profiles;
/**
* 付款订单Api,根据传入的订单号 生成付款二维码
*
* @param courseid
* @param response
*/
@RequestMapping("/weixinpay")
@ResponseBody
public byte[] weixinpay(String courseid, HttpServletResponse response) throws JsonProcessingException {
Course course = courseService.getById(courseid);
if (course == null) return null;
//封装请求参数
Map map = new HashMap();
map.put("appid", KsdStaticParameter.appId);
map.put("mchid", KsdStaticParameter.mchId);
//临时写死配置
map.put("description", course.getTitle());
map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + "");
map.put("notify_url", KsdStaticParameter.notifyUrl);
Map amount = new HashMap();
// 附属参数
Map attachMap = new HashMap<>();
attachMap.put("courseid",courseid);
attachMap.put("userid",1);
map.put("attach",JsonUtil.obj2String(attachMap));
//订单金额 单位分
amount.put("total", Integer.parseInt(getMoney(course.getPrice())));
amount.put("currency", "CNY");
map.put("amount", amount);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(map);
Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body);
String codeUrl = stringObjectMap.get("code_url").toString();
//生成付款二维码
//生成二维码配置
Map hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
try {
// 7: 生成微信支付二维码
ByteArrayOutputStream output = new ByteArrayOutputStream();
String logopath = null;
if(profiles.equals("dev")) {
logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
}else{
logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath();
}
BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false);
ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
ImageIO.write(buff, "JPEG", imageOut);
imageOut.close();
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return FileCopyUtils.copyToByteArray(input);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 支付回调通知
*
* @param body
* @param request
* @return
*/
@PostMapping("pay/callback")
public Map orderPayCallback(@RequestBody Map body, HttpServletRequest request) {
log.info("1----------->微信支付回调开始");
Map result = new HashMap();
//1:获取微信支付回调的获取签名信息
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
ObjectMapper objectMapper = new ObjectMapper();
try {
// 2: 开始解析报文体
String data = objectMapper.writeValueAsString(body);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
//3:获取应答签名
String sign = request.getHeader("Wechatpay-Signature");
//4:获取平台对应的证书
String serialNo = request.getHeader("Wechatpay-Serial");
if (!KsdStaticParameter.certificateMap.containsKey(serialNo)) {
KsdStaticParameter.certificateMap = WechatPayUtils.refreshCertificate();
}
X509Certificate x509Certificate = KsdStaticParameter.certificateMap.get(serialNo);
if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) {
throw new IllegalArgumentException("微信支付签名验证失败:" + message);
}
log.info("签名验证成功");
Map resource = (Map) body.get("resource");
// 5:回调报文解密
AesUtil aesUtil = new AesUtil(KsdStaticParameter.v3Key.getBytes());
//解密后json字符串
String decryptToString = aesUtil.decryptToString(
resource.get("associated_data").getBytes(),
resource.get("nonce").getBytes(),
resource.get("ciphertext"));
log.info("2------------->decryptToString====>{}",decryptToString);
//6:获取微信支付返回的信息
Map jsonData = objectMapper.readValue(decryptToString, Map.class);
//7: 支付状态的判断 如果是success就代表支付成功
if ("SUCCESS".equals(jsonData.get("trade_state"))) {
// 8:获取支付的交易单号,流水号,和附属参数
String out_trade_no = jsonData.get("out_trade_no").toString();
String transaction_id = jsonData.get("transaction_id").toString();
String attach = jsonData.get("attach").toString();
//TODO 根据订单号查询支付状态,如果未支付,更新支付状态 为已支付
log.info("3----------->微信支付成功,支付流水号是:{},附属参数是:{}", out_trade_no,attach);
log.info("4----------->微信支付成功,支付流水号是:{}", transaction_id);
// 转换附属参数
HashMap map = JsonUtil.string2Obj(attach,HashMap.class);
// 9:保存用户支付信息
UserPay userPay = new UserPay();
userPay.setUserid(String.valueOf(map.get("userid")));
userPay.setNickname("飞哥");
userPay.setPrice("1");
userPay.setCourseid(String.valueOf(map.get("courseid")));
userPay.setTradeno(out_trade_no);
userPay.setCreateTime(new Date());
userPayService.saveOrUpdate(userPay);
}
result.put("code", "SUCCESS");
result.put("message", "成功");
} catch (Exception e) {
result.put("code", "fail");
result.put("message", "系统错误");
e.printStackTrace();
}
return result;
}
/**
* 元转换成分
*
* @param money
* @return
*/
private String getMoney(String money) {
if (money == null || money.equalsIgnoreCase("0")) {
return "";
}
// 金额转化为分为单位
// 处理包含, ¥ 或者$的金额
String currency = money.replaceAll("\\$|\\¥|\\,", "");
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}
}
```
### 02-3、测试结果如下

# 10、实战开发-保存用户支支付明细
## 01、目标
1、定义用户支付订单明细表
2、定义用户支付订单entity、service、mapper、controller
3、微信支付回调中添加用户支付成功业务
4、完成支付回调处理
## 02、具体实现
### 02-1、定义用户支付订单明细表
```sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50733
Source Host : localhost:3306
Source Database : weixindb
Target Server Type : MYSQL
Target Server Version : 50733
File Encoding : 65001
Date: 2021-05-10 23:27:10
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for kss_user_pay
-- ----------------------------
DROP TABLE IF EXISTS `kss_user_pay`;
CREATE TABLE `kss_user_pay` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`courseid` varchar(32) NOT NULL COMMENT '课程唯一id',
`userid` varchar(32) DEFAULT NULL COMMENT '课程标题',
`nickname` varchar(500) DEFAULT NULL COMMENT '课程简短介绍',
`tradeno` varchar(300) DEFAULT NULL COMMENT '课程封面地址',
`price` decimal(10,2) DEFAULT NULL COMMENT '课程的活动价',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of kss_user_pay
-- ----------------------------
INSERT INTO `kss_user_pay` VALUES ('1', '1317503462556848129', '1', '飞哥', '11', '1.00', '2021-05-10 23:06:58', null);
```
### 02-2、定义用户支付订单entity,service,mapper,controller
#### entity
```java
package com.kuangstudy.wxpay.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1022:43
*/
@Data
@Accessors(chain = true)
@ToString
@AllArgsConstructor
@NoArgsConstructor
@TableName("kss_user_pay")
public class UserPay {
// 主键
@TableId(type = IdType.AUTO)
private Integer id;
// 购买课程
private String courseid;
// 支付用户
private String userid;
// 支付用户名称
private String nickname;
// 课程价格
private String price;
// 交易流水号
private String tradeno;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
```
### mapper
```java
package com.kuangstudy.wxpay.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kuangstudy.wxpay.entity.UserPay;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1022:43
*/
public interface UserPayMapper extends BaseMapper {
}
```
### service
```java
package com.kuangstudy.wxpay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kuangstudy.wxpay.entity.UserPay;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1022:43
*/
public interface UserPayService extends IService {
}
```
### serviceimpl
```java
package com.kuangstudy.wxpay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kuangstudy.wxpay.entity.UserPay;
import com.kuangstudy.wxpay.mapper.UserPayMapper;
import org.springframework.stereotype.Service;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1022:43
*/
@Service
public class UserPayServiceImpl extends ServiceImpl implements UserPayService {
}
```
### 02-3、微信支付回调中添加用户支付成功业务
```java
package com.kuangstudy.wxpay.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.kuangstudy.wxpay.common.KsdStaticParameter;
import com.kuangstudy.wxpay.entity.Course;
import com.kuangstudy.wxpay.entity.UserPay;
import com.kuangstudy.wxpay.service.CourseService;
import com.kuangstudy.wxpay.service.UserPayService;
import com.kuangstudy.wxpay.utils.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@RestController
@Log4j2
@RequestMapping("/api")
public class WeixinNavtiveController {
@Autowired
private CourseService courseService;
@Autowired
private UserPayService userPayService;
@Value("${spring.profiles.active}")
private String profiles;
/**
* 付款订单Api,根据传入的订单号 生成付款二维码
*
* @param courseid
* @param response
*/
@RequestMapping("/weixinpay")
@ResponseBody
public byte[] weixinpay(String courseid, HttpServletResponse response) throws JsonProcessingException {
Course course = courseService.getById(courseid);
if (course == null) return null;
//封装请求参数
Map map = new HashMap();
map.put("appid", KsdStaticParameter.appId);
map.put("mchid", KsdStaticParameter.mchId);
//临时写死配置
map.put("description", course.getTitle());
map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + "");
map.put("notify_url", KsdStaticParameter.notifyUrl);
Map amount = new HashMap();
// 附属参数
Map attachMap = new HashMap<>();
attachMap.put("courseid",courseid);
attachMap.put("userid",1);
map.put("attach",JsonUtil.obj2String(attachMap));
//订单金额 单位分
amount.put("total", Integer.parseInt(getMoney(course.getPrice())));
amount.put("currency", "CNY");
map.put("amount", amount);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(map);
Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body);
String codeUrl = stringObjectMap.get("code_url").toString();
//生成付款二维码
//生成二维码配置
Map hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
try {
// 7: 生成微信支付二维码
ByteArrayOutputStream output = new ByteArrayOutputStream();
String logopath = null;
if(profiles.equals("dev")) {
logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
}else{
logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath();
}
BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false);
ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
ImageIO.write(buff, "JPEG", imageOut);
imageOut.close();
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return FileCopyUtils.copyToByteArray(input);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 支付回调通知
*
* @param body
* @param request
* @return
*/
@RequestMapping("pay/callback")
public Map orderPayCallback(@RequestBody Map body, HttpServletRequest request) {
log.info("1----------->微信支付回调开始");
Map result = new HashMap();
//获取签名信息
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
ObjectMapper objectMapper = new ObjectMapper();
try {
String data = objectMapper.writeValueAsString(body);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
//获取应答签名
String sign = request.getHeader("Wechatpay-Signature");
//获取平台对应的证书
String serialNo = request.getHeader("Wechatpay-Serial");
if (!KsdStaticParameter.certificateMap.containsKey(serialNo)) {
KsdStaticParameter.certificateMap = WechatPayUtils.refreshCertificate();
}
X509Certificate x509Certificate = KsdStaticParameter.certificateMap.get(serialNo);
if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) {
throw new IllegalArgumentException("微信支付签名验证失败:" + message);
}
log.info("签名验证成功");
Map resource = (Map) body.get("resource");
AesUtil aesUtil = new AesUtil(KsdStaticParameter.v3Key.getBytes());
//解密后json字符串
String decryptToString = aesUtil.decryptToString(
resource.get("associated_data").getBytes(),
resource.get("nonce").getBytes(),
resource.get("ciphertext"));
log.info("2------------->decryptToString====>{}",decryptToString);
Map jsonData = objectMapper.readValue(decryptToString, Map.class);
//支付状态的判断
if ("SUCCESS".equals(jsonData.get("trade_state"))) {
String out_trade_no = jsonData.get("out_trade_no").toString();
String transaction_id = jsonData.get("transaction_id").toString();
String attach = jsonData.get("attach").toString();
//TODO 根据订单号查询支付状态,如果未支付,更新支付状态 为已支付
log.info("3----------->微信支付成功,支付流水号是:{},附属参数是:{}", out_trade_no,attach);
log.info("4----------->微信支付成功,支付流水号是:{}", transaction_id);
// 保存用户订单明细
UserPay userPay = new UserPay();
userPay.setUserid("1");
userPay.setNickname("飞哥");
userPay.setPrice("1");
userPay.setTradeno(out_trade_no);
userPay.setCreateTime(new Date());
userPayService.saveOrUpdate(userPay);
}
result.put("code", "SUCCESS");
result.put("message", "成功");
} catch (Exception e) {
result.put("code", "fail");
result.put("message", "系统错误");
e.printStackTrace();
}
return result;
}
/**
* 元转换成分
*
* @param money
* @return
*/
private String getMoney(String money) {
if (money == null || money.equalsIgnoreCase("0")) {
return "";
}
// 金额转化为分为单位
// 处理包含, ¥ 或者$的金额
String currency = money.replaceAll("\\$|\\¥|\\,", "");
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}
}
```
### 02-4、完成支付回调处理

# 11、实战开发-回调监听和支付超时
## 01、目标
1、定义监听支付成功的MonitorController
2、利用轮询机制监听微信支付成功的回调处理
3、优化轮询机制的支付超时问题
## 02、具体实现
### 02-1、定义监听支付成功的MonitorController
```java
package com.kuangstudy.wxpay.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.kuangstudy.wxpay.entity.UserPay;
import com.kuangstudy.wxpay.service.UserPayService;
import com.kuangstudy.wxpay.vo.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author 徐柯
* @Title:
* @Package
* @Description:
* @date 2021/5/1022:59
*/
@Controller
@RequestMapping("/api")
public class MonitorController {
@Autowired
private UserPayService userPayService;
/**
* 定义监听类
* @param courseid
* @return
*/
@ResponseBody
@GetMapping("/paySuccess")
public R paySuccess(String courseid) {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("courseid", courseid);
queryWrapper.eq("userid", 1);
int count = userPayService.count(queryWrapper);
return count > 0 ? R.ok() : R.error();
}
}
```
### 02-2、利用轮询机制监听微信支付成功的回调处理
```html
Document
学相伴微信支付V3-Native系列
你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }
```
### 02-3、优化轮询机制的支付超时问题
```html
Document
学相伴微信支付V3-Native系列
你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }
```
# 12、项目的发布和部署
## 01、目标
完成项目的发布和部署
## 02、步骤
1、准备一个阿里云服务器
2、将项目打成jar包
3、部署到云服务器接口
## 03、项目打包命令
```shell
mvn clean package -Dmaven.skip.test=true
```
## 04、启动项目
```shell
# 启动项目带端口和日志
nohup java -jar xxxx.jar --server.port=8080 >>1.txt &
# 启动项目
nohup java -jar xxxx.jar >>1.txt &
```