longz
2026-07-17 da9b7a52dbc6fcd2173efef90528d8fccc322554
P1问题fix
17个文件已添加
17个文件已修改
1646 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260715_reservation_ahead_minute.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260715_sys_faq_missing_table_fix.sql 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260716_site_transformer_member_discount.sql 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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")
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")
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;
    }
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));
    }
}
}
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java
New file
@@ -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));
    }
}
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();
    }
    
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, "充电桩数据");
    }
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java
New file
@@ -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;
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java
New file
@@ -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;
    }
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java
New file
@@ -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;
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java
New file
@@ -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<>();
}
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;
    }
}
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;
    
    /**
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) {
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()));
                        }
                    });
                }
            }
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);
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>
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());
    }
}
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java
New file
@@ -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());
    }
}
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java
New file
@@ -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());
    }
}
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java
New file
@@ -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"));
    }
}
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"));
    }
}
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 = {"后台-充电桩订单"})
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);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java
New file
@@ -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);
    }
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java
New file
@@ -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;
    }
}
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));
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java
New file
@@ -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);
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java
New file
@@ -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;
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java
New file
@@ -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));
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java
New file
@@ -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;
    }
}
sql/20260715_reservation_ahead_minute.sql
New file
@@ -0,0 +1,3 @@
-- 预约充电:最早可提前预约分钟数
ALTER TABLE `t_site`
    ADD COLUMN `reservation_ahead_minute` int NULL COMMENT '最早可提前预约分钟数' AFTER `reservation_end_time`;
sql/20260715_sys_faq_missing_table_fix.sql
New file
@@ -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'
);
sql/20260716_site_transformer_member_discount.sql
New file
@@ -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;