skyline 3 ay önce
ebeveyn
işleme
364c228e14

+ 49 - 3
admin/src/main/java/com/kym/admin/jobs/ActivityDelayJob.java

@@ -18,6 +18,7 @@ import java.util.Comparator;
 import java.util.concurrent.DelayQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
 /**
@@ -36,6 +37,16 @@ public class ActivityDelayJob implements DelayService<DelayActivity> {
      */
     private final static DelayQueue<DelayedItem<DelayActivity>> DELAY_QUEUE = new DelayQueue<>();
 
+    /**
+     * 队列最大容量
+     */
+    private static final int MAX_QUEUE_SIZE = 10000;
+
+    /**
+     * 控制线程运行状态的标志位
+     */
+    private volatile boolean running = true;
+
     private final ActivityService activityService;
     private final ActivityStationService activityStationService;
     private final RechargeRightsService rechargeRightsService;
@@ -90,9 +101,12 @@ public class ActivityDelayJob implements DelayService<DelayActivity> {
             ThreadLocal<Long> threadLocal = new ThreadLocal<>();
             log.info("活动延迟启闭处理线程:{}", Thread.currentThread().getName());
             DelayedItem<DelayActivity> delayedItem;
-            while (true) {
+            while (running) {
                 try {
                     delayedItem = DELAY_QUEUE.take();
+                    if (!running) {
+                        break;
+                    }
                     var delayActivity = delayedItem.data;
                     threadLocal.set(delayActivity.getId());
                     if (delayActivity.getType().equals(DelayActivity.TYPE_启动)) {
@@ -142,25 +156,36 @@ public class ActivityDelayJob implements DelayService<DelayActivity> {
                     Thread.sleep(100);
                 } catch (Exception e) {
                     if (e instanceof InterruptedException) {
-                        log.error("活动到期停止队列take异常", e);
+                        log.info("活动延迟任务线程被中断,准备退出");
+                        Thread.currentThread().interrupt();
+                        break;
                     } else {
-                        log.info("活动到期停止,主活动id:{}", threadLocal.get(), e);
+                        log.error("活动到期停止处理异常,主活动id:{}", threadLocal.get(), e);
                     }
                 } finally {
                     threadLocal.remove();
                 }
             }
+            log.info("活动延迟启闭处理线程已退出");
         });
 
     }
 
     @Override
     public boolean addToDelayQueue(DelayedItem<DelayActivity> delayedItem) {
+        if (DELAY_QUEUE.size() >= MAX_QUEUE_SIZE) {
+            log.error("活动延迟队列已满,拒绝添加任务,当前大小: {}", DELAY_QUEUE.size());
+            return false;
+        }
         return DELAY_QUEUE.add(delayedItem);
     }
 
     @Override
     public boolean addToDelayQueue(DelayActivity activity) {
+        if (DELAY_QUEUE.size() >= MAX_QUEUE_SIZE) {
+            log.error("活动延迟队列已满,拒绝添加任务,当前大小: {}", DELAY_QUEUE.size());
+            return false;
+        }
         DelayedItem<DelayActivity> activityDelayed = new DelayedItem<>(activity, activity.getExecuteTime());
         return DELAY_QUEUE.add(activityDelayed);
     }
@@ -173,4 +198,25 @@ public class ActivityDelayJob implements DelayService<DelayActivity> {
         }
         return DELAY_QUEUE.removeIf(queue -> queue.data.getId().equals(activityId));
     }
+
+    /**
+     * 应用关闭时释放资源
+     */
+    @jakarta.annotation.PreDestroy
+    public void destroy() {
+        log.info("正在关闭活动延迟任务线程池...");
+        running = false;
+        executor.shutdown();
+        try {
+            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
+                executor.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executor.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+        // 清理队列
+        DELAY_QUEUE.clear();
+        log.info("活动延迟任务线程池已关闭");
+    }
 }

+ 36 - 17
admin/src/main/java/com/kym/admin/jobs/InvoiceStatusJob.java

@@ -10,6 +10,9 @@ import com.kym.service.miniapp.ChargeOrderService;
 import com.kym.service.miniapp.InvoiceService;
 import com.kym.service.wechat.WxPayService;
 import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
@@ -43,24 +46,40 @@ public class InvoiceStatusJob {
     @Scheduled(cron = "0 0 18 * * ?")
     public void execute() {
         log.info("执行发票状态处理定时任务...开始");
-        // 所有开票中状态的发票
-        DynamicDataSourceContextHolder.push("db-miniapp");
-        var invoiceList = invoiceService.lambdaQuery().eq(Invoice::getStatus, Invoice.STATUS_开票中).list();
-        DynamicDataSourceContextHolder.poll();
-        invoiceList.forEach(invoice -> {
-            var fapiaoApplications = wxPayService.queryFapiao(invoice.getApplyId());
-            // 更新发票详情
-            invoiceDetailService.updateInvoiceDetail(invoice.getApplyId(), fapiaoApplications);
-            // 更新订单开票状态
-            if (InvoiceNotification.FapiaoStatus.ISSUED.name().equals(fapiaoApplications.getFapiao_information().get(0).getStatus())) {
-                var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
-                DynamicDataSourceContextHolder.push("db-miniapp");
-                chargeOrderService.lambdaUpdate().in(ChargeOrder::getStartChargeSeq, startChargeSeqs).set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票).update();
-                // 修改发票状态
-                invoiceService.lambdaUpdate().eq(Invoice::getApplyId, invoice.getApplyId()).set(Invoice::getStatus, Invoice.STATUS_已开票).update();
-                DynamicDataSourceContextHolder.poll();
+            
+        // 查询所有开票中状态的发票
+        List<Invoice> invoiceList;
+        try {
+            DynamicDataSourceContextHolder.push("db-miniapp");
+            invoiceList = invoiceService.lambdaQuery().eq(Invoice::getStatus, Invoice.STATUS_开票中).list();
+        } finally {
+            DynamicDataSourceContextHolder.poll();
+        }
+            
+        // 处理每个发票
+        for (Invoice invoice : invoiceList) {
+            try {
+                var fapiaoApplications = wxPayService.queryFapiao(invoice.getApplyId());
+                // 更新发票详情
+                invoiceDetailService.updateInvoiceDetail(invoice.getApplyId(), fapiaoApplications);
+                // 更新订单开票状态
+                if (fapiaoApplications.getFapiao_information() != null 
+                        && !fapiaoApplications.getFapiao_information().isEmpty()
+                        && InvoiceNotification.FapiaoStatus.ISSUED.name().equals(fapiaoApplications.getFapiao_information().get(0).getStatus())) {
+                    var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
+                    try {
+                        DynamicDataSourceContextHolder.push("db-miniapp");
+                        chargeOrderService.lambdaUpdate().in(ChargeOrder::getStartChargeSeq, startChargeSeqs).set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票).update();
+                        // 修改发票状态
+                        invoiceService.lambdaUpdate().eq(Invoice::getApplyId, invoice.getApplyId()).set(Invoice::getStatus, Invoice.STATUS_已开票).update();
+                    } finally {
+                        DynamicDataSourceContextHolder.poll();
+                    }
+                }
+            } catch (Exception e) {
+                log.error("处理发票{}失败", invoice.getApplyId(), e);
             }
-        });
+        }
         log.info("执行发票状态处理定时任务...结束");
     }
 

+ 16 - 7
admin/src/main/resources/application-prod.yml

@@ -50,11 +50,11 @@ spring:
       #监控统计拦截的filters
       filters: stat,slf4j
       #配置初始化大小/最小/最大
-      initial-size: 2
-      min-idle: 2
-      max-active: 20
-      #获取连接等待超时时间
-      max-wait: 60000
+      initial-size: 5
+      min-idle: 5
+      max-active: 50
+      #获取连接等待超时时间(从60秒优化到10秒)
+      max-wait: 10000
       #间隔多久进行一次检测,检测需要关闭的空闲连接
       time-between-eviction-runs-millis: 60000
       #一个连接在池中最小生存的时间
@@ -66,17 +66,26 @@ spring:
       #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
       pool-prepared-statements: false
       max-pool-prepared-statement-per-connection-size: 20
+      #连接泄漏检测配置
+      remove-abandoned: true
+      remove-abandoned-timeout: 300
+      log-abandoned: true
+      #连接超时配置
+      connect-timeout: 5000
+      socket-timeout: 60000
+      #慢SQL监控
+      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000
     dynamic:
       primary: db-admin
       strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
       datasource:
         db-admin:
-          url: jdbc:mysql://127.0.0.1:3306/charge_admin?serverTimezone=Asia/Shanghai
+          url: jdbc:mysql://121.40.98.15:3306/charge_admin?serverTimezone=Asia/Shanghai
           username: kym
           password: qYhQLZLP6e7paVmQN5foEwRYJ1yFNpwM
           driver-class-name: com.mysql.cj.jdbc.Driver
         db-miniapp:
-          url: jdbc:mysql://127.0.0.1:3306/charge_app?serverTimezone=Asia/Shanghai
+          url: jdbc:mysql://121.40.98.15:3306/charge_app?serverTimezone=Asia/Shanghai
           username: kym
           password: qYhQLZLP6e7paVmQN5foEwRYJ1yFNpwM
           driver-class-name: com.mysql.cj.jdbc.Driver

+ 4 - 0
entity/src/main/java/com/kym/entity/common/RedisKeys.java

@@ -16,6 +16,10 @@ public interface RedisKeys {
     String OFFLINE = "OFFLINE:";
     String OFFLINE_EXPIRED = "OFFLINE_EXPIRED:";
     String CONNECTOR_ID_TO_SHORT_ID = "CONNECTOR_ID_TO_SHORT_ID:";
+    /**
+     * 短编号到connectorId的反向索引
+     */
+    String SHORT_ID_TO_CONNECTOR_ID = "SHORT_ID_TO_CONNECTOR_ID:";
     String CONNECTOR_ID_TO_STATUS = "CONNECTOR_ID_TO_STATUS:";
     String CONNECTOR_ID_TO_STATION_ID = "CONNECTOR_ID_TO_STATION_ID:";
     String STATION_ID_TO_NAME = "STATION_ID_TO_NAME:";

+ 6 - 4
miniapp/src/main/java/com/kym/miniapp/jobs/StopChargeDelayJob.java

@@ -21,8 +21,6 @@ import org.springframework.stereotype.Component;
 import java.util.concurrent.DelayQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -101,8 +99,12 @@ public class StopChargeDelayJob implements DelayService<DelayChargeOrder> {
                         // 停止充电
                         var order = delayedItem.data;
                         threadLocal.set(order.getStartChargeSeq());
-                        // 查询该设备最新订单是否是当前预约订单,是则按计划停止
-                        var currentChargeOrder = chargeOrderService.lambdaQuery().eq(ChargeOrder::getConnectorId, order.getConnectorId()).orderByDesc(ChargeOrder::getCreateTime).list().get(0);
+                        // 查询该设备最新订单是否是当前预约订单,是则按计划停止(优化:使用LIMIT 1代替查询所有)
+                        var currentChargeOrder = chargeOrderService.lambdaQuery()
+                                .eq(ChargeOrder::getConnectorId, order.getConnectorId())
+                                .orderByDesc(ChargeOrder::getCreateTime)
+                                .last("LIMIT 1")
+                                .one();
                         if (currentChargeOrder.getStartChargeSeq().equals(order.getStartChargeSeq())) {
                             chargeService.queryStopCharge(order.getUserId(), order.getConnectorId());
                             log.info("预约充电停止成功:用户:{}, 订单号:{}, 预约停止时间:{}", order.getUserId(), order.getStartChargeSeq(), order.getEndTime());

+ 14 - 5
miniapp/src/main/resources/application-prod.yml

@@ -43,11 +43,11 @@ spring:
       #监控统计拦截的filters
       filters: stat,slf4j
       #配置初始化大小/最小/最大
-      initial-size: 2
-      min-idle: 2
-      max-active: 20
-      #获取连接等待超时时间
-      max-wait: 60000
+      initial-size: 5
+      min-idle: 5
+      max-active: 50
+      #获取连接等待超时时间(从60秒优化到10秒)
+      max-wait: 10000
       #间隔多久进行一次检测,检测需要关闭的空闲连接
       time-between-eviction-runs-millis: 60000
       #一个连接在池中最小生存的时间
@@ -59,6 +59,15 @@ spring:
       #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
       pool-prepared-statements: false
       max-pool-prepared-statement-per-connection-size: 20
+      #连接泄漏检测配置
+      remove-abandoned: true
+      remove-abandoned-timeout: 300
+      log-abandoned: true
+      #连接超时配置
+      connect-timeout: 5000
+      socket-timeout: 60000
+      #慢SQL监控
+      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000
     dynamic:
       primary: db-miniapp
       strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源

+ 243 - 44
service/src/main/java/com/kym/service/cache/KymCache.java

@@ -15,6 +15,8 @@ import com.kym.service.admin.ConnectorInfoService;
 import com.kym.service.admin.EquipmentRelationService;
 import com.kym.service.admin.PlatformService;
 import com.kym.service.admin.StationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.context.event.ApplicationStartedEvent;
 import org.springframework.context.ApplicationListener;
@@ -23,8 +25,8 @@ import org.springframework.stereotype.Component;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 /**
@@ -35,90 +37,220 @@ import java.util.stream.Collectors;
 public enum KymCache {
     INSTANCE;
 
+    private static final Logger log = LoggerFactory.getLogger(KymCache.class);
 
-    private static ConcurrentHashMap<String, String> CONNECTOR_ID_SHORT_ID_MAPPING = new ConcurrentHashMap<>();
+    /**
+     * 缓存过期时间:24小时
+     */
+    private static final long CACHE_EXPIRE_HOURS = 24;
 
+    /**
+     * 带数据源切换的数据库查询(用于缓存miss时按需加载)
+     *
+     * @param supplier 数据库查询逻辑
+     * @param <T>      返回类型
+     * @return 查询结果
+     */
+    private <T> T executeWithDataSource(Supplier<T> supplier) {
+        DynamicDataSourceContextHolder.push("db-admin");
+        try {
+            return supplier.get();
+        } finally {
+            DynamicDataSourceContextHolder.poll();
+        }
+    }
 
     /**
-     * 获取枪头状态
+     * 获取枪头状态(带空值保护和缓存miss时按需加载)
      *
-     * @param connectorId
-     * @return
+     * @param connectorId 充电接口ID
+     * @return 状态值,如果不存在返回null
      */
     public Integer getConnectorStatus(String connectorId) {
-        return Integer.valueOf(Objects.requireNonNull(KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_STATUS + connectorId)));
+        if (connectorId == null) {
+            return null;
+        }
+        try {
+            String value = KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_STATUS + connectorId);
+            if (value == null) {
+                // 缓存miss,从数据库加载
+                Integer status = loadConnectorStatusFromDb(connectorId);
+                if (status != null) {
+                    // 重新缓存
+                    KymCacheInjector.redisTemplate.opsForValue()
+                            .set(RedisKeys.CONNECTOR_ID_TO_STATUS + connectorId, String.valueOf(status), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+                }
+                return status;
+            }
+            if ("-1".equals(value)) {
+                return null;
+            }
+            return Integer.valueOf(value);
+        } catch (Exception e) {
+            log.warn("获取枪头状态失败,connectorId: {}", connectorId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 从数据库加载枪头状态
+     */
+    private Integer loadConnectorStatusFromDb(String connectorId) {
+        return executeWithDataSource(() -> {
+            ConnectorInfoService service = SpringUtil.getBean(ConnectorInfoService.class);
+            ConnectorInfo info = service.lambdaQuery().eq(ConnectorInfo::getConnectorId, connectorId).one();
+            return info != null ? info.getStatus() : null;
+        });
     }
 
     /**
-     * 缓存枪头状态
+     * 缓存枪头状态(带过期时间)
      *
-     * @param map
+     * @param map 状态映射
      */
     public void putConnectorId2Status(Map<String, Integer> map) {
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.CONNECTOR_ID_TO_STATUS + k, String.valueOf(v)));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.CONNECTOR_ID_TO_STATUS + k, String.valueOf(v), CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
 
     /**
-     * 缓存枪头短编号
+     * 缓存枪头短编号(带过期时间,移除本地Map缓存)
      *
-     * @param map
+     * @param map 短编号映射
      */
     public void putConnectorId2ShortId(Map<String, String> map) {
-        CONNECTOR_ID_SHORT_ID_MAPPING.putAll(map);
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + k, v));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + k, v, CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
     /**
-     * 通过设备编号或者枪头编号获取设备短编号
+     * 缓存短编号到connectorId的反向索引(带过期时间)
      *
-     * @param id
-     * @return
+     * @param map 短编号 -> connectorId 映射
+     */
+    public void putShortId2ConnectorId(Map<String, String> map) {
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.SHORT_ID_TO_CONNECTOR_ID + k, v, CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
+    }
+
+    /**
+     * 通过设备编号或者枪头编号获取设备短编号(带缓存miss时按需加载)
+     *
+     * @param id 设备编号或枪头编号
+     * @return 短编号
      */
     public String getShortIdByEquipmentIdOrConnectorId(String id) {
         var connectorId = getConnectorId(id);
-        return KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + connectorId);
+        if (connectorId == null) {
+            return null;
+        }
+        String shortId = KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + connectorId);
+        if (shortId == null) {
+            // 缓存miss,从数据库加载
+            EquipmentRelation relation = loadEquipmentRelationByConnectorId(connectorId);
+            if (relation != null) {
+                shortId = relation.getShortId();
+                // 重新缓存
+                KymCacheInjector.redisTemplate.opsForValue()
+                        .set(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + connectorId, shortId, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+                // 同时缓存反向索引
+                KymCacheInjector.redisTemplate.opsForValue()
+                        .set(RedisKeys.SHORT_ID_TO_CONNECTOR_ID + shortId, connectorId, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            }
+        }
+        return shortId;
+    }
+
+    /**
+     * 从数据库加载设备关系
+     */
+    private EquipmentRelation loadEquipmentRelationByConnectorId(String connectorId) {
+        return executeWithDataSource(() -> {
+            EquipmentRelationService service = SpringUtil.getBean(EquipmentRelationService.class);
+            return service.getByConnectorId(connectorId);
+        });
     }
 
 
     public void putConnectorId2StationId(Map<String, String> map) {
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.CONNECTOR_ID_TO_STATION_ID + k, v));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.CONNECTOR_ID_TO_STATION_ID + k, v, CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
     /**
-     * 通过设备编号或者枪头编号获取站点id
+     * 通过设备编号或者枪头编号获取站点id(带缓存miss时按需加载)
      *
-     * @param id
-     * @return
+     * @param id 设备编号或枪头编号
+     * @return 站点ID
      */
     public String getStationIdByEquipmentIdOrConnectorId(String id) {
         var connectorId = getConnectorId(id);
-        return KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_STATION_ID + connectorId);
+        if (connectorId == null) {
+            return null;
+        }
+        String stationId = KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_STATION_ID + connectorId);
+        if (stationId == null) {
+            // 缓存miss,从数据库加载
+            EquipmentRelation relation = loadEquipmentRelationByConnectorId(connectorId);
+            if (relation != null) {
+                stationId = relation.getStationId();
+                // 重新缓存
+                KymCacheInjector.redisTemplate.opsForValue()
+                        .set(RedisKeys.CONNECTOR_ID_TO_STATION_ID + connectorId, stationId, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            }
+        }
+        return stationId;
     }
 
     public void putStationId2Name(Map<String, String> map) {
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.STATION_ID_TO_NAME + k, v));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.STATION_ID_TO_NAME + k, v, CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
     /**
-     * 通过站点id获取站点名称
+     * 通过站点id获取站点名称(带缓存miss时按需加载)
      *
-     * @param stationId
-     * @return
+     * @param stationId 站点ID
+     * @return 站点名称
      */
     public String getStationNameById(String stationId) {
-        return KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.STATION_ID_TO_NAME + stationId);
+        if (stationId == null) {
+            return null;
+        }
+        String stationName = KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.STATION_ID_TO_NAME + stationId);
+        if (stationName == null) {
+            // 缓存miss,从数据库加载
+            stationName = loadStationNameFromDb(stationId);
+            if (stationName != null) {
+                // 重新缓存
+                KymCacheInjector.redisTemplate.opsForValue()
+                        .set(RedisKeys.STATION_ID_TO_NAME + stationId, stationName, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            }
+        }
+        return stationName;
+    }
+
+    /**
+     * 从数据库加载站点名称
+     */
+    private String loadStationNameFromDb(String stationId) {
+        return executeWithDataSource(() -> {
+            StationService service = SpringUtil.getBean(StationService.class);
+            Station station = service.lambdaQuery().eq(Station::getStationId, stationId).one();
+            return station != null ? station.getStationName() : null;
+        });
     }
 
     /**
-     * 通过充电接口id获取站点名称
+     * 通过充电接口id获取站点名称(复用getStationNameById的缓存miss加载逻辑)
      *
-     * @param connectorId
-     * @return
+     * @param connectorId 充电接口ID
+     * @return 站点名称
      */
     public String getStationNameByConnectorId(String connectorId) {
         var stationId = getStationIdByEquipmentIdOrConnectorId(connectorId);
-        return KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.STATION_ID_TO_NAME + stationId);
+        return getStationNameById(stationId);
     }
 
     /**
@@ -147,16 +279,18 @@ public enum KymCache {
     }
 
     public void putConnectorId2ParkingNo(Map<String, String> map) {
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.CONNECTOR_ID_TO_PARKING_NO + k, v));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.CONNECTOR_ID_TO_PARKING_NO + k, v, CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
     /**
-     * 站点费率
+     * 站点费率(带过期时间)
      *
-     * @param map
+     * @param map 费率映射
      */
     public void putStationId2PolicyInfo(Map<String, List<PlatformPolicyInfoVo>> map) {
-        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.STATION_ID_POLICY_INFO + k, JSONObject.toJSONString(v)));
+        map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue()
+                .set(RedisKeys.STATION_ID_POLICY_INFO + k, JSONObject.toJSONString(v), CACHE_EXPIRE_HOURS, TimeUnit.HOURS));
     }
 
     /**
@@ -171,30 +305,92 @@ public enum KymCache {
     }
 
     /**
-     * @param id
-     * @return
+     * 通过设备编号或枪头编号获取停车位号(带缓存miss时按需加载)
+     *
+     * @param id 设备编号或枪头编号
+     * @return 停车位号
      */
     public String getParkNoByEquipmentIdOrConnectorId(String id) {
         var connectorId = getConnectorId(id);
-        return KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_PARKING_NO + connectorId);
+        if (connectorId == null) {
+            return null;
+        }
+        String parkingNo = KymCacheInjector.redisTemplate.opsForValue().get(RedisKeys.CONNECTOR_ID_TO_PARKING_NO + connectorId);
+        if (parkingNo == null) {
+            // 缓存miss,从数据库加载
+            EquipmentRelation relation = loadEquipmentRelationByConnectorId(connectorId);
+            if (relation != null) {
+                parkingNo = relation.getParkingNo();
+                // 重新缓存
+                KymCacheInjector.redisTemplate.opsForValue()
+                        .set(RedisKeys.CONNECTOR_ID_TO_PARKING_NO + connectorId, parkingNo, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            }
+        }
+        return parkingNo;
     }
 
     /**
-     * 将equipmentId,connectorId,shortId转化成17为的connectorId
+     * 将equipmentId,connectorId,shortId转化成17位的connectorId
+     * 从Redis查找短编号对应的完整connectorId
      *
-     * @param id
-     * @return
+     * @param id 设备ID(可能是17位、16位或6位短编号)
+     * @return 17位的connectorId
      */
     public String getConnectorId(String id) {
+        if (id == null) {
+            return null;
+        }
         return switch (id.length()) {
             case 17 -> id;
             case 16 -> id.concat("1");
-            case 6 ->
-                    CONNECTOR_ID_SHORT_ID_MAPPING.entrySet().stream().filter(entry -> id.equals(entry.getValue())).map(Map.Entry::getKey).findFirst().get();
+            case 6 -> {
+                // 从Redis反向查找短编号对应的connectorId
+                String connectorId = getConnectorIdByShortId(id);
+                yield connectorId;
+            }
             default -> null;
         };
     }
 
+    /**
+     * 通过短编号从Redis查找对应的connectorId(带缓存miss时按需加载)
+     *
+     * @param shortId 短编号
+     * @return connectorId
+     */
+    private String getConnectorIdByShortId(String shortId) {
+        // 从Redis反向索引中查找
+        String connectorId = KymCacheInjector.redisTemplate.opsForValue()
+                .get(RedisKeys.SHORT_ID_TO_CONNECTOR_ID + shortId);
+        if (connectorId != null) {
+            return connectorId;
+        }
+        // 缓存miss,从数据库加载
+        connectorId = loadConnectorIdByShortId(shortId);
+        if (connectorId != null) {
+            // 重新缓存反向索引
+            KymCacheInjector.redisTemplate.opsForValue()
+                    .set(RedisKeys.SHORT_ID_TO_CONNECTOR_ID + shortId, connectorId, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            // 同时缓存正向索引
+            KymCacheInjector.redisTemplate.opsForValue()
+                    .set(RedisKeys.CONNECTOR_ID_TO_SHORT_ID + connectorId, shortId, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+            return connectorId;
+        }
+        log.warn("未找到短编号对应的connectorId,shortId: {}", shortId);
+        return null;
+    }
+
+    /**
+     * 从数据库通过短编号加载connectorId
+     */
+    private String loadConnectorIdByShortId(String shortId) {
+        return executeWithDataSource(() -> {
+            EquipmentRelationService service = SpringUtil.getBean(EquipmentRelationService.class);
+            EquipmentRelation relation = service.getByShortId(shortId);
+            return relation != null ? relation.getConnectorId() : null;
+        });
+    }
+
 
     @Component
     public static class KymCacheInjector implements ApplicationListener<ApplicationStartedEvent> {
@@ -227,6 +423,9 @@ public enum KymCache {
 
             var connectorId2ShortId = equipmentRelations.stream().collect(Collectors.toMap(EquipmentRelation::getConnectorId, EquipmentRelation::getShortId));
             KymCache.INSTANCE.putConnectorId2ShortId(connectorId2ShortId);
+            // 同时建立反向索引:短编号 -> connectorId
+            var shortId2ConnectorId = equipmentRelations.stream().collect(Collectors.toMap(EquipmentRelation::getShortId, EquipmentRelation::getConnectorId));
+            KymCache.INSTANCE.putShortId2ConnectorId(shortId2ConnectorId);
 
             var connector2Station = equipmentRelations.stream().collect(Collectors.toMap(EquipmentRelation::getConnectorId, EquipmentRelation::getStationId));
             KymCache.INSTANCE.putConnectorId2StationId(connector2Station);