puzhibing
2026-09-14 34e836924716843f64a6c68442e5fb297d0e25e4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -1,46 +1,125 @@
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.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.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
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.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 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.function.Function;
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;
    @Resource
    private TokenService tokenService;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private SiteClient siteClient;
    @Resource
@@ -51,6 +130,22 @@
    private AppUserCarClient appUserCarClient;
    @Resource
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private ChargingCardClient chargingCardClient;
    @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) {
@@ -193,7 +288,9 @@
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<UploadRealTimeMonitoringData> monitoring = uploadRealTimeMonitoringDataService.getDataByOrderCodeAsc(order.getCode());
        List<UploadRealTimeMonitoringData> monitoring = order.getCode() == null || order.getCode().trim().isEmpty()
                ? Collections.emptyList()
                : uploadRealTimeMonitoringDataService.getDataByOrderCodeAsc(order.getCode());
        if (monitoring == null) {
            monitoring = Collections.emptyList();
        }
@@ -203,6 +300,11 @@
            }
        }
        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()) {
@@ -384,4 +486,685 @@
    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. 启动余额校验(余额>1 元方可启动,与 CD 费用不足终止阈值对齐;结算允许扣成负余额)
            if (card.getBalance() == null || card.getBalance().compareTo(BigDecimal.ONE) <= 0) {
                return DeviceStartChargingVO.fail(3);
            }
            // 并发防护(文档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());
                // 可用金额基数与启动剩余金额(与返回设备的 balance 同源,供监控/结算扣减与前端展示)
                order.setChargeAmount(card.getBalance());
                order.setResidualAmount(card.getBalance());
                order.setCreateTime(LocalDateTime.now());
                save(order);
                // 5. 计费策略快照(枪策略优先,站点策略兜底;无策略照常建单,结算按0价并告警)
                Integer strategyId = gun.getAccountingStrategyId() != null
                        ? gun.getAccountingStrategyId()
                        : (site == null ? null : site.getAccountingStrategyId());
                try {
                    saveStrategySnapshot(order.getId(), strategyId);
                } catch (Exception snapshotEx) {
                    log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
                }
                // 6. 锁卡(条件更新:仅空闲卡可锁;失败回滚删除订单与快照)
                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());
                    return DeviceStartChargingVO.fail(4);
                }
                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={}", order.getCode(), card.getCardNo(), dto.getChargingGunCode());
                return DeviceStartChargingVO.success(order.getCode(), card.getBalance());
            } 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();
            }
        }
    }
    /**
     * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
     */
    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
            return;
        }
        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
        if (strategy == null || details == null || details.isEmpty()) {
            return;
        }
        TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setChargingOrderId(orderId);
        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);
        }
        cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
    }
    /**
     * 账户服务鉴权失败消息 -> 协议失败原因码(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;
        }
        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, "实时数据恢复");
        }
    }
    @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("订单已关闭,跳过结算");
        }
        // 分时电量(以平台快照计价,V0.2 定稿)
        Map<Integer, BigDecimal> stageElectricity = TransactionStageParser.parseStageElectricity(vo);
        BigDecimal totalElectricity = stageElectricity.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
            totalElectricity = vo.getTotal_electricity();
        }
        // 快照明细(1=尖 2=峰 3=平 4=谷)
        Map<Integer, TCardAccountingStrategyDetailOrder> snapshotByType = cardAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list().stream()
                .filter(item -> item.getType() != null)
                .collect(Collectors.toMap(TCardAccountingStrategyDetailOrder::getType, Function.identity(), (a, b) -> a));
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        List<TCardChargingOrderAccountingStrategy> details = new ArrayList<>();
        for (Map.Entry<Integer, BigDecimal> stage : stageElectricity.entrySet()) {
            TCardAccountingStrategyDetailOrder snapshot = snapshotByType.get(stage.getKey());
            BigDecimal capacity = stage.getValue();
            BigDecimal electricPrice = snapshot == null || snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
            BigDecimal servicePrice = snapshot == null || snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
            BigDecimal originalServicePrice = snapshot == null || 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 originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
            TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
            detail.setChargingOrderId(order.getId());
            detail.setType(stage.getKey());
            detail.setElectrovalence(electricPrice);
            detail.setServiceCharge(servicePrice);
            detail.setCostServiceCharge(originalServicePrice);
            detail.setChargingCapacity(capacity);
            detail.setPeriodElectricPrice(electricAmount);
            detail.setPeriodServicePrice(serviceAmount);
            detail.setPeriodOriginalServicePrice(originalServiceAmount);
            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 (snapshotByType.isEmpty()) {
            log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        // 与 CD 充电 TaskUtil.endOrder 同口径:details 按 type 升序对齐业务时段顺序,
        // 首段 start_time 用订单实际开始时间的 HH:mm 覆盖(对应 CD 的时段内首条监控数据 HH:mm),
        // 末段 end_time 用订单实际结束时间的 HH:mm 覆盖(对应 CD 的时段内末条监控数据 HH:mm)。
        // 若 order.startTime/endTime 为空(结算事务内才回填),则跳过覆盖以保留快照时段位兜底。
        if (!details.isEmpty()) {
            details.sort(Comparator.comparingInt(TCardChargingOrderAccountingStrategy::getType));
            DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
            if (order.getStartTime() != null) {
                details.get(0).setStartTime(order.getStartTime().format(hhmm));
            }
            if (order.getEndTime() != null) {
                details.get(details.size() - 1).setEndTime(order.getEndTime().format(hhmm));
            }
        }
        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
        // lambda 引用要求有效final,拷贝最终值
        final BigDecimal totalElec = totalElectricity;
        final BigDecimal elecTotal = electrovalenceTotal;
        final BigDecimal svcTotal = serviceChargeTotal;
        // 剩余金额口径:可用金额基数(建单写入的 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);
        // 本地事务:写计费明细 + 更新订单(状态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);
            // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示)
            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);
            }
            updateById(update);
            return true;
        });
        // 回写内存订单对象,保证结算后监管推送/日志使用最新金额字段
        order.setChargeAmount(chargeBase);
        order.setResidualAmount(residualAmount);
        order.setBalancePayAmount(orderAmount);
        order.setOrderAmount(orderAmount);
        order.setPaymentAmount(orderAmount);
        // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单)
        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());
            }
        }
        // 解锁卡片(无论扣款成败必须解锁,失败人工介入)
        unlockCardQuietly(order);
        // 复位充电枪
        resetGunQuietly(order);
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
        return R.ok();
    }
    /**
     * 异步推送监管平台(复用 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 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;
        }
        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);
        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
        vo.setTransaction_serial_number(order.getCode());
        vo.setFlat_charge1(capacity);
        vo.setTotal_electricity(capacity);
        log.warn("[SD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
        cardEndChargeBillingCharge(vo);
    }
}