puzhibing
2026-09-30 64464568546b827fc5778a20efb347bf56ba34ac
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -1,45 +1,174 @@
package com.ruoyi.order.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
import com.ruoyi.account.api.feignClient.AppUserCarClient;
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;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
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;
import com.ruoyi.common.core.domain.R;
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.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder;
import com.ruoyi.order.api.model.TCardAccountingStrategyOrder;
import com.ruoyi.order.api.model.TCardChargingOrder;
import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.CardBalanceDetailQuery;
import com.ruoyi.order.api.query.CardChargingOrderQuery;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
import com.ruoyi.order.api.vo.CardBalanceDetailVO;
import com.ruoyi.order.api.vo.ChargingOrderVO;
import com.ruoyi.order.api.vo.DeviceStartChargingVO;
import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
import com.ruoyi.order.mapper.TCardChargingOrderMapper;
import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TCardAccountingStrategyOrderService;
import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TCardChargingOrderService;
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;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
public class TCardChargingOrderServiceImpl extends ServiceImpl<TCardChargingOrderMapper, TCardChargingOrder>
        implements TCardChargingOrderService {
    private static final Logger log = LoggerFactory.getLogger(TCardChargingOrderServiceImpl.class);
    /** SD 订单号前缀(设备刷卡充电) */
    private static final String ORDER_CODE_PREFIX_SD = "SD";
    /** 订单状态:启动中 */
    private static final int ORDER_STATUS_STARTING = 2;
    /** 订单状态:充电中 */
    private static final int ORDER_STATUS_CHARGING = 3;
    /** 订单状态:停止中 */
    private static final int ORDER_STATUS_STOPPING = 4;
    /** 订单状态:已结束 */
    private static final int ORDER_STATUS_FINISHED = 5;
    /** 订单状态:启动失败(沿用 t_charging_order 枚举约定:-1 为负值语义,前端按 status<0 渲染) */
    private static final int ORDER_STATUS_START_FAILED = -1;
    /** 充电枪状态:充电中 */
    private static final int GUN_STATUS_CHARGING = 4;
    /** 充电枪状态:空闲 */
    private static final int GUN_STATUS_IDLE = 2;
    /** SD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
    private static final String LOCK_SD_START_GUN = "SD_START_GUN:";
    /** SD 建单卡锁前缀(卡ID维度,防同卡多枪并发启动,文档13.2) */
    private static final String LOCK_SD_START_CARD = "SD_START_CARD:";
    /** SD 结算锁前缀(订单号维度,防并发重复结算/扣款,文档13.3) */
    private static final String LOCK_SD_SETTLE = "SD_SETTLE:";
    /** SD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
    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;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private SiteClient siteClient;
    @Resource
    private ChargingPileClient chargingPileClient;
    @Resource
    private ChargingGunClient chargingGunClient;
    @Resource
    private AppUserCarClient appUserCarClient;
    @Resource
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private ChargingCardClient chargingCardClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private ChargeAuthorizationClient chargeAuthorizationClient;
    @Resource
    private ChargeAccountSupport chargeAccountSupport;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private AccountingStrategyDetailClient accountingStrategyDetailClient;
    @Resource
    private TCardAccountingStrategyOrderService cardAccountingStrategyOrderService;
    @Resource
    private TCardAccountingStrategyDetailOrderService cardAccountingStrategyDetailOrderService;
    @Resource
    private TCardChargingOrderAccountingStrategyService cardChargingOrderAccountingStrategyService;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private RedissonClient redissonClient;
    @Override
    public PageInfo<ChargingOrderVO> pageList(CardChargingOrderQuery query) {
@@ -66,9 +195,167 @@
        if (result == null) {
            result = new HashMap<>();
        }
        result.putIfAbsent("platformIncome", BigDecimal.ZERO);
        result.putIfAbsent("partnerIncome", BigDecimal.ZERO);
        BigDecimal platformIncome = BigDecimal.ZERO;
        BigDecimal partnerIncome = BigDecimal.ZERO;
        Map<String, BigDecimal> accountingSummary = emptyAccountingSummary();
        List<Long> orderIds = baseMapper.summaryOrderIds(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        if (orderIds != null && !orderIds.isEmpty()) {
            int batchSize = 500;
            for (int i = 0; i < orderIds.size(); i += batchSize) {
                List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
                Map<String, Object> shareSummary = baseMapper.shareIncomeSummaryByOrderIds(batchIds);
                platformIncome = platformIncome.add(summaryDecimal(shareSummary, "platformIncome"));
                partnerIncome = partnerIncome.add(summaryDecimal(shareSummary, "partnerIncome"));
                mergeAccountingSummary(accountingSummary, baseMapper.accountingSummaryByOrderIds(batchIds));
            }
        }
        result.put("platformIncome", platformIncome);
        result.put("partnerIncome", partnerIncome);
        result.put("platformIncomeTotal", platformIncome);
        result.put("partnerIncomeTotal", partnerIncome);
        result.put("capacityProportion", buildProportion(accountingSummary, "capacity"));
        result.put("serviceProportion", buildProportion(accountingSummary, "service"));
        result.put("electronicProportion", buildProportion(accountingSummary, "electric"));
        return result;
    }
    private Map<String, BigDecimal> emptyAccountingSummary() {
        Map<String, BigDecimal> summary = new HashMap<>();
        for (String prefix : new String[]{"capacity", "service", "electric"}) {
            summary.put(prefix + "Total", BigDecimal.ZERO);
            for (int type = 1; type <= 4; type++) {
                summary.put(prefix + type, BigDecimal.ZERO);
            }
        }
        return summary;
    }
    private void mergeAccountingSummary(Map<String, BigDecimal> total, Map<String, Object> batch) {
        if (batch == null) {
            return;
        }
        for (String key : total.keySet()) {
            total.put(key, total.get(key).add(summaryDecimal(batch, key)));
        }
    }
    private BigDecimal summaryDecimal(Map<String, Object> summary, String key) {
        if (summary == null || summary.get(key) == null) {
            return BigDecimal.ZERO;
        }
        Object value = summary.get(key);
        return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
    }
    private String buildProportion(Map<String, BigDecimal> summary, String prefix) {
        BigDecimal denominator = summary.get(prefix + "Total");
        List<String> proportions = new ArrayList<>(4);
        for (int type = 1; type <= 4; type++) {
            BigDecimal percentage = denominator.signum() == 0 ? BigDecimal.ZERO
                    : summary.get(prefix + type).multiply(new BigDecimal("100"))
                    .divide(denominator, 2, RoundingMode.HALF_UP).stripTrailingZeros();
            proportions.add(percentage.toPlainString() + "%");
        }
        return String.join("-", proportions);
    }
    @Override
    public List<ChargingOrderVO> exportList(CardChargingOrderQuery query) {
        String[] timeRange = parseTimeRange(query);
        setAuthSiteIds(query);
        long total = baseMapper.count(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        List<ChargingOrderVO> result = new ArrayList<>();
        if (total == 0) {
            return result;
        }
        int batchSize = 5000;
        for (long offset = 0; offset < total; offset += batchSize) {
            List<ChargingOrderVO> batch = baseMapper.pageList(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3], offset, batchSize);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        fillPageExtra(result);
        return result;
    }
    @Override
    public Map<String, Object> detail(Long chargingOrderId) {
        TCardChargingOrder order = getById(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", order.getId());
        result.put("uid", String.valueOf(order.getId()));
        result.put("code", order.getCode());
        result.put("cardNo", order.getCardNo());
        result.put("status", order.getStatus());
        result.put("siteId", order.getSiteId());
        result.put("chargingPileId", order.getChargingPileId());
        result.put("chargingGunId", order.getChargingGunId());
        result.put("startTime", order.getStartTime());
        result.put("endTime", order.getEndTime());
        result.put("chargingCapacity", order.getChargingCapacity());
        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());
        result.put("maxEndTime", order.getMaxEndTime());
        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", "-");
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<UploadRealTimeMonitoringData> monitoring = order.getCode() == null || order.getCode().trim().isEmpty()
                ? Collections.emptyList()
                : uploadRealTimeMonitoringDataService.getDataByOrderCodeAsc(order.getCode());
        if (monitoring == null) {
            monitoring = Collections.emptyList();
        }
        for (UploadRealTimeMonitoringData data : monitoring) {
            if (data.getOutput_current() != null && data.getOutput_voltage() != null) {
                data.setPower(data.getOutput_voltage().multiply(data.getOutput_current()).divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP));
            }
        }
        result.put("list", monitoring);
        UploadRealTimeMonitoringData latest = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
        result.put("cdElectronic", latest == null || latest.getOutput_current() == null ? "" : latest.getOutput_current().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("cdVoltage", latest == null || latest.getOutput_voltage() == null ? "" : latest.getOutput_voltage().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("totalPower", latest == null || latest.getPower() == null ? "" : latest.getPower().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("surplus", latest == null || latest.getSoc() == null ? "" : String.valueOf(latest.getSoc()));
        result.put("list1", baseMapper.accountingStrategyList(order.getId()));
        List<Integer> siteIds = order.getSiteId() == null ? Collections.emptyList() : Collections.singletonList(order.getSiteId());
        if (!siteIds.isEmpty()) {
            List<Site> sites = Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList());
            if (!sites.isEmpty()) {
                result.put("siteName", sites.get(0).getName());
                result.put("siteAddress", sites.get(0).getAddress());
            }
        }
        if (order.getChargingGunId() != null) {
            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
            if (gun != null) {
                result.put("gunCode", gun.getCode());
                result.put("terminalName", gun.getName());
            }
        }
        return result;
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    @Override
@@ -79,6 +366,56 @@
    @Override
    public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
        return baseMapper.accountingStrategyList(chargingOrderId);
    }
    @Override
    public PageInfo<CardBalanceDetailVO> balanceDetailPage(CardBalanceDetailQuery query) {
        int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize();
        setAuthSiteIds(query);
        PageInfo<CardBalanceDetailVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        long total = baseMapper.balanceDetailCount(query);
        pageInfo.setTotal(total);
        if (total == 0) {
            pageInfo.setRecords(Collections.emptyList());
            return pageInfo;
        }
        long offset = (long) (pageCurr - 1) * pageSize;
        List<CardBalanceDetailVO> list = baseMapper.balanceDetailList(query, offset, pageSize);
        pageInfo.setRecords(list);
        return pageInfo;
    }
    @Override
    public Map<String, Object> balanceDetailSummary(CardBalanceDetailQuery query) {
        setAuthSiteIds(query);
        Map<String, Object> result = baseMapper.balanceDetailSummary(query);
        if (result == null) {
            result = new HashMap<>();
        }
        result.putIfAbsent("orderCount", 0);
        result.putIfAbsent("incomeAmount", BigDecimal.ZERO);
        result.putIfAbsent("expenseAmount", BigDecimal.ZERO);
        return result;
    }
    @Override
    public List<CardBalanceDetailVO> balanceDetailExport(CardBalanceDetailQuery query) {
        setAuthSiteIds(query);
        long total = baseMapper.balanceDetailCount(query);
        if (total == 0) {
            return new ArrayList<>();
        }
        List<CardBalanceDetailVO> result = new ArrayList<>();
        int batchSize = 5000;
        for (long offset = 0; offset < total; offset += batchSize) {
            List<CardBalanceDetailVO> batch = baseMapper.balanceDetailList(query, offset, batchSize);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        return result;
    }
    private void setAuthSiteIds(CardChargingOrderQuery query) {
@@ -98,12 +435,58 @@
        query.setSiteIds(siteIds);
    }
    private void setAuthSiteIds(CardBalanceDetailQuery query) {
        List<Integer> siteIds = new ArrayList<>();
        Long userId = tokenService.getLoginUser().getUserid();
        if (userId != null) {
            siteIds = Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()).orElse(Collections.emptyList())
                    .stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
        }
        if (siteIds.isEmpty()) {
            siteIds.add(-1);
        }
        query.setSiteIds(siteIds);
    }
    private void fillElectronicProportion(List<ChargingOrderVO> list) {
        List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
        if (orderIds.isEmpty()) {
            return;
        }
        List<Map<String, Object>> capacityRows = new ArrayList<>();
        int batchSize = 500;
        for (int i = 0; i < orderIds.size(); i += batchSize) {
            List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
            capacityRows.addAll(Optional.ofNullable(baseMapper.capacitySummaryByOrderIds(batchIds))
                    .orElse(Collections.emptyList()));
        }
        Map<Long, Map<String, Object>> capacityByOrderId = capacityRows.stream()
                .filter(item -> item.get("orderId") != null)
                .collect(Collectors.toMap(item -> Long.valueOf(item.get("orderId").toString()), item -> item, (a, b) -> a));
        for (ChargingOrderVO order : list) {
            Map<String, Object> capacity = capacityByOrderId.get(order.getId());
            Map<String, BigDecimal> summary = emptyAccountingSummary();
            if (capacity != null) {
                for (String key : new String[]{"capacityTotal", "capacity1", "capacity2", "capacity3", "capacity4"}) {
                    summary.put(key, summaryDecimal(capacity, key));
                }
            }
            order.setElectronicProportion(buildProportion(summary, "capacity"));
        }
    }
    private void fillPageExtra(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            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());
        Map<Long, TAppUserCar> cars = carIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(appUserCarClient.getCarByIds(carIds).getData()).orElse(Collections.emptyList()).stream()
                        .collect(Collectors.toMap(TAppUserCar::getId, item -> item, (a, b) -> a));
        Map<Integer, Site> sites = Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        Map<Integer, TChargingGun> guns = gunIds.isEmpty() ? Collections.emptyMap()
@@ -115,15 +498,1303 @@
            item.setSiteName(site == null ? null : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setTerminalName(gun == null ? null : gun.getName());
            TAppUserCar car = cars.get(item.getAppUserCarId());
            item.setVin(car == null ? null : car.getVin());
            if (item.getStartTime() != null && item.getEndTime() != null) {
                item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime()));
            }
            UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getLastDataById(item.getCode());
            if (monitoringData != null) {
                item.setChargeCurrent(monitoringData.getOutput_current());
                item.setChargeVoltage(monitoringData.getOutput_voltage());
                item.setChargePower(monitoringData.getPower());
            }
            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) {
        return new String[]{query.getStartTime1(), query.getStartTime2(), query.getEndTime1(), query.getEndTime2()};
    }
    // ===== 设备侧刷卡充电链路(V0.2 刷卡充电) =====
    @Override
    public DeviceStartChargingVO startCardCharging(DeviceStartChargingDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
            return DeviceStartChargingVO.fail(1);
        }
        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
        RLock gunLock = redissonClient.getLock(LOCK_SD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
        try {
            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
                return DeviceStartChargingVO.fail(10);
            }
            // 1. 充电枪校验(不存在->5 桩停用;充电中->10 该桩存在未结账记录)
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(dto.getChargingPileCode());
            gunQuery.setCharging_gun_code(dto.getChargingGunCode());
            TChargingGun gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
            if (gun == null) {
                return DeviceStartChargingVO.fail(5);
            }
            if (gun.getStatus() != null && gun.getStatus() == GUN_STATUS_CHARGING) {
                return DeviceStartChargingVO.fail(10);
            }
            TChargingPile pile = gun.getChargingPileId() == null ? null
                    : chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData();
            Site site = gun.getSiteId() == null ? null
                    : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(gun.getSiteId())).getData())
                            .orElse(Collections.emptyList()).stream().findFirst().orElse(null);
            // 2. 卡片鉴权(卡号存在性/禁用/密码条件校验在账户服务内完成)
            ChargingCardAuthDTO authDTO = new ChargingCardAuthDTO();
            authDTO.setCardNo(dto.getAccount().trim());
            authDTO.setPassword(dto.getPassword());
            R<TChargingCard> verifyResult = chargingCardClient.verifyCharging(authDTO);
            TChargingCard card = verifyResult.getData();
            if (card == null) {
                return DeviceStartChargingVO.fail(mapAuthFailure(verifyResult.getMsg()));
            }
            // 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维度锁,防同卡多枪并发建单(数据库锁卡条件更新为第二道保护)
            RLock cardLock = redissonClient.getLock(LOCK_SD_START_CARD + card.getId());
            boolean cardLocked = false;
            try {
                cardLocked = cardLock.tryLock(0, 30, TimeUnit.SECONDS);
                if (!cardLocked) {
                    return DeviceStartChargingVO.fail(4);
                }
                // 4. 建单(状态2 启动中)
                TCardChargingOrder order = new TCardChargingOrder();
                order.setCode(ORDER_CODE_PREFIX_SD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
                order.setOrderType(2);
                order.setOrderSource(1);
                order.setOrderClassification(2);
                // 充电支付模式(沿用 CD payMethod 枚举:1=余额支付):SD 全程卡余额扣款
                order.setPayMethod(1);
                order.setStatus(ORDER_STATUS_STARTING);
                order.setTitle("【充电桩充电】" + (pile != null && pile.getNumber() != null ? pile.getNumber() : dto.getChargingPileCode())
                        + "号桩/" + dto.getChargingGunCode() + "号枪");
                order.setSiteId(gun.getSiteId());
                order.setChargingPileId(gun.getChargingPileId());
                order.setChargingGunId(gun.getId());
                order.setCardId(card.getId());
                order.setCardNo(card.getCardNo());
                // 所属公司/车队快照落库(充电卡当前归属),供订单列表/汇总/导出按公司、车队直接筛选
                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. 计费策略快照(枪策略优先,站点策略兜底,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)) {
                    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());
                    unlockCardChargeContextQuietly(order, "锁卡失败");
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(4);
                }
                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();
                }
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[SD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
            return DeviceStartChargingVO.fail(null);
        } catch (Exception e) {
            log.error("[SD建单] 刷卡充电启动异常 pile={} gun={} account={}",
                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
            return DeviceStartChargingVO.fail(null);
        } finally {
            if (gunLock.isHeldByCurrentThread()) {
                gunLock.unlock();
            }
        }
    }
    /**
     * 刷卡充电账户上下文解析(与 VIN 链路同构):
     * 1) 企业状态校验(未归属/不存在/停用 -> 失败码2),账户模式取企业配置(默认共享);
     * 2) 企业充电授权校验:站点范围(超范围 -> 失败码2)、可用时段(不在时段 -> 失败码2),未配置授权按放行处理;
     * 3) 可用余额校验(共享=企业可用余额,分配=卡可用余额,<=0 -> 失败码3);
     * 4) 下发金额 = min(授权下发额, 可用余额),锁定额度与下发金额一致;授权截止时间用于监控提前停充。
     */
    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 = 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 false;
        }
        TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setChargingOrderId(orderId);
        boolean mainSaved = cardAccountingStrategyOrderService.save(strategyOrder);
        List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
        for (TAccountingStrategyDetail detail : details) {
            TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
            BeanUtils.copyProperties(detail, detailOrder);
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        return mainSaved && detailSaved;
    }
    /**
     * 账户服务鉴权失败消息 -> 协议失败原因码(1=账户不存在 2=账户冻结 7=密码错误)
     */
    private Integer mapAuthFailure(String msg) {
        if (StringUtils.isNotEmpty(msg)) {
            if (msg.contains("密码")) {
                return 7;
            }
            if (msg.contains("禁用") || msg.contains("冻结")) {
                return 2;
            }
            if (msg.contains("不存在")) {
                return 1;
            }
        }
        return 1;
    }
    @Override
    public TCardChargingOrder getCardOrderByCode(String code) {
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardChargingOrder>()
                .eq(TCardChargingOrder::getCode, code)
                .last("limit 1"));
    }
    @Override
    public void cardStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(vo.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只有启动中订单可消费启动应答,防止迟到消息复活已关闭/已结束订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) {
            return;
        }
        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
            boolean updated = lambdaUpdate()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                    .set(TCardChargingOrder::getStartTime, LocalDateTime.now())
                    .update();
            if (!updated) {
                return;
            }
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功");
            log.info("[SD启动] 刷卡订单进入充电中 code={}", order.getCode());
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
        } else {
            closeTimeoutStartingOrder(order);
            log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
        }
    }
    @Override
    public boolean handleHandshakeVin(String orderCode, String vin) {
        if (StringUtils.isEmpty(orderCode)) {
            return false;
        }
        TCardChargingOrder order = getCardOrderByCode(orderCode);
        if (order == null) {
            log.warn("[SD握手] 订单不存在 code={}", orderCode);
            return false;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        boolean needUpdate = false;
        if (StringUtils.isNotEmpty(vin)) {
            update.setActualVin(vin);
            needUpdate = true;
        }
        boolean advanced = order.getStatus() != null && order.getStatus() == ORDER_STATUS_STARTING;
        if (advanced) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
            needUpdate = true;
        }
        if (!needUpdate) {
            return true;
        }
        boolean updated;
        if (advanced) {
            updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING));
        } else {
            updated = updateById(update);
        }
        if (advanced && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "握手恢复");
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
            log.info("[SD握手] 握手推进订单进入充电中 code={}", orderCode);
        }
        return updated;
    }
    @Override
    public void cardChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(query.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只接收启动中/充电中的监控数据,禁止迟到数据复活终态订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())
                && !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());
        if (starting) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
        }
        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
                && query.getOutput_voltage() != null) {
            BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage())
                    .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            update.setChargingPower(power);
            update.setCurrent(query.getOutput_current());
            update.setVoltage(query.getOutput_voltage());
            update.setPower(power);
            update.setNeedElec(query.getOutput_current());
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setChargingPower(power);
            chargingGunClient.updateChargingGunById(gun);
        }
        if (query.getCharging_degree() != null) {
            update.setChargingCapacity(query.getCharging_degree());
            update.setElectricity(query.getCharging_degree());
        }
        // 剩余金额口径与 CD 一致:可用金额基数(建单写入 charge_amount) - 设备已充金额
        if (query.getPaid_amount() != null && order.getChargeAmount() != null) {
            update.setResidualAmount(order.getChargeAmount().subtract(query.getPaid_amount())
                    .setScale(2, RoundingMode.HALF_EVEN));
        }
        boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>()
                .eq(TCardChargingOrder::getId, order.getId())
                .in(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING));
        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
    public void cardEndCharge(String orderCode, Integer endMode) {
        if (StringUtils.isEmpty(orderCode)) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(orderCode);
        if (order == null) {
            return;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        update.setEndMode(endMode);
        updateById(update);
        pushRegulatoryAsync(order, false, false);
    }
    @Override
    public R cardEndChargeBillingCharge(TransactionRecordMessageVO vo) {
        String code = vo == null ? null : vo.getTransaction_serial_number();
        log.info("[SD结算] 收到交易记录 code={}", code);
        if (StringUtils.isEmpty(code)) {
            return R.fail("交易记录缺少交易流水号");
        }
        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等与扣款唯一键两层兜底
        RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + code);
        try {
            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.warn("[SD结算] 并发结算进行中,跳过重复账单 code={}", code);
                return R.ok("重复账单,结算处理中");
            }
            return doCardEndChargeBillingCharge(vo, code);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return R.fail("结算锁获取被中断:" + code);
        } finally {
            if (settleLock.isHeldByCurrentThread()) {
                settleLock.unlock();
            }
        }
    }
    /**
     * SD 结算实际执行体(调用前须持有订单号结算锁)
     */
    private R doCardEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
        TCardChargingOrder order = getCardOrderByCode(code);
        if (order == null) {
            return R.fail("刷卡充电订单不存在:" + code);
        }
        // 幂等:已结算订单直接返回成功(重复账单/重复推送)
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
            log.warn("[SD结算] 重复账单跳过 code={}", code);
            return R.ok("重复账单,已按首次账单结算");
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
            return R.ok("订单已关闭,跳过结算");
        }
        List<TransactionStageParser.PeriodElectricity> reportedPeriods =
                TransactionStageParser.parsePeriodElectricity(vo);
        BigDecimal totalElectricity = reportedPeriods.stream()
                .map(TransactionStageParser.PeriodElectricity::getElectricity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
            totalElectricity = vo.getTotal_electricity();
        }
        List<TCardAccountingStrategyDetailOrder> snapshots = new ArrayList<>(cardAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list());
        snapshots.removeIf(item -> item.getType() == null);
        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++) {
            TCardAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex);
            BigDecimal capacity = TransactionStageParser.getPeriodElectricity(
                    vo, snapshot.getType(), snapshotIndex + 1);
            if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
            BigDecimal originalServicePrice = snapshot.getCostServiceCharge() == null
                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
            // 原价金额(策略单价),优惠金额 = 原价 - 最终
            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(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());
                // 时段位:与 CD 充电 TaskUtil.endOrder 同口径,截取快照时段的 HH:mm
                // (snapshot 已为 HH:mm;split 兜底兼容上游拼接的 yyyy-MM-dd HH:mm 情形)
                detail.setStartTime(splitPeriodHourMinute(snapshot.getStartTime()));
                detail.setEndTime(splitPeriodHourMinute(snapshot.getEndTime()));
            }
            // 首次添加时间:结算落库时刻(CD结算同口径)
            detail.setCreateTime(LocalDateTime.now());
            details.add(detail);
        }
        if (snapshots.isEmpty()) {
            log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
        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");
            if (settlementStartTime != null) {
                details.get(0).setStartTime(settlementStartTime.format(hhmm));
            }
            if (settlementEndTime != null) {
                details.get(details.size() - 1).setEndTime(settlementEndTime.format(hhmm));
            }
        }
        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
        // lambda 引用要求有效final,拷贝最终值
        final BigDecimal totalElec = totalElectricity;
        final BigDecimal elecTotal = electrovalenceTotal;
        final BigDecimal svcTotal = serviceChargeTotal;
        final BigDecimal discountElecTotal = discountElectricTotal;
        final BigDecimal discountSvcTotal = discountServiceTotal;
        // 剩余金额口径:可用金额基数(建单写入的 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 -> {
            cardChargingOrderAccountingStrategyService.lambdaUpdate()
                    .eq(TCardChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
                    .remove();
            if (!details.isEmpty()) {
                cardChargingOrderAccountingStrategyService.saveBatch(details);
            }
            TCardChargingOrder update = new TCardChargingOrder();
            update.setId(order.getId());
            update.setTotalElectricity(totalElec);
            update.setChargingCapacity(totalElec);
            update.setElectrovalence(elecTotal);
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            // 优惠合计(原始电费/服务费 - 最终),与 VIN 链路同口径
            update.setDiscountElectrovalence(discountElecTotal);
            update.setDiscountServiceCharge(discountSvcTotal);
            // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示)
            update.setBalancePayAmount(orderAmount);
            update.setChargeAmount(chargeBase);
            update.setResidualAmount(residualAmount);
            update.setEndTime(parseDeviceTime(vo.getEnd_time(), LocalDateTime.now()));
            if (order.getStartTime() == null) {
                update.setStartTime(parseDeviceTime(vo.getStart_time(), null));
            }
            update.setStatus(ORDER_STATUS_FINISHED);
            if (order.getEndMode() == null) {
                update.setEndMode(1);
            }
            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;
        });
        // 回写内存订单对象,保证结算后监管推送/日志使用最新金额字段
        order.setChargeAmount(chargeBase);
        order.setResidualAmount(residualAmount);
        order.setBalancePayAmount(orderAmount);
        order.setOrderAmount(orderAmount);
        order.setPaymentAmount(orderAmount);
        order.setDiscountElectrovalence(discountElectricTotal);
        order.setDiscountServiceCharge(discountServiceTotal);
        // 账户结算(实扣冻结口径):解锁申请时锁定的额度、按实际消费扣款并退回差额,同时写分配记录消费流水;
        // 无法确认扣款成功时回退状态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={} discountElec={} discountSvc={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal,
                discountServiceTotal);
        return R.ok();
    }
    /**
     * 结算刷卡账户资金(实扣冻结口径,与 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;
    }
    /**
     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
     */
    private void pushRegulatoryAsync(TCardChargingOrder order, boolean pushOrderStatus, boolean pushOrderInfo) {
        if (chargingMessageClient == null && chengDuJianGuanClient == null) {
            return;
        }
        new Thread(() -> {
            try {
                if (pushOrderStatus && chargingMessageClient != null) {
                    ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
                    // 与 CD 一致使用 Spring BeanUtils(本类 ruoyi BeanUtils 同名,故用全限定名)
                    org.springframework.beans.BeanUtils.copyProperties(order, chargingOrderVo);
                    chargingMessageClient.pushOrderStatus(chargingOrderVo);
                }
                if (chengDuJianGuanClient != null) {
                    NotificationConnectorChargeStatusVo statusVo = new NotificationConnectorChargeStatusVo();
                    statusVo.setOrderCode(order.getCode());
                    statusVo.setChargingGunId(order.getChargingGunId());
                    chengDuJianGuanClient.notificationConnectorChargeStatus(statusVo);
                    if (pushOrderInfo) {
                        chengDuJianGuanClient.notificationOrderInfo(statusVo);
                    }
                }
            } catch (Exception ex) {
                log.error("[SD监管] 推送监管平台失败 code={}", order.getCode(), ex);
            }
        }, "SD-Regulatory-Push").start();
    }
    private boolean unlockCardQuietly(TCardChargingOrder order) {
        if (order.getCardId() == null) {
            return true;
        }
        try {
            R<Boolean> result = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode());
            boolean success = result != null && result.getCode() == R.SUCCESS && Boolean.TRUE.equals(result.getData());
            if (!success) {
                log.error("[SD资源] 解锁卡片失败 code={} cardId={} msg={}", order.getCode(), order.getCardId(),
                        result == null ? "null" : result.getMsg());
            }
            return success;
        } catch (Exception ex) {
            log.error("[SD资源] 解锁卡片异常 code={} cardId={}", order.getCode(), order.getCardId(), ex);
            return false;
        }
    }
    private void resetGunQuietly(TCardChargingOrder order) {
        updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位");
    }
    private boolean updateGunStatusQuietly(TCardChargingOrder order, int status, String scene) {
        if (order.getChargingGunId() == null) {
            return true;
        }
        try {
            R<TChargingGun> queryResult = chargingGunClient.getChargingGunById(order.getChargingGunId());
            TChargingGun gun = queryResult == null ? null : queryResult.getData();
            if (queryResult == null || queryResult.getCode() != R.SUCCESS || gun == null) {
                log.error("[SD资源] {}查询充电枪失败 code={} gunId={}", scene, order.getCode(), order.getChargingGunId());
                return false;
            }
            gun.setStatus(status);
            R<String> result = chargingGunClient.updateChargingGunById(gun);
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[SD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(),
                        order.getChargingGunId(), result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[SD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex);
            return false;
        }
    }
    private LocalDateTime parseDeviceTime(String value, LocalDateTime fallback) {
        if (StringUtils.isEmpty(value)) {
            return fallback;
        }
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendPattern("yyyy-MM-dd HH:mm:ss")
                .optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd()
                .toFormatter();
        try {
            return LocalDateTime.parse(value.trim().replace('T', ' '), formatter);
        } catch (DateTimeParseException ex) {
            log.warn("[SD结算] 设备时间格式异常 value={}", value);
            return fallback;
        }
    }
    /**
     * 截取快照时段位 HH:mm(对齐 CD 充电 TaskUtil.endOrder 中 detailOrder.getStartTime().split(" ")[1]):
     * 1) snapshot 已为 HH:mm 直接返回;
     * 2) 若被上游拼接为 yyyy-MM-dd HH:mm:ss / yyyy-MM-dd HH:mm 等带日期格式,按空格 split 取末段。
     */
    private static String splitPeriodHourMinute(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty()) {
            return trimmed;
        }
        int spaceIdx = trimmed.lastIndexOf(' ');
        return spaceIdx < 0 ? trimmed : trimmed.substring(spaceIdx + 1);
    }
    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
    @Override
    public List<TCardChargingOrder> listTimeoutStartingOrders() {
        return lambdaQuery()
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(SD_STARTING_TIMEOUT_MINUTES))
                .list();
    }
    @Override
    public List<TCardChargingOrder> listTimeoutChargingOrders() {
        return lambdaQuery()
                .in(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .list();
    }
    @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);
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>()
                .eq(TCardChargingOrder::getId, order.getId())
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING));
        if (!closed) {
            log.info("[SD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode());
            return;
        }
        if (!unlockCardQuietly(order)) {
            // 解锁失败恢复为启动中,下轮继续补偿,避免卡片永久占用
            lambdaUpdate().eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getStatus, ORDER_STATUS_START_FAILED)
                    .set(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TCardChargingOrder::getEndMode, null)
                    .set(TCardChargingOrder::getEndTime, null)
                    .update();
            return;
        }
        // 释放申请时锁定的账户额度(未产生消费;失败仅告警,由人工/后续补偿核对)
        unlockCardChargeContextQuietly(order, "启动关单");
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[SD补偿] 启动超时关单 code={} cardId={} gunId={}", order.getCode(), order.getCardId(), order.getChargingGunId());
    }
    @Override
    public void compensationSettle(TCardChargingOrder order) {
        // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
        // (结算锁/状态幂等/扣款幂等/解锁/复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
        BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
                : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
        int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId());
        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
        vo.setTransaction_serial_number(order.getCode());
        TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity);
        vo.setTotal_electricity(capacity);
        log.warn("[SD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
                order.getCode(), flatPeriodIndex, capacity);
        cardEndChargeBillingCharge(vo);
    }
    private int resolveCompensationFlatPeriodIndex(Long orderId) {
        List<TCardAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(cardAccountingStrategyDetailOrderService
                        .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
                .orElse(Collections.emptyList()).stream()
                .filter(item -> item.getType() != null)
                .sorted(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
                        Comparator.nullsLast(String::compareTo)))
                .collect(Collectors.toList());
        for (int i = 0; i < snapshots.size(); i++) {
            if (Integer.valueOf(3).equals(snapshots.get(i).getType())) {
                return i + 1;
            }
        }
        log.error("[SD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
        return 1;
    }
}