package com.ruoyi.order.service.impl;
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
|
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
|
import com.ruoyi.account.api.feignClient.AppUserCarClient;
|
import com.ruoyi.account.api.feignClient.ChargingCardClient;
|
import com.ruoyi.account.api.model.TAppUserCar;
|
import com.ruoyi.account.api.model.TChargingCard;
|
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
|
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.dto.GetSiteListDTO;
|
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.page.PageInfo;
|
import com.ruoyi.common.security.service.TokenService;
|
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
|
import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder;
|
import com.ruoyi.order.api.model.TCardAccountingStrategyOrder;
|
import com.ruoyi.order.api.model.TCardChargingOrder;
|
import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy;
|
import com.ruoyi.order.api.query.CardBalanceDetailQuery;
|
import com.ruoyi.order.api.query.CardChargingOrderQuery;
|
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
|
import com.ruoyi.order.api.vo.CardBalanceDetailVO;
|
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.TransactionRecordMessageVO;
|
import com.ruoyi.order.mapper.TCardChargingOrderMapper;
|
import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
|
import com.ruoyi.order.service.TCardAccountingStrategyOrderService;
|
import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
|
import com.ruoyi.order.service.TCardChargingOrderService;
|
import com.ruoyi.chargingPile.api.model.Site;
|
import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
|
import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
|
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.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.temporal.ChronoUnit;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.Collections;
|
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.function.Function;
|
import java.util.stream.Collectors;
|
|
@Service
|
public class TCardChargingOrderServiceImpl extends ServiceImpl<TCardChargingOrderMapper, TCardChargingOrder>
|
implements TCardChargingOrderService {
|
|
private static final Logger log = LoggerFactory.getLogger(TCardChargingOrderServiceImpl.class);
|
|
/** SD 订单号前缀(设备刷卡充电) */
|
private static final String ORDER_CODE_PREFIX_SD = "SD";
|
/** 订单状态:启动中 */
|
private static final int ORDER_STATUS_STARTING = 2;
|
/** 订单状态:充电中 */
|
private static final int ORDER_STATUS_CHARGING = 3;
|
/** 订单状态:已结束 */
|
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;
|
/** SD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
|
private static final String LOCK_SD_START_GUN = "SD_START_GUN:";
|
/** SD 建单卡锁前缀(卡ID维度,防同卡多枪并发启动,文档13.2) */
|
private static final String LOCK_SD_START_CARD = "SD_START_CARD:";
|
/** SD 结算锁前缀(订单号维度,防并发重复结算/扣款,文档13.3) */
|
private static final String LOCK_SD_SETTLE = "SD_SETTLE:";
|
/** SD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
|
private static final int SD_STARTING_TIMEOUT_MINUTES = 30;
|
/** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
|
private static final int SD_CHARGING_TIMEOUT_HOURS = 24;
|
|
@Resource
|
private TokenService tokenService;
|
@Resource
|
private ChargingMessageClient chargingMessageClient;
|
@Resource
|
private ChengDuJianGuanClient chengDuJianGuanClient;
|
@Resource
|
private SiteClient siteClient;
|
@Resource
|
private ChargingPileClient chargingPileClient;
|
@Resource
|
private ChargingGunClient chargingGunClient;
|
@Resource
|
private AppUserCarClient appUserCarClient;
|
@Resource
|
private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
|
@Resource
|
private ChargingCardClient chargingCardClient;
|
@Resource
|
private AccountingStrategyClient accountingStrategyClient;
|
@Resource
|
private AccountingStrategyDetailClient accountingStrategyDetailClient;
|
@Resource
|
private TCardAccountingStrategyOrderService cardAccountingStrategyOrderService;
|
@Resource
|
private TCardAccountingStrategyDetailOrderService cardAccountingStrategyDetailOrderService;
|
@Resource
|
private TCardChargingOrderAccountingStrategyService cardChargingOrderAccountingStrategyService;
|
@Resource
|
private TransactionTemplate transactionTemplate;
|
@Resource
|
private RedissonClient redissonClient;
|
|
@Override
|
public PageInfo<ChargingOrderVO> pageList(CardChargingOrderQuery query) {
|
String[] timeRange = parseTimeRange(query);
|
int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
|
int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize();
|
PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
|
setAuthSiteIds(query);
|
long offset = (long) (pageCurr - 1) * pageSize;
|
long total = baseMapper.count(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
|
pageInfo.setTotal(total);
|
List<ChargingOrderVO> list = total == 0 ? Collections.emptyList()
|
: baseMapper.pageList(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3], offset, pageSize);
|
fillPageExtra(list);
|
pageInfo.setRecords(list);
|
return pageInfo;
|
}
|
|
@Override
|
public Map<String, Object> summary(CardChargingOrderQuery query) {
|
String[] timeRange = parseTimeRange(query);
|
setAuthSiteIds(query);
|
Map<String, Object> result = baseMapper.summary(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
|
if (result == null) {
|
result = new HashMap<>();
|
}
|
BigDecimal platformIncome = BigDecimal.ZERO;
|
BigDecimal partnerIncome = BigDecimal.ZERO;
|
Map<String, BigDecimal> accountingSummary = emptyAccountingSummary();
|
List<Long> orderIds = baseMapper.summaryOrderIds(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
|
if (orderIds != null && !orderIds.isEmpty()) {
|
int batchSize = 500;
|
for (int i = 0; i < orderIds.size(); i += batchSize) {
|
List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
|
Map<String, Object> shareSummary = baseMapper.shareIncomeSummaryByOrderIds(batchIds);
|
platformIncome = platformIncome.add(summaryDecimal(shareSummary, "platformIncome"));
|
partnerIncome = partnerIncome.add(summaryDecimal(shareSummary, "partnerIncome"));
|
mergeAccountingSummary(accountingSummary, baseMapper.accountingSummaryByOrderIds(batchIds));
|
}
|
}
|
result.put("platformIncome", platformIncome);
|
result.put("partnerIncome", partnerIncome);
|
result.put("platformIncomeTotal", platformIncome);
|
result.put("partnerIncomeTotal", partnerIncome);
|
result.put("capacityProportion", buildProportion(accountingSummary, "capacity"));
|
result.put("serviceProportion", buildProportion(accountingSummary, "service"));
|
result.put("electronicProportion", buildProportion(accountingSummary, "electric"));
|
return result;
|
}
|
|
private Map<String, BigDecimal> emptyAccountingSummary() {
|
Map<String, BigDecimal> summary = new HashMap<>();
|
for (String prefix : new String[]{"capacity", "service", "electric"}) {
|
summary.put(prefix + "Total", BigDecimal.ZERO);
|
for (int type = 1; type <= 4; type++) {
|
summary.put(prefix + type, BigDecimal.ZERO);
|
}
|
}
|
return summary;
|
}
|
|
private void mergeAccountingSummary(Map<String, BigDecimal> total, Map<String, Object> batch) {
|
if (batch == null) {
|
return;
|
}
|
for (String key : total.keySet()) {
|
total.put(key, total.get(key).add(summaryDecimal(batch, key)));
|
}
|
}
|
|
private BigDecimal summaryDecimal(Map<String, Object> summary, String key) {
|
if (summary == null || summary.get(key) == null) {
|
return BigDecimal.ZERO;
|
}
|
Object value = summary.get(key);
|
return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
|
}
|
|
private String buildProportion(Map<String, BigDecimal> summary, String prefix) {
|
BigDecimal denominator = summary.get(prefix + "Total");
|
List<String> proportions = new ArrayList<>(4);
|
for (int type = 1; type <= 4; type++) {
|
BigDecimal percentage = denominator.signum() == 0 ? BigDecimal.ZERO
|
: summary.get(prefix + type).multiply(new BigDecimal("100"))
|
.divide(denominator, 2, RoundingMode.HALF_UP).stripTrailingZeros();
|
proportions.add(percentage.toPlainString() + "%");
|
}
|
return String.join("-", proportions);
|
}
|
|
@Override
|
public List<ChargingOrderVO> exportList(CardChargingOrderQuery query) {
|
String[] timeRange = parseTimeRange(query);
|
setAuthSiteIds(query);
|
long total = baseMapper.count(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
|
List<ChargingOrderVO> result = new ArrayList<>();
|
if (total == 0) {
|
return result;
|
}
|
int batchSize = 5000;
|
for (long offset = 0; offset < total; offset += batchSize) {
|
List<ChargingOrderVO> batch = baseMapper.pageList(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3], offset, batchSize);
|
if (batch == null || batch.isEmpty()) {
|
break;
|
}
|
result.addAll(batch);
|
}
|
fillPageExtra(result);
|
return result;
|
}
|
|
@Override
|
public Map<String, Object> detail(Long chargingOrderId) {
|
TCardChargingOrder order = getById(chargingOrderId);
|
if (order == null) {
|
return Collections.emptyMap();
|
}
|
Map<String, Object> result = new HashMap<>();
|
result.put("id", order.getId());
|
result.put("uid", String.valueOf(order.getId()));
|
result.put("code", order.getCode());
|
result.put("cardNo", order.getCardNo());
|
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());
|
result.put("total", defaultDecimal(order.getElectrovalence()).add(defaultDecimal(order.getServiceCharge())));
|
result.put("realTotal", order.getPaymentAmount());
|
result.put("endMode", order.getEndMode());
|
result.put("phone", order.getPhone());
|
result.put("licensePlate", order.getPlateNum());
|
result.put("startSoc", "-");
|
result.put("endSoc", "-");
|
if (order.getStartTime() != null && order.getEndTime() != null) {
|
result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
|
}
|
List<UploadRealTimeMonitoringData> monitoring = order.getCode() == null || order.getCode().trim().isEmpty()
|
? Collections.emptyList()
|
: uploadRealTimeMonitoringDataService.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));
|
}
|
}
|
result.put("list", monitoring);
|
UploadRealTimeMonitoringData latest = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
|
result.put("cdElectronic", latest == null || latest.getOutput_current() == null ? "" : latest.getOutput_current().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
|
result.put("cdVoltage", latest == null || latest.getOutput_voltage() == null ? "" : latest.getOutput_voltage().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
|
result.put("totalPower", latest == null || latest.getPower() == null ? "" : latest.getPower().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
|
result.put("surplus", latest == null || latest.getSoc() == null ? "" : String.valueOf(latest.getSoc()));
|
result.put("list1", baseMapper.accountingStrategyList(order.getId()));
|
List<Integer> siteIds = order.getSiteId() == null ? Collections.emptyList() : Collections.singletonList(order.getSiteId());
|
if (!siteIds.isEmpty()) {
|
List<Site> sites = Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList());
|
if (!sites.isEmpty()) {
|
result.put("siteName", sites.get(0).getName());
|
result.put("siteAddress", sites.get(0).getAddress());
|
}
|
}
|
if (order.getChargingGunId() != null) {
|
TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
|
if (gun != null) {
|
result.put("gunCode", gun.getCode());
|
result.put("terminalName", gun.getName());
|
}
|
}
|
return result;
|
}
|
|
private BigDecimal defaultDecimal(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
@Override
|
public List<Map<String, Object>> balanceIncomeDetail(CardChargingOrderQuery query) {
|
return baseMapper.balanceIncomeDetail(query);
|
}
|
|
@Override
|
public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
|
return baseMapper.accountingStrategyList(chargingOrderId);
|
}
|
|
@Override
|
public PageInfo<CardBalanceDetailVO> balanceDetailPage(CardBalanceDetailQuery 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<CardBalanceDetailVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
|
long total = baseMapper.balanceDetailCount(query);
|
pageInfo.setTotal(total);
|
if (total == 0) {
|
pageInfo.setRecords(Collections.emptyList());
|
return pageInfo;
|
}
|
long offset = (long) (pageCurr - 1) * pageSize;
|
List<CardBalanceDetailVO> list = baseMapper.balanceDetailList(query, offset, pageSize);
|
pageInfo.setRecords(list);
|
return pageInfo;
|
}
|
|
@Override
|
public Map<String, Object> balanceDetailSummary(CardBalanceDetailQuery query) {
|
setAuthSiteIds(query);
|
Map<String, Object> result = baseMapper.balanceDetailSummary(query);
|
if (result == null) {
|
result = new HashMap<>();
|
}
|
result.putIfAbsent("orderCount", 0);
|
result.putIfAbsent("incomeAmount", BigDecimal.ZERO);
|
result.putIfAbsent("expenseAmount", BigDecimal.ZERO);
|
return result;
|
}
|
|
@Override
|
public List<CardBalanceDetailVO> balanceDetailExport(CardBalanceDetailQuery query) {
|
setAuthSiteIds(query);
|
long total = baseMapper.balanceDetailCount(query);
|
if (total == 0) {
|
return new ArrayList<>();
|
}
|
List<CardBalanceDetailVO> result = new ArrayList<>();
|
int batchSize = 5000;
|
for (long offset = 0; offset < total; offset += batchSize) {
|
List<CardBalanceDetailVO> batch = baseMapper.balanceDetailList(query, offset, batchSize);
|
if (batch == null || batch.isEmpty()) {
|
break;
|
}
|
result.addAll(batch);
|
}
|
return result;
|
}
|
|
private void setAuthSiteIds(CardChargingOrderQuery query) {
|
List<Integer> siteIds = new ArrayList<>();
|
if (query.getSiteId() != null) {
|
siteIds.add(query.getSiteId());
|
} else {
|
Long userId = tokenService.getLoginUser().getUserid();
|
if (userId != null) {
|
siteIds = Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()).orElse(Collections.emptyList())
|
.stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
|
}
|
}
|
if (siteIds.isEmpty()) {
|
siteIds.add(-1);
|
}
|
query.setSiteIds(siteIds);
|
}
|
|
private void setAuthSiteIds(CardBalanceDetailQuery query) {
|
List<Integer> siteIds = new ArrayList<>();
|
Long userId = tokenService.getLoginUser().getUserid();
|
if (userId != null) {
|
siteIds = Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()).orElse(Collections.emptyList())
|
.stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
|
}
|
if (siteIds.isEmpty()) {
|
siteIds.add(-1);
|
}
|
query.setSiteIds(siteIds);
|
}
|
|
private void fillElectronicProportion(List<ChargingOrderVO> list) {
|
List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
if (orderIds.isEmpty()) {
|
return;
|
}
|
List<Map<String, Object>> capacityRows = new ArrayList<>();
|
int batchSize = 500;
|
for (int i = 0; i < orderIds.size(); i += batchSize) {
|
List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
|
capacityRows.addAll(Optional.ofNullable(baseMapper.capacitySummaryByOrderIds(batchIds))
|
.orElse(Collections.emptyList()));
|
}
|
Map<Long, Map<String, Object>> capacityByOrderId = capacityRows.stream()
|
.filter(item -> item.get("orderId") != null)
|
.collect(Collectors.toMap(item -> Long.valueOf(item.get("orderId").toString()), item -> item, (a, b) -> a));
|
for (ChargingOrderVO order : list) {
|
Map<String, Object> capacity = capacityByOrderId.get(order.getId());
|
Map<String, BigDecimal> summary = emptyAccountingSummary();
|
if (capacity != null) {
|
for (String key : new String[]{"capacityTotal", "capacity1", "capacity2", "capacity3", "capacity4"}) {
|
summary.put(key, summaryDecimal(capacity, key));
|
}
|
}
|
order.setElectronicProportion(buildProportion(summary, "capacity"));
|
}
|
}
|
|
private void fillPageExtra(List<ChargingOrderVO> list) {
|
if (list == null || list.isEmpty()) {
|
return;
|
}
|
fillElectronicProportion(list);
|
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());
|
List<Long> carIds = list.stream().map(ChargingOrderVO::getAppUserCarId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
Map<Long, TAppUserCar> cars = carIds.isEmpty() ? Collections.emptyMap()
|
: Optional.ofNullable(appUserCarClient.getCarByIds(carIds).getData()).orElse(Collections.emptyList()).stream()
|
.collect(Collectors.toMap(TAppUserCar::getId, item -> item, (a, b) -> a));
|
Map<Integer, Site> sites = 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());
|
TAppUserCar car = cars.get(item.getAppUserCarId());
|
item.setVin(car == null ? null : car.getVin());
|
if (item.getStartTime() != null && item.getEndTime() != null) {
|
item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime()));
|
}
|
UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getLastDataById(item.getCode());
|
if (monitoringData != null) {
|
item.setChargeCurrent(monitoringData.getOutput_current());
|
item.setChargeVoltage(monitoringData.getOutput_voltage());
|
item.setChargePower(monitoringData.getPower());
|
}
|
item.setPlatformIncome(BigDecimal.ZERO);
|
item.setPartnerIncome(BigDecimal.ZERO);
|
}
|
}
|
|
private String[] parseTimeRange(CardChargingOrderQuery query) {
|
return new String[]{query.getStartTime1(), query.getStartTime2(), query.getEndTime1(), query.getEndTime2()};
|
}
|
|
// ===== 设备侧刷卡充电链路(V0.2 刷卡充电) =====
|
|
@Override
|
public DeviceStartChargingVO startCardCharging(DeviceStartChargingDTO dto) {
|
if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
|
|| StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
|
return DeviceStartChargingVO.fail(1);
|
}
|
// 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
|
RLock gunLock = redissonClient.getLock(LOCK_SD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
|
try {
|
if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
|
return DeviceStartChargingVO.fail(10);
|
}
|
// 1. 充电枪校验(不存在->5 桩停用;充电中->10 该桩存在未结账记录)
|
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. 卡片鉴权(卡号存在性/禁用/密码条件校验在账户服务内完成)
|
ChargingCardAuthDTO authDTO = new ChargingCardAuthDTO();
|
authDTO.setCardNo(dto.getAccount().trim());
|
authDTO.setPassword(dto.getPassword());
|
R<TChargingCard> verifyResult = chargingCardClient.verifyCharging(authDTO);
|
TChargingCard card = verifyResult.getData();
|
if (card == null) {
|
return DeviceStartChargingVO.fail(mapAuthFailure(verifyResult.getMsg()));
|
}
|
// 3. 启动余额校验(余额>1 元方可启动,与 CD 费用不足终止阈值对齐;结算允许扣成负余额)
|
if (card.getBalance() == null || card.getBalance().compareTo(BigDecimal.ONE) <= 0) {
|
return DeviceStartChargingVO.fail(3);
|
}
|
|
// 并发防护(文档13.2):卡ID维度锁,防同卡多枪并发建单(数据库锁卡条件更新为第二道保护)
|
RLock cardLock = redissonClient.getLock(LOCK_SD_START_CARD + card.getId());
|
boolean cardLocked = false;
|
try {
|
cardLocked = cardLock.tryLock(0, 30, TimeUnit.SECONDS);
|
if (!cardLocked) {
|
return DeviceStartChargingVO.fail(4);
|
}
|
// 4. 建单(状态2 启动中)
|
TCardChargingOrder order = new TCardChargingOrder();
|
order.setCode(ORDER_CODE_PREFIX_SD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
|
order.setOrderType(2);
|
order.setOrderSource(1);
|
order.setOrderClassification(2);
|
// 充电支付模式(沿用 CD payMethod 枚举:1=余额支付):SD 全程卡余额扣款
|
order.setPayMethod(1);
|
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.setCardId(card.getId());
|
order.setCardNo(card.getCardNo());
|
order.setCreateTime(LocalDateTime.now());
|
save(order);
|
|
// 5. 计费策略快照(枪策略优先,站点策略兜底;无策略照常建单,结算按0价并告警)
|
Integer strategyId = gun.getAccountingStrategyId() != null
|
? gun.getAccountingStrategyId()
|
: (site == null ? null : site.getAccountingStrategyId());
|
try {
|
saveStrategySnapshot(order.getId(), strategyId);
|
} catch (Exception snapshotEx) {
|
log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
|
}
|
|
// 6. 锁卡(条件更新:仅空闲卡可锁;失败回滚删除订单与快照)
|
R<Boolean> lockResult = chargingCardClient.lockForCharging(card.getId(), order.getCode());
|
if (!Boolean.TRUE.equals(lockResult.getData())) {
|
log.warn("[SD建单] 锁卡失败回滚订单 code={} cardId={} msg={}", order.getCode(), card.getId(), lockResult.getMsg());
|
cardAccountingStrategyDetailOrderService.remove(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
|
.eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId()));
|
cardAccountingStrategyOrderService.remove(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
|
.eq(TCardAccountingStrategyOrder::getChargingOrderId, order.getId()));
|
removeById(order.getId());
|
return DeviceStartChargingVO.fail(4);
|
}
|
log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={}", order.getCode(), card.getCardNo(), dto.getChargingGunCode());
|
return DeviceStartChargingVO.success(order.getCode(), card.getBalance());
|
} finally {
|
if (cardLocked && cardLock.isHeldByCurrentThread()) {
|
cardLock.unlock();
|
}
|
}
|
} catch (InterruptedException ie) {
|
Thread.currentThread().interrupt();
|
log.error("[SD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
|
return DeviceStartChargingVO.fail(null);
|
} catch (Exception e) {
|
log.error("[SD建单] 刷卡充电启动异常 pile={} gun={} account={}",
|
dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
|
return DeviceStartChargingVO.fail(null);
|
} finally {
|
if (gunLock.isHeldByCurrentThread()) {
|
gunLock.unlock();
|
}
|
}
|
}
|
|
/**
|
* 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
|
*/
|
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;
|
}
|
TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
|
BeanUtils.copyProperties(strategy, strategyOrder);
|
strategyOrder.setChargingOrderId(orderId);
|
cardAccountingStrategyOrderService.save(strategyOrder);
|
List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
|
for (TAccountingStrategyDetail detail : details) {
|
TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
|
BeanUtils.copyProperties(detail, detailOrder);
|
detailOrder.setChargingOrderId(orderId);
|
detailOrders.add(detailOrder);
|
}
|
cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
|
}
|
|
/**
|
* 账户服务鉴权失败消息 -> 协议失败原因码(1=账户不存在 2=账户冻结 7=密码错误)
|
*/
|
private Integer mapAuthFailure(String msg) {
|
if (StringUtils.isNotEmpty(msg)) {
|
if (msg.contains("密码")) {
|
return 7;
|
}
|
if (msg.contains("禁用") || msg.contains("冻结")) {
|
return 2;
|
}
|
if (msg.contains("不存在")) {
|
return 1;
|
}
|
}
|
return 1;
|
}
|
|
@Override
|
public TCardChargingOrder getCardOrderByCode(String code) {
|
if (StringUtils.isEmpty(code)) {
|
return null;
|
}
|
return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardChargingOrder>()
|
.eq(TCardChargingOrder::getCode, code)
|
.last("limit 1"));
|
}
|
|
@Override
|
public void cardStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
|
if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
|
return;
|
}
|
TCardChargingOrder order = getCardOrderByCode(vo.getTransaction_serial_number());
|
if (order == null) {
|
return;
|
}
|
// 幂等:已进入充电中/已结束的订单不再处理
|
if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_CHARGING)) {
|
return;
|
}
|
TCardChargingOrder update = new TCardChargingOrder();
|
update.setId(order.getId());
|
if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
|
update.setStatus(ORDER_STATUS_CHARGING);
|
update.setStartTime(LocalDateTime.now());
|
updateById(update);
|
TChargingGun gun = new TChargingGun();
|
gun.setId(order.getChargingGunId());
|
gun.setStatus(GUN_STATUS_CHARGING);
|
chargingGunClient.updateChargingGunById(gun);
|
log.info("[SD启动] 刷卡订单进入充电中 code={}", order.getCode());
|
// 推送监管平台(复用 CD 约定:充电中推平台订单状态 + 成都监管枪状态)
|
order.setStatus(ORDER_STATUS_CHARGING);
|
pushRegulatoryAsync(order, true, false);
|
} else {
|
// 启动失败:订单关闭 + 解锁卡片 + 复位枪
|
update.setStatus(ORDER_STATUS_START_FAILED);
|
update.setEndTime(LocalDateTime.now());
|
updateById(update);
|
unlockCardQuietly(order);
|
resetGunQuietly(order);
|
log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
|
pushRegulatoryAsync(order, false, false);
|
}
|
}
|
|
@Override
|
public boolean handleHandshakeVin(String orderCode, String vin) {
|
if (StringUtils.isEmpty(orderCode)) {
|
return false;
|
}
|
TCardChargingOrder order = getCardOrderByCode(orderCode);
|
if (order == null) {
|
log.warn("[SD握手] 订单不存在 code={}", orderCode);
|
return false;
|
}
|
TCardChargingOrder update = new TCardChargingOrder();
|
update.setId(order.getId());
|
boolean needUpdate = false;
|
if (StringUtils.isNotEmpty(vin)) {
|
update.setActualVin(vin);
|
needUpdate = true;
|
}
|
boolean advanced = order.getStatus() != null && order.getStatus() == ORDER_STATUS_STARTING;
|
if (advanced) {
|
update.setStatus(ORDER_STATUS_CHARGING);
|
if (order.getStartTime() == null) {
|
update.setStartTime(LocalDateTime.now());
|
}
|
needUpdate = true;
|
}
|
if (!needUpdate) {
|
return true;
|
}
|
updateById(update);
|
if (advanced) {
|
TChargingGun gun = new TChargingGun();
|
gun.setId(order.getChargingGunId());
|
gun.setStatus(GUN_STATUS_CHARGING);
|
chargingGunClient.updateChargingGunById(gun);
|
order.setStatus(ORDER_STATUS_CHARGING);
|
pushRegulatoryAsync(order, true, false);
|
log.info("[SD握手] 握手推进订单进入充电中 code={}", orderCode);
|
}
|
return true;
|
}
|
|
@Override
|
public void cardChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
|
if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
|
return;
|
}
|
TCardChargingOrder order = getCardOrderByCode(query.getTransaction_serial_number());
|
if (order == null) {
|
return;
|
}
|
// 已结束/已关闭的订单不再更新监控数据
|
if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_FINISHED)) {
|
return;
|
}
|
TCardChargingOrder update = new TCardChargingOrder();
|
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 cardEndCharge(String orderCode, Integer endMode) {
|
if (StringUtils.isEmpty(orderCode)) {
|
return;
|
}
|
TCardChargingOrder order = getCardOrderByCode(orderCode);
|
if (order == null) {
|
return;
|
}
|
TCardChargingOrder update = new TCardChargingOrder();
|
update.setId(order.getId());
|
update.setEndMode(endMode);
|
updateById(update);
|
pushRegulatoryAsync(order, false, false);
|
}
|
|
@Override
|
public R cardEndChargeBillingCharge(TransactionRecordMessageVO vo) {
|
String code = vo == null ? null : vo.getTransaction_serial_number();
|
log.info("[SD结算] 收到交易记录 code={}", code);
|
if (StringUtils.isEmpty(code)) {
|
return R.fail("交易记录缺少交易流水号");
|
}
|
// 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
|
// 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等与扣款唯一键两层兜底
|
RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + code);
|
try {
|
if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
|
log.warn("[SD结算] 并发结算进行中,跳过重复账单 code={}", code);
|
return R.ok("重复账单,结算处理中");
|
}
|
return doCardEndChargeBillingCharge(vo, code);
|
} catch (InterruptedException ie) {
|
Thread.currentThread().interrupt();
|
return R.fail("结算锁获取被中断:" + code);
|
} finally {
|
if (settleLock.isHeldByCurrentThread()) {
|
settleLock.unlock();
|
}
|
}
|
}
|
|
/**
|
* SD 结算实际执行体(调用前须持有订单号结算锁)
|
*/
|
private R doCardEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
|
TCardChargingOrder order = getCardOrderByCode(code);
|
if (order == null) {
|
return R.fail("刷卡充电订单不存在:" + code);
|
}
|
// 幂等:已结算订单直接返回成功(重复账单/重复推送)
|
if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
|
log.warn("[SD结算] 重复账单跳过 code={}", code);
|
return R.ok("重复账单,已按首次账单结算");
|
}
|
if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
|
return R.ok("订单已关闭,跳过结算");
|
}
|
|
// 分时电量(以平台快照计价,V0.2 定稿)
|
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();
|
}
|
|
// 快照明细(1=尖 2=峰 3=平 4=谷)
|
Map<Integer, TCardAccountingStrategyDetailOrder> snapshotByType = cardAccountingStrategyDetailOrderService
|
.lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
|
.list().stream()
|
.filter(item -> item.getType() != null)
|
.collect(Collectors.toMap(TCardAccountingStrategyDetailOrder::getType, Function.identity(), (a, b) -> a));
|
|
BigDecimal electrovalenceTotal = BigDecimal.ZERO;
|
BigDecimal serviceChargeTotal = BigDecimal.ZERO;
|
List<TCardChargingOrderAccountingStrategy> details = new ArrayList<>();
|
for (Map.Entry<Integer, BigDecimal> stage : stageElectricity.entrySet()) {
|
TCardAccountingStrategyDetailOrder 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);
|
|
TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
|
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("[SD结算] 订单无计费策略快照,按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,允许负余额金额记录)
|
transactionTemplate.execute(status -> {
|
cardChargingOrderAccountingStrategyService.lambdaUpdate()
|
.eq(TCardChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
|
.remove();
|
if (!details.isEmpty()) {
|
cardChargingOrderAccountingStrategyService.saveBatch(details);
|
}
|
TCardChargingOrder update = new TCardChargingOrder();
|
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;
|
});
|
|
// 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单)
|
if (orderAmount.compareTo(BigDecimal.ZERO) > 0 && order.getCardId() != null) {
|
ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
|
deductDTO.setCardId(order.getCardId());
|
deductDTO.setOrderCode(order.getCode());
|
deductDTO.setAmount(orderAmount);
|
R<ChargingCardDeductVO> deductResult = chargingCardClient.deductForCharging(deductDTO);
|
if (deductResult != null && deductResult.getData() != null
|
&& deductResult.getData().isDuplicated()) {
|
log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
|
} else if (deductResult == null || deductResult.getCode() != R.SUCCESS) {
|
log.error("[SD结算] 扣款失败,欠费记账 code={} cardId={} amount={} msg={}",
|
code, order.getCardId(), orderAmount, deductResult == null ? "null" : deductResult.getMsg());
|
}
|
}
|
|
// 解锁卡片(无论扣款成败必须解锁,失败人工介入)
|
unlockCardQuietly(order);
|
// 复位充电枪
|
resetGunQuietly(order);
|
// 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
|
pushRegulatoryAsync(order, false, true);
|
log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
|
code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
|
return R.ok();
|
}
|
|
/**
|
* 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
|
* notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
|
* 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
|
*/
|
private void pushRegulatoryAsync(TCardChargingOrder 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("[SD监管] 推送监管平台失败 code={}", order.getCode(), ex);
|
}
|
}, "SD-Regulatory-Push").start();
|
}
|
|
/**
|
* 解锁卡片(按订单号条件释放,失败仅告警不阻断结算流程)
|
*/
|
private void unlockCardQuietly(TCardChargingOrder order) {
|
if (order.getCardId() == null) {
|
return;
|
}
|
try {
|
R<Boolean> unlockResult = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode());
|
if (!Boolean.TRUE.equals(unlockResult.getData())) {
|
log.error("[SD结算] 解锁卡片失败,需人工处理 code={} cardId={} msg={}",
|
order.getCode(), order.getCardId(), unlockResult.getMsg());
|
}
|
} catch (Exception ex) {
|
log.error("[SD结算] 解锁卡片异常 code={} cardId={}", order.getCode(), order.getCardId(), ex);
|
}
|
}
|
|
/**
|
* 复位充电枪为空闲(失败仅告警)
|
*/
|
private void resetGunQuietly(TCardChargingOrder 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("[SD结算] 复位充电枪异常 code={} gunId={}", order.getCode(), order.getChargingGunId(), ex);
|
}
|
}
|
|
// ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
|
|
@Override
|
public List<TCardChargingOrder> listTimeoutStartingOrders() {
|
return lambdaQuery()
|
.eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
|
.lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(SD_STARTING_TIMEOUT_MINUTES))
|
.list();
|
}
|
|
@Override
|
public List<TCardChargingOrder> listTimeoutChargingOrders() {
|
return lambdaQuery()
|
.eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
|
.lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusHours(SD_CHARGING_TIMEOUT_HOURS))
|
.list();
|
}
|
|
@Override
|
public void closeTimeoutStartingOrder(TCardChargingOrder order) {
|
TCardChargingOrder update = new TCardChargingOrder();
|
update.setId(order.getId());
|
update.setStatus(ORDER_STATUS_START_FAILED);
|
// 异常终止(沿用 CD 约定:启动失败/异常关单 endMode=0,避免前端结束方式显示空)
|
update.setEndMode(0);
|
update.setEndTime(LocalDateTime.now());
|
updateById(update);
|
unlockCardQuietly(order);
|
resetGunQuietly(order);
|
pushRegulatoryAsync(order, false, false);
|
log.warn("[SD补偿] 启动超时关单 code={} cardId={} gunId={}", order.getCode(), order.getCardId(), order.getChargingGunId());
|
}
|
|
@Override
|
public void compensationSettle(TCardChargingOrder 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("[SD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
|
cardEndChargeBillingCharge(vo);
|
}
|
}
|