|
@@ -1,5 +1,6 @@
|
|
|
package org.jeecg.modules.app.service;
|
|
|
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
import com.alibaba.fastjson2.JSONArray;
|
|
@@ -8,22 +9,36 @@ import com.aliyun.oss.ServiceException;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.RandomStringUtils;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.jeecg.common.constant.CommonConstant;
|
|
|
import org.jeecg.modules.pay.config.WechatConstants;
|
|
|
import org.jeecg.modules.pay.config.WechatPayV3Utils;
|
|
|
import org.jeecg.modules.pay.config.WechatUrlConstants;
|
|
|
+import org.jeecg.modules.pay.serverPay.WXPayUtility;
|
|
|
import org.jeecg.modules.system.app.entity.AppOrder;
|
|
|
+import org.jeecg.modules.system.app.entity.AppOrderProInfo;
|
|
|
import org.jeecg.modules.system.app.mapper.AppOrderMapper;
|
|
|
+import org.jeecg.modules.system.app.mapper.AppOrderProInfoMapper;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.NoSuchPaddingException;
|
|
|
+import javax.crypto.spec.GCMParameterSpec;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.io.IOException;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.GeneralSecurityException;
|
|
|
+import java.security.InvalidAlgorithmParameterException;
|
|
|
+import java.security.InvalidKeyException;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
import java.text.DateFormat;
|
|
|
import java.text.ParseException;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.Map;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.ThreadLocalRandom;
|
|
|
|
|
|
/**
|
|
|
* @author wangzhiqiang
|
|
@@ -40,6 +55,9 @@ public class WeChatPayService {
|
|
|
@Resource
|
|
|
private AppOrderMapper appOrderMapper;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private AppOrderProInfoMapper appOrderProInfoMapper;
|
|
|
+
|
|
|
|
|
|
/**
|
|
|
* 小程序支付拉起
|
|
@@ -60,7 +78,7 @@ public class WeChatPayService {
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
//返回给小程序拉起微信支付的参数
|
|
|
Map<String, String> result = new HashMap<>();
|
|
|
- result.put("appId", WechatConstants.WECHAT_SP_APPID); //小程序appid
|
|
|
+ result.put("appId", WechatConstants.WECHAT_SUB_APPID); //小程序appid
|
|
|
sb.append(result.get("appId")).append("\n");
|
|
|
result.put("timeStamp", (new Date().getTime() / 1000) + ""); //时间戳
|
|
|
sb.append(result.get("timeStamp")).append("\n");
|
|
@@ -178,9 +196,25 @@ public class WeChatPayService {
|
|
|
// 处理支付成功后的业务 例如 将订单状态修改为已支付 具体参数键值可参考文档 注意!!! 微信可能会多次发送重复的通知 因此要判断业务是否已经处理过了 避免重复处理
|
|
|
try {
|
|
|
String orderCode = res.getString("out_trade_no");
|
|
|
- //查询订单,判断是否已修改未已支付状态
|
|
|
+ //查询订单,判断是否已修改为已支付状态
|
|
|
AppOrder appOrder = appOrderMapper.selectOne(Wrappers.<AppOrder>lambdaQuery().eq(AppOrder::getOrderCode, orderCode).last("limit 1"));
|
|
|
-
|
|
|
+ if (ObjectUtil.isNotEmpty(appOrder)) {
|
|
|
+ if (Objects.equals(appOrder.getOrderStatus(), CommonConstant.ORDER_STATUS_0)){
|
|
|
+ appOrder.setOrderStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ appOrder.setPayStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ appOrder.setPayTime(new Date());
|
|
|
+ appOrder.setPayType(CommonConstant.STATUS_0_INT);
|
|
|
+ appOrder.setCallbackStatus(CommonConstant.STATUS_1_INT);
|
|
|
+ appOrderMapper.updateById(appOrder);
|
|
|
+ List<AppOrderProInfo> proInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, appOrder.getId()));
|
|
|
+ if (ObjectUtil.isNotEmpty(proInfoList)){
|
|
|
+ for (AppOrderProInfo appOrderProInfo : proInfoList) {
|
|
|
+ appOrderProInfo.setOrderStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ appOrderProInfoMapper.updateById(appOrderProInfo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
result.put("code", "SUCCESS");
|
|
|
result.put("message", "OK");
|
|
|
result.put("orderCode",orderCode);
|
|
@@ -190,7 +224,6 @@ public class WeChatPayService {
|
|
|
result.put("message", "失败");
|
|
|
return result;
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -200,8 +233,165 @@ public class WeChatPayService {
|
|
|
* @return null代表查询失败 SUCCESS-成功 USERPAYING和ACCEPT为中间态 其他为支付失败
|
|
|
*/
|
|
|
public String orderQueryByOutTradeNo(String out_trade_no) {
|
|
|
- JSONObject res = wechatPayV3Utils.sendGet(String.format(WechatUrlConstants.PAY_V3_QUERY_OUT, out_trade_no, WechatConstants.WECHAT_SP_APPID));
|
|
|
+ String url = WechatUrlConstants.PAY_V3_QUERY_OUT;
|
|
|
+ url = url.replace("{out_trade_no}", WXPayUtility.urlEncode(out_trade_no));
|
|
|
+ Map<String, Object> args = new HashMap<>();
|
|
|
+ args.put("sp_mchid", WechatConstants.WECHAT_SP_MCH_ID);
|
|
|
+ args.put("sub_mchid", WechatConstants.WECHAT_SUB_MCH_ID);
|
|
|
+ url = url + "?" + WXPayUtility.urlEncode(args);
|
|
|
+ JSONObject res = wechatPayV3Utils.sendGet(url);
|
|
|
return res == null ? null : res.getString("trade_state");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 申请退款
|
|
|
+ * @param orderCode 原订单号
|
|
|
+ */
|
|
|
+ public void refundOrder(String orderCode,String reason){
|
|
|
+
|
|
|
+ log.info("进入退款接口------>");
|
|
|
+ log.info("执行操作的 原支付交易对应的商户订单号:{}", orderCode);
|
|
|
+
|
|
|
+ Integer totalFee = 1;
|
|
|
+ Integer total = 1;
|
|
|
+ AppOrder appOrder = appOrderMapper.selectOne(Wrappers.<AppOrder>lambdaQuery().eq(AppOrder::getOrderCode, orderCode).last("limit 1"));
|
|
|
+ if (ObjectUtil.isNotEmpty(appOrder)) {
|
|
|
+ total = appOrder.getPrice().intValue();
|
|
|
+ totalFee = appOrder.getPrice().intValue();
|
|
|
+ }
|
|
|
+ //退款单号
|
|
|
+ String out_refund_no = generateOrderNumber(1);
|
|
|
+
|
|
|
+ //todo 创建退款订单
|
|
|
+
|
|
|
+
|
|
|
+ //todo 发起分账回退
|
|
|
+
|
|
|
+ try {
|
|
|
+ JSONObject params = new JSONObject();
|
|
|
+ params.put("out_trade_no", orderCode);//商户订单号
|
|
|
+ params.put("out_refund_no", out_refund_no);//商户退款单号
|
|
|
+ params.put("reason", reason);//退款原因
|
|
|
+ params.put("notify_url", WechatUrlConstants.PAY_V3_REFUND_NOTIFY);//退款通知
|
|
|
+
|
|
|
+ JSONObject amount = new JSONObject();
|
|
|
+ amount.put("refund", (long) (totalFee * 100));//退款金额
|
|
|
+ amount.put("currency", "CNY");
|
|
|
+ amount.put("total", (long) (total * 100));//原订单金额
|
|
|
+ params.put("amount", amount);
|
|
|
+ // 执行请求POST 请求发送到微信退款接口
|
|
|
+ JSONObject res = wechatPayV3Utils.sendPost(WechatUrlConstants.PAY_V3_REFUND, params);
|
|
|
+
|
|
|
+ log.info("最终拿到的微信支付通知数据:" + res);
|
|
|
+
|
|
|
+ final String status = res.getString("status");
|
|
|
+ switch (status) {
|
|
|
+ case "SUCCESS":
|
|
|
+ log.info("退款成功");
|
|
|
+ break;
|
|
|
+ case "CLOSED":
|
|
|
+ log.info("退款关闭");
|
|
|
+ break;
|
|
|
+ case "PROCESSING":
|
|
|
+ log.info("退款处理中");
|
|
|
+
|
|
|
+ break;
|
|
|
+ case "ABNORMAL":
|
|
|
+ log.info("退款异常");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // TODO Auto-generated catch block
|
|
|
+ log.info(e.toString());
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return String 订单号
|
|
|
+ * @Author SheepHy
|
|
|
+ * @Description 订单编号生成逻辑
|
|
|
+ * @Date 17:18 2025/7/15
|
|
|
+ * @params 类型:0-(D:订单) 1-(T:退单) 2-(B:保单)
|
|
|
+ **/
|
|
|
+ private String generateOrderNumber(int type) {
|
|
|
+ String format = DateUtil.format(new Date(), "yyyyMMddHHmmss");
|
|
|
+ int nextInt = ThreadLocalRandom.current().nextInt(1000, 10000);
|
|
|
+
|
|
|
+ if (type == 0) {
|
|
|
+ return "D" + format + nextInt;
|
|
|
+ } else {
|
|
|
+ return "T" + format + nextInt;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> refundOrderNotify(String jsonData) throws IOException, GeneralSecurityException {
|
|
|
+
|
|
|
+ //转为map格式
|
|
|
+ Map<String, String> jsonMap = JSONObject.parseObject(jsonData, Map.class);
|
|
|
+
|
|
|
+ //退款成功后返回一个加密字段resource,以下为解密
|
|
|
+ /**
|
|
|
+ * 解密需要从resource参数中,获取到ciphertext,nonce,associated_data这三个参数进行解密
|
|
|
+ */
|
|
|
+ String resource = JSONObject.toJSONString(jsonMap.get("resource"));
|
|
|
+ JSONObject object = JSONObject.parseObject(resource);
|
|
|
+
|
|
|
+ String ciphertext = String.valueOf(object.get("ciphertext"));
|
|
|
+ String nonce = String.valueOf(object.get("nonce"));
|
|
|
+ String associated_data = String.valueOf(object.get("associated_data"));
|
|
|
+
|
|
|
+ String resultStr = decryptToString(associated_data.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
|
|
|
+ Map<String, String> reqInfo = JSONObject.parseObject(resultStr, Map.class);
|
|
|
+
|
|
|
+ String refund_status = reqInfo.get("refund_status");//退款状态
|
|
|
+ String out_trade_no = reqInfo.get("out_trade_no"); //订单号
|
|
|
+
|
|
|
+ Map<String, Object> parm = new HashMap<>();
|
|
|
+ if (!StringUtils.isEmpty(refund_status) && "SUCCESS".equals(refund_status)) {
|
|
|
+
|
|
|
+ //查询订单
|
|
|
+ AppOrder order = appOrderMapper.selectOne(Wrappers.<AppOrder>lambdaQuery().eq(AppOrder::getOrderCode, out_trade_no).last("limit 1"));
|
|
|
+ if (ObjectUtil.isNotEmpty(order)) {
|
|
|
+ if (order.getOrderStatus() == 0) {
|
|
|
+ order.setOrderStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ order.setPayStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ order.setPayTime(new Date());
|
|
|
+ order.setPayType(CommonConstant.STATUS_0_INT);
|
|
|
+ order.setCallbackStatus(CommonConstant.STATUS_1_INT);
|
|
|
+ appOrderMapper.updateById(order);
|
|
|
+ List<AppOrderProInfo> proInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, order.getId()));
|
|
|
+ if (ObjectUtil.isNotEmpty(proInfoList)) {
|
|
|
+ for (AppOrderProInfo appOrderProInfo : proInfoList) {
|
|
|
+ appOrderProInfo.setOrderStatus(CommonConstant.ORDER_STATUS_1);
|
|
|
+ appOrderProInfoMapper.updateById(appOrderProInfo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ parm.put("code", "SUCCESS");
|
|
|
+ parm.put("message", "成功");
|
|
|
+ } else {
|
|
|
+ parm.put("code", "FAIL");
|
|
|
+ parm.put("message", "失败");
|
|
|
+ throw new RuntimeException("退款失败");
|
|
|
+ }
|
|
|
+ return parm; //返回给前端的参数
|
|
|
+ }
|
|
|
+
|
|
|
+ //退款回调 解密数据
|
|
|
+ public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
|
|
|
+ try {
|
|
|
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
|
+ SecretKeySpec key = new SecretKeySpec(WechatConstants.WECHAT_MCH_SECRET_V3.getBytes(), "AES");// 这里的apiV3key是你的商户APIV3密钥
|
|
|
+ GCMParameterSpec spec = new GCMParameterSpec(128, nonce);//规定为128
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
|
|
+ cipher.updateAAD(associatedData);
|
|
|
+ return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
|
|
|
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
|
|
+ throw new IllegalStateException(e);
|
|
|
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
|
|
+ throw new IllegalArgumentException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|