| | |
| | | import com.ruoyi.account.api.model.TAppCoupon; |
| | | import com.ruoyi.account.api.model.TAppUser; |
| | | import com.ruoyi.account.api.model.TAppUserIntegralChange; |
| | | import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO; |
| | | import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; |
| | | import com.ruoyi.chargingPile.api.feignClient.SiteClient; |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.chargingPile.api.model.Site; |
| | | import com.ruoyi.chargingPile.api.model.TBillingTemplateDetail; |
| | | import com.ruoyi.chargingPile.api.model.TChargingGun; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.utils.StringUtils; |
| | |
| | | import com.ruoyi.order.api.vo.TransactionRecordMessageVO; |
| | | import com.ruoyi.order.service.*; |
| | | import com.ruoyi.order.service.impl.ChargingCouponSupport; |
| | | import com.ruoyi.order.support.ChargeAccountSupport; |
| | | import com.ruoyi.order.util.mongodb.service.TransactionRecordService; |
| | | import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService; |
| | | import com.ruoyi.other.api.domain.TCoupon; |
| | | import com.ruoyi.other.api.domain.TIntegralRule; |
| | | import com.ruoyi.other.api.domain.TVip; |
| | | import com.ruoyi.other.api.dto.CompanyPreferentialDTO; |
| | | import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO; |
| | | import com.ruoyi.other.api.feignClient.IntegralRuleClient; |
| | | import com.ruoyi.other.api.feignClient.OtherClient; |
| | |
| | | private SiteClient siteClient; |
| | | @Resource |
| | | private TChargingOrderService chargingOrderService; |
| | | |
| | | @Resource |
| | | private TCardChargingOrderService cardChargingOrderService; |
| | | |
| | | @Resource |
| | | private TVinChargingOrderService vinChargingOrderService; |
| | | |
| | | @Resource |
| | | private TChargingOrderRefundService chargingOrderRefundService; |
| | |
| | | |
| | | @Resource |
| | | private RedissonClient redissonClient; |
| | | |
| | | @Resource |
| | | private ChargeAccountSupport chargeAccountSupport; |
| | | |
| | | |
| | | |
| | |
| | | chargingOrderService.updateById(order); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 实时监控静默阈值:超过该时间仍无交易记录时按异常断电补偿 */ |
| | | private static final long MONITORING_SILENT_MILLIS = TimeUnit.MINUTES.toMillis(10); |
| | | /** 无任何设备数据时的最长保护时间,避免 Mongo 短暂延迟造成误关单 */ |
| | | private static final long NO_DEVICE_DATA_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(24); |
| | | |
| | | /** |
| | | * 刷卡/VIN 订单补偿(10分钟一轮):真实交易记录优先统一结算;启动中有实时数据则恢复充电中; |
| | | * 仅在设备数据缺失或静默达到保护阈值后执行关单/累计电量兜底结算。 |
| | | */ |
| | | @Scheduled(fixedRate = 1000 * 60 * 10, initialDelay = 1000 * 60) |
| | | public void cardVinOrderCompensationTask() { |
| | | if (isScheduledTaskPort(port)) { |
| | | compensateCardOrders(); |
| | | compensateVinOrders(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * VIN 结算失败重试(1分钟一轮,结算失败主触发):处理账户结算失败回退的状态4订单, |
| | | * 与10分钟补偿任务(DB兜底扫描)互补;重试幂等由账户侧消费流水保证,不会重复扣款。 |
| | | */ |
| | | @Scheduled(fixedRate = 1000 * 60, initialDelay = 1000 * 30) |
| | | public void vinSettleRetryTask() { |
| | | if (isScheduledTaskPort(port)) { |
| | | try { |
| | | vinChargingOrderService.retryPendingVinSettle(); |
| | | } catch (Exception e) { |
| | | log.error("[VD结算重试] 定时任务异常", e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void compensateCardOrders() { |
| | | try { |
| | | int recovered = cardChargingOrderService.recoverFinishedOrdersWithoutDeduct(); |
| | | if (recovered > 0) { |
| | | log.warn("[SD扣款补偿] 本轮回退未扣款已结束订单数量:{}", recovered); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[SD扣款补偿] 已结束未扣款扫描异常", e); |
| | | } |
| | | try { |
| | | for (TCardChargingOrder order : cardChargingOrderService.listTimeoutStartingOrders()) { |
| | | compensateCardStartingOrder(order); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[SD补偿] 启动超时扫描异常", e); |
| | | } |
| | | try { |
| | | for (TCardChargingOrder order : cardChargingOrderService.listTimeoutChargingOrders()) { |
| | | compensateCardActiveOrder(order); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[SD补偿] 活动订单扫描异常", e); |
| | | } |
| | | } |
| | | |
| | | private void compensateCardStartingOrder(TCardChargingOrder order) { |
| | | try { |
| | | TransactionRecord record = findCompleteTransactionRecord(order.getCode()); |
| | | if (record != null) { |
| | | cardChargingOrderService.cardEndChargeBillingCharge(toTransactionRecordVO(record)); |
| | | return; |
| | | } |
| | | UploadRealTimeMonitoringData latest = uploadRealTimeMonitoringDataService.getLastDataById(order.getCode()); |
| | | if (latest != null && (latest.getCreate_time() == null |
| | | || System.currentTimeMillis() - latest.getCreate_time().getTime() <= MONITORING_SILENT_MILLIS)) { |
| | | cardChargingOrderService.handleHandshakeVin(order.getCode(), null); |
| | | log.info("[SD补偿] 启动应答缺失但存在活跃实时数据,恢复充电中 code={}", order.getCode()); |
| | | return; |
| | | } |
| | | cardChargingOrderService.closeTimeoutStartingOrder(order); |
| | | } catch (Exception ex) { |
| | | log.error("[SD补偿] 启动订单处理异常,保留原状态待下轮重试 code={}", order.getCode(), ex); |
| | | } |
| | | } |
| | | |
| | | private void compensateCardActiveOrder(TCardChargingOrder order) { |
| | | try { |
| | | TransactionRecord record = findCompleteTransactionRecord(order.getCode()); |
| | | if (record != null) { |
| | | cardChargingOrderService.cardEndChargeBillingCharge(toTransactionRecordVO(record)); |
| | | return; |
| | | } |
| | | UploadRealTimeMonitoringData latest = uploadRealTimeMonitoringDataService.getLastDataById(order.getCode()); |
| | | if (shouldFallbackSettle(latest, order.getCreateTime(), false)) { |
| | | cardChargingOrderService.compensationSettle(order); |
| | | } |
| | | } catch (Exception ex) { |
| | | log.error("[SD补偿] 活动订单处理异常,保留原状态待下轮重试 code={}", order.getCode(), ex); |
| | | } |
| | | } |
| | | |
| | | private void compensateVinOrders() { |
| | | try { |
| | | for (TVinChargingOrder order : vinChargingOrderService.listTimeoutStartingOrders()) { |
| | | compensateVinStartingOrder(order); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[VD补偿] 启动超时扫描异常", e); |
| | | } |
| | | try { |
| | | for (TVinChargingOrder order : vinChargingOrderService.listTimeoutChargingOrders()) { |
| | | compensateVinActiveOrder(order); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[VD补偿] 活动订单扫描异常", e); |
| | | } |
| | | } |
| | | |
| | | private void compensateVinStartingOrder(TVinChargingOrder order) { |
| | | try { |
| | | TransactionRecord record = findCompleteTransactionRecord(order.getCode()); |
| | | if (record != null) { |
| | | vinChargingOrderService.vinEndChargeBillingCharge(toTransactionRecordVO(record)); |
| | | return; |
| | | } |
| | | UploadRealTimeMonitoringData latest = uploadRealTimeMonitoringDataService.getLastDataById(order.getCode()); |
| | | if (latest != null && (latest.getCreate_time() == null |
| | | || System.currentTimeMillis() - latest.getCreate_time().getTime() <= MONITORING_SILENT_MILLIS)) { |
| | | vinChargingOrderService.handleHandshakeVin(order.getCode(), null); |
| | | log.info("[VD补偿] 启动应答缺失但存在活跃实时数据,恢复充电中 code={}", order.getCode()); |
| | | return; |
| | | } |
| | | vinChargingOrderService.closeTimeoutStartingOrder(order); |
| | | } catch (Exception ex) { |
| | | log.error("[VD补偿] 启动订单处理异常,保留原状态待下轮重试 code={}", order.getCode(), ex); |
| | | } |
| | | } |
| | | |
| | | private void compensateVinActiveOrder(TVinChargingOrder order) { |
| | | try { |
| | | TransactionRecord record = findCompleteTransactionRecord(order.getCode()); |
| | | if (record != null) { |
| | | vinChargingOrderService.vinEndChargeBillingCharge(toTransactionRecordVO(record)); |
| | | return; |
| | | } |
| | | UploadRealTimeMonitoringData latest = uploadRealTimeMonitoringDataService.getLastDataById(order.getCode()); |
| | | boolean stopping = Integer.valueOf(4).equals(order.getStatus()); |
| | | if (shouldFallbackSettle(latest, order.getCreateTime(), stopping)) { |
| | | vinChargingOrderService.compensationSettle(order); |
| | | } |
| | | } catch (Exception ex) { |
| | | log.error("[VD补偿] 活动订单处理异常,保留原状态待下轮重试 code={}", order.getCode(), ex); |
| | | } |
| | | } |
| | | |
| | | private TransactionRecord findCompleteTransactionRecord(String code) { |
| | | TransactionRecord record = transactionRecordService.findOne(code); |
| | | return record != null && StringUtils.isNotEmpty(record.getResult()) ? record : null; |
| | | } |
| | | |
| | | private TransactionRecordMessageVO toTransactionRecordVO(TransactionRecord record) { |
| | | TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); |
| | | BeanUtils.copyProperties(record, vo); |
| | | return vo; |
| | | } |
| | | |
| | | static boolean shouldFallbackSettle(UploadRealTimeMonitoringData latest, LocalDateTime createTime, boolean stopping) { |
| | | if (latest != null && latest.getCreate_time() != null) { |
| | | return stopping || System.currentTimeMillis() - latest.getCreate_time().getTime() > MONITORING_SILENT_MILLIS; |
| | | } |
| | | if (stopping) { |
| | | return true; |
| | | } |
| | | return createTime != null |
| | | && ChronoUnit.MILLIS.between(createTime, LocalDateTime.now()) > NO_DEVICE_DATA_TIMEOUT_MILLIS; |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | //企业账户流程:白名单企业账户订单按企业优惠结算(方式2=服务费比例优惠,方式3=按计费模板同阶段单价) |
| | | boolean enterpriseSettlement = chargingOrderService.isWhitelistEnterpriseOrder(order); |
| | | CompanyPreferentialDTO enterprisePreferential = enterpriseSettlement |
| | | ? chargeAccountSupport.loadCompanyPreferential(order.getCompanyId()) : null; |
| | | Integer enterprisePreferentialType = null == enterprisePreferential ? null : enterprisePreferential.getPreferentialType(); |
| | | TBillingTemplateDTO enterpriseTemplate = enterpriseSettlement |
| | | ? chargeAccountSupport.loadBillingTemplate(enterprisePreferential) : null; |
| | | BigDecimal enterpriseElectricDiscount = BigDecimal.ZERO; |
| | | BigDecimal enterpriseServiceDiscount = BigDecimal.ZERO; |
| | | |
| | | SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMddHHmm"); |
| | | List<TChargingOrderAccountingStrategy> list = new ArrayList<>(); |
| | | for (AccountingStrategyDetailOrder detailOrder : detailOrders) { |
| | |
| | | chargingOrderAccountingStrategy.setEndTime(detailOrder.getEndTime().split(" ")[1]); |
| | | |
| | | //已充电总度数 |
| | | BigDecimal electrovalenc = detailOrder.getElectrovalence().multiply(subtract); |
| | | BigDecimal finalElectricPrice = detailOrder.getElectrovalence(); |
| | | BigDecimal finalServicePrice = detailOrder.getServiceCharge(); |
| | | //企业账户流程:按企业优惠计算最终用电单价 |
| | | if (enterpriseSettlement) { |
| | | if (Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE).equals(enterprisePreferentialType)) { |
| | | //方式2:服务费按比例优惠 |
| | | finalServicePrice = ChargeAccountSupport.applyServiceDiscountRate(finalServicePrice, enterprisePreferential.getServiceDiscountRate()); |
| | | } else if (Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE).equals(enterprisePreferentialType)) { |
| | | //方式3:按计费模板同电价阶段单价 |
| | | TBillingTemplateDetail templateDetail = ChargeAccountSupport.findTemplateDetail(enterpriseTemplate, detailOrder.getType()); |
| | | if (null == templateDetail) { |
| | | log.error("[CD补偿结算] 计费模板缺少对应电价阶段,该阶段按原价结算 code={} type={}", order.getCode(), detailOrder.getType()); |
| | | } else { |
| | | if (null != templateDetail.getElectrovalence()) { |
| | | finalElectricPrice = templateDetail.getElectrovalence(); |
| | | } |
| | | if (null != templateDetail.getServiceCharge()) { |
| | | finalServicePrice = templateDetail.getServiceCharge(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | BigDecimal electrovalenc = finalElectricPrice.multiply(subtract); |
| | | BigDecimal originalServicePrice = detailOrder.getServiceCharge().multiply(subtract); |
| | | BigDecimal serviceCharge = originalServicePrice; |
| | | BigDecimal serviceCharge = finalServicePrice.multiply(subtract); |
| | | BigDecimal vipDiscountAmount = BigDecimal.ZERO; |
| | | //计算优惠金额;余额支付(含组合支付)不参与会员折扣 |
| | | if (!isWhitelistOrder(order) && !isBalancePayMode(order.getPayMethod()) && null != order.getVipDiscount()) { |
| | | //计算优惠金额;企业账户流程按企业优惠结算,其余订单中余额支付(含组合支付)不参与会员折扣 |
| | | if (enterpriseSettlement) { |
| | | //企业优惠金额(原价-优惠价),用于订单优惠统计与 whitelist_* 快照 |
| | | enterpriseElectricDiscount = enterpriseElectricDiscount.add(detailOrder.getElectrovalence().multiply(subtract).subtract(electrovalenc)); |
| | | enterpriseServiceDiscount = enterpriseServiceDiscount.add(originalServicePrice.subtract(serviceCharge)); |
| | | } else if (!isWhitelistOrder(order) && !isBalancePayMode(order.getPayMethod()) && null != order.getVipDiscount()) { |
| | | //0.58折 |
| | | vipDiscountAmount = serviceCharge.multiply(new BigDecimal(1).subtract(order.getVipDiscount())); |
| | | serviceCharge = serviceCharge.multiply(order.getVipDiscount()); |
| | | } |
| | | //企业账户流程:明细单价记录优惠后单价,金额与单价口径一致 |
| | | if (enterpriseSettlement) { |
| | | chargingOrderAccountingStrategy.setElectrovalence(finalElectricPrice); |
| | | chargingOrderAccountingStrategy.setServiceCharge(finalServicePrice); |
| | | } |
| | | chargingOrderAccountingStrategy.setPeriodElectricPrice(electrovalenc.setScale(4, RoundingMode.HALF_EVEN)); |
| | | chargingOrderAccountingStrategy.setPeriodServicePrice(serviceCharge.setScale(4, RoundingMode.HALF_EVEN)); |
| | |
| | | BigDecimal electricDiscount = whitelistRefundSplit[0]; |
| | | BigDecimal serviceDiscount = whitelistRefundSplit[1]; |
| | | BigDecimal whitelistDiscount = electricDiscount.add(serviceDiscount).setScale(4, RoundingMode.HALF_EVEN); |
| | | if (whitelistDiscount.compareTo(BigDecimal.ZERO) > 0) { |
| | | if (enterpriseSettlement) { |
| | | // 企业账户流程:企业优惠已在明细阶段计入金额,这里只做统计快照(金额口径与实时结算一致) |
| | | electricDiscount = enterpriseElectricDiscount.max(BigDecimal.ZERO).setScale(4, RoundingMode.HALF_EVEN); |
| | | serviceDiscount = enterpriseServiceDiscount.max(BigDecimal.ZERO).setScale(4, RoundingMode.HALF_EVEN); |
| | | whitelistDiscount = electricDiscount.add(serviceDiscount); |
| | | } else if (whitelistDiscount.compareTo(BigDecimal.ZERO) > 0) { |
| | | // 补偿任务需与实时结算使用同一拆分口径,避免财务导出金额不一致。 |
| | | periodElectricPrice_total = periodElectricPrice_total.subtract(electricDiscount).max(BigDecimal.ZERO); |
| | | serviceCharge_total = serviceCharge_total.subtract(serviceDiscount).max(BigDecimal.ZERO); |
| | |
| | | serviceCharge_total = serviceCharge_total.subtract(couponDiscount); |
| | | } |
| | | order.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistServiceRefundAmount(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | //企业账户流程:企业优惠金额与 whitelist_* 三列快照双写(订单优惠统计沿用 whitelist_* 口径) |
| | | if (enterpriseSettlement) { |
| | | order.setDiscountElectrovalence(electricDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setDiscountServiceCharge(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistServiceRefundAmount(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | } else { |
| | | order.setWhitelistDiscountAmount(whitelistDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistElectricRefundAmount(electricDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setWhitelistServiceRefundAmount(serviceDiscount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | } |
| | | order.setElectrovalence(periodElectricPrice_total.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setServiceCharge(serviceCharge_total.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundAmount(refundAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundStatus(1); |
| | | if (enterpriseSettlement) { |
| | | // 企业账户流程:资金来自企业账户额度(差额结算时退回企业账户),订单无用户侧退款,实付金额=实际消费金额 |
| | | order.setBalancePayAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundStatus(2); |
| | | refundAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_EVEN); |
| | | } else { |
| | | order.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundAmount(refundAmount.setScale(2, RoundingMode.HALF_EVEN)); |
| | | order.setRefundStatus(1); |
| | | } |
| | | UploadRealTimeMonitoringData asc = uploadRealTimeMonitoringDataService.getDataByOrderCode(order.getCode(), "asc"); |
| | | UploadRealTimeMonitoringData desc = uploadRealTimeMonitoringDataService.getDataByOrderCode(order.getCode(), "desc"); |
| | | if(null != asc){ |
| | |
| | | } |
| | | chargingOrderService.updateById(order); |
| | | |
| | | //企业账户流程:结算企业额度(按实际消费扣减锁定额度、差额退回企业账户)并写消费流水 |
| | | if (enterpriseSettlement && !chargingOrderService.settleWhitelistEnterpriseAccount(order, payAmount)) { |
| | | // 结算失败回退为停止中,交由补偿任务下一轮重新结算,避免企业额度长期冻结 |
| | | order.setStatus(4); |
| | | chargingOrderService.updateById(order); |
| | | log.error("[CD补偿结算] 企业账户额度结算失败,订单回退停止中等待重试 code={}", order.getCode()); |
| | | return; |
| | | } |
| | | |
| | | order = chargingOrderService.getById(order.getId()); |
| | | Site site = siteClient.getSiteByIds(Arrays.asList(order.getSiteId())).getData().get(0); |
| | | //推送三方平台 |