前言
最近公司在做一款自用的点餐小程序, 对接微信支付,所以我第二次进行了对接微信支付的业务代码开发。以小程序支付和退款为例,整理了服务器和微信交互的代码,以备将来使用。
依赖配置
pom.xml配置微信支付SDK
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
application.yml配置信息
wechat:
# 商户号
mch-id: 1666840757
# 商户API证书序列号
mch-serial-no: 6396D77164223E3CD722C28CD2DD2DF28485F0180021
# 商户私钥文件 注意:该文件放在项目根目录下
private-key-path: apiclient_key.pem
# APIv3密钥
api-v3-key: Admin123Admin123Admin123Admin12301
# APPID
appid: wx8e5c28426745f9fb7855
# 支付结果微信回调
notify-url: https://myweb-url/notify/notifyPayResult
# 退款结果微信回调
refund-notify-Url: https://myweb-url/notify/refundNotify
业务步骤
- 前端调用“预支付”接口,获取唤醒微信支付页面需要的参数。
- 后端系统进行业务处理,并与微信端交互,获取prepayId参数,并组装数据返给前端。
- 通过解析微信异步返回的支付结果,对业务进行处理。
业务代码
微信配置信息,用来读取yml文件中的配置。
/**
* @author xsong.Y
* @description 微信支付配置信息
*/
@Configuration
@ConfigurationProperties(prefix = "wechat")
@Data
public class WeChatConfig {
/** 商户号 */
private String mchId;
/** 商户API证书序列号 */
private String mchSerialNo;
/** 商户私钥文件路径 */
private String privateKeyPath;
/** APIv3密钥 */
private String apiV3Key;
/** APPID */
private String appid;
/** 支付通知回调 */
private String notifyUrl;
/** 退款通知回调 */
private String refundNotifyUrl;
}
支付服务代码,主要实现预支付和申请退款
package com.yxsong.wechatpay.service;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.util.PemUtil;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.yxsong.common.tools.DateTimeUtil;
import com.yxsong.wechatpay.config.WeChatConfig;
import com.yxsong.wechatpay.dto.PayOrderDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.FileCopyUtils;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author xsong.Y
* @description 支付
*/
@Service
@Slf4j
public class PayService {
@Resource
private WeChatConfig weChatConfig;
/**
* 小程序支付。首先需要进行预支付获取"预支付交易会话标识",前端获取到该数据后,唤起微信支付页面。
* 预支付
* @param order 请求参数,可以根据具体业务来设计
* @return Map
*/
public Map<String, Object> prepaid(PayOrderDto order) throws Exception {
// 业务-交易订单信息校验,如重复支付、订单是否存在等。
//.......
// 预支付业务
// 时间戳
long timestamp = DateTimeUtil.getTimestamp();
// 随机字符串
String nonceStr = UUID.randomUUID().toString().replaceAll("-", "").substring(0,32);
String signType = "RSA";
// 创建SDK中的JsapiService对象
JsapiService jsapiService = new JsapiService.Builder().config(this.createConfig()).build();
// 构建业务信息
PrepayRequest prepayRequest = this.buildPrepayRequest("", 100, "", "");
// 与微信端交互
PrepayResponse res = jsapiService.prepay(prepayRequest);
String prepayId = "";
// Assert.notNull(res, "预支付交互响应数据为空!");
Optional<PrepayResponse> prepayResponseOptional = Optional.ofNullable(res);
if (!prepayResponseOptional.isPresent()) {
// 失败业务处理
} else {
prepayId = res.getPrepayId();
}
// 返回前端数据
Map<String, Object> result = new HashMap<>();
result.put("appId", this.weChatConfig.getAppid());
result.put("timeStamp",timestamp);
result.put("nonceStr", nonceStr);
result.put("signType", signType);
result.put("package", "prepay_id="+prepayId);
String signatureStr = Stream.of(this.weChatConfig.getAppid(), String.valueOf(timestamp), nonceStr, "prepay_id=" + prepayId)
.collect(Collectors.joining("\n", "", "\n"));
//
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKeyFromString("");
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(merchantPrivateKey);
signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));
String sign = Base64Utils.encodeToString(signature.sign());
result.put("sign", sign);
return result;
}
/**
* 退款申请
* @param order 可以根据具体业务来设计
* @return
* @throws Exception
*/
public Map<String, Object> refundOrder(PayOrderDto order) throws Exception {
Map<String, Object> result = new HashMap<>();
// 业务处理 ...
// 退款申请对接微信
CreateRequest createRequest = this.buildCreateRequest("", "", "", "", 0);
RefundService refundService = new RefundService.Builder().config(this.createConfig()).build();
// 数据交互
Refund refund = refundService.create(createRequest);
Optional<Refund> refundOptional = Optional.ofNullable(refund);
if (!refundOptional.isPresent()) {
// 通信失败 业务处理 .......
}
if("SUCCESS".equals(refund.getStatus().name())) {
// 成功
}
return new HashMap<>();
}
private Config createConfig() throws Exception {
// 生成privateKey
ClassPathResource resource = new ClassPathResource(this.weChatConfig.getPrivateKeyPath());
byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream());
String privateKey = new String(byteArray, StandardCharsets.UTF_8);
// 构建Config
return new RSAAutoCertificateConfig.Builder()
.merchantId(this.weChatConfig.getMchId())
.privateKey(privateKey)
.merchantSerialNumber(this.weChatConfig.getMchSerialNo())
.apiV3Key(this.weChatConfig.getApiV3Key())
.build();
}
/**
* 创建预支付请求对象
* @param tradeOrderId 商户订单号,唯一
* @param total 总金额,int类型,单位分
* @param openId 用户openId
* @param payerClientIp 用户IP
*/
private PrepayRequest buildPrepayRequest(String tradeOrderId, int total, String openId, String payerClientIp) {
PrepayRequest request = new PrepayRequest();
//
request.setAppid(this.weChatConfig.getAppid());
request.setMchid(this.weChatConfig.getMchId());
request.setDescription("预支付获取-预支付交易会话标识");
// 商户订单号
request.setOutTradeNo(tradeOrderId);
// 交易结束时间
request.setTimeExpire(DateTimeUtil.getZoneTime());
// 附加数据
request.setAttach("");
// 支付结果回调URL
request.setNotifyUrl(this.weChatConfig.getNotifyUrl());
// 订单优惠标记
request.setGoodsTag("");
// 发票标识,需要商户后台申请开启
request.setSupportFapiao(true);
// 订单金额
Amount amount = new Amount();
amount.setTotal(total);
amount.setCurrency("CNY");
request.setAmount(amount);
// 支付人信息
Payer payer = new Payer();
payer.setOpenid(openId);
request.setPayer(payer);
// 优惠功能
GoodsDetail temp = new GoodsDetail();
temp.setMerchantGoodsId("1110"); // 商户侧商品编码
temp.setQuantity(1); // 数量
temp.setUnitPrice(total); // 单价
List<GoodsDetail> goodsDetails = new ArrayList<>();
goodsDetails.add(temp);
Detail detail = new Detail();
detail.setGoodsDetail(goodsDetails);
request.setDetail(detail);
// 场景信息
SceneInfo sceneInfo = new SceneInfo();
sceneInfo.setPayerClientIp("payerClientIp");
request.setSceneInfo(sceneInfo);
// 结算信息
SettleInfo settleInfo = new SettleInfo();
settleInfo.setProfitSharing(false);
request.setSettleInfo(settleInfo);
return request;
}
/**
* 创建申请退款对象
* @param transactionId 原支付交易对应的微信订单号
* @param outTradeNo 原支付交易对应的商户订单号
* @param outRefundNo 商户系统内部的退款单号
* @param reason 退款原因
* @param total 退款金额,单位为分,只能为整数
*/
private CreateRequest buildCreateRequest(String transactionId, String outTradeNo, String outRefundNo,
String reason, long total) {
// 组装CreateRequest 退款申请类
CreateRequest createRequest = new CreateRequest();
// 文档中未说明该字段作用
// createRequest.setSubMchid("");
// 原支付交易对应的微信订单号
createRequest.setTransactionId(transactionId);
// 原支付交易对应的商户订单号
createRequest.setOutTradeNo(outTradeNo);
// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
createRequest.setOutRefundNo(outRefundNo);
// 退款原因
createRequest.setReason(reason);
// 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
// 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。
createRequest.setNotifyUrl(this.weChatConfig.getRefundNotifyUrl());
AmountReq amountReq = new AmountReq();
// 退款金额,单位为分,只能为整数
amountReq.setRefund(total);
// 原支付交易的订单总金额,单位为分,只能为整数。
amountReq.setTotal(total);
// 退款币种,目前只支持人民币:CNY。
amountReq.setCurrency("CNY");
createRequest.setAmount(amountReq);
return createRequest;
}
}
结果通知
package com.yxsong.wechatpay.service;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.yxsong.wechatpay.config.WeChatConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import javax.annotation.Resource;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* @author xsong.Y
* @description 结果通知
*/
@Service
@Slf4j
public class NotifyService {
@Resource
private WeChatConfig weChatConfig;
/**
* 支付结果通知
* @param request
* @return
* @throws Exception
*/
public String payNotify(HttpServletRequest request) throws Exception {
RequestParam requestParam = this.create(request);
// 构造一个RSAAutoCertificateConfig
NotificationConfig config = (NotificationConfig) this.createConfig();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
Transaction transaction = parser.parse(requestParam, Transaction.class);
Optional<Transaction> optionalTransaction = Optional.ofNullable(transaction);
if (!optionalTransaction.isPresent()) {
// 解析失败,业务处理 ...
}
// 业务处理....
return "";
}
public String refundNotify(HttpServletRequest request) throws Exception {
RequestParam requestParam = this.create(request);
// 构造一个RSAAutoCertificateConfig
NotificationConfig config = (NotificationConfig) this.createConfig();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RefundNotification notification = parser.parse(requestParam, RefundNotification.class);
Optional<RefundNotification> refundOptional = Optional.ofNullable(notification);
if (!refundOptional.isPresent()) {
// 解析失败,业务处理....
}
// 业务处理.....
return "";
}
private Config createConfig() throws Exception {
// 生成privateKey
ClassPathResource resource = new ClassPathResource(this.weChatConfig.getPrivateKeyPath());
byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream());
String privateKey = new String(byteArray, StandardCharsets.UTF_8);
// 构建Config
return new RSAAutoCertificateConfig.Builder()
.merchantId(this.weChatConfig.getMchId())
.privateKey(privateKey)
.merchantSerialNumber(this.weChatConfig.getMchSerialNo())
.apiV3Key(this.weChatConfig.getApiV3Key())
.build();
}
private RequestParam create(HttpServletRequest request) throws Exception{
// 请求头信息
String wechatSignature = request.getHeader("Wechatpay-Signature");
// 微信支付平台证书的序列号,验签必须使用序列号对应的微信支付平台证书。
String wechatPaySerial= request.getHeader("Wechatpay-Serial");
// 签名中的随机数
String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
// 签名中的时间戳。
String wechatTimestamp = request.getHeader("Wechatpay-Timestamp");
// 签名类型
String wechatpaySignatureType = request.getHeader("Wechatpay-Signature-Type");
// 请求体信息
ServletInputStream inputStream = request.getInputStream();
StringBuilder stringBuffer = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String body = stringBuffer.toString();
// 构造 RequestParam
return new RequestParam.Builder()
.serialNumber(wechatPaySerial)
.nonce(wechatpayNonce)
.signature(wechatSignature)
.timestamp(wechatTimestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(wechatpaySignatureType)
.body(body)
.build();
}
}