KKSU
2025-02-07 6539b325b5ce95d1fafa864c75a32279c954167f
refactor(mall): 重构订单退款流程

- 引入 ValidateEntityUtils 工具类,优化实体对象查询逻辑
- 重构 refundOrder 和 agreeRefund 方法,提高代码可读性和维护性
- 新增 FiuuRefundUtil 工具类,用于处理 FIUU 退款相关操作
- 更新 MallRefundEntity,将退款方式改为 FIUU
- 新增 ProfitJob 定时任务,用于查询退款记录并验证退款状态
- 更新 WebMvcConfigure,开放 FIUU 回调接口访问
3 files added
7 files modified
352 ■■■■ changed files
src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/entity/MallRefundEntity.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/quartz/ProfitJob.java 116 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallOrderInfoServiceImpl.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/mall/service/impl/ApiMallTeamLeaderServiceImpl.java 67 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/controller/FIUUController.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/model/RefundStatus.java 13 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/util/FiuuRefundUtil.java 83 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/util/FiuuUtil.java 24 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/util/HashUtils.java 18 ●●●●● patch | view | raw | blame | history
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);
        }
    }
}