场景介绍
开发指引
接入准备
下面是我们必须带入的几个值,需要自己去 微信支付官网 获取
所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--解密依赖-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
配置文件
MyWXPayConfig.java
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConfig;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
@Configuration
public class MyWXPayConfig implements WXPayConfig {
/**
* appid
* @return
*/
public String getAppID() {
return "小程序appid";
}
/**
* 商户号
* @return
*/
public String getMchID() {
return "商户号";
}
/**
* API密钥
*/
public String getKey() {
return "v2API密钥";
}
/**
* 证书
* @return
*/
public InputStream getCertStream() {
try {
return new FileInputStream("apiclient_cert.p12");
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 回调通知地址
* @return
*/
public String getNotifyDomain(){
return "https://bcae-119-39-4-32.jp.ngrok.io";
}
public int getHttpConnectTimeoutMs() {
return 8000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
/**
* 将配置文件及参数带入获取微信支付的接口调取类
*/
public WXPay getWxPay(){
return new WXPay(this);
}
}
工具类
** 回调通知解密工具类 AESUtil.java**
import com.github.wxpay.sdk.WXPayUtil;
import com.hai.wxpaycode.config.MyWXPayConfig;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.Security;
import java.util.Base64;
/**
* 微信支付AES加解密工具类
*
* @author yclimb
* @date 2018/6/21
*/
@Component
public class AESUtil {
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
/**
* 生成key
*/
private static SecretKeySpec KEY;
static {
MyWXPayConfig myWXPayConfig = new MyWXPayConfig();
try {
KEY = new SecretKeySpec(WXPayUtil.MD5(myWXPayConfig.getKey()).toLowerCase().getBytes(), ALGORITHM);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* AES加密
*
* @param data d
* @return str
* @throws Exception e
*/
public static String encryptData(String data) throws Exception {
// 创建密码器
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, KEY);
return base64Encode8859(new String(cipher.doFinal(data.getBytes()), "ISO-8859-1"));
}
/**
* AES解密
*
* @param base64Data 64
* @return str
* @throws Exception e
*/
public static String decryptData(String base64Data) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
cipher.init(Cipher.DECRYPT_MODE, KEY);
return new String(cipher.doFinal(base64Decode8859(base64Data).getBytes("ISO-8859-1")), "utf-8");
}
/**
* Base64解码
* @param source base64 str
* @return str
*/
public static String base64Decode8859(final String source) {
String result = "";
final Base64.Decoder decoder = Base64.getDecoder();
try {
// 此处的字符集是ISO-8859-1
result = new String(decoder.decode(source), "ISO-8859-1");
} catch (final UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
/**
* Base64加密
* @param source str
* @return base64 str
*/
public static String base64Encode8859(final String source) {
String result = "";
final Base64.Encoder encoder = Base64.getEncoder();
byte[] textByte = null;
try {
//注意此处的编码是ISO-8859-1
textByte = source.getBytes("ISO-8859-1");
result = encoder.encodeToString(textByte);
} catch (final UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
}
获取ip地址工具类 IPUtil.java
/**
* @author LiFucheng
* @version 1.0
* @description: TODO 获取ip地址工具
* @date 2022/5/2 15:19
*/
public class IPUtil {
//获取真实IP
public static String getRemoteHost(javax.servlet.http.HttpServletRequest request){
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1")?"127.0.0.1":ip;
}
}
**订单号工具类 OrderNoUtils.java **
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtils {
/**
* 获取订单编号
* @return
*/
public static String getOrderNo() {
return "ORDER_" + getNo();
}
/**
* 获取退款单编号
* @return
*/
public static String getRefundNo() {
return "REFUND_" + getNo();
}
/**
* 获取编号
* @return
*/
public static String getNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}
做完这些准备工作我们就可以开始正式的去调取支付接口了
调起支付
开始支付
/**
* 付款码支付
* @param request
*/
@GetMapping("/weChatPayMicroPay")
public void weChatPayMicroPay(HttpServletRequest request) throws Exception {
String auth_code = request.getParameter("auth_code");
//组装参数
Map<String,String> requestData = new HashMap<String, String>();
//商品描述
requestData.put("body","测试商品");
// 支付的商户订单号
requestData.put("out_trade_no", OrderNoUtils.getOrderNo());
// 支付金额
requestData.put("total_fee","1");
// 终端IP
requestData.put("spbill_create_ip",IPUtil.getRemoteHost(request));
// 付款码
requestData.put("auth_code",auth_code);
//发起支付
Map<String, String> mapBack = myWXPayConfig.getWxPay().microPay(requestData);
if("SUCCESS".equals(mapBack.get("return_code"))){
//支付成功,处理业务
System.out.println("支付成功");
}else{
System.out.println("通信异常信息====="+mapBack.get("return_msg"));
}
}
撤销订单 (需要证书—证书使用)
/**
* 撤销订单
* @param outTradeNo 商户订单号
*/
@GetMapping("/weChatPayReverse")
public void weChatPayReverse(String outTradeNo) throws Exception {
//获取配置
WXPay wxPay = new WXPay(myWXPayConfig);
Map<String,String> requestData = new HashMap<String, String>();
//传入商户订单号 这里还可以传入微信的订单号, 具体可以去看官方的api文档
requestData.put("out_trade_no",outTradeNo);
myWXPayConfig.getWxPay().reverse(requestData);
}
查询订单
/**
* 查询订单
* @param orderNo 订单号
* @throws Exception
*/
@GetMapping("/weChatPayOrderQuery")
public void weChatPayOrderQuery(@RequestParam(required = true,value = "orderNo")String orderNo) throws Exception {
Map<String,String> requestData = new HashMap<String, String>();
requestData.put("out_trade_no", orderNo);
Map<String, String> mapBack = null;
try {
mapBack = myWXPayConfig.getWxPay().orderQuery(requestData);
} catch (Exception e) {
e.printStackTrace();
}
}
申请退款 ( 需要证书—证书使用)
/**
* 申请退款
* @param outTradeNo 商户订单号
* @throws Exception
*/
@GetMapping("/weChatPayRefund")
public void weChatPayRefund(String outTradeNo) throws Exception {
Map<String,String> requestData = new HashMap<String, String>();
//商户订单号 同样也可以使用微信订单号
requestData.put("out_trade_no",outTradeNo);
// 退款商户号
requestData.put("out_refund_no", OrderNoUtils.getRefundNo());
// 订单总金额
requestData.put("total_fee","1");
// 退款金额
requestData.put("refund_fee","1");
// 退款通知回调地址
requestData.put("notify_url",myWXPayConfig.getNotifyDomain().concat("/wxPay/refunds/code"));
log.info("退款参数--->{}",requestData);
Map<String, String> refund = myWXPayConfig.getWxPay().refund(requestData);
System.out.println(refund);
}
查询退款
/**
* 查询退款
* @param outRefundNo 商户订单号
*/
@GetMapping("/weChatPayRefundQuery")
public void weChatPayRefundQuery(String outRefundNo){
Map<String,String> requestData = new HashMap<String, String>();
// 商户退款号 可以使用其他三个(商户订单号,微信订单号,微信退款订单号)
requestData.put("out_refund_no",outRefundNo);
try {
Map<String, String> map = myWXPayConfig.getWxPay().refundQuery(requestData);
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
}
}
退款回调通知
/**
* 退款回调通知
* @param request
* @param response
*/
@PostMapping("/refunds/code")
public void refundNotify(HttpServletRequest request, HttpServletResponse response){
log.info("进入退款回调通知");
String resXml = "";
InputStream inStream;
try {
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
// 获取微信调用我们notify_url的返回信息
String result = new String(outSteam.toByteArray(), "utf-8");
// 关闭流
outSteam.close();
inStream.close();
// xml转换为map
Map<String, String> map = WXPayUtil.xmlToMap(result);
// 加密信息:加密信息请用商户秘钥进行解密,详见解密方式
String req_info = map.get("req_info");
/**
* 解密方式
* 解密步骤如下:
* (1)对加密串A做base64解码,得到加密串B
* (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
* (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
*/
String resultStr = AESUtil.decryptData(req_info);
// WXPayUtil.getLogger().info("refund:解密后的字符串:" + resultStr);
Map<String, String> aesMap = WXPayUtil.xmlToMap(resultStr);
log.info("结果---->{}",aesMap);
/** 以下为返回的加密字段: */
// 商户退款单号 是 String(64) 1.21775E+27 商户退款单号
String out_refund_no = aesMap.get("out_refund_no");
// 退款状态 是 String(16) SUCCESS SUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭
String refund_status = aesMap.get("refund_status");
// 商户订单号 是 String(32) 1.21775E+27 商户系统内部的订单号
String out_trade_no = aesMap.get("out_trade_no");
// 退款是否成功
if (!WXPayConstants.SUCCESS.equals(refund_status)) {
log.info("退款状态---{}",refund_status);
} else {
// 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
log.info("退款状态---{}",refund_status);
}
} catch (Exception e) {
log.error("refund:微信退款回调发布异常:", e);
} finally {
try {
// 处理业务完毕
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (IOException e) {
log.error("refund:微信退款回调发布异常:out:", e);
}
}
}
到这里我们从支付到退款一条龙服务就做完了
结尾
这里我们唯一要注意的是我们有些码友不知道的一点, 就是在证书这里
如图中一样,证书就是我左下角圈出来的文件,它一定与src目录同级,这边格式也一定是.p12格式,作者本人在这踩了一个大坑,所以在此告诉各位码友防止大家又踩同样的坑。
如果大家觉得有所帮助就点赞收藏吧,防止找不到,后续将持续更新其他技术点