package com.ruoyi.order.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.account.api.feignClient.AppUserClient; import com.ruoyi.account.api.feignClient.CompanyAllocateRecordClient; import com.ruoyi.account.api.feignClient.VinManagementClient; import com.ruoyi.account.api.model.TAppUser; import com.ruoyi.account.api.model.TCompanyAllocateRecord; import com.ruoyi.account.api.model.TVinManagement; import com.ruoyi.chargingPile.api.dto.GetSiteListDTO; import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO; import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient; import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient; import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient; 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.TBillingTemplateDetail; import com.ruoyi.chargingPile.api.model.TChargingGun; import com.ruoyi.chargingPile.api.model.TChargingPile; import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.utils.DateUtils; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.core.utils.bean.BeanUtils; import com.ruoyi.common.core.web.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 com.ruoyi.other.api.domain.TFleet; import com.ruoyi.other.api.domain.TGroupCompany; import com.ruoyi.other.api.dto.ChargeAuthorizationDTO; import com.ruoyi.other.api.dto.ChargeTimeRange; import com.ruoyi.other.api.dto.CompanyPreferentialDTO; import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient; import com.ruoyi.other.api.feignClient.CompanyPreferentialClient; import com.ruoyi.other.api.feignClient.GroupCompanyClient; 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.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; 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 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; /** 列表/详情回填 VIN 信息时,单次批量远程查询的最大 ids 数量 */ private static final int VIN_BACKFILL_BATCH_SIZE = 500; /** 账户模式:共享(锁企业账户余额) */ private static final int ACCOUNT_MODE_SHARED = 1; /** 账户模式:分配(锁VIN账户余额) */ private static final int ACCOUNT_MODE_ALLOCATED = 2; /** 未配置充电授权时的默认下发金额上限(元) */ private static final BigDecimal VD_DEFAULT_DISPATCH_AMOUNT = new BigDecimal("200"); /** 授权时间到期停充提前量(秒):剩余授权时间不足该值时提前下发停充,规避数据传输延迟 */ private static final int AUTHORIZE_STOP_AHEAD_SECONDS = 60; /** 授权时间方式:全天 */ private static final int AUTHORIZE_TIME_MODE_ALL_DAY = 1; /** 授权时间方式:指定时段 */ private static final int AUTHORIZE_TIME_MODE_RANGE = 2; /** 授权时间方式:指定电价时段 */ private static final int AUTHORIZE_TIME_MODE_PRICE = 3; /** 企业优惠方式:不优惠 */ private static final int PREFERENTIAL_TYPE_NONE = 1; /** 企业优惠方式:按比例优惠(服务费折扣) */ private static final int PREFERENTIAL_TYPE_RATE = 2; /** 企业优惠方式:按优惠单价收费(取计费模板单价) */ private static final int PREFERENTIAL_TYPE_TEMPLATE = 3; /** 金额比较用零值常量 */ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO; @Resource private TokenService tokenService; @Resource private ChargingMessageClient chargingMessageClient; @Resource private ChengDuJianGuanClient chengDuJianGuanClient; @Resource private VinManagementClient vinManagementClient; @Resource private AppUserClient appUserClient; @Resource private SiteClient siteClient; @Resource private ChargingGunClient chargingGunClient; @Resource private ChargingPileClient chargingPileClient; @Resource private SendMessageClient sendMessageClient; @Resource private GroupCompanyClient groupCompanyClient; @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; @Resource private ChargeAuthorizationClient chargeAuthorizationClient; @Resource private CompanyPreferentialClient companyPreferentialClient; @Resource private BillingTemplateClient billingTemplateClient; @Resource private CompanyAllocateRecordClient companyAllocateRecordClient; @Override public PageInfo pageList(VinChargingOrderQuery query) { int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr(); int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize(); setAuthSiteIds(query); PageInfo page = new PageInfo<>(pageCurr, pageSize); long total = baseMapper.count(query); page.setTotal(total); List records = total == 0 ? Collections.emptyList() : baseMapper.pageList(query, (long) (pageCurr - 1) * pageSize, pageSize); fillExtra(records); page.setRecords(records); return page; } @Override public PageInfo 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 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> vinIdResponse = vinManagementClient.getVinIdsByAppUserId(appUserId); List 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 records = total == 0 ? Collections.emptyList() : baseMapper.pageAppletOrders(vinIds, (long) (current - 1) * size, size); fillAppletExtra(records); page.setRecords(records); return page; } @Override public Map appletDetail(Long chargingOrderId) { TVinChargingOrder order = getAuthorizedAppletOrder(chargingOrderId); if (order == null) { return Collections.emptyMap(); } Map 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 getChargingVinStatus(List vinIds) { if (vinIds == null || vinIds.isEmpty()) { return Collections.emptyList(); } List distinctVinIds = vinIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); return distinctVinIds.isEmpty() ? Collections.emptyList() : baseMapper.getChargingVinStatus(distinctVinIds); } @Override public List exportList(VinChargingOrderQuery query) { setAuthSiteIds(query); long total = baseMapper.count(query); List result = new ArrayList<>(); for (long offset = 0; offset < total; offset += 5000) { List batch = baseMapper.pageList(query, offset, 5000); if (batch == null || batch.isEmpty()) { break; } result.addAll(batch); } fillExtra(result); fillRealtimeData(result); return result; } @Override public Map summary(VinChargingOrderQuery query) { setAuthSiteIds(query); Map result = baseMapper.summary(query); return result == null ? new HashMap<>() : result; } @Override public Map detail(Long chargingOrderId) { TVinChargingOrder order = getAuthorizedOrder(chargingOrderId); if (order == null) { return Collections.emptyMap(); } Map result = new HashMap<>(); result.put("id", order.getId() == null ? null : String.valueOf(order.getId())); result.put("code", order.getCode()); // 订单VIN快照为空时回填 VIN 管理登记值(历史订单无快照),前端按 actualVin → expectedVin → vin 取值 TVinManagement vinRecord = order.getVinId() == null ? null : loadVinRecords(Collections.singletonList(order.getVinId())).get(order.getVinId()); String vinCode = StringUtils.isEmpty(order.getActualVin()) ? order.getExpectedVin() : order.getActualVin(); if (StringUtils.isEmpty(vinCode) && vinRecord != null) { vinCode = vinRecord.getVin(); } result.put("vin", vinCode); result.put("actualVin", order.getActualVin()); result.put("expectedVin", order.getExpectedVin()); result.put("plateNum", StringUtils.isEmpty(order.getPlateNum()) && vinRecord != null ? vinRecord.getPlateNumber() : order.getPlateNum()); result.put("phone", resolvePhone(order.getPhone(), order.getAppUserId(), vinRecord)); result.put("status", order.getStatus()); result.put("siteId", order.getSiteId()); result.put("chargingPileId", order.getChargingPileId()); result.put("chargingGunId", order.getChargingGunId()); result.put("startTime", order.getStartTime()); result.put("endTime", order.getEndTime()); result.put("chargingCapacity", order.getChargingCapacity()); result.put("paymentAmount", order.getPaymentAmount()); result.put("electrovalence", order.getElectrovalence()); result.put("serviceCharge", order.getServiceCharge()); if (order.getStartTime() != null && order.getEndTime() != null) { result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime())); } List 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 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 monitoring = monitoringDataService.getDataByOrderCodeAsc(order.getCode()); if (monitoring == null) { monitoring = Collections.emptyList(); } for (UploadRealTimeMonitoringData data : monitoring) { if (data.getOutput_current() != null && data.getOutput_voltage() != null) { data.setPower(data.getOutput_voltage().multiply(data.getOutput_current()).divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP)); } } 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 public List> accountingStrategyList(Long chargingOrderId) { TVinChargingOrder order = getAuthorizedOrder(chargingOrderId); return order == null ? Collections.emptyList() : baseMapper.accountingStrategyList(chargingOrderId); } private void setAuthSiteIds(VinChargingOrderQuery query) { List authorizedSiteIds = getAuthorizedSiteIds(); if (query.getSiteId() != null) { authorizedSiteIds = authorizedSiteIds.contains(query.getSiteId()) ? Collections.singletonList(query.getSiteId()) : Collections.singletonList(-1); } if (authorizedSiteIds.isEmpty()) { authorizedSiteIds = Collections.singletonList(-1); } query.setSiteIds(authorizedSiteIds); } private TVinChargingOrder getAuthorizedOrder(Long chargingOrderId) { if (chargingOrderId == null) { return null; } TVinChargingOrder order = getById(chargingOrderId); if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) { return null; } return order; } private List getAuthorizedSiteIds() { Long userId = tokenService.getLoginUser().getUserid(); if (userId == null) { return Collections.emptyList(); } return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()) .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> response = vinManagementClient.getVinIdsByAppUserId( tokenService.getLoginUserApplet().getUserId()); List 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> response = siteClient.getSiteByIds(Collections.singletonList(siteId)); List 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 response = chargingGunClient.getChargingGunById(gunId); return response == null ? null : response.getData(); } private void fillAppletMonitoring(Map result, String orderCode) { List 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 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> currentAndVoltage = new ArrayList<>(); List> power = new ArrayList<>(); for (Map.Entry entry : minuteData.entrySet()) { UploadRealTimeMonitoringData data = entry.getValue(); Map currentItem = new HashMap<>(); currentItem.put("time", entry.getKey()); currentItem.put("electricity", data.getOutput_current()); currentItem.put("voltage", data.getOutput_voltage()); currentAndVoltage.add(currentItem); Map 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 list) { if (list == null || list.isEmpty()) { return; } List siteIds = list.stream().map(MyVinChargingOrderList::getSiteId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); List gunIds = list.stream().map(MyVinChargingOrderList::getChargingGunId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); Map 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 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 list) { if (list == null || list.isEmpty()) { return; } List orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); Map periodCounts = new HashMap<>(); if (!orderIds.isEmpty()) { for (Map item : baseMapper.countAccountingStrategies(orderIds)) { Number chargingOrderId = (Number) item.get("chargingOrderId"); Number periodCount = (Number) item.get("periodCount"); if (chargingOrderId != null && periodCount != null) { periodCounts.put(chargingOrderId.longValue(), periodCount.intValue()); } } } List siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); List gunIds = list.stream().map(ChargingOrderVO::getChargingGunId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); Map 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 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 (ChargingOrderVO item : list) { Site site = sites.get(item.getSiteId()); item.setSiteName(site == null ? null : site.getName()); TChargingGun gun = guns.get(item.getChargingGunId()); item.setTerminalName(gun == null ? null : gun.getName()); if (item.getStartTime() != null && item.getEndTime() != null) { item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime())); } item.setCount(periodCounts.getOrDefault(item.getId(), 0)); item.setPlatformIncome(BigDecimal.ZERO); item.setPartnerIncome(BigDecimal.ZERO); } fillVinInfoAndPhone(list); fillCompanyFleetNames(list); } /** * 回填 VIN 码/车牌号/手机号:订单快照为空时取 VIN 管理登记值(历史订单无快照), * 手机号再以 App 用户当前手机号兜底。远程调用失败仅跳过回填,不影响列表其他字段。 */ private void fillVinInfoAndPhone(List list) { if (list == null || list.isEmpty()) { return; } List vinIds = list.stream().map(ChargingOrderVO::getVinId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); Map vinRecords = loadVinRecords(vinIds); for (ChargingOrderVO item : list) { TVinManagement vinRecord = item.getVinId() == null ? null : vinRecords.get(item.getVinId()); if (vinRecord == null) { continue; } if (StringUtils.isEmpty(item.getVin())) { item.setVin(vinRecord.getVin()); } if (StringUtils.isEmpty(item.getPlateNum())) { item.setPlateNum(vinRecord.getPlateNumber()); } if (StringUtils.isEmpty(item.getPhone())) { item.setPhone(vinRecord.getPhone()); } } List appUserIds = list.stream() .filter(item -> StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null) .map(ChargingOrderVO::getAppUserId).distinct().collect(Collectors.toList()); if (appUserIds.isEmpty()) { return; } Map userPhones = loadAppUserPhones(appUserIds); for (ChargingOrderVO item : list) { if (StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null) { String phone = userPhones.get(item.getAppUserId()); if (StringUtils.isNotEmpty(phone)) { item.setPhone(phone); } } } } /** * 回填所属公司/车队名称:按订单快照 companyId/fleetId 批量远程取名(公司/车队改名后列表即时生效), * 远程调用失败仅跳过回填,不影响列表其他字段。 */ private void fillCompanyFleetNames(List list) { if (list == null || list.isEmpty()) { return; } List companyIds = list.stream().map(ChargingOrderVO::getCompanyId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (!companyIds.isEmpty()) { Map companyNames = loadCompanyNames(companyIds); for (ChargingOrderVO item : list) { String name = item.getCompanyId() == null ? null : companyNames.get(item.getCompanyId()); if (StringUtils.isNotEmpty(name)) { item.setCompanyName(name); } } } List fleetIds = list.stream().map(ChargingOrderVO::getFleetId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (fleetIds.isEmpty()) { return; } Map fleetNames = loadFleetNames(fleetIds); for (ChargingOrderVO item : list) { String name = item.getFleetId() == null ? null : fleetNames.get(item.getFleetId()); if (StringUtils.isNotEmpty(name)) { item.setFleetName(name); } } } /** 批量查询公司名称,失败返回已取到的数据 */ private Map loadCompanyNames(List companyIds) { Map result = new HashMap<>(); try { List companies = Optional.ofNullable(groupCompanyClient.listCompaniesByIds(companyIds).getData()) .orElse(Collections.emptyList()); for (TGroupCompany company : companies) { if (company != null && company.getId() != null && StringUtils.isNotEmpty(company.getName())) { result.put(company.getId(), company.getName()); } } } catch (Exception e) { log.warn("[VD回填] 批量查询公司名称失败 ids={}", companyIds, e); } return result; } /** 批量查询车队名称,失败返回已取到的数据 */ private Map loadFleetNames(List fleetIds) { Map result = new HashMap<>(); try { List fleets = Optional.ofNullable(groupCompanyClient.listFleetsByIds(fleetIds).getData()) .orElse(Collections.emptyList()); for (TFleet fleet : fleets) { if (fleet != null && fleet.getId() != null && StringUtils.isNotEmpty(fleet.getFleetName())) { result.put(fleet.getId(), fleet.getFleetName()); } } } catch (Exception e) { log.warn("[VD回填] 批量查询车队名称失败 ids={}", fleetIds, e); } return result; } /** 手机号回填优先级:订单值 → VIN 记录手机号 → App 用户当前手机号 */ private String resolvePhone(String phone, Long appUserId, TVinManagement vinRecord) { if (StringUtils.isNotEmpty(phone)) { return phone; } if (vinRecord != null && StringUtils.isNotEmpty(vinRecord.getPhone())) { return vinRecord.getPhone(); } if (appUserId == null) { return null; } return loadAppUserPhones(Collections.singletonList(appUserId)).get(appUserId); } /** 按 VIN 记录ID批量查询,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */ private Map loadVinRecords(List vinIds) { Map result = new HashMap<>(); if (vinIds == null || vinIds.isEmpty()) { return result; } for (int index = 0; index < vinIds.size(); index += VIN_BACKFILL_BATCH_SIZE) { List batch = vinIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, vinIds.size())); try { R> response = vinManagementClient.getVinByIds(new ArrayList<>(batch)); if (response == null || response.getData() == null) { continue; } for (TVinManagement record : response.getData()) { if (record != null && record.getId() != null) { result.putIfAbsent(record.getId(), record); } } } catch (Exception e) { log.warn("[VD回填] 批量查询VIN记录失败 ids={}", batch, e); } } return result; } /** 按 App 用户ID批量查询当前手机号,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */ private Map loadAppUserPhones(List appUserIds) { Map result = new HashMap<>(); for (int index = 0; index < appUserIds.size(); index += VIN_BACKFILL_BATCH_SIZE) { List batch = appUserIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, appUserIds.size())); try { R> response = appUserClient.getUserByIds(new ArrayList<>(batch)); if (response == null || response.getData() == null) { continue; } for (TAppUser user : response.getData()) { if (user != null && user.getId() != null && StringUtils.isNotEmpty(user.getPhone())) { result.putIfAbsent(user.getId(), user.getPhone()); } } } catch (Exception e) { log.warn("[VD回填] 批量查询App用户手机号失败 ids={}", batch, e); } } return result; } private void fillRealtimeData(List 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); } // 计费策略(枪策略优先,站点策略兜底):授权校验(指定电价时段)与策略快照共用 Integer strategyId = gun.getAccountingStrategyId() != null ? gun.getAccountingStrategyId() : (site == null ? null : site.getAccountingStrategyId()); // 3. 企业充电授权校验 + 账户额度计算(共享=企业账户,分配=VIN账户) VinChargeContext chargeContext = resolveVinChargeContext(gun.getSiteId(), strategyId, vinRecord); if (chargeContext.getFailCode() != null) { log.warn("[VD建单] 授权校验不通过 vinId={} gun={} reason={}", vinRecord.getId(), dto.getChargingGunCode(), chargeContext.getFailReason()); return DeviceStartChargingVO.fail(chargeContext.getFailCode()); } // 并发防护(文档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()); // VIN 码/车牌快照落库(协议上报为反序VIN,此处存解析后的正序VIN),保证列表与详情直接可见 order.setExpectedVin(vinRecord.getVin()); order.setActualVin(vin); order.setPlateNum(vinRecord.getPlateNumber()); // 所属公司/车队快照落库(VIN 管理记录当前归属),供订单列表/汇总/导出按公司、车队直接筛选 order.setCompanyId(vinRecord.getCompanyId()); order.setFleetId(vinRecord.getFleetId()); order.setCreateTime(LocalDateTime.now()); save(order); // 3.1 锁定账户额度(实扣冻结:可用余额减少、锁定金额增加;锁定失败则关单并回告余额不足) if (!lockVinChargeContext(vinRecord, chargeContext)) { closeTimeoutStartingOrder(order); return DeviceStartChargingVO.fail(3); } // 3.2 回填账户模式/锁定额/授权截止时间(订单列表与充电中授权到期停充使用) TVinChargingOrder accountUpdate = new TVinChargingOrder(); accountUpdate.setId(order.getId()); accountUpdate.setAccountMode(chargeContext.getAccountMode()); accountUpdate.setLockedAmount(chargeContext.getDispatchAmount()); accountUpdate.setMaxEndTime(chargeContext.getMaxEndTime()); updateById(accountUpdate); // 4. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析) try { saveStrategySnapshot(order.getId(), strategyId); } catch (Exception snapshotEx) { log.error("[VD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx); } log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={} accountMode={} lockAmount={} maxEndTime={}", order.getCode(), vinRecord.getId(), dto.getChargingGunCode(), chargeContext.getAccountMode(), chargeContext.getDispatchAmount(), chargeContext.getMaxEndTime()); return DeviceStartChargingVO.success(order.getCode(), chargeContext.getDispatchAmount()); } 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 static class VinChargeContext { /** 账户模式(1=共享,2=分配) */ private Integer accountMode; /** 锁定额度(元),同时作为下发给充电桩的可用余额 */ private BigDecimal dispatchAmount; /** 授权截止时间(全天授权或未开启自动停充时为空) */ private LocalDateTime maxEndTime; /** 失败码(为 null 表示校验通过) */ private Integer failCode; /** 失败原因(仅日志排查用) */ private String failReason; private static VinChargeContext fail(int failCode, String failReason) { VinChargeContext context = new VinChargeContext(); context.failCode = failCode; context.failReason = failReason; return context; } private static VinChargeContext ok(Integer accountMode, BigDecimal dispatchAmount, LocalDateTime maxEndTime) { VinChargeContext context = new VinChargeContext(); context.accountMode = accountMode; context.dispatchAmount = dispatchAmount; context.maxEndTime = maxEndTime; return context; } public Integer getAccountMode() { return accountMode; } public BigDecimal getDispatchAmount() { return dispatchAmount; } public LocalDateTime getMaxEndTime() { return maxEndTime; } public Integer getFailCode() { return failCode; } public String getFailReason() { return failReason; } } /** * 授权时间窗解析结果:maxEndTime 为空表示不做授权到期停充 */ private static class AuthorizationWindow { private final LocalDateTime maxEndTime; private AuthorizationWindow(LocalDateTime maxEndTime) { this.maxEndTime = maxEndTime; } } /** * 当前命中的电价阶段(授权时间方式=指定电价时段校验用) */ private static class PeriodTypeWindow { private final Integer type; private final LocalTime endTime; private PeriodTypeWindow(Integer type, LocalTime endTime) { this.type = type; this.endTime = endTime; } } /** * 解析 VIN 码充电账户上下文(建单前调用): * 1) 校验公司充电授权(站点范围、可用时段/电价阶段),未配置授权按公司口径放行; * 2) 计算下发给充电桩的可用余额:共享模式=min(授权下发金额, 企业可用余额), * 分配模式=VIN 账户余额;账户可用余额为 0 时直接拒绝发起充电; * 3) 计算授权截止时间(充电中提前停充使用,未开启自动停充时为空)。 */ private VinChargeContext resolveVinChargeContext(Integer siteId, Integer strategyId, TVinManagement vinRecord) { Integer companyId = vinRecord.getCompanyId(); if (companyId == null) { return VinChargeContext.fail(2, "VIN码未归属公司 vinId=" + vinRecord.getId()); } TGroupCompany company; try { R companyResult = groupCompanyClient.getCompanyById(companyId); company = companyResult == null ? null : companyResult.getData(); } catch (Exception ex) { log.error("[VD建单] 查询所属公司失败 companyId={}", companyId, ex); return VinChargeContext.fail(2, "查询所属公司失败 companyId=" + companyId); } if (company == null) { return VinChargeContext.fail(2, "所属公司不存在 companyId=" + companyId); } if (company.getStatus() != null && company.getStatus() != 1) { return VinChargeContext.fail(2, "所属公司已停用 companyId=" + companyId); } int accountMode = company.getAccountMode() == null ? ACCOUNT_MODE_SHARED : company.getAccountMode(); ChargeAuthorizationDTO authorization = null; try { R authResult = chargeAuthorizationClient.getDetailByCompanyId(companyId); authorization = authResult == null ? null : authResult.getData(); } catch (Exception ex) { log.error("[VD建单] 查询充电授权失败,按未配置授权放行 companyId={}", companyId, ex); } LocalDateTime maxEndTime = null; if (authorization != null) { if (authorization.getSiteIds() != null && !authorization.getSiteIds().isEmpty() && (siteId == null || !authorization.getSiteIds().contains(siteId))) { return VinChargeContext.fail(5, "站点不在授权范围 siteId=" + siteId + " companyId=" + companyId); } AuthorizationWindow window = resolveAuthorizationWindow(authorization, strategyId, companyId); if (window == null) { return VinChargeContext.fail(2, "当前时间不在充电授权时段 companyId=" + companyId); } maxEndTime = window.maxEndTime; } BigDecimal usableAmount = accountMode == ACCOUNT_MODE_ALLOCATED ? vinRecord.getBalance() : company.getAvailableBalance(); BigDecimal usable = usableAmount == null ? ZERO_AMOUNT : usableAmount; if (usable.compareTo(ZERO_AMOUNT) <= 0) { return VinChargeContext.fail(3, "账户可用余额为0 accountMode=" + accountMode + " companyId=" + companyId); } BigDecimal dispatch = authorization == null ? null : authorization.getDispatchAmount(); if (dispatch == null || dispatch.compareTo(ZERO_AMOUNT) <= 0) { dispatch = VD_DEFAULT_DISPATCH_AMOUNT; } if (dispatch.compareTo(usable) > 0) { dispatch = usable; } return VinChargeContext.ok(accountMode, dispatch, maxEndTime); } /** * 锁定账户额度(共享=企业账户,分配=VIN账户),返回是否锁定成功 */ private boolean lockVinChargeContext(TVinManagement vinRecord, VinChargeContext context) { try { R result; if (Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(context.accountMode)) { result = vinManagementClient.lockBalance(vinRecord.getId(), context.dispatchAmount); } else { result = groupCompanyClient.lockBalance(vinRecord.getCompanyId(), context.dispatchAmount); } if (result == null || result.getCode() != R.SUCCESS) { log.error("[VD建单] 锁定账户额度失败 accountMode={} companyId={} vinId={} amount={} msg={}", context.accountMode, vinRecord.getCompanyId(), vinRecord.getId(), context.dispatchAmount, result == null ? "null" : result.getMsg()); return false; } return true; } catch (Exception ex) { log.error("[VD建单] 锁定账户额度异常 accountMode={} companyId={} vinId={} amount={}", context.accountMode, vinRecord.getCompanyId(), vinRecord.getId(), context.dispatchAmount, ex); return false; } } /** * 释放账户锁定额度(订单未产生消费时使用:启动失败关单、异常终止等),失败仅记录日志由人工核对 */ private void unlockVinChargeContextQuietly(TVinChargingOrder order, String scene) { if (order == null || order.getLockedAmount() == null || order.getLockedAmount().compareTo(ZERO_AMOUNT) <= 0) { return; } Integer accountMode = order.getAccountMode(); try { R result; if (Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(accountMode)) { result = vinManagementClient.unlockBalance(order.getVinId(), order.getLockedAmount()); } else { result = groupCompanyClient.unlockBalance(order.getCompanyId(), order.getLockedAmount()); } if (result == null || result.getCode() != R.SUCCESS) { log.error("[VD账户] {}释放锁定额度失败 code={} accountMode={} amount={} msg={}", scene, order.getCode(), accountMode, order.getLockedAmount(), result == null ? "null" : result.getMsg()); return; } log.info("[VD账户] {}释放锁定额度成功 code={} accountMode={} amount={} balance={}", scene, order.getCode(), accountMode, order.getLockedAmount(), result.getData()); } catch (Exception ex) { log.error("[VD账户] {}释放锁定额度异常 code={} accountMode={} amount={}", scene, order.getCode(), accountMode, order.getLockedAmount(), ex); } } /** * 校验充电授权时间方式并计算授权截止时间: * 1) 全天:不做时间校验,截止时间为空; * 2) 指定时段:当前时间须落在某段可用时段内,截止时间=该段结束时间(未开启自动停充时为空); * 3) 指定电价时段:当前电价阶段须在授权范围内,截止时间=该阶段结束时间(未开启自动停充时为空)。 * 返回 null 表示当前时间不在授权范围内。 */ private AuthorizationWindow resolveAuthorizationWindow(ChargeAuthorizationDTO authorization, Integer strategyId, Integer companyId) { Integer timeMode = authorization.getTimeMode(); if (timeMode == null || timeMode == AUTHORIZE_TIME_MODE_ALL_DAY) { return new AuthorizationWindow(null); } boolean autoStop = Boolean.TRUE.equals(authorization.getAutoCharge()); LocalDateTime now = LocalDateTime.now(); LocalTime current = now.toLocalTime(); if (timeMode == AUTHORIZE_TIME_MODE_RANGE) { List ranges = authorization.getTimeRanges(); if (ranges == null || ranges.isEmpty()) { log.warn("[VD建单] 指定时段授权未配置时段,按放行处理 companyId={}", companyId); return new AuthorizationWindow(null); } for (ChargeTimeRange range : ranges) { if (range == null) { continue; } LocalTime start = parseLocalTime(range.getStartTime()); LocalTime end = parseLocalTime(range.getEndTime()); if (start == null || end == null || !start.isBefore(end)) { continue; } if (!current.isBefore(start) && current.isBefore(end)) { return new AuthorizationWindow(autoStop ? LocalDateTime.of(now.toLocalDate(), end) : null); } } return null; } if (timeMode == AUTHORIZE_TIME_MODE_PRICE) { List priceTypes = authorization.getPriceTypes(); if (priceTypes == null || priceTypes.isEmpty()) { log.warn("[VD建单] 指定电价时段授权未配置电价类型,按放行处理 companyId={}", companyId); return new AuthorizationWindow(null); } PeriodTypeWindow period = findCurrentPeriodType(strategyId, current); if (period == null) { log.warn("[VD建单] 未匹配到当前电价阶段,按放行处理 companyId={} strategyId={}", companyId, strategyId); return new AuthorizationWindow(null); } if (!priceTypes.contains(period.type)) { return null; } return new AuthorizationWindow(autoStop ? LocalDateTime.of(now.toLocalDate(), period.endTime) : null); } return new AuthorizationWindow(null); } /** * 按计费策略明细匹配当前时间命中的电价阶段(HH:mm 口径,跨天/非法时段跳过) */ private PeriodTypeWindow findCurrentPeriodType(Integer strategyId, LocalTime current) { if (strategyId == null) { return null; } List details; try { R> detailResult = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId); details = detailResult == null ? null : detailResult.getData(); } catch (Exception ex) { log.error("[VD建单] 查询计费策略明细失败 strategyId={}", strategyId, ex); return null; } if (details == null || details.isEmpty()) { return null; } for (TAccountingStrategyDetail detail : details) { if (detail == null || detail.getType() == null) { continue; } LocalTime start = parseLocalTime(detail.getStartTime()); LocalTime end = parseLocalTime(detail.getEndTime()); if (start == null || end == null || !start.isBefore(end)) { continue; } if (!current.isBefore(start) && current.isBefore(end)) { return new PeriodTypeWindow(detail.getType(), end); } } return null; } /** * 解析 HH:mm 时段(兼容 yyyy-MM-dd HH:mm 等带日期拼接格式;24:00 按当日末刻处理) */ private static LocalTime parseLocalTime(String value) { if (StringUtils.isEmpty(value)) { return null; } String trimmed = splitPeriodHourMinute(value); if (trimmed == null || trimmed.isEmpty()) { return null; } if ("24:00".equals(trimmed) || "24:00:00".equals(trimmed)) { return LocalTime.of(23, 59, 59); } try { return LocalTime.parse(trimmed, DateTimeFormatter.ofPattern("HH:mm")); } catch (DateTimeParseException ex) { try { return LocalTime.parse(trimmed); } catch (DateTimeParseException ex2) { log.warn("[VD建单] 时段格式异常 value={}", value); return null; } } } /** * 查询企业优惠设置(失败或未配置返回 null,按不优惠结算) */ private CompanyPreferentialDTO loadCompanyPreferential(Integer companyId) { if (companyId == null) { return null; } try { R result = companyPreferentialClient.getDetailByCompanyId(companyId); return result == null ? null : result.getData(); } catch (Exception ex) { log.error("[VD结算] 查询企业优惠设置失败,按不优惠结算 companyId={}", companyId, ex); return null; } } /** * 按优惠方式加载计费模板(仅方式3需要,取不到时该段按原价结算) */ private TBillingTemplateDTO loadBillingTemplate(CompanyPreferentialDTO preferential) { if (preferential == null || !Integer.valueOf(PREFERENTIAL_TYPE_TEMPLATE).equals(preferential.getPreferentialType()) || preferential.getBillingTemplateId() == null) { return null; } try { R result = billingTemplateClient.getTemplateDetail(preferential.getBillingTemplateId()); return result == null ? null : result.getData(); } catch (Exception ex) { log.error("[VD结算] 查询计费模板失败,按原价结算 templateId={}", preferential.getBillingTemplateId(), ex); return null; } } /** * 服务费按比例优惠:最终单价 = 原价 × (1 - 折扣率/100),折扣率缺失或越界时按原价 */ private static BigDecimal applyServiceDiscountRate(BigDecimal servicePrice, BigDecimal discountRate) { if (servicePrice == null || discountRate == null || discountRate.compareTo(ZERO_AMOUNT) <= 0) { return servicePrice; } if (discountRate.compareTo(new BigDecimal("100")) >= 0) { return ZERO_AMOUNT; } BigDecimal rate = BigDecimal.ONE.subtract(discountRate.divide(new BigDecimal("100"), 4, BigDecimal.ROUND_HALF_UP)); return servicePrice.multiply(rate).setScale(2, BigDecimal.ROUND_HALF_UP); } /** * 按电价阶段类型匹配计费模板明细(type:1=尖 2=峰 3=平 4=谷) */ private static TBillingTemplateDetail findTemplateDetail(TBillingTemplateDTO template, Integer type) { if (template == null || template.getDetails() == null || type == null) { return null; } for (TBillingTemplateDetail detail : template.getDetails()) { if (detail != null && type.equals(detail.getType())) { return detail; } } return null; } /** * 结算账户资金(实扣冻结口径): * 1) 解锁申请时锁定的额度,并按实际消费扣减(允许扣成负数),差额退回原账户; * 2) 写入企业额度分配记录-消费流水(按订单号+change_type 幂等)。 * 旧订单(无锁定金额)保持既有"仅记账不扣款"行为。 */ private void settleVinChargeAccount(TVinChargingOrder order, BigDecimal actualAmount, String code) { BigDecimal lockedAmount = order.getLockedAmount() == null ? ZERO_AMOUNT : order.getLockedAmount(); BigDecimal actual = actualAmount == null ? ZERO_AMOUNT : actualAmount; if (lockedAmount.compareTo(ZERO_AMOUNT) <= 0) { log.info("[VD结算] 订单无锁定金额,跳过账户结算 code={}", code); return; } boolean allocated = Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode()); BigDecimal afterBalance; try { R settleResult = allocated ? vinManagementClient.settleBalance(order.getVinId(), lockedAmount, actual) : groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual); if (settleResult == null || settleResult.getCode() != R.SUCCESS) { log.error("[VD结算] 账户结算失败 code={} accountMode={} locked={} actual={} msg={}", code, order.getAccountMode(), lockedAmount, actual, settleResult == null ? "null" : settleResult.getMsg()); return; } afterBalance = settleResult.getData(); } catch (Exception ex) { log.error("[VD结算] 账户结算异常 code={} accountMode={} locked={} actual={}", code, order.getAccountMode(), lockedAmount, actual, ex); return; } recordVinConsumeAllocate(order, actual, lockedAmount, afterBalance, code); log.info("[VD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code, order.getAccountMode(), lockedAmount, actual, afterBalance); } /** * 写企业额度分配记录-消费流水(分配模式记该VIN账户;共享模式记企业账户): * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程 */ private void recordVinConsumeAllocate(TVinChargingOrder order, BigDecimal actual, BigDecimal lockedAmount, BigDecimal afterBalance, String code) { if (order.getCompanyId() == null) { return; } BigDecimal after = afterBalance == null ? ZERO_AMOUNT : afterBalance; BigDecimal before = after.add(actual).subtract(lockedAmount); boolean allocated = Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode()); String userInfo; String userName; if (allocated) { userInfo = order.getExpectedVin(); userName = order.getPlateNum(); } else { TGroupCompany company = queryCompanyQuietly(order.getCompanyId()); userInfo = company == null || StringUtils.isEmpty(company.getName()) ? String.valueOf(order.getCompanyId()) : company.getName(); userName = company == null ? null : company.getName(); } try { R result = companyAllocateRecordClient.recordConsume(order.getCompanyId(), allocated ? TCompanyAllocateRecord.USER_TYPE_VIN : TCompanyAllocateRecord.USER_TYPE_COMPANY, userInfo, userName, before, actual.negate(), code, "VIN码充电结算"); if (result == null || result.getCode() != R.SUCCESS) { log.error("[VD结算] 消费流水写入失败 code={} companyId={} amount={} msg={}", code, order.getCompanyId(), actual.negate(), result == null ? "null" : result.getMsg()); } } catch (Exception ex) { log.error("[VD结算] 消费流水写入异常 code={} companyId={} amount={}", code, order.getCompanyId(), actual.negate(), ex); } } /** * 查询公司(失败返回 null,仅用于流水展示) */ private TGroupCompany queryCompanyQuietly(Integer companyId) { try { R result = groupCompanyClient.getCompanyById(companyId); return result == null ? null : result.getData(); } catch (Exception ex) { log.error("[VD结算] 查询公司失败 companyId={}", companyId, ex); return null; } } /** * 保存VIN订单计费策略快照(主表 + 明细表) */ private void saveStrategySnapshot(Long orderId, Integer strategyId) { if (strategyId == null) { return; } TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData(); List details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData(); if (strategy == null || details == null || details.isEmpty()) { return; } TVinAccountingStrategyOrder strategyOrder = new TVinAccountingStrategyOrder(); BeanUtils.copyProperties(strategy, strategyOrder); strategyOrder.setChargingOrderId(orderId); vinAccountingStrategyOrderService.save(strategyOrder); List detailOrders = new ArrayList<>(); for (TAccountingStrategyDetail detail : details) { TVinAccountingStrategyDetailOrder detailOrder = new TVinAccountingStrategyDetailOrder(); BeanUtils.copyProperties(detail, detailOrder); 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() .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 (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) { return; } if (vo.getStartup_result() != null && vo.getStartup_result() == 1) { boolean updated = lambdaUpdate() .eq(TVinChargingOrder::getId, order.getId()) .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING) .set(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING) .set(TVinChargingOrder::getStartTime, LocalDateTime.now()) .update(); if (!updated) { return; } updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功"); log.info("[VD启动] VIN订单进入充电中 code={}", order.getCode()); order.setStatus(ORDER_STATUS_CHARGING); pushRegulatoryAsync(order, true, false); } else { closeTimeoutStartingOrder(order); log.warn("[VD启动] VIN订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result()); } } @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; } boolean updated; if (advanced) { updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() .eq(TVinChargingOrder::getId, order.getId()) .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)); } else { updated = updateById(update); } if (advanced && updated) { updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "握手恢复"); order.setStatus(ORDER_STATUS_CHARGING); pushRegulatoryAsync(order, true, false); log.info("[VD握手] 握手推进订单进入充电中 code={}", orderCode); } return updated; } @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 (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus()) && !Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus())) { return; } // 充电中授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 下发,规避数据传输延迟) if (Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus()) && needAuthorizeStop(order)) { stopByAuthorizeExpired(order); return; } TVinChargingOrder update = new TVinChargingOrder(); update.setId(order.getId()); boolean starting = Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus()); if (starting) { update.setStatus(ORDER_STATUS_CHARGING); if (order.getStartTime() == null) { update.setStartTime(LocalDateTime.now()); } } if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0 && query.getOutput_voltage() != null) { BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage()) .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP); update.setChargingPower(power); update.setCurrent(query.getOutput_current()); update.setVoltage(query.getOutput_voltage()); update.setPower(power); update.setNeedElec(query.getOutput_current()); TChargingGun gun = new TChargingGun(); gun.setId(order.getChargingGunId()); gun.setChargingPower(power); chargingGunClient.updateChargingGunById(gun); } if (query.getCharging_degree() != null) { update.setChargingCapacity(query.getCharging_degree()); update.setElectricity(query.getCharging_degree()); } boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() .eq(TVinChargingOrder::getId, order.getId()) .in(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING)); if (starting && updated) { updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复"); } } /** * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟): * 授权时长不足提前量时按授权时长的一半提前,避免刚启动即被停充; * maxEndTime 为空(全天授权/未开启自动停充)时不介入。 */ private boolean needAuthorizeStop(TVinChargingOrder order) { LocalDateTime maxEndTime = order.getMaxEndTime(); if (maxEndTime == null) { return false; } LocalDateTime startTime = order.getStartTime() != null ? order.getStartTime() : order.getCreateTime(); LocalDateTime now = LocalDateTime.now(); if (startTime == null || !now.isAfter(startTime)) { return false; } long remainSeconds = Duration.between(now, maxEndTime).getSeconds(); if (remainSeconds <= 0) { return true; } long authorizeSeconds = Duration.between(startTime, maxEndTime).getSeconds(); long aheadSeconds = Math.min(AUTHORIZE_STOP_AHEAD_SECONDS, Math.max(authorizeSeconds / 2, 1)); return remainSeconds <= aheadSeconds; } /** * 授权时间到期主动停充:复用小程序主动停充链路(状态4 + 结束方式1 + 下发停止充电命令), * 状态推进使用条件更新,与小程序停充/设备回调并发时只有一方生效 */ private void stopByAuthorizeExpired(TVinChargingOrder order) { TChargingGun gun; TChargingPile pile; try { R gunResult = chargingGunClient.getChargingGunById(order.getChargingGunId()); gun = gunResult == null ? null : gunResult.getData(); pile = order.getChargingPileId() == null ? null : Optional.ofNullable(chargingPileClient.getChargingPileById(order.getChargingPileId()).getData()) .orElse(null); } catch (Exception ex) { log.error("[VD停充] 授权到期停充查询设备异常 code={}", order.getCode(), ex); return; } if (gun == null || pile == null) { log.error("[VD停充] 授权到期停充缺少设备信息 code={} gunId={}", order.getCode(), order.getChargingGunId()); return; } boolean updated = lambdaUpdate() .eq(TVinChargingOrder::getId, order.getId()) .eq(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING) .set(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING) .set(TVinChargingOrder::getEndMode, 1) .set(TVinChargingOrder::getEndTime, LocalDateTime.now()) .update(); if (!updated) { return; } order.setStatus(ORDER_STATUS_STOPPING); order.setEndMode(1); PlatformStopCharging command = new PlatformStopCharging(); command.setCharging_pile_code(pile.getCode()); command.setCharging_gun_code(gun.getCode()); sendMessageClient.platformStopCharging(command); log.warn("[VD停充] 授权时间到期主动停充 code={} maxEndTime={}", order.getCode(), order.getMaxEndTime()); } @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("订单已关闭,跳过结算"); } List reportedPeriods = TransactionStageParser.parsePeriodElectricity(vo); BigDecimal totalElectricity = reportedPeriods.stream() .map(TransactionStageParser.PeriodElectricity::getElectricity) .reduce(BigDecimal.ZERO, BigDecimal::add); if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) { totalElectricity = vo.getTotal_electricity(); } List snapshots = new ArrayList<>(vinAccountingStrategyDetailOrderService .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, order.getId()) .list()); snapshots.removeIf(item -> item.getType() == null); snapshots.sort(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime, Comparator.nullsLast(String::compareTo))); // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费) CompanyPreferentialDTO preferential = loadCompanyPreferential(order.getCompanyId()); TBillingTemplateDTO billingTemplate = loadBillingTemplate(preferential); BigDecimal electrovalenceTotal = BigDecimal.ZERO; BigDecimal serviceChargeTotal = BigDecimal.ZERO; BigDecimal discountElectricTotal = BigDecimal.ZERO; BigDecimal discountServiceTotal = BigDecimal.ZERO; List details = new ArrayList<>(); // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。 for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) { TVinAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex); BigDecimal capacity = TransactionStageParser.getPeriodElectricity( vo, snapshot.getType(), snapshotIndex + 1); if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) { continue; } BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence(); BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge(); BigDecimal originalServicePrice = snapshot.getCostServiceCharge() == null ? BigDecimal.ZERO : snapshot.getCostServiceCharge(); // 原价金额(策略单价),优惠金额 = 原价 - 最终 BigDecimal originalElectricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP); // 优惠后的最终单价:方式2=服务费按比例优惠;方式3=电费/服务费取计费模板同阶段单价 BigDecimal finalElectricPrice = electricPrice; BigDecimal finalServicePrice = servicePrice; if (preferential != null && Integer.valueOf(PREFERENTIAL_TYPE_RATE).equals(preferential.getPreferentialType())) { finalServicePrice = applyServiceDiscountRate(servicePrice, preferential.getServiceDiscountRate()); } else if (preferential != null && Integer.valueOf(PREFERENTIAL_TYPE_TEMPLATE).equals(preferential.getPreferentialType())) { TBillingTemplateDetail templateDetail = findTemplateDetail(billingTemplate, snapshot.getType()); if (templateDetail == null) { log.warn("[VD结算] 计费模板缺少对应阶段,该段按原价结算 code={} type={} templateId={}", code, snapshot.getType(), preferential.getBillingTemplateId()); } else { if (templateDetail.getElectrovalence() != null) { finalElectricPrice = templateDetail.getElectrovalence(); } if (templateDetail.getServiceCharge() != null) { finalServicePrice = templateDetail.getServiceCharge(); } } } BigDecimal electricAmount = capacity.multiply(finalElectricPrice).setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal serviceAmount = capacity.multiply(finalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal discountElectricAmount = originalElectricAmount.subtract(electricAmount); BigDecimal discountServiceAmount = originalServiceAmount.subtract(serviceAmount); electrovalenceTotal = electrovalenceTotal.add(electricAmount); serviceChargeTotal = serviceChargeTotal.add(serviceAmount); discountElectricTotal = discountElectricTotal.add(discountElectricAmount); discountServiceTotal = discountServiceTotal.add(discountServiceAmount); TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy(); detail.setChargingOrderId(order.getId()); detail.setType(snapshot.getType()); detail.setElectrovalence(finalElectricPrice); detail.setServiceCharge(finalServicePrice); detail.setCostServiceCharge(originalServicePrice); detail.setChargingCapacity(capacity); detail.setPeriodElectricPrice(electricAmount); detail.setPeriodServicePrice(serviceAmount); detail.setPeriodOriginalServicePrice(originalServiceAmount); detail.setPeriodOriginalElectricPrice(originalElectricAmount); detail.setPeriodDiscountElectricPrice(discountElectricAmount); detail.setPeriodDiscountServicePrice(discountServiceAmount); if (snapshot != null) { // 计费策略明细id:快照id(IdType.INPUT复制自原明细 id),与CD结算口径一致 detail.setAccountingStrategyDetailId(snapshot.getId()); // 时段位:与 CD 充电 TaskUtil.endOrder 同口径,截取快照时段的 HH:mm // (snapshot 已为 HH:mm;split 兜底兼容上游拼接的 yyyy-MM-dd HH:mm 情形) detail.setStartTime(splitPeriodHourMinute(snapshot.getStartTime())); detail.setEndTime(splitPeriodHourMinute(snapshot.getEndTime())); } // 首次添加时间:结算落库时刻(CD结算同口径) detail.setCreateTime(LocalDateTime.now()); details.add(detail); } if (snapshots.isEmpty()) { log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity); } // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。 LocalDateTime settlementStartTime = parseDeviceTime(vo.getStart_time(), order.getStartTime()); LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime()); if (!details.isEmpty()) { DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm"); if (settlementStartTime != null) { details.get(0).setStartTime(settlementStartTime.format(hhmm)); } if (settlementEndTime != null) { details.get(details.size() - 1).setEndTime(settlementEndTime.format(hhmm)); } } BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal); // lambda 引用要求有效final,拷贝最终值 final BigDecimal totalElec = totalElectricity; final BigDecimal elecTotal = electrovalenceTotal; final BigDecimal svcTotal = serviceChargeTotal; final BigDecimal discountElecTotal = discountElectricTotal; final BigDecimal discountSvcTotal = discountServiceTotal; final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc"); final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc"); // 本地事务:写计费明细 + 更新订单(状态5;账户结算在事务外执行,避免长事务占用链接) 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.setDiscountElectrovalence(discountElecTotal); update.setDiscountServiceCharge(discountSvcTotal); if (startSoc != null) { update.setStartSoc(startSoc); } if (endSoc != null) { update.setEndSoc(endSoc); } update.setEndTime(parseDeviceTime(vo.getEnd_time(), LocalDateTime.now())); if (order.getStartTime() == null) { update.setStartTime(parseDeviceTime(vo.getStart_time(), null)); } update.setStatus(ORDER_STATUS_FINISHED); if (order.getEndMode() == null) { update.setEndMode(1); } updateById(update); return true; }); // 账户结算:解锁申请时锁定的额度、按实际消费扣款并退回差额,同时写分配记录消费流水 settleVinChargeAccount(order, orderAmount, code); // 复位充电枪 resetGunQuietly(order); // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致) pushRegulatoryAsync(order, false, true); log.info("[VD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={} discountElec={} discountSvc={}", code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal, discountServiceTotal); return R.ok(); } @Override public TVinChargingOrder getLatestStoppingByGunId(Integer gunId) { if (gunId == null) { return null; } return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .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) { updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位"); } private boolean updateGunStatusQuietly(TVinChargingOrder order, int status, String scene) { if (order.getChargingGunId() == null) { return true; } try { R queryResult = chargingGunClient.getChargingGunById(order.getChargingGunId()); TChargingGun gun = queryResult == null ? null : queryResult.getData(); if (queryResult == null || queryResult.getCode() != R.SUCCESS || gun == null) { log.error("[VD资源] {}查询充电枪失败 code={} gunId={}", scene, order.getCode(), order.getChargingGunId()); return false; } gun.setStatus(status); R result = chargingGunClient.updateChargingGunById(gun); if (result == null || result.getCode() != R.SUCCESS) { log.error("[VD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(), order.getChargingGunId(), result == null ? "null" : result.getMsg()); return false; } return true; } catch (Exception ex) { log.error("[VD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex); return false; } } /** * 容错读取订单最早/最新监控记录的 SOC;Mongo 数据缺失或查询异常不阻断主结算流程。 */ private BigDecimal getMonitoringSocQuietly(String code, String sort) { if (monitoringDataService == null) { return null; } try { UploadRealTimeMonitoringData monitoringData = monitoringDataService.getDataByOrderCode(code, sort); return monitoringData == null || monitoringData.getSoc() == null ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue()); } catch (Exception ex) { log.error("[VD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex); return null; } } private LocalDateTime parseDeviceTime(String value, LocalDateTime fallback) { if (StringUtils.isEmpty(value)) { return fallback; } DateTimeFormatter formatter = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd HH:mm:ss") .optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd() .toFormatter(); try { return LocalDateTime.parse(value.trim().replace('T', ' '), formatter); } catch (DateTimeParseException ex) { log.warn("[VD结算] 设备时间格式异常 value={}", value); return fallback; } } /** * 截取快照时段位 HH:mm(对齐 CD 充电 TaskUtil.endOrder 中 detailOrder.getStartTime().split(" ")[1]): * 1) snapshot 已为 HH:mm 直接返回; * 2) 若被上游拼接为 yyyy-MM-dd HH:mm:ss / yyyy-MM-dd HH:mm 等带日期格式,按空格 split 取末段。 */ private static String splitPeriodHourMinute(String value) { if (value == null) { return null; } String trimmed = value.trim(); if (trimmed.isEmpty()) { return trimmed; } int spaceIdx = trimmed.lastIndexOf(' '); return spaceIdx < 0 ? trimmed : trimmed.substring(spaceIdx + 1); } // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) ===== @Override public List listTimeoutStartingOrders() { return lambdaQuery() .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING) .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(VD_STARTING_TIMEOUT_MINUTES)) .list(); } @Override public List listTimeoutChargingOrders() { return lambdaQuery() .in(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING) .list(); } @Override public void closeTimeoutStartingOrder(TVinChargingOrder order) { TVinChargingOrder update = new TVinChargingOrder(); update.setStatus(ORDER_STATUS_START_FAILED); update.setEndMode(0); update.setEndTime(LocalDateTime.now()); boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() .eq(TVinChargingOrder::getId, order.getId()) .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)); if (!closed) { log.info("[VD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode()); return; } // 释放申请时锁定的账户额度(未产生消费) unlockVinChargeContextQuietly(order, "启动关单"); 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); int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId()); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number(order.getCode()); TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity); vo.setTotal_electricity(capacity); log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}", order.getCode(), flatPeriodIndex, capacity); vinEndChargeBillingCharge(vo); } private int resolveCompensationFlatPeriodIndex(Long orderId) { List snapshots = Optional.ofNullable(vinAccountingStrategyDetailOrderService .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, orderId).list()) .orElse(Collections.emptyList()).stream() .filter(item -> item.getType() != null) .sorted(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime, Comparator.nullsLast(String::compareTo))) .collect(Collectors.toList()); for (int i = 0; i < snapshots.size(); i++) { if (Integer.valueOf(3).equals(snapshots.get(i).getType())) { return i + 1; } } log.error("[VD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId); return 1; } }