puzhibing
2026-09-30 67d25f3d9eb0124c7aab544bf8f77031d6d8adc3
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java
@@ -26,14 +26,17 @@
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
 * VIN码/刷卡充电公共账户支持(VIN 与刷卡共用,防止两套逻辑漂移):
 * 1) 企业充电授权窗口解析(时间方式=全天/指定时段/指定电价时段)与授权截止时间计算;
 * 2) 企业优惠设置装载(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费);
 * 3) 授权时间到期停充判定(提前量 = min(60秒, 授权时长/2));
 * 4) 企业额度分配记录-消费流水写入(charge_type=2,按订单号+变更类型幂等,失败不影响主流程);
 * 3) 授权时间到期停充判定(提前量 = min(60秒, 授权总时长));
 * 4) 企业额度分配记录-消费流水写入与已结算订单号查询(charge_type=2,按订单号+变更类型幂等,失败不影响主流程);
 * 5) 充电账户上下文(账户模式/锁定额度/授权截止时间/失败码)承载。
 */
@Component
@@ -47,7 +50,7 @@
    public static final int ACCOUNT_MODE_ALLOCATED = 2;
    /** 未配置充电授权时的默认下发金额上限(元) */
    public static final BigDecimal DEFAULT_DISPATCH_AMOUNT = new BigDecimal("200");
    /** 授权时间到期停充提前量(秒):剩余授权时间不足该值时提前下发停充,规避数据传输延迟 */
    /** 授权时间到期停充提前量(秒):固定提前量,剩余授权时间不足该值即下发停充(授权总时长更短时按总时长提前) */
    public static final int AUTHORIZE_STOP_AHEAD_SECONDS = 60;
    /** 授权时间方式:全天 */
    public static final int AUTHORIZE_TIME_MODE_ALL_DAY = 1;
@@ -361,8 +364,8 @@
    }
    /**
     * 授权时间到期停充判定:剩余授权时间不足提前量时需主动停充;
     * 授权时长不足提前量时按授权时长的一半提前,避免刚启动即被停充;
     * 授权时间到期停充判定:剩余授权时间不足固定提前量(60 秒)时需主动停充,规避数据传输延迟;
     * 授权总时长不足提前量时按整个授权时长提前(表现为启动后即触发停充),确保授权截止时间前结束充电;
     * maxEndTime 为空(全天授权/未开启自动停充)时不介入。
     */
    public boolean needAuthorizeStop(LocalDateTime maxEndTime, LocalDateTime startTime) {
@@ -378,7 +381,7 @@
            return true;
        }
        long authorizeSeconds = Duration.between(startTime, maxEndTime).getSeconds();
        long aheadSeconds = Math.min(AUTHORIZE_STOP_AHEAD_SECONDS, Math.max(authorizeSeconds / 2, 1));
        long aheadSeconds = Math.min(AUTHORIZE_STOP_AHEAD_SECONDS, Math.max(authorizeSeconds, 1));
        return remainSeconds <= aheadSeconds;
    }
@@ -396,6 +399,28 @@
    }
    /**
     * 批量查询已落消费流水的订单号(订单侧兜底扫描双判据使用)
     *
     * @return 已结算订单号集合;查询失败返回 null 表示无法确认,调用方须按「不得回退订单状态」保守处理
     */
    public Set<String> listConsumedOrderCodesQuietly(List<String> orderCodes) {
        if (orderCodes == null || orderCodes.isEmpty()) {
            return Collections.emptySet();
        }
        try {
            R<List<String>> result = companyAllocateRecordClient.listConsumedOrderCodes(orderCodes);
            if (result == null || result.getCode() != R.SUCCESS || result.getData() == null) {
                log.error("[充电账户] 查询已结算订单号失败 msg={}", result == null ? "null" : result.getMsg());
                return null;
            }
            return new HashSet<>(result.getData());
        } catch (Exception ex) {
            log.error("[充电账户] 查询已结算订单号异常", ex);
            return null;
        }
    }
    /**
     * 写企业额度分配记录-消费流水(订单号+change_type 幂等):失败不影响结算主流程
     *
     * @param amount 本次变更金额(消费传负数)