微信支付-开发者文档 (qq.com)
源码地址:前端后端都有
链接: https://pan.baidu.com/s/1Nx-jLJ1gaZD0rmoGOw9MhA
提取码: qwer
APIV2、APIV3一个模式
4.0.0
jar
org.example
weixinZF
1.0-SNAPSHOT
8
8
org.springframework.boot
spring-boot-starter-parent
2.1.9.RELEASE
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
log4j
log4j
1.2.14
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.3.0
com.alipay.sdk
alipay-sdk-java
4.31.65.ALL
org.springframework.boot
spring-boot-dependencies
2.1.9.RELEASE
pom
import
com.alibaba
fastjson
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
3.4.3
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
provided
org.springframework.boot
spring-boot-starter-test
org.junit.jupiter
junit-jupiter-engine
com.alibaba
fastjson
1.2.60
compile
com.google.code.gson
gson
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-maven-plugin
2.1.9.RELEASE
repackage
true
com.wx.WXapplication
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
将私钥放置项目下
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 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 param) {
this.url = url;
this.param = param;
}
public HttpClientUtils(String url) {
this.url = url;
}
public void setParameter(Map map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap();
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 nvps = new LinkedList();
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) : "";
}
}
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 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;
}
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 {
}
继承BaseMapper<>
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 list = productService.list();
return R.ok().data("productList",list);
}
}
结果:
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测试获取
前面的依赖是完整的,已经添加
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.3.0
证书密钥使用说明:上面的配置中已经建立,此处再说明一下
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
获取签名验证器
接口规则
com.google.code.gson
gson
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 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 map = new HashMap<>();//
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map 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 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);
}
}
@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 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 list = productService.list();
return R.ok().data("productList",list);
}
}
package com.wx.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.OrderInfo;
import com.wx.enums.OrderStatus;
import java.util.List;
public interface OrderInfoService extends IService {
OrderInfo createOrderByProductId(Long productId,String type);//获取订单信息并存入数据库中
//订单号 二维码地址
void saveCodeUrl(String orderNo,String codeUrl);//因为扫码有俩个小时的时间,所以进行数据库的更新
List listOrderByCreateTimeDesc(); //查询订单列表,并倒序
void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);//更改订单状态
String getOrderStatus(String orderNo);//处理重复的通知
List getNopayOrderByDuration(int minutes,String type);//定时任务
OrderInfo getOrderByOrderNo(String orderNo);// //根据订单号获取订单信息
}
package com.wx.service;
public interface PaymentInfoService {
void createPaymentInfo(String plainText);//记录支付日志
}
package com.wx.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.Product;
public interface ProductService extends IService {
}
package com.wx.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;
import java.util.List;
public interface RefundInfoService extends IService {
RefundInfo createRefundByOrderNo(String orderNo, String reason);//根据订单编号创建退款
void updateRefund(String content);//更新退款单
List getNoRefundOrderByDuration(int minutes, String type);//找出申请退款超过5分钟并且未成功的退款单
RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason);
void updateRefundForAliPay(String refundNo, String content, String refundStatus);
}
package com.wx.service;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Map;
public interface WxPayService {
Map nativePay(Long productId) throws Exception;
//解密
void processOrder(Map bodyMap) throws GeneralSecurityException;
void canceOrder(String orderNo) throws Exception;//取消订单
String queryOrder(String orderNo) throws IOException;//微信支付查询订单
void checkOrderStatus(String orderNo) throws Exception;//查询核实订单状态
void refund(String orderNo, String reason) throws Exception;//申请退款
String queryRefund(String refundNo) throws Exception;//查询退款
void processRefund(Map bodyMap) throws Exception;//退款结果通知
void checkRefundStatus(String refundNo) throws Exception;//核实订单状态:调用微信支付查询退款接口
String queryBill(String billDate, String type) throws Exception;//获取账单url
String downloadBill(String billDate, String type) throws Exception;//下载账单
}
package com.wx.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wx.entity.OrderInfo;
import com.wx.entity.Product;
import com.wx.enums.OrderStatus;
import com.wx.mapper.OrderInfoMapper;
import com.wx.mapper.ProductMapper;
import com.wx.service.OrderInfoService;
import com.wx.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@Slf4j
@Service
public class OrderInfoServiceImpl extends ServiceImpl implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Resource
private OrderInfoMapper orderInfoMapper;
/**
* 生成订单到数据库中
* @param productId
* @return
*/
@Override
public OrderInfo createOrderByProductId(Long productId,String type) {
//查找已存在但未支付的订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId,type);
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()); //订单状态
// orderInfo.setUserId(); 真实项目中需要存入用户的id,谁下的订单
// orderInfo.setCodeUrl(); //二维码链接
orderInfo.setPaymentType(type);
baseMapper.insert(orderInfo);
return orderInfo;
}
//查找已存在但未支付的订单.如果订单存在且没有支付则返回没支付的订单,防止重复创建订单对象
private OrderInfo getNoPayOrderByProductId(Long productId,String type) {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id",productId);
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
queryWrapper.eq("payment_type",type);
// queryWrapper.eq("user_id",userId); //再根据用户的id获取用户的订单
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
/**
* 存储订单二维码,在数据库中直接更改
* @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);
}
/**
* 查询订单列表,并倒序查询
* @return
*/
@Override
public List listOrderByCreateTimeDesc() { //订单管理
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}
/**
* 根据订单号更新订单状态
* @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);
}
/**
* 处理未支付的订单
* @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();
}
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List getNopayOrderByDuration(int minutes,String type) {
Instant instant = Instant.now().minus(Duration.ofMillis(minutes));//时间实例,用当前时间减去输入的时间
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
queryWrapper.le("create_time",instant);//小于
queryWrapper.eq("payment_type",type);//根据支付的类型查询 微信or支付宝
List orderInfos = baseMapper.selectList(queryWrapper);
return orderInfos;
}
/**
* 根据订单号获取订单
* @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;
}
}
package com.wx.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.wx.entity.PaymentInfo;
import com.wx.enums.PayType;
import com.wx.mapper.PaymentInfoMapper;
import com.wx.service.PaymentInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService {
@Override
public void createPaymentInfo(String plainText) {
log.info("记录微信支付日志");
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
//获取商户的订单号
String orderNo = plainTextMap.get("out_trade_no").toString();
//微信支付单号 如果支付有问题可以通过支付单号进行处理
String transactionId = plainTextMap.get("transaction_id").toString();
//支付类型,因为可能是通过网页,或者app及其他进行支付
String tradeType = plainTextMap.get("trade_type").toString();
//支付状态
String tradeState = plainTextMap.get("trade_state").toString();
//用户支付的金额
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);//插入数据库中
}
/**
* 记录支付宝日志
* @param params
*/
@Override
public void createPaymentInfoForAlipay(Map params) {
String orderNo = params.get("out_trade_no");//获取订单号
String tradeNo = params.get("trade_no"); //业务编号
String tradeStatus = params.get("trade_status");//交易状态
String totalAmount = params.get("total_amount");
int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();//交易金额
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentType(PayType.ALIPAY.getType());
paymentInfo.setTransactionId(tradeNo);
paymentInfo.setTradeType("电脑网站支付");
paymentInfo.setTradeState(tradeStatus);
paymentInfo.setPayerTotal(totalAmoutInt);
Gson gson = new Gson();
String json = gson.toJson(params, HashMap.class);
paymentInfo.setContent(json);
baseMapper.insert(paymentInfo);//存储到日志表格中
}
}
package com.wx.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wx.entity.Product;
import com.wx.mapper.ProductMapper;
import com.wx.service.ProductService;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl extends ServiceImpl implements ProductService {
}
package com.wx.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.WxRefundStatus;
import com.wx.mapper.RefundInfoMapper;
import com.wx.service.OrderInfoService;
import com.wx.service.RefundInfoService;
import com.wx.util.OrderNoUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class RefundInfoServiceImpl extends ServiceImpl implements RefundInfoService {
@Resource
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;
}
/**
* 记录退款记录
* @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);
}
/**
* 找出申请退款超过minutes分钟并且未成功的退款单
* @param minutes
* @return
*/
@Override
public List getNoRefundOrderByDuration(int minutes,String tpye) {
//minutes分钟之前的时间
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
queryWrapper.eq("payment_type", PayType.WXPAY.getType());
queryWrapper.le("create_time", instant);
List refundInfoList = baseMapper.selectList(queryWrapper);
return refundInfoList;
}
/**
* 根据订单号创建退款订单
* @param orderNo
* @return
*/
@Override
public RefundInfo createRefundByOrderNoForAliPay(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;
}
/**
* 更新退款记录
* @param refundNo
* @param content
* @param refundStatus
*/
@Override
public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
//根据退款单编号修改退款单
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", refundNo);
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundStatus(refundStatus);//退款状态
refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
}
package com.wx.service.impl;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wx.config.WxPayConfig;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.OrderStatus;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.WxApiType;
import com.wx.enums.wxpay.WxNotifyType;
import com.wx.enums.wxpay.WxRefundStatus;
import com.wx.enums.wxpay.WxTradeState;
import com.wx.service.OrderInfoService;
import com.wx.service.PaymentInfoService;
import com.wx.service.RefundInfoService;
import com.wx.service.WxPayService;
import com.wx.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
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.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource //注入配置中的对象 保函了验签的过程
private CloseableHttpClient wxPayClient;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private OrderInfoService orderInfoService; //将订单存入数据库中
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
@Resource
private CloseableHttpClient wxPayNoSignClient; //无需应答签名
private final ReentrantLock lock = new ReentrantLock(); //可重入锁
/**
* 创建订单,调用Native支付接口
* @param productId
* @return code_url(二维码地址) 和 订单号
* @throws Exception
*/
@Override
public Map nativePay(Long productId) throws Exception {
/**
* 如果调用不成功需要去商户平台登录该商户号,在产品中心-我的产品-开通“公众号支付”,这样就可以用于小程序支付了 。
*/
log.info("生成订单");
//生成订单 TODO : 存入数据库
OrderInfo orderInfo= orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
String codeUrl = orderInfo.getCodeUrl();
if (orderInfo != null && codeUrl!=null){
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());//应用ID
paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
paramsMap.put("description",orderInfo.getTitle());//商品描述 用了上面的title
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);
String jsonParams = gson.toJson(paramsMap);//转换成json的格式
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
System.out.println("成功");
} else {
System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
System.out.println("resultMap = " + resultMap);
//保存新二维码
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();
}
}
@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 = plainTextMap.get("out_trade_no").toString();//获取商户订单号
/**
*在对业务数据进行状态检查和处理之前
* 要采用数据锁进行并发控制
* 以避免函数重入造成的数据混乱
*/
//尝试获取锁,成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if (lock.tryLock()){
try {
//处理重复的通知 因为微信通知可能会出现重复的原因,所以进行处理一下
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){//如果支付状态不等于未支付的
return;
}
//更新订单状态,支付成功更改状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
//需要主动释放锁
lock.unlock();
}
}
}
/**
* 取消订单
* @param orderNo
*/
@Override
public void canceOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
}
/**
* 微信支付查询订单
* @param orderNo
* @return
*/
@Override
public String queryOrder(String orderNo) throws IOException {
log.info("查询订单接口调用 ===》");
//因为路径中有占位符,所以进行替换
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
System.out.println("成功");
} else {
System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已经支付,则更新商户端订单状态
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===》"+orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
HashMap 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);
}
}
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Override
public void refund(String orderNo, String reason) throws Exception {
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();
}
}
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public String queryRefund(String refundNo) throws Exception {
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();
}
}
/**
* 根据退款单号核实退款单状态
* @param refundNo
* @return
*/
@Override
public void checkRefundStatus(String refundNo) throws Exception {
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);
}
}
/**
* 申请账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String queryBill(String billDate, String type) throws Exception {
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();
}
}
/**
* 下载账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String downloadBill(String billDate, String type) throws Exception {
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();
}
}
/**
* 处理退款单 退款通知
*/
@Override
public void processRefund(Map bodyMap) throws Exception {
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();
}
}
}
/**
* 关单接口的调用
* @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 paramMap = new HashMap<>();
paramMap.put("mchid",wxPayConfig.getMchId());
String jsonParams = gson.toJson(paramMap);
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
System.out.println("成功204");
} else {
System.out.println("Native下单失败,响应码 = " + statusCode);
throw new IOException("request failed");
}
} finally {
response.close();
}
}
/**
* 对称解密
* @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");
//解密工具
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;
}
}
/**
* 调用统一下单api 服务订单
* @param wnOrderDecoration
* @return
* @throws Exception
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map nativePay2(WnOrderDecoration wnOrderDecoration) throws Exception {
wnOrderDecorationService.updateById(wnOrderDecoration);
log.info("调用统一下单API");
Double money = null;
String orderNumber = null;
if (wnOrderDecoration.getPayState().equals(OrderStatus.NOTPAY.getType())){
money = wnOrderDecoration.getVisitCost();//上门金额 先支付上面费用
orderNumber = wnOrderDecoration.getDoorOrderNumber();
}
if (wnOrderDecoration.getPayState().equals(OrderStatus.SUCCESS.getType())){
money = wnOrderDecoration.getCost();//设计师报价
orderNumber = wnOrderDecoration.getOrderNumber();
}
//调用统一下单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());//应用ID
paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
paramsMap.put("description",wnOrderDecoration.getOrderName());//商品描述 用了上面的title
paramsMap.put("out_trade_no",orderNumber);//订单号
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//通知地址
Map amountMap = new HashMap<>();
amountMap.put("total",money);//金额
amountMap.put("currency","CNY"); //货币类型
paramsMap.put("amount",amountMap);
String jsonParams = gson.toJson(paramsMap);//转换成json的格式
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());//响应头
//响应结果
HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);
//预支付id
String codeUrl = resultMap.get("prepay_id");
System.out.println("resultMap = " + resultMap);
//时间戳
Long timeStamp = genTimeStamp();
//随机字符串
String nonce = RandomUtil.randomString(32);
//获取签名sign------------------
StringBuilder builder1 = new StringBuilder();
builder1.append("wxde22cf376c634bea").append("\n");// 应用id
//时间戳
builder1.append(timeStamp).append("\n"); //时间戳
//随机字符串
builder1.append(nonce).append("\n"); //字符串
//预支付id
builder1.append(codeUrl).append("\n"); // 预支付id
//签名
String sign = sign(builder1.toString().getBytes(StandardCharsets.UTF_8));
Map map = new HashMap<>();
map.put("prepay_id",codeUrl); //预支付id
map.put("orderNo",wnOrderDecoration.getOrderNumber());//订单号
map.put("appid",wxPayConfig.getAppid());//应用id
map.put("partnerid",wxPayConfig.getMchId()); //商户号
map.put("package","Sign=WXPay");//订单详情扩展字符串 固定的
map.put("noncestr",nonce);//随机字符串
map.put("timestamp",timeStamp);//时间戳
map.put("sign",sign);//签名
return map;
} finally {
response.close();
}
}
//获取签名
String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA");
// PrivateKey privateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(wxPayConfig.getPrivateKeyPath().getBytes("utf-8")));
// sign.initSign(privateKey);
sign.initSign(getPrivateKey(wxPayConfig.getPrivateKeyPath()));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
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("无效的密钥格式");
}
}
//获取时间戳
private static long genTimeStamp() {
return System.currentTimeMillis() / 1000;
}
获取token
package com.ruoyi.web.jxxt.util.wxGz;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import lombok.Data;
@Data
public class AccessToken {
private String token;
private long expiresTime;//过期时间
public AccessToken(String token, String expiresIn) {
super();
this.token = token;
//当前时间+有效期 = 过期时间
this.expiresTime = System.currentTimeMillis()+Integer.parseInt(expiresIn);
}
/**
* 判断token是否过期
* @return
*/
public boolean isExpire() {
return System.currentTimeMillis() > expiresTime;
}
//get and set ...
private static AccessToken at;//token获取的次数有限,有效期也有限,所以需要保存起来
private static String GET_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
//登录测试号管理界面-测试号信息下面可以得到你的APPID和APPSECRET
private static String APPID = "wxde22xxxx";
private static String APPSECRET = "e8186658cxxxxx";
/**
* 发送get请求获取AccessToken
*/
private static String getToken() {
String url = GET_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
String tokenStr = HttpUtils.sendGet(url);//调用工具类发get请求
System.out.println(tokenStr);
JSONObject jsonObject = JSONObject.parseObject(tokenStr);
String token = jsonObject.getString("access_token");
String expiresIn = jsonObject.getString("expires_in");
at = new AccessToken(token, expiresIn);
return at.token;
}
/**
* todo 获取AccessToken 向外提供 调用的ip必须在公众号设置白名单。不然获取不到token!!!!!!!!!!
*/
public static String getAccessToken() {
//过期了或者没有值再去发送请求获取
if(at == null || at.isExpire()) {
getToken();
}
return at.getToken();
}
public static void main(String[] args) {
String accessToken = getAccessToken();
System.out.println("accessToken = " + accessToken);
}
}
public String getWXaccessToken() {
String accessToken = AccessToken.getAccessToken();
return accessToken;
}
public String getWXJsapiTicket(String token) {
String ticket = null;
if (StringUtils.isBlank(ticket)) {
String url ="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + token +"&type=jsapi";
RestTemplate restTemplate = new RestTemplate();
String resp = restTemplate.getForObject(url, String.class);
JSONObject resJson = JSONObject.parseObject(resp);
return resJson.getString("ticket");
}
return ticket;
}
public static String sha1(String decript) {
try {
MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");
digest.update(decript.getBytes());
byte[] messageDigest = digest.digest();
// Create Hex String
StringBuilder hexString = new StringBuilder();
// 字节数组转换为 十六进制 数
for (byte b : messageDigest) {
String shaHex = Integer.toHexString(b & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
log.error("微信签名时失败,请检查!", e);
}
return "";
}
/**
* 入参为url
* @param url
* @return
*/
@PostMapping("/getWXSign")
public String getWXSign(@RequestBody String url) {
log.info("urlsssss 参数 为:"+url);
JSONObject jsonObject = JSONObject.parseObject(url);
url = jsonObject.getString("url");
log.info("url 参数 为:"+url);
long timestamp = System.currentTimeMillis() / 1000;
// //随机字符串
// int noncestr = Math.abs(new Random().nextInt());
//随机字符串
String noncestr = RandomUtil.randomString(32);
String[] urls = url.split("#");
String newUrl = urls[0];
JSONObject respJson =new JSONObject();
String[] signArr =new String[]{"url=" + newUrl,"jsapi_ticket=" + getWXJsapiTicket(getWXaccessToken()),"noncestr=" + noncestr,"timestamp=" + timestamp};
Arrays.sort(signArr);
String signStr = StringUtils.join(signArr,"&");
log.info("signStr 参数为 :"+ signStr);
// String resSign = DigestUtils.sha1Hex(signStr);
String resSign = sha1(signStr);
respJson.put("appId", "wxde22cf376c634bea");
respJson.put("timestamp", timestamp);
respJson.put("nonceStr", noncestr);
respJson.put("signature", resSign);
return respJson.toJSONString();
}
package com.wx.task;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;
import com.wx.service.OrderInfoService;
import com.wx.service.RefundInfoService;
import com.wx.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
@Slf4j
@Component
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
@Resource
private RefundInfoService refundInfoService;
/**
* 从第0秒开始,每隔30秒执行一次,查询创建超过五分钟,并且未支付的订单
*/
//@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("定时任务启动====");
List orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.WXPAY.getType());
for (OrderInfo orderInfo : orderInfoList){
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 === > {}", orderNo);
//核实订单状态:调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
* */
//@Scheduled(cron = "0/30 * * * * ?")
public void refundConfirm() throws Exception {
log.info("refundConfirm 被执行......");
//找出申请退款超过5分钟并且未成功的退款单
List refundInfoList = refundInfoService.getNoRefundOrderByDuration(5, PayType.WXPAY.getType());
for (RefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo.getRefundNo();
log.warn("超时未退款的退款单号 ===> {}", refundNo);
//核实订单状态:调用微信支付查询退款接口
wxPayService.checkRefundStatus(refundNo);
}
}
}
引入参数文件,获取参数并组装到client中。后面方便调用
package com.wx.config;
import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import javax.annotation.Resource;
/**
* 加载支付宝配置参数文件
*/
@Configuration
//加载配置文件
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {
@Resource
private Environment config;//注入此对象,方便读取配置文件中数据
@Bean
public AlipayClient alipayClient() throws AlipayApiException {
AlipayConfig alipayConfig = new AlipayConfig();
//设置网关地址
alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
//设置应用Id
alipayConfig.setAppId(config.getProperty("alipay.app-id"));
//设置应用私钥
alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
//设置请求格式,固定值json
alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
//设置字符集
alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
//设置支付宝公钥
alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
//设置签名类型
alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
//构造client
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
return alipayClient;
}
}
alipay-sandbox.properties
支付宝参数:此处用的是沙箱模式
# 支付宝支付相关参数
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021000121632501
# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088621987731295
# 支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq309YMdt/Kt2leisVuMbA6fTSmc2s9iY6wtuCDSbqz3RK187qsZepa2S7l6J16BWKXak0QIus70ZCGZ61U//ToQqDXc3JKlKvp19Pcq8YpvzByv0Z2FdtvGi9tbjX1icB2Xt/6uO9BYuixi9d3e1kzx9/M2RDiVuPmTSPvmIGJLYSmjsmJO1FOCyZQa5X/d0no5Ko4vKtV/DanqoqWNsOGpoU7bCFA/Y+PtS4xSEUgnsSWjymEQlfSublENadXhSLEP144ZrHKRDdFwTrua64KbQVFR5dnXvcVd4ERCD5C2Vtl+b3qx1puYlCxFPXp/dgC6f4iqQNZCj+W4m3NqmVAgMBAAECggEASVD34ofB/paN8+qvgep+nVfFTHfh4EzdqmjhdrPd9vJ8m4BtsBXzVSZXWoZ9lsm2NGBrsZfgVpt0Mfh8OKGKK2v17tfY7G/Uern+E0DKEHHWEfDfGK/TE6q75mqKnVGt+wUuEHzgqsIuX/FZcZU/vvmAMjwC0Vemib7a5rJxrOBvP40siA/e9se4PwmQHqfXH5J6vyJna6dH1r4f+sxhWdCb4O1VxZgI52J7rMStYGqwnEMKv5h7aB2zpq6BQbcblvNw6hBA80sn+F+LJM0Auebqk+HX/wZXHKsJVoRYEtCUNhl4YoNo5V3U3WYci1JXPJ+Op6PMI8n4iJZSTj6YIQKBgQDnDh2DkPR5RCLjJ1F+Kq5EotDNwLA21/xibLHE/gTT9kdxfKdSckZjOVp+nlMQ2Z+L8khD3YfRDD4sUheL8fKA22G9GnY31/4c2/XsjWPogr0BpgxFRt954OyPIoL+FQLkZnH05MOY5bq9N9/gfuuF3txCTJgMUYEWTba3Q52hkwKBgQC9Ud2wwwhVED5x80Tl4z4QVKro3ubbdat+IiCLOAOoW1IyRG+HV3CbG82DMT0F6h3YRBaRtC/UUoeh/YFpsYjhH30SghiM7N9l4Sk4X9z4eMvYklE02P81TOOukTmJzugHxtwb6k2YZC4LOu7+S2sRc0kTmUfX1CgmZ2L07ochNwKBgQCf4m6d6iKh/3o8wapsqdApgpkGp73IVbE50ok5DaX9nsBVUbLfJGB8rOVoFNraIB19U8yZ2aPwDo6/UJcmqefrLuP1XWhMwFQBWFxWsoheDooHqAV5ss9VoUVQzsriU1vK/PECS4LmPKH56b4rtOf5nPvBjQryCzxOWLyFGG7trQKBgElOCakH25IUWBmHOIZLFxz7q7G/nWQci+qrDC7b4Y6uzYTpOsYM9W0Zttm1lwtTO3sh4htIybxMuHfg0Ns8AuQobSVdemQW0+l+5ZcOh2EuZL/W59quqyLYQtC1KrJRi0Z3mYK1lpYLNEjk6OVODocTPJh6IXdQjrtQDOEJ+wjBAoGBAJopA4T5OnunBZkvYZPXya747m6U4nJFIcS0lJfM9ZGP2hw/mCOhXb2khL789v4G78kwUgfnVAcPPyyG1jjFtPiWb9+DrANZijBWb6HxnvTRytjeROlxIgXiLmb7MqTZHLujyb3H80rhTu3rqk8Jp1Yx26sEKKJXz3rc5KkSVOFt
# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgjkP008ZFSXVqWoSClXeKDTX7leNDVabMQcexv3TxZY4KYNUSd091BeYlE59feUaGTcehHc9r3N48jaqbZJyX8M2ogdqFlA/8iep22WcQu5ybIEUX45L40ClYqiqKLYpj/uuPFrekEKdZrS1DxaawDaazGypFFzpz/Lf6ijjbDeQhVsSqaPDAZEqmGWUo6oF1bahCpYJb9q/orqaihqA1vb7oRm7k3n8e76H6O1xxDVNenIsi4tit0wlZ6XneOVxnzEgsk0NAGa8BEH2gKrkVycVgBAUxjr7yWVyJuL0pYJkHnQbg6WxLDaDhe8iqGC1faSGqlB4PcIJp+pXHwv0DwIDAQAB
# 接口内容加密秘钥,对称秘钥
alipay.content-key=DNxJbSgGPbQwXL3jnKw42A==
# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success
# 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=http://ddme2g.natappfree.cc/api/ali-pay/trade/notify
package com.wx.controller;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConstants;
import com.alipay.api.internal.util.AlipaySignature;
import com.wx.entity.OrderInfo;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
@RestController
@CrossOrigin
@RequestMapping("/api/ali-pay")
@Api(tags = "支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@Resource
private Environment config;
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("统一收单下单支付页面接口调用")
@PostMapping("/trade/page/pay/{productId}")
public R tradePage(@PathVariable Long productId){
log.info("统一收单下单支付页面接口调用");
//支付报开放平台接受 request 请求对象后
//会被开放者生成一个html形式的from表单,包含自动提交的脚本
String formStr = aliPayService.tradeCteate(productId);
//我们将from表单字符串返回给前端程序.之后前端将会调用自动提交脚本,进行表单的提交
//此时,表单会自动提交的action属性所执行的支付宝开放平台中,从而为用户展示一个支付页面
return R.ok().data("formStr",formStr);
}
@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map params){
log.info("支付通知正在进行");
log.info("通知参数 ===》 :"+params);
String result = "failure";
try {
//异步通知验签
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2);//调用SDK验证签名
if(!signVerified){
//验签失败则记录异常日志,并在response中返回failure.
log.error("异步通知验签失败");
return result;
}
//验签成功后
log.info("支付成功异步通知验签成功!");
//按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
//1.商家需要验证该通知数据中的 out_trade_no 是否为商家系统中创建的订单号。
String outTradeNo = params.get("out_trade_no");
OrderInfo oeder = orderInfoService.getOrderByOrderNo(outTradeNo);
if (oeder == null){
log.error("订单不存在");
return result;
}
//2.判断 total_amount 是否确实为该订单的实际金额(即商家订单创建时的金额)。
String totalAmount = params.get("total_amount");
int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
int totalFeeInt = oeder.getTotalFee().intValue();
if (totalAmoutInt != totalFeeInt){
log.error("金额校验失败");
return result;
}
//3.校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商家可能有多个 seller_id/seller_email)。
String sellerId = params.get("seller_id");
String sellerIdPro = config.getProperty("alipay.seller-id");
if (!sellerId.equals(sellerIdPro)){
log.error("商家pid校验失败");
return result;
}
//4.验证 app_id 是否为该商家本身。
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if (!appId.equals(appIdProperty)){
log.error("appid校验失败");
return result;
}
//只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
//处理业务,修改订单状态,记录支付日志
aliPayService.processOrder(params);
//校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
//向支付宝返回成功的标识,否则会一直不间断的发送通知给我们
result = "success";
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
/**
* 用户取消订单
* @param orderNo
* @return
*/
@ApiOperation("用户取消订单")
@PostMapping("/trade/close/{orderNo}")
public R cancel(@PathVariable String orderNo){
log.info("用户取消订单");
aliPayService.cancelOrder(orderNo);
return R.ok().setMessage("用户已取消订单");
}
/**
* 查询订单
* @param orderNo
* @return
*/
@ApiOperation("支付宝支付查询订单")
@GetMapping("/trade/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo){
log.info("查询订单");
String result = aliPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result",result);
}
/**
* 申请退款
* @param orderNo
* @param reason
* @return
*/
@ApiOperation("申请退款")
@PostMapping("/trade/refund/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String reason){
log.info("申请退款");
aliPayService.refund(orderNo, reason);
return R.ok();
}
/**
* 查询退款
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("查询退款")
@GetMapping("/trade/fastpay/refund/{orderNo}")
public R queryRefund(@PathVariable String orderNo) throws Exception {
log.info("查询退款");
String result = aliPayService.queryRefund(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
/**
* 根据账单类型和日期获取账单url地址
*
* @param billDate
* @param type
* @return
*/
@ApiOperation("获取账单url")
@GetMapping("/bill/downloadurl/query/{billDate}/{type}")
public R queryTradeBill(
@PathVariable String billDate,
@PathVariable String type) {
log.info("获取账单url");
String downloadUrl = aliPayService.queryBill(billDate, type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}
}
支付类型
package com.wx.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum AliPayTradeState {
/**
* 支付成功
*/
SUCCESS("TRADE_SUCCESS"),
/**
* 未支付
*/
NOTPAY("WAIT_BUYER_PAY"),
/**
* 已关闭
*/
CLOSED("TRADE_CLOSED"),
/**
* 退款成功
*/
REFUND_SUCCESS("REFUND_SUCCESS"),
/**
* 退款失败
*/
REFUND_ERROR("REFUND_ERROR");
/**
* 类型
*/
private final String type;
}
package com.wx.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.internal.LinkedTreeMap;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.OrderStatus;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.AliPayTradeState;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import com.wx.service.PaymentInfoService;
import com.wx.service.RefundInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
private final ReentrantLock lock = new ReentrantLock();
/**
* 统一收单下单支付页面接口调用
* @param productId
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String tradeCteate(Long productId) {
try {
//生成订单
log.info("生成订单");
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
//调用支付宝接口
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//配置需要的公共请求参数
//支付完成后,支付宝发起异步通知的地址
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
//支付完成后,我们想让页面跳转回成功的页面,配置returnUrl
request.setReturnUrl(config.getProperty("alipay.return-url"));
//组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderInfo.getOrderNo());
//因为微信是分,这里支付宝是元,所以进行更改
BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
bizContent.put("total_amount", total);
bizContent.put("subject", orderInfo.getTitle());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
//执行请求,调用支付宝接口
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("创建支付交易失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建支付交易失败");
}
}
/**
* 处理业务,修改订单状态,记录支付日志
* @param params
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map params) {
log.info("处理订单");
//获取订单号
String orderNo = params.get("out_trade_no");
/**
* 在对业务数据进行状态检查和处理之前
* 要采用数据锁进行控制
* 以避免函数重入造成数据混乱
*/
if (lock.tryLock()) {
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,以下业务只执行一次
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//如果不等于未支付
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录订单日志
paymentInfoService.createPaymentInfoForAlipay(params);
}finally {
//释放锁
lock.unlock();
}
}
}
/**
* 用户取消支付宝订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) {
//调用支付宝提供的统一收单交易关闭接口
this.closeOrder(orderNo);
//更新用户订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
}
/**
* 支付宝查询订单
* @param orderNo
* @return 返回订单查询结果 如果返回null则表示订单不存在
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查询接口调用 === 》"+orderNo);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("查询接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
return null;
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查询接口调用失败");
}
}
/**
* 根据订单号查询支付宝支付查单接口,核实订单状态
* 如果订单未创建,则直接更新商户端订单状态
* 如果订单已经支付,则更新商户端订单状态
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) {
log.warn("根据订单号核实订单状态 ===》"+orderNo);
String result = this.queryOrder(orderNo);
//订单未创建
if (result == null){
log.warn("核实订单未创建 ===》"+orderNo);
//更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
}
try {
//解析查单响应结果
Gson gson = new Gson();
HashMap resultMap = gson.fromJson(result, HashMap.class);
LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
String tradeStatus = alipayTradeQueryResponse.get("trade_status").toString();//获取订单状态
//判断订单是否是未支付的订单
if (AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
log.info("未支付订单==== 》"+orderNo);
//如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
this.closeOrder(orderNo);
//并更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
log.info("更改订单状态==== 》"+OrderStatus.CLOSED);
}
if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)) {
log.warn("核实订单已支付 ===> {}", orderNo);
//如果订单已支付,则更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//并记录支付日志
paymentInfoService.createPaymentInfoForAlipay(alipayTradeQueryResponse);
}
} catch (NullPointerException e) {
log.info("支付宝未创建订单,改为超时已关闭"+orderNo);
}
}
/**
* 关单接口的调用
* @param orderNo
*/
private void closeOrder(String orderNo) {
try {
log.info("关单接口的调用,订单号:"+orderNo);
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
//调用关单接口
AlipayTradeCloseResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
// throw new RuntimeException("关单接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("关单接口调用失败");
}
}
/**
* 退款
* @param orderNo
* @param reason
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {
try {
log.info("调用退款API");
//创建退款单
RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
//调用统一收单交易退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();
//组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);//订单编号
BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
//BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
bizContent.put("refund_reason", reason);//退款原因(可选)
request.setBizContent(bizContent.toString());
//执行请求,调用支付宝接口
AlipayTradeRefundResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundsInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
//更新退款单
refundsInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_ERROR.getType()); //退款失败
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建退款申请失败");
}
}
/**
* 查询退款
* @param orderNo
* @return
*/
@Override
public String queryRefund(String orderNo) {
try {
log.info("查询退款接口调用 ===> {}", orderNo);
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
bizContent.put("out_request_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("查单接口的调用失败");
return null;//订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口的调用失败");
}
}
/**
* 申请账单
* @param billDate
* @param type
* @return
*/
@Override
public String queryBill(String billDate, String type) {
try {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type", type);
bizContent.put("bill_date", billDate);
request.setBizContent(bizContent.toString());
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
//获取账单下载地址
Gson gson = new Gson();
HashMap resultMap = gson.fromJson(response.getBody(), HashMap.class);
LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");
return billDownloadUrl;
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("申请账单失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("申请账单失败");
}
}
}
package com.wx.service;
import com.alipay.api.AlipayApiException;
import java.util.Map;
public interface AliPayService {
String tradeCteate(Long productId);//统一收单下单支付页面接口调用
void processOrder(Map params);//处理业务,修改订单状态,记录支付日志
void cancelOrder(String orderNo);//支付宝用户取消订单
String queryOrder(String orderNo);//支付宝查询订单
void checkOrderStatus(String orderNo);//处理超时的订单
void refund(String orderNo, String reason);//申请退款
String queryRefund(String orderNo);
String queryBill(String billDate, String type);//下载账单
}
package com.wx.task;
import com.google.gson.Gson;
import com.wx.entity.OrderInfo;
import com.wx.enums.PayType;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AliPayService aliPayService;
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
* @throws Exception
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("支付宝定时任务启动====");
List orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.ALIPAY.getType());
for (OrderInfo orderInfo : orderInfoList){
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 === > {}", orderNo);
//核实订单状态:调用支付宝支付查单接口
aliPayService.checkOrderStatus(orderNo);
}
}
}
alipay.trade.page.pay(统一收单下单并支付页面接口)
NATAPP -https://natapp.cn/login内网穿透教程:
NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具https://natapp.cn/article/natapp_newbie