| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | @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); |
| | |
| | | result.addAll(batch); |
| | | } |
| | | fillExtra(result); |
| | | fillRealtimeData(result); |
| | | return result; |
| | | } |
| | | |
| | |
| | | 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()); |
| | |
| | | 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 |
| | |
| | | .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; |
| | |
| | | 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); |
| | | } |
| | | } |