longz
2026-07-20 faaf70b3fc0cff90d25fa30b37f225f679a0d5a5
特殊退款查询
1个文件已添加
9个文件已修改
544 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUser.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/logback.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderRefundDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 294 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TUserTagController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260720_special_refund_menu.sql 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUser.java
@@ -132,6 +132,10 @@
    @TableField(exist = false)
    private String tagName;
    @ApiModelProperty(value = "是否白名单:1=是,2=否")
    @TableField(exist = false)
    private Integer whitelistFlag;
    @ApiModelProperty(value = "单位名称")
    @TableField(exist = false)
    private String companyName;
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -428,6 +428,31 @@
            return R.ok(page);
        }
        List<Long> pageUserIds = page.getRecords().stream().map(TAppUser::getId).collect(Collectors.toList());
        List<String> pageUserPhones = page.getRecords().stream()
                .map(TAppUser::getPhone)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .distinct()
                .collect(Collectors.toList());
        LambdaQueryChainWrapper<TAppUserWhitelist> pageWhitelistQuery = appUserWhitelistService.lambdaQuery()
                .eq(TAppUserWhitelist::getStatus, 1)
                .eq(TAppUserWhitelist::getDelFlag, false)
                .and(wrapper -> {
                    wrapper.in(TAppUserWhitelist::getAppUserId, pageUserIds);
                    if (!pageUserPhones.isEmpty()) {
                        wrapper.or().in(TAppUserWhitelist::getPhone, pageUserPhones);
                    }
                });
        List<TAppUserWhitelist> pageWhitelistList = pageWhitelistQuery.list();
        Set<Long> activeWhitelistUserIds = pageWhitelistList.stream()
                .map(TAppUserWhitelist::getAppUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Set<String> activeWhitelistPhones = pageWhitelistList.stream()
                .map(TAppUserWhitelist::getPhone)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet());
        Map<Long, String> userTagNameMap = getUserTagNameMap(pageUserIds);
        Map<Long, BigDecimal> exchangeCodeBalanceMap = otherClient.getExchangeCodeBalanceMap(pageUserIds).getData();
        if (exchangeCodeBalanceMap == null) {
@@ -441,6 +466,9 @@
        for (TAppUser appUser : page.getRecords()) {
            appUser.setExchangeCodeBalance(exchangeCodeBalanceMap.getOrDefault(appUser.getId(), BigDecimal.ZERO));
            appUser.setTagName(userTagNameMap.get(appUser.getId()));
            boolean activeWhitelist = activeWhitelistUserIds.contains(appUser.getId())
                    || (StringUtils.isNotBlank(appUser.getPhone()) && activeWhitelistPhones.contains(appUser.getPhone().trim()));
            appUser.setWhitelistFlag(activeWhitelist ? 1 : 2);
            if (appUser.getVipEndTime()!=null&&appUser.getVipEndTime().isAfter(LocalDateTime.now())) {
                //匹配vipMap的值
                appUser.setVipName(vipMap.getData().get(appUser.getVipId()));
@@ -731,11 +759,7 @@
        List<TAppUserCar> carList = appUserCarService.lambdaQuery().eq(TAppUserCar::getAppUserId, id).orderByDesc(BasePojo::getCreateTime).list();
        userDetailDto.setCarList(carList);
        List<Integer> tagIds = appUserTagService.lambdaQuery().eq(TAppUserTag::getAppUserId, id).orderByDesc(TAppUserTag::getCreateTime).list().stream().map(TAppUserTag::getUserTagId).collect(Collectors.toList());
        R<Map<Integer, String>> tagMap = otherClient.getTagMap(tagIds);
        userDetailDto.setTagName(tagMap.getData().values()
                .stream()
                .collect(Collectors.joining(",")));
        userDetailDto.setTagName(getUserTagNameMap(Collections.singletonList(id)).get(id));
ruoyi-service/ruoyi-chargingPile/src/main/resources/logback.xml
@@ -8,7 +8,7 @@
    
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
<!--    <property name="log.path" value="/data/log/app/ruoyi-chargingPile"/>-->
    <property name="log.path" value="/Users/mitao/Documents/log/mx_charging_pile/log/app/ruoyi-chargingPile"/>
    <property name="log.path" value="/Users/PC/Documents/log/mx_charging_pile/log/app/ruoyi-chargingPile"/>
    <!--0. 日志格式和颜色渲染 -->
    <!-- 彩色日志依赖的渲染类 -->
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -440,14 +440,16 @@
    @ResponseBody
    @PostMapping(value = "/pay/order/special/refund/query")
    @ApiOperation(value = "特殊退款查询", tags = {"管理后台-支付订单-订单信息"})
    @RequiresPermissions(value = {"/paymentOrder/specialRefund", "/paymentOrder/specialRefund/refund"}, logical = Logical.OR)
    @ApiOperation(value = "特殊退款查询", tags = {"管理后台-支付订单-特殊退款处理"})
    public R<PayOrderInfoDto> specialRefundQuery(@RequestBody PayOrderRefundDto payOrderQueryDto) {
        return chargingOrderService.getSpecialRefundOrder(payOrderQueryDto);
    }
    @ResponseBody
    @PostMapping(value = "/pay/order/special/refund")
    @ApiOperation(value = "特殊退款", tags = {"管理后台-支付订单-订单信息"})
    @RequiresPermissions(value = {"/paymentOrder/specialRefund/refund"}, logical = Logical.OR)
    @ApiOperation(value = "特殊退款", tags = {"管理后台-支付订单-特殊退款处理"})
    public R specialRefund(@RequestBody PayOrderRefundDto payOrderQueryDto) {
        return chargingOrderService.specialRefund(payOrderQueryDto);
    }
@@ -616,7 +618,7 @@
    
    
    
    @RequiresPermissions(value = {"/paymentOrder/refund", "/paymentOrder/order"}, logical = Logical.OR)
    @RequiresPermissions(value = {"/paymentOrder/refund", "/paymentOrder/order", "/paymentOrder/specialRefund"}, logical = Logical.OR)
    @ResponseBody
    @PostMapping(value = "/pay/order/refund/list")
    @ApiOperation(value = "列表", tags = {"管理后台-支付订单-退款订单"})
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderRefundDto.java
@@ -16,7 +16,7 @@
    private String code;
    @ApiModelProperty("支付流水号")
    private String serialNumber;
    @ApiModelProperty("支付宝特殊退款方式:ALI=支付宝退款,ICBC=工商退款")
    @ApiModelProperty("退款通道(兼容旧参数,特殊退款按原支付路径退款)")
    private String refundChannel;
    private BigDecimal refundAmount;
    private String reason;
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
@@ -36,6 +36,7 @@
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
@@ -43,6 +44,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -1057,10 +1060,10 @@
        List<Map<String, Object>> list = chargingSettlementBillService.queryList(pageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
        ChargingBillWapper chargingBillVO = new ChargingBillWapper();
        List<Map<String, Object>> pageList = new ArrayList<>();
        List<TChargingOrder> total = chargingOrderList.selectList(new LambdaQueryWrapper<>(TChargingOrder.class)
                .isNotNull(TChargingOrder::getPayTime));
        List<TChargingOrderAccountingStrategy> strategyList = chargingOrderAccountingStrategyService.list();
        Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
        PageInfo<Map<String, Object>> sumPageInfo = new PageInfo<>(1, 100000);
        List<Map<String, Object>> sumList = chargingSettlementBillService.queryList(sumPageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
        Map<String, Map<String, BigDecimal>> incomeIndex = buildChargingBillIncomeIndex(sumList, siteServiceShareRatioMap);
        if(null != list){
            for (Map<String, Object> map : list) {
                Integer site_id = Integer.valueOf(map.get("site_id").toString());
@@ -1077,7 +1080,7 @@
                String chargingCapacity = map.get("total_electricity").toString();
                String electrovalence = map.get("total_electricity_charge").toString();
                String serviceCharge = map.get("total_service_fee").toString();
                Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), site_id, billingCycle, total, strategyList, siteServiceShareRatioMap);
                Map<String, BigDecimal> incomeMap = getChargingBillIncome(incomeIndex, site_id, billingCycle);
                Map<String, Object> data1 = new HashMap<>();
                data1.put("code", code);
                data1.put("orderState", 2);
@@ -1118,13 +1121,11 @@
        BigDecimal discount = (null == map || null == map.get("couponDeduction")) ? BigDecimal.ZERO : new BigDecimal(map.get("couponDeduction").toString());
        BigDecimal platformIncome = BigDecimal.ZERO;
        BigDecimal partnerIncome = BigDecimal.ZERO;
        PageInfo<Map<String, Object>> sumPageInfo = new PageInfo<>(1, 100000);
        List<Map<String, Object>> sumList = chargingSettlementBillService.queryList(sumPageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
        if (sumList != null) {
            for (Map<String, Object> row : sumList) {
                Integer siteId = Integer.valueOf(row.get("site_id").toString());
                String billingCycle = row.get("billingCycle").toString();
                Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), siteId, billingCycle, total, strategyList, siteServiceShareRatioMap);
                Map<String, BigDecimal> incomeMap = getChargingBillIncome(incomeIndex, siteId, billingCycle);
                platformIncome = platformIncome.add(incomeMap.get("platformIncome"));
                partnerIncome = partnerIncome.add(incomeMap.get("partnerIncome"));
            }
@@ -1145,6 +1146,111 @@
        return chargingBillVO;
    }
    private Map<String, Map<String, BigDecimal>> buildChargingBillIncomeIndex(List<Map<String, Object>> billRows,
                                                                               Map<Integer, BigDecimal> siteServiceShareRatioMap) {
        Map<String, Map<String, BigDecimal>> incomeIndex = new HashMap<>();
        if (billRows == null || billRows.isEmpty()) {
            return incomeIndex;
        }
        Set<String> billingCycles = billRows.stream()
                .map(row -> row.get("billingCycle"))
                .filter(Objects::nonNull)
                .map(Object::toString)
                .collect(Collectors.toSet());
        if (billingCycles.isEmpty()) {
            return incomeIndex;
        }
        LocalDate firstMonth = billingCycles.stream()
                .map(cycle -> LocalDate.parse(cycle + "-01"))
                .min(LocalDate::compareTo)
                .orElse(null);
        LocalDate lastMonth = billingCycles.stream()
                .map(cycle -> LocalDate.parse(cycle + "-01"))
                .max(LocalDate::compareTo)
                .orElse(null);
        if (firstMonth == null || lastMonth == null) {
            return incomeIndex;
        }
        List<TChargingOrder> orders = chargingOrderList.selectList(new LambdaQueryWrapper<>(TChargingOrder.class)
                .select(TChargingOrder::getId, TChargingOrder::getSiteId, TChargingOrder::getPayTime, TChargingOrder::getOrderSource)
                .ge(TChargingOrder::getPayTime, firstMonth.atStartOfDay())
                .lt(TChargingOrder::getPayTime, lastMonth.plusMonths(1).atStartOfDay()));
        if (orders == null || orders.isEmpty()) {
            return incomeIndex;
        }
        Map<Long, BigDecimal[]> strategyAmountMap = new HashMap<>();
        List<Long> orderIds = orders.stream().map(TChargingOrder::getId).filter(Objects::nonNull).collect(Collectors.toList());
        final int batchSize = 1000;
        for (int from = 0; from < orderIds.size(); from += batchSize) {
            List<Long> batchIds = orderIds.subList(from, Math.min(from + batchSize, orderIds.size()));
            List<TChargingOrderAccountingStrategy> strategies = chargingOrderAccountingStrategyService.list(
                    new LambdaQueryWrapper<TChargingOrderAccountingStrategy>()
                            .select(TChargingOrderAccountingStrategy::getChargingOrderId,
                                    TChargingOrderAccountingStrategy::getPeriodElectricPrice,
                                    TChargingOrderAccountingStrategy::getPeriodServicePrice)
                            .in(TChargingOrderAccountingStrategy::getChargingOrderId, batchIds));
            for (TChargingOrderAccountingStrategy strategy : strategies) {
                BigDecimal[] amounts = strategyAmountMap.computeIfAbsent(strategy.getChargingOrderId(), key -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
                amounts[0] = amounts[0].add(strategy.getPeriodElectricPrice() == null ? BigDecimal.ZERO : strategy.getPeriodElectricPrice());
                amounts[1] = amounts[1].add(strategy.getPeriodServicePrice() == null ? BigDecimal.ZERO : strategy.getPeriodServicePrice());
            }
        }
        for (TChargingOrder order : orders) {
            if (order.getPayTime() == null || order.getSiteId() == null) {
                continue;
            }
            String billingCycle = order.getPayTime().format(DateTimeFormatter.ofPattern("yyyy-MM"));
            if (!billingCycles.contains(billingCycle)) {
                continue;
            }
            BigDecimal[] amounts = strategyAmountMap.getOrDefault(order.getId(), new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
            BigDecimal paymentAmount;
            BigDecimal partnerIncome = BigDecimal.ZERO;
            if (Integer.valueOf(2).equals(order.getOrderSource())) {
                paymentAmount = amounts[0].add(amounts[1].multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.DOWN));
            } else {
                paymentAmount = amounts[0].add(amounts[1]);
                if (Integer.valueOf(1).equals(order.getOrderSource())) {
                    BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(order.getSiteId());
                    if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
                        partnerIncome = amounts[1].multiply(serviceShareRatio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
                    }
                }
            }
            addChargingBillIncome(incomeIndex, chargingBillIncomeKey(0, billingCycle), paymentAmount, partnerIncome);
            addChargingBillIncome(incomeIndex, chargingBillIncomeKey(order.getSiteId(), billingCycle), paymentAmount, partnerIncome);
        }
        return incomeIndex;
    }
    private void addChargingBillIncome(Map<String, Map<String, BigDecimal>> incomeIndex, String key,
                                       BigDecimal paymentAmount, BigDecimal partnerIncome) {
        Map<String, BigDecimal> income = incomeIndex.computeIfAbsent(key, ignored -> {
            Map<String, BigDecimal> value = new HashMap<>();
            value.put("platformIncome", BigDecimal.ZERO);
            value.put("partnerIncome", BigDecimal.ZERO);
            return value;
        });
        income.put("platformIncome", income.get("platformIncome").add(paymentAmount).subtract(partnerIncome));
        income.put("partnerIncome", income.get("partnerIncome").add(partnerIncome));
    }
    private Map<String, BigDecimal> getChargingBillIncome(Map<String, Map<String, BigDecimal>> incomeIndex,
                                                           Integer siteId, String billingCycle) {
        Map<String, BigDecimal> income = incomeIndex.get(chargingBillIncomeKey(siteId, billingCycle));
        Map<String, BigDecimal> result = new HashMap<>();
        result.put("platformIncome", income == null ? BigDecimal.ZERO : income.get("platformIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP));
        result.put("partnerIncome", income == null ? BigDecimal.ZERO : income.get("partnerIncome").setScale(2, RoundingMode.HALF_UP));
        return result;
    }
    private String chargingBillIncomeKey(Integer siteId, String billingCycle) {
        return (siteId == null ? 0 : siteId) + "|" + billingCycle;
    }
    @Override
    public ChargingBillVO chargingBillListExport(ChargingListQuery dto) {
        if (dto.getState()!=null){
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -966,7 +966,13 @@
            req.setReturnUrl(addChargingOrder.getReturnUrl());
            req.setOpenId(appUser.getWxOpenid());
            req.setProfitSharing(buildIcbcProfitSharing(site));
            IcbcPaymentResp data = icbcPaymentClient.payment(req).getData();
            R<IcbcPaymentResp> result = icbcPaymentClient.payment(req);
            if (result == null || R.isError(result)) {
                String reason = result == null ? "工行支付服务无响应" : firstNotBlank(result.getMsg(), "工行支付服务调用失败");
                log.error("调起工行支付失败,订单号:{},原因:{}", chargingOrder.getCode(), reason);
                throw new RuntimeException("调起工行支付失败:" + reason);
            }
            IcbcPaymentResp data = result.getData();
            if (isIcbcPaymentCreated(data)) {
                if (StringUtils.hasLength(data.getTradeNo())) {
                    chargingOrder.setRechargeSerialNumber(data.getTradeNo());
@@ -974,7 +980,9 @@
                }
                return AjaxResult.success(data);
            }
            throw new RuntimeException("调起工行支付失败");
            String reason = buildIcbcPaymentFailReason(data);
            log.error("调起工行支付失败,订单号:{},工行返回:{}", chargingOrder.getCode(), reason);
            throw new RuntimeException("调起工行支付失败:" + reason);
        }
        if (Integer.valueOf(2).equals(addChargingOrder.getPaymentType())) {
            AliPaymentReq req = new AliPaymentReq();
@@ -2385,12 +2393,23 @@
     * @param chargingOrder
     */
    public void editUserTag(TChargingOrder chargingOrder) {
        if (chargingOrder == null || chargingOrder.getAppUserId() == null) {
            return;
        }
        //处理用户标签数据
        List<TUserTag> data = userTagClient.getAllUserTag().getData();
        R<List<TUserTag>> tagResult = userTagClient.getAllUserTag();
        List<TUserTag> data = tagResult == null ? null : tagResult.getData();
        if (data == null || data.isEmpty()) {
            return;
        }
        //累计充电次数
        List<TUserTag> userTagList1 = data.stream().filter(s -> s.getStandardCondition() == 1).collect(Collectors.toList());
        List<TUserTag> userTagList1 = data.stream().filter(s -> Integer.valueOf(1).equals(s.getStandardCondition())).collect(Collectors.toList());
        for (TUserTag tUserTag : userTagList1) {
            Integer times = JSON.parseObject(tUserTag.getConditions()).getInteger("times");
            JSONObject conditions = JSON.parseObject(tUserTag.getConditions());
            Integer times = conditions == null ? null : conditions.getInteger("times");
            if (times == null || times <= 0 || tUserTag.getCreateTime() == null) {
                continue;
            }
            TAppUserTag appUserTag = new TAppUserTag();
            appUserTag.setAppUserId(chargingOrder.getAppUserId());
            appUserTag.setUserTagId(tUserTag.getId());
@@ -2409,18 +2428,21 @@
            }
        }
        //充电频率
        List<TUserTag> userTagList2 = data.stream().filter(s -> s.getStandardCondition() == 2).collect(Collectors.toList());
        List<TUserTag> userTagList2 = data.stream().filter(s -> Integer.valueOf(2).equals(s.getStandardCondition())).collect(Collectors.toList());
        for (TUserTag tUserTag : userTagList2) {
            TAppUserTag appUserTag = new TAppUserTag();
            appUserTag.setAppUserId(chargingOrder.getAppUserId());
            appUserTag.setUserTagId(tUserTag.getId());
            TAppUserTag data1 = appUserTagClient.getUserTag(appUserTag).getData();
            
            Integer day = JSON.parseObject(tUserTag.getConditions()).getInteger("day");
            Integer times = JSON.parseObject(tUserTag.getConditions()).getInteger("times");
            JSONObject conditions = JSON.parseObject(tUserTag.getConditions());
            Integer day = conditions == null ? null : conditions.getInteger("day");
            Integer times = conditions == null ? null : conditions.getInteger("times");
            if (day == null || day <= 0 || times == null || times <= 0) {
                continue;
            }
            LocalDateTime endTime = LocalDateTime.now();
            endTime.plusDays(day);
            String start = tUserTag.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            String start = endTime.minusDays(day).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            String end = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            long count1 = this.count(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getAppUserId, chargingOrder.getAppUserId())
                    .eq(TChargingOrder::getRechargePaymentStatus, 2).between(TChargingOrder::getStartTime, start, end).eq(TChargingOrder::getDelFlag, 0));
@@ -2934,13 +2956,23 @@
    @Override
    public R<PageInfo<PayOrderDto>> payOrderQuery(PayOrderQueryDto payOrderQueryDto) {
        if (payOrderQueryDto == null) {
            payOrderQueryDto = new PayOrderQueryDto();
        }
        PageInfo<PayOrderDto> pageInfo = new PageInfo<>(payOrderQueryDto.getPageCurr(), payOrderQueryDto.getPageSize());
        List<PayOrderDto> list = this.baseMapper.payOrderQuery(pageInfo, payOrderQueryDto);
        if (list == null || list.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        for (PayOrderDto payOrderDto : list) {
            if (payOrderDto.getType() == 2 || payOrderDto.getType() == 3) {
            if (payOrderDto == null) {
                continue;
            }
            if (Integer.valueOf(2).equals(payOrderDto.getType()) || Integer.valueOf(3).equals(payOrderDto.getType())) {
                payOrderDto.setFinalAmount(payOrderDto.getOrderAmount());
            }
            if (payOrderDto.getType() == 1 && payOrderDto.getStatus() == 3) {
            if (Integer.valueOf(1).equals(payOrderDto.getType()) && Integer.valueOf(3).equals(payOrderDto.getStatus())) {
                payOrderDto.setFinalAmount(payOrderDto.getOrderAmount());
                payOrderDto.setOrderAmount(payOrderDto.getOrderAmount());
                payOrderDto.setPaymentAmount(payOrderDto.getOrderAmount());
@@ -4700,61 +4732,189 @@
        if (order == null) {
            return R.fail("未查询到支付成功的充电订单");
        }
        BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount());
        BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP);
        if (refundAmount.compareTo(refundableAmount) > 0) {
            return R.fail("退款金额不能超过剩余可退金额");
        RLock lock = redissonClient.getLock("SPECIAL_REFUND_ORDER_LOCK:" + order.getId());
        if (!lock.tryLock()) {
            return R.fail("该订单正在退款处理中,请勿重复提交");
        }
        BigDecimal refundedThirdAmount = money(order.getThirdRefundAmount());
        BigDecimal refundableThirdAmount = money(order.getThirdPayAmount()).subtract(refundedThirdAmount);
        if (refundableThirdAmount.compareTo(BigDecimal.ZERO) < 0) {
            refundableThirdAmount = BigDecimal.ZERO;
        try {
            order = this.getById(order.getId());
            if (order == null) {
                return R.fail("充电订单不存在");
            }
            BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount());
            BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP);
            if (refundAmount.compareTo(refundableAmount) > 0) {
                return R.fail("退款金额不能超过剩余可退金额");
            }
            BigDecimal refundedThirdAmount = money(order.getThirdRefundAmount());
            BigDecimal refundableThirdAmount = money(order.getThirdPayAmount()).subtract(refundedThirdAmount);
            if (refundableThirdAmount.compareTo(BigDecimal.ZERO) < 0) {
                refundableThirdAmount = BigDecimal.ZERO;
            }
            BigDecimal thirdRefundAmount = isBalancePayMode(order.getPayMethod())
                    ? refundAmount.min(refundableThirdAmount).setScale(2, RoundingMode.HALF_UP)
                    : refundAmount;
            BigDecimal balanceRefundAmount = refundAmount.subtract(thirdRefundAmount).setScale(2, RoundingMode.HALF_UP);
            if (balanceRefundAmount.compareTo(BigDecimal.ZERO) < 0) {
                balanceRefundAmount = BigDecimal.ZERO;
            }
            TChargingOrderRefund refund = buildSpecialChargingRefund(order, payOrderQueryDto, refundAmount, balanceRefundAmount, thirdRefundAmount);
            if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) {
                refund.setRefundSerialNumber("BALANCE");
                finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
                return R.ok();
            }
            Integer payType = order.getRechargePaymentType();
            boolean success = false;
            if (Integer.valueOf(1).equals(payType)) {
                R shareRefund = refundIcbcShareBeforeMainRefund(order, refund, thirdRefundAmount);
                if (shareRefund.getCode() != 200) {
                    return shareRefund;
                }
                success = refundByIcbcAndFinish(order, refund, thirdRefundAmount, SPECIAL_REFUND_TITLE, true, refundAmount, balanceRefundAmount);
            } else if (Integer.valueOf(2).equals(payType)) {
                RefundReq dto = new RefundReq();
                dto.setOutTradeNo(order.getCode());
                dto.setOutRequestNo(refund.getRefundCode());
                dto.setRefundAmount(thirdRefundAmount.toString());
                dto.setRefundReason(SPECIAL_REFUND_TITLE);
                RefundResp resp = aliPaymentClient.refund(dto).getData();
                if (resp != null) {
                    refund.setRefundSerialNumber(resp.getTradeNo());
                    refund.setRefundTime(LocalDateTime.now());
                    finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
                    success = true;
                }
            } else if (Integer.valueOf(3).equals(payType)) {
                RefundReq dto = new RefundReq();
                dto.setOutTradeNo(order.getRechargeSerialNumber());
                dto.setOutRequestNo(refund.getRefundCode());
                dto.setRefundAmount(thirdRefundAmount.toString());
                dto.setRefundReason(SPECIAL_REFUND_TITLE);
                RefundResp resp = h5AliPaymentClient.refund(dto).getData();
                if (resp != null && "10000".equals(resp.getCode())) {
                    refund.setRefundSerialNumber(resp.getTradeNo());
                    refund.setRefundTime(LocalDateTime.now());
                    finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
                    success = true;
                }
            }
            return success ? R.ok() : R.fail("退款失败,请检查支付渠道或稍后重试");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        BigDecimal thirdRefundAmount = isBalancePayMode(order.getPayMethod())
                ? refundAmount.min(refundableThirdAmount).setScale(2, RoundingMode.HALF_UP)
                : refundAmount;
        BigDecimal balanceRefundAmount = refundAmount.subtract(thirdRefundAmount).setScale(2, RoundingMode.HALF_UP);
        if (balanceRefundAmount.compareTo(BigDecimal.ZERO) < 0) {
            balanceRefundAmount = BigDecimal.ZERO;
        }
        TChargingOrderRefund refund = buildSpecialChargingRefund(order, payOrderQueryDto, refundAmount, balanceRefundAmount, thirdRefundAmount);
        if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) {
            refund.setRefundSerialNumber("BALANCE");
            finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
    }
    private R refundIcbcShareBeforeMainRefund(TChargingOrder order, TChargingOrderRefund refund, BigDecimal thirdRefundAmount) {
        if (order == null || refund == null || thirdRefundAmount == null || thirdRefundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return R.ok();
        }
        Integer payType = order.getRechargePaymentType();
        boolean success = false;
        if (Integer.valueOf(1).equals(payType) || "ICBC".equalsIgnoreCase(payOrderQueryDto.getRefundChannel())) {
            success = refundByIcbcAndFinish(order, refund, thirdRefundAmount, SPECIAL_REFUND_TITLE, true, refundAmount, balanceRefundAmount);
        } else if (Integer.valueOf(2).equals(payType)) {
            RefundReq dto = new RefundReq();
            dto.setOutTradeNo(order.getCode());
            dto.setOutRequestNo(refund.getRefundCode());
            dto.setRefundAmount(thirdRefundAmount.toString());
            dto.setRefundReason(SPECIAL_REFUND_TITLE);
            RefundResp resp = aliPaymentClient.refund(dto).getData();
            if (resp != null) {
                refund.setRefundSerialNumber(resp.getTradeNo());
                refund.setRefundTime(LocalDateTime.now());
                finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
                success = true;
        if (!"ICBC".equals(order.getSourceName())) {
            return R.ok();
        }
        List<TChargingOrderShareRecord> shareRecords = chargingOrderShareRecordMapper.selectList(
                new LambdaQueryWrapper<TChargingOrderShareRecord>()
                        .eq(TChargingOrderShareRecord::getChargingOrderId, order.getId())
                        .eq(TChargingOrderShareRecord::getPayChannel, "ICBC")
                        .eq(TChargingOrderShareRecord::getNeedOnlineShare, true)
                        .eq(TChargingOrderShareRecord::getOnlineShareStatus, "SUCCESS")
                        .eq(TChargingOrderShareRecord::getDelFlag, false)
                        .orderByAsc(TChargingOrderShareRecord::getId));
        if (shareRecords == null || shareRecords.isEmpty()) {
            return R.ok();
        }
        BigDecimal thirdPayAmount = getRefundableThirdPayAmount(order);
        if (thirdPayAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return R.fail("订单已分账但第三方支付金额异常,禁止退款");
        }
        BigDecimal cumulativeThirdRefund = money(order.getThirdRefundAmount()).add(thirdRefundAmount).min(thirdPayAmount).setScale(2, RoundingMode.HALF_UP);
        for (TChargingOrderShareRecord shareRecord : shareRecords) {
            BigDecimal shareAmount = money(shareRecord.getShareAmount());
            if (shareAmount.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
        } else if (Integer.valueOf(3).equals(payType)) {
            RefundReq dto = new RefundReq();
            dto.setOutTradeNo(order.getRechargeSerialNumber());
            dto.setOutRequestNo(refund.getRefundCode());
            dto.setRefundAmount(thirdRefundAmount.toString());
            dto.setRefundReason(SPECIAL_REFUND_TITLE);
            RefundResp resp = h5AliPaymentClient.refund(dto).getData();
            if (resp != null && "10000".equals(resp.getCode())) {
                refund.setRefundSerialNumber(resp.getTradeNo());
                refund.setRefundTime(LocalDateTime.now());
                finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
                success = true;
            BigDecimal targetRefundAmount = shareAmount.multiply(cumulativeThirdRefund).divide(thirdPayAmount, 2, RoundingMode.HALF_UP).min(shareAmount).setScale(2, RoundingMode.HALF_UP);
            BigDecimal refundedShareAmount;
            try {
                refundedShareAmount = queryIcbcShareRefundAmount(order, shareRecord);
            } catch (ServiceException e) {
                return R.fail(e.getMessage());
            }
            BigDecimal shareRefundAmount = targetRefundAmount.subtract(refundedShareAmount).setScale(2, RoundingMode.HALF_UP);
            if (shareRefundAmount.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            R result = refundIcbcShareRecord(order, shareRecord, refund, shareRefundAmount);
            if (result.getCode() != 200) {
                return result;
            }
        }
        return success ? R.ok() : R.fail("退款失败,请检查支付渠道或稍后重试");
        return R.ok();
    }
    private BigDecimal queryIcbcShareRefundAmount(TChargingOrder order, TChargingOrderShareRecord shareRecord) {
        try {
            IcbcSubOrderQueryReq queryReq = IcbcAdminFundOperationSupport.buildSubOrderQueryRequest(order, shareRecord);
            R<IcbcSubOrderQueryResp> queryResult = icbcPaymentClient.querySubOrder(queryReq);
            IcbcSubOrderQueryResp queryResp = queryResult == null ? null : queryResult.getData();
            if (queryResult == null || queryResult.getCode() != 200 || queryResp == null || !"SUCCESS".equals(queryResp.getStatus())) {
                throw new ServiceException(queryResult == null ? "工行子订单查询无响应" : firstNotBlank(queryResult.getMsg(), "工行子订单查询失败"));
            }
            return parseIcbcAmount(queryResp.getOrderRefundAmnt());
        } catch (IllegalArgumentException e) {
            throw new ServiceException(e.getMessage());
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new ServiceException("工行子订单查询异常:" + firstNotBlank(e.getMessage(), "未知异常"));
        }
    }
    private R refundIcbcShareRecord(TChargingOrder order, TChargingOrderShareRecord shareRecord, TChargingOrderRefund refund, BigDecimal shareRefundAmount) {
        String suffix = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
        String retSubOrderId = ("SPSUB" + refund.getRefundCode() + shareRecord.getId()).substring(0, Math.min(50, ("SPSUB" + refund.getRefundCode() + shareRecord.getId()).length()));
        String outtrxSerialNo = ("SPSUBREF" + suffix + shareRecord.getId()).substring(0, Math.min(50, ("SPSUBREF" + suffix + shareRecord.getId()).length()));
        try {
            IcbcSubOrderRefundReq refundReq = IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
                    order, shareRecord, shareRefundAmount, retSubOrderId, outtrxSerialNo);
            refundReq.setMerSpInfor("特殊退款分账退回:" + order.getCode());
            R<IcbcSubOrderRefundResp> refundResult = icbcPaymentClient.refundSubOrder(refundReq);
            IcbcSubOrderRefundResp refundResp = refundResult == null ? null : refundResult.getData();
            if (refundResult == null || refundResult.getCode() != 200 || refundResp == null || !"SUCCESS".equals(refundResp.getStatus())) {
                String message = refundResult == null ? "工行分账退款无响应" : firstNotBlank(refundResult.getMsg(), refundResp == null ? null : refundResp.getRawResponse(), "工行分账退款失败");
                log.error("特殊退款分账退回失败,订单号:{},退款单号:{},分账记录:{},金额:{},原因:{}",
                        order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, message);
                return R.fail("分账退回失败:" + message);
            }
            log.info("特殊退款分账退回成功,订单号:{},退款单号:{},分账记录:{},金额:{}",
                    order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount);
            return R.ok();
        } catch (IllegalArgumentException | ServiceException e) {
            log.error("特殊退款分账退回失败,订单号:{},退款单号:{},分账记录:{},金额:{}",
                    order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, e);
            return R.fail("分账退回失败:" + e.getMessage());
        } catch (Exception e) {
            log.error("特殊退款分账退回异常,订单号:{},退款单号:{},分账记录:{},金额:{}",
                    order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, e);
            return R.fail("分账退回异常:" + firstNotBlank(e.getMessage(), "未知异常"));
        }
    }
    private BigDecimal parseIcbcAmount(String amount) {
        if (!StringUtils.hasLength(amount)) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        String value = amount.trim();
        try {
            if (value.contains(".")) {
                return money(new BigDecimal(value));
            }
            return new BigDecimal(value).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        } catch (NumberFormatException e) {
            throw new ServiceException("工行金额格式异常:" + amount);
        }
    }
    private R validateOperationPassword(PayOrderRefundDto payOrderQueryDto) {
@@ -7038,6 +7198,20 @@
        }
    }
    private String buildIcbcPaymentFailReason(IcbcPaymentResp resp) {
        if (resp == null) {
            return "工行支付服务未返回支付数据";
        }
        String reason = firstNotBlank(resp.getRawResponse(), resp.getStatus());
        if (!StringUtils.hasLength(reason)) {
            return "工行支付服务返回数据不完整";
        }
        if (reason.length() > 500) {
            return reason.substring(0, 500);
        }
        return reason;
    }
    private void triggerChargingOrderShare(TChargingOrder chargingOrder, Site site) {
        if (!isNeedChargingOrderShare(chargingOrder)) {
            return;
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TUserTagController.java
@@ -14,6 +14,7 @@
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -59,8 +60,11 @@
    //获取标签map
    @PostMapping(value = "/getTagMap")
    public R<Map<Integer, String>> getTagMap(@RequestBody List<Integer> ids) {
        if (ids == null || ids.isEmpty()) {
            return R.ok(new HashMap<>());
        }
        List<TUserTag> list = tUserTagService.lambdaQuery().in(!ids.isEmpty(), TUserTag::getId, ids).list();
        List<TUserTag> list = tUserTagService.lambdaQuery().in(TUserTag::getId, ids).list();
        Map<Integer, String> idNameMap = list.stream()
                .collect(Collectors.toMap(
                        TUserTag::getId,
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
@@ -13,18 +13,20 @@
@Configuration
@ConfigurationProperties(prefix = "icbc.pay")
public class IcbcPayProperties {
    private static final String ICBC_GATEWAY = "https://gw.open.icbc.com.cn/api/cardbusiness";
    private String appId;
    private String privateKey;
    private String icbcPublicKey;
    private String signType = "RSA2";
    private String charset = "UTF-8";
    private String format = "json";
    private String gatewayUrl;
    private String orderQueryUrl;
    private String refundUrl;
    private String subOrderSplitUrl;
    private String subOrderRefundUrl;
    private String subOrderQueryUrl;
    private String gatewayUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/consumepurchase/V2";
    private String orderQueryUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/orderqry/V2";
    private String refundUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/merrefund/V1";
    private String subOrderSplitUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/sersuborder/V1";
    private String subOrderRefundUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/sersuborderrefund/V1";
    private String subOrderQueryUrl = ICBC_GATEWAY + "/order/suborderquery/V1";
    private String notifyUrl;
    private String merId;
    private String merPrtclNo;
sql/20260720_special_refund_menu.sql
New file
@@ -0,0 +1,60 @@
-- 支付订单:特殊退款处理菜单及执行按钮权限
SET @payment_order_id := (
    SELECT menu_id
    FROM sys_menu
    WHERE menu_name = '支付订单'
      AND (perms = '/paymentOrder' OR path = '/order/payMentOrder' OR path = 'payMentOrder')
    ORDER BY menu_id ASC
    LIMIT 1
);
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '特殊退款处理', @payment_order_id, 3, '/order/payMentOrder/specialRefund', 'order/payMentOrder/specialRefund', 1, 0, 'C', '0', 0, '/paymentOrder/specialRefund', '#', 'admin', NOW(), '支付订单特殊退款处理'
WHERE @payment_order_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM sys_menu
      WHERE parent_id = @payment_order_id
        AND (perms = '/paymentOrder/specialRefund' OR path = '/order/payMentOrder/specialRefund')
  );
SET @special_refund_id := (
    SELECT menu_id
    FROM sys_menu
    WHERE parent_id = @payment_order_id
      AND (perms = '/paymentOrder/specialRefund' OR path = '/order/payMentOrder/specialRefund')
    ORDER BY menu_id ASC
    LIMIT 1
);
UPDATE sys_menu
SET menu_name = '特殊退款处理',
    parent_id = @payment_order_id,
    order_num = 3,
    path = '/order/payMentOrder/specialRefund',
    component = 'order/payMentOrder/specialRefund',
    menu_type = 'C',
    visible = '0',
    status = 0,
    perms = '/paymentOrder/specialRefund',
    update_time = NOW()
WHERE @special_refund_id IS NOT NULL
  AND menu_id = @special_refund_id;
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '发起特殊退款', @special_refund_id, 1, '', NULL, 1, 0, 'F', '0', 0, '/paymentOrder/specialRefund/refund', '#', 'admin', NOW(), '发起特殊退款按钮权限'
WHERE @special_refund_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM sys_menu
      WHERE parent_id = @special_refund_id
        AND perms = '/paymentOrder/specialRefund/refund'
  );
-- 管理员角色补上新权限;其他角色请在角色管理中按需勾选。
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id
FROM sys_menu
WHERE menu_id IN (@special_refund_id)
   OR parent_id = @special_refund_id;