一、废话不多说,直接上教程:
写代码之前首先要明白微信支付的支付流程。
二、支付流程:
小程序调用后端预支付接口 => 预支付接口调用成功返回给小程序支付凭证id => 小程序拿到支付凭证调用微信后台支付接口 => 小程序支付成功后,微信后台执行支付回调将支付订单信息返回(预支付调用微信后台是需要传入支付成功后自己本地的回调接口地址,可以在回调接口里设置自己的业务逻辑)
三、退款流程:
服务端自定义设置商户退款单号进行退款申请=>服务端通过微信退款sdk对微信服务器发起验签请求=>退款成功后,微信后台执行退款回调将退款信息返回(退款地址回调微信后台是需要传入退款申请成功后自己本地的回调接口地址,可以在回调接口里设置自己的业务逻辑)
四、官方文档:
微信小程序支付官方文档
如果我们按照微信支付官方文档去一步步写会很慢很繁琐,我教大家使用微信支付官网推荐java-sdk工具包去实现。
微信支付java-sdk工具包
五、开始手撸代码:
1.导入依赖
Gradle:
implementation 'com.github.wechatpay-apiv3:wechatpay-java:0.2.11'
Maven:
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.11</version>
</dependency>
2.配置微信支付前必要的密钥和商户信息
资源文件配置:
# 微信小程序支付配置信息
wx:
# 微信小程序appid
app-id: wx************
# 商户号
mch-id: ************
# 证书序列号
mch-serial-no: ************
# 小程序密钥
app-secret: ************
# apiV3密钥
api-key: ************
# 回调接口地址
notify-url: https://************/wxpay/payNotify
# 退款回调接口地址
refund-notify-url: https://************/wxpay/refund/payNotify
# 证书地址
key-path: ************/src/main/resources/cert/apiclient_key.pem
cert-path: ************/src/main/resources/cert/apiclient_key.pem
cert-p12-path: ************/src/main/resources/cert/apiclient_key.pem
获取配置信息:
@Component
@ConfigurationProperties(prefix = "wx")
@Data
@ToString
public class WxPayV3Bean {
private String appId;
private String mchId;
private String mchSerialNo;
private String appSecret;
private String apiKey;
private String notifyUrl;
private String refundNotifyUrl;
private String keyPath;
}
3.创建接口请求类和返回类
查看官方文档,查看调用微信支付时需要传哪些必传的参数。
预支付请求类:
@Data
@Accessors(chain = true)
public class WXPayOrderReqVO {
@Schema(description = "订单支付类型(1.申请保证金 2. 客户保证金)",required = true)
@NotBlank(message = "订单支付类型不能为空!")
private String orderType;//附加数据,回调时可根据这个数据辨别订单类型或其他
@Schema(description = "总金额(单位:分)",required = true)
@NotNull(message = "总金额不能为空!")
private Integer totalPrice;
@Schema(description = "商品名称",required = true)
@NotBlank(message = "商品名称不能为空!")
private String goodsName;
@Schema(description = "openid",required = true)
@NotBlank(message = "openId不能为空!")
private String openId;
@Schema(description = "商品订单号",required = true)
@NotNull(message = "商品订单号不能为空!")
private Long orderSn;
}
预退款请求类:
@Data
@Accessors(chain = true)
public class WXRefundOrderReqVO {
/**
* 微信支付订单号,微信支付订单号和商家订单号二选一
*/
@Schema(description = "微信支付订单号")
private String transactionId;
/**
* 商家订单号,对应 out_trade_no,
*/
@Schema(description = "商家订单号")
private String orderId;
/**
* 商户退款单号,对应out_refund_no
*/
@Schema(description = "商户退款单号")
private String outRefundNo;
/**
* 退款原因,选填
*/
@Schema(description = "退款原因")
private String reason;
/**
* 回调地址
*/
@Schema(description = "回调地址")
private String notify;
/**
* 退款金额
*/
@Schema(description = "退款金额")
private Integer refundMoney;
/**
* 原订单金额,必填
*/
@Schema(description = "原订单金额")
private Integer totalMoney;
}
由于使用的这个sdk工具类里面有返回类,可以不用自己创建返回类。
这里我自己创了个返回类。
预支付返回类:
@Data
@Accessors(chain = true)
public class WxPayRespVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 预支付交易会话标识小程序下单接口返回的prepay_id参数值
*/
@Schema(description = "预支付交易会话标识小程序下单接口返回的prepay_id参数值")
private String prepayId;
/**
* 随机字符串
*/
@Schema(description = "随机字符串")
private String nonceStr;
/**
* 时间戳
*/
@Schema(description = "时间戳")
private Long timeStamp;
/**
* 签名
*/
@Schema(description = "签名")
private String paySign;
}
预退款返回类(可不用):
@Data
@Accessors(chain = true)
public class WXRefundOrderRespVO {
/**
* 微信支付退款号
*/
@Schema(description = "微信支付退款号")
private String refundId;
/**
* 商户退款单号
*/
@Schema(description = "商户退款单号")
private String outRefundNo;
/**
* 微信支付订单号
*/
@Schema(description = "微信支付订单号")
private String transactionId;
/**
* 商户订单号
*/
@Schema(description = "商户订单号")
private String outTradeNo;
/**
* 退款渠道
*/
@Schema(description = "退款渠道")
private String channel;
/**
* 退款入账账户
*/
@Schema(description = "退款入账账户")
private String userReceivedAccount;
/**
* 退款成功时间
*/
@Schema(description = "退款成功时间")
private String successTime;
/**
* 退款创建时间
*/
@Schema(description = "退款创建时间")
private String createTime;
/**
* 退款状态
*/
@Schema(description = "退款状态")
private String status;
/**
* 资金账户
*/
@Schema(description = "资金账户")
private String fundsAccount;
}
工具类:
/**
* @project 小程序支付
* @Classname WXPayUtil
* @Description TODO
* @Author: whl
* @CreateTime: 2023-10-16 16:00
*/
public class WXPayUtil {
//随机字符串设置
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
public static String getSign(String signatureStr,String privateKey) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException, URISyntaxException {
//replace 根据实际情况,不一定都需要
String replace = privateKey.replace("\\n", "\n");
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKeyFromPath(replace);
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(merchantPrivateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
}
4.开始写支付接口
Controller层:
预支付:
/**
* 微信预支付测试接口
* @return R
*/
@Operation(summary = "微信预支付测试接口" , description = "微信预支付测试接口" )
@SysLog("微信预支付测试接口" )
@PostMapping("/createOrder")
public R<WxPayRespVO> createOrder(@RequestBody @Validated WXPayOrderReqVO req) throws Exception {
return R.ok(wxPayService.createOrder(req));
}
支付回调:
/**
* 微信支付回调
* @return R
*/
@Operation(summary = "微信支付回调" , description = "微信支付回调" )
@SysLog("微信支付回调" )
@PostMapping("/payNotify")
public R payNotify(HttpServletRequest request) throws Exception {
wxPayService.payNotify(request);//注意:回调接口需要暴露到公网上,且要放开token验证
return R.ok();
}
Service、ServiceImpl层:
欲支付:
Service
/**
* 功能描述:
* 〈微信预支付〉
* @Param: [req]
* @Return: [WxPayRespVO]
* @Author: whl
* @Date: 2023/10/10 13:46
*/
WxPayRespVO createOrder(WXPayOrderReqVO req) throws Exception;
ServiceImpl
@Override
public WxPayRespVO createOrder(WXPayOrderReqVO req) throws Exception {
try {
// 使用自动更新平台证书的RSA配置
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 构建service
JsapiService service = new JsapiService.Builder().config(config).build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(req.getTotalPrice());
request.setAmount(amount);
request.setAppid(wxPayV3Bean.getAppId());
request.setMchid(wxPayV3Bean.getMchId());
request.setDescription(req.getGoodsName());
request.setNotifyUrl(wxPayV3Bean.getNotifyUrl());
request.setOutTradeNo(req.getOrderSn().toString());
request.setAttach(req.getOrderType());
Payer payer = new Payer();
payer.setOpenid(req.getOpenId());
request.setPayer(payer);
// 调用下单方法,得到应答
// 调用微信sdk接口,生成预支付交易单
PrepayResponse response = service.prepay(request);
WxPayRespVO vo = new WxPayRespVO();
Long timeStamp = System.currentTimeMillis() / 1000;
vo.setTimeStamp(timeStamp);
String substring = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
vo.setNonceStr(substring);
String signatureStr = Stream.of(wxPayV3Bean.getAppId(), String.valueOf(timeStamp), substring, "prepay_id=" + response.getPrepayId())
.collect(Collectors.joining("\n", "", "\n"));
String sign = WXPayUtil.getSign(signatureStr, wxPayV3Bean.getKeyPath());
vo.setPaySign(sign);
vo.setPrepayId("prepay_id=" + response.getPrepayId());
return vo;
}catch (ServiceException e){
JSONObject parse = JSONObject.parseObject(e.getResponseBody());
throw new ResultException(ResultEnum.ERROR,parse.getString("message"));
}catch (Exception e){
throw new ResultException(ResultEnum.ERROR,e.toString());
}
}
这个签名是工具类生成的,之前找v3版本的生成签名找半天才找到,这个特别难找。
注意:这里有个特别大的一个坑,大家一定要注意。JSONObject千万要引fastjson2的包,之前没注意引用的jsonfast的包测试环境一直报错,特别坑。找了半天才找到,直接当场破防。
支付回调:
Service
/**
* 功能描述:
* 〈微信预支付〉
* @Param: [req]
* @Return: [WxPayRespVO]
* @Author: whl
* @Date: 2023/10/10 13:46
*/
WxPayRespVO createOrder(WXPayOrderReqVO req) throws Exception;
ServiceImpl
@Override
public void payNotify(HttpServletRequest request) throws Exception {
try {
//读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String signType = request.getHeader("Wechatpay-Signature-Type");
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
// 没有的话,则构造一个
log.error(JSON.toJSONString(wxPayV3Bean));
NotificationConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RequestParam requestParam=new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(s1)
.build();
Transaction parse = parser.parse(requestParam, Transaction.class);
System.out.println("parse = " + parse);
//parse.getTradeState().equals("SUCCESS");说明支付成功
if (parse.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)){
//你的业务代码
}
} catch (Exception e) {
throw new ResultException(ResultEnum.ERROR,e.toString());
}
}
5.开始写退款接口
Controller层:
退款接口:
/**
* 退款
* @param id id(这里可以是你业务的订单id或者就商户下的退款id,具体看你的业务)
* @return R
*/
@Operation(summary = "交易订单退款申请" , description = "交易订单退款申请" )
@SysLog("交易订单退款申请" )
@GetMapping("/reviewRefund/{id}" )
public R reviewRefundById(@PathVariable("id" ) String id) throws Exception {
return R.ok(wxPayService.refundOrder(id));
}
退款回调接口:
/**
* 微信退款回调
* @return R
*/
@Operation(summary = "微信退款回调" , description = "微信退款回调" )
@SysLog("微信退款回调" )
@PostMapping("/refund/payNotify")
public R refundPayNotify(HttpServletRequest request) throws Exception {
wxPayService.refundNotify(request);//注意:回调接口需要暴露到公网上,且要放开token验证
return R.ok();
}
退款涉及我的部分业务代码,这里就不一一展示了,具体传参可以看我写的退款请求类
Service、ServiceImpl层:
退款申请:
Service
/**
* 功能描述:
* 〈微信退款〉
* @Param: [req]
* @Return: [void]
* @Author: whl
* @Date: 2023/10/12 15:46
*/
WXRefundOrderRespVO refund(WXRefundOrderReqVO refundOrder) throws Exception;
ServiceImpl
/**
* 功能描述:
* 〈微信退款〉
* @Param: [req]
* @Return: [void]
* @Author: whl
* @Date: 2023/10/12 15:46
*/
@Override
public WXRefundOrderRespVO refund(WXRefundOrderReqVO refundOrder) throws Exception {
try {
// 使用自动更新平台证书的RSA配置
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
//使用 SDK 不需要计算请求签名和验证应答签名
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 构建退款service
RefundService service = new RefundService.Builder().config(config).build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
//构建退款请求
CreateRequest request = new CreateRequest();
//构建订单金额信息
AmountReq amountReq = new AmountReq();
//退款金额
amountReq.setRefund(Long.valueOf(refundOrder.getRefundMoney()));
//原订单金额
amountReq.setTotal(Long.valueOf(refundOrder.getTotalMoney()));
//货币类型(默认人民币)
amountReq.setCurrency("CNY");
request.setAmount(amountReq);
//商户退款单号
request.setOutRefundNo(String.valueOf(refundOrder.getOutRefundNo()));
//退款通知回调地址
request.setNotifyUrl(wxPayV3Bean.getRefundNotifyUrl());
// 调用退款方法,得到应答
// 调用微信sdk接口
Refund refund = service.create(request);
//接收退款返回参数
Refund refundResponse = new Refund();
refundResponse.setStatus(refund.getStatus());
if (refundResponse.getStatus().equals(Status.SUCCESS)){
//说明退款成功,开始接下来的业务操作
WXRefundOrderRespVO refundOrderRespVO = new WXRefundOrderRespVO();
//你的业务代码
return refundOrderRespVO;
}
}catch (ServiceException e){
JSONObject parse = JSONObject.parseObject(e.getResponseBody());
throw new ResultException(ResultEnum.ERROR,parse.getString("message"));
}catch (Exception e){
throw new ResultException(ResultEnum.ERROR,e.toString());
}
return null;
}
退款回调:
Service
/**
* 功能描述:
* 〈微信退款回调〉
* @Param: [request, response]
* @Return: [void]
* @Author: whl
* @Date: 2023/10/13 10:46
*/
void refundNotify(HttpServletRequest request) throws Exception ;
ServiceImpl
/**
* 功能描述:
* 〈微信退款回调〉
* @Param: [request, response]
* @Return: [void]
* @Author: whl
* @Date: 2023/10/13 10:46
*/
@Override
public void refundNotify(HttpServletRequest request) throws Exception {
try {
//读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String signType = request.getHeader("Wechatpay-Signature-Type");
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
// 没有的话,则构造一个
log.error(JSON.toJSONString(wxPayV3Bean));
NotificationConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RequestParam requestParam=new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(s1)
.build();
RefundNotification parse = parser.parse(requestParam, RefundNotification.class);
System.out.println("parse = " + parse);
//parse.getRefundStatus().equals("SUCCESS");说明退款成功
if (parse.getRefundStatus().equals(Transaction.TradeStateEnum.SUCCESS)){
//你的业务代码
}
} catch (Exception e) {
throw new ResultException(ResultEnum.ERROR,e.toString());
}
}
自此整合微信支付、退款流程就全部完毕,又由本次是基于微信sdk的开发,所有省略了大量重复接口请求的编写和签名的验证等,代码相对精简,便于开发学习