longz
2026-07-29 36e527ebd18d95d8d0f94f3aec3d3ff3f49b6570
各类修复
4个文件已添加
9个文件已修改
643 ■■■■■ 已修改文件
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/ChargingPileEditAsyncService.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TChargingPileServiceImpl.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/TChargingPileEditPerformanceTest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQuery.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQueryTest.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingDetailMapperTest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TOrderInvoicePageListQueryTest.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/ChargingPileEditAsyncService.java
New file
@@ -0,0 +1,73 @@
package com.ruoyi.chargingPile.service.impl;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
import com.ruoyi.integration.api.feignClient.DongFuStationClient;
import com.ruoyi.integration.api.feignClient.SendMessageClient;
import com.ruoyi.integration.api.feignClient.TCECClient;
import com.ruoyi.integration.api.model.QrCodeDelivery;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class ChargingPileEditAsyncService {
    private static final String QR_CODE_PREFIX = "https://mxcd.zhinenganguan.com?No=";
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private TCECClient tcecClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private DongFuStationClient dongFuStationClient;
    @Async
    public void publishEditNotifications(TChargingPile chargingPile, List<TChargingGun> chargingGuns) {
        if (chargingPile == null) {
            return;
        }
        if (chargingGuns != null) {
            for (TChargingGun chargingGun : chargingGuns) {
                deliverQrCode(chargingPile, chargingGun);
            }
        }
        runSafely("TCEC监管推送", () -> tcecClient.superviseNotificationStationInfo(chargingPile.getSiteId()));
        runSafely("成都监管推送", () -> chengDuJianGuanClient.notificationStationInfo(chargingPile.getSiteId()));
        runSafely("东涪推送", () -> dongFuStationClient.pushChargingPile());
    }
    private void deliverQrCode(TChargingPile chargingPile, TChargingGun chargingGun) {
        if (chargingGun == null) {
            return;
        }
        runSafely("二维码下发", () -> {
            QrCodeDelivery delivery = new QrCodeDelivery();
            delivery.setCharging_pile_code(chargingPile.getCode());
            delivery.setCharging_gun_code(chargingGun.getCode());
            delivery.setCode_format(1);
            delivery.setPrefix_length(QR_CODE_PREFIX.length());
            delivery.setCode_prefix(QR_CODE_PREFIX);
            delivery.setQr_code(QR_CODE_PREFIX + chargingGun.getFullNumber());
            delivery.setCreate_time(new Date());
            delivery.setLast_time(new Date());
            sendMessageClient.qrCodeDelivery(delivery);
        });
    }
    private void runSafely(String operation, Runnable action) {
        try {
            action.run();
        } catch (Exception e) {
            log.error("编辑充电桩后{}失败", operation, e);
        }
    }
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TChargingPileServiceImpl.java
@@ -36,7 +36,6 @@
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.integration.api.feignClient.*;
import com.ruoyi.integration.api.model.QrCodeDelivery;
import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
import com.ruoyi.integration.api.vo.AddDevice;
import com.ruoyi.integration.api.vo.AddDeviceResp;
@@ -134,8 +133,6 @@
    private TAccountingStrategyDetailService accountingStrategyDetailService;
    
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private TokenService tokenService;
    @Resource
    private TChargingPileNotificationService chargingPileNotificationService;
@@ -147,6 +144,8 @@
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private DongFuStationClient dongFuStationClient;
    @Resource
    private ChargingPileEditAsyncService chargingPileEditAsyncService;
    
@@ -574,73 +573,52 @@
            return AjaxResult.error("设备编号已存在");
        }
        TChargingPile tChargingPile = this.getById(chargingPile.getId());
        //桩号不相同,需要重新绑定设备
        if(!tChargingPile.getCode().equals(chargingPile.getCode())){
            ShowDeviceResp showDeviceResp = iotInterfaceClient.showDeviceRequest(tChargingPile.getCode()).getData();
            if(null != showDeviceResp){
        boolean codeChanged = shouldSyncIotDevice(tChargingPile, chargingPile);
        // 设备编号变化时才重新绑定华为 IoT,普通资料编辑不访问外部平台。
        if(codeChanged){
            ShowDeviceResp oldDevice = iotInterfaceClient.showDeviceRequest(tChargingPile.getCode()).getData();
            if(null != oldDevice){
                DeleteDeviceResp deleteDeviceResp = iotInterfaceClient.deleteDevice(tChargingPile.getCode()).getData();
                if(null == deleteDeviceResp){
                    return AjaxResult.error("删除设备异常,请查看华为设备管理");
                }
            }
        }
        ShowDeviceResp showDeviceResp = iotInterfaceClient.showDeviceRequest(tChargingPile.getCode()).getData();
        if(null == showDeviceResp){
            //调用华为Iot创建设备
            AddDevice addDevice = new AddDevice();
            addDevice.setProductId("66da68d21837002b28b34ec0");
            addDevice.setNodeId(chargingPile.getCode());
            addDevice.setDeviceName(chargingPile.getName());
            addDevice.setDescription(chargingPile.getNumber().toString());
            AddDeviceResp deviceResp = iotInterfaceClient.addDevice(addDevice).getData();
            if(null != deviceResp){
                int httpStatusCode = deviceResp.getHttpStatusCode();
                if(httpStatusCode == 201){
            ShowDeviceResp newDevice = iotInterfaceClient.showDeviceRequest(chargingPile.getCode()).getData();
            if(null == newDevice){
                AddDevice addDevice = new AddDevice();
                addDevice.setProductId("66da68d21837002b28b34ec0");
                addDevice.setNodeId(chargingPile.getCode());
                addDevice.setDeviceName(chargingPile.getName());
                addDevice.setDescription(chargingPile.getNumber().toString());
                AddDeviceResp deviceResp = iotInterfaceClient.addDevice(addDevice).getData();
                if(null != deviceResp && deviceResp.getHttpStatusCode() == 201){
                    chargingPile.setIotdDeviceId(deviceResp.getDeviceId());
                }else{
                    log.error("华为创建设备失败" + JSON.toJSONString(deviceResp));
                    log.error("华为创建设备失败:{}", JSON.toJSONString(deviceResp));
                    return AjaxResult.error("华为创建设备失败");
                }
            }else{
                return AjaxResult.error("华为创建设备失败");
                chargingPile.setIotdDeviceId(newDevice.getDeviceId());
            }
        }
        this.updateById(chargingPile);
        
        List<TChargingGun> list = chargingGunService.list(new LambdaQueryWrapper<TChargingGun>().eq(TChargingGun::getChargingPileId, chargingPile.getId()).eq(TChargingGun::getDelFlag, 0));
        for (TChargingGun chargingGun : list) {
            TChargingGun chargingGun1 = new TChargingGun();
            chargingGun1.setId(chargingGun.getId());
            String stableFullNumber = StringUtils.isNotEmpty(chargingGun.getFullNumber()) ? chargingGun.getFullNumber() : chargingPile.getCode() + chargingGun.getCode();
            chargingGun1.setFullNumber(stableFullNumber);
            chargingGun1.setSiteId(chargingPile.getSiteId());
            chargingGunService.updateById(chargingGun1);
            //下发硬件充电二维码
            String code_prefix = "https://mxcd.zhinenganguan.com?No=";
            QrCodeDelivery qrCodeDelivery = new QrCodeDelivery();
            qrCodeDelivery.setCharging_pile_code(chargingPile.getCode());
            qrCodeDelivery.setCharging_gun_code(chargingGun.getCode());
            qrCodeDelivery.setCode_format(1);
            qrCodeDelivery.setPrefix_length(code_prefix.length());
            qrCodeDelivery.setCode_prefix(code_prefix);
            qrCodeDelivery.setQr_code(code_prefix + stableFullNumber);
            qrCodeDelivery.setCreate_time(new Date());
            qrCodeDelivery.setLast_time(new Date());
            sendMessageClient.qrCodeDelivery(qrCodeDelivery);
            chargingGun.setFullNumber(stableFullNumber);
            chargingGun.setSiteId(chargingPile.getSiteId());
        }
        tcecClient.superviseNotificationStationInfo(chargingPile.getSiteId());
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
        threadPoolExecutor.execute(()->{
            //推送成都监管
            chengDuJianGuanClient.notificationStationInfo(chargingPile.getSiteId());
            // TODO 东涪推送
            dongFuStationClient.pushChargingPile();
        });
        if (!list.isEmpty()) {
            chargingGunService.updateBatchById(list);
        }
        chargingPileEditAsyncService.publishEditNotifications(chargingPile, list);
        return AjaxResult.success();
    }
    static boolean shouldSyncIotDevice(TChargingPile current, TChargingPile edited) {
        return current != null && edited != null && !Objects.equals(current.getCode(), edited.getCode());
    }
    
    
    /**
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/TChargingPileEditPerformanceTest.java
New file
@@ -0,0 +1,23 @@
package com.ruoyi.chargingPile.service.impl;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TChargingPileEditPerformanceTest {
    @Test
    void onlySynchronizesIotWhenDeviceCodeChanges() {
        TChargingPile current = new TChargingPile();
        current.setCode("PILE-001");
        TChargingPile unchanged = new TChargingPile();
        unchanged.setCode("PILE-001");
        TChargingPile changed = new TChargingPile();
        changed.setCode("PILE-002");
        assertFalse(TChargingPileServiceImpl.shouldSyncIotDevice(current, unchanged));
        assertTrue(TChargingPileServiceImpl.shouldSyncIotDevice(current, changed));
    }
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -8,6 +8,7 @@
import com.ruoyi.account.api.dto.GiveVipDto;
import com.ruoyi.account.api.dto.UserListQueryDto;
import com.ruoyi.account.api.feignClient.AppUserClient;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.WebUtils;
@@ -51,6 +52,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * <p>
@@ -265,6 +270,8 @@
        orderQuery.setPageCurr(1);
        orderQuery.setPageSize(100000);
        List<ChargingOrderVO> list = chargingOrderService.chargingOrderList(orderQuery).getRecords();
        R<List<TAppUser>> usersResult = appUserClient.getUserByIds(userIds);
        fillVipChargingOrderPhones(list, usersResult == null ? null : usersResult.getData());
        List<EnterpriseChargingDetailExport> exports = new ArrayList<>();
        for (ChargingOrderVO order : list) {
            exports.add(buildVipChargingOrderExport(order));
@@ -272,6 +279,25 @@
        writeVipChargingOrderWorkbook(exports);
    }
    static void fillVipChargingOrderPhones(List<ChargingOrderVO> orders, List<TAppUser> users) {
        if (orders == null || orders.isEmpty() || users == null || users.isEmpty()) {
            return;
        }
        Map<Long, TAppUser> userMap = users.stream()
                .filter(Objects::nonNull)
                .filter(user -> user.getId() != null)
                .collect(Collectors.toMap(TAppUser::getId, Function.identity(), (first, second) -> first));
        for (ChargingOrderVO order : orders) {
            if (order == null || StringUtils.hasLength(order.getPhone())) {
                continue;
            }
            TAppUser user = userMap.get(order.getAppUserId());
            if (user != null) {
                order.setPhone(user.getPhone());
            }
        }
    }
    static EnterpriseChargingDetailExport buildVipChargingOrderExport(ChargingOrderVO order) {
        EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport();
        export.setCode(order.getCode());
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQuery.java
@@ -1,5 +1,6 @@
package com.ruoyi.order.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -30,9 +31,11 @@
    private Integer siteId;
    @ApiModelProperty("Order create start time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime1;
    @ApiModelProperty("Order create end time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime2;
    @ApiModelProperty("Order code")
    private String code;
@@ -53,14 +56,18 @@
    private Integer orderSource;
    @ApiModelProperty("Charging start time begin")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime startTime1;
    @ApiModelProperty("Charging start time end")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime startTime2;
    @ApiModelProperty("Charging end time begin")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime endTime1;
    @ApiModelProperty("Charging end time end")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime endTime2;
}
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -89,8 +89,13 @@
    List<EnterpriseChargingDetailVO> enterpriseChargingDetailExport(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<Map<String, Object>> enterpriseChargingPeriodCounts(@Param("orderIds") List<Long> orderIds);
    List<TChargingOrderRefund> getRefundList(@Param("pageInfo")PageInfo<TChargingOrderRefund> pageInfo,@Param("data")  ChargingRefundDto chargingRefundDto);
    List<TChargingOrderRefund> getRefundListWithoutOrderJoin(@Param("pageInfo") PageInfo<TChargingOrderRefund> pageInfo,
            @Param("data") ChargingRefundDto chargingRefundDto);
    List<ChargingOrderGroup> chargingOrderGroup(@Param("chargingPercentProvinceDto")ChargingPercentProvinceDto chargingPercentProvinceDto);
    List<SixChargingDto> chargePartner(@Param("sixBefore") String sixBefore, @Param("siteIds") List<Integer> siteIds);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -140,6 +140,7 @@
    private static final String SHARE_AFTER_COMMIT_RESOURCE_PREFIX = "chargingOrderShareAfterCommit:";
    private static final String SPECIAL_REFUND_TITLE = "特殊退款处理";
    private static final int RESERVATION_RECOMMENDATION_HISTORY_DAYS = 30;
    private static final int ENTERPRISE_DETAIL_PERIOD_BATCH_SIZE = 500;
    @Override
    public ReservationRecommendationVO getReservationRecommendation(Integer siteId) {
@@ -3139,13 +3140,74 @@
    public R<PageInfo<EnterpriseChargingDetailVO>> enterpriseChargingDetail(EnterpriseChargingAnalysisQuery query) {
        PageInfo<EnterpriseChargingDetailVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<EnterpriseChargingDetailVO> list = this.baseMapper.enterpriseChargingDetail(pageInfo, query);
        fillEnterpriseChargingPeriodCounts(list);
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    @Override
    public List<EnterpriseChargingDetailVO> enterpriseChargingDetailExport(EnterpriseChargingAnalysisQuery query) {
        return this.baseMapper.enterpriseChargingDetailExport(query);
        List<EnterpriseChargingDetailVO> list = this.baseMapper.enterpriseChargingDetailExport(query);
        fillEnterpriseChargingPeriodCounts(list);
        return list;
    }
    private void fillEnterpriseChargingPeriodCounts(List<EnterpriseChargingDetailVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> orderIds = list.stream()
                .map(EnterpriseChargingDetailVO::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, Integer> periodCountMap = new HashMap<>();
        for (int start = 0; start < orderIds.size(); start += ENTERPRISE_DETAIL_PERIOD_BATCH_SIZE) {
            int end = Math.min(start + ENTERPRISE_DETAIL_PERIOD_BATCH_SIZE, orderIds.size());
            List<Map<String, Object>> rows = this.baseMapper.enterpriseChargingPeriodCounts(orderIds.subList(start, end));
            if (rows == null) {
                continue;
            }
            for (Map<String, Object> row : rows) {
                Long orderId = mapLong(row, "orderId", "orderid", "charging_order_id");
                Integer periodCount = mapInteger(row, "periodCount", "periodcount");
                if (orderId != null) {
                    periodCountMap.put(orderId, periodCount == null ? 0 : periodCount);
                }
            }
        }
        for (EnterpriseChargingDetailVO detail : list) {
            detail.setPeriodCount(periodCountMap.getOrDefault(detail.getId(), 0));
        }
    }
    private Long mapLong(Map<String, Object> row, String... keys) {
        Object value = mapValue(row, keys);
        return value instanceof Number ? ((Number) value).longValue() : null;
    }
    private Integer mapInteger(Map<String, Object> row, String... keys) {
        Object value = mapValue(row, keys);
        return value instanceof Number ? ((Number) value).intValue() : null;
    }
    private Object mapValue(Map<String, Object> row, String... keys) {
        if (row == null) {
            return null;
        }
        for (String key : keys) {
            if (row.containsKey(key)) {
                return row.get(key);
            }
        }
        for (Map.Entry<String, Object> entry : row.entrySet()) {
            for (String key : keys) {
                if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }
    private EnterpriseChargingAnalysisQuery buildEnterpriseRowQuery(EnterpriseChargingAnalysisQuery source, EnterpriseChargingAnalysisVO row) {
@@ -3176,7 +3238,14 @@
    @Override
    public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) {
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize());
        List<TChargingOrderRefund> list = this.baseMapper.getRefundList(pageInfo, chargingRefundDto);
        boolean withoutOrderJoin = !StringUtils.hasLength(chargingRefundDto.getSerialNumber())
                && (chargingRefundDto.getSiteIds() == null || chargingRefundDto.getSiteIds().isEmpty());
        List<TChargingOrderRefund> list = withoutOrderJoin
                ? this.baseMapper.getRefundListWithoutOrderJoin(pageInfo, chargingRefundDto)
                : this.baseMapper.getRefundList(pageInfo, chargingRefundDto);
        if (withoutOrderJoin) {
            enrichRefundOrderInfo(list);
        }
        for (TChargingOrderRefund refund : list) {
            refund.setUid(refund.getType() + "-" + refund.getId());
        }
@@ -3184,6 +3253,63 @@
        
        return R.ok(pageInfo);
    }
    private void enrichRefundOrderInfo(List<TChargingOrderRefund> refunds) {
        if (refunds == null || refunds.isEmpty()) {
            return;
        }
        Map<Integer, List<Long>> orderIdsByType = refunds.stream()
                .filter(Objects::nonNull)
                .filter(refund -> refund.getType() != null && StringUtils.hasLength(refund.getOrderId()))
                .collect(Collectors.groupingBy(TChargingOrderRefund::getType,
                        Collectors.mapping(refund -> Long.valueOf(refund.getOrderId()), Collectors.toList())));
        Map<Long, TChargingOrder> chargingOrderMap = orderIdsByType.getOrDefault(1, Collections.emptyList()).isEmpty()
                ? Collections.emptyMap()
                : indexByLongId(listByIds(orderIdsByType.get(1)), TChargingOrder::getId);
        Map<Long, TShoppingOrder> shoppingOrderMap = orderIdsByType.getOrDefault(2, Collections.emptyList()).isEmpty()
                ? Collections.emptyMap()
                : indexByLongId(shoppingOrderService.listByIds(orderIdsByType.get(2)), TShoppingOrder::getId);
        Map<Long, TVipOrder> vipOrderMap = orderIdsByType.getOrDefault(3, Collections.emptyList()).isEmpty()
                ? Collections.emptyMap()
                : indexByLongId(vipOrderService.listByIds(orderIdsByType.get(3)), TVipOrder::getId);
        for (TChargingOrderRefund refund : refunds) {
            if (refund == null || refund.getType() == null || !StringUtils.hasLength(refund.getOrderId())) {
                continue;
            }
            Long orderId = Long.valueOf(refund.getOrderId());
            if (refund.getType() == 1) {
                TChargingOrder order = chargingOrderMap.get(orderId);
                if (order != null) {
                    refund.setSerailNumber(order.getRechargeSerialNumber());
                    refund.setPaymentAmount(order.getPaymentAmount());
                    refund.setPayTime(order.getCreateTime());
                    refund.setUserId(order.getAppUserId() == null ? null : String.valueOf(order.getAppUserId()));
                    refund.setSiteId(order.getSiteId());
                    refund.setPayMethod(order.getPayMethod());
                }
            } else if (refund.getType() == 2) {
                TShoppingOrder order = shoppingOrderMap.get(orderId);
                if (order != null) {
                    refund.setSerailNumber(order.getSerialNumber());
                    refund.setPaymentAmount(order.getPaymentAmount());
                    refund.setPayTime(order.getCreateTime());
                    refund.setUserId(order.getAppUserId() == null ? null : String.valueOf(order.getAppUserId()));
                    refund.setPayMethod(3);
                }
            } else if (refund.getType() == 3) {
                TVipOrder order = vipOrderMap.get(orderId);
                if (order != null) {
                    refund.setSerailNumber(order.getSerialNumber());
                    refund.setPaymentAmount(order.getPaymentAmount());
                    refund.setPayTime(order.getCreateTime());
                    refund.setUserId(order.getAppUserId() == null ? null : String.valueOf(order.getAppUserId()));
                    refund.setPayMethod(3);
                }
            }
        }
    }
    
    @Override
    public List<ChargingOrderGroup> chargingOrderGroup(ChargingPercentProvinceDto chargingPercentProvinceDto) {
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
@@ -8,6 +8,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.feignClient.AppUserClient;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.account.api.model.TAppUserCar;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
@@ -390,14 +391,32 @@
                return new PageInfo<>();
            }
        }
        query.setUserIds(tAppUsers.stream().map(TAppUser::getId).collect(Collectors.toList()));
        PageInfo<TOrderInvoiceVO> pageInfo = new PageInfo<>(query.getPageCurr(),query.getPageSize());
        List<TOrderInvoiceVO> list = this.baseMapper.pageList(query,pageInfo);
        List<Site> siteList = siteClient.getSiteAll().getData();
        for (TOrderInvoiceVO tOrderInvoiceVO : list) {
            tOrderInvoiceVO.setUid(tOrderInvoiceVO.getId().toString());
            List<Long> collect = orderInvoiceDetailService.lambdaQuery().eq(TOrderInvoiceDetail::getOrderInvoiceId, tOrderInvoiceVO.getId()).list().stream()
                    .map(TOrderInvoiceDetail::getOrderId).collect(Collectors.toList());
            query.setUserIds(tAppUsers.stream().map(TAppUser::getId).collect(Collectors.toList()));
            PageInfo<TOrderInvoiceVO> pageInfo = new PageInfo<>(query.getPageCurr(),query.getPageSize());
            List<TOrderInvoiceVO> list = this.baseMapper.pageList(query,pageInfo);
            if (CollectionUtils.isEmpty(list)) {
                pageInfo.setRecords(list);
                return pageInfo;
            }
            List<Long> invoiceIds = list.stream().map(TOrderInvoiceVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
            List<TOrderInvoiceDetail> orderInvoiceDetailList = invoiceIds.isEmpty() ? new ArrayList<>()
                    : orderInvoiceDetailService.list(new LambdaQueryWrapper<TOrderInvoiceDetail>()
                    .in(TOrderInvoiceDetail::getOrderInvoiceId, invoiceIds));
            Map<Long, List<TOrderInvoiceDetail>> invoiceDetailMap = orderInvoiceDetailList.stream()
                    .collect(Collectors.groupingBy(TOrderInvoiceDetail::getOrderInvoiceId));
            List<Long> appUserIds = list.stream().map(TOrderInvoiceVO::getAppUserId)
                    .filter(Objects::nonNull).distinct().collect(Collectors.toList());
            R<List<TAppUser>> appUserResult = appUserIds.isEmpty() ? null : appUserClient.getUserByIds(appUserIds);
            List<TAppUser> pageUsers = appUserResult == null || appUserResult.getData() == null
                    ? new ArrayList<>() : appUserResult.getData();
            Map<Long, TAppUser> appUserMap = pageUsers.stream()
                    .filter(user -> user != null && user.getId() != null)
                    .collect(Collectors.toMap(TAppUser::getId, user -> user, (first, second) -> first));
            List<Site> siteList = siteClient.getSiteAll().getData();
            for (TOrderInvoiceVO tOrderInvoiceVO : list) {
                tOrderInvoiceVO.setUid(tOrderInvoiceVO.getId().toString());
                List<Long> collect = invoiceDetailMap.getOrDefault(tOrderInvoiceVO.getId(), Collections.emptyList()).stream()
                        .map(TOrderInvoiceDetail::getOrderId).collect(Collectors.toList());
            Integer siteId = tOrderInvoiceVO.getSiteId();
            if (siteId == null && !CollectionUtils.isEmpty(collect) && Integer.valueOf(1).equals(tOrderInvoiceVO.getOrderType())) {
                List<TChargingOrder> chargingOrders = chargingOrderService.listByIds(collect);
@@ -426,28 +445,16 @@
                    }
                }
            }
            // 将其全部转化为String
            tOrderInvoiceVO.setIds(collect.stream().map(String::valueOf).collect(Collectors.toList()));
            TAppUser appUser = appUserClient.getUserById(tOrderInvoiceVO.getAppUserId()).getData();
            if (appUser!=null){
                tOrderInvoiceVO.setUserPhone(appUser.getPhone());
                // 将其全部转化为String
                tOrderInvoiceVO.setIds(collect.stream().map(String::valueOf).collect(Collectors.toList()));
                TAppUser appUser = appUserMap.get(tOrderInvoiceVO.getAppUserId());
                if (appUser!=null){
                    tOrderInvoiceVO.setUserPhone(appUser.getPhone());
                }
                fillInvoiceDetailAmount(tOrderInvoiceVO, orderInvoiceDetailList);
            }
        }
        // 查询这个开票的订单ids
        if (!list.isEmpty()){
            List<Long> ids = list.stream().map(TOrderInvoiceVO::getId).collect(Collectors.toList());
            List<TOrderInvoiceDetail> orderInvoiceDetailList = orderInvoiceDetailService.list(new LambdaQueryWrapper<TOrderInvoiceDetail>()
                    .in(TOrderInvoiceDetail::getOrderInvoiceId, ids));
            List<TAppUser> finalTAppUsers = tAppUsers;
            list.forEach(e -> fillInvoiceDetailAmount(e, orderInvoiceDetailList));
        }
        pageInfo.setRecords(list);
            pageInfo.setRecords(list);
        return pageInfo;
    }
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -400,6 +400,93 @@
        order by o.refund_time desc
    </select>
    <select id="getRefundListWithoutOrderJoin" resultType="com.ruoyi.order.api.model.TChargingOrderRefund">
        SELECT * FROM (
        SELECT
        re.id,
        re.`code`,
        re.refund_code,
        re.refund_title,
        re.refund_reason,
        re.refund_amount,
        re.balance_refund_amount,
        re.third_refund_amount,
        re.refund_total_amount,
        re.refund_time,
        re.pay_type,
        re.pay_amount,
        re.refund_remark,
        re.charging_order_id AS order_id,
        1 AS type
        FROM t_charging_order_refund re
        <where>
            <if test="data.specialRefund != null and data.specialRefund">
                AND re.refund_title = '特殊退款处理'
            </if>
            <if test="data.code != null and data.code != ''">
                AND re.code LIKE CONCAT('%', #{data.code}, '%')
            </if>
            <if test="data.refundCode != null and data.refundCode != ''">
                AND re.refund_code LIKE CONCAT('%', #{data.refundCode}, '%')
            </if>
        </where>
        <if test="data.specialRefund == null or !data.specialRefund">
        UNION ALL
        SELECT
        re.id,
        re.`code`,
        re.refund_code,
        re.refund_title,
        re.refund_reason,
        re.refund_amount,
        0 AS balance_refund_amount,
        re.refund_amount AS third_refund_amount,
        re.refund_total_amount,
        re.refund_time,
        re.pay_type,
        re.pay_amount,
        re.refund_remark,
        re.shopping_order_id AS order_id,
        2 AS type
        FROM t_shopping_order_refund re
        <where>
            <if test="data.code != null and data.code != ''">
                AND re.code LIKE CONCAT('%', #{data.code}, '%')
            </if>
            <if test="data.refundCode != null and data.refundCode != ''">
                AND re.refund_code LIKE CONCAT('%', #{data.refundCode}, '%')
            </if>
        </where>
        UNION ALL
        SELECT
        re.id,
        re.`code`,
        re.refund_code,
        re.refund_title,
        re.refund_reason,
        re.refund_amount,
        0 AS balance_refund_amount,
        re.refund_amount AS third_refund_amount,
        re.refund_total_amount,
        re.refund_time,
        re.pay_type,
        re.pay_amount,
        re.refund_remark,
        re.vip_order_id AS order_id,
        3 AS type
        FROM t_vip_order_refund re
        <where>
            <if test="data.code != null and data.code != ''">
                AND re.code LIKE CONCAT('%', #{data.code}, '%')
            </if>
            <if test="data.refundCode != null and data.refundCode != ''">
                AND re.refund_code LIKE CONCAT('%', #{data.refundCode}, '%')
            </if>
        </where>
        </if>
        ) o
        ORDER BY o.refund_time DESC
    </select>
    <select id="chargingOrderGroup" resultType="com.ruoyi.common.core.dto.ChargingOrderGroup">
        SELECT SUM(tc.charging_capacity) as charging_capacity,tc.site_id
        FROM t_charging_order tc
@@ -1135,7 +1222,7 @@
        a.recharge_amount as rechargeAmount,
        a.payment_amount as orderAmount,
        a.vip_discount_amount as vipDiscountAmount,
        e.refund_amount as refundAmount,
        a.refund_amount as refundAmount,
        a.is_whitelist as isWhitelist,
        IFNULL(a.whitelist_electric_refund_amount, 0) + IFNULL(a.whitelist_service_refund_amount, 0) as whitelistRefundAmount,
        IFNULL(a.whitelist_electric_refund_amount, 0) as whitelistElectricRefundAmount,
@@ -1145,7 +1232,7 @@
        a.payment_amount as paymentAmount,
        a.third_pay_amount as thirdPayAmount,
        a.balance_pay_amount as balancePayAmount,
        e.refund_amount as residualAmount,
        a.refund_amount as residualAmount,
        h.electrovalence as electrovalence,
        CASE WHEN a.order_source = 1 THEN h.serviceCharge ELSE h.serviceCharge * 0.8 END as serviceCharge,
        CASE WHEN a.order_source = 2 THEN h.serviceCharge * 0.2 ELSE 0 END as sharingAmount,
@@ -1165,10 +1252,19 @@
        left join charging_pile_service.t_site b on (a.site_id = b.id)
        left join charging_pile_account.t_app_user_car c on (a.app_user_car_id = c.id)
        left join charging_pile_service.t_charging_gun d on (a.charging_gun_id = d.id)
        left join charging_pile_order.t_charging_order_refund e on (a.id = e.charging_order_id)
        left join charging_pile_account.t_app_user f on (a.app_user_id = f.id)
        left join charging_pile_order.t_accounting_strategy_order g on (a.id = g.charging_order_id)
        left join (select sum(period_electric_price) as electrovalence,sum(period_service_price) as serviceCharge,charging_order_id from charging_pile_order.t_charging_order_accounting_strategy group by charging_order_id) as h on (a.id = h.charging_order_id)
        left join (
            select charging_order_id, max(`name`) as `name`
            from charging_pile_order.t_accounting_strategy_order
            group by charging_order_id
        ) g on (a.id = g.charging_order_id)
        inner join (
            select sum(period_electric_price) as electrovalence,
                   sum(period_service_price) as serviceCharge,
                   charging_order_id
            from charging_pile_order.t_charging_order_accounting_strategy
            group by charging_order_id
        ) as h on (a.id = h.charging_order_id)
        where a.del_flag = 0 and a.recharge_payment_status = 2
        <if test="null != req.code and req.code!=''">
            and a.code LIKE CONCAT('%',#{req.code},'%')
@@ -2813,7 +2909,7 @@
    </sql>
    <sql id="enterpriseChargingDetailSelect">
        SELECT DISTINCT
        SELECT
            co.id,
            co.code,
            co.order_type AS orderType,
@@ -2832,10 +2928,9 @@
            au.phone AS phone,
            car.license_plate AS licensePlate,
            co.charging_capacity AS chargingCapacity,
            IFNULL(price.electrovalence, 0) AS electrovalence,
            IFNULL(price.serviceCharge, 0) AS serviceCharge,
            IFNULL(tod.electrovalence, 0) AS electrovalence,
            IFNULL(tod.service_charge, 0) AS serviceCharge,
            co.end_mode AS endMode,
            IFNULL(price.periodCount, 0) AS periodCount,
            co.create_time AS createTime,
            co.start_time AS startTime,
            co.end_time AS endTime,
@@ -2849,14 +2944,6 @@
        LEFT JOIN charging_pile_service.t_charging_gun cg ON cg.id = co.charging_gun_id
        LEFT JOIN charging_pile_account.t_app_user au ON au.id = co.app_user_id
        LEFT JOIN charging_pile_account.t_app_user_car car ON car.id = co.app_user_car_id
        LEFT JOIN (
            SELECT charging_order_id,
                   SUM(period_electric_price) AS electrovalence,
                   SUM(period_service_price) AS serviceCharge,
                   COUNT(1) AS periodCount
            FROM t_charging_order_accounting_strategy
            GROUP BY charging_order_id
        ) price ON price.charging_order_id = co.id
    </sql>
    <select id="enterpriseChargingDetail" resultType="com.ruoyi.order.dto.EnterpriseChargingDetailVO">
@@ -2875,6 +2962,17 @@
        ORDER BY co.create_time DESC
    </select>
    <select id="enterpriseChargingPeriodCounts" resultType="map">
        SELECT charging_order_id AS orderId,
               COUNT(1) AS periodCount
        FROM t_charging_order_accounting_strategy
        WHERE charging_order_id IN
        <foreach collection="orderIds" item="orderId" open="(" separator="," close=")">
            #{orderId}
        </foreach>
        GROUP BY charging_order_id
    </select>
    <select id="queryChargingBillIncome" resultType="map">
        select
        income.site_id,
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
@@ -1,17 +1,33 @@
package com.ruoyi.order.controller;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.order.api.vo.ChargingOrderVO;
import com.ruoyi.order.export.EnterpriseChargingDetailExport;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TVipOrderControllerTest {
    @Test
    void fillsMissingVipChargingOrderPhoneFromAccountUser() {
        ChargingOrderVO order = new ChargingOrderVO();
        order.setAppUserId(123L);
        TAppUser user = new TAppUser();
        user.setId(123L);
        user.setPhone("13800000000");
        TVipOrderController.fillVipChargingOrderPhones(
                Collections.singletonList(order), Collections.singletonList(user));
        assertEquals("13800000000", order.getPhone());
    }
    @Test
    void formatExportDateTimeReturnsEmptyStringForMissingOrderTime() {
        assertEquals("", TVipOrderController.formatExportDateTime(null));
    }
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQueryTest.java
New file
@@ -0,0 +1,26 @@
package com.ruoyi.order.dto;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
class EnterpriseChargingAnalysisQueryTest {
    @Test
    void shouldDeserializeDateTimeWithSpaceSeparator() throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        EnterpriseChargingAnalysisQuery query = objectMapper.readValue(
            "{\"startTime1\":\"2026-07-01 00:00:00\",\"startTime2\":\"2026-07-29 00:00:00\"}",
            EnterpriseChargingAnalysisQuery.class
        );
        assertEquals(LocalDateTime.of(2026, 7, 1, 0, 0), query.getStartTime1());
        assertEquals(LocalDateTime.of(2026, 7, 29, 0, 0), query.getStartTime2());
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingDetailMapperTest.java
@@ -9,6 +9,7 @@
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -28,11 +29,28 @@
        String detailSelect = mapper.substring(
                mapper.indexOf("<sql id=\"enterpriseChargingDetailSelect\""),
                mapper.indexOf("<select id=\"enterpriseChargingDetail\""));
        String baseWhere = mapper.substring(
                mapper.indexOf("<sql id=\"enterpriseChargingBaseWhere\""),
                mapper.indexOf("<select id=\"enterpriseChargingAnalysis\""));
        assertFalse(detailSelect.contains("SELECT DISTINCT"));
        assertFalse(detailSelect.contains("FROM t_charging_order_accounting_strategy"));
        assertTrue(detailSelect.contains("IFNULL(tod.electrovalence, 0) AS electrovalence"));
        assertTrue(detailSelect.contains("IFNULL(tod.service_charge, 0) AS serviceCharge"));
        assertTrue(detailSelect.contains("AS licensePlate"));
        assertTrue(detailSelect.contains("AS chargingCapacity"));
        assertTrue(detailSelect.contains("AS electrovalence"));
        assertTrue(detailSelect.contains("AS serviceCharge"));
        assertTrue(detailSelect.contains("AS endMode"));
        assertTrue(detailSelect.contains("AS periodCount"));
        assertTrue(baseWhere.contains("AND toi.status IN (2, 3)"));
        assertFalse(baseWhere.contains("AND toi.status IN (1, 2, 3)"));
        assertTrue(mapper.contains("<select id=\"enterpriseChargingPeriodCounts\""));
        String periodCounts = mapper.substring(
                mapper.indexOf("<select id=\"enterpriseChargingPeriodCounts\""),
                mapper.indexOf("<select id=\"queryChargingBillIncome\""));
        assertTrue(periodCounts.contains("FROM t_charging_order_accounting_strategy"));
        assertTrue(periodCounts.contains("WHERE charging_order_id IN"));
        assertTrue(periodCounts.contains("GROUP BY charging_order_id"));
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TOrderInvoicePageListQueryTest.java
New file
@@ -0,0 +1,42 @@
package com.ruoyi.order.service.impl;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TOrderInvoicePageListQueryTest {
    @Test
    void pageListLoadsInvoiceDetailsAndUsersInBatches() throws IOException {
        Path sourcePath = Paths.get("src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java");
        if (!Files.exists(sourcePath)) {
            sourcePath = Paths.get("ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java");
        }
        String source = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8);
        String method = source.substring(
                source.indexOf("public PageInfo<TOrderInvoiceVO> pageList"),
                source.indexOf("public List<String> invoicingCompanyOptions"));
        assertEquals(1, occurrences(method, "orderInvoiceDetailService.list("));
        assertTrue(method.contains("appUserClient.getUserByIds(appUserIds)"));
        assertFalse(method.contains("appUserClient.getUserById("));
    }
    private int occurrences(String source, String target) {
        int count = 0;
        int index = 0;
        while ((index = source.indexOf(target, index)) >= 0) {
            count++;
            index += target.length();
        }
        return count;
    }
}