目录
前言
代码实现
1、yaml配置
#微信配置
wechat:
appId: xxxxxxxx
secret: xxxxxxxxxxxxxxxxxxxxxxxxxxx
#商户号
mchId: xxxxxxxx
#商户秘钥
key: xxxxxxxxxxxxxxxxxxxxxxx
#终端IP
clientIp: xxx.xx.xxx.xxx
#订单微信支付回调地址
orderPayNotifyUrl: https://xx.xxxxxxx.com/wechat/callback
#订单微信退款回调地址
orderRefundNotifyUrl: https://xx.xxxxxxx.com/wechat/refund/callback
#退款证书
refundCertPath: cert/apiclient_cert.p12
2、前端唤醒微信支付controller
@PostMapping(value = "/wechat")
@ApiOperation("微信支付")
@ApiImplicitParam(paramType = Constants.HEADER, dataType = Constants.STRING, name = Constants.AUTHORIZATION, value = "授权token", required = true)
public Result<OrderWechatPayVO> wechat(@Valid @RequestBody OrderPayDTO params) {
OrderWechatPayVO orderWechatPayVO = orderPayService.wechatPay(params);
return Result.ok(orderWechatPayVO);
}
3、前端唤醒微信支付controller传参
@Data
@Accessors(chain = true)
@ApiModel("订单支付")
public class OrderPayDTO {
@ApiModelProperty(value = "订单id", required = true)
@NotNull(message = "订单id不能为空")
private Long orderId;
@ApiModelProperty(value = "下单用户终端 1.app 2.微信小程序", required = true)
@NotNull(message = "下单用户终端不能为空")
private Integer terminal;
@ApiModelProperty(value = "是否部分支付", required = true)
@NotNull(message = "是否部分支付不能为空")
private Boolean partPay;
@ApiModelProperty(value = "部分支付金额,部分支付时必传", required = false)
private BigDecimal payPrice;
}
4、前端唤醒微信支付controller返回参数
@Data
@ApiModel(value = "订单微信支付返回")
public class OrderWechatPayVO {
@ApiModelProperty(value = "支付流水id")
private Long id;
@ApiModelProperty(value = "支付流水号")
private String payNo;
@ApiModelProperty(value = "支付金额")
private BigDecimal payPrice;
@ApiModelProperty(value = "支付唤醒参数")
private Map<String, Object> payData;
}
5、前端唤醒微信支付service具体实现
/**
* 微信支付
*
* @param params
*/
@Transactional(rollbackFor = Exception.class)
public OrderWechatPayVO wechatPay(OrderPayDTO params) {
//创建支付流水
OrderPay orderPay = createOrderPay(params, PayMethodCode.WECHAT_PAY.getCode());
Map<String, Object> payResult;
//app支付
if (orderPay.getTerminal() == OrderTerminalCode.APP.getCode()) {
payResult = wechatPayUtil.appOrder(orderPay.getPayNo(), orderPay.getPayPrice(), "购买", wechatProperties.getCourseOrderPayNotifyUrl());
} else {
//TODO 小程序支付获取会员openid
payResult = wechatPayUtil.mpOrder(orderPay.getPayNo(), orderPay.getPayPrice(), "购买", "", wechatProperties.getCourseOrderPayNotifyUrl());
}
OrderWechatPayVO wechatPayVO = new OrderWechatPayVO();
wechatPayVO.setId(orderPay.getId());
wechatPayVO.setPayNo(orderPay.getPayNo());
wechatPayVO.setPayPrice(orderPay.getPayPrice());
wechatPayVO.setPayData(payResult);
return wechatPayVO;
}
/**
* 创建支付流水
*
* @param params
* @param patMethod
* @return
*/
private OrderPay createOrderPay(OrderPayDTO params, int patMethod) {
Order order = orderRepository.findByIdAndMemberId(params.getOrderId(), SecurityContextHolder.getUserId());
Assert.notNull(order, "订单不存在");
Assert.isFalse(order.getStatus() != OrderStatusCode.WAIT_PAY.getCode() && order.getStatus() != OrderStatusCode.PART_PAY.getCode(), "该订单无法支付");
OrderPay orderPay = new OrderPay();
orderPay.setMemberId(courseOrder.getMemberId());
orderPay.setOrderId(courseOrder.getId());
orderPay.setOrderNo(courseOrder.getOrderNo());
orderPay.setTerminal(params.getTerminal());
orderPay.setPayMethod(patMethod);
orderPay.setOrderType(PayOrderTypeCode.COURSE_ORDER.getCode());
orderPay.setStatus(PayStatusCode.WAIT_PAY.getCode());
String payNo = DateUtil.format(new Date(), "yyyyMMddHHmmssSSS") + RandomUtil.randomNumbers(7);
orderPay.setPayNo(payNo);
orderPay.setPayPrice(courseOrder.getWaitPayPrice());
orderPayRepository.insert(orderPay);
return orderPay;
}
6、常量配置
/**
* 微信支付常量
*
* @author: CYL
* @date: 2022-01-17 16:09
*/
public final class WechatPayConstants {
/**
* 小程序支付url
*/
public static final String JSAPI_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* app支付url
*/
public static final String APP_PAY_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
/**
* 查询订单url
*/
public static final String QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 退款url
*/
public static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
/**
* 响应成功
*/
public static final String SUCCESS = "SUCCESS";
/**
* 响应失败
*/
public static final String FAIL = "FAIL";
/**
* JSAPI--JSAPI支付(或小程序支付)
*/
public static final String JSAPI = "JSAPI";
/**
* APP--app支付
*/
public static final String APP = "APP";
/**
* 支付加密方式md5
*/
public static final String MD5 = "MD5";
}
/**
* 订单状态
*
* @author: cyl
* @date: 2022-01-14 11:53
*/
@Getter
public enum OrderStatusCode {
WAIT_PAY(1, "待支付"),
PART_PAY(2, "部分支付"),
PAY_SUCCESS(3, "支付成功"),
CANCEL(7, "已取消"),
CLOSE(8, "已关闭"),
;
private final int code;
private final String msg;
OrderStatusCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
/**
* 支付方式
*
* @author: cyl
* @date: 2022-01-17 15:52
*/
@Getter
public enum PayMethodCode {
WECHAT_PAY(1, "微信支付"),
ALIPAY_PAY(2, "支付宝支付"),
BALANCE_PAY(3, "余额支付"),
POS_PAY(4, "POS机支付");
private final int code;
private final String msg;
PayMethodCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
package com.lw.order.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 微信支付配置
*
* @author: cyl
* @date: 2022-01-17 16:14
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatProperties {
/**
* 微信appId
*/
private String appId;
/**
* 微信secret
*/
private String secret;
/**
* 商户号
*/
private String mchId;
/**
* 商户key
*/
private String key;
/**
* 服务端ip地址
*/
private String clientIp;
/**
* 订单支付回调地址
*/
private String courseOrderPayNotifyUrl;
/**
* 订单微信退款回调地址
*/
private String courseOrderRefundNotifyUrl;
/**
* 退款证书存储路径
*/
private String refundCertPath;
}
7、微信支付工具类
package com.lw.order.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpRequest;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.lw.order.constant.WechatPayConstants;
import com.lw.order.properties.WechatProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.math.BigDecimal;
import java.security.KeyStore;
import java.util.List;
import java.util.Map;
/**
* 微信支付
*
* @author: cyl
* @date: 2022-01-17 16:06
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class WechatPayUtil {
private final WechatProperties wechatProperties;
/**
* 小程序微信支付
*
* @param outTradeNo 订单号
* @param amount 支付金额
* @param body 支付明细
* @param openid 用户openid
* @param notifyUrl 支付回调地址
* @return
*/
public Map<String, Object> mpOrder(String outTradeNo, BigDecimal amount, String body, String openid, String notifyUrl) {
//订单金额单位分
int totalAmount = NumberUtil.mul(amount, 100).intValue();
Map<String, Object> params = Maps.newHashMap();
params.put("appid", wechatProperties.getAppId());
params.put("mch_id", wechatProperties.getMchId());
params.put("nonce_str", RandomUtil.randomString(30));
params.put("sign_type", WechatPayConstants.MD5);
params.put("body", body);
params.put("openid", openid);
params.put("trade_type", WechatPayConstants.JSAPI);
params.put("out_trade_no", outTradeNo);
params.put("total_fee", String.valueOf(totalAmount));
params.put("spbill_create_ip", wechatProperties.getClientIp());
params.put("notify_url", notifyUrl);
params.put("sign", buildSign(params));
String xml = XmlUtil.mapToXmlStr(params);
log.info("app小程序支付xml:{}", xml);
String result = HttpRequest.post(WechatPayConstants.JSAPI_PAY_URL)
.body(xml)
.timeout(20000)
.execute()
.body();
log.info("微信支付响应:{}", result);
Map<String, Object> map = XmlUtil.xmlToMap(result);
if (!WechatPayConstants.SUCCESS.equals(map.get("return_code"))) {
log.error("微信支付异常:{}", map.get("return_msg").toString());
throw new IllegalArgumentException("微信支付异常");
}
if (!WechatPayConstants.SUCCESS.equals(map.get("result_code"))) {
log.error("微信支付异常:{}", map.get("err_code_des").toString());
throw new IllegalArgumentException("微信支付异常");
}
long timestamp = System.currentTimeMillis() / 1000;
Map<String, Object> data = Maps.newHashMap();
data.put("appid", map.get("appid"));
data.put("noncestr", map.get("nonce_str"));
data.put("package", "Sign=WXPay");
data.put("partnerid", map.get("mch_id"));
data.put("prepayid", map.get("prepay_id"));
data.put("timestamp", timestamp);
String sign = buildSign(data);
data.put("paySign", sign);
return data;
}
/**
* app微信支付
*
* @param outTradeNo 订单号
* @param amount 支付金额
* @param body 商品描述
* @param notifyUrl 支付回调地址
* @return
*/
public Map<String, Object> appOrder(String outTradeNo, BigDecimal amount, String body, String notifyUrl) {
//订单金额单位分
int totalAmount = NumberUtil.mul(amount, 100).intValue();
Map<String, Object> params = Maps.newHashMap();
params.put("appid", wechatProperties.getAppId());
params.put("mch_id", wechatProperties.getMchId());
params.put("nonce_str", RandomUtil.randomString(30));
params.put("sign_type", WechatPayConstants.MD5);
params.put("body", body);
params.put("trade_type", WechatPayConstants.APP);
params.put("out_trade_no", outTradeNo);
params.put("total_fee", String.valueOf(totalAmount));
params.put("spbill_create_ip", wechatProperties.getClientIp());
params.put("notify_url", notifyUrl);
params.put("sign", buildSign(params));
String xml = XmlUtil.mapToXmlStr(params);
log.info("app微信支付xml:{}", xml);
String result = HttpRequest.post(WechatPayConstants.APP_PAY_URL)
.body(xml)
.timeout(20000)
.execute()
.body();
log.info("微信支付响应:{}", result);
Map<String, Object> map = XmlUtil.xmlToMap(result);
if (!WechatPayConstants.SUCCESS.equals(map.get("return_code"))) {
log.error("微信支付异常:{}", map.get("return_msg").toString());
throw new IllegalArgumentException("微信支付异常");
}
if (!WechatPayConstants.SUCCESS.equals(map.get("result_code"))) {
log.error("微信支付异常:{}", map.get("err_code_des").toString());
throw new IllegalArgumentException("微信支付异常");
}
long timestamp = System.currentTimeMillis() / 1000;
Map<String, Object> data = Maps.newHashMap();
data.put("appid", map.get("appid"));
data.put("partnerid", map.get("mch_id"));
data.put("prepayid", map.get("prepay_id"));
data.put("noncestr", map.get("nonce_str"));
data.put("timestamp", String.valueOf(timestamp));
data.put("package", "Sign=WXPay");
data.put("sign", buildSign(map));
return data;
}
/**
* 微信退款
*
* @param outTradeNo 订单号
* @param outRefundNo 订单退款单号
* @param totalAmount 订单总金额
* @param refundAmount 退款金额
* @param notifyUrl 退款回调地址
* @return
*/
public String refund(String outTradeNo, String outRefundNo, BigDecimal totalAmount, BigDecimal refundAmount, String notifyUrl) {
try {
//订单金额单位分
int totalAmountFee = NumberUtil.mul(totalAmount, 100).intValue();
//退款金额单位分
int refundAmountFee = NumberUtil.mul(refundAmount, 100).intValue();
Map<String, Object> params = Maps.newHashMap();
params.put("appid", wechatProperties.getAppId());
params.put("mch_id", wechatProperties.getMchId());
params.put("nonce_str", RandomUtil.randomString(30));
params.put("out_trade_no", outTradeNo);
params.put("out_refund_no", outRefundNo);
params.put("total_fee", String.valueOf(totalAmountFee));
params.put("refund_fee", String.valueOf(refundAmountFee));
params.put("notify_url", notifyUrl);
params.put("sign", buildSign(params));
String xml = XmlUtil.mapToXmlStr(params);
log.info("退款xml:{}", xml);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(wechatProperties.getRefundCertPath())) {
keyStore.load(inputStream, wechatProperties.getMchId().toCharArray());
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, wechatProperties.getMchId().toCharArray()).build();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, new DefaultHostnameVerifier());
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
HttpPost httpPost = new HttpPost(WechatPayConstants.REFUND_URL);
httpPost.setEntity(new StringEntity(xml, "UTF-8"));
HttpResponse response = httpClient.execute(httpPost);
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
Map<String, Object> map = XmlUtil.xmlToMap(result);
if (WechatPayConstants.FAIL.equals(map.get("return_code"))) {
log.error(map.get("return_msg").toString());
return map.get("return_msg").toString();
}
if (WechatPayConstants.FAIL.equals(map.get("result_code"))) {
log.error("退款申请提交业务失败," + map.get("err_code_des") + ";错误代码:" + map.get("err_code"));
return "退款申请提交业务失败," + map.get("err_code_des") + ";错误代码:" + map.get("err_code");
}
if (WechatPayConstants.SUCCESS.equals(map.get("result_code"))) {
return WechatPayConstants.SUCCESS;
}
} catch (Exception e) {
log.error("微信退款异常", e);
}
return WechatPayConstants.FAIL;
}
/**
* 查询订单状态
*
* @param outTradeNo
* @return
*/
public boolean queryOrder(String outTradeNo) {
Map<String, Object> params = Maps.newHashMap();
params.put("appid", wechatProperties.getAppId());
params.put("mch_id", wechatProperties.getMchId());
params.put("nonce_str", RandomUtil.randomString(30));
params.put("sign_type", WechatPayConstants.MD5);
params.put("out_trade_no", outTradeNo);
params.put("sign", buildSign(params));
String xml = XmlUtil.mapToXmlStr(params);
String result = HttpRequest.post(WechatPayConstants.QUERY_URL)
.body(xml)
.timeout(20000)
.execute()
.body();
Map<String, Object> map = XmlUtil.xmlToMap(result);
if (!WechatPayConstants.SUCCESS.equals(map.get("return_code"))) {
log.error("微信查询接口失败:{}", result);
return false;
}
if (!WechatPayConstants.SUCCESS.equals(map.get("result_code"))) {
log.error("微信查询接口失败:{}", result);
return false;
}
return WechatPayConstants.SUCCESS.equals(map.get("trade_state"));
}
/**
* 参数签名
*
* @param params 参数
* @return 签名字符串
*/
public String buildSign(Map<String, Object> params) {
List<String> list = Lists.newArrayList();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if ("sign".equals(entry.getKey())) {
continue;
}
//参数值为空不参与签名
if (StrUtil.isNotEmpty(entry.getValue().toString())) {
list.add(entry.getKey() + "=" + entry.getValue());
}
}
list.sort(String::compareTo);
list.add("key=" + wechatProperties.getKey());
return SecureUtil.md5(Joiner.on("&").join(list)).toUpperCase();
}
/**
* 支付回调响应
*
* @param returnCode 返回状态
* @param returnMsg 返回提示
* @return
*/
public String buildResponse(String returnCode, String returnMsg) {
Map<String, String> map = Maps.newHashMap();
map.put("return_code", returnCode);
map.put("return_msg", returnMsg);
return XmlUtil.mapToXmlStr(map);
}
}
8、微信支付回调
@PostMapping(value = "/wechat/callback")
@ApiOperation(value = "微信支付回调", hidden = true)
public String wechatCallback(HttpServletRequest request) {
RLock lock = null;
try {
Document xml = XmlUtil.readXML(request.getInputStream());
String xmlStr = XmlUtil.toStr(xml);
log.info("[微信支付异步回调通知报文]:" + xmlStr);
Map<String, Object> resultMap = XmlUtil.xmlToMap(xmlStr);
if (!WechatPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("return_code").toString())) {
//响应不成功
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "return_code不为SUCCESS");
}
String tradeNo = resultMap.get("out_trade_no").toString();
String sign = wechatPayUtil.buildSign(resultMap);
if (!sign.equals(resultMap.get("sign"))) {
log.error("[微信支付回调通知签名校验不通过]");
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "签名校验不通过");
}
if (!WechatPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("result_code").toString())) {
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "支付未成功");
}
//使用redisson获取分布式锁
lock = redissonClient.getLock(DLockConstants.WECHAT_OUT_TRADE_NO + tradeNo);
//尝试加锁,设置等待时间10秒,锁超时时间10秒
boolean locked = lock.tryLock(10, 10, TimeUnit.SECONDS);
if (locked) {
OrderPay orderPay = orderPayService.findByPayNo(tradeNo);
if (null == orderPay || orderPay.getStatus() != PayStatusCode.WAIT_PAY.getCode()) {
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "订单不存在或已支付");
}
//重新调用微信接口查询订单状态
if (wechatPayUtil.queryOrder(tradeNo)) {
orderPayService.paySuccess(orderPay, PayMethodCode.WECHAT_PAY.getCode(), resultMap.get("transaction_id").toString());
return wechatPayUtil.buildResponse(WechatPayConstants.SUCCESS, "支付成功");
}
log.error("微信支付回调通知二次校验不通过:{}", tradeNo);
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "二次校验订单不通过");
}
log.error("微信支付回调通知排队中:{}", tradeNo);
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "处理异常");
} catch (Exception e) {
log.error("[微信支付回调处理异常]", e);
return wechatPayUtil.buildResponse(WechatPayConstants.FAIL, "处理异常");
} finally {
if (null != lock && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
9、解析回调参数
/**
* 解析回调参数
*
* @param request
* @return
*/
private static Map<String, String> requestParam(HttpServletRequest request) {
Map<String, String> params = Maps.newHashMap();
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
String valueStr = String.join(",", entry.getValue());
params.put(entry.getKey(), valueStr);
}
return params;
}