puzhibing
昨天 c825a6f7e52ce1d61b21c352861c1da082a9fcb6
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -15,6 +15,7 @@
import com.ruoyi.account.api.vo.GetAppUserVipDetail;
import com.ruoyi.account.api.vo.GetInviteUser;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
import com.ruoyi.chargingPile.api.feignClient.*;
import com.ruoyi.chargingPile.api.model.*;
import com.ruoyi.chargingPile.api.support.ReservationTimeRangeSupport;
@@ -55,11 +56,13 @@
import com.ruoyi.order.mapper.TChargingOrderMapper;
import com.ruoyi.order.mapper.TChargingOrderShareRecordMapper;
import com.ruoyi.order.mapper.TSettlementConfirmMapper;
import com.ruoyi.order.mapper.TVinChargingOrderMapper;
import com.ruoyi.order.service.TChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TChargingOrderRefundService;
import com.ruoyi.order.service.TChargingOrderService;
import com.ruoyi.order.service.TOrderEvaluateService;
import com.ruoyi.order.service.*;
import com.ruoyi.order.support.ChargeAccountSupport;
import com.ruoyi.order.support.ReservationOrderSupport;
import com.ruoyi.order.support.ReservationRecommendationCalculator;
import com.ruoyi.order.util.mongodb.service.PlatformStartChargingReplyService;
@@ -74,6 +77,8 @@
import com.ruoyi.other.api.domain.*;
import com.ruoyi.order.vo.ChargingOrderListInfoVO;
import com.ruoyi.other.api.domain.TVip;
import com.ruoyi.other.api.dto.ChargeAuthorizationDTO;
import com.ruoyi.other.api.dto.CompanyPreferentialDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.feignClient.*;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
@@ -136,8 +141,6 @@
    private Logger log = LoggerFactory.getLogger(TChargingOrderServiceImpl.class);
    private static final int WHITELIST_BENEFIT_ALL_FREE = 1;
    private static final int WHITELIST_BENEFIT_ELECTRIC_FREE = 2;
    private static final int WHITELIST_BENEFIT_SERVICE_FREE = 3;
    private static final String SHARE_AFTER_COMMIT_RESOURCE_PREFIX = "chargingOrderShareAfterCommit:";
    private static final String SPECIAL_REFUND_TITLE = "特殊退款处理";
    private static final int RESERVATION_RECOMMENDATION_HISTORY_DAYS = 30;
@@ -238,6 +241,8 @@
    private AppCouponClient appCouponClient;
    @Resource
    private AppUserWhitelistClient appUserWhitelistClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private AppUserVipDetailClient appUserVipDetailClient;
@@ -247,6 +252,9 @@
    @Resource
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private TVinChargingOrderMapper vinChargingOrderMapper;
    @Resource
    private TCECClient tcecClient;
@@ -267,6 +275,12 @@
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private ChargeAccountSupport chargeAccountSupport;
    @Resource
    private ChargeAuthorizationClient chargeAuthorizationClient;
    @Resource
    private PlatformStartChargingReplyService platformStartChargingReplyService;
@@ -766,6 +780,140 @@
    }
    /**
     * 判断订单是否为白名单专属订单(企业账户流程订单或历史白名单权益订单):
     * 此类订单按企业优惠/历史权益结算,不参与会员折扣、优惠券等普通用户特权;
     * 白名单用户选择第三方支付的订单(无企业账户模式、无权益类型)不在此列,按普通用户流程执行。
     *
     * @param chargingOrder 充电订单
     * @return true=白名单专属订单
     */
    private boolean isWhitelistSpecialOrder(TChargingOrder chargingOrder) {
        return isWhitelistOrder(chargingOrder)
                && (chargingOrder.getAccountMode() != null || chargingOrder.getWhitelistBenefitType() != null);
    }
    /**
     * 判断订单是否为白名单企业账户订单(企业账户流程的唯一判定口径):
     * 白名单订单且已写入账户模式(共享/分配)时按企业优惠与额度结算,第三方支付白名单订单/历史权益订单返回false。
     *
     * @param chargingOrder 充电订单
     * @return true=白名单企业账户订单
     */
    @Override
    public boolean isWhitelistEnterpriseOrder(TChargingOrder chargingOrder) {
        return null != chargingOrder && isWhitelistSpecialOrder(chargingOrder) && null != chargingOrder.getAccountMode();
    }
    /**
     * 解析白名单企业账户充电上下文(白名单用户选择余额支付时调用):
     * 1) 校验归属公司存在且启用,未归属/停用直接拒绝;
     * 2) 校验企业充电授权(站点范围、可用时段/电价阶段)并计算授权截止时间,未配置授权按放行处理(对齐 VIN 口径);
     * 3) 下发额度 = min(用户输入充值金额, 账户可用额度, 企业设定的充值金额):
     *    - 账户可用额度:共享模式=公司可用余额,分配模式=白名单账户余额;
     *    - 企业设定充值金额=授权配置下发金额,未配置默认 200 元;
     *    - 用户输入金额为空或非正数时按 min(企业设定充值金额, 账户可用额度) 下发。
     *
     * @param whitelist       白名单命中结果(含归属公司与账户余额快照)
     * @param userInputAmount 用户输入的充值金额
     * @param siteId          站点ID(授权站点范围校验用)
     * @param strategyId      计费策略ID(指定电价时段授权校验用)
     * @return 企业账户上下文;失败时 failReason 为提示文案
     */
    private ChargeAccountSupport.ChargeContext resolveWhitelistEnterpriseContext(AppUserWhitelistMatchVO whitelist,
                                                                                BigDecimal userInputAmount,
                                                                                Integer siteId,
                                                                                Integer strategyId) {
        Integer companyId = whitelist.getCompanyId();
        if (companyId == null) {
            return ChargeAccountSupport.ChargeContext.fail(2, "账号未归属公司,无法使用企业额度充电,请选择其他支付方式");
        }
        TGroupCompany company;
        try {
            R<TGroupCompany> companyResult = groupCompanyClient.getCompanyById(companyId);
            company = companyResult == null ? null : companyResult.getData();
        } catch (Exception ex) {
            log.error("[CD建单] 查询所属公司失败 companyId={}", companyId, ex);
            return ChargeAccountSupport.ChargeContext.fail(2, "查询所属公司失败,请稍后重试或选择其他支付方式");
        }
        if (company == null) {
            return ChargeAccountSupport.ChargeContext.fail(2, "所属公司不存在,无法使用企业额度充电");
        }
        if (company.getStatus() != null && company.getStatus() != 1) {
            return ChargeAccountSupport.ChargeContext.fail(2, "所属公司已停用,无法使用企业额度充电");
        }
        int accountMode = company.getAccountMode() == null
                ? ChargeAccountSupport.ACCOUNT_MODE_SHARED : company.getAccountMode();
        ChargeAuthorizationDTO authorization = null;
        try {
            R<ChargeAuthorizationDTO> authResult = chargeAuthorizationClient.getDetailByCompanyId(companyId);
            authorization = authResult == null ? null : authResult.getData();
        } catch (Exception ex) {
            log.error("[CD建单] 查询充电授权失败,按未配置授权放行 companyId={}", companyId, ex);
        }
        LocalDateTime maxEndTime = null;
        if (authorization != null) {
            if (authorization.getSiteIds() != null && !authorization.getSiteIds().isEmpty()
                    && (siteId == null || !authorization.getSiteIds().contains(siteId))) {
                return ChargeAccountSupport.ChargeContext.fail(5, "当前站点不在企业充电授权范围内,请选择其他支付方式");
            }
            ChargeAccountSupport.AuthorizationWindow window = chargeAccountSupport
                    .resolveAuthorizationWindow(authorization, strategyId, companyId);
            if (window == null) {
                return ChargeAccountSupport.ChargeContext.fail(2, "当前时间不在企业充电授权时段内,请选择其他支付方式");
            }
            maxEndTime = window.getMaxEndTime();
        }
        BigDecimal usableAmount = accountMode == ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED
                ? whitelist.getBalance() : company.getAvailableBalance();
        BigDecimal usable = money(usableAmount);
        if (usable.compareTo(BigDecimal.ZERO) <= 0) {
            return ChargeAccountSupport.ChargeContext.fail(3, "企业额度不足,请选择其他支付方式");
        }
        BigDecimal enterpriseLimit = authorization == null ? null : authorization.getDispatchAmount();
        if (enterpriseLimit == null || enterpriseLimit.compareTo(BigDecimal.ZERO) <= 0) {
            enterpriseLimit = ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT;
        }
        BigDecimal dispatch = money(userInputAmount);
        dispatch = dispatch.compareTo(BigDecimal.ZERO) <= 0
                ? enterpriseLimit.min(usable)
                : dispatch.min(enterpriseLimit).min(usable);
        return ChargeAccountSupport.ChargeContext.ok(accountMode, dispatch.setScale(2, RoundingMode.HALF_UP), maxEndTime);
    }
    /**
     * 锁定企业充电额度(共享=企业账户余额,分配=白名单账户余额),返回是否锁定成功
     *
     * @param context   企业账户上下文(含账户模式与下发额度)
     * @param whitelist 白名单命中结果(含白名单ID与归属公司ID)
     * @return true=锁定成功
     */
    private boolean lockWhitelistEnterpriseAmount(ChargeAccountSupport.ChargeContext context,
                                                  AppUserWhitelistMatchVO whitelist) {
        try {
            R<BigDecimal> result;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(context.getAccountMode())) {
                result = appUserWhitelistClient.lockBalance(whitelist.getWhitelistId(), context.getDispatchAmount());
            } else {
                result = groupCompanyClient.lockBalance(whitelist.getCompanyId(), context.getDispatchAmount());
            }
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[CD建单] 锁定企业额度失败 accountMode={} companyId={} whitelistId={} amount={} msg={}",
                        context.getAccountMode(), whitelist.getCompanyId(), whitelist.getWhitelistId(),
                        context.getDispatchAmount(), result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[CD建单] 锁定企业额度异常 accountMode={} companyId={} whitelistId={} amount={}",
                    context.getAccountMode(), whitelist.getCompanyId(), whitelist.getWhitelistId(),
                    context.getDispatchAmount(), ex);
            return false;
        }
    }
    /**
     * 按白名单权益拆分电费和服务费减免金额,财务统计需要分开展示两类退款金额
     *
     * @param chargingOrder  充电订单
@@ -774,11 +922,229 @@
     * @return 数组下标0=电费减免,下标1=服务费减免
     */
    private BigDecimal[] calculateWhitelistRefundSplit(TChargingOrder chargingOrder, BigDecimal electrovalence, BigDecimal serviceCharge) {
        return ChargingCouponSupport.calculateWhitelistRefundSplit(
        return ChargingCouponSupport.calculateWhitelistRefundSplitByBenefitType(
                isWhitelistOrder(chargingOrder),
                isWhitelistAllFreeOrder(chargingOrder),
                chargingOrder == null ? null : chargingOrder.getWhitelistBenefitType(),
                electrovalence,
                serviceCharge);
    }
    /**
     * 结算企业账户额度(白名单企业账户流程):
     * 共享模式结算公司余额,分配模式结算白名单账户余额;按实际消费扣减锁定额度、差额退回原账户,
     * 结算失败返回false,由调用方回滚订单状态等待补偿重试。
     *
     * @param chargingOrder 充电订单(含companyId/accountMode/lockedAmount/whitelistId)
     * @param actualAmount  实际消费金额
     * @return true=结算成功或非企业账户订单
     */
    @Override
    public boolean settleWhitelistEnterpriseAccount(TChargingOrder chargingOrder, BigDecimal actualAmount) {
        if (!isWhitelistEnterpriseOrder(chargingOrder)) {
            return true;
        }
        BigDecimal lockedAmount = money(chargingOrder.getLockedAmount());
        BigDecimal actual = money(actualAmount).max(BigDecimal.ZERO);
        if (lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            log.info("[CD结算] 企业账户订单无锁定金额,跳过额度结算 code={}", chargingOrder.getCode());
            return true;
        }
        boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(chargingOrder.getAccountMode());
        BigDecimal afterBalance;
        try {
            R<BigDecimal> settleResult;
            if (allocated && null != chargingOrder.getWhitelistId()) {
                settleResult = appUserWhitelistClient.settleBalance(chargingOrder.getWhitelistId(), lockedAmount, actual);
            } else {
                // 订单侧自行写消费流水,此处不传订单号(账户侧仅在订单号非空时做幂等与流水,避免重复记账)
                settleResult = groupCompanyClient.settleBalance(chargingOrder.getCompanyId(), lockedAmount, actual,
                        null, null, null, null);
            }
            if (null == settleResult || R.SUCCESS != settleResult.getCode()) {
                log.error("[CD结算] 企业账户额度结算失败 code={} accountMode={} companyId={} locked={} actual={} msg={}",
                        chargingOrder.getCode(), chargingOrder.getAccountMode(), chargingOrder.getCompanyId(),
                        lockedAmount, actual, null == settleResult ? "null" : settleResult.getMsg());
                return false;
            }
            afterBalance = settleResult.getData();
        } catch (Exception ex) {
            log.error("[CD结算] 企业账户额度结算异常 code={} accountMode={} companyId={} locked={} actual={}",
                    chargingOrder.getCode(), chargingOrder.getAccountMode(), chargingOrder.getCompanyId(), lockedAmount, actual, ex);
            return false;
        }
        recordWhitelistEnterpriseConsume(chargingOrder, actual, lockedAmount, afterBalance, allocated);
        log.info("[CD结算] 企业账户额度结算完成 code={} accountMode={} locked={} actual={} balance={}",
                chargingOrder.getCode(), chargingOrder.getAccountMode(), lockedAmount, actual, afterBalance);
        return true;
    }
    /**
     * 写企业额度分配记录-消费流水:分配模式记白名单账户,共享模式记企业账户;
     * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推),流水失败不影响结算结果。
     */
    private void recordWhitelistEnterpriseConsume(TChargingOrder chargingOrder, BigDecimal actual, BigDecimal lockedAmount,
                                                  BigDecimal afterBalance, boolean allocated) {
        if (actual.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        String userInfo;
        String userName;
        if (allocated) {
            userInfo = null == chargingOrder.getWhitelistId() ? null : String.valueOf(chargingOrder.getWhitelistId());
            userName = userInfo;
        } else {
            TGroupCompany company = chargeAccountSupport.queryCompanyQuietly(chargingOrder.getCompanyId());
            String companyName = null == company ? null : company.getName();
            userInfo = null == companyName || companyName.isEmpty() ? String.valueOf(chargingOrder.getCompanyId()) : companyName;
            userName = companyName;
        }
        BigDecimal after = null == afterBalance ? BigDecimal.ZERO : afterBalance;
        BigDecimal before = after.add(actual).subtract(lockedAmount);
        chargeAccountSupport.recordConsume(chargingOrder.getCompanyId(),
                allocated ? TCompanyAllocateRecord.USER_TYPE_WHITELIST : TCompanyAllocateRecord.USER_TYPE_COMPANY,
                userInfo, userName, before, actual.negate(), chargingOrder.getCode(), "充电桩充电结算");
    }
    /**
     * 释放企业账户锁定额度(订单未结算/关单时调用;失败仅记录日志,不影响主流程)
     * 释放幂等:先用条件更新抢占 lock_released 标记(0->1),保证同一订单的锁定额在多路径并发或
     * 重复补偿下只释放一次;释放失败把标记回滚为 0,留待后续补偿或人工重试。
     */
    @Override
    public void unlockWhitelistEnterpriseAmountQuietly(TChargingOrder chargingOrder, String scene) {
        if (!isWhitelistEnterpriseOrder(chargingOrder)) {
            return;
        }
        BigDecimal lockedAmount = money(chargingOrder.getLockedAmount());
        if (lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        boolean claimed;
        try {
            claimed = lambdaUpdate()
                    .eq(TChargingOrder::getId, chargingOrder.getId())
                    .eq(TChargingOrder::getLockReleased, 0)
                    .set(TChargingOrder::getLockReleased, 1)
                    .update();
        } catch (Exception ex) {
            log.error("[CD关单] 抢占锁定额释放标记异常 scene={} code={}", scene, chargingOrder.getCode(), ex);
            return;
        }
        if (!claimed) {
            log.info("[CD关单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, chargingOrder.getCode());
            return;
        }
        boolean unlocked = false;
        try {
            R<BigDecimal> result;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(chargingOrder.getAccountMode())
                    && null != chargingOrder.getWhitelistId()) {
                result = appUserWhitelistClient.unlockBalance(chargingOrder.getWhitelistId(), lockedAmount);
            } else {
                result = groupCompanyClient.unlockBalance(chargingOrder.getCompanyId(), lockedAmount);
            }
            if (null == result || R.SUCCESS != result.getCode()) {
                log.error("[CD关单] 释放企业账户锁定额度失败 scene={} code={} companyId={} amount={} msg={}",
                        scene, chargingOrder.getCode(), chargingOrder.getCompanyId(), lockedAmount,
                        null == result ? "null" : result.getMsg());
            } else {
                unlocked = true;
                log.info("[CD关单] 释放企业账户锁定额度完成 scene={} code={} amount={}", scene, chargingOrder.getCode(), lockedAmount);
            }
        } catch (Exception ex) {
            log.error("[CD关单] 释放企业账户锁定额度异常 scene={} code={} companyId={} amount={}",
                    scene, chargingOrder.getCode(), chargingOrder.getCompanyId(), lockedAmount, ex);
        }
        if (!unlocked) {
            // 释放失败回滚标记(1->0),保证锁定额不被“标记已释放”而漏释放,可被后续补偿重试
            lambdaUpdate()
                    .eq(TChargingOrder::getId, chargingOrder.getId())
                    .eq(TChargingOrder::getLockReleased, 1)
                    .set(TChargingOrder::getLockReleased, 0)
                    .update();
        }
    }
    /**
     * 企业账户订单无任何设备数据/账单时的兜底关单:释放锁定额度并终结订单(金额0、状态5、结算完成标记),
     * 保证订单不被任何补偿任务重复扫描重跑。释放幂等由 lock_released 抢占保证;状态条件更新保证只终结一次。
     */
    @Override
    public void closeEnterpriseOrderWithoutData(TChargingOrder chargingOrder) {
        if (!isWhitelistEnterpriseOrder(chargingOrder) || null == chargingOrder.getStatus()) {
            return;
        }
        if (2 != chargingOrder.getStatus() && 3 != chargingOrder.getStatus() && 4 != chargingOrder.getStatus()) {
            return;
        }
        // 释放企业账户锁定额度(lock_released 抢占保证只释放一次,失败回滚标记留待后续补偿重试)
        unlockWhitelistEnterpriseAmountQuietly(chargingOrder, "无设备数据关单");
        boolean closed = lambdaUpdate()
                .eq(TChargingOrder::getId, chargingOrder.getId())
                .in(TChargingOrder::getStatus, 2, 3, 4)
                .set(TChargingOrder::getStatus, 5)
                .set(TChargingOrder::getSettleStatus, 1)
                .set(TChargingOrder::getRefundStatus, 2)
                .set(TChargingOrder::getEndMode, 1)
                .set(TChargingOrder::getEndTime, LocalDateTime.now())
                .update();
        if (closed) {
            log.info("[CD关单] 企业账户订单无设备数据兜底关单完成 code={} companyId={} lockedAmount={}",
                    chargingOrder.getCode(), chargingOrder.getCompanyId(), chargingOrder.getLockedAmount());
        } else {
            log.info("[CD关单] 企业账户订单状态已变更,跳过无设备数据兜底关单 code={}", chargingOrder.getCode());
        }
    }
    /**
     * 判断是否触发授权时间到期主动停充(白名单企业账户订单按授权截止时间提前下发停充指令)
     */
    private boolean needAuthorizeStop(TChargingOrder chargingOrder) {
        if (null == chargingOrder || !isWhitelistSpecialOrder(chargingOrder) || null == chargingOrder.getMaxEndTime()) {
            return false;
        }
        LocalDateTime startTime = null != chargingOrder.getStartTime() ? chargingOrder.getStartTime() : chargingOrder.getCreateTime();
        return chargeAccountSupport.needAuthorizeStop(chargingOrder.getMaxEndTime(), startTime);
    }
    /**
     * 授权时间到期主动停充:复用小程序主动停充口径(状态3-&gt;4、结束方式1)并下发硬件停止充电指令
     */
    private void stopByAuthorizeExpired(TChargingOrder chargingOrder) {
        TChargingGun gun;
        TChargingPile pile;
        try {
            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(chargingOrder.getChargingGunId());
            gun = null == gunResult ? null : gunResult.getData();
            R<TChargingPile> pileResult = null == chargingOrder.getChargingPileId() ? null
                    : chargingPileClient.getChargingPileById(chargingOrder.getChargingPileId());
            pile = null == pileResult ? null : pileResult.getData();
        } catch (Exception ex) {
            log.error("[CD停充] 授权到期停充查询设备异常 code={}", chargingOrder.getCode(), ex);
            return;
        }
        if (null == gun || null == pile) {
            log.error("[CD停充] 授权到期停充缺少设备信息 code={} gunId={}", chargingOrder.getCode(), chargingOrder.getChargingGunId());
            return;
        }
        boolean updated = this.lambdaUpdate()
                .eq(TChargingOrder::getId, chargingOrder.getId())
                .eq(TChargingOrder::getStatus, 3)
                .set(TChargingOrder::getStatus, 4)
                .set(TChargingOrder::getEndMode, 1)
                .set(TChargingOrder::getEndTime, LocalDateTime.now())
                .update();
        if (!updated) {
            return;
        }
        chargingOrder.setStatus(4);
        chargingOrder.setEndMode(1);
        chargingOrder.setEndTime(LocalDateTime.now());
        PlatformStopCharging platformStopCharging = new PlatformStopCharging();
        platformStopCharging.setCharging_pile_code(pile.getCode());
        platformStopCharging.setCharging_gun_code(gun.getCode());
        sendMessageClient.platformStopCharging(platformStopCharging);
        log.warn("[CD停充] 授权时间到期主动停充 code={} maxEndTime={}", chargingOrder.getCode(), chargingOrder.getMaxEndTime());
    }
    private TVip getVipSnapshot(TAppUserVipDetail vipDetail, Integer vipId) {
@@ -834,34 +1200,44 @@
        }
        AppUserWhitelistMatchVO whitelist = appUserWhitelistClient.matchWhitelist(userId, tChargingGun.getSiteId()).getData();
        boolean whitelistMatched = whitelist != null && Boolean.TRUE.equals(whitelist.getMatched());
        if (whitelistMatched) {
            addChargingOrder.setAppUserCouponId(null);
        if (whitelistMatched && Integer.valueOf(2).equals(payMethod)) {
            // 白名单用户不支持组合支付:仅支持余额支付(企业额度)与第三方支付
            return AjaxResult.error("白名单用户不支持组合支付,请选择余额支付或第三方支付");
        }
        boolean whitelistAllFree = whitelistMatched
                && Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(whitelist.getBenefitType());
        // 白名单用户按支付方式分流:余额支付=企业账户流程(锁定额度+公司优惠结算),第三方支付=按普通用户流程执行
        boolean enterpriseCharging = whitelistMatched && Integer.valueOf(1).equals(payMethod);
        BigDecimal userBalance = money(appUser.getBalance());
        BigDecimal balancePayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        if (payMethod == 2 && userBalance.compareTo(rechargeAmount) >= 0) {
            payMethod = 1;
            addChargingOrder.setPayMethod(1);
        }
        if (isBalancePayMode(payMethod)) {
        BigDecimal payableAmount;
        BigDecimal thirdPayAmount;
        if (enterpriseCharging) {
            // 企业账户流程不使用用户余额与优惠券,额度在企业上下文解析后锁定并作为下发金额
            addChargingOrder.setAppUserCouponId(null);
        } else if (!whitelistMatched && addChargingOrder.getAppUserCouponId() != null) {
            // 这里只校验优惠券,不抵扣预付金额;实际门槛和优惠金额在充电结束时计算。
            validateSelectedChargingCoupon(addChargingOrder.getAppUserCouponId(), userId);
        }
        BigDecimal payableAmount = ChargingCouponSupport.calculatePrepaymentAmount(rechargeAmount, whitelistAllFree);
        BigDecimal thirdPayAmount = payableAmount;
        if (isBalancePayMode(payMethod)) {
            if (payMethod == 1 && userBalance.compareTo(payableAmount) < 0) {
                return AjaxResult.error("余额不足,请选择组合支付或第三方支付");
            payableAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
            thirdPayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        } else {
            if (payMethod == 2 && userBalance.compareTo(rechargeAmount) >= 0) {
                payMethod = 1;
                addChargingOrder.setPayMethod(1);
            }
            balancePayAmount = payMethod == 1 ? payableAmount : userBalance.min(payableAmount).setScale(2, RoundingMode.HALF_UP);
            thirdPayAmount = payableAmount.subtract(balancePayAmount).setScale(2, RoundingMode.HALF_UP);
            if (isBalancePayMode(payMethod)) {
                addChargingOrder.setAppUserCouponId(null);
            } else if (addChargingOrder.getAppUserCouponId() != null) {
                // 这里只校验优惠券,不抵扣预付金额;实际门槛和优惠金额在充电结束时计算。
                validateSelectedChargingCoupon(addChargingOrder.getAppUserCouponId(), userId);
            }
            payableAmount = ChargingCouponSupport.calculatePrepaymentAmount(rechargeAmount, false);
            thirdPayAmount = payableAmount;
            if (isBalancePayMode(payMethod)) {
                if (payMethod == 1 && userBalance.compareTo(payableAmount) < 0) {
                    return AjaxResult.error("余额不足,请选择组合支付或第三方支付");
                }
                balancePayAmount = payMethod == 1 ? payableAmount : userBalance.min(payableAmount).setScale(2, RoundingMode.HALF_UP);
                thirdPayAmount = payableAmount.subtract(balancePayAmount).setScale(2, RoundingMode.HALF_UP);
            }
            payMethod = ChargingPaymentSupport.resolvePayMethod(balancePayAmount, thirdPayAmount, payMethod);
            addChargingOrder.setPayMethod(payMethod);
        }
        payMethod = ChargingPaymentSupport.resolvePayMethod(balancePayAmount, thirdPayAmount, payMethod);
        addChargingOrder.setPayMethod(payMethod);
        //检查当前枪是否是正在使用中
        TChargingOrder one = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getChargingGunId, addChargingOrder.getId())
                .in(TChargingOrder::getStatus, Arrays.asList(3)).eq(TChargingOrder::getDelFlag, 0));
@@ -958,7 +1334,10 @@
        chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO);
        chargingOrder.setIsWhitelist(whitelistMatched ? 1 : 0);
        chargingOrder.setWhitelistId(whitelistMatched ? whitelist.getWhitelistId() : null);
        chargingOrder.setWhitelistBenefitType(whitelistMatched ? whitelist.getBenefitType() : null);
        // 白名单权益已下线:订单不再记录权益类型,企业优惠通过 discount_* 列体现
        chargingOrder.setWhitelistBenefitType(null);
        chargingOrder.setCompanyId(whitelistMatched ? whitelist.getCompanyId() : null);
        chargingOrder.setFleetId(whitelistMatched ? whitelist.getFleetId() : null);
        chargingOrder.setWhitelistDiscountAmount(BigDecimal.ZERO);
        chargingOrder.setWhitelistElectricRefundAmount(BigDecimal.ZERO);
        chargingOrder.setWhitelistServiceRefundAmount(BigDecimal.ZERO);
@@ -981,8 +1360,26 @@
        }
        TAccountingStrategy accountingStrategy = accountingStrategyClient.getAccountingStrategyById(accountingStrategyId).getData();
        //会员折扣支持直营网点和加盟商站点
        if (!whitelistMatched && Integer.valueOf(1).equals(site.getMemberDiscountFlag()) && null != appUser.getVipId()) {
        // 白名单企业账户流程:解析企业授权上下文并锁定额度(下发额度=min(用户输入充值金额, 账户可用额度, 企业设定充值金额))
        if (enterpriseCharging) {
            ChargeAccountSupport.ChargeContext chargeContext =
                    resolveWhitelistEnterpriseContext(whitelist, rechargeAmount, tChargingGun.getSiteId(), accountingStrategyId);
            if (!chargeContext.isOk()) {
                return AjaxResult.error(chargeContext.getFailReason());
            }
            if (!lockWhitelistEnterpriseAmount(chargeContext, whitelist)) {
                return AjaxResult.error("企业额度锁定失败,请稍后重试或选择其他支付方式");
            }
            chargingOrder.setRechargeAmount(chargeContext.getDispatchAmount());
            chargingOrder.setAccountMode(chargeContext.getAccountMode());
            chargingOrder.setLockedAmount(chargeContext.getDispatchAmount());
            chargingOrder.setMaxEndTime(chargeContext.getMaxEndTime());
            // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
            chargeAccountSupport.fillPreferentialSnapshot(chargingOrder, whitelist.getCompanyId());
        }
        //会员折扣支持直营网点和加盟商站点;余额支付(含组合支付)与企业账户流程不参与会员折扣
        if (!enterpriseCharging && !isBalancePayMode(payMethod) && Integer.valueOf(1).equals(site.getMemberDiscountFlag()) && null != appUser.getVipId()) {
            GetAppUserVipDetail getAppUserVipDetail = new GetAppUserVipDetail();
            getAppUserVipDetail.setAppUserId(appUser.getId());
            getAppUserVipDetail.setVipId(appUser.getVipId());
@@ -998,29 +1395,37 @@
        }
        this.save(chargingOrder);
        //添加订单的计费策略
        List<TAccountingStrategyDetail> strategyDetailList = accountingStrategyDetailClient.getListByAccountingStrategyId(accountingStrategyId).getData();
        AccountingStrategyOrder accountingStrategyOrder = new AccountingStrategyOrder();
        BeanUtils.copyProperties(accountingStrategy, accountingStrategyOrder);
        accountingStrategyOrder.setChargingOrderId(chargingOrder.getId());
        accountingStrategyOrderService.save(accountingStrategyOrder);
        List<AccountingStrategyDetailOrder> list1 = new ArrayList<>();
        for (TAccountingStrategyDetail tAccountingStrategyDetail : strategyDetailList) {
            AccountingStrategyDetailOrder accountingStrategyDetailOrder = new AccountingStrategyDetailOrder();
            BeanUtils.copyProperties(tAccountingStrategyDetail, accountingStrategyDetailOrder);
            accountingStrategyDetailOrder.setChargingOrderId(chargingOrder.getId());
            accountingStrategyDetailOrder.setCreateTime(LocalDateTime.now());
            accountingStrategyDetailOrder.setDelFlag(false);
            list1.add(accountingStrategyDetailOrder);
        }
        accountingStrategyDetailOrderService.saveBatch(list1);
        try {
            //添加订单的计费策略
            List<TAccountingStrategyDetail> strategyDetailList = accountingStrategyDetailClient.getListByAccountingStrategyId(accountingStrategyId).getData();
            AccountingStrategyOrder accountingStrategyOrder = new AccountingStrategyOrder();
            BeanUtils.copyProperties(accountingStrategy, accountingStrategyOrder);
            accountingStrategyOrder.setChargingOrderId(chargingOrder.getId());
            accountingStrategyOrderService.save(accountingStrategyOrder);
            List<AccountingStrategyDetailOrder> list1 = new ArrayList<>();
            for (TAccountingStrategyDetail tAccountingStrategyDetail : strategyDetailList) {
                AccountingStrategyDetailOrder accountingStrategyDetailOrder = new AccountingStrategyDetailOrder();
                BeanUtils.copyProperties(tAccountingStrategyDetail, accountingStrategyDetailOrder);
                accountingStrategyDetailOrder.setChargingOrderId(chargingOrder.getId());
                accountingStrategyDetailOrder.setCreateTime(LocalDateTime.now());
                accountingStrategyDetailOrder.setDelFlag(false);
                list1.add(accountingStrategyDetailOrder);
            }
            accountingStrategyDetailOrderService.saveBatch(list1);
        if (thirdPayAmount.compareTo(BigDecimal.ZERO) == 0 && balancePayAmount.compareTo(BigDecimal.ZERO) > 0) {
            changeUserBalance(userId, balancePayAmount.negate());
            recordBalanceConsume(chargingOrder, balancePayAmount);
        }
        if (thirdPayAmount.compareTo(BigDecimal.ZERO) == 0) {
            return completeChargingOrderPayment(chargingOrder, whitelistMatched ? "WHITELIST" : "BALANCE");
            if (thirdPayAmount.compareTo(BigDecimal.ZERO) == 0 && balancePayAmount.compareTo(BigDecimal.ZERO) > 0) {
                changeUserBalance(userId, balancePayAmount.negate());
                recordBalanceConsume(chargingOrder, balancePayAmount);
            }
            if (thirdPayAmount.compareTo(BigDecimal.ZERO) == 0) {
                return completeChargingOrderPayment(chargingOrder, enterpriseCharging ? "WHITELIST" : "BALANCE");
            }
        } catch (RuntimeException ex) {
            if (enterpriseCharging) {
                //企业账户流程:建单后续流程异常时释放已锁定的企业额度,避免企业额度被长期占用
                unlockWhitelistEnterpriseAmountQuietly(chargingOrder, "建单后续流程异常");
            }
            throw ex;
        }
        //会员优惠折扣将其计入增加充电时长(增加总充电金额)
        //如果充电总金额未使用完,则需要退回费用=(原金额/总金额)*(总金额-实际充电金额)
@@ -1553,8 +1958,8 @@
        BigDecimal s_degrees = tChargingGun.getRatedPower().divide(new BigDecimal(3600), 6, RoundingMode.HALF_EVEN);
        TAppUser appUser = appUserClient.getUserById(chargingOrder.getAppUserId()).getData();
        //判断会员是否还有充电优惠次数,计算会员优惠的折扣金额
        if (!isWhitelistOrder(chargingOrder) && null != appUser.getVipId() && isEffectiveVipDiscount(discount)) {
        //判断会员是否还有充电优惠次数,计算会员优惠的折扣金额;余额支付(含组合支付)不参与会员折扣
        if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != appUser.getVipId() && isEffectiveVipDiscount(discount)) {
            GetAppUserVipDetail getAppUserVipDetail = new GetAppUserVipDetail();
            getAppUserVipDetail.setAppUserId(chargingOrder.getAppUserId());
            getAppUserVipDetail.setVipId(appUser.getVipId());
@@ -2068,10 +2473,16 @@
                            changeUserBalance(chargingOrder.getAppUserId(), balanceRefundAmount);
                            recordBalanceRefund(chargingOrder, balanceRefundAmount, chargingOrderRefund.getRefundCode());
                        }
                        chargingOrderRefund.setRefundStatus(2);
                        chargingOrderRefund.setRefundSerialNumber(isWhitelistAllFreeOrder(chargingOrder) ? "WHITELIST" : "BALANCE");
                        chargingOrderRefund.setRefundTime(LocalDateTime.now());
                        chargingOrderRefundService.save(chargingOrderRefund);
                        //企业账户流程:启动失败全额释放已锁定的企业额度(订单未产生消费)
                        unlockWhitelistEnterpriseAmountQuietly(chargingOrder, "启动失败退款");
                        //企业账户订单资金全部来自企业账户额度,无用户侧渠道/余额退款,不落退款明细(保持 0 行),
                        //仅更新订单退款快照字段与状态,避免 0 元退款记录进入退款对账
                        if (!isWhitelistEnterpriseOrder(chargingOrder)) {
                            chargingOrderRefund.setRefundStatus(2);
                            chargingOrderRefund.setRefundSerialNumber(isWhitelistAllFreeOrder(chargingOrder) ? "WHITELIST" : "BALANCE");
                            chargingOrderRefund.setRefundTime(LocalDateTime.now());
                            chargingOrderRefundService.save(chargingOrderRefund);
                        }
                        this.updateById(chargingOrder);
                        return;
                    }
@@ -2360,7 +2771,19 @@
                .orderByDesc(TChargingOrder::getCreateTime)
                .last(" limit 0, 1"));
        if (null == one || !one.getAppUserId().equals(userId)) {
            return null;
            // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order,且后付模式无支付动作,
            // 不按 rechargePaymentStatus 过滤;TVinChargingOrder 继承 TChargingOrder,后续组装原样复用
            TVinChargingOrder vinOrder = vinChargingOrderMapper.selectOne(new LambdaQueryWrapper<TVinChargingOrder>()
                    .eq(TVinChargingOrder::getAppUserId, userId)
                    .eq(TVinChargingOrder::getChargingGunId, id)
                    .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4))
                    .eq(TVinChargingOrder::getDelFlag, 0)
                    .orderByDesc(TVinChargingOrder::getCreateTime)
                    .last(" limit 0, 1"));
            if (null == vinOrder) {
                return null;
            }
            one = vinOrder;
        }
        ChargingDetails chargingDetails = new ChargingDetails();
        chargingDetails.setId(one.getId().toString());
@@ -2693,15 +3116,32 @@
        String startTime2 = timeRange[1];
        String endTime1 = timeRange[2];
        String endTime2 = timeRange[3];
        PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(dto.getPageCurr(), dto.getPageSize());
        int pageCurr = dto.getPageCurr() == null ? 1 : dto.getPageCurr();
        int pageSize = dto.getPageSize() == null ? 50 : dto.getPageSize();
        PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        Long userId = tokenService.getLoginUser().getUserid();
        SysUser sysUser = sysUserClient.getSysUser(userId).getData();
        Integer roleType = sysUser.getRoleType();
        setChargingOrderAuthSiteIds(dto, sysUser, userId, roleType);
        List<ChargingOrderVO> list = this.baseMapper.chargingOrder(pageInfo, dto, startTime1, startTime2, endTime1, endTime2);
        if (StringUtils.hasLength(dto.getPhone()) && (dto.getUserIds() == null || dto.getUserIds().isEmpty())) {
            List<Long> data = appUserClient.getUserIdsByPhone(dto.getPhone()).getData();
            if (data == null || data.isEmpty()) {
                pageInfo.setTotal(0L);
                pageInfo.setRecords(Collections.emptyList());
                return pageInfo;
            }
            dto.setUserIds(data);
        }
        // 手动分页:不传 IPage(避免分页拦截器生成无法下推的全量 COUNT),COUNT 与 ORDER BY + LIMIT 均由 SQL 下推到分片
        long offset = (long) (pageCurr - 1) * pageSize;
        long total = this.baseMapper.chargingOrderCount(dto, startTime1, startTime2, endTime1, endTime2);
        pageInfo.setTotal(total);
        List<ChargingOrderVO> list = total == 0L ? Collections.emptyList()
                : this.baseMapper.chargingOrder(dto, startTime1, startTime2, endTime1, endTime2, offset, pageSize);
        fillChargingOrderBalanceIncome(list);
        fillChargingOrderShareIncome(list);
        fillChargingOrderPageExtra(list, dto.getSiteIds(), sysUser, roleType);
        fillChargingOrderCompanyFleetNames(list);
        pageInfo.setRecords(list);
        return pageInfo;
    }
@@ -2710,6 +3150,11 @@
    public TCharingOrderVO chargingOrderSummary(ChargingOrderQuery dto) {
        TCharingOrderVO tCharingOrderVO = new TCharingOrderVO();
        fillEmptyChargingOrderSummary(tCharingOrderVO);
        if (isChargingOrderFullSweep(dto)) {
            // 无任何筛选即全量扫描:汇总聚合跨分片 JOIN(accounting_strategy,分片键 charging_order_id 与 t_charging_order 不一致)
            // 且需把全量订单 id 反查 share/balance,MyCat 只能全量拉回代理层,必然超时。汇总没有 LIMIT 可下推,直接返回空汇总。
            return tCharingOrderVO;
        }
        String[] timeRange = parseChargingOrderTimeRange(dto);
        String startTime1 = timeRange[0];
        String startTime2 = timeRange[1];
@@ -2758,6 +3203,25 @@
        return new String[]{startTime1, startTime2, endTime1, endTime2};
    }
    /** 是否全量扫描:用户未提供任何可收窄的筛选条件(时间/订单号/手机号/站点/桩/枪/状态等),无界聚合无法下推。 */
    private boolean isChargingOrderFullSweep(ChargingOrderQuery dto) {
        return !StringUtils.hasLength(dto.getStartTime())
                && !StringUtils.hasLength(dto.getEndTime())
                && !StringUtils.hasLength(dto.getCode())
                && !StringUtils.hasLength(dto.getPhone())
                && dto.getSiteId() == null
                && dto.getPileId() == null
                && dto.getGunId() == null
                && dto.getOrderType() == null
                && dto.getStatus() == null
                && dto.getIsWhitelist() == null
                && dto.getPaymentType() == null
                && dto.getOrderSource() == null
                && dto.getCompanyId() == null
                && dto.getFleetId() == null
                && (dto.getUserIds() == null || dto.getUserIds().isEmpty());
    }
    private void fillChargingOrderPageExtra(List<ChargingOrderVO> list, List<Integer> siteIds, SysUser sysUser, Integer roleType) {
        if (list == null || list.isEmpty()) {
            return;
@@ -2777,6 +3241,25 @@
        Map<Integer, Site> siteMap = pageSites.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        // 列表 SQL 不再跨库 JOIN app_user/partner(避免分片表无筛选全量广播),手机号与合作商名在此回填
        Map<Long, String> phoneMap = new HashMap<>();
        List<Long> pageUserIds = list.stream().map(ChargingOrderVO::getAppUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (!pageUserIds.isEmpty()) {
            List<TAppUser> pageUsers = Optional.ofNullable(appUserClient.getUserByIds(pageUserIds).getData()).orElse(new ArrayList<>());
            for (TAppUser user : pageUsers) {
                if (user != null && user.getId() != null) {
                    phoneMap.put(user.getId(), user.getPhone());
                }
            }
        }
        Map<Integer, String> partnerNameMap = new HashMap<>();
        List<Integer> pagePartnerIds = siteMap.values().stream().map(Site::getPartnerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        for (Integer partnerId : pagePartnerIds) {
            Partner partner = Optional.ofNullable(partnerClient.getPartnerById(partnerId).getData()).orElse(null);
            if (partner != null && partner.getId() != null) {
                partnerNameMap.put(partner.getId(), partner.getName());
            }
        }
        Map<Long, List<TChargingOrderAccountingStrategy>> strategyMap = orderIds.isEmpty() ? new HashMap<>() :
                chargingOrderAccountingStrategyService.lambdaQuery()
                        .in(TChargingOrderAccountingStrategy::getChargingOrderId, orderIds)
@@ -2794,12 +3277,14 @@
            if (roleType == 2 && chargingOrderVO.getSiteId() != null) {
                chargingOrderVO.setAuthRecord(Boolean.TRUE.equals(authRecordMap.get(chargingOrderVO.getSiteId())));
            }
            chargingOrderVO.setPhone(phoneMap.get(chargingOrderVO.getAppUserId()));
            chargingOrderVO.setCommissionAmount(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")) : new BigDecimal("0"));
            chargingOrderVO.setPlatFormMoney(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")).setScale(2, BigDecimal.ROUND_DOWN) : new BigDecimal("0"));
            chargingOrderVO.setUid(chargingOrderVO.getId() + "");
            Site site = siteMap.get(chargingOrderVO.getSiteId());
            if (site != null) {
                chargingOrderVO.setSiteName(site.getName());
                chargingOrderVO.setPartnerName(partnerNameMap.get(site.getPartnerId()));
            }
            TChargingGun gun = gunMap.get(chargingOrderVO.getChargingGunId());
            TChargingPile pile = pileMap.get(chargingOrderVO.getChargingPileId());
@@ -2824,6 +3309,73 @@
            chargingOrderVO.setCount(strategies.size());
            chargingOrderVO.setLicensePlate(chargingOrderVO.getPlateNum());
        }
    }
    /**
     * 回填充电桩订单所属公司/车队名称:按订单快照 companyId/fleetId 批量远程取名(公司/车队改名后列表即时生效),
     * 远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillChargingOrderCompanyFleetNames(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> companyIds = list.stream().map(ChargingOrderVO::getCompanyId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (!companyIds.isEmpty()) {
            Map<Integer, String> companyNames = loadChargingOrderCompanyNames(companyIds);
            for (ChargingOrderVO item : list) {
                String name = item.getCompanyId() == null ? null : companyNames.get(item.getCompanyId());
                if (StringUtils.hasLength(name)) {
                    item.setCompanyName(name);
                }
            }
        }
        List<Integer> fleetIds = list.stream().map(ChargingOrderVO::getFleetId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
        }
        Map<Integer, String> fleetNames = loadChargingOrderFleetNames(fleetIds);
        for (ChargingOrderVO item : list) {
            String name = item.getFleetId() == null ? null : fleetNames.get(item.getFleetId());
            if (StringUtils.hasLength(name)) {
                item.setFleetName(name);
            }
        }
    }
    /** 批量查询公司名称,失败返回已取到的数据 */
    private Map<Integer, String> loadChargingOrderCompanyNames(List<Integer> companyIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TGroupCompany> companies = Optional.ofNullable(groupCompanyClient.listCompaniesByIds(companyIds).getData())
                    .orElse(Collections.emptyList());
            for (TGroupCompany company : companies) {
                if (company != null && company.getId() != null && StringUtils.hasLength(company.getName())) {
                    result.put(company.getId(), company.getName());
                }
            }
        } catch (Exception e) {
            log.warn("[充电桩订单回填] 批量查询公司名称失败 ids={}", companyIds, e);
        }
        return result;
    }
    /** 批量查询车队名称,失败返回已取到的数据 */
    private Map<Integer, String> loadChargingOrderFleetNames(List<Integer> fleetIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TFleet> fleets = Optional.ofNullable(groupCompanyClient.listFleetsByIds(fleetIds).getData())
                    .orElse(Collections.emptyList());
            for (TFleet fleet : fleets) {
                if (fleet != null && fleet.getId() != null && StringUtils.hasLength(fleet.getFleetName())) {
                    result.put(fleet.getId(), fleet.getFleetName());
                }
            }
        } catch (Exception e) {
            log.warn("[充电桩订单回填] 批量查询车队名称失败 ids={}", fleetIds, e);
        }
        return result;
    }
    /**
@@ -2890,6 +3442,10 @@
     */
    @Override
    public List<ChargingOrderBalanceIncomeDetailVO> balanceIncomeDetail(ChargingOrderQuery dto) {
        if (isChargingOrderFullSweep(dto)) {
            // 与汇总同根因:全量扫描会把全量订单 id 反查 exchange_code_use_record,MyCat 超时,直接返回空明细
            return new ArrayList<>();
        }
        String startTime1 = null;
        String startTime2 = null;
        String endTime1 = null;
@@ -3014,6 +3570,7 @@
                platformIncomeTotal = platformIncomeTotal.add(getSummaryDecimal(batchMap, "platformIncomeTotal"));
                partnerIncomeTotal = partnerIncomeTotal.add(getSummaryDecimal(batchMap, "partnerIncomeTotal"));
            }
            platformIncomeTotal = platformIncomeTotal.add(nullToZero(this.baseMapper.sumWhitelistRefundByOrderIds(batchIds)));
        }
        totalMap.put("platformIncomeTotal", platformIncomeTotal);
        totalMap.put("partnerIncomeTotal", partnerIncomeTotal);
@@ -3087,66 +3644,217 @@
    @Override
    public R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(EnterpriseChargingAnalysisQuery query) {
        PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<EnterpriseChargingAnalysisVO> list = this.baseMapper.enterpriseChargingAnalysis(pageInfo, query);
        if (list == null || list.isEmpty()) {
        int pageCurr = query.getPageCurr() == null ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null ? 50 : query.getPageSize();
        PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        // 逆反查聚合(不再跨分片 JOIN,构建块见 TChargingOrderMapper.xml enterpriseInvoiceRows 等):
        //   1) t_order_invoice 单表查企业发票(10 片按 app_user_id 分片,抬头级条件可下推)
        //   2) t_order_invoice_detail 按 order_invoice_id IN 路由查充电明细
        //   3) 命中订单级条件(站点/时间等)→ 命中订单 id
        //   4) t_charging_order 按 id IN 走主键取 start_time/site_id
        //   5) t_site 单表取站点名
        //   6) 按公司聚合 + 手工分页(不再传 IPage,避免分页拦截器生成无法下推的全量 COUNT)
        List<EnterpriseChargingAggRow.Invoice> invoices = this.baseMapper.enterpriseInvoiceRows(query);
        if (invoices == null || invoices.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<String> companyNames = list.stream()
                .map(EnterpriseChargingAnalysisVO::getCompanyName)
        Map<Long, List<EnterpriseChargingAggRow.Detail>> detailsByInvoice = new HashMap<>();
        Map<Long, EnterpriseChargingAggRow.Detail> detailByOrder = new LinkedHashMap<>();
        List<Long> invoiceIds = invoices.stream()
                .map(EnterpriseChargingAggRow.Invoice::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<String, List<Map<String, Object>>> topSitesByCompany = groupByCompany(
                this.baseMapper.enterpriseTopSitesByCompanies(query, companyNames));
        Map<String, List<Map<String, Object>>> chargingTimesByCompany = groupByCompany(
                this.baseMapper.enterpriseChargingTimesByCompanies(query, companyNames));
        for (EnterpriseChargingAnalysisVO row : list) {
            String companyKey = companyKey(row.getCompanyName(), row.getSocialCreditCode());
            List<Map<String, Object>> topSites = topSitesByCompany.getOrDefault(companyKey, Collections.emptyList());
            String topSiteNames = topSites.stream()
                    .sorted(Comparator.comparing((Map<String, Object> item) -> ((Number) item.getOrDefault("counts", 0)).longValue()).reversed())
                    .limit(3)
                    .map(item -> Objects.toString(item.get("siteName"), ""))
                    .filter(org.springframework.util.StringUtils::hasText)
                    .collect(Collectors.joining("、"));
            row.setTopSiteNames(topSiteNames);
            List<LocalDateTime> chargingTimes = chargingTimesByCompany.getOrDefault(companyKey, Collections.emptyList()).stream()
                    .map(item -> toLocalDateTime(item.get("startTime")))
                    .filter(Objects::nonNull)
                    .sorted()
                    .collect(Collectors.toList());
            row.setAverageChargingInterval(calculateAverageChargingInterval(chargingTimes));
        for (int start = 0; start < invoiceIds.size(); start += ENTERPRISE_INVOICE_BATCH_SIZE) {
            int end = Math.min(start + ENTERPRISE_INVOICE_BATCH_SIZE, invoiceIds.size());
            List<EnterpriseChargingAggRow.Detail> details =
                    this.baseMapper.enterpriseInvoiceDetailRows(invoiceIds.subList(start, end));
            if (details == null) {
                continue;
            }
            for (EnterpriseChargingAggRow.Detail detail : details) {
                if (detail.getOrderId() == null) {
                    continue;
                }
                detailsByInvoice.computeIfAbsent(detail.getInvoiceId(), k -> new ArrayList<>()).add(detail);
                detailByOrder.putIfAbsent(detail.getOrderId(), detail);
            }
        }
        pageInfo.setRecords(list);
        if (detailByOrder.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<Long> candidateOrderIds = new ArrayList<>(detailByOrder.keySet());
        boolean orderFilter = hasEnterpriseOrderFilter(query);
        Set<Long> matchedOrderIds = null;
        if (orderFilter) {
            matchedOrderIds = new HashSet<>();
            for (int start = 0; start < candidateOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) {
                int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, candidateOrderIds.size());
                List<Long> matched = this.baseMapper.enterpriseAnalysisMatchingOrderIds(
                        candidateOrderIds.subList(start, end), query);
                if (matched != null) {
                    matchedOrderIds.addAll(matched);
                }
            }
            if (matchedOrderIds.isEmpty()) {
                pageInfo.setRecords(Collections.emptyList());
                return R.ok(pageInfo);
            }
        }
        List<Long> effectiveOrderIds = orderFilter ? new ArrayList<>(matchedOrderIds) : candidateOrderIds;
        Map<Long, EnterpriseChargingAggRow.Order> orderMap = new HashMap<>();
        for (int start = 0; start < effectiveOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) {
            int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, effectiveOrderIds.size());
            List<EnterpriseChargingAggRow.Order> orders =
                    this.baseMapper.enterpriseAnalysisChargingOrders(effectiveOrderIds.subList(start, end));
            if (orders == null) {
                continue;
            }
            for (EnterpriseChargingAggRow.Order order : orders) {
                orderMap.put(order.getOrderId(), order);
            }
        }
        Set<Long> siteIds = orderMap.values().stream()
                .map(EnterpriseChargingAggRow.Order::getSiteId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> siteNameMap = new HashMap<>();
        if (!siteIds.isEmpty()) {
            List<EnterpriseChargingAggRow.Site> sites = this.baseMapper.enterpriseAnalysisSites(new ArrayList<>(siteIds));
            if (sites != null) {
                for (EnterpriseChargingAggRow.Site site : sites) {
                    siteNameMap.put(site.getSiteId(), site.getSiteName());
                }
            }
        }
        Map<String, EnterpriseCompanyAgg> companyAggs = new LinkedHashMap<>();
        for (EnterpriseChargingAggRow.Invoice invoice : invoices) {
            List<EnterpriseChargingAggRow.Detail> details =
                    detailsByInvoice.getOrDefault(invoice.getId(), Collections.emptyList());
            if (details.isEmpty()) {
                // 无充电明细(order_type=1)的发票不计,等同原 INNER JOIN tod.order_type=1 语义
                continue;
            }
            List<EnterpriseChargingAggRow.Order> invoiceOrders = new ArrayList<>();
            BigDecimal detailAmount = BigDecimal.ZERO;
            for (EnterpriseChargingAggRow.Detail detail : details) {
                if (orderFilter && !matchedOrderIds.contains(detail.getOrderId())) {
                    continue;
                }
                detailAmount = detailAmount.add(nullToZero(detail.getInvoiceAmount()));
                EnterpriseChargingAggRow.Order order = orderMap.get(detail.getOrderId());
                if (order != null) {
                    invoiceOrders.add(order);
                }
            }
            if (orderFilter && invoiceOrders.isEmpty()) {
                // 订单级筛选后该发票无命中订单 → 剔除,等同原 LEFT JOIN + 订单级 WHERE 语义
                continue;
            }
            BigDecimal invoiceAmount = detailAmount.signum() > 0 ? detailAmount : nullToZero(invoice.getTotalAmount());
            String key = companyKey(invoice.getCompanyName(), invoice.getSocialCreditCode());
            EnterpriseCompanyAgg agg = companyAggs.get(key);
            if (agg == null) {
                agg = new EnterpriseCompanyAgg(invoice.getCompanyName(), invoice.getSocialCreditCode(),
                        invoice.getCompanyAddress(), invoice.getCompanyPhone());
                companyAggs.put(key, agg);
            }
            agg.invoiceCount++;
            agg.invoiceAmount = agg.invoiceAmount.add(invoiceAmount);
            for (EnterpriseChargingAggRow.Order order : invoiceOrders) {
                if (order.getStartTime() != null) {
                    agg.startTimes.add(order.getStartTime());
                    if (agg.latestChargingTime == null || order.getStartTime().isAfter(agg.latestChargingTime)) {
                        agg.latestChargingTime = order.getStartTime();
                    }
                }
                if (order.getSiteId() != null) {
                    agg.siteOrderIds.computeIfAbsent(order.getSiteId(), k -> new HashSet<>()).add(order.getOrderId());
                }
            }
        }
        if (companyAggs.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<EnterpriseChargingAnalysisVO> all = companyAggs.values().stream()
                .sorted(Comparator.comparingLong((EnterpriseCompanyAgg agg) -> agg.invoiceCount).reversed())
                .map(agg -> buildEnterpriseAnalysisVO(agg, siteNameMap))
                .collect(Collectors.toList());
        pageInfo.setTotal(all.size());
        long offset = (long) (pageCurr - 1L) * pageSize;
        if (offset >= all.size()) {
            pageInfo.setRecords(Collections.emptyList());
        } else {
            int toIndex = (int) Math.min(offset + pageSize, all.size());
            pageInfo.setRecords(all.subList((int) offset, toIndex));
        }
        return R.ok(pageInfo);
    }
    private Map<String, List<Map<String, Object>>> groupByCompany(List<Map<String, Object>> rows) {
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
    private boolean hasEnterpriseOrderFilter(EnterpriseChargingAnalysisQuery query) {
        return query.getSiteType() != null
                || query.getPartnerId() != null
                || query.getSiteId() != null
                || query.getCreateTime1() != null
                || query.getCreateTime2() != null
                || (query.getCode() != null && !query.getCode().isEmpty())
                || query.getOrderType() != null
                || (query.getPhone() != null && !query.getPhone().isEmpty())
                || query.getStatus() != null
                || (query.getPartnerName() != null && !query.getPartnerName().isEmpty())
                || query.getOrderSource() != null
                || query.getStartTime1() != null
                || query.getStartTime2() != null
                || query.getEndTime1() != null
                || query.getEndTime2() != null;
    }
    private EnterpriseChargingAnalysisVO buildEnterpriseAnalysisVO(EnterpriseCompanyAgg agg,
                                                                  Map<Long, String> siteNameMap) {
        EnterpriseChargingAnalysisVO vo = new EnterpriseChargingAnalysisVO();
        vo.setCompanyName(agg.companyName);
        vo.setSocialCreditCode(agg.socialCreditCode);
        vo.setCompanyAddress(agg.companyAddress);
        vo.setCompanyPhone(agg.companyPhone);
        vo.setInvoiceCount(agg.invoiceCount);
        vo.setInvoiceAmount(agg.invoiceAmount);
        vo.setLatestChargingTime(agg.latestChargingTime);
        String topSiteNames = agg.siteOrderIds.entrySet().stream()
                .sorted(Comparator.comparingInt((Map.Entry<Long, Set<Long>> e) -> e.getValue().size()).reversed())
                .limit(3)
                .map(e -> siteNameMap.getOrDefault(e.getKey(), ""))
                .filter(org.springframework.util.StringUtils::hasText)
                .collect(Collectors.joining("、"));
        vo.setTopSiteNames(topSiteNames);
        vo.setAverageChargingInterval(calculateAverageChargingInterval(new ArrayList<>(agg.startTimes)));
        return vo;
    }
    private static class EnterpriseCompanyAgg {
        private final String companyName;
        private final String socialCreditCode;
        private final String companyAddress;
        private final String companyPhone;
        private long invoiceCount;
        private BigDecimal invoiceAmount = BigDecimal.ZERO;
        private LocalDateTime latestChargingTime;
        private final Map<Long, Set<Long>> siteOrderIds = new HashMap<>();
        private final TreeSet<LocalDateTime> startTimes = new TreeSet<>();
        private EnterpriseCompanyAgg(String companyName, String socialCreditCode,
                                     String companyAddress, String companyPhone) {
            this.companyName = companyName;
            this.socialCreditCode = socialCreditCode;
            this.companyAddress = companyAddress;
            this.companyPhone = companyPhone;
        }
        return rows.stream().collect(Collectors.groupingBy(row -> companyKey(
                Objects.toString(row.get("companyName"), null),
                Objects.toString(row.get("socialCreditCode"), ""))));
    }
    private String companyKey(String companyName, String socialCreditCode) {
        return Objects.toString(companyName, "") + "\u0000" + Objects.toString(socialCreditCode, "");
    }
    private LocalDateTime toLocalDateTime(Object value) {
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof Date) {
            return LocalDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault());
        }
        return null;
    }
    @Override
@@ -3347,23 +4055,115 @@
                .divide(BigDecimal.valueOf(3600), 2, RoundingMode.HALF_UP);
    }
    private static final int SERIAL_ORDER_ID_SEARCH_LIMIT = 5000;
    private static final int SITE_ORDER_ID_SEARCH_LIMIT = 50000;
    @Override
    public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) {
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize());
        boolean withoutOrderJoin = !StringUtils.hasLength(chargingRefundDto.getSerialNumber())
                && (chargingRefundDto.getSiteIds() == null || chargingRefundDto.getSiteIds().isEmpty());
        List<TChargingOrderRefund> list = withoutOrderJoin
                ? this.baseMapper.getRefundListWithoutOrderJoin(pageInfo, chargingRefundDto)
                : this.baseMapper.getRefundList(pageInfo, chargingRefundDto);
        if (withoutOrderJoin) {
            enrichRefundOrderInfo(list);
        int pageCurr = chargingRefundDto.getPageCurr() == null ? 1 : chargingRefundDto.getPageCurr();
        int pageSize = chargingRefundDto.getPageSize() == null ? 50 : chargingRefundDto.getPageSize();
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(pageCurr, pageSize);
        long offset = (long) (pageCurr - 1) * pageSize;
        // 每个 UNION 分支须取 offset+pageSize 行,外层再按 offset,pageSize 切页,否则翻页时池子不足导致丢数据
        int branchLimit = (int) Math.min(offset + pageSize, Integer.MAX_VALUE);
        boolean filterBySerial = StringUtils.hasLength(chargingRefundDto.getSerialNumber());
        boolean filterBySite = chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty();
        List<TChargingOrderRefund> list;
        if (!filterBySerial && !filterBySite) {
            // 无筛选:手工分页。分页拦截器会对 UNION 查询生成全量 COUNT 且无法把 ORDER BY/LIMIT 下推到分片,
            // 这里改为分支内下推(getRefundListPage)加分支 COUNT 求和(countRefundList)
            pageInfo.setTotal(this.baseMapper.countRefundList(chargingRefundDto));
            list = this.baseMapper.getRefundListPage(chargingRefundDto, offset, pageSize, branchLimit);
        } else {
            // 筛选:逆反查。先按条件查订单 id(t_charging_order 等可下推/走索引),
            // 再按退款表分片键 IN 过滤(可路由),替代跨分片 LEFT JOIN
            RefundOrderIds ids = resolveRefundOrderIds(chargingRefundDto);
            if (ids.isEmpty()) {
                pageInfo.setRecords(Collections.emptyList());
                return R.ok(pageInfo);
            }
            pageInfo.setTotal(this.baseMapper.countRefundListByOrderIds(chargingRefundDto,
                    ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds));
            list = this.baseMapper.getRefundListByOrderIds(chargingRefundDto,
                    ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds, offset, pageSize, branchLimit);
        }
        enrichRefundOrderInfo(list);
        for (TChargingOrderRefund refund : list) {
            refund.setUid(refund.getType() + "-" + refund.getId());
        }
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    /**
     * 逆反查订单 id:serialNumber 匹配充电/商城/会员订单 id(各自 LIMIT 防爆),
     * siteIds 匹配充电订单 id;两者同时存在时取交集。
     * 返回约定:null = 该类型不过滤;空列表 = 该类型无匹配(SQL 侧 AND 1=0)。
     */
    private RefundOrderIds resolveRefundOrderIds(ChargingRefundDto chargingRefundDto) {
        List<Long> type1OrderIds = null;
        List<Long> type2OrderIds = null;
        List<Long> type3OrderIds = null;
        if (StringUtils.hasLength(chargingRefundDto.getSerialNumber())) {
            // 模糊匹配结果可能很大(短流水号),截断上限防 IN 列表爆炸;匹配不到则列表为空
            type1OrderIds = safeIds(this.baseMapper.listChargingOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            type2OrderIds = safeIds(this.baseMapper.listShoppingOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            type3OrderIds = safeIds(this.baseMapper.listVipOrderIdsBySerialNumber(
                    chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT));
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type1OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "充电订单");
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type2OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "商城订单");
            warnIfTruncated(chargingRefundDto.getSerialNumber(), type3OrderIds,
                    SERIAL_ORDER_ID_SEARCH_LIMIT, "会员订单");
        }
        if (chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty()) {
            List<Long> siteMatched = safeIds(this.baseMapper.listChargingOrderIdsBySiteIds(
                    chargingRefundDto.getSiteIds(), SITE_ORDER_ID_SEARCH_LIMIT));
            warnIfTruncated(String.valueOf(chargingRefundDto.getSiteIds()), siteMatched,
                    SITE_ORDER_ID_SEARCH_LIMIT, "站点订单");
            if (type1OrderIds == null) {
                type1OrderIds = siteMatched;
            } else {
                type1OrderIds.retainAll(new HashSet<>(siteMatched));
            }
        }
        return new RefundOrderIds(type1OrderIds, type2OrderIds, type3OrderIds);
    }
    /** 反查结果达到上限说明可能被截断,告警便于排查(截断会导致部分退款不在列表内) */
    private void warnIfTruncated(String condition, List<Long> ids, int limit, String orderType) {
        if (ids.size() >= limit) {
            log.warn("退款列表逆反查:条件[{}]匹配{}数达到上限{},结果可能被截断", condition, orderType, limit);
        }
    }
    private static List<Long> safeIds(List<Long> ids) {
        return ids == null ? new ArrayList<>() : new ArrayList<>(ids);
    }
    /** 逆反查结果:null = 不过滤;空列表 = 无匹配 */
    private static class RefundOrderIds {
        private final List<Long> type1OrderIds;
        private final List<Long> type2OrderIds;
        private final List<Long> type3OrderIds;
        private RefundOrderIds(List<Long> type1OrderIds, List<Long> type2OrderIds, List<Long> type3OrderIds) {
            this.type1OrderIds = type1OrderIds;
            this.type2OrderIds = type2OrderIds;
            this.type3OrderIds = type3OrderIds;
        }
        /** 三个类型全部明确无匹配(非 null 且为空)时,整页为空,可短路 */
        private boolean isEmpty() {
            return type1OrderIds != null && type1OrderIds.isEmpty()
                    && type2OrderIds != null && type2OrderIds.isEmpty()
                    && type3OrderIds != null && type3OrderIds.isEmpty();
        }
    }
    private void enrichRefundOrderInfo(List<TChargingOrderRefund> refunds) {
@@ -3440,9 +4240,8 @@
            }
        } else {
            List<SixChargingDto> settlementIncome = this.baseMapper.chargeSettlement(sixBeforeValue, siteIds);
            if (settlementIncome != null) {
                result.addAll(settlementIncome);
            }
            List<SixChargingDto> orderHistoryIncome = this.baseMapper.chargeOrderHistory(sixBeforeValue, siteIds);
            result.addAll(mergeMissingMonthlyIncome(settlementIncome, orderHistoryIncome));
            String siteIdsCsv = siteIds == null ? null : siteIds.stream()
                    .filter(Objects::nonNull)
                    .map(String::valueOf)
@@ -3477,6 +4276,45 @@
        return result;
    }
    static List<SixChargingDto> mergeMissingMonthlyIncome(List<SixChargingDto> settlementIncome,
                                                            List<SixChargingDto> orderHistoryIncome) {
        List<SixChargingDto> merged = new ArrayList<>();
        Set<Integer> settledMonths = new HashSet<>();
        if (settlementIncome != null) {
            for (SixChargingDto item : settlementIncome) {
                if (item == null) {
                    continue;
                }
                merged.add(item);
                Integer month = incomeMonth(item);
                if (month != null) {
                    settledMonths.add(month);
                }
            }
        }
        if (orderHistoryIncome != null) {
            for (SixChargingDto item : orderHistoryIncome) {
                Integer month = incomeMonth(item);
                if (item != null && month != null && settledMonths.add(month)) {
                    merged.add(item);
                }
            }
        }
        return merged;
    }
    private static Integer incomeMonth(SixChargingDto item) {
        if (item == null || item.getMonth() == null) {
            return null;
        }
        try {
            int month = Integer.parseInt(item.getMonth());
            return month >= 1 && month <= 12 ? month : null;
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    @Override
    public List<SixCircleDto> circle(List<Integer> siteIds, LocalDate sixBefore, Boolean partnerView) {
        List<SixCircleDto> result = this.baseMapper.circle(siteIds, sixBefore, partnerView);
@@ -3491,21 +4329,21 @@
    }
    @Override
    public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView) {
        Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView);
    public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView, LocalDate sixBefore) {
        Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView, sixBefore);
        if (!Boolean.TRUE.equals(partnerView) && result != null) {
            result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds));
            result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds, sixBefore));
        }
        return result;
    }
    /**
     * 统计近六月订单收入支付方式占比;服务层聚合避免复杂CASE SQL在当前数据库驱动中转换异常。
     * 统计近六月订单收入支付方式占比;历史订单缺少支付渠道时归入其他支付,避免漏算。
     */
    @Override
    public List<SixIncomeCircleDto> payTypeCircle(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView) {
        if (siteIds == null || siteIds.isEmpty()) {
            return buildPayTypeCircleData(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
            return buildPayTypeCircleData(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
        }
        if (Boolean.TRUE.equals(partnerView)) {
            return buildPartnerShareCircleData(this.baseMapper.chargingOrderShareIncomeByPayChannel(sixBefore, siteIds));
@@ -3525,28 +4363,31 @@
        BigDecimal wechatAmount = BigDecimal.ZERO;
        BigDecimal alipayAmount = BigDecimal.ZERO;
        BigDecimal balanceAmount = BigDecimal.ZERO;
        BigDecimal otherAmount = BigDecimal.ZERO;
        for (TChargingOrder order : orders) {
            BigDecimal thirdPayAmount = getThirdPayNetAmount(order);
            BigDecimal balancePayAmount = getBalancePayNetAmount(order);
            Integer rechargePaymentType = order.getRechargePaymentType();
            if (Integer.valueOf(1).equals(rechargePaymentType)) {
                wechatAmount = wechatAmount.add(thirdPayAmount);
            }
            if (Integer.valueOf(2).equals(rechargePaymentType) || Integer.valueOf(3).equals(rechargePaymentType)) {
            } else if (Integer.valueOf(2).equals(rechargePaymentType) || Integer.valueOf(3).equals(rechargePaymentType)) {
                alipayAmount = alipayAmount.add(thirdPayAmount);
            } else {
                otherAmount = otherAmount.add(thirdPayAmount);
            }
            Integer payMethod = order.getPayMethod();
            if (Integer.valueOf(1).equals(payMethod) || Integer.valueOf(2).equals(payMethod)) {
                balanceAmount = balanceAmount.add(balancePayAmount);
            }
        }
        return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount);
        return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount, otherAmount);
    }
    private List<SixIncomeCircleDto> buildPartnerShareCircleData(List<Map<String, Object>> rows) {
        BigDecimal wechatAmount = BigDecimal.ZERO;
        BigDecimal alipayAmount = BigDecimal.ZERO;
        BigDecimal balanceAmount = BigDecimal.ZERO;
        BigDecimal otherAmount = BigDecimal.ZERO;
        if (rows != null) {
            for (Map<String, Object> row : rows) {
                String channel = row.get("payChannel") == null ? "" : row.get("payChannel").toString();
@@ -3558,21 +4399,23 @@
                } else if ("BALANCE".equalsIgnoreCase(channel) || "WHITELIST".equalsIgnoreCase(channel)) {
                    balanceAmount = balanceAmount.add(shareAmount);
                } else {
                    wechatAmount = wechatAmount.add(shareAmount);
                    otherAmount = otherAmount.add(shareAmount);
                }
            }
        }
        return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount);
        return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount, otherAmount);
    }
    /**
     * 构造固定三类支付方式数据,保证前端饼图在无订单时也有稳定结构。
     */
    private List<SixIncomeCircleDto> buildPayTypeCircleData(BigDecimal wechatAmount, BigDecimal alipayAmount, BigDecimal balanceAmount) {
    private List<SixIncomeCircleDto> buildPayTypeCircleData(BigDecimal wechatAmount, BigDecimal alipayAmount,
                                                            BigDecimal balanceAmount, BigDecimal otherAmount) {
        List<SixIncomeCircleDto> data = new ArrayList<>();
        data.add(buildIncomeCircleDto("微信支付", wechatAmount));
        data.add(buildIncomeCircleDto("支付宝支付", alipayAmount));
        data.add(buildIncomeCircleDto("余额支付", balanceAmount));
        data.add(buildIncomeCircleDto("其他支付", otherAmount));
        return data;
    }
@@ -4214,9 +5057,52 @@
        }
        dto.setSiteIds(siteIds);
        List<ChargingOrderListVO> list1 = this.baseMapper.chargingList(dto, startTime1, startTime2, endTime1, endTime2);
        fillChargingPeriodCouponSource(list1);
        fillChargingPeriodExportBalanceIncome(list1);
        fillChargingPeriodShareIncome(list1);
        return list1;
    }
    private void fillChargingPeriodCouponSource(List<ChargingOrderListVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> couponIds = list.stream()
                .map(ChargingOrderListVO::getAppCouponId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (couponIds.isEmpty()) {
            return;
        }
        Map<Long, TAppCoupon> couponMap = new HashMap<>();
        int batchSize = 500;
        for (int i = 0; i < couponIds.size(); i += batchSize) {
            List<Long> batchIds = couponIds.subList(i, Math.min(i + batchSize, couponIds.size()));
            R<List<TAppCoupon>> response = appCouponClient.getAppCouponByIds(batchIds);
            List<TAppCoupon> coupons = response == null ? null : response.getData();
            couponMap.putAll(indexByLongId(coupons, TAppCoupon::getId));
        }
        for (ChargingOrderListVO order : list) {
            TAppCoupon coupon = couponMap.get(order.getAppCouponId());
            if (coupon == null || coupon.getWaysToObtain() == null) {
                continue;
            }
            switch (coupon.getWaysToObtain()) {
                case 1:
                    order.setCouponSource("积分兑换");
                    break;
                case 2:
                    order.setCouponSource("会员购买");
                    break;
                case 3:
                case 4:
                    order.setCouponSource("后台赠送");
                    break;
                default:
                    break;
            }
        }
    }
    private void fillChargingPeriodShareIncome(List<ChargingOrderListVO> list) {
@@ -4252,7 +5138,8 @@
        if (item == null) {
            return;
        }
        item.setPlatformIncome(getSummaryDecimal(row, "platformIncome"));
        item.setPlatformIncome(getSummaryDecimal(row, "platformIncome")
                .add(getSummaryDecimal(row, "whitelistRefund")));
        item.setPartnerIncome(getSummaryDecimal(row, "partnerIncome"));
    }
@@ -4304,7 +5191,8 @@
        }
        for (ChargingOrderVO item : list) {
            Map<String, Object> row = incomeMap.get(item.getId());
            item.setPlatformIncome(getSummaryDecimal(row, "platformIncome"));
            item.setPlatformIncome(getSummaryDecimal(row, "platformIncome")
                    .add(getSummaryDecimal(row, "whitelistRefund")));
            item.setPartnerIncome(getSummaryDecimal(row, "partnerIncome"));
        }
    }
@@ -4439,6 +5327,14 @@
        if (com.ruoyi.common.core.utils.StringUtils.isNotEmpty(query.getTransaction_serial_number())) {
            //获取当前的计费策略
            TChargingOrder chargingOrder1 = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getCode, query.getTransaction_serial_number()));
            if (null == chargingOrder1) {
                return;
            }
            //企业账户流程:充电中授权时间到期主动停充(按授权截止时间提前下发停充指令)
            if (Integer.valueOf(3).equals(chargingOrder1.getStatus()) && needAuthorizeStop(chargingOrder1)) {
                stopByAuthorizeExpired(chargingOrder1);
                return;
            }
            BigDecimal residualAmount = chargingOrder1.getRechargeAmount().add(chargingOrder1.getVipDiscountAmount()).subtract(query.getPaid_amount());
            TChargingOrder chargingOrder = new TChargingOrder();
            chargingOrder.setId(chargingOrder1.getId());
@@ -4669,6 +5565,15 @@
                    }
                }
                //企业账户流程:白名单企业账户订单按企业优惠结算(方式2=服务费比例优惠,方式3=按计费模板同阶段单价)
                //优惠口径优先取订单快照(建单时固化,策略后续变更不影响本单结算),快照为空的历史订单实时查询并回填
                boolean enterpriseSettlement = isWhitelistEnterpriseOrder(chargingOrder);
                ChargeAccountSupport.PreferentialSnapshot enterprisePreferential = enterpriseSettlement
                        ? chargeAccountSupport.resolvePreferential(chargingOrder) : null;
                Integer enterprisePreferentialType = null == enterprisePreferential ? null : enterprisePreferential.getPreferentialType();
                TBillingTemplateDTO enterpriseTemplate = null == enterprisePreferential ? null : enterprisePreferential.getTemplate();
                BigDecimal enterpriseElectricDiscount = BigDecimal.ZERO;
                BigDecimal enterpriseServiceDiscount = BigDecimal.ZERO;
                //开始处理明细
                for (int i = 0; i < lists.size(); i++) {
                    AccountingStrategyDetailOrderVo strategyDetail = lists.get(i);
@@ -4694,15 +5599,46 @@
                    }
                    //已充电总度数
                    BigDecimal electrovalenc = strategyDetail.getElectrovalence().multiply(sharp_peak_charge);
                    BigDecimal finalElectricPrice = strategyDetail.getElectrovalence();
                    BigDecimal finalServicePrice = strategyDetail.getServiceCharge();
                    //企业账户流程:按企业优惠计算最终用电单价
                    if (enterpriseSettlement) {
                        if (Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE).equals(enterprisePreferentialType)) {
                            //方式2:服务费按比例优惠
                            finalServicePrice = ChargeAccountSupport.applyServiceDiscountRate(finalServicePrice, enterprisePreferential.getServiceDiscountRate());
                        } else if (Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE).equals(enterprisePreferentialType)) {
                            //方式3:按计费模板同电价阶段单价
                            TBillingTemplateDetail templateDetail = ChargeAccountSupport.findTemplateDetail(enterpriseTemplate, strategyDetail.getType());
                            if (null == templateDetail) {
                                log.error("[CD结算] 计费模板缺少对应电价阶段,该阶段按原价结算 code={} type={}", chargingOrder.getCode(), strategyDetail.getType());
                            } else {
                                if (null != templateDetail.getElectrovalence()) {
                                    finalElectricPrice = templateDetail.getElectrovalence();
                                }
                                if (null != templateDetail.getServiceCharge()) {
                                    finalServicePrice = templateDetail.getServiceCharge();
                                }
                            }
                        }
                    }
                    BigDecimal electrovalenc = finalElectricPrice.multiply(sharp_peak_charge);
                    BigDecimal originalServicePrice = strategyDetail.getServiceCharge().multiply(sharp_peak_charge);
                    BigDecimal serviceCharge = originalServicePrice;
                    BigDecimal serviceCharge = finalServicePrice.multiply(sharp_peak_charge);
                    BigDecimal vipDiscountAmount = BigDecimal.ZERO;
                    //计算优惠金额
                    if (null != chargingOrder.getVipDiscount()) {
                    //计算优惠金额;企业账户流程按企业优惠结算,其余订单中余额支付(含组合支付)不参与会员折扣
                    if (enterpriseSettlement) {
                        //企业优惠金额(原价-优惠价),用于订单优惠统计与 whitelist_* 快照
                        enterpriseElectricDiscount = enterpriseElectricDiscount.add(strategyDetail.getElectrovalence().multiply(sharp_peak_charge).subtract(electrovalenc));
                        enterpriseServiceDiscount = enterpriseServiceDiscount.add(originalServicePrice.subtract(serviceCharge));
                    } else if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != chargingOrder.getVipDiscount()) {
                        //0.58折
                        vipDiscountAmount = serviceCharge.multiply(new BigDecimal(1).subtract(chargingOrder.getVipDiscount()));
                        serviceCharge = serviceCharge.multiply(chargingOrder.getVipDiscount());
                    }
                    //企业账户流程:明细单价记录优惠后单价,金额与单价口径一致
                    if (enterpriseSettlement) {
                        chargingOrderAccountingStrategy.setElectrovalence(finalElectricPrice);
                        chargingOrderAccountingStrategy.setServiceCharge(finalServicePrice);
                    }
                    chargingOrderAccountingStrategy.setChargingCapacity(sharp_peak_charge);
                    chargingOrderAccountingStrategy.setPeriodElectricPrice(electrovalenc.setScale(4, RoundingMode.HALF_EVEN));
@@ -4795,9 +5731,20 @@
                payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO);
                serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO);
                chargingOrder.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setWhitelistServiceRefundAmount(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN));
                //企业账户流程:企业优惠金额与 whitelist_* 三列快照双写(订单优惠统计沿用 whitelist_* 口径)
                BigDecimal enterpriseElectricDiscountAmount = enterpriseElectricDiscount.max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_EVEN);
                BigDecimal enterpriseServiceDiscountAmount = enterpriseServiceDiscount.max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_EVEN);
                if (enterpriseSettlement) {
                    chargingOrder.setDiscountElectrovalence(enterpriseElectricDiscountAmount);
                    chargingOrder.setDiscountServiceCharge(enterpriseServiceDiscountAmount);
                    chargingOrder.setWhitelistElectricRefundAmount(enterpriseElectricDiscountAmount);
                    chargingOrder.setWhitelistServiceRefundAmount(enterpriseServiceDiscountAmount);
                    chargingOrder.setWhitelistDiscountAmount(enterpriseElectricDiscountAmount.add(enterpriseServiceDiscountAmount));
                } else {
                    chargingOrder.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN));
                    chargingOrder.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN));
                    chargingOrder.setWhitelistServiceRefundAmount(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN));
                }
                chargingOrder.setElectrovalence(periodElectricPrice_total.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setServiceCharge(serviceCharge_total.setScale(2, RoundingMode.HALF_EVEN));
                refundAmount = refundAmount.max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
@@ -4830,6 +5777,17 @@
                if (actualBalancePayAmount.compareTo(BigDecimal.ZERO) < 0) {
                    actualBalancePayAmount = BigDecimal.ZERO;
                }
                if (enterpriseSettlement) {
                    // 企业账户流程:资金来自企业账户额度(差额结算时退回企业账户),订单无用户侧退款,实付金额=实际消费金额
                    recordedRefundAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                    refundAmount = recordedRefundAmount;
                    channelRefundAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                    thirdRefundAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                    balanceRefundAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                    actualThirdPayAmount = BigDecimal.ZERO;
                    actualBalancePayAmount = payAmount.setScale(2, RoundingMode.HALF_EVEN);
                    chargingOrder.setBalancePayAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN));
                }
                refundAmount = recordedRefundAmount;
                chargingOrder.setBalanceRefundAmount(balanceRefundAmount.setScale(2, RoundingMode.HALF_UP));
                chargingOrder.setThirdRefundAmount(thirdRefundAmount.setScale(2, RoundingMode.HALF_UP));
@@ -4845,8 +5803,26 @@
                if (null != desc) {
                    chargingOrder.setEndSoc(new BigDecimal(desc.getSoc()));
                }
                //企业账户流程:历史订单(快照为空)实时解析的优惠结果回填订单快照列,保证后续补偿重算口径一致;
                //同时置结算完成标记,兜底扫描(findEndChargingOrder)不再回退已结算订单,防止重复结算与重复扣款
                if (enterpriseSettlement) {
                    if (null != enterprisePreferential && enterprisePreferential.needBackfill()) {
                        chargingOrder.setPreferentialType(enterprisePreferential.getPreferentialType());
                        chargingOrder.setServiceDiscountRate(enterprisePreferential.getServiceDiscountRate());
                        chargingOrder.setBillingTemplateId(enterprisePreferential.getBillingTemplateId());
                        chargingOrder.setBillingTemplateSnapshot(
                                ChargeAccountSupport.serializeTemplateSnapshot(enterprisePreferential.getTemplate()));
                        chargingOrder.setPreferentialSnapshotTime(LocalDateTime.now());
                    }
                    chargingOrder.setSettleStatus(1);
                }
                this.updateById(chargingOrder);
                //企业账户流程:结算企业额度(按实际消费扣减锁定额度、差额退回企业账户)并写消费流水
                if (enterpriseSettlement && !settleWhitelistEnterpriseAccount(chargingOrder, payAmount)) {
                    // 结算失败抛异常回滚本地状态,订单保持停止中,由补偿任务重新结算
                    throw new ServiceException("企业账户额度结算失败,订单未结算,请稍后重试 code=" + chargingOrder.getCode());
                }
                chargingOrder = this.getById(chargingOrder.getId());
                Site site = siteClient.getSiteByIds(Arrays.asList(chargingOrder.getSiteId())).getData().get(0);
                //推送三方平台
@@ -5242,6 +6218,10 @@
    private BigDecimal getManualRefundableAmount(TChargingOrder order) {
        if (order == null || order.getId() == null) {
            return BigDecimal.ZERO;
        }
        //企业账户流程:企业账户订单资金来自企业额度,差额在结算时退回企业账户,不支持后台手动退款
        if (isWhitelistEnterpriseOrder(order)) {
            return BigDecimal.ZERO;
        }
        List<TChargingOrderRefund> successfulRefunds = chargingOrderRefundService.list(
@@ -6738,26 +7718,27 @@
        if (cached != null) {
            return cached;
        }
        LocalDate currentMonth = LocalDate.now().withDayOfMonth(1);
        LocalDate currentDay = LocalDate.now();
        List<SixChargingDto> result = new ArrayList<>();
        try {
            String sixBeforeMonth = sixBefore.withDayOfMonth(1).format(LARGE_SCREEN_MONTH_FMT);
            List<SixChargingDto> incomeList = this.baseMapper.incomeTrendFromStat(siteIds, sixBeforeMonth);
            Map<String, SixChargingDto> incomeByMonth = new HashMap<>();
            List<SixChargingDto> incomeList = this.baseMapper.incomeTrendFromOrders(siteIds,
                    sixBefore.atStartOfDay().format(LARGE_SCREEN_SQL_FMT),
                    currentDay.plusDays(1).atStartOfDay().format(LARGE_SCREEN_SQL_FMT));
            Map<String, SixChargingDto> incomeByDay = new HashMap<>();
            if (incomeList != null) {
                for (SixChargingDto income : incomeList) {
                    if (income != null && income.getMonth() != null) {
                        incomeByMonth.put(income.getMonth(), income);
                        incomeByDay.put(income.getMonth(), income);
                    }
                }
            }
            LocalDate month = sixBefore.withDayOfMonth(1);
            while (!month.isAfter(currentMonth)) {
                SixChargingDto income = incomeByMonth.get(month.format(LARGE_SCREEN_MONTH_FMT));
            LocalDate day = sixBefore;
            while (!day.isAfter(currentDay)) {
                SixChargingDto income = incomeByDay.get(day.format(LARGE_SCREEN_DAY_FMT));
                if (income == null) {
                    income = new SixChargingDto();
                }
                income.setMonth(String.format("%02d", month.getMonthValue()));
                income.setMonth(day.format(LARGE_SCREEN_DAY_FMT));
                if (income.getElectrovalence() == null) {
                    income.setElectrovalence(BigDecimal.ZERO);
                }
@@ -6765,18 +7746,18 @@
                    income.setServiceCharge(BigDecimal.ZERO);
                }
                result.add(income);
                month = month.plusMonths(1);
                day = day.plusDays(1);
            }
        } catch (Exception e) {
            log.warn("大屏收入趋势读取预聚合表失败, siteIds={}, sixBefore={}", siteIds, sixBefore, e);
            LocalDate month = sixBefore.withDayOfMonth(1);
            while (!month.isAfter(currentMonth)) {
            LocalDate day = sixBefore;
            while (!day.isAfter(currentDay)) {
                SixChargingDto income = new SixChargingDto();
                income.setMonth(String.format("%02d", month.getMonthValue()));
                income.setMonth(day.format(LARGE_SCREEN_DAY_FMT));
                income.setElectrovalence(BigDecimal.ZERO);
                income.setServiceCharge(BigDecimal.ZERO);
                result.add(income);
                month = month.plusMonths(1);
                day = day.plusDays(1);
            }
        }
        redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS);
@@ -6856,6 +7837,7 @@
        }
        // 3. 全局注册用户数
        writeGlobal();
        clearLargeScreenCache();
        log.info("大屏预聚合表全量重建完成, 耗时 {}ms", System.currentTimeMillis() - start);
    }
@@ -6867,6 +7849,7 @@
        writeDaySlice(LocalDate.now());
        writeUserSitePairs(LocalDate.now(), LocalDate.now().plusDays(1));
        writeGlobal();
        clearLargeScreenCache();
        log.info("大屏预聚合表刷新完成, 耗时 {}ms", System.currentTimeMillis() - start);
    }
@@ -6897,7 +7880,7 @@
    /**
     * 写入一个月度切片聚合(M 行)
     * 金额/订单数取 monthSliceAgg(已结束+已支付口径);
     * 订单数取未删除、状态1-5口径;金额取已结束且已支付口径。
     * 充电用户数取 monthSliceUserAgg(工作台口径: 该站点所有订单的去重用户, 不区分状态/删除标记)
     */
    private void writeMonthSlice(LocalDate monthStart, LocalDate monthEnd) {
@@ -6987,9 +7970,11 @@
            stat.put("orderCount", decimalValue(row, "order_count"));
            stat.put("userCount", BigDecimal.ZERO);
            stat.put("capacity", BigDecimal.ZERO);
            stat.put("electricFee", BigDecimal.ZERO);
            stat.put("serviceFee", BigDecimal.ZERO);
            stat.put("income", BigDecimal.ZERO);
            BigDecimal electricFee = decimalValue(row, "electric_fee");
            BigDecimal serviceFee = decimalValue(row, "service_fee");
            stat.put("electricFee", electricFee);
            stat.put("serviceFee", serviceFee);
            stat.put("income", electricFee.add(serviceFee));
            stat.put("totalPower", decimalValue(row, "total_power"));
            stat.put("registeredUserCount", 0L);
            statRows.add(stat);
@@ -7024,6 +8009,13 @@
                siteIds.stream().sorted().map(String::valueOf).collect(Collectors.joining(","))
                        .getBytes(StandardCharsets.UTF_8));
        return LARGE_SCREEN_CACHE_PREFIX + endpoint + ":" + idPart;
    }
    private void clearLargeScreenCache() {
        Collection<String> keys = redisService.keys(LARGE_SCREEN_CACHE_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            redisService.deleteObject(keys);
        }
    }
    private BigDecimal decimalValue(Map<String, Object> values, String key) {
@@ -7082,6 +8074,14 @@
    public EndOfChargePageInfo getEndOfChargePageInfo(String id) {
        EndOfChargePageInfo info = new EndOfChargePageInfo();
        TChargingOrder chargingOrder = this.getById(id);
        if (chargingOrder == null) {
            // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order(继承 TChargingOrder,字段兼容;
            // 结算已写 paymentAmount/startTime/endTime,后续组装原样复用)
            chargingOrder = vinChargingOrderMapper.selectById(id);
        }
        if (chargingOrder == null) {
            throw new RuntimeException("充电订单不存在");
        }
        Long userId = tokenService.getLoginUserApplet().getUserId();
        if (!chargingOrder.getAppUserId().equals(userId)) {
            throw new RuntimeException("权限不足");
@@ -7442,7 +8442,7 @@
            for (List<Integer> chunk : chunkList(gunIds, 500)) {
                Long chunkCount = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>()
                        .eq(TChargingOrder::getDelFlag, 0)//未删除
                        .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束
                        .in(TChargingOrder::getStatus, Arrays.asList(3, 4, 5))//充电中、停止中、已结束
                        .in(TChargingOrder::getChargingGunId, chunk));//符合条件的充电枪
                total += chunkCount == null ? 0L : chunkCount;
            }
@@ -7451,7 +8451,7 @@
        List<LocalDate> dates = new ArrayList<>();
        List<Integer> counts = new ArrayList<>();
        LocalDate today = LocalDate.now();
        for (int i = 7; i >= 1; i--) {//升序
        for (int i = 6; i >= 0; i--) {//升序,最近七日(含当天)
            LocalDate day = today.minusDays(i);
            LocalDateTime dayStart = day.atStartOfDay();
            LocalDateTime dayEnd = day.plusDays(1).atStartOfDay();
@@ -7837,8 +8837,8 @@
                    BigDecimal originalServicePrice = strategyDetail.getServiceCharge().multiply(sharp_peak_charge);
                    BigDecimal serviceCharge = originalServicePrice;
                    BigDecimal vipDiscountAmount = BigDecimal.ZERO;
                    //计算优惠金额
                    if (null != chargingOrder.getVipDiscount()) {
                    //计算优惠金额;余额支付(含组合支付)不参与会员折扣
                    if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != chargingOrder.getVipDiscount()) {
                        //0.58折
                        vipDiscountAmount = serviceCharge.multiply(new BigDecimal(1).subtract(chargingOrder.getVipDiscount()));
                        serviceCharge = serviceCharge.multiply(chargingOrder.getVipDiscount());
@@ -8026,7 +9026,7 @@
                                BigDecimal serviceCharge = originalServicePrice;
                                BigDecimal vipDiscountAmount = BigDecimal.ZERO;
                                //计算优惠金额
                                if (null != chargingOrder.getVipDiscount()) {
                                if (!isWhitelistOrder(chargingOrder) && null != chargingOrder.getVipDiscount()) {
                                    //0.58折
                                    vipDiscountAmount = serviceCharge.multiply(new BigDecimal(1).subtract(chargingOrder.getVipDiscount()));
                                    serviceCharge = serviceCharge.multiply(chargingOrder.getVipDiscount());
@@ -8329,7 +9329,7 @@
        BigDecimal shareElectricityAmount = getChargingOrderShareElectricityAmount(chargingOrder);
        BigDecimal shareServiceAmount = getChargingOrderShareServiceAmount(chargingOrder);
        BigDecimal shareBaseAmount = shareElectricityAmount.add(shareServiceAmount).setScale(2, RoundingMode.HALF_UP);
        if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0 && isWhitelistOrder(chargingOrder)) {
        if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0 && isWhitelistAllFreeOrder(chargingOrder)) {
            actualTotalPay = shareBaseAmount;
        }
        if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0) {
@@ -8349,7 +9349,7 @@
            records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, resolveThirdPayChannel(chargingOrder), actualThirdPay,
                    BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
        }
        if (records.isEmpty() && isWhitelistOrder(chargingOrder)) {
        if (records.isEmpty() && isWhitelistAllFreeOrder(chargingOrder)) {
            records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, "WHITELIST", actualTotalPay,
                    BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
        }