From 64464568546b827fc5778a20efb347bf56ba34ac Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 30 九月 2026 11:05:05 +0800
Subject: [PATCH] 修改充电流程

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java             |   97 +++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java          |   34 +-
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java                       |  159 +++++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java         |   20 +
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderWhitelistEnterpriseTest.java |   86 +++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java                                 |   93 ++++-
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ChargePreferentialSnapshotTest.java             |  201 +++++++++++
 sql/20260930_company_preferential_snapshot.sql                                                                  |  152 ++++++++
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java                       |    4 
 sql/20260930_charging_order_lock_released.sql                                                                   |  107 ++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java                      |    8 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java                           |   28 +
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java     |    3 
 13 files changed, 935 insertions(+), 57 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
index f8309df..66b259d 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
@@ -51,8 +51,4 @@
     @ApiModelProperty("优惠服务费合计(原始服务费-最终服务费)")
     @TableField("discount_service_charge")
     private BigDecimal discountServiceCharge;
-
-    @ApiModelProperty("锁定额度是否已释放(0=未释放,1=已释放;建单回滚/关单路径幂等标记,防重复释放)")
-    @TableField("lock_released")
-    private Integer lockReleased;
 }
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
index 6b4dcfd..19e9da4 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
@@ -252,6 +252,34 @@
     @TableField("discount_service_charge")
     private BigDecimal discountServiceCharge;
 
+    @ApiModelProperty(value = "锁定额度是否已释放(0=未释放,1=已释放;防重复释放幂等标记)")
+    @TableField("lock_released")
+    private Integer lockReleased;
+
+    @ApiModelProperty(value = "结算完成标记(NULL/0=未结算,1=已结算;结算成功后置位,兜底扫描不回退已结算订单)")
+    @TableField("settle_status")
+    private Integer settleStatus;
+
+    @ApiModelProperty(value = "企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费;为空=建单时未固化,结算按实时查询并回填)")
+    @TableField("preferential_type")
+    private Integer preferentialType;
+
+    @ApiModelProperty(value = "服务费优惠比例快照(%,方式2使用)")
+    @TableField("service_discount_rate")
+    private BigDecimal serviceDiscountRate;
+
+    @ApiModelProperty(value = "计费模板ID快照(方式3使用)")
+    @TableField("billing_template_id")
+    private Integer billingTemplateId;
+
+    @ApiModelProperty(value = "计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)")
+    @TableField("billing_template_snapshot")
+    private String billingTemplateSnapshot;
+
+    @ApiModelProperty(value = "优惠快照固化时间")
+    @TableField("preferential_snapshot_time")
+    private LocalDateTime preferentialSnapshotTime;
+
     @ApiModelProperty(value = "会员折扣")
     @TableField("vip_discount")
     private BigDecimal vipDiscount;
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
index 03e8b09..d68924d 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -503,4 +503,12 @@
      * @param scene         调用场景(日志用)
      */
     void unlockWhitelistEnterpriseAmountQuietly(TChargingOrder chargingOrder, String scene);
+
+    /**
+     * 企业账户订单无任何设备数据/账单时的兜底关单:释放锁定额度并终结订单(金额0、状态5、结算完成标记),
+     * 保证订单不被任何补偿任务重复扫描重跑;非企业账户订单或状态不在(启动中/充电中/停止中)时直接跳过(幂等)。
+     *
+     * @param chargingOrder 充电订单(含companyId/accountMode/lockedAmount/whitelistId)
+     */
+    void closeEnterpriseOrderWithoutData(TChargingOrder chargingOrder);
 }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
index 439d08a..9457b9c 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -698,6 +698,8 @@
                 TCardChargingOrder accountUpdate = new TCardChargingOrder();
                 accountUpdate.setId(order.getId());
                 accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
+                // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
+                chargeAccountSupport.fillPreferentialSnapshot(accountUpdate, card.getCompanyId());
                 updateById(accountUpdate);
                 order.setLockedAmount(chargeContext.getDispatchAmount());
 
@@ -1224,9 +1226,10 @@
         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;
@@ -1329,6 +1332,8 @@
         // 改造前创建的老订单无基数时以本单金额兜底(剩余金额记 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 -> {
@@ -1361,6 +1366,15 @@
             if (order.getEndMode() == null) {
                 update.setEndMode(1);
             }
+            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;
         });
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
index 60e03c1..9e39242 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -1003,6 +1003,8 @@
 
     /**
      * 释放企业账户锁定额度(订单未结算/关单时调用;失败仅记录日志,不影响主流程)
+     * 释放幂等:先用条件更新抢占 lock_released 标记(0->1),保证同一订单的锁定额在多路径并发或
+     * 重复补偿下只释放一次;释放失败把标记回滚为 0,留待后续补偿或人工重试。
      */
     @Override
     public void unlockWhitelistEnterpriseAmountQuietly(TChargingOrder chargingOrder, String scene) {
@@ -1013,6 +1015,22 @@
         if (lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
             return;
         }
+        boolean claimed;
+        try {
+            claimed = lambdaUpdate()
+                    .eq(TChargingOrder::getId, chargingOrder.getId())
+                    .eq(TChargingOrder::getLockReleased, 0)
+                    .set(TChargingOrder::getLockReleased, 1)
+                    .update();
+        } catch (Exception ex) {
+            log.error("[CD关单] 抢占锁定额释放标记异常 scene={} code={}", scene, chargingOrder.getCode(), ex);
+            return;
+        }
+        if (!claimed) {
+            log.info("[CD关单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, chargingOrder.getCode());
+            return;
+        }
+        boolean unlocked = false;
         try {
             R<BigDecimal> result;
             if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(chargingOrder.getAccountMode())
@@ -1025,12 +1043,52 @@
                 log.error("[CD关单] 释放企业账户锁定额度失败 scene={} code={} companyId={} amount={} msg={}",
                         scene, chargingOrder.getCode(), chargingOrder.getCompanyId(), lockedAmount,
                         null == result ? "null" : result.getMsg());
-                return;
+            } else {
+                unlocked = true;
+                log.info("[CD关单] 释放企业账户锁定额度完成 scene={} code={} amount={}", scene, chargingOrder.getCode(), lockedAmount);
             }
-            log.info("[CD关单] 释放企业账户锁定额度完成 scene={} code={} amount={}", scene, chargingOrder.getCode(), lockedAmount);
         } catch (Exception ex) {
             log.error("[CD关单] 释放企业账户锁定额度异常 scene={} code={} companyId={} amount={}",
                     scene, chargingOrder.getCode(), chargingOrder.getCompanyId(), lockedAmount, ex);
+        }
+        if (!unlocked) {
+            // 释放失败回滚标记(1->0),保证锁定额不被“标记已释放”而漏释放,可被后续补偿重试
+            lambdaUpdate()
+                    .eq(TChargingOrder::getId, chargingOrder.getId())
+                    .eq(TChargingOrder::getLockReleased, 1)
+                    .set(TChargingOrder::getLockReleased, 0)
+                    .update();
+        }
+    }
+
+    /**
+     * 企业账户订单无任何设备数据/账单时的兜底关单:释放锁定额度并终结订单(金额0、状态5、结算完成标记),
+     * 保证订单不被任何补偿任务重复扫描重跑。释放幂等由 lock_released 抢占保证;状态条件更新保证只终结一次。
+     */
+    @Override
+    public void closeEnterpriseOrderWithoutData(TChargingOrder chargingOrder) {
+        if (!isWhitelistEnterpriseOrder(chargingOrder) || null == chargingOrder.getStatus()) {
+            return;
+        }
+        if (2 != chargingOrder.getStatus() && 3 != chargingOrder.getStatus() && 4 != chargingOrder.getStatus()) {
+            return;
+        }
+        // 释放企业账户锁定额度(lock_released 抢占保证只释放一次,失败回滚标记留待后续补偿重试)
+        unlockWhitelistEnterpriseAmountQuietly(chargingOrder, "无设备数据关单");
+        boolean closed = lambdaUpdate()
+                .eq(TChargingOrder::getId, chargingOrder.getId())
+                .in(TChargingOrder::getStatus, 2, 3, 4)
+                .set(TChargingOrder::getStatus, 5)
+                .set(TChargingOrder::getSettleStatus, 1)
+                .set(TChargingOrder::getRefundStatus, 2)
+                .set(TChargingOrder::getEndMode, 1)
+                .set(TChargingOrder::getEndTime, LocalDateTime.now())
+                .update();
+        if (closed) {
+            log.info("[CD关单] 企业账户订单无设备数据兜底关单完成 code={} companyId={} lockedAmount={}",
+                    chargingOrder.getCode(), chargingOrder.getCompanyId(), chargingOrder.getLockedAmount());
+        } else {
+            log.info("[CD关单] 企业账户订单状态已变更,跳过无设备数据兜底关单 code={}", chargingOrder.getCode());
         }
     }
 
@@ -1312,6 +1370,8 @@
             chargingOrder.setAccountMode(chargeContext.getAccountMode());
             chargingOrder.setLockedAmount(chargeContext.getDispatchAmount());
             chargingOrder.setMaxEndTime(chargeContext.getMaxEndTime());
+            // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
+            chargeAccountSupport.fillPreferentialSnapshot(chargingOrder, whitelist.getCompanyId());
         }
 
         //会员折扣支持直营网点和加盟商站点;余额支付(含组合支付)与企业账户流程不参与会员折扣
@@ -2411,10 +2471,14 @@
                         }
                         //企业账户流程:启动失败全额释放已锁定的企业额度(订单未产生消费)
                         unlockWhitelistEnterpriseAmountQuietly(chargingOrder, "启动失败退款");
-                        chargingOrderRefund.setRefundStatus(2);
-                        chargingOrderRefund.setRefundSerialNumber(isWhitelistAllFreeOrder(chargingOrder) ? "WHITELIST" : "BALANCE");
-                        chargingOrderRefund.setRefundTime(LocalDateTime.now());
-                        chargingOrderRefundService.save(chargingOrderRefund);
+                        //企业账户订单资金全部来自企业账户额度,无用户侧渠道/余额退款,不落退款明细(保持 0 行),
+                        //仅更新订单退款快照字段与状态,避免 0 元退款记录进入退款对账
+                        if (!isWhitelistEnterpriseOrder(chargingOrder)) {
+                            chargingOrderRefund.setRefundStatus(2);
+                            chargingOrderRefund.setRefundSerialNumber(isWhitelistAllFreeOrder(chargingOrder) ? "WHITELIST" : "BALANCE");
+                            chargingOrderRefund.setRefundTime(LocalDateTime.now());
+                            chargingOrderRefundService.save(chargingOrderRefund);
+                        }
                         this.updateById(chargingOrder);
                         return;
                     }
@@ -5486,12 +5550,12 @@
                 }
 
                 //企业账户流程:白名单企业账户订单按企业优惠结算(方式2=服务费比例优惠,方式3=按计费模板同阶段单价)
+                //优惠口径优先取订单快照(建单时固化,策略后续变更不影响本单结算),快照为空的历史订单实时查询并回填
                 boolean enterpriseSettlement = isWhitelistEnterpriseOrder(chargingOrder);
-                CompanyPreferentialDTO enterprisePreferential = enterpriseSettlement
-                        ? chargeAccountSupport.loadCompanyPreferential(chargingOrder.getCompanyId()) : null;
+                ChargeAccountSupport.PreferentialSnapshot enterprisePreferential = enterpriseSettlement
+                        ? chargeAccountSupport.resolvePreferential(chargingOrder) : null;
                 Integer enterprisePreferentialType = null == enterprisePreferential ? null : enterprisePreferential.getPreferentialType();
-                TBillingTemplateDTO enterpriseTemplate = enterpriseSettlement
-                        ? chargeAccountSupport.loadBillingTemplate(enterprisePreferential) : null;
+                TBillingTemplateDTO enterpriseTemplate = null == enterprisePreferential ? null : enterprisePreferential.getTemplate();
                 BigDecimal enterpriseElectricDiscount = BigDecimal.ZERO;
                 BigDecimal enterpriseServiceDiscount = BigDecimal.ZERO;
                 //开始处理明细
@@ -5723,6 +5787,19 @@
                 if (null != desc) {
                     chargingOrder.setEndSoc(new BigDecimal(desc.getSoc()));
                 }
+                //企业账户流程:历史订单(快照为空)实时解析的优惠结果回填订单快照列,保证后续补偿重算口径一致;
+                //同时置结算完成标记,兜底扫描(findEndChargingOrder)不再回退已结算订单,防止重复结算与重复扣款
+                if (enterpriseSettlement) {
+                    if (null != enterprisePreferential && enterprisePreferential.needBackfill()) {
+                        chargingOrder.setPreferentialType(enterprisePreferential.getPreferentialType());
+                        chargingOrder.setServiceDiscountRate(enterprisePreferential.getServiceDiscountRate());
+                        chargingOrder.setBillingTemplateId(enterprisePreferential.getBillingTemplateId());
+                        chargingOrder.setBillingTemplateSnapshot(
+                                ChargeAccountSupport.serializeTemplateSnapshot(enterprisePreferential.getTemplate()));
+                        chargingOrder.setPreferentialSnapshotTime(LocalDateTime.now());
+                    }
+                    chargingOrder.setSettleStatus(1);
+                }
                 this.updateById(chargingOrder);
 
                 //企业账户流程:结算企业额度(按实际消费扣减锁定额度、差额退回企业账户)并写消费流水
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
index b634d08..800454b 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
@@ -901,6 +901,8 @@
                 accountUpdate.setAccountMode(chargeContext.getAccountMode());
                 accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
                 accountUpdate.setMaxEndTime(chargeContext.getMaxEndTime());
+                // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
+                chargeAccountSupport.fillPreferentialSnapshot(accountUpdate, vinRecord.getCompanyId());
                 updateById(accountUpdate);
                 // 同步内存对象,保证后续失败回滚能按账户模式/锁定额释放额度
                 order.setAccountMode(chargeContext.getAccountMode());
@@ -1150,20 +1152,6 @@
     private PeriodTypeWindow findCurrentPeriodType(Integer strategyId, LocalTime current) {
         ChargeAccountSupport.PeriodTypeWindow period = chargeAccountSupport.findCurrentPeriodType(strategyId, current);
         return period == null ? null : new PeriodTypeWindow(period.getType(), period.getEndTime());
-    }
-
-    /**
-     * 查询企业优惠设置(失败或未配置返回 null,按不优惠结算;实现见公共支持类)
-     */
-    private CompanyPreferentialDTO loadCompanyPreferential(Integer companyId) {
-        return chargeAccountSupport.loadCompanyPreferential(companyId);
-    }
-
-    /**
-     * 按优惠方式加载计费模板(仅方式3需要,取不到时该段按原价结算;实现见公共支持类)
-     */
-    private TBillingTemplateDTO loadBillingTemplate(CompanyPreferentialDTO preferential) {
-        return chargeAccountSupport.loadBillingTemplate(preferential);
     }
 
     /**
@@ -1528,9 +1516,10 @@
         snapshots.sort(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
                 Comparator.nullsLast(String::compareTo)));
 
-        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费)
-        CompanyPreferentialDTO preferential = loadCompanyPreferential(order.getCompanyId());
-        TBillingTemplateDTO billingTemplate = 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;
@@ -1630,6 +1619,8 @@
         final BigDecimal discountSvcTotal = discountServiceTotal;
         final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc");
         final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc");
+        final ChargeAccountSupport.PreferentialSnapshot preferentialSnapshot = preferential;
+        final boolean preferentialBackfill = preferentialSnapshot != null && preferentialSnapshot.needBackfill();
 
         // 本地事务:写计费明细 + 更新订单(状态5;账户结算在事务外执行,避免长事务占用链接)
         transactionTemplate.execute(status -> {
@@ -1663,6 +1654,15 @@
             if (order.getEndMode() == null) {
                 update.setEndMode(1);
             }
+            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;
         });
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java
index b57c6fd..695e368 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/ChargeAccountSupport.java
@@ -1,5 +1,6 @@
 package com.ruoyi.order.support;
 
+import com.alibaba.fastjson2.JSON;
 import com.ruoyi.account.api.feignClient.CompanyAllocateRecordClient;
 import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
 import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
@@ -8,6 +9,7 @@
 import com.ruoyi.chargingPile.api.model.TBillingTemplateDetail;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.order.api.model.TChargingOrder;
 import com.ruoyi.other.api.domain.TGroupCompany;
 import com.ruoyi.other.api.dto.ChargeAuthorizationDTO;
 import com.ruoyi.other.api.dto.ChargeTimeRange;
@@ -163,6 +165,61 @@
 
         public String getFailReason() {
             return failReason;
+        }
+    }
+
+    /**
+     * 企业优惠快照载体(建单时固化 / 结算与补偿时解析的结果):
+     * 与既有 CompanyPreferentialDTO + TBillingTemplateDTO 调用点同构(getPreferentialType/getServiceDiscountRate/
+     * getBillingTemplateId 方法名保持一致),三链路优惠计算代码仅需把数据来源从 Feign 实时查询改为本解析结果,
+     * 计算逻辑不变。
+     */
+    public static class PreferentialSnapshot {
+        /** 优惠方式(1=不优惠,2=按比例优惠,3=按优惠单价收费;null=无优惠配置) */
+        private final Integer preferentialType;
+        /** 服务费优惠比例(%,方式2) */
+        private final BigDecimal serviceDiscountRate;
+        /** 计费模板ID(方式3) */
+        private final Integer billingTemplateId;
+        /** 方式3计费模板(订单快照反序列化或实时查询结果) */
+        private final TBillingTemplateDTO template;
+        /** 订单快照为空、本次为实时查询所得(结算链路需回填订单快照列,保证补偿重算口径一致) */
+        private final boolean fromRealtimeQuery;
+
+        public PreferentialSnapshot(Integer preferentialType, BigDecimal serviceDiscountRate, Integer billingTemplateId,
+                                    TBillingTemplateDTO template, boolean fromRealtimeQuery) {
+            this.preferentialType = preferentialType;
+            this.serviceDiscountRate = serviceDiscountRate;
+            this.billingTemplateId = billingTemplateId;
+            this.template = template;
+            this.fromRealtimeQuery = fromRealtimeQuery;
+        }
+
+        public Integer getPreferentialType() {
+            return preferentialType;
+        }
+
+        public BigDecimal getServiceDiscountRate() {
+            return serviceDiscountRate;
+        }
+
+        public Integer getBillingTemplateId() {
+            return billingTemplateId;
+        }
+
+        public TBillingTemplateDTO getTemplate() {
+            return template;
+        }
+
+        public boolean isFromRealtimeQuery() {
+            return fromRealtimeQuery;
+        }
+
+        /**
+         * 是否需要把本次解析结果回填到订单快照列(订单快照为空且实时解析到了配置)
+         */
+        public boolean needBackfill() {
+            return fromRealtimeQuery && preferentialType != null;
         }
     }
 
@@ -335,6 +392,108 @@
     }
 
     /**
+     * 建单时固化企业优惠快照:读取当前生效的公司优惠设置(方式3连带加载计费模板明细),
+     * 供调用方写入订单 preferential_* 快照列;未配置或查询失败返回 null(结算时按实时查询兼容)。
+     */
+    public PreferentialSnapshot snapshotPreferential(Integer companyId) {
+        CompanyPreferentialDTO preferential = loadCompanyPreferential(companyId);
+        if (preferential == null) {
+            return null;
+        }
+        return new PreferentialSnapshot(preferential.getPreferentialType(), preferential.getServiceDiscountRate(),
+                preferential.getBillingTemplateId(), loadBillingTemplate(preferential), false);
+    }
+
+    /**
+     * 建单时把当前生效的企业优惠设置写入订单快照字段(含方式3模板明细JSON,未配置/查询失败时不落快照)
+     */
+    public void fillPreferentialSnapshot(TChargingOrder order, Integer companyId) {
+        if (order == null) {
+            return;
+        }
+        PreferentialSnapshot snapshot = snapshotPreferential(companyId);
+        if (snapshot == null || snapshot.getPreferentialType() == null) {
+            return;
+        }
+        order.setPreferentialType(snapshot.getPreferentialType());
+        order.setServiceDiscountRate(snapshot.getServiceDiscountRate());
+        order.setBillingTemplateId(snapshot.getBillingTemplateId());
+        order.setBillingTemplateSnapshot(serializeTemplateSnapshot(snapshot.getTemplate()));
+        order.setPreferentialSnapshotTime(LocalDateTime.now());
+    }
+
+    /**
+     * 结算/补偿时解析企业优惠:订单快照优先(建单后策略变更不影响本单口径),
+     * 快照为空(历史订单)才实时查询并标记回填;返回 null 表示无优惠配置(按不优惠结算)。
+     */
+    public PreferentialSnapshot resolvePreferential(TChargingOrder order) {
+        if (order != null && order.getPreferentialType() != null) {
+            TBillingTemplateDTO template = null;
+            if (Integer.valueOf(PREFERENTIAL_TYPE_TEMPLATE).equals(order.getPreferentialType())) {
+                template = parseTemplateSnapshot(order.getBillingTemplateSnapshot());
+                if (template == null && order.getBillingTemplateId() != null) {
+                    // 快照JSON缺失(历史订单/序列化失败)按模板ID实时查询兜底,尽量不倒退回原价
+                    template = loadTemplateById(order.getBillingTemplateId());
+                }
+            }
+            return new PreferentialSnapshot(order.getPreferentialType(), order.getServiceDiscountRate(),
+                    order.getBillingTemplateId(), template, false);
+        }
+        CompanyPreferentialDTO preferential = loadCompanyPreferential(order == null ? null : order.getCompanyId());
+        if (preferential == null) {
+            return null;
+        }
+        return new PreferentialSnapshot(preferential.getPreferentialType(), preferential.getServiceDiscountRate(),
+                preferential.getBillingTemplateId(), loadBillingTemplate(preferential), true);
+    }
+
+    /**
+     * 计费模板明细序列化为订单快照JSON(方式3建单固化:模板后续被改动/删除不影响本单结算与补偿口径)
+     */
+    public static String serializeTemplateSnapshot(TBillingTemplateDTO template) {
+        if (template == null) {
+            return null;
+        }
+        try {
+            return JSON.toJSONString(template);
+        } catch (Exception ex) {
+            log.error("[充电优惠] 计费模板快照序列化失败 templateId={}", template.getId(), ex);
+            return null;
+        }
+    }
+
+    /**
+     * 订单快照JSON反序列化为计费模板(失败返回 null,由调用方按模板ID实时查询或按原价兜底)
+     */
+    public static TBillingTemplateDTO parseTemplateSnapshot(String snapshotJson) {
+        if (StringUtils.isEmpty(snapshotJson)) {
+            return null;
+        }
+        try {
+            return JSON.parseObject(snapshotJson, TBillingTemplateDTO.class);
+        } catch (Exception ex) {
+            log.error("[充电优惠] 计费模板快照反序列化失败,按模板ID实时查询兜底", ex);
+            return null;
+        }
+    }
+
+    /**
+     * 按模板ID查询计费模板(仅方式3快照兜底使用,取不到时该段按原价结算)
+     */
+    public TBillingTemplateDTO loadTemplateById(Integer templateId) {
+        if (templateId == null) {
+            return null;
+        }
+        try {
+            R<TBillingTemplateDTO> result = billingTemplateClient.getTemplateDetail(templateId);
+            return result == null ? null : result.getData();
+        } catch (Exception ex) {
+            log.error("[充电优惠] 查询计费模板失败,按原价结算 templateId={}", templateId, ex);
+            return null;
+        }
+    }
+
+    /**
      * 服务费按比例优惠:最终单价 = 原价 × (1 - 折扣率/100),折扣率缺失或越界时按原价
      */
     public static BigDecimal applyServiceDiscountRate(BigDecimal servicePrice, BigDecimal discountRate) {
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
index 22b88b7..e15a804 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
@@ -465,6 +465,16 @@
                 && ChronoUnit.MILLIS.between(createTime, LocalDateTime.now()) > NO_DEVICE_DATA_TIMEOUT_MILLIS;
     }
 
+    /**
+     * 企业账户订单是否已完成闭环(已结算 settle_status=1 或锁定额度已释放 lock_released=1):
+     * 兜底扫描不回退这类订单重跑,避免结算完成后被回退重跑造成重复结算与重复扣款。
+     */
+    static boolean isEnterpriseOrderSettledOrReleased(TChargingOrder order) {
+        return null != order
+                && (Integer.valueOf(1).equals(order.getSettleStatus())
+                || Integer.valueOf(1).equals(order.getLockReleased()));
+    }
+
 
     //十分钟循环执行的定时任务
     @Scheduled(fixedRate = 1000 * 60 * 10)
@@ -482,22 +492,28 @@
                     log.info("定时任务执行,查询到启动失败的订单,执行退款:{}", order.getCode());
                     if(1 == order.getOrderSource()){
                         chargingOrderService.refund(order.getCode());
-                        int num = 0;
-                        while (true){
-                            TChargingOrderRefund one = chargingOrderRefundService.getOne(new LambdaQueryWrapper<TChargingOrderRefund>().eq(TChargingOrderRefund::getChargingOrderId, order.getId()));
-                            if(null != one && 2 == one.getRefundStatus()){
-                                order.setStatus(-1);
-                                chargingOrderService.updateById(order);
-                                break;
-                            }
-                            try {
-                                Thread.sleep(1000);
-                            } catch (InterruptedException e) {
-                                throw new RuntimeException(e);
-                            }
-                            num++;
-                            if(num > 30){
-                                break;
+                        //企业账户订单启动失败不落退款明细(资金来自企业额度,仅释放额度并终结),无需等待退款记录,直接终结
+                        if (chargingOrderService.isWhitelistEnterpriseOrder(order)) {
+                            order.setStatus(-1);
+                            chargingOrderService.updateById(order);
+                        } else {
+                            int num = 0;
+                            while (true){
+                                TChargingOrderRefund one = chargingOrderRefundService.getOne(new LambdaQueryWrapper<TChargingOrderRefund>().eq(TChargingOrderRefund::getChargingOrderId, order.getId()));
+                                if(null != one && 2 == one.getRefundStatus()){
+                                    order.setStatus(-1);
+                                    chargingOrderService.updateById(order);
+                                    break;
+                                }
+                                try {
+                                    Thread.sleep(1000);
+                                } catch (InterruptedException e) {
+                                    throw new RuntimeException(e);
+                                }
+                                num++;
+                                if(num > 30){
+                                    break;
+                                }
                             }
                         }
                     }else{
@@ -533,7 +549,13 @@
                     R r = chargingOrderService.endChargeBillingCharge(vo);
                     log.info("定时任务执行,停止中的订单处理结果:{}", JSON.toJSONString(r));
                 }else{
-                    endOrder(order);
+                    if (chargingOrderService.isWhitelistEnterpriseOrder(order)
+                            && shouldFallbackSettle(null, order.getCreateTime(), false)) {
+                        // 企业账户订单停止中且长时间无任何设备数据与账单:释放锁定额度并兜底关单(金额0、状态5),避免悬挂
+                        chargingOrderService.closeEnterpriseOrderWithoutData(order);
+                    } else {
+                        endOrder(order);
+                    }
                 }
             }
 
@@ -572,6 +594,11 @@
                         if(System.currentTimeMillis() - createTime.getTime() - (8 * 60 * 60 * 1000) > 1000 * 60 * 10){
                             endOrder(order);
                         }
+                    } else if (chargingOrderService.isWhitelistEnterpriseOrder(order)
+                            && shouldFallbackSettle(null, order.getCreateTime(), false)) {
+                        // 企业账户订单长时间无任何实时监控数据(设备未上报/报文丢失)且无账单:
+                        // 释放企业账户锁定额度并兜底关单(金额0、状态5、结算完成标记),避免订单长期悬挂被反复扫描
+                        chargingOrderService.closeEnterpriseOrderWithoutData(order);
                     }
                 }
             }
@@ -580,6 +607,13 @@
             List<TChargingOrder> endChargingOrder = chargingOrderService.findEndChargingOrder();
             log.info("定时任务执行,查询到已结束未退款的订单数量:{}", endChargingOrder.size());
             for (TChargingOrder order : endChargingOrder) {
+                // 企业账户订单:已结算(settle_status=1)或锁定额度已释放(lock_released=1)说明已完成闭环,
+                // 不再回退状态重跑,避免重复结算与重复扣款(企业账户订单结算不落退款记录,无法靠退款记录过滤)
+                if (chargingOrderService.isWhitelistEnterpriseOrder(order)
+                        && isEnterpriseOrderSettledOrReleased(order)) {
+                    log.info("定时任务执行,企业账户订单已结算/已终结,跳过回退:{}", order.getCode());
+                    continue;
+                }
                 if(-1 == order.getStatus()){
                     order.setStatus(2);
                 }
@@ -691,12 +725,12 @@
                     }
 
                     //企业账户流程:白名单企业账户订单按企业优惠结算(方式2=服务费比例优惠,方式3=按计费模板同阶段单价)
+                    //优惠口径优先取订单快照(建单时固化,策略后续变更不影响补偿重算),快照为空的历史订单实时查询并回填
                     boolean enterpriseSettlement = chargingOrderService.isWhitelistEnterpriseOrder(order);
-                    CompanyPreferentialDTO enterprisePreferential = enterpriseSettlement
-                            ? chargeAccountSupport.loadCompanyPreferential(order.getCompanyId()) : null;
+                    ChargeAccountSupport.PreferentialSnapshot enterprisePreferential = enterpriseSettlement
+                            ? chargeAccountSupport.resolvePreferential(order) : null;
                     Integer enterprisePreferentialType = null == enterprisePreferential ? null : enterprisePreferential.getPreferentialType();
-                    TBillingTemplateDTO enterpriseTemplate = enterpriseSettlement
-                            ? chargeAccountSupport.loadBillingTemplate(enterprisePreferential) : null;
+                    TBillingTemplateDTO enterpriseTemplate = null == enterprisePreferential ? null : enterprisePreferential.getTemplate();
                     BigDecimal enterpriseElectricDiscount = BigDecimal.ZERO;
                     BigDecimal enterpriseServiceDiscount = BigDecimal.ZERO;
 
@@ -947,12 +981,27 @@
                     if(null != desc){
                         order.setEndSoc(new BigDecimal(desc.getSoc()));
                     }
+                    //企业账户流程:历史订单(快照为空)实时解析的优惠结果回填订单快照列,保证后续补偿重算口径一致;
+                    //同时置结算完成标记,兜底扫描(findEndChargingOrder)不再回退已结算订单,防止重复结算与重复扣款
+                    if (enterpriseSettlement) {
+                        if (null != enterprisePreferential && enterprisePreferential.needBackfill()) {
+                            order.setPreferentialType(enterprisePreferential.getPreferentialType());
+                            order.setServiceDiscountRate(enterprisePreferential.getServiceDiscountRate());
+                            order.setBillingTemplateId(enterprisePreferential.getBillingTemplateId());
+                            order.setBillingTemplateSnapshot(
+                                    ChargeAccountSupport.serializeTemplateSnapshot(enterprisePreferential.getTemplate()));
+                            order.setPreferentialSnapshotTime(LocalDateTime.now());
+                        }
+                        order.setSettleStatus(1);
+                    }
                     chargingOrderService.updateById(order);
 
                     //企业账户流程:结算企业额度(按实际消费扣减锁定额度、差额退回企业账户)并写消费流水
                     if (enterpriseSettlement && !chargingOrderService.settleWhitelistEnterpriseAccount(order, payAmount)) {
-                        // 结算失败回退为停止中,交由补偿任务下一轮重新结算,避免企业额度长期冻结
+                        // 结算失败回退为停止中,交由补偿任务下一轮重新结算,避免企业额度长期冻结;
+                        // 同时清除结算完成标记,保证下一轮补偿仍会处理该订单
                         order.setStatus(4);
+                        order.setSettleStatus(0);
                         chargingOrderService.updateById(order);
                         log.error("[CD补偿结算] 企业账户额度结算失败,订单回退停止中等待重试 code={}", order.getCode());
                         return;
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderWhitelistEnterpriseTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderWhitelistEnterpriseTest.java
index a489d08..e6b227f 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderWhitelistEnterpriseTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderWhitelistEnterpriseTest.java
@@ -1,12 +1,17 @@
 package com.ruoyi.order.service.impl;
 
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.ruoyi.account.api.feignClient.AppUserWhitelistClient;
 import com.ruoyi.account.api.model.TCompanyAllocateRecord;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.order.mapper.TChargingOrderMapper;
 import com.ruoyi.order.support.ChargeAccountSupport;
 import com.ruoyi.other.api.domain.TGroupCompany;
 import com.ruoyi.other.api.feignClient.GroupCompanyClient;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.test.util.ReflectionTestUtils;
@@ -21,12 +26,13 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
- * 充电桩订单白名单企业账户流程单测:企业账户订单判定、额度结算(共享/分配)、关单释放、消费流水
+ * 充电桩订单白名单企业账户流程单测:企业账户订单判定、额度结算(共享/分配)、关单释放(含释放幂等)、消费流水
  */
 class TChargingOrderWhitelistEnterpriseTest {
 
@@ -36,7 +42,18 @@
     private static final BigDecimal LOCKED_AMOUNT = new BigDecimal("200.00");
     private static final BigDecimal ACTUAL_AMOUNT = new BigDecimal("36.50");
 
+    /**
+     * 纯单测无 Spring 容器,MyBatis-Plus 的 lambda 列缓存需手工初始化,
+     * 否则服务实现里的 LambdaUpdateWrapper 解析列名时抛 "can not find lambda cache"
+     */
+    @BeforeAll
+    static void initMybatisPlusTableInfo() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, TChargingOrder.class);
+    }
+
     private TChargingOrderServiceImpl service;
+    private TChargingOrderMapper mapper;
     private AppUserWhitelistClient appUserWhitelistClient;
     private GroupCompanyClient groupCompanyClient;
     private ChargeAccountSupport chargeAccountSupport;
@@ -44,12 +61,16 @@
     @BeforeEach
     void setUp() {
         service = new TChargingOrderServiceImpl();
+        mapper = mock(TChargingOrderMapper.class);
         appUserWhitelistClient = mock(AppUserWhitelistClient.class);
         groupCompanyClient = mock(GroupCompanyClient.class);
         chargeAccountSupport = mock(ChargeAccountSupport.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
         ReflectionTestUtils.setField(service, "appUserWhitelistClient", appUserWhitelistClient);
         ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
         ReflectionTestUtils.setField(service, "chargeAccountSupport", chargeAccountSupport);
+        // 默认抢占释放标记成功(lock_released 0->1)
+        when(mapper.update(any(), any())).thenReturn(1);
     }
 
     @Test
@@ -181,6 +202,69 @@
         assertDoesNotThrow(() -> service.unlockWhitelistEnterpriseAmountQuietly(order, "取消订单"));
     }
 
+    @Test
+    void releasedOrderIsNotUnlockedTwice() {
+        TChargingOrder order = order(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
+        // 抢占释放标记失败(lock_released 已是 1):说明锁定额度已释放,跳过重复释放
+        when(mapper.update(any(), any())).thenReturn(0);
+
+        service.unlockWhitelistEnterpriseAmountQuietly(order, "重复补偿");
+
+        verify(groupCompanyClient, never()).unlockBalance(any(), any());
+        verify(appUserWhitelistClient, never()).unlockBalance(any(), any());
+        verify(mapper, times(1)).update(any(), any());
+    }
+
+    @Test
+    void unlockFailureRollsBackLockReleasedFlagForRetry() {
+        TChargingOrder order = order(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
+        when(groupCompanyClient.unlockBalance(COMPANY_ID, LOCKED_AMOUNT)).thenReturn(R.fail("服务不可用"));
+
+        service.unlockWhitelistEnterpriseAmountQuietly(order, "启动失败");
+
+        // 一次抢占(0->1)+ 一次失败回滚(1->0),保证锁定额不被"标记已释放"而漏释放,可被后续补偿重试
+        verify(mapper, times(2)).update(any(), any());
+    }
+
+    @Test
+    void closeEnterpriseOrderWithoutDataReleasesThenFinishesOrder() {
+        TChargingOrder order = order(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
+        order.setId(9L);
+        order.setStatus(3);
+        when(groupCompanyClient.unlockBalance(COMPANY_ID, LOCKED_AMOUNT))
+                .thenReturn(R.ok(new BigDecimal("500.00")));
+
+        service.closeEnterpriseOrderWithoutData(order);
+
+        // 释放成功不回滚标记:1次抢占释放标记 + 1次关单状态条件更新
+        verify(groupCompanyClient).unlockBalance(COMPANY_ID, LOCKED_AMOUNT);
+        verify(mapper, times(2)).update(any(), any());
+    }
+
+    @Test
+    void closeEnterpriseOrderWithoutDataSkipsNonEnterpriseOrder() {
+        TChargingOrder order = new TChargingOrder();
+        order.setId(9L);
+        order.setStatus(3);
+
+        service.closeEnterpriseOrderWithoutData(order);
+
+        verifyNoInteractions(appUserWhitelistClient, groupCompanyClient);
+        verify(mapper, never()).update(any(), any());
+    }
+
+    @Test
+    void closeEnterpriseOrderWithoutDataSkipsFinishedOrder() {
+        TChargingOrder order = order(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
+        order.setId(9L);
+        order.setStatus(5);
+
+        service.closeEnterpriseOrderWithoutData(order);
+
+        verifyNoInteractions(appUserWhitelistClient, groupCompanyClient);
+        verify(mapper, never()).update(any(), any());
+    }
+
     private TChargingOrder order(int accountMode) {
         TChargingOrder order = new TChargingOrder();
         order.setCode(ORDER_CODE);
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java
index 11b9f1b..47a94c7 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java
@@ -16,6 +16,7 @@
 import com.ruoyi.order.api.vo.ChargingOrderVO;
 import com.ruoyi.order.api.vo.DeviceStartChargingVO;
 import com.ruoyi.order.mapper.TVinChargingOrderMapper;
+import com.ruoyi.order.support.ChargeAccountSupport;
 import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
 import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
 import com.ruoyi.other.api.domain.TFleet;
@@ -74,6 +75,8 @@
     @Mock
     private ChargeAuthorizationClient chargeAuthorizationClient;
     @Mock
+    private ChargeAccountSupport chargeAccountSupport;
+    @Mock
     private AccountingStrategyClient accountingStrategyClient;
     @Mock
     private AccountingStrategyDetailClient accountingStrategyDetailClient;
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ChargePreferentialSnapshotTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ChargePreferentialSnapshotTest.java
new file mode 100644
index 0000000..e5425ec
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/ChargePreferentialSnapshotTest.java
@@ -0,0 +1,201 @@
+package com.ruoyi.order.support;
+
+import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
+import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.other.api.dto.CompanyPreferentialDTO;
+import com.ruoyi.other.api.feignClient.CompanyPreferentialClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * 企业优惠快照单测:
+ * 1) 建单固化(snapshotPreferential / fillPreferentialSnapshot,方式3连带模板明细JSON);
+ * 2) 结算与补偿解析(resolvePreferential):订单快照优先、不回查 Feign;
+ * 3) 历史订单(快照为空)实时查询并标记回填;快照JSON缺失时按模板ID兜底查询。
+ */
+class ChargePreferentialSnapshotTest {
+
+    private static final Integer COMPANY_ID = 12;
+    private static final Integer TEMPLATE_ID = 5;
+
+    private ChargeAccountSupport support;
+    private CompanyPreferentialClient companyPreferentialClient;
+    private BillingTemplateClient billingTemplateClient;
+
+    @BeforeEach
+    void setUp() {
+        support = new ChargeAccountSupport();
+        companyPreferentialClient = mock(CompanyPreferentialClient.class);
+        billingTemplateClient = mock(BillingTemplateClient.class);
+        ReflectionTestUtils.setField(support, "companyPreferentialClient", companyPreferentialClient);
+        ReflectionTestUtils.setField(support, "billingTemplateClient", billingTemplateClient);
+    }
+
+    @Test
+    void snapshotPreferentialLoadsRateSetting() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID))
+                .thenReturn(R.ok(rateSetting(new BigDecimal("20.00"))));
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.snapshotPreferential(COMPANY_ID);
+
+        assertNotNull(snapshot);
+        assertEquals(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE, snapshot.getPreferentialType());
+        assertEquals(new BigDecimal("20.00"), snapshot.getServiceDiscountRate());
+        assertFalse(snapshot.isFromRealtimeQuery());
+        assertFalse(snapshot.needBackfill());
+    }
+
+    @Test
+    void snapshotPreferentialLoadsTemplateDetailsForTemplateType() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID)).thenReturn(R.ok(templateSetting()));
+        when(billingTemplateClient.getTemplateDetail(TEMPLATE_ID)).thenReturn(R.ok(template("标准模板")));
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.snapshotPreferential(COMPANY_ID);
+
+        assertNotNull(snapshot);
+        assertEquals(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE, snapshot.getPreferentialType());
+        assertEquals(TEMPLATE_ID, snapshot.getBillingTemplateId());
+        assertNotNull(snapshot.getTemplate());
+        assertEquals("标准模板", snapshot.getTemplate().getName());
+    }
+
+    @Test
+    void snapshotPreferentialReturnsNullWhenUnconfiguredOrQueryFails() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID)).thenReturn(R.ok(null));
+        assertNull(support.snapshotPreferential(COMPANY_ID));
+
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID))
+                .thenThrow(new RuntimeException("other service unavailable"));
+        assertNull(support.snapshotPreferential(COMPANY_ID));
+        assertNull(support.snapshotPreferential(null));
+    }
+
+    @Test
+    void fillPreferentialSnapshotWritesOrderSnapshotColumns() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID)).thenReturn(R.ok(templateSetting()));
+        when(billingTemplateClient.getTemplateDetail(TEMPLATE_ID)).thenReturn(R.ok(template("标准模板")));
+        TChargingOrder order = new TChargingOrder();
+
+        support.fillPreferentialSnapshot(order, COMPANY_ID);
+
+        assertEquals(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE, order.getPreferentialType());
+        assertEquals(TEMPLATE_ID, order.getBillingTemplateId());
+        assertNotNull(order.getPreferentialSnapshotTime());
+        // 方式3模板明细JSON快照可反序列化还原(抵抗模板后续变更/删除)
+        TBillingTemplateDTO parsed = ChargeAccountSupport.parseTemplateSnapshot(order.getBillingTemplateSnapshot());
+        assertNotNull(parsed);
+        assertEquals("标准模板", parsed.getName());
+    }
+
+    @Test
+    void resolvePreferentialPrefersOrderSnapshotWithoutRealtimeQuery() {
+        TChargingOrder order = new TChargingOrder();
+        order.setCompanyId(COMPANY_ID);
+        order.setPreferentialType(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE);
+        order.setServiceDiscountRate(new BigDecimal("30.00"));
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.resolvePreferential(order);
+
+        assertNotNull(snapshot);
+        assertEquals(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE, snapshot.getPreferentialType());
+        assertEquals(new BigDecimal("30.00"), snapshot.getServiceDiscountRate());
+        assertFalse(snapshot.isFromRealtimeQuery());
+        assertFalse(snapshot.needBackfill());
+        verifyNoInteractions(companyPreferentialClient, billingTemplateClient);
+    }
+
+    @Test
+    void resolvePreferentialQueriesRealtimeAndMarksBackfillWhenSnapshotEmpty() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID))
+                .thenReturn(R.ok(rateSetting(new BigDecimal("25.00"))));
+        TChargingOrder order = new TChargingOrder();
+        order.setCompanyId(COMPANY_ID);
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.resolvePreferential(order);
+
+        assertNotNull(snapshot);
+        assertEquals(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE, snapshot.getPreferentialType());
+        assertEquals(new BigDecimal("25.00"), snapshot.getServiceDiscountRate());
+        assertTrue(snapshot.isFromRealtimeQuery());
+        assertTrue(snapshot.needBackfill());
+    }
+
+    @Test
+    void resolvePreferentialParsesTemplateSnapshotJsonWithoutRealtimeQuery() {
+        TChargingOrder order = new TChargingOrder();
+        order.setCompanyId(COMPANY_ID);
+        order.setPreferentialType(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE);
+        order.setBillingTemplateId(TEMPLATE_ID);
+        order.setBillingTemplateSnapshot(ChargeAccountSupport.serializeTemplateSnapshot(template("历史模板")));
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.resolvePreferential(order);
+
+        assertNotNull(snapshot);
+        assertNotNull(snapshot.getTemplate());
+        assertEquals("历史模板", snapshot.getTemplate().getName());
+        assertFalse(snapshot.needBackfill());
+        verifyNoInteractions(companyPreferentialClient, billingTemplateClient);
+    }
+
+    @Test
+    void resolvePreferentialFallsBackToTemplateIdWhenSnapshotJsonMissing() {
+        when(billingTemplateClient.getTemplateDetail(TEMPLATE_ID)).thenReturn(R.ok(template("兜底模板")));
+        TChargingOrder order = new TChargingOrder();
+        order.setCompanyId(COMPANY_ID);
+        order.setPreferentialType(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE);
+        order.setBillingTemplateId(TEMPLATE_ID);
+
+        ChargeAccountSupport.PreferentialSnapshot snapshot = support.resolvePreferential(order);
+
+        assertNotNull(snapshot);
+        assertNotNull(snapshot.getTemplate());
+        assertEquals("兜底模板", snapshot.getTemplate().getName());
+        verifyNoInteractions(companyPreferentialClient);
+    }
+
+    @Test
+    void resolvePreferentialReturnsNullWhenHistoryOrderHasNoConfiguration() {
+        when(companyPreferentialClient.getDetailByCompanyId(COMPANY_ID)).thenReturn(R.ok(null));
+        TChargingOrder order = new TChargingOrder();
+        order.setCompanyId(COMPANY_ID);
+
+        assertNull(support.resolvePreferential(order));
+    }
+
+    private CompanyPreferentialDTO rateSetting(BigDecimal rate) {
+        CompanyPreferentialDTO dto = new CompanyPreferentialDTO();
+        dto.setCompanyId(COMPANY_ID);
+        dto.setPreferentialType(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE);
+        dto.setServiceDiscountRate(rate);
+        return dto;
+    }
+
+    private CompanyPreferentialDTO templateSetting() {
+        CompanyPreferentialDTO dto = new CompanyPreferentialDTO();
+        dto.setCompanyId(COMPANY_ID);
+        dto.setPreferentialType(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE);
+        dto.setBillingTemplateId(TEMPLATE_ID);
+        return dto;
+    }
+
+    private TBillingTemplateDTO template(String name) {
+        TBillingTemplateDTO dto = new TBillingTemplateDTO();
+        dto.setId(TEMPLATE_ID);
+        dto.setName(name);
+        return dto;
+    }
+}
diff --git a/sql/20260930_charging_order_lock_released.sql b/sql/20260930_charging_order_lock_released.sql
new file mode 100644
index 0000000..2c6d6c7
--- /dev/null
+++ b/sql/20260930_charging_order_lock_released.sql
@@ -0,0 +1,107 @@
+-- 充电订单改造(order库):三张订单表新增「释放幂等标记 / 结算完成标记」列
+-- 目的:
+--   1) lock_released(锁定额度是否已释放):企业账户/白名单锁定额的释放路径(建单回滚、启动失败关单、
+--      无实时数据关单等)先用条件更新抢占该标记,保证同一订单的锁定额在多路径并发或重复补偿下只释放一次;
+--      释放失败时把标记回滚为 0,留待后续补偿或人工重试;
+--      t_card_charging_order 已随刷卡改造建列(20260930_card_order_lock_released.sql),本脚本补齐
+--      t_charging_order(充电桩订单)与 t_vin_charging_order(VIN 订单,订单实体父类字段会参与 SELECT,必须建列);
+--   2) settle_status(结算完成标记):企业账户订单结算成功后置 1,兜底扫描(findEndChargingOrder 等)
+--      不再把已结算/已终结的订单回退重跑,避免重复结算与重复扣款。
+-- 创建日期: 2026-09-30
+-- 目标库:charging_pile_order
+--   分片表:t_charging_order、t_vin_charging_order、t_card_charging_order(Mycat 逻辑表,物理分表逐片执行)
+-- 执行方式:
+--   1) 首选「小节 A」经数据库代理(MyCat 8066)执行,代理会把 DDL 广播到全部物理分表;
+--   2) 代理不支持分片表 DDL 时,改用「小节 B」直连后端 MySQL 实例执行(存储过程,逐物理分表,幂等);
+--   3) 最后执行「小节 C」回查校验。
+-- 幂等说明:小节 A 为裸 ALTER,重复执行会报 Duplicate column,忽略即可(以小节 C 回查为准);
+--           小节 B 先查 information_schema 再变更,可重复执行。
+
+-- ============ 小节 A:经代理执行(推荐) ============
+USE `charging_pile_order`;
+
+ALTER TABLE `t_charging_order`
+  ADD COLUMN `lock_released` tinyint(1) NOT NULL DEFAULT 0 COMMENT '锁定额度是否已释放(0=未释放,1=已释放;防重复释放幂等标记)';
+
+ALTER TABLE `t_charging_order`
+  ADD COLUMN `settle_status` tinyint(1) DEFAULT NULL COMMENT '结算完成标记(NULL/0=未结算,1=已结算;兜底扫描不回退已结算订单)';
+
+ALTER TABLE `t_vin_charging_order`
+  ADD COLUMN `lock_released` tinyint(1) NOT NULL DEFAULT 0 COMMENT '锁定额度是否已释放(0=未释放,1=已释放;防重复释放幂等标记)';
+
+ALTER TABLE `t_vin_charging_order`
+  ADD COLUMN `settle_status` tinyint(1) DEFAULT NULL COMMENT '结算完成标记(NULL/0=未结算,1=已结算;兜底扫描不回退已结算订单)';
+
+ALTER TABLE `t_card_charging_order`
+  ADD COLUMN `settle_status` tinyint(1) DEFAULT NULL COMMENT '结算完成标记(NULL/0=未结算,1=已结算;兜底扫描不回退已结算订单)';
+
+-- ============ 小节 B:直连后端 MySQL 实例执行(代理不可用时分表执行,幂等) ============
+USE `charging_pile_order`;
+
+DROP PROCEDURE IF EXISTS `add_charging_order_lock_released`;
+DELIMITER $$
+CREATE PROCEDURE `add_charging_order_lock_released`()
+BEGIN
+    DECLARE done INT DEFAULT 0;
+    DECLARE target_table VARCHAR(64);
+    DECLARE column_exists INT DEFAULT 0;
+
+    -- 覆盖逻辑表(若物理存在)与 TBPARTITION 物理分表
+    DECLARE table_cursor CURSOR FOR
+        SELECT t.table_name
+        FROM information_schema.tables t
+        WHERE t.table_schema = DATABASE()
+          AND t.table_name REGEXP '^t_charging_order(_[0-9]+)?$|^t_vin_charging_order(_[0-9]+)?$|^t_card_charging_order(_[0-9]+)?$';
+
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
+
+    OPEN table_cursor;
+    table_loop: LOOP
+        FETCH table_cursor INTO target_table;
+        IF done = 1 THEN
+            LEAVE table_loop;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'lock_released';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `lock_released` tinyint(1) NOT NULL DEFAULT 0 COMMENT ''锁定额度是否已释放(0=未释放,1=已释放;防重复释放幂等标记)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'settle_status';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `settle_status` tinyint(1) DEFAULT NULL COMMENT ''结算完成标记(NULL/0=未结算,1=已结算;兜底扫描不回退已结算订单)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+    END LOOP;
+    CLOSE table_cursor;
+END$$
+DELIMITER ;
+
+CALL `add_charging_order_lock_released`();
+DROP PROCEDURE IF EXISTS `add_charging_order_lock_released`;
+
+-- ============ 小节 C:回查校验 ============
+-- 1) 列存在性与注释(三张订单表均应覆盖逻辑表与全部物理分表):
+--    SELECT table_name, column_name, column_type, column_default, column_comment
+--    FROM information_schema.columns
+--    WHERE table_schema = 'charging_pile_order'
+--      AND column_name IN ('lock_released', 'settle_status')
+--    ORDER BY table_name, column_name;
+--    预期:t_charging_order / t_vin_charging_order / t_card_charging_order 及各自物理分表均存在两列,
+--          lock_released column_default=0、column_comment 中文无乱码
+-- 2) 历史订单默认未释放、未结算标记为空:
+--    SELECT COUNT(*) FROM t_charging_order WHERE lock_released = 0 AND settle_status IS NULL;
+--    预期:等于存量订单数(存量订单按「实时查询 + 回填」兼容路径处理)
diff --git a/sql/20260930_company_preferential_snapshot.sql b/sql/20260930_company_preferential_snapshot.sql
new file mode 100644
index 0000000..00399d4
--- /dev/null
+++ b/sql/20260930_company_preferential_snapshot.sql
@@ -0,0 +1,152 @@
+-- 企业优惠快照改造(order库):三张订单表新增「企业优惠方式/折扣率/计费模板」快照列
+-- 目的:
+--   企业账户(白名单/企业卡/VIN)订单建单时把当时生效的公司优惠设置固化到订单快照列;
+--   结算与补偿重算时优先读订单快照(快照为空的历史订单才实时查询并回填),保证:
+--     1) 建单后公司优惠设置变更(换折扣率/换模板)不影响历史订单的结算金额与对账口径;
+--     2) 方式3(按优惠单价收费)的计费模板被修改/删除时,补偿重算与首次结算口径一致,不降级为原价。
+-- 新增列(允许 NULL,历史订单走「实时查询 + 回填」兼容路径):
+--   preferential_type           企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费)
+--   service_discount_rate       服务费优惠比例快照(%,方式2使用)
+--   billing_template_id         计费模板ID快照(方式3使用)
+--   billing_template_snapshot   计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)
+--   preferential_snapshot_time  优惠快照固化时间
+-- 创建日期: 2026-09-30
+-- 目标库:charging_pile_order
+--   分片表:t_charging_order、t_vin_charging_order、t_card_charging_order(Mycat 逻辑表,物理分表逐片执行)
+-- 执行方式:
+--   1) 首选「小节 A」经数据库代理(MyCat 8066)执行,代理会把 DDL 广播到全部物理分表;
+--   2) 代理不支持分片表 DDL 时,改用「小节 B」直连后端 MySQL 实例执行(存储过程,逐物理分表,幂等);
+--   3) 最后执行「小节 C」回查校验。
+-- 幂等说明:小节 A 为裸 ALTER,重复执行会报 Duplicate column,忽略即可(以小节 C 回查为准);
+--           小节 B 先查 information_schema 再变更,可重复执行。
+
+-- ============ 小节 A:经代理执行(推荐) ============
+USE `charging_pile_order`;
+
+ALTER TABLE `t_charging_order`
+  ADD COLUMN `preferential_type` tinyint(4) DEFAULT NULL COMMENT '企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费;NULL=建单时未固化)',
+  ADD COLUMN `service_discount_rate` decimal(5,2) DEFAULT NULL COMMENT '服务费优惠比例快照(%,方式2使用)',
+  ADD COLUMN `billing_template_id` int(11) DEFAULT NULL COMMENT '计费模板ID快照(方式3使用)',
+  ADD COLUMN `billing_template_snapshot` text COMMENT '计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)',
+  ADD COLUMN `preferential_snapshot_time` datetime DEFAULT NULL COMMENT '优惠快照固化时间';
+
+ALTER TABLE `t_vin_charging_order`
+  ADD COLUMN `preferential_type` tinyint(4) DEFAULT NULL COMMENT '企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费;NULL=建单时未固化)',
+  ADD COLUMN `service_discount_rate` decimal(5,2) DEFAULT NULL COMMENT '服务费优惠比例快照(%,方式2使用)',
+  ADD COLUMN `billing_template_id` int(11) DEFAULT NULL COMMENT '计费模板ID快照(方式3使用)',
+  ADD COLUMN `billing_template_snapshot` text COMMENT '计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)',
+  ADD COLUMN `preferential_snapshot_time` datetime DEFAULT NULL COMMENT '优惠快照固化时间';
+
+ALTER TABLE `t_card_charging_order`
+  ADD COLUMN `preferential_type` tinyint(4) DEFAULT NULL COMMENT '企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费;NULL=建单时未固化)',
+  ADD COLUMN `service_discount_rate` decimal(5,2) DEFAULT NULL COMMENT '服务费优惠比例快照(%,方式2使用)',
+  ADD COLUMN `billing_template_id` int(11) DEFAULT NULL COMMENT '计费模板ID快照(方式3使用)',
+  ADD COLUMN `billing_template_snapshot` text COMMENT '计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)',
+  ADD COLUMN `preferential_snapshot_time` datetime DEFAULT NULL COMMENT '优惠快照固化时间';
+
+-- ============ 小节 B:直连后端 MySQL 实例执行(代理不可用时分表执行,幂等) ============
+USE `charging_pile_order`;
+
+DROP PROCEDURE IF EXISTS `add_company_preferential_snapshot`;
+DELIMITER $$
+CREATE PROCEDURE `add_company_preferential_snapshot`()
+BEGIN
+    DECLARE done INT DEFAULT 0;
+    DECLARE target_table VARCHAR(64);
+    DECLARE column_exists INT DEFAULT 0;
+
+    -- 覆盖逻辑表(若物理存在)与 TBPARTITION 物理分表
+    DECLARE table_cursor CURSOR FOR
+        SELECT t.table_name
+        FROM information_schema.tables t
+        WHERE t.table_schema = DATABASE()
+          AND t.table_name REGEXP '^t_charging_order(_[0-9]+)?$|^t_vin_charging_order(_[0-9]+)?$|^t_card_charging_order(_[0-9]+)?$';
+
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
+
+    OPEN table_cursor;
+    table_loop: LOOP
+        FETCH table_cursor INTO target_table;
+        IF done = 1 THEN
+            LEAVE table_loop;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'preferential_type';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `preferential_type` tinyint(4) DEFAULT NULL COMMENT ''企业优惠方式快照(1=不优惠,2=按比例优惠,3=按优惠单价收费;NULL=建单时未固化)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'service_discount_rate';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `service_discount_rate` decimal(5,2) DEFAULT NULL COMMENT ''服务费优惠比例快照(%,方式2使用)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'billing_template_id';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `billing_template_id` int(11) DEFAULT NULL COMMENT ''计费模板ID快照(方式3使用)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'billing_template_snapshot';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `billing_template_snapshot` text COMMENT ''计费模板明细快照JSON(方式3使用,抵抗模板后续变更/删除)''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+
+        SELECT COUNT(*) INTO column_exists
+        FROM information_schema.columns
+        WHERE table_schema = DATABASE()
+          AND table_name = target_table
+          AND column_name = 'preferential_snapshot_time';
+        IF column_exists = 0 THEN
+            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `preferential_snapshot_time` datetime DEFAULT NULL COMMENT ''优惠快照固化时间''');
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+    END LOOP;
+    CLOSE table_cursor;
+END$$
+DELIMITER ;
+
+CALL `add_company_preferential_snapshot`();
+DROP PROCEDURE IF EXISTS `add_company_preferential_snapshot`;
+
+-- ============ 小节 C:回查校验 ============
+-- 1) 列存在性与注释(三张订单表均应覆盖逻辑表与全部物理分表):
+--    SELECT table_name, column_name, column_type, column_comment
+--    FROM information_schema.columns
+--    WHERE table_schema = 'charging_pile_order'
+--      AND column_name IN ('preferential_type', 'service_discount_rate', 'billing_template_id',
+--                          'billing_template_snapshot', 'preferential_snapshot_time')
+--    ORDER BY table_name, column_name;
+--    预期:三张订单表及各自物理分表均存在 5 列,column_comment 中文无乱码
+-- 2) 历史订单快照为空(走实时查询 + 回填兼容路径):
+--    SELECT COUNT(*) FROM t_charging_order WHERE preferential_type IS NULL;
+--    预期:等于存量订单数

--
Gitblit v1.7.1