From a298275d134ac8b2b44c79e5440aa21d7fa83abd Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期六, 25 七月 2026 20:38:51 +0800
Subject: [PATCH] 大屏fix

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java |  390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 390 insertions(+), 0 deletions(-)

diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index faa2788..9314981 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -15,6 +15,7 @@
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.ruoyi.account.api.feignClient.AppUserCarClient;
 import com.ruoyi.account.api.feignClient.AppUserClient;
+import com.ruoyi.account.api.model.TAppUser;
 import com.ruoyi.account.api.model.TAppUserCar;
 import com.ruoyi.chargingPile.api.domain.SiteMenu;
 import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
@@ -23,6 +24,7 @@
 import com.ruoyi.chargingPile.api.model.TChargingGun;
 import com.ruoyi.chargingPile.api.model.TChargingPile;
 import com.ruoyi.chargingPile.api.model.TParkingRecord;
+import com.ruoyi.chargingPile.api.vo.GetChargingGunByChargingPileIdsVo;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.dto.ChargingOrderGroup;
 import com.ruoyi.common.core.dto.ChargingPercentProvinceDto;
@@ -65,6 +67,10 @@
 import com.ruoyi.order.vo.WatchChargingOrderVo;
 import com.ruoyi.other.api.feignClient.RoleSiteClient;
 import com.ruoyi.other.api.feignClient.UserSiteClient;
+import com.ruoyi.other.api.feignClient.VipClient;
+import com.ruoyi.other.api.feignClient.OtherClient;
+import com.ruoyi.other.api.domain.TCompany;
+import com.ruoyi.other.api.domain.TVip;
 import com.ruoyi.payment.api.feignClient.AliPaymentClient;
 import com.ruoyi.payment.api.feignClient.WxPaymentClient;
 import com.ruoyi.payment.api.vo.IcbcPaymentResp;
@@ -169,8 +175,392 @@
     @Resource
     private RoleSiteClient roleSiteClient;
     @Resource
+    private VipClient vipClient;
+    @Resource
+    private OtherClient otherClient;
+    @Resource
     private SysUserRoleClient sysUserRoleClient;
 
+    /** 小程序加盟商看板:站点范围始终从登录账号绑定关系解析。 */
+    @PostMapping("/app/franchisee/charge-data")
+    public R<Map<String, Object>> franchiseeChargeData(@RequestBody(required = false) Map<String, Object> params) {
+        List<TChargingOrder> orders = franchiseeOrders(params);
+        LocalDateTime[] range = franchiseeRange(params);
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put("orderAmountWan", sum(orders, TChargingOrder::getOrderAmount).divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP));
+        summary.put("serviceFeeWan", sum(orders, TChargingOrder::getServiceCharge).divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP));
+        summary.put("chargingPowerWan", sum(orders, TChargingOrder::getChargingCapacity).divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP));
+        summary.put("chargingAmount", sum(orders, TChargingOrder::getPaymentAmount));
+        summary.put("chargingOrderCount", orders.size());
+        summary.put("powerUtilizationRate", observedPowerUtilization(orders));
+        summary.put("demandPowerSatisfactionRate", demandPowerSatisfaction(orders));
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("summary", summary);
+        result.put("keyDataLine", franchiseeDailyTrend(orders, range, "订单金额", order -> firstNonNull(order.getOrderAmount(), order.getPaymentAmount())));
+        result.put("keyDataRing", franchiseePhaseRing(orders));
+        result.put("orderStats", franchiseeOrderTrend(orders, range));
+        result.put("efficiency", franchiseeEfficiencyTrend(orders, range));
+        result.put("powerDistribution", franchiseePowerDistribution(orders));
+        return R.ok(result);
+    }
+
+    @PostMapping("/app/franchisee/income")
+    public R<Map<String, Object>> franchiseeIncome(@RequestBody(required = false) Map<String, Object> params) {
+        List<Integer> siteIds = franchiseeSiteIds(params);
+        LocalDate firstMonth = YearMonth.now().minusMonths(5).atDay(1);
+        LocalDate nextMonth = YearMonth.now().plusMonths(1).atDay(1);
+        List<TChargingOrder> orders = franchiseeOrders(siteIds, firstMonth.atStartOfDay(), nextMonth.atStartOfDay());
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("totalIncome", sum(orders, TChargingOrder::getPaymentAmount));
+        result.put("electricityFee", sum(orders, TChargingOrder::getElectrovalence));
+        result.put("serviceFee", sum(orders, TChargingOrder::getServiceCharge));
+        result.put("incomeColumn", franchiseeIncomeTrend(orders, firstMonth, 6));
+        List<Map<String, Object>> incomePie = new ArrayList<>();
+        Map<String, Object> electricityItem = new LinkedHashMap<>();
+        electricityItem.put("name", "充电费");
+        electricityItem.put("value", sum(orders, TChargingOrder::getElectrovalence));
+        incomePie.add(electricityItem);
+        Map<String, Object> serviceItem = new LinkedHashMap<>();
+        serviceItem.put("name", "服务费");
+        serviceItem.put("value", sum(orders, TChargingOrder::getServiceCharge));
+        incomePie.add(serviceItem);
+        result.put("incomePie", incomePie);
+        return R.ok(result);
+    }
+
+    @PostMapping("/app/franchisee/user")
+    public R<Map<String, Object>> franchiseeUser(@RequestBody(required = false) Map<String, Object> params) {
+        List<TChargingOrder> orders = franchiseeOrders(params);
+        Set<Long> users = orders.stream().map(TChargingOrder::getAppUserId).filter(Objects::nonNull).collect(Collectors.toSet());
+        List<Integer> siteIds = franchiseeSiteIds(params);
+        LocalDateTime[] range = franchiseeRange(params);
+        Set<Long> newUsers = new HashSet<>();
+        if (!siteIds.isEmpty() && range[0] != null && range[1] != null) {
+            List<TChargingOrder> allOrders = franchiseeOrders(siteIds, null, range[1]);
+            Map<Long, LocalDateTime> firstOrder = new HashMap<>();
+            for (TChargingOrder order : allOrders) {
+                if (order.getAppUserId() == null || order.getEndTime() == null) continue;
+                firstOrder.merge(order.getAppUserId(), order.getEndTime(), (oldTime, newTime) -> oldTime.isBefore(newTime) ? oldTime : newTime);
+            }
+            firstOrder.forEach((userId, firstTime) -> { if (!firstTime.isBefore(range[0])) newUsers.add(userId); });
+        }
+
+        List<TAppUser> appUsers = Collections.emptyList();
+        if (!users.isEmpty()) {
+            R<List<TAppUser>> userResult = appUserClient.getUserByIds(new ArrayList<>(users));
+            if (userResult != null && userResult.getData() != null) appUsers = userResult.getData();
+        }
+        Map<Long, TAppUser> userMap = appUsers.stream().filter(user -> user.getId() != null)
+                .collect(Collectors.toMap(TAppUser::getId, user -> user, (left, right) -> left));
+
+        List<Long> carIds = orders.stream().map(TChargingOrder::getAppUserCarId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        List<TAppUserCar> cars = Collections.emptyList();
+        if (!carIds.isEmpty()) {
+            R<List<TAppUserCar>> carResult = appUserCarClient.getCarByIds(carIds);
+            if (carResult != null && carResult.getData() != null) cars = carResult.getData();
+        }
+
+        Map<String, Object> evaluation = franchiseeEvaluation(siteIds);
+        Map<String, Object> result = new LinkedHashMap<>();
+        Map<String, Object> summary = new LinkedHashMap<>(); summary.put("chargeUserCount", users.size()); summary.put("newUserCount", newUsers.size());
+        result.put("summary", summary);
+        result.put("userStats", franchiseeUserTrend(orders, newUsers, range));
+        result.put("userTags", franchiseeUserTags(orders));
+        Map<Integer, String> vipNames = new HashMap<>();
+        R<List<TVip>> vipResult = vipClient.getAllVip();
+        if (vipResult != null && vipResult.getData() != null) vipResult.getData().forEach(vip -> vipNames.put(vip.getId(), vip.getName()));
+        result.put("memberType", franchiseeMemberTypes(appUsers, vipNames));
+        result.put("corpRank", franchiseeCompanyRank(orders, userMap));
+        result.put("evaluation", evaluation.get("summary"));
+        result.put("goodTags", evaluation.get("goodTags"));
+        result.put("badTags", evaluation.get("badTags"));
+        result.put("traffic", franchiseeTraffic(orders));
+        result.put("vehicleClass", franchiseeCarGroup(cars, TAppUserCar::getVehicleUse, "未知用途", 5));
+        result.put("vehicleBrand", franchiseeCarGroup(cars, TAppUserCar::getVehicleBrand, "其他", 5));
+        return R.ok(result);
+    }
+
+    @PostMapping("/app/franchisee/operation")
+    public R<Map<String, Object>> franchiseeOperation(@RequestBody(required = false) Map<String, Object> params) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        List<Integer> siteIds = franchiseeSiteIds(params);
+        List<TChargingPile> piles = Collections.emptyList();
+        if (!siteIds.isEmpty()) { R<List<TChargingPile>> pileResult = chargingPileClient.getChargingPileBySiteIds(siteIds); if (pileResult != null && pileResult.getData() != null) piles = pileResult.getData(); }
+        List<TChargingGun> guns = Collections.emptyList();
+        List<Integer> pileIds = piles.stream().map(TChargingPile::getId).filter(Objects::nonNull).collect(Collectors.toList());
+        if (!pileIds.isEmpty()) {
+            GetChargingGunByChargingPileIdsVo query = new GetChargingGunByChargingPileIdsVo(); query.setChargingPileIds(pileIds);
+            R<List<TChargingGun>> gunResult = chargingGunClient.getChargingGunByChargingPileIds(query);
+            if (gunResult != null && gunResult.getData() != null) guns = gunResult.getData();
+        }
+        BigDecimal total = BigDecimal.valueOf(guns.size());
+        BigDecimal available = BigDecimal.valueOf(guns.stream().filter(this::isAvailableGun).count());
+        BigDecimal fault = countGuns(guns, 7), offline = countGuns(guns, 1);
+        List<TChargingOrder> operationOrders = franchiseeOrders(params);
+        BigDecimal powerSatisfaction = demandPowerSatisfaction(operationOrders);
+        Map<String, Object> summary = new LinkedHashMap<>(); summary.put("availableRate", rate(available, total)); summary.put("faultRate", rate(fault, total)); summary.put("offlineRate", rate(offline, total)); summary.put("powerSatisfactionRate", powerSatisfaction);
+        result.put("summary", summary); result.put("dcCharts", operationCharts(guns, operationOrders, true)); result.put("acCharts", operationCharts(guns, operationOrders, false));
+        return R.ok(result);
+    }
+
+    private List<TChargingOrder> franchiseeOrders(Map<String, Object> params) {
+        List<Integer> siteIds = franchiseeSiteIds(params);
+        if (siteIds.isEmpty()) return Collections.emptyList();
+        LocalDateTime[] range = franchiseeRange(params);
+        return franchiseeOrders(siteIds, range[0], range[1]);
+    }
+
+    private List<TChargingOrder> franchiseeOrders(List<Integer> siteIds, LocalDateTime start, LocalDateTime end) {
+        if (siteIds == null || siteIds.isEmpty()) return Collections.emptyList();
+        LambdaQueryWrapper<TChargingOrder> query = Wrappers.lambdaQuery(TChargingOrder.class).in(TChargingOrder::getSiteId, siteIds).eq(TChargingOrder::getDelFlag, 0).eq(TChargingOrder::getRechargePaymentStatus, 2);
+        if (start != null) query.ge(TChargingOrder::getEndTime, start);
+        if (end != null) query.lt(TChargingOrder::getEndTime, end);
+        return chargingOrderService.list(query);
+    }
+
+    private List<Integer> franchiseeSiteIds(Map<String, Object> params) {
+        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) return Collections.emptyList();
+        Long appUserId = tokenService.getLoginUserApplet().getUserId();
+        R<com.ruoyi.chargingPile.api.model.Partner> partnerResult = partnerClient.getPartnerByAppUserId(appUserId);
+        com.ruoyi.chargingPile.api.model.Partner partner = partnerResult == null ? null : partnerResult.getData();
+        if (partner == null || partner.getId() == null) return Collections.emptyList();
+        R<List<Site>> siteResult = siteClient.getSiteAll();
+        List<Site> sites = siteResult == null ? null : siteResult.getData();
+        List<Integer> siteIds = sites == null ? new ArrayList<>() : sites.stream().filter(s -> Objects.equals(s.getPartnerId(), partner.getId())).map(Site::getId).collect(Collectors.toList());
+        Object requestedSite = params == null ? null : params.get("siteId");
+        if (requestedSite != null && !String.valueOf(requestedSite).isEmpty()) {
+            Integer siteId = Integer.valueOf(String.valueOf(requestedSite)); siteIds = siteIds.contains(siteId) ? Collections.singletonList(siteId) : Collections.singletonList(-1);
+        }
+        return siteIds;
+    }
+
+    private Map<String, Object> franchiseeUserTrend(List<TChargingOrder> orders, Set<Long> newUsers, LocalDateTime[] range) {
+        LocalDate start = range[0] == null ? LocalDate.now() : range[0].toLocalDate();
+        LocalDate end = range[1] == null ? start.plusDays(1) : range[1].toLocalDate();
+        List<String> categories = new ArrayList<>();
+        List<BigDecimal> chargeUsers = new ArrayList<>(), newUserCounts = new ArrayList<>();
+        for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1)) {
+            LocalDateTime dayStart = day.atStartOfDay(), dayEnd = day.plusDays(1).atStartOfDay();
+            Set<Long> dayUsers = orders.stream().filter(order -> order.getAppUserId() != null && order.getEndTime() != null
+                            && !order.getEndTime().isBefore(dayStart) && order.getEndTime().isBefore(dayEnd))
+                    .map(TChargingOrder::getAppUserId).collect(Collectors.toSet());
+            long newCount = dayUsers.stream().filter(newUsers::contains).count();
+            categories.add(day.toString().substring(5));
+            chargeUsers.add(BigDecimal.valueOf(dayUsers.size()));
+            newUserCounts.add(BigDecimal.valueOf(newCount));
+        }
+        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", categories);
+        Map<String, Object> chargeSeries = new LinkedHashMap<>(); chargeSeries.put("name", "充电用户"); chargeSeries.put("data", chargeUsers);
+        Map<String, Object> newSeries = new LinkedHashMap<>(); newSeries.put("name", "新增用户"); newSeries.put("data", newUserCounts);
+        chart.put("series", Arrays.asList(chargeSeries, newSeries)); return chart;
+    }
+
+    private List<Map<String, Object>> franchiseeUserTags(List<TChargingOrder> orders) {
+        Map<Long, Long> counts = orders.stream().filter(order -> order.getAppUserId() != null)
+                .collect(Collectors.groupingBy(TChargingOrder::getAppUserId, Collectors.counting()));
+        long active = counts.values().stream().filter(count -> count >= 5).count();
+        long normal = counts.values().stream().filter(count -> count >= 2 && count < 5).count();
+        long lowFrequency = counts.values().stream().filter(count -> count == 1).count();
+        return namedCounts(new String[]{"活跃用户", "普通用户", "低频用户"}, new long[]{active, normal, lowFrequency});
+    }
+
+    private List<Map<String, Object>> franchiseeMemberTypes(List<TAppUser> users, Map<Integer, String> vipNames) {
+        long ordinary = users.stream().filter(user -> user.getVipId() == null || user.getVipId() == 0).count();
+        Map<Integer, Long> vipCounts = users.stream().filter(user -> user.getVipId() != null && user.getVipId() != 0)
+                .collect(Collectors.groupingBy(TAppUser::getVipId, Collectors.counting()));
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (ordinary > 0) result.add(namedCount("普通用户", ordinary));
+        vipCounts.forEach((vipId, count) -> result.add(namedCount(vipNames.getOrDefault(vipId, "会员" + vipId), count)));
+        return result;
+    }
+
+    private Map<String, Object> franchiseeCompanyRank(List<TChargingOrder> orders, Map<Long, TAppUser> userMap) {
+        Map<Integer, BigDecimal> totals = new HashMap<>();
+        orders.forEach(order -> {
+            TAppUser user = userMap.get(order.getAppUserId());
+            if (user != null && user.getCompanyId() != null) {
+                totals.merge(user.getCompanyId(), value(order.getChargingCapacity()), BigDecimal::add);
+            }
+        });
+        List<Map.Entry<Integer, BigDecimal>> sorted = totals.entrySet().stream()
+                .sorted(Map.Entry.<Integer, BigDecimal>comparingByValue().reversed()).limit(5).collect(Collectors.toList());
+        Map<String, Object> chart = new LinkedHashMap<>();
+        chart.put("categories", sorted.stream().map(entry -> {
+            R<TCompany> companyResult = otherClient.unitDetail(entry.getKey());
+            TCompany company = companyResult == null ? null : companyResult.getData();
+            return company == null || StringUtils.isBlank(company.getName()) ? "单位" + entry.getKey() : company.getName();
+        }).collect(Collectors.toList()));
+        Map<String, Object> series = new LinkedHashMap<>(); series.put("name", "充电量");
+        series.put("data", sorted.stream().map(Map.Entry::getValue).collect(Collectors.toList()));
+        chart.put("series", Collections.singletonList(series)); return chart;
+    }
+
+    private Map<String, Object> franchiseeEvaluation(List<Integer> siteIds) {
+        if (siteIds == null || siteIds.isEmpty()) {
+            Map<String, Object> emptySummary = new LinkedHashMap<>();
+            emptySummary.put("score", 0D); emptySummary.put("goodCount", 0); emptySummary.put("mediumCount", 0); emptySummary.put("badCount", 0);
+            emptySummary.put("goodRate", BigDecimal.ZERO); emptySummary.put("mediumRate", BigDecimal.ZERO); emptySummary.put("badRate", BigDecimal.ZERO);
+            Map<String, Object> empty = new LinkedHashMap<>(); empty.put("summary", emptySummary); empty.put("goodTags", Collections.emptyList()); empty.put("badTags", Collections.emptyList()); return empty;
+        }
+        Double score = chargingOrderService.getAver(siteIds);
+        List<Map<String, Object>> levels = chargingOrderService.getLevelEvaluate(siteIds);
+        long good = 0, medium = 0, bad = 0;
+        if (levels != null) for (Map<String, Object> level : levels) {
+            String range = String.valueOf(level.get("score_range")); long count = numberValue(level.get("count"));
+            if ("4-5".equals(range)) good += count; else if ("3".equals(range)) medium += count; else if ("1-2".equals(range)) bad += count;
+        }
+        long total = good + medium + bad;
+        Map<String, Object> summary = new LinkedHashMap<>(); summary.put("score", score == null ? 0D : score);
+        summary.put("goodCount", good); summary.put("mediumCount", medium); summary.put("badCount", bad);
+        summary.put("goodRate", ratePercent(good, total)); summary.put("mediumRate", ratePercent(medium, total)); summary.put("badRate", ratePercent(bad, total));
+        Map<String, Object> result = new LinkedHashMap<>(); result.put("summary", summary);
+        result.put("goodTags", safeList(orderEvaluateService.goodTop(siteIds))); result.put("badTags", safeList(orderEvaluateService.badTop(siteIds))); return result;
+    }
+
+    private List<Map<String, Object>> franchiseeTraffic(List<TChargingOrder> orders) {
+        Map<String, Long> counts = orders.stream().collect(Collectors.groupingBy(order -> {
+            String source = order.getTripartitePlatformName(); return source == null || source.isEmpty() ? "明星充电" : source;
+        }, Collectors.counting()));
+        return counts.entrySet().stream().map(entry -> namedCount(entry.getKey(), entry.getValue())).collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> franchiseeCarGroup(List<TAppUserCar> cars, java.util.function.Function<TAppUserCar, String> getter, String emptyName, int limit) {
+        Map<String, Long> counts = cars.stream().collect(Collectors.groupingBy(car -> {
+            String value = getter.apply(car); return value == null || value.trim().isEmpty() ? emptyName : value;
+        }, Collectors.counting()));
+        return counts.entrySet().stream().sorted(Map.Entry.<String, Long>comparingByValue().reversed()).limit(limit)
+                .map(entry -> namedCount(entry.getKey(), entry.getValue())).collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> namedCounts(String[] names, long[] values) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (int i = 0; i < names.length; i++) if (values[i] > 0) result.add(namedCount(names[i], values[i]));
+        return result;
+    }
+
+    private Map<String, Object> namedCount(String name, long value) { Map<String, Object> result = new LinkedHashMap<>(); result.put("name", name); result.put("value", value); return result; }
+    private long numberValue(Object value) { return value instanceof Number ? ((Number) value).longValue() : 0L; }
+    private BigDecimal ratePercent(long numerator, long denominator) { return denominator == 0 ? BigDecimal.ZERO : BigDecimal.valueOf(numerator * 100D / denominator).setScale(2, RoundingMode.HALF_UP); }
+    private <T> List<T> safeList(List<T> values) { return values == null ? Collections.emptyList() : values; }
+
+    private BigDecimal countGuns(List<TChargingGun> guns, int status) { return BigDecimal.valueOf(guns.stream().filter(gun -> Objects.equals(gun.getStatus(), status)).count()); }
+    private boolean isAvailableGun(TChargingGun gun) { return gun.getStatus() != null && gun.getStatus() >= 2 && gun.getStatus() <= 6; }
+    private BigDecimal rate(BigDecimal numerator, BigDecimal denominator) { return denominator.signum() == 0 ? BigDecimal.ZERO : numerator.multiply(BigDecimal.valueOf(100)).divide(denominator, 2, RoundingMode.HALF_UP); }
+    private List<Map<String, Object>> operationCharts(List<TChargingGun> guns, List<TChargingOrder> orders, boolean dc) {
+        List<TChargingGun> selected = guns.stream().filter(gun -> dc ? Objects.equals(gun.getType(), 4) : gun.getType() != null && gun.getType() >= 1 && gun.getType() <= 3).collect(Collectors.toList());
+        List<Map<String, Object>> charts = new ArrayList<>();
+        String[] keys = {"availableRate", "faultRate", "offlineRate", "powerSatisfactionRate"}; int[] statuses = {0, 7, 1, 0};
+        String[] names = {"可用率", "故障率", "离网率", "电量满足率"};
+        for (int i = 0; i < keys.length; i++) {
+            final int status = statuses[i]; Map<String, Object> chart = new LinkedHashMap<>(); chart.put("key", keys[i]);
+            List<String> categories = selected.stream().map(TChargingGun::getName).map(name -> name == null ? "未命名充电枪" : name).collect(Collectors.toList());
+            final int metricIndex = i;
+            List<BigDecimal> values = selected.stream().map(gun -> metricIndex == 3
+                    ? demandPowerSatisfaction(orders.stream().filter(order -> Objects.equals(order.getChargingGunId(), gun.getId())).collect(Collectors.toList()))
+                    : BigDecimal.valueOf(metricIndex == 0 ? (isAvailableGun(gun) ? 100 : 0) : (Objects.equals(gun.getStatus(), status) ? 100 : 0))).collect(Collectors.toList());
+            chart.put("categories", categories);
+            Map<String, Object> series = new LinkedHashMap<>(); series.put("name", names[i]); series.put("data", values); chart.put("series", Collections.singletonList(series)); charts.add(chart);
+        }
+        return charts;
+    }
+
+    private LocalDateTime[] franchiseeRange(Map<String, Object> params) {
+        LocalDate today = LocalDate.now();
+        if (params != null && params.get("startTime") != null && params.get("endTime") != null) {
+            String startText = String.valueOf(params.get("startTime"));
+            String endText = String.valueOf(params.get("endTime"));
+            if (!startText.isEmpty() && !endText.isEmpty()) {
+                try {
+                    return new LocalDateTime[]{LocalDate.parse(startText).atStartOfDay(), LocalDate.parse(endText).plusDays(1).atStartOfDay()};
+                } catch (RuntimeException ignored) {
+                    // 日期格式异常时回退到快捷日期范围。
+                }
+            }
+        }
+        String type = params == null ? "today" : String.valueOf(params.getOrDefault("dateType", "today"));
+        if ("year".equals(type)) return new LocalDateTime[]{today.withDayOfYear(1).atStartOfDay(), today.plusDays(1).atStartOfDay()};
+        if ("month".equals(type)) return new LocalDateTime[]{today.withDayOfMonth(1).atStartOfDay(), today.plusDays(1).atStartOfDay()};
+        if ("week".equals(type)) { LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1); return new LocalDateTime[]{start.atStartOfDay(), today.plusDays(1).atStartOfDay()}; }
+        return new LocalDateTime[]{today.atStartOfDay(), today.plusDays(1).atStartOfDay()};
+    }
+
+    private BigDecimal sum(List<TChargingOrder> orders, java.util.function.Function<TChargingOrder, BigDecimal> getter) { return orders.stream().map(getter).map(this::value).reduce(BigDecimal.ZERO, BigDecimal::add); }
+    private BigDecimal firstNonNull(BigDecimal first, BigDecimal second) { return first != null ? first : value(second); }
+    private BigDecimal value(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
+    private Map<String, Object> franchiseeTrend(List<TChargingOrder> orders, String name, java.util.function.Function<TChargingOrder, BigDecimal> getter) { Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", Collections.singletonList(LocalDate.now().toString())); Map<String, Object> series = new LinkedHashMap<>(); series.put("name", name); series.put("data", Collections.singletonList(sum(orders, getter))); chart.put("series", new ArrayList<>(Collections.singletonList(series))); return chart; }
+    private Map<String, Object> franchiseeDailyTrend(List<TChargingOrder> orders, LocalDateTime[] range, String name, java.util.function.Function<TChargingOrder, BigDecimal> getter) {
+        List<String> categories = new ArrayList<>(); List<BigDecimal> data = new ArrayList<>();
+        LocalDate start = range[0] == null ? LocalDate.now() : range[0].toLocalDate();
+        LocalDate end = range[1] == null ? start.plusDays(1) : range[1].toLocalDate();
+        for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1)) {
+            categories.add(day.toString().substring(5));
+            LocalDateTime dayStart = day.atStartOfDay(), dayEnd = day.plusDays(1).atStartOfDay();
+            data.add(sum(orders.stream().filter(order -> order.getEndTime() != null && !order.getEndTime().isBefore(dayStart) && order.getEndTime().isBefore(dayEnd)).collect(Collectors.toList()), getter));
+        }
+        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", categories);
+        Map<String, Object> series = new LinkedHashMap<>(); series.put("name", name); series.put("data", data); chart.put("series", new ArrayList<>(Collections.singletonList(series))); return chart;
+    }
+    private Map<String, Object> franchiseeOrderTrend(List<TChargingOrder> orders, LocalDateTime[] range) {
+        Map<String, Object> chart = franchiseeDailyTrend(orders, range, "充电金额", order -> firstNonNull(order.getPaymentAmount(), order.getOrderAmount()));
+        ((List<Map<String, Object>>) chart.get("series")).get(0).put("type", "column");
+        List<BigDecimal> countData = new ArrayList<>(); List<String> categories = (List<String>) chart.get("categories");
+        LocalDate start = range[0] == null ? LocalDate.now() : range[0].toLocalDate();
+        for (int i = 0; i < categories.size(); i++) { LocalDate day = start.plusDays(i); LocalDateTime dayStart = day.atStartOfDay(), dayEnd = day.plusDays(1).atStartOfDay(); countData.add(BigDecimal.valueOf(orders.stream().filter(order -> order.getEndTime() != null && !order.getEndTime().isBefore(dayStart) && order.getEndTime().isBefore(dayEnd)).count())); }
+        Map<String, Object> countSeries = new LinkedHashMap<>(); countSeries.put("name", "充电订单"); countSeries.put("data", countData); countSeries.put("type", "line"); countSeries.put("style", "curve"); countSeries.put("index", 1); ((List<Map<String, Object>>) chart.get("series")).add(countSeries); return chart;
+    }
+    private Map<String, Object> franchiseePhaseRing(List<TChargingOrder> orders) {
+        Map<Integer, BigDecimal> phase = new LinkedHashMap<>(); phase.put(1, BigDecimal.ZERO); phase.put(2, BigDecimal.ZERO); phase.put(3, BigDecimal.ZERO); phase.put(4, BigDecimal.ZERO);
+        List<Long> ids = orders.stream().map(TChargingOrder::getId).filter(Objects::nonNull).collect(Collectors.toList());
+        if (!ids.isEmpty()) { chargingOrderAccountingStrategyService.lambdaQuery().in(TChargingOrderAccountingStrategy::getChargingOrderId, ids).list().forEach(item -> phase.computeIfPresent(item.getType(), (key, total) -> total.add(value(item.getChargingCapacity())))); }
+        String[] names = {"尖", "峰", "平", "谷"}; List<Map<String, Object>> data = new ArrayList<>();
+        for (int i = 1; i <= 4; i++) { Map<String, Object> item = new LinkedHashMap<>(); item.put("name", names[i - 1]); item.put("value", phase.get(i)); data.add(item); }
+        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("series", Collections.singletonList(Collections.singletonMap("data", data))); return chart;
+    }
+    private Map<String, Object> franchiseeEfficiencyTrend(List<TChargingOrder> orders, LocalDateTime[] range) {
+        Map<String, Object> chart = franchiseeDailyTrend(orders, range, "功率", order -> value(order.getChargingPower() == null ? order.getPower() : order.getChargingPower()));
+        List<BigDecimal> powerData = new ArrayList<>(), utilization = new ArrayList<>(); List<String> categories = (List<String>) chart.get("categories");
+        LocalDate start = range[0] == null ? LocalDate.now() : range[0].toLocalDate();
+        for (int i = 0; i < categories.size(); i++) { LocalDate day = start.plusDays(i); LocalDateTime dayStart = day.atStartOfDay(), dayEnd = day.plusDays(1).atStartOfDay(); List<TChargingOrder> dayOrders = orders.stream().filter(order -> order.getEndTime() != null && !order.getEndTime().isBefore(dayStart) && order.getEndTime().isBefore(dayEnd)).collect(Collectors.toList()); powerData.add(averagePower(dayOrders)); utilization.add(observedPowerUtilization(dayOrders)); }
+        ((List<Map<String, Object>>) chart.get("series")).get(0).put("data", powerData);
+        Map<String, Object> utilizationSeries = new LinkedHashMap<>(); utilizationSeries.put("name", "功率利用率"); utilizationSeries.put("data", utilization); ((List<Map<String, Object>>) chart.get("series")).add(utilizationSeries); return chart;
+    }
+    private BigDecimal averagePower(List<TChargingOrder> orders) {
+        List<BigDecimal> powers = orders.stream().map(order -> order.getChargingPower() == null ? order.getPower() : order.getChargingPower()).filter(Objects::nonNull).collect(Collectors.toList());
+        return powers.isEmpty() ? BigDecimal.ZERO : powers.stream().reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(powers.size()), 2, RoundingMode.HALF_UP);
+    }
+    private BigDecimal observedPowerUtilization(List<TChargingOrder> orders) {
+        List<BigDecimal> powers = orders.stream().map(order -> order.getChargingPower() == null ? order.getPower() : order.getChargingPower()).filter(Objects::nonNull).filter(power -> power.signum() > 0).collect(Collectors.toList());
+        if (powers.isEmpty()) return BigDecimal.ZERO; BigDecimal maximum = powers.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
+        return maximum.signum() == 0 ? BigDecimal.ZERO : powers.stream().reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(powers.size()), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)).divide(maximum, 2, RoundingMode.HALF_UP);
+    }
+    private BigDecimal demandPowerSatisfaction(List<TChargingOrder> orders) {
+        List<TChargingOrder> valid = orders.stream().filter(order -> order.getCurrent() != null && order.getNeedElec() != null && order.getNeedElec().signum() > 0).collect(Collectors.toList());
+        if (valid.isEmpty()) return BigDecimal.ZERO;
+        BigDecimal total = valid.stream().map(order -> order.getCurrent().multiply(BigDecimal.valueOf(100)).divide(order.getNeedElec(), 4, RoundingMode.HALF_UP).min(BigDecimal.valueOf(100))).reduce(BigDecimal.ZERO, BigDecimal::add);
+        return total.divide(BigDecimal.valueOf(valid.size()), 2, RoundingMode.HALF_UP);
+    }
+    private Map<String, Object> franchiseePowerDistribution(List<TChargingOrder> orders) {
+        String[] names = {"0-30kW", "30-60kW", "60-120kW", "120-300kW", "300kW以上"}; long[] counts = new long[5];
+        orders.forEach(order -> { BigDecimal power = order.getChargingPower() == null ? order.getPower() : order.getChargingPower(); if (power == null) return; double value = power.doubleValue(); int index = value < 30 ? 0 : value < 60 ? 1 : value < 120 ? 2 : value < 300 ? 3 : 4; counts[index]++; });
+        List<Map<String, Object>> data = new ArrayList<>(); for (int i = 0; i < names.length; i++) { Map<String, Object> item = new LinkedHashMap<>(); item.put("name", names[i]); item.put("value", counts[i]); data.add(item); }
+        return Collections.singletonMap("series", Collections.singletonList(Collections.singletonMap("data", data)));
+    }
+    private Map<String, Object> franchiseeIncomeTrend(List<TChargingOrder> orders, LocalDate firstMonth, int monthCount) {
+        List<String> categories = new ArrayList<>();
+        List<BigDecimal> electricity = new ArrayList<>(), service = new ArrayList<>();
+        for (int i = 0; i < monthCount; i++) {
+            YearMonth month = YearMonth.from(firstMonth).plusMonths(i);
+            categories.add(String.format("%02d月", month.getMonthValue()));
+            LocalDateTime start = month.atDay(1).atStartOfDay(), end = month.plusMonths(1).atDay(1).atStartOfDay();
+            electricity.add(sum(orders.stream().filter(o -> o.getEndTime() != null && !o.getEndTime().isBefore(start) && o.getEndTime().isBefore(end)).collect(Collectors.toList()), TChargingOrder::getElectrovalence));
+            service.add(sum(orders.stream().filter(o -> o.getEndTime() != null && !o.getEndTime().isBefore(start) && o.getEndTime().isBefore(end)).collect(Collectors.toList()), TChargingOrder::getServiceCharge));
+        }
+        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", categories);
+        Map<String, Object> electricitySeries = new LinkedHashMap<>(); electricitySeries.put("name", "充电费"); electricitySeries.put("data", electricity);
+        Map<String, Object> serviceSeries = new LinkedHashMap<>(); serviceSeries.put("name", "服务费"); serviceSeries.put("data", service);
+        chart.put("series", new ArrayList<>(Arrays.asList(electricitySeries, serviceSeries))); return chart;
+    }
+
 
 
     /**

--
Gitblit v1.7.1