| | |
| | | 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.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.Comparator; |
| | | 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_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; |
| | | /** 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 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) { |
| | |
| | | if (order.getStartTime() != null && order.getEndTime() != null) { |
| | | result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime())); |
| | | } |
| | | List<UploadRealTimeMonitoringData> monitoring = uploadRealTimeMonitoringDataService.getDataByOrderCodeAsc(order.getCode()); |
| | | List<UploadRealTimeMonitoringData> monitoring = order.getCode() == null || order.getCode().trim().isEmpty() |
| | | ? Collections.emptyList() |
| | | : uploadRealTimeMonitoringDataService.getDataByOrderCodeAsc(order.getCode()); |
| | | if (monitoring == null) { |
| | | monitoring = Collections.emptyList(); |
| | | } |
| | |
| | | } |
| | | } |
| | | 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()) { |
| | |
| | | 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()); |
| | | // 可用金额基数与启动剩余金额(与返回设备的 balance 同源,供监控/结算扣减与前端展示) |
| | | order.setChargeAmount(card.getBalance()); |
| | | order.setResidualAmount(card.getBalance()); |
| | | 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 (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) { |
| | | return; |
| | | } |
| | | if (vo.getStartup_result() != null && vo.getStartup_result() == 1) { |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING) |
| | | .set(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING) |
| | | .set(TCardChargingOrder::getStartTime, LocalDateTime.now()) |
| | | .update(); |
| | | if (!updated) { |
| | | return; |
| | | } |
| | | updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功"); |
| | | log.info("[SD启动] 刷卡订单进入充电中 code={}", order.getCode()); |
| | | order.setStatus(ORDER_STATUS_CHARGING); |
| | | pushRegulatoryAsync(order, true, false); |
| | | } else { |
| | | closeTimeoutStartingOrder(order); |
| | | log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result()); |
| | | } |
| | | } |
| | | |
| | | @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; |
| | | } |
| | | boolean updated; |
| | | if (advanced) { |
| | | updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::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("[SD握手] 握手推进订单进入充电中 code={}", orderCode); |
| | | } |
| | | return updated; |
| | | } |
| | | |
| | | @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 (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus()) |
| | | && !Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus())) { |
| | | return; |
| | | } |
| | | TCardChargingOrder update = new TCardChargingOrder(); |
| | | 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()); |
| | | } |
| | | // 剩余金额口径与 CD 一致:可用金额基数(建单写入 charge_amount) - 设备已充金额 |
| | | if (query.getPaid_amount() != null && order.getChargeAmount() != null) { |
| | | update.setResidualAmount(order.getChargeAmount().subtract(query.getPaid_amount()) |
| | | .setScale(2, RoundingMode.HALF_EVEN)); |
| | | } |
| | | boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .in(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING)); |
| | | if (starting && updated) { |
| | | updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复"); |
| | | } |
| | | } |
| | | |
| | | @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) { |
| | | // 计费策略明细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 (snapshotByType.isEmpty()) { |
| | | log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity); |
| | | } |
| | | // 与 CD 充电 TaskUtil.endOrder 同口径:details 按 type 升序对齐业务时段顺序, |
| | | // 首段 start_time 用订单实际开始时间的 HH:mm 覆盖(对应 CD 的时段内首条监控数据 HH:mm), |
| | | // 末段 end_time 用订单实际结束时间的 HH:mm 覆盖(对应 CD 的时段内末条监控数据 HH:mm)。 |
| | | // 若 order.startTime/endTime 为空(结算事务内才回填),则跳过覆盖以保留快照时段位兜底。 |
| | | if (!details.isEmpty()) { |
| | | details.sort(Comparator.comparingInt(TCardChargingOrderAccountingStrategy::getType)); |
| | | DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm"); |
| | | if (order.getStartTime() != null) { |
| | | details.get(0).setStartTime(order.getStartTime().format(hhmm)); |
| | | } |
| | | if (order.getEndTime() != null) { |
| | | details.get(details.size() - 1).setEndTime(order.getEndTime().format(hhmm)); |
| | | } |
| | | } |
| | | BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal); |
| | | // lambda 引用要求有效final,拷贝最终值 |
| | | final BigDecimal totalElec = totalElectricity; |
| | | final BigDecimal elecTotal = electrovalenceTotal; |
| | | final BigDecimal svcTotal = serviceChargeTotal; |
| | | // 剩余金额口径:可用金额基数(建单写入的 charge_amount) - 本单金额; |
| | | // 改造前创建的老订单无基数时以本单金额兜底(剩余金额记 0),并顺带补齐 charge_amount |
| | | final BigDecimal chargeBase = order.getChargeAmount() != null ? order.getChargeAmount() : orderAmount; |
| | | final BigDecimal residualAmount = chargeBase.subtract(orderAmount).setScale(2, RoundingMode.HALF_EVEN); |
| | | |
| | | // 本地事务:写计费明细 + 更新订单(状态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.setBalancePayAmount(orderAmount); |
| | | update.setChargeAmount(chargeBase); |
| | | update.setResidualAmount(residualAmount); |
| | | 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; |
| | | }); |
| | | |
| | | // 回写内存订单对象,保证结算后监管推送/日志使用最新金额字段 |
| | | order.setChargeAmount(chargeBase); |
| | | order.setResidualAmount(residualAmount); |
| | | order.setBalancePayAmount(orderAmount); |
| | | order.setOrderAmount(orderAmount); |
| | | order.setPaymentAmount(orderAmount); |
| | | |
| | | // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单) |
| | | 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 boolean unlockCardQuietly(TCardChargingOrder order) { |
| | | if (order.getCardId() == null) { |
| | | return true; |
| | | } |
| | | try { |
| | | R<Boolean> result = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode()); |
| | | boolean success = result != null && result.getCode() == R.SUCCESS && Boolean.TRUE.equals(result.getData()); |
| | | if (!success) { |
| | | log.error("[SD资源] 解锁卡片失败 code={} cardId={} msg={}", order.getCode(), order.getCardId(), |
| | | result == null ? "null" : result.getMsg()); |
| | | } |
| | | return success; |
| | | } catch (Exception ex) { |
| | | log.error("[SD资源] 解锁卡片异常 code={} cardId={}", order.getCode(), order.getCardId(), ex); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void resetGunQuietly(TCardChargingOrder order) { |
| | | updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位"); |
| | | } |
| | | |
| | | private boolean updateGunStatusQuietly(TCardChargingOrder order, int status, String scene) { |
| | | if (order.getChargingGunId() == null) { |
| | | return true; |
| | | } |
| | | try { |
| | | R<TChargingGun> queryResult = chargingGunClient.getChargingGunById(order.getChargingGunId()); |
| | | TChargingGun gun = queryResult == null ? null : queryResult.getData(); |
| | | if (queryResult == null || queryResult.getCode() != R.SUCCESS || gun == null) { |
| | | log.error("[SD资源] {}查询充电枪失败 code={} gunId={}", scene, order.getCode(), order.getChargingGunId()); |
| | | return false; |
| | | } |
| | | gun.setStatus(status); |
| | | R<String> result = chargingGunClient.updateChargingGunById(gun); |
| | | if (result == null || result.getCode() != R.SUCCESS) { |
| | | log.error("[SD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(), |
| | | order.getChargingGunId(), result == null ? "null" : result.getMsg()); |
| | | return false; |
| | | } |
| | | return true; |
| | | } catch (Exception ex) { |
| | | log.error("[SD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | 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("[SD结算] 设备时间格式异常 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<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() |
| | | .in(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING) |
| | | .list(); |
| | | } |
| | | |
| | | @Override |
| | | public void closeTimeoutStartingOrder(TCardChargingOrder order) { |
| | | TCardChargingOrder update = new TCardChargingOrder(); |
| | | 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<TCardChargingOrder>() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)); |
| | | if (!closed) { |
| | | log.info("[SD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode()); |
| | | return; |
| | | } |
| | | if (!unlockCardQuietly(order)) { |
| | | // 解锁失败恢复为启动中,下轮继续补偿,避免卡片永久占用 |
| | | lambdaUpdate().eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_START_FAILED) |
| | | .set(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING) |
| | | .set(TCardChargingOrder::getEndMode, null) |
| | | .set(TCardChargingOrder::getEndTime, null) |
| | | .update(); |
| | | return; |
| | | } |
| | | 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); |
| | | } |
| | | } |