puzhibing
2026-08-19 70b5c8587de6376d463fba0278d123a9d0f32827
fix: exclude fully refunded orders from income stats & align franchisee dashboard amount/count
6个文件已修改
156 ■■■■■ 已修改文件
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderPhoneFilterMapperTest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -219,6 +219,7 @@
        List<Integer> siteIds = franchiseeSiteIds(params);
        LocalDateTime[] range = franchiseeRange(params);
        List<FranchiseeDayStat> dayStats = franchiseeDayStats(siteIds, range);
        List<Map<String, Object>> managementOrderStats = franchiseeManagementOrderStats(siteIds, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        BigDecimal incomeWan = franchiseeIncomeWan(dayStats);
        BigDecimal totalChargingCapacityWan = sum(dayStats, FranchiseeDayStat::getCapacity).divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP);
@@ -227,15 +228,15 @@
        summary.put("serviceFeeWan", sum(dayStats, FranchiseeDayStat::getServiceFee).divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP));
        summary.put("totalChargingCapacityWan", totalChargingCapacityWan);
        summary.put("chargingPowerWan", totalChargingCapacityWan);
        summary.put("chargingAmount", sum(dayStats, FranchiseeDayStat::getChargingIncome));
        summary.put("chargingOrderCount", dayStats.stream().mapToLong(stat -> numberValue(stat.getOrderCount())).sum());
        summary.put("chargingAmount", sumManagementOrderField(managementOrderStats, "paymoney"));
        summary.put("chargingOrderCount", sumManagementOrderField(managementOrderStats, "orderCount").longValue());
        summary.put("powerUtilizationRate", observedPowerUtilization(dayStats));
        summary.put("demandPowerSatisfactionRate", demandPowerSatisfaction(dayStats));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("summary", summary);
        result.put("keyDataLine", franchiseeDailyTrend(dayStats, range, "收入合计", FranchiseeDayStat::getChargingIncome));
        result.put("keyDataRing", franchiseePhaseRing(siteIds, range));
        result.put("orderStats", franchiseeOrderTrend(dayStats, range));
        result.put("orderStats", franchiseeManagementOrderTrend(managementOrderStats));
        result.put("efficiency", franchiseeEfficiencyTrend(dayStats, range));
        result.put("powerDistribution", franchiseePowerDistribution(siteIds, range));
        return R.ok(result);
@@ -679,48 +680,52 @@
        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<FranchiseeDayStat> stats, LocalDateTime[] range) {
        if (franchiseeUseMonthlyTrend(range)) {
            return franchiseeMonthlyOrderTrend(stats, range);
    private List<Map<String, Object>> franchiseeManagementOrderStats(List<Integer> siteIds, LocalDateTime[] range) {
        if (siteIds == null || siteIds.isEmpty() || range == null || range.length < 2
                || range[0] == null || range[1] == null || !range[1].isAfter(range[0])) {
            return Collections.emptyList();
        }
        Map<String, Object> chart = franchiseeDailyTrend(stats, range, "充电金额", FranchiseeDayStat::getOrderAmountEffective);
        ((List<Map<String, Object>>) chart.get("series")).get(0).put("type", "column");
        Map<LocalDate, FranchiseeDayStat> byDay = dayStatByDay(stats);
        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); FranchiseeDayStat stat = byDay.get(day); countData.add(BigDecimal.valueOf(stat == null ? 0L : numberValue(stat.getOrderCount()))); }
        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;
    }
    /** Ranges longer than 31 days use monthly buckets so the mobile chart stays readable. */
    private boolean franchiseeUseMonthlyTrend(LocalDateTime[] range) {
        if (range == null || range.length < 2 || range[0] == null || range[1] == null) {
            return false;
        }
        return range[0].toLocalDate().plusDays(31).isBefore(range[1].toLocalDate());
    }
    private Map<String, Object> franchiseeMonthlyOrderTrend(List<FranchiseeDayStat> stats, LocalDateTime[] range) {
        Map<YearMonth, List<FranchiseeDayStat>> statsByMonth = stats.stream()
                .filter(stat -> stat != null && stat.getStatDay() != null)
                .collect(Collectors.groupingBy(stat -> YearMonth.from(stat.getStatDay())));
        LocalDate start = range[0].toLocalDate();
        LocalDate end = range[1].toLocalDate();
        YearMonth firstMonth = YearMonth.from(start);
        YearMonth lastMonth = YearMonth.from(end.minusDays(1));
        LocalDate end = range[1].minusNanos(1).toLocalDate();
        return safeList(chargingOrderService.getByDate(start, end, siteIds));
    }
    private BigDecimal sumManagementOrderField(List<Map<String, Object>> stats, String field) {
        return safeList(stats).stream()
                .map(row -> decimalValue(row == null ? null : row.get(field)))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    private BigDecimal decimalValue(Object value) {
        if (value == null) return BigDecimal.ZERO;
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (NumberFormatException ignored) {
            return BigDecimal.ZERO;
        }
    }
    private Map<String, Object> franchiseeManagementOrderTrend(List<Map<String, Object>> stats) {
        List<String> categories = new ArrayList<>();
        List<BigDecimal> amountData = new ArrayList<>();
        List<BigDecimal> countData = new ArrayList<>();
        for (YearMonth month = firstMonth; !month.isAfter(lastMonth); month = month.plusMonths(1)) {
            List<FranchiseeDayStat> monthStats = statsByMonth.getOrDefault(month, Collections.emptyList());
            categories.add(month.toString());
            amountData.add(sum(monthStats, FranchiseeDayStat::getOrderAmountEffective));
            countData.add(BigDecimal.valueOf(monthStats.stream().mapToLong(stat -> numberValue(stat.getOrderCount())).sum()));
        for (Map<String, Object> row : safeList(stats)) {
            if (row == null) continue;
            categories.add(String.valueOf(row.get("time")));
            amountData.add(decimalValue(row.get("paymoney")));
            countData.add(decimalValue(row.get("orderCount")));
        }
        Map<String, Object> amountSeries = new LinkedHashMap<>(); amountSeries.put("name", "充电金额"); amountSeries.put("data", amountData); amountSeries.put("type", "column");
        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);
        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", categories); chart.put("series", new ArrayList<>(Arrays.asList(amountSeries, countSeries))); return chart;
        Map<String, Object> amountSeries = new LinkedHashMap<>();
        amountSeries.put("name", "充电金额"); amountSeries.put("data", amountData); amountSeries.put("type", "column");
        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);
        Map<String, Object> chart = new LinkedHashMap<>();
        chart.put("categories", categories);
        chart.put("series", new ArrayList<>(Arrays.asList(amountSeries, countSeries)));
        return chart;
    }
    private Map<String, Object> franchiseePhaseRing(List<Integer> siteIds, LocalDateTime[] range) {
        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);
        if (siteIds != null && !siteIds.isEmpty() && range != null && range[0] != null && range[1] != null) {
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -2652,9 +2652,9 @@
    <select id="chargingOrderBaseSummary" resultType="map">
        select
        sum(COALESCE(summary.charging_duration, UNIX_TIMESTAMP(b.end_time) - UNIX_TIMESTAMP(b.start_time))) as `time`,
        sum(COALESCE(summary.income, IFNULL(b.electrovalence, 0) + CASE WHEN b.order_source = 1 THEN IFNULL(b.service_charge, 0) ELSE IFNULL(b.service_charge, 0) * 0.8 END)) as income,
        sum(COALESCE(summary.electric_price, IFNULL(b.electrovalence, 0))) as electronicMoney,
        sum(COALESCE(summary.service_price, CASE WHEN b.order_source = 1 THEN IFNULL(b.service_charge, 0) ELSE IFNULL(b.service_charge, 0) * 0.8 END)) as serviceMoney,
        sum(CASE WHEN b.refund_status = 2 THEN 0 ELSE COALESCE(summary.income, IFNULL(b.electrovalence, 0) + CASE WHEN b.order_source = 1 THEN IFNULL(b.service_charge, 0) ELSE IFNULL(b.service_charge, 0) * 0.8 END) END) as income,
        sum(CASE WHEN b.refund_status = 2 THEN 0 ELSE COALESCE(summary.electric_price, IFNULL(b.electrovalence, 0)) END) as electronicMoney,
        sum(CASE WHEN b.refund_status = 2 THEN 0 ELSE COALESCE(summary.service_price, CASE WHEN b.order_source = 1 THEN IFNULL(b.service_charge, 0) ELSE IFNULL(b.service_charge, 0) * 0.8 END) END) as serviceMoney,
        sum(COALESCE(summary.charging_capacity, IFNULL(b.charging_capacity, 0))) as total,
        sum(CASE WHEN b.recharge_payment_type = 1 AND IFNULL(b.third_pay_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.payment_amount, IFNULL(b.recharge_amount, 0)) ELSE 0 END) - IFNULL(b.third_refund_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.refund_amount, 0) ELSE 0 END) &gt; 0 THEN IFNULL(b.third_pay_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.payment_amount, IFNULL(b.recharge_amount, 0)) ELSE 0 END) - IFNULL(b.third_refund_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.refund_amount, 0) ELSE 0 END) ELSE 0 END) as wechatIncome,
        sum(CASE WHEN b.recharge_payment_type in (2, 3) AND IFNULL(b.third_pay_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.payment_amount, IFNULL(b.recharge_amount, 0)) ELSE 0 END) - IFNULL(b.third_refund_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.refund_amount, 0) ELSE 0 END) &gt; 0 THEN IFNULL(b.third_pay_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.payment_amount, IFNULL(b.recharge_amount, 0)) ELSE 0 END) - IFNULL(b.third_refund_amount, CASE WHEN b.pay_method = 3 THEN IFNULL(b.refund_amount, 0) ELSE 0 END) ELSE 0 END) as alipayIncome,
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java
@@ -23,7 +23,9 @@
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -235,4 +237,50 @@
        assertEquals("企业车", result.get(1).get("name"));
        assertEquals(3L, result.get(1).get("value"));
    }
    @Test
    void franchiseeManagementOrderStatsUsesSameServiceAndDateRangeAsManagementStatistics() {
        TChargingOrderController controller = new TChargingOrderController();
        TChargingOrderService chargingOrderService = mock(TChargingOrderService.class);
        ReflectionTestUtils.setField(controller, "chargingOrderService", chargingOrderService);
        List<Integer> siteIds = Arrays.asList(11, 12);
        LocalDate start = LocalDate.of(2026, 8, 1);
        LocalDate end = LocalDate.of(2026, 8, 31);
        List<Map<String, Object>> managementStats = Arrays.asList(managementStat("2026-08", "123.45", 7));
        when(chargingOrderService.getByDate(start, end, siteIds)).thenReturn(managementStats);
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> result = ReflectionTestUtils.invokeMethod(controller,
                "franchiseeManagementOrderStats", siteIds,
                new LocalDateTime[]{start.atStartOfDay(), end.plusDays(1).atStartOfDay()});
        assertEquals(managementStats, result);
        verify(chargingOrderService).getByDate(start, end, siteIds);
    }
    @Test
    void franchiseeManagementOrderTrendKeepsManagementValuesWithoutRecalculating() {
        TChargingOrderController controller = new TChargingOrderController();
        List<Map<String, Object>> stats = Arrays.asList(
                managementStat("2026-08-01", "12.34", 2),
                managementStat("2026-08-02", "56.78", 3));
        @SuppressWarnings("unchecked")
        Map<String, Object> chart = ReflectionTestUtils.invokeMethod(controller,
                "franchiseeManagementOrderTrend", stats);
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> series = (List<Map<String, Object>>) chart.get("series");
        assertEquals(Arrays.asList("2026-08-01", "2026-08-02"), chart.get("categories"));
        assertEquals(Arrays.asList(new BigDecimal("12.34"), new BigDecimal("56.78")), series.get(0).get("data"));
        assertEquals(Arrays.asList(new BigDecimal("2"), new BigDecimal("3")), series.get(1).get("data"));
    }
    private Map<String, Object> managementStat(String time, String paymoney, int orderCount) {
        Map<String, Object> stat = new LinkedHashMap<>();
        stat.put("time", time);
        stat.put("paymoney", new BigDecimal(paymoney));
        stat.put("orderCount", orderCount);
        return stat;
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java
@@ -33,8 +33,10 @@
                .toLowerCase(Locale.ROOT)
                .replaceAll("\\s+", " ");
        assertTrue(summarySql.contains("a.period_electric_price"));
        assertTrue(summarySql.contains("a.period_service_price"));
        // 8/05 起汇总改为 summary 预计算表优先(性能优化),电费/服务费/收入仍须对全额退款订单(refund_status=2)计 0
        assertTrue(summarySql.contains("coalesce(summary.income"));
        assertTrue(summarySql.contains("coalesce(summary.electric_price"));
        assertTrue(summarySql.contains("coalesce(summary.service_price"));
        assertEquals(3, countOccurrences(summarySql, "when b.refund_status = 2 then 0"));
    }
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderPhoneFilterMapperTest.java
@@ -27,7 +27,8 @@
        }
        assertTrue(mapper.contains("app_user.phone LIKE CONCAT('%',#{req.phone},'%')"));
        assertEquals(5, countOccurrences(mapper,
        // 8/18 起 VIP 列表 chargingOrder 改为单表查询,手机号由服务层回填,剩余 4 处为汇总类查询的 phone 内联过滤
        assertEquals(4, countOccurrences(mapper,
                "inner join `charging_pile_account`.`t_app_user` app_user on app_user.id ="));
    }
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java
@@ -9,13 +9,14 @@
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;
class VipChargingOrderExportMapperTest {
    @Test
    void chargingOrderQueryIncludesUserPhoneAndPartnerName() throws IOException {
    void chargingOrderQueryIsSingleTableWithServiceLayerBackfill() throws IOException {
        InputStream input = getClass().getClassLoader()
                .getResourceAsStream("mapper/order/TChargingOrderMapper.xml");
        assertNotNull(input);
@@ -28,9 +29,10 @@
                mapper.indexOf("<select id=\"chargingOrder\""),
                mapper.indexOf("<select id=\"payOrderQuery\""));
        assertTrue(statement.contains("app_user.phone"));
        assertTrue(statement.contains("partner.name as partnerName"));
        assertTrue(statement.contains("left join `charging_pile_account`.`t_app_user`"));
        assertTrue(statement.contains("left join `charging_pile_service`.`t_partner`"));
        assertTrue(statement.contains("from t_charging_order t1"));
        // 列表 SQL 不再跨库 JOIN app_user/partner(分片表无筛选全量广播会超时),
        // 手机号与合作商名由服务层 fillChargingOrderPageExtra 回填(TChargingOrderServiceImpl#2803)
        assertFalse(statement.contains("app_user"));
        assertFalse(statement.contains("partner.name"));
    }
}