From da9b7a52dbc6fcd2173efef90528d8fccc322554 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期五, 17 七月 2026 16:22:40 +0800
Subject: [PATCH] P1问题fix

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java                    |  184 +++--
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TAccountingStrategyServiceImpl.java |    8 
 sql/20260716_site_transformer_member_discount.sql                                                                      |   33 +
 ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Site.java                              |   12 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FileController.java                             |   35 +
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportErrorDTO.java                      |   14 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteImportResultDTO.java                     |   16 
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/FileControllerTest.java                         |   19 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingPaymentSupportTest.java                   |   34 +
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/BatchSetSiteAccountingStrategyDTO.java       |   13 
 ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml                                 |   19 
 ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFaq.java                                           |    2 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java                   |   37 +
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderPaymentConfirmationTest.java        |   99 +++
 ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/export/ChargingPileExportTest.java               |   70 ++
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/dto/SiteExcelDTO.java                            |  143 +++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java                             |    2 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java                   |   75 ++
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java             |   21 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/export/ChargingPileExport.java                   |  134 ++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java                       |    9 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingPaymentSupport.java                       |   23 
 ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/dto/SiteExcelDTOTest.java                        |   55 +
 sql/20260715_sys_faq_missing_table_fix.sql                                                                             |  104 +++
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/ISiteService.java                        |    4 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java                    |   55 +
 ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java         |   55 +
 ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/controller/SiteControllerTest.java               |   37 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java                        |   47 +
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TChargingPileController.java          |   39 +
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java                |  220 +++++++
 ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/mapper/SiteMapperVisibilityTest.java             |   23 
 sql/20260715_reservation_ahead_minute.sql                                                                              |    3 
 ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java                           |    2 
 34 files changed, 1,535 insertions(+), 111 deletions(-)

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

--
Gitblit v1.7.1