一、前言
微信支付账号类型分为商户平台和合作伙伴平台,今天主要是梳理商品平台微信支付流程。
商品平台文档地址,(在接入前建议仔细阅读这份文档,会少走很多弯路!!!)
小程序下单 - 小程序支付 | 微信支付商户文档中心
二、接入流程
以下是我SpringBoot项目接入微信支付V3流程,如果还有更好的集成方式,欢迎留言探讨。
2.1、pom.xml导入微信支付SDK
<!--微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
2.2、application.yaml配置微信支付相关配置
# 微信支付相关参数
wxpay:
# 商户号
mch-id: 1888888888
# 商户API证书序列号
mch-serial-no: 188D8D888888888A476B7DEB5F820082570CA4BA
# APIv3密钥
api-v3-key: 88888888
# APPID
appId: wx88888ff5ee888f88
# 微信服务器地址
domain: https://api.mch.weixin.qq.com
# 微信支付回调地址 https和外网可以访问的地址
notify-domain: https://35p3811d81.yicp.fun
# 商户私钥 此处注意换行,回车等不可见字符
private-key: BIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDV9Hl2qVkpVaAfBiH4SCv6dFbWS/Rem+HhFwQ8mV1XtT7kZUmaSEbrfOE3NSHx5oI+zpnr/nNHSh9vTYUbbH0LcE7Ad+oLtLNVTsZXLOrUPoEgcxSokaNHXd9gfGpKX/i8rkdNnOKTESBxGf8tGrQniBycWwGGJMUAngVeaJQKVN3PC+4WrLFm/x1gWUeGOvXNuKuAyu7VPgN+UKAUC1BuadwM6eokA
2.3、WxPayController.java(回调通知接口一定要从权限拦截器中剔除)
/**
* 微信支付API
*/
@PostMapping("/save")
@Resubmit
public ApiResponse<?> saveWxPayOrder(@RequestBody OrderWxPayAppReq orderWxPayAppReq) throws Exception {
orderWxPayAppReq.setUserId(getUserId());
return ApiResponse.ok(orderWxPayService.saveWxPayOrder(orderWxPayAppReq));
}
/**
* 微信退款API
*/
@PostMapping("/refunds/apply")
@Resubmit
public ApiResponse<?> refunds(@RequestBody OrderWxPayAppReq orderWxPayAppReq) throws Exception {
log.info("申请退款");
orderWxPayAppReq.setUserId(getUserId());
return ApiResponse.ok(orderWxPayService.wxRefund(orderWxPayAppReq));
}
/**
* 微信支付回调通知
*/
@PostMapping("/callback/pay/notify")
public ApiResponse<?> payCallBack(HttpServletRequest request) {
Gson gson = new Gson();
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if (!wechatPay2ValidatorForRequest.validate(request)) {
//失败应答
return ApiResponse.error("通知验签失败");
}
//处理订单
orderWxPayService.processOrder(bodyMap);
//成功应答
return ApiResponse.ok("成功");
} catch (Exception e) {
e.printStackTrace();
//失败应答
return ApiResponse.error("失败");
}
}
/**
* 微信退款结果通知
*/
@PostMapping("/callback/refunds/notify")
public ApiResponse<?> refundsNotify(HttpServletRequest request) {
log.info("退款通知执行");
try {
//处理通知参数
String body = HttpUtils.readData(request);
Gson gson = new Gson();
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if (!wechatPay2ValidatorForRequest.validate(request)) {
return ApiResponse.error("通知验签失败");
}
//处理退款单
orderWxPayService.processRefund(bodyMap);
return ApiResponse.ok("成功");
} catch (Exception e) {
e.printStackTrace();
//失败应答
return ApiResponse.error("失败");
}
}
2.4、WxPayService.java
@Override
public Map<String, String> saveWxPayOrder(OrderWxPayAppReq orderWxPayAppReq) throws IOException {
log.info("调用统一下单API");
List<Long> orderIdList = orderWxPayAppReq.getOrderIdList();
if (CollUtil.isEmpty(orderIdList)) {
throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003);
}
//查询订单
LambdaQueryWrapper<OrderInfo> orderQueryWrapper = new LambdaQueryWrapper<OrderInfo>()
.in(OrderInfo::getOrderId, orderIdList)
.eq(OrderInfo::getDeleted, Boolean.FALSE);
List<OrderInfo> orderInfoList = orderInfoMapper.selectList(orderQueryWrapper);
if (CollUtil.isEmpty(orderInfoList)) {
throw new BaseRuntimeException(OrderErrorCodeEnums.ORDER_001);
}
AtomicReference<Long> totalPayMoney = new AtomicReference<>(0L);
String orderIdStr = orderInfoList.stream().map(o -> {
totalPayMoney.updateAndGet(v -> v + o.getPayMoney());
return String.valueOf(o.getOrderId());
}).collect(Collectors.joining(","));
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType()));
String paymentKey = OrderNoUtils.createOrderNo(OrderNoUtils.PAYMENT_SERIAL_NUMBER_PREFIX);
// key值有效期为1小时
redisUtil.set(RedisKeys.OrderKeys.ORDER_PAYMENT_KEY + paymentKey, orderIdStr, 3600);
// 请求body参数
Gson gson = new Gson();
Map<String, Object> paramsMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
paramsMap.put("appid", wxPayConfig.getAppId());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderIdStr);
paramsMap.put("out_trade_no", paymentKey);
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.JSAPI_NOTIFY.getType()));
Map<String, Object> amountMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
Long totalMoney = totalPayMoney.get();
Boolean enableVirtualPay = Optional.ofNullable(enabled.getEnableVirtualPay()).orElse(Boolean.TRUE);
if (enableVirtualPay) {
totalMoney = 1L;
}
amountMap.put("total", totalMoney);
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
Map<String, String> payerMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
final UserThirdInfoApiVo userThirdInfoApiVo = userFeignApi.getUserThirdInfoRsp(orderWxPayAppReq.getUserId(), "WX", wxPayConfig.getManagerAppId());
payerMap.put("openid", userThirdInfoApiVo.getOpenId());
paramsMap.put("payer", payerMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
//返回二维码
Map<String, String> map = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
log.info("成功, 支付接口返回结果:{} ", bodyAsString);
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//处理成功
if (statusCode != 200 && statusCode != 204) {
throw new BaseRuntimeException(OrderErrorCodeEnums.ORDER_002.code, resultMap.get("message"));
}
//预支付交易会话标识
String prePayId = resultMap.get("prepay_id");
String prePayIdStr = "prepay_id=" + prePayId;
long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
String timestamp = String.valueOf(epochSecond);
String nonceStr = NonceUtil.createNonce(32);
//关联的公众号的appid
map.put("appId", wxPayConfig.getAppId());
//时间戳
map.put("timeStamp", timestamp);
//生成随机字符串
map.put("nonceStr", nonceStr);
map.put("package", prePayIdStr);
map.put("signType", "RSA");
map.put("paySign", wxPayConfig.getPaySign(timestamp, nonceStr, prePayIdStr));
} finally {
response.close();
}
return map;
}
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
//将明文转换成map
HashMap plainTextMap = new Gson().fromJson(decryptFromResource(bodyMap), HashMap.class);
String paymentKey = String.valueOf(plainTextMap.get("out_trade_no"));
Object orderIdObj = redisUtil.get(RedisKeys.OrderKeys.ORDER_PAYMENT_KEY + paymentKey);
if (ObjectUtil.isNull(orderIdObj)) {
return Boolean.FALSE;
}
// 处理支付成功后业务数据
... ...
return Boolean.TRUE;
}
/**
* 退款
*
* @param orderWxPayAppReq
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean wxRefund(OrderWxPayAppReq orderWxPayAppReq) throws IOException {
if (ObjectUtil.isEmpty(orderWxPayAppReq.getAfterOrderId())) {
throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003.code, OrderErrorCodeEnums.BASE_003.desc);
}
OrderAfter orderAfter = orderAfterMapper.selectById(orderWxPayAppReq.getAfterOrderId());
OrderInfo orderInfo = orderInfoMapper.selectById(orderAfter.getOrderId());
if (ObjectUtil.isNull(orderInfo.getPaySerialNumber())) {
return Boolean.FALSE;
}
// 请求body参数
Gson gson = new Gson();
Map<String, Object> paramsMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
paramsMap.put("out_trade_no", orderInfo.getPaySerialNumber());
paramsMap.put("out_refund_no", String.valueOf(orderAfter.getAfterOrderId()));
paramsMap.put("reason", orderWxPayAppReq.getRefundReason());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
Map<String, Object> amountMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
final Boolean enableVirtualPay = Optional.ofNullable(enabled.getEnableVirtualPay()).orElse(Boolean.TRUE);
Long refund = orderAfter.getApplyRefundAmount();
Long total = orderInfo.getPayMoney();
if (enableVirtualPay) {
refund = 1L;
total = 1L;
}
amountMap.put("refund", refund);
amountMap.put("total", total);
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
//调用统一下单API
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
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();
Map<String, Object> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
if (statusCode != 200 && statusCode != 204) {
throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003.getCode(), String.valueOf(bodyMap.get("message")));
}
//保存退款日志
return saveRefundPaymentBillInfo(bodyMap);
} finally {
response.close();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean processRefund(Map<String, Object> bodyMap) throws GeneralSecurityException, InterruptedException {
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
HashMap plainTextMap = new Gson().fromJson(plainText, HashMap.class);
String paySerialNumber = (String) plainTextMap.get("out_trade_no");
String outRefundNo = (String) plainTextMap.get("out_refund_no");
// 处理退款后的业务逻辑
... ...
return Boolean.TRUE;
}
/**
* 对称解密
*
* @param bodyMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> 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);
return plainText;
}
2.5、WxApiType.java
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* JSAPI下单
* https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
*/
JSAPI_PAY("/v3/pay/transactions/jsapi"),
/**
* 申请退款
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
*/
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds");
/**
* 类型
*/
private final String type;
}
2.6、WxPayConfig.java
/**
* 微信支付配置类
*/
@Configuration
@ConfigurationProperties(prefix = "wxpay")
@Data
@Slf4j
public class WxPayConfig {
/**
* 商户号
*/
private String mchId;
/**
* 商户API证书序列号
*/
private String mchSerialNo;
/**
* APIv3密钥
*/
private String apiV3Key;
/**
* APPID
*/
private String appId;
/**
* 微信服务器地址
*/
private String domain;
/**
* 微信回调通知地址
*/
private String notifyDomain;
/**
* 商户密钥
*/
private String privateKey;
/**
* 获取商户的私钥
*
* @return
*/
public PrivateKey getPrivateKey(String privateKey) {
return PemUtil.loadPrivateKey(privateKey);
}
/**
* 获取签名验证器
*
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier() {
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKeyObj = getPrivateKey(privateKey);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKeyObj);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取http请求对象
*
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) throws FileNotFoundException {
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKeyObj = getPrivateKey(privateKey);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKeyObj)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
public String getPaySign(String timestamp, String nonceStr, String prePayIdStr) {
String paySign = "";
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKey));
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prePayIdStr + "\n";
sign.update(message.getBytes(StandardCharsets.UTF_8));
paySign = Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return paySign;
}
public static void main(String[] args) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
WxPayConfig wxPayConfig = new WxPayConfig();
long timestamp = 1414561699L;
String nonceStr = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS";
String packageVal = "prepay_id=wx201410272009395522657a690389285100";
String appId = "wx8b888b888e888d8a";
String sign = wxPayConfig.getPaySign(String.valueOf(timestamp), nonceStr, packageVal);
System.out.println(sign);
}
}
2.7、WxNotifyType.java
/**
* 微信支付,退款回调地址
* 1.此处注意接口路径地址
* 2.在权限控制拦截器中放开接口限制
*/
@AllArgsConstructor
@Getter
public enum WxNotifyType {
/**
* 支付通知
*/
JSAPI_NOTIFY("/app/order/wx/pay/callback/pay/notify"),
/**
* 退款结果通知
*/
REFUND_NOTIFY("/app/order/wx/pay/callback/refunds/notify");
/**
* 类型
*/
private final String type;
}
三、参考资料
产品介绍 - JSAPI支付 | 微信支付商户文档中心