package org.springblade.modules.pay.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.github.wxpay.sdk.WXPayUtil; import com.google.gson.Gson; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springblade.common.config.WxPayConfig; import org.springblade.common.enums.OrderStatus; import org.springblade.common.enums.wxpay.WxApiType; import org.springblade.common.enums.wxpay.WxNotifyType; import org.springblade.common.enums.wxpay.WxRefundStatus; import org.springblade.common.enums.wxpay.WxTradeState; import org.springblade.common.exception.CustomException; import org.springblade.common.utils.HttpUtils; import org.springblade.common.utils.SpringUtils; import org.springblade.common.utils.WechatPay2ValidatorForRequest; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.modules.pay.entity.OrderInfoEntity; import org.springblade.modules.pay.entity.RefundInfoEntity; import org.springblade.modules.pay.entity.WxPayInfo; import org.springblade.modules.pay.mapper.WxPayMapper; import org.springblade.modules.pay.service.IOrderInfoService; import org.springblade.modules.pay.service.IPaymentInfoService; import org.springblade.modules.pay.service.IRefundInfoService; import org.springblade.modules.pay.service.IWxPayService; import org.springblade.modules.system.entity.User; import org.springblade.modules.system.service.IUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.FileInputStream; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @Service public class WxPayServiceImpl extends ServiceImpl implements IWxPayService { private static Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class); @Resource private WxPayConfig wxPayConfig; // @Resource // private CloseableHttpClient wxPayClient; @Resource private IOrderInfoService orderInfoService; @Resource private IPaymentInfoService paymentInfoService; @Resource private IRefundInfoService refundsInfoService; private final ReentrantLock lock = new ReentrantLock(); @Transactional(rollbackFor = Exception.class) @Override public void processOrder(Map bodyMap) throws GeneralSecurityException { logger.info("处理订单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); /*在对业务数据进行状态检查和处理之前, 要采用数据锁进行并发控制, 以避免函数重入造成的数据混乱*/ //尝试获取锁: // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放 if (lock.tryLock()) { try { //处理重复的通知 //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return; } //模拟通知并发 try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(plainText); } finally { //要主动释放锁 lock.unlock(); } } } /** * 用户取消订单 * * @param orderNo */ @Override public void cancelOrder(String orderNo) throws Exception { //调用微信支付的关单接口 this.closeOrder(orderNo); //更新商户端的订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); } @Override public String queryOrder(String orderNo) throws Exception { logger.info("查单接口调用 ===> {}", orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId()); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = null;//wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 logger.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body logger.info("成功"); } else { logger.info("查单接口调用,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } return bodyAsString; } finally { response.close(); } } /** * 根据订单号查询微信支付查单接口,核实订单状态 * 如果订单已支付,则更新商户端订单状态,并记录支付日志 * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态 * * @param orderNo */ @Transactional(rollbackFor = Exception.class) @Override public void checkOrderStatus(String orderNo) throws Exception { logger.info("根据订单号核实订单状态 ===> {}", orderNo); //调用微信支付查单接口 String result = this.queryOrder(orderNo); Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端的订单状态 String tradeState = resultMap.get("trade_state"); //判断订单状态 if (WxTradeState.SUCCESS.getType().equals(tradeState)) { logger.info("核实订单已支付 ===> {}", orderNo); //如果确认订单已支付则更新本地订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(result); } if (WxTradeState.NOTPAY.getType().equals(tradeState)) { logger.info("核实订单未支付 ===> {}", orderNo); //如果订单未支付,则调用关单接口 this.closeOrder(orderNo); //更新本地订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); } } /** * 退款 * * @param orderNo * @param reason * @throws IOException */ @Transactional(rollbackFor = Exception.class) @Override public void refund(String orderNo, String reason) throws Exception { logger.info("创建退款单记录"); //根据订单编号创建退款单 RefundInfoEntity refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason); logger.info("调用退款API"); //调用统一下单API String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()); HttpPost httpPost = new HttpPost(url); // 请求body参数 Gson gson = new Gson(); Map paramsMap = new HashMap(); paramsMap.put("out_trade_no", orderNo);//订单编号 paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号 paramsMap.put("reason", reason);//退款原因 paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址 Map amountMap = new HashMap(); amountMap.put("refund", refundsInfo.getRefund());//退款金额 amountMap.put("total", refundsInfo.getTotalFee());//原订单金额 amountMap.put("currency", "CNY");//退款币种 paramsMap.put("amount", amountMap); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); logger.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json");//设置请求报文格式 httpPost.setEntity(entity);//将请求报文放入请求对象 httpPost.setHeader("Accept", "application/json");//设置响应报文格式 //完成签名并执行请求,并完成验签 CloseableHttpResponse response =null;// wxPayClient.execute(httpPost); try { //解析响应结果 String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { logger.info("成功, 退款返回结果 = " + bodyAsString); } else if (statusCode == 204) { logger.info("成功"); } else { throw new RuntimeException("退款异常, 响应码 = " + statusCode + ", 退款返回结果 = " + bodyAsString); } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING); //更新退款单 refundsInfoService.updateRefund(bodyAsString); } finally { response.close(); } } /** * 查询退款接口调用 * * @param refundNo * @return */ @Override public String queryRefund(String refundNo) throws Exception { logger.info("查询退款接口调用 ===> {}", refundNo); String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo); url = wxPayConfig.getDomain().concat(url); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = null;//wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { logger.info("成功, 查询退款返回结果 = " + bodyAsString); } else if (statusCode == 204) { logger.info("成功"); } else { throw new RuntimeException("查询退款异常, 响应码 = " + statusCode + ", 查询退款返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } } /** * 根据退款单号核实退款单状态 * * @param refundNo * @return */ @Transactional(rollbackFor = Exception.class) @Override public void checkRefundStatus(String refundNo) throws Exception { logger.info("根据退款单号核实退款单状态 ===> {}", refundNo); //调用查询退款单接口 String result = this.queryRefund(refundNo); //组装json请求体字符串 Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端退款状态 String status = resultMap.get("status"); String orderNo = resultMap.get("out_trade_no"); if (WxRefundStatus.SUCCESS.getType().equals(status)) { logger.info("核实订单已退款成功 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 refundsInfoService.updateRefund(result); } if (WxRefundStatus.ABNORMAL.getType().equals(status)) { logger.info("核实订单退款异常 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); //更新退款单 refundsInfoService.updateRefund(result); } } /** * 处理退款单 */ @Transactional(rollbackFor = Exception.class) @Override public void processRefund(Map bodyMap) throws Exception { logger.info("退款单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); if (lock.tryLock()) { try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { return; } //更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 refundsInfoService.updateRefund(plainText); } finally { //要主动释放锁 lock.unlock(); } } } @Override public Map jsapiPay(Long properChargeRecordId) throws IOException { String prepayId = ""; //生成订单 OrderInfoEntity orderInfo = orderInfoService.getOne(Wrappers.lambdaQuery().eq(OrderInfoEntity::getProperChargeRecordId, properChargeRecordId)); if (orderInfo.getTotalFee() <= 0) { throw new CustomException("支付金额不能低于等于0元"); } IUserService userService = SpringUtils.getBean(IUserService.class); User serviceOne = userService.getOne(Wrappers.lambdaQuery().eq(User::getId, AuthUtil.getUserId())); logger.info("调用统一下单API"); //调用统一下单API HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType())); // 请求body参数 Gson gson = new Gson(); Map paramsMap = new HashMap(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); paramsMap.put("description", orderInfo.getTitle()); paramsMap.put("out_trade_no", orderInfo.getOrderNo()); paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map amountMap = new HashMap(); amountMap.put("total", orderInfo.getTotalFee()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); Map payerMap = new HashMap(); payerMap.put("openid", serviceOne.getMiniOpenId()); paramsMap.put("payer", payerMap); // 是否指定分账,枚举值 true:是 false:否 paramsMap.put("profit_sharing", true); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); logger.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = null;//wxPayClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 logger.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body logger.info("成功"); } else { logger.info("jszpi下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } //响应结果 Map resultMap = gson.fromJson(bodyAsString, HashMap.class); //预支付交易会话标识 prepayId = resultMap.get("prepay_id"); //返回预支付交易会话标识 // 组装前端预下单参数 /** 返回给前端所需要的数据 */ SortedMap payMap = new TreeMap<>(); String nonceStr = WXPayUtil.generateNonceStr(); String timestamp = String.valueOf(System.currentTimeMillis() / 1000); payMap.put("appId", wxPayConfig.getAppid()); payMap.put("timeStamp", timestamp); payMap.put("nonceStr", nonceStr); payMap.put("signType", "RSA"); payMap.put("package", "prepay_id=" + prepayId); String paySign = getSign(wxPayConfig.getAppid(), prepayId, timestamp, nonceStr); payMap.put("paySign", paySign); logger.info("返回参数 ===> {}" + gson.toJson(payMap)); return payMap; } catch (IOException e) { throw new RuntimeException(e); } catch (SignatureException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (InvalidKeyException e) { throw new RuntimeException(e); } finally { response.close(); } } /** * 通过prepay_id获取签名 * * @param appid * @param prepay_id * @param timestamp * @param nonceStr * @return * @throws IOException * @throws SignatureException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ String getSign(String appid, String prepay_id, String timestamp, String nonceStr) throws IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException { //从下往上依次生成 String message = getSignStr(appid, timestamp, nonceStr, "prepay_id=" + prepay_id); //签名 String signature = sign(message.getBytes("utf-8")); return signature; } String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException { //签名方式 Signature sign = Signature.getInstance("SHA256withRSA"); //私钥,通过MyPrivateKey来获取,这是个静态类可以接调用方法 ,需要的是_key.pem文件的绝对路径配上文件名 sign.initSign(getPrivateKey(wxPayConfig.getPrivateKeyPath())); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } /** * 按照前端签名文档规范进行排序 * * @param appId * @param timeStamp * @param nonceStr * @param packageValue * @return */ private String getSignStr(String appId, String timeStamp, String nonceStr, String packageValue) { return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue); } private PrivateKey getPrivateKey(String filename) { try { FileInputStream fileInputStream = new FileInputStream(filename); logger.info("文件内容:" + fileInputStream); return PemUtil.loadPrivateKey(fileInputStream); } catch (Exception e) { logger.info("私钥文件不存在", e); throw new RuntimeException("私钥文件不存在", e); } } /** * 关单接口的调用 * * @param orderNo */ private void closeOrder(String orderNo) throws Exception { logger.info("关单接口的调用,订单号 ===> {}", orderNo); //创建远程请求对象 String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url); HttpPost httpPost = new HttpPost(url); //组装json请求体 Gson gson = new Gson(); Map paramsMap = new HashMap<>(); paramsMap.put("mchid", wxPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); logger.info("请求参数 ===> {}", jsonParams); //将请求参数设置到请求对象中 StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = null;//wxPayClient.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 logger.info("成功200"); } else if (statusCode == 204) { //处理成功,无返回Body logger.info("成功204"); } else { logger.info("Native下单失败,响应码 = " + statusCode); throw new IOException("request failed"); } } finally { response.close(); } } /** * 对称解密 * * @param bodyMap * @return */ private String decryptFromResource(Map bodyMap) throws GeneralSecurityException { logger.info("密文解密"); //通知数据 Map resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); logger.info("密文 ===> {}", ciphertext); 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); logger.info("明文 ===> {}", plainText); return plainText; } @Override public String nativeNotify(HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 try { //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); logger.info("支付通知的id ===> {}", requestId); logger.info("支付通知的完整数据 ===> {}", body); //int a = 9 / 0; //签名的验证 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(wxPayConfig.getVerifier(), requestId, body); if (!wechatPay2ValidatorForRequest.validate(request)) { logger.error("通知验签失败"); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } logger.info("通知验签成功"); //处理订单 processOrder(bodyMap); //成功应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "失败"); return gson.toJson(map); } } @Override public String refundsNotify(HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 try { //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); logger.info("支付通知的id ===> {}", requestId); //签名的验证 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(wxPayConfig.getVerifier(), requestId, body); if (!wechatPay2ValidatorForRequest.validate(request)) { logger.error("通知验签失败"); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } logger.info("通知验签成功"); //处理退款单 processRefund(bodyMap); //成功应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "失败"); return gson.toJson(map); } } //创建签名所需要的参数xml格式 // public static String createXML(Map map) { // String xml = ""; // Set set = map.keySet(); // Iterator i = set.iterator(); // while (i.hasNext()) { // String str = i.next(); // xml += "<" + str + ">" + "" + ""; // } // xml += ""; // return xml; // } }