微信支付开发文件:
微信支付-开发者文档 (qq.com)
源码地址:前端和后端都有
链接: https://pan.baidu.com/s/1Nx-jLJ1gaZD0rmoGOw9MhA
提取码: qwer
用户显示微信钱包中的用户
“
付款码
”
给商家,商家扫描后直接完成支付,适用于线下面对面出纳场景。
离线场所:商户显示支付二维码,用户使用微信扫描二维码,输入支付金额,完成支付
付。
微信官方账号场景:用户进入微信商户微信官方账号,打开页面,选择产品,完成支付。
PC
网站场景:在网站上显示二维码,用户使用微信扫描二维码,输入支付金额,完成支付
付。
特点:用户在客户端输入支付金额
在微信小程序平台上实现支付功能。
Native
支付是指商家显示支付二维码,用户使用微信
“
扫一扫
”
完成支付的模式。这种方式适用于
PC
网
站。
特点:商户提前指定支付金额
商家在移动终端独立通过
APP
将微信支付模块集成到应用程序中,完成支付。
用户在刷脸设备前通过摄像头刷脸,识别身份。
微信商户平台:
https://pay.weixin.qq.com/
场景:
Native
支付
步骤:提交信息
=>
签署协议
=>
获取商户号
步骤:登录商户平台
=>
选择 账户中心
=>
安全中心
=> API
安全
=>
设置
API
密钥
随机密码生成工具:
https://suijimimashengcheng.bmcx.com/
APIv3
版本的所有接口都需要;
APIv2
版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台
=>
选择 账户中心
=>
安全中心
=> API
安全
=>
申请
API
证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有
API
秘钥和证书需妥善保管防止泄露
:
商户
API
证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请:
https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
微信支付平台证书是指由
微信支付
负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使
用平台证书中的公钥进行验签。
平台证书的获取:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
使用微信支付商户平台证书工具进行生成
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API
密钥对应
V2
版本的
API
APIv3
密钥对应
V3
版本的
API
项目实现
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>org.example</groupId>
<artifactId>weixinZF</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
<!-- 微信支付依赖-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
<!-- 支付宝支付依赖-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.31.65.ALL</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.9.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
</dependency>
<!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- <scope>test</scope><!–test表示只能在test目录下使用此依赖–>-->
<exclusions>
<exclusion><!--排除一些不需要的依赖-->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--其它依赖...-->
</dependencies>
<build>
<plugins>
<!--通过maven-compiler-plugin插件设置项目
的统一的jdk编译和运行版本-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.9.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<includeSystemScope>true</includeSystemScope>
<mainClass>com.wx.WXapplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置yml
server:
port: 8090
spring:
application:
name: weixinZF
datasource:
#高版本驱动使用
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/payment_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
#设定用户名和密码
username: root
password: root
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#SpringBoot整合Mybatis
mybatis-plus:
#指定别名包
type-aliases-package: com.jt.pojo
#扫描指定路径下的映射文件
mapper-locations: classpath:/mapper/*.xml
#开启驼峰映射
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志
map-underscore-to-camel-case: true
# 一二级缓存默认开始 所以可以简化
#打印mysql日志
logging:
level:
com.jt.mapper: debug
数据库创建
订单表
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 '更新时间',
`payment_type` varchar(255) DEFAULT NULL COMMENT '支付类型(支付宝~微信)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
记录表
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 DEFAULT NULL COMMENT '通知参数',
`create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
`update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
商品表
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`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
退款表
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 DEFAULT NULL COMMENT '申请退款返回参数',
`content_notify` text DEFAULT NULL COMMENT '退款结果通知参数',
`create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
`update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
配置微信相关支付参数
wxpay.properties
# 微信支付相关参数
# 商户号
wxpay.mch-id=11111111
# 商户API证书序列号
wxpay.mch-serial-no=1111111111111111
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=111111111111
# APPID
wxpay.appid=111111111111111
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址 使用内网穿透工具获取
wxpay.notify-domain=http://pw46ia.natappfree.cc
将私钥放置项目下
配置Util工具类
http请求客户端工具类
package com.wx.util;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* http请求客户端
*/
public class HttpClientUtils {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClientUtils(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClientUtils(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
isFirst = false;
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
参数转换字符串工具类
package com.wx.util;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
订单号工具类:我们需要为我们的订单生成一个编号
package com.wx.util;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtils {
/**
* 获取订单编号
* @return
*/
public static String getOrderNo() {
return "ORDER_" + getNo();
}
/**
* 获取退款单编号
* @return
*/
public static String getRefundNo() {
return "REFUND_" + getNo();
}
/**
* 获取编号
* @return
*/
public static String getNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}
微信验签应答工具类
package com.wx.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
* @author xy-peng
*/
public class WechatPay2ValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
创建统一结果R类
package com.wx.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.HashMap;
import java.util.Map;
@Data
@Accessors(chain = true)
public class R {
private Integer code; //响应码
private String message; //响应消息
private Map<String, Object> data = new HashMap<>();
public static R ok(){
R r = new R();
r.setCode(200);
r.setMessage("成功");
return r;
}
public static R error(){
R r = new R();
r.setCode(201);
r.setMessage("失败");
return r;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
}
定义实体类
package com.wx.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
@Data
public class BaseEntity {
//定义主键策略:跟随数据库的主键自增
@TableId(value = "id", type = IdType.AUTO)
private String id; //主键
private Date createTime;//创建时间
private Date updateTime;//更新时间
}
package com.wx.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@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;//订单状态
}
package com.wx.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@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;//通知参数
}
package com.wx.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("t_product")
public class Product extends BaseEntity{
private String title; //商品名称
private Integer price; //价格(分)
}
package com.wx.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@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;//退款结果通知参数
}
API接口地址,封装了微信支付的所有接口
package com.wx.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* api接口地址
*/
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* Native下单
*/
NATIVE_PAY_V2("/pay/unifiedorder"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 申请退款
*/
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
/**
* 查询单笔退款
*/
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
/**
* 申请交易账单
*/
TRADE_BILLS("/v3/bill/tradebill"),
/**
* 申请资金账单
*/
FUND_FLOW_BILLS("/v3/bill/fundflowbill");
/**
* 类型
*/
private final String type;
}
封装了通知接口地址
package com.wx.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 封装了通知接口地址
*/
@AllArgsConstructor
@Getter
public enum WxNotifyType {
/**
* 支付通知
*/
NATIVE_NOTIFY("/api/wx-pay/native/notify"),
/**
* 支付通知
*/
NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
/**
* 退款结果通知
*/
REFUND_NOTIFY("/api/wx-pay/refunds/notify");
/**
* 类型
*/
private final String type;
}
退款类型
package com.wx.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 退款
*/
@AllArgsConstructor
@Getter
public enum WxRefundStatus {
/**
* 退款成功
*/
SUCCESS("SUCCESS"),
/**
* 退款关闭
*/
CLOSED("CLOSED"),
/**
* 退款处理中
*/
PROCESSING("PROCESSING"),
/**
* 退款异常
*/
ABNORMAL("ABNORMAL");
/**
* 类型
*/
private final String type;
}
支付类型
package com.wx.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单状态
*/
@AllArgsConstructor
@Getter
public enum WxTradeState {
/**
* 支付成功
*/
SUCCESS("SUCCESS"),
/**
* 未支付
*/
NOTPAY("NOTPAY"),
/**
* 已关闭
*/
CLOSED("CLOSED"),
/**
* 转入退款
*/
REFUND("REFUND");
/**
* 类型
*/
private final String type;
}
支付状态
package com.wx.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 已关闭
*/
CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
/**
* 类型
*/
private final String type;
}
付款类型
package com.wx.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
/**
* 类型
*/
private final String type;
}
在
confifig
包中创建配置文件
MybatisPlusConfifig
package com.wx.config;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.annotation.MapperScans;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan("com.wx.mapper")
@EnableTransactionManagement
public class MybatisPlusConfig {
}
定义Mapper层
继承BaseMapper<>
定义业务层接口继承
IService<>
定义业务层接口的实现类,并继承
ServiceImpl<,>
以上配置完成才可进行业务实现!!!!!!!!!!!!!!!!!!!
业务代码实现
获取商品列表接口
在
public class ProductController
中添加一个方法
package com.wx.controller;
import com.wx.entity.Product;
import com.wx.service.ProductService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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 javax.annotation.Resource;
import java.util.List;
@CrossOrigin
@RestController
@RequestMapping("/api/product")
@Api(tags = "商品管理")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping("/list")
public R list(){
List<Product> list = productService.list();
return R.ok().data("productList",list);
}
}
结果:
定义读取支付参数配置WxPayConfig
package com.wx.config;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.*;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
// APIv2密钥
private String partnerKey;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器.定时更新签名证书
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
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;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
测试获取支付参数:
import com.wx.config.WxPayConfig;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.security.PrivateKey;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
@Resource
private WxPayConfig wxPayConfig;
@GetMapping
public R getWxPayConfig(){
String mchId = wxPayConfig.getMchId();
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
// PrivateKey privateKey = wxPayConfig.getPrivateKey("apiclient_key.pem");
// System.out.println("privateKey = " + privateKey);
return R.ok().data("mchId",mchId).data("privateKeyPath",privateKeyPath);
}
}
swagger测试获取
前面的依赖是完整的,已经添加
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的
SDK
,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
2.
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(如何加载商户私钥)
上面测试中注释的部门就是获取商户私钥
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(定时更新平台证书功能)
HttpClient
对象:是建立远程连接的基础,我们通过
SDK
创建这个对象。
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有
API
的功能。
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付
APIv3
使用
JSON
作为消息体的数据交换格式。
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
业务功能接口代码实现
Controller
package com.wx.controller;
import com.google.gson.JsonSyntaxException;
import com.wx.service.WxPayService;
import com.wx.util.HttpUtils;
import com.wx.util.WechatPay2ValidatorForRequest;
import com.wx.vo.R;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {//传递商品id
log.info("发起支付请求");
//返回支付二维码链接和订单号
Map<String,Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
/**
* 接收微信的通知,支付成功处理,失败处理
* @param request
* @param response
* @return
*/
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
Map<String,String> map = new HashMap<>();//
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
log.info("支付通知的id =====》 {}",bodyMap.get("id"));
log.info("支付通知的完整数据 =====》 {}",body);
String requestId = bodyMap.get("id").toString();
// 签名的验证 针对请求的 因为与微信交互,传递信息需要进行验证
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("通知验签成功");
//处理订单 将具有密文数据的bodyMap进行解密获取参数。并存入数据库,存入日志
wxPayService.processOrder(bodyMap);
//成功应答
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
return gson.toJson(map);
} catch (JsonSyntaxException | IOException | GeneralSecurityException e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code","ERROR");
map.put("message","失败");
return gson.toJson(map);
}
}
@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
wxPayService.canceOrder(orderNo);
return R.ok().setMessage("订单已经取消");
}
@ApiOperation("微信支付查询订单")
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) throws IOException {
log.info("查询订单");
String result = wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result",result);
}
@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();
}
/**
* 查询退款
* @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);
}
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> 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);
}
}
@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);
}
@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);
}
}
package com.wx.controller;
import com.wx.entity.OrderInfo;
import com.wx.enums.OrderStatus;
import com.wx.service.OrderInfoService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@CrossOrigin
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@GetMapping("/list")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list",list);
}
/**
* 查询订单状态
* @param orderNo
* @return
*/
@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().setCode(0).setMessage("支付成功");//支付成功
}
return R.ok().setCode(101).setMessage("支付中...");
}
}
package com.wx.controller;
import com.wx.entity.Product;
import com.wx.service.ProductService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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 javax.annotation.Resource;
import java.util.List;
@CrossOrigin
@RestController
@RequestMapping("/api/product")
@Api(tags = "商品管理")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping("/list")
public R list(){
List<Product> list = productService.list();
return R.ok().data("productList",list);
}
}
Service
package com.wx.service;
import com.baomido