ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -68,7 +68,6 @@
import com.ruoyi.order.vo.EndOfChargePageInfo;
import com.ruoyi.order.vo.ChargingSimulationVO;
import com.ruoyi.order.vo.IcbcAdminSubOrderVO;
import com.ruoyi.order.vo.OrderCountByDate;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import com.ruoyi.order.vo.LargeChargingPowerVo;
import com.ruoyi.order.vo.ReservationRecommendationVO;
@@ -103,6 +102,7 @@
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
@@ -111,6 +111,7 @@
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
@@ -144,6 +145,12 @@
    private static final int ENTERPRISE_INVOICE_BATCH_SIZE = 500;
    private static final int ENTERPRISE_ORDER_ID_BATCH_SIZE = 500;
    private static final int CHARGING_PERIOD_ACCOUNTING_BATCH_SIZE = 500;
    private static final String LARGE_SCREEN_CACHE_PREFIX = "large:screen:";
    private static final long LARGE_SCREEN_CACHE_SECONDS = 60L;
    private static final LocalDate LARGE_SCREEN_STAT_DATA_START = LocalDate.of(2020, 1, 1);
    private static final DateTimeFormatter LARGE_SCREEN_MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
    private static final DateTimeFormatter LARGE_SCREEN_DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter LARGE_SCREEN_SQL_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public ReservationRecommendationVO getReservationRecommendation(Integer siteId) {
@@ -2686,12 +2693,28 @@
        String startTime2 = timeRange[1];
        String endTime1 = timeRange[2];
        String endTime2 = timeRange[3];
        PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(dto.getPageCurr(), dto.getPageSize());
        int pageCurr = dto.getPageCurr() == null ? 1 : dto.getPageCurr();
        int pageSize = dto.getPageSize() == null ? 50 : dto.getPageSize();
        PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        Long userId = tokenService.getLoginUser().getUserid();
        SysUser sysUser = sysUserClient.getSysUser(userId).getData();
        Integer roleType = sysUser.getRoleType();
        setChargingOrderAuthSiteIds(dto, sysUser, userId, roleType);
        List<ChargingOrderVO> list = this.baseMapper.chargingOrder(pageInfo, dto, startTime1, startTime2, endTime1, endTime2);
        if (StringUtils.hasLength(dto.getPhone()) && (dto.getUserIds() == null || dto.getUserIds().isEmpty())) {
            List<Long> data = appUserClient.getUserIdsByPhone(dto.getPhone()).getData();
            if (data == null || data.isEmpty()) {
                pageInfo.setTotal(0L);
                pageInfo.setRecords(Collections.emptyList());
                return pageInfo;
            }
            dto.setUserIds(data);
        }
        // 手动分页:不传 IPage(避免分页拦截器生成无法下推的全量 COUNT),COUNT 与 ORDER BY + LIMIT 均由 SQL 下推到分片
        long offset = (long) (pageCurr - 1) * pageSize;
        long total = this.baseMapper.chargingOrderCount(dto, startTime1, startTime2, endTime1, endTime2);
        pageInfo.setTotal(total);
        List<ChargingOrderVO> list = total == 0L ? Collections.emptyList()
                : this.baseMapper.chargingOrder(dto, startTime1, startTime2, endTime1, endTime2, offset, pageSize);
        fillChargingOrderBalanceIncome(list);
        fillChargingOrderShareIncome(list);
        fillChargingOrderPageExtra(list, dto.getSiteIds(), sysUser, roleType);
@@ -2703,6 +2726,11 @@
    public TCharingOrderVO chargingOrderSummary(ChargingOrderQuery dto) {
        TCharingOrderVO tCharingOrderVO = new TCharingOrderVO();
        fillEmptyChargingOrderSummary(tCharingOrderVO);
        if (isChargingOrderFullSweep(dto)) {
            // 无任何筛选即全量扫描:汇总聚合跨分片 JOIN(accounting_strategy,分片键 charging_order_id 与 t_charging_order 不一致)
            // 且需把全量订单 id 反查 share/balance,MyCat 只能全量拉回代理层,必然超时。汇总没有 LIMIT 可下推,直接返回空汇总。
            return tCharingOrderVO;
        }
        String[] timeRange = parseChargingOrderTimeRange(dto);
        String startTime1 = timeRange[0];
        String startTime2 = timeRange[1];
@@ -2751,6 +2779,23 @@
        return new String[]{startTime1, startTime2, endTime1, endTime2};
    }
    /** 是否全量扫描:用户未提供任何可收窄的筛选条件(时间/订单号/手机号/站点/桩/枪/状态等),无界聚合无法下推。 */
    private boolean isChargingOrderFullSweep(ChargingOrderQuery dto) {
        return !StringUtils.hasLength(dto.getStartTime())
                && !StringUtils.hasLength(dto.getEndTime())
                && !StringUtils.hasLength(dto.getCode())
                && !StringUtils.hasLength(dto.getPhone())
                && dto.getSiteId() == null
                && dto.getPileId() == null
                && dto.getGunId() == null
                && dto.getOrderType() == null
                && dto.getStatus() == null
                && dto.getIsWhitelist() == null
                && dto.getPaymentType() == null
                && dto.getOrderSource() == null
                && (dto.getUserIds() == null || dto.getUserIds().isEmpty());
    }
    private void fillChargingOrderPageExtra(List<ChargingOrderVO> list, List<Integer> siteIds, SysUser sysUser, Integer roleType) {
        if (list == null || list.isEmpty()) {
            return;
@@ -2770,6 +2815,25 @@
        Map<Integer, Site> siteMap = pageSites.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        // 列表 SQL 不再跨库 JOIN app_user/partner(避免分片表无筛选全量广播),手机号与合作商名在此回填
        Map<Long, String> phoneMap = new HashMap<>();
        List<Long> pageUserIds = list.stream().map(ChargingOrderVO::getAppUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (!pageUserIds.isEmpty()) {
            List<TAppUser> pageUsers = Optional.ofNullable(appUserClient.getUserByIds(pageUserIds).getData()).orElse(new ArrayList<>());
            for (TAppUser user : pageUsers) {
                if (user != null && user.getId() != null) {
                    phoneMap.put(user.getId(), user.getPhone());
                }
            }
        }
        Map<Integer, String> partnerNameMap = new HashMap<>();
        List<Integer> pagePartnerIds = siteMap.values().stream().map(Site::getPartnerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        for (Integer partnerId : pagePartnerIds) {
            Partner partner = Optional.ofNullable(partnerClient.getPartnerById(partnerId).getData()).orElse(null);
            if (partner != null && partner.getId() != null) {
                partnerNameMap.put(partner.getId(), partner.getName());
            }
        }
        Map<Long, List<TChargingOrderAccountingStrategy>> strategyMap = orderIds.isEmpty() ? new HashMap<>() :
                chargingOrderAccountingStrategyService.lambdaQuery()
                        .in(TChargingOrderAccountingStrategy::getChargingOrderId, orderIds)
@@ -2787,12 +2851,14 @@
            if (roleType == 2 && chargingOrderVO.getSiteId() != null) {
                chargingOrderVO.setAuthRecord(Boolean.TRUE.equals(authRecordMap.get(chargingOrderVO.getSiteId())));
            }
            chargingOrderVO.setPhone(phoneMap.get(chargingOrderVO.getAppUserId()));
            chargingOrderVO.setCommissionAmount(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")) : new BigDecimal("0"));
            chargingOrderVO.setPlatFormMoney(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")).setScale(2, BigDecimal.ROUND_DOWN) : new BigDecimal("0"));
            chargingOrderVO.setUid(chargingOrderVO.getId() + "");
            Site site = siteMap.get(chargingOrderVO.getSiteId());
            if (site != null) {
                chargingOrderVO.setSiteName(site.getName());
                chargingOrderVO.setPartnerName(partnerNameMap.get(site.getPartnerId()));
            }
            TChargingGun gun = gunMap.get(chargingOrderVO.getChargingGunId());
            TChargingPile pile = pileMap.get(chargingOrderVO.getChargingPileId());
@@ -2883,6 +2949,10 @@
     */
    @Override
    public List<ChargingOrderBalanceIncomeDetailVO> balanceIncomeDetail(ChargingOrderQuery dto) {
        if (isChargingOrderFullSweep(dto)) {
            // 与汇总同根因:全量扫描会把全量订单 id 反查 exchange_code_use_record,MyCat 超时,直接返回空明细
            return new ArrayList<>();
        }
        String startTime1 = null;
        String startTime2 = null;
        String endTime1 = null;
@@ -3080,66 +3150,217 @@
    @Override
    public R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(EnterpriseChargingAnalysisQuery query) {
        PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<EnterpriseChargingAnalysisVO> list = this.baseMapper.enterpriseChargingAnalysis(pageInfo, query);
        if (list == null || list.isEmpty()) {
        int pageCurr = query.getPageCurr() == null ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null ? 50 : query.getPageSize();
        PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        // 逆反查聚合(不再跨分片 JOIN,构建块见 TChargingOrderMapper.xml enterpriseInvoiceRows 等):
        //   1) t_order_invoice 单表查企业发票(10 片按 app_user_id 分片,抬头级条件可下推)
        //   2) t_order_invoice_detail 按 order_invoice_id IN 路由查充电明细
        //   3) 命中订单级条件(站点/时间等)→ 命中订单 id
        //   4) t_charging_order 按 id IN 走主键取 start_time/site_id
        //   5) t_site 单表取站点名
        //   6) 按公司聚合 + 手工分页(不再传 IPage,避免分页拦截器生成无法下推的全量 COUNT)
        List<EnterpriseChargingAggRow.Invoice> invoices = this.baseMapper.enterpriseInvoiceRows(query);
        if (invoices == null || invoices.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<String> companyNames = list.stream()
                .map(EnterpriseChargingAnalysisVO::getCompanyName)
        Map<Long, List<EnterpriseChargingAggRow.Detail>> detailsByInvoice = new HashMap<>();
        Map<Long, EnterpriseChargingAggRow.Detail> detailByOrder = new LinkedHashMap<>();
        List<Long> invoiceIds = invoices.stream()
                .map(EnterpriseChargingAggRow.Invoice::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<String, List<Map<String, Object>>> topSitesByCompany = groupByCompany(
                this.baseMapper.enterpriseTopSitesByCompanies(query, companyNames));
        Map<String, List<Map<String, Object>>> chargingTimesByCompany = groupByCompany(
                this.baseMapper.enterpriseChargingTimesByCompanies(query, companyNames));
        for (EnterpriseChargingAnalysisVO row : list) {
            String companyKey = companyKey(row.getCompanyName(), row.getSocialCreditCode());
            List<Map<String, Object>> topSites = topSitesByCompany.getOrDefault(companyKey, Collections.emptyList());
            String topSiteNames = topSites.stream()
                    .sorted(Comparator.comparing((Map<String, Object> item) -> ((Number) item.getOrDefault("counts", 0)).longValue()).reversed())
                    .limit(3)
                    .map(item -> Objects.toString(item.get("siteName"), ""))
                    .filter(org.springframework.util.StringUtils::hasText)
                    .collect(Collectors.joining("、"));
            row.setTopSiteNames(topSiteNames);
            List<LocalDateTime> chargingTimes = chargingTimesByCompany.getOrDefault(companyKey, Collections.emptyList()).stream()
                    .map(item -> toLocalDateTime(item.get("startTime")))
                    .filter(Objects::nonNull)
                    .sorted()
                    .collect(Collectors.toList());
            row.setAverageChargingInterval(calculateAverageChargingInterval(chargingTimes));
        for (int start = 0; start < invoiceIds.size(); start += ENTERPRISE_INVOICE_BATCH_SIZE) {
            int end = Math.min(start + ENTERPRISE_INVOICE_BATCH_SIZE, invoiceIds.size());
            List<EnterpriseChargingAggRow.Detail> details =
                    this.baseMapper.enterpriseInvoiceDetailRows(invoiceIds.subList(start, end));
            if (details == null) {
                continue;
            }
            for (EnterpriseChargingAggRow.Detail detail : details) {
                if (detail.getOrderId() == null) {
                    continue;
                }
                detailsByInvoice.computeIfAbsent(detail.getInvoiceId(), k -> new ArrayList<>()).add(detail);
                detailByOrder.putIfAbsent(detail.getOrderId(), detail);
            }
        }
        pageInfo.setRecords(list);
        if (detailByOrder.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<Long> candidateOrderIds = new ArrayList<>(detailByOrder.keySet());
        boolean orderFilter = hasEnterpriseOrderFilter(query);
        Set<Long> matchedOrderIds = null;
        if (orderFilter) {
            matchedOrderIds = new HashSet<>();
            for (int start = 0; start < candidateOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) {
                int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, candidateOrderIds.size());
                List<Long> matched = this.baseMapper.enterpriseAnalysisMatchingOrderIds(
                        candidateOrderIds.subList(start, end), query);
                if (matched != null) {
                    matchedOrderIds.addAll(matched);
                }
            }
            if (matchedOrderIds.isEmpty()) {
                pageInfo.setRecords(Collections.emptyList());
                return R.ok(pageInfo);
            }
        }
        List<Long> effectiveOrderIds = orderFilter ? new ArrayList<>(matchedOrderIds) : candidateOrderIds;
        Map<Long, EnterpriseChargingAggRow.Order> orderMap = new HashMap<>();
        for (int start = 0; start < effectiveOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) {
            int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, effectiveOrderIds.size());
            List<EnterpriseChargingAggRow.Order> orders =
                    this.baseMapper.enterpriseAnalysisChargingOrders(effectiveOrderIds.subList(start, end));
            if (orders == null) {
                continue;
            }
            for (EnterpriseChargingAggRow.Order order : orders) {
                orderMap.put(order.getOrderId(), order);
            }
        }
        Set<Long> siteIds = orderMap.values().stream()
                .map(EnterpriseChargingAggRow.Order::getSiteId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> siteNameMap = new HashMap<>();
        if (!siteIds.isEmpty()) {
            List<EnterpriseChargingAggRow.Site> sites = this.baseMapper.enterpriseAnalysisSites(new ArrayList<>(siteIds));
            if (sites != null) {
                for (EnterpriseChargingAggRow.Site site : sites) {
                    siteNameMap.put(site.getSiteId(), site.getSiteName());
                }
            }
        }
        Map<String, EnterpriseCompanyAgg> companyAggs = new LinkedHashMap<>();
        for (EnterpriseChargingAggRow.Invoice invoice : invoices) {
            List<EnterpriseChargingAggRow.Detail> details =
                    detailsByInvoice.getOrDefault(invoice.getId(), Collections.emptyList());
            if (details.isEmpty()) {
                // 无充电明细(order_type=1)的发票不计,等同原 INNER JOIN tod.order_type=1 语义
                continue;
            }
            List<EnterpriseChargingAggRow.Order> invoiceOrders = new ArrayList<>();
            BigDecimal detailAmount = BigDecimal.ZERO;
            for (EnterpriseChargingAggRow.Detail detail : details) {
                if (orderFilter && !matchedOrderIds.contains(detail.getOrderId())) {
                    continue;
                }
                detailAmount = detailAmount.add(nullToZero(detail.getInvoiceAmount()));
                EnterpriseChargingAggRow.Order order = orderMap.get(detail.getOrderId());
                if (order != null) {
                    invoiceOrders.add(order);
                }
            }
            if (orderFilter && invoiceOrders.isEmpty()) {
                // 订单级筛选后该发票无命中订单 → 剔除,等同原 LEFT JOIN + 订单级 WHERE 语义
                continue;
            }
            BigDecimal invoiceAmount = detailAmount.signum() > 0 ? detailAmount : nullToZero(invoice.getTotalAmount());
            String key = companyKey(invoice.getCompanyName(), invoice.getSocialCreditCode());
            EnterpriseCompanyAgg agg = companyAggs.get(key);
            if (agg == null) {
                agg = new EnterpriseCompanyAgg(invoice.getCompanyName(), invoice.getSocialCreditCode(),
                        invoice.getCompanyAddress(), invoice.getCompanyPhone());
                companyAggs.put(key, agg);
            }
            agg.invoiceCount++;
            agg.invoiceAmount = agg.invoiceAmount.add(invoiceAmount);
            for (EnterpriseChargingAggRow.Order order : invoiceOrders) {
                if (order.getStartTime() != null) {
                    agg.startTimes.add(order.getStartTime());
                    if (agg.latestChargingTime == null || order.getStartTime().isAfter(agg.latestChargingTime)) {
                        agg.latestChargingTime = order.getStartTime();
                    }
                }
                if (order.getSiteId() != null) {
                    agg.siteOrderIds.computeIfAbsent(order.getSiteId(), k -> new HashSet<>()).add(order.getOrderId());
                }
            }
        }
        if (companyAggs.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<EnterpriseChargingAnalysisVO> all = companyAggs.values().stream()
                .sorted(Comparator.comparingLong((EnterpriseCompanyAgg agg) -> agg.invoiceCount).reversed())
                .map(agg -> buildEnterpriseAnalysisVO(agg, siteNameMap))
                .collect(Collectors.toList());
        pageInfo.setTotal(all.size());
        long offset = (long) (pageCurr - 1L) * pageSize;
        if (offset >= all.size()) {
            pageInfo.setRecords(Collections.emptyList());
        } else {
            int toIndex = (int) Math.min(offset + pageSize, all.size());
            pageInfo.setRecords(all.subList((int) offset, toIndex));
        }
        return R.ok(pageInfo);
    }
    private Map<String, List<Map<String, Object>>> groupByCompany(List<Map<String, Object>> rows) {
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
    private boolean hasEnterpriseOrderFilter(EnterpriseChargingAnalysisQuery query) {
        return query.getSiteType() != null
                || query.getPartnerId() != null
                || query.getSiteId() != null
                || query.getCreateTime1() != null
                || query.getCreateTime2() != null
                || (query.getCode() != null && !query.getCode().isEmpty())
                || query.getOrderType() != null
                || (query.getPhone() != null && !query.getPhone().isEmpty())
                || query.getStatus() != null
                || (query.getPartnerName() != null && !query.getPartnerName().isEmpty())
                || query.getOrderSource() != null
                || query.getStartTime1() != null
                || query.getStartTime2() != null
                || query.getEndTime1() != null
                || query.getEndTime2() != null;
    }
    private EnterpriseChargingAnalysisVO buildEnterpriseAnalysisVO(EnterpriseCompanyAgg agg,
                                                                  Map<Long, String> siteNameMap) {
        EnterpriseChargingAnalysisVO vo = new EnterpriseChargingAnalysisVO();
        vo.setCompanyName(agg.companyName);
        vo.setSocialCreditCode(agg.socialCreditCode);
        vo.setCompanyAddress(agg.companyAddress);
        vo.setCompanyPhone(agg.companyPhone);
        vo.setInvoiceCount(agg.invoiceCount);
        vo.setInvoiceAmount(agg.invoiceAmount);
        vo.setLatestChargingTime(agg.latestChargingTime);
        String topSiteNames = agg.siteOrderIds.entrySet().stream()
                .sorted(Comparator.comparingInt((Map.Entry<Long, Set<Long>> e) -> e.getValue().size()).reversed())
                .limit(3)
                .map(e -> siteNameMap.getOrDefault(e.getKey(), ""))
                .filter(org.springframework.util.StringUtils::hasText)
                .collect(Collectors.joining("、"));
        vo.setTopSiteNames(topSiteNames);
        vo.setAverageChargingInterval(calculateAverageChargingInterval(new ArrayList<>(agg.startTimes)));
        return vo;
    }
    private static class EnterpriseCompanyAgg {
        private final String companyName;
        private final String socialCreditCode;
        private final String companyAddress;
        private final String companyPhone;
        private long invoiceCount;
        private BigDecimal invoiceAmount = BigDecimal.ZERO;
        private LocalDateTime latestChargingTime;
        private final Map<Long, Set<Long>> siteOrderIds = new HashMap<>();
        private final TreeSet<LocalDateTime> startTimes = new TreeSet<>();
        private EnterpriseCompanyAgg(String companyName, String socialCreditCode,
                                     String companyAddress, String companyPhone) {
            this.companyName = companyName;
            this.socialCreditCode = socialCreditCode;
            this.companyAddress = companyAddress;
            this.companyPhone = companyPhone;
        }
        return rows.stream().collect(Collectors.groupingBy(row -> companyKey(
                Objects.toString(row.get("companyName"), null),
                Objects.toString(row.get("socialCreditCode"), ""))));
    }
    private String companyKey(String companyName, String socialCreditCode) {
        return Objects.toString(companyName, "") + "\u0000" + Objects.toString(socialCreditCode, "");
    }
    private LocalDateTime toLocalDateTime(Object value) {
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof Date) {
            return LocalDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault());
        }
        return null;
    }
    @Override
@@ -3340,23 +3561,115 @@
                .divide(BigDecimal.valueOf(3600), 2, RoundingMode.HALF_UP);
    }
    private static final int SERIAL_ORDER_ID_SEARCH_LIMIT = 5000;
    private static final int SITE_ORDER_ID_SEARCH_LIMIT = 50000;
    @Override
    public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) {
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize());
        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);
        int pageCurr = chargingRefundDto.getPageCurr() == null ? 1 : chargingRefundDto.getPageCurr();
        int pageSize = chargingRefundDto.getPageSize() == null ? 50 : chargingRefundDto.getPageSize();
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(pageCurr, pageSize);
        long offset = (long) (pageCurr - 1) * pageSize;
        // 每个 UNION 分支须取 offset+pageSize 行,外层再按 offset,pageSize 切页,否则翻页时池子不足导致丢数据
        int branchLimit = (int) Math.min(offset + pageSize, Integer.MAX_VALUE);
        boolean filterBySerial = StringUtils.hasLength(chargingRefundDto.getSerialNumber());
        boolean filterBySite = chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty();
        List<TChargingOrderRefund> list;
        if (!filterBySerial && !filterBySite) {
            // 无筛选:手工分页。分页拦截器会对 UNION 查询生成全量 COUNT 且无法把 ORDER BY/LIMIT 下推到分片,
            // 这里改为分支内下推(getRefundListPage)加分支 COUNT 求和(countRefundList)
            pageInfo.setTotal(this.baseMapper.countRefundList(chargingRefundDto));
            list = this.baseMapper.getRefundListPage(chargingRefundDto, offset, pageSize, branchLimit);
        } else {
            // 筛选:逆反查。先按条件查订单 id(t_charging_order 等可下推/走索引),
            // 再按退款表分片键 IN 过滤(可路由),替代跨分片 LEFT JOIN
            RefundOrderIds ids = resolveRefundOrderIds(chargingRefundDto);
            if (ids.isEmpty()) {
                pageInfo.setRecords(Collections.emptyList());
                return R.ok(pageInfo);
            }
            pageInfo.setTotal(this.baseMapper.countRefundListByOrderIds(chargingRefundDto,
                    ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds));
            list = this.baseMapper.getRefundListByOrderIds(chargingRefundDto,
                    ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds, offset, pageSize, branchLimit);
        }
        enrichRefundOrderInfo(list);
        for (TChargingOrderRefund refund : list) {
            refund.setUid(refund.getType() + "-" + refund.getId());
        }
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    /**
     * 逆反查订单 id:serialNumber 匹配充电/商城/会员订单 id(各自 LIMIT 防爆),
     * siteIds 匹配充电订单 id;两者同时存在时取交集。
     * 返回约定:null = 该类型不过滤;空列表 = 该类型无匹配(SQL 侧 AND 1=0)。
     */
    private RefundOrderIds resolveRefundOrderIds(ChargingRefundDto chargingRefundDto) {
        List<Long> type1OrderIds = null;
        List<Long> type2OrderIds = null;
        List<Long> type3OrderIds = null;
        if (StringUtils.hasLength(chargingRefundDto.getSerialNumber())) {
            // 模糊匹配结果可能很大(短流水号),截断上限防 IN 列表爆炸;匹配不到则列表为空
            type1OrderIds = safeIds(this.baseMapper.listChargingOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            type2OrderIds = safeIds(this.baseMapper.listShoppingOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            type3OrderIds = safeIds(this.baseMapper.listVipOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type1OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "充电订单");
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type2OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "商城订单");
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type3OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "会员订单");
        }
        if (chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty()) {
            List<Long> siteMatched = safeIds(this.baseMapper.listChargingOrderIdsBySiteIds(
                    chargingRefundDto.getSiteIds(), SITE_ORDER_ID_SEARCH_LIMIT));
            warnIfTruncated(String.valueOf(chargingRefundDto.getSiteIds()), siteMatched,
                    SITE_ORDER_ID_SEARCH_LIMIT, "站点订单");
            if (type1OrderIds == null) {
                type1OrderIds = siteMatched;
            } else {
                type1OrderIds.retainAll(new HashSet<>(siteMatched));
            }
        }
        return new RefundOrderIds(type1OrderIds, type2OrderIds, type3OrderIds);
    }
    /** 反查结果达到上限说明可能被截断,告警便于排查(截断会导致部分退款不在列表内) */
    private void warnIfTruncated(String condition, List<Long> ids, int limit, String orderType) {
        if (ids.size() >= limit) {
            log.warn("退款列表逆反查:条件[{}]匹配{}数达到上限{},结果可能被截断", condition, orderType, limit);
        }
    }
    private static List<Long> safeIds(List<Long> ids) {
        return ids == null ? new ArrayList<>() : new ArrayList<>(ids);
    }
    /** 逆反查结果:null = 不过滤;空列表 = 无匹配 */
    private static class RefundOrderIds {
        private final List<Long> type1OrderIds;
        private final List<Long> type2OrderIds;
        private final List<Long> type3OrderIds;
        private RefundOrderIds(List<Long> type1OrderIds, List<Long> type2OrderIds, List<Long> type3OrderIds) {
            this.type1OrderIds = type1OrderIds;
            this.type2OrderIds = type2OrderIds;
            this.type3OrderIds = type3OrderIds;
        }
        /** 三个类型全部明确无匹配(非 null 且为空)时,整页为空,可短路 */
        private boolean isEmpty() {
            return type1OrderIds != null && type1OrderIds.isEmpty()
                    && type2OrderIds != null && type2OrderIds.isEmpty()
                    && type3OrderIds != null && type3OrderIds.isEmpty();
        }
    }
    private void enrichRefundOrderInfo(List<TChargingOrderRefund> refunds) {
@@ -3433,9 +3746,8 @@
            }
        } else {
            List<SixChargingDto> settlementIncome = this.baseMapper.chargeSettlement(sixBeforeValue, siteIds);
            if (settlementIncome != null) {
                result.addAll(settlementIncome);
            }
            List<SixChargingDto> orderHistoryIncome = this.baseMapper.chargeOrderHistory(sixBeforeValue, siteIds);
            result.addAll(mergeMissingMonthlyIncome(settlementIncome, orderHistoryIncome));
            String siteIdsCsv = siteIds == null ? null : siteIds.stream()
                    .filter(Objects::nonNull)
                    .map(String::valueOf)
@@ -3470,6 +3782,45 @@
        return result;
    }
    static List<SixChargingDto> mergeMissingMonthlyIncome(List<SixChargingDto> settlementIncome,
                                                            List<SixChargingDto> orderHistoryIncome) {
        List<SixChargingDto> merged = new ArrayList<>();
        Set<Integer> settledMonths = new HashSet<>();
        if (settlementIncome != null) {
            for (SixChargingDto item : settlementIncome) {
                if (item == null) {
                    continue;
                }
                merged.add(item);
                Integer month = incomeMonth(item);
                if (month != null) {
                    settledMonths.add(month);
                }
            }
        }
        if (orderHistoryIncome != null) {
            for (SixChargingDto item : orderHistoryIncome) {
                Integer month = incomeMonth(item);
                if (item != null && month != null && settledMonths.add(month)) {
                    merged.add(item);
                }
            }
        }
        return merged;
    }
    private static Integer incomeMonth(SixChargingDto item) {
        if (item == null || item.getMonth() == null) {
            return null;
        }
        try {
            int month = Integer.parseInt(item.getMonth());
            return month >= 1 && month <= 12 ? month : null;
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    @Override
    public List<SixCircleDto> circle(List<Integer> siteIds, LocalDate sixBefore, Boolean partnerView) {
        List<SixCircleDto> result = this.baseMapper.circle(siteIds, sixBefore, partnerView);
@@ -3484,10 +3835,10 @@
    }
    @Override
    public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView) {
        Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView);
    public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView, LocalDate sixBefore) {
        Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView, sixBefore);
        if (!Boolean.TRUE.equals(partnerView) && result != null) {
            result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds));
            result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds, sixBefore));
        }
        return result;
    }
@@ -6693,59 +7044,339 @@
    @Override
    public Map<String, Object> getLargeScreenSummary(List<Integer> siteIds) {
        return this.baseMapper.getLargeScreenSummary(siteIds);
        String cacheKey = largeScreenCacheKey("summary", siteIds);
        Map<String, Object> cached = redisService.getCacheObject(cacheKey);
        if (cached != null) {
            return cached;
        }
        Map<String, Object> result = new HashMap<>();
        try {
            Map<String, Object> summary = this.baseMapper.summaryFromStat(siteIds);
            if (summary != null) {
                result.putAll(summary);
            }
            // 充电用户数精确去重(用户-站点对表, 跨站不重复); 对表未回填时回退到按站点求和
            Long pairCount = this.baseMapper.userSiteTableCount();
            if (pairCount != null && pairCount > 0) {
                Long exactUserCount = this.baseMapper.userCountFromStat(siteIds);
                if (exactUserCount != null) {
                    result.put("chargingUserCount", exactUserCount);
                }
            }
            Long registered = this.baseMapper.registeredFromStat();
            result.put("registeredUserCount", registered == null ? 0L : registered);
        } catch (Exception e) {
            log.warn("大屏累计指标读取预聚合表失败, siteIds={}", siteIds, e);
        }
        Arrays.asList("validOrderCount", "chargingUserCount", "registeredUserCount",
                "totalChargingCapacity", "electricityFee", "serviceFee", "totalIncome")
                .forEach(key -> result.putIfAbsent(key, 0));
        redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
        return result;
    }
    @Override
    public List<SixChargingDto> getLargeScreenIncomeTrend(List<Integer> siteIds, LocalDate sixBefore) {
        String siteIdsCsv = toSiteIdsCsv(siteIds);
        List<SixChargingDto> result = new ArrayList<>();
        LocalDate currentMonth = LocalDate.now().withDayOfMonth(1);
        LocalDate month = sixBefore.withDayOfMonth(1);
        while (!month.isAfter(currentMonth)) {
            LocalDate nextMonth = month.plusMonths(1);
            SixChargingDto income = month.equals(currentMonth)
                    ? this.baseMapper.getLargeScreenCurrentMonthIncome(
                    siteIdsCsv, month.toString(), nextMonth.toString())
                    : this.baseMapper.getLargeScreenSettlementIncome(
                    siteIdsCsv, month.toString(), nextMonth.toString());
            if (income == null) {
                income = new SixChargingDto();
            }
            income.setMonth(String.format("%02d", month.getMonthValue()));
            if (income.getElectrovalence() == null) {
                income.setElectrovalence(BigDecimal.ZERO);
            }
            if (income.getServiceCharge() == null) {
                income.setServiceCharge(BigDecimal.ZERO);
            }
            result.add(income);
            month = nextMonth;
        String cacheKey = largeScreenCacheKey("incomeTrend", siteIds);
        List<SixChargingDto> cached = redisService.getCacheObject(cacheKey);
        if (cached != null) {
            return cached;
        }
        LocalDate currentMonth = LocalDate.now().withDayOfMonth(1);
        List<SixChargingDto> result = new ArrayList<>();
        try {
            String sixBeforeMonth = sixBefore.withDayOfMonth(1).format(LARGE_SCREEN_MONTH_FMT);
            List<SixChargingDto> incomeList = this.baseMapper.incomeTrendFromStat(siteIds, sixBeforeMonth);
            Map<String, SixChargingDto> incomeByMonth = new HashMap<>();
            if (incomeList != null) {
                for (SixChargingDto income : incomeList) {
                    if (income != null && income.getMonth() != null) {
                        incomeByMonth.put(income.getMonth(), income);
                    }
                }
            }
            LocalDate month = sixBefore.withDayOfMonth(1);
            while (!month.isAfter(currentMonth)) {
                SixChargingDto income = incomeByMonth.get(month.format(LARGE_SCREEN_MONTH_FMT));
                if (income == null) {
                    income = new SixChargingDto();
                }
                income.setMonth(String.format("%02d", month.getMonthValue()));
                if (income.getElectrovalence() == null) {
                    income.setElectrovalence(BigDecimal.ZERO);
                }
                if (income.getServiceCharge() == null) {
                    income.setServiceCharge(BigDecimal.ZERO);
                }
                result.add(income);
                month = month.plusMonths(1);
            }
        } catch (Exception e) {
            log.warn("大屏收入趋势读取预聚合表失败, siteIds={}, sixBefore={}", siteIds, sixBefore, e);
            LocalDate month = sixBefore.withDayOfMonth(1);
            while (!month.isAfter(currentMonth)) {
                SixChargingDto income = new SixChargingDto();
                income.setMonth(String.format("%02d", month.getMonthValue()));
                income.setElectrovalence(BigDecimal.ZERO);
                income.setServiceCharge(BigDecimal.ZERO);
                result.add(income);
                month = month.plusMonths(1);
            }
        }
        redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
        return result;
    }
    @Override
    public List<LargeChargingPowerVo> getLargeScreenChargingPower(List<Integer> siteIds, LocalDateTime startTime, LocalDateTime endTime) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String siteIdsCsv = toSiteIdsCsv(siteIds);
        List<LargeChargingPowerVo> result = new ArrayList<>();
        LocalDateTime dayStart = startTime;
        while (dayStart.isBefore(endTime)) {
            LocalDateTime dayEnd = dayStart.plusDays(1);
            Map<String, Object> aggregate = this.baseMapper.getLargeScreenChargingPower(
                    siteIdsCsv, dayStart.format(formatter), dayEnd.format(formatter));
            BigDecimal totalPower = decimalValue(aggregate, "total_power");
            BigDecimal orderCount = decimalValue(aggregate, "order_count");
            BigDecimal power = orderCount.signum() == 0 ? BigDecimal.ZERO
                    : totalPower.divide(orderCount, 0, RoundingMode.HALF_UP);
            LargeChargingPowerVo item = new LargeChargingPowerVo();
            item.setTime(dayStart.toLocalDate().toString());
            item.setPower(power == null ? BigDecimal.ZERO : power);
            result.add(item);
            dayStart = dayEnd;
        String cacheKey = largeScreenCacheKey("power", siteIds);
        List<LargeChargingPowerVo> cached = redisService.getCacheObject(cacheKey);
        if (cached != null) {
            return cached;
        }
        List<LargeChargingPowerVo> result = new ArrayList<>();
        try {
            String dayStart = startTime.toLocalDate().format(LARGE_SCREEN_DAY_FMT);
            String dayEnd = endTime.toLocalDate().format(LARGE_SCREEN_DAY_FMT);
            List<Map<String, Object>> dayAggs = this.baseMapper.powerFromStat(siteIds, dayStart, dayEnd);
            if (dayAggs != null) {
                for (Map<String, Object> agg : dayAggs) {
                    BigDecimal totalPower = decimalValue(agg, "total_power");
                    BigDecimal orderCount = decimalValue(agg, "order_count");
                    BigDecimal power = orderCount.signum() == 0 ? BigDecimal.ZERO
                            : totalPower.divide(orderCount, 0, RoundingMode.HALF_UP);
                    LargeChargingPowerVo item = new LargeChargingPowerVo();
                    Object day = agg.get("day");
                    item.setTime(day == null ? null : String.valueOf(day));
                    item.setPower(power);
                    result.add(item);
                }
            }
        } catch (Exception e) {
            log.warn("大屏功率曲线读取预聚合表失败, siteIds={}", siteIds, e);
        }
        redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
        return result;
    }
    @Override
    public BigDecimal getLargeScreenTotalCapacity(List<Integer> siteIds) {
        String cacheKey = largeScreenCacheKey("capacity", siteIds);
        BigDecimal cached = redisService.getCacheObject(cacheKey);
        if (cached != null) {
            return cached;
        }
        BigDecimal capacity = BigDecimal.ZERO;
        try {
            BigDecimal fromStat = this.baseMapper.capacityFromStat(siteIds);
            if (fromStat != null) {
                capacity = fromStat;
            }
        } catch (Exception e) {
            log.warn("大屏累计充电量读取预聚合表失败, siteIds={}", siteIds, e);
        }
        redisService.setCacheObject(cacheKey, capacity, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
        return capacity;
    }
    @Override
    public void rebuildLargeScreenStat() {
        log.info("大屏预聚合表全量重建开始");
        long start = System.currentTimeMillis();
        // 1. 月度切片全量(数据起始月 ~ 当前月), 每次切片一次查询, 避免单条大查询超时
        LocalDate month = LARGE_SCREEN_STAT_DATA_START.withDayOfMonth(1);
        LocalDate currentMonth = LocalDate.now().withDayOfMonth(1);
        while (!month.isAfter(currentMonth)) {
            writeMonthSlice(month, month.plusMonths(1));
            writeUserSitePairs(month, month.plusMonths(1));
            month = month.plusMonths(1);
        }
        // 2. 近90天日切片
        LocalDate day = LocalDate.now().minusDays(89);
        LocalDate today = LocalDate.now();
        while (!day.isAfter(today)) {
            writeDaySlice(day);
            day = day.plusDays(1);
        }
        // 3. 全局注册用户数
        writeGlobal();
        clearLargeScreenCache();
        log.info("大屏预聚合表全量重建完成, 耗时 {}ms", System.currentTimeMillis() - start);
    }
    @Override
    public void refreshLargeScreenStat() {
        long start = System.currentTimeMillis();
        LocalDate currentMonth = LocalDate.now().withDayOfMonth(1);
        writeMonthSlice(currentMonth, currentMonth.plusMonths(1));
        writeDaySlice(LocalDate.now());
        writeUserSitePairs(LocalDate.now(), LocalDate.now().plusDays(1));
        writeGlobal();
        clearLargeScreenCache();
        log.info("大屏预聚合表刷新完成, 耗时 {}ms", System.currentTimeMillis() - start);
    }
    /**
     * 写入时间切片内的 (site_id, app_user_id) 对(充电用户精确去重用, INSERT IGNORE 幂等)
     */
    private void writeUserSitePairs(LocalDate sliceStart, LocalDate sliceEnd) {
        List<Map<String, Object>> pairs = this.baseMapper.userSitePairsAgg(
                sliceStart.atStartOfDay().format(LARGE_SCREEN_SQL_FMT),
                sliceEnd.atStartOfDay().format(LARGE_SCREEN_SQL_FMT));
        if (pairs == null || pairs.isEmpty()) {
            return;
        }
        List<Map<String, Object>> rows = new ArrayList<>();
        for (Map<String, Object> pair : pairs) {
            if (pair == null || pair.get("site_id") == null || pair.get("app_user_id") == null) {
                continue;
            }
            Map<String, Object> row = new HashMap<>();
            row.put("siteId", pair.get("site_id"));
            row.put("appUserId", pair.get("app_user_id"));
            rows.add(row);
        }
        if (!rows.isEmpty()) {
            this.baseMapper.insertUserSitePairs(rows);
        }
    }
    /**
     * 写入一个月度切片聚合(M 行)
     * 订单数取未删除、状态1-5口径;金额取已结束且已支付口径。
     * 充电用户数取 monthSliceUserAgg(工作台口径: 该站点所有订单的去重用户, 不区分状态/删除标记)
     */
    private void writeMonthSlice(LocalDate monthStart, LocalDate monthEnd) {
        String startTime = monthStart.atStartOfDay().format(LARGE_SCREEN_SQL_FMT);
        String endTime = monthEnd.atStartOfDay().format(LARGE_SCREEN_SQL_FMT);
        List<Map<String, Object>> rows = this.baseMapper.monthSliceAgg(startTime, endTime);
        List<Map<String, Object>> userRows = this.baseMapper.monthSliceUserAgg(startTime, endTime);
        if ((rows == null || rows.isEmpty()) && (userRows == null || userRows.isEmpty())) {
            return;
        }
        Map<Object, BigDecimal> userCountBySite = new HashMap<>();
        if (userRows != null) {
            for (Map<String, Object> userRow : userRows) {
                if (userRow != null && userRow.get("site_id") != null) {
                    userCountBySite.put(userRow.get("site_id"), decimalValue(userRow, "user_count"));
                }
            }
        }
        String period = monthStart.format(LARGE_SCREEN_MONTH_FMT);
        List<Map<String, Object>> statRows = new ArrayList<>();
        Map<Object, Boolean> written = new HashMap<>();
        if (rows != null) {
            for (Map<String, Object> row : rows) {
                if (row == null || row.get("site_id") == null) {
                    continue;
                }
                BigDecimal electricFee = decimalValue(row, "electric_fee");
                BigDecimal serviceFee = decimalValue(row, "service_fee");
                Map<String, Object> stat = new HashMap<>();
                stat.put("statType", "M");
                stat.put("period", period);
                stat.put("siteId", row.get("site_id"));
                stat.put("orderCount", decimalValue(row, "order_count"));
                stat.put("userCount", userCountBySite.getOrDefault(row.get("site_id"), BigDecimal.ZERO));
                stat.put("capacity", decimalValue(row, "capacity"));
                stat.put("electricFee", electricFee);
                stat.put("serviceFee", serviceFee);
                stat.put("income", electricFee.add(serviceFee));
                stat.put("totalPower", BigDecimal.ZERO);
                stat.put("registeredUserCount", 0L);
                statRows.add(stat);
                written.put(row.get("site_id"), Boolean.TRUE);
            }
        }
        // 当月仅有充电用户、暂无已结束订单的站点, 补一行保用户数(金额为0)
        for (Map.Entry<Object, BigDecimal> entry : userCountBySite.entrySet()) {
            if (!written.containsKey(entry.getKey())) {
                Map<String, Object> stat = new HashMap<>();
                stat.put("statType", "M");
                stat.put("period", period);
                stat.put("siteId", entry.getKey());
                stat.put("orderCount", BigDecimal.ZERO);
                stat.put("userCount", entry.getValue());
                stat.put("capacity", BigDecimal.ZERO);
                stat.put("electricFee", BigDecimal.ZERO);
                stat.put("serviceFee", BigDecimal.ZERO);
                stat.put("income", BigDecimal.ZERO);
                stat.put("totalPower", BigDecimal.ZERO);
                stat.put("registeredUserCount", 0L);
                statRows.add(stat);
            }
        }
        if (!statRows.isEmpty()) {
            this.baseMapper.replaceSiteStatBatch(statRows);
        }
    }
    /**
     * 写入一个日切片聚合(D 行)
     */
    private void writeDaySlice(LocalDate day) {
        List<Map<String, Object>> rows = this.baseMapper.daySliceAgg(
                day.atStartOfDay().format(LARGE_SCREEN_SQL_FMT),
                day.plusDays(1).atStartOfDay().format(LARGE_SCREEN_SQL_FMT));
        if (rows == null || rows.isEmpty()) {
            return;
        }
        List<Map<String, Object>> statRows = new ArrayList<>();
        for (Map<String, Object> row : rows) {
            if (row == null || row.get("site_id") == null) {
                continue;
            }
            Map<String, Object> stat = new HashMap<>();
            stat.put("statType", "D");
            stat.put("period", day.format(LARGE_SCREEN_DAY_FMT));
            stat.put("siteId", row.get("site_id"));
            stat.put("orderCount", decimalValue(row, "order_count"));
            stat.put("userCount", BigDecimal.ZERO);
            stat.put("capacity", BigDecimal.ZERO);
            stat.put("electricFee", BigDecimal.ZERO);
            stat.put("serviceFee", BigDecimal.ZERO);
            stat.put("income", BigDecimal.ZERO);
            stat.put("totalPower", decimalValue(row, "total_power"));
            stat.put("registeredUserCount", 0L);
            statRows.add(stat);
        }
        if (!statRows.isEmpty()) {
            this.baseMapper.replaceSiteStatBatch(statRows);
        }
    }
    /**
     * 写入全局注册用户数(G 行)
     */
    private void writeGlobal() {
        Long registered = this.baseMapper.globalRegisteredUsers();
        Map<String, Object> stat = new HashMap<>();
        stat.put("statType", "G");
        stat.put("period", "ALL");
        stat.put("siteId", 0);
        stat.put("orderCount", 0L);
        stat.put("userCount", 0L);
        stat.put("capacity", BigDecimal.ZERO);
        stat.put("electricFee", BigDecimal.ZERO);
        stat.put("serviceFee", BigDecimal.ZERO);
        stat.put("income", BigDecimal.ZERO);
        stat.put("totalPower", BigDecimal.ZERO);
        stat.put("registeredUserCount", registered == null ? 0L : registered);
        this.baseMapper.replaceSiteStatBatch(Collections.singletonList(stat));
    }
    private String largeScreenCacheKey(String endpoint, List<Integer> siteIds) {
        String idPart = siteIds == null ? "all" : DigestUtils.md5DigestAsHex(
                siteIds.stream().sorted().map(String::valueOf).collect(Collectors.joining(","))
                        .getBytes(StandardCharsets.UTF_8));
        return LARGE_SCREEN_CACHE_PREFIX + endpoint + ":" + idPart;
    }
    private void clearLargeScreenCache() {
        Collection<String> keys = redisService.keys(LARGE_SCREEN_CACHE_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            redisService.deleteObject(keys);
        }
    }
    private BigDecimal decimalValue(Map<String, Object> values, String key) {
@@ -7139,6 +7770,11 @@
     */
    @Override
    public R<StatisticsOfBatteryVO> getStatisticsOfBattery(List<Integer> siteIds) {
        String cacheKey = largeScreenCacheKey("battery", siteIds);
        StatisticsOfBatteryVO cached = redisService.getCacheObject(cacheKey);
        if (cached != null) {
            return R.ok(cached);
        }
        StatisticsOfBatteryVO vo = new StatisticsOfBatteryVO();
        List<Integer> effectiveSiteIds = siteIds == null ? Arrays.asList(25, 26) : siteIds;
        if (effectiveSiteIds.isEmpty()) {
@@ -7153,47 +7789,50 @@
            return R.ok(vo);
        }
        List<Integer> gunIds = r.getData();
        //统计
        // 统计: gunIds 分批查询, 避免超大 IN 列表撑爆 MyCat 规划器
        Long total = 0L;
        if (gunIds != null && !gunIds.isEmpty()) {
            total = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>()
                    .eq(TChargingOrder::getDelFlag, 0)//未删除
                    .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束
                    .in(TChargingOrder::getChargingGunId, gunIds));//符合条件的充电枪
            for (List<Integer> chunk : chunkList(gunIds, 500)) {
                Long chunkCount = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>()
                        .eq(TChargingOrder::getDelFlag, 0)//未删除
                        .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束
                        .in(TChargingOrder::getChargingGunId, chunk));//符合条件的充电枪
                total += chunkCount == null ? 0L : chunkCount;
            }
        }
        vo.setTotal(total.intValue());
        //2.折线图
   /*   // 计算日期范围:前七天(不含今日)直接查找数据量太大,内存报错
      LocalDate today = LocalDate.now();
      LocalDateTime startDate = today.minusDays(7).atStartOfDay();
      LocalDateTime endDate = today.minusDays(1).atTime(LocalTime.MAX);
      // 查询数据库
      List<OrderCountByDate> orderCounts = this.baseMapper.countOrdersByDate(startDate, endDate, gunIds);*/
        List<LocalDate> dates = new ArrayList<>();
        List<Integer> counts = new ArrayList<>();
        LocalDate today = LocalDate.now();
        // 直接查找数据量太大,内存报错,分每天查找
        for (int i = 7; i >= 1; i--) {//升序
            LocalDate day = today.minusDays(i);
            LocalDateTime startOfDay = day.atStartOfDay();
            LocalDateTime endOfDay = day.atTime(LocalTime.MAX);
            //统计
            Long count = 0L;
            LocalDateTime dayStart = day.atStartOfDay();
            LocalDateTime dayEnd = day.plusDays(1).atStartOfDay();
            long dayCount = 0L;
            if (gunIds != null && !gunIds.isEmpty()) {
                count = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>()
                        .eq(TChargingOrder::getDelFlag, 0)//未删除
                        .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束
                        .in(TChargingOrder::getChargingGunId, gunIds)//符合条件的充电枪 站点 超充快充
                        .between(TChargingOrder::getCreateTime, startOfDay, endOfDay));//每天
                for (List<Integer> chunk : chunkList(gunIds, 500)) {
                    Long chunkCount = this.baseMapper.countOrdersInRangeByGunIds(dayStart, dayEnd, chunk);
                    dayCount += chunkCount == null ? 0L : chunkCount;
                }
            }
            dates.add(day);
            counts.add(count.intValue());
            counts.add((int) dayCount);
        }
        vo.setDates(dates);
        vo.setCounts(counts);
        redisService.setCacheObject(cacheKey, vo, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
        return R.ok(vo);
    }
    /**
     * 列表分批, 控制单条 SQL 的 IN 列表规模
     */
    private <T> List<List<T>> chunkList(List<T> list, int size) {
        List<List<T>> chunks = new ArrayList<>();
        for (int i = 0; i < list.size(); i += size) {
            chunks.add(list.subList(i, Math.min(i + size, list.size())));
        }
        return chunks;
    }
    @Override
@@ -7958,10 +8597,8 @@
        if (chargingOrder == null || !StringUtils.hasLength(chargingOrder.getCode())) {
            return;
        }
        LambdaQueryWrapper<TChargingOrderShareRecord> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TChargingOrderShareRecord::getChargingOrderCode, chargingOrder.getCode())
                .notIn(TChargingOrderShareRecord::getOnlineShareStatus, "SUCCESS", "PROCESSING");
        int count = chargingOrderShareRecordMapper.delete(wrapper);
        // 使用自定义 Mapper 方法执行原生 DELETE,绕过 MyBatis-Plus @TableLogic 逻辑删除拦截
        int count = chargingOrderShareRecordMapper.physicalDeleteStaleByOrderCode(chargingOrder.getCode());
        if (count > 0) {
            log.info("物理删除充电订单旧分账记录,订单号:{},删除数量:{}", chargingOrder.getCode(), count);
        }
@@ -8024,9 +8661,8 @@
    private boolean isNeedChargingOrderShare(TChargingOrder chargingOrder) {
        return chargingOrder != null
                && Integer.valueOf(5).equals(chargingOrder.getStatus())
                && Integer.valueOf(2).equals(chargingOrder.getRechargePaymentStatus())
                && Integer.valueOf(2).equals(chargingOrder.getRefundStatus())
                && (Integer.valueOf(2).equals(chargingOrder.getRefundStatus()) || Integer.valueOf(5).equals(chargingOrder.getStatus()))
                && getChargingOrderShareBaseAmount(chargingOrder).compareTo(BigDecimal.ZERO) > 0;
    }