From 79f1a19bb3344de47dff029cf5498d787e223782 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 28 七月 2026 19:56:11 +0800
Subject: [PATCH] 各类修复
---
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java | 176 +++-
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java | 40 +
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/dto/EmindIncomeHistoryDto.java | 22
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationOrderSupport.java | 22
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationSyncScheduler.java | 24
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupport.java | 135 +++
sql/20260728_reservation_time_ranges.sql | 17
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TParkingRecordServiceImpl.java | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java | 7
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TChargingPileMapper.xml | 1
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java | 3
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/TParkingRecordMapper.java | 2
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingBillIncomeIndexTest.java | 32
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java | 82 +
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml | 195 +++--
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java | 42 +
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java | 59
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/InvoicePayOrderQueryTest.java | 55 +
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TAccountSettlementBillExport.java | 34
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TAccountSettlementStatement.java | 12
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationOrderSupportTest.java | 35
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java | 5
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java | 3
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationRecommendationCalculator.java | 181 ++++
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/util/EmindCloudApiUtilTest.java | 17
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java | 17
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java | 9
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/ParkingLotClient.java | 2
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java | 2
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/service/EmindStationSyncSchedulerTest.java | 50 +
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupportTest.java | 68 +
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PageChargingPileListDTO.java | 2
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java | 3
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java | 12
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml | 20
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TAccountSettlementStatementMapper.xml | 11
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationRecommendationCalculatorTest.java | 115 +++
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml | 1
sql/20260728_account_settlement_v2.sql | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java | 5
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ReservationRecommendationVO.java | 23
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TParkingRecordService.java | 2
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java | 3
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java | 44
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java | 35
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java | 40
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TParkingLotController.java | 4
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/PlatformRevenueAnalysisTest.java | 36
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/ParkingLotFallbackFactory.java | 2
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java | 35
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/ReservationTimeRange.java | 27
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TParkingRecordMapper.xml | 12
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java | 51
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java | 113 ++
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java | 113 +++
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java | 22
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java | 54 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java | 38
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java | 8
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderQueryDto.java | 2
60 files changed, 1,878 insertions(+), 312 deletions(-)
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PageChargingPileListDTO.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PageChargingPileListDTO.java
index 80d1d66..b07bb30 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PageChargingPileListDTO.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PageChargingPileListDTO.java
@@ -43,6 +43,8 @@
private String reservationStartTime;
@ApiModelProperty(value = "允许预约结束时间,格式HH:mm")
private String reservationEndTime;
+ @ApiModelProperty(value = "允许预约多时段JSON数组")
+ private String reservationTimeRanges;
@ApiModelProperty(value = "最早可提前预约分钟数")
private Integer reservationAheadMinute;
@ApiModelProperty(value = "查看权限")
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/ParkingLotFallbackFactory.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/ParkingLotFallbackFactory.java
index 255b1dc..62aa9ae 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/ParkingLotFallbackFactory.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/ParkingLotFallbackFactory.java
@@ -47,7 +47,7 @@
}
@Override
- public R<BigDecimal> getRecordAmount() {
+ public R<BigDecimal> getRecordAmount(List<Integer> siteIds) {
return R.fail("获取数据失败:" + throwable.getMessage());
}
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/ParkingLotClient.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/ParkingLotClient.java
index 918138e..fc4ff28 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/ParkingLotClient.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/ParkingLotClient.java
@@ -41,7 +41,7 @@
R<TParkingRecord> getRecordById(@RequestParam("id") Long siteId);
@PostMapping(value = "/t-parking-lot/getRecordAmount")
- R<BigDecimal> getRecordAmount();
+ R<BigDecimal> getRecordAmount(@RequestParam(value = "siteIds", required = false) List<Integer> siteIds);
/**
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/ReservationTimeRange.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/ReservationTimeRange.java
new file mode 100644
index 0000000..8b841f7
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/ReservationTimeRange.java
@@ -0,0 +1,27 @@
+package com.ruoyi.chargingPile.api.model;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@ApiModel(value = "ReservationTimeRange", description = "预约充电允许时段")
+public class ReservationTimeRange implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty(value = "开始时间,格式HH:mm")
+ private String startTime;
+
+ @ApiModelProperty(value = "结束时间,格式HH:mm")
+ private String endTime;
+
+ public ReservationTimeRange(String startTime, String endTime) {
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+}
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java
index af49cf8..ec5b1dc 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java
@@ -471,6 +471,9 @@
@TableField("reservation_end_time")
@ApiModelProperty(value = "允许预约结束时间,格式HH:mm")
private String reservationEndTime;
+ @TableField("reservation_time_ranges")
+ @ApiModelProperty(value = "允许预约多时段JSON数组")
+ private String reservationTimeRanges;
@TableField("reservation_ahead_minute")
@ApiModelProperty(value = "最早可提前预约分钟数")
private Integer reservationAheadMinute;
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupport.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupport.java
new file mode 100644
index 0000000..46a1042
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupport.java
@@ -0,0 +1,135 @@
+package com.ruoyi.chargingPile.api.support;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.chargingPile.api.model.ReservationTimeRange;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** 预约充电多时段 JSON 的统一解析及校验规则。 */
+public final class ReservationTimeRangeSupport {
+
+ private static final DateTimeFormatter CLOCK_FORMATTER = DateTimeFormatter
+ .ofPattern("HH:mm")
+ .withResolverStyle(ResolverStyle.STRICT);
+ private static final int MINUTES_PER_DAY = 24 * 60;
+
+ private ReservationTimeRangeSupport() {
+ }
+
+ /**
+ * 优先解析多时段 JSON;JSON 为空时兼容旧的单段开始、结束字段。
+ */
+ public static List<ReservationTimeRange> parse(String json, String legacyStart, String legacyEnd) {
+ if (hasText(json)) {
+ return validate(parseJson(json), false);
+ }
+ if (hasText(legacyStart) || hasText(legacyEnd)) {
+ if (!hasText(legacyStart) || !hasText(legacyEnd)) {
+ throw new IllegalArgumentException("预约时段配置不完整");
+ }
+ return validate(Collections.singletonList(new ReservationTimeRange(legacyStart, legacyEnd)), false);
+ }
+ return Collections.emptyList();
+ }
+
+ /** 保存前校验并生成格式统一的 JSON。 */
+ public static String normalizeJson(String json) {
+ List<ReservationTimeRange> ranges = validate(parseJson(json), true);
+ return JSON.toJSONString(ranges);
+ }
+
+ public static boolean contains(List<ReservationTimeRange> ranges, LocalTime value) {
+ if (ranges == null || ranges.isEmpty() || value == null) {
+ return false;
+ }
+ for (ReservationTimeRange range : ranges) {
+ LocalTime start = parseClock(range.getStartTime());
+ LocalTime end = parseClock(range.getEndTime());
+ if (start.isBefore(end)) {
+ if (!value.isBefore(start) && !value.isAfter(end)) {
+ return true;
+ }
+ } else if (!value.isBefore(start) || !value.isAfter(end)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static String describe(List<ReservationTimeRange> ranges) {
+ if (ranges == null || ranges.isEmpty()) {
+ return "";
+ }
+ return ranges.stream()
+ .map(item -> item.getStartTime() + "-" + item.getEndTime())
+ .collect(Collectors.joining("、"));
+ }
+
+ private static List<ReservationTimeRange> parseJson(String json) {
+ try {
+ List<ReservationTimeRange> ranges = JSON.parseArray(json, ReservationTimeRange.class);
+ return ranges == null ? Collections.emptyList() : ranges;
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("预约时段JSON格式不正确", e);
+ }
+ }
+
+ private static List<ReservationTimeRange> validate(List<ReservationTimeRange> source, boolean required) {
+ if (source == null || source.isEmpty()) {
+ if (required) {
+ throw new IllegalArgumentException("请至少配置一个预约时段");
+ }
+ return Collections.emptyList();
+ }
+ boolean[] occupiedMinutes = new boolean[MINUTES_PER_DAY];
+ List<ReservationTimeRange> normalized = new ArrayList<>(source.size());
+ for (ReservationTimeRange item : source) {
+ if (item == null) {
+ throw new IllegalArgumentException("预约时段不能为空");
+ }
+ LocalTime start = parseClock(item.getStartTime());
+ LocalTime end = parseClock(item.getEndTime());
+ if (start.equals(end)) {
+ throw new IllegalArgumentException("预约时段开始时间和结束时间不能相同");
+ }
+ markOccupied(occupiedMinutes, start, end);
+ normalized.add(new ReservationTimeRange(start.toString(), end.toString()));
+ }
+ return normalized;
+ }
+
+ private static void markOccupied(boolean[] occupiedMinutes, LocalTime start, LocalTime end) {
+ int startMinute = start.getHour() * 60 + start.getMinute();
+ int endMinute = end.getHour() * 60 + end.getMinute();
+ int length = (endMinute - startMinute + MINUTES_PER_DAY) % MINUTES_PER_DAY;
+ for (int offset = 0; offset < length; offset++) {
+ int minute = (startMinute + offset) % MINUTES_PER_DAY;
+ if (occupiedMinutes[minute]) {
+ throw new IllegalArgumentException("预约时段不能重叠");
+ }
+ occupiedMinutes[minute] = true;
+ }
+ }
+
+ private static LocalTime parseClock(String value) {
+ if (!hasText(value) || !value.matches("(?:[01]\\d|2[0-3]):[0-5]\\d")) {
+ throw new IllegalArgumentException("预约时间格式必须为HH:mm");
+ }
+ try {
+ return LocalTime.parse(value, CLOCK_FORMATTER);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("预约时间格式必须为HH:mm", e);
+ }
+ }
+
+ private static boolean hasText(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+}
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
index 0c250d6..3b4a479 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
@@ -43,6 +43,8 @@
private String reservationStartTime;
@ApiModelProperty(value = "允许预约结束时间,格式HH:mm")
private String reservationEndTime;
+ @ApiModelProperty(value = "允许预约多时段JSON数组")
+ private String reservationTimeRanges;
@ApiModelProperty(value = "最早可提前预约分钟数")
private Integer reservationAheadMinute;
@ApiModelProperty(value = "是否支持VIN码识别")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TAccountSettlementStatement.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TAccountSettlementStatement.java
index 412b1ae..1aa4e8f 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TAccountSettlementStatement.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TAccountSettlementStatement.java
@@ -43,7 +43,7 @@
@TableField("code")
private String code;
/**
- * 账户类型(1=微信,2=支付宝)
+ * 账户类型(3=工商银行商户)
*/
@TableField("account_type")
private Integer accountType;
@@ -73,6 +73,16 @@
@TableField("recorded_amount")
private BigDecimal recordedAmount;
/**
+ * 平台收入
+ */
+ @TableField("platform_income")
+ private BigDecimal platformIncome;
+ /**
+ * 合作商收入
+ */
+ @TableField("partner_income")
+ private BigDecimal partnerIncome;
+ /**
* 账单生成日期
*/
@TableField("create_time")
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
index cf13183..1bcdfc2 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
@@ -95,6 +95,9 @@
public R<AppUserWhitelistImportResultVO> importData(@RequestParam("file") MultipartFile file) throws Exception {
AppUserWhitelistImportResultVO result = whitelistService.importWhitelist(file);
if (result.getErrorCount() != null && result.getErrorCount() > 0) {
+ if (result.getSuccessCount() != null && result.getSuccessCount() > 0) {
+ return R.ok(result, "导入完成,部分数据导入失败");
+ }
return R.fail(result, "导入失败,请检查错误数据");
}
return R.ok(result, "导入成功");
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
index 8fab620..db281a9 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -37,7 +37,10 @@
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@@ -84,6 +87,8 @@
private TokenService tokenService;
@Resource
private SysUserClient sysUserClient;
+ @Resource
+ private PlatformTransactionManager transactionManager;
@Override
public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
@@ -155,7 +160,6 @@
}
@Override
- @Transactional(rollbackFor = Exception.class)
public AppUserWhitelistImportResultVO importWhitelist(MultipartFile file) throws Exception {
if (file == null || file.isEmpty()) {
throw new ServiceException("导入文件不能为空");
@@ -169,6 +173,7 @@
Map<String, Site> siteCodeMap = getSiteCodeMap();
Map<String, AppUserWhitelistSaveDTO> validRows = new LinkedHashMap<>();
+ Map<String, Integer> validRowNumbers = new HashMap<>();
List<AppUserWhitelistImportErrorVO> errors = new ArrayList<>();
Set<String> importPhones = new HashSet<>();
for (int i = 0; i < rows.size(); i++) {
@@ -189,23 +194,44 @@
continue;
}
validRows.put(dto.getPhone(), dto);
+ validRowNumbers.put(dto.getPhone(), rowNum);
+ }
+
+ TransactionTemplate rowTransaction = new TransactionTemplate(transactionManager);
+ rowTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+ int successCount = 0;
+ for (Map.Entry<String, AppUserWhitelistSaveDTO> entry : validRows.entrySet()) {
+ try {
+ rowTransaction.execute(status -> {
+ createWhitelist(entry.getValue());
+ return null;
+ });
+ successCount++;
+ } catch (Exception exception) {
+ errors.add(new AppUserWhitelistImportErrorVO(validRowNumbers.get(entry.getKey()), entry.getKey(),
+ getImportSaveErrorMessage(exception)));
+ }
}
AppUserWhitelistImportResultVO result = new AppUserWhitelistImportResultVO();
result.setTotalCount(rows.size());
result.setErrorCount(errors.size());
- result.setSuccessCount(errors.isEmpty() ? validRows.size() : 0);
+ result.setSuccessCount(successCount);
result.setErrorMessage(errors);
- if (!errors.isEmpty()) {
- return result;
- }
-
- for (AppUserWhitelistSaveDTO dto : validRows.values()) {
- createWhitelist(dto);
- }
return result;
}
+ private String getImportSaveErrorMessage(Exception exception) {
+ Throwable current = exception;
+ while (current != null) {
+ if (current instanceof ServiceException && StringUtils.isNotBlank(current.getMessage())) {
+ return current.getMessage();
+ }
+ current = current.getCause();
+ }
+ return "数据保存失败";
+ }
+
/**
* 判断指定用户在指定站点是否命中启用状态的白名单规则
*
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
new file mode 100644
index 0000000..f7d525a
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
@@ -0,0 +1,113 @@
+package com.ruoyi.account.service.impl;
+
+import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
+import com.ruoyi.account.api.model.TAppUser;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+import com.ruoyi.account.api.vo.AppUserWhitelistImportResultVO;
+import com.ruoyi.account.mapper.TAppUserWhitelistMapper;
+import com.ruoyi.account.service.TAppUserService;
+import com.ruoyi.account.service.TAppUserWhitelistSiteService;
+import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
+import com.ruoyi.chargingPile.api.feignClient.SiteClient;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.system.api.feignClient.SysUserClient;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionStatus;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TAppUserWhitelistServiceImplTest {
+ @Mock private TAppUserWhitelistMapper mapper;
+ @Mock private TAppUserWhitelistSiteService whitelistSiteService;
+ @Mock private TAppUserService appUserService;
+ @Mock private SiteClient siteClient;
+ @Mock private PartnerClient partnerClient;
+ @Mock private TokenService tokenService;
+ @Mock private SysUserClient sysUserClient;
+ @Mock private PlatformTransactionManager transactionManager;
+
+ private TAppUserWhitelistServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ service = new TAppUserWhitelistServiceImpl();
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ ReflectionTestUtils.setField(service, "whitelistSiteService", whitelistSiteService);
+ ReflectionTestUtils.setField(service, "appUserService", appUserService);
+ ReflectionTestUtils.setField(service, "siteClient", siteClient);
+ ReflectionTestUtils.setField(service, "partnerClient", partnerClient);
+ ReflectionTestUtils.setField(service, "tokenService", tokenService);
+ ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
+ ReflectionTestUtils.setField(service, "transactionManager", transactionManager);
+ }
+
+ @Test
+ void importsValidRowsAndReturnsOnlyInvalidRows() throws Exception {
+ when(siteClient.getSiteAll()).thenReturn(R.ok(Collections.emptyList()));
+ when(mapper.selectValidByPhone(any())).thenReturn(null);
+ TransactionStatus transactionStatus = mock(TransactionStatus.class);
+ when(transactionManager.getTransaction(any())).thenReturn(transactionStatus);
+ LambdaQueryChainWrapper<TAppUser> appUserQuery = mock(LambdaQueryChainWrapper.class);
+ when(appUserService.lambdaQuery()).thenReturn(appUserQuery);
+ when(appUserQuery.eq(any(), any())).thenReturn(appUserQuery);
+ when(appUserQuery.last(any())).thenReturn(appUserQuery);
+ when(appUserQuery.one()).thenReturn(null);
+ AtomicLong ids = new AtomicLong(1L);
+ doAnswer(invocation -> {
+ TAppUserWhitelist whitelist = invocation.getArgument(0);
+ whitelist.setId(ids.getAndIncrement());
+ return 1;
+ }).when(mapper).insert(any(TAppUserWhitelist.class));
+
+ AppUserWhitelistImportResultVO result = service.importWhitelist(importFile());
+
+ assertEquals(3, result.getTotalCount());
+ assertEquals(2, result.getSuccessCount());
+ assertEquals(1, result.getErrorCount());
+ assertEquals("13540211237", result.getErrorMessage().get(0).getPhone());
+ verify(mapper, org.mockito.Mockito.times(2)).insert(any(TAppUserWhitelist.class));
+ }
+
+ private MockMultipartFile importFile() throws Exception {
+ try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("白名单用户");
+ org.apache.poi.ss.usermodel.Row header = sheet.createRow(0);
+ header.createCell(0).setCellValue("用户手机号");
+ header.createCell(1).setCellValue("享受权益");
+ header.createCell(2).setCellValue("白名单电站");
+ addRow(sheet, 1, "13540211236", "1", "all");
+ addRow(sheet, 2, "13540211238", "2", "all");
+ addRow(sheet, 3, "13540211237", "", "all");
+ workbook.write(output);
+ return new MockMultipartFile("file", "whitelist.xlsx",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", output.toByteArray());
+ }
+ }
+
+ private void addRow(org.apache.poi.ss.usermodel.Sheet sheet, int index,
+ String phone, String benefitType, String whitelistSite) {
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(index);
+ row.createCell(0).setCellValue(phone);
+ row.createCell(1).setCellValue(benefitType);
+ row.createCell(2).setCellValue(whitelistSite);
+ }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java
index 673f328..53b2834 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java
@@ -18,6 +18,7 @@
import com.ruoyi.chargingPile.api.query.GetSiteList;
import com.ruoyi.chargingPile.api.query.SiteDetailQuery;
import com.ruoyi.chargingPile.api.query.SiteQuery;
+import com.ruoyi.chargingPile.api.support.ReservationTimeRangeSupport;
import com.ruoyi.chargingPile.api.vo.*;
import com.ruoyi.chargingPile.api.domain.SiteMenu;
import com.ruoyi.chargingPile.dto.ChargingPercentBack;
@@ -206,6 +207,7 @@
siteInfoVO.setSupportOrder(site.getSupportOrder());
siteInfoVO.setReservationStartTime(site.getReservationStartTime());
siteInfoVO.setReservationEndTime(site.getReservationEndTime());
+ siteInfoVO.setReservationTimeRanges(site.getReservationTimeRanges());
siteInfoVO.setReservationAheadMinute(site.getReservationAheadMinute());
siteInfoVO.setVinFlag(one.getVinFlag());
return R.ok(siteInfoVO);
@@ -285,6 +287,7 @@
siteInfoVO.setSupportOrder(site.getSupportOrder());
siteInfoVO.setReservationStartTime(site.getReservationStartTime());
siteInfoVO.setReservationEndTime(site.getReservationEndTime());
+ siteInfoVO.setReservationTimeRanges(site.getReservationTimeRanges());
siteInfoVO.setReservationAheadMinute(site.getReservationAheadMinute());
siteInfoVO.setVinFlag(one.getVinFlag());
return R.ok(siteInfoVO);
@@ -647,7 +650,8 @@
@ApiOperation(value = "获取站点预约设置", tags = {"管理后台-站点管理"})
public AjaxResult<Site> getReservationSetting(@PathVariable Integer id){
Site site = siteService.lambdaQuery()
- .select(Site::getId, Site::getName, Site::getSupportOrder, Site::getReservationStartTime, Site::getReservationEndTime, Site::getReservationAheadMinute)
+ .select(Site::getId, Site::getName, Site::getSupportOrder, Site::getReservationStartTime,
+ Site::getReservationEndTime, Site::getReservationTimeRanges, Site::getReservationAheadMinute)
.eq(Site::getId, id)
.eq(Site::getDelFlag, 0)
.one();
@@ -662,12 +666,37 @@
if (site == null || site.getId() == null) {
return AjaxResult.error("站点id不能为空");
}
+ String reservationTimeRanges = null;
+ String reservationStartTime = null;
+ String reservationEndTime = null;
+ if (Integer.valueOf(1).equals(site.getSupportOrder())) {
+ String sourceJson = site.getReservationTimeRanges();
+ if (StringUtils.isEmpty(sourceJson)
+ && StringUtils.isNotEmpty(site.getReservationStartTime())
+ && StringUtils.isNotEmpty(site.getReservationEndTime())) {
+ sourceJson = JSON.toJSONString(Collections.singletonList(
+ new ReservationTimeRange(site.getReservationStartTime(), site.getReservationEndTime())));
+ }
+ try {
+ reservationTimeRanges = ReservationTimeRangeSupport.normalizeJson(sourceJson);
+ List<ReservationTimeRange> ranges = ReservationTimeRangeSupport.parse(
+ reservationTimeRanges, null, null);
+ reservationStartTime = ranges.get(0).getStartTime();
+ reservationEndTime = ranges.get(0).getEndTime();
+ } catch (IllegalArgumentException e) {
+ return AjaxResult.error(e.getMessage());
+ }
+ }
+ if (site.getReservationAheadMinute() != null && site.getReservationAheadMinute() < 0) {
+ return AjaxResult.error("提前预约分钟数不能小于0");
+ }
boolean success = siteService.lambdaUpdate()
.eq(Site::getId, site.getId())
.eq(Site::getDelFlag, 0)
.set(Site::getSupportOrder, site.getSupportOrder())
- .set(Site::getReservationStartTime, site.getReservationStartTime())
- .set(Site::getReservationEndTime, site.getReservationEndTime())
+ .set(Site::getReservationStartTime, reservationStartTime)
+ .set(Site::getReservationEndTime, reservationEndTime)
+ .set(Site::getReservationTimeRanges, reservationTimeRanges)
.set(Site::getReservationAheadMinute, site.getReservationAheadMinute())
.update();
return success ? AjaxResult.success() : AjaxResult.error("保存预约设置失败");
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
index d42478d..668ff31 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
@@ -194,6 +194,16 @@
if(Objects.isNull(dto.getDiscount())){
accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate().set(TAccountingStrategy::getDiscount, null).eq(TAccountingStrategy::getId, dto.getId()));
}
+ // 历史审核通过的策略在替换旧策略后可能仍保留已删除策略的 parentId。
+ // 这类正式策略再次编辑会被改为待审核子记录,并被计费策略列表过滤掉,先将其恢复为主策略。
+ if (original.getParentId() != null && Objects.equals(original.getAuditStatus(), 3)
+ && Objects.isNull(accountingStrategyService.getById(original.getParentId()))) {
+ accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate()
+ .set(TAccountingStrategy::getParentId, null)
+ .eq(TAccountingStrategy::getId, original.getId()));
+ original.setParentId(null);
+ dto.setParentId(null);
+ }
if (original.getParentId() != null) {
dto.setParentId(original.getParentId());
dto.setSiteId(original.getSiteId());
@@ -465,21 +475,25 @@
//如果parentId不为空,则将parentId对应的siteId改为现在的,并且将parenId对应的数据删除
if (byId.getParentId()!=null) {
TAccountingStrategy one = accountingStrategyService.getById(byId.getParentId());
- byId.setSiteId(one.getSiteId());
- //将site表对应的模板id修改为新的这个
- List<Site> list = siteService.lambdaQuery().eq(Site::getAccountingStrategyId, one.getId()).list();
- if(!list.isEmpty()){
- for (Site site : list) {
- site.setAccountingStrategyId(byId.getId());
+ if (Objects.nonNull(one)) {
+ byId.setSiteId(one.getSiteId());
+ //将site表对应的模板id修改为新的这个
+ List<Site> list = siteService.lambdaQuery().eq(Site::getAccountingStrategyId, one.getId()).list();
+ if(!list.isEmpty()){
+ for (Site site : list) {
+ site.setAccountingStrategyId(byId.getId());
+ }
+ siteService.updateBatchById(list);
+ //推送三方平台
+ List<Integer> siteIds = list.stream().filter(s->1 == s.getIsOpenSynchronization()).map(Site::getId).collect(Collectors.toList());
+ if(!siteIds.isEmpty()){
+ tcecClient.notificationStationChange(siteIds);
+ }
}
- siteService.updateBatchById(list);
- //推送三方平台
- List<Integer> siteIds = list.stream().filter(s->1 == s.getIsOpenSynchronization()).map(Site::getId).collect(Collectors.toList());
- if(!siteIds.isEmpty()){
- tcecClient.notificationStationChange(siteIds);
- }
+ accountingStrategyService.removeById(one.getId());
}
- accountingStrategyService.removeById(one.getId());
+ // 审核通过后当前记录已成为正式策略,不能继续保留旧策略的 parentId。
+ byId.setParentId(null);
}
//硬件
List<TAccountingStrategyDetail> accountingStrategyDetails = accountingStrategyDetailService.list(new LambdaQueryWrapper<TAccountingStrategyDetail>()
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TParkingLotController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TParkingLotController.java
index 05580c7..36aa792 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TParkingLotController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TParkingLotController.java
@@ -181,8 +181,8 @@
@PostMapping(value = "/getRecordAmount")
- public R<BigDecimal> getRecordAmount(){
- BigDecimal sum = parkingRecordService.getSum();
+ public R<BigDecimal> getRecordAmount(@RequestParam(value = "siteIds", required = false) List<Integer> siteIds){
+ BigDecimal sum = parkingRecordService.getSum(siteIds);
return R.ok(sum);
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/TParkingRecordMapper.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/TParkingRecordMapper.java
index c6a67c2..299ea9d 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/TParkingRecordMapper.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/TParkingRecordMapper.java
@@ -24,7 +24,7 @@
*/
public interface TParkingRecordMapper extends BaseMapper<TParkingRecord> {
- BigDecimal getSum();
+ BigDecimal getSum(@Param("siteIds") List<Integer> siteIds);
/**
* 停车缴费订单列表
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TParkingRecordService.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TParkingRecordService.java
index d319020..cd7f4dc 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TParkingRecordService.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TParkingRecordService.java
@@ -22,7 +22,7 @@
*/
public interface TParkingRecordService extends IService<TParkingRecord> {
- BigDecimal getSum();
+ BigDecimal getSum(List<Integer> siteIds);
/**
* 停车缴费订单列表
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
index cbedc24..81efa23 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -16,6 +16,7 @@
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.other.api.feignClient.RoleSiteClient;
import com.ruoyi.other.api.feignClient.UserSiteClient;
import com.ruoyi.system.api.domain.SysUser;
@@ -60,6 +61,9 @@
@Resource
private SysUserRoleClient userRoleClient;
+
+ @Resource
+ private TokenService tokenService;
@@ -249,6 +253,10 @@
}
void prepareExistingUserForUpdate(SysUser user, Partner partner) {
+ // 合作商资料/类型编辑只更新账号资料和角色,密码只能通过重置密码接口修改。
+ // 显式清空可避免远程查询返回的密码值(原始哈希或脱敏值)被再次写回 sys_user。
+ user.setPassword(null);
+ user.setPassWordUpdate(null);
user.setPhonenumber(partner.getPhoneOne());
user.setUserName(partner.getAccount());
user.setNickName(partner.getName());
@@ -434,19 +442,22 @@
@Override
public Set<Integer> authSite(Integer partnerId, SiteMenu siteMenu) {
TSiteMenu one = siteMenuService.getOne(new LambdaQueryWrapper<TSiteMenu>().eq(TSiteMenu::getPath, siteMenu.getValue()).eq(TSiteMenu::getStatus, 0));
- List<TPartnerSite> list = partnerSiteService.list(new LambdaQueryWrapper<TPartnerSite>()
- .eq(TPartnerSite::getPartnerId, partnerId)
- .eq(TPartnerSite::getSiteMenuId, one.getMenuId()));
- List<Integer> collect = list.stream().map(TPartnerSite::getSiteId).collect(Collectors.toList());
- //获取合作商用户关联的站点数据
+ Set<Integer> siteIds = new HashSet<>();
+ if (one != null) {
+ List<TPartnerSite> list = partnerSiteService.list(new LambdaQueryWrapper<TPartnerSite>()
+ .eq(TPartnerSite::getPartnerId, partnerId)
+ .eq(TPartnerSite::getSiteMenuId, one.getMenuId()));
+ siteIds.addAll(list.stream().map(TPartnerSite::getSiteId).collect(Collectors.toSet()));
+ }
+ // 保留合作商主账号原有的用户/角色关联站点。
Partner partner = this.getById(partnerId);
- SysUser sysUser = sysUserClient.queryUserByUserName(partner.getAccount()).getData();
- List<Integer> data = userSiteClient.getSiteIds(sysUser.getUserId()).getData();
- List<SysUserRoleVo> data2 = userRoleClient.getRoleByUserId(sysUser.getUserId()).getData();
- List<Integer> data1 = roleSiteClient.getSiteIds(data2.get(0).getRoleId()).getData();
- collect.addAll(data);
- collect.addAll(data1);
- Set<Integer> siteIds = new HashSet<>(collect);
+ R<SysUser> partnerUserResult = partner == null ? null : sysUserClient.queryUserByUserName(partner.getAccount());
+ SysUser sysUser = partnerUserResult == null ? null : partnerUserResult.getData();
+ if (sysUser != null) {
+ siteIds.addAll(getUserAssignedSiteIds(sysUser.getUserId()));
+ }
+ // 系统内置合作商角色统一叠加当前登录用户直接关联及其所有角色关联的站点。
+ siteIds.addAll(getUserAssignedSiteIds(getCurrentUserId()));
if(siteIds.size() == 0){
siteIds.add(-1);
}
@@ -463,10 +474,47 @@
@Override
public boolean authMenu(Integer partnerId, Integer siteId, SiteMenu siteMenu) {
TSiteMenu one = siteMenuService.getOne(new LambdaQueryWrapper<TSiteMenu>().eq(TSiteMenu::getPath, siteMenu.getValue()).eq(TSiteMenu::getStatus, 0));
- TPartnerSite partnerSite = partnerSiteService.getOne(new LambdaQueryWrapper<TPartnerSite>()
- .eq(TPartnerSite::getPartnerId, partnerId)
- .eq(TPartnerSite::getSiteMenuId, one.getMenuId())
- .eq(TPartnerSite::getSiteId, siteId));
- return null == partnerSite ? false : true;
+ if (one != null) {
+ TPartnerSite partnerSite = partnerSiteService.getOne(new LambdaQueryWrapper<TPartnerSite>()
+ .eq(TPartnerSite::getPartnerId, partnerId)
+ .eq(TPartnerSite::getSiteMenuId, one.getMenuId())
+ .eq(TPartnerSite::getSiteId, siteId));
+ if (partnerSite != null) {
+ return true;
+ }
+ }
+ return getUserAssignedSiteIds(getCurrentUserId()).contains(siteId);
+ }
+
+ Set<Integer> getUserAssignedSiteIds(Long userId) {
+ Set<Integer> siteIds = new HashSet<>();
+ if (userId == null) {
+ return siteIds;
+ }
+ R<List<Integer>> userSiteResult = userSiteClient.getSiteIds(userId);
+ if (userSiteResult != null && userSiteResult.getData() != null) {
+ siteIds.addAll(userSiteResult.getData());
+ }
+ R<List<SysUserRoleVo>> roleResult = userRoleClient.getRoleByUserId(userId);
+ if (roleResult != null && roleResult.getData() != null) {
+ for (SysUserRoleVo role : roleResult.getData()) {
+ if (role == null || role.getRoleId() == null) {
+ continue;
+ }
+ R<List<Integer>> roleSiteResult = roleSiteClient.getSiteIds(role.getRoleId());
+ if (roleSiteResult != null && roleSiteResult.getData() != null) {
+ siteIds.addAll(roleSiteResult.getData());
+ }
+ }
+ }
+ return siteIds;
+ }
+
+ private Long getCurrentUserId() {
+ try {
+ return tokenService.getLoginUser() == null ? null : tokenService.getLoginUser().getUserid();
+ } catch (RuntimeException ignored) {
+ return null;
+ }
}
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java
index 5c4ed94..e53fd32 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java
@@ -144,13 +144,7 @@
//非管理员需要根据角色和用户配置查询允许的站点数据
//改用roleId=1来判断
if(admin.getData()!=1){
- List<Integer> data = userSiteClient.getSiteIds(sysUser.getUserId()).getData();
- List<SysUserRoleVo> data2 = sysUserRoleClient.getRoleByUserId(sysUser.getUserId()).getData();
- if(data2.size() > 0){
- List<Integer> data1 = roleSiteClient.getSiteIds(data2.get(0).getRoleId()).getData();
- data.addAll(data1);
- }
- ids = new HashSet<>(data);
+ ids = getAssignedSiteIds(sysUser.getUserId());
}
}
PageInfo<GetSiteListDTO> pageInfo = new PageInfo<>(siteList.getPageCurr(), siteList.getPageSize());
@@ -391,11 +385,7 @@
}else{
//非管理员需要根据角色和用户配置查询允许的站点数据
if(admin.getData()!=1){
- List<Integer> data = userSiteClient.getSiteIds(sysUser.getUserId()).getData();
- List<SysUserRoleVo> data2 = sysUserRoleClient.getRoleByUserId(sysUser.getUserId()).getData();
- List<Integer> data1 = roleSiteClient.getSiteIds(data2.get(0).getRoleId()).getData();
- data.addAll(data1);
- ids = new HashSet<>(data);
+ ids = getAssignedSiteIds(sysUser.getUserId());
}
}
PageInfo<GetSiteListDTO> pageInfo = new PageInfo<>(siteList.getPageCurr(), siteList.getPageSize());
@@ -411,6 +401,30 @@
return pageInfo.setRecords(list);
}
+ private Set<Integer> getAssignedSiteIds(Long userId) {
+ Set<Integer> siteIds = new HashSet<>();
+ if (userId == null) {
+ return siteIds;
+ }
+ R<List<Integer>> userSiteResult = userSiteClient.getSiteIds(userId);
+ if (userSiteResult != null && userSiteResult.getData() != null) {
+ siteIds.addAll(userSiteResult.getData());
+ }
+ R<List<SysUserRoleVo>> roleResult = sysUserRoleClient.getRoleByUserId(userId);
+ if (roleResult != null && roleResult.getData() != null) {
+ for (SysUserRoleVo role : roleResult.getData()) {
+ if (role == null || role.getRoleId() == null) {
+ continue;
+ }
+ R<List<Integer>> roleSiteResult = roleSiteClient.getSiteIds(role.getRoleId());
+ if (roleSiteResult != null && roleSiteResult.getData() != null) {
+ siteIds.addAll(roleSiteResult.getData());
+ }
+ }
+ }
+ return siteIds;
+ }
+
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TParkingRecordServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TParkingRecordServiceImpl.java
index 414e8d7..83c567b 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TParkingRecordServiceImpl.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TParkingRecordServiceImpl.java
@@ -50,8 +50,8 @@
private SiteClient siteClient;
@Override
- public BigDecimal getSum() {
- return this.baseMapper.getSum();
+ public BigDecimal getSum(List<Integer> siteIds) {
+ return this.baseMapper.getSum(siteIds);
}
@Autowired
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml
index 61b9461..ae5fc07 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml
@@ -221,6 +221,7 @@
end_service_time, service_description, vehicle_description, parking_space, rate_description, space_charge_explain,
accounting_strategy_id, establishment_time, sort, remark, create_time, del_flag, mark, supportOrder,
reservation_start_time AS reservationStartTime, reservation_end_time AS reservationEndTime,
+ reservation_time_ranges AS reservationTimeRanges,
reservation_ahead_minute AS reservationAheadMinute,
ROUND(
6378.138 * 2 * ASIN(
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TChargingPileMapper.xml b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TChargingPileMapper.xml
index 19af873..938211e 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TChargingPileMapper.xml
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TChargingPileMapper.xml
@@ -46,6 +46,7 @@
b.supportOrder as supportOrder,
b.reservation_start_time as reservationStartTime,
b.reservation_end_time as reservationEndTime,
+ b.reservation_time_ranges as reservationTimeRanges,
b.reservation_ahead_minute as reservationAheadMinute
from t_charging_pile a
left join t_site b on (a.site_id = b.id)
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TParkingRecordMapper.xml b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TParkingRecordMapper.xml
index e4825e7..978ae98 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TParkingRecordMapper.xml
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/TParkingRecordMapper.xml
@@ -25,7 +25,17 @@
id, code,app_user_id, license_plate, vehicle_color, charging_order_id, parking_lot_id, in_parking_time, out_parking_time, parking_duration, order_amount, status, out_parking_type, create_time
</sql>
<select id="getSum" resultType="java.math.BigDecimal">
- select sum(timeout_amount) from t_parking_record
+ select COALESCE(sum(tpr.timeout_amount), 0)
+ from t_parking_record tpr
+ left join t_parking_lot tpl on tpr.parking_lot_id = tpl.id
+ <where>
+ <if test="siteIds != null and siteIds.size() > 0">
+ tpl.site_id in
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ </where>
</select>
<select id="pageList" resultType="com.ruoyi.chargingPile.api.vo.TParkingRecordVO">
select
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupportTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupportTest.java
new file mode 100644
index 0000000..3705db8
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/api/support/ReservationTimeRangeSupportTest.java
@@ -0,0 +1,68 @@
+package com.ruoyi.chargingPile.api.support;
+
+import com.ruoyi.chargingPile.api.model.ReservationTimeRange;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalTime;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ReservationTimeRangeSupportTest {
+
+ @Test
+ void parsesMultipleJsonRangesAndKeepsTheirOrder() {
+ List<ReservationTimeRange> ranges = ReservationTimeRangeSupport.parse(
+ "[{\"startTime\":\"08:00\",\"endTime\":\"10:00\"},"
+ + "{\"startTime\":\"14:30\",\"endTime\":\"16:00\"}]",
+ null,
+ null);
+
+ assertEquals(2, ranges.size());
+ assertEquals("08:00", ranges.get(0).getStartTime());
+ assertEquals("16:00", ranges.get(1).getEndTime());
+ assertEquals("08:00-10:00、14:30-16:00", ReservationTimeRangeSupport.describe(ranges));
+ }
+
+ @Test
+ void fallsBackToLegacySingleRangeWhenJsonIsBlank() {
+ List<ReservationTimeRange> ranges = ReservationTimeRangeSupport.parse(null, "09:00", "11:00");
+
+ assertEquals(1, ranges.size());
+ assertEquals("09:00", ranges.get(0).getStartTime());
+ assertEquals("11:00", ranges.get(0).getEndTime());
+ }
+
+ @Test
+ void supportsCrossMidnightAndAdjacentRanges() {
+ String normalized = ReservationTimeRangeSupport.normalizeJson(
+ "[{\"startTime\":\"22:00\",\"endTime\":\"02:00\"},"
+ + "{\"startTime\":\"02:00\",\"endTime\":\"04:00\"}]");
+ List<ReservationTimeRange> ranges = ReservationTimeRangeSupport.parse(normalized, null, null);
+
+ assertTrue(ReservationTimeRangeSupport.contains(ranges, LocalTime.of(23, 30)));
+ assertTrue(ReservationTimeRangeSupport.contains(ranges, LocalTime.of(1, 30)));
+ assertTrue(ReservationTimeRangeSupport.contains(ranges, LocalTime.of(3, 0)));
+ assertFalse(ReservationTimeRangeSupport.contains(ranges, LocalTime.of(12, 0)));
+ }
+
+ @Test
+ void rejectsMalformedEqualOrOverlappingRanges() {
+ assertThrows(IllegalArgumentException.class, () -> ReservationTimeRangeSupport.normalizeJson(
+ "[{\"startTime\":\"8:00\",\"endTime\":\"10:00\"}]"));
+ assertThrows(IllegalArgumentException.class, () -> ReservationTimeRangeSupport.normalizeJson(
+ "[{\"startTime\":\"08:00\",\"endTime\":\"08:00\"}]"));
+ assertThrows(IllegalArgumentException.class, () -> ReservationTimeRangeSupport.normalizeJson(
+ "[{\"startTime\":\"08:00\",\"endTime\":\"10:00\"},"
+ + "{\"startTime\":\"09:30\",\"endTime\":\"11:00\"}]"));
+ }
+
+ @Test
+ void rejectsEmptyConfigurationWhenNormalizingForSave() {
+ assertThrows(IllegalArgumentException.class,
+ () -> ReservationTimeRangeSupport.normalizeJson("[]"));
+ }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java
index 3c1194a..ba20b2c 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java
@@ -92,4 +92,21 @@
assertTrue(Arrays.asList(savePermissions.value()).contains("/chargingPile/update"));
assertTrue(Arrays.asList(savePermissions.value()).contains("/chargingPile/add"));
}
+
+ @Test
+ void saveReservationSettingRejectsOverlappingRangesBeforeDatabaseUpdate() {
+ SiteController controller = new SiteController();
+ ISiteService siteService = mock(ISiteService.class);
+ ReflectionTestUtils.setField(controller, "siteService", siteService);
+ Site request = new Site();
+ request.setId(12);
+ request.setSupportOrder(1);
+ request.setReservationTimeRanges("[{\"startTime\":\"08:00\",\"endTime\":\"10:00\"},"
+ + "{\"startTime\":\"09:00\",\"endTime\":\"11:00\"}]");
+
+ AjaxResult result = controller.saveReservationSetting(request);
+
+ assertTrue(result.isError());
+ assertEquals("预约时段不能重叠", result.get("msg"));
+ }
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java
index f3bb31f..67dbf02 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java
@@ -1,8 +1,17 @@
package com.ruoyi.chargingPile.service.impl;
import com.ruoyi.chargingPile.api.model.Partner;
+import com.ruoyi.chargingPile.api.domain.SiteMenu;
import com.ruoyi.chargingPile.mapper.PartnerMapper;
+import com.ruoyi.chargingPile.service.TSiteMenuService;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.other.api.feignClient.RoleSiteClient;
+import com.ruoyi.other.api.feignClient.UserSiteClient;
import com.ruoyi.system.api.domain.SysUser;
+import com.ruoyi.system.api.feignClient.SysUserRoleClient;
+import com.ruoyi.system.api.model.LoginUser;
+import com.ruoyi.system.api.model.SysUserRoleVo;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.junit.jupiter.api.Test;
@@ -10,19 +19,24 @@
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertTrue;
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;
+import static org.mockito.ArgumentMatchers.any;
class PartnerServiceImplTest {
@Test
- void editingPartnerDoesNotReplaceExistingLoginPasswordHash() {
+ void editingPartnerExcludesPasswordFromSystemUserUpdate() {
PartnerServiceImpl service = new PartnerServiceImpl();
Partner partner = new Partner();
partner.setAccount("partner-user");
@@ -32,10 +46,12 @@
partner.setRoleId(9);
SysUser user = new SysUser();
user.setPassword("$2a$10$existing-bcrypt-hash");
+ user.setPassWordUpdate(new java.util.Date());
service.prepareExistingUserForUpdate(user, partner);
- assertEquals("$2a$10$existing-bcrypt-hash", user.getPassword());
+ assertNull(user.getPassword());
+ assertNull(user.getPassWordUpdate());
assertEquals("partner-user", user.getUserName());
assertEquals(Long.valueOf(9), user.getRoleId());
}
@@ -109,4 +125,38 @@
assertTrue(wrapper.getSqlSet().contains("electricity_share_ratio"));
assertTrue(wrapper.getSqlSet().contains("service_share_ratio"));
}
+
+ @Test
+ void partnerSitePermissionIncludesCurrentUsersDirectAndAllRoleSites() {
+ PartnerServiceImpl service = new PartnerServiceImpl();
+ PartnerMapper mapper = mock(PartnerMapper.class);
+ UserSiteClient userSiteClient = mock(UserSiteClient.class);
+ RoleSiteClient roleSiteClient = mock(RoleSiteClient.class);
+ SysUserRoleClient userRoleClient = mock(SysUserRoleClient.class);
+ TokenService tokenService = mock(TokenService.class);
+ TSiteMenuService siteMenuService = mock(TSiteMenuService.class);
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ ReflectionTestUtils.setField(service, "userSiteClient", userSiteClient);
+ ReflectionTestUtils.setField(service, "roleSiteClient", roleSiteClient);
+ ReflectionTestUtils.setField(service, "userRoleClient", userRoleClient);
+ ReflectionTestUtils.setField(service, "tokenService", tokenService);
+ ReflectionTestUtils.setField(service, "siteMenuService", siteMenuService);
+
+ LoginUser loginUser = new LoginUser();
+ loginUser.setUserid(100L);
+ SysUserRoleVo firstRole = new SysUserRoleVo();
+ firstRole.setRoleId(10L);
+ SysUserRoleVo secondRole = new SysUserRoleVo();
+ secondRole.setRoleId(20L);
+ when(tokenService.getLoginUser()).thenReturn(loginUser);
+ when(siteMenuService.getOne(any())).thenReturn(null);
+ when(userSiteClient.getSiteIds(100L)).thenReturn(R.ok(Arrays.asList(1, 2)));
+ when(userRoleClient.getRoleByUserId(100L)).thenReturn(R.ok(Arrays.asList(firstRole, secondRole)));
+ when(roleSiteClient.getSiteIds(10L)).thenReturn(R.ok(Arrays.asList(2, 3)));
+ when(roleSiteClient.getSiteIds(20L)).thenReturn(R.ok(Arrays.asList(4, 5)));
+
+ Set<Integer> result = service.authSite(9, SiteMenu.SITE_LIST);
+
+ assertEquals(new HashSet<>(Arrays.asList(1, 2, 3, 4, 5)), result);
+ }
}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
index 37cf4b7..35e504d 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
@@ -23,6 +23,7 @@
import com.ruoyi.order.api.vo.*;
import com.ruoyi.order.export.*;
import com.ruoyi.order.service.*;
+import com.ruoyi.order.util.task.TaskUtil;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -40,6 +41,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
@@ -69,6 +71,21 @@
private TChargingOrderRefundService tChargingOrderRefundService;
@Resource
private TChargingBillService tChargingBillService;
+ @Resource
+ private TaskUtil taskUtil;
+
+ @RequiresPermissions(value = {"/chargeBill"}, logical = Logical.OR)
+ @PostMapping(value = "/generate")
+ @ApiOperation(value = "主动生成指定月份未出账充电结算账单", tags = {"管理后台-充电算账单"})
+ public R generateChargingBill(@RequestParam(value = "billingMonth", required = false) String billingMonth) {
+ YearMonth targetMonth = billingMonth != null && !billingMonth.trim().isEmpty()
+ ? YearMonth.parse(billingMonth.trim()) : YearMonth.now();
+ if (targetMonth.isAfter(YearMonth.now())) {
+ return R.fail("不能生成未来月份账单");
+ }
+ taskUtil.refreshChargingSettlementBill(targetMonth.atDay(1));
+ return R.ok();
+ }
@GetMapping(value = "/test")
public R accountAdd() {
// List<TChargingBill> list = tChargingBillService.lambdaQuery().list();
@@ -191,7 +208,7 @@
tChargingBillExport.setType(orderInvoiceVO.getType().toString());
tChargingBillExport.setBillWeek(orderInvoiceVO.getBillWeek());
tChargingBillExport.setSiteName(orderInvoiceVO.getSiteName());
- tChargingBillExport.setPaymentAmount(orderInvoiceVO.getPaymentAmount());
+ tChargingBillExport.setPaymentAmount(orderInvoiceVO.getOrderAmount());
tChargingBillExport.setPlatformIncome(orderInvoiceVO.getPlatformIncome());
tChargingBillExport.setPartnerIncome(orderInvoiceVO.getPartnerIncome());
tChargingBillExport.setElectrovalence(orderInvoiceVO.getElectrovalence());
@@ -253,9 +270,9 @@
dto.setPageSize(100000);
AccountListVO res = chargingBillService.accountSettlementBillList(dto);
List<ChargingBillListVO> list = res.getList().getRecords();
- List<TChargingBillExport> tChargingBillExports = new ArrayList<>();
+ List<TAccountSettlementBillExport> tChargingBillExports = new ArrayList<>();
for (ChargingBillListVO orderInvoiceVO : list) {
- TChargingBillExport tChargingBillExport = new TChargingBillExport();
+ TAccountSettlementBillExport tChargingBillExport = new TAccountSettlementBillExport();
tChargingBillExport.setCode(orderInvoiceVO.getCode());
switch (orderInvoiceVO.getOrderState()){
case 1:
@@ -265,39 +282,19 @@
tChargingBillExport.setBillType("月账单");
break;
}
- tChargingBillExport.setType(orderInvoiceVO.getType().toString());
+ tChargingBillExport.setType(Integer.valueOf(1).equals(orderInvoiceVO.getType()) ? "全站账单" : "站点账单");
tChargingBillExport.setBillWeek(orderInvoiceVO.getBillWeek());
+ tChargingBillExport.setAccountType("工商银行商户");
tChargingBillExport.setSiteName(orderInvoiceVO.getSiteName());
tChargingBillExport.setPaymentAmount(orderInvoiceVO.getPaymentAmount());
+ tChargingBillExport.setRefundAmount(orderInvoiceVO.getRefundAmount());
tChargingBillExport.setPlatformIncome(orderInvoiceVO.getPlatformIncome());
tChargingBillExport.setPartnerIncome(orderInvoiceVO.getPartnerIncome());
- tChargingBillExport.setElectrovalence(orderInvoiceVO.getElectrovalence());
- tChargingBillExport.setServiceCharge(orderInvoiceVO.getServiceCharge());
- tChargingBillExport.setChargingCapacity(orderInvoiceVO.getChargingCapacity());
- Integer chargingSecond = Optional.ofNullable(orderInvoiceVO.getChargingSecond()).orElse(0);
- // 根据秒数 转换为xx小时xx分钟xx秒 如果小时为0不展示 如果分钟为0则不展示
- // 计算小时、分钟和秒
- int hours = chargingSecond / 60 / 60;
- int minutes = chargingSecond /60 % 60;
- int seconds = 0; // 如果没有秒数,则默认是0
- // 构造结果字符串
- StringBuilder result = new StringBuilder();
- if (hours > 0) {
- result.append(hours).append("小时");
- }
- if (minutes > 0) {
- result.append(minutes).append("分钟");
- }
- if (seconds > 0 || result.length() == 0) { // 如果秒数大于0,或者小时和分钟都为0,则显示秒数
- result.append(seconds).append("秒");
- }
- tChargingBillExport.setChargingTime(result.toString());
- tChargingBillExport.setOrderCount(orderInvoiceVO.getOrderCount());
tChargingBillExport.setBillTime(orderInvoiceVO.getCreateTime() == null ? "-"
: orderInvoiceVO.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
tChargingBillExports.add(tChargingBillExport);
}
- Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), TChargingBillExport.class, tChargingBillExports);
+ Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), TAccountSettlementBillExport.class, tChargingBillExports);
HttpServletResponse response = WebUtils.response();
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index 5ca26f5..54fb61d 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -66,6 +66,7 @@
import com.ruoyi.order.vo.ChargingSimulationVO;
import com.ruoyi.order.vo.IcbcAdminSubOrderVO;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
+import com.ruoyi.order.vo.ReservationRecommendationVO;
import com.ruoyi.order.vo.UpdateOrderPaymentAmountDto;
import com.ruoyi.order.vo.WatchChargingOrderVo;
import com.ruoyi.other.api.feignClient.RoleSiteClient;
@@ -166,6 +167,8 @@
@Resource
private TOrderInvoiceService invoiceService;
@Resource
+ private TOrderInvoiceDetailService orderInvoiceDetailService;
+ @Resource
private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
@Resource
private PartnerClient partnerClient;
@@ -183,6 +186,12 @@
private OtherClient otherClient;
@Resource
private SysUserRoleClient sysUserRoleClient;
+
+ @GetMapping("/reservationRecommendation")
+ @ApiOperation(value = "获取站点预约充电推荐时段", tags = {"管理后台-充电桩信息"})
+ public R<ReservationRecommendationVO> getReservationRecommendation(@RequestParam Integer siteId) {
+ return R.ok(chargingOrderService.getReservationRecommendation(siteId));
+ }
/** 小程序加盟商看板:站点范围始终从登录账号绑定关系解析。 */
@PostMapping("/app/franchisee/charge-data")
@@ -731,6 +740,7 @@
@PostMapping(value = "/pay/order/list")
@ApiOperation(value = "列表", tags = {"管理后台-支付订单-订单信息"})
public R<PageInfo<PayOrderDto>> payOrderList(@RequestBody PayOrderQueryDto payOrderQueryDto) {
+ resolveInvoiceOrderIds(payOrderQueryDto);
return chargingOrderService.payOrderQuery(payOrderQueryDto);
}
@@ -740,6 +750,7 @@
@ApiOperation(value = "导出查看订单列表", tags = {"管理后台-支付订单-订单信息"})
@Log(title = "发票管理查看订单导出", businessType = BusinessType.EXPORT)
public void exportPayOrder(@RequestBody PayOrderQueryDto payOrderQueryDto) {
+ resolveInvoiceOrderIds(payOrderQueryDto);
payOrderQueryDto.setPageCurr(1);
payOrderQueryDto.setPageSize(10000);
R<PageInfo<PayOrderDto>> result = chargingOrderService.payOrderQuery(payOrderQueryDto);
@@ -761,6 +772,26 @@
} catch (IOException e) {
log.error("导出查看订单列表失败", e);
}
+ }
+
+ /**
+ * 查看开票订单时以数据库中的开票明细为准,避免前端遗漏或篡改订单 ID。
+ */
+ private void resolveInvoiceOrderIds(PayOrderQueryDto query) {
+ if (query == null || query.getInvoiceId() == null) {
+ return;
+ }
+ LambdaQueryWrapper<TOrderInvoiceDetail> wrapper = new LambdaQueryWrapper<TOrderInvoiceDetail>()
+ .eq(TOrderInvoiceDetail::getOrderInvoiceId, query.getInvoiceId());
+ if (query.getType() != null) {
+ wrapper.eq(TOrderInvoiceDetail::getOrderType, query.getType());
+ }
+ List<String> orderIds = orderInvoiceDetailService.list(wrapper).stream()
+ .map(TOrderInvoiceDetail::getOrderId)
+ .filter(Objects::nonNull)
+ .map(String::valueOf)
+ .collect(Collectors.toList());
+ query.setIds(orderIds);
}
private PayOrderExport buildPayOrderExport(PayOrderDto dto) {
@@ -1741,7 +1772,9 @@
}
boolean platformVisible = !partnerView;
if(platformVisible){
- BigDecimal data = parkingLotClient.getRecordAmount().getData();
+ R<BigDecimal> parkingAmountResult = parkingLotClient.getRecordAmount(siteIds);
+ BigDecimal data = parkingAmountResult == null || parkingAmountResult.getData() == null
+ ? BigDecimal.ZERO : parkingAmountResult.getData();
map.put("parkingAmount",data);
BigDecimal data1 = shoppingOrderService.getSumAmount();
map.put("shopAmount",data1);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
index e3e82cf..52078eb 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
@@ -82,6 +82,13 @@
return AjaxResult.success(orderInvoiceService.pageList(query));
}
+ @RequiresPermissions(value = {"/invoiceManagement"}, logical = Logical.OR)
+ @PostMapping("/invoicingCompanyOptions")
+ @ApiOperation(value = "查询开票公司选项", tags = {"管理后台-发票管理"})
+ public AjaxResult<List<String>> invoicingCompanyOptions(@RequestBody TOrderInvoiceQuery query) {
+ return AjaxResult.success(orderInvoiceService.invoicingCompanyOptions(query));
+ }
+
@RequiresPermissions(value = {"/invoiceManagement/accept_hear_case"}, logical = Logical.OR)
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
index 2009a50..ad70d09 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -282,8 +282,8 @@
export.setRefundAmount(order.getRefundAmount());
export.setPhone(order.getPhone());
export.setOrderSource(getChargingOrderSource(order.getOrderSource()));
- export.setStartTime(DateUtils.localDateTimeToString(order.getStartTime()));
- export.setEndTime(DateUtils.localDateTimeToString(order.getEndTime()));
+ export.setStartTime(formatExportDateTime(order.getStartTime()));
+ export.setEndTime(formatExportDateTime(order.getEndTime()));
exports.add(export);
}
writeVipChargingOrderWorkbook(exports);
@@ -302,6 +302,10 @@
return "";
}
+ static String formatExportDateTime(LocalDateTime time) {
+ return time == null ? "" : DateUtils.localDateTimeToString(time);
+ }
+
private void writeVipChargingOrderWorkbook(List<EnterpriseChargingDetailExport> exports) {
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), EnterpriseChargingDetailExport.class, exports);
writeWorkbook(workbook, "会员充电订单导出.xls");
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderQueryDto.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderQueryDto.java
index ebe73e6..1f1eaef 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderQueryDto.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderQueryDto.java
@@ -17,6 +17,8 @@
private String code;
@ApiModelProperty("开票跳转-订单ids数组")
private List<String> ids;
+ @ApiModelProperty("开票申请ID;查看开票订单时由后端重新解析关联订单")
+ private Long invoiceId;
@ApiModelProperty("1充电订单2购物订单3vip订单4停车订单")
private Integer type;
@ApiModelProperty("手机号")
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TAccountSettlementBillExport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TAccountSettlementBillExport.java
new file mode 100644
index 0000000..aabb391
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TAccountSettlementBillExport.java
@@ -0,0 +1,34 @@
+package com.ruoyi.order.export;
+
+import cn.afterturn.easypoi.excel.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class TAccountSettlementBillExport implements Serializable {
+
+ @Excel(name = "账单编号", width = 30)
+ private String code;
+ @Excel(name = "账单类型", width = 20)
+ private String billType;
+ @Excel(name = "账单分类", width = 20)
+ private String type;
+ @Excel(name = "账单周期", width = 20)
+ private String billWeek;
+ @Excel(name = "账户类型", width = 20)
+ private String accountType;
+ @Excel(name = "站点名称", width = 30)
+ private String siteName;
+ @Excel(name = "支付结算金额", width = 20)
+ private BigDecimal paymentAmount;
+ @Excel(name = "退款结算金额", width = 20)
+ private BigDecimal refundAmount;
+ @Excel(name = "平台收入", width = 20)
+ private BigDecimal platformIncome;
+ @Excel(name = "合作商收入", width = 20)
+ private BigDecimal partnerIncome;
+ @Excel(name = "账单生成日期", width = 25)
+ private String billTime;
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
index a856fbf..e75ec70 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -93,7 +93,11 @@
List<ChargingOrderGroup> chargingOrderGroup(@Param("chargingPercentProvinceDto")ChargingPercentProvinceDto chargingPercentProvinceDto);
- List<SixChargingDto> charge(@Param("sixBefore")LocalDate sixBefore,@Param("siteIds") List<Integer> siteIds, @Param("partnerView") Boolean partnerView);
+ List<SixChargingDto> chargePartner(@Param("sixBefore") String sixBefore, @Param("siteIds") List<Integer> siteIds);
+
+ List<SixChargingDto> chargeSettlement(@Param("sixBefore") String sixBefore, @Param("siteIds") List<Integer> siteIds);
+
+ List<SixChargingDto> chargeCurrentMonth(@Param("siteIds") List<Integer> siteIds);
List<SixCircleDto> circle(@Param("siteIds")List<Integer> siteIds,@Param("sixBefore")LocalDate sixBefore, @Param("partnerView") Boolean partnerView);
@@ -248,7 +252,11 @@
* @param endTime
*/
List<Map<String, Object>> getChargingSettlementBill(@Param("startTime") String startTime, @Param("endTime") String endTime,
- @Param("dateType") Integer dateType);
+ @Param("dateType") Integer dateType);
+
+ List<Map<String, Object>> queryChargingBillIncome(@Param("billingCycles") List<String> billingCycles,
+ @Param("dateType") Integer dateType,
+ @Param("siteIds") List<Integer> siteIds);
/**
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
index 9a6fdad..a2c0a79 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
@@ -31,6 +31,11 @@
List<TOrderInvoiceVO> pageList(@Param("query") TOrderInvoiceQuery query, @Param("pageInfo")PageInfo<TOrderInvoiceVO> pageInfo);
/**
+ * 查询开票公司下拉选项
+ */
+ List<String> invoicingCompanyOptions(@Param("query") TOrderInvoiceQuery query);
+
+ /**
* 查询开票状态数量
* @param query
* @return
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
index 976550e..dc3dd99 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -19,6 +19,7 @@
import com.ruoyi.order.vo.EndOfChargePageInfo;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import com.ruoyi.order.vo.LargeChargingPowerVo;
+import com.ruoyi.order.vo.ReservationRecommendationVO;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
@@ -40,6 +41,8 @@
* @since 2024-08-07
*/
public interface TChargingOrderService extends IService<TChargingOrder> {
+
+ ReservationRecommendationVO getReservationRecommendation(Integer siteId);
/**
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
index 366df75..c47f46e 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
@@ -56,6 +56,11 @@
PageInfo<TOrderInvoiceVO> pageList(TOrderInvoiceQuery query);
/**
+ * 查询当前账号可见的开票公司选项
+ */
+ List<String> invoicingCompanyOptions(TOrderInvoiceQuery query);
+
+ /**
* 查询开票状态数量
* @param query
* @return
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
index 5df6900..bfef8d4 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
@@ -197,8 +197,8 @@
vo.setCommissionAmount(zeroIfNull(statement.getPlatformFee()));
vo.setSharingAmount(zeroIfNull(statement.getTripartiteCommission()));
vo.setBillMoney(zeroIfNull(statement.getRecordedAmount()));
- vo.setPlatformIncome(BigDecimal.ZERO);
- vo.setPartnerIncome(BigDecimal.ZERO);
+ vo.setPlatformIncome(zeroIfNull(statement.getPlatformIncome()));
+ vo.setPartnerIncome(zeroIfNull(statement.getPartnerIncome()));
vo.setAuthDownLoad(false);
return vo;
}
@@ -1230,8 +1230,24 @@
ChargingBillWapper chargingBillVO = new ChargingBillWapper();
List<Map<String, Object>> pageList = new ArrayList<>();
Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
- List<Map<String, Object>> incomeRows = chargingSettlementBillService.queryIncomeList(
+ List<Map<String, Object>> settlementIncomeRows = chargingSettlementBillService.queryIncomeList(
dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
+ List<String> incomeBillingCycles = settlementIncomeRows == null ? Collections.emptyList() : settlementIncomeRows.stream()
+ .map(row -> row.get("billingCycle"))
+ .filter(Objects::nonNull)
+ .map(Object::toString)
+ .distinct()
+ .collect(Collectors.toList());
+ List<Integer> incomeSiteIds = settlementIncomeRows == null ? Collections.emptyList() : settlementIncomeRows.stream()
+ .map(row -> row.get("site_id"))
+ .filter(Objects::nonNull)
+ .map(value -> Integer.valueOf(value.toString()))
+ .filter(siteId -> siteId > 0)
+ .distinct()
+ .collect(Collectors.toList());
+ List<Map<String, Object>> incomeRows = incomeBillingCycles.isEmpty() || incomeSiteIds.isEmpty()
+ ? Collections.emptyList()
+ : chargingOrderList.queryChargingBillIncome(incomeBillingCycles, statisticalDateType, incomeSiteIds);
Map<String, Map<String, BigDecimal>> incomeIndex = buildChargingBillIncomeIndex(incomeRows, siteServiceShareRatioMap);
Set<Integer> settlementSiteIds = new HashSet<>();
if (list != null) {
@@ -1298,10 +1314,14 @@
BigDecimal discount = (null == map || null == map.get("couponDeduction")) ? BigDecimal.ZERO : new BigDecimal(map.get("couponDeduction").toString());
BigDecimal platformIncome = BigDecimal.ZERO;
BigDecimal partnerIncome = BigDecimal.ZERO;
- if (incomeRows != null) {
- for (Map<String, Object> row : incomeRows) {
+ if (settlementIncomeRows != null) {
+ Set<String> summarizedIncomeKeys = new HashSet<>();
+ for (Map<String, Object> row : settlementIncomeRows) {
Integer siteId = Integer.valueOf(row.get("site_id").toString());
String billingCycle = row.get("billingCycle").toString();
+ if (!summarizedIncomeKeys.add(chargingBillIncomeKey(siteId, billingCycle))) {
+ continue;
+ }
Map<String, BigDecimal> incomeMap = getChargingBillIncome(incomeIndex, siteId, billingCycle);
platformIncome = platformIncome.add(incomeMap.get("platformIncome"));
partnerIncome = partnerIncome.add(incomeMap.get("partnerIncome"));
@@ -1335,12 +1355,9 @@
}
Integer siteId = Integer.valueOf(row.get("site_id").toString());
String billingCycle = row.get("billingCycle").toString();
- BigDecimal electricAmount = decimalValue(row.get("total_electricity_charge"));
- BigDecimal serviceAmount = decimalValue(row.get("total_service_fee"));
- BigDecimal tripartiteCommission = decimalValue(row.get("tripartite_commission"));
- BigDecimal paymentAmount = electricAmount.add(serviceAmount).subtract(tripartiteCommission);
- BigDecimal tripartiteServiceAmount = tripartiteCommission.multiply(new BigDecimal("5"));
- BigDecimal commissionableServiceAmount = serviceAmount.subtract(tripartiteServiceAmount).max(BigDecimal.ZERO);
+ BigDecimal paymentAmount = decimalValue(row.get("payment_amount"));
+ // SQL 已按订单来源排除三方引流订单,避免通过三方分佣金额反推服务费产生舍入误差。
+ BigDecimal commissionableServiceAmount = decimalValue(row.get("commissionable_service_fee"));
BigDecimal partnerIncome = BigDecimal.ZERO;
BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(siteId);
if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
@@ -1882,24 +1899,16 @@
continue;
}
List<TChargingOrderAccountingStrategy> orderStrategies = strategies.stream()
- .filter(strategy -> strategy.getChargingOrderId().equals(order.getId()))
+ .filter(strategy -> Objects.equals(strategy.getChargingOrderId(), order.getId()))
.collect(Collectors.toList());
- BigDecimal electronicReduce = orderStrategies.stream()
- .map(TChargingOrderAccountingStrategy::getPeriodElectricPrice)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal serviceReduce = orderStrategies.stream()
.map(TChargingOrderAccountingStrategy::getPeriodServicePrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal paymentAmount;
- if (Integer.valueOf(2).equals(order.getOrderSource())) {
- paymentAmount = electronicReduce.add(serviceReduce.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.DOWN));
- } else {
- paymentAmount = electronicReduce.add(serviceReduce);
- if (Integer.valueOf(1).equals(order.getOrderSource())) {
- BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(order.getSiteId());
- if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
- partnerIncome = partnerIncome.add(serviceReduce.multiply(serviceShareRatio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
- }
+ BigDecimal paymentAmount = order.getPaymentAmount() == null ? BigDecimal.ZERO : order.getPaymentAmount();
+ if (Integer.valueOf(1).equals(order.getOrderSource())) {
+ BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(order.getSiteId());
+ if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
+ partnerIncome = partnerIncome.add(serviceReduce.multiply(serviceShareRatio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
}
}
platformIncome = platformIncome.add(paymentAmount);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
index ae56473..2e00b15 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -17,6 +17,7 @@
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.feignClient.*;
import com.ruoyi.chargingPile.api.model.*;
+import com.ruoyi.chargingPile.api.support.ReservationTimeRangeSupport;
import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.dto.ChargingOrderGroup;
@@ -59,6 +60,8 @@
import com.ruoyi.order.service.TChargingOrderService;
import com.ruoyi.order.service.TOrderEvaluateService;
import com.ruoyi.order.service.*;
+import com.ruoyi.order.support.ReservationOrderSupport;
+import com.ruoyi.order.support.ReservationRecommendationCalculator;
import com.ruoyi.order.util.mongodb.service.PlatformStartChargingReplyService;
import com.ruoyi.order.util.mongodb.service.TransactionRecordService;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
@@ -68,6 +71,7 @@
import com.ruoyi.order.vo.OrderCountByDate;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import com.ruoyi.order.vo.LargeChargingPowerVo;
+import com.ruoyi.order.vo.ReservationRecommendationVO;
import com.ruoyi.other.api.domain.*;
import com.ruoyi.order.vo.ChargingOrderListInfoVO;
import com.ruoyi.other.api.domain.TVip;
@@ -135,6 +139,48 @@
private static final int WHITELIST_BENEFIT_SERVICE_FREE = 3;
private static final String SHARE_AFTER_COMMIT_RESOURCE_PREFIX = "chargingOrderShareAfterCommit:";
private static final String SPECIAL_REFUND_TITLE = "特殊退款处理";
+ private static final int RESERVATION_RECOMMENDATION_HISTORY_DAYS = 30;
+
+ @Override
+ public ReservationRecommendationVO getReservationRecommendation(Integer siteId) {
+ if (siteId == null) {
+ throw new ServiceException("站点不能为空");
+ }
+ R<List<Site>> siteResult = siteClient.getSiteByIds(Collections.singletonList(siteId));
+ List<Site> sites = siteResult == null ? null : siteResult.getData();
+ if (sites == null || sites.isEmpty()) {
+ throw new ServiceException("站点不存在");
+ }
+ Site site = sites.get(0);
+ if (site.getAccountingStrategyId() == null) {
+ throw new ServiceException("该站点尚未配置充电计费模板");
+ }
+ R<List<TAccountingStrategyDetail>> detailResult = accountingStrategyDetailClient
+ .getListByAccountingStrategyId(site.getAccountingStrategyId());
+ List<TAccountingStrategyDetail> details = detailResult == null ? null : detailResult.getData();
+ if (details == null) {
+ throw new ServiceException("计费模板服务暂不可用");
+ }
+
+ LocalDateTime historyEnd = LocalDate.now().atStartOfDay();
+ LocalDateTime historyStart = historyEnd.minusDays(RESERVATION_RECOMMENDATION_HISTORY_DAYS);
+ List<TChargingOrder> orders = list(Wrappers.<TChargingOrder>lambdaQuery()
+ .select(TChargingOrder::getStartTime, TChargingOrder::getEndTime)
+ .eq(TChargingOrder::getSiteId, siteId)
+ .eq(TChargingOrder::getStatus, 5)
+ .eq(TChargingOrder::getDelFlag, 0)
+ .isNotNull(TChargingOrder::getStartTime)
+ .isNotNull(TChargingOrder::getEndTime)
+ .ge(TChargingOrder::getEndTime, historyStart)
+ .lt(TChargingOrder::getStartTime, historyEnd));
+ ReservationRecommendationCalculator.Recommendation recommendation =
+ ReservationRecommendationCalculator.recommend(details, orders, historyStart, historyEnd);
+ if (recommendation == null) {
+ throw new ServiceException("该站点计费模板未配置谷时段");
+ }
+ return new ReservationRecommendationVO(recommendation.getStartTime(), recommendation.getEndTime(),
+ recommendation.getUtilizationRate());
+ }
@Resource
private ChargingGunClient chargingGunClient;
@@ -1603,44 +1649,21 @@
&& reservationTime.isAfter(LocalDateTime.now().plusMinutes(site.getReservationAheadMinute()))) {
return AjaxResult.error("预约时间不能超过最早可提前预约时限");
}
- if (!StringUtils.hasLength(site.getReservationStartTime())
- || !StringUtils.hasLength(site.getReservationEndTime())) {
- return AjaxResult.error("电站尚未配置预约时段");
- }
- LocalTime start = parseReservationClock(site.getReservationStartTime());
- LocalTime end = parseReservationClock(site.getReservationEndTime());
- if (start == null || end == null) {
+ List<ReservationTimeRange> ranges;
+ try {
+ ranges = ReservationTimeRangeSupport.parse(site.getReservationTimeRanges(),
+ site.getReservationStartTime(), site.getReservationEndTime());
+ } catch (IllegalArgumentException e) {
return AjaxResult.error("电站预约时段配置不正确");
}
- if (!isTimeInWindow(reservationTime.toLocalTime(), start, end)) {
- return AjaxResult.error("预约时间不在允许预约时段内");
+ if (ranges.isEmpty()) {
+ return AjaxResult.error("电站尚未配置预约时段");
+ }
+ if (!ReservationTimeRangeSupport.contains(ranges, reservationTime.toLocalTime())) {
+ return AjaxResult.error("预约时间不在允许预约时段内("
+ + ReservationTimeRangeSupport.describe(ranges) + ")");
}
return null;
- }
-
- private LocalTime parseReservationClock(String value) {
- if (!StringUtils.hasLength(value)) {
- return null;
- }
- String[] parts = value.trim().split(":");
- if (parts.length < 2) {
- return null;
- }
- try {
- return LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
- } catch (RuntimeException e) {
- return null;
- }
- }
-
- private boolean isTimeInWindow(LocalTime value, LocalTime start, LocalTime end) {
- if (start.equals(end)) {
- return true;
- }
- if (start.isBefore(end)) {
- return !value.isBefore(start) && !value.isAfter(end);
- }
- return !value.isBefore(start) || !value.isAfter(end);
}
private boolean isSafetyCheckPassed(PreChargeCheck check) {
@@ -1697,7 +1720,7 @@
if (latest == null || latest.getStatus() != 2 || Boolean.TRUE.equals(check.getStartCommandSent())) {
return;
}
- if (latest.getReservationTime() != null && latest.getReservationTime().isAfter(LocalDateTime.now())) {
+ if (!ReservationOrderSupport.isStartDue(latest.getReservationTime(), LocalDateTime.now())) {
return;
}
TChargingGun gun = chargingGunClient.getChargingGunById(latest.getChargingGunId()).getData();
@@ -2348,9 +2371,8 @@
chargingDetails.setName(site.getName() + "-" + chargingPile.getName());
chargingDetails.setCode(one.getCode());
chargingDetails.setReservationTime(one.getReservationTime());
- boolean reservationPending = one.getReservationTime() != null
- && one.getReservationTime().isAfter(LocalDateTime.now())
- && Integer.valueOf(2).equals(one.getStatus());
+ boolean reservationPending = ReservationOrderSupport.isPending(
+ one.getStatus(), one.getReservationTime(), LocalDateTime.now());
chargingDetails.setStatus(reservationPending ? 1 : one.getStatus());
chargingDetails.setChargingCost(one.getResidualAmount());
UploadRealTimeMonitoringData data = uploadRealTimeMonitoringDataService.getLastDataById(one.getCode());
@@ -2388,17 +2410,23 @@
@Override
public AjaxResult stopCharging(String id) {
TChargingOrder order = this.getById(id);
- Integer status = order.getStatus();
- if (status != 3) {
- return AjaxResult.error("还未开始充电");
- }
- if (status == 4 || status == 5) {
- return AjaxResult.error("不能重复操作");
+ if (order == null) {
+ return AjaxResult.error("充电订单不存在");
}
Long userId = tokenService.getLoginUserApplet().getUserId();
if (!order.getAppUserId().equals(userId)) {
- log.info("【" + order.getCode() + "】主动停止充电失败,用户权限不足。当前登录用户id:{},订单用户id:{}", userId, order.getAppUserId());
+ log.info("【" + order.getCode() + "】停止或取消充电失败,用户权限不足。当前登录用户id:{},订单用户id:{}", userId, order.getAppUserId());
return AjaxResult.error("权限不足");
+ }
+ if (ReservationOrderSupport.isPending(order.getStatus(), order.getReservationTime(), LocalDateTime.now())) {
+ return cancelPendingReservation(order);
+ }
+ Integer status = order.getStatus();
+ if (status == 4 || status == 5) {
+ return AjaxResult.error("不能重复操作");
+ }
+ if (status != 3) {
+ return AjaxResult.error("还未开始充电");
}
order.setEndTime(LocalDateTime.now());
order.setStatus(4);
@@ -2434,6 +2462,40 @@
chengDuJianGuanClient.notificationConnectorChargeStatus(vo);
});
return AjaxResult.success();
+ }
+
+ private AjaxResult cancelPendingReservation(TChargingOrder order) {
+ RLock lock = redissonClient.getLock("START_AFTER_SAFETY:" + order.getId());
+ boolean locked = false;
+ try {
+ locked = lock.tryLock(1, 120, TimeUnit.SECONDS);
+ if (!locked) {
+ return AjaxResult.error("预约订单正在处理中,请稍后重试");
+ }
+ TChargingOrder latest = this.getById(order.getId());
+ if (latest == null || !ReservationOrderSupport.isPending(
+ latest.getStatus(), latest.getReservationTime(), LocalDateTime.now())) {
+ return AjaxResult.error("预约已到时间或订单状态已变化,无法取消");
+ }
+ refund(latest.getCode());
+ TChargingOrder cancelled = this.getById(latest.getId());
+ if (cancelled != null) {
+ cancelled.setStatus(-1);
+ cancelled.setEndMode(1);
+ cancelled.setEndTime(LocalDateTime.now());
+ this.updateById(cancelled);
+ }
+ redisService.deleteObject("AQJC_" + latest.getChargingGunId());
+ log.info("【{}】用户取消预约充电,预约时间:{}", latest.getCode(), latest.getReservationTime());
+ return AjaxResult.success("预约充电已取消,退款将原路退回");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return AjaxResult.error("取消预约被中断,请稍后重试");
+ } finally {
+ if (locked && lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
}
@@ -2996,6 +3058,12 @@
payOrderQueryDto = new PayOrderQueryDto();
}
PageInfo<PayOrderDto> pageInfo = new PageInfo<>(payOrderQueryDto.getPageCurr(), payOrderQueryDto.getPageSize());
+ // 开票申请没有关联明细时必须返回空结果,不能退化成支付订单全量查询。
+ if (payOrderQueryDto.getInvoiceId() != null
+ && (payOrderQueryDto.getIds() == null || payOrderQueryDto.getIds().isEmpty())) {
+ pageInfo.setRecords(Collections.emptyList());
+ return R.ok(pageInfo);
+ }
List<PayOrderDto> list = this.baseMapper.payOrderQuery(pageInfo, payOrderQueryDto);
if (list == null || list.isEmpty()) {
pageInfo.setRecords(Collections.emptyList());
@@ -3139,7 +3207,23 @@
@Override
public List<SixChargingDto> charge(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView) {
- List<SixChargingDto> result = this.baseMapper.charge(sixBefore, siteIds, partnerView);
+ String sixBeforeValue = sixBefore == null ? null : sixBefore.toString();
+ List<SixChargingDto> result = new ArrayList<>();
+ if (Boolean.TRUE.equals(partnerView)) {
+ List<SixChargingDto> partnerIncome = this.baseMapper.chargePartner(sixBeforeValue, siteIds);
+ if (partnerIncome != null) {
+ result.addAll(partnerIncome);
+ }
+ } else {
+ List<SixChargingDto> settlementIncome = this.baseMapper.chargeSettlement(sixBeforeValue, siteIds);
+ if (settlementIncome != null) {
+ result.addAll(settlementIncome);
+ }
+ List<SixChargingDto> currentMonthIncome = this.baseMapper.chargeCurrentMonth(siteIds);
+ if (currentMonthIncome != null) {
+ result.addAll(currentMonthIncome);
+ }
+ }
if (result != null) {
for (SixChargingDto item : result) {
if (item == null) {
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java
index b06eb3b..ab0bcc4 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java
@@ -333,6 +333,9 @@
*/
@Override
public AjaxResult addOrderEvaluate(OrderEvaluateVo query) {
+ if (query.getMark() == null || query.getMark() < 1 || query.getMark() > 5) {
+ return AjaxResult.error("评分必须在1到5星之间");
+ }
TOrderEvaluate orderEvaluate = this.getOne(new LambdaQueryWrapper<TOrderEvaluate>().eq(TOrderEvaluate::getOrderId, query.getOrderId())
.eq(TOrderEvaluate::getOrderType, query.getOrderType()).eq(TOrderEvaluate::getDelFlag, 0));
if(null != orderEvaluate){
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
index 8a4c5e1..d369fd1 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
@@ -452,6 +452,15 @@
}
@Override
+ public List<String> invoicingCompanyOptions(TOrderInvoiceQuery query) {
+ if (query == null) {
+ query = new TOrderInvoiceQuery();
+ }
+ applyPartnerSiteScope(query);
+ return this.baseMapper.invoicingCompanyOptions(query);
+ }
+
+ @Override
public Map<String, Integer> statusCount(TOrderInvoiceQuery query) {
applyPartnerSiteScope(query);
Map<String, Integer> map = new HashMap<>();
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationOrderSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationOrderSupport.java
new file mode 100644
index 0000000..194cbb6
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationOrderSupport.java
@@ -0,0 +1,22 @@
+package com.ruoyi.order.support;
+
+import java.time.LocalDateTime;
+
+/** 预约订单等待及到时启动规则。 */
+public final class ReservationOrderSupport {
+
+ private ReservationOrderSupport() {
+ }
+
+ public static boolean isPending(Integer status, LocalDateTime reservationTime, LocalDateTime now) {
+ return Integer.valueOf(2).equals(status)
+ && reservationTime != null
+ && now != null
+ && reservationTime.isAfter(now);
+ }
+
+ public static boolean isStartDue(LocalDateTime reservationTime, LocalDateTime now) {
+ return reservationTime == null
+ || (now != null && !reservationTime.isAfter(now));
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationRecommendationCalculator.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationRecommendationCalculator.java
new file mode 100644
index 0000000..ba461a8
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ReservationRecommendationCalculator.java
@@ -0,0 +1,181 @@
+package com.ruoyi.order.support;
+
+import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
+import com.ruoyi.order.api.model.TChargingOrder;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** 根据历史充电占用情况,从计费模板谷时段中选择预约推荐时段。 */
+public final class ReservationRecommendationCalculator {
+
+ private static final int MINUTES_PER_DAY = 24 * 60;
+
+ private ReservationRecommendationCalculator() {
+ }
+
+ public static Recommendation recommend(List<TAccountingStrategyDetail> details,
+ List<TChargingOrder> orders,
+ LocalDateTime historyStart,
+ LocalDateTime historyEnd) {
+ if (historyStart == null || historyEnd == null || !historyEnd.isAfter(historyStart)) {
+ return null;
+ }
+ long historyDays = ChronoUnit.DAYS.between(historyStart.toLocalDate(), historyEnd.toLocalDate());
+ if (historyDays <= 0) {
+ historyDays = 1;
+ }
+
+ List<Candidate> candidates = new ArrayList<>();
+ for (TAccountingStrategyDetail detail : details == null ? Collections.<TAccountingStrategyDetail>emptyList() : details) {
+ Candidate candidate = Candidate.from(detail);
+ if (candidate == null) {
+ continue;
+ }
+ long occupiedSeconds = occupiedSeconds(candidate,
+ orders == null ? Collections.<TChargingOrder>emptyList() : orders,
+ historyStart,
+ historyEnd);
+ long availableSeconds = candidate.durationMinutes * 60L * historyDays;
+ candidate.utilizationRate = availableSeconds <= 0
+ ? BigDecimal.ZERO
+ : BigDecimal.valueOf(occupiedSeconds)
+ .divide(BigDecimal.valueOf(availableSeconds), 6, RoundingMode.HALF_UP);
+ candidates.add(candidate);
+ }
+ return candidates.stream()
+ .min(Comparator.comparing((Candidate candidate) -> candidate.utilizationRate)
+ .thenComparingInt(candidate -> candidate.startMinute)
+ .thenComparingInt(candidate -> candidate.endMinute))
+ .map(candidate -> new Recommendation(
+ formatMinute(candidate.startMinute),
+ formatMinute(candidate.endMinute == MINUTES_PER_DAY ? 0 : candidate.endMinute),
+ candidate.utilizationRate))
+ .orElse(null);
+ }
+
+ private static long occupiedSeconds(Candidate candidate,
+ List<TChargingOrder> orders,
+ LocalDateTime historyStart,
+ LocalDateTime historyEnd) {
+ long total = 0L;
+ for (TChargingOrder order : orders) {
+ if (order == null || order.getStartTime() == null || order.getEndTime() == null
+ || !order.getEndTime().isAfter(order.getStartTime())) {
+ continue;
+ }
+ LocalDateTime orderStart = max(order.getStartTime(), historyStart);
+ LocalDateTime orderEnd = min(order.getEndTime(), historyEnd);
+ if (!orderEnd.isAfter(orderStart)) {
+ continue;
+ }
+ LocalDate firstSlotDate = orderStart.toLocalDate().minusDays(1);
+ LocalDate lastSlotDate = orderEnd.toLocalDate();
+ for (LocalDate slotDate = firstSlotDate; !slotDate.isAfter(lastSlotDate); slotDate = slotDate.plusDays(1)) {
+ LocalDateTime slotStart = slotDate.atStartOfDay().plusMinutes(candidate.startMinute);
+ LocalDateTime slotEnd = candidate.endMinute > candidate.startMinute
+ ? slotDate.atStartOfDay().plusMinutes(candidate.endMinute)
+ : slotDate.plusDays(1).atStartOfDay().plusMinutes(candidate.endMinute);
+ LocalDateTime overlapStart = max(orderStart, slotStart);
+ LocalDateTime overlapEnd = min(orderEnd, slotEnd);
+ if (overlapEnd.isAfter(overlapStart)) {
+ total += Duration.between(overlapStart, overlapEnd).getSeconds();
+ }
+ }
+ }
+ return total;
+ }
+
+ private static LocalDateTime max(LocalDateTime left, LocalDateTime right) {
+ return left.isAfter(right) ? left : right;
+ }
+
+ private static LocalDateTime min(LocalDateTime left, LocalDateTime right) {
+ return left.isBefore(right) ? left : right;
+ }
+
+ private static String formatMinute(int minuteOfDay) {
+ return String.format("%02d:%02d", minuteOfDay / 60, minuteOfDay % 60);
+ }
+
+ private static Integer parseMinute(String value, boolean allowTwentyFour) {
+ if (value == null) {
+ return null;
+ }
+ String[] parts = value.trim().split(":");
+ if (parts.length < 2) {
+ return null;
+ }
+ try {
+ int hour = Integer.parseInt(parts[0]);
+ int minute = Integer.parseInt(parts[1]);
+ if (allowTwentyFour && hour == 24 && minute == 0) {
+ return MINUTES_PER_DAY;
+ }
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
+ return null;
+ }
+ return hour * 60 + minute;
+ } catch (NumberFormatException exception) {
+ return null;
+ }
+ }
+
+ private static final class Candidate {
+ private final int startMinute;
+ private final int endMinute;
+ private final int durationMinutes;
+ private BigDecimal utilizationRate;
+
+ private Candidate(int startMinute, int endMinute, int durationMinutes) {
+ this.startMinute = startMinute;
+ this.endMinute = endMinute;
+ this.durationMinutes = durationMinutes;
+ }
+
+ private static Candidate from(TAccountingStrategyDetail detail) {
+ if (detail == null || !Integer.valueOf(4).equals(detail.getType())) {
+ return null;
+ }
+ Integer start = parseMinute(detail.getStartTime(), false);
+ Integer end = parseMinute(detail.getEndTime(), true);
+ if (start == null || end == null || start.equals(end)) {
+ return null;
+ }
+ int duration = end > start ? end - start : MINUTES_PER_DAY - start + end;
+ return duration <= 0 ? null : new Candidate(start, end, duration);
+ }
+ }
+
+ public static final class Recommendation {
+ private final String startTime;
+ private final String endTime;
+ private final BigDecimal utilizationRate;
+
+ private Recommendation(String startTime, String endTime, BigDecimal utilizationRate) {
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.utilizationRate = utilizationRate;
+ }
+
+ public String getStartTime() {
+ return startTime;
+ }
+
+ public String getEndTime() {
+ return endTime;
+ }
+
+ public BigDecimal getUtilizationRate() {
+ return utilizationRate;
+ }
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
index 9cf9b3d..5077b98 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
@@ -13,6 +13,7 @@
import com.ruoyi.account.api.model.TAppUserIntegralChange;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
+import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.common.core.domain.R;
@@ -988,29 +989,36 @@
* 添加充电算账单汇总数据
*/
public void addChargingSettlementBill(){
+ int day = LocalDate.now().getDayOfMonth();
+ int lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
+ if (2 <= day && day <= lastDay) {
+ refreshChargingSettlementBill(LocalDate.now());
+ }
+ }
+
+ /**
+ * 主动刷新指定月份的充电结算账单。只重建该月未出账数据,已出账数据不受影响。
+ */
+ public synchronized void refreshChargingSettlementBill(LocalDate billingMonth){
try {
- int day = LocalDate.now().getDayOfMonth();
- int lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
- //时间在当月的2号到当月的最后一天才执行汇总数据和生成新的汇总数据(中间有修改数据的情况)
- if(2 <= day && day <= lastDay){
- // 获取上个月的开始和结束日期
- LocalDate firstDayOfLastMonth = LocalDate.now().minusMonths(1).
- withDayOfMonth(2);
- String string = firstDayOfLastMonth.toString();
+ if (billingMonth == null) {
+ throw new IllegalArgumentException("账单月份不能为空");
+ }
+ LocalDate firstDayOfBillingMonth = billingMonth.withDayOfMonth(2);
+ String string = firstDayOfBillingMonth.toString();
// 将-替换为空字符串
string = string.replace("-", "");
- //上个月
- String lastMonth = LocalDate.now().minusMonths(1).format(DateTimeFormatter.ofPattern("yyyy-MM"));
+ String targetMonth = billingMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
//统计数据的开始时间
- String startTime = lastMonth + "-01 00:00:00";
+ String startTime = targetMonth + "-01 00:00:00";
//统计数据的结束时间
- int lasDay = LocalDate.now().minusMonths(1).with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
- String endTime = lastMonth + "-" + String.format("%02d", lasDay) + " 23:59:59";
- LocalDateTime createTime = LocalDateTime.parse(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")) + "-02 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+ int lastDay = billingMonth.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
+ String endTime = targetMonth + "-" + String.format("%02d", lastDay) + " 23:59:59";
+ LocalDateTime createTime = LocalDateTime.now();
//删除之前生成的汇总数据,在添加最新的汇总数据
- String format = firstDayOfLastMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
+ String format = firstDayOfBillingMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
chargingSettlementBillService.remove(new QueryWrapper<TChargingSettlementBill>().eq("status", 1).last(" and DATE_FORMAT(billing_cycle, '%Y-%m') = '" + format + "'"));
//循环生成按照开始时间和结束时间分别统计的汇总数据
@@ -1023,7 +1031,7 @@
.collect(Collectors.joining()); // 将其连接成一个字符串
chargingSettlementBill.setSiteId(0);
chargingSettlementBill.setStatisticalDateType(i);
- chargingSettlementBill.setBillingCycle(firstDayOfLastMonth);
+ chargingSettlementBill.setBillingCycle(firstDayOfBillingMonth);
chargingSettlementBill.setStatus(1);
chargingSettlementBill.setCode("JSD"+string+randomDigits);
@@ -1058,7 +1066,7 @@
.collect(Collectors.joining()); // 将其连接成一个字符串
chargingSettlementBill1.setSiteId(Integer.valueOf(map.get("siteId").toString()));
chargingSettlementBill1.setStatisticalDateType(i);
- chargingSettlementBill1.setBillingCycle(firstDayOfLastMonth);
+ chargingSettlementBill1.setBillingCycle(firstDayOfBillingMonth);
chargingSettlementBill1.setStatus(1);
chargingSettlementBill1.setCode("JSD"+string+randomDigits1);
chargingSettlementBill1.setChargingDuration(charging_duration);
@@ -1100,9 +1108,9 @@
chargingSettlementBillService.save(chargingSettlementBill);
chargingSettlementBillService.saveBatch(list);
}
- }
} catch (Exception e) {
- e.printStackTrace();
+ log.error("生成{}充电结算账单失败", billingMonth, e);
+ throw new RuntimeException("生成充电结算账单失败", e);
}
}
@@ -1137,8 +1145,9 @@
String format = firstDayOfLastMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
accountSettlementStatementService.remove(new QueryWrapper<TAccountSettlementStatement>().eq("status", 1).last(" and DATE_FORMAT(billing_cycle, '%Y-%m') = '" + format + "'"));
- //循环生成账户类型分别统计的汇总数据
- for (int i = 1; i < 3; i++) {
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
+ // V2.0 账户类型统一为工商银行商户,只生成明星充电小程序订单账单。
+ for (int i = 3; i < 4; i++) {
TAccountSettlementStatement accountSettlementStatement = new TAccountSettlementStatement();
// 订单生成规则JSD+20231201(账单所属月份)+1131304205(随机10位数)+001(当月账单序号,每月重置)
Random random = new Random();
@@ -1158,36 +1167,43 @@
BigDecimal platformFee = BigDecimal.ZERO;
BigDecimal tripartiteCommission = BigDecimal.ZERO;
BigDecimal recordedAmount = BigDecimal.ZERO;
+ BigDecimal platformIncome = BigDecimal.ZERO;
+ BigDecimal partnerIncome = BigDecimal.ZERO;
List<TAccountSettlementStatement> list = new ArrayList<>();
for (Map<String, Object> map : mapList) {
+ Integer siteId = Integer.valueOf(map.get("siteId").toString());
BigDecimal recharge_amount = null == map.get("rechargeAmount") ? BigDecimal.ZERO : new BigDecimal(map.get("rechargeAmount").toString());
BigDecimal refund_amount = null == map.get("refundAmount") ? BigDecimal.ZERO : new BigDecimal(map.get("refundAmount").toString());
- BigDecimal platform_fee = null == map.get("platformFee") ? BigDecimal.ZERO : new BigDecimal(map.get("platformFee").toString());
- BigDecimal tripartite_commission = null == map.get("tripartiteCommission") ? BigDecimal.ZERO : new BigDecimal(map.get("tripartiteCommission").toString());
- BigDecimal recorded_amount = recharge_amount.subtract(refund_amount).subtract(platform_fee).subtract(tripartite_commission);
+ BigDecimal service_amount = null == map.get("serviceAmount") ? BigDecimal.ZERO : new BigDecimal(map.get("serviceAmount").toString());
+ BigDecimal partner_income = calculateAccountPartnerIncome(
+ service_amount, siteServiceShareRatioMap.get(siteId), false);
+ BigDecimal platform_income = calculateAccountPlatformIncome(recharge_amount, partner_income);
+ BigDecimal recorded_amount = recharge_amount.subtract(refund_amount);
//构建每个站点的汇总数据
TAccountSettlementStatement accountSettlementStatement1 = new TAccountSettlementStatement();
String randomDigits1 = random.ints(10, 0, 10) // 生成10个随机数字,范围在0-9
.mapToObj(String::valueOf)
.collect(Collectors.joining()); // 将其连接成一个字符串
- accountSettlementStatement1.setSiteId(Integer.valueOf(map.get("siteId").toString()));
+ accountSettlementStatement1.setSiteId(siteId);
accountSettlementStatement1.setAccountType(i);
accountSettlementStatement1.setBillingCycle(firstDayOfLastMonth);
accountSettlementStatement1.setStatus(1);
accountSettlementStatement1.setCode("JSD"+string+randomDigits1);
accountSettlementStatement1.setRechargeAmount(recharge_amount.setScale(2, RoundingMode.HALF_EVEN));
accountSettlementStatement1.setRefundAmount(refund_amount.setScale(2, RoundingMode.HALF_EVEN));
- accountSettlementStatement1.setPlatformFee(platform_fee.setScale(2, RoundingMode.HALF_EVEN));
- accountSettlementStatement1.setTripartiteCommission(tripartite_commission.setScale(2, RoundingMode.HALF_EVEN));
+ accountSettlementStatement1.setPlatformFee(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_EVEN));
+ accountSettlementStatement1.setTripartiteCommission(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_EVEN));
accountSettlementStatement1.setRecordedAmount(recorded_amount.setScale(2, RoundingMode.HALF_EVEN));
+ accountSettlementStatement1.setPlatformIncome(platform_income.setScale(2, RoundingMode.HALF_UP));
+ accountSettlementStatement1.setPartnerIncome(partner_income.setScale(2, RoundingMode.HALF_UP));
accountSettlementStatement1.setCreateTime(createTime);
list.add(accountSettlementStatement1);
//计算全站汇总数据
rechargeAmount = rechargeAmount.add(recharge_amount);
refundAmount = refundAmount.add(refund_amount);
- platformFee = platformFee.add(platform_fee);
- tripartiteCommission = tripartiteCommission.add(tripartite_commission);
recordedAmount = recordedAmount.add(recorded_amount);
+ platformIncome = platformIncome.add(platform_income);
+ partnerIncome = partnerIncome.add(partner_income);
}
accountSettlementStatement.setRechargeAmount(rechargeAmount.setScale(2, RoundingMode.HALF_EVEN));
@@ -1195,6 +1211,8 @@
accountSettlementStatement.setPlatformFee(platformFee.setScale(2, RoundingMode.HALF_EVEN));
accountSettlementStatement.setTripartiteCommission(tripartiteCommission.setScale(2, RoundingMode.HALF_EVEN));
accountSettlementStatement.setRecordedAmount(recordedAmount.setScale(2, RoundingMode.HALF_EVEN));
+ accountSettlementStatement.setPlatformIncome(platformIncome.setScale(2, RoundingMode.HALF_UP));
+ accountSettlementStatement.setPartnerIncome(partnerIncome.setScale(2, RoundingMode.HALF_UP));
accountSettlementStatement.setCreateTime(createTime);
accountSettlementStatementService.save(accountSettlementStatement);
accountSettlementStatementService.saveBatch(list);
@@ -1205,6 +1223,43 @@
}
}
+ static BigDecimal calculateAccountPartnerIncome(BigDecimal paidServiceAmount, BigDecimal serviceShareRatio,
+ boolean thirdPartyOrder) {
+ if (thirdPartyOrder || paidServiceAmount == null || serviceShareRatio == null
+ || serviceShareRatio.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO;
+ }
+ return paidServiceAmount.multiply(serviceShareRatio)
+ .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+ }
+
+ static BigDecimal calculateAccountPlatformIncome(BigDecimal paymentAmount, BigDecimal partnerIncome) {
+ BigDecimal paid = paymentAmount == null ? BigDecimal.ZERO : paymentAmount;
+ BigDecimal partner = partnerIncome == null ? BigDecimal.ZERO : partnerIncome;
+ return paid.subtract(partner).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private Map<Integer, BigDecimal> loadSiteServiceShareRatioMap() {
+ List<Site> sites = Optional.ofNullable(siteClient.getSiteAll())
+ .map(R::getData).orElse(Collections.emptyList());
+ List<Partner> partners = Optional.ofNullable(siteClient.getAllPartner())
+ .map(R::getData).orElse(Collections.emptyList());
+ Map<Integer, Partner> partnerMap = partners.stream()
+ .filter(partner -> partner != null && partner.getId() != null)
+ .collect(Collectors.toMap(Partner::getId, partner -> partner, (left, right) -> left));
+ Map<Integer, BigDecimal> ratioMap = new HashMap<>();
+ for (Site site : sites) {
+ if (site == null || site.getId() == null || site.getPartnerId() == null) {
+ continue;
+ }
+ Partner partner = partnerMap.get(site.getPartnerId());
+ if (partner != null && partner.getServiceShareRatio() != null) {
+ ratioMap.put(site.getId(), partner.getServiceShareRatio());
+ }
+ }
+ return ratioMap;
+ }
+
/**
* 推送三方引流平台充电数据
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ReservationRecommendationVO.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ReservationRecommendationVO.java
new file mode 100644
index 0000000..0607386
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ReservationRecommendationVO.java
@@ -0,0 +1,23 @@
+package com.ruoyi.order.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@ApiModel("预约充电推荐时段")
+public class ReservationRecommendationVO {
+
+ @ApiModelProperty("推荐开始时间,格式HH:mm")
+ private String startTime;
+
+ @ApiModelProperty("推荐结束时间,格式HH:mm")
+ private String endTime;
+
+ @ApiModelProperty("近30天时段利用率,0到1")
+ private BigDecimal utilizationRate;
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TAccountSettlementStatementMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TAccountSettlementStatementMapper.xml
index a03ea0c..4c1dc9a 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TAccountSettlementStatementMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TAccountSettlementStatementMapper.xml
@@ -13,9 +13,7 @@
<if test="req.state != null">
AND status = #{req.state}
</if>
- <if test="req.payType != null and req.payType != 0">
- AND account_type = #{req.payType}
- </if>
+ AND account_type = 3
<if test="req.uid != null and req.uid != ''">
AND id = #{req.uid}
</if>
@@ -35,7 +33,8 @@
<select id="selectStatementPage" resultType="com.ruoyi.order.api.model.TAccountSettlementStatement">
SELECT id, site_id, billing_cycle, status, code, account_type,
recharge_amount, refund_amount, platform_fee,
- tripartite_commission, recorded_amount, create_time
+ tripartite_commission, recorded_amount,
+ platform_income, partner_income, create_time
FROM t_account_settlement_statement
<include refid="statementWhere"/>
ORDER BY billing_cycle DESC, account_type ASC, site_id ASC
@@ -48,8 +47,8 @@
COALESCE(SUM(refund_amount), 0) AS refundAmount,
COALESCE(SUM(platform_fee), 0) AS commissionAmount,
COALESCE(SUM(tripartite_commission), 0) AS sharingAmount,
- 0 AS platformIncome,
- 0 AS partnerIncome
+ COALESCE(SUM(platform_income), 0) AS platformIncome,
+ COALESCE(SUM(partner_income), 0) AS partnerIncome
FROM t_account_settlement_statement
<include refid="statementWhere"/>
</select>
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
index 85ffec4..7f24b82 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -410,76 +410,67 @@
</select>
- <select id="charge" resultType="com.ruoyi.order.dto.SixChargingDto">
- <choose>
- <when test="partnerView != null and partnerView">
- SELECT EXTRACT(MONTH FROM create_time) AS MONTH,
- SUM(IFNULL(electricity_share_amount, 0)) AS electrovalence,
- SUM(IFNULL(service_share_amount, 0)) AS service_charge
- FROM t_charging_order_share_record
- WHERE del_flag = 0 AND online_share_status IN ('SUCCESS', 'RECORDED')
- <if test="sixBefore != null">
- AND create_time >= #{sixBefore}
- </if>
- <if test="siteIds != null and siteIds.size() > 0">
- AND site_id IN
- <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
- #{siteId}
- </foreach>
- </if>
- GROUP BY EXTRACT(MONTH FROM create_time)
- </when>
- <otherwise>
- SELECT income.MONTH,
- SUM(income.electrovalence) AS electrovalence,
- SUM(income.service_charge) AS service_charge
- FROM (
- SELECT EXTRACT(MONTH FROM billing_cycle) AS MONTH,
- SUM(IFNULL(total_electricity_charge, 0)) AS electrovalence,
- SUM(IFNULL(total_service_fee, 0) - IFNULL(tripartite_commission, 0)) AS service_charge
- FROM t_charging_settlement_bill
- WHERE statistical_date_type = 1
- AND site_id != 0
- AND billing_cycle < DATE_FORMAT(CURDATE(), '%Y-%m-01')
- <if test="sixBefore != null">
- AND billing_cycle >= #{sixBefore}
- </if>
- <if test="siteIds != null and siteIds.size() > 0">
- AND site_id IN
- <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
- #{siteId}
- </foreach>
- </if>
- GROUP BY EXTRACT(MONTH FROM billing_cycle)
+ <select id="chargePartner" resultType="com.ruoyi.order.dto.SixChargingDto">
+ SELECT EXTRACT(MONTH FROM create_time) AS MONTH,
+ SUM(IFNULL(electricity_share_amount, 0)) AS electrovalence,
+ SUM(IFNULL(service_share_amount, 0)) AS service_charge
+ FROM t_charging_order_share_record
+ WHERE del_flag = 0 AND online_share_status IN ('SUCCESS', 'RECORDED')
+ <if test="sixBefore != null and sixBefore != ''">
+ AND create_time >= #{sixBefore}
+ </if>
+ <if test="siteIds != null and siteIds.size() > 0">
+ AND site_id IN
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ GROUP BY EXTRACT(MONTH FROM create_time)
+ </select>
- UNION ALL
+ <select id="chargeSettlement" resultType="com.ruoyi.order.dto.SixChargingDto">
+ SELECT EXTRACT(MONTH FROM billing_cycle) AS MONTH,
+ SUM(IFNULL(total_electricity_charge, 0)) AS electrovalence,
+ SUM(IFNULL(total_service_fee, 0) - IFNULL(tripartite_commission, 0)) AS service_charge
+ FROM t_charging_settlement_bill
+ WHERE statistical_date_type = 1
+ AND site_id != 0
+ AND billing_cycle < DATE_FORMAT(CURDATE(), '%Y-%m-01')
+ <if test="sixBefore != null and sixBefore != ''">
+ AND billing_cycle >= #{sixBefore}
+ </if>
+ <if test="siteIds != null and siteIds.size() > 0">
+ AND site_id IN
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ GROUP BY EXTRACT(MONTH FROM billing_cycle)
+ </select>
- SELECT EXTRACT(MONTH FROM charging_order.create_time) AS MONTH,
- SUM(IFNULL(accounting.period_electric_price, 0)) AS electrovalence,
- SUM(CASE
- WHEN charging_order.order_source = 2
- THEN IFNULL(accounting.period_service_price, 0) * 0.8
- ELSE IFNULL(accounting.period_service_price, 0)
- END) AS service_charge
- FROM t_charging_order_accounting_strategy accounting
- INNER JOIN t_charging_order charging_order
- ON accounting.charging_order_id = charging_order.id
- WHERE charging_order.del_flag = 0
- AND charging_order.status = 5
- AND charging_order.recharge_payment_status = 2
- AND charging_order.create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
- AND charging_order.create_time <= NOW()
- <if test="siteIds != null and siteIds.size() > 0">
- AND charging_order.site_id IN
- <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
- #{siteId}
- </foreach>
- </if>
- GROUP BY EXTRACT(MONTH FROM charging_order.create_time)
- ) income
- GROUP BY income.MONTH
- </otherwise>
- </choose>
+ <select id="chargeCurrentMonth" resultType="com.ruoyi.order.dto.SixChargingDto">
+ SELECT EXTRACT(MONTH FROM charging_order.create_time) AS MONTH,
+ SUM(IFNULL(accounting.period_electric_price, 0)) AS electrovalence,
+ SUM(CASE
+ WHEN charging_order.order_source = 2
+ THEN IFNULL(accounting.period_service_price, 0) * 0.8
+ ELSE IFNULL(accounting.period_service_price, 0)
+ END) AS service_charge
+ FROM t_charging_order_accounting_strategy accounting
+ INNER JOIN t_charging_order charging_order
+ ON accounting.charging_order_id = charging_order.id
+ WHERE charging_order.del_flag = 0
+ AND charging_order.status = 5
+ AND charging_order.recharge_payment_status = 2
+ AND charging_order.create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
+ AND charging_order.create_time <= NOW()
+ <if test="siteIds != null and siteIds.size() > 0">
+ AND charging_order.site_id IN
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ GROUP BY EXTRACT(MONTH FROM charging_order.create_time)
</select>
<select id="circle" resultType="com.ruoyi.order.dto.SixCircleDto">
@@ -2616,34 +2607,37 @@
<select id="getAccountSettlementStatement" resultType="map">
select
aa.site_id as siteId,
- aa.recharge_payment_type as rechargePaymentType,
sum(aa.recharge_amount) as rechargeAmount,
sum(aa.refund_amount) as refundAmount,
- sum(aa.platformFee) as platformFee,
- sum(aa.tripartiteCommission) as tripartiteCommission
+ sum(aa.serviceAmount) as serviceAmount
from (
select
b.id as orderId,
- b.recharge_payment_type,
b.site_id,
- b.recharge_amount,
+ ifnull(b.payment_amount, 0) as recharge_amount,
ifnull(c.refund_amount, 0) as refund_amount,
- (CASE WHEN b.order_source != 2 THEN b.recharge_amount * 0.006 ELSE 0 END) as platformFee,
- sum((CASE WHEN b.order_source = 2 THEN a.period_service_price * 0.2 ELSE 0 END)) as tripartiteCommission
+ sum(ifnull(a.period_service_price, 0)) as serviceAmount
from t_charging_order_accounting_strategy a
left join t_charging_order b on (a.charging_order_id = b.id)
- left join t_charging_order_refund c on (b.id = c.charging_order_id)
- where b.`status` = 5 and b.del_flag = 0 and b.recharge_payment_type = #{rechargePaymentType} and b.start_time between #{startTime} and #{endTime}
- group by orderId,recharge_payment_type,site_id,recharge_amount,refund_amount,platformFee
- ) as aa group by aa.site_id,aa.recharge_payment_type order by aa.site_id
+ left join (
+ select charging_order_id, sum(ifnull(refund_amount, 0)) as refund_amount
+ from t_charging_order_refund
+ where refund_status = 2
+ group by charging_order_id
+ ) c on b.id = c.charging_order_id
+ where b.`status` = 5
+ and b.del_flag = 0
+ and b.order_source = 1
+ and b.source_name = 'ICBC'
+ and b.start_time between #{startTime} and #{endTime}
+ group by orderId, site_id, recharge_amount, refund_amount
+ ) as aa group by aa.site_id order by aa.site_id
</select>
<sql id="enterpriseChargingBaseWhere">
toi.invoicing_object_type = 2
AND toi.status IN (1, 2, 3)
AND tod.order_type = 1
- AND co.del_flag = 0
- AND co.recharge_payment_status = 2
<if test="data.exactCompanyName != null and data.exactCompanyName != ''">
AND toi.name = #{data.exactCompanyName}
</if>
@@ -2693,7 +2687,7 @@
MAX(co.start_time) AS latestChargingTime
FROM t_order_invoice toi
INNER JOIN t_order_invoice_detail tod ON tod.order_invoice_id = toi.id
- INNER JOIN t_charging_order co ON co.id = tod.order_id
+ LEFT JOIN t_charging_order co ON co.id = tod.order_id
LEFT JOIN charging_pile_service.t_site s ON s.id = co.site_id
<where>
<include refid="enterpriseChargingBaseWhere" />
@@ -2872,6 +2866,43 @@
ORDER BY co.create_time DESC
</select>
+ <select id="queryChargingBillIncome" resultType="map">
+ select
+ income.site_id,
+ income.billingCycle,
+ sum(income.payment_amount) as payment_amount,
+ sum(income.commissionable_service_fee) as commissionable_service_fee
+ from (
+ select
+ b.id as order_id,
+ b.site_id,
+ DATE_FORMAT(
+ CASE WHEN #{dateType} = 2 THEN b.end_time ELSE b.start_time END,
+ '%Y-%m'
+ ) as billingCycle,
+ ifnull(b.payment_amount, 0) as payment_amount,
+ sum(CASE WHEN b.order_source = 1 THEN ifnull(a.period_service_price, 0) ELSE 0 END)
+ as commissionable_service_fee
+ from `charging_pile_order`.t_charging_order_accounting_strategy a
+ inner join `charging_pile_order`.t_charging_order b on a.charging_order_id = b.id
+ where b.`status` = 5
+ and b.del_flag = 0
+ and DATE_FORMAT(
+ CASE WHEN #{dateType} = 2 THEN b.end_time ELSE b.start_time END,
+ '%Y-%m'
+ ) in
+ <foreach collection="billingCycles" item="item" open="(" separator="," close=")">
+ #{item}
+ </foreach>
+ and b.site_id in
+ <foreach collection="siteIds" item="item" open="(" separator="," close=")">
+ #{item}
+ </foreach>
+ group by b.id, b.site_id, billingCycle, b.payment_amount
+ ) income
+ group by income.site_id, income.billingCycle
+ </select>
+
<select id="getLargeScreenSummary" resultType="java.util.Map">
SELECT COUNT(DISTINCT charging_order.id) AS validOrderCount,
COUNT(DISTINCT charging_order.app_user_id) AS chargingUserCount,
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
index f79e6d6..9de73d5 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
@@ -97,6 +97,26 @@
</where>
ORDER BY toi.create_time DESC
</select>
+ <select id="invoicingCompanyOptions" resultType="java.lang.String">
+ SELECT DISTINCT TRIM(toi.invoicing_company)
+ FROM t_order_invoice toi
+ <where>
+ toi.invoicing_company IS NOT NULL
+ AND TRIM(toi.invoicing_company) != ''
+ <if test="query.siteIds != null and query.siteIds.size() > 0">
+ AND toi.site_id IN
+ <foreach collection="query.siteIds" close=")" open="(" item="siteId" separator=",">
+ #{siteId}
+ </foreach>
+ </if>
+ <if test="query.directOnly != null and query.directOnly == 1">
+ AND toi.site_id IN (
+ SELECT id FROM charging_pile_service.t_site WHERE business_category = 1
+ )
+ </if>
+ </where>
+ ORDER BY TRIM(toi.invoicing_company)
+ </select>
<select id="statusCount" resultType="java.util.Map">
select
COUNT(CASE WHEN status = 1 THEN 1 END) AS `notAccepted`,
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
new file mode 100644
index 0000000..768f0e5
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
@@ -0,0 +1,22 @@
+package com.ruoyi.order.controller;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class TVipOrderControllerTest {
+
+ @Test
+ void formatExportDateTimeReturnsEmptyStringForMissingOrderTime() {
+ assertEquals("", TVipOrderController.formatExportDateTime(null));
+ }
+
+ @Test
+ void formatExportDateTimeFormatsPresentOrderTime() {
+ LocalDateTime time = LocalDateTime.of(2026, 7, 28, 9, 8, 7);
+
+ assertEquals("2026-07-28 09:08:07", TVipOrderController.formatExportDateTime(time));
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
new file mode 100644
index 0000000..df5d7d8
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
@@ -0,0 +1,42 @@
+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 EnterpriseChargingAnalysisMapperTest {
+
+ @Test
+ void countsInvoicesWithoutRequiringTheHistoricalChargingOrderToStillExist() 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 baseWhere = mapper.substring(
+ mapper.indexOf("<sql id=\"enterpriseChargingBaseWhere\""),
+ mapper.indexOf("<select id=\"enterpriseChargingAnalysis\""));
+ String analysis = mapper.substring(
+ mapper.indexOf("<select id=\"enterpriseChargingAnalysis\""),
+ mapper.indexOf("<select id=\"enterpriseTopSites\""));
+
+ assertFalse(baseWhere.contains("co.del_flag"));
+ assertFalse(baseWhere.contains("co.recharge_payment_status"));
+ assertTrue(analysis.contains("LEFT JOIN t_charging_order co ON co.id = tod.order_id"));
+ assertTrue(analysis.contains("COUNT(1) AS invoiceCount"));
+ assertTrue(analysis.contains("detailInvoiceAmount"));
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingBillIncomeIndexTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingBillIncomeIndexTest.java
index 9ba9fd4..5344946 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingBillIncomeIndexTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingBillIncomeIndexTest.java
@@ -20,28 +20,42 @@
Map<String, Map<String, BigDecimal>> incomeIndex = service.buildChargingBillIncomeIndex(
Arrays.asList(
- settlementRow(1, "2026-06", "6.12", "1.85", "0.00"),
- settlementRow(2, "2026-06", "1.00", "1.00", "0.20")
+ incomeRow(1, "2026-06", "7.97", "1.85"),
+ incomeRow(2, "2026-06", "2.00", "0.00")
),
ratios
);
assertEquals(new BigDecimal("7.69"), incomeIndex.get("1|2026-06").get("platformIncome"));
assertEquals(new BigDecimal("0.28"), incomeIndex.get("1|2026-06").get("partnerIncome"));
- assertEquals(new BigDecimal("1.80"), incomeIndex.get("2|2026-06").get("platformIncome"));
+ assertEquals(new BigDecimal("2.00"), incomeIndex.get("2|2026-06").get("platformIncome"));
assertEquals(new BigDecimal("0.00"), incomeIndex.get("2|2026-06").get("partnerIncome"));
- assertEquals(new BigDecimal("9.49"), incomeIndex.get("0|2026-06").get("platformIncome"));
+ assertEquals(new BigDecimal("9.69"), incomeIndex.get("0|2026-06").get("platformIncome"));
assertEquals(new BigDecimal("0.28"), incomeIndex.get("0|2026-06").get("partnerIncome"));
}
- private Map<String, Object> settlementRow(Integer siteId, String billingCycle, String electricAmount,
- String serviceAmount, String tripartiteCommission) {
+ @Test
+ void givesAllPaidAmountToPlatformWhenSharingIsDisabled() {
+ TChargingBillServiceImpl service = new TChargingBillServiceImpl();
+ Map<Integer, BigDecimal> ratios = new HashMap<>();
+ ratios.put(1, BigDecimal.ZERO);
+
+ Map<String, Map<String, BigDecimal>> incomeIndex = service.buildChargingBillIncomeIndex(
+ Arrays.asList(incomeRow(1, "2026-07", "12.34", "2.00")),
+ ratios
+ );
+
+ assertEquals(new BigDecimal("12.34"), incomeIndex.get("1|2026-07").get("platformIncome"));
+ assertEquals(new BigDecimal("0"), incomeIndex.get("1|2026-07").get("partnerIncome"));
+ }
+
+ private Map<String, Object> incomeRow(Integer siteId, String billingCycle, String paymentAmount,
+ String commissionableServiceFee) {
Map<String, Object> row = new HashMap<>();
row.put("site_id", siteId);
row.put("billingCycle", billingCycle);
- row.put("total_electricity_charge", new BigDecimal(electricAmount));
- row.put("total_service_fee", new BigDecimal(serviceAmount));
- row.put("tripartite_commission", new BigDecimal(tripartiteCommission));
+ row.put("payment_amount", new BigDecimal(paymentAmount));
+ row.put("commissionable_service_fee", new BigDecimal(commissionableServiceFee));
return row;
}
}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/InvoicePayOrderQueryTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/InvoicePayOrderQueryTest.java
new file mode 100644
index 0000000..9575a27
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/InvoicePayOrderQueryTest.java
@@ -0,0 +1,55 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.order.dto.PayOrderDto;
+import com.ruoyi.order.dto.PayOrderQueryDto;
+import com.ruoyi.order.mapper.TChargingOrderMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class InvoicePayOrderQueryTest {
+ private TChargingOrderServiceImpl service;
+ private TChargingOrderMapper mapper;
+
+ @BeforeEach
+ void setUp() {
+ service = new TChargingOrderServiceImpl();
+ mapper = mock(TChargingOrderMapper.class);
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ }
+
+ @Test
+ void invoiceWithoutOrderDetailsReturnsEmptyInsteadOfAllOrders() {
+ PayOrderQueryDto query = new PayOrderQueryDto();
+ query.setInvoiceId(1001L);
+ query.setIds(Collections.emptyList());
+
+ R<PageInfo<PayOrderDto>> result = service.payOrderQuery(query);
+
+ assertNotNull(result.getData());
+ assertTrue(result.getData().getRecords().isEmpty());
+ verify(mapper, never()).payOrderQuery(any(), any());
+ }
+
+ @Test
+ void ordinaryPayOrderQueryStillUsesMapperWithoutInvoiceId() {
+ PayOrderQueryDto query = new PayOrderQueryDto();
+ when(mapper.payOrderQuery(any(), any())).thenReturn(Collections.emptyList());
+
+ service.payOrderQuery(query);
+
+ verify(mapper).payOrderQuery(any(), any());
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/PlatformRevenueAnalysisTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/PlatformRevenueAnalysisTest.java
index a8851a2..2633fda 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/PlatformRevenueAnalysisTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/PlatformRevenueAnalysisTest.java
@@ -66,18 +66,40 @@
}
@Test
- void chargeRoundsMapperTotalsInJavaForMycatCompatibility() {
- SixChargingDto item = new SixChargingDto();
- item.setMonth("7");
- item.setElectrovalence(new BigDecimal("1.234"));
- item.setServiceCharge(new BigDecimal("2.345"));
- when(mapper.charge(LocalDate.of(2026, 1, 1), siteIds, false)).thenReturn(Arrays.asList(item));
+ void chargeSplitsSettlementAndCurrentMonthQueriesForMycatCompatibility() {
+ SixChargingDto settlement = new SixChargingDto();
+ settlement.setMonth("6");
+ settlement.setElectrovalence(new BigDecimal("1.234"));
+ settlement.setServiceCharge(new BigDecimal("2.345"));
+ SixChargingDto currentMonth = new SixChargingDto();
+ currentMonth.setMonth("7");
+ currentMonth.setElectrovalence(new BigDecimal("3.456"));
+ currentMonth.setServiceCharge(new BigDecimal("4.567"));
+ when(mapper.chargeSettlement("2026-01-01", siteIds)).thenReturn(Arrays.asList(settlement));
+ when(mapper.chargeCurrentMonth(siteIds)).thenReturn(Arrays.asList(currentMonth));
List<SixChargingDto> result = service.charge(LocalDate.of(2026, 1, 1), siteIds, false);
- assertEquals("07", result.get(0).getMonth());
+ assertEquals(2, result.size());
+ assertEquals("06", result.get(0).getMonth());
assertEquals(new BigDecimal("1.23"), result.get(0).getElectrovalence());
assertEquals(new BigDecimal("2.35"), result.get(0).getServiceCharge());
+ assertEquals("07", result.get(1).getMonth());
+ assertEquals(new BigDecimal("3.46"), result.get(1).getElectrovalence());
+ assertEquals(new BigDecimal("4.57"), result.get(1).getServiceCharge());
+ }
+
+ @Test
+ void chargePartnerUsesOnlyShareRecordQuery() {
+ SixChargingDto item = new SixChargingDto();
+ item.setMonth("7");
+ when(mapper.chargePartner("2026-01-01", siteIds)).thenReturn(Arrays.asList(item));
+
+ List<SixChargingDto> result = service.charge(LocalDate.of(2026, 1, 1), siteIds, true);
+
+ assertEquals("07", result.get(0).getMonth());
+ verify(mapper, never()).chargeSettlement("2026-01-01", siteIds);
+ verify(mapper, never()).chargeCurrentMonth(siteIds);
}
@Test
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationOrderSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationOrderSupportTest.java
new file mode 100644
index 0000000..1d4c5a8
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationOrderSupportTest.java
@@ -0,0 +1,35 @@
+package com.ruoyi.order.support;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ReservationOrderSupportTest {
+
+ private static final LocalDateTime NOW = LocalDateTime.of(2026, 7, 28, 12, 0);
+
+ @Test
+ void futurePaidReservationRemainsPendingAndCannotStart() {
+ LocalDateTime reservationTime = NOW.plusMinutes(30);
+
+ assertTrue(ReservationOrderSupport.isPending(2, reservationTime, NOW));
+ assertFalse(ReservationOrderSupport.isStartDue(reservationTime, NOW));
+ }
+
+ @Test
+ void reservationCanStartAtOrAfterSelectedTime() {
+ assertTrue(ReservationOrderSupport.isStartDue(NOW, NOW));
+ assertTrue(ReservationOrderSupport.isStartDue(NOW.minusSeconds(1), NOW));
+ assertFalse(ReservationOrderSupport.isPending(2, NOW, NOW));
+ }
+
+ @Test
+ void ordinaryOrderCanStartImmediatelyAndOtherStatusesAreNotPending() {
+ assertTrue(ReservationOrderSupport.isStartDue(null, NOW));
+ assertFalse(ReservationOrderSupport.isPending(3, NOW.plusMinutes(30), NOW));
+ assertFalse(ReservationOrderSupport.isPending(null, NOW.plusMinutes(30), NOW));
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationRecommendationCalculatorTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationRecommendationCalculatorTest.java
new file mode 100644
index 0000000..631f12d
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ReservationRecommendationCalculatorTest.java
@@ -0,0 +1,115 @@
+package com.ruoyi.order.support;
+
+import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
+import com.ruoyi.order.api.model.TChargingOrder;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class ReservationRecommendationCalculatorTest {
+
+ private static final LocalDateTime START = LocalDateTime.of(2026, 7, 1, 0, 0);
+ private static final LocalDateTime END = LocalDateTime.of(2026, 7, 3, 0, 0);
+
+ @Test
+ void selectsOnlyValleyPeriodWithLowestUtilization() {
+ ReservationRecommendationCalculator.Recommendation result = ReservationRecommendationCalculator.recommend(
+ Arrays.asList(
+ period(4, "00:00", "03:00"),
+ period(4, "12:00", "18:00"),
+ period(2, "06:00", "09:00")
+ ),
+ Collections.singletonList(order("2026-07-01T12:00:00", "2026-07-01T15:00:00")),
+ START,
+ END
+ );
+
+ assertEquals("00:00", result.getStartTime());
+ assertEquals("03:00", result.getEndTime());
+ }
+
+ @Test
+ void normalizesOccupiedTimeByCandidateDuration() {
+ ReservationRecommendationCalculator.Recommendation result = ReservationRecommendationCalculator.recommend(
+ Arrays.asList(period(4, "00:00", "06:00"), period(4, "10:00", "11:00")),
+ Arrays.asList(
+ order("2026-07-01T00:00:00", "2026-07-01T01:00:00"),
+ order("2026-07-01T10:00:00", "2026-07-01T10:30:00")
+ ),
+ START,
+ END
+ );
+
+ assertEquals("00:00", result.getStartTime());
+ assertEquals("06:00", result.getEndTime());
+ }
+
+ @Test
+ void supportsCrossMidnightPeriodsAndTwentyFourHourEndBoundary() {
+ ReservationRecommendationCalculator.Recommendation crossMidnight = ReservationRecommendationCalculator.recommend(
+ Arrays.asList(period(4, "23:00", "07:00"), period(4, "12:00", "13:00")),
+ Arrays.asList(
+ order("2026-07-02T01:00:00", "2026-07-02T02:00:00"),
+ order("2026-07-01T12:00:00", "2026-07-01T13:00:00")
+ ),
+ START,
+ END
+ );
+ ReservationRecommendationCalculator.Recommendation twentyFourHour = ReservationRecommendationCalculator.recommend(
+ Arrays.asList(period(4, "22:00", "24:00"), period(4, "00:00", "01:00")),
+ Collections.singletonList(order("2026-07-01T22:00:00", "2026-07-02T00:00:00")),
+ START,
+ END
+ );
+ ReservationRecommendationCalculator.Recommendation normalizedEnd = ReservationRecommendationCalculator.recommend(
+ Collections.singletonList(period(4, "22:00", "24:00")),
+ Collections.emptyList(),
+ START,
+ END
+ );
+
+ assertEquals("23:00", crossMidnight.getStartTime());
+ assertEquals("07:00", crossMidnight.getEndTime());
+ assertEquals("00:00", twentyFourHour.getStartTime());
+ assertEquals("01:00", twentyFourHour.getEndTime());
+ assertEquals("00:00", normalizedEnd.getEndTime());
+ }
+
+ @Test
+ void usesEarliestStartAsTieBreakerAndReturnsNullWithoutValleyPeriods() {
+ ReservationRecommendationCalculator.Recommendation tied = ReservationRecommendationCalculator.recommend(
+ Arrays.asList(period(4, "05:00", "06:00"), period(4, "01:00", "02:00")),
+ Collections.emptyList(),
+ START,
+ END
+ );
+
+ assertEquals("01:00", tied.getStartTime());
+ assertNull(ReservationRecommendationCalculator.recommend(
+ Collections.singletonList(period(2, "01:00", "02:00")),
+ Collections.emptyList(),
+ START,
+ END
+ ));
+ }
+
+ private TAccountingStrategyDetail period(int type, String startTime, String endTime) {
+ TAccountingStrategyDetail detail = new TAccountingStrategyDetail();
+ detail.setType(type);
+ detail.setStartTime(startTime);
+ detail.setEndTime(endTime);
+ return detail;
+ }
+
+ private TChargingOrder order(String startTime, String endTime) {
+ TChargingOrder order = new TChargingOrder();
+ order.setStartTime(LocalDateTime.parse(startTime));
+ order.setEndTime(LocalDateTime.parse(endTime));
+ return order;
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java
new file mode 100644
index 0000000..24424a4
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java
@@ -0,0 +1,40 @@
+package com.ruoyi.order.util.task;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class AccountSettlementIncomeTest {
+
+ @Test
+ void calculatesPartnerAndPlatformIncomeForMiniProgramOrder() {
+ BigDecimal partnerIncome = TaskUtil.calculateAccountPartnerIncome(
+ new BigDecimal("10.00"), new BigDecimal("30"), false);
+
+ assertEquals(new BigDecimal("3.00"), partnerIncome);
+ assertEquals(new BigDecimal("17.00"), TaskUtil.calculateAccountPlatformIncome(
+ new BigDecimal("20.00"), partnerIncome));
+ }
+
+ @Test
+ void givesAllPaidAmountToPlatformWhenSharingIsDisabled() {
+ BigDecimal partnerIncome = TaskUtil.calculateAccountPartnerIncome(
+ new BigDecimal("10.00"), BigDecimal.ZERO, false);
+
+ assertEquals(BigDecimal.ZERO, partnerIncome);
+ assertEquals(new BigDecimal("20.00"), TaskUtil.calculateAccountPlatformIncome(
+ new BigDecimal("20.00"), partnerIncome));
+ }
+
+ @Test
+ void givesAllPaidAmountToPlatformForThirdPartyOrder() {
+ BigDecimal partnerIncome = TaskUtil.calculateAccountPartnerIncome(
+ new BigDecimal("10.00"), new BigDecimal("30"), true);
+
+ assertEquals(BigDecimal.ZERO, partnerIncome);
+ assertEquals(new BigDecimal("20.00"), TaskUtil.calculateAccountPlatformIncome(
+ new BigDecimal("20.00"), partnerIncome));
+ }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/dto/EmindIncomeHistoryDto.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/dto/EmindIncomeHistoryDto.java
index 2bcd0c5..dce5abd 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/dto/EmindIncomeHistoryDto.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/dto/EmindIncomeHistoryDto.java
@@ -9,7 +9,8 @@
public class EmindIncomeHistoryDto {
private Long stationId;
private LocalDate date;
- private BigDecimal price;
+ // 厂商可能返回单一数值,也可能返回包含尖峰平谷价格的 JSON 字符串。
+ private Object price;
private String priceName;
@JSONField(name = "sum_income")
private BigDecimal income;
@@ -33,4 +34,23 @@
private BigDecimal dischargedEnergyFlat;
@JSONField(name = "sum_e_reverse_valley")
private BigDecimal dischargedEnergyValley;
+
+ public BigDecimal getPrice() {
+ if (price == null || price instanceof java.util.Map) {
+ return null;
+ }
+ String value = String.valueOf(price).trim();
+ if (value.isEmpty() || value.startsWith("{")) {
+ return null;
+ }
+ try {
+ return new BigDecimal(value);
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ public void setPrice(Object price) {
+ this.price = price;
+ }
}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationSyncScheduler.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationSyncScheduler.java
index 1a3cbe6..4756cf8 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationSyncScheduler.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationSyncScheduler.java
@@ -26,17 +26,23 @@
@Scheduled(cron = "${emind.station-sync-cron:0 0/30 * * * ?}")
public void sync() {
+ runSyncStep("eMind电站", syncService::syncStations);
+ runSyncStep("eMind储能看板", dashboardSyncService::syncDashboardData);
+ runSyncStep("eMind储能能量链", energyChainSyncService::syncEnergyChains);
+ runSyncStep("eMind储能能量块", energyBlockSyncService::syncEnergyBlocks);
+ }
+
+ private void runSyncStep(String name, SyncStep step) {
try {
- int count = syncService.syncStations();
- log.info("eMind电站同步完成,共{}条", count);
- int energyChainCount = energyChainSyncService.syncEnergyChains();
- log.info("eMind储能能量链同步完成,共{}条", energyChainCount);
- int energyBlockCount = energyBlockSyncService.syncEnergyBlocks();
- log.info("eMind储能能量块同步完成,共{}条", energyBlockCount);
- int dashboardCount = dashboardSyncService.syncDashboardData();
- log.info("eMind储能看板同步完成,共{}个站点", dashboardCount);
+ int count = step.execute();
+ log.info("{}同步完成,共{}条", name, count);
} catch (Exception exception) {
- log.error("eMind电站、能量链或能量块同步失败", exception);
+ log.error("{}同步失败,继续执行其他eMind同步任务", name, exception);
}
}
+
+ @FunctionalInterface
+ private interface SyncStep {
+ int execute();
+ }
}
diff --git a/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/service/EmindStationSyncSchedulerTest.java b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/service/EmindStationSyncSchedulerTest.java
new file mode 100644
index 0000000..e910c1b
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/service/EmindStationSyncSchedulerTest.java
@@ -0,0 +1,50 @@
+package com.ruoyi.other.emind.service;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class EmindStationSyncSchedulerTest {
+ @Mock private EmindStationSyncService stationSyncService;
+ @Mock private EmindEnergyChainSyncService energyChainSyncService;
+ @Mock private EmindEnergyBlockSyncService energyBlockSyncService;
+ @Mock private EmindStorageDashboardSyncService dashboardSyncService;
+
+ @Test
+ void continuesDashboardAndDeviceSyncWhenStationSyncFails() {
+ when(stationSyncService.syncStations()).thenThrow(new IllegalStateException("station unavailable"));
+ EmindStationSyncScheduler scheduler = scheduler();
+
+ scheduler.sync();
+
+ verify(dashboardSyncService).syncDashboardData();
+ verify(energyChainSyncService).syncEnergyChains();
+ verify(energyBlockSyncService).syncEnergyBlocks();
+ }
+
+ @Test
+ void updatesDashboardBeforeOptionalDeviceDetails() {
+ EmindStationSyncScheduler scheduler = scheduler();
+
+ scheduler.sync();
+
+ InOrder order = inOrder(stationSyncService, dashboardSyncService,
+ energyChainSyncService, energyBlockSyncService);
+ order.verify(stationSyncService).syncStations();
+ order.verify(dashboardSyncService).syncDashboardData();
+ order.verify(energyChainSyncService).syncEnergyChains();
+ order.verify(energyBlockSyncService).syncEnergyBlocks();
+ }
+
+ private EmindStationSyncScheduler scheduler() {
+ return new EmindStationSyncScheduler(stationSyncService, energyChainSyncService,
+ energyBlockSyncService, dashboardSyncService);
+ }
+}
diff --git a/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/util/EmindCloudApiUtilTest.java b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/util/EmindCloudApiUtilTest.java
index 8cce53b..a006add 100644
--- a/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/util/EmindCloudApiUtilTest.java
+++ b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/emind/util/EmindCloudApiUtilTest.java
@@ -224,4 +224,21 @@
assertEquals(new java.math.BigDecimal("1"), rows.get(0).getChargedEnergySharp());
assertEquals(new java.math.BigDecimal("2.6"), rows.get(0).getDischargedEnergyValley());
}
+
+ @Test
+ void acceptsTimeOfUsePriceObjectReturnedAsJsonString() {
+ when(redisService.getCacheObject(EmindCloudApiUtil.ACCESS_TOKEN_CACHE_KEY)).thenReturn("cached-token");
+ when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(ResponseEntity.ok("{\"code\":200,\"data\":[{\"stationId\":758," +
+ "\"date\":\"2026-07-25\",\"price\":\"{\\\"in_peak_price\\\":1.053295}\"," +
+ "\"priceName\":\"2026年7月国网电价\",\"sum_income\":12.3}]}"));
+
+ List<EmindIncomeHistoryDto> rows = api.getIncomeHistory(758L,
+ java.time.LocalDateTime.of(2026, 7, 25, 0, 0),
+ java.time.LocalDateTime.of(2026, 7, 25, 23, 59, 59));
+
+ assertEquals(1, rows.size());
+ assertEquals(null, rows.get(0).getPrice());
+ assertEquals(new java.math.BigDecimal("12.3"), rows.get(0).getIncome());
+ }
}
diff --git a/sql/20260728_account_settlement_v2.sql b/sql/20260728_account_settlement_v2.sql
new file mode 100644
index 0000000..bfa7cb7
--- /dev/null
+++ b/sql/20260728_account_settlement_v2.sql
@@ -0,0 +1,4 @@
+ALTER TABLE `t_account_settlement_statement`
+ ADD COLUMN `platform_income` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '平台收入' AFTER `recorded_amount`,
+ ADD COLUMN `partner_income` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '合作商收入' AFTER `platform_income`;
+
diff --git a/sql/20260728_reservation_time_ranges.sql b/sql/20260728_reservation_time_ranges.sql
new file mode 100644
index 0000000..7bb2c4f
--- /dev/null
+++ b/sql/20260728_reservation_time_ranges.sql
@@ -0,0 +1,17 @@
+-- 预约充电:允许配置多个预约时段。
+-- 保留 reservation_start_time / reservation_end_time 并同步第一段,便于旧版本回滚兼容。
+ALTER TABLE `t_site`
+ ADD COLUMN `reservation_time_ranges` JSON NULL COMMENT '允许预约时段JSON数组' AFTER `reservation_end_time`;
+
+UPDATE `t_site`
+SET `reservation_time_ranges` = JSON_ARRAY(
+ JSON_OBJECT(
+ 'startTime', `reservation_start_time`,
+ 'endTime', `reservation_end_time`
+ )
+ )
+WHERE `reservation_time_ranges` IS NULL
+ AND `reservation_start_time` IS NOT NULL
+ AND `reservation_start_time` <> ''
+ AND `reservation_end_time` IS NOT NULL
+ AND `reservation_end_time` <> '';
--
Gitblit v1.7.1