puzhibing
2026-09-09 aa4b21bea0e3278872801183a434527ebd25c09c
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
@@ -1,46 +1,146 @@
package com.ruoyi.order.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.feignClient.VinManagementClient;
import com.ruoyi.account.api.model.TVinManagement;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
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.model.Site;
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.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
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.api.dto.DeviceStartChargingDTO;
import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
import com.ruoyi.order.api.model.TVinChargingOrder;
import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
import com.ruoyi.order.api.query.VinChargingOrderQuery;
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.PlatformStopChargingReplyVO;
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
import com.ruoyi.order.api.vo.VinChargingStatusVO;
import com.ruoyi.order.dto.MyVinChargingOrderList;
import com.ruoyi.order.mapper.TVinChargingOrderMapper;
import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TVinChargingOrderService;
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.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
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 TVinChargingOrderServiceImpl extends ServiceImpl<TVinChargingOrderMapper, TVinChargingOrder>
        implements TVinChargingOrderService {
    private static final Logger log = LoggerFactory.getLogger(TVinChargingOrderServiceImpl.class);
    /** VD 订单号前缀(设备VIN码充电) */
    private static final String ORDER_CODE_PREFIX_VD = "VD";
    /** 订单状态:启动中 */
    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;
    /** VD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
    private static final String LOCK_VD_START_GUN = "VD_START_GUN:";
    /** VD 建单VIN锁前缀(VIN ID维度,防同VIN多枪并发启动,文档13.2) */
    private static final String LOCK_VD_START_VIN = "VD_START_VIN:";
    /** VD 结算锁前缀(订单号维度,防并发重复结算,文档13.3) */
    private static final String LOCK_VD_SETTLE = "VD_SETTLE:";
    /** VD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
    private static final int VD_STARTING_TIMEOUT_MINUTES = 30;
    /** VD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
    private static final int VD_CHARGING_TIMEOUT_HOURS = 24;
    @Resource
    private TokenService tokenService;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private VinManagementClient vinManagementClient;
    @Resource
    private SiteClient siteClient;
    @Resource
    private ChargingGunClient chargingGunClient;
    @Resource
    private ChargingPileClient chargingPileClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private UploadRealTimeMonitoringDataService monitoringDataService;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private AccountingStrategyDetailClient accountingStrategyDetailClient;
    @Resource
    private TVinAccountingStrategyOrderService vinAccountingStrategyOrderService;
    @Resource
    private TVinAccountingStrategyDetailOrderService vinAccountingStrategyDetailOrderService;
    @Resource
    private TVinChargingOrderAccountingStrategyService vinChargingOrderAccountingStrategyService;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private RedissonClient redissonClient;
    @Override
    public PageInfo<ChargingOrderVO> pageList(VinChargingOrderQuery query) {
@@ -58,6 +158,81 @@
    }
    @Override
    public PageInfo<MyVinChargingOrderList> pageAppletOrders(Integer pageCurr, Integer pageSize) {
        int current = pageCurr == null || pageCurr < 1 ? 1 : pageCurr;
        int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
        PageInfo<MyVinChargingOrderList> page = new PageInfo<>(current, size);
        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        Long appUserId = tokenService.getLoginUserApplet().getUserId();
        R<List<Long>> vinIdResponse = vinManagementClient.getVinIdsByAppUserId(appUserId);
        List<Long> vinIds = vinIdResponse == null ? Collections.emptyList()
                : Optional.ofNullable(vinIdResponse.getData()).orElse(Collections.emptyList());
        if (vinIds.isEmpty()) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        long total = baseMapper.countAppletOrders(vinIds);
        page.setTotal(total);
        List<MyVinChargingOrderList> records = total == 0 ? Collections.emptyList()
                : baseMapper.pageAppletOrders(vinIds, (long) (current - 1) * size, size);
        fillAppletExtra(records);
        page.setRecords(records);
        return page;
    }
    @Override
    public Map<String, Object> appletDetail(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedAppletOrder(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", String.valueOf(order.getId()));
        result.put("chargingGunId", order.getChargingGunId());
        result.put("code", order.getCode());
        result.put("createTime", order.getCreateTime() == null ? null
                : order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("status", order.getStatus());
        result.put("licensePlate", order.getPlateNum());
        result.put("startTime", toEpochMilli(order.getStartTime()));
        result.put("endTime", toEpochMilli(order.getEndTime()));
        result.put("orderAmount", order.getOrderAmount());
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("rechargeAmount", order.getRechargeAmount());
        result.put("couponDiscountAmount", order.getCouponDiscountAmount());
        result.put("vipDiscountAmount", order.getVipDiscountAmount());
        result.put("refundAmount", order.getRefundAmount());
        result.put("electricMoney", order.getElectrovalence());
        result.put("serviceMoney", order.getServiceCharge());
        result.put("totalElectricQuantity", order.getChargingCapacity());
        result.put("stageCost", baseMapper.accountingStrategyList(order.getId()));
        result.put("actionable", 0);
        result.put("isAppeal", 0);
        Site site = getSite(order.getSiteId());
        result.put("title", site == null ? "未知电站" : site.getName());
        TChargingGun gun = getGun(order.getChargingGunId());
        result.put("chargeMode", gun == null ? null : gun.getChargeMode());
        result.put("name", gun == null ? null : gun.getName());
        fillAppletMonitoring(result, order.getCode());
        return result;
    }
    @Override
    public List<VinChargingStatusVO> getChargingVinStatus(List<Long> vinIds) {
        if (vinIds == null || vinIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> distinctVinIds = vinIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        return distinctVinIds.isEmpty() ? Collections.emptyList() : baseMapper.getChargingVinStatus(distinctVinIds);
    }
    @Override
    public List<ChargingOrderVO> exportList(VinChargingOrderQuery query) {
        setAuthSiteIds(query);
        long total = baseMapper.count(query);
@@ -70,6 +245,7 @@
            result.addAll(batch);
        }
        fillExtra(result);
        fillRealtimeData(result);
        return result;
    }
@@ -87,9 +263,9 @@
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", order.getId());
        result.put("id", order.getId() == null ? null : String.valueOf(order.getId()));
        result.put("code", order.getCode());
        result.put("vin", order.getVin());
        result.put("vin", order.getActualVin());
        result.put("actualVin", order.getActualVin());
        result.put("expectedVin", order.getExpectedVin());
        result.put("plateNum", order.getPlateNum());
@@ -104,10 +280,72 @@
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("electrovalence", order.getElectrovalence());
        result.put("serviceCharge", order.getServiceCharge());
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<Site> sites = order.getSiteId() == null ? Collections.emptyList()
                : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(order.getSiteId())).getData()).orElse(Collections.emptyList());
        Site site = sites.stream().findFirst().orElse(null);
        result.put("siteName", site == null ? null : site.getName());
        List<TChargingGun> guns = order.getChargingGunId() == null ? Collections.emptyList()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(Collections.singletonList(order.getChargingGunId())).getData()).orElse(Collections.emptyList());
        TChargingGun gun = guns.stream().findFirst().orElse(null);
        result.put("terminalName", gun == null ? null : gun.getName());
        List<UploadRealTimeMonitoringData> monitoring = monitoringDataService.getDataByOrderCodeAsc(order.getCode());
        result.put("list", monitoring == null ? Collections.emptyList() : monitoring);
        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));
            }
        }
        UploadRealTimeMonitoringData latestMonitoring = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
        result.put("cdElectronic", latestMonitoring == null ? null : latestMonitoring.getOutput_current());
        result.put("cdVoltage", latestMonitoring == null ? null : latestMonitoring.getOutput_voltage());
        result.put("totalPower", latestMonitoring == null ? null : latestMonitoring.getPower());
        result.put("surplus", latestMonitoring == null ? null : latestMonitoring.getSoc());
        result.put("list", monitoring);
        result.put("list1", baseMapper.accountingStrategyList(order.getId()));
        return result;
    }
    @Override
    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
        Long orderId;
        try {
            orderId = Long.valueOf(chargingOrderId);
        } catch (NumberFormatException e) {
            return AjaxResult.error("VIN码充电订单ID无效");
        }
        TVinChargingOrder order = getAuthorizedOrder(orderId);
        if (order == null) {
            return AjaxResult.error("VIN码充电订单不存在或无权操作");
        }
        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
            return AjaxResult.error("充电枪与订单不匹配");
        }
        Integer status = order.getStatus();
        if (status == null || !status.equals(3)) {
            return AjaxResult.error(status != null && (status.equals(4) || status.equals(5)) ? "不能重复操作" : "还未开始充电");
        }
        TChargingGun chargingGun = chargingGunClient.getChargingGunById(chargingGunId).getData();
        TChargingPile chargingPile = order.getChargingPileId() == null ? null
                : chargingPileClient.getChargingPileById(order.getChargingPileId()).getData();
        if (chargingGun == null || chargingPile == null) {
            return AjaxResult.error("未找到订单对应的充电设备");
        }
        order.setStatus(4);
        order.setEndMode(1);
        order.setEndTime(LocalDateTime.now());
        if (!updateById(order)) {
            return AjaxResult.error("更新VIN码充电订单状态失败");
        }
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(chargingPile.getCode());
        command.setCharging_gun_code(chargingGun.getCode());
        sendMessageClient.platformStopCharging(command);
        return AjaxResult.success();
    }
    @Override
@@ -148,6 +386,104 @@
                .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
    }
    private TVinChargingOrder getAuthorizedAppletOrder(Long chargingOrderId) {
        if (chargingOrderId == null || tokenService.getLoginUserApplet() == null
                || tokenService.getLoginUserApplet().getUserId() == null) {
            return null;
        }
        TVinChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getVinId() == null) {
            return null;
        }
        R<List<Long>> response = vinManagementClient.getVinIdsByAppUserId(
                tokenService.getLoginUserApplet().getUserId());
        List<Long> vinIds = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return vinIds.contains(order.getVinId()) ? order : null;
    }
    private Long toEpochMilli(LocalDateTime time) {
        return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }
    private Site getSite(Integer siteId) {
        if (siteId == null) {
            return null;
        }
        R<List<Site>> response = siteClient.getSiteByIds(Collections.singletonList(siteId));
        List<Site> sites = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return sites.stream().findFirst().orElse(null);
    }
    private TChargingGun getGun(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        R<TChargingGun> response = chargingGunClient.getChargingGunById(gunId);
        return response == null ? null : response.getData();
    }
    private void fillAppletMonitoring(Map<String, Object> result, String orderCode) {
        List<UploadRealTimeMonitoringData> dataList = orderCode == null ? Collections.emptyList()
                : monitoringDataService.getDataByOrderCode(orderCode);
        if (dataList == null || dataList.isEmpty()) {
            result.put("currentAndVoltage", Collections.emptyList());
            result.put("power", Collections.emptyList());
            return;
        }
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
        Map<String, UploadRealTimeMonitoringData> minuteData = new java.util.LinkedHashMap<>();
        for (UploadRealTimeMonitoringData data : dataList) {
            if (data != null && data.getCreate_time() != null) {
                minuteData.putIfAbsent(formatter.format(data.getCreate_time()), data);
            }
        }
        List<Map<String, Object>> currentAndVoltage = new ArrayList<>();
        List<Map<String, Object>> power = new ArrayList<>();
        for (Map.Entry<String, UploadRealTimeMonitoringData> entry : minuteData.entrySet()) {
            UploadRealTimeMonitoringData data = entry.getValue();
            Map<String, Object> currentItem = new HashMap<>();
            currentItem.put("time", entry.getKey());
            currentItem.put("electricity", data.getOutput_current());
            currentItem.put("voltage", data.getOutput_voltage());
            currentAndVoltage.add(currentItem);
            Map<String, Object> powerItem = new HashMap<>();
            powerItem.put("time", entry.getKey());
            powerItem.put("soc", data.getSoc());
            BigDecimal outputPower = data.getOutput_current() == null || data.getOutput_voltage() == null ? null
                    : data.getOutput_current().multiply(data.getOutput_voltage())
                    .divide(new BigDecimal("1000"), 2, RoundingMode.HALF_DOWN);
            powerItem.put("power", outputPower);
            power.add(powerItem);
        }
        result.put("currentAndVoltage", currentAndVoltage);
        result.put("power", power);
    }
    private void fillAppletExtra(List<MyVinChargingOrderList> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> siteIds = list.stream().map(MyVinChargingOrderList::getSiteId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(MyVinChargingOrderList::getChargingGunId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Integer, Site> sites = siteIds.isEmpty() ? Collections.emptyMap()
                : 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()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(gunIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(TChargingGun::getId, item -> item, (a, b) -> a));
        for (MyVinChargingOrderList item : list) {
            Site site = sites.get(item.getSiteId());
            item.setTitle(site == null ? "未知电站" : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setName(gun == null ? null : gun.getName());
        }
    }
    private void fillExtra(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
@@ -184,4 +520,567 @@
            item.setPartnerIncome(BigDecimal.ZERO);
        }
    }
    private void fillRealtimeData(List<ChargingOrderVO> list) {
        for (ChargingOrderVO item : list) {
            if (item.getCode() == null || item.getCode().trim().isEmpty()) {
                continue;
            }
            UploadRealTimeMonitoringData data = monitoringDataService.getLastDataById(item.getCode());
            if (data == null) {
                continue;
            }
            item.setChargeCurrent(data.getOutput_current());
            item.setChargeVoltage(data.getOutput_voltage());
            BigDecimal power = data.getPower();
            if (power == null && data.getOutput_current() != null && data.getOutput_voltage() != null) {
                power = data.getOutput_current().multiply(data.getOutput_voltage())
                        .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            }
            item.setChargePower(power);
        }
    }
    // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) =====
    @Override
    public DeviceStartChargingVO startVinCharging(DeviceStartChargingDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
            return DeviceStartChargingVO.fail(9);
        }
        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
        RLock gunLock = redissonClient.getLock(LOCK_VD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
        try {
            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
                return DeviceStartChargingVO.fail(10);
            }
            // 1. 充电枪校验
            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. VIN反序解析与绑定校验(协议要求VIN码反序上送)
            String vin = new StringBuilder(dto.getAccount().trim()).reverse().toString();
            TVinManagement vinRecord = vinManagementClient.getVinByCode(vin).getData();
            if (vinRecord == null) {
                return DeviceStartChargingVO.fail(9);
            }
            if (vinRecord.getStatus() == null || vinRecord.getStatus() != 1) {
                return DeviceStartChargingVO.fail(2);
            }
            // 并发防护(文档13.2):VIN ID维度锁,防同VIN多枪并发建单
            RLock vinLock = redissonClient.getLock(LOCK_VD_START_VIN + vinRecord.getId());
            boolean vinLocked = false;
            try {
                vinLocked = vinLock.tryLock(0, 30, TimeUnit.SECONDS);
                if (!vinLocked) {
                    return DeviceStartChargingVO.fail(10);
                }
                // 3. 建单(状态2 启动中)
                TVinChargingOrder order = new TVinChargingOrder();
                order.setCode(ORDER_CODE_PREFIX_VD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
                order.setOrderType(2);
                order.setOrderSource(1);
                order.setOrderClassification(2);
                // VD 无支付动作(后付记账模式,不执行在线支付/扣款):payMethod 留空,
                // 不套用 CD 余额支付枚举,避免下游按支付模式误判走余额扣款逻辑
                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.setVinId(vinRecord.getId());
                order.setAppUserId(vinRecord.getAppUserId());
                order.setCreateTime(LocalDateTime.now());
                save(order);
                // 4. 计费策略快照(枪策略优先,站点策略兜底)
                Integer strategyId = gun.getAccountingStrategyId() != null
                        ? gun.getAccountingStrategyId()
                        : (site == null ? null : site.getAccountingStrategyId());
                try {
                    saveStrategySnapshot(order.getId(), strategyId);
                } catch (Exception snapshotEx) {
                    log.error("[VD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
                }
                log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={}", order.getCode(), vinRecord.getId(), dto.getChargingGunCode());
                return DeviceStartChargingVO.success(order.getCode(), null);
            } finally {
                if (vinLocked && vinLock.isHeldByCurrentThread()) {
                    vinLock.unlock();
                }
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[VD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
            return DeviceStartChargingVO.fail(null);
        } catch (Exception e) {
            log.error("[VD建单] VIN码充电启动异常 pile={} gun={} account={}",
                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
            return DeviceStartChargingVO.fail(null);
        } finally {
            if (gunLock.isHeldByCurrentThread()) {
                gunLock.unlock();
            }
        }
    }
    /**
     * 保存VIN订单计费策略快照(主表 + 明细表)
     */
    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;
        }
        TVinAccountingStrategyOrder strategyOrder = new TVinAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setId(null);
        strategyOrder.setChargingOrderId(orderId);
        vinAccountingStrategyOrderService.save(strategyOrder);
        List<TVinAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
        for (TAccountingStrategyDetail detail : details) {
            TVinAccountingStrategyDetailOrder detailOrder = new TVinAccountingStrategyDetailOrder();
            BeanUtils.copyProperties(detail, detailOrder);
            detailOrder.setId(null);
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        vinAccountingStrategyDetailOrderService.saveBatch(detailOrders);
    }
    @Override
    public TVinChargingOrder getVinOrderByCode(String code) {
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getCode, code)
                .last("limit 1"));
    }
    @Override
    public void vinStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(vo.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        if (order.getStatus() != null && order.getStatus() >= ORDER_STATUS_CHARGING) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        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);
            log.info("[VD启动] VIN订单进入充电中 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);
            resetGunQuietly(order);
            log.warn("[VD启动] VIN订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
            pushRegulatoryAsync(order, false, false);
        }
    }
    @Override
    public boolean handleHandshakeVin(String orderCode, String vin) {
        if (StringUtils.isEmpty(orderCode)) {
            return false;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            log.warn("[VD握手] 订单不存在 code={}", orderCode);
            return false;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        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;
        }
        updateById(update);
        if (advanced) {
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setStatus(GUN_STATUS_CHARGING);
            chargingGunClient.updateChargingGunById(gun);
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
            log.info("[VD握手] 握手推进订单进入充电中 code={}", orderCode);
        }
        return true;
    }
    @Override
    public void vinChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(query.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        if (order.getStatus() != null && order.getStatus() >= ORDER_STATUS_FINISHED) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        if (order.getStatus() == null || order.getStatus() < ORDER_STATUS_CHARGING) {
            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) {
            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());
        }
        updateById(update);
    }
    @Override
    public void vinEndCharge(String orderCode, Integer endMode) {
        if (StringUtils.isEmpty(orderCode)) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(endMode);
        updateById(update);
        pushRegulatoryAsync(order, false, false);
    }
    @Override
    public R vinEndChargeBillingCharge(TransactionRecordMessageVO vo) {
        String code = vo == null ? null : vo.getTransaction_serial_number();
        log.info("[VD结算] 收到交易记录 code={}", code);
        if (StringUtils.isEmpty(code)) {
            return R.fail("交易记录缺少交易流水号");
        }
        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等第二层兜底
        RLock settleLock = redissonClient.getLock(LOCK_VD_SETTLE + code);
        try {
            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.warn("[VD结算] 并发结算进行中,跳过重复账单 code={}", code);
                return R.ok("重复账单,结算处理中");
            }
            return doVinEndChargeBillingCharge(vo, code);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return R.fail("结算锁获取被中断:" + code);
        } finally {
            if (settleLock.isHeldByCurrentThread()) {
                settleLock.unlock();
            }
        }
    }
    /**
     * VD 结算实际执行体(调用前须持有订单号结算锁)
     */
    private R doVinEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
        TVinChargingOrder order = getVinOrderByCode(code);
        if (order == null) {
            return R.fail("VIN码充电订单不存在:" + code);
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
            log.warn("[VD结算] 重复账单跳过 code={}", code);
            return R.ok("重复账单,已按首次账单结算");
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
            return R.ok("订单已关闭,跳过结算");
        }
        // 分时电量(以平台快照计价)
        java.util.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();
        }
        Map<Integer, TVinAccountingStrategyDetailOrder> snapshotByType = vinAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list().stream()
                .filter(item -> item.getType() != null)
                .collect(Collectors.toMap(TVinAccountingStrategyDetailOrder::getType, java.util.function.Function.identity(), (a, b) -> a));
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        List<TVinChargingOrderAccountingStrategy> details = new ArrayList<>();
        for (Map.Entry<Integer, BigDecimal> stage : stageElectricity.entrySet()) {
            TVinAccountingStrategyDetailOrder 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);
            TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy();
            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) {
                detail.setStartTime(snapshot.getStartTime());
                detail.setEndTime(snapshot.getEndTime());
            }
            details.add(detail);
        }
        if (snapshotByType.isEmpty()) {
            log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
        // lambda 引用要求有效final,拷贝最终值
        final BigDecimal totalElec = totalElectricity;
        final BigDecimal elecTotal = electrovalenceTotal;
        final BigDecimal svcTotal = serviceChargeTotal;
        // 本地事务:写计费明细 + 更新订单(状态5;VD后付记账模式,不做账户扣款)
        transactionTemplate.execute(status -> {
            vinChargingOrderAccountingStrategyService.lambdaUpdate()
                    .eq(TVinChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
                    .remove();
            if (!details.isEmpty()) {
                vinChargingOrderAccountingStrategyService.saveBatch(details);
            }
            TVinChargingOrder update = new TVinChargingOrder();
            update.setId(order.getId());
            update.setTotalElectricity(totalElec);
            update.setChargingCapacity(totalElec);
            update.setElectrovalence(elecTotal);
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            update.setEndTime(LocalDateTime.now());
            update.setStatus(ORDER_STATUS_FINISHED);
            if (order.getEndMode() == null) {
                update.setEndMode(1);
            }
            updateById(update);
            return true;
        });
        // 复位充电枪
        resetGunQuietly(order);
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[VD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
        return R.ok();
    }
    @Override
    public TVinChargingOrder getLatestStoppingByGunId(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getChargingGunId, gunId)
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .orderByDesc(TVinChargingOrder::getId)
                .last("limit 1"));
    }
    @Override
    public void vinTerminateStopByReply(PlatformStopChargingReplyVO vo) {
        // stopCharging 已前置更新状态4/结束方式1,此处仅兜底确认(幂等)
        if (vo == null) {
            return;
        }
        TChargingGun gun = null;
        try {
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(vo.getCharging_pile_code());
            gunQuery.setCharging_gun_code(vo.getCharging_gun_code());
            gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
        } catch (Exception ex) {
            log.error("[VD停止] 查询充电枪失败 pile={} gun={}", vo.getCharging_pile_code(), vo.getCharging_gun_code(), ex);
            return;
        }
        if (gun == null) {
            return;
        }
        TVinChargingOrder order = getLatestStoppingByGunId(gun.getId());
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(1);
        updateById(update);
        log.info("[VD停止] 平台停止应答兜底确认 code={}", order.getCode());
    }
    /**
     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
     */
    private void pushRegulatoryAsync(TVinChargingOrder 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("[VD监管] 推送监管平台失败 code={}", order.getCode(), ex);
            }
        }, "VD-Regulatory-Push").start();
    }
    /**
     * 复位充电枪为空闲(失败仅告警)
     */
    private void resetGunQuietly(TVinChargingOrder order) {
        if (order.getChargingGunId() == null) {
            return;
        }
        try {
            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
            if (gun != null) {
                gun.setStatus(GUN_STATUS_IDLE);
                chargingGunClient.updateChargingGunById(gun);
            }
        } catch (Exception ex) {
            log.error("[VD结算] 复位充电枪异常 code={} gunId={}", order.getCode(), order.getChargingGunId(), ex);
        }
    }
    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
    @Override
    public List<TVinChargingOrder> listTimeoutStartingOrders() {
        return lambdaQuery()
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(VD_STARTING_TIMEOUT_MINUTES))
                .list();
    }
    @Override
    public List<TVinChargingOrder> listTimeoutChargingOrders() {
        return lambdaQuery()
                .in(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusHours(VD_CHARGING_TIMEOUT_HOURS))
                .list();
    }
    @Override
    public void closeTimeoutStartingOrder(TVinChargingOrder order) {
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setStatus(ORDER_STATUS_START_FAILED);
        // 异常终止(沿用 CD 约定:启动失败/异常关单 endMode=0,避免前端结束方式显示空)
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        updateById(update);
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[VD补偿] 启动超时关单 code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
    }
    @Override
    public void compensationSettle(TVinChargingOrder 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("[VD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
        vinEndChargeBillingCharge(vo);
    }
}