puzhibing
4 小时以前 8ea02ca30907216aece38fda1ba918e5a5246abe
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -26,6 +26,7 @@
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.bean.BeanUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
@@ -306,15 +307,18 @@
        result.put("discountServiceCharge", order.getDiscountServiceCharge());
        // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造)
        result.put("accountMode", order.getAccountMode());
        result.put("maxEndTime", order.getMaxEndTime());
        // Map 值不走实体字段上的 @JsonFormat,显式格式化,避免前端展示 ISO 格式(含 T)
        result.put("maxEndTime", order.getMaxEndTime() == null ? null
                : order.getMaxEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("lockedAmount", order.getLockedAmount());
        result.put("total", defaultDecimal(order.getElectrovalence()).add(defaultDecimal(order.getServiceCharge())));
        result.put("realTotal", order.getPaymentAmount());
        result.put("endMode", order.getEndMode());
        result.put("phone", order.getPhone());
        result.put("licensePlate", order.getPlateNum());
        result.put("startSoc", "-");
        result.put("endSoc", "-");
        // SOC 展示口径:优先展示结算回填的开始/结束 SOC;历史订单未采集时保持占位符
        result.put("startSoc", order.getStartSoc() == null ? "-" : order.getStartSoc().toPlainString());
        result.put("endSoc", order.getEndSoc() == null ? "-" : order.getEndSoc().toPlainString());
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
@@ -366,6 +370,65 @@
    @Override
    public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
        return baseMapper.accountingStrategyList(chargingOrderId);
    }
    /**
     * 管理后台停止刷卡充电:与 VIN 码停充口径一致,
     * 状态推进使用条件更新(仅当前状态为充电中时生效),防止重复点击/设备回调并发。
     */
    @Override
    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
        Long orderId;
        try {
            orderId = Long.valueOf(chargingOrderId);
        } catch (NumberFormatException e) {
            return AjaxResult.error("刷卡充电订单ID无效");
        }
        TCardChargingOrder order = getAuthorizedOrder(orderId);
        if (order == null) {
            return AjaxResult.error("刷卡充电订单不存在或无权操作");
        }
        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
            return AjaxResult.error("充电枪与订单不匹配");
        }
        Integer status = order.getStatus();
        if (status == null || status != ORDER_STATUS_CHARGING) {
            return AjaxResult.error(status != null && (status == ORDER_STATUS_STOPPING || status == ORDER_STATUS_FINISHED)
                    ? "不能重复操作" : "还未开始充电");
        }
        TChargingGun chargingGun;
        TChargingPile chargingPile;
        try {
            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(chargingGunId);
            chargingGun = gunResult == null ? null : gunResult.getData();
            R<TChargingPile> pileResult = order.getChargingPileId() == null ? null
                    : chargingPileClient.getChargingPileById(order.getChargingPileId());
            chargingPile = pileResult == null ? null : pileResult.getData();
        } catch (Exception ex) {
            log.error("[SD停充] 管理后台停充查询设备异常 code={} gunId={}", order.getCode(), chargingGunId, ex);
            return AjaxResult.error("查询充电设备失败");
        }
        if (chargingGun == null || chargingPile == null) {
            return AjaxResult.error("未找到订单对应的充电设备");
        }
        boolean updated = lambdaUpdate()
                .eq(TCardChargingOrder::getId, order.getId())
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .set(TCardChargingOrder::getEndMode, 1)
                .set(TCardChargingOrder::getEndTime, LocalDateTime.now())
                .update();
        if (!updated) {
            return AjaxResult.error("订单状态已变化,请刷新后重试");
        }
        order.setStatus(ORDER_STATUS_STOPPING);
        order.setEndMode(1);
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(chargingPile.getCode());
        command.setCharging_gun_code(chargingGun.getCode());
        sendMessageClient.platformStopCharging(command);
        log.warn("[SD停充] 管理后台主动停充 code={} gunId={}", order.getCode(), chargingGunId);
        return AjaxResult.success();
    }
    @Override
@@ -433,6 +496,29 @@
            siteIds.add(-1);
        }
        query.setSiteIds(siteIds);
    }
    /**
     * 按订单ID查询并校验数据范围(订单所属站点须在当前登录用户授权站点内),与 VIN 码停充校验口径一致
     */
    private TCardChargingOrder getAuthorizedOrder(Long chargingOrderId) {
        if (chargingOrderId == null) {
            return null;
        }
        TCardChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) {
            return null;
        }
        return order;
    }
    private List<Integer> getAuthorizedSiteIds() {
        Long userId = tokenService.getLoginUser().getUserid();
        if (userId == null) {
            return Collections.emptyList();
        }
        return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData())
                .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
    }
    private void setAuthSiteIds(CardBalanceDetailQuery query) {
@@ -673,37 +759,42 @@
                order.setCreateTime(LocalDateTime.now());
                save(order);
                // 5. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
                // 5. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析):
                //    无可用策略(未配置/已被删除、明细为空)或保存异常时无法计费,直接回滚删除订单并回告启动失败,
                //    避免产生无策略订单(结算侧的价格兜底仅用于历史订单)
                try {
                    if (!saveStrategySnapshot(order.getId(), strategyId)) {
                        log.warn("[SD建单] 无可用计费策略,回滚删除订单 code={} strategyId={}", order.getCode(), strategyId);
                        removeOrderWithSnapshot(order.getId());
                        return DeviceStartChargingVO.fail(null);
                    }
                } catch (Exception snapshotEx) {
                    log.error("[SD建单] 保存计费策略快照异常,回滚删除订单 orderId={} strategyId={}", order.getId(),
                            strategyId, snapshotEx);
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(null);
                }
                // 6. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
                if (!lockCardChargeContext(card, chargeContext)) {
                    removeById(order.getId());
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(3);
                }
                // 5.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放
                // 6.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放
                TCardChargingOrder accountUpdate = new TCardChargingOrder();
                accountUpdate.setId(order.getId());
                accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
                // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
                chargeAccountSupport.fillPreferentialSnapshot(accountUpdate, card.getCompanyId());
                updateById(accountUpdate);
                order.setLockedAmount(chargeContext.getDispatchAmount());
                // 6. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析)
                try {
                    saveStrategySnapshot(order.getId(), strategyId);
                } catch (Exception snapshotEx) {
                    log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
                }
                // 7. 锁卡(条件更新:仅空闲卡可锁;失败释放账户锁定额并回滚删除订单与快照)
                R<Boolean> lockResult = chargingCardClient.lockForCharging(card.getId(), order.getCode());
                if (!Boolean.TRUE.equals(lockResult.getData())) {
                    log.warn("[SD建单] 锁卡失败回滚订单 code={} cardId={} msg={}", order.getCode(), card.getId(), lockResult.getMsg());
                    unlockCardChargeContextQuietly(order, "锁卡失败");
                    cardAccountingStrategyDetailOrderService.remove(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
                                    .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId()));
                    cardAccountingStrategyOrderService.remove(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
                                    .eq(TCardAccountingStrategyOrder::getChargingOrderId, order.getId()));
                    removeById(order.getId());
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(4);
                }
                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={} accountMode={} lockAmount={} maxEndTime={}",
@@ -735,7 +826,7 @@
     * 1) 企业状态校验(未归属/不存在/停用 -> 失败码2),账户模式取企业配置(默认共享);
     * 2) 企业充电授权校验:站点范围(超范围 -> 失败码2)、可用时段(不在时段 -> 失败码2),未配置授权按放行处理;
     * 3) 可用余额校验(共享=企业可用余额,分配=卡可用余额,<=0 -> 失败码3);
     * 4) 下发金额 = min(授权下发额, 可用余额),锁定额度与下发金额一致;授权截止时间用于监控提前停充。
     * 4) 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定额度与下发金额一致;授权截止时间用于监控提前停充。
     */
    private ChargeAccountSupport.ChargeContext resolveCardChargeContext(Integer siteId, Integer strategyId,
                                                                       TChargingCard card) {
@@ -791,11 +882,16 @@
            return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足");
        }
        // 4. 下发金额 = min(授权下发额, 可用余额),锁定金额与下发金额一致
        BigDecimal dispatchAmount = authorization == null || authorization.getDispatchAmount() == null
                ? ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT : authorization.getDispatchAmount();
        if (dispatchAmount.compareTo(usable) > 0) {
        // 4. 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定金额与下发金额一致
        BigDecimal dispatchAmount;
        if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(accountMode)) {
            dispatchAmount = usable;
        } else {
            dispatchAmount = authorization == null || authorization.getDispatchAmount() == null
                    ? ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT : authorization.getDispatchAmount();
            if (dispatchAmount.compareTo(usable) > 0) {
                dispatchAmount = usable;
            }
        }
        if (dispatchAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足");
@@ -830,16 +926,34 @@
    }
    /**
     * 释放账户锁定额(建单失败/回滚场景):共享=企业账户,分配=卡账户;失败仅告警,留待补偿任务/人工处理
     * 释放账户锁定额(建单失败/回滚/关单场景):共享=企业账户,分配=卡账户;
     * 订单侧 lock_released 条件更新抢占,保证同一订单的锁定额在多路径/重复补偿下只释放一次;
     * 释放失败时把标记回滚为未释放,留待补偿任务/人工重试
     */
    private void unlockCardChargeContextQuietly(TCardChargingOrder order, String scene) {
        if (order == null || order.getCompanyId() == null) {
        if (order == null || order.getId() == null || order.getCompanyId() == null) {
            return;
        }
        BigDecimal amount = order.getLockedAmount();
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        boolean claimed;
        try {
            claimed = lambdaUpdate()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getLockReleased, 0)
                    .set(TCardChargingOrder::getLockReleased, 1)
                    .update();
        } catch (Exception ex) {
            log.error("[SD建单] 抢占锁定额释放标记异常 scene={} code={}", scene, order.getCode(), ex);
            return;
        }
        if (!claimed) {
            log.info("[SD建单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, order.getCode());
            return;
        }
        boolean unlocked = false;
        try {
            R<BigDecimal> unlockResult;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode())) {
@@ -847,31 +961,61 @@
            } else {
                unlockResult = groupCompanyClient.unlockBalance(order.getCompanyId(), amount);
            }
            if (unlockResult == null || unlockResult.getCode() != R.SUCCESS) {
            unlocked = unlockResult != null && unlockResult.getCode() == R.SUCCESS;
            if (!unlocked) {
                log.error("[SD建单] 释放账户锁定额失败 scene={} code={} amount={} msg={}", scene, order.getCode(),
                        amount, unlockResult == null ? "null" : unlockResult.getMsg());
            }
        } catch (Exception ex) {
            log.error("[SD建单] 释放账户锁定额异常 scene={} code={} amount={}", scene, order.getCode(), amount, ex);
        }
        if (!unlocked) {
            lambdaUpdate()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getLockReleased, 1)
                    .set(TCardChargingOrder::getLockReleased, 0)
                    .update();
        }
    }
    /**
     * 回滚删除订单及其计费策略快照(建单失败场景;快照可能尚未写入,删除为幂等操作)
     */
    private void removeOrderWithSnapshot(Long orderId) {
        if (orderId == null) {
            return;
        }
        try {
            cardAccountingStrategyDetailOrderService.remove(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
                            .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId));
            cardAccountingStrategyOrderService.remove(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
                            .eq(TCardAccountingStrategyOrder::getChargingOrderId, orderId));
        } catch (Exception ex) {
            log.error("[SD建单] 清理计费策略快照异常 orderId={}", orderId, ex);
        }
        removeById(orderId);
    }
    /**
     * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
     *
     * @return 是否成功写入快照(策略ID为空、策略/明细不存在或明细为空、持久化失败均返回 false,调用方需回滚建单)
     */
    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
    private boolean saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
            return;
            return false;
        }
        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
        if (strategy == null || details == null || details.isEmpty()) {
            return;
            return false;
        }
        TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setChargingOrderId(orderId);
        cardAccountingStrategyOrderService.save(strategyOrder);
        boolean mainSaved = cardAccountingStrategyOrderService.save(strategyOrder);
        List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
        for (TAccountingStrategyDetail detail : details) {
            TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
@@ -879,7 +1023,8 @@
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        return mainSaved && detailSaved;
    }
    /**
@@ -1049,7 +1194,7 @@
    /**
     * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟):
     * 授权时长不足提前量时按授权时长的一半提前,避免刚启动即被停充;
     * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电;
     * maxEndTime 为空(全天授权/未开启自动停充)时不介入。
     */
    private boolean needAuthorizeStop(TCardChargingOrder order) {
@@ -1172,9 +1317,10 @@
        snapshots.sort(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
                Comparator.nullsLast(String::compareTo)));
        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费)
        CompanyPreferentialDTO preferential = chargeAccountSupport.loadCompanyPreferential(order.getCompanyId());
        TBillingTemplateDTO billingTemplate = chargeAccountSupport.loadBillingTemplate(preferential);
        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费):
        // 订单快照优先(建单时固化,策略后续变更不影响本单结算口径),快照为空的历史订单实时查询并在本事务回填
        ChargeAccountSupport.PreferentialSnapshot preferential = chargeAccountSupport.resolvePreferential(order);
        TBillingTemplateDTO billingTemplate = preferential == null ? null : preferential.getTemplate();
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
@@ -1273,10 +1419,15 @@
        final BigDecimal svcTotal = serviceChargeTotal;
        final BigDecimal discountElecTotal = discountElectricTotal;
        final BigDecimal discountSvcTotal = discountServiceTotal;
        // SOC 回填(与 VIN 结算同口径):取订单最早/最新实时监控上报,读取失败不阻断结算
        final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc");
        final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc");
        // 剩余金额口径:可用金额基数(建单写入的 charge_amount) - 本单金额;
        // 改造前创建的老订单无基数时以本单金额兜底(剩余金额记 0),并顺带补齐 charge_amount
        final BigDecimal chargeBase = order.getChargeAmount() != null ? order.getChargeAmount() : orderAmount;
        final BigDecimal residualAmount = chargeBase.subtract(orderAmount).setScale(2, RoundingMode.HALF_EVEN);
        final ChargeAccountSupport.PreferentialSnapshot preferentialSnapshot = preferential;
        final boolean preferentialBackfill = preferentialSnapshot != null && preferentialSnapshot.needBackfill();
        // 本地事务:写计费明细 + 更新订单(状态5,允许负余额金额记录)
        transactionTemplate.execute(status -> {
@@ -1309,6 +1460,22 @@
            if (order.getEndMode() == null) {
                update.setEndMode(1);
            }
            // SOC 回填:监控数据缺失/查询异常时不覆盖既有值,与 VIN 结算行为一致
            if (startSoc != null) {
                update.setStartSoc(startSoc);
            }
            if (endSoc != null) {
                update.setEndSoc(endSoc);
            }
            if (preferentialBackfill) {
                // 历史订单(快照为空)实时查询结果回填订单快照列,保证后续补偿重算口径一致
                update.setPreferentialType(preferentialSnapshot.getPreferentialType());
                update.setServiceDiscountRate(preferentialSnapshot.getServiceDiscountRate());
                update.setBillingTemplateId(preferentialSnapshot.getBillingTemplateId());
                update.setBillingTemplateSnapshot(
                        ChargeAccountSupport.serializeTemplateSnapshot(preferentialSnapshot.getTemplate()));
                update.setPreferentialSnapshotTime(LocalDateTime.now());
            }
            updateById(update);
            return true;
        });
@@ -1339,6 +1506,24 @@
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal,
                discountServiceTotal);
        return R.ok();
    }
    /**
     * 容错读取订单最早/最新监控记录的 SOC(与 VIN 结算同口径):
     * Mongo 数据缺失、SOC 未上报或查询异常返回 null,不阻断主结算流程。
     */
    private BigDecimal getMonitoringSocQuietly(String code, String sort) {
        if (uploadRealTimeMonitoringDataService == null) {
            return null;
        }
        try {
            UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(code, sort);
            return monitoringData == null || monitoringData.getSoc() == null
                    ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue());
        } catch (Exception ex) {
            log.error("[SD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex);
            return null;
        }
    }
    /**
@@ -1383,10 +1568,10 @@
                }
                afterBalance = settleResult.getData().getAfterBalance();
            } else {
                // 订单侧自行写消费流水,此处不传订单号(账户侧仅在订单号非空时做幂等与流水,避免重复记账)
                // 共享模式:传订单号启用账户侧结算幂等(账户侧按订单号+变更类型落企业消费流水并作为已结算标记)
                R<BigDecimal> settleResult =
                        groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual,
                                null, null, null, null);
                                code, order.getCardNo(), null, "刷卡充电结算");
                if (settleResult == null || settleResult.getCode() != R.SUCCESS) {
                    log.error("[SD结算] 企业账户结算失败 code={} companyId={} locked={} actual={} msg={}", code,
                            order.getCompanyId(), lockedAmount, actual,
@@ -1400,7 +1585,10 @@
                    lockedAmount, actual, ex);
            return false;
        }
        recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code);
        if (allocated) {
            // 分配模式消费流水记卡账户;共享模式消费流水已由账户侧 settleBalance 按订单号写入,避免重复记账
            recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code);
        }
        log.info("[SD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code,
                order.getAccountMode(), lockedAmount, actual, afterBalance);
        return true;
@@ -1438,7 +1626,8 @@
    }
    /**
     * 写企业额度分配记录-消费流水(分配模式记该卡账户;共享模式记企业账户):
     * 写企业额度分配记录-消费流水(仅卡侧扣款场景:分配模式结算与历史订单卡扣款,记该卡账户):
     * 共享模式消费流水由账户侧 settleBalance 按订单号写入(同一订单号+变更类型只能有一个写入方,避免重复记账);
     * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程
     */
    private void recordCardConsumeAllocate(TCardChargingOrder order, BigDecimal actual, BigDecimal lockedAmount,
@@ -1448,21 +1637,8 @@
        }
        BigDecimal after = afterBalance == null ? BigDecimal.ZERO : afterBalance;
        BigDecimal before = after.add(actual).subtract(lockedAmount);
        boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode());
        String userInfo;
        String userName;
        if (allocated) {
            userInfo = order.getCardNo();
            userName = order.getCardNo();
        } else {
            TGroupCompany company = chargeAccountSupport.queryCompanyQuietly(order.getCompanyId());
            userInfo = company == null || StringUtils.isEmpty(company.getName())
                    ? String.valueOf(order.getCompanyId()) : company.getName();
            userName = company == null ? null : company.getName();
        }
        chargeAccountSupport.recordConsume(order.getCompanyId(),
                allocated ? TCompanyAllocateRecord.USER_TYPE_CARD : TCompanyAllocateRecord.USER_TYPE_COMPANY,
                userInfo, userName, before, actual.negate(), code, "刷卡充电结算");
        chargeAccountSupport.recordConsume(order.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_CARD,
                order.getCardNo(), order.getCardNo(), before, actual.negate(), code, "刷卡充电结算");
    }
    private boolean rollbackFinishedToStopping(Long orderId, String code) {
@@ -1622,14 +1798,21 @@
        List<String> orderCodes = candidates.stream().map(TCardChargingOrder::getCode)
                .filter(StringUtils::isNotEmpty).collect(Collectors.toList());
        // 双判据:卡侧扣款流水(分配模式/历史卡扣款)与企业侧消费流水(共享模式),任一存在即视为已结算
        R<List<String>> deductedResult = chargingCardClient.listDeductedOrderCodes(orderCodes);
        if (deductedResult == null || deductedResult.getCode() != R.SUCCESS || deductedResult.getData() == null) {
            log.error("[SD扣款补偿] 查询扣款流水失败,本轮不修改订单 msg={}",
            log.error("[SD扣款补偿] 查询卡侧扣款流水失败,本轮不修改订单 msg={}",
                    deductedResult == null ? "null" : deductedResult.getMsg());
            return 0;
        }
        java.util.Set<String> consumedCodes = chargeAccountSupport.listConsumedOrderCodesQuietly(orderCodes);
        if (consumedCodes == null) {
            log.error("[SD扣款补偿] 查询企业侧消费流水失败,本轮不修改订单");
            return 0;
        }
        java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData());
        deductedCodes.addAll(consumedCodes);
        int recovered = 0;
        for (TCardChargingOrder candidate : candidates) {
            if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) {
@@ -1642,8 +1825,14 @@
                }
                R<List<String>> recheck = chargingCardClient.listDeductedOrderCodes(
                        Collections.singletonList(candidate.getCode()));
                if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null
                        || recheck.getData().contains(candidate.getCode())) {
                if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null) {
                    continue;
                }
                // 双判据复核:任一侧查询失败或存在流水即不回退(保守处理,避免重复扣款)
                java.util.Set<String> consumedRecheck = chargeAccountSupport.listConsumedOrderCodesQuietly(
                        Collections.singletonList(candidate.getCode()));
                if (consumedRecheck == null || recheck.getData().contains(candidate.getCode())
                        || consumedRecheck.contains(candidate.getCode())) {
                    continue;
                }
                if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) {