之前写了支付接口,现在补充一下 微信退款接口
import com.alibaba.fastjson.JSON;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.xhsoft.device.entity.DeviceOrdersEntity;
import com.xhsoft.device.feign.IDeviceOrdersClient;
import com.xhsoft.device.vo.DeviceOrdersVO;
import com.xhsoft.mp.config.PayConfig;
import com.xhsoft.mp.entity.OrderInfo;
import com.xhsoft.mp.service.OrderInfoService;
import com.xhsoft.mp.vo.OrderInfoVO;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import org.springblade.core.social.props.SocialProperties;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springblade.core.tool.api.R;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 小程序支付
* @Author Lenovo
* @Date 2022/4/6 11:21
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/v1/order")
@Api(value = "首页显示", tags = "查询结果接口")
public class PayController {
private final OrderInfoService orderInfoService;
private final SocialProperties socialProperties;
@Resource
private PayConfig payConfig;
@Resource
private IDeviceOrdersClient deviceOrdersClient;
/**
* 调用统一下单接口,并组装生成支付所需参数对象.
*
* @param orderInfoVO 统一下单请求参数
*/
@PostMapping("/unifiedOrder")
public R unifiedOrder(HttpServletRequest request, @RequestBody DeviceOrdersEntity orderInfoVO) {
return orderInfoService.unifiedOrder(request, orderInfoVO);
}
/**
* 支付回调(微信)
*
* @param xmlData 微信XML数据
* @param orderId 订单id
* @return 返回支付结果
*/
@PostMapping("/notify-order-wx/{orderId}")
public String notifyOrderWx(HttpServletRequest request, HttpServletResponse response,
@RequestBody String xmlData,
@PathVariable("orderId") Long orderId
) throws IOException {
AuthConfig config = socialProperties.getOauth().get(AuthDefaultSource.WECHAT_MP);
log.info("支付回调(微信):" + xmlData);
if( orderId == null ){
System.out.println("验签失败");
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
}
String resXml = "";
try {
//通过商家id查询支付配置
// PayConfig payConfig = payConfigService.selectPayConfigByTenantId(tenantId,"1");
Map<String, Object> ResultMap = orderInfoService.WeChatPayCallback(xmlData, payConfig.getMchKey());
Map<String, String> WxResultMapData = new HashMap<>();
if (ResultMap.get("Verify").equals("YES")) {
//验签成功
System.out.println("验签成功");
WxResultMapData = (Map<String, String>) ResultMap.get("data");
System.out.println("WxResultMapData:" + JSON.toJSONString(WxResultMapData));
log.info("收到微信回调:{}", JSON.toJSONString(WxResultMapData));
log.info("查询订单");
log.info("查询订单id:{}", orderId);
R<DeviceOrdersVO> deviceOrdersVOR = deviceOrdersClient.detailById(orderId);
if (!deviceOrdersVOR.isSuccess() || deviceOrdersVOR.getData() == null) {
log.info("查询订单失败");
log.info("无法变更订单状态,通知微信验签失败");
return WxPayNotifyResponse.fail("失败");
}
System.out.println("通知微信验签成功");
//自信业务逻辑处理
log.info("自己的业务处理");
DeviceOrdersVO ordersVO = deviceOrdersVOR.getData();
// 验证金额
// BigDecimal payAmount = new BigDecimal(WxResultMapData.get("total_fee"));
// BigDecimal amount = ordersVO.getAmount();
// if(payAmount == null || amount == null || amount.compareTo(payAmount) != 0) {
// log.error("微信支付回调时,支付金额{}与待支付金额{}不符",payAmount,amount);
// return WxPayNotifyResponse.fail("支付金额不符");
// }
ordersVO.setPayStatus(1);// 支付状态 已支付
ordersVO.setPayTime(new Date());// 支付时间
// 输出 更新的订单信息
log.info("回调方法,要更新的订单信息:{}", JSON.toJSONString(ordersVO));
R updateR = deviceOrdersClient.submit(ordersVO);
if (!updateR.isSuccess()) {
log.info("无法变更订单状态,通知微信验签失败");
return WxPayNotifyResponse.fail("失败");
}else {
log.info("变更订单状态成功");
}
// orderInfoService.notifyOrder(orderInfo,tenantId,orderId,typeId,WxResultMapData);
resXml = String.valueOf(ResultMap.get("returnWeChat"));
} else if (ResultMap.get("Verify").equals("NO")) {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[" + WxResultMapData.get("err_code_des") + "]]></return_msg>" + "</xml> ";
}
}catch (Exception e) {
throw new RuntimeException(e);
}
return WxPayNotifyResponse.success("成功");
}
/**
* 微信退款接口
*
* @param orderId 订单id
* @param refundTotal 退款金额
* @return 退款结果
*/
@PostMapping("/refund/{orderId}/{refundTotal}")
public R refund(@PathVariable("orderId") Long orderId, @PathVariable("refundTotal") BigDecimal refundTotal) {
return orderInfoService.refund(orderId, refundTotal);
}
/**
* 退款回调(微信)
*
* @param xmlData 微信XML数据
* @return 返回退款结果
*/
@PostMapping("/notify-refund-wx")
public String notifyRefundWx(@RequestBody String xmlData) {
try {
Map<String, Object> resultMap = orderInfoService.WeChatRefundCallback(xmlData, payConfig.getMchKey());
if ("YES".equals(resultMap.get("Verify"))) {
// 验签成功,处理业务逻辑
log.info("退款回调验签成功,处理业务逻辑");
return WxPayNotifyResponse.success("成功");
} else {
log.info("退款回调验签失败");
return WxPayNotifyResponse.fail("失败");
}
} catch (Exception e) {
log.error("退款回调处理异常", e);
return WxPayNotifyResponse.fail("失败");
}
}
}
public interface OrderInfoService {
R unifiedOrder(HttpServletRequest request, DeviceOrdersEntity orderInfoVO);
Map<String, Object> WeChatPayCallback(String xmlData, String apiKey);
R refund(Long orderId, BigDecimal refundTotal);
Map<String, Object> WeChatRefundCallback(String xmlData, String mchKey);
}
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import com.xhsoft.device.entity.DeviceOrdersEntity;
import com.xhsoft.device.feign.IDeviceOrdersClient;
import com.xhsoft.device.vo.DeviceOrdersVO;
import com.xhsoft.mp.config.PayConfig;
import com.xhsoft.mp.entity.WechatUser;
import com.xhsoft.mp.service.OrderInfoService;
import com.xhsoft.mp.util.*;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import org.springblade.common.constant.HeaderConstant;
import org.springblade.core.social.props.SocialProperties;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class OrderInfoServiceImpl implements OrderInfoService {
@Resource
private SocialProperties socialProperties;
@Resource
private IDeviceOrdersClient deviceOrdersClient;
@Resource
private WechatUserServiceImpl wechatUserService;
@Autowired
private PayConfig payConfig;
@Override
public R unifiedOrder(HttpServletRequest request, DeviceOrdersEntity orderInfoVO) {
String openid = request.getHeader(HeaderConstant.TOKEN);
WechatUser user = wechatUserService.getOne(Wrappers.lambdaQuery(WechatUser.class).eq(WechatUser::getOpenid,openid));
if (user == null) {
return R.fail("用户信息不存在");
}
AuthConfig config = socialProperties.getOauth().get(AuthDefaultSource.WECHAT_MP);
String clientId = config.getClientId(); // 小程序id
//根据typeId查询支付金额 根据自己的业务逻辑自行处理
// SysFunctionType sysFunctionType = sysFunctionTypeMapper.selectFunctionTypeById(orderInfoVO.getTypeId());
//通过customId 查询用户信息 根据自己的业务逻辑自行处理
// SysCustom sysCustom = sysCustomMapper.selectSysCustomById(orderInfoVO.getCustomId());
//根据自己的业务逻辑自行处理 OrderInfo为我自己业务中的实体类
DeviceOrdersEntity orderInfo = new DeviceOrdersEntity();
// 下单商品id
orderInfo.setGoodsId(orderInfoVO.getGoodsId());
// 生成雪花id
orderInfo.setId(IdUtil.getSnowflake(0,0).nextId());
// 下单用户
orderInfo.setOpenid(openid);
// 充值给哪个用户
orderInfo.setUserId(orderInfoVO.getUserId());
orderInfo.setAmount(orderInfoVO.getAmount());// 订单金额
orderInfo.setPayStatus(0);// 0 未支付 1 已支付
//支付类型
orderInfo.setPayMethod(1);// 1 微信支付 2 支付宝支付
// 单价 折扣 购买天数 需要用户传来 后台与之匹配
orderInfo.setUnitPrice(orderInfoVO.getUnitPrice());
orderInfo.setDiscount(orderInfoVO.getDiscount());
orderInfo.setOrderDays(orderInfoVO.getOrderDays());
// orderInfo.setPaymentPrice(sysFunctionType.getPrice());
String body = "测试支付下单商品";
body = body.length() > 40 ? body.substring(0,39) : body;
//更新编号防止不同终端微信报重复订单号
orderInfo.setOrderNumber(IdUtil.getSnowflake(0,0).nextIdStr());
Map<String, String> req = new HashMap<>();
// JSAPI支付必须传openid]
req.put("openid", openid);
//公众号
req.put("appid", clientId);
// 商户号
req.put("mch_id", payConfig.getMchId());
// 32位随机字符串
req.put("nonce_str", WXPayUtil.generateNonceStr());
// 商品描述
req.put("body", body);
// 商户订单号
req.put("out_trade_no", orderInfo.getOrderNumber());
// 标价金额(分)
String total_fee = String.valueOf(MoneyUtils.yuanToFen(String.valueOf(orderInfo.getAmount())));
log.info("传入的金额=》getAmount:{}", orderInfo.getAmount());
log.info("标价金额=》total_fee:{}", total_fee);
req.put("total_fee", total_fee);
// 终端IP
req.put("spbill_create_ip", request.getRemoteAddr());
// 回调地址+携带的返回参数 domain 为配置的域名[不可为ip地址]
req.put("notify_url", payConfig.getDomain()+"/api/xxxxxx/xx/xx/notify-order-wx"+"/"+orderInfo.getId());
// 交易类型
req.put("trade_type", "JSAPI"); //JSAPI 是微信公众号支付或小程序支付的一种交易类型 //NATIVE 交易类型主要用于线下扫码支付场景
try {
// 签名
req.put("sign", WXPayUtil.generateSignature(req, payConfig.getMchKey(), WXPayConstants.SignType.MD5));
String xmlBody = WXPayUtil.generateSignedXml(req, payConfig.getMchKey());
System.err.println(String.format("微信支付预下单请求 xml 格式:\n%s", xmlBody));
String result = WxChatPayCommonUtil.httpsRequest(WeChatPayUrl.Uifiedorder, "POST", xmlBody);
log.info("保存订单信息");
// 保存订单信息
R submitR = deviceOrdersClient.submit(orderInfo);
if (submitR.isSuccess()) {
log.info("保存订单信息成功");
} else {
log.info("保存订单信息失败");
}
System.err.println(String.format("%s", result));
Map<String, String> WxResultMap = WXPayUtil.xmlToMap(result);
WxResultMap.put("orderNo",orderInfo.getOrderNumber());
if (ObjectUtil.isNotEmpty(WxResultMap.get("return_code")) && WxResultMap.get("return_code").equals("SUCCESS")) {
if (WxResultMap.get("result_code").equals("SUCCESS")) {
System.out.println("预下单成功");
System.out.println("QrCode:"+WxResultMap.get("code_url"));
// 二次签名 让前端唤起支付
SignInfo signInfo = new SignInfo();
signInfo.setAppId(config.getClientId());
long time = System.currentTimeMillis() / 1000;
signInfo.setTimeStamp(String.valueOf(time));
signInfo.setNonceStr(RandomStringGenerator.getRandomStringByLength(32));
signInfo.setRepay_id("prepay_id=" + WxResultMap.get("prepay_id"));
signInfo.setSignType("MD5");
//生成签名
String sign1 = Signature.getSign(signInfo, payConfig.getMchKey());
Map<String, String> payInfo = new HashMap<>();
payInfo.put("timeStamp", signInfo.getTimeStamp());
payInfo.put("nonceStr", signInfo.getNonceStr());
payInfo.put("package", signInfo.getRepay_id());
payInfo.put("signType", signInfo.getSignType());
payInfo.put("paySign", sign1);
// 直接返回二次签名给前端
return R.data(payInfo);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return R.fail("下单失败");
}
@Override
public Map<String, Object> WeChatPayCallback(String xmlData, String apiKey) {
Map<String, Object> ResultMap = new HashMap<String, Object>();
//解析到微信返回过来的xml数据
try {
//xml转Map
Map<String, String> WxResultMap = WXPayUtil.xmlToMap(xmlData);
//验证签名
boolean SignStatus = WXPayUtil.isSignatureValid(WxResultMap, apiKey);
if (SignStatus) {
//验证成功
//要返回给微信的xml数据
String returnWeChat = WxChatPayCommonUtil.setReturnXml("SUCCESS", "OK");
ResultMap.put("Verify", "YES");
ResultMap.put("returnWeChat", returnWeChat);
ResultMap.put("data", WxResultMap);
} else {
//验证失败(表示可能接口被他人调用 需要留意)
ResultMap.put("Verify", "NO");
ResultMap.put("msg", "验签失败。");
}
return ResultMap;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public R refund(Long orderId, BigDecimal refundTotal) {
R<DeviceOrdersVO> deviceOrdersVOR = deviceOrdersClient.detailById(orderId);
if (!deviceOrdersVOR.isSuccess() || deviceOrdersVOR.getData() == null) {
return R.fail("查询订单失败");
}
DeviceOrdersVO ordersVO = deviceOrdersVOR.getData();
if (ordersVO.getPayStatus() != 1) {
return R.fail("订单未支付,不能退款");
}
AuthConfig config = socialProperties.getOauth().get(AuthDefaultSource.WECHAT_MP);
String clientId = config.getClientId();
Map<String, String> req = new HashMap<>();
req.put("appid", clientId);
req.put("mch_id", payConfig.getMchId());
req.put("nonce_str", WXPayUtil.generateNonceStr());
req.put("out_trade_no", ordersVO.getOrderNumber());
req.put("out_refund_no", IdUtil.getSnowflake(0, 0).nextIdStr());
req.put("total_fee", String.valueOf(MoneyUtils.yuanToFen(String.valueOf(ordersVO.getAmount()))));
req.put("refund_fee", String.valueOf(MoneyUtils.yuanToFen(String.valueOf(refundTotal))));
req.put("notify_url", payConfig.getDomain() + "/api/xxxxx/xx/xx/notify-refund-wx");
try {
req.put("sign", WXPayUtil.generateSignature(req, payConfig.getMchKey(), WXPayConstants.SignType.MD5));
String xmlBody = WXPayUtil.generateSignedXml(req, payConfig.getMchKey());
System.err.println(String.format("微信退款请求 xml 格式:\n%s", xmlBody));
String result = WxChatPayCommonUtil.httpsRequest(WeChatPayUrl.Refund, "POST", xmlBody);
System.err.println(String.format("%s", result));
Map<String, String> WxResultMap = WXPayUtil.xmlToMap(result);
if ("SUCCESS".equals(WxResultMap.get("return_code")) && "SUCCESS".equals(WxResultMap.get("result_code"))) {
ordersVO.setPayStatus(2); // 退款状态 已退款
// TODO: 2025/4/6 后续需要完善业务逻辑 退款并且恢复用户使用时长等
R updateR = deviceOrdersClient.submit(ordersVO);
if (updateR.isSuccess()) {
log.info("变更订单状态为已退款成功");
return R.data(WxResultMap);
} else {
log.info("变更订单状态为已退款失败");
return R.fail("变更订单状态为已退款失败");
}
} else {
return R.fail("退款失败: " + WxResultMap.get("err_code_des"));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Map<String, Object> WeChatRefundCallback(String xmlData, String apiKey) {
Map<String, Object> ResultMap = new HashMap<>();
try {
Map<String, String> WxResultMap = WXPayUtil.xmlToMap(xmlData);
boolean signStatus = WXPayUtil.isSignatureValid(WxResultMap, apiKey);
if (signStatus) {
String returnWeChat = WxChatPayCommonUtil.setReturnXml("SUCCESS", "OK");
ResultMap.put("Verify", "YES");
ResultMap.put("returnWeChat", returnWeChat);
ResultMap.put("data", WxResultMap);
} else {
ResultMap.put("Verify", "NO");
ResultMap.put("msg", "验签失败。");
}
return ResultMap;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
需要注意的是 退款 必须要加上 证书 而 支付不需要,所以在发送请求的时候 需要修改请求方法:
package com.xhsoft.mp.util;
import com.xhsoft.mp.config.PayConfig;
import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.*;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.SecureRandom;
/**
* 微信支付工具类
*/
public class WxChatPayCommonUtil {
private static PayConfig payConfig;
public static void setPayConfig(PayConfig config) {
payConfig = config;
}
/**
* 发送 http 请求
* @param requestUrl 请求路径
* @param requestMethod 请求方式(GET/POST/PUT/DELETE/...)
* @param outputStr 请求参数体
* @return 结果信息
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
// 加载证书
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// 使用类加载器获取证书输入流
InputStream instream = WxChatPayCommonUtil.class.getClassLoader().getResourceAsStream("apiclient_cert.p12");
if (instream == null) {
throw new FileNotFoundException("未找到 apiclient_cert.p12 证书文件");
}
try {
// 请将此处替换为你的商户ID,通常证书密码和商户ID相同
keyStore.load(instream, payConfig.getMchId().toCharArray());
} finally {
instream.close();
}
// 创建SSL上下文
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// 请将此处替换为你的商户ID,通常证书密码和商户ID相同
kmf.init(keyStore, payConfig.getMchId().toCharArray());
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
}};
sslContext.init(kmf.getKeyManagers(), trustAllCerts, new SecureRandom());
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
conn.setHostnameVerifier((hostname, session) -> true);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取ip
* @param request 请求
* @return ip 地址
*/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return "";
}
String ip = request.getHeader("X-Requested-For");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 从流中读取微信返回的xml数据
* @param httpServletRequest
* @return
* @throws IOException
*/
public static String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException {
InputStream inputStream = httpServletRequest.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuffer sb = new StringBuffer();
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
} finally {
bufferedReader.close();
inputStream.close();
}
return sb.toString();
}
/**
* 设置返回给微信服务器的xml信息
* @param returnCode
* @param returnMsg
* @return
*/
public static String setReturnXml(String returnCode, String returnMsg) {
return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
}
}
还有就是 注入配置的问题,你需要拿到咱们的商户id:
@Data
@Component
@ConfigurationProperties(prefix = "pay")
public class PayConfig {
private String mchId;
private String mchKey;
// 域名
private String domain;
}
@Configuration
public class MpConfiguration {
@Autowired
private PayConfig payConfig;
/**
* PostConstruct注解表示在依赖注入完成后,在对象的初始化阶段调用该方法。
* 这里的目的是在PayConfig实例被成功注入后,将其设置到WxChatPayCommonUtil工具类中,
* 以便WxChatPayCommonUtil类在后续执行httpsRequest方法(例如加载微信支付证书时)
* 时能够获取到有效的PayConfig配置信息,进而正确地使用其中的商户ID等配置来进行操作。
*/
@PostConstruct
public void setupWxPayUtil() {
WxChatPayCommonUtil.setPayConfig(payConfig);
}
}
下面 分享一下 支付所需要的工具类:
public class HexUtils {
public static String bytesToHexStr(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}
package com.xhsoft.mp.util;
import java.security.MessageDigest;
/**
* @author rsz
* @version 1.0.0
* @ClassName MD5.java
* @Description MD5工具类
* @createTime 2022年04月09日 12:58:00
*/
public class MD5 {
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 转换字节数组为16进制字串
*
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
*
* @param b 要转换的byte
* @return 16进制格式
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* MD5编码
*
* @param origin 原始字符串
* @return 经过MD5加密之后的结果
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
}
package com.xhsoft.mp.util;
import java.math.BigDecimal;
/**
* 钱 工具类
*
* Created by YouHan on 2019-06-28 09:12:00
* Copyright © 2019 YouHan All rights reserved.
*/
public class MoneyUtils {
public static final String YUAN = "元";
public static final String FEN = "分";
/**
* 元转分
*
* @param s
* @return java.lang.Integer
* @date 2020/9/10 9:03
* @author YouHan
*/
public static Integer yuanToFen(String s) {
if (!YouNumberUtil.isNumber(s)) {
return 0;
}
return new BigDecimal(s).multiply(new BigDecimal(100)).intValue();
}
/**
* 处理分
*
* @param s
* @return java.lang.Integer
* @author YouHan
* @date 2022/7/23
*/
public static Integer handleFen(String s) {
if (!YouNumberUtil.isNumber(s)) {
return 0;
}
return new BigDecimal(s).intValue();
}
/**
* 分转元
* 可以为正负小数(这里保留2位小数)
*
* @param s
* @return java.lang.String
* @date 2020/9/10 9:01
* @author YouHan
*/
public static String fenToYuan(String s) {
if (!YouNumberUtil.isNumber(s) || "0".equals(s) || "-0".equals(s)) {
return "0.00";
}
return new BigDecimal(s)
.divide(new BigDecimal(100))
.setScale(2, BigDecimal.ROUND_DOWN)
.toString();
}
/**
* 处理元
* 可以为正负小数(这里保留2位小数)
*
* @param s
* @return java.lang.String
* @author YouHan
* @date 2022/7/23
*/
public static String handleYuan(String s) {
if (!YouNumberUtil.isNumber(s) || "0".equals(s) || "-0".equals(s)) {
return "0.00";
}
return new BigDecimal(s)
.setScale(2, BigDecimal.ROUND_DOWN)
.toString();
}
}
package com.xhsoft.mp.util;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
/**
*/
public class Signature {
/**
* 签名算法
*
* @param o 要参与签名的数据对象
* @return 签名
* @throws IllegalAccessException
*/
public static String getSign(Object o,String mchKey) throws IllegalAccessException {
ArrayList<String> list = new ArrayList<String>();
Class cls = o.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
f.setAccessible(true);
if (f.get(o) != null && f.get(o) != "") {
String name = f.getName();
XStreamAlias anno = f.getAnnotation(XStreamAlias.class);
if (anno != null)
name = anno.value();
list.add(name + "=" + f.get(o) + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + mchKey;
System.out.println("签名数据:" + result);
result = MD5.MD5Encode(result).toUpperCase();
return result;
}
public static String getSign(Map<String, Object> map,String mchKey) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + mchKey;
//Util.log("Sign Before MD5:" + result);
result = MD5.MD5Encode(result).toUpperCase();
//Util.log("Sign Result:" + result);
return result;
}
}
/**
* 微信支付接口地址
*
*/
public class WeChatPayUrl {
//统一下单预下单接口url
public static final String Uifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//订单状态查询接口URL
public static final String Orderquery = "https://api.mch.weixin.qq.com/pay/orderquery";
//订单申请退款
public static final String Refund = "https://api.mch.weixin.qq.com/secapi/pay/refund";
//付款码 支付
public static final String MicroPay = "https://api.mch.weixin.qq.com/pay/micropay";
//微信网页授权 获取“code”请求地址
public static final String GainCodeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize";
}
package com.xhsoft.mp.util;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.ThreadLocalRandom;
/**
* 数字 client
*
* Created by YouHan on 2020-09-09 13:28:40
* Copyright © 2021 YouHan All rights reserved.
*/
public class YouNumberUtil {
/**
* 整数正则表达式
*/
public static final String INTEGER_REGEX = "^[-\\+]?[\\d]*$";
/**
* 数字正则表达式
*/
public static final String NUMBER_REGEX = "^-?\\d+(\\.\\d+)?$";
/**
* 是否是整数
*
* @param s
* @return java.lang.Boolean
* @date 2020/9/12 8:38
* @author YouHan
*/
public static boolean isInteger(String s) {
if (StringUtils.isBlank(s)) {
return false;
}
return s.matches(INTEGER_REGEX);
}
/**
* 判断给定字符串是否为十六进制数
*
* @param value 值
* @return 是否为十六进制
*/
public static boolean isHex(String value) {
final int index = (value.startsWith("-") ? 1 : 0);
if (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index)) {
try {
Long.decode(value);
} catch (NumberFormatException e) {
return false;
}
return true;
}
return false;
}
/**
* 是否是数字(包括小数)
*
* @param s
* @return java.lang.Boolean
* @date 2020/9/9 14:01
* @author YouHan
*/
public static boolean isNumber(String s) {
if (StringUtils.isBlank(s)) {
return false;
}
return s.matches(NUMBER_REGEX);
}
/**
* 十进制转十六进制
*
* @param n 十进制数
* @return java.lang.String
* @date 2019/4/8 09:22
* @author YouHan
*/
public static String intToHex(Integer n) {
if (null == n) {
return null;
}
return String.format("%X", n);
}
/**
* 十进制转十六进制
*
* @param n 十进制数
* @return java.lang.String
* @date 2019/4/8 09:22
* @author YouHan
*/
public static String longToHex(Long n) {
if (null == n) {
return null;
}
return String.format("%X", n);
}
/**
* 十进制转十六进制
*
* @param n
* @param length
* @return java.lang.String
* @date 2019-08-12 09:56
* @author YouHan
*/
public static String intToHexPrefix(Integer n, Integer length) {
if (null == n) {
return null;
}
if (null == length || length <= 0) {
return null;
}
String result = String.format("%X", n);
if (result.length() < length) {
result = YouStringUtil.appendPrefixContent(result, "0", length - result.length());
}
return result;
}
/**
* 十进制转十六进制
*
* @param n
* @param length
* @return java.lang.String
* @date 2019-08-12 09:56
* @author YouHan
*/
public static String longToHexPrefix(Long n, Integer length) {
if (null == n) {
return null;
}
if (null == length || length <= 0) {
return null;
}
String result = String.format("%X", n);
if (result.length() < length) {
result = YouStringUtil.appendPrefixContent(result, "0", length - result.length());
}
return result;
}
/**
* 十进制转十六进制
*
* @param n 十进制数
* @return java.lang.String
* @date 2019/4/8 09:22
* @author YouHan
*/
public static String intToHexSuffix(Integer n, Integer length) {
if (null == n) {
return null;
}
if (null == length || length <= 0) {
return null;
}
String result = String.format("%X", n);
if (result.length() < length) {
result = YouStringUtil.appendSuffixContent(result, "0", length - result.length());
}
return result;
}
/**
* 十进制转十六进制
*
* @param n 十进制数
* @return java.lang.String
* @date 2019/4/8 09:22
* @author YouHan
*/
public static String longToHexSuffix(Long n, Integer length) {
if (null == n) {
return null;
}
if (null == length || length <= 0) {
return null;
}
String result = String.format("%X", n);
if (result.length() < length) {
result = YouStringUtil.appendSuffixContent(result, "0", length - result.length());
}
return result;
}
/**
* 十六进制转十进制
*
* @param hex
* @return java.lang.Integer
* @date 2019/4/8 09:49
* @author YouHan
*/
public static Integer hexToInt(String hex) {
Long n = hexToLong(hex);
if (null == n) {
return null;
}
// 超出整数最大值,不予处理
if (Integer.MAX_VALUE < n) {
return null;
}
return Integer.valueOf(String.valueOf(n));
}
/**
* 十六进制转十进制
*
* @param hex
* @return java.lang.Integer
* @date 2019/4/8 09:49
* @author YouHan
*/
public static Long hexToLong(String hex) {
if (StringUtils.isBlank(hex)) {
return null;
}
// 去除前缀为 0 的 十六进制
if (hex.length() > 1 && hex.startsWith("0")) {
hex = hex.substring(1);
}
return Long.valueOf(hex, 16);
}
/**
* 字符串转十六进制
*
* @param s
* @return
*/
public static String stringToHex(String s) {
char[] chars = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder("");
byte[] bs = s.getBytes();
int bit;
for (int i = 0; i < bs.length; i++) {
bit = (bs[i] & 0x0f0) >> 4;
sb.append(chars[bit]);
bit = bs[i] & 0x0f;
sb.append(chars[bit]);
}
return sb.toString().trim();
}
/**
* 十六进制转字符串
*
* @param s
* @return
*/
public static String hexToString(String s) {
String str = "0123456789ABCDEF";
char[] hexs = s.toCharArray();
byte[] bytes = new byte[s.length() / 2];
int n;
for (int i = 0; i < bytes.length; i++) {
n = str.indexOf(hexs[2 * i]) * 16;
n += str.indexOf(hexs[2 * i + 1]);
bytes[i] = (byte) (n & 0xff);
}
return new String(bytes);
}
/**
* 去除末尾多余的 0
*
* @param s
* @return java.lang.String
* @author YouHan
* @date 2021/7/2 15:39
*/
public static String stripTrailingZeros(String s) {
if (StringUtils.isBlank(s)) {
return "0";
}
if (!isNumber(s)) {
return "0";
}
if (!s.contains(".")) {
return s;
}
while (s.endsWith("0")) {
s = s.substring(0, s.length() - 1);
}
if (s.endsWith(".")) {
s = s.substring(0, s.length() - 1);
}
return s;
}
/**
* int 转 String
* 1024以内高效,超出后,正常转换
*/
static int cacheSize = 1024;
static String[] caches = new String[cacheSize];
static {
for (int i = 0; i < cacheSize; i++) {
caches[i] = String.valueOf(i);
}
}
public static String int2String(int data) {
if (data < cacheSize) {
return caches[data];
}
return String.valueOf(data);
}
/**
* 获取几位的 int 随机数
*
* @param length
* @return int
* @author YouHan
* @date 2021/12/19
*/
public static int getRandomInt(int length) {
return (int) ((Math.random() * 9 + 1) * 10 * length);
}
/**
* 获取几位的 long 随机数
*
* @param length
* @return long
* @author YouHan
* @date 2021/12/19
*/
public static long getRandomLong(long length) {
return (long) ((Math.random() * 9 + 1) * 10 * length);
}
/**
* 获取随机数
*
* @param
* @return java.client.concurrent.ThreadLocalRandom
* @author YouHan
* @date 2021/6/3 10:29
*/
public static ThreadLocalRandom getRandom() {
return ThreadLocalRandom.current();
}
/**
* 获取缓存穿透时间(单位秒),最长不超过 5 分钟
*
* @param
* @return java.lang.Long
* @date 2021/4/26 9:50
* @author YouHan
*/
public static Long getCachePenetrationTime() {
return Long.valueOf(int2String(getRandom().nextInt(300)));
}
/**
* 获取数据库缓存时间(单位秒),最长不超过 1 小时
*
* @param
* @return java.lang.Long
* @date 2021/4/26 9:50
* @author YouHan
*/
public static Long getDBCacheTime() {
return Long.valueOf(int2String(getRandom().nextInt(3600)));
}
/**
* 十六进制高低位转换
*
* @param hexString
* @return java.lang.String
* @author YouHan
* @date 2021/12/11
*/
public static String hexHighLowPositionConvert(String hexString) {
if (StringUtils.isBlank(hexString) || hexString.length() % 2 != 0) {
return null;
}
StringBuilder result = new StringBuilder();
for (int i = hexString.length() - 2; i >= 0; i = i - 2) {
result.append(hexString.substring(i, i + 2));
}
return result.toString();
}
}
package com.xhsoft.mp.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* String client
*
* Created by YouHan on 2019-09-11 08:57:56
* Copyright © 2019 YouHan All rights reserved.
*/
public class YouStringUtil {
/**
* 下划线
*/
public static final Pattern LINE = Pattern.compile("_(\\w)");
/**
* 驼峰
*/
public static final Pattern HUMP = Pattern.compile("[A-Z]");
/**
* 添加内容
*
* @param content
* @param length
* @return java.lang.String
* @author YouHan
* @date 2021/6/17 9:59
*/
public static String appendContent(String content, int length) {
if (length <= 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i ++) {
sb.append(content);
}
return sb.toString();
}
/**
* 添加前缀内容
*
* @param s
* @param content
* @param length
* @return java.lang.String
* @date 2019-08-12 09:53
* @author YouHan
*/
public static String appendPrefixContent(String s, String content, int length) {
if (length <= 0) {
return null;
}
StringBuilder sb = new StringBuilder(s);
for (int i = 0; i < length; i ++) {
sb.append(content, 0, content.length());
}
return sb.toString();
}
/**
* 添加后缀内容
*
* @param s
* @param content
* @param length
* @return java.lang.String
* @date 2019-08-12 09:56
* @author YouHan
*/
public static String appendSuffixContent(String s, String content, int length) {
if (length <= 0) {
return null;
}
StringBuilder sb = new StringBuilder(s);
for (int i = 0; i < length; i ++) {
sb.append(content);
}
return sb.toString();
}
/**
* Set 转 String
*
* @param stringSet
* @return java.lang.String
* @author YouHan
* @date 2021/6/3 9:26
*/
public static String setToString(Set<String> stringSet) {
return setToString(stringSet, null);
}
/**
* Set 转 String
*
* @param stringSet
* @param regex
* @return java.lang.String
* @date 2021/6/3 9:21
* @author YouHan
*/
public static String setToString(Set<String> stringSet, String regex) {
// 参数校验
if (CollectionUtils.isEmpty(stringSet)) {
return null;
}
if (StringUtils.isBlank(regex)) {
regex = ",";
}
// List to String
StringBuilder sb = new StringBuilder(stringSet.size());
for (String s : stringSet) {
sb.append(s).append(regex);
}
// 返回结果
return sb.substring(0, sb.length() - 1);
}
/**
* 字符串列表转字符串
*
* @author YouHan
* @generatedDate: 2018/10/9 17:25
* @param stringList 要转换的字符串列表
* @return
*/
public static String listToString(List<String> stringList) {
return listToString(stringList, null);
}
/**
* 字符串列表转字符串
*
* @author YouHan
* @generatedDate: 2018/10/9 17:25
* @param stringList 要转换的字符串列表
* @return
*/
public static String listToString(List<String> stringList, String regex) {
// 参数校验
if (CollectionUtils.isEmpty(stringList)) {
return null;
}
if (StringUtils.isBlank(regex)) {
regex = ",";
}
// List to String
StringBuilder sb = new StringBuilder(stringList.size());
for (String s : stringList) {
sb.append(s).append(regex);
}
// 返回结果
return sb.substring(0, sb.length() - 1);
}
/**
* 字符串转列表
*
* @param s
* @return java.client.List<java.lang.String>
* @date 2019-09-11 09:11
* @author YouHan
*/
public static List<String> stringToList(String s) {
/**
* 参数校验
*/
if (StringUtils.isBlank(s)) {
return null;
}
return stringToList(s, null);
}
/**
* 字符串转列表
*
* @param s
* @param regex 分割规则,默认为逗号
* @return java.client.List<java.lang.String>
* @date 2019-09-11 09:11
* @author YouHan
*/
public static List<String> stringToList(String s, String regex) {
/**
* 参数校验
*/
if (StringUtils.isBlank(s)) {
return null;
}
/**
* 默认逗号隔开
*/
if (StringUtils.isBlank(regex)) {
regex = ",";
}
/**
* 去除首尾空格
*/
String blankString = " ";
while (s.startsWith(blankString)) {
s = s.substring(1);
}
while (s.endsWith(blankString)) {
s = s.substring(0, s.length() -1);
}
/**
* 返回结果列表
*/
List<String> resultList = new ArrayList<>();
/**
* 只有单个元素
*/
if (!s.contains(regex)) {
resultList.add(s);
return resultList;
}
String[] strings = s.split(regex);
for (String e : strings) {
resultList.add(e);
}
return resultList;
}
/**
* 过滤逗号
* @param s
* @return
*/
public static String filterCommaString(String s) {
// 数据为空校验
if (StringUtils.isEmpty(s)) {
return null;
}
// 去除 并列逗号
s = s.replace(",,", ",");
// 去除 首逗号
if (s.startsWith(",")) {
s = s.substring(1, s.length() - 1);
}
// 去除 尾逗号
if (s.endsWith(",")) {
s = s.substring(0, s.length() - 1);
}
return s;
}
/**
* 是否包含中文(包括中文标点符号和空格)
*
* @param s
* @return boolean
* @date 2020/9/9 13:30
* @author YouHan
*/
public static Boolean isContainChinese(String s) {
/**
* 参数校验
*/
if (StringUtils.isBlank(s)) {
return false;
}
if (s.contains(" ")) {
return true;
}
/**
* 中文正则表达式
*/
String regex = "[\u4e00-\u9fa5]";
if (s.matches(regex)) {
return Boolean.TRUE;
}
/**
* 中文标点符号处理
*/
char[] chars = s.toCharArray();
for (char c : chars) {
if (isChinesePunctuation(c)) {
return true;
}
}
return false;
}
/**
* 过滤中文(包括标点符号和空格)
*
* @param s
* @return java.lang.String
* @date 2020/9/9 14:08
* @author YouHan
*/
public static String filterChinese(String s) {
/**
* 参数校验
*/
if (StringUtils.isBlank(s)) {
return "";
}
s = s.replace(" ", "");
if (!isContainChinese(s)) {
return s;
}
/**
* 过滤中文字符
*/
char[] chars = s.toCharArray();
StringBuilder sb = new StringBuilder(chars.length);
for (char c : chars) {
if (isContainChinese(String.valueOf(c))) {
continue;
}
sb.append(c);
}
return sb.toString();
}
/**
* 判断是否为中文标点符号
*
* @param c
* @return java.lang.Boolean
* @date 2020/9/9 13:43
* @author YouHan
*/
public static boolean isChinesePunctuation(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS
|| ub == Character.UnicodeBlock.VERTICAL_FORMS) {
return true;
}
return false;
}
/**
* 获取 UUID
*
* @param
* @return java.lang.String
* @date 2021/4/9 14:08
* @author YouHan
*/
public static String getUUID() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 安全比较(可防止时序攻击 timing attack)
*/
public static boolean safeEqual(String a, String b) {
if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
return false;
}
if (a.length() != b.length()) {
return false;
}
int equal = 0;
for (int i = 0; i < a.length(); i++) {
equal |= a.charAt(i) ^ b.charAt(i);
}
return equal == 0;
}
/**
* 驼峰转下划线
*
* @param s
* @return java.lang.String
* @date 2021/5/6 22:20
* @author YouHan
*/
public static String humpToLine(String s) {
Matcher matcher = HUMP.matcher(s);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
if (sb.toString().startsWith("_")) {
sb.deleteCharAt(0);
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 下划线转驼峰
*
* @param s
* @return java.lang.String
* @date 2021/5/6 22:21
* @author YouHan
*/
public static String lineToHump(String s) {
s = s.toLowerCase();
Matcher matcher = LINE.matcher(s);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 生成加密的内容
*
* @param s
* @return java.lang.String
* @author YouHan
* @date 2021/6/17 10:10
*/
public static String hide(String s) {
/**
* 1
* 1*
* 1**
* 1***
* 1***5
* 12***6
* 12***67
* 123***78
* 123***789
* 123****890
* 123*****8901
*/
if (s.isEmpty() || s.length() == 1) {
return s;
}
if (s.length() == 2) {
return s.substring(0, 1) + "*";
}
if (s.length() == 3 || s.length() == 4) {
return s.substring(0, 1) + appendContent("*", s.length() - 1);
}
if (s.length() == 5) {
return s.substring(0, 1) + "***" + s.substring(4);
}
if (s.length() == 6 || s.length() == 7) {
return s.substring(0, 2) + appendContent("*", 3) + s.substring(5);
}
if (s.length() == 8) {
return s.substring(0, 3) + "***" + s.substring(6);
}
return s.substring(0, 3) + appendContent("*", s.length() - 6) + s.substring(s.length() - 3);
}
}