|
@@ -0,0 +1,478 @@
|
|
|
+package com.bosshand.virgo.api.workark.service;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.bosshand.virgo.api.workark.config.WxPayConfig;
|
|
|
+import com.bosshand.virgo.api.workark.enums.OrderStatus;
|
|
|
+import com.bosshand.virgo.api.workark.enums.wxpay.WxNotifyType;
|
|
|
+import com.bosshand.virgo.api.workark.enums.wxpay.WxRefundStatus;
|
|
|
+import com.bosshand.virgo.api.workark.enums.wxpay.WxTradeState;
|
|
|
+import com.bosshand.virgo.api.workark.model.OrderInfo;
|
|
|
+import com.bosshand.virgo.api.workark.model.RefundInfo;
|
|
|
+import com.bosshand.virgo.api.workark.util.AesUtil;
|
|
|
+import com.bosshand.virgo.api.workark.util.QrRcodeGenUtil;
|
|
|
+import com.google.gson.Gson;
|
|
|
+import com.wechat.pay.java.core.Config;
|
|
|
+import com.wechat.pay.java.core.RSAPublicKeyConfig;
|
|
|
+import com.wechat.pay.java.service.payments.model.Transaction;
|
|
|
+import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
|
|
+import com.wechat.pay.java.service.payments.nativepay.model.*;
|
|
|
+import com.wechat.pay.java.service.refund.RefundService;
|
|
|
+import com.wechat.pay.java.service.refund.model.AmountReq;
|
|
|
+import com.wechat.pay.java.service.refund.model.CreateRequest;
|
|
|
+import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
|
|
|
+import com.wechat.pay.java.service.refund.model.Refund;
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.core.io.ClassPathResource;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.GeneralSecurityException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.locks.ReentrantLock;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class WxPayService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private WxPayConfig wxPayConfig;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private OrderInfoService orderInfoService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PaymentInfoService paymentInfoService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RefundInfoService refundInfoService;
|
|
|
+
|
|
|
+ private final ReentrantLock lock = new ReentrantLock();
|
|
|
+
|
|
|
+ private final static Log log = LogFactory.getLog(WxPayService.class);
|
|
|
+
|
|
|
+ public String getFileContent(String filePath) {
|
|
|
+ try {
|
|
|
+ ClassPathResource resource = new ClassPathResource(filePath);
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
|
|
|
+ StringBuilder content = new StringBuilder();
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ content.append(line).append("\n");
|
|
|
+ }
|
|
|
+ return content.toString();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("Error reading file: " + filePath, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Config getConfig(){
|
|
|
+ // 初始化商户配置
|
|
|
+ Config config =
|
|
|
+ new RSAPublicKeyConfig.Builder()
|
|
|
+ .merchantId(wxPayConfig.getMchId())
|
|
|
+ .privateKey(getFileContent(wxPayConfig.getPrivateKeyPath()))
|
|
|
+ .publicKey(getFileContent(wxPayConfig.getPublicKeyPath()))
|
|
|
+ .publicKeyId(wxPayConfig.getPublicKeyId())
|
|
|
+ .merchantSerialNumber(wxPayConfig.getMchSerialNo())
|
|
|
+ .apiV3Key(wxPayConfig.getApiV3Key())
|
|
|
+ .build();
|
|
|
+ return config;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建订单,调用Native支付接口
|
|
|
+ * @param orderNo
|
|
|
+ * @return code_url 和 订单号
|
|
|
+ * @throws Exception
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public Map<String, Object> nativePay(String orderNo) throws Exception {
|
|
|
+
|
|
|
+ log.info("获取订单");
|
|
|
+
|
|
|
+ // 生成订单
|
|
|
+ OrderInfo orderInfo = orderInfoService.getOrderNo(orderNo);
|
|
|
+ String codeUrl = orderInfo.getCodeUrl();
|
|
|
+ if (orderInfo != null && !StringUtils.isEmpty(codeUrl)) {
|
|
|
+ log.info("订单已存在,二维码已保存");
|
|
|
+ //返回二维码
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put("codeUrl", codeUrl);
|
|
|
+ map.put("orderNo", orderInfo.getOrderNo());
|
|
|
+ map.put("base64", QrRcodeGenUtil.jumpToQRcodeGen(codeUrl));
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("调用统一下单API");
|
|
|
+
|
|
|
+ // 初始化服务
|
|
|
+ NativePayService service = new NativePayService.Builder().config(getConfig()).build();
|
|
|
+
|
|
|
+ PrepayRequest request = new PrepayRequest();
|
|
|
+ request.setAppid(wxPayConfig.getAppid());
|
|
|
+ request.setMchid(wxPayConfig.getMchId());
|
|
|
+ request.setDescription(orderInfo.getTitle());
|
|
|
+ request.setNotifyUrl(wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
|
|
|
+ request.setOutTradeNo(orderInfo.getOrderNo());
|
|
|
+
|
|
|
+ // 元 转换 分
|
|
|
+ BigDecimal multiplier = new BigDecimal("100");
|
|
|
+ // 使用multiply方法乘以100
|
|
|
+ BigDecimal result = orderInfo.getTotalFee().multiply(multiplier);
|
|
|
+
|
|
|
+ Amount amount = new Amount();
|
|
|
+ amount.setTotal(result.intValue());
|
|
|
+ amount.setCurrency("CNY");
|
|
|
+ request.setAmount(amount);
|
|
|
+
|
|
|
+ // 调用下单方法,得到应答
|
|
|
+ PrepayResponse response = service.prepay(request);
|
|
|
+
|
|
|
+ // 使用微信扫描 code_url 对应的二维码
|
|
|
+ codeUrl = response.getCodeUrl();
|
|
|
+
|
|
|
+ //保存二维码
|
|
|
+ orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);
|
|
|
+
|
|
|
+ //返回二维码
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put("codeUrl", codeUrl);
|
|
|
+ map.put("orderNo", orderInfo.getOrderNo());
|
|
|
+ map.put("base64", QrRcodeGenUtil.jumpToQRcodeGen(codeUrl));
|
|
|
+ return map;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关单接口的调用
|
|
|
+ * @param orderNo
|
|
|
+ */
|
|
|
+ private void closeOrder(String orderNo) throws Exception {
|
|
|
+
|
|
|
+ log.info("关单接口的调用,订单号 ===>"+ orderNo);
|
|
|
+
|
|
|
+ NativePayService service = new NativePayService.Builder().config(getConfig()).build();
|
|
|
+
|
|
|
+ CloseOrderRequest request = new CloseOrderRequest();
|
|
|
+
|
|
|
+ request.setMchid(wxPayConfig.getMchId());
|
|
|
+ request.setOutTradeNo(orderNo);
|
|
|
+
|
|
|
+ service.closeOrder(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对称解密
|
|
|
+ * @param bodyMap
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
|
|
|
+
|
|
|
+ log.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");
|
|
|
+
|
|
|
+ log.info("associatedData ===>"+ associatedData);
|
|
|
+
|
|
|
+ log.info("密文 ===>"+ ciphertext);
|
|
|
+ AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
|
|
|
+ String plainText = null;
|
|
|
+ try {
|
|
|
+ plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("明文 ===>"+ plainText);
|
|
|
+
|
|
|
+ return plainText;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用户取消订单
|
|
|
+ * @param orderNo
|
|
|
+ */
|
|
|
+ public void cancelOrder(String orderNo) throws Exception {
|
|
|
+ //调用微信支付的关单接口
|
|
|
+ this.closeOrder(orderNo);
|
|
|
+ //更新商户端的订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String queryOrder(String orderNo) throws Exception {
|
|
|
+
|
|
|
+ log.info("查单接口调用 ===>"+ orderNo);
|
|
|
+
|
|
|
+ NativePayService service = new NativePayService.Builder().config(getConfig()).build();
|
|
|
+
|
|
|
+ QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
|
|
|
+ request.setMchid(wxPayConfig.getMchId());
|
|
|
+ request.setOutTradeNo(orderNo);
|
|
|
+
|
|
|
+ Transaction transaction = service.queryOrderByOutTradeNo(request);
|
|
|
+
|
|
|
+ return JSONObject.toJSONString(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退款
|
|
|
+ * @param orderNo
|
|
|
+ * @param reason
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void refund(String orderNo, String reason) throws Exception {
|
|
|
+
|
|
|
+ log.info("创建退款单记录");
|
|
|
+ //根据订单编号创建退款单
|
|
|
+ RefundInfo refundsInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
|
|
|
+
|
|
|
+ log.info("调用退款API");
|
|
|
+
|
|
|
+ RefundService service = new RefundService.Builder().config(getConfig()).build();
|
|
|
+
|
|
|
+ CreateRequest request = new CreateRequest();
|
|
|
+
|
|
|
+ request.setOutTradeNo(orderNo);//订单编号
|
|
|
+ request.setOutRefundNo(refundsInfo.getRefundNo());//退款单编号
|
|
|
+ request.setReason(reason);//退款原因
|
|
|
+ request.setNotifyUrl(wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
|
|
|
+
|
|
|
+ // 元 转换 分
|
|
|
+ BigDecimal multiplier = new BigDecimal("100");
|
|
|
+ // 使用multiply方法乘以100
|
|
|
+ BigDecimal refund = refundsInfo.getRefund().multiply(multiplier);
|
|
|
+ BigDecimal totalFee = refundsInfo.getTotalFee().multiply(multiplier);
|
|
|
+
|
|
|
+ AmountReq amount = new AmountReq();
|
|
|
+ amount.setCurrency("CNY");
|
|
|
+ amount.setRefund(refund.longValue());//退款金额
|
|
|
+ amount.setTotal(totalFee.longValue());//原订单金额
|
|
|
+
|
|
|
+ request.setAmount(amount);
|
|
|
+
|
|
|
+ Refund refund1 = service.create(request);
|
|
|
+
|
|
|
+ //更新订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
|
|
|
+
|
|
|
+ //更新退款单
|
|
|
+ refundInfoService.updateRefund(JSONObject.toJSONString(refund1));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询退款接口调用
|
|
|
+ * @param refundNo
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public String queryRefund(String refundNo) throws Exception {
|
|
|
+
|
|
|
+ log.info("查询退款接口调用 ===>"+ refundNo);
|
|
|
+
|
|
|
+ RefundService service = new RefundService.Builder().config(getConfig()).build();
|
|
|
+
|
|
|
+ QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
|
|
|
+
|
|
|
+ request.setOutRefundNo(refundNo);
|
|
|
+ request.setSubMchid(wxPayConfig.getMchId());
|
|
|
+
|
|
|
+ Refund refund = service.queryByOutRefundNo(request);
|
|
|
+
|
|
|
+ return JSONObject.toJSONString(refund);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理退款单
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void processRefund(Map<String, Object> bodyMap) throws Exception {
|
|
|
+
|
|
|
+ log.info("退款单");
|
|
|
+
|
|
|
+ //解密报文
|
|
|
+ String plainText = decryptFromResource(bodyMap);
|
|
|
+
|
|
|
+ //将明文转换成map
|
|
|
+ Gson gson = new Gson();
|
|
|
+ HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
|
|
|
+ String orderNo = (String)plainTextMap.get("outTradeNo");
|
|
|
+
|
|
|
+ if(lock.tryLock()){
|
|
|
+ try {
|
|
|
+
|
|
|
+ String orderStatus = orderInfoService.getOrderStatus(orderNo);
|
|
|
+ if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ //更新订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
|
|
|
+
|
|
|
+ //更新退款单
|
|
|
+ refundInfoService.updateRefund(plainText);
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ //要主动释放锁
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据订单号查询微信支付查单接口,核实订单状态
|
|
|
+ * 如果订单已支付,则更新商户端订单状态,并记录支付日志
|
|
|
+ * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
|
|
|
+ * @param orderNo
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void checkOrderStatus(String orderNo) throws Exception {
|
|
|
+
|
|
|
+ log.warn("根据订单号核实订单状态 ===>"+ orderNo);
|
|
|
+
|
|
|
+ //调用微信支付查单接口
|
|
|
+ String result = this.queryOrder(orderNo);
|
|
|
+
|
|
|
+ Gson gson = new Gson();
|
|
|
+ Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
|
|
|
+
|
|
|
+ //获取微信支付端的订单状态
|
|
|
+ String tradeState = resultMap.get("tradeState");
|
|
|
+
|
|
|
+ //判断订单状态
|
|
|
+ if(WxTradeState.SUCCESS.getType().equals(tradeState)){
|
|
|
+
|
|
|
+ log.warn("核实订单已支付 ===>"+ orderNo);
|
|
|
+
|
|
|
+ //如果确认订单已支付则更新本地订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
|
|
|
+ //记录支付日志
|
|
|
+ paymentInfoService.createPaymentInfo(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ if(WxTradeState.NOTPAY.getType().equals(tradeState)){
|
|
|
+ log.warn("核实订单未支付 ===>"+ orderNo);
|
|
|
+
|
|
|
+ //如果订单未支付,则调用关单接口
|
|
|
+ this.closeOrder(orderNo);
|
|
|
+
|
|
|
+ //更新本地订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据退款单号核实退款单状态
|
|
|
+ * @param refundNo
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void checkRefundStatus(String refundNo) throws Exception {
|
|
|
+
|
|
|
+ log.warn("根据退款单号核实退款单状态 ===>"+ refundNo);
|
|
|
+
|
|
|
+ //调用查询退款单接口
|
|
|
+ String result = this.queryRefund(refundNo);
|
|
|
+
|
|
|
+ //组装json请求体字符串
|
|
|
+ Gson gson = new Gson();
|
|
|
+ Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
|
|
|
+
|
|
|
+ //获取微信支付端退款状态
|
|
|
+ String status = resultMap.get("status");
|
|
|
+
|
|
|
+ String orderNo = resultMap.get("outTradeNo");
|
|
|
+
|
|
|
+ if (WxRefundStatus.SUCCESS.getType().equals(status)) {
|
|
|
+
|
|
|
+ log.warn("核实订单已退款成功 ===>"+ refundNo);
|
|
|
+
|
|
|
+ //如果确认退款成功,则更新订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
|
|
|
+
|
|
|
+ //更新退款单
|
|
|
+ refundInfoService.updateRefund(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
|
|
|
+
|
|
|
+ log.warn("核实订单退款异常 ===>"+ refundNo);
|
|
|
+
|
|
|
+ //如果确认退款成功,则更新订单状态
|
|
|
+ orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
|
|
|
+
|
|
|
+ //更新退款单
|
|
|
+ refundInfoService.updateRefund(result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
|
|
|
+ log.info("处理订单");
|
|
|
+
|
|
|
+ //解密报文
|
|
|
+ String plainText = decryptFromResource(bodyMap);
|
|
|
+
|
|
|
+ log.info("处理订单");
|
|
|
+
|
|
|
+ //将明文转换成map
|
|
|
+ Gson gson = new Gson();
|
|
|
+ HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
|
|
|
+ String orderNo = (String)plainTextMap.get("outTradeNo");
|
|
|
+
|
|
|
+
|
|
|
+ /*在对业务数据进行状态检查和处理之前,
|
|
|
+ 要采用数据锁进行并发控制,
|
|
|
+ 以避免函数重入造成的数据混乱*/
|
|
|
+ //尝试获取锁:
|
|
|
+ // 成功获取则立即返回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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+}
|