puzhibing
4 小时以前 8ea02ca30907216aece38fda1ba918e5a5246abe
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -7,6 +7,7 @@
import com.ruoyi.account.api.feignClient.ChargingCardClient;
import com.ruoyi.account.api.model.TAppUserCar;
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.model.TCompanyAllocateRecord;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
@@ -14,8 +15,10 @@
import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
import com.ruoyi.chargingPile.api.model.TBillingTemplateDetail;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
@@ -23,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;
@@ -46,11 +50,20 @@
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
import com.ruoyi.integration.api.feignClient.SendMessageClient;
import com.ruoyi.integration.api.model.PlatformStopCharging;
import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
import com.ruoyi.integration.api.vo.ChargingOrderVo;
import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
import com.ruoyi.order.util.TransactionStageParser;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.other.api.domain.TFleet;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.api.dto.ChargeAuthorizationDTO;
import com.ruoyi.other.api.dto.CompanyPreferentialDTO;
import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import com.ruoyi.order.support.ChargeAccountSupport;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
@@ -112,6 +125,10 @@
    private static final int SD_STARTING_TIMEOUT_MINUTES = 30;
    /** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
    private static final int SD_CHARGING_TIMEOUT_HOURS = 24;
    /** 状态5未扣款扫描保护窗,避免与正在提交的扣款事务竞态 */
    private static final int SD_DEDUCT_COMPENSATION_DELAY_MINUTES = 10;
    /** 单轮最多检查的已结束订单数,避免大批量远程查询 */
    private static final int SD_DEDUCT_COMPENSATION_BATCH_SIZE = 100;
    @Resource
    private TokenService tokenService;
@@ -119,6 +136,8 @@
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private SiteClient siteClient;
    @Resource
@@ -131,6 +150,12 @@
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private ChargingCardClient chargingCardClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private ChargeAuthorizationClient chargeAuthorizationClient;
    @Resource
    private ChargeAccountSupport chargeAccountSupport;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
@@ -277,13 +302,23 @@
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("electrovalence", order.getElectrovalence());
        result.put("serviceCharge", order.getServiceCharge());
        // 优惠金额(原始电费/服务费 - 最终,企业优惠设置改造)
        result.put("discountElectrovalence", order.getDiscountElectrovalence());
        result.put("discountServiceCharge", order.getDiscountServiceCharge());
        // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造)
        result.put("accountMode", order.getAccountMode());
        // 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()));
        }
@@ -335,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
@@ -404,6 +498,29 @@
        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) {
        List<Integer> siteIds = new ArrayList<>();
        Long userId = tokenService.getLoginUser().getUserid();
@@ -449,6 +566,7 @@
            return;
        }
        fillElectronicProportion(list);
        fillCompanyFleetNames(list);
        List<Integer> siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(ChargingOrderVO::getChargingGunId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Long> carIds = list.stream().map(ChargingOrderVO::getAppUserCarId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
@@ -480,6 +598,73 @@
            item.setPlatformIncome(BigDecimal.ZERO);
            item.setPartnerIncome(BigDecimal.ZERO);
        }
    }
    /**
     * 回填所属公司/车队名称:按订单快照 companyId/fleetId 批量远程取名(公司/车队改名后列表即时生效),
     * 远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillCompanyFleetNames(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 = loadCompanyNames(companyIds);
            for (ChargingOrderVO item : list) {
                String name = item.getCompanyId() == null ? null : companyNames.get(item.getCompanyId());
                if (StringUtils.isNotEmpty(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 = loadFleetNames(fleetIds);
        for (ChargingOrderVO item : list) {
            String name = item.getFleetId() == null ? null : fleetNames.get(item.getFleetId());
            if (StringUtils.isNotEmpty(name)) {
                item.setFleetName(name);
            }
        }
    }
    /** 批量查询公司名称,失败返回已取到的数据 */
    private Map<Integer, String> loadCompanyNames(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.isNotEmpty(company.getName())) {
                    result.put(company.getId(), company.getName());
                }
            }
        } catch (Exception e) {
            log.warn("[SD回填] 批量查询公司名称失败 ids={}", companyIds, e);
        }
        return result;
    }
    /** 批量查询车队名称,失败返回已取到的数据 */
    private Map<Integer, String> loadFleetNames(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.isNotEmpty(fleet.getFleetName())) {
                    result.put(fleet.getId(), fleet.getFleetName());
                }
            }
        } catch (Exception e) {
            log.warn("[SD回填] 批量查询车队名称失败 ids={}", fleetIds, e);
        }
        return result;
    }
    private String[] parseTimeRange(CardChargingOrderQuery query) {
@@ -526,9 +711,16 @@
            if (card == null) {
                return DeviceStartChargingVO.fail(mapAuthFailure(verifyResult.getMsg()));
            }
            // 3. 启动余额校验(余额>1 元方可启动,与 CD 费用不足终止阈值对齐;结算允许扣成负余额)
            if (card.getBalance() == null || card.getBalance().compareTo(BigDecimal.ONE) <= 0) {
                return DeviceStartChargingVO.fail(3);
            // 3. 计费策略(枪策略优先,站点策略兜底):授权校验(指定电价时段)与策略快照共用
            Integer strategyId = gun.getAccountingStrategyId() != null
                    ? gun.getAccountingStrategyId()
                    : (site == null ? null : site.getAccountingStrategyId());
            // 3.1 账户上下文解析(企业状态、充电授权站点范围/可用时段、账户模式、可用余额、授权截止时间)
            ChargeAccountSupport.ChargeContext chargeContext = resolveCardChargeContext(gun.getSiteId(), strategyId, card);
            if (!chargeContext.isOk()) {
                log.warn("[SD建单] 账户校验不通过 cardId={} gun={} reason={}", card.getId(),
                        dto.getChargingGunCode(), chargeContext.getFailReason());
                return DeviceStartChargingVO.fail(chargeContext.getFailCode());
            }
            // 并发防护(文档13.2):卡ID维度锁,防同卡多枪并发建单(数据库锁卡条件更新为第二道保护)
@@ -555,37 +747,60 @@
                order.setChargingGunId(gun.getId());
                order.setCardId(card.getId());
                order.setCardNo(card.getCardNo());
                // 可用金额基数与启动剩余金额(与返回设备的 balance 同源,供监控/结算扣减与前端展示)
                order.setChargeAmount(card.getBalance());
                order.setResidualAmount(card.getBalance());
                // 所属公司/车队快照落库(充电卡当前归属),供订单列表/汇总/导出按公司、车队直接筛选
                order.setCompanyId(card.getCompanyId());
                order.setFleetId(card.getFleetId());
                // 可用金额基数与启动剩余金额:共享模式=min(授权下发额, 企业可用余额),分配模式=min(授权下发额, 卡可用余额)
                order.setChargeAmount(chargeContext.getDispatchAmount());
                order.setResidualAmount(chargeContext.getDispatchAmount());
                // 账户模式与授权截止时间快照(锁定额度在锁定成功后回填,与 VIN 链路一致)
                order.setAccountMode(chargeContext.getAccountMode());
                order.setMaxEndTime(chargeContext.getMaxEndTime());
                order.setCreateTime(LocalDateTime.now());
                save(order);
                // 5. 计费策略快照(枪策略优先,站点策略兜底;无策略照常建单,结算按0价并告警)
                Integer strategyId = gun.getAccountingStrategyId() != null
                        ? gun.getAccountingStrategyId()
                        : (site == null ? null : site.getAccountingStrategyId());
                // 5. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析):
                //    无可用策略(未配置/已被删除、明细为空)或保存异常时无法计费,直接回滚删除订单并回告启动失败,
                //    避免产生无策略订单(结算侧的价格兜底仅用于历史订单)
                try {
                    saveStrategySnapshot(order.getId(), strategyId);
                    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);
                    log.error("[SD建单] 保存计费策略快照异常,回滚删除订单 orderId={} strategyId={}", order.getId(),
                            strategyId, snapshotEx);
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(null);
                }
                // 6. 锁卡(条件更新:仅空闲卡可锁;失败回滚删除订单与快照)
                // 6. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
                if (!lockCardChargeContext(card, chargeContext)) {
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(3);
                }
                // 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());
                // 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());
                    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());
                    unlockCardChargeContextQuietly(order, "锁卡失败");
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(4);
                }
                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={}", order.getCode(), card.getCardNo(), dto.getChargingGunCode());
                return DeviceStartChargingVO.success(order.getCode(), card.getBalance());
                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={} accountMode={} lockAmount={} maxEndTime={}",
                        order.getCode(), card.getCardNo(), dto.getChargingGunCode(), chargeContext.getAccountMode(),
                        chargeContext.getDispatchAmount(), chargeContext.getMaxEndTime());
                return DeviceStartChargingVO.success(order.getCode(), chargeContext.getDispatchAmount());
            } finally {
                if (cardLocked && cardLock.isHeldByCurrentThread()) {
                    cardLock.unlock();
@@ -607,21 +822,200 @@
    }
    /**
     * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
     * 刷卡充电账户上下文解析(与 VIN 链路同构):
     * 1) 企业状态校验(未归属/不存在/停用 -> 失败码2),账户模式取企业配置(默认共享);
     * 2) 企业充电授权校验:站点范围(超范围 -> 失败码2)、可用时段(不在时段 -> 失败码2),未配置授权按放行处理;
     * 3) 可用余额校验(共享=企业可用余额,分配=卡可用余额,<=0 -> 失败码3);
     * 4) 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定额度与下发金额一致;授权截止时间用于监控提前停充。
     */
    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
    private ChargeAccountSupport.ChargeContext resolveCardChargeContext(Integer siteId, Integer strategyId,
                                                                       TChargingCard card) {
        // 1. 企业校验:刷卡充电按企业账户模式计费,卡必须归属有效企业
        Integer companyId = card.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("[SD建单] 查询企业失败 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();
        // 2. 充电授权校验(站点范围 + 可用时段 + 授权截止时间)
        ChargeAuthorizationDTO authorization = null;
        LocalDateTime maxEndTime = null;
        try {
            R<ChargeAuthorizationDTO> authResult = chargeAuthorizationClient.getDetailByCompanyId(companyId);
            authorization = authResult == null ? null : authResult.getData();
        } catch (Exception ex) {
            log.error("[SD建单] 查询企业充电授权失败 companyId={}", companyId, ex);
        }
        if (authorization != null) {
            List<Integer> siteIds = authorization.getSiteIds();
            if (siteIds != null && !siteIds.isEmpty() && siteId != null && !siteIds.contains(siteId)) {
                return ChargeAccountSupport.ChargeContext.fail(2, "站点不在企业充电授权范围");
            }
            ChargeAccountSupport.AuthorizationWindow window =
                    chargeAccountSupport.resolveAuthorizationWindow(authorization, strategyId, companyId);
            if (window == null) {
                return ChargeAccountSupport.ChargeContext.fail(2, "当前时间不在企业充电授权时段");
            }
            maxEndTime = window.getMaxEndTime();
        }
        // 3. 可用余额校验(实扣冻结口径,可用余额<=0 不允许启动)
        BigDecimal usable = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(accountMode)
                ? card.getBalance() : company.getAvailableBalance();
        usable = usable == null ? BigDecimal.ZERO : usable;
        if (usable.compareTo(BigDecimal.ZERO) <= 0) {
            return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足");
        }
        // 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, "账户可用余额不足");
        }
        return ChargeAccountSupport.ChargeContext.ok(accountMode, dispatchAmount, maxEndTime);
    }
    /**
     * 锁定账户额度(实扣冻结):共享模式锁企业账户,分配模式锁卡账户;锁定失败回告余额不足
     */
    private boolean lockCardChargeContext(TChargingCard card, ChargeAccountSupport.ChargeContext context) {
        BigDecimal amount = context.getDispatchAmount();
        try {
            R<BigDecimal> lockResult;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(context.getAccountMode())) {
                lockResult = chargingCardClient.lockBalance(card.getId(), amount);
            } else {
                lockResult = groupCompanyClient.lockBalance(card.getCompanyId(), amount);
            }
            if (lockResult == null || lockResult.getCode() != R.SUCCESS) {
                log.warn("[SD建单] 锁定账户额度失败 cardId={} companyId={} accountMode={} amount={} msg={}",
                        card.getId(), card.getCompanyId(), context.getAccountMode(), amount,
                        lockResult == null ? "null" : lockResult.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[SD建单] 锁定账户额度异常 cardId={} companyId={} amount={}", card.getId(), card.getCompanyId(),
                    amount, ex);
            return false;
        }
    }
    /**
     * 释放账户锁定额(建单失败/回滚/关单场景):共享=企业账户,分配=卡账户;
     * 订单侧 lock_released 条件更新抢占,保证同一订单的锁定额在多路径/重复补偿下只释放一次;
     * 释放失败时把标记回滚为未释放,留待补偿任务/人工重试
     */
    private void unlockCardChargeContextQuietly(TCardChargingOrder order, String scene) {
        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())) {
                unlockResult = chargingCardClient.unlockBalance(order.getCardId(), amount);
            } else {
                unlockResult = groupCompanyClient.unlockBalance(order.getCompanyId(), amount);
            }
            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 boolean saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
            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();
@@ -629,7 +1023,8 @@
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        return mainSaved && detailSaved;
    }
    /**
@@ -752,6 +1147,11 @@
                && !Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus())) {
            return;
        }
        // 充电中授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 下发,规避数据传输延迟)
        if (Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus()) && needAuthorizeStop(order)) {
            stopByAuthorizeExpired(order);
            return;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        boolean starting = Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus());
@@ -790,6 +1190,56 @@
        if (starting && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复");
        }
    }
    /**
     * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟):
     * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电;
     * maxEndTime 为空(全天授权/未开启自动停充)时不介入。
     */
    private boolean needAuthorizeStop(TCardChargingOrder order) {
        LocalDateTime startTime = order.getStartTime() != null ? order.getStartTime() : order.getCreateTime();
        return chargeAccountSupport.needAuthorizeStop(order.getMaxEndTime(), startTime);
    }
    /**
     * 授权时间到期主动停充:复用小程序主动停充链路(状态4 + 结束方式1 + 下发停止充电命令),
     * 状态推进使用条件更新,与设备回调/其他停充入口并发时只有一方生效
     */
    private void stopByAuthorizeExpired(TCardChargingOrder order) {
        TChargingGun gun;
        TChargingPile pile;
        try {
            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(order.getChargingGunId());
            gun = gunResult == null ? null : gunResult.getData();
            pile = order.getChargingPileId() == null ? null
                    : Optional.ofNullable(chargingPileClient.getChargingPileById(order.getChargingPileId()).getData())
                            .orElse(null);
        } catch (Exception ex) {
            log.error("[SD停充] 授权到期停充查询设备异常 code={}", order.getCode(), ex);
            return;
        }
        if (gun == null || pile == null) {
            log.error("[SD停充] 授权到期停充缺少设备信息 code={} gunId={}", order.getCode(), order.getChargingGunId());
            return;
        }
        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;
        }
        order.setStatus(ORDER_STATUS_STOPPING);
        order.setEndMode(1);
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(pile.getCode());
        command.setCharging_gun_code(gun.getCode());
        sendMessageClient.platformStopCharging(command);
        log.warn("[SD停充] 授权时间到期主动停充 code={} maxEndTime={}", order.getCode(), order.getMaxEndTime());
    }
    @Override
@@ -867,8 +1317,15 @@
        snapshots.sort(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
                Comparator.nullsLast(String::compareTo)));
        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费):
        // 订单快照优先(建单时固化,策略后续变更不影响本单结算口径),快照为空的历史订单实时查询并在本事务回填
        ChargeAccountSupport.PreferentialSnapshot preferential = chargeAccountSupport.resolvePreferential(order);
        TBillingTemplateDTO billingTemplate = preferential == null ? null : preferential.getTemplate();
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        BigDecimal discountElectricTotal = BigDecimal.ZERO;
        BigDecimal discountServiceTotal = BigDecimal.ZERO;
        List<TCardChargingOrderAccountingStrategy> details = new ArrayList<>();
        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
@@ -882,22 +1339,52 @@
            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
            BigDecimal originalServicePrice = snapshot.getCostServiceCharge() == null
                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
            BigDecimal electricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal serviceAmount = capacity.multiply(servicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            // 原价金额(策略单价),优惠金额 = 原价 - 最终
            BigDecimal originalElectricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            // 优惠后的最终单价:方式2=服务费按比例优惠;方式3=电费/服务费取计费模板同阶段单价
            BigDecimal finalElectricPrice = electricPrice;
            BigDecimal finalServicePrice = servicePrice;
            if (preferential != null
                    && Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE).equals(preferential.getPreferentialType())) {
                finalServicePrice = ChargeAccountSupport.applyServiceDiscountRate(servicePrice, preferential.getServiceDiscountRate());
            } else if (preferential != null
                    && Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE).equals(preferential.getPreferentialType())) {
                TBillingTemplateDetail templateDetail = ChargeAccountSupport.findTemplateDetail(billingTemplate, snapshot.getType());
                if (templateDetail == null) {
                    log.warn("[SD结算] 计费模板缺少对应阶段,该段按原价结算 code={} type={} templateId={}", code,
                            snapshot.getType(), preferential.getBillingTemplateId());
                } else {
                    if (templateDetail.getElectrovalence() != null) {
                        finalElectricPrice = templateDetail.getElectrovalence();
                    }
                    if (templateDetail.getServiceCharge() != null) {
                        finalServicePrice = templateDetail.getServiceCharge();
                    }
                }
            }
            BigDecimal electricAmount = capacity.multiply(finalElectricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal serviceAmount = capacity.multiply(finalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal discountElectricAmount = originalElectricAmount.subtract(electricAmount);
            BigDecimal discountServiceAmount = originalServiceAmount.subtract(serviceAmount);
            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
            discountElectricTotal = discountElectricTotal.add(discountElectricAmount);
            discountServiceTotal = discountServiceTotal.add(discountServiceAmount);
            TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
            detail.setChargingOrderId(order.getId());
            detail.setType(snapshot.getType());
            detail.setElectrovalence(electricPrice);
            detail.setServiceCharge(servicePrice);
            detail.setElectrovalence(finalElectricPrice);
            detail.setServiceCharge(finalServicePrice);
            detail.setCostServiceCharge(originalServicePrice);
            detail.setChargingCapacity(capacity);
            detail.setPeriodElectricPrice(electricAmount);
            detail.setPeriodServicePrice(serviceAmount);
            detail.setPeriodOriginalServicePrice(originalServiceAmount);
            detail.setPeriodOriginalElectricPrice(originalElectricAmount);
            detail.setPeriodDiscountElectricPrice(discountElectricAmount);
            detail.setPeriodDiscountServicePrice(discountServiceAmount);
            if (snapshot != null) {
                // 计费策略明细id:快照id(IdType.INPUT复制自原明细id),与CD结算口径一致
                detail.setAccountingStrategyDetailId(snapshot.getId());
@@ -914,8 +1401,7 @@
            log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
        LocalDateTime settlementStartTime = order.getStartTime() != null
                ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null);
        LocalDateTime settlementStartTime = parseDeviceTime(vo.getStart_time(), order.getStartTime());
        LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime());
        if (!details.isEmpty()) {
            DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
@@ -931,10 +1417,17 @@
        final BigDecimal totalElec = totalElectricity;
        final BigDecimal elecTotal = electrovalenceTotal;
        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 -> {
@@ -952,6 +1445,9 @@
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            // 优惠合计(原始电费/服务费 - 最终),与 VIN 链路同口径
            update.setDiscountElectrovalence(discountElecTotal);
            update.setDiscountServiceCharge(discountSvcTotal);
            // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示)
            update.setBalancePayAmount(orderAmount);
            update.setChargeAmount(chargeBase);
@@ -964,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;
        });
@@ -974,32 +1486,171 @@
        order.setBalancePayAmount(orderAmount);
        order.setOrderAmount(orderAmount);
        order.setPaymentAmount(orderAmount);
        order.setDiscountElectrovalence(discountElectricTotal);
        order.setDiscountServiceCharge(discountServiceTotal);
        // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单)
        if (orderAmount.compareTo(BigDecimal.ZERO) > 0 && order.getCardId() != null) {
            ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
            deductDTO.setCardId(order.getCardId());
            deductDTO.setOrderCode(order.getCode());
            deductDTO.setAmount(orderAmount);
            R<ChargingCardDeductVO> deductResult = chargingCardClient.deductForCharging(deductDTO);
            if (deductResult != null && deductResult.getData() != null
                    && deductResult.getData().isDuplicated()) {
                log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
            } else if (deductResult == null || deductResult.getCode() != R.SUCCESS) {
                log.error("[SD结算] 扣款失败,欠费记账 code={} cardId={} amount={} msg={}",
                        code, order.getCardId(), orderAmount, deductResult == null ? "null" : deductResult.getMsg());
            }
        // 账户结算(实扣冻结口径):解锁申请时锁定的额度、按实际消费扣款并退回差额,同时写分配记录消费流水;
        // 无法确认扣款成功时回退状态4,交由补偿任务重试
        if (!settleCardChargeAccount(order, orderAmount, code)) {
            rollbackFinishedToStopping(order.getId(), code);
            return R.fail("充电卡结算失败,订单已进入补偿队列");
        }
        // 解锁卡片(无论扣款成败必须解锁,失败人工介入)
        // 结算成功后解锁卡片
        unlockCardQuietly(order);
        // 复位充电枪
        resetGunQuietly(order);
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
        log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={} discountElec={} discountSvc={}",
                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;
        }
    }
    /**
     * 结算刷卡账户资金(实扣冻结口径,与 VIN 链路同构):
     * 1) 分配模式:解锁卡片锁定额并按实际消费扣减(允许扣成负数),差额退回卡账户;
     * 2) 共享模式:解锁企业账户锁定额并按实际消费扣减,差额退回企业账户;
     * 3) 写入企业额度分配记录-消费流水(按订单号+change_type 幂等)。
     * 改造前的历史订单(无锁定金额)沿用历史扣款接口扣卡余额,保持既有行为不变。
     *
     * @return 是否确认结算成功(false 表示需回退状态等待补偿重试)
     */
    private boolean settleCardChargeAccount(TCardChargingOrder order, BigDecimal actualAmount, String code) {
        BigDecimal lockedAmount = order.getLockedAmount() == null ? BigDecimal.ZERO : order.getLockedAmount();
        BigDecimal actual = actualAmount == null ? BigDecimal.ZERO : actualAmount;
        if (actual.compareTo(BigDecimal.ZERO) <= 0 && lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            log.info("[SD结算] 订单金额为0且无锁定额度,跳过账户结算 code={}", code);
            return true;
        }
        boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode());
        if (lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            // 历史订单(改造前无锁定金额):沿用仅按实扣金额扣卡余额的旧接口
            log.info("[SD结算] 订单无锁定金额,按历史扣款接口结算 code={} accountMode={}", code, order.getAccountMode());
            return legacyCardDeduct(order, actual, code);
        }
        BigDecimal afterBalance;
        try {
            if (allocated) {
                ChargingCardDeductDTO settleDTO = new ChargingCardDeductDTO();
                settleDTO.setCardId(order.getCardId());
                settleDTO.setOrderCode(code);
                settleDTO.setAmount(actual);
                settleDTO.setLockedAmount(lockedAmount);
                R<ChargingCardDeductVO> settleResult = chargingCardClient.settleForCharging(settleDTO);
                if (settleResult == null || settleResult.getCode() != R.SUCCESS || settleResult.getData() == null) {
                    log.error("[SD结算] 卡账户结算失败 code={} cardId={} locked={} actual={} msg={}", code,
                            order.getCardId(), lockedAmount, actual,
                            settleResult == null ? "null" : settleResult.getMsg());
                    return false;
                }
                if (settleResult.getData().isDuplicated()) {
                    log.warn("[SD结算] 卡账户结算幂等命中(重复请求不重复扣)code={}", code);
                }
                afterBalance = settleResult.getData().getAfterBalance();
            } else {
                // 共享模式:传订单号启用账户侧结算幂等(账户侧按订单号+变更类型落企业消费流水并作为已结算标记)
                R<BigDecimal> settleResult =
                        groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual,
                                code, order.getCardNo(), null, "刷卡充电结算");
                if (settleResult == null || settleResult.getCode() != R.SUCCESS) {
                    log.error("[SD结算] 企业账户结算失败 code={} companyId={} locked={} actual={} msg={}", code,
                            order.getCompanyId(), lockedAmount, actual,
                            settleResult == null ? "null" : settleResult.getMsg());
                    return false;
                }
                afterBalance = settleResult.getData();
            }
        } catch (Exception ex) {
            log.error("[SD结算] 账户结算异常 code={} accountMode={} locked={} actual={}", code, order.getAccountMode(),
                    lockedAmount, actual, ex);
            return false;
        }
        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;
    }
    /**
     * 历史订单(改造前无锁定金额)扣款:沿用原有扣卡余额接口(订单号幂等)
     */
    private boolean legacyCardDeduct(TCardChargingOrder order, BigDecimal actual, String code) {
        if (actual.compareTo(BigDecimal.ZERO) <= 0 || order.getCardId() == null) {
            return true;
        }
        ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
        deductDTO.setCardId(order.getCardId());
        deductDTO.setOrderCode(code);
        deductDTO.setAmount(actual);
        R<ChargingCardDeductVO> deductResult;
        try {
            deductResult = chargingCardClient.deductForCharging(deductDTO);
        } catch (Exception ex) {
            log.error("[SD结算] 扣款调用异常 code={} cardId={} amount={}", code, order.getCardId(), actual, ex);
            return false;
        }
        if (deductResult == null || deductResult.getCode() != R.SUCCESS || deductResult.getData() == null) {
            log.error("[SD结算] 扣款失败 code={} cardId={} amount={} msg={}", code, order.getCardId(), actual,
                    deductResult == null ? "null" : deductResult.getMsg());
            return false;
        }
        if (deductResult.getData().isDuplicated()) {
            log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
            return true;
        }
        recordCardConsumeAllocate(order, actual, BigDecimal.ZERO, deductResult.getData().getAfterBalance(), code);
        return true;
    }
    /**
     * 写企业额度分配记录-消费流水(仅卡侧扣款场景:分配模式结算与历史订单卡扣款,记该卡账户):
     * 共享模式消费流水由账户侧 settleBalance 按订单号写入(同一订单号+变更类型只能有一个写入方,避免重复记账);
     * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程
     */
    private void recordCardConsumeAllocate(TCardChargingOrder order, BigDecimal actual, BigDecimal lockedAmount,
                                           BigDecimal afterBalance, String code) {
        if (order.getCompanyId() == null) {
            return;
        }
        BigDecimal after = afterBalance == null ? BigDecimal.ZERO : afterBalance;
        BigDecimal before = after.add(actual).subtract(lockedAmount);
        chargeAccountSupport.recordConsume(order.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_CARD,
                order.getCardNo(), order.getCardNo(), before, actual.negate(), code, "刷卡充电结算");
    }
    private boolean rollbackFinishedToStopping(Long orderId, String code) {
        boolean rolledBack = lambdaUpdate()
                .eq(TCardChargingOrder::getId, orderId)
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .update();
        if (!rolledBack) {
            log.warn("[SD结算] 状态5回退4未命中,订单状态可能已变化 code={}", code);
        }
        return rolledBack;
    }
    /**
@@ -1132,6 +1783,78 @@
    }
    @Override
    public int recoverFinishedOrdersWithoutDeduct() {
        List<TCardChargingOrder> candidates = lambdaQuery()
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
                .isNotNull(TCardChargingOrder::getCardId)
                .gt(TCardChargingOrder::getOrderAmount, BigDecimal.ZERO)
                .lt(TCardChargingOrder::getEndTime, LocalDateTime.now().minusMinutes(SD_DEDUCT_COMPENSATION_DELAY_MINUTES))
                .orderByAsc(TCardChargingOrder::getEndTime)
                .last("LIMIT " + SD_DEDUCT_COMPENSATION_BATCH_SIZE)
                .list();
        if (candidates.isEmpty()) {
            return 0;
        }
        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={}",
                    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())) {
                continue;
            }
            RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + candidate.getCode());
            try {
                if (!settleLock.tryLock(0, 60, TimeUnit.SECONDS)) {
                    continue;
                }
                R<List<String>> recheck = chargingCardClient.listDeductedOrderCodes(
                        Collections.singletonList(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())) {
                    recovered++;
                    log.warn("[SD扣款补偿] 已结束订单无扣款流水,状态回退停止中 code={}", candidate.getCode());
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                log.warn("[SD扣款补偿] 获取结算锁被中断 code={}", candidate.getCode());
                break;
            } catch (Exception ex) {
                log.error("[SD扣款补偿] 订单处理异常,保留原状态 code={}", candidate.getCode(), ex);
            } finally {
                if (settleLock.isHeldByCurrentThread()) {
                    settleLock.unlock();
                }
            }
        }
        return recovered;
    }
    @Override
    public void closeTimeoutStartingOrder(TCardChargingOrder order) {
        TCardChargingOrder update = new TCardChargingOrder();
        update.setStatus(ORDER_STATUS_START_FAILED);
@@ -1154,6 +1877,8 @@
                    .update();
            return;
        }
        // 释放申请时锁定的账户额度(未产生消费;失败仅告警,由人工/后续补偿核对)
        unlockCardChargeContextQuietly(order, "启动关单");
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[SD补偿] 启动超时关单 code={} cardId={} gunId={}", order.getCode(), order.getCardId(), order.getChargingGunId());