| | |
| | | private static final int SD_STARTING_TIMEOUT_MINUTES = 30; |
| | | /** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */ |
| | | private static final int SD_CHARGING_TIMEOUT_HOURS = 24; |
| | | /** 状态5未扣款扫描保护窗,避免与正在提交的扣款事务竞态 */ |
| | | private static final int SD_DEDUCT_COMPENSATION_DELAY_MINUTES = 10; |
| | | /** 单轮最多检查的已结束订单数,避免大批量远程查询 */ |
| | | private static final int SD_DEDUCT_COMPENSATION_BATCH_SIZE = 100; |
| | | |
| | | @Resource |
| | | private TokenService tokenService; |
| | |
| | | log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity); |
| | | } |
| | | // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。 |
| | | LocalDateTime settlementStartTime = order.getStartTime() != null |
| | | ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null); |
| | | LocalDateTime settlementStartTime = parseDeviceTime(vo.getStart_time(), order.getStartTime()); |
| | | LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime()); |
| | | if (!details.isEmpty()) { |
| | | DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm"); |
| | |
| | | order.setOrderAmount(orderAmount); |
| | | order.setPaymentAmount(orderAmount); |
| | | |
| | | // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单) |
| | | // 卡账户扣款(订单号幂等键;允许扣成负余额);无法确认扣款成功时回退状态4,交由补偿任务重试 |
| | | 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={}", |
| | | R<ChargingCardDeductVO> deductResult; |
| | | try { |
| | | deductResult = chargingCardClient.deductForCharging(deductDTO); |
| | | } catch (Exception ex) { |
| | | rollbackFinishedToStopping(order.getId(), code); |
| | | log.error("[SD结算] 扣款调用异常,订单回退停止中 code={} cardId={} amount={}", |
| | | code, order.getCardId(), orderAmount, ex); |
| | | return R.fail("充电卡扣款失败,订单已进入补偿队列"); |
| | | } |
| | | if (deductResult == null || deductResult.getCode() != R.SUCCESS || deductResult.getData() == null) { |
| | | rollbackFinishedToStopping(order.getId(), code); |
| | | log.error("[SD结算] 扣款失败,订单回退停止中 code={} cardId={} amount={} msg={}", |
| | | code, order.getCardId(), orderAmount, deductResult == null ? "null" : deductResult.getMsg()); |
| | | return R.fail("充电卡扣款失败,订单已进入补偿队列"); |
| | | } |
| | | if (deductResult.getData().isDuplicated()) { |
| | | log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code); |
| | | } |
| | | } |
| | | |
| | | // 解锁卡片(无论扣款成败必须解锁,失败人工介入) |
| | | // 扣款成功后解锁卡片 |
| | | unlockCardQuietly(order); |
| | | // 复位充电枪 |
| | | resetGunQuietly(order); |
| | |
| | | log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}", |
| | | code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount); |
| | | return R.ok(); |
| | | } |
| | | |
| | | private boolean rollbackFinishedToStopping(Long orderId, String code) { |
| | | boolean rolledBack = lambdaUpdate() |
| | | .eq(TCardChargingOrder::getId, orderId) |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED) |
| | | .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING) |
| | | .update(); |
| | | if (!rolledBack) { |
| | | log.warn("[SD结算] 状态5回退4未命中,订单状态可能已变化 code={}", code); |
| | | } |
| | | return rolledBack; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | @Override |
| | | public int recoverFinishedOrdersWithoutDeduct() { |
| | | List<TCardChargingOrder> candidates = lambdaQuery() |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED) |
| | | .isNotNull(TCardChargingOrder::getCardId) |
| | | .gt(TCardChargingOrder::getOrderAmount, BigDecimal.ZERO) |
| | | .lt(TCardChargingOrder::getEndTime, LocalDateTime.now().minusMinutes(SD_DEDUCT_COMPENSATION_DELAY_MINUTES)) |
| | | .orderByAsc(TCardChargingOrder::getEndTime) |
| | | .last("LIMIT " + SD_DEDUCT_COMPENSATION_BATCH_SIZE) |
| | | .list(); |
| | | if (candidates.isEmpty()) { |
| | | return 0; |
| | | } |
| | | |
| | | List<String> orderCodes = candidates.stream().map(TCardChargingOrder::getCode) |
| | | .filter(StringUtils::isNotEmpty).collect(Collectors.toList()); |
| | | R<List<String>> deductedResult = chargingCardClient.listDeductedOrderCodes(orderCodes); |
| | | if (deductedResult == null || deductedResult.getCode() != R.SUCCESS || deductedResult.getData() == null) { |
| | | log.error("[SD扣款补偿] 查询扣款流水失败,本轮不修改订单 msg={}", |
| | | deductedResult == null ? "null" : deductedResult.getMsg()); |
| | | return 0; |
| | | } |
| | | |
| | | java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData()); |
| | | int recovered = 0; |
| | | for (TCardChargingOrder candidate : candidates) { |
| | | if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) { |
| | | continue; |
| | | } |
| | | RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + candidate.getCode()); |
| | | try { |
| | | if (!settleLock.tryLock(0, 60, TimeUnit.SECONDS)) { |
| | | continue; |
| | | } |
| | | R<List<String>> recheck = chargingCardClient.listDeductedOrderCodes( |
| | | Collections.singletonList(candidate.getCode())); |
| | | if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null |
| | | || recheck.getData().contains(candidate.getCode())) { |
| | | continue; |
| | | } |
| | | if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) { |
| | | recovered++; |
| | | log.warn("[SD扣款补偿] 已结束订单无扣款流水,状态回退停止中 code={}", candidate.getCode()); |
| | | } |
| | | } catch (InterruptedException ex) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("[SD扣款补偿] 获取结算锁被中断 code={}", candidate.getCode()); |
| | | break; |
| | | } catch (Exception ex) { |
| | | log.error("[SD扣款补偿] 订单处理异常,保留原状态 code={}", candidate.getCode(), ex); |
| | | } finally { |
| | | if (settleLock.isHeldByCurrentThread()) { |
| | | settleLock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | return recovered; |
| | | } |
| | | |
| | | @Override |
| | | public void closeTimeoutStartingOrder(TCardChargingOrder order) { |
| | | TCardChargingOrder update = new TCardChargingOrder(); |
| | | update.setStatus(ORDER_STATUS_START_FAILED); |