longz
2026-07-29 7858ae894c398f2f9248ac898b2ff200fa36225d
各类修复
4个文件已添加
24个文件已修改
985 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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());
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);
//    /**
//     * 获取充电订单列表
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 = "记录按钮权限 ")
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);
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();
    }
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
New file
@@ -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();
    }
}
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;
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
New file
@@ -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);
    }
}
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));
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));
    }
    //订单详情
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());
    }
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);
    }
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;
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);
    
    
    /**
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();
    /**
     * 查询开票状态数量
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小时的订单
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();
    /**
     * 查询开票状态数量
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;
    }
}
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));
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
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}
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">
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());
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());
    }
}
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"));
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java
New file
@@ -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"));
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java
New file
@@ -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`"));
    }
}
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) {