src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
@@ -33,7 +33,7 @@ registration.excludePathPatterns("/api/xcxPay/wxpayCallback"); registration.excludePathPatterns("/api/xcxPay/rechargeCallBack"); registration.excludePathPatterns("/api/xcxPay/fapiaoCallBack"); // registration.excludePathPatterns("/api/fuPay/callback"); registration.excludePathPatterns("/api/fuPay/callback"); registration.excludePathPatterns("/api/fuPay/notify"); // 添加Swagger UI相关路径 src/main/java/cc/mrbird/febs/mall/entity/MallRefundEntity.java
@@ -21,7 +21,7 @@ private Long orderId; //退款订单详情ID private Long itemId; //退款方式 1:微信 2:支付宝 3:其他 //退款方式 1:FIUU 2:支付宝 3:其他 private Integer type; //退款状态 1:成功 2:失败 3:退款中 private Integer state; src/main/java/cc/mrbird/febs/mall/quartz/ProfitJob.java
@@ -1,8 +1,13 @@ package cc.mrbird.febs.mall.quartz; import cc.mrbird.febs.common.enumerates.FlowTypeEnum; import cc.mrbird.febs.common.enumerates.MoneyFlowTypeEnum; import cc.mrbird.febs.common.enumerates.YesOrNoOrIngEnum; import cc.mrbird.febs.mall.entity.MallActivity; import cc.mrbird.febs.mall.mapper.MallActivityMapper; import cc.mrbird.febs.mall.entity.*; import cc.mrbird.febs.mall.mapper.*; import cc.mrbird.febs.mall.service.IMallMoneyFlowService; import cc.mrbird.febs.pay.model.RefundStatus; import cc.mrbird.febs.pay.util.FiuuRefundUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; @@ -26,6 +31,27 @@ @Autowired private MallActivityMapper mallActivityMapper; @Autowired private MallRefundMapper mallRefundMapper; @Autowired private MallOrderInfoMapper mallOrderInfoMapper; @Autowired private MallOrderItemMapper mallOrderItemMapper; @Autowired private MallGoodsSkuMapper mallGoodsSkuMapper; @Autowired private MallGoodsMapper mallGoodsMapper; @Autowired private FiuuRefundUtil fiuuRefundUtil; @Autowired private IMallMoneyFlowService mallMoneyFlowService; /** * 一分钟运行一次 @@ -74,4 +100,90 @@ }); } /** * 一分钟运行一次 * 查询退款记录,去验证是否已经退款成功 */ @Scheduled(cron = "0 0/1 * * * ? ") public void refundJob() { LambdaQueryWrapper<MallRefundEntity> mallOrderRefundLambdaQueryWrapper = new LambdaQueryWrapper<>(); mallOrderRefundLambdaQueryWrapper.eq(MallRefundEntity::getStatus, 3); mallOrderRefundLambdaQueryWrapper.eq(MallRefundEntity::getType, 1); List<MallRefundEntity> mallRefundEntities = mallRefundMapper.selectList(mallOrderRefundLambdaQueryWrapper); if(CollUtil.isEmpty(mallRefundEntities)){ return; } mallRefundEntities.forEach(mallRefundEntity -> { processRefund(mallRefundEntity); }); } private void processRefund(MallRefundEntity mallRefundEntity) { try { MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectById(mallRefundEntity.getOrderId()); MallOrderItem mallOrderItem = mallOrderItemMapper.selectById(mallRefundEntity.getItemId()); MallGoodsSku mallGoodsSku = mallGoodsSkuMapper.selectById(mallOrderItem.getSkuId()); String txnId = mallOrderInfo.getPayOrderNo(); RefundStatus status = fiuuRefundUtil.pollRefundStatus(txnId); switch (status.getStatus()) { case "success": updateOnSuccess(mallRefundEntity, mallOrderInfo, mallOrderItem, mallGoodsSku); break; case "rejected": updateOnRejected(mallRefundEntity, mallOrderItem); break; default: log.warn("未知状态:{}", status.getStatus()); } } catch (Exception e) { log.error("处理退款失败: {}", e.getMessage(), e); } } private void updateOnSuccess(MallRefundEntity mallRefundEntity, MallOrderInfo mallOrderInfo, MallOrderItem mallOrderItem, MallGoodsSku mallGoodsSku) { mallOrderItem.setState(3); mallOrderItemMapper.updateById(mallOrderItem); MallGoods mallGoods = mallGoodsMapper.selectById(mallOrderItem.getGoodsId()); mallGoods.setStock(mallGoods.getStock() + mallOrderItem.getCnt()); mallGoods.setVolume(mallGoods.getVolume() - mallOrderItem.getCnt()); mallGoodsMapper.updateById(mallGoods); mallGoodsSku.setStock(mallGoodsSku.getStock() + mallOrderItem.getCnt()); mallGoodsSku.setSkuVolume(mallGoodsSku.getSkuVolume() - mallOrderItem.getCnt()); mallGoodsSkuMapper.updateById(mallGoodsSku); mallRefundEntity.setState(1); mallRefundEntity.setUpdatedTime(DateUtil.date()); mallRefundMapper.updateById(mallRefundEntity); mallMoneyFlowService.addMoneyFlow( mallOrderInfo.getMemberId(), mallRefundEntity.getAmount(), MoneyFlowTypeEnum.WECHAT_REFUND.getValue(), mallOrderInfo.getOrderNo(), FlowTypeEnum.WECHAT.getValue(), "FIUU退款", 2); List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByNotInStateAndOrderId(3, mallRefundEntity.getOrderId()); if (CollUtil.isEmpty(mallOrderItemList)) { MallOrderInfo mallOrderRefund = mallOrderInfoMapper.selectById(mallRefundEntity.getOrderId()); mallOrderRefund.setStatus(6); mallOrderInfoMapper.updateById(mallOrderRefund); } } private void updateOnRejected(MallRefundEntity mallRefundEntity, MallOrderItem mallOrderItem) { mallOrderItem.setState(1); mallOrderItemMapper.updateById(mallOrderItem); mallRefundEntity.setState(2); mallRefundMapper.updateById(mallRefundEntity); } } src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallOrderInfoServiceImpl.java
@@ -900,23 +900,16 @@ @Transactional public FebsResponse refundOrder(Long id) { MallMember member = LoginUserUtil.getLoginUser(); MallOrderInfo mallOrderInfo = this.baseMapper.selectById(id); if(ObjectUtil.isEmpty(mallOrderInfo)){ return new FebsResponse().fail().message("订单不存在"); } MallOrderInfo mallOrderInfo = ValidateEntityUtils .ensureColumnReturnEntity(id, MallOrderInfo::getId, this.baseMapper::selectOne, "订单不存在"); Integer status = mallOrderInfo.getStatus(); if(OrderStatusEnum.WAIT_SHIPPING.getValue() != status){ return new FebsResponse().fail().message("订单不是待发货状态"); } Integer deliveryState = mallOrderInfo.getDeliveryState(); if(1 != deliveryState){ return new FebsResponse().fail().message("订单不是待配送状态"); } ValidateEntityUtils.ensureEqual(OrderStatusEnum.WAIT_SHIPPING.getValue(),status,"订单不是待发货状态"); ValidateEntityUtils.ensureEqual(1,deliveryState,"订单不是待配送状态"); //根据子订单生成退款记录 List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByOrderId(id); if(CollUtil.isEmpty(mallOrderItemList)){ return new FebsResponse().fail().message("订单不存在"); } List<MallOrderItem> mallOrderItemList = ValidateEntityUtils .ensureColumnReturnEntityList(id, MallOrderItem::getOrderId, mallOrderItemMapper::selectList, "订单不存在"); for(MallOrderItem mallOrderItem : mallOrderItemList){ QueryWrapper<MallRefundEntity> objectQueryWrapper = new QueryWrapper<>(); objectQueryWrapper.eq("member_id",member.getId()); @@ -940,9 +933,7 @@ mallRefundEntity.setAmount(mallOrderItem.getAmount()); mallRefundMapper.insert(mallRefundEntity); }else{ if(mallRefund.getState() == 1){ return new FebsResponse().fail().message("订单已退款"); } ValidateEntityUtils.ensureNotEqual(1,mallRefund.getState(),"订单已退款"); if(mallRefund.getState() == 2 || mallRefund.getState() == 3){ mallRefundEntity.setId(mallRefund.getId()); mallRefundEntity.setRefundNo(mallRefund.getRefundNo()); src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallTeamLeaderServiceImpl.java
@@ -5,6 +5,7 @@ import cc.mrbird.febs.common.properties.XcxProperties; import cc.mrbird.febs.common.utils.LoginUserUtil; import cc.mrbird.febs.common.utils.SpringContextHolder; import cc.mrbird.febs.common.utils.ValidateEntityUtils; import cc.mrbird.febs.mall.conversion.MallLeaderAchieveConversion; import cc.mrbird.febs.mall.conversion.MallOrderInfoConversion; import cc.mrbird.febs.mall.conversion.MallTeamLeaderConversion; @@ -395,13 +396,12 @@ if(!(1 == agreeType || 2 == agreeType)){ return new FebsResponse().fail().message("退款失败,请联系客服人员"); } MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectById(orderId); MallOrderItem mallOrderItem = mallOrderItemMapper.selectById(itemId); MallGoodsSku mallGoodsSku = mallGoodsSkuMapper.selectById(mallOrderItem.getSkuId()); if(ObjectUtil.isEmpty(mallGoodsSku)){ return new FebsResponse().fail().message("退款失败,请联系客服人员"); } MallOrderInfo mallOrderInfo = ValidateEntityUtils .ensureColumnReturnEntity(orderId, MallOrderInfo::getId, mallOrderInfoMapper::selectOne, "订单不存在"); MallOrderItem mallOrderItem = ValidateEntityUtils .ensureColumnReturnEntity(itemId, MallOrderItem::getId, mallOrderItemMapper::selectOne, "订单不存在"); MallGoodsSku mallGoodsSku = ValidateEntityUtils .ensureColumnReturnEntity(mallOrderItem.getSkuId(), MallGoodsSku::getId, mallGoodsSkuMapper::selectOne, "订单不存在,退款失败,请联系客服人员"); List<MallRefundEntity> mallRefundEntities = mallRefundMapper.selectByItemIdAndOrderIdAndState(itemId, orderId, 3); if(CollUtil.isEmpty(mallRefundEntities)){ return new FebsResponse().fail().message("退款失败,请联系客服人员"); @@ -453,7 +453,6 @@ } } } // refundAmount = refundAmount.add(mallOrderInfo.getCarriage()); BigDecimal bb = new BigDecimal(100); int refundMoney = refundAmount.multiply(bb).intValue(); @@ -486,55 +485,9 @@ return new FebsResponse().success().message("退款成功"); } Boolean flag = false; Boolean debug = xcxProperties.getDebug(); if (debug) { boolean b = fiuuUtil.comRefund(mallOrderInfo.getPayOrderNo(), refundNo, "1"); flag = b; } else { log.info("开始调用退款接口。。。退款编号为{}", refundNo); boolean b = fiuuUtil.comRefund(mallOrderInfo.getPayOrderNo(), refundNo, orderAmount.toString()); flag = b; } if(flag){ //更新订单详情 mallOrderItem.setState(3); mallOrderItemMapper.updateById(mallOrderItem); //更新库存信息 MallGoods mallGoods = mallGoodsMapper.selectById(mallOrderItem.getGoodsId()); mallGoods.setStock(mallGoods.getStock() + mallOrderItem.getCnt()); mallGoods.setVolume(mallGoods.getVolume() - mallOrderItem.getCnt()); mallGoodsMapper.updateById(mallGoods); // MallGoodsSku mallGoodsSku = mallGoodsSkuMapper.selectById(mallOrderItem.getSkuId()); mallGoodsSku.setStock(mallGoodsSku.getStock() + mallOrderItem.getCnt()); mallGoodsSku.setSkuVolume(mallGoodsSku.getSkuVolume() - mallOrderItem.getCnt()); mallGoodsSkuMapper.updateById(mallGoodsSku); //更新退款订单 mallRefundEntity.setState(1); mallRefundEntity.setUpdatedTime(DateUtil.date()); mallRefundMapper.updateById(mallRefundEntity); mallMoneyFlowService.addMoneyFlow(mallOrderInfo.getMemberId(), refundAmount, MoneyFlowTypeEnum.WECHAT_REFUND.getValue(), mallOrderInfo.getOrderNo(), FlowTypeEnum.WECHAT.getValue(),"微信退款",2); List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByNotInStateAndOrderId(3,orderId); if(CollUtil.isEmpty(mallOrderItemList)){ MallOrderInfo mallOrderRefund = mallOrderInfoMapper.selectById(orderId); mallOrderRefund.setStatus(6); mallOrderInfoMapper.updateById(mallOrderRefund); } }else{ //更新订单详情 mallOrderItem.setState(1); mallOrderItemMapper.updateById(mallOrderItem); mallRefundEntity.setState(2); mallRefundMapper.updateById(mallRefundEntity); return new FebsResponse().fail().message("退款失败,请联系客服人员"); } return new FebsResponse().success().message("退款成功"); log.info("开始调用退款接口。。。退款编号为{}", refundNo); fiuuUtil.comRefund(mallOrderInfo.getPayOrderNo(), refundNo, orderAmount.toString()); return new FebsResponse().success().message("已申请退款"); } } src/main/java/cc/mrbird/febs/pay/controller/FIUUController.java
@@ -64,7 +64,7 @@ params.put("bill_desc", productNames); params.put("currency", "MYR"); // 默认 MYR params.put("vcode", vcode); // params.put("returnurl", returnUrl); params.put("returnurl", returnUrl); return new FebsResponse().success().data(params); } catch (Exception e) { src/main/java/cc/mrbird/febs/pay/model/RefundStatus.java
New file @@ -0,0 +1,13 @@ package cc.mrbird.febs.pay.model; import lombok.Data; @Data public class RefundStatus { private String TxnID; private String RefID; private String RefundID; private String Status; private String LastUpdate; private String FPXTxnID; } src/main/java/cc/mrbird/febs/pay/util/FiuuRefundUtil.java
New file @@ -0,0 +1,83 @@ package cc.mrbird.febs.pay.util; import cc.mrbird.febs.pay.model.RefundStatus; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.springframework.stereotype.Service; import java.util.concurrent.*; @Service(value="FiuuRefundUtil") public class FiuuRefundUtil { private static final String API_BASE_URL = "https://api.fiuu.com/RMS/API/refundAPI/"; private static final String MERCHANT_ID = "e2umart01"; private static final String VERIFY_KEY = "4e3a4ed58e62ddbfacf41f6d5ec56bf2"; private static final int MAX_RETRIES = 3; private static final int POLL_INTERVAL = 1000; // 5秒 private static final int TIMEOUT = 60000; // 60秒超时 private final ObjectMapper objectMapper = new ObjectMapper(); // 退款状态查询(根据TxnID) public RefundStatus queryByTxnId(String txnId) throws Exception { String signature = HashUtils.md5(txnId + MERCHANT_ID + VERIFY_KEY); String url = API_BASE_URL + "q_by_txn.php?TxnID=" + txnId + "&MerchantID=" + MERCHANT_ID + "&Signature=" + signature; return executeQuery(url); } // 退款状态查询(根据RefID) public RefundStatus queryByRefId(String refId) throws Exception { String signature = HashUtils.md5(refId + MERCHANT_ID + VERIFY_KEY); String url = API_BASE_URL + "q_by_refID.php?RefID=" + refId + "&MerchantID=" + MERCHANT_ID + "&Signature=" + signature; return executeQuery(url); } private RefundStatus executeQuery(String url) throws Exception { HttpGet request = new HttpGet(url); try (CloseableHttpClient client = HttpClients.createDefault()) { String response = EntityUtils.toString(client.execute(request).getEntity()); return objectMapper.readValue(response, RefundStatus.class); } } // 异步轮询退款状态 public RefundStatus pollRefundStatus(String txnId) throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<RefundStatus> future = executor.submit(() -> { int retryCount = 0; while (retryCount < MAX_RETRIES) { try { RefundStatus status = queryByTxnId(txnId); if (!"pending".equals(status.getStatus())) { return status; } Thread.sleep(POLL_INTERVAL); retryCount++; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Polling interrupted", e); } } throw new TimeoutException("Max retries reached"); }); try { return future.get(TIMEOUT, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); throw new RuntimeException("Refund status check timeout"); } finally { executor.shutdown(); } } } src/main/java/cc/mrbird/febs/pay/util/FiuuUtil.java
@@ -1,7 +1,5 @@ package cc.mrbird.febs.pay.util; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; @@ -25,7 +23,7 @@ private static final String SECRET_KEY = "59c709fc18978a6a83b87f05d37cecbf"; @Transactional public boolean comRefund(String outTradeNo, String outRefundNo,String amount){ public void comRefund(String outTradeNo, String outRefundNo,String amount){ // 退款请求参数 Map<String, String> params = new LinkedHashMap<>(); params.put("RefundType", "P"); // P: Partial Refund, F: Full Refund @@ -52,14 +50,14 @@ } System.out.println("退款响应: " + response); JSONObject jsonObject = JSONUtil.parseObj(response); String status = jsonObject.getStr("status"); if ("00".equals(status)) { return true; }else{ return false; } // JSONObject jsonObject = JSONUtil.parseObj(response); // // String status = jsonObject.getStr("status"); // if ("00".equals(status)) { // return true; // }else{ // return false; // } } public static void main(String[] args) { @@ -68,8 +66,8 @@ Map<String, String> params = new LinkedHashMap<>(); params.put("RefundType", "P"); // P: Partial Refund, F: Full Refund params.put("MerchantID", MERCHANT_ID); params.put("RefID", "2025020614313541756_RITEM426"); // 商户唯一退款ID params.put("TxnID", "2685173864"); // Fiuu原始交易ID params.put("RefID", "2025020611124868512_RITEM42432121"); // 商户唯一退款ID params.put("TxnID", "2685352601"); // Fiuu原始交易ID params.put("Amount", "1.00"); // 退款金额 // 生成签名 src/main/java/cc/mrbird/febs/pay/util/HashUtils.java
New file @@ -0,0 +1,18 @@ package cc.mrbird.febs.pay.util; public class HashUtils { public static String md5(String input) { try { java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); byte[] array = md.digest(input.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : array) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (java.security.NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }