From da9b7a52dbc6fcd2173efef90528d8fccc322554 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期五, 17 七月 2026 16:22:40 +0800
Subject: [PATCH] P1问题fix
---
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java | 184 +++--
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java | 8
sql/20260716_site_transformer_member_discount.sql | 33 +
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java | 12
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java | 35 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java | 14
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java | 16
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java | 19
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java | 34 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java | 13
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml | 19
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java | 2
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java | 37 +
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java | 99 +++
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java | 70 ++
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java | 143 +++++
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java | 2
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java | 75 ++
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java | 21
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java | 134 ++++
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java | 9
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java | 23
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java | 55 +
sql/20260715_sys_faq_missing_table_fix.sql | 104 +++
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java | 4
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java | 55 +
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java | 55 +
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java | 37 +
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java | 47 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java | 39 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java | 220 +++++++
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java | 23
sql/20260715_reservation_ahead_minute.sql | 3
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java | 2
34 files changed, 1,535 insertions(+), 111 deletions(-)
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 6d2d9b6..af49cf8 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
@@ -76,6 +76,18 @@
@ApiModelProperty(value = "建设场所(0=其他,1=居民区,2=公共机构,3=企事业单位,4=写字楼,5=工业园区,6=交通枢纽,7=大型文体设施,8=城市绿地,9=大型建筑配建停车场,10=路边停车位,11=城际高速服务区),12=风景区,13=公交场站,14=加油加气站,15=出租车", required = true)
private Integer constructionSite;
/**
+ * 变压器功率(kW)
+ */
+ @TableField("transformer_power")
+ @ApiModelProperty(value = "变压器功率(kW)", required = true)
+ private BigDecimal transformerPower;
+ /**
+ * 是否参与会员折扣(0=否,1=是)
+ */
+ @TableField("member_discount_flag")
+ @ApiModelProperty(value = "是否参与会员折扣(0=否,1=是)", required = true)
+ private Integer memberDiscountFlag;
+ /**
* 站点环境图
*/
@TableField("img_url")
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 024eee3..c175952 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
@@ -16,6 +16,8 @@
private Integer chargingPileId;
@ApiModelProperty(value = "站点id")
private Integer siteId;
+ @ApiModelProperty(value = "是否参与会员折扣(0=否,1=是)")
+ private Integer memberDiscountFlag;
@ApiModelProperty(value = "桩编号")
private String number;
@ApiModelProperty(value = "充电枪id")
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java
index 1d41ea2..9fc6378 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java
@@ -72,7 +72,7 @@
}
@NotBlank(message = "问题标题不能为空")
- @Size(min = 0, max = 500, message = "问题标题不能超过500个字符")
+ @Size(max = 50, message = "问题标题不能超过50个字符")
public String getQuestion() {
return question;
}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java
index 373f4d4..82ca91a 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java
@@ -16,6 +16,8 @@
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -29,6 +31,8 @@
@RestController
@RequestMapping("/file/")
public class FileController {
+
+ private static final String MANAGE_MEDIA_EXTENSIONS = "mp4,avi,rmvb,mov,wmv,flv,mkv,mpeg,mpg";
@Autowired
private FileUploadConfig fileUploadConfig;
@@ -105,7 +109,7 @@
// 获取文件后缀
String ext = filename.substring(filename.lastIndexOf(".") + 1);
// 检查文件类型
- String allowedExt = getAllowedExt(fileUploadConfig.getDocExt());
+ String allowedExt = getManageAllowedExt();
if (!isAllowedExt(ext, allowedExt)) {
return invalidExtResult(allowedExt);
}
@@ -205,7 +209,32 @@
return fileUploadConfig.getAllowExt();
}
- private boolean isAllowedExt(String ext, String allowedExt) {
+ String getManageAllowedExt() {
+ return mergeAllowedExt(getAllowedExt(fileUploadConfig.getDocExt()), MANAGE_MEDIA_EXTENSIONS);
+ }
+
+ static String mergeAllowedExt(String... extensionGroups) {
+ Set<String> extensions = new LinkedHashSet<>();
+ if (extensionGroups != null) {
+ for (String group : extensionGroups) {
+ if (group == null) {
+ continue;
+ }
+ for (String item : group.split(",")) {
+ String normalizedItem = item.trim();
+ if (normalizedItem.startsWith(".")) {
+ normalizedItem = normalizedItem.substring(1);
+ }
+ if (!normalizedItem.isEmpty()) {
+ extensions.add(normalizedItem.toLowerCase());
+ }
+ }
+ }
+ }
+ return String.join(",", extensions);
+ }
+
+ static boolean isAllowedExt(String ext, String allowedExt) {
if (ext == null || allowedExt == null || allowedExt.trim().length() == 0) {
return false;
}
@@ -225,4 +254,4 @@
private AjaxResult invalidExtResult(String allowedExt) {
return AjaxResult.error("上传文件格式不正确,仅支持" + (allowedExt == null ? "" : allowedExt));
}
-}
\ No newline at end of file
+}
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java
new file mode 100644
index 0000000..776f9ab
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java
@@ -0,0 +1,19 @@
+package com.ruoyi.account.controller;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FileControllerTest {
+
+ @Test
+ void managementUploadAllowsConfiguredDocumentsAndFaqVideos() {
+ String allowed = FileController.mergeAllowedExt("png,jpg,pdf,doc,docx", "mp4,avi,rmvb,mov");
+
+ assertTrue(FileController.isAllowedExt("pdf", allowed));
+ assertTrue(FileController.isAllowedExt("MP4", allowed));
+ assertTrue(FileController.isAllowedExt(".mov", allowed));
+ assertFalse(FileController.isAllowedExt("jsp", allowed));
+ }
+}
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 3154286..9a2e3c7 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
@@ -21,6 +21,9 @@
import com.ruoyi.chargingPile.api.vo.*;
import com.ruoyi.chargingPile.api.domain.SiteMenu;
import com.ruoyi.chargingPile.dto.ChargingPercentBack;
+import com.ruoyi.chargingPile.dto.BatchSetSiteAccountingStrategyDTO;
+import com.ruoyi.chargingPile.dto.SiteExcelDTO;
+import com.ruoyi.chargingPile.dto.SiteImportResultDTO;
import com.ruoyi.common.core.dto.ChargingOrderGroup;
import com.ruoyi.common.core.dto.ChargingPercentProvinceDto;
import com.ruoyi.chargingPile.service.*;
@@ -28,6 +31,7 @@
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.core.utils.poi.ExcelUtil;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.log.annotation.Log;
@@ -55,9 +59,11 @@
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
@@ -144,6 +150,7 @@
SiteInfoVO siteInfoVO = new SiteInfoVO();
TChargingGun chargingGun = chargingGunService.getOne(new LambdaQueryWrapper<TChargingGun>().eq(TChargingGun::getFullNumber, number));
Site site = siteService.getById(chargingGun.getSiteId());
+ siteInfoVO.setMemberDiscountFlag(site.getMemberDiscountFlag());
Integer accountingStrategyId = chargingGun.getAccountingStrategyId();
if(null == accountingStrategyId){
accountingStrategyId = site.getAccountingStrategyId();
@@ -157,9 +164,10 @@
if(DateUtils.string2LocalTime(tAccountingStrategyDetail.getStartTime() + ":00").compareTo(LocalTime.now()) <= 0
&& DateUtils.string2LocalTime(tAccountingStrategyDetail.getEndTime() + ("23:59:59".equals(tAccountingStrategyDetail.getEndTime())?"":":00")).compareTo(LocalTime.now()) > 0){
siteInfoVO.setElectrovalence(tAccountingStrategyDetail.getElectrovalence().add(tAccountingStrategyDetail.getServiceCharge()).setScale(4, BigDecimal.ROUND_HALF_UP));
+ siteInfoVO.setVipElectrovalence(siteInfoVO.getElectrovalence());
// 获取当前登录用户id
String token = SecurityUtils.getToken(ServletUtils.getRequest());
- if(StringUtils.isNotEmpty(token)){
+ if(Integer.valueOf(1).equals(site.getMemberDiscountFlag()) && StringUtils.isNotEmpty(token)){
Long userId = tokenService.getLoginUserApplet().getUserId();
// 根据id查询用户信息
TAppUser appUser = appUserClient.getUserById(userId).getData();
@@ -210,6 +218,7 @@
TChargingPile one = chargingPileService.lambdaQuery().eq(TChargingPile::getCode, number).one();
TChargingGun chargingGun = chargingGunService.getOne(new LambdaQueryWrapper<TChargingGun>().eq(TChargingGun::getChargingPileId, one.getId()).last(" limit 0, 1"));
Site site = siteService.getById(chargingGun.getSiteId());
+ siteInfoVO.setMemberDiscountFlag(site.getMemberDiscountFlag());
Integer accountingStrategyId = chargingGun.getAccountingStrategyId();
if(null == accountingStrategyId){
accountingStrategyId = site.getAccountingStrategyId();
@@ -222,9 +231,10 @@
if(DateUtils.string2LocalTime(tAccountingStrategyDetail.getStartTime() + ":00").compareTo(LocalTime.now()) <= 0
&& DateUtils.string2LocalTime(tAccountingStrategyDetail.getEndTime() + ("23:59:59".equals(tAccountingStrategyDetail.getEndTime())?"":":00")).compareTo(LocalTime.now()) > 0){
siteInfoVO.setElectrovalence(tAccountingStrategyDetail.getElectrovalence().add(tAccountingStrategyDetail.getServiceCharge()).setScale(4, BigDecimal.ROUND_HALF_UP));
+ siteInfoVO.setVipElectrovalence(siteInfoVO.getElectrovalence());
// 获取当前登录用户id
String token = SecurityUtils.getToken(ServletUtils.getRequest());
- if(StringUtils.isNotEmpty(token)){
+ if(Integer.valueOf(1).equals(site.getMemberDiscountFlag()) && StringUtils.isNotEmpty(token)){
Long userId = tokenService.getLoginUserApplet().getUserId();
// 根据id查询用户信息
TAppUser appUser = appUserClient.getUserById(userId).getData();
@@ -245,7 +255,7 @@
}
}
- if(Objects.nonNull(byId1.getDiscount())){
+ if(Integer.valueOf(1).equals(site.getMemberDiscountFlag()) && Objects.nonNull(byId1.getDiscount())){
siteInfoVO.setVipElectrovalence((tAccountingStrategyDetail.getServiceCharge().multiply(byId1.getDiscount()).add(tAccountingStrategyDetail.getElectrovalence())).setScale(4, BigDecimal.ROUND_HALF_UP));
}else {
siteInfoVO.setVipElectrovalence((tAccountingStrategyDetail.getServiceCharge().add(tAccountingStrategyDetail.getElectrovalence())).setScale(4, BigDecimal.ROUND_HALF_UP));
@@ -535,6 +545,55 @@
public AjaxResult<Map<String,ChargingGunCountVO>> getChargingGunCount1(@RequestParam(value = "siteId")@ApiParam(value = "站点id", required = true)Integer siteId){
return AjaxResult.success(chargingGunService.getChargingGunCount(siteId));
}
+
+ @RequiresPermissions(value = {"/site/add"}, logical = Logical.OR)
+ @GetMapping("/importTemplate")
+ @ApiOperation(value = "下载站点导入模板", tags = {"管理后台-站点管理"})
+ public void importTemplate(HttpServletResponse response) {
+ ExcelUtil<SiteExcelDTO> util = new ExcelUtil<>(SiteExcelDTO.class);
+ util.importTemplateExcel(response, "站点导入模板",
+ "填写说明:所在区域填写区县行政区划代码;带*字段为必填;合作商、服务商、场地方和计费模板填写系统中的完整名称。") ;
+ }
+
+ @RequiresPermissions(value = {"/site/add"}, logical = Logical.OR)
+ @PostMapping("/import")
+ @Log(title = "【站点管理】导入站点", businessType = BusinessType.IMPORT, operatorType = OperatorType.MANAGE)
+ @ApiOperation(value = "导入站点", tags = {"管理后台-站点管理"})
+ public AjaxResult importSites(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "cover", defaultValue = "false") Boolean cover) throws Exception {
+ SiteImportResultDTO result = siteService.importSites(file, cover);
+ if (Boolean.TRUE.equals(result.getNeedConfirm())) {
+ return AjaxResult.error("发现重复站点编号,请确认是否覆盖", result);
+ }
+ if (result.getErrorCount() != null && result.getErrorCount() > 0) {
+ return AjaxResult.error("导入失败,请检查错误信息", result);
+ }
+ return AjaxResult.success("导入成功", result);
+ }
+
+ @RequiresPermissions(value = {"/site/export", "/site"}, logical = Logical.OR)
+ @PutMapping("/export")
+ @Log(title = "【站点管理】导出站点", businessType = BusinessType.EXPORT, operatorType = OperatorType.MANAGE)
+ @ApiOperation(value = "导出站点", tags = {"管理后台-站点管理"})
+ public void exportSites(@RequestBody GetSiteList query, HttpServletResponse response) {
+ List<Site> sites = siteService.listSitesForExport(query);
+ Set<Integer> partnerIds = sites.stream()
+ .flatMap(site -> Arrays.asList(site.getPartnerId(), site.getVenuePartnerId()).stream())
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Map<Integer, String> partnerNames = partnerIds.isEmpty() ? Collections.emptyMap()
+ : partnerService.listByIds(partnerIds).stream()
+ .collect(Collectors.toMap(Partner::getId, Partner::getName, (a, b) -> a));
+ Set<Integer> strategyIds = sites.stream().map(Site::getAccountingStrategyId)
+ .filter(Objects::nonNull).collect(Collectors.toSet());
+ Map<Integer, String> strategyNames = strategyIds.isEmpty() ? Collections.emptyMap()
+ : accountingStrategyService.listByIds(strategyIds).stream()
+ .collect(Collectors.toMap(TAccountingStrategy::getId, TAccountingStrategy::getName, (a, b) -> a));
+ List<SiteExcelDTO> rows = sites.stream().map(site -> SiteExcelDTO.from(site,
+ partnerNames.get(site.getPartnerId()), partnerNames.get(site.getVenuePartnerId()),
+ strategyNames.get(site.getAccountingStrategyId()))).collect(Collectors.toList());
+ new ExcelUtil<>(SiteExcelDTO.class).exportExcel(response, rows, "站点数据");
+ }
@RequiresPermissions(value = {"/site/add"}, logical = Logical.OR)
@ResponseBody
@@ -632,8 +691,14 @@
@PostMapping("/batchSetAccountingStrategy")
@ApiOperation(value = "批量设置站点计费策略", tags = {"管理后台-站点管理"})
@Log(title = "【站点管理】批量设置站点计费策略", businessType = BusinessType.UPDATE)
- public AjaxResult batchSetAccountingStrategy(@RequestParam("ids") List<Integer> ids, @RequestParam("accountingStrategyId") Integer accountingStrategyId){
- siteService.batchSetAccountingStrategy(ids, accountingStrategyId);
+ public AjaxResult batchSetAccountingStrategy(@RequestBody BatchSetSiteAccountingStrategyDTO request){
+ if (request == null || CollectionUtils.isEmpty(request.getIds())) {
+ return AjaxResult.error("请选择要设置计费策略的站点");
+ }
+ if (request.getAccountingStrategyId() == null) {
+ return AjaxResult.error("请选择计费模板");
+ }
+ siteService.batchSetAccountingStrategy(request.getIds(), request.getAccountingStrategyId());
return AjaxResult.success();
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java
index 4494a6a..b4b25e5 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java
@@ -49,7 +49,6 @@
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.multipart.MultipartFile;
@@ -60,7 +59,10 @@
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -186,13 +188,34 @@
@Log(title = "【充电桩信息】导出充电桩数据", businessType = BusinessType.EXPORT,operatorType = OperatorType.MANAGE)
@ApiOperation(value = "导出充电桩数据", tags = {"管理后台-充电桩信息"})
public void export(@RequestBody PageChargingPileList page, HttpServletResponse response){
- List<PageChargingPileListDTO> records = chargingPileService.listChargingPileForExport(page);
- List<ChargingPileExport> exports = records.stream().map(record -> {
- ChargingPileExport export = new ChargingPileExport();
- BeanUtils.copyProperties(record, export);
- return export;
- }).collect(Collectors.toList());
- ExcelUtil<ChargingPileExport> util = new ExcelUtil<>(ChargingPileExport.class);
+ List<PageChargingPileListDTO> records = chargingPileService.listChargingPileForExport(page);
+ Set<Integer> pileIds = records.stream()
+ .map(PageChargingPileListDTO::getId)
+ .filter(id -> id != null)
+ .collect(Collectors.toSet());
+ Map<Integer, TChargingPile> pileMap = pileIds.isEmpty()
+ ? Collections.emptyMap()
+ : chargingPileService.listByIds(pileIds).stream()
+ .collect(Collectors.toMap(TChargingPile::getId, item -> item, (left, right) -> left));
+ List<TChargingGunVO> guns = pileIds.isEmpty()
+ ? Collections.emptyList()
+ : chargingGunService.exportQRCodeList(new TChargingGunQuery()).stream()
+ .filter(gun -> pileIds.contains(gun.getChargingPileId()))
+ .collect(Collectors.toList());
+ Map<Integer, List<TChargingGunVO>> gunsByPile = guns.stream()
+ .collect(Collectors.groupingBy(TChargingGunVO::getChargingPileId));
+ List<ChargingPileExport> exports = new ArrayList<>();
+ for (PageChargingPileListDTO record : records) {
+ List<TChargingGunVO> pileGuns = gunsByPile.getOrDefault(record.getId(), Collections.emptyList());
+ if (pileGuns.isEmpty()) {
+ exports.add(ChargingPileExport.from(record, pileMap.get(record.getId()), null));
+ continue;
+ }
+ for (TChargingGunVO gun : pileGuns) {
+ exports.add(ChargingPileExport.from(record, pileMap.get(record.getId()), gun));
+ }
+ }
+ ExcelUtil<ChargingPileExport> util = new ExcelUtil<>(ChargingPileExport.class);
util.exportExcel(response, exports, "充电桩数据");
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java
new file mode 100644
index 0000000..48d944c
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java
@@ -0,0 +1,13 @@
+package com.ruoyi.chargingPile.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class BatchSetSiteAccountingStrategyDTO {
+
+ private List<Integer> ids;
+
+ private Integer accountingStrategyId;
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java
new file mode 100644
index 0000000..31dc5bc
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java
@@ -0,0 +1,143 @@
+package com.ruoyi.chargingPile.dto;
+
+import com.ruoyi.chargingPile.api.model.Site;
+import com.ruoyi.common.core.annotation.Excel;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.math.BigDecimal;
+import java.time.format.DateTimeFormatter;
+
+@Data
+public class SiteExcelDTO {
+
+ @Excel(name = "*归属合作商/服务商", width = 28)
+ private String partnerName;
+ @Excel(name = "场地方", width = 25)
+ private String venuePartnerName;
+ @Excel(name = "*站点编号", width = 22)
+ private String code;
+ @Excel(name = "*站点名称", width = 30)
+ private String name;
+ @Excel(name = "*站点类型", width = 20, readConverterExp = "0=其他,1=公共,2=个人,3=公交(专用),4=环卫(专用),5=物流(专用),6=出租车(专用)")
+ private Integer siteType;
+ @Excel(name = "*经营类别", width = 15, readConverterExp = "1=直营,2=非直营")
+ private Integer businessCategory;
+ @Excel(name = "*站点状态", width = 18, readConverterExp = "1=正常使用,2=维护中,3=关闭下线")
+ private Integer status;
+ @Excel(name = "*建设场所", width = 24, readConverterExp = "0=其他,1=居民区,2=公共机构,3=企事业单位,4=写字楼,5=工业园区,6=交通枢纽,7=大型文体设施,8=城市绿地,9=大型建筑配建停车场,10=路边停车位,11=城际高速服务区,12=风景区,13=公交场站,14=加油加气站,15=出租车")
+ private Integer constructionSite;
+ @Excel(name = "*变压器功率", width = 18)
+ private BigDecimal transformerPower;
+ @Excel(name = "*是否参与会员折扣", width = 22, readConverterExp = "0=否,1=是")
+ private Integer memberDiscountFlag;
+ @Excel(name = "*所在区域", width = 20, prompt = "填写区县行政区划代码,如510903")
+ private String areaCode;
+ @Excel(name = "*详细地址", width = 35)
+ private String address;
+ @Excel(name = "*经度", width = 18)
+ private String lon;
+ @Excel(name = "*纬度", width = 18)
+ private String lat;
+ @Excel(name = "*充电站国家代码", width = 20)
+ private String countryCode;
+ @Excel(name = "*站点电话", width = 22)
+ private String phone;
+ @Excel(name = "*服务电话", width = 22)
+ private String servicePhone;
+ @Excel(name = "站点引导", width = 30)
+ private String guide;
+ @Excel(name = "运营时间", width = 30)
+ private String startServiceTime;
+ @Excel(name = "站内提供服务", width = 30)
+ private String serviceDescription;
+ @Excel(name = "车型描述", width = 30)
+ private String vehicleDescription;
+ @Excel(name = "*车位数量", width = 15)
+ private Integer parkingSpace;
+ @Excel(name = "*充电电费描述", width = 35)
+ private String rateDescription;
+ @Excel(name = "超时占位费描述", width = 35)
+ private String spaceChargeExplain;
+ @Excel(name = "计费模板", width = 25)
+ private String accountingStrategyName;
+ @Excel(name = "建站时间", width = 22, prompt = "格式:yyyy-MM-dd HH:mm:ss")
+ private String establishmentTime;
+ @Excel(name = "*排序", width = 12)
+ private Integer sort;
+ @Excel(name = "备注", width = 30)
+ private String remark;
+ @Excel(name = "*电站分类", width = 22, readConverterExp = "1=充电站,2=换电站,3=充换电一体站")
+ private Integer stationClassification;
+ @Excel(name = "通用类型", width = 18, readConverterExp = "1=通用类型,2=非通用")
+ private Integer generalApplicationType;
+ @Excel(name = "充换电站所在县以下行政区划代码", width = 34)
+ private String areaCodeCountryside;
+ @Excel(name = "*7*24小时营业", width = 20, readConverterExp = "0=否,1=是")
+ private Integer roundTheClock;
+ @Excel(name = "*停车费类型", width = 22, readConverterExp = "0=免费,1=不免费,2=限时免费停车,3=充电限时减免,4=参考场地实际收费标准")
+ private Integer parkType;
+ @Excel(name = "停车费描述", width = 35)
+ private String parkFee;
+ @Excel(name = "服务车型描述", width = 30)
+ private String swapMatchCars;
+ @Excel(name = "支付方式", width = 18, readConverterExp = "1=刷卡,2=线上,3=现金")
+ private Integer payment;
+ @Excel(name = "是否支持预约", width = 20, readConverterExp = "1=支持,2=不支持")
+ private Integer supportOrder;
+ @Excel(name = "*电费类型", width = 22, readConverterExp = "0=其他用电,1=商业用电,2=普通工业用电,3=大工业用电")
+ private Integer electricityType;
+ @Excel(name = "*报装类型", width = 20, readConverterExp = "1=独立报装,2=非独立报装")
+ private Integer businessExpandType;
+ @Excel(name = "*报装电源容量", width = 20)
+ private BigDecimal capacity;
+ @Excel(name = "*站点额定总功率", width = 22)
+ private BigDecimal ratedPower;
+ @Excel(name = "*是否峰谷分时", width = 20, readConverterExp = "0=否,1=是")
+ private Integer periodFee;
+ @Excel(name = "*正式投运时间", width = 22, prompt = "格式:yyyy-MM-dd")
+ private String officialRunTime;
+ @Excel(name = "充换电站方位", width = 24, readConverterExp = "1=地面-停车场,2=地面-路侧,3=地下停车,4=立体式停车楼")
+ private Integer stationOrientation;
+ @Excel(name = "充换电站建设面积", width = 24)
+ private BigDecimal stationArea;
+ @Excel(name = "充换电站人工值守", width = 24, readConverterExp = "0=无,1=有")
+ private Integer havePerson;
+ @Excel(name = "*视频监控配套情况", width = 24, readConverterExp = "0=无,1=有")
+ private Integer videoMonitor;
+ @Excel(name = "周边配套设施", width = 30)
+ private String supportingFacilities;
+ @Excel(name = "是否有小票机", width = 20, readConverterExp = "0=无,1=有")
+ private Integer printerFlag;
+ @Excel(name = "是否有道闸", width = 18, readConverterExp = "0=无,1=有")
+ private Integer barrierFlag;
+ @Excel(name = "是否有地锁", width = 18, readConverterExp = "0=无,1=有")
+ private Integer parkingLockFlag;
+ @Excel(name = "*设备所属方名称", width = 28)
+ private String equipmentOwnerName;
+ @Excel(name = "*供电局用户编号", width = 25)
+ private String residentNo;
+ @Excel(name = "*供电类型", width = 18, readConverterExp = "1=直供电,2=转供电")
+ private Integer supplyType;
+ @Excel(name = "*表号", width = 20)
+ private String wattHourMeterNo;
+ @Excel(name = "*外电功率", width = 18)
+ private BigDecimal forwardPower;
+ @Excel(name = "*服务费描述", width = 35)
+ private String serviceFee;
+ @Excel(name = "*充电站全省唯一备案号", width = 32)
+ private String recordUniqueNo;
+
+ public static SiteExcelDTO from(Site site, String partnerName, String venuePartnerName, String strategyName) {
+ SiteExcelDTO dto = new SiteExcelDTO();
+ BeanUtils.copyProperties(site, dto);
+ dto.setPartnerName(partnerName);
+ dto.setVenuePartnerName(venuePartnerName);
+ dto.setAccountingStrategyName(strategyName);
+ dto.setAreaCode(site.getDistrictsCode());
+ if (site.getEstablishmentTime() != null) {
+ dto.setEstablishmentTime(site.getEstablishmentTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+ }
+ return dto;
+ }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java
new file mode 100644
index 0000000..a9c5a76
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java
@@ -0,0 +1,14 @@
+package com.ruoyi.chargingPile.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SiteImportErrorDTO {
+ private Integer rowNum;
+ private String code;
+ private String reason;
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java
new file mode 100644
index 0000000..263d427
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java
@@ -0,0 +1,16 @@
+package com.ruoyi.chargingPile.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class SiteImportResultDTO {
+ private Integer successCount = 0;
+ private Integer updateCount = 0;
+ private Integer errorCount = 0;
+ private Boolean needConfirm = false;
+ private List<String> duplicateCodes = new ArrayList<>();
+ private List<SiteImportErrorDTO> errorMessage = new ArrayList<>();
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java
index 8965601..34ed298 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java
@@ -1,5 +1,8 @@
package com.ruoyi.chargingPile.export;
+import com.ruoyi.chargingPile.api.dto.PageChargingPileListDTO;
+import com.ruoyi.chargingPile.api.model.TChargingPile;
+import com.ruoyi.chargingPile.api.vo.TChargingGunVO;
import com.ruoyi.common.core.annotation.Excel;
import lombok.Data;
@@ -7,22 +10,141 @@
@Data
public class ChargingPileExport {
- @Excel(name = "设备编码", width = 25)
+ @Excel(name = "*设备编码", width = 25)
private String code;
- @Excel(name = "充电设备名称", width = 25)
+ @Excel(name = "*设备名称", width = 25)
private String name;
- @Excel(name = "桩号", width = 10)
+ @Excel(name = "*桩号", width = 10)
private String number;
- @Excel(name = "\u8bbe\u5907\u7c7b\u578b", width = 20, readConverterExp = "0=\u5176\u4ed6,1=\u76f4\u6d41\u8bbe\u5907,2=\u4ea4\u6d41\u8bbe\u5907,3=\u4ea4\u76f4\u6d41\u4e00\u4f53\u8bbe\u5907,4=\u65e0\u7ebf\u8bbe\u5907,5=\u6709\u5e8f")
+ @Excel(name = "设备类型", width = 20, readConverterExp = "0=其他,1=直流设备,2=交流设备,3=交直流一体设备,4=无线设备,5=有序,6=V2G")
private Integer type;
@Excel(name = "归属电站", width = 30)
private String siteName;
- @Excel(name = "归属合作商", width = 30)
+ @Excel(name = "设备合作商", width = 30)
private String partnerName;
+ @Excel(name = "生产机构代码", width = 25)
+ private String manufacturerCode;
@Excel(name = "生产商名称", width = 30)
private String manufacturer;
@Excel(name = "设备型号", width = 25)
private String equipmentType;
- @Excel(name = "额定功率", width = 15)
+ @Excel(name = "设备生产日期", width = 15)
+ private String productionDate;
+ @Excel(name = "*额定功率", width = 15)
private BigDecimal ratedPower;
+ @Excel(name = "设备充电站编号", width = 25)
+ private String chargingStationNumber;
+ @Excel(name = "设备唯一编码", width = 25)
+ private String equipmentUniqueNumber;
+ @Excel(name = "*设备分类", width = 30, readConverterExp = "1=车辆充电设备,2=换电站内的电池箱充电设备")
+ private Integer equipmentClassification;
+ @Excel(name = "设备经度", width = 20)
+ private String equipmentLng;
+ @Excel(name = "设备纬度", width = 20)
+ private String equipmentLat;
+
+ @Excel(name = "*接口编码", width = 20)
+ private String interfaceCode;
+ @Excel(name = "*接口名称", width = 25)
+ private String interfaceName;
+ @Excel(name = "*设备接口分类", width = 30, readConverterExp = "1=车辆充电设备接口,2=换电站内的电池箱充电设备接口")
+ private Integer interfaceEquipmentClassification;
+ @Excel(name = "*接口类型", width = 35, readConverterExp = "0=其他,1=家用插座(模式2),2=交流接口插座(模式3,连接方式B),3=交流接口插头(带枪线,模式3,连接方式C),4=直流接口枪头(带枪线,模式4),5=无线充电座")
+ private Integer interfaceType;
+ @Excel(name = "*接口状态", width = 20, readConverterExp = "1=离线,2=空闲,3=占用(未充电),4=占用(充电中),5=占用(已充满),6=占用(预约锁定),7=故障")
+ private Integer interfaceStatus;
+ @Excel(name = "*充电方式", width = 15, readConverterExp = "1=超级快充,2=快充,3=慢充")
+ private Integer chargeMode;
+ @Excel(name = "计费策略", width = 30)
+ private String strategyName;
+ @Excel(name = "*额定电压上限", width = 18)
+ private BigDecimal upperRatedVoltage;
+ @Excel(name = "*额定电压下限", width = 18)
+ private BigDecimal lowerLimitOfRatedVoltage;
+ @Excel(name = "恒功率电压上限", width = 18)
+ private BigDecimal constantVoltageUpperLimits;
+ @Excel(name = "恒功率电压下限", width = 18)
+ private BigDecimal constantVoltageLowerLimits;
+ @Excel(name = "*额定电流", width = 15)
+ private BigDecimal ratedCurrent;
+ @Excel(name = "恒功率电流上限", width = 18)
+ private BigDecimal constantCurrentUpperLimits;
+ @Excel(name = "恒功率电流下限", width = 18)
+ private BigDecimal constantCurrentLowerLimits;
+ @Excel(name = "*额定功率", width = 15)
+ private BigDecimal interfaceRatedPower;
+ @Excel(name = "车位号", width = 15)
+ private String parkingNumber;
+ @Excel(name = "车位状态", width = 15, readConverterExp = "0=未知,1=空闲,2=占用")
+ private Integer parkingStatus;
+ @Excel(name = "地锁状态", width = 15, readConverterExp = "0=未知,1=已解锁,2=已上锁")
+ private Integer parkingLockState;
+ @Excel(name = "*国家标准", width = 20, readConverterExp = "1=2011,2=2015,3=兼容2011和2015")
+ private String nationalStandard;
+ @Excel(name = "接口说明", width = 30)
+ private String remark;
+ @Excel(name = "辅助电源", width = 20, readConverterExp = "1=12V,2=24V,3=兼容12V和24V")
+ private Integer auxPower;
+ @Excel(name = "*运营状态", width = 18, readConverterExp = "0=未知,1=建设中,5=关闭下线,6=维护中,50=正常使用")
+ private Integer operateStatus;
+ @Excel(name = "运营时间", width = 25)
+ private String operateHours;
+
+ public static ChargingPileExport from(PageChargingPileListDTO record, TChargingPile pile, TChargingGunVO gun) {
+ ChargingPileExport export = new ChargingPileExport();
+ if (pile != null) {
+ export.setCode(pile.getCode());
+ export.setName(pile.getName());
+ export.setNumber(pile.getNumber() == null ? null : String.valueOf(pile.getNumber()));
+ export.setType(pile.getType());
+ export.setManufacturerCode(pile.getManufacturerCode());
+ export.setManufacturer(pile.getManufacturer());
+ export.setEquipmentType(pile.getEquipmentType());
+ export.setProductionDate(pile.getProductionDate() == null ? null : pile.getProductionDate().toString());
+ export.setRatedPower(pile.getRatedPower());
+ export.setChargingStationNumber(pile.getChargingStationNumber());
+ export.setEquipmentUniqueNumber(pile.getEquipmentUniqueNumber());
+ export.setEquipmentClassification(pile.getEquipmentClassification());
+ export.setEquipmentLng(pile.getEquipmentLng());
+ export.setEquipmentLat(pile.getEquipmentLat());
+ } else if (record != null) {
+ export.setCode(record.getCode());
+ export.setName(record.getName());
+ export.setNumber(record.getNumber());
+ export.setType(record.getType());
+ export.setManufacturer(record.getManufacturer());
+ export.setEquipmentType(record.getEquipmentType());
+ export.setRatedPower(record.getRatedPower());
+ }
+ if (record != null) {
+ export.setSiteName(record.getSiteName());
+ export.setPartnerName(record.getPartnerName());
+ }
+ if (gun != null) {
+ export.setInterfaceCode(gun.getCode());
+ export.setInterfaceName(gun.getName());
+ export.setInterfaceEquipmentClassification(gun.getEquipmentClassification());
+ export.setInterfaceType(gun.getType());
+ export.setInterfaceStatus(gun.getStatus());
+ export.setChargeMode(gun.getChargeMode());
+ export.setStrategyName(gun.getStrategyName());
+ export.setUpperRatedVoltage(gun.getUpperRatedVoltage());
+ export.setLowerLimitOfRatedVoltage(gun.getLowerLimitOfRatedVoltage());
+ export.setConstantVoltageUpperLimits(gun.getConstantVoltageUpperLimits());
+ export.setConstantVoltageLowerLimits(gun.getConstantVoltageLowerLimits());
+ export.setRatedCurrent(gun.getRatedCurrent());
+ export.setConstantCurrentUpperLimits(gun.getConstantCurrentUpperLimits());
+ export.setConstantCurrentLowerLimits(gun.getConstantCurrentLowerLimits());
+ export.setInterfaceRatedPower(gun.getRatedPower());
+ export.setParkingNumber(gun.getParkingNumber());
+ export.setParkingStatus(gun.getParkingStatus());
+ export.setParkingLockState(gun.getParkingLockState());
+ export.setNationalStandard(gun.getNationalStandard());
+ export.setRemark(gun.getRemark());
+ export.setAuxPower(gun.getAuxPower());
+ export.setOperateStatus(gun.getOpreateStatus());
+ export.setOperateHours(gun.getOpreateHours());
+ }
+ return export;
+ }
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java
index f7adc47..9d9fab6 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java
@@ -8,11 +8,13 @@
import com.ruoyi.chargingPile.api.query.SiteQuery;
import com.ruoyi.chargingPile.api.vo.SiteDetailVO;
import com.ruoyi.chargingPile.api.vo.SiteVO;
+import com.ruoyi.chargingPile.dto.SiteImportResultDTO;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -30,6 +32,8 @@
*/
PageInfo<GetSiteListDTO> getSiteList(GetSiteList siteList);
PageInfo<GetSiteListDTO> getSiteList1(Long userId);
+ List<Site> listSitesForExport(GetSiteList siteList);
+ SiteImportResultDTO importSites(MultipartFile file, Boolean cover) throws Exception;
/**
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 96cf4d1..2e46482 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
@@ -1,6 +1,7 @@
package com.ruoyi.chargingPile.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.chargingPile.api.dto.GetPermissionConfigurationDTO;
import com.ruoyi.chargingPile.api.dto.PartnerListDTO;
@@ -251,17 +252,33 @@
//修改数据
this.updateById(partner);
+ updateShareRatios(partner);
return AjaxResult.success();
+ }
+
+ void updateShareRatios(Partner partner) {
+ this.update(new UpdateWrapper<Partner>()
+ .eq("id", partner.getId())
+ .set("electricity_share_ratio", partner.getElectricityShareRatio())
+ .set("service_share_ratio", partner.getServiceShareRatio()));
}
private void assignDefaultRole(Partner partner) {
- String roleName = partnerTypeRoleName(partner.getPartnerType());
+ Integer roleId = resolveDefaultRoleId(partner.getPartnerType());
+ partner.setRoleId(roleId);
+ }
+
+ Integer resolveDefaultRoleId(Integer partnerType) {
+ String roleName = partnerTypeRoleName(partnerType);
Integer roleId = this.baseMapper.getDefaultPartnerRoleId(roleName);
+ if (roleId == null && !Integer.valueOf(1).equals(partnerType)) {
+ roleId = this.baseMapper.getDefaultPartnerRoleId("合作商");
+ }
if (roleId == null) {
throw new RuntimeException("系统内置角色【" + roleName + "】不存在,请先配置角色");
}
- partner.setRoleId(roleId);
+ return roleId;
}
private String partnerTypeRoleName(Integer partnerType) {
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 7d0e753..a0a1f82 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
@@ -16,11 +16,16 @@
import com.ruoyi.chargingPile.api.vo.SiteDetailVO;
import com.ruoyi.chargingPile.api.vo.SiteVO;
import com.ruoyi.chargingPile.api.domain.SiteMenu;
+import com.ruoyi.chargingPile.dto.SiteExcelDTO;
+import com.ruoyi.chargingPile.dto.SiteImportErrorDTO;
+import com.ruoyi.chargingPile.dto.SiteImportResultDTO;
import com.ruoyi.chargingPile.mapper.SiteMapper;
import com.ruoyi.chargingPile.service.*;
import com.ruoyi.chargingPile.util.UUIDUtil;
import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.core.utils.poi.ExcelUtil;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
@@ -28,6 +33,8 @@
import com.ruoyi.integration.api.elutong.model.*;
import com.ruoyi.integration.api.feignClient.*;
import com.ruoyi.other.api.domain.TVip;
+import com.ruoyi.other.api.domain.Region;
+import com.ruoyi.other.api.feignClient.OtherClient;
import com.ruoyi.other.api.feignClient.RoleSiteClient;
import com.ruoyi.other.api.feignClient.UserSiteClient;
import com.ruoyi.other.api.feignClient.VipClient;
@@ -36,9 +43,11 @@
import com.ruoyi.system.api.feignClient.SysUserRoleClient;
import com.ruoyi.system.api.model.SysUserRoleVo;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
+import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.math.BigDecimal;
@@ -75,6 +84,8 @@
@Resource
private RoleSiteClient roleSiteClient;
@Resource
+ private OtherClient otherClient;
+ @Resource
private TokenService tokenService;
@Resource
private AppUserClient appUserClient;
@@ -90,6 +101,8 @@
@Resource
private TAccountingStrategyDetailService accountingStrategyDetailService;
+ @Resource
+ private TAccountingStrategyService accountingStrategyService;
@Resource
private TPartnerSiteService partnerSiteService;
@@ -146,6 +159,201 @@
getSiteListDTO.setAuthDelete(roleType == 1 ? true : partnerService.authMenu(objectId, siteId, SiteMenu.SITE_DELETE));
}
return pageInfo.setRecords(list);
+ }
+
+ @Override
+ public List<Site> listSitesForExport(GetSiteList siteList) {
+ siteList.setPageCurr(1);
+ siteList.setPageSize(100000);
+ List<Integer> ids = getSiteList(siteList).getRecords().stream()
+ .map(GetSiteListDTO::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ return ids.isEmpty() ? Collections.emptyList() : listByIds(ids);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public SiteImportResultDTO importSites(MultipartFile file, Boolean cover) throws Exception {
+ if (file == null || file.isEmpty()) {
+ throw new ServiceException("请选择要导入的Excel文件");
+ }
+ ExcelUtil<SiteExcelDTO> util = new ExcelUtil<>(SiteExcelDTO.class);
+ List<SiteExcelDTO> rows = util.importExcel(file.getInputStream(), 0);
+ if (rows == null || rows.isEmpty()) {
+ throw new ServiceException("导入文件没有数据");
+ }
+
+ SysUser sysUser = sysUserClient.getSysUser(tokenService.getLoginUser().getUserid()).getData();
+ Integer roleType = sysUser.getRoleType();
+ Integer objectId = sysUser.getObjectId();
+ Map<String, Partner> partnerMap = partnerService.list(new LambdaQueryWrapper<Partner>().eq(Partner::getDelFlag, 0)).stream()
+ .filter(item -> StringUtils.isNotEmpty(item.getName()))
+ .collect(Collectors.toMap(item -> normalizeImportValue(item.getName()), item -> item, (a, b) -> a));
+ List<TAccountingStrategy> strategies = accountingStrategyService.list(
+ new LambdaQueryWrapper<TAccountingStrategy>().eq(TAccountingStrategy::getDelFlag, 0));
+ Map<String, TAccountingStrategy> strategyMap = strategies.stream().filter(item -> StringUtils.isNotEmpty(item.getName()))
+ .collect(Collectors.toMap(item -> normalizeImportValue(item.getName()), item -> item, (a, b) -> a));
+ List<Region> regions = otherClient.listAllRegion().getData();
+ if (regions == null) {
+ regions = Collections.emptyList();
+ }
+ Map<String, Region> regionByCode = regions.stream().filter(item -> StringUtils.isNotEmpty(item.getCode()))
+ .collect(Collectors.toMap(Region::getCode, item -> item, (a, b) -> a));
+ Map<Integer, Region> regionById = regions.stream().filter(item -> item.getId() != null)
+ .collect(Collectors.toMap(Region::getId, item -> item, (a, b) -> a));
+
+ SiteImportResultDTO result = new SiteImportResultDTO();
+ List<Site> validRows = new ArrayList<>();
+ Set<String> fileCodes = new HashSet<>();
+ Set<String> duplicateCodes = new LinkedHashSet<>();
+ for (int i = 0; i < rows.size(); i++) {
+ SiteExcelDTO row = rows.get(i);
+ int rowNum = i + 2;
+ List<String> errors = validateSiteImportRow(row, partnerMap, strategyMap, regionByCode, regionById,
+ roleType, objectId, fileCodes);
+ String code = row == null ? null : normalizeImportValue(row.getCode());
+ if (!errors.isEmpty()) {
+ result.getErrorMessage().add(new SiteImportErrorDTO(rowNum, code, String.join(";", errors)));
+ continue;
+ }
+ Site site = buildImportedSite(row, partnerMap, strategyMap, regionByCode, regionById);
+ Site exists = getOne(new LambdaQueryWrapper<Site>().eq(Site::getCode, site.getCode()).eq(Site::getDelFlag, 0));
+ if (exists != null) {
+ duplicateCodes.add(site.getCode());
+ site.setId(exists.getId());
+ site.setMark(exists.getMark());
+ }
+ validRows.add(site);
+ }
+ result.setErrorCount(result.getErrorMessage().size());
+ if (result.getErrorCount() > 0) {
+ return result;
+ }
+ if (!duplicateCodes.isEmpty() && !Boolean.TRUE.equals(cover)) {
+ result.setNeedConfirm(true);
+ result.setDuplicateCodes(new ArrayList<>(duplicateCodes));
+ return result;
+ }
+ for (Site site : validRows) {
+ site.setDelFlag(false);
+ if (site.getId() == null) {
+ site.setMark(0);
+ save(site);
+ result.setSuccessCount(result.getSuccessCount() + 1);
+ } else {
+ updateById(site);
+ result.setUpdateCount(result.getUpdateCount() + 1);
+ }
+ }
+ return result;
+ }
+
+ private List<String> validateSiteImportRow(SiteExcelDTO row, Map<String, Partner> partnerMap,
+ Map<String, TAccountingStrategy> strategyMap, Map<String, Region> regionByCode, Map<Integer, Region> regionById,
+ Integer roleType, Integer objectId, Set<String> fileCodes) {
+ List<String> errors = new ArrayList<>();
+ if (row == null) {
+ errors.add("空行");
+ return errors;
+ }
+ String code = normalizeImportValue(row.getCode());
+ String partnerName = normalizeImportValue(row.getPartnerName());
+ Partner partner = partnerMap.get(partnerName);
+ if (StringUtils.isEmpty(partnerName)) errors.add("归属合作商/服务商不能为空");
+ else if (partner == null || !Arrays.asList(1, 3).contains(partner.getPartnerType())) errors.add("归属合作商/服务商不存在");
+ else if (Integer.valueOf(2).equals(roleType) && !Objects.equals(objectId, partner.getId())) errors.add("无权导入其他合作商/服务商站点");
+ if (StringUtils.isNotEmpty(row.getVenuePartnerName())) {
+ Partner venue = partnerMap.get(normalizeImportValue(row.getVenuePartnerName()));
+ if (venue == null || !Integer.valueOf(2).equals(venue.getPartnerType())) errors.add("场地方不存在");
+ }
+ if (StringUtils.isEmpty(code)) errors.add("站点编号不能为空");
+ else if (!fileCodes.add(code)) errors.add("导入文件内站点编号重复");
+ if (StringUtils.isEmpty(row.getName())) errors.add("站点名称不能为空");
+ if (row.getSiteType() == null) errors.add("站点类型不能为空");
+ if (row.getBusinessCategory() == null) errors.add("经营类别不能为空");
+ if (row.getStatus() == null) errors.add("站点状态不能为空");
+ if (row.getConstructionSite() == null) errors.add("建设场所不能为空");
+ if (row.getTransformerPower() == null) errors.add("变压器功率不能为空");
+ if (row.getMemberDiscountFlag() == null) errors.add("是否参与会员折扣不能为空");
+ if (StringUtils.isEmpty(row.getAreaCode()) || !regionByCode.containsKey(row.getAreaCode().trim())) {
+ errors.add("所在区域行政区划代码不存在");
+ } else {
+ Region district = regionByCode.get(row.getAreaCode().trim());
+ Region city = regionById.get(district.getParentId());
+ Region province = city == null ? null : regionById.get(city.getParentId());
+ if (city == null || province == null) errors.add("所在区域必须填写区县级行政区划代码");
+ }
+ if (StringUtils.isEmpty(row.getAddress())) errors.add("详细地址不能为空");
+ if (StringUtils.isEmpty(row.getLon())) errors.add("经度不能为空");
+ if (StringUtils.isEmpty(row.getLat())) errors.add("纬度不能为空");
+ if (StringUtils.isEmpty(row.getCountryCode())) errors.add("充电站国家代码不能为空");
+ if (StringUtils.isEmpty(row.getPhone())) errors.add("站点电话不能为空");
+ if (StringUtils.isEmpty(row.getServicePhone())) errors.add("服务电话不能为空");
+ if (row.getParkingSpace() == null) errors.add("车位数量不能为空");
+ if (StringUtils.isEmpty(row.getRateDescription())) errors.add("充电电费描述不能为空");
+ if (row.getSort() == null) errors.add("排序不能为空");
+ if (row.getStationClassification() == null) errors.add("电站分类不能为空");
+ if (row.getRoundTheClock() == null) errors.add("7*24小时营业不能为空");
+ if (row.getParkType() == null) errors.add("停车费类型不能为空");
+ if (row.getElectricityType() == null) errors.add("电费类型不能为空");
+ if (row.getBusinessExpandType() == null) errors.add("报装类型不能为空");
+ if (row.getCapacity() == null) errors.add("报装电源容量不能为空");
+ if (row.getRatedPower() == null) errors.add("站点额定总功率不能为空");
+ if (row.getPeriodFee() == null) errors.add("是否峰谷分时不能为空");
+ if (StringUtils.isEmpty(row.getOfficialRunTime())) errors.add("正式投运时间不能为空");
+ if (row.getVideoMonitor() == null) errors.add("视频监控配套情况不能为空");
+ if (StringUtils.isEmpty(row.getEquipmentOwnerName())) errors.add("设备所属方名称不能为空");
+ if (StringUtils.isEmpty(row.getResidentNo())) errors.add("供电局用户编号不能为空");
+ if (row.getSupplyType() == null) errors.add("供电类型不能为空");
+ if (StringUtils.isEmpty(row.getWattHourMeterNo())) errors.add("表号不能为空");
+ if (row.getForwardPower() == null) errors.add("外电功率不能为空");
+ if (StringUtils.isEmpty(row.getServiceFee())) errors.add("服务费描述不能为空");
+ if (StringUtils.isEmpty(row.getRecordUniqueNo())) errors.add("充电站全省唯一备案号不能为空");
+ if (StringUtils.isNotEmpty(row.getAccountingStrategyName()) && !strategyMap.containsKey(normalizeImportValue(row.getAccountingStrategyName()))) errors.add("计费模板不存在");
+ if (StringUtils.isNotEmpty(row.getEstablishmentTime())) {
+ try { java.time.LocalDateTime.parse(row.getEstablishmentTime().trim(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }
+ catch (Exception e) { errors.add("建站时间格式应为yyyy-MM-dd HH:mm:ss"); }
+ }
+ return errors;
+ }
+
+ private Site buildImportedSite(SiteExcelDTO row, Map<String, Partner> partnerMap,
+ Map<String, TAccountingStrategy> strategyMap, Map<String, Region> regionByCode,
+ Map<Integer, Region> regionById) {
+ Site site = new Site();
+ BeanUtils.copyProperties(row, site);
+ site.setCode(normalizeImportValue(row.getCode()));
+ site.setName(normalizeImportValue(row.getName()));
+ site.setPartnerId(partnerMap.get(normalizeImportValue(row.getPartnerName())).getId());
+ if (StringUtils.isNotEmpty(row.getVenuePartnerName())) {
+ site.setVenuePartnerId(partnerMap.get(normalizeImportValue(row.getVenuePartnerName())).getId());
+ }
+ if (StringUtils.isNotEmpty(row.getAccountingStrategyName())) {
+ site.setAccountingStrategyId(strategyMap.get(normalizeImportValue(row.getAccountingStrategyName())).getId());
+ }
+ Region district = regionByCode.get(row.getAreaCode().trim());
+ Region city = regionById.get(district.getParentId());
+ Region province = city == null ? null : regionById.get(city.getParentId());
+ site.setDistricts(district.getName());
+ site.setDistrictsCode(district.getCode());
+ if (city != null) {
+ site.setCity(city.getName());
+ site.setCityCode(city.getCode());
+ }
+ if (province != null) {
+ site.setProvince(province.getName());
+ site.setProvinceCode(province.getCode());
+ }
+ if (StringUtils.isNotEmpty(row.getEstablishmentTime())) {
+ site.setEstablishmentTime(java.time.LocalDateTime.parse(row.getEstablishmentTime().trim(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+ }
+ site.setImgUrl("");
+ return site;
+ }
+
+ private String normalizeImportValue(String value) {
+ return value == null ? null : value.trim();
}
@@ -480,6 +688,12 @@
if(null == site.getConstructionSite()){
return AjaxResult.error("建设场所不能为空");
}
+ if(null == site.getTransformerPower()){
+ return AjaxResult.error("变压器功率不能为空");
+ }
+ if(null == site.getMemberDiscountFlag()){
+ return AjaxResult.error("请选择是否参与会员折扣");
+ }
if(StringUtils.isEmpty(site.getProvince()) || StringUtils.isEmpty(site.getProvinceCode()) ||
StringUtils.isEmpty(site.getCity()) || StringUtils.isEmpty(site.getCityCode()) ||
StringUtils.isEmpty(site.getDistricts()) || StringUtils.isEmpty(site.getDistrictsCode())){
@@ -602,7 +816,7 @@
TVip vip = JSON.parseObject(vipJson, TVip.class);
if(Objects.nonNull(vip) && vip.getType() == 2){
list.forEach(item -> {
- if(Objects.nonNull(vip.getDiscount())){
+ if(Integer.valueOf(1).equals(item.getMemberDiscountFlag()) && Objects.nonNull(vip.getDiscount())){
item.setVipElectrovalence(vip.getDiscount().divide(new BigDecimal(10)).multiply(item.getServiceCharge()).add(item.getElectrovalenceOriginal()));
}
});
@@ -704,7 +918,9 @@
TVip vip = JSON.parseObject(vipJson, TVip.class);
if(Objects.nonNull(vip) && vip.getType() == 2){
list.forEach(item -> {
- item.setVipElectrovalence(vip.getDiscount().divide(new BigDecimal(10)).multiply(item.getServiceCharge()).add(item.getElectrovalenceOriginal()));
+ if (Integer.valueOf(1).equals(item.getMemberDiscountFlag())) {
+ item.setVipElectrovalence(vip.getDiscount().divide(new BigDecimal(10)).multiply(item.getServiceCharge()).add(item.getElectrovalenceOriginal()));
+ }
});
}
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java
index ededda2..67f8474 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java
@@ -2,11 +2,13 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.query.TAccountingStrategyQuery;
import com.ruoyi.chargingPile.api.vo.TAccountingStrategyVO;
import com.ruoyi.chargingPile.api.vo.TParkingLotVO;
import com.ruoyi.chargingPile.mapper.TAccountingStrategyMapper;
+import com.ruoyi.chargingPile.service.IPartnerService;
import com.ruoyi.chargingPile.service.TAccountingStrategyService;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.feignClient.OtherClient;
@@ -35,6 +37,8 @@
@Resource
private SysUserClient sysUserClient;
+ @Resource
+ private IPartnerService partnerService;
@Override
public PageInfo<TAccountingStrategyVO> pageList(TAccountingStrategyQuery query) {
PageInfo<TAccountingStrategyVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
@@ -67,6 +71,10 @@
tAccountingStrategyVO.setAuditStatus(auditRecord.getAuditStatus());
tAccountingStrategyVO.setAuditRecordId(auditRecord.getId());
tAccountingStrategyVO.setAuditParentId(auditRecord.getParentId());
+ tAccountingStrategyVO.setPartnerId(auditRecord.getPartnerId());
+ Partner partner = auditRecord.getPartnerId() == null
+ ? null : partnerService.getById(auditRecord.getPartnerId());
+ tAccountingStrategyVO.setPartnerName(partner == null ? null : partner.getName());
}
}
pageInfo.setRecords(list);
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 84c1971..4fdf178 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
@@ -130,10 +130,12 @@
<select id="pageList" resultType="com.ruoyi.chargingPile.api.vo.SiteVO">
select
- ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.status, ts.construction_site, ts.img_url,
+ ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.member_discount_flag as memberDiscountFlag, ts.status, ts.construction_site, ts.img_url,
ts.lon, ts.lat, ts.address, ts.country_code, ts.phone,ts.guide, ts.service_description, ts.vehicle_description,
ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id, ts.park_type as parkType, ts.park_fee as parkFee, ts.park_fee_free as parkFeeFree, ts.supportOrder as supportOrder, ts.equipment_owner_name as equipmentOwnerName, ts.rated_power as ratedPower, ts.del_flag,tcg.fastCount,
- tcg.slowCount,tcg.superCount,tasd.electrovalence,tasd.vipElectrovalence,tasd.serviceCharge,tasd.electrovalenceOriginal,tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount,
+ tcg.slowCount,tcg.superCount,tasd.electrovalence,
+ CASE WHEN ts.member_discount_flag = 1 THEN tasd.vipElectrovalence ELSE tasd.electrovalence END as vipElectrovalence,
+ tasd.serviceCharge,tasd.electrovalenceOriginal,tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount,
ROUND(
6378.138 * 2 * ASIN(
SQRT(
@@ -156,6 +158,7 @@
) * 1000
) AS distance
from t_site ts
+ left join t_partner tp on (ts.partner_id = tp.id)
left join (
select site_id,
SUM(CASE WHEN charge_mode = 1 THEN 1 ELSE 0 END) AS superCount,
@@ -194,6 +197,7 @@
AND ts.del_flag = ${@com.ruoyi.common.core.enums.DelFlagEnum@NO.getCode()}
AND ts.status = ${@com.ruoyi.common.core.enums.status.SiteStatusEnum@NORMAL_USE.getCode()}
AND ts.accounting_strategy_id IS NOT NULL
+ AND (tp.partner_type IS NULL OR tp.partner_type != 3)
</where>
<if test="query.sortType != null">
<choose>
@@ -210,7 +214,7 @@
</if>
</select>
<select id="getDetailById" resultType="com.ruoyi.chargingPile.api.vo.SiteDetailVO">
- SELECT id, partner_id, venue_partner_id, code, `name`, site_type, business_category, status, construction_site, img_url, province, province_code,
+ SELECT id, partner_id, venue_partner_id, code, `name`, site_type, business_category, member_discount_flag as memberDiscountFlag, status, construction_site, img_url, province, province_code,
city, city_code, districts, districts_code, lon, lat, address, country_code, phone, service_phone, guide, start_service_time,
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,
@@ -236,6 +240,7 @@
) * 1000
) AS distance
FROM t_site WHERE id = #{query.siteId} AND del_flag = ${@com.ruoyi.common.core.enums.DelFlagEnum@NO.getCode()}
+ AND NOT EXISTS (SELECT 1 FROM t_partner p WHERE p.id = t_site.partner_id AND p.partner_type = 3)
</select>
<select id="getSiteListParkLot" resultType="com.ruoyi.chargingPile.api.model.Site">
select ts.id, ts.partner_id, ts.venue_partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.status
@@ -265,10 +270,12 @@
</select>
<select id="getMapSiteList" resultType="com.ruoyi.chargingPile.api.vo.SiteVO">
select
- ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.status, ts.construction_site, ts.img_url,
+ ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.member_discount_flag as memberDiscountFlag, ts.status, ts.construction_site, ts.img_url,
ts.lon, ts.lat, ts.address, ts.country_code, ts.phone,ts.guide, ts.service_description, ts.vehicle_description,
ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id, ts.park_type as parkType, ts.park_fee as parkFee, ts.park_fee_free as parkFeeFree, ts.supportOrder as supportOrder, ts.equipment_owner_name as equipmentOwnerName, ts.rated_power as ratedPower, ts.del_flag,tcg.fastCount,
- tcg.slowCount,tcg.superCount,tasd.electrovalence,tasd.vipElectrovalence,tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount,
+ tcg.slowCount,tcg.superCount,tasd.electrovalence,
+ CASE WHEN ts.member_discount_flag = 1 THEN tasd.vipElectrovalence ELSE tasd.electrovalence END as vipElectrovalence,
+ tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount,
ROUND(
6378.138 * 2 * ASIN(
SQRT(
@@ -291,6 +298,7 @@
) * 1000
) AS distance
from t_site ts
+ left join t_partner tp on (ts.partner_id = tp.id)
left join (
select site_id,
SUM(CASE WHEN charge_mode = 1 THEN 1 ELSE 0 END) AS superCount,
@@ -328,6 +336,7 @@
</if>
AND ts.del_flag = ${@com.ruoyi.common.core.enums.DelFlagEnum@NO.getCode()}
AND ts.status = ${@com.ruoyi.common.core.enums.status.SiteStatusEnum@NORMAL_USE.getCode()}
+ AND (tp.partner_type IS NULL OR tp.partner_type != 3)
</where>
<if test="query.sortType != null">
<choose>
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 5a1e71a..5e8a42b 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
@@ -2,13 +2,17 @@
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.chargingPile.api.model.Site;
+import com.ruoyi.chargingPile.dto.BatchSetSiteAccountingStrategyDTO;
import com.ruoyi.chargingPile.service.ISiteService;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.web.domain.AjaxResult;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.bind.annotation.RequestBody;
import java.util.Collections;
+import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -36,4 +40,37 @@
assertTrue(result.getData().isEmpty());
verify(siteService, never()).list(any(Wrapper.class));
}
+
+ @Test
+ void batchSetAccountingStrategyUsesJsonRequestBody() throws NoSuchMethodException {
+ SiteController controller = new SiteController();
+ ISiteService siteService = mock(ISiteService.class);
+ ReflectionTestUtils.setField(controller, "siteService", siteService);
+ BatchSetSiteAccountingStrategyDTO request = new BatchSetSiteAccountingStrategyDTO();
+ request.setIds(Arrays.asList(11, 12));
+ request.setAccountingStrategyId(23);
+
+ controller.batchSetAccountingStrategy(request);
+
+ assertNotNull(SiteController.class
+ .getMethod("batchSetAccountingStrategy", BatchSetSiteAccountingStrategyDTO.class)
+ .getParameters()[0]
+ .getAnnotation(RequestBody.class));
+ verify(siteService).batchSetAccountingStrategy(Arrays.asList(11, 12), 23);
+ }
+
+ @Test
+ void batchSetAccountingStrategyRejectsEmptySiteSelection() {
+ SiteController controller = new SiteController();
+ ISiteService siteService = mock(ISiteService.class);
+ ReflectionTestUtils.setField(controller, "siteService", siteService);
+ BatchSetSiteAccountingStrategyDTO request = new BatchSetSiteAccountingStrategyDTO();
+ request.setIds(Collections.emptyList());
+ request.setAccountingStrategyId(23);
+
+ AjaxResult result = controller.batchSetAccountingStrategy(request);
+
+ assertTrue(result.isError());
+ verify(siteService, never()).batchSetAccountingStrategy(any(), any());
+ }
}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java
new file mode 100644
index 0000000..960d2a4
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java
@@ -0,0 +1,55 @@
+package com.ruoyi.chargingPile.dto;
+
+import com.ruoyi.chargingPile.api.model.Site;
+import com.ruoyi.common.core.annotation.Excel;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SiteExcelDTOTest {
+
+ @Test
+ void columnsFollowSitePrototypeOrder() {
+ List<String> actual = Arrays.stream(SiteExcelDTO.class.getDeclaredFields())
+ .map(field -> field.getAnnotation(Excel.class))
+ .filter(annotation -> annotation != null)
+ .map(Excel::name)
+ .collect(Collectors.toList());
+ assertEquals(Arrays.asList(
+ "*归属合作商/服务商", "场地方", "*站点编号", "*站点名称", "*站点类型", "*经营类别",
+ "*站点状态", "*建设场所", "*变压器功率", "*是否参与会员折扣", "*所在区域", "*详细地址",
+ "*经度", "*纬度", "*充电站国家代码", "*站点电话", "*服务电话", "站点引导", "运营时间",
+ "站内提供服务", "车型描述", "*车位数量", "*充电电费描述", "超时占位费描述", "计费模板",
+ "建站时间", "*排序", "备注", "*电站分类", "通用类型", "充换电站所在县以下行政区划代码",
+ "*7*24小时营业", "*停车费类型", "停车费描述", "服务车型描述", "支付方式", "是否支持预约",
+ "*电费类型", "*报装类型", "*报装电源容量", "*站点额定总功率", "*是否峰谷分时", "*正式投运时间",
+ "充换电站方位", "充换电站建设面积", "充换电站人工值守", "*视频监控配套情况", "周边配套设施",
+ "是否有小票机", "是否有道闸", "是否有地锁", "*设备所属方名称", "*供电局用户编号", "*供电类型",
+ "*表号", "*外电功率", "*服务费描述", "*充电站全省唯一备案号"
+ ), actual);
+ }
+
+ @Test
+ void exportRowContainsNewSiteFieldsAndNames() {
+ Site site = new Site();
+ site.setCode("SITE001");
+ site.setTransformerPower(new BigDecimal("630.00"));
+ site.setMemberDiscountFlag(1);
+ site.setDistrictsCode("510903");
+
+ SiteExcelDTO row = SiteExcelDTO.from(site, "合作商A", "场地方B", "计费模板C");
+
+ assertEquals("SITE001", row.getCode());
+ assertEquals(new BigDecimal("630.00"), row.getTransformerPower());
+ assertEquals(1, row.getMemberDiscountFlag());
+ assertEquals("510903", row.getAreaCode());
+ assertEquals("合作商A", row.getPartnerName());
+ assertEquals("场地方B", row.getVenuePartnerName());
+ assertEquals("计费模板C", row.getAccountingStrategyName());
+ }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java
new file mode 100644
index 0000000..f652bb8
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java
@@ -0,0 +1,70 @@
+package com.ruoyi.chargingPile.export;
+
+import com.ruoyi.chargingPile.api.dto.PageChargingPileListDTO;
+import com.ruoyi.chargingPile.api.model.TChargingPile;
+import com.ruoyi.chargingPile.api.vo.TChargingGunVO;
+import com.ruoyi.common.core.annotation.Excel;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ChargingPileExportTest {
+
+ @Test
+ void shouldMatchPrototypeColumnOrder() {
+ List<String> actual = Arrays.stream(ChargingPileExport.class.getDeclaredFields())
+ .map(field -> field.getAnnotation(Excel.class))
+ .filter(annotation -> annotation != null)
+ .map(Excel::name)
+ .collect(Collectors.toList());
+
+ assertEquals(Arrays.asList(
+ "*设备编码", "*设备名称", "*桩号", "设备类型", "归属电站", "设备合作商",
+ "生产机构代码", "生产商名称", "设备型号", "设备生产日期", "*额定功率",
+ "设备充电站编号", "设备唯一编码", "*设备分类", "设备经度", "设备纬度",
+ "*接口编码", "*接口名称", "*设备接口分类", "*接口类型", "*接口状态", "*充电方式",
+ "计费策略", "*额定电压上限", "*额定电压下限", "恒功率电压上限", "恒功率电压下限",
+ "*额定电流", "恒功率电流上限", "恒功率电流下限", "*额定功率", "车位号",
+ "车位状态", "地锁状态", "*国家标准", "接口说明", "辅助电源", "*运营状态", "运营时间"
+ ), actual);
+ }
+
+ @Test
+ void shouldCombinePileAndGunIntoOneExportRow() {
+ PageChargingPileListDTO record = new PageChargingPileListDTO();
+ record.setSiteName("测试电站");
+ record.setPartnerName("测试合作商");
+ TChargingPile pile = new TChargingPile();
+ pile.setCode("PILE001");
+ pile.setName("1号桩");
+ pile.setNumber(1);
+ pile.setManufacturerCode("ORG001");
+ pile.setProductionDate(LocalDate.of(2026, 7, 16));
+ TChargingGunVO gun = new TChargingGunVO();
+ gun.setCode("01");
+ gun.setName("1号枪");
+ gun.setStrategyName("标准计费");
+ gun.setRatedPower(new BigDecimal("120"));
+ gun.setOpreateStatus(50);
+
+ ChargingPileExport actual = ChargingPileExport.from(record, pile, gun);
+
+ assertEquals("PILE001", actual.getCode());
+ assertEquals("测试电站", actual.getSiteName());
+ assertEquals("测试合作商", actual.getPartnerName());
+ assertEquals("ORG001", actual.getManufacturerCode());
+ assertEquals("2026-07-16", actual.getProductionDate());
+ assertEquals("01", actual.getInterfaceCode());
+ assertEquals("1号枪", actual.getInterfaceName());
+ assertEquals("标准计费", actual.getStrategyName());
+ assertEquals(new BigDecimal("120"), actual.getInterfaceRatedPower());
+ assertEquals(50, actual.getOperateStatus());
+ }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java
new file mode 100644
index 0000000..c5001a0
--- /dev/null
+++ b/ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java
@@ -0,0 +1,23 @@
+package com.ruoyi.chargingPile.mapper;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SiteMapperVisibilityTest {
+
+ @Test
+ void miniProgramQueriesExcludeServiceProviderSitesAndExposeMemberDiscountFlag() throws Exception {
+ String mapper = new String(Files.readAllBytes(Paths.get(getClass().getClassLoader()
+ .getResource("mapper/chargingPile/SiteMapper.xml").toURI())), StandardCharsets.UTF_8);
+
+ assertTrue(mapper.contains("tp.partner_type != 3"));
+ assertTrue(mapper.contains("p.partner_type = 3"));
+ assertTrue(mapper.contains("member_discount_flag as memberDiscountFlag"));
+ assertTrue(mapper.contains("CASE WHEN ts.member_discount_flag = 1 THEN tasd.vipElectrovalence"));
+ }
+}
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 ae17d17..6380e03 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,11 +1,22 @@
package com.ruoyi.chargingPile.service.impl;
import com.ruoyi.chargingPile.api.model.Partner;
+import com.ruoyi.chargingPile.mapper.PartnerMapper;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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;
class PartnerServiceImplTest {
@@ -22,4 +33,48 @@
assertTrue(new PartnerServiceImpl().addPartnerParameterCheck(partner).isSuccess());
}
+
+ @Test
+ void venuePartnerFallsBackToPartnerRoleWhenDedicatedRoleIsMissing() {
+ PartnerServiceImpl service = new PartnerServiceImpl();
+ PartnerMapper mapper = mock(PartnerMapper.class);
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ when(mapper.getDefaultPartnerRoleId("场地方")).thenReturn(null);
+ when(mapper.getDefaultPartnerRoleId("合作商")).thenReturn(7);
+
+ assertEquals(7, service.resolveDefaultRoleId(2));
+ verify(mapper).getDefaultPartnerRoleId("场地方");
+ verify(mapper).getDefaultPartnerRoleId("合作商");
+ }
+
+ @Test
+ void serviceProviderUsesDedicatedRoleWhenItExists() {
+ PartnerServiceImpl service = new PartnerServiceImpl();
+ PartnerMapper mapper = mock(PartnerMapper.class);
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ when(mapper.getDefaultPartnerRoleId("服务商")).thenReturn(9);
+
+ assertEquals(9, service.resolveDefaultRoleId(3));
+ verify(mapper).getDefaultPartnerRoleId("服务商");
+ verify(mapper, never()).getDefaultPartnerRoleId("合作商");
+ }
+
+ @Test
+ void clearingShareRatiosStillProducesExplicitDatabaseUpdate() {
+ PartnerServiceImpl service = new PartnerServiceImpl();
+ PartnerMapper mapper = mock(PartnerMapper.class);
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ Partner partner = new Partner();
+ partner.setId(8);
+ partner.setElectricityShareRatio(null);
+ partner.setServiceShareRatio(null);
+
+ service.updateShareRatios(partner);
+
+ ArgumentCaptor<Wrapper<Partner>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
+ verify(mapper).update(isNull(), wrapperCaptor.capture());
+ UpdateWrapper<Partner> wrapper = (UpdateWrapper<Partner>) wrapperCaptor.getValue();
+ assertTrue(wrapper.getSqlSet().contains("electricity_share_ratio"));
+ assertTrue(wrapper.getSqlSet().contains("service_share_ratio"));
+ }
}
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 9c4a40e..18d0dca 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
@@ -703,8 +703,6 @@
@PostMapping(value = "/chargingOrder")
@ApiOperation(value = "充电桩订单列表", tags = {"管理后台-订单管理"})
public AjaxResult<TCharingOrderVO> chargingOrder(@RequestBody ChargingOrderQuery dto) {
- List<Long> data = appUserClient.getUserIdsByPhone(dto.getPhone()).getData();
- dto.setUserIds(data);
TCharingOrderVO res = chargingOrderService.chargingOrder(dto);
return AjaxResult.success(res);
}
@@ -954,6 +952,13 @@
return chargingOrderService.paymentChargingOrder(addChargingOrder);
}
+ @ResponseBody
+ @GetMapping(value = "/confirmIcbcPayment")
+ @ApiOperation(value = "主动确认工行充电支付结果", tags = {"小程序-扫一扫"})
+ public AjaxResult confirmIcbcPayment(@RequestParam("orderCode") String orderCode) {
+ return chargingOrderService.confirmIcbcPayment(orderCode);
+ }
+
@RequiresPermissions(value = {"/chargingPileOrder", "/paymentOrder/refund"}, logical = Logical.AND)
@PostMapping("/icbc/fund/payment-query")
@ApiOperation(value = "管理后台查询工行支付订单", tags = {"后台-充电桩订单"})
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 90878cb..58639b2 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
@@ -72,6 +72,8 @@
*/
AjaxResult paymentChargingOrder(AddChargingOrder addChargingOrder);
+ AjaxResult confirmIcbcPayment(String orderCode);
+
ChargingSimulationVO getCurrentSimulationOrder(Integer gunId);
ChargingSimulationVO completeSimulation(ChargingSimulationReq request);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java
new file mode 100644
index 0000000..24be087
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java
@@ -0,0 +1,47 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.other.api.domain.TCoupon;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+final class ChargingCouponSupport {
+ private static final BigDecimal TEN = new BigDecimal("10");
+
+ private ChargingCouponSupport() {
+ }
+
+ static BigDecimal calculateUpfrontDiscount(TCoupon coupon, BigDecimal rechargeAmount) {
+ if (coupon == null || rechargeAmount == null || rechargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ if (!Integer.valueOf(1).equals(coupon.getType())) {
+ throw new IllegalArgumentException("请选择充电优惠券");
+ }
+ BigDecimal threshold = coupon.getMeetTheConditions() == null
+ ? BigDecimal.ZERO : coupon.getMeetTheConditions();
+ if (rechargeAmount.compareTo(threshold) < 0) {
+ throw new IllegalArgumentException("预付金额未达到优惠券使用门槛");
+ }
+
+ BigDecimal discount;
+ if (Integer.valueOf(1).equals(coupon.getPreferentialMode())) {
+ discount = coupon.getDiscountAmount();
+ } else if (Integer.valueOf(2).equals(coupon.getPreferentialMode())) {
+ if (coupon.getDiscount() == null) {
+ throw new IllegalArgumentException("优惠券折扣配置无效");
+ }
+ discount = rechargeAmount.multiply(TEN.subtract(coupon.getDiscount()))
+ .divide(TEN, 2, RoundingMode.HALF_UP);
+ if (coupon.getMaximumDiscountAmount() != null) {
+ discount = discount.min(coupon.getMaximumDiscountAmount());
+ }
+ } else {
+ throw new IllegalArgumentException("优惠券优惠方式无效");
+ }
+ if (discount == null || discount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("优惠券优惠金额无效");
+ }
+ return discount.min(rechargeAmount).setScale(2, RoundingMode.HALF_UP);
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java
new file mode 100644
index 0000000..8d02d16
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java
@@ -0,0 +1,23 @@
+package com.ruoyi.order.service.impl;
+
+import java.math.BigDecimal;
+
+final class ChargingPaymentSupport {
+ private ChargingPaymentSupport() {
+ }
+
+ static int resolvePayMethod(BigDecimal balancePayAmount, BigDecimal thirdPayAmount, int fallback) {
+ boolean hasBalance = balancePayAmount != null && balancePayAmount.compareTo(BigDecimal.ZERO) > 0;
+ boolean hasThirdPay = thirdPayAmount != null && thirdPayAmount.compareTo(BigDecimal.ZERO) > 0;
+ if (hasBalance && hasThirdPay) {
+ return 2;
+ }
+ if (hasBalance) {
+ return 1;
+ }
+ if (hasThirdPay) {
+ return 3;
+ }
+ return fallback;
+ }
+}
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 0703407..e9311f9 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
@@ -662,6 +662,27 @@
return chargingOrder != null && "ICBC".equals(chargingOrder.getSourceName());
}
+ private BigDecimal resolveUpfrontCouponDiscount(Long appCouponId, Long userId, BigDecimal rechargeAmount) {
+ TAppCoupon appCoupon = appCouponClient.getAppCouponById(appCouponId).getData();
+ if (appCoupon == null || !userId.equals(appCoupon.getAppUserId())) {
+ throw new ServiceException("优惠券不存在或不属于当前用户");
+ }
+ if (!Integer.valueOf(1).equals(appCoupon.getStatus())) {
+ throw new ServiceException("优惠券已使用");
+ }
+ LocalDateTime now = LocalDateTime.now();
+ if ((appCoupon.getStartTime() != null && appCoupon.getStartTime().isAfter(now))
+ || (appCoupon.getEndTime() != null && appCoupon.getEndTime().isBefore(now))) {
+ throw new ServiceException("优惠券不在有效期内");
+ }
+ try {
+ TCoupon coupon = JSON.parseObject(appCoupon.getCouponJson(), TCoupon.class);
+ return ChargingCouponSupport.calculateUpfrontDiscount(coupon, rechargeAmount);
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ }
+
/**
* 判断订单是否为白名单订单
*
@@ -733,74 +754,36 @@
AppUserWhitelistMatchVO whitelist = appUserWhitelistClient.matchWhitelist(userId, tChargingGun.getSiteId()).getData();
boolean whitelistMatched = whitelist != null && Boolean.TRUE.equals(whitelist.getMatched());
if (whitelistMatched) {
- payMethod = 3;
- addChargingOrder.setPayMethod(3);
addChargingOrder.setAppUserCouponId(null);
}
+ boolean whitelistAllFree = whitelistMatched
+ && Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(whitelist.getBenefitType());
BigDecimal userBalance = money(appUser.getBalance());
BigDecimal balancePayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
- BigDecimal thirdPayAmount = rechargeAmount;
+ BigDecimal upfrontCouponDiscount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
if (payMethod == 2 && userBalance.compareTo(rechargeAmount) >= 0) {
payMethod = 1;
addChargingOrder.setPayMethod(1);
}
if (isBalancePayMode(payMethod)) {
addChargingOrder.setAppUserCouponId(null);
- if (payMethod == 1 && userBalance.compareTo(rechargeAmount) < 0) {
+ } else if (!whitelistMatched && addChargingOrder.getAppUserCouponId() != null) {
+ upfrontCouponDiscount = resolveUpfrontCouponDiscount(
+ addChargingOrder.getAppUserCouponId(), userId, rechargeAmount);
+ }
+ BigDecimal payableAmount = whitelistAllFree
+ ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
+ : rechargeAmount.subtract(upfrontCouponDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal thirdPayAmount = payableAmount;
+ if (isBalancePayMode(payMethod)) {
+ if (payMethod == 1 && userBalance.compareTo(payableAmount) < 0) {
return AjaxResult.error("余额不足,请选择组合支付或第三方支付");
}
- balancePayAmount = payMethod == 1 ? rechargeAmount : userBalance.min(rechargeAmount).setScale(2, RoundingMode.HALF_UP);
- thirdPayAmount = rechargeAmount.subtract(balancePayAmount).setScale(2, RoundingMode.HALF_UP);
+ balancePayAmount = payMethod == 1 ? payableAmount : userBalance.min(payableAmount).setScale(2, RoundingMode.HALF_UP);
+ thirdPayAmount = payableAmount.subtract(balancePayAmount).setScale(2, RoundingMode.HALF_UP);
}
- if (whitelistMatched) {
- balancePayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
- thirdPayAmount = Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(whitelist.getBenefitType())
- ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
- : rechargeAmount;
- }
- //查询待支付状态的订单,没有支付则删除订单
- List<TChargingOrder> list = this.list(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getAppUserId, userId)
- .eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
- for (TChargingOrder tChargingOrder : list) {
- Integer rechargePaymentType = tChargingOrder.getRechargePaymentType();
- if ("ICBC".equals(tChargingOrder.getSourceName())) {
- IcbcPaymentResp data = icbcPaymentClient.query(tChargingOrder.getCode()).getData();
- if (null != data && isIcbcRemovableStatus(data.getStatus(), tChargingOrder)) {
- this.removeById(tChargingOrder.getId());
- }
- }
- if (!"ICBC".equals(tChargingOrder.getSourceName()) && 1 == rechargePaymentType) {
- NotifyV3PayDecodeRespBody data = wxPaymentClient.queryOrderInfo(tChargingOrder.getCode()).getData();
- if (null != data) {
- String trade_state = data.getTrade_state();
- //支付失败,删除无效的订单
- if (trade_state.equals("REFUND") || trade_state.equals("NOTPAY") || trade_state.equals("REVOKED") || trade_state.equals("PAYERROR")) {
- this.removeById(tChargingOrder.getId());
- }
- }
- }
- if (!"ICBC".equals(tChargingOrder.getSourceName()) && 2 == rechargePaymentType) {
- AliQueryOrder data = aliPaymentClient.query(tChargingOrder.getCode()).getData();
- if (null != data) {
- //支付失败,删除无效的订单
- String tradeStatus = data.getTradeStatus();
- if (null != tradeStatus && tradeStatus.equals("TRADE_CLOSED")) {
- this.removeById(tChargingOrder.getId());
- }
- }
- }
- if (!"ICBC".equals(tChargingOrder.getSourceName()) && 3 == rechargePaymentType) {
- AliQueryOrder data = h5AliPaymentClient.query(tChargingOrder.getCode()).getData();
- if (null != data) {
- //支付失败,删除无效的订单
- String tradeStatus = data.getTradeStatus();
- if (null != tradeStatus && tradeStatus.equals("TRADE_CLOSED")) {
- this.removeById(tChargingOrder.getId());
- }
- }
- }
- }
-
+ payMethod = ChargingPaymentSupport.resolvePayMethod(balancePayAmount, thirdPayAmount, payMethod);
+ addChargingOrder.setPayMethod(payMethod);
//检查当前枪是否是正在使用中
TChargingOrder one = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getChargingGunId, addChargingOrder.getId())
.in(TChargingOrder::getStatus, Arrays.asList(3)).eq(TChargingOrder::getDelFlag, 0));
@@ -809,7 +792,8 @@
}
//查询当前充电枪是否有正在支付的订单
one = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getChargingGunId, addChargingOrder.getId())
- .eq(TChargingOrder::getAppUserCarId, userId).eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
+ .eq(TChargingOrder::getAppUserId, userId).eq(TChargingOrder::getRechargePaymentStatus, 1)
+ .eq(TChargingOrder::getDelFlag, 0).last("limit 1"));
if (null != one) {
//查询三方支付数据,支付中直接结束三方订单
if ("ICBC".equals(one.getSourceName())) {
@@ -893,6 +877,7 @@
chargingOrder.setThirdRefundAmount(BigDecimal.ZERO);
chargingOrder.setRechargeAmount(rechargeAmount);
chargingOrder.setAppCouponId(addChargingOrder.getAppUserCouponId());
+ chargingOrder.setCouponDiscountAmount(upfrontCouponDiscount);
chargingOrder.setIsWhitelist(whitelistMatched ? 1 : 0);
chargingOrder.setWhitelistId(whitelistMatched ? whitelist.getWhitelistId() : null);
chargingOrder.setWhitelistBenefitType(whitelistMatched ? whitelist.getBenefitType() : null);
@@ -919,7 +904,8 @@
TAccountingStrategy accountingStrategy = accountingStrategyClient.getAccountingStrategyById(accountingStrategyId).getData();
//会员折扣支持直营网点和加盟商站点
- if (!whitelistMatched && !isBalancePayMode(payMethod) && null != appUser.getVipId()) {
+ if (!whitelistMatched && Integer.valueOf(1).equals(site.getMemberDiscountFlag())
+ && !isBalancePayMode(payMethod) && null != appUser.getVipId()) {
GetAppUserVipDetail getAppUserVipDetail = new GetAppUserVipDetail();
getAppUserVipDetail.setAppUserId(appUser.getId());
getAppUserVipDetail.setVipId(appUser.getVipId());
@@ -1146,6 +1132,43 @@
public IcbcPaymentResp queryIcbcAdminPayment(String orderCode) {
TChargingOrder order = loadIcbcAdminOrder(orderCode, false);
return requireFeignData(icbcPaymentClient.query(order.getCode()), "工行支付查询失败");
+ }
+
+ @Transactional
+ @Override
+ public AjaxResult confirmIcbcPayment(String orderCode) {
+ if (!StringUtils.hasLength(orderCode)) {
+ return AjaxResult.error("订单号不能为空");
+ }
+ Long userId = tokenService.getLoginUserApplet().getUserId();
+ TChargingOrder order = this.getOne(new LambdaQueryWrapper<TChargingOrder>()
+ .eq(TChargingOrder::getCode, orderCode)
+ .eq(TChargingOrder::getAppUserId, userId)
+ .eq(TChargingOrder::getDelFlag, 0)
+ .last("limit 1"));
+ if (order == null) {
+ return AjaxResult.error("支付订单不存在");
+ }
+ if (Integer.valueOf(2).equals(order.getRechargePaymentStatus())) {
+ return AjaxResult.success(Collections.singletonMap("confirmed", true));
+ }
+ if (!isIcbcOrder(order)) {
+ return AjaxResult.error("当前订单不是工行支付订单");
+ }
+
+ R<IcbcPaymentResp> queryResult = icbcPaymentClient.query(orderCode);
+ IcbcPaymentResp payment = queryResult == null ? null : queryResult.getData();
+ if (!IcbcAdminFundOperationSupport.isBankPaid(payment)
+ || payment == null
+ || !orderCode.equals(payment.getOrderId())
+ || !StringUtils.hasLength(payment.getTradeNo())) {
+ return AjaxResult.success(Collections.singletonMap("confirmed", false));
+ }
+ AjaxResult callbackResult = chargingOrderCallback(1, orderCode, payment.getTradeNo(), "1");
+ if (!Integer.valueOf(200).equals(callbackResult.get("code"))) {
+ return callbackResult;
+ }
+ return AjaxResult.success(Collections.singletonMap("confirmed", true));
}
@Override
@@ -4104,14 +4127,18 @@
}
//计算优惠券
- BigDecimal couponDiscount = BigDecimal.ZERO;
+ boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0;
+ BigDecimal couponDiscount = couponAppliedUpfront
+ ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO;
if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != chargingOrder.getAppCouponId()) {
- //判断实际充电金额是否满足优惠券使用条件,如果不满足则不适用优惠券。
TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData();
String couponJson = appCoupon.getCouponJson();
TCoupon tCoupon = JSON.parseObject(couponJson, TCoupon.class);
Integer preferentialMode = tCoupon.getPreferentialMode();
- if (1 == preferentialMode) {
+ if (couponAppliedUpfront) {
+ appCoupon.setStatus(2);
+ appCouponClient.updateAppCoupon(appCoupon);
+ } else if (1 == preferentialMode) {
//满减
if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount();
@@ -4130,7 +4157,7 @@
chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
}
}
- if (2 == preferentialMode) {
+ if (!couponAppliedUpfront && 2 == preferentialMode) {
//抵扣
if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
//折扣金额
@@ -4166,13 +4193,13 @@
}
//优惠券优惠金额
couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN);
- //退款金额+优惠券
- refundAmount = refundAmount.add(couponDiscount);
- //实际支付金额-优惠券
- payAmount = payAmount.subtract(couponDiscount);
- if (serviceCharge_total.compareTo(BigDecimal.ZERO) > 0) {
- serviceCharge_total = serviceCharge_total.subtract(couponDiscount);
+ //历史订单在结算时计算优惠;新订单已在预付时少收优惠金额,不能再次增加退款。
+ if (!couponAppliedUpfront) {
+ refundAmount = refundAmount.add(couponDiscount);
}
+ //实际支付金额-优惠券
+ payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO);
+ serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO);
chargingOrder.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN));
chargingOrder.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN));
chargingOrder.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN));
@@ -6495,14 +6522,18 @@
}
//计算优惠券
- BigDecimal couponDiscount = BigDecimal.ZERO;
+ boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0;
+ BigDecimal couponDiscount = couponAppliedUpfront
+ ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO;
if (null != chargingOrder.getAppCouponId()) {
- //判断实际充电金额是否满足优惠券使用条件,如果不满足则不适用优惠券。
TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData();
String couponJson = appCoupon.getCouponJson();
TCoupon tCoupon = JSON.parseObject(couponJson, TCoupon.class);
Integer preferentialMode = tCoupon.getPreferentialMode();
- if (1 == preferentialMode) {
+ if (couponAppliedUpfront) {
+ appCoupon.setStatus(2);
+ appCouponClient.updateAppCoupon(appCoupon);
+ } else if (1 == preferentialMode) {
//满减
if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount();
@@ -6521,7 +6552,7 @@
chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
}
}
- if (2 == preferentialMode) {
+ if (!couponAppliedUpfront && 2 == preferentialMode) {
//抵扣
if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
//折扣金额
@@ -6546,13 +6577,12 @@
}
//优惠券优惠金额
couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN);
- //退款金额+优惠券
- refundAmount = refundAmount.add(couponDiscount);
- //实际支付金额-优惠券
- payAmount = payAmount.subtract(couponDiscount);
- if (serviceCharge_total.compareTo(BigDecimal.ZERO) > 0) {
- serviceCharge_total = serviceCharge_total.subtract(couponDiscount);
+ if (!couponAppliedUpfront) {
+ refundAmount = refundAmount.add(couponDiscount);
}
+ //实际支付金额-优惠券
+ payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO);
+ serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO);
chargingOrder.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN));
chargingOrder.setServiceCharge(serviceCharge_total.setScale(2, RoundingMode.HALF_EVEN));
chargingOrder.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN));
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java
new file mode 100644
index 0000000..4e4614a
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java
@@ -0,0 +1,37 @@
+package com.ruoyi.order.controller;
+
+import com.ruoyi.account.api.feignClient.AppUserClient;
+import com.ruoyi.order.api.query.ChargingOrderQuery;
+import com.ruoyi.order.api.vo.TCharingOrderVO;
+import com.ruoyi.order.service.TChargingOrderService;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class TChargingOrderControllerTest {
+
+ @Test
+ void blankPhoneDoesNotExpandQueryToAllUserIds() {
+ TChargingOrderService chargingOrderService = mock(TChargingOrderService.class);
+ AppUserClient appUserClient = mock(AppUserClient.class);
+ TChargingOrderController controller = new TChargingOrderController();
+ ReflectionTestUtils.setField(controller, "chargingOrderService", chargingOrderService);
+ ReflectionTestUtils.setField(controller, "appUserClient", appUserClient);
+
+ ChargingOrderQuery query = new ChargingOrderQuery();
+ query.setPhone("");
+ when(chargingOrderService.chargingOrder(same(query))).thenReturn(new TCharingOrderVO());
+
+ controller.chargingOrder(query);
+
+ assertNull(query.getUserIds());
+ verify(chargingOrderService).chargingOrder(same(query));
+ verifyNoInteractions(appUserClient);
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java
new file mode 100644
index 0000000..602ffcd
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java
@@ -0,0 +1,55 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.other.api.domain.TCoupon;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ChargingCouponSupportTest {
+
+ @Test
+ void fullReductionDiscountsThePrepaymentBeforeThirdPartyPay() {
+ TCoupon coupon = chargingCoupon(1);
+ coupon.setMeetTheConditions(new BigDecimal("3"));
+ coupon.setDiscountAmount(new BigDecimal("1"));
+
+ BigDecimal discount = ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("3"));
+
+ assertEquals(new BigDecimal("1.00"), discount);
+ assertEquals(new BigDecimal("2.00"), new BigDecimal("3").subtract(discount));
+ }
+
+ @Test
+ void percentageDiscountHonorsMaximumDiscount() {
+ TCoupon coupon = chargingCoupon(2);
+ coupon.setMeetTheConditions(new BigDecimal("10"));
+ coupon.setDiscount(new BigDecimal("8"));
+ coupon.setMaximumDiscountAmount(new BigDecimal("1.50"));
+
+ BigDecimal discount = ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("10"));
+
+ assertEquals(new BigDecimal("1.50"), discount);
+ }
+
+ @Test
+ void rejectsPrepaymentBelowCouponThreshold() {
+ TCoupon coupon = chargingCoupon(1);
+ coupon.setMeetTheConditions(new BigDecimal("3"));
+ coupon.setDiscountAmount(new BigDecimal("1"));
+
+ IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+ () -> ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("2.99")));
+
+ assertEquals("预付金额未达到优惠券使用门槛", error.getMessage());
+ }
+
+ private TCoupon chargingCoupon(int preferentialMode) {
+ TCoupon coupon = new TCoupon();
+ coupon.setType(1);
+ coupon.setPreferentialMode(preferentialMode);
+ return coupon;
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java
new file mode 100644
index 0000000..ca33620
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java
@@ -0,0 +1,34 @@
+package com.ruoyi.order.service.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ChargingPaymentSupportTest {
+
+ @Test
+ void classifiesPartialBalanceAsCombinationPayment() {
+ assertEquals(2, ChargingPaymentSupport.resolvePayMethod(
+ new BigDecimal("1.00"), new BigDecimal("2.00"), 3));
+ }
+
+ @Test
+ void classifiesZeroBalanceCombinationSelectionAsThirdPartyPayment() {
+ assertEquals(3, ChargingPaymentSupport.resolvePayMethod(
+ BigDecimal.ZERO, new BigDecimal("3.00"), 2));
+ }
+
+ @Test
+ void classifiesFullyCoveredPaymentAsBalancePayment() {
+ assertEquals(1, ChargingPaymentSupport.resolvePayMethod(
+ new BigDecimal("3.00"), BigDecimal.ZERO, 2));
+ }
+
+ @Test
+ void preservesSelectedBalanceMethodForAllFreeWhitelistPayment() {
+ assertEquals(1, ChargingPaymentSupport.resolvePayMethod(
+ BigDecimal.ZERO, BigDecimal.ZERO, 1));
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java
new file mode 100644
index 0000000..e49bf0b
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java
@@ -0,0 +1,99 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.payment.api.feignClient.IcbcPaymentClient;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.system.api.model.LoginUserApplet;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TChargingOrderPaymentConfirmationTest {
+ private static final String ORDER_CODE = "CD202607160001";
+ private static final String BANK_ORDER_ID = "231011990056000012345";
+
+ private TChargingOrderServiceImpl service;
+ private IcbcPaymentClient icbcPaymentClient;
+
+ @BeforeEach
+ void setUp() {
+ service = spy(new TChargingOrderServiceImpl());
+ TokenService tokenService = mock(TokenService.class);
+ icbcPaymentClient = mock(IcbcPaymentClient.class);
+ LoginUserApplet loginUser = new LoginUserApplet();
+ loginUser.setUserId(88L);
+ when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
+ ReflectionTestUtils.setField(service, "tokenService", tokenService);
+ ReflectionTestUtils.setField(service, "icbcPaymentClient", icbcPaymentClient);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void paidBankOrderActivelyRunsBusinessCallback() {
+ TChargingOrder order = pendingIcbcOrder();
+ doReturn(order).when(service).getOne(any(Wrapper.class));
+ IcbcPaymentResp payment = bankPayment("0");
+ when(icbcPaymentClient.query(ORDER_CODE)).thenReturn(R.ok(payment));
+ doReturn(AjaxResult.success()).when(service)
+ .chargingOrderCallback(1, ORDER_CODE, BANK_ORDER_ID, "1");
+
+ AjaxResult result = service.confirmIcbcPayment(ORDER_CODE);
+
+ Map<String, Object> data = (Map<String, Object>) result.get("data");
+ assertEquals(200, result.get("code"));
+ assertTrue((Boolean) data.get("confirmed"));
+ verify(service).chargingOrderCallback(1, ORDER_CODE, BANK_ORDER_ID, "1");
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void pendingBankOrderReturnsUnconfirmedWithoutRunningCallback() {
+ doReturn(pendingIcbcOrder()).when(service).getOne(any(Wrapper.class));
+ when(icbcPaymentClient.query(ORDER_CODE)).thenReturn(R.ok(bankPayment("1")));
+
+ AjaxResult result = service.confirmIcbcPayment(ORDER_CODE);
+
+ Map<String, Object> data = (Map<String, Object>) result.get("data");
+ assertEquals(200, result.get("code"));
+ assertFalse((Boolean) data.get("confirmed"));
+ verify(service, never()).chargingOrderCallback(eq(1), eq(ORDER_CODE), any(), eq("1"));
+ }
+
+ private TChargingOrder pendingIcbcOrder() {
+ TChargingOrder order = new TChargingOrder();
+ order.setCode(ORDER_CODE);
+ order.setAppUserId(88L);
+ order.setSourceName("ICBC");
+ order.setRechargePaymentStatus(1);
+ order.setDelFlag(false);
+ return order;
+ }
+
+ private IcbcPaymentResp bankPayment(String payStatus) {
+ IcbcPaymentResp payment = new IcbcPaymentResp();
+ payment.setOrderId(ORDER_CODE);
+ payment.setTradeNo(BANK_ORDER_ID);
+ payment.setStatus(payStatus);
+ payment.setRawResponse("{\"returnCode\":0,\"pay_status\":\"" + payStatus
+ + "\",\"out_trade_no\":\"" + ORDER_CODE + "\",\"order_id\":\"" + BANK_ORDER_ID + "\"}");
+ return payment;
+ }
+}
diff --git a/sql/20260715_reservation_ahead_minute.sql b/sql/20260715_reservation_ahead_minute.sql
new file mode 100644
index 0000000..3a4bcaa
--- /dev/null
+++ b/sql/20260715_reservation_ahead_minute.sql
@@ -0,0 +1,3 @@
+-- 预约充电:最早可提前预约分钟数
+ALTER TABLE `t_site`
+ ADD COLUMN `reservation_ahead_minute` int NULL COMMENT '最早可提前预约分钟数' AFTER `reservation_end_time`;
diff --git a/sql/20260715_sys_faq_missing_table_fix.sql b/sql/20260715_sys_faq_missing_table_fix.sql
new file mode 100644
index 0000000..1129462
--- /dev/null
+++ b/sql/20260715_sys_faq_missing_table_fix.sql
@@ -0,0 +1,104 @@
+-- 常见问题管理缺表修复
+-- 报错示例:Object 'sys_faq' not found within 'charging_pile_other'
+-- 请在 charging_pile_other 库执行;脚本可重复执行,不会删除已有数据。
+
+USE `charging_pile_other`;
+
+CREATE TABLE IF NOT EXISTS `sys_faq` (
+ `faq_id` bigint NOT NULL AUTO_INCREMENT COMMENT '问题ID',
+ `question` varchar(500) NOT NULL COMMENT '问题标题',
+ `answer` text COMMENT '问题答案',
+ `video_url` varchar(500) DEFAULT NULL COMMENT '视频教程URL',
+ `sort_order` int DEFAULT '0' COMMENT '排序号',
+ `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+ PRIMARY KEY (`faq_id`),
+ KEY `idx_sys_faq_status_del` (`status`, `del_flag`),
+ KEY `idx_sys_faq_sort` (`sort_order`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='常见问题表';
+
+SET @faq_video_url_col_exists := (
+ SELECT COUNT(1)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'sys_faq'
+ AND COLUMN_NAME = 'video_url'
+);
+
+SET @faq_add_video_url_sql := IF(
+ @faq_video_url_col_exists = 0,
+ 'ALTER TABLE `sys_faq` ADD COLUMN `video_url` varchar(500) DEFAULT NULL COMMENT ''视频教程URL'' AFTER `answer`',
+ 'SELECT 1'
+);
+
+PREPARE faq_add_video_url_stmt FROM @faq_add_video_url_sql;
+EXECUTE faq_add_video_url_stmt;
+DEALLOCATE PREPARE faq_add_video_url_stmt;
+
+INSERT INTO `sys_faq` (
+ `question`,
+ `answer`,
+ `sort_order`,
+ `status`,
+ `create_by`,
+ `create_time`,
+ `del_flag`
+)
+SELECT
+ '如何使用充电桩?',
+ '1. 停好车辆;2. 扫码启动充电桩;3. 连接充电枪;4. 开始充电。',
+ 1,
+ '0',
+ 'admin',
+ NOW(),
+ '0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_faq` WHERE `question` = '如何使用充电桩?' AND `del_flag` = '0'
+);
+
+INSERT INTO `sys_faq` (
+ `question`,
+ `answer`,
+ `sort_order`,
+ `status`,
+ `create_by`,
+ `create_time`,
+ `del_flag`
+)
+SELECT
+ '充电费用如何计算?',
+ '充电费用=充电电量×电价+服务费,具体以实际订单为准。',
+ 2,
+ '0',
+ 'admin',
+ NOW(),
+ '0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_faq` WHERE `question` = '充电费用如何计算?' AND `del_flag` = '0'
+);
+
+INSERT INTO `sys_faq` (
+ `question`,
+ `answer`,
+ `sort_order`,
+ `status`,
+ `create_by`,
+ `create_time`,
+ `del_flag`
+)
+SELECT
+ '充电过程中可以拔枪吗?',
+ '充电过程中请勿随意拔枪,如需停止充电请先在APP或充电桩上停止充电后再拔枪。',
+ 3,
+ '0',
+ 'admin',
+ NOW(),
+ '0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_faq` WHERE `question` = '充电过程中可以拔枪吗?' AND `del_flag` = '0'
+);
diff --git a/sql/20260716_site_transformer_member_discount.sql b/sql/20260716_site_transformer_member_discount.sql
new file mode 100644
index 0000000..207aaca
--- /dev/null
+++ b/sql/20260716_site_transformer_member_discount.sql
@@ -0,0 +1,33 @@
+SET @schema_name = DATABASE();
+
+SET @transformer_power_exists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @schema_name
+ AND TABLE_NAME = 't_site'
+ AND COLUMN_NAME = 'transformer_power'
+);
+SET @transformer_power_sql = IF(
+ @transformer_power_exists = 0,
+ 'ALTER TABLE `t_site` ADD COLUMN `transformer_power` decimal(12,2) NULL DEFAULT NULL COMMENT ''变压器功率(kW)'' AFTER `construction_site`',
+ 'SELECT 1'
+);
+PREPARE transformer_power_stmt FROM @transformer_power_sql;
+EXECUTE transformer_power_stmt;
+DEALLOCATE PREPARE transformer_power_stmt;
+
+SET @member_discount_flag_exists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @schema_name
+ AND TABLE_NAME = 't_site'
+ AND COLUMN_NAME = 'member_discount_flag'
+);
+SET @member_discount_flag_sql = IF(
+ @member_discount_flag_exists = 0,
+ 'ALTER TABLE `t_site` ADD COLUMN `member_discount_flag` tinyint NULL DEFAULT 0 COMMENT ''是否参与会员折扣(0=否,1=是)'' AFTER `transformer_power`',
+ 'SELECT 1'
+);
+PREPARE member_discount_flag_stmt FROM @member_discount_flag_sql;
+EXECUTE member_discount_flag_stmt;
+DEALLOCATE PREPARE member_discount_flag_stmt;
--
Gitblit v1.7.1