| | |
| | | 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; |
| | |
| | | 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 渲染) */ |
| | |
| | | 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); |
| | | |
| | |
| | | if (order == null) { |
| | | return; |
| | | } |
| | | // 幂等:已进入充电中/已结束的订单不再处理 |
| | | if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_CHARGING)) { |
| | | // 只有启动中订单可消费启动应答,防止迟到消息复活已关闭/已结束订单 |
| | | if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) { |
| | | 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); |
| | | 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()); |
| | | // 推送监管平台(复用 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); |
| | | closeTimeoutStartingOrder(order); |
| | | log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result()); |
| | | pushRegulatoryAsync(order, false, false); |
| | | } |
| | | } |
| | | |
| | |
| | | if (!needUpdate) { |
| | | return true; |
| | | } |
| | | updateById(update); |
| | | boolean updated; |
| | | if (advanced) { |
| | | TChargingGun gun = new TChargingGun(); |
| | | gun.setId(order.getChargingGunId()); |
| | | gun.setStatus(GUN_STATUS_CHARGING); |
| | | chargingGunClient.updateChargingGunById(gun); |
| | | 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 true; |
| | | return updated; |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (order == null) { |
| | | return; |
| | | } |
| | | // 已结束/已关闭的订单不再更新监控数据 |
| | | if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_FINISHED)) { |
| | | // 只接收启动中/充电中的监控数据,禁止迟到数据复活终态订单 |
| | | 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()); |
| | | // 首次监控数据:启动中 -> 充电中 |
| | | if (order.getStatus() == null || order.getStatus() < ORDER_STATUS_CHARGING) { |
| | | 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()); |
| | | } |
| | | 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) { |
| | |
| | | update.setChargingCapacity(query.getCharging_degree()); |
| | | update.setElectricity(query.getCharging_degree()); |
| | | } |
| | | updateById(update); |
| | | // 剩余金额口径与 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 |
| | |
| | | detail.setPeriodServicePrice(serviceAmount); |
| | | detail.setPeriodOriginalServicePrice(originalServiceAmount); |
| | | if (snapshot != null) { |
| | | detail.setStartTime(snapshot.getStartTime()); |
| | | detail.setEndTime(snapshot.getEndTime()); |
| | | // 计费策略明细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 -> { |
| | |
| | | update.setServiceCharge(svcTotal); |
| | | update.setOrderAmount(orderAmount); |
| | | update.setPaymentAmount(orderAmount); |
| | | update.setEndTime(LocalDateTime.now()); |
| | | // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示) |
| | | 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) { |
| | |
| | | }, "SD-Regulatory-Push").start(); |
| | | } |
| | | |
| | | /** |
| | | * 解锁卡片(按订单号条件释放,失败仅告警不阻断结算流程) |
| | | */ |
| | | private void unlockCardQuietly(TCardChargingOrder order) { |
| | | private boolean unlockCardQuietly(TCardChargingOrder order) { |
| | | if (order.getCardId() == null) { |
| | | return; |
| | | return true; |
| | | } |
| | | 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()); |
| | | 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); |
| | | 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 void resetGunQuietly(TCardChargingOrder order) { |
| | | if (order.getChargingGunId() == null) { |
| | | return; |
| | | private static String splitPeriodHourMinute(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | 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); |
| | | 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> listTimeoutChargingOrders() { |
| | | return lambdaQuery() |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING) |
| | | .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusHours(SD_CHARGING_TIMEOUT_HOURS)) |
| | | .in(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING) |
| | | .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); |
| | | 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()); |