| New file |
| | |
| | | # 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.** |
| New file |
| | |
| | | # 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. |
| | |
| | | */ |
| | | 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(); |
| | |
| | | public int getOrder() { |
| | | return -300; |
| | | } |
| | | } |
| | | } |
| | |
| | | @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()); |
| | |
| | | |
| | | 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); |
| | | // 计算冲会员充电需要多少钱 |
| New file |
| | |
| | | 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")); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | 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", |
| | |
| | | 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) |
| | |
| | | // 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); |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | private final TAccountingStrategyDetailService accountingStrategyDetailService; |
| | | @Autowired |
| | | private TChargingPileService chargingPileService; |
| | | @Autowired |
| | | private TChargingGunService chargingGunService; |
| | | @Autowired |
| | | private ISiteService siteService; |
| | | @Resource |
| | |
| | | tcecClient.notificationStationChange(siteIds); |
| | | } |
| | | } |
| | | chargingGunService.update(Wrappers.<TChargingGun>lambdaUpdate() |
| | | .set(TChargingGun::getAccountingStrategyId, byId.getId()) |
| | | .eq(TChargingGun::getAccountingStrategyId, one.getId())); |
| | | accountingStrategyService.removeById(one.getId()); |
| | | } |
| | | // 审核通过后当前记录已成为正式策略,不能继续保留旧策略的 parentId。 |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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); |
| | | } |
| | |
| | | */ |
| | | 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; |
| | | |
| | |
| | | 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()) |
| | |
| | | |
| | | |
| | | 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("合作商类型无效"); |
| | | } |
| | | /** |
| | | * 删除合作商 |
| | |
| | | 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) { |
| | |
| | | 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> |
| | |
| | | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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); |
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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 |
| | |
| | | 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 |
| | |
| | | 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); |
| | | |
| | | |
| | | |
| | |
| | | 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) { |
| | |
| | | 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); |
| | |
| | | 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(); |
| | |
| | | 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},'%') |
| | |
| | | </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> |
| | | |
| | | |
| | |
| | | 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, |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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")); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | 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")); |
| | | } |
| | | } |