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) {
|
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<ChargingOrderVO> page = new PageInfo<>(pageCurr, pageSize);
|
long total = baseMapper.count(query);
|
page.setTotal(total);
|
List<ChargingOrderVO> records = total == 0 ? Collections.emptyList()
|
: baseMapper.pageList(query, (long) (pageCurr - 1) * pageSize, pageSize);
|
fillExtra(records);
|
page.setRecords(records);
|
return page;
|
}
|
|
@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);
|
List<ChargingOrderVO> result = new ArrayList<>();
|
for (long offset = 0; offset < total; offset += 5000) {
|
List<ChargingOrderVO> batch = baseMapper.pageList(query, offset, 5000);
|
if (batch == null || batch.isEmpty()) {
|
break;
|
}
|
result.addAll(batch);
|
}
|
fillExtra(result);
|
fillRealtimeData(result);
|
return result;
|
}
|
|
@Override
|
public Map<String, Object> summary(VinChargingOrderQuery query) {
|
setAuthSiteIds(query);
|
Map<String, Object> result = baseMapper.summary(query);
|
return result == null ? new HashMap<>() : result;
|
}
|
|
@Override
|
public Map<String, Object> detail(Long chargingOrderId) {
|
TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
|
if (order == null) {
|
return Collections.emptyMap();
|
}
|
Map<String, Object> result = new HashMap<>();
|
result.put("id", order.getId() == null ? null : String.valueOf(order.getId()));
|
result.put("code", order.getCode());
|
result.put("vin", order.getActualVin());
|
result.put("actualVin", order.getActualVin());
|
result.put("expectedVin", order.getExpectedVin());
|
result.put("plateNum", order.getPlateNum());
|
result.put("phone", order.getPhone());
|
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<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());
|
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<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
|
TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
|
return order == null ? Collections.emptyList() : baseMapper.accountingStrategyList(chargingOrderId);
|
}
|
|
private void setAuthSiteIds(VinChargingOrderQuery query) {
|
List<Integer> 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<Integer> 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<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;
|
}
|
List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
Map<Long, Integer> periodCounts = new HashMap<>();
|
if (!orderIds.isEmpty()) {
|
for (Map<String, Object> 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<Integer> siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
List<Integer> gunIds = list.stream().map(ChargingOrderVO::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 (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);
|
}
|
}
|
|
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 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);
|
}
|
}
|