longz
2026-07-30 907f8421d36050ab7a24d4c151e04cbb4244305c
各类修复
7个文件已添加
15个文件已修改
817 ■■■■■ 已修改文件
docs/superpowers/plans/2026-07-30-charging-list-mycat-pagination.md 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/superpowers/plans/2026-07-30-charging-order-actual-income.md 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TAppCouponControllerTest.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/ChargingPileFilterOptionsDTO.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/PartnerMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/ChargingPileFilterOptionsDTOTest.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingPeriodPaginationMapperTest.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPeriodIncomeTest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/superpowers/plans/2026-07-30-charging-list-mycat-pagination.md
New file
@@ -0,0 +1,63 @@
# Charging List MyCat Pagination Optimization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Reduce late-page latency for `/order/financial/settlement/chargingList/page` by removing the cross-shard aggregate join from the paginated query.
**Architecture:** Page and count only `t_charging_order`, which MyCat shards by `app_user_id`, then load accounting totals for the small set of order IDs on the current page from `t_charging_order_accounting_strategy`, which is sharded by `charging_order_id`. Merge the accounting totals in Java before constructing the response, preserving the existing response fields and summary endpoint.
**Tech Stack:** Java 8, Spring Boot, MyBatis, MyBatis-Plus pagination, MyCat 2, JUnit 5
## Global Constraints
- Preserve the existing `/chargingList/page` request and response contracts.
- Do not change `/chargingList/summary` calculations.
- Do not join `t_charging_order` and `t_charging_order_accounting_strategy` in the paginated list query.
- Query accounting rows only for order IDs returned on the current page.
- Preserve existing filters and descending `start_time` ordering.
- Do not modify unrelated workspace changes.
---
### Task 1: Split paginated order and accounting queries
**Files:**
- Modify: `ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml`
- Modify: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java`
- Create: `ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingPeriodPaginationMapperTest.java`
**Interfaces:**
- Existing: `chargingList1(PageInfo<ChargingOrderListVO>, ChargingListQuery, String, String, String, String)` returns paginated base-order maps.
- New: `chargingListAccountingSummary(List<Long> orderIds)` returns maps containing `orderId`, `periodElectricPrice`, `periodServicePrice`, `jian`, `feng`, `ping`, and `gu`.
- [x] **Step 1: Add a mapper-structure test asserting that `chargingList1` reads only `t_charging_order` and that `chargingListAccountingSummary` filters by `charging_order_id IN (...)` before grouping.**
- [x] **Step 2: Run `ChargingPeriodPaginationMapperTest` and verify it fails because the split query is not implemented.**
- [x] **Step 3: Remove the derived accounting aggregation join from `chargingList1`, select the same base-order fields directly from `t_charging_order`, and retain all existing filters and ordering.**
- [x] **Step 4: Add `chargingListAccountingSummary(@Param("orderIds") List<Long> orderIds)` to the mapper interface and XML.**
### Task 2: Merge current-page accounting totals
**Files:**
- Modify: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java`
- Modify: `ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPeriodIncomeTest.java`
**Interfaces:**
- Consumes: `chargingListAccountingSummary(List<Long>)`.
- Produces: page-row maps enriched with accounting totals before response conversion.
- [x] **Step 1: Add a service test for merging accounting totals into matching order rows while leaving missing totals as zero.**
- [x] **Step 2: Run `ChargingPeriodIncomeTest` and verify it fails before the merge helper exists.**
- [x] **Step 3: After `chargingList1`, collect current-page order IDs, query accounting totals once, and merge them by `orderId`.**
- [x] **Step 4: Keep existing response calculations unchanged so electric fee, service fee, payment amount, and peak/flat/valley ratios use the merged values.**
### Task 3: Verification
**Files:**
- Verify only the files listed above.
**Interfaces:**
- Produces: build and test evidence for the optimized query path.
- [x] **Step 1: Run the two focused regression test classes.**
- [x] **Step 2: Compile the order module and all required reactor dependencies.**
- [x] **Step 3: Run `git diff --check` and confirm the paginated query contains no cross-table aggregate join.**
docs/superpowers/plans/2026-07-30-charging-order-actual-income.md
New file
@@ -0,0 +1,108 @@
# Charging Order Actual Income Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the charging-order summary income, electric-fee, and service-fee fields exclude orders whose refund has completed.
**Architecture:** Keep charging duration and charging-capacity statistics unchanged, but make the three highlighted financial aggregates refund-aware inside `chargingOrderBaseSummary`. A successful refund is identified by `t_charging_order.refund_status = 2`; refunding or unrefunded orders retain the existing calculation.
**Tech Stack:** Java 8, MyBatis XML, MyCat 2, JUnit 5
## Global Constraints
- Change only the charging-order summary financial fields `income`, `electronicMoney`, and `serviceMoney`.
- Do not remove refunded orders from charging duration or charging-capacity statistics.
- Treat `refund_status = 2` as refunded, including successful partial refunds, because current refund flows set this status after any completed refund.
- Preserve the `/chargingOrder/summary` request and response contracts.
- Do not modify unrelated workspace changes.
---
### Task 1: Add refund-aware summary regression coverage
**Files:**
- Create: `ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java`
**Interfaces:**
- Consumes: MyBatis statement `chargingOrderBaseSummary` from `mapper/order/TChargingOrderMapper.xml`.
- Produces: a structural regression test requiring all three financial expressions to return zero for `refund_status = 2`.
- [x] **Step 1: Write the failing mapper test**
```java
assertTrue(summarySql.contains("when b.refund_status = 2 then 0"));
assertTrue(summarySql.contains("a.period_electric_price"));
assertTrue(summarySql.contains("a.period_service_price"));
```
- [x] **Step 2: Run the test and verify it fails before the SQL change**
Run:
```powershell
mvn -pl ruoyi-service/ruoyi-order -am `
  "-Dtest=ChargingOrderActualIncomeMapperTest" `
  "-Dsurefire.failIfNoSpecifiedTests=false" test
```
Expected: the new assertion fails because `chargingOrderBaseSummary` still counts refunded orders.
### Task 2: Exclude refunded orders from the three income aggregates
**Files:**
- Modify: `ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml`
**Interfaces:**
- Consumes: `t_charging_order.refund_status`, where `2` means refund completed.
- Produces: unchanged `income`, `electronicMoney`, and `serviceMoney` aliases with refunded orders contributing zero.
- [x] **Step 1: Make the inner financial expressions refund-aware**
Use this calculation shape while retaining the existing third-party service-fee coefficient:
```sql
sum(CASE WHEN b.refund_status = 2 THEN 0 ELSE a.period_electric_price END) as electronicMoney,
sum(CASE
        WHEN b.refund_status = 2 THEN 0
        WHEN b.order_source = 1 THEN a.period_service_price
        ELSE a.period_service_price * 0.8
    END) as serviceMoney,
sum(CASE
        WHEN b.refund_status = 2 THEN 0
        ELSE a.period_electric_price
             + CASE WHEN b.order_source = 1 THEN a.period_service_price ELSE a.period_service_price * 0.8 END
    END) as income
```
- [x] **Step 2: Run the focused test and verify it passes**
Run the Task 1 Maven command.
Expected: `Tests run: 1, Failures: 0, Errors: 0` and `BUILD SUCCESS`.
### Task 3: Verify the order module
**Files:**
- Verify only the files listed above.
**Interfaces:**
- Produces: compilation and diff evidence for deployment.
- [x] **Step 1: Compile the order module and reactor dependencies**
```powershell
mvn -pl ruoyi-service/ruoyi-order -am -DskipTests compile
```
Expected: `BUILD SUCCESS`.
- [x] **Step 2: Check the final patch**
```powershell
git diff --check -- `
  ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml `
  ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java `
  docs/superpowers/plans/2026-07-30-charging-order-actual-income.md
```
Expected: no diff errors; the charging duration and charging-capacity expressions remain unchanged.
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java
@@ -136,7 +136,9 @@
     */
    public void antiShake(ServerHttpRequest request) throws Exception{
        HttpMethod method = request.getMethod();
        if(HttpMethod.OPTIONS == method || !antiShakeProperties.getEnable()){
        // 查询请求允许并发和快速切页,防重复提交只用于会修改数据的请求。
        if(HttpMethod.GET == method || HttpMethod.HEAD == method || HttpMethod.OPTIONS == method
                || !antiShakeProperties.getEnable()){
            return;
        }
        HttpHeaders headers = request.getHeaders();
@@ -199,4 +201,4 @@
    public int getOrder() {
        return -300;
    }
}
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
@@ -142,9 +142,25 @@
    @ApiOperation(value = "通过充电枪id和预付金额获取电站详情", tags = {"小程序-扫一扫"})
    @PostMapping(value = "/scan/siteInfo")
    public AjaxResult<SiteInfoVO> siteInfo(Integer id, BigDecimal money) {
        TChargingGun chargingGun = chargingGunClient.getChargingGunById(id).getData();
        TChargingPile chargingPile = chargingPileClient.getChargingPileById(chargingGun.getChargingPileId()).getData();
        SiteInfoVO data = chargingPileClient.getSiteInfoByNumber1(chargingPile.getCode().toString()).getData();
        if (id == null || money == null) {
            return AjaxResult.error("充电枪和预付金额不能为空");
        }
        R<TChargingGun> chargingGunResult = chargingGunClient.getChargingGunById(id);
        TChargingGun chargingGun = chargingGunResult == null ? null : chargingGunResult.getData();
        if (chargingGun == null) {
            return AjaxResult.error("充电枪不存在或已停用");
        }
        R<TChargingPile> chargingPileResult = chargingPileClient.getChargingPileById(chargingGun.getChargingPileId());
        TChargingPile chargingPile = chargingPileResult == null ? null : chargingPileResult.getData();
        if (chargingPile == null || chargingPile.getCode() == null) {
            return AjaxResult.error("充电桩不存在或已停用");
        }
        R<SiteInfoVO> siteInfoResult = chargingPileClient.getSiteInfoByNumber1(chargingPile.getCode().toString());
        SiteInfoVO data = siteInfoResult == null ? null : siteInfoResult.getData();
        if (data == null) {
            String message = siteInfoResult == null ? null : siteInfoResult.getMsg();
            return AjaxResult.error(StringUtils.hasLength(message) ? message : "获取电站计费信息失败");
        }
        data.setChargingGunId(id);
        data.setSiteId(chargingGun.getSiteId());
        data.setChargeMode(chargingGun.getChargeMode());
@@ -171,6 +187,12 @@
        BigDecimal electrovalence = data.getElectrovalence();
        BigDecimal vipElectrovalence = data.getVipElectrovalence();
        if (electrovalence == null || electrovalence.compareTo(BigDecimal.ZERO) <= 0) {
            return AjaxResult.error("当前时段未配置有效费率");
        }
        if (vipElectrovalence == null) {
            vipElectrovalence = electrovalence;
        }
        // 计算预付金额能充多少度普通电
        BigDecimal divide1 = money.divide(electrovalence, 2, BigDecimal.ROUND_HALF_UP);
        // 计算冲会员充电需要多少钱
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TAppCouponControllerTest.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.account.controller;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.chargingPile.api.vo.SiteInfoVO;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.domain.AjaxResult;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class TAppCouponControllerTest {
    @Test
    void siteInfoReturnsChargingPileErrorInsteadOfNullPointerException() {
        TAppCouponController controller = new TAppCouponController();
        ChargingGunClient chargingGunClient = mock(ChargingGunClient.class);
        ChargingPileClient chargingPileClient = mock(ChargingPileClient.class);
        TChargingGun gun = new TChargingGun();
        gun.setChargingPileId(88);
        TChargingPile pile = new TChargingPile();
        pile.setCode("100200300");
        when(chargingGunClient.getChargingGunById(1474)).thenReturn(R.ok(gun));
        when(chargingPileClient.getChargingPileById(88)).thenReturn(R.ok(pile));
        when(chargingPileClient.getSiteInfoByNumber1("100200300"))
                .thenReturn(R.<SiteInfoVO>fail("电站未配置有效的计费模板"));
        ReflectionTestUtils.setField(controller, "chargingGunClient", chargingGunClient);
        ReflectionTestUtils.setField(controller, "chargingPileClient", chargingPileClient);
        AjaxResult<SiteInfoVO> result = controller.siteInfo(1474, new BigDecimal("50"));
        assertTrue(result.isError());
        assertEquals("电站未配置有效的计费模板", result.get("msg"));
    }
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java
@@ -23,6 +23,7 @@
import com.ruoyi.chargingPile.api.domain.SiteMenu;
import com.ruoyi.chargingPile.dto.ChargingPercentBack;
import com.ruoyi.chargingPile.dto.BatchSetSiteAccountingStrategyDTO;
import com.ruoyi.chargingPile.dto.ChargingPileFilterOptionsDTO;
import com.ruoyi.chargingPile.dto.SiteExcelDTO;
import com.ruoyi.chargingPile.dto.SiteImportResultDTO;
import com.ruoyi.common.core.dto.ChargingOrderGroup;
@@ -159,7 +160,10 @@
            accountingStrategyId = site.getAccountingStrategyId();
        }
        TChargingPile one = chargingPileService.getById(chargingGun.getChargingPileId());
        TAccountingStrategy byId1 = accountingStrategyService.getById(accountingStrategyId);
        TAccountingStrategy byId1 = resolveAccountingStrategy(accountingStrategyId, site);
        if (Objects.isNull(byId1)) {
            return R.fail("电站未配置有效的计费模板");
        }
        List<TAccountingStrategyDetail> list = accountingStrategyDetailService.lambdaQuery().eq(TAccountingStrategyDetail::getAccountingStrategyId, byId1.getId()).list();
        list.stream().filter(item -> "00:00".equals(item.getEndTime())).forEach(item -> item.setEndTime("23:59:59"));
        for (TAccountingStrategyDetail tAccountingStrategyDetail : list) {
@@ -228,7 +232,10 @@
        if(null == accountingStrategyId){
            accountingStrategyId = site.getAccountingStrategyId();
        }
        TAccountingStrategy byId1 = accountingStrategyService.getById(accountingStrategyId);
        TAccountingStrategy byId1 = resolveAccountingStrategy(accountingStrategyId, site);
        if (Objects.isNull(byId1)) {
            return R.fail("电站未配置有效的计费模板");
        }
        List<TAccountingStrategyDetail> list = accountingStrategyDetailService.lambdaQuery().eq(TAccountingStrategyDetail::getAccountingStrategyId, byId1.getId()).list();
        list.stream().filter(item -> "00:00".equals(item.getEndTime())).forEach(item -> item.setEndTime("23:59:59"));
        for (TAccountingStrategyDetail tAccountingStrategyDetail : list) {
@@ -291,6 +298,16 @@
        siteInfoVO.setReservationAheadMinute(site.getReservationAheadMinute());
        siteInfoVO.setVinFlag(one.getVinFlag());
        return R.ok(siteInfoVO);
    }
    TAccountingStrategy resolveAccountingStrategy(Integer accountingStrategyId, Site site) {
        TAccountingStrategy strategy = accountingStrategyId == null
                ? null : accountingStrategyService.getById(accountingStrategyId);
        if (strategy == null && site != null && site.getAccountingStrategyId() != null
                && !Objects.equals(accountingStrategyId, site.getAccountingStrategyId())) {
            strategy = accountingStrategyService.getById(site.getAccountingStrategyId());
        }
        return strategy;
    }
    
    @RequiresPermissions(value = {"/systemUserManagement/update", "/site", "/platformRevenueAnalysis", "/chargingOperationAnalysis",
@@ -504,6 +521,13 @@
    public AjaxResult<List<Site>> getSiteListGun(){
        return AjaxResult.success(siteService.getSiteListGun());
    }
    @RequiresPermissions(value = {"/chargingPile", "/chargingPile/select"}, logical = Logical.OR)
    @GetMapping("/chargingPileFilterOptions")
    @ApiOperation(value = "获取充电桩筛选级联选项", tags = {"管理后台-充电桩管理"})
    public AjaxResult<ChargingPileFilterOptionsDTO> getChargingPileFilterOptions() {
        return AjaxResult.success(ChargingPileFilterOptionsDTO.fromSites(siteService.getSiteOptions()));
    }
    
    
    @RequiresPermissions(value = {"/parkingLot/add", "/parkingLot/select", "/parkingLot/update"}, logical = Logical.OR)
@@ -544,11 +568,15 @@
//        Map<String, Double> distance = GeodesyUtil.getDistance(query.getLat() + "," + query.getLon(), siteDetailVO.getLat() + "," + siteDetailVO.getLon());
//        siteDetailVO.setDistance(distance.get("WGS84"));
        // 查询绑定车牌提示文案
        TParkingLot parkingLot = parkingLotService.getOne(Wrappers.lambdaQuery(TParkingLot.class)
                .eq(TParkingLot::getSiteId, query.getSiteId()));
        if(Objects.nonNull(parkingLot)){
            siteDetailVO.setRemark(parkingLot.getRemark());
        }
        // 一个站点允许绑定多个停车场,不能使用 getOne,否则多条数据会直接抛异常。
        // 详情页只展示一条绑定车牌提示,取最新配置的非空提示。
        parkingLotService.list(Wrappers.lambdaQuery(TParkingLot.class)
                    .eq(TParkingLot::getSiteId, query.getSiteId()))
                .stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotEmpty(item.getRemark()))
                .max(Comparator.comparingInt(item -> Objects.isNull(item.getId()) ? 0 : item.getId()))
                .ifPresent(item -> siteDetailVO.setRemark(item.getRemark()));
        return AjaxResult.success(siteDetailVO);
    }
    
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
@@ -15,6 +15,7 @@
import com.ruoyi.chargingPile.service.ISiteService;
import com.ruoyi.chargingPile.service.TAccountingStrategyDetailService;
import com.ruoyi.chargingPile.service.TAccountingStrategyService;
import com.ruoyi.chargingPile.service.TChargingGunService;
import com.ruoyi.chargingPile.service.TChargingPileService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.dto.SteategyPassDto;
@@ -69,6 +70,8 @@
    private final TAccountingStrategyDetailService accountingStrategyDetailService;
    @Autowired
    private TChargingPileService chargingPileService;
    @Autowired
    private TChargingGunService chargingGunService;
    @Autowired
    private ISiteService siteService;
    @Resource
@@ -493,6 +496,9 @@
                                tcecClient.notificationStationChange(siteIds);
                            }
                        }
                        chargingGunService.update(Wrappers.<TChargingGun>lambdaUpdate()
                                .set(TChargingGun::getAccountingStrategyId, byId.getId())
                                .eq(TChargingGun::getAccountingStrategyId, one.getId()));
                        accountingStrategyService.removeById(one.getId());
                    }
                    // 审核通过后当前记录已成为正式策略,不能继续保留旧策略的 parentId。
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/ChargingPileFilterOptionsDTO.java
New file
@@ -0,0 +1,69 @@
package com.ruoyi.chargingPile.dto;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChargingPileFilterOptionsDTO {
    private List<SiteOption> sites;
    private List<PartnerOption> partners;
    public static ChargingPileFilterOptionsDTO fromSites(List<GetSiteListDTO> sites) {
        List<GetSiteListDTO> safeSites = sites == null ? Collections.emptyList() : sites;
        List<SiteOption> siteOptions = new ArrayList<>(safeSites.size());
        Map<String, PartnerOption> partnerOptions = new LinkedHashMap<>();
        for (GetSiteListDTO site : safeSites) {
            if (site == null) {
                continue;
            }
            siteOptions.add(new SiteOption(
                    site.getId(),
                    site.getName(),
                    site.getPartnerId(),
                    site.getPartnerType(),
                    site.getVenuePartnerId()
            ));
            addPartner(partnerOptions, site.getPartnerId(), site.getPartnerName(), site.getPartnerType());
            addPartner(partnerOptions, site.getVenuePartnerId(), site.getVenuePartnerName(), 2);
        }
        return new ChargingPileFilterOptionsDTO(siteOptions, new ArrayList<>(partnerOptions.values()));
    }
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SiteOption {
        private Integer id;
        private String name;
        private Integer partnerId;
        private Integer partnerType;
        private Integer venuePartnerId;
    }
    private static void addPartner(Map<String, PartnerOption> options, Integer id, String name, Integer type) {
        if (id == null || name == null || name.trim().isEmpty() || type == null) {
            return;
        }
        options.putIfAbsent(type + ":" + id, new PartnerOption(id, name, type));
    }
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class PartnerOption {
        private Integer id;
        private String name;
        private Integer partnerType;
    }
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/PartnerMapper.java
@@ -24,12 +24,7 @@
    List<PartnerListDTO> getPartnerList(PageInfo<PartnerListDTO> pageInfo, @Param("item") GetPartnerList partnerList);
    /**
     * Query built-in role by partner type role name.
     * Query the enabled built-in role assigned to a partner type.
     */
    Integer getDefaultPartnerRoleId(@Param("roleName") String roleName);
    /**
     * Validate that a non-administrator role can be assigned to a partner account.
     */
    Integer getAssignablePartnerRoleId(@Param("roleId") Integer roleId);
    Integer getPartnerRoleIdByRoleKey(@Param("roleKey") String roleKey);
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java
@@ -32,6 +32,7 @@
     */
    PageInfo<GetSiteListDTO> getSiteList(GetSiteList siteList);
    PageInfo<GetSiteListDTO> getSiteList1(Long userId);
    List<GetSiteListDTO> getSiteOptions();
    List<Site> listSitesForExport(GetSiteList siteList);
    SiteImportResultDTO importSites(MultipartFile file, Boolean cover) throws Exception;
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -201,9 +201,6 @@
            return ajaxResult;
        }
        Partner oldPartner = this.getById(partner.getId());
        if (partner.getRoleId() == null && oldPartner != null) {
            partner.setRoleId(oldPartner.getRoleId());
        }
        assignPartnerRole(partner);
        Partner one = this.getOne(new LambdaQueryWrapper<Partner>()
                .eq(Partner::getCode, partner.getCode())
@@ -276,40 +273,29 @@
    
    
    void assignPartnerRole(Partner partner) {
        if (partner.getRoleId() == null) {
            partner.setRoleId(resolveDefaultRoleId(partner.getPartnerType()));
            return;
        }
        Integer roleId = this.baseMapper.getAssignablePartnerRoleId(partner.getRoleId());
        if (roleId == null) {
            throw new RuntimeException("所选合作商角色不存在或不可用");
        }
        partner.setRoleId(roleId);
        partner.setRoleId(resolveDefaultRoleId(partner.getPartnerType()));
    }
    Integer resolveDefaultRoleId(Integer partnerType) {
        List<String> roleNames = partnerTypeRoleNames(partnerType);
        Integer roleId = null;
        for (String roleName : roleNames) {
            roleId = this.baseMapper.getDefaultPartnerRoleId(roleName);
            if (roleId != null) {
                break;
            }
        }
        String roleKey = partnerTypeRoleKey(partnerType);
        Integer roleId = this.baseMapper.getPartnerRoleIdByRoleKey(roleKey);
        if (roleId == null) {
            throw new RuntimeException("系统内置角色【" + roleNames.get(0) + "】不存在,请先配置角色");
            throw new RuntimeException("系统内置角色【" + roleKey + "】不存在或已停用,请先配置角色");
        }
        return roleId;
    }
    private List<String> partnerTypeRoleNames(Integer partnerType) {
    private String partnerTypeRoleKey(Integer partnerType) {
        if (Integer.valueOf(1).equals(partnerType)) {
            return "independent_partner";
        }
        if (Integer.valueOf(2).equals(partnerType)) {
            return Arrays.asList("场地方", "合作商");
            return "site_owner";
        }
        if (Integer.valueOf(3).equals(partnerType)) {
            return Arrays.asList("服务商", "合作商");
            return "service_provider";
        }
        return Arrays.asList("独立合作商", "合作商");
        throw new RuntimeException("合作商类型无效");
    }
    /**
     * 删除合作商
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java
@@ -389,6 +389,11 @@
        return pageInfo.setRecords(this.baseMapper.getSiteOptions(ids, partnerId));
    }
    @Override
    public List<GetSiteListDTO> getSiteOptions() {
        return getSiteList1(tokenService.getLoginUser().getUserid()).getRecords();
    }
    private Set<Integer> getAssignedSiteIds(Long userId) {
        Set<Integer> siteIds = new HashSet<>();
        if (userId == null) {
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
@@ -60,23 +60,13 @@
        order by p.create_time desc
    </select>
    <select id="getDefaultPartnerRoleId" resultType="java.lang.Integer">
    <select id="getPartnerRoleIdByRoleKey" resultType="java.lang.Integer">
        select role_id
        from charging_pile_other.sys_role
        where del_flag = '0'
          and status = '0'
          and role_name = #{roleName}
          and role_key = #{roleKey}
        order by role_id asc
        limit 1
    </select>
    <select id="getAssignablePartnerRoleId" resultType="java.lang.Integer">
        select role_id
        from charging_pile_other.sys_role
        where role_id = #{roleId}
          and role_id != 1
          and del_flag = '0'
          and status = '0'
        limit 1
    </select>
</mapper>
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java
@@ -2,8 +2,15 @@
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TParkingLot;
import com.ruoyi.chargingPile.api.query.SiteDetailQuery;
import com.ruoyi.chargingPile.api.vo.SiteDetailVO;
import com.ruoyi.chargingPile.dto.BatchSetSiteAccountingStrategyDTO;
import com.ruoyi.chargingPile.service.IPartnerService;
import com.ruoyi.chargingPile.service.ISiteService;
import com.ruoyi.chargingPile.service.TParkingLotService;
import com.ruoyi.chargingPile.service.TAccountingStrategyService;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.domain.AjaxResult;
@@ -94,6 +101,60 @@
    }
    @Test
    void getDetailByIdUsesLatestNonEmptyParkingLotRemarkWhenSiteHasMultipleLots() {
        SiteController controller = new SiteController();
        ISiteService siteService = mock(ISiteService.class);
        IPartnerService partnerService = mock(IPartnerService.class);
        TParkingLotService parkingLotService = mock(TParkingLotService.class);
        SiteDetailVO detail = new SiteDetailVO();
        detail.setPartnerId(23);
        when(siteService.getDetailById(any(SiteDetailQuery.class))).thenReturn(detail);
        TParkingLot older = new TParkingLot();
        older.setId(10);
        older.setRemark("旧提示");
        TParkingLot latest = new TParkingLot();
        latest.setId(12);
        latest.setRemark("最新提示");
        TParkingLot empty = new TParkingLot();
        empty.setId(13);
        empty.setRemark("");
        when(parkingLotService.list(any(Wrapper.class))).thenReturn(Arrays.asList(older, latest, empty));
        ReflectionTestUtils.setField(controller, "siteService", siteService);
        ReflectionTestUtils.setField(controller, "partnerService", partnerService);
        ReflectionTestUtils.setField(controller, "parkingLotService", parkingLotService);
        SiteDetailQuery query = new SiteDetailQuery();
        query.setSiteId(19);
        query.setLat("30.64242");
        query.setLon("104.04311");
        AjaxResult<SiteDetailVO> result = controller.getDetailById(query);
        assertEquals("最新提示", ((SiteDetailVO) result.get("data")).getRemark());
        verify(parkingLotService).list(any(Wrapper.class));
    }
    @Test
    void resolveAccountingStrategyFallsBackToSiteStrategyWhenGunStrategyWasDeleted() {
        SiteController controller = new SiteController();
        TAccountingStrategyService strategyService = mock(TAccountingStrategyService.class);
        Site site = new Site();
        site.setAccountingStrategyId(202);
        TAccountingStrategy currentStrategy = new TAccountingStrategy();
        currentStrategy.setId(202);
        when(strategyService.getById(101)).thenReturn(null);
        when(strategyService.getById(202)).thenReturn(currentStrategy);
        ReflectionTestUtils.setField(controller, "accountingStrategyService", strategyService);
        TAccountingStrategy result = controller.resolveAccountingStrategy(101, site);
        assertEquals(202, result.getId());
        verify(strategyService).getById(101);
        verify(strategyService).getById(202);
    }
    @Test
    void saveReservationSettingRejectsOverlappingRangesBeforeDatabaseUpdate() {
        SiteController controller = new SiteController();
        ISiteService siteService = mock(ISiteService.class);
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/ChargingPileFilterOptionsDTOTest.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.chargingPile.dto;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ChargingPileFilterOptionsDTOTest {
    @Test
    void buildsDistinctPartnerOptionsFromVisibleSites() {
        GetSiteListDTO first = new GetSiteListDTO();
        first.setId(1);
        first.setName("一号站");
        first.setPartnerId(10);
        first.setPartnerName("合作商甲");
        first.setPartnerType(1);
        first.setVenuePartnerId(20);
        first.setVenuePartnerName("场地方乙");
        GetSiteListDTO second = new GetSiteListDTO();
        second.setId(2);
        second.setName("二号站");
        second.setPartnerId(10);
        second.setPartnerName("合作商甲");
        second.setPartnerType(1);
        ChargingPileFilterOptionsDTO result = ChargingPileFilterOptionsDTO.fromSites(Arrays.asList(first, second));
        assertEquals(2, result.getSites().size());
        assertEquals(1, result.getSites().get(0).getId());
        assertEquals("一号站", result.getSites().get(0).getName());
        assertEquals(10, result.getSites().get(0).getPartnerId());
        assertEquals(20, result.getSites().get(0).getVenuePartnerId());
        assertEquals(2, result.getPartners().size());
        assertEquals(10, result.getPartners().get(0).getId());
        assertEquals(1, result.getPartners().get(0).getPartnerType());
        assertEquals(20, result.getPartners().get(1).getId());
        assertEquals(2, result.getPartners().get(1).getPartnerType());
    }
}
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java
@@ -27,7 +27,6 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.isNull;
@@ -71,16 +70,14 @@
    }
    @Test
    void venuePartnerFallsBackToPartnerRoleWhenDedicatedRoleIsMissing() {
    void venuePartnerUsesSiteOwnerRoleKey() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        when(mapper.getDefaultPartnerRoleId("场地方")).thenReturn(null);
        when(mapper.getDefaultPartnerRoleId("合作商")).thenReturn(7);
        when(mapper.getPartnerRoleIdByRoleKey("site_owner")).thenReturn(7);
        assertEquals(7, service.resolveDefaultRoleId(2));
        verify(mapper).getDefaultPartnerRoleId("场地方");
        verify(mapper).getDefaultPartnerRoleId("合作商");
        verify(mapper).getPartnerRoleIdByRoleKey("site_owner");
    }
    @Test
@@ -88,55 +85,37 @@
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        when(mapper.getDefaultPartnerRoleId("服务商")).thenReturn(9);
        when(mapper.getPartnerRoleIdByRoleKey("service_provider")).thenReturn(9);
        assertEquals(9, service.resolveDefaultRoleId(3));
        verify(mapper).getDefaultPartnerRoleId("服务商");
        verify(mapper, never()).getDefaultPartnerRoleId("合作商");
        verify(mapper).getPartnerRoleIdByRoleKey("service_provider");
    }
    @Test
    void independentPartnerUsesUnifiedRoleNameBeforeLegacyAlias() {
    void independentPartnerUsesIndependentPartnerRoleKey() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        when(mapper.getDefaultPartnerRoleId("独立合作商")).thenReturn(11);
        when(mapper.getPartnerRoleIdByRoleKey("independent_partner")).thenReturn(11);
        assertEquals(11, service.resolveDefaultRoleId(1));
        verify(mapper).getDefaultPartnerRoleId("独立合作商");
        verify(mapper, never()).getDefaultPartnerRoleId("合作商");
        verify(mapper).getPartnerRoleIdByRoleKey("independent_partner");
    }
    @Test
    void selectedPartnerRoleIsNotOverwrittenByDefaultRole() {
    void incomingPartnerRoleIsOverwrittenByPartnerType() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        Partner partner = new Partner();
        partner.setPartnerType(1);
        partner.setRoleId(25);
        when(mapper.getAssignablePartnerRoleId(25)).thenReturn(25);
        service.assignPartnerRole(partner);
        assertEquals(Integer.valueOf(25), partner.getRoleId());
        verify(mapper).getAssignablePartnerRoleId(25);
        verify(mapper, never()).getDefaultPartnerRoleId(any());
    }
    @Test
    void missingPartnerRoleStillUsesTypeDefault() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        Partner partner = new Partner();
        partner.setPartnerType(1);
        when(mapper.getDefaultPartnerRoleId("独立合作商")).thenReturn(11);
        when(mapper.getPartnerRoleIdByRoleKey("independent_partner")).thenReturn(11);
        service.assignPartnerRole(partner);
        assertEquals(Integer.valueOf(11), partner.getRoleId());
        verify(mapper, never()).getAssignablePartnerRoleId(any());
        verify(mapper).getPartnerRoleIdByRoleKey("independent_partner");
    }
    @Test
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -132,6 +132,7 @@
    List<ChargingOrderListVO> chargingList(@Param("req") ChargingListQuery dto, @Param("startTime1")String startTime1, @Param("startTime2")String startTime2, @Param("endTime1")String endTime1, @Param("endTime2")String endTime2);
    List<Map<String, Object>> chargingList1(@Param("pageInfo")PageInfo<ChargingOrderListVO> pageInfo, @Param("req") ChargingListQuery dto, @Param("startTime1")String startTime1, @Param("startTime2")String startTime2, @Param("endTime1")String endTime1, @Param("endTime2")String endTime2);
    List<Map<String, Object>> chargingListAccountingSummary(@Param("orderIds") List<Long> orderIds);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -143,6 +143,7 @@
    private static final int ENTERPRISE_DETAIL_PERIOD_BATCH_SIZE = 500;
    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;
    @Override
    public ReservationRecommendationVO getReservationRecommendation(Integer siteId) {
@@ -3776,6 +3777,8 @@
        if (includeList) {
            pageInfo = new PageInfo<>(dto.getPageCurr(), dto.getPageSize());
        List<Map<String, Object>> maps = this.baseMapper.chargingList1(pageInfo, dto, startTime1, startTime2, endTime1, endTime2);
        List<Map<String, Object>> accountingSummaries = loadChargingPeriodAccountingSummaries(maps);
        mergeChargingPeriodAccountingRows(maps, accountingSummaries);
        Map<Integer, Site> siteMap = loadChargingPeriodSites(maps);
        Map<Integer, TChargingPile> pileMap = loadChargingPeriodPiles(maps);
        Map<Integer, TChargingGun> gunMap = loadChargingPeriodGuns(maps);
@@ -3987,6 +3990,69 @@
        return chargingOrderTimeVO;
    }
    private List<Map<String, Object>> loadChargingPeriodAccountingSummaries(List<Map<String, Object>> rows) {
        List<Long> orderIds = collectLongIds(rows, "id");
        if (orderIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Map<String, Object>> summaries = new ArrayList<>();
        for (int start = 0; start < orderIds.size(); start += CHARGING_PERIOD_ACCOUNTING_BATCH_SIZE) {
            int end = Math.min(start + CHARGING_PERIOD_ACCOUNTING_BATCH_SIZE, orderIds.size());
            List<Map<String, Object>> batch = this.baseMapper.chargingListAccountingSummary(orderIds.subList(start, end));
            if (batch != null) {
                summaries.addAll(batch);
            }
        }
        return summaries;
    }
    static void mergeChargingPeriodAccountingRows(List<Map<String, Object>> orders,
            List<Map<String, Object>> summaries) {
        if (orders == null || orders.isEmpty()) {
            return;
        }
        Map<Long, Map<String, Object>> summaryByOrderId = new HashMap<>();
        if (summaries != null) {
            for (Map<String, Object> summary : summaries) {
                Long orderId = mapLongIgnoreCase(summary, "orderId");
                if (orderId != null) {
                    summaryByOrderId.put(orderId, summary);
                }
            }
        }
        String[] amountKeys = {"periodElectricPrice", "periodServicePrice", "jian", "feng", "ping", "gu"};
        for (Map<String, Object> order : orders) {
            Map<String, Object> summary = summaryByOrderId.get(mapLongIgnoreCase(order, "id"));
            for (String key : amountKeys) {
                Object value = mapValueIgnoreCase(summary, key);
                order.put(key, value == null ? BigDecimal.ZERO : value);
            }
        }
    }
    private static Long mapLongIgnoreCase(Map<String, Object> row, String key) {
        Object value = mapValueIgnoreCase(row, key);
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        return value == null ? null : Long.valueOf(value.toString());
    }
    private static Object mapValueIgnoreCase(Map<String, Object> row, String key) {
        if (row == null) {
            return null;
        }
        if (row.containsKey(key)) {
            return row.get(key);
        }
        for (Map.Entry<String, Object> entry : row.entrySet()) {
            if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) {
                return entry.getValue();
            }
        }
        return null;
    }
    private Map<Integer, Site> loadChargingPeriodSites(List<Map<String, Object>> rows) {
        List<Integer> ids = collectIntegerIds(rows, "siteId");
        if (ids.isEmpty()) return Collections.emptyMap();
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -1343,26 +1343,8 @@
        b.start_soc as startSoc,
        b.end_soc as endSoc,
        b.`status`,
        b.end_mode as endMode,
        a.periodElectricPrice,
        a.periodServicePrice,
        a.jian,
        a.feng,
        a.ping,
        a.gu
        b.end_mode as endMode
        from t_charging_order b
        inner join (
            select
            charging_order_id,
            sum(period_electric_price) as periodElectricPrice,
            sum(period_service_price) as periodServicePrice,
            sum(CASE WHEN type = 1 THEN charging_capacity ELSE 0 END) as jian,
            sum(CASE WHEN type = 2 THEN charging_capacity ELSE 0 END) as feng,
            sum(CASE WHEN type = 3 THEN charging_capacity ELSE 0 END) as ping,
            sum(CASE WHEN type = 4 THEN charging_capacity ELSE 0 END) as gu
            from t_charging_order_accounting_strategy
            group by charging_order_id
        ) a on a.charging_order_id = b.id
        where b.del_flag = 0 and b.recharge_payment_status = 2
        <if test="null != req.code and req.code!=''">
            and b.code LIKE CONCAT('%',#{req.code},'%')
@@ -1405,6 +1387,23 @@
        </if>
        and b.start_time IS NOT NULL and b.end_time is not null
        order by b.start_time desc
    </select>
    <select id="chargingListAccountingSummary" resultType="map">
        select
        charging_order_id as orderId,
        sum(period_electric_price) as periodElectricPrice,
        sum(period_service_price) as periodServicePrice,
        sum(CASE WHEN type = 1 THEN charging_capacity ELSE 0 END) as jian,
        sum(CASE WHEN type = 2 THEN charging_capacity ELSE 0 END) as feng,
        sum(CASE WHEN type = 3 THEN charging_capacity ELSE 0 END) as ping,
        sum(CASE WHEN type = 4 THEN charging_capacity ELSE 0 END) as gu
        from t_charging_order_accounting_strategy
        where charging_order_id in
        <foreach collection="orderIds" item="orderId" open="(" separator="," close=")">
            #{orderId}
        </foreach>
        group by charging_order_id
    </select>
@@ -2345,9 +2344,17 @@
            b.id,
            CASE WHEN count(a.id) > 0 THEN (UNIX_TIMESTAMP(MAX(b.end_time)) - UNIX_TIMESTAMP(MAX(b.start_time))) ELSE 0 END as time,
            sum(a.charging_capacity) as total,
            sum(a.period_electric_price) as electronicMoney,
            sum(CASE WHEN b.order_source = 1 THEN a.period_service_price ELSE a.period_service_price * 0.8 END) as serviceMoney,
            sum(a.period_electric_price + (CASE WHEN b.order_source = 1 THEN a.period_service_price ELSE a.period_service_price * 0.8 END)) as income,
            sum(CASE WHEN b.refund_status = 2 THEN 0 ELSE a.period_electric_price END) as electronicMoney,
            sum(CASE
                    WHEN b.refund_status = 2 THEN 0
                    WHEN b.order_source = 1 THEN a.period_service_price
                    ELSE a.period_service_price * 0.8
                END) as serviceMoney,
            sum(CASE
                    WHEN b.refund_status = 2 THEN 0
                    ELSE a.period_electric_price
                        + CASE WHEN b.order_source = 1 THEN a.period_service_price ELSE a.period_service_price * 0.8 END
                END) as income,
            MAX(b.recharge_payment_type) as recharge_payment_type,
            MAX(b.is_whitelist) as is_whitelist,
            MAX(b.electrovalence) as electrovalence,
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingOrderActualIncomeMapperTest.java
New file
@@ -0,0 +1,50 @@
package com.ruoyi.order.mapper;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChargingOrderActualIncomeMapperTest {
    @Test
    void refundedOrdersDoNotContributeToIncomeElectricOrServiceFees() throws IOException {
        InputStream input = getClass().getClassLoader()
                .getResourceAsStream("mapper/order/TChargingOrderMapper.xml");
        assertNotNull(input);
        String mapper;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
            mapper = reader.lines().collect(Collectors.joining("\n"));
        }
        String summarySql = mapper.substring(
                mapper.indexOf("<select id=\"chargingOrderBaseSummary\""),
                mapper.indexOf("<select id=\"chargingOrderNolimit\""))
                .toLowerCase(Locale.ROOT)
                .replaceAll("\\s+", " ");
        assertTrue(summarySql.contains("a.period_electric_price"));
        assertTrue(summarySql.contains("a.period_service_price"));
        assertEquals(3, countOccurrences(summarySql, "when b.refund_status = 2 then 0"));
    }
    private static int countOccurrences(String value, String token) {
        int count = 0;
        int index = 0;
        while ((index = value.indexOf(token, index)) >= 0) {
            count++;
            index += token.length();
        }
        return count;
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/ChargingPeriodPaginationMapperTest.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.order.mapper;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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 ChargingPeriodPaginationMapperTest {
    @Test
    void pagesOrdersBeforeLoadingAccountingSummaries() throws IOException {
        InputStream input = getClass().getClassLoader()
                .getResourceAsStream("mapper/order/TChargingOrderMapper.xml");
        assertNotNull(input);
        String mapper;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
            mapper = reader.lines().collect(Collectors.joining("\n"));
        }
        String pageQuery = mapper.substring(
                mapper.indexOf("<select id=\"chargingList1\""),
                mapper.indexOf("<select id=\"chargingListAccountingSummary\""));
        assertTrue(pageQuery.contains("from t_charging_order b"));
        assertFalse(pageQuery.contains("t_charging_order_accounting_strategy"));
        assertFalse(pageQuery.contains("group by charging_order_id"));
        int summaryStart = mapper.indexOf("<select id=\"chargingListAccountingSummary\"");
        assertTrue(summaryStart >= 0);
        String accountingQuery = mapper.substring(summaryStart, mapper.indexOf("<select id=\"chargingListSum\""));
        assertTrue(accountingQuery.contains("from t_charging_order_accounting_strategy"));
        assertTrue(accountingQuery.contains("where charging_order_id in"));
        assertTrue(accountingQuery.contains("group by charging_order_id"));
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPeriodIncomeTest.java
@@ -4,7 +4,9 @@
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,4 +37,31 @@
        assertEquals(BigDecimal.ZERO, item.getPlatformIncome());
        assertEquals(BigDecimal.ZERO, item.getPartnerIncome());
    }
    @Test
    void mergesAccountingSummaryIntoCurrentPageOrders() {
        Map<String, Object> firstOrder = new HashMap<>();
        firstOrder.put("id", 101L);
        Map<String, Object> secondOrder = new HashMap<>();
        secondOrder.put("id", 102L);
        List<Map<String, Object>> orders = Arrays.asList(firstOrder, secondOrder);
        Map<String, Object> summary = new HashMap<>();
        summary.put("orderId", 101L);
        summary.put("periodElectricPrice", new BigDecimal("3.20"));
        summary.put("periodServicePrice", new BigDecimal("1.10"));
        summary.put("jian", new BigDecimal("2.00"));
        summary.put("feng", new BigDecimal("1.00"));
        summary.put("ping", BigDecimal.ZERO);
        summary.put("gu", BigDecimal.ZERO);
        TChargingOrderServiceImpl.mergeChargingPeriodAccountingRows(
                orders, Arrays.asList(summary));
        assertEquals(new BigDecimal("3.20"), firstOrder.get("periodElectricPrice"));
        assertEquals(new BigDecimal("1.10"), firstOrder.get("periodServicePrice"));
        assertEquals(new BigDecimal("2.00"), firstOrder.get("jian"));
        assertEquals(BigDecimal.ZERO, secondOrder.get("periodElectricPrice"));
        assertEquals(BigDecimal.ZERO, secondOrder.get("gu"));
    }
}