puzhibing
2026-09-14 34e836924716843f64a6c68442e5fb297d0e25e4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -63,8 +63,13 @@
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;
@@ -88,6 +93,8 @@
    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 渲染) */
@@ -549,6 +556,9 @@
                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);
@@ -660,33 +670,27 @@
        if (order == null) {
            return;
        }
        // 幂等:已进入充电中/已结束的订单不再处理
        if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_CHARGING)) {
        // 只有启动中订单可消费启动应答,防止迟到消息复活已关闭/已结束订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) {
            return;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
            update.setStatus(ORDER_STATUS_CHARGING);
            update.setStartTime(LocalDateTime.now());
            updateById(update);
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setStatus(GUN_STATUS_CHARGING);
            chargingGunClient.updateChargingGunById(gun);
            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());
            // 推送监管平台(复用 CD 约定:充电中推平台订单状态 + 成都监管枪状态)
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
        } else {
            // 启动失败:订单关闭 + 解锁卡片 + 复位枪
            update.setStatus(ORDER_STATUS_START_FAILED);
            update.setEndTime(LocalDateTime.now());
            updateById(update);
            unlockCardQuietly(order);
            resetGunQuietly(order);
            closeTimeoutStartingOrder(order);
            log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
            pushRegulatoryAsync(order, false, false);
        }
    }
@@ -718,17 +722,21 @@
        if (!needUpdate) {
            return true;
        }
        updateById(update);
        boolean updated;
        if (advanced) {
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setStatus(GUN_STATUS_CHARGING);
            chargingGunClient.updateChargingGunById(gun);
            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 true;
        return updated;
    }
    @Override
@@ -740,22 +748,19 @@
        if (order == null) {
            return;
        }
        // 已结束/已关闭的订单不再更新监控数据
        if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_FINISHED)) {
        // 只接收启动中/充电中的监控数据,禁止迟到数据复活终态订单
        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());
        // 首次监控数据:启动中 -> 充电中
        if (order.getStatus() == null || order.getStatus() < ORDER_STATUS_CHARGING) {
        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());
            }
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setStatus(GUN_STATUS_CHARGING);
            chargingGunClient.updateChargingGunById(gun);
        }
        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
                && query.getOutput_voltage() != null) {
@@ -775,7 +780,17 @@
            update.setChargingCapacity(query.getCharging_degree());
            update.setElectricity(query.getCharging_degree());
        }
        updateById(update);
        // 剩余金额口径与 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
@@ -878,19 +893,43 @@
            detail.setPeriodServicePrice(serviceAmount);
            detail.setPeriodOriginalServicePrice(originalServiceAmount);
            if (snapshot != null) {
                detail.setStartTime(snapshot.getStartTime());
                detail.setEndTime(snapshot.getEndTime());
                // 计费策略明细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 -> {
@@ -908,7 +947,14 @@
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            update.setEndTime(LocalDateTime.now());
            // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示)
            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);
@@ -916,6 +962,13 @@
            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) {
@@ -976,40 +1029,84 @@
        }, "SD-Regulatory-Push").start();
    }
    /**
     * 解锁卡片(按订单号条件释放,失败仅告警不阻断结算流程)
     */
    private void unlockCardQuietly(TCardChargingOrder order) {
    private boolean unlockCardQuietly(TCardChargingOrder order) {
        if (order.getCardId() == null) {
            return;
            return true;
        }
        try {
            R<Boolean> unlockResult = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode());
            if (!Boolean.TRUE.equals(unlockResult.getData())) {
                log.error("[SD结算] 解锁卡片失败,需人工处理 code={} cardId={} msg={}",
                        order.getCode(), order.getCardId(), unlockResult.getMsg());
            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);
            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 void resetGunQuietly(TCardChargingOrder order) {
        if (order.getChargingGunId() == null) {
            return;
    private static String splitPeriodHourMinute(String value) {
        if (value == null) {
            return null;
        }
        try {
            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
            if (gun != null) {
                gun.setStatus(GUN_STATUS_IDLE);
                chargingGunClient.updateChargingGunById(gun);
            }
        } catch (Exception ex) {
            log.error("[SD结算] 复位充电枪异常 code={} gunId={}", order.getCode(), order.getChargingGunId(), ex);
        String trimmed = value.trim();
        if (trimmed.isEmpty()) {
            return trimmed;
        }
        int spaceIdx = trimmed.lastIndexOf(' ');
        return spaceIdx < 0 ? trimmed : trimmed.substring(spaceIdx + 1);
    }
    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
@@ -1025,21 +1122,33 @@
    @Override
    public List<TCardChargingOrder> listTimeoutChargingOrders() {
        return lambdaQuery()
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusHours(SD_CHARGING_TIMEOUT_HOURS))
                .in(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .list();
    }
    @Override
    public void closeTimeoutStartingOrder(TCardChargingOrder order) {
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        update.setStatus(ORDER_STATUS_START_FAILED);
        // 异常终止(沿用 CD 约定:启动失败/异常关单 endMode=0,避免前端结束方式显示空)
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        updateById(update);
        unlockCardQuietly(order);
        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());