本文采用springboot+微信v3+mybatis-plus来实现的微信支付,
详情请参考微信官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml
下面的异步消息通知,必须是在公网ip才能接收到,不然,是接收不到消息的
首先第一步,依赖
<!--微信支付的sdk-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.5</version>
</dependency>
<!--微信支付 APIv2 SDK-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
第二步创建wx支付的属性文件wxpay.properties,
下面的所有属性都可以在微信支付平台中获取,具体的就不做介绍,如果实在不懂,可以私聊或者留言
商户私钥文件apiclient_key.pem存放在resource目录下即可
# 微信支付相关参数
# 商户号
wxpay.mch-id=161387xxxx
# 商户API证书序列号
wxpay.mch-serial-no=7D6432494D17BB46xxxxxxxxxxxxxxxxxxxxxxx
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=AbdAWozW8RWCFDxxxxxxxxxxxxxxxxxx
# APPID
wxpay.appid=wx9afbd6xxxxxxxxxx
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=http://127.0.0.1:8090
第三步创建配置文件
配置文件中会读取wxpay.properties的属性,便于后面的使用,
超级注意:在获取私钥文件时,如果是本地获取采用PemUtil.loadPrivateKey(new FileInputStream(filename));即可,但是当你打包到liunx服务器上部署时,就会报错,找不到死要文件,所以需要采用我下面未注释的方法区获取私钥文件,
package com.lcj.config;
import com.lcj.test.test;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.exception.ParseException;
import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.NonNull;
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 org.springframework.core.io.ClassPathResource;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE;
@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
* @throws FileNotFoundException
*/
private PrivateKey getPrivateKey(String filename) {
log.info("私钥路径为=========》:"+filename);
try {
//liunx系统运行找不到文件路径,需要采用这种方式加载
ClassPathResource resource = new ClassPathResource(filename);
InputStream inputStream = resource.getInputStream();
// return PemUtil.loadPrivateKey(new FileInputStream(filename));
return PemUtil.loadPrivateKey(inputStream);
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
} catch (IOException e) {
throw new RuntimeException("IOException", e);
}
}
/**
* 获取签名验证器
* @return
*/
@Bean
public Verifier getVerifier() throws NotFoundException, HttpCodeException, GeneralSecurityException, IOException {
// log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
/*wx支付版本0.3.0进行签名验证*/
//私钥签名对象
// PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
// WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
// ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
// wechatPay2Credentials,
// apiV3Key.getBytes(StandardCharsets.UTF_8));
/*wx支付版本0.4.5进行签名验证*/
//获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId,
new PrivateKeySigner(mchSerialNo, privateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(mchId);
return verifier;
}
public final boolean notificationHandler(HttpServletRequest req, Verifier verifier, String body) {
boolean result = false;
try {
// 构建request,传入必要参数
NotificationRequest request = new NotificationRequest.Builder()
.withSerialNumber(req.getHeader(WECHAT_PAY_SERIAL))
.withNonce(req.getHeader(WECHAT_PAY_NONCE))
.withTimestamp(req.getHeader(WECHAT_PAY_TIMESTAMP))
.withSignature(req.getHeader(WECHAT_PAY_SIGNATURE))
.withBody(body)
.build();
NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
// 验签和解析请求体
Notification notification = handler.parse(request);
// Assert.assertNotNull(notification);
System.out.println(notification.toString());
result = true;
} catch (ValidationException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
return result;
}
/**
* 获取http请求对象
*
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getwxPayClient(Verifier 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;
}
}
上面就是把准备工作做完了,完成了HttpClient的创建,签名验证器的创建等。下面就开始进行接口开发,后面的代码只涉及下单接口,和异步消息通知,其他接口就照猫画虎即可
我这里还是先提供一下,本人使用的数据库脚本
/*
Navicat Premium Data Transfer
Source Server : 本地mysql
Source Server Type : MySQL
Source Server Version : 50737
Source Host : localhost:3306
Source Schema : payment_demo
Target Server Type : MySQL
Target Server Version : 50737
File Encoding : 65001
Date: 13/05/2022 14:01:06
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_order_info
-- ----------------------------
DROP TABLE IF EXISTS `t_order_info`;
CREATE TABLE `t_order_info` (
`id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单id',
`title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单标题',
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单编号',
`user_id` bigint(20) NULL DEFAULT NULL COMMENT '用户id',
`product_id` bigint(20) NULL DEFAULT NULL COMMENT '支付产品id',
`total_fee` int(11) NULL DEFAULT NULL COMMENT '订单金额(分)',
`code_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单二维码连接',
`order_status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单状态',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`payment_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_order_info
-- ----------------------------
-- ----------------------------
-- Table structure for t_payment_info
-- ----------------------------
DROP TABLE IF EXISTS `t_payment_info`;
CREATE TABLE `t_payment_info` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单编号',
`transaction_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付系统交易编号',
`payment_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付类型',
`trade_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易类型',
`trade_state` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易状态',
`payer_total` int(11) NULL DEFAULT NULL COMMENT '支付金额(分)',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '通知参数',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_payment_info
-- ----------------------------
-- ----------------------------
-- Table structure for t_product
-- ----------------------------
DROP TABLE IF EXISTS `t_product`;
CREATE TABLE `t_product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商Bid',
`title` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称',
`price` int(11) NULL DEFAULT NULL COMMENT '价格(分)',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_product
-- ----------------------------
INSERT INTO `t_product` VALUES (1, 'Java程', 1, '2022-04-25 16:48:32', '2022-04-25 16:48:32');
INSERT INTO `t_product` VALUES (2, '大数据课程', 1, '2022-04-25 16:48:32', '2022-04-25 16:48:32');
INSERT INTO `t_product` VALUES (3, '前端端程', 1, '2022-04-25 16:48:32', '2022-04-25 16:48:32');
INSERT INTO `t_product` VALUES (4, 'UI程', 1, '2022-04-25 16:48:32', '2022-04-25 16:48:32');
-- ----------------------------
-- Table structure for t_refund_info
-- ----------------------------
DROP TABLE IF EXISTS `t_refund_info`;
CREATE TABLE `t_refund_info` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '款单id',
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单编号',
`refund_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户退款单编号',
`refund_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付系统退款单号',
`total_fee` int(11) NULL DEFAULT NULL COMMENT '原订单金额(分)',
`refund` int(11) NULL DEFAULT NULL COMMENT '退款金额(分)',
`reason` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退款原因',
`refund_status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退款状态',
`content_return` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '申请退款返回参数',
`content_notify` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '退款结果通知参数',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_refund_info
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
第四步实现微信网站下单
微信网站下单,我采用的是Native支付支付实现的,
下单支付的逻辑流程,
首先,我们先要创建订单
其次,将订单信息返回,并封装
然后,将订单信息作为参数,调用微信接口,进行支付
再然后,判断接口返回的状态,并将微信接口返回的支付url保存到数据库一份,
最后,将url返回给前端
那我这里就直接上代码
WxPayController
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
@Resource
private WxPayConfig wxPayConfig;
@ApiOperation("调用同一下单API,生产二维码")
@PostMapping("native/{productId}")
public R nativePay(@PathVariable Long productId) throws IOException {
log.info("发起支付请求");
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
Gson gson = new Gson();
HashMap<String, String> map = null;//应答对象
try {
map = new HashMap<>();
// 处理通知参数
String body = HttpUtils.readData(request);
HashMap<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id====>{}", requestId);
// log.info("支付通知的完整数据====>{}",body);
//签名验证
// WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, body, requestId);
//if (!wechatPay2ValidatorForRequest.validate(request)) {
//签名验证
if (!wxPayConfig.notificationHandler(request, verifier, body)) {
log.error("通知验签失败");
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理订单
wxPayService.processOrder(bodyMap);
//应答超时
//模拟接收微信端的重复通知
//TimeUnit.SECONDS.sleep(5);
//成功的应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
} catch (Exception e) {
e.printStackTrace();
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
}
return gson.toJson(map);
}
}
接下来就是WxpayServerImpl
@Slf4j
@Service
public class WxPayServiceIml implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayNoSignClient;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
private ReentrantLock lock = new ReentrantLock();
/**
* 创建订单调用native支付接口
*
* @param productId
* @return
* @throws IOException
*/
@Override
public Map<String, Object> nativePay(Long productId) throws IOException {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
//获取二维码地址
String codeUrl = orderInfo.getCodeUrl();
//判断订单存在且二维码地址不为空
if (!ObjectUtils.isEmpty(orderInfo) && StringUtils.hasText(codeUrl)) {
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
//订单存入数据库
log.info("调用统一下单API");
//TODO 调用同一下单接口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());//APPID
paramsMap.put("mchid", wxPayConfig.getMchId());//商户号
paramsMap.put("description", orderInfo.getTitle());//订单标题
paramsMap.put("out_trade_no", orderInfo.getOrderNo());//订单编号
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//支付信息通知
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());//支付金额
amountMap.put("currency", "CNY");//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 = wxPayNoSignClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
@Override
public void processOrder(HashMap<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText = decryptFromResource(bodyMap);
Gson gson = new Gson();
//将明文转换为map
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String) plainTextMap.get("out_trade_no");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁,成功则返回true,获取失败则返回false,不必一直等待锁的释放
if (lock.tryLock()) {
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService.getOrderStatus(orderNo);
//判断订单不是未支付状态直接返回;
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
//主动释放锁
lock.unlock();
}
}
}
OrderInfoServiceImpl 订单类
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Override
public OrderInfo createOrderByProductId(Long productId,String paymentType) {
//根据productId查到已存在的未支付订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId,paymentType);
if (!ObjectUtils.isEmpty(orderInfo)) {
return orderInfo;
}
Product product = productMapper.selectById(productId);
//生成订单
orderInfo = new OrderInfo();
orderInfo.setTitle(product.getTitle());//设置标题
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//设置订单号
orderInfo.setProductId(productId);//设置商品id
orderInfo.setTotalFee(product.getPrice());//设置金额,单位是分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());//设置订单状态
orderInfo.setPaymentType(paymentType);
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 根据订单号获取订单状态
*
* @param orderNo
* @return
*/
@Override
public String getOrderStatus(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(orderInfo)) {
return null;
}
return orderInfo.getOrderStatus();
}
/**
* 根据订单号,更新订单状态
*
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("订单状态更新成了====>{}", orderStatus.getType());
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo, queryWrapper);
}
}
PaymentInfoServiceImpl 支付日记记录类
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
/**
* 记录支付日志:微信支付
*
* @param plainText
*/
@Override
public void createPaymentInfo(String plainText) {
log.info("记录支付日志");
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
//订单号
String orderNo = (String) plainTextMap.get("out_trade_no");
//业务编号
String transactionId = (String) plainTextMap.get("transaction_id");
//支付类型
String tradeType = (String) plainTextMap.get("trade_type");
//交易状态
String tradeState = (String) plainTextMap.get("trade_state");
Map<String, Object> amount = (Map) plainTextMap.get("amount");
//用户实际支付金额
int 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);
}
}
接下来就是手机H5支付了,H5支付特别简单,但是微信返回的接口是无法直接访问的,必须是你在微信支付平台申请绑定的域名进行跳转之后才能够进行访问,否则就会报错。什么意思呢,就是你的项目必须在你绑定的域名的服务器启动,才能够跳转成功微信的H5支付接口
/**
* h5下单
* @param request
* @return
*/
@ApiOperation("微信H5支付")
@PostMapping("/h5Pay/{productId}")
public R h5pay(HttpServletRequest request , @PathVariable Long productId) {
log.info("发起支付请求");
Map<String, Object> map = null;
try {
map = wxPayService.h5pay(productId, request);
} catch (IOException e) {
e.printStackTrace();
new RuntimeException(e.getMessage());
}
return R.ok().setData(map);
}
@Override
public Map<String, Object> h5pay(Long productId, HttpServletRequest request) throws IOException {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXMOBILEPAY.getType());
String codeUrl = orderInfo.getCodeUrl();
//如果已存在订单,且订单未支付,将订单的二维码直接返回给用户
if (orderInfo != null && codeUrl != null) {
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
//订单入库
log.info("调用同一下单API");
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.H5_PAY.getType()));
//请求body的参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());//APPID
paramsMap.put("mchid", wxPayConfig.getMchId());//商户号
paramsMap.put("description", orderInfo.getTitle());//订单标题
paramsMap.put("out_trade_no", orderInfo.getOrderNo());//订单编号
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//支付信息通知
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());//支付金额
amountMap.put("currency", "CNY");//CNY:人民币
paramsMap.put("amount", amountMap);
Map sceneInfoMap = new HashMap();
String requestIp = CommonUtils.getIpAddr(request);
sceneInfoMap.put("payer_client_ip",requestIp);
paramsMap.put("scene_info",sceneInfoMap);
Map h5InfoMap = new HashMap();
h5InfoMap.put("type","Wap");
sceneInfoMap.put("h5_info",h5InfoMap);
//将参数转换成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 = wxPayNoSignClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException(resultMap.get("message"));
}
//二维码
codeUrl = resultMap.get("h5_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderNo);
return map;
} finally {
response.close();
}
}
是不是很简单,但是如果没研究过的话,会有很多坑,让人触不及防。
最后在贴上上面用到的状态类
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* h5下单
*/
H5_PAY("/v3/pay/transactions/h5"),
/**
* 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;
}
@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;
}
@AllArgsConstructor
@Getter
public enum WxTradeState {
/**
* 支付成功
*/
SUCCESS("SUCCESS"),
/**
* 未支付
*/
NOTPAY("NOTPAY"),
/**
* 已关闭
*/
CLOSED("CLOSED"),
/**
* 转入退款
*/
REFUND("REFUND");
/**
* 类型
*/
private final String type;
}
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 超时已关闭
*/
CLOSED("超时已关闭"),
/**
* 用户已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
/**
* 类型
*/
private final String type;
}
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝"),
/**
* 支付宝手机网站
*/
ALIMOBILEPAY("支付宝-手机"),
/**
* 微信手机网站
*/
WXMOBILEPAY("微信-手机");
/**
* 类型
*/
private final String type;
}
最后贴一个将阿里支付的返回接口进行跳转的测试的html
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta charset="utf-8">
<title>支付</title>
</head>
<body>
<div id="myQrcode"></div>
<script src="https://cdn.bootcss.com/jquery/1.5.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script>
jQuery('#myQrcode').qrcode({
text: "${codeUrl}"
});
</script>
</body>
</html>
微信H5支付返回的支付url的html测试
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="./js/jquery-3.6.0.min.js"></script>
<script src="./js/jquery.qrcode.js"></script>
<script>
$(function(){
$("#s").click(function(){
var ins=$("#ins").val();
$.ajax({
type: "post",
url: api/alipay/h5Pay/ins,
async: false,
dataType: "json",
success: function (json) {
if (json.result == "ok") {
//重定向微信H5支付返回的url
window.location.href = json.data.codeUrl;
} else {
alert(json.code);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest.status);
console.log(XMLHttpRequest.readyState);
console.log(textStatus);
}
});
});
});
</script>
</head>
<body>
<h1>我是html页面</h1>
<input type="text" id="ins" />
<button id="s">发起ajax</button>
<span id="re"></span>
</body>
</html>