| | |
| | | package org.springblade.modules.pay.service.impl; |
| | | |
| | | 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.HttpPost; |
| | | import org.apache.http.entity.StringEntity; |
| | | import org.apache.http.util.EntityUtils; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springblade.common.enums.OrderStatus; |
| | | import org.springblade.common.enums.wxpay.WxApiType; |
| | | import org.springblade.common.enums.wxpay.WxNotifyType; |
| | | import org.springblade.modules.pay.controller.WxPayController; |
| | | import org.springblade.modules.pay.entity.WxPayInfo; |
| | | import org.springblade.modules.pay.mapper.WxPayMapper; |
| | | import org.springblade.modules.pay.service.IWxPayService; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.io.FileInputStream; |
| | | import java.io.IOException; |
| | | 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<WxPayMapper,WxPayInfo> implements IWxPayService { |
| | | |
| | | private final String MINI_PROGRAM_APP_ID = "wx41aa8a5d2e565a05"; |
| | | private static final Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class); |
| | | |
| | | private final ReentrantLock lock = new ReentrantLock(); |
| | | |
| | | @Override |
| | | public Object getOpenId(String code) { |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, String> jsapiPay(Long productId) throws IOException { |
| | | |
| | | String prepayId = ""; |
| | | //生成订单 |
| | | // OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId); |
| | | |
| | | |
| | | 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", "oSX3G6OUuGaKJbWzSHUpPbzgtDXo"); |
| | | // |
| | | // paramsMap.put("payer", payerMap); |
| | | // |
| | | // //将参数转换成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 = 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<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); |
| | | // //预支付交易会话标识 |
| | | // prepayId = resultMap.get("prepay_id"); |
| | | // |
| | | // //返回预支付交易会话标识 |
| | | // // 组装前端预下单参数 |
| | | // /** 返回给前端所需要的数据 */ |
| | | // SortedMap<String, String> 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; |
| | | // } finally { |
| | | // response.close(); |
| | | // } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void processOrder(Map<String, Object> 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(); |
| | | // } |
| | | // } |
| | | } |
| | | |
| | | /** |
| | | * 通过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 bodyMap |
| | | * @return |
| | | */ |
| | | private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException { |
| | | |
| | | logger.info("密文解密"); |
| | | |
| | | //通知数据 |
| | | Map<String, String> 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; |
| | | return null; |
| | | } |
| | | |
| | | } |