From 7858ae894c398f2f9248ac898b2ff200fa36225d Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期三, 29 七月 2026 18:42:15 +0800
Subject: [PATCH] 各类修复

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java                 |  244 ++++----------
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java                            |    2 
 ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml                                   |   17 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java                |  130 +++++++
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java                         |   13 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java           |   37 ++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java                     |    4 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java                           |    4 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java                |   22 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java                          |    5 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java          |    5 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java |   31 +
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java                      |   20 +
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java                    |    6 
 ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml                                  |   23 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java                    |    9 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java            |    2 
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java            |  122 +++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java                             |    2 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java                     |   31 +
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java               |    5 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java                 |   67 +++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java                     |   77 +++-
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java                  |    8 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java                         |   58 ++-
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java                  |    3 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java                                 |    2 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java                |   36 ++
 28 files changed, 701 insertions(+), 284 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
index 8ea4cee..c04f9de 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
@@ -224,6 +224,11 @@
                 return R.fail("获取站点集合中总的充电量失败:" + throwable.getMessage());
             }
 
+            @Override
+            public R<List<Long>> getAppUserIdsBySiteIds(List<Integer> siteIds) {
+                return R.fail("查询站点充电用户失败:" + throwable.getMessage());
+            }
+
 //            @Override
 //            public R<List<TChargingOrder>> getOrderListByStatus(List<Integer> statusList) {
 //                return R.fail("获取站点集合中总的充电量失败:" + throwable.getMessage());
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
index 66c8d7d..26e3157 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
@@ -326,6 +326,12 @@
     @GetMapping("/t-charging-order/getSumDegreeBySiteIds")
     R<BigDecimal> getSumDegreeBySiteIds(@RequestParam("siteIds") List<Integer> SiteIds);
 
+    /**
+     * 查询指定站点中实际发生过充电的用户ID
+     */
+    @PostMapping("/t-charging-order/getAppUserIdsBySiteIds")
+    R<List<Long>> getAppUserIdsBySiteIds(@RequestBody List<Integer> siteIds);
+
 
 //    /**
 //     * 获取充电订单列表
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
index 687897a..f3bd415 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -38,6 +38,8 @@
     private String licensePlate;
     @ApiModelProperty(value = "手机号")
     private String phone;
+    @ApiModelProperty(value = "归属合作商/服务商")
+    private String partnerName;
     @ApiModelProperty(value = "uid")
     private String uid;
     @ApiModelProperty(value = "记录按钮权限 ")
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
index 2373bea..53b0243 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -157,6 +157,8 @@
     private TAppUserWhitelistService appUserWhitelistService;
     @Resource
     private PartnerClient partnerClient;
+    @Resource
+    private PartnerAppUserAccessService partnerAppUserAccessService;
 
     @Autowired
     private RedissonClient redissonClient;
@@ -400,6 +402,10 @@
                 .eq(userListQueryDto.getVipTypeId() != null, TAppUser::getVipId, userListQueryDto.getVipTypeId())
                 .in(!userIds.isEmpty(),TAppUser::getId,userIds)
                 .orderByDesc(BasePojo::getCreateTime);
+        List<Long> partnerVisibleUserIds = partnerAppUserAccessService.getVisibleUserIdsForCurrentPartner();
+        if (partnerVisibleUserIds != null) {
+            query.in(TAppUser::getId, partnerVisibleUserIds);
+        }
         if (Integer.valueOf(1).equals(userListQueryDto.getWhitelistFlag())) {
             if (whitelistUserIds.isEmpty() && whitelistPhones.isEmpty()) {
                 query.eq(TAppUser::getId, -1L);
@@ -690,6 +696,7 @@
     @PostMapping(value = "/user/addOrUpdate")
     @Log(title = "【用户列表】添加编辑用户", businessType = BusinessType.INSERT,operatorType = OperatorType.MANAGE)
     public R addOrUpdate(@RequestBody TAppUser  tAppUser) {
+        partnerAppUserAccessService.assertWritable();
         if (tAppUser.getId() != null){
             //为编辑操作,要进行vip的逻辑处理
             TAppUser nowUser = appUserService.getById(tAppUser.getId());
@@ -734,10 +741,11 @@
     }
     
     
-    @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR)
+    @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR)
     @ApiOperation(value = "用户详情", tags = {"用户管理-用户列表"})
     @GetMapping(value = "/user/detail/{id}")
     public R<UserDetailDto> userDetail(@PathVariable Long id) {
+        partnerAppUserAccessService.assertUserVisible(id);
         TAppUser user = appUserService.getById(id);
         R<Map<Integer, String>> vipMap = otherClient.getVipMap(Collections.singletonList(user.getVipId()));
         if (user.getVipEndTime()!=null&&user.getVipEndTime().isAfter(LocalDateTime.now())) {
@@ -772,13 +780,14 @@
         return R.ok(userDetailDto);
     }
     
-    @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR)
+    @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR)
     @ApiOperation(value = "用户详情积分明细", tags = {"后台-用户管理-用户列表"})
     @PostMapping(value = "/user/points/page")
     public R<Page<TAppUserIntegralChange>> pointsDetail(@RequestBody PointsQueryDto pointsQueryDto) {
         if (pointsQueryDto.getUserId()==null){
             pointsQueryDto.setUserId(tokenService.getLoginUserApplet().getUserId());
         }
+        partnerAppUserAccessService.assertUserVisible(pointsQueryDto.getUserId());
         List<Integer> types = new ArrayList<>();
         if (pointsQueryDto.getType()==null){
             types.add(1);
@@ -874,10 +883,11 @@
 
     }
 
-    @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR)
+    @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR)
     @ApiOperation(value = "用户详情余额明细", tags = {"后台-用户管理-用户列表"})
     @PostMapping(value = "/user/balance/page")
     public R<Page<TExchangeCodeUseRecord>> adminBalanceDetail(@RequestBody ExchangeCodeUseRecordQuery queryDto) {
+        partnerAppUserAccessService.assertUserVisible(queryDto.getUserId());
         return otherClient.exchangeCodeUseRecordPage(queryDto);
     }
 
@@ -945,6 +955,7 @@
     @PostMapping(value = "/user/status/change")
     @Log(title = "【用户列表】冻结解冻用户", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE)
     public R pointsDetail(@RequestBody UserChangeDto userChangeDto) {
+        partnerAppUserAccessService.assertWritable();
         TAppUser appUser = appUserService.getById(userChangeDto.getUserId());
         appUser.setStatus(userChangeDto.getStatus());
         appUser.setRemark(userChangeDto.getRemark());
@@ -958,6 +969,7 @@
     @PostMapping(value = "/user/unit/change")
     @Log(title = "【用户列表】修改用户单位", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE)
     public R unitChange(@RequestBody UnitChangeDto unitChangeDto) {
+        partnerAppUserAccessService.assertWritable();
         for (String s : unitChangeDto.getIds().split(",")) {
             TAppUser byId = appUserService.getById(s);
             byId.setCompanyId(unitChangeDto.getUnitId());
@@ -972,6 +984,7 @@
     @DeleteMapping(value = "/user/delete")
     @Log(title = "【用户列表】删除用户", businessType = BusinessType.DELETE,operatorType = OperatorType.MANAGE)
     public R userDelete(String ids) {
+        partnerAppUserAccessService.assertWritable();
         String[] split = ids.split(",");
         for (String s : split) {
             appUserService.removeById(s);
@@ -1151,6 +1164,7 @@
     @PostMapping(value = "/user/give/vip")
     @Log(title = "【用户列表】赠送会员", businessType = BusinessType.OTHER,operatorType = OperatorType.MANAGE)
     public R giveVip(@RequestBody GiveVipDto  giveVipDto) {
+        partnerAppUserAccessService.assertWritable();
         String[] split = giveVipDto.getUserIds().split(",");
         for (String s : split) {
             TAppUser nowUser = appUserService.getById(s);
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java
index 4a4c1a7..ce99718 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java
@@ -81,11 +81,14 @@
     @ApiOperation(value = "开启站点降价提醒", tags = {"小程序-收藏"})
     public AjaxResult<Void> subscribe(@PathVariable Integer siteId) {
         Long userId = tokenService.getLoginUserApplet().getUserId();
-        favoriteService.lambdaUpdate()
+        boolean updated = favoriteService.lambdaUpdate()
                 .eq(TAppUserSiteFavorite::getAppUserId, userId)
                 .eq(TAppUserSiteFavorite::getSiteId, siteId)
                 .set(TAppUserSiteFavorite::getNotifyEnabled, true)
                 .update();
+        if (!updated) {
+            return AjaxResult.error("收藏记录不存在,请重新收藏后再开启提醒");
+        }
         return AjaxResult.success();
     }
 
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
new file mode 100644
index 0000000..d665936
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
@@ -0,0 +1,130 @@
+package com.ruoyi.account.service;
+
+import com.ruoyi.chargingPile.api.domain.SiteMenu;
+import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.order.api.feignClient.ChargingOrderClient;
+import com.ruoyi.system.api.domain.SysUser;
+import com.ruoyi.system.api.feignClient.SysUserClient;
+import com.ruoyi.system.api.model.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 合作商后台用户数据范围及只读权限控制。
+ */
+@Slf4j
+@Service
+public class PartnerAppUserAccessService {
+
+    private static final Integer PARTNER_ROLE_TYPE = 2;
+    private static final Long IMPOSSIBLE_USER_ID = -1L;
+    private static final String PARTNER_READ_ONLY_MESSAGE = "合作商账号仅支持查看用户信息";
+
+    @Resource
+    private TokenService tokenService;
+    @Resource
+    private SysUserClient sysUserClient;
+    @Resource
+    private PartnerClient partnerClient;
+    @Resource
+    private ChargingOrderClient chargingOrderClient;
+
+    /**
+     * 非合作商返回null,合作商返回允许查看的用户ID;空权限始终返回不可命中的ID。
+     */
+    public List<Long> getVisibleUserIdsForCurrentPartner() {
+        SysUser sysUser = getCurrentSysUser();
+        if (!isPartner(sysUser)) {
+            return null;
+        }
+        if (sysUser.getObjectId() == null) {
+            return impossibleUserIds();
+        }
+        R<Set<Integer>> siteResult = partnerClient.authSite(sysUser.getObjectId(), SiteMenu.SITE_LIST.getValue());
+        if (siteResult == null || !R.isSuccess(siteResult) || siteResult.getData() == null) {
+            return impossibleUserIds();
+        }
+        List<Integer> siteIds = siteResult.getData().stream()
+                .filter(Objects::nonNull)
+                .filter(siteId -> siteId > 0)
+                .distinct()
+                .sorted()
+                .collect(Collectors.toList());
+        if (siteIds.isEmpty()) {
+            return impossibleUserIds();
+        }
+        R<List<Long>> userResult = chargingOrderClient.getAppUserIdsBySiteIds(siteIds);
+        if (userResult == null || !R.isSuccess(userResult) || userResult.getData() == null) {
+            log.warn("查询合作商充电用户失败,partnerId={}", sysUser.getObjectId());
+            return impossibleUserIds();
+        }
+        List<Long> userIds = userResult.getData().stream()
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        return userIds.isEmpty() ? impossibleUserIds() : userIds;
+    }
+
+    public boolean isCurrentPartner() {
+        return isPartner(getCurrentSysUser());
+    }
+
+    public void assertUserVisible(Long userId) {
+        List<Long> visibleUserIds = getVisibleUserIdsForCurrentPartner();
+        if (visibleUserIds != null && (userId == null || !visibleUserIds.contains(userId))) {
+            throw new ServiceException("无权查看该用户信息");
+        }
+    }
+
+    public void assertWritable() {
+        if (isCurrentPartner()) {
+            throw new ServiceException(PARTNER_READ_ONLY_MESSAGE);
+        }
+    }
+
+    private boolean isPartner(SysUser sysUser) {
+        return sysUser != null && Objects.equals(PARTNER_ROLE_TYPE, sysUser.getRoleType());
+    }
+
+    private List<Long> impossibleUserIds() {
+        return Collections.singletonList(IMPOSSIBLE_USER_ID);
+    }
+
+    private SysUser getCurrentSysUser() {
+        LoginUser loginUser = null;
+        try {
+            loginUser = tokenService.getLoginUser();
+        } catch (Exception ignored) {
+            // 尝试从安全上下文继续获取。
+        }
+        if (loginUser == null) {
+            try {
+                loginUser = SecurityUtils.getLoginUser();
+            } catch (Exception ignored) {
+                // 当前请求不是管理后台登录上下文。
+            }
+        }
+        if (loginUser == null) {
+            return null;
+        }
+        if (loginUser.getSysUser() != null && loginUser.getSysUser().getRoleType() != null) {
+            return loginUser.getSysUser();
+        }
+        if (loginUser.getUserid() == null) {
+            return loginUser.getSysUser();
+        }
+        R<SysUser> userResult = sysUserClient.getSysUser(loginUser.getUserid());
+        return userResult != null && userResult.getData() != null ? userResult.getData() : loginUser.getSysUser();
+    }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java
index 0d19882..ea0af40 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java
@@ -92,12 +92,16 @@
                 }
                 TAppUser user = appUserService.getById(favorite.getAppUserId());
                 if (user == null || !StringUtils.hasText(user.getWxOpenid())) {
+                    log.info("收藏站点降价提醒未发送,用户openid为空,userId={}, siteId={}",
+                            favorite.getAppUserId(), favorite.getSiteId());
                     continue;
                 }
                 if (sendSubscribeMessage(user.getWxOpenid(), favorite, price)) {
                     favorite.setNotifyEnabled(false);
                     favorite.setLastNotifiedPrice(price.getCurrentPrice());
                     favoriteService.updateById(favorite);
+                    log.info("收藏站点降价提醒发送成功,userId={}, siteId={}, currentPrice={}",
+                            favorite.getAppUserId(), favorite.getSiteId(), price.getCurrentPrice());
                 }
             }
         } catch (InterruptedException e) {
@@ -112,16 +116,13 @@
     }
 
     private boolean sendSubscribeMessage(String openId, TAppUserSiteFavorite favorite, FavoriteSitePriceVO price) {
-        BigDecimal decrease = favorite.getFavoritePrice().subtract(price.getCurrentPrice());
         JSONObject body = new JSONObject();
         body.put("touser", openId);
         body.put("template_id", templateId);
         body.put("page", page + "?siteId=" + favorite.getSiteId());
         JSONObject data = new JSONObject();
         data.put("thing1", value(limit(price.getSiteName(), 20)));
-        data.put("amount2", value(favorite.getFavoritePrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString()));
-        data.put("amount3", value(price.getCurrentPrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString()));
-        data.put("amount4", value(decrease.setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString()));
+        data.put("amount4", value(formatAmount(price.getCurrentPrice())));
         body.put("data", data);
         String accessToken = new WxAppletTools(wxRestTemplate, wxConfig, redisService).getAccessToken("");
         String result = HttpUtils.post(SEND_URL + accessToken, body.toJSONString());
@@ -140,6 +141,10 @@
         return item;
     }
 
+    private String formatAmount(BigDecimal amount) {
+        return amount.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "元";
+    }
+
     private String limit(String value, int length) {
         if (!StringUtils.hasText(value) || value.length() <= length) {
             return value;
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
new file mode 100644
index 0000000..f2aa127
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
@@ -0,0 +1,122 @@
+package com.ruoyi.account.service;
+
+import com.ruoyi.chargingPile.api.domain.SiteMenu;
+import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.order.api.feignClient.ChargingOrderClient;
+import com.ruoyi.system.api.domain.SysUser;
+import com.ruoyi.system.api.feignClient.SysUserClient;
+import com.ruoyi.system.api.model.LoginUser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PartnerAppUserAccessServiceTest {
+
+    @Mock private TokenService tokenService;
+    @Mock private SysUserClient sysUserClient;
+    @Mock private PartnerClient partnerClient;
+    @Mock private ChargingOrderClient chargingOrderClient;
+
+    private PartnerAppUserAccessService service;
+
+    @BeforeEach
+    void setUp() {
+        service = new PartnerAppUserAccessService();
+        ReflectionTestUtils.setField(service, "tokenService", tokenService);
+        ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
+        ReflectionTestUtils.setField(service, "partnerClient", partnerClient);
+        ReflectionTestUtils.setField(service, "chargingOrderClient", chargingOrderClient);
+    }
+
+    @Test
+    void partnerCanSeeOnlyUsersWhoChargedAtAuthorizedSites() {
+        mockCurrentUser(2, 9);
+        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
+                .thenReturn(R.ok(new HashSet<>(Arrays.asList(11, 12))));
+        when(chargingOrderClient.getAppUserIdsBySiteIds(Arrays.asList(11, 12)))
+                .thenReturn(R.ok(Arrays.asList(101L, 102L, 101L)));
+
+        List<Long> result = service.getVisibleUserIdsForCurrentPartner();
+
+        assertEquals(Arrays.asList(101L, 102L), result);
+        verify(chargingOrderClient).getAppUserIdsBySiteIds(Arrays.asList(11, 12));
+    }
+
+    @Test
+    void partnerWithoutAuthorizedSitesGetsImpossibleUserId() {
+        mockCurrentUser(2, 9);
+        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
+                .thenReturn(R.ok(Collections.singleton(-1)));
+
+        assertEquals(Collections.singletonList(-1L), service.getVisibleUserIdsForCurrentPartner());
+        verifyNoInteractions(chargingOrderClient);
+    }
+
+    @Test
+    void orderServiceFailureDoesNotExposeAllUsers() {
+        mockCurrentUser(2, 9);
+        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
+                .thenReturn(R.ok(Collections.singleton(11)));
+        when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
+                .thenReturn(R.fail("order unavailable"));
+
+        assertEquals(Collections.singletonList(-1L), service.getVisibleUserIdsForCurrentPartner());
+    }
+
+    @Test
+    void platformUserKeepsUnrestrictedScope() {
+        mockCurrentUser(1, null);
+
+        assertNull(service.getVisibleUserIdsForCurrentPartner());
+        verifyNoInteractions(partnerClient, chargingOrderClient);
+    }
+
+    @Test
+    void partnerCannotReadUserOutsideVisibleScope() {
+        mockCurrentUser(2, 9);
+        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
+                .thenReturn(R.ok(Collections.singleton(11)));
+        when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
+                .thenReturn(R.ok(Collections.singletonList(101L)));
+
+        assertThrows(ServiceException.class, () -> service.assertUserVisible(999L));
+    }
+
+    @Test
+    void partnerUserManagementIsReadOnly() {
+        mockCurrentUser(2, 9);
+
+        ServiceException exception = assertThrows(ServiceException.class, service::assertWritable);
+
+        assertEquals("合作商账号仅支持查看用户信息", exception.getMessage());
+    }
+
+    private void mockCurrentUser(Integer roleType, Integer objectId) {
+        SysUser sysUser = new SysUser();
+        sysUser.setRoleType(roleType);
+        sysUser.setObjectId(objectId);
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUserid(1L);
+        loginUser.setSysUser(sysUser);
+        when(tokenService.getLoginUser()).thenReturn(loginUser);
+    }
+}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
index 266309d..7b7a762 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
@@ -36,6 +36,7 @@
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -173,6 +174,7 @@
     @Log(title = "【计费策略】修改计费策略", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE)
     @ApiOperation(tags = {"后台-计费策略", "管理后台-站点管理"},value = "修改计费策略")
     @PostMapping(value = "/update")
+    @Transactional(rollbackFor = Exception.class)
     public AjaxResult<String> update(@RequestBody TAccountingStrategyDTO dto) {
         String partnerCheck = fillAndCheckPartner(dto);
         if (partnerCheck != null) {
@@ -191,9 +193,7 @@
         if (Objects.isNull(original)) {
             return AjaxResult.error("计费策略不存在");
         }
-        if(Objects.isNull(dto.getDiscount())){
-            accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate().set(TAccountingStrategy::getDiscount, null).eq(TAccountingStrategy::getId, dto.getId()));
-        }
+        boolean clearDiscount = Objects.isNull(dto.getDiscount());
         // 历史审核通过的策略在替换旧策略后可能仍保留已删除策略的 parentId。
         // 这类正式策略再次编辑会被改为待审核子记录,并被计费策略列表过滤掉,先将其恢复为主策略。
         if (original.getParentId() != null && Objects.equals(original.getAuditStatus(), 3)
@@ -220,12 +220,14 @@
             } else {
                 dto.setId(original.getId());
                 accountingStrategyService.updateById(dto);
+                clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount);
                 replaceStrategyDetails(dto.getId(), dto.getAccountingStrategyDetails());
             }
             return AjaxResult.success();
         }
-        // 判断修改的计费策略是否为已通过或已驳回
-        if(null != dto.getAuditStatus() && (dto.getAuditStatus() == 3 || dto.getAuditStatus() == 4)){
+        // 是否为正式/驳回策略必须以数据库原记录为准,不能信任前端回传的审核状态。
+        // 正式策略修改时仅维护待审核子记录,审核通过前站点仍引用原策略。
+        if (Objects.equals(original.getAuditStatus(), 3) || Objects.equals(original.getAuditStatus(), 4)) {
             // 查询是否有下级审核中的策略
             TAccountingStrategy children = accountingStrategyService.getOne(Wrappers.lambdaQuery(TAccountingStrategy.class)
                     .eq(TAccountingStrategy::getParentId, dto.getId())
@@ -243,6 +245,7 @@
                 dto.setFirstAuditTime(null);
                 dto.setTwoAuditTime(null);
                 accountingStrategyService.updateById(dto);
+                clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount);
                 replaceStrategyDetails(children.getId(), dto.getAccountingStrategyDetails());
             }else {
                 Long userId = SecurityUtils.getLoginUser().getUserid();
@@ -257,20 +260,20 @@
         }else {
             dto.setAuditStatus(1);
             accountingStrategyService.updateById(dto);
+            clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount);
             replaceStrategyDetails(dto.getId(), dto.getAccountingStrategyDetails());
         }
-
-        if(null != dto.getSiteId()){
-            //下发硬件数据
-            List<TChargingPile> list = chargingPileService.lambdaQuery().eq(TChargingPile::getSiteId, dto.getSiteId()).eq(TChargingPile::getDelFlag, 0).list();
-            for (TChargingPile tChargingPile : list) {
-                integrationClient.setupBillingModel1(tChargingPile.getCode(), JSON.toJSONString(dto.getAccountingStrategyDetails()));
-                chengDuJianGuanClient.notificationStationFee(tChargingPile.getId());
-            }
-        }
         return AjaxResult.success();
     }
 
+    private void clearStrategyDiscountIfNeeded(Integer strategyId, boolean clearDiscount) {
+        if (clearDiscount) {
+            accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate()
+                    .set(TAccountingStrategy::getDiscount, null)
+                    .eq(TAccountingStrategy::getId, strategyId));
+        }
+    }
+
     private void replaceStrategyDetails(Integer strategyId, List<TAccountingStrategyDetail> details) {
         accountingStrategyDetailService.remove(Wrappers.lambdaQuery(TAccountingStrategyDetail.class)
                 .eq(TAccountingStrategyDetail::getAccountingStrategyId, strategyId));
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 47d8d27..587ee8a 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
@@ -670,6 +670,7 @@
         export.setPartnerName(dto.getPartnerName());
         export.setPhone(dto.getPhone());
         export.setOrderSource(formatOrderSource(dto.getOrderSource()));
+        export.setCreateTime(dto.getCreateTime() == null ? null : DateUtils.localDateTimeToString(dto.getCreateTime()));
         export.setStartTime(dto.getStartTime() == null ? null : DateUtils.localDateTimeToString(dto.getStartTime()));
         export.setEndTime(dto.getEndTime() == null ? null : DateUtils.localDateTimeToString(dto.getEndTime()));
         export.setLicensePlate(dto.getLicensePlate());
@@ -1285,6 +1286,14 @@
         return R.ok(count);
     }
 
+    /**
+     * 查询指定站点中实际发生过充电的用户ID
+     */
+    @PostMapping(value = "/getAppUserIdsBySiteIds")
+    public R<List<Long>> getAppUserIdsBySiteIds(@RequestBody List<Integer> siteIds) {
+        return R.ok(chargingOrderService.getAppUserIdsBySiteIds(siteIds));
+    }
+
 
 
     //订单详情
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
index 52078eb..703b103 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
@@ -85,8 +85,8 @@
 	@RequiresPermissions(value = {"/invoiceManagement"}, logical = Logical.OR)
 	@PostMapping("/invoicingCompanyOptions")
 	@ApiOperation(value = "查询开票公司选项", tags = {"管理后台-发票管理"})
-	public AjaxResult<List<String>> invoicingCompanyOptions(@RequestBody TOrderInvoiceQuery query) {
-		return AjaxResult.success(orderInvoiceService.invoicingCompanyOptions(query));
+	public AjaxResult<List<String>> invoicingCompanyOptions() {
+		return AjaxResult.success(orderInvoiceService.invoicingCompanyOptions());
 	}
 
 
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
index ad70d09..fc09307 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -267,41 +267,59 @@
         List<ChargingOrderVO> list = chargingOrderService.chargingOrderList(orderQuery).getRecords();
         List<EnterpriseChargingDetailExport> exports = new ArrayList<>();
         for (ChargingOrderVO order : list) {
-            EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport();
-            export.setCode(order.getCode());
-            export.setOrderType(Integer.valueOf(1).equals(order.getOrderType()) ? "小程序" : "刷卡");
-            export.setOrderClassification("充电订单");
-            export.setSiteName(order.getSiteName());
-            export.setTerminalName(order.getTerminalName());
-            export.setStatus(getChargingOrderStatus(order.getStatus()));
-            export.setChargingSecond(order.getChargingSecond());
-            export.setTotalAmount(order.getOrderAmount());
-            export.setCouponDiscountAmount(order.getCouponDiscountAmount());
-            export.setVipDiscountAmount(order.getVipDiscountAmount());
-            export.setPaymentAmount(order.getPaymentAmount());
-            export.setRefundAmount(order.getRefundAmount());
-            export.setPhone(order.getPhone());
-            export.setOrderSource(getChargingOrderSource(order.getOrderSource()));
-            export.setStartTime(formatExportDateTime(order.getStartTime()));
-            export.setEndTime(formatExportDateTime(order.getEndTime()));
-            exports.add(export);
+            exports.add(buildVipChargingOrderExport(order));
         }
         writeVipChargingOrderWorkbook(exports);
     }
 
-    private String getChargingOrderStatus(Integer status) {
+    static EnterpriseChargingDetailExport buildVipChargingOrderExport(ChargingOrderVO order) {
+        EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport();
+        export.setCode(order.getCode());
+        export.setOrderType(Integer.valueOf(1).equals(order.getOrderType()) ? "小程序" : "刷卡");
+        export.setOrderClassification("充电订单");
+        export.setSiteName(order.getSiteName());
+        export.setTerminalName(order.getTerminalName());
+        export.setStatus(getChargingOrderStatus(order.getStatus()));
+        export.setChargingSecond(order.getChargingSecond());
+        export.setTotalAmount(order.getOrderAmount());
+        export.setCouponDiscountAmount(order.getCouponDiscountAmount());
+        export.setVipDiscountAmount(order.getVipDiscountAmount());
+        export.setPaymentAmount(order.getPaymentAmount());
+        export.setRefundAmount(order.getRefundAmount());
+        export.setPartnerName(order.getPartnerName());
+        export.setPhone(order.getPhone());
+        export.setOrderSource(getChargingOrderSource(order.getOrderSource()));
+        export.setCreateTime(formatExportDateTime(order.getCreateTime()));
+        export.setStartTime(formatExportDateTime(order.getStartTime()));
+        export.setEndTime(formatExportDateTime(order.getEndTime()));
+        export.setLicensePlate(order.getLicensePlate());
+        export.setChargingCapacity(order.getChargingCapacity());
+        export.setElectrovalence(order.getElectrovalence());
+        export.setServiceCharge(order.getServiceCharge());
+        export.setEndMode(getChargingOrderEndMode(order.getEndMode()));
+        export.setPeriodCount(order.getCount());
+        return export;
+    }
+
+    private static String getChargingOrderStatus(Integer status) {
         if (status == null) return "未知";
         String[] names = {"未知", "等待中", "启动中", "充电中", "停止中", "已结束"};
         return status >= 0 && status < names.length ? names[status] : "未知";
     }
 
-    private String getChargingOrderSource(Integer source) {
+    private static String getChargingOrderSource(Integer source) {
         if (Integer.valueOf(1).equals(source)) return "明星充电";
         if (Integer.valueOf(2).equals(source)) return "快电";
         if (Integer.valueOf(3).equals(source)) return "新电途";
         return "";
     }
 
+    private static String getChargingOrderEndMode(Integer endMode) {
+        if (endMode == null) return "";
+        String[] names = {"异常终止", "主动终止", "满电终止", "费用不足终止"};
+        return endMode >= 0 && endMode < names.length ? names[endMode] : "";
+    }
+
     static String formatExportDateTime(LocalDateTime time) {
         return time == null ? "" : DateUtils.localDateTimeToString(time);
     }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
index 4297e66..91be121 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
@@ -54,6 +54,9 @@
     @Excel(name = "订单来源", width = 20)
     private String orderSource;
 
+    @Excel(name = "下单时间", width = 30)
+    private String createTime;
+
     @Excel(name = "开始时间", width = 30)
     private String startTime;
 
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
index a0b6ddf..041be32 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -209,6 +209,8 @@
     List<OrderCountByDate> countOrdersByDate(@Param("startTime")LocalDateTime startTime, @Param("endTime")LocalDateTime endTime,@Param("gunIds")List<Integer> gunIds);
 
 	Double getSumDegreeBySiteIds(@Param("siteIds") List<Integer> siteIds);
+
+	List<Long> getAppUserIdsBySiteIds(@Param("siteIds") List<Integer> siteIds);
 	
 	
 	/**
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
index a2c0a79..c1f26ca 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
@@ -33,7 +33,7 @@
     /**
      * 查询开票公司下拉选项
      */
-    List<String> invoicingCompanyOptions(@Param("query") TOrderInvoiceQuery query);
+    List<String> invoicingCompanyOptions();
 
     /**
      * 查询开票状态数量
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 dc3dd99..58221c3 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
@@ -393,6 +393,11 @@
     R<StatisticsOfBatteryVO> getStatisticsOfBattery(List<Integer> siteIds);
 
 	Double getSumDegreeBySiteIds(List<Integer> siteIds);
+
+	/**
+	 * 查询指定站点中实际发生过充电的用户ID
+	 */
+	List<Long> getAppUserIdsBySiteIds(List<Integer> siteIds);
 	
 	/**
 	 * 获取启动失败且时间超过1小时的订单
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
index c47f46e..8b05130 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
@@ -56,9 +56,9 @@
 	PageInfo<TOrderInvoiceVO> pageList(TOrderInvoiceQuery query);
 
 	/**
-	 * 查询当前账号可见的开票公司选项
+	 * 查询全部开票公司选项
 	 */
-	List<String> invoicingCompanyOptions(TOrderInvoiceQuery query);
+	List<String> invoicingCompanyOptions();
 
 	/**
 	 * 查询开票状态数量
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
index 24be087..6512e0f 100644
--- 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
@@ -11,37 +11,72 @@
     private ChargingCouponSupport() {
     }
 
-    static BigDecimal calculateUpfrontDiscount(TCoupon coupon, BigDecimal rechargeAmount) {
-        if (coupon == null || rechargeAmount == null || rechargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
+    static BigDecimal calculatePrepaymentAmount(BigDecimal rechargeAmount, boolean whitelistAllFree) {
+        if (whitelistAllFree) {
             return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
         }
-        if (!Integer.valueOf(1).equals(coupon.getType())) {
-            throw new IllegalArgumentException("请选择充电优惠券");
+        return money(rechargeAmount).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 兼容曾在预付阶段直接减掉优惠券金额的在途订单,退款基数不能超过真实收款。
+     */
+    static BigDecimal resolveSettlementPrepayment(BigDecimal rechargeAmount,
+                                                  BigDecimal balancePayAmount,
+                                                  BigDecimal thirdPayAmount,
+                                                  BigDecimal upfrontCouponDiscount) {
+        BigDecimal recharge = money(rechargeAmount);
+        BigDecimal collected = money(balancePayAmount).add(money(thirdPayAmount));
+        if (money(upfrontCouponDiscount).compareTo(BigDecimal.ZERO) > 0
+                && collected.compareTo(BigDecimal.ZERO) > 0
+                && collected.compareTo(recharge) < 0) {
+            return collected;
         }
-        BigDecimal threshold = coupon.getMeetTheConditions() == null
-                ? BigDecimal.ZERO : coupon.getMeetTheConditions();
-        if (rechargeAmount.compareTo(threshold) < 0) {
-            throw new IllegalArgumentException("预付金额未达到优惠券使用门槛");
+        return recharge;
+    }
+
+    /**
+     * 优惠券只在充电结束时结算,且优惠金额不能超过剩余服务费。
+     */
+    static BigDecimal calculateSettlementDiscount(TCoupon coupon,
+                                                   BigDecimal actualAmountBeforeCoupon,
+                                                   BigDecimal remainingServiceCharge) {
+        BigDecimal actualAmount = money(actualAmountBeforeCoupon);
+        BigDecimal serviceCharge = money(remainingServiceCharge);
+        if (coupon == null || actualAmount.compareTo(BigDecimal.ZERO) <= 0
+                || serviceCharge.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_EVEN);
+        }
+        BigDecimal threshold = money(coupon.getMeetTheConditions());
+        if (actualAmount.compareTo(threshold) < 0) {
+            return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_EVEN);
         }
 
-        BigDecimal discount;
+        BigDecimal discount = BigDecimal.ZERO;
         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);
+            discount = money(coupon.getDiscountAmount());
+        } else if (Integer.valueOf(2).equals(coupon.getPreferentialMode()) && coupon.getDiscount() != null) {
+            discount = actualAmount.multiply(TEN.subtract(coupon.getDiscount())).divide(TEN);
             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.max(BigDecimal.ZERO).min(serviceCharge).setScale(4, RoundingMode.HALF_EVEN);
+    }
+
+    static BigDecimal[] calculateWhitelistRefundSplit(boolean whitelistOrder,
+                                                       boolean whitelistAllFree,
+                                                       BigDecimal electrovalence,
+                                                       BigDecimal serviceCharge) {
+        if (!whitelistOrder) {
+            return new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO};
         }
-        return discount.min(rechargeAmount).setScale(2, RoundingMode.HALF_UP);
+        BigDecimal electricDiscount = whitelistAllFree ? money(electrovalence) : BigDecimal.ZERO;
+        BigDecimal serviceDiscount = money(serviceCharge);
+        return new BigDecimal[]{electricDiscount, serviceDiscount};
+    }
+
+    private static BigDecimal money(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
     }
 }
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 851d458..6c1ed87 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
@@ -714,7 +714,7 @@
 		return chargingOrder != null && "ICBC".equals(chargingOrder.getSourceName());
 	}
 
-	private BigDecimal resolveUpfrontCouponDiscount(Long appCouponId, Long userId, BigDecimal rechargeAmount) {
+	private void validateSelectedChargingCoupon(Long appCouponId, Long userId) {
 		TAppCoupon appCoupon = appCouponClient.getAppCouponById(appCouponId).getData();
 		if (appCoupon == null || !userId.equals(appCoupon.getAppUserId())) {
 			throw new ServiceException("优惠券不存在或不属于当前用户");
@@ -727,11 +727,9 @@
 				|| (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());
+		TCoupon coupon = JSON.parseObject(appCoupon.getCouponJson(), TCoupon.class);
+		if (coupon == null || !Integer.valueOf(1).equals(coupon.getType())) {
+			throw new ServiceException("请选择充电优惠券");
 		}
 	}
 
@@ -765,20 +763,11 @@
      * @return 数组下标0=电费减免,下标1=服务费减免
      */
     private BigDecimal[] calculateWhitelistRefundSplit(TChargingOrder chargingOrder, BigDecimal electrovalence, BigDecimal serviceCharge) {
-        BigDecimal electricDiscount = BigDecimal.ZERO;
-        BigDecimal serviceDiscount = BigDecimal.ZERO;
-        if (!isWhitelistOrder(chargingOrder)) {
-            return new BigDecimal[]{electricDiscount, serviceDiscount};
-        }
-        if (Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(chargingOrder.getWhitelistBenefitType())) {
-            electricDiscount = electrovalence == null ? BigDecimal.ZERO : electrovalence;
-            serviceDiscount = serviceCharge == null ? BigDecimal.ZERO : serviceCharge;
-        } else if (Integer.valueOf(WHITELIST_BENEFIT_ELECTRIC_FREE).equals(chargingOrder.getWhitelistBenefitType())) {
-            electricDiscount = electrovalence == null ? BigDecimal.ZERO : electrovalence;
-        } else if (Integer.valueOf(WHITELIST_BENEFIT_SERVICE_FREE).equals(chargingOrder.getWhitelistBenefitType())) {
-            serviceDiscount = serviceCharge == null ? BigDecimal.ZERO : serviceCharge;
-        }
-        return new BigDecimal[]{electricDiscount, serviceDiscount};
+        return ChargingCouponSupport.calculateWhitelistRefundSplit(
+                isWhitelistOrder(chargingOrder),
+                isWhitelistAllFreeOrder(chargingOrder),
+                electrovalence,
+                serviceCharge);
     }
     private TVip getVipSnapshot(TAppUserVipDetail vipDetail, Integer vipId) {
         if (vipDetail != null && StringUtils.hasLength(vipDetail.getVipJson())) {
@@ -839,7 +828,6 @@
 				&& Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(whitelist.getBenefitType());
 		BigDecimal userBalance = money(appUser.getBalance());
 		BigDecimal balancePayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
-		BigDecimal upfrontCouponDiscount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
 		if (payMethod == 2 && userBalance.compareTo(rechargeAmount) >= 0) {
 			payMethod = 1;
 			addChargingOrder.setPayMethod(1);
@@ -847,12 +835,10 @@
 		if (isBalancePayMode(payMethod)) {
 			addChargingOrder.setAppUserCouponId(null);
 		} else if (!whitelistMatched && addChargingOrder.getAppUserCouponId() != null) {
-			upfrontCouponDiscount = resolveUpfrontCouponDiscount(
-					addChargingOrder.getAppUserCouponId(), userId, rechargeAmount);
+			// 这里只校验优惠券,不抵扣预付金额;实际门槛和优惠金额在充电结束时计算。
+			validateSelectedChargingCoupon(addChargingOrder.getAppUserCouponId(), userId);
 		}
-		BigDecimal payableAmount = whitelistAllFree
-				? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
-				: rechargeAmount.subtract(upfrontCouponDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+		BigDecimal payableAmount = ChargingCouponSupport.calculatePrepaymentAmount(rechargeAmount, whitelistAllFree);
 		BigDecimal thirdPayAmount = payableAmount;
 		if (isBalancePayMode(payMethod)) {
 			if (payMethod == 1 && userBalance.compareTo(payableAmount) < 0) {
@@ -956,7 +942,7 @@
 		chargingOrder.setThirdRefundAmount(BigDecimal.ZERO);
 		chargingOrder.setRechargeAmount(rechargeAmount);
 		chargingOrder.setAppCouponId(addChargingOrder.getAppUserCouponId());
-		chargingOrder.setCouponDiscountAmount(upfrontCouponDiscount);
+		chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
 		chargingOrder.setIsWhitelist(whitelistMatched ? 1 : 0);
 		chargingOrder.setWhitelistId(whitelistMatched ? whitelist.getWhitelistId() : null);
 		chargingOrder.setWhitelistBenefitType(whitelistMatched ? whitelist.getBenefitType() : null);
@@ -4439,8 +4425,11 @@
                 BigDecimal rechargeAmount = chargingOrder.getRechargeAmount();
                 //支付金额
                 BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total);
-                //退款金额=充值金额-实际支付金额
-                BigDecimal refundAmount = rechargeAmount.subtract(payAmount);
+                //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。
+                BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment(
+                        rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(),
+                        chargingOrder.getCouponDiscountAmount());
+                BigDecimal refundAmount = settlementPrepayment.subtract(payAmount);
 
                 UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode());
                 if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) {
@@ -4468,57 +4457,21 @@
                 }
 
                 //计算优惠券
-                boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0;
-                BigDecimal couponDiscount = couponAppliedUpfront
-                        ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO;
+                BigDecimal couponDiscount = BigDecimal.ZERO;
                 if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != chargingOrder.getAppCouponId()) {
+                    Long appCouponId = 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 (couponAppliedUpfront) {
+                    couponDiscount = ChargingCouponSupport.calculateSettlementDiscount(
+                            tCoupon, payAmount, serviceCharge_total);
+                    if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
                         appCoupon.setStatus(2);
                         appCouponClient.updateAppCoupon(appCoupon);
-                    } else if (1 == preferentialMode) {
-                        //满减
-                        if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                            BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount();
-                            //如果优惠金额大于服务费金额,以服务费作为最大限制
-                            if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) {
-                                couponDiscount = serviceCharge_total;
-                                serviceCharge_total = BigDecimal.ZERO;
-                            } else {
-                                couponDiscount = couponDiscountAmount;
-                            }
-                            appCoupon.setStatus(2);
-                            appCouponClient.updateAppCoupon(appCoupon);
-                        } else {
-                            appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                            chargingOrder.setAppCouponId(null);
-                            chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                        }
-                    }
-                    if (!couponAppliedUpfront && 2 == preferentialMode) {
-                        //抵扣
-                        if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                            //折扣金额
-                            BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10));
-                            divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide;
-                            //如果优惠金额大于服务费金额,以服务费作为最大限制
-                            if (serviceCharge_total.compareTo(divide) < 0) {
-                                couponDiscount = serviceCharge_total;
-                                serviceCharge_total = BigDecimal.ZERO;
-                            } else {
-                                couponDiscount = divide;
-                            }
-
-                            appCoupon.setStatus(2);
-                            appCouponClient.updateAppCoupon(appCoupon);
-                        } else {
-                            chargingOrder.setAppCouponId(null);
-                            chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                            appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                        }
+                    } else {
+                        appCouponClient.refund(appCouponId.toString());
+                        chargingOrder.setAppCouponId(null);
+                        chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
                     }
                 }
                 BigDecimal[] whitelistRefundSplit = calculateWhitelistRefundSplit(chargingOrder, periodElectricPrice_total, serviceCharge_total);
@@ -4534,10 +4487,7 @@
                 }
                 //优惠券优惠金额
                 couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN);
-                //历史订单在结算时计算优惠;新订单已在预付时少收优惠金额,不能再次增加退款。
-                if (!couponAppliedUpfront) {
-                    refundAmount = refundAmount.add(couponDiscount);
-                }
+                refundAmount = refundAmount.add(couponDiscount);
                 //实际支付金额-优惠券
                 payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO);
                 serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO);
@@ -6614,6 +6564,14 @@
 		}
 		return this.list(queryWrapper.orderByDesc(TChargingOrder::getCreateTime));
 	}
+
+	@Override
+	public List<Long> getAppUserIdsBySiteIds(List<Integer> siteIds) {
+		if (siteIds == null || siteIds.isEmpty()) {
+			return Collections.emptyList();
+		}
+		return this.baseMapper.getAppUserIdsBySiteIds(siteIds);
+	}
 	
 	@Override
 	public Long countCar(List<String> brands) {
@@ -7323,8 +7281,11 @@
                 BigDecimal rechargeAmount = chargingOrder.getRechargeAmount();
                 //支付金额
                 BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total);
-                //退款金额=充值金额-实际支付金额
-                BigDecimal refundAmount = rechargeAmount.subtract(payAmount);
+                //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。
+                BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment(
+                        rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(),
+                        chargingOrder.getCouponDiscountAmount());
+                BigDecimal refundAmount = settlementPrepayment.subtract(payAmount);
 
                 UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode());
                 if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) {
@@ -7352,64 +7313,26 @@
                 }
 
                 //计算优惠券
-                boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0;
-                BigDecimal couponDiscount = couponAppliedUpfront
-                        ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO;
+                BigDecimal couponDiscount = BigDecimal.ZERO;
                 if (null != chargingOrder.getAppCouponId()) {
+                    Long appCouponId = 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 (couponAppliedUpfront) {
+                    couponDiscount = ChargingCouponSupport.calculateSettlementDiscount(
+                            tCoupon, payAmount, serviceCharge_total);
+                    if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
                         appCoupon.setStatus(2);
                         appCouponClient.updateAppCoupon(appCoupon);
-                    } else if (1 == preferentialMode) {
-                        //满减
-                        if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                            BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount();
-                            //如果优惠金额大于服务费金额,以服务费作为最大限制
-                            if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) {
-                                couponDiscount = serviceCharge_total;
-                                serviceCharge_total = BigDecimal.ZERO;
-                            } else {
-                                couponDiscount = couponDiscountAmount;
-                            }
-                            appCoupon.setStatus(2);
-                            appCouponClient.updateAppCoupon(appCoupon);
-                        } else {
-                            appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                            chargingOrder.setAppCouponId(null);
-                            chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                        }
-                    }
-                    if (!couponAppliedUpfront && 2 == preferentialMode) {
-                        //抵扣
-                        if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                            //折扣金额
-                            BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10));
-                            divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide;
-                            //如果优惠金额大于服务费金额,以服务费作为最大限制
-                            if (serviceCharge_total.compareTo(divide) < 0) {
-                                couponDiscount = serviceCharge_total;
-                                serviceCharge_total = BigDecimal.ZERO;
-                            } else {
-                                couponDiscount = divide;
-                            }
-
-                            appCoupon.setStatus(2);
-                            appCouponClient.updateAppCoupon(appCoupon);
-                        } else {
-                            chargingOrder.setAppCouponId(null);
-                            chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                            appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                        }
+                    } else {
+                        appCouponClient.refund(appCouponId.toString());
+                        chargingOrder.setAppCouponId(null);
+                        chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
                     }
                 }
                 //优惠券优惠金额
                 couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN);
-                if (!couponAppliedUpfront) {
-                    refundAmount = refundAmount.add(couponDiscount);
-                }
+                refundAmount = refundAmount.add(couponDiscount);
                 //实际支付金额-优惠券
                 payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO);
                 serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO);
@@ -7550,8 +7473,11 @@
                     BigDecimal rechargeAmount = chargingOrder.getRechargeAmount();
                     //支付金额
                     BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total);
-                    //退款金额=充值金额-实际支付金额
-                    BigDecimal refundAmount = rechargeAmount.subtract(payAmount);
+                    //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。
+                    BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment(
+                            rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(),
+                            chargingOrder.getCouponDiscountAmount());
+                    BigDecimal refundAmount = settlementPrepayment.subtract(payAmount);
 
                     UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode());
                     if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) {
@@ -7581,51 +7507,19 @@
                     //计算优惠券
                     BigDecimal couponDiscount = BigDecimal.ZERO;
                     if (null != chargingOrder.getAppCouponId()) {
-                        //判断实际充电金额是否满足优惠券使用条件,如果不满足则不适用优惠券。
+                        Long appCouponId = chargingOrder.getAppCouponId();
                         TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData();
                         String couponJson = appCoupon.getCouponJson();
                         TCoupon tCoupon = com.alibaba.fastjson2.JSON.parseObject(couponJson, TCoupon.class);
-                        Integer preferentialMode = tCoupon.getPreferentialMode();
-                        if (1 == preferentialMode) {
-                            //满减
-                            if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                                BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount();
-                                //如果优惠金额大于服务费金额,以服务费作为最大限制
-                                if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) {
-                                    couponDiscount = serviceCharge_total;
-                                    serviceCharge_total = BigDecimal.ZERO;
-                                } else {
-                                    couponDiscount = couponDiscountAmount;
-                                }
-                                appCoupon.setStatus(2);
-                                appCouponClient.updateAppCoupon(appCoupon);
-                            } else {
-                                appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                                chargingOrder.setAppCouponId(null);
-                                chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                            }
-                        }
-                        if (2 == preferentialMode) {
-                            //抵扣
-                            if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) {
-                                //折扣金额
-                                BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10));
-                                divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide;
-                                //如果优惠金额大于服务费金额,以服务费作为最大限制
-                                if (serviceCharge_total.compareTo(divide) < 0) {
-                                    couponDiscount = serviceCharge_total;
-                                    serviceCharge_total = BigDecimal.ZERO;
-                                } else {
-                                    couponDiscount = divide;
-                                }
-
-                                appCoupon.setStatus(2);
-                                appCouponClient.updateAppCoupon(appCoupon);
-                            } else {
-                                chargingOrder.setAppCouponId(null);
-                                chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
-                                appCouponClient.refund(chargingOrder.getAppCouponId().toString());
-                            }
+                        couponDiscount = ChargingCouponSupport.calculateSettlementDiscount(
+                                tCoupon, payAmount, serviceCharge_total);
+                        if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
+                            appCoupon.setStatus(2);
+                            appCouponClient.updateAppCoupon(appCoupon);
+                        } else {
+                            appCouponClient.refund(appCouponId.toString());
+                            chargingOrder.setAppCouponId(null);
+                            chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
                         }
                     }
                     //优惠券优惠金额
@@ -7633,10 +7527,8 @@
                     //退款金额+优惠券
                     refundAmount = refundAmount.add(couponDiscount);
                     //实际支付金额-优惠券
-                    payAmount = payAmount.subtract(couponDiscount);
-                    if (serviceCharge_total.compareTo(BigDecimal.ZERO) > 0) {
-                        serviceCharge_total = serviceCharge_total.subtract(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/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
index d369fd1..a6e1774 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
@@ -452,12 +452,8 @@
 	}
 
 	@Override
-	public List<String> invoicingCompanyOptions(TOrderInvoiceQuery query) {
-		if (query == null) {
-			query = new TOrderInvoiceQuery();
-		}
-		applyPartnerSiteScope(query);
-		return this.baseMapper.invoicingCompanyOptions(query);
+	public List<String> invoicingCompanyOptions() {
+		return this.baseMapper.invoicingCompanyOptions();
 	}
 
 	@Override
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
index c6a986f..decb83e 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -2,6 +2,19 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ruoyi.order.mapper.TChargingOrderMapper">
 
+    <select id="getAppUserIdsBySiteIds" resultType="java.lang.Long">
+        SELECT app_user_id
+        FROM t_charging_order
+        WHERE del_flag = 0
+          AND app_user_id IS NOT NULL
+          AND status IN (3, 4, 5)
+          AND site_id IN
+        <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+            #{siteId}
+        </foreach>
+        GROUP BY app_user_id
+    </select>
+
     <!-- 通用查询映射结果 -->
     <resultMap id="BaseResultMap" type="com.ruoyi.order.api.model.TChargingOrder">
         <id column="id" property="id"/>
@@ -92,11 +105,13 @@
 
     <select id="chargingOrder" resultType="com.ruoyi.order.api.vo.ChargingOrderVO">
         select t1.*,
+        app_user.phone,
+        partner.name as partnerName,
         IFNULL(t1.whitelist_electric_refund_amount, 0) + IFNULL(t1.whitelist_service_refund_amount, 0) as whitelistRefundAmount
         from t_charging_order t1
-        <if test="req.phone != null and req.phone != ''">
-            inner join `charging_pile_account`.`t_app_user` app_user on app_user.id = t1.app_user_id
-        </if>
+        left join `charging_pile_account`.`t_app_user` app_user on app_user.id = t1.app_user_id
+        left join `charging_pile_service`.`t_site` site on site.id = t1.site_id
+        left join `charging_pile_service`.`t_partner` partner on partner.id = site.partner_id
         where t1.del_flag = 0
         <if test="null != req.code and req.code!=''">
             and t1.code LIKE CONCAT('%',#{req.code},'%')
@@ -2630,7 +2645,7 @@
 
     <sql id="enterpriseChargingBaseWhere">
         toi.invoicing_object_type = 2
-        AND toi.status IN (1, 2, 3)
+        AND toi.status IN (2, 3)
         AND tod.order_type = 1
         <if test="data.exactCompanyName != null and data.exactCompanyName != ''">
             AND toi.name = #{data.exactCompanyName}
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
index 9de73d5..cfeef00 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
@@ -100,21 +100,8 @@
     <select id="invoicingCompanyOptions" resultType="java.lang.String">
         SELECT DISTINCT TRIM(toi.invoicing_company)
         FROM t_order_invoice toi
-        <where>
-            toi.invoicing_company IS NOT NULL
-            AND TRIM(toi.invoicing_company) != ''
-            <if test="query.siteIds != null and query.siteIds.size() > 0">
-                AND toi.site_id IN
-                <foreach collection="query.siteIds" close=")" open="(" item="siteId" separator=",">
-                    #{siteId}
-                </foreach>
-            </if>
-            <if test="query.directOnly != null and query.directOnly == 1">
-                AND toi.site_id IN (
-                    SELECT id FROM charging_pile_service.t_site WHERE business_category = 1
-                )
-            </if>
-        </where>
+        WHERE toi.invoicing_company IS NOT NULL
+          AND TRIM(toi.invoicing_company) != ''
         ORDER BY TRIM(toi.invoicing_company)
     </select>
     <select id="statusCount" resultType="java.util.Map">
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
index d49b658..de31d5c 100644
--- 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
@@ -12,6 +12,7 @@
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.math.BigDecimal;
 import java.util.Arrays;
 import java.util.List;
@@ -27,9 +28,27 @@
 class TChargingOrderControllerTest {
 
     @Test
+    void appUserIdsBySiteIdsDelegatesToOrderService() {
+        TChargingOrderService chargingOrderService = mock(TChargingOrderService.class);
+        TChargingOrderController controller = new TChargingOrderController();
+        ReflectionTestUtils.setField(controller, "chargingOrderService", chargingOrderService);
+        List<Integer> siteIds = Arrays.asList(11, 12);
+        List<Long> userIds = Arrays.asList(101L, 102L);
+        when(chargingOrderService.getAppUserIdsBySiteIds(siteIds)).thenReturn(userIds);
+
+        com.ruoyi.common.core.domain.R<List<Long>> result = controller.getAppUserIdsBySiteIds(siteIds);
+
+        assertEquals(userIds, result.getData());
+        verify(chargingOrderService).getAppUserIdsBySiteIds(siteIds);
+    }
+
+    @Test
     void enterpriseChargingDetailExportContainsVisibleDetailColumns() {
         EnterpriseChargingDetailVO detail = new EnterpriseChargingDetailVO();
         detail.setLicensePlate("川JD20960");
+        detail.setPartnerName("测试合作商");
+        detail.setOrderSource(0);
+        detail.setCreateTime(LocalDateTime.of(2026, 7, 29, 10, 20, 30));
         detail.setChargingCapacity(new BigDecimal("40.693"));
         detail.setElectrovalence(new BigDecimal("43.0857"));
         detail.setServiceCharge(new BigDecimal("16.2772"));
@@ -40,6 +59,9 @@
                 new TChargingOrderController(), "buildEnterpriseChargingDetailExport", detail);
 
         assertEquals("川JD20960", export.getLicensePlate());
+        assertEquals("测试合作商", export.getPartnerName());
+        assertEquals("自有平台", export.getOrderSource());
+        assertEquals("2026-07-29 10:20:30", export.getCreateTime());
         assertEquals(new BigDecimal("40.693"), export.getChargingCapacity());
         assertEquals(new BigDecimal("43.0857"), export.getElectrovalence());
         assertEquals(new BigDecimal("16.2772"), export.getServiceCharge());
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
index 768f0e5..ac4db38 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
@@ -1,7 +1,10 @@
 package com.ruoyi.order.controller;
 
+import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.export.EnterpriseChargingDetailExport;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -19,4 +22,32 @@
 
         assertEquals("2026-07-28 09:08:07", TVipOrderController.formatExportDateTime(time));
     }
+
+    @Test
+    void fillsVipChargingOrderExportDetailFields() {
+        ChargingOrderVO order = new ChargingOrderVO();
+        order.setPartnerName("测试合作商");
+        order.setPhone("13800000000");
+        order.setOrderSource(1);
+        order.setCreateTime(LocalDateTime.of(2026, 7, 29, 10, 20, 30));
+        order.setLicensePlate("川A12345");
+        order.setChargingCapacity(new BigDecimal("12.3456"));
+        order.setElectrovalence(new BigDecimal("8.12"));
+        order.setServiceCharge(new BigDecimal("3.45"));
+        order.setEndMode(2);
+        order.setCount(4);
+
+        EnterpriseChargingDetailExport export = TVipOrderController.buildVipChargingOrderExport(order);
+
+        assertEquals("测试合作商", export.getPartnerName());
+        assertEquals("13800000000", export.getPhone());
+        assertEquals("明星充电", export.getOrderSource());
+        assertEquals("2026-07-29 10:20:30", export.getCreateTime());
+        assertEquals("川A12345", export.getLicensePlate());
+        assertEquals(new BigDecimal("12.3456"), export.getChargingCapacity());
+        assertEquals(new BigDecimal("8.12"), export.getElectrovalence());
+        assertEquals(new BigDecimal("3.45"), export.getServiceCharge());
+        assertEquals("满电终止", export.getEndMode());
+        assertEquals(4, export.getPeriodCount());
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
index df5d7d8..53b2a48 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
@@ -35,6 +35,8 @@
 
         assertFalse(baseWhere.contains("co.del_flag"));
         assertFalse(baseWhere.contains("co.recharge_payment_status"));
+        assertTrue(baseWhere.contains("AND toi.status IN (2, 3)"));
+        assertFalse(baseWhere.contains("AND toi.status IN (1, 2, 3)"));
         assertTrue(analysis.contains("LEFT JOIN t_charging_order co ON co.id = tod.order_id"));
         assertTrue(analysis.contains("COUNT(1) AS invoiceCount"));
         assertTrue(analysis.contains("detailInvoiceAmount"));
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java
new file mode 100644
index 0000000..2cc9925
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java
@@ -0,0 +1,37 @@
+package com.ruoyi.order.mapper;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TOrderInvoiceCompanyOptionsMapperTest {
+
+    @Test
+    void invoicingCompanyOptionsQueriesAllNonBlankCompanies() throws IOException {
+        InputStream input = getClass().getClassLoader()
+                .getResourceAsStream("mapper/order/TOrderInvoiceMapper.xml");
+        assertNotNull(input);
+
+        String mapper;
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
+            mapper = reader.lines().collect(Collectors.joining("\n"));
+        }
+        int start = mapper.indexOf("<select id=\"invoicingCompanyOptions\"");
+        int end = mapper.indexOf("</select>", start);
+        String statement = mapper.substring(start, end);
+
+        assertTrue(statement.contains("SELECT DISTINCT TRIM(toi.invoicing_company)"));
+        assertTrue(statement.contains("toi.invoicing_company IS NOT NULL"));
+        assertFalse(statement.contains("query."));
+        assertFalse(statement.contains("site_id"));
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java
new file mode 100644
index 0000000..72fbfc6
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java
@@ -0,0 +1,36 @@
+package com.ruoyi.order.mapper;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class VipChargingOrderExportMapperTest {
+
+    @Test
+    void chargingOrderQueryIncludesUserPhoneAndPartnerName() throws IOException {
+        InputStream input = getClass().getClassLoader()
+                .getResourceAsStream("mapper/order/TChargingOrderMapper.xml");
+        assertNotNull(input);
+
+        String mapper;
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
+            mapper = reader.lines().collect(Collectors.joining("\n"));
+        }
+        String statement = mapper.substring(
+                mapper.indexOf("<select id=\"chargingOrder\""),
+                mapper.indexOf("<select id=\"payOrderQuery\""));
+
+        assertTrue(statement.contains("app_user.phone"));
+        assertTrue(statement.contains("partner.name as partnerName"));
+        assertTrue(statement.contains("left join `charging_pile_account`.`t_app_user`"));
+        assertTrue(statement.contains("left join `charging_pile_service`.`t_partner`"));
+    }
+}
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
index 602ffcd..c7b2cce 100644
--- 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
@@ -6,44 +6,81 @@
 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() {
+    void couponDoesNotReducePrepayment() {
         TCoupon coupon = chargingCoupon(1);
-        coupon.setMeetTheConditions(new BigDecimal("3"));
-        coupon.setDiscountAmount(new BigDecimal("1"));
+        coupon.setMeetTheConditions(new BigDecimal("30"));
+        coupon.setDiscountAmount(new BigDecimal("10"));
 
-        BigDecimal discount = ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("3"));
+        BigDecimal paymentAmount = ChargingCouponSupport.calculatePrepaymentAmount(new BigDecimal("30"), false);
 
-        assertEquals(new BigDecimal("1.00"), discount);
-        assertEquals(new BigDecimal("2.00"), new BigDecimal("3").subtract(discount));
+        assertEquals(new BigDecimal("30.00"), paymentAmount);
     }
 
     @Test
-    void percentageDiscountHonorsMaximumDiscount() {
+    void legacyUpfrontDiscountOrderUsesActuallyCollectedAmountAsRefundBase() {
+        BigDecimal prepayment = ChargingCouponSupport.resolveSettlementPrepayment(
+                new BigDecimal("30"), BigDecimal.ZERO, new BigDecimal("20"), new BigDecimal("10"));
+
+        assertEquals(new BigDecimal("20"), prepayment);
+    }
+
+    @Test
+    void fullReductionCannotDiscountElectricity() {
+        TCoupon coupon = chargingCoupon(1);
+        coupon.setMeetTheConditions(new BigDecimal("10"));
+        coupon.setDiscountAmount(new BigDecimal("10"));
+
+        BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount(
+                coupon, new BigDecimal("15"), new BigDecimal("3"));
+
+        assertEquals(new BigDecimal("3.0000"), discount);
+    }
+
+    @Test
+    void percentageDiscountHonorsMaximumDiscountAndServiceCharge() {
         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"));
+        BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount(
+                coupon, new BigDecimal("10"), new BigDecimal("5"));
 
-        assertEquals(new BigDecimal("1.50"), discount);
+        assertEquals(new BigDecimal("1.5000"), discount);
     }
 
     @Test
-    void rejectsPrepaymentBelowCouponThreshold() {
+    void couponBelowActualSettlementThresholdIsNotUsed() {
         TCoupon coupon = chargingCoupon(1);
-        coupon.setMeetTheConditions(new BigDecimal("3"));
+        coupon.setMeetTheConditions(new BigDecimal("30"));
         coupon.setDiscountAmount(new BigDecimal("1"));
 
-        IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
-                () -> ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("2.99")));
+        BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount(
+                coupon, new BigDecimal("29.99"), new BigDecimal("5"));
 
-        assertEquals("预付金额未达到优惠券使用门槛", error.getMessage());
+        assertEquals(new BigDecimal("0.0000"), discount);
+    }
+
+    @Test
+    void nonFullWhitelistOnlyWaivesServiceCharge() {
+        BigDecimal[] split = ChargingCouponSupport.calculateWhitelistRefundSplit(
+                true, false, new BigDecimal("10"), new BigDecimal("5"));
+
+        assertEquals(BigDecimal.ZERO, split[0]);
+        assertEquals(new BigDecimal("5"), split[1]);
+    }
+
+    @Test
+    void fullWhitelistWaivesElectricityAndServiceCharge() {
+        BigDecimal[] split = ChargingCouponSupport.calculateWhitelistRefundSplit(
+                true, true, new BigDecimal("10"), new BigDecimal("5"));
+
+        assertEquals(new BigDecimal("10"), split[0]);
+        assertEquals(new BigDecimal("5"), split[1]);
     }
 
     private TCoupon chargingCoupon(int preferentialMode) {

--
Gitblit v1.7.1