| | |
| | | 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.domain.AjaxResult; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import com.ruoyi.common.security.service.TokenService; |
| | | import com.ruoyi.order.api.dto.DeviceStartChargingDTO; |
| | |
| | | result.put("discountServiceCharge", order.getDiscountServiceCharge()); |
| | | // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造) |
| | | result.put("accountMode", order.getAccountMode()); |
| | | result.put("maxEndTime", order.getMaxEndTime()); |
| | | // Map 值不走实体字段上的 @JsonFormat,显式格式化,避免前端展示 ISO 格式(含 T) |
| | | result.put("maxEndTime", order.getMaxEndTime() == null ? null |
| | | : order.getMaxEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
| | | result.put("lockedAmount", order.getLockedAmount()); |
| | | 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", "-"); |
| | | // SOC 展示口径:优先展示结算回填的开始/结束 SOC;历史订单未采集时保持占位符 |
| | | result.put("startSoc", order.getStartSoc() == null ? "-" : order.getStartSoc().toPlainString()); |
| | | result.put("endSoc", order.getEndSoc() == null ? "-" : order.getEndSoc().toPlainString()); |
| | | if (order.getStartTime() != null && order.getEndTime() != null) { |
| | | result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime())); |
| | | } |
| | |
| | | @Override |
| | | public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) { |
| | | return baseMapper.accountingStrategyList(chargingOrderId); |
| | | } |
| | | |
| | | /** |
| | | * 管理后台停止刷卡充电:与 VIN 码停充口径一致, |
| | | * 状态推进使用条件更新(仅当前状态为充电中时生效),防止重复点击/设备回调并发。 |
| | | */ |
| | | @Override |
| | | public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) { |
| | | Long orderId; |
| | | try { |
| | | orderId = Long.valueOf(chargingOrderId); |
| | | } catch (NumberFormatException e) { |
| | | return AjaxResult.error("刷卡充电订单ID无效"); |
| | | } |
| | | TCardChargingOrder order = getAuthorizedOrder(orderId); |
| | | if (order == null) { |
| | | return AjaxResult.error("刷卡充电订单不存在或无权操作"); |
| | | } |
| | | if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) { |
| | | return AjaxResult.error("充电枪与订单不匹配"); |
| | | } |
| | | Integer status = order.getStatus(); |
| | | if (status == null || status != ORDER_STATUS_CHARGING) { |
| | | return AjaxResult.error(status != null && (status == ORDER_STATUS_STOPPING || status == ORDER_STATUS_FINISHED) |
| | | ? "不能重复操作" : "还未开始充电"); |
| | | } |
| | | TChargingGun chargingGun; |
| | | TChargingPile chargingPile; |
| | | try { |
| | | R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(chargingGunId); |
| | | chargingGun = gunResult == null ? null : gunResult.getData(); |
| | | R<TChargingPile> pileResult = order.getChargingPileId() == null ? null |
| | | : chargingPileClient.getChargingPileById(order.getChargingPileId()); |
| | | chargingPile = pileResult == null ? null : pileResult.getData(); |
| | | } catch (Exception ex) { |
| | | log.error("[SD停充] 管理后台停充查询设备异常 code={} gunId={}", order.getCode(), chargingGunId, ex); |
| | | return AjaxResult.error("查询充电设备失败"); |
| | | } |
| | | if (chargingGun == null || chargingPile == null) { |
| | | return AjaxResult.error("未找到订单对应的充电设备"); |
| | | } |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING) |
| | | .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING) |
| | | .set(TCardChargingOrder::getEndMode, 1) |
| | | .set(TCardChargingOrder::getEndTime, LocalDateTime.now()) |
| | | .update(); |
| | | if (!updated) { |
| | | return AjaxResult.error("订单状态已变化,请刷新后重试"); |
| | | } |
| | | order.setStatus(ORDER_STATUS_STOPPING); |
| | | order.setEndMode(1); |
| | | PlatformStopCharging command = new PlatformStopCharging(); |
| | | command.setCharging_pile_code(chargingPile.getCode()); |
| | | command.setCharging_gun_code(chargingGun.getCode()); |
| | | sendMessageClient.platformStopCharging(command); |
| | | log.warn("[SD停充] 管理后台主动停充 code={} gunId={}", order.getCode(), chargingGunId); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @Override |
| | |
| | | siteIds.add(-1); |
| | | } |
| | | query.setSiteIds(siteIds); |
| | | } |
| | | |
| | | /** |
| | | * 按订单ID查询并校验数据范围(订单所属站点须在当前登录用户授权站点内),与 VIN 码停充校验口径一致 |
| | | */ |
| | | private TCardChargingOrder getAuthorizedOrder(Long chargingOrderId) { |
| | | if (chargingOrderId == null) { |
| | | return null; |
| | | } |
| | | TCardChargingOrder order = getById(chargingOrderId); |
| | | if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) { |
| | | return null; |
| | | } |
| | | return order; |
| | | } |
| | | |
| | | private List<Integer> getAuthorizedSiteIds() { |
| | | Long userId = tokenService.getLoginUser().getUserid(); |
| | | if (userId == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()) |
| | | .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private void setAuthSiteIds(CardBalanceDetailQuery query) { |
| | |
| | | order.setCreateTime(LocalDateTime.now()); |
| | | save(order); |
| | | |
| | | // 5. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足) |
| | | // 5. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析): |
| | | // 无可用策略(未配置/已被删除、明细为空)或保存异常时无法计费,直接回滚删除订单并回告启动失败, |
| | | // 避免产生无策略订单(结算侧的价格兜底仅用于历史订单) |
| | | try { |
| | | if (!saveStrategySnapshot(order.getId(), strategyId)) { |
| | | log.warn("[SD建单] 无可用计费策略,回滚删除订单 code={} strategyId={}", order.getCode(), strategyId); |
| | | removeOrderWithSnapshot(order.getId()); |
| | | return DeviceStartChargingVO.fail(null); |
| | | } |
| | | } catch (Exception snapshotEx) { |
| | | log.error("[SD建单] 保存计费策略快照异常,回滚删除订单 orderId={} strategyId={}", order.getId(), |
| | | strategyId, snapshotEx); |
| | | removeOrderWithSnapshot(order.getId()); |
| | | return DeviceStartChargingVO.fail(null); |
| | | } |
| | | |
| | | // 6. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足) |
| | | if (!lockCardChargeContext(card, chargeContext)) { |
| | | removeById(order.getId()); |
| | | removeOrderWithSnapshot(order.getId()); |
| | | return DeviceStartChargingVO.fail(3); |
| | | } |
| | | // 5.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放 |
| | | // 6.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放 |
| | | TCardChargingOrder accountUpdate = new TCardChargingOrder(); |
| | | accountUpdate.setId(order.getId()); |
| | | accountUpdate.setLockedAmount(chargeContext.getDispatchAmount()); |
| | | // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更) |
| | | chargeAccountSupport.fillPreferentialSnapshot(accountUpdate, card.getCompanyId()); |
| | | updateById(accountUpdate); |
| | | order.setLockedAmount(chargeContext.getDispatchAmount()); |
| | | |
| | | // 6. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析) |
| | | try { |
| | | saveStrategySnapshot(order.getId(), strategyId); |
| | | } catch (Exception snapshotEx) { |
| | | log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx); |
| | | } |
| | | |
| | | // 7. 锁卡(条件更新:仅空闲卡可锁;失败释放账户锁定额并回滚删除订单与快照) |
| | | 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()); |
| | | unlockCardChargeContextQuietly(order, "锁卡失败"); |
| | | 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()); |
| | | removeOrderWithSnapshot(order.getId()); |
| | | return DeviceStartChargingVO.fail(4); |
| | | } |
| | | log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={} accountMode={} lockAmount={} maxEndTime={}", |
| | |
| | | * 1) 企业状态校验(未归属/不存在/停用 -> 失败码2),账户模式取企业配置(默认共享); |
| | | * 2) 企业充电授权校验:站点范围(超范围 -> 失败码2)、可用时段(不在时段 -> 失败码2),未配置授权按放行处理; |
| | | * 3) 可用余额校验(共享=企业可用余额,分配=卡可用余额,<=0 -> 失败码3); |
| | | * 4) 下发金额 = min(授权下发额, 可用余额),锁定额度与下发金额一致;授权截止时间用于监控提前停充。 |
| | | * 4) 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定额度与下发金额一致;授权截止时间用于监控提前停充。 |
| | | */ |
| | | private ChargeAccountSupport.ChargeContext resolveCardChargeContext(Integer siteId, Integer strategyId, |
| | | TChargingCard card) { |
| | |
| | | return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足"); |
| | | } |
| | | |
| | | // 4. 下发金额 = min(授权下发额, 可用余额),锁定金额与下发金额一致 |
| | | BigDecimal dispatchAmount = authorization == null || authorization.getDispatchAmount() == null |
| | | ? ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT : authorization.getDispatchAmount(); |
| | | if (dispatchAmount.compareTo(usable) > 0) { |
| | | // 4. 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定金额与下发金额一致 |
| | | BigDecimal dispatchAmount; |
| | | if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(accountMode)) { |
| | | dispatchAmount = usable; |
| | | } else { |
| | | dispatchAmount = authorization == null || authorization.getDispatchAmount() == null |
| | | ? ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT : authorization.getDispatchAmount(); |
| | | if (dispatchAmount.compareTo(usable) > 0) { |
| | | dispatchAmount = usable; |
| | | } |
| | | } |
| | | if (dispatchAmount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足"); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 释放账户锁定额(建单失败/回滚场景):共享=企业账户,分配=卡账户;失败仅告警,留待补偿任务/人工处理 |
| | | * 释放账户锁定额(建单失败/回滚/关单场景):共享=企业账户,分配=卡账户; |
| | | * 订单侧 lock_released 条件更新抢占,保证同一订单的锁定额在多路径/重复补偿下只释放一次; |
| | | * 释放失败时把标记回滚为未释放,留待补偿任务/人工重试 |
| | | */ |
| | | private void unlockCardChargeContextQuietly(TCardChargingOrder order, String scene) { |
| | | if (order == null || order.getCompanyId() == null) { |
| | | if (order == null || order.getId() == null || order.getCompanyId() == null) { |
| | | return; |
| | | } |
| | | BigDecimal amount = order.getLockedAmount(); |
| | | if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return; |
| | | } |
| | | boolean claimed; |
| | | try { |
| | | claimed = lambdaUpdate() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getLockReleased, 0) |
| | | .set(TCardChargingOrder::getLockReleased, 1) |
| | | .update(); |
| | | } catch (Exception ex) { |
| | | log.error("[SD建单] 抢占锁定额释放标记异常 scene={} code={}", scene, order.getCode(), ex); |
| | | return; |
| | | } |
| | | if (!claimed) { |
| | | log.info("[SD建单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, order.getCode()); |
| | | return; |
| | | } |
| | | boolean unlocked = false; |
| | | try { |
| | | R<BigDecimal> unlockResult; |
| | | if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode())) { |
| | |
| | | } else { |
| | | unlockResult = groupCompanyClient.unlockBalance(order.getCompanyId(), amount); |
| | | } |
| | | if (unlockResult == null || unlockResult.getCode() != R.SUCCESS) { |
| | | unlocked = unlockResult != null && unlockResult.getCode() == R.SUCCESS; |
| | | if (!unlocked) { |
| | | log.error("[SD建单] 释放账户锁定额失败 scene={} code={} amount={} msg={}", scene, order.getCode(), |
| | | amount, unlockResult == null ? "null" : unlockResult.getMsg()); |
| | | } |
| | | } catch (Exception ex) { |
| | | log.error("[SD建单] 释放账户锁定额异常 scene={} code={} amount={}", scene, order.getCode(), amount, ex); |
| | | } |
| | | if (!unlocked) { |
| | | lambdaUpdate() |
| | | .eq(TCardChargingOrder::getId, order.getId()) |
| | | .eq(TCardChargingOrder::getLockReleased, 1) |
| | | .set(TCardChargingOrder::getLockReleased, 0) |
| | | .update(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 回滚删除订单及其计费策略快照(建单失败场景;快照可能尚未写入,删除为幂等操作) |
| | | */ |
| | | private void removeOrderWithSnapshot(Long orderId) { |
| | | if (orderId == null) { |
| | | return; |
| | | } |
| | | try { |
| | | cardAccountingStrategyDetailOrderService.remove( |
| | | new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>() |
| | | .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId)); |
| | | cardAccountingStrategyOrderService.remove( |
| | | new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>() |
| | | .eq(TCardAccountingStrategyOrder::getChargingOrderId, orderId)); |
| | | } catch (Exception ex) { |
| | | log.error("[SD建单] 清理计费策略快照异常 orderId={}", orderId, ex); |
| | | } |
| | | removeById(orderId); |
| | | } |
| | | |
| | | /** |
| | | * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致 |
| | | * |
| | | * @return 是否成功写入快照(策略ID为空、策略/明细不存在或明细为空、持久化失败均返回 false,调用方需回滚建单) |
| | | */ |
| | | private void saveStrategySnapshot(Long orderId, Integer strategyId) { |
| | | private boolean saveStrategySnapshot(Long orderId, Integer strategyId) { |
| | | if (strategyId == null) { |
| | | return; |
| | | return false; |
| | | } |
| | | TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData(); |
| | | List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData(); |
| | | if (strategy == null || details == null || details.isEmpty()) { |
| | | return; |
| | | return false; |
| | | } |
| | | TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder(); |
| | | BeanUtils.copyProperties(strategy, strategyOrder); |
| | | strategyOrder.setChargingOrderId(orderId); |
| | | cardAccountingStrategyOrderService.save(strategyOrder); |
| | | boolean mainSaved = cardAccountingStrategyOrderService.save(strategyOrder); |
| | | List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>(); |
| | | for (TAccountingStrategyDetail detail : details) { |
| | | TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder(); |
| | |
| | | detailOrder.setChargingOrderId(orderId); |
| | | detailOrders.add(detailOrder); |
| | | } |
| | | cardAccountingStrategyDetailOrderService.saveBatch(detailOrders); |
| | | boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders); |
| | | return mainSaved && detailSaved; |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | /** |
| | | * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟): |
| | | * 授权时长不足提前量时按授权时长的一半提前,避免刚启动即被停充; |
| | | * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电; |
| | | * maxEndTime 为空(全天授权/未开启自动停充)时不介入。 |
| | | */ |
| | | private boolean needAuthorizeStop(TCardChargingOrder order) { |
| | |
| | | snapshots.sort(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime, |
| | | Comparator.nullsLast(String::compareTo))); |
| | | |
| | | // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费) |
| | | CompanyPreferentialDTO preferential = chargeAccountSupport.loadCompanyPreferential(order.getCompanyId()); |
| | | TBillingTemplateDTO billingTemplate = chargeAccountSupport.loadBillingTemplate(preferential); |
| | | // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费): |
| | | // 订单快照优先(建单时固化,策略后续变更不影响本单结算口径),快照为空的历史订单实时查询并在本事务回填 |
| | | ChargeAccountSupport.PreferentialSnapshot preferential = chargeAccountSupport.resolvePreferential(order); |
| | | TBillingTemplateDTO billingTemplate = preferential == null ? null : preferential.getTemplate(); |
| | | |
| | | BigDecimal electrovalenceTotal = BigDecimal.ZERO; |
| | | BigDecimal serviceChargeTotal = BigDecimal.ZERO; |
| | |
| | | final BigDecimal svcTotal = serviceChargeTotal; |
| | | final BigDecimal discountElecTotal = discountElectricTotal; |
| | | final BigDecimal discountSvcTotal = discountServiceTotal; |
| | | // SOC 回填(与 VIN 结算同口径):取订单最早/最新实时监控上报,读取失败不阻断结算 |
| | | final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc"); |
| | | final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc"); |
| | | // 剩余金额口径:可用金额基数(建单写入的 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); |
| | | final ChargeAccountSupport.PreferentialSnapshot preferentialSnapshot = preferential; |
| | | final boolean preferentialBackfill = preferentialSnapshot != null && preferentialSnapshot.needBackfill(); |
| | | |
| | | // 本地事务:写计费明细 + 更新订单(状态5,允许负余额金额记录) |
| | | transactionTemplate.execute(status -> { |
| | |
| | | if (order.getEndMode() == null) { |
| | | update.setEndMode(1); |
| | | } |
| | | // SOC 回填:监控数据缺失/查询异常时不覆盖既有值,与 VIN 结算行为一致 |
| | | if (startSoc != null) { |
| | | update.setStartSoc(startSoc); |
| | | } |
| | | if (endSoc != null) { |
| | | update.setEndSoc(endSoc); |
| | | } |
| | | if (preferentialBackfill) { |
| | | // 历史订单(快照为空)实时查询结果回填订单快照列,保证后续补偿重算口径一致 |
| | | update.setPreferentialType(preferentialSnapshot.getPreferentialType()); |
| | | update.setServiceDiscountRate(preferentialSnapshot.getServiceDiscountRate()); |
| | | update.setBillingTemplateId(preferentialSnapshot.getBillingTemplateId()); |
| | | update.setBillingTemplateSnapshot( |
| | | ChargeAccountSupport.serializeTemplateSnapshot(preferentialSnapshot.getTemplate())); |
| | | update.setPreferentialSnapshotTime(LocalDateTime.now()); |
| | | } |
| | | updateById(update); |
| | | return true; |
| | | }); |
| | |
| | | code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal, |
| | | discountServiceTotal); |
| | | return R.ok(); |
| | | } |
| | | |
| | | /** |
| | | * 容错读取订单最早/最新监控记录的 SOC(与 VIN 结算同口径): |
| | | * Mongo 数据缺失、SOC 未上报或查询异常返回 null,不阻断主结算流程。 |
| | | */ |
| | | private BigDecimal getMonitoringSocQuietly(String code, String sort) { |
| | | if (uploadRealTimeMonitoringDataService == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(code, sort); |
| | | return monitoringData == null || monitoringData.getSoc() == null |
| | | ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue()); |
| | | } catch (Exception ex) { |
| | | log.error("[SD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | afterBalance = settleResult.getData().getAfterBalance(); |
| | | } else { |
| | | // 订单侧自行写消费流水,此处不传订单号(账户侧仅在订单号非空时做幂等与流水,避免重复记账) |
| | | // 共享模式:传订单号启用账户侧结算幂等(账户侧按订单号+变更类型落企业消费流水并作为已结算标记) |
| | | R<BigDecimal> settleResult = |
| | | groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual, |
| | | null, null, null, null); |
| | | code, order.getCardNo(), null, "刷卡充电结算"); |
| | | if (settleResult == null || settleResult.getCode() != R.SUCCESS) { |
| | | log.error("[SD结算] 企业账户结算失败 code={} companyId={} locked={} actual={} msg={}", code, |
| | | order.getCompanyId(), lockedAmount, actual, |
| | |
| | | lockedAmount, actual, ex); |
| | | return false; |
| | | } |
| | | recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code); |
| | | if (allocated) { |
| | | // 分配模式消费流水记卡账户;共享模式消费流水已由账户侧 settleBalance 按订单号写入,避免重复记账 |
| | | recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code); |
| | | } |
| | | log.info("[SD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code, |
| | | order.getAccountMode(), lockedAmount, actual, afterBalance); |
| | | return true; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 写企业额度分配记录-消费流水(分配模式记该卡账户;共享模式记企业账户): |
| | | * 写企业额度分配记录-消费流水(仅卡侧扣款场景:分配模式结算与历史订单卡扣款,记该卡账户): |
| | | * 共享模式消费流水由账户侧 settleBalance 按订单号写入(同一订单号+变更类型只能有一个写入方,避免重复记账); |
| | | * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程 |
| | | */ |
| | | private void recordCardConsumeAllocate(TCardChargingOrder order, BigDecimal actual, BigDecimal lockedAmount, |
| | |
| | | } |
| | | BigDecimal after = afterBalance == null ? BigDecimal.ZERO : afterBalance; |
| | | BigDecimal before = after.add(actual).subtract(lockedAmount); |
| | | boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode()); |
| | | String userInfo; |
| | | String userName; |
| | | if (allocated) { |
| | | userInfo = order.getCardNo(); |
| | | userName = order.getCardNo(); |
| | | } else { |
| | | TGroupCompany company = chargeAccountSupport.queryCompanyQuietly(order.getCompanyId()); |
| | | userInfo = company == null || StringUtils.isEmpty(company.getName()) |
| | | ? String.valueOf(order.getCompanyId()) : company.getName(); |
| | | userName = company == null ? null : company.getName(); |
| | | } |
| | | chargeAccountSupport.recordConsume(order.getCompanyId(), |
| | | allocated ? TCompanyAllocateRecord.USER_TYPE_CARD : TCompanyAllocateRecord.USER_TYPE_COMPANY, |
| | | userInfo, userName, before, actual.negate(), code, "刷卡充电结算"); |
| | | chargeAccountSupport.recordConsume(order.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_CARD, |
| | | order.getCardNo(), order.getCardNo(), before, actual.negate(), code, "刷卡充电结算"); |
| | | } |
| | | |
| | | private boolean rollbackFinishedToStopping(Long orderId, String code) { |
| | |
| | | |
| | | 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={}", |
| | | log.error("[SD扣款补偿] 查询卡侧扣款流水失败,本轮不修改订单 msg={}", |
| | | deductedResult == null ? "null" : deductedResult.getMsg()); |
| | | return 0; |
| | | } |
| | | java.util.Set<String> consumedCodes = chargeAccountSupport.listConsumedOrderCodesQuietly(orderCodes); |
| | | if (consumedCodes == null) { |
| | | log.error("[SD扣款补偿] 查询企业侧消费流水失败,本轮不修改订单"); |
| | | return 0; |
| | | } |
| | | |
| | | java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData()); |
| | | deductedCodes.addAll(consumedCodes); |
| | | int recovered = 0; |
| | | for (TCardChargingOrder candidate : candidates) { |
| | | if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) { |
| | |
| | | } |
| | | 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())) { |
| | | if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null) { |
| | | continue; |
| | | } |
| | | // 双判据复核:任一侧查询失败或存在流水即不回退(保守处理,避免重复扣款) |
| | | java.util.Set<String> consumedRecheck = chargeAccountSupport.listConsumedOrderCodesQuietly( |
| | | Collections.singletonList(candidate.getCode())); |
| | | if (consumedRecheck == null || recheck.getData().contains(candidate.getCode()) |
| | | || consumedRecheck.contains(candidate.getCode())) { |
| | | continue; |
| | | } |
| | | if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) { |