package com.pay.wxpayback.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.text.StrFormatter; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSON; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.pay.wxpayback.Enum.WxApiType; import com.pay.wxpayback.Enum.WxPayStatusEnum; import com.pay.wxpayback.constant.SystemConstant; import com.pay.wxpayback.constant.wxpay.WXOrderConstant; import com.pay.wxpayback.constant.wxpay.WechatPayHttpHeaders; import com.pay.wxpayback.exception.ApiException; import com.pay.wxpayback.pojo.Order; import com.pay.wxpayback.mapper.OrderMapper; import com.pay.wxpayback.pojo.ToolWxConfig; import com.pay.wxpayback.pojo.vo.ReCreateOrderVO; import com.pay.wxpayback.pojo.vo.ToCreateOrderVO; import com.pay.wxpayback.pojo.vo.WxLoginVO; import com.pay.wxpayback.service.OrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.pay.wxpayback.utils.WxPayUtil; import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; import lombok.extern.slf4j.Slf4j; 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.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; /** *

* 订单表 服务实现类 *

* * @author 小王八 * @since 2023-07-28 */ @Service @Slf4j public class OrderServiceImpl extends ServiceImpl implements OrderService { private final ReentrantLock reentrantLock1 = new ReentrantLock(); private final ReentrantLock reentrantLock2 = new ReentrantLock(); /** * 调用微信支付 get请求 统一配置 *要微信签名认证 * @param url * @return String * @author zhangjunrong * @date 2022/5/9 20:39 */ private String getHttpGet(ToolWxConfig wxConfig, String url) throws URISyntaxException, IOException { //1.构造httpGet请求 URIBuilder uriBuilder = null; uriBuilder = new URIBuilder(url); HttpGet httpGet = new HttpGet(uriBuilder.build()); httpGet.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON); //2.调起微信查询订单接口 CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpGet); //3.返回结果信息 return EntityUtils.toString(response.getEntity()); } public WxLoginVO getOpenId(ToolWxConfig wxConfig, WxLoginVO loginVO){ //获取当前的openid Map hashMap = new HashMap(); hashMap.put("appid", wxConfig.getAppId()); hashMap.put("secret", wxConfig.getAppSecret()); hashMap.put("js_code", loginVO.getCode()); hashMap.put("grant_type", "authorization_code"); String json = HttpUtil.get(WxApiType.WX_LOGIN_URL.getValue(), hashMap); JSONObject jsonObject = JSONUtil.parseObj(json); String openid = jsonObject.getStr("openid"); String sessionKey = jsonObject.getStr("session_key"); WxLoginVO result = new WxLoginVO(); result.setOpenId(openid); result.setSession_key(sessionKey); return result; } @Override public ReCreateOrderVO createOrder(ToolWxConfig wxConfig, ToCreateOrderVO toCreatOrderVO) { try { //1.请求配置参数 HttpPost httpPost = new HttpPost(WxApiType.CREATE_ORDER.getValue()); //格式配置 httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON); httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF); //2.读取privateKey私钥 PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(wxConfig.getPrivateKey().getBytes(StandardCharsets.UTF_8))); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectMapper objectMapper = new ObjectMapper(); //3.配置下单参数 ObjectNode rootNode = objectMapper.createObjectNode(); rootNode.put(WXOrderConstant.MCHID, wxConfig.getMchId()) .put(WXOrderConstant.APPID, wxConfig.getAppId()) .put(WXOrderConstant.DESCRIPTION, toCreatOrderVO.getDescription()) .put(WXOrderConstant.NOTIFY_URL, SystemConstant.PAY_NOTIFY_URL) .put(WXOrderConstant.OUT_TRADE_NO, toCreatOrderVO.getOutTradeNo()); rootNode.putObject(WXOrderConstant.AMOUNT) //total 微信需要int类型 为了不丢失精度 单位为分 .put(WXOrderConstant.AMOUNT_TOTAL, toCreatOrderVO.getTotal()); // 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid rootNode.putObject(WXOrderConstant.PAYER) .put(WXOrderConstant.OPENID, toCreatOrderVO.getOpenId()); objectMapper.writeValue(bos, rootNode); //4.调用微信下单接口 httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8")); //接口返回值 申请获取prepay_id CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost); //预支付交易会话标识 prepay_id String bodyAsString = EntityUtils.toString(response.getEntity()); //5.APP调起支付API 构造签名串 String timestamp = DateUtil.currentSeconds() + ""; String nonce = RandomUtil.randomString(SystemConstant.NUM_32); StringBuilder builder = new StringBuilder(); //应用id builder.append(wxConfig.getAppId()).append("\n"); //时间戳 builder.append(timestamp).append("\n"); //随机字符串 builder.append(nonce).append("\n"); //预支付交易会话标识 prepay_id 通过bodyAsString获取 prepay_id JsonNode node = objectMapper.readTree(bodyAsString); // prepay_id=wx201410272009395522657a690389285100 必须以此格式 String wxPackage = SystemConstant.WX_PACKAGE + node.get(WXOrderConstant.PREPAY_ID).textValue(); builder.append(wxPackage).append("\n"); log.info("微信签名请求体: {}", builder.toString()); //6.计算签名 String sign = WxPayUtil.sign(builder.toString().getBytes(StandardCharsets.UTF_8), merchantPrivateKey); //7.返回参数 让前端调微信支付 return new ReCreateOrderVO(timestamp, nonce, wxPackage, "RSA", sign,toCreatOrderVO.getOutTradeNo()); } catch (Exception e) { log.error(toCreatOrderVO.getOutTradeNo() + "订单下单失败"); log.error(e.toString()); //下单失败抛异常 throw new ApiException("下单失败 重新尝试付款"); } } Integer queryAmountByOrderNo(String orderNo) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("amount").eq("pk_id",orderNo); return getOne(wrapper).getAmount(); } void updateStatus(Integer status,String pkId) { UpdateWrapper wrapper = new UpdateWrapper<>(); wrapper.set("pay_status",status).eq("pk_id",pkId); update(wrapper); } Integer getStatus(String pkId) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("pay_status").eq("pk_id",pkId); return getOne(wrapper).getPayStatus(); } @Override public Boolean verifyCreateOrder(String decryptOrder) { //在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 //实现: 加入一把可重入锁 if (reentrantLock1.tryLock()) { try { log.info("===================================进入微信支付回调核对订单中========================================"); ObjectMapper objectMapper = new ObjectMapper(); //微信回调 解密后 信息 JsonNode node = objectMapper.readTree(decryptOrder); //获取订单商户号 String orderNo = node.get(WXOrderConstant.OUT_TRADE_NO).textValue(); log.info(node.get(WXOrderConstant.OUT_TRADE_NO) + "订单回调信息记录:订单状态:" + orderNo); //2.如果回调 支付类型为成功 核对金额 入数据库 //获取支付状态 String tradeState = node.get(WXOrderConstant.TRADE_STATE).textValue(); if (StrUtil.equals(WXOrderConstant.WX_BACK_OK, tradeState)) { //1.查询数据库 对比支付金额 if(WxPayUtil.verifyMoney(node, queryAmountByOrderNo(orderNo))) { //2.对比成功 账单状态 已支付 updateStatus(SystemConstant.NUM_ONE,orderNo); } //支付成功 就返回true return true; } } catch (Exception e) { log.error("订单支付异常===>订单回调信息记录:订单状态:" + decryptOrder); }finally { //释放锁 reentrantLock1.unlock(); } } return false; } @Override public void closeOrder(ToolWxConfig wxConfig, String outTradeNo) { try { //1.微信接口编辑 String url = StrFormatter.format(WxApiType.CLOSE_ORDER.getValue(), outTradeNo); HttpPost httpPost = new HttpPost(url); //格式配置 httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON); httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF); ByteArrayOutputStream bos = new ByteArrayOutputStream(); //2.添加商户id ObjectMapper objectMapper = new ObjectMapper(); ObjectNode rootNode = objectMapper.createObjectNode(); rootNode.put(WXOrderConstant.MCHID, wxConfig.getMchId()); objectMapper.writeValue(bos, rootNode); //3.调起微信关单接口 httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8")); CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost); log.info("=====关单结果======={}====",response); } catch (Exception e) { log.error("关单失败" + outTradeNo + e); } } @Override public String refundOrder(ToolWxConfig wxConfig,Order order) { try { //1.请求配置参数 HttpPost httpPost = new HttpPost(WxApiType.REFUND_ORDER.getValue()); //格式配置 httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON); httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectMapper objectMapper = new ObjectMapper(); ObjectNode rootNode = objectMapper.createObjectNode(); //2.配置参数 订单商户号 退款订单号 微信订单号(微信生成) 退款回调地址(与下单回调地址不一样) 金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分) 退款币种 CNY 人民币 //商户订单号 下单时生成 rootNode.put(WXOrderConstant.OUT_TRADE_NO, order.getPkId().toString()) //微信支付系统生成的订单号 下单时生成(系统生成) .put(WXOrderConstant.OUT_REFUND_NO, order.getPkId().toString()) //退款回调地址 .put(WXOrderConstant.NOTIFY_URL, SystemConstant.REFUND_NOTIFY_URL); //金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分) rootNode.putObject(WXOrderConstant.AMOUNT) //现阶段 total==refund 不支持部分退款 //原订单金额 total .put(WXOrderConstant.AMOUNT_TOTAL, order.getAmount()) //退款金额 refund .put(WXOrderConstant.AMOUNT_REFUND, order.getAmount()) //退款币种 CNY 人民币 .put(WXOrderConstant.AMOUNT_CURRENCY, SystemConstant.CURRENCY_RMB); objectMapper.writeValue(bos, rootNode); //3.调用微信退款接口 httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8")); //接口返回值 CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost); String bodyAsString = EntityUtils.toString(response.getEntity()); log.info("微信申请退款返回结果" + "response:" + bodyAsString); JsonNode refundNode = objectMapper.readTree(bodyAsString); // 修改订单退款状态 微信退款入数据库 String status = refundNode.get(WXOrderConstant.STATUS).textValue(); log.info("微信方=====退款状态====={}======",status); Integer statusCode = WxPayStatusEnum.getCode(status); updateStatus(statusCode,order.getPkId().toString()); return "退款申请成功 注意退款查收"; } catch (Exception e) { log.info("微信退款" +order.getPkId() + "失败"); throw new ApiException("退款失败,请联系客服解决"); } } @Override public Boolean updateRefundOrder(String decryptOrder) { //在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 //实现: 加入一把可重入锁 if (reentrantLock2.tryLock()) { try { if (ObjectUtil.isNotEmpty(decryptOrder)) { ObjectMapper objectMapper = new ObjectMapper(); //string=>json 方便获取数据 JsonNode jsonNode = objectMapper.readTree(decryptOrder); log.info("微信退款回调参数===================={}====================", decryptOrder); //获取退款单编号(系统生成) String outRefundNo = jsonNode.get(WXOrderConstant.OUT_REFUND_NO).textValue(); String refundStatus = jsonNode.get(WXOrderConstant.REFUND_STATUS).textValue(); //数据库核对 如果退款状态已退款说明已处理 直接返回即可 if (getStatus(outRefundNo) == SystemConstant.NUM_THREE) { return true; } Integer statusCode = WxPayStatusEnum.getCode(refundStatus); log.info("微信退款回调 退款订单编号(系统生成)============{}===========订单状态======{}", outRefundNo, refundStatus); //1.退款表状态修改 微信退款id(微信生成) 退款 调起退款则给用户退款 不根据本系统回调反馈 所以修改状态不成功 依旧返回true updateStatus(statusCode,outRefundNo); return true; } } catch (Exception e) { log.warn("微信退款回调接口====修改订单状态失败" + e); } finally { //释放锁 reentrantLock2.unlock(); } } return false; } @Override public String queryCreateOrder(ToolWxConfig wxConfig, String outTradeNo) { try { //1.查单 微信接口 编辑 微信订单号 + 商户号 String url = StrFormatter.format(WxApiType.QUERY_CREATE_ORDER.getValue(), outTradeNo, wxConfig.getMchId()); //2.调用微信接口 String bodyAsString = getHttpGet(wxConfig, url); log.info("支付查单信息" + bodyAsString); //返回查单结果信息 if (ObjectUtil.isNotEmpty(bodyAsString)){ ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(bodyAsString); return jsonNode.get(WXOrderConstant.TRADE_STATE).textValue(); } } catch (Exception e) { log.error("支付查单失败" + outTradeNo); } return null; } @Override public String queryRefundOrder(ToolWxConfig wxConfig, String refundNo) { try { //1.查单 微信接口 拼接url String url = StrFormatter.format(WxApiType.QUERY_REFUND_ORDER.getValue(), refundNo); //2.调用微信接口 String bodyAsString = getHttpGet(wxConfig, url); log.info("退单查询信息============{}========" + bodyAsString); //返回查单结果信息 if (ObjectUtil.isNotEmpty(bodyAsString)){ ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(bodyAsString); return jsonNode.get(WXOrderConstant.STATUS).textValue(); } } catch (Exception e) { log.error("退单查单失败" + refundNo); } return null; } }