Compare commits

...

21 Commits

Author SHA1 Message Date
longguancheng
b46d40b7a3 Remove Redis and optimize initialization code 2026-03-06 15:43:53 +08:00
longguancheng
1da7eff80e Merge branch 'main' into dev_20260130_RemoveRedis 2026-03-06 15:43:34 +08:00
longguancheng
bff31311de Remove Redis and optimize initialization code 2026-03-06 14:14:10 +08:00
longguancheng
27dfb2d878 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis
# Conflicts:
#	backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/controller/JmImageController.java
#	backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/impl/JmJobStatusServiceImpl.java
#	backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/controller/SysUserController.java
2026-03-05 16:22:25 +08:00
longguancheng
29a81bea9b Optimize caching related content 2026-03-04 11:35:38 +08:00
longguancheng
90d8311840 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-03-02 11:24:21 +08:00
longguancheng
a3b29f6b90 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-10 10:30:08 +08:00
longguancheng
d3ab2f2278 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-08 23:58:14 +08:00
longguancheng
8d84e52e44 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-06 14:46:15 +08:00
longguancheng
f775c6f173 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis
# Conflicts:
#	backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/controller/JmImageController.java
2026-02-05 15:51:47 +08:00
longguancheng
5946d692c5 Optimize radar command code 2026-02-05 14:13:26 +08:00
longguancheng
f2f829a2f8 fix short caffeine. 2026-02-05 14:08:39 +08:00
longguancheng
6ce25eda79 Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-05 14:00:46 +08:00
longguancheng
75aeed56be Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-05 11:44:43 +08:00
longguancheng
c1575d47dd Merge branch 'main' into dev_20260130_RemoveRedis 2026-02-04 11:16:24 +08:00
longguancheng
f34844f110 Merge branch 'main' into dev_20260130_RemoveRedis 2026-02-03 15:52:50 +08:00
longguancheng
77e6de4c8b Merge branch 'main' into dev_20260130_RemoveRedis 2026-02-03 14:39:01 +08:00
longguancheng
af1cf9c662 Merge branch 'main' into dev_20260130_RemoveRedis 2026-02-02 16:22:38 +08:00
longguancheng
e82b63c8a8 Replace all Redis entries with Caffeine. 2026-02-02 16:18:42 +08:00
longguancheng
b0e3347d6b Merge branch 'refs/heads/main' into dev_20260130_RemoveRedis 2026-02-02 15:50:15 +08:00
longguancheng
b2f61137a9 Initialize Caffeine configuration and modify user and login settings to replace Redis 2026-02-02 15:46:39 +08:00
44 changed files with 2203 additions and 1370 deletions

View File

@ -12,6 +12,14 @@
<artifactId>skyeye-service-manager</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>

View File

@ -1,5 +1,6 @@
package com.zhangy.skyeye;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -9,8 +10,10 @@ import java.io.File;
@EnableScheduling
@MapperScan("com.zhangy.skyeye.**.mapper")
@SpringBootApplication(scanBasePackages = "com.zhangy.**")
@SpringBootApplication(scanBasePackages = "com.zhangy")
@Slf4j
public class SEApplication {
public static void main(String[] args) {
String projectRoot = System.getProperty("user.dir");
// String libPath = projectRoot + "/library/logisen/GMTI";

View File

@ -0,0 +1,51 @@
package com.zhangy.skyeye.cache;
import org.springframework.cache.Cache;
import java.util.Map;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 抽象类型缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/3 14:31
*/
public abstract class AbstractTypedCache {
private final Cache cache;
protected AbstractTypedCache(Cache cache) {
this.cache = cache;
}
protected void put(String key, Object value) {
cache.put(key, value);
}
protected <T> T get(String key, Class<T> type) {
return cache.get(key, type);
}
protected Object getRaw(String key) {
Cache.ValueWrapper wrapper = cache.get(key);
return wrapper == null ? null : wrapper.get();
}
protected void evict(String key) {
cache.evict(key);
}
/** 获取原生 Caffeine Map用于全量读取 */
public Map<Object, Object> asMap() {
Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof com.github.benmanes.caffeine.cache.Cache) {
return ((com.github.benmanes.caffeine.cache.Cache<Object, Object>) nativeCache).asMap();
}
return null;
}
protected Object getNativeCache() {
return cache.getNativeCache();
}
}

View File

@ -0,0 +1,101 @@
package com.zhangy.skyeye.cache.config;
import com.zhangy.skyeye.cache.image.ImageJoinTypedCache;
import com.zhangy.skyeye.cache.publics.UserTokenTypedCache;
import com.zhangy.skyeye.cache.sar.SarTypedCache;
import com.zhangy.skyeye.cache.uav.UavTypedCache;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: Cache Bean 显式注册
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/3 14:15
*/
@Configuration
public class CacheBeanConfig {
/* ================= USER ================= */
@Bean("userTokenNativeCache")
public Cache userTokens(CacheManager manager) {
return require(manager, "user-tokens");
}
// TypedCache 封装层
@Bean
public UserTokenTypedCache userTokensCache(
@Qualifier("userTokenNativeCache") Cache cache) {
return new UserTokenTypedCache(cache);
}
/* ================= IMAGE ================= */
@Bean("imageJoinShortNativeCache")
public Cache imageJoinShort(CacheManager manager) {
return require(manager, "image-join-short");
}
@Bean
public ImageJoinTypedCache imageJoinShortCache(
@Qualifier("imageJoinShortNativeCache") Cache cache) {
return new ImageJoinTypedCache(cache);
}
/* ================= SAR ================= */
@Bean("sarPermanentNativeCache")
public Cache sarPermanent(CacheManager manager) {
return require(manager, "sar-permanent");
}
@Bean
public SarTypedCache sarPermanentCache(
@Qualifier("sarPermanentNativeCache") Cache cache) {
return new SarTypedCache(cache);
}
@Bean("sarShortNativeCache")
public Cache sarShort(CacheManager manager) {
return require(manager, "sar-short");
}
@Bean
public SarTypedCache sarShortCache(
@Qualifier("sarShortNativeCache") Cache cache) {
return new SarTypedCache(cache);
}
/* ================= UAV ================= */
@Bean("uavPermanentNativeCache")
public Cache uavPermanent(CacheManager manager) {
return require(manager, "uav-permanent");
}
@Bean
public UavTypedCache uavPermanentCache(
@Qualifier("uavPermanentNativeCache") Cache cache) {
return new UavTypedCache(cache);
}
@Bean("uavShortNativeCache")
public Cache uavShort(CacheManager manager) {
return require(manager, "uav-short");
}
@Bean
public UavTypedCache uavShortCache(
@Qualifier("uavShortNativeCache") Cache cache) {
return new UavTypedCache(cache);
}
private Cache require(CacheManager manager, String name) {
Cache cache = manager.getCache(name);
if (cache == null) {
throw new IllegalStateException("未找到缓存: " + name);
}
return cache;
}
}

View File

@ -0,0 +1,68 @@
package com.zhangy.skyeye.cache.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.concurrent.TimeUnit;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 缓存配置
* @AUTHOR: GuanCheng Long
* @DATE: 2026/1/30 12:51
*/
@Configuration
@EnableCaching
@Slf4j
public class CacheConfig {
@Bean
@Primary
public CacheManager cacheManager() {
log.info("【重要】CacheConfig 被加载,开始创建 CacheManager");
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS) // 与原来 TOKEN_EXPIRE 一致24小时
.maximumSize(10000) // 根据用户量调整防止 OOM
.recordStats() // 可监控命中率
// 可加 .removalListener(...) 监听移除事件
);
// 专门给 token 一个独立的 cache name便于细粒度管理
cacheManager.registerCustomCache("user-tokens",
Caffeine.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.maximumSize(5000) // 预计活跃用户数
.build());
// 永久缓存SAR 载荷专用不设过期
cacheManager.registerCustomCache("sar-permanent", Caffeine.newBuilder()
.maximumSize(5000) // 根据 SAR 载荷数量调整
.build());
// 短时状态/连接已存在确认过期时间
cacheManager.registerCustomCache("sar-short", Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS) // 建议 3~5 防边界丢失
.maximumSize(2000)
.build());
// 永久缓存UAV 专用不设过期
cacheManager.registerCustomCache("uav-permanent", Caffeine.newBuilder()
.maximumSize(5000) // 根据无人机数量调整
.build());
cacheManager.registerCustomCache("uav-short", Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 与原 CACHE_EXPIRE_SECONDS 一致
.maximumSize(1000) // 按设备/IP 数量预估
.recordStats()
.build());
cacheManager.registerCustomCache("image-join-short", Caffeine.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS) // 与原 CACHE_EXPIRE_SECOND 一致
.maximumSize(5000) // 航线数量预估
.build());
log.info("【重要】CacheManager 创建完成,缓存列表:{}", cacheManager.getCacheNames());
return cacheManager;
}
}

View File

@ -0,0 +1,85 @@
package com.zhangy.skyeye.cache.debug;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Set;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 缓存调试工具统一查看所有缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/6 12:55
*/
@RestController
@RequestMapping("/debug/cache")
@RequiredArgsConstructor
@Profile("dev")
public class CacheDebugController {
private final CacheDebugService cacheDebugService;
/**
* 查看所有缓存
*/
@GetMapping("/all")
public Map<String, Object> allCaches() {
return cacheDebugService.allCaches();
}
/**
* 查看缓存key
*/
@GetMapping("/keys/{name}")
public Set<Object> keys(@PathVariable String name) {
return cacheDebugService.keys(name);
}
/**
* 查看某个缓存
*/
@GetMapping("/{name}")
public Map<Object, Object> cache(@PathVariable String name) {
return cacheDebugService.cache(name);
}
/**
* 查看某个key
*/
@GetMapping("/{name}/{key}")
public Object get(@PathVariable String name,
@PathVariable String key) {
return cacheDebugService.get(name, key);
}
/**
* 删除key
*/
@DeleteMapping("/{name}/{key}")
public String evict(@PathVariable String name,
@PathVariable String key) {
cacheDebugService.evict(name, key);
return "OK";
}
/**
* 清空缓存
*/
@DeleteMapping("/{name}")
public String clear(@PathVariable String name) {
cacheDebugService.clear(name);
return "OK";
}
/**
* 缓存统计
*/
@GetMapping("/stats")
public Object stats() {
return cacheDebugService.stats();
}
}

View File

@ -0,0 +1,93 @@
package com.zhangy.skyeye.cache.debug;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 监控逻辑
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/6 13:02
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Profile("dev")
public class CacheDebugService {
private final CacheManager cacheManager;
public Map<String, Object> allCaches() {
Map<String, Object> map = new LinkedHashMap<>();
for (String name : cacheManager.getCacheNames()) {
map.put(name, cache(name));
}
return map;
}
public Map<Object, Object> cache(String name) {
Cache cache = cacheManager.getCache(name);
if (!(cache instanceof CaffeineCache)) {
return Collections.emptyMap();
}
return ((CaffeineCache) cache)
.getNativeCache()
.asMap();
}
public Set<Object> keys(String name) {
return cache(name).keySet();
}
public Object get(String name, Object key) {
Cache cache = cacheManager.getCache(name);
if (cache == null) {
return null;
}
Cache.ValueWrapper value = cache.get(key);
return value == null ? null : value.get();
}
public void evict(String name, Object key) {
Cache cache = cacheManager.getCache(name);
if (cache != null) {
cache.evict(key);
}
}
public void clear(String name) {
Cache cache = cacheManager.getCache(name);
if (cache != null) {
cache.clear();
}
}
public Map<String, Object> stats() {
Map<String, Object> result = new LinkedHashMap<>();
for (String name : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(name);
if (!(cache instanceof CaffeineCache)) {
continue;
}
CaffeineCache caffeine = (CaffeineCache) cache;
CacheStats stats = caffeine.getNativeCache().stats();
Map<String, Object> info = new HashMap<>();
info.put("size", caffeine.getNativeCache().asMap().size());
info.put("hitCount", stats.hitCount());
info.put("missCount", stats.missCount());
info.put("hitRate", stats.hitRate());
info.put("evictionCount", stats.evictionCount());
result.put(name, info);
}
return result;
}
}

View File

@ -0,0 +1,48 @@
package com.zhangy.skyeye.cache.debug;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.cache.Cache;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 自动监控缓存操作
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/6 13:06
*/
@Slf4j
@Aspect
@Component
@Profile("dev")
public class CacheMonitorAspect {
@After("execution(* org.springframework.cache.Cache.put(..))")
public void afterPut(JoinPoint jp) {
Cache cache = (Cache) jp.getTarget();
Object key = jp.getArgs()[0];
log.debug("CACHE PUT -> {} : {}",
cache.getName(),
key);
}
@After("execution(* org.springframework.cache.Cache.get(..))")
public void afterGet(JoinPoint jp) {
Cache cache = (Cache) jp.getTarget();
Object key = jp.getArgs()[0];
log.debug("CACHE GET -> {} : {}",
cache.getName(),
key);
}
@After("execution(* org.springframework.cache.Cache.evict(..))")
public void afterEvict(JoinPoint jp) {
Cache cache = (Cache) jp.getTarget();
Object key = jp.getArgs()[0];
log.warn("CACHE EVICT -> {} : {}",
cache.getName(),
key);
}
}

View File

@ -0,0 +1,53 @@
package com.zhangy.skyeye.cache.debug;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 实时监控任务
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/6 13:07
*/
@Component
@RequiredArgsConstructor
@Slf4j
@Profile("dev")
public class CacheWatcherUltimate {
private final CacheManager cacheManager;
@Scheduled(fixedDelay = 30000)
public void watchCaches() {
log.info("========== CACHE STATUS BEGIN ==========");
for (String name : cacheManager.getCacheNames()) {
org.springframework.cache.Cache springCache = cacheManager.getCache(name);
if (!(springCache instanceof CaffeineCache)) {
continue;
}
CaffeineCache caffeineCache = (CaffeineCache) springCache;
Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();
Map<Object, Object> map = nativeCache.asMap();
log.info("CACHE [{}] SIZE = {}", name, map.size());
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
log.info(
" KEY = {} | VALUE_TYPE = {}",
key,
value == null ? "null" : value.getClass().getSimpleName()
);
}
}
log.info("========== CACHE STATUS END ==========");
}
}

View File

@ -0,0 +1,36 @@
package com.zhangy.skyeye.cache.image;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: IMAGE 缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/5 15:56
*/
@Component
@RequiredArgsConstructor
public class ImageCache {
private final Map<String, ImageJoinTypedCache> caches;
private ImageJoinTypedCache getShort() {
return caches.get("imageJoinShortCache");
}
public void put(String key, Object value) {
getShort().putValue(key, value);
}
public <T> T get(String key, Class<T> clazz) {
return getShort().getValue(key, clazz);
}
public void evict(String key) {
getShort().evictValue(key);
}
}

View File

@ -0,0 +1,32 @@
package com.zhangy.skyeye.cache.image;
import com.zhangy.skyeye.cache.AbstractTypedCache;
import org.springframework.cache.Cache;
import java.util.Map;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: IMAGE 缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/5 15:55
*/
public class ImageJoinTypedCache extends AbstractTypedCache {
public ImageJoinTypedCache(Cache cache) {
super(cache);
}
public void putValue(String key, Object value) {
super.put(key, value);
}
public <T> T getValue(String key, Class<T> clazz) {
return super.get(key, clazz);
}
public void evictValue(String key) {
super.evict(key);
}
}

View File

@ -0,0 +1,49 @@
package com.zhangy.skyeye.cache.publics;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 业务聚合层对外暴露用户 token 操作
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/5 14:06
*/
@Component
@RequiredArgsConstructor
public class UserTokenCache {
private final Map<String, UserTokenTypedCache> caches;
private UserTokenTypedCache getUserTokenCache() {
return caches.get("userTokensCache");
}
// 保存 token
public void storeToken(String tokenKey, String token) {
getUserTokenCache().put(tokenKey, token);
}
// 获取并验证 token 是否一致
public boolean validateToken(String tokenKey, String currentToken) {
String stored = getUserTokenCache().get(tokenKey);
return stored != null && stored.equals(currentToken);
}
// 获取 token
public String getToken(String tokenKey) {
return getUserTokenCache().get(tokenKey);
}
// 删除 token
public void evictToken(String tokenKey) {
getUserTokenCache().evictValue(tokenKey);
}
public Object getNativeCache() {
return getUserTokenCache().getNativeCacheObject(); // UserTokenTypedCache 里需要有 getCache()
}
}

View File

@ -0,0 +1,34 @@
package com.zhangy.skyeye.cache.publics;
import com.zhangy.skyeye.cache.AbstractTypedCache;
import org.springframework.cache.Cache;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 安全封装 User Token Cache
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/5 14:14
*/
public class UserTokenTypedCache extends AbstractTypedCache {
public UserTokenTypedCache(Cache cache) {
super(cache);
}
public void put(String key, String token) {
super.put(key, token);
}
public String get(String key) {
return super.get(key, String.class);
}
public void evictValue(String key) {
super.evict(key);
}
public Object getNativeCacheObject() {
return super.getNativeCache(); // UserTokenTypedCache 里需要有 getCache()
}
}

View File

@ -0,0 +1,95 @@
package com.zhangy.skyeye.cache.sar;
import com.zhangy.skyeye.device.entity.SkyeyePayload;
import com.zhangy.skyeye.jm.dto.JmSarStatusDTO;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.sar.dto.SarErrorDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: Sar 缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/3 14:24
*/
@Component
@RequiredArgsConstructor
public class SarCache {
private final Map<String, SarTypedCache> caches;
private SarTypedCache getPermanent() {
return caches.get("sarPermanentCache");
}
private SarTypedCache getShort() {
return caches.get("sarShortCache");
}
/* ========== 永久缓存SAR 载荷操作 ========== */
// 缓存 SAR 载荷
public void cacheSar(SkyeyePayload sar) {
getPermanent().putValue(sar.getId().toString(), sar);
}
// 获取单个 SAR
public SkyeyePayload getOne(Long sarId) {
return getPermanent().getValue(sarId.toString(), SkyeyePayload.class);
}
// 删除指定 SAR
public void evictSar(Long sarId) {
getPermanent().evictValue(sarId.toString());
}
// 获取永久缓存中所有 SAR 对象
public List<SkyeyePayload> getAll() {
Map<Object, Object> map = getPermanent().asMap();
if (map == null || map.isEmpty()) return List.of();
return map.values().stream()
.filter(v -> v instanceof SkyeyePayload)
.map(v -> (SkyeyePayload) v)
.collect(Collectors.toList());
}
/* ========== 连接状态 ========== */
public void markConnected(String ip) {
getPermanent().putValue(CacheKey.getSarConnected(ip), Boolean.TRUE);
}
public boolean isConnected(String ip) {
return getPermanent().getValue(CacheKey.getSarConnected(ip), Boolean.class) != null;
}
public void removeConnection(String ip) {
getPermanent().evictValue(CacheKey.getSarConnected(ip));
}
/* ========== 实时状态 ========== */
public void saveStatus(String ip, JmSarStatusDTO dto) {
getShort().putValue(CacheKey.getSarStatus(ip), dto);
}
public JmSarStatusDTO getLatestStatus(String ip) {
return getShort().getValue(CacheKey.getSarStatus(ip), JmSarStatusDTO.class);
}
/* ========== 控制回执 ========== */
public void saveControlBack(String ip, SarErrorDTO dto) {
getShort().putValue(CacheKey.getSarControlBack(ip), dto);
}
public SarErrorDTO getControlBack(String ip) {
return getShort().getValue(CacheKey.getSarControlBack(ip), SarErrorDTO.class);
}
public void clearControlBack(String ip) {
getShort().evictValue(CacheKey.getSarControlBack(ip));
}
}

View File

@ -0,0 +1,39 @@
package com.zhangy.skyeye.cache.sar;
import com.zhangy.skyeye.cache.AbstractTypedCache;
import org.springframework.cache.Cache;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 通用 SAR 缓存可作为永久或短期缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/5 12:06
*/
public class SarTypedCache extends AbstractTypedCache {
public SarTypedCache(Cache cache) {
super(cache);
}
/**
* 类型安全读取
*/
public <T> T getValue(String key, Class<T> type) {
return super.get(key, type);
}
/**
* 类型安全写入
*/
public void putValue(String key, Object value) {
super.put(key, value);
}
/**
* 类型安全删除
*/
public void evictValue(String key) {
super.evict(key);
}
}

View File

@ -0,0 +1,68 @@
package com.zhangy.skyeye.cache.uav;
import com.zhangy.skyeye.cache.sar.SarTypedCache;
import com.zhangy.skyeye.device.entity.SkyeyeUav;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: Uav 缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/4 14:19
*/
@Component
@RequiredArgsConstructor
public class UavCache {
private final Map<String, UavTypedCache> caches;
private UavTypedCache getPermanent() {
return caches.get("uavPermanentCache");
}
private UavTypedCache getShort() {
return caches.get("uavShortCache");
}
/* ========== 永久缓存UAV 操作 ========== */
public void put(SkyeyeUav uav) {
getPermanent().putValue(uav.getId().toString(), uav);
}
public SkyeyeUav get(Long id) {
return getPermanent().getValue(id.toString(), SkyeyeUav.class);
}
public void evict(Long id) {
getPermanent().evictValue(id.toString());
}
public List<SkyeyeUav> getAll() {
Map<Object, Object> map = getPermanent().asMap();
if (map == null) {
return List.of();
}
return map.values().stream()
.map(v -> (SkyeyeUav) v)
.collect(Collectors.toList());
}
/* ========== 短期缓存 ========== */
public void putShort(String key, Object value) {
getShort().putValue(key, value);
}
public <T> T getShort(String key, Class<T> clazz) {
return getShort().getValue(key, clazz);
}
public void evictShort(String key) {
getShort().evictValue(key);
}
}

View File

@ -0,0 +1,30 @@
package com.zhangy.skyeye.cache.uav;
import com.zhangy.skyeye.cache.AbstractTypedCache;
import org.springframework.cache.Cache;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: Uav 缓存可作为永久或短期缓存
* @AUTHOR: GuanCheng Long
* @DATE: 2026/3/4 14:20
*/
public class UavTypedCache extends AbstractTypedCache {
public UavTypedCache(Cache cache) {
super(cache);
}
public <T> T getValue(String key, Class<T> type) {
return super.get(key, type);
}
public void putValue(String key, Object value) {
super.put(key, value);
}
public void evictValue(String key) {
super.evict(key);
}
}

View File

@ -1,6 +1,7 @@
package com.zhangy.skyeye.device.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.common.extend.dto.PageDTO;
import com.zhangy.skyeye.common.extend.dto.QueryDTO;
import com.zhangy.skyeye.common.extend.enums.EnumUtil;
@ -15,68 +16,67 @@ import com.zhangy.skyeye.jm.dto.JmJobStatusDTO;
import com.zhangy.skyeye.jm.dto.JmSarStatusDTO;
import com.zhangy.skyeye.jm.dto.JmUavStatusDTO;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.publics.consts.ExecStatusEnum;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.sar.service.ISarControlService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class PayloadServiceImpl implements IPayloadService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private PayloadMapper payloadMapper;
@Autowired
private JmJobStatusService sarJobStatusService;
@Autowired
private ISarControlService sarControlService;
private final SarCache sarCache;
@PostConstruct
public void initAndCacheAllSar() {
// 开始缓存所有 SAR如果 permanentCache null这里会安全跳过
PayloadQueryDTO payloadQueryDTO = new PayloadQueryDTO();
payloadQueryDTO.setType(PayloadTypeEnum.SAR.getCode());
List<SkyeyePayload> sarList = selectList(payloadQueryDTO);
sarList.forEach(sarCache::cacheSar);
log.info("SAR 载荷缓存完成,共 {} 条", sarList.size());
}
@Override
public IPage<SkyeyePayload> selectPage(PageDTO param) {
return payloadMapper.selectPage(param);
}
@PostConstruct
public void cacheAllSar() {
PayloadQueryDTO payloadQueryDTO = new PayloadQueryDTO();
payloadQueryDTO.setType(PayloadTypeEnum.SAR.getCode());
List<SkyeyePayload> sarList = selectList(payloadQueryDTO);
sarList.forEach(this::cacheSar);
}
private void cacheSar(SkyeyePayload e) {
redisTemplate.opsForHash().put(CacheKey.DEVICE_SAR, e.getId().toString(), e);
}
@Override
public List<SkyeyePayload> getSar(Long... sarId) {
if (ObjectUtil.isNotEmpty(sarId)) {
return redisTemplate.opsForHash().multiGet(CacheKey.DEVICE_SAR, Arrays.asList(sarId));
return Arrays.stream(sarId)
.map(sarCache::getOne)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
return redisTemplate.opsForHash().values(CacheKey.DEVICE_SAR);
// 获取所有 SAR全量从缓存读取
return sarCache.getAll().stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
public SkyeyePayload getOne(Long payloadId) {
SkyeyePayload p = (SkyeyePayload) redisTemplate.opsForHash().get(CacheKey.DEVICE_SAR, payloadId.toString());
// 若有其它种类载荷则判空继续查询
return p;
return sarCache.getOne(payloadId);
}
@Override
@ -84,10 +84,6 @@ public class PayloadServiceImpl implements IPayloadService {
return payloadMapper.selectList(param);
}
@Autowired
private RedisUtil redisUtil;
@Override
public List<SkyeyePayload> getEnableList() {
// 从缓存查询所有sar
@ -98,12 +94,14 @@ public class PayloadServiceImpl implements IPayloadService {
// 从内存查询所有任务中的雷达
Collection<JmJobStatusDTO> statusVos = sarJobStatusService.getAll();
Set<Long> jobSarSet = statusVos.stream()
.flatMap(job -> job.getUavMap().values().stream().filter(uav -> uav.getSarStatus() != ExecStatusEnum.OVER))
.map(JmUavStatusDTO::getSarId)
.flatMap(job -> job.getUavMap().values().stream()
.filter(uav -> uav.getSarStatus() != ExecStatusEnum.OVER))
.map(JmUavStatusDTO::getSarId) // 正确取 SAR ID
.collect(Collectors.toSet());
// 筛选出未在任务中且已连接的雷达
return sarList.stream()
.filter(sar -> !jobSarSet.contains(sar.getId()) && redisUtil.hasKey(CacheKey.getSarConnect(sar.getIp())))
.filter(sar -> !jobSarSet.contains(sar.getId())
&& sarCache.getLatestStatus(sar.getIp()) != null)
.collect(Collectors.toList());
}
@ -128,7 +126,7 @@ public class PayloadServiceImpl implements IPayloadService {
} else {
payloadMapper.insert(e);
}
cacheSar(e);
sarCache.cacheSar(e);
return e;
}
@ -142,9 +140,7 @@ public class PayloadServiceImpl implements IPayloadService {
payloadMapper.update(e);
// 若是sar则更新缓存
List<SkyeyePayload> list = selectById(PayloadTypeEnum.SAR, e.getId());
if (ObjectUtil.isNotEmpty(list)) {
cacheSar(list.get(0));
}
if (ObjectUtil.isNotEmpty(list)) sarCache.cacheSar(list.get(0));
return e;
}
@ -171,38 +167,42 @@ public class PayloadServiceImpl implements IPayloadService {
@Transactional
@Override
public void delete(Long... id) {
// 查询任务中的sar
List<Long> inJobSarIds = sarJobStatusService.getAll()
.stream()
.flatMap(vo -> vo.getUavMap().values().stream())
.filter(vo -> vo.getSarStatus() != ExecStatusEnum.OVER)
.map(JmUavStatusDTO::getSarId)
.collect(Collectors.toList());
// 任务中的不可删除
for (Long sarId : id) {
if (inJobSarIds.contains(sarId)) {
throw ServiceException.noLog("[" + getSar(sarId).get(0).getName() + "]在执行任务,不可删除");
List<SkyeyePayload> sar = getSar(sarId);
if (!sar.isEmpty()) {
throw ServiceException.noLog("[" + sar.get(0).getName() + "]在执行任务,不可删除");
}
}
}
payloadMapper.deleteLogic(id);
redisTemplate.opsForHash().delete(CacheKey.DEVICE_SAR, id);
Arrays.stream(id).forEach(sarCache::evictSar);
}
@Override
public JmSarStatusDTO getLastStatus(String payloadIp) {
String statusKey = CacheKey.getSarConnect(payloadIp);
if (!redisUtil.hasKey(statusKey)) {
JmSarStatusDTO status = sarCache.getLatestStatus(payloadIp);
if (status == null) {
try {
sarControlService.connect(payloadIp);
TimeUnit.SECONDS.sleep(1);
status = sarCache.getLatestStatus(payloadIp);
} catch (InterruptedException ex) {
throw new ServiceException("获取雷达状态失败:" + ex.getMessage());
}
if (!redisUtil.hasKey(statusKey)) {
if (status == null) {
throw ServiceException.noLog("无法连接到SAR载荷[" + payloadIp + "]");
}
}
return (JmSarStatusDTO) redisUtil.get(statusKey);
return status;
}
@Override

View File

@ -1,6 +1,7 @@
package com.zhangy.skyeye.device.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.zhangy.skyeye.cache.uav.UavCache;
import com.zhangy.skyeye.common.extend.dto.PageDTO;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
@ -9,77 +10,77 @@ import com.zhangy.skyeye.device.mapper.UavMapper;
import com.zhangy.skyeye.device.service.IUavService;
import com.zhangy.skyeye.jm.dto.JmUavStatusDTO;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.publics.consts.ExecStatusEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class UavServiceImpl implements IUavService {
@Autowired
private UavMapper uavMapper;
@Autowired
private JmJobStatusService sarJobStatusService;
@Autowired
private RedisTemplate redisTemplate;
private final UavCache uavCache;
@PostConstruct
public void cacheAll() {
selectList(null).forEach(uavCache::put);
}
@Override
public IPage<SkyeyeUav> selectPage(PageDTO param) {
return uavMapper.selectPage(param);
}
@Override
public List<SkyeyeUav> selectList(PageDTO param) {
return uavMapper.selectList(param);
}
@Override
public List<SkyeyeUav> selectById(Long... id) {
return uavMapper.selectById(id);
}
@PostConstruct
public void cacheAll() {
selectList(null).forEach(e -> {
cache(e);
});
}
private void cache(SkyeyeUav e) {
redisTemplate.opsForHash().put(CacheKey.DEVICE_UAV, e.getId(), e);
}
@Override
public List<SkyeyeUav> get(Long... id) {
if (ObjectUtil.isNotEmpty(id)) {
return redisTemplate.opsForHash().multiGet(CacheKey.DEVICE_UAV, Arrays.asList(id));
return Arrays.stream(id)
.map(uavCache::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
return redisTemplate.opsForHash().values(CacheKey.DEVICE_UAV);
// 全量获取
return uavCache.getAll();
}
@Override
public SkyeyeUav getOne(Long id) {
List<SkyeyeUav> list = get(id);
if (ObjectUtil.isNotEmpty(list)) {
return list.get(0);
SkyeyeUav uav = uavCache.get(id);
if (uav != null) {
return uav;
}
list = uavMapper.selectById(id);
// 缓存 miss DB 查并回填
List<SkyeyeUav> list = uavMapper.selectById(id);
if (ObjectUtil.isEmpty(list)) {
throw new ServiceException("无效的无人机ID" + id);
}
SkyeyeUav uav = list.get(0);
cache(uav);
uav = list.get(0);
uavCache.put(uav);
return uav;
}
@ -92,8 +93,9 @@ public class UavServiceImpl implements IUavService {
}
// 从内存查询所有任务中的无人机暂时用SAR状态判断
Set<Long> jobUavSet = sarJobStatusService.getAll().stream()
.flatMap(job -> job.getUavMap().values().stream().filter(uav -> uav.getSarStatus() != ExecStatusEnum.OVER))
.map(JmUavStatusDTO::getSarId)
.flatMap(job -> job.getUavMap().values().stream()
.filter(uav -> uav.getSarStatus() != ExecStatusEnum.OVER))
.map(JmUavStatusDTO::getUavId) // 注意这里是 uavId不是 sarId
.collect(Collectors.toSet());
// 筛选出未在任务中的无人机
return uavList.stream()
@ -110,11 +112,11 @@ public class UavServiceImpl implements IUavService {
uavMapper.update(e);
} else {
uavMapper.insert(e);
cache(e);
}
uavCache.put(e);
return e;
}
@Transactional
@Override
public SkyeyeUav update(SkyeyeUav e) {
@ -123,13 +125,14 @@ public class UavServiceImpl implements IUavService {
// 若是sar则更新缓存
List<SkyeyeUav> list = selectById(e.getId());
if (ObjectUtil.isNotEmpty(list)) {
cache(list.get(0));
uavCache.put(list.get(0));
}
return e;
}
/**
* 保存前校验
*
* @param e
* @return 返回已逻辑删除的载荷
*/
@ -144,16 +147,15 @@ public class UavServiceImpl implements IUavService {
return local;
}
}
@Transactional
@Override
public void delete(Long... ids) {
// 查询任务中的设备
List<Long> inJobUavIds = sarJobStatusService.getAll()
.stream()
.flatMap(vo -> vo.getUavMap().values().stream())
.filter(vo -> vo.getSarStatus() != ExecStatusEnum.OVER)
.map(vo -> vo.getUavId())
.map(JmUavStatusDTO::getUavId)
.collect(Collectors.toList());
// 任务中的不可删除
for (Long id : ids) {
@ -162,6 +164,6 @@ public class UavServiceImpl implements IUavService {
}
}
uavMapper.deleteLogic(ids);
redisTemplate.opsForHash().delete(CacheKey.DEVICE_UAV, ids);
Arrays.stream(ids).forEach(uavCache::evict);
}
}

View File

@ -11,20 +11,14 @@ import com.zhangy.skyeye.jm.dto.JmJobImageDTO;
import com.zhangy.skyeye.jm.entity.JmImage;
import com.zhangy.skyeye.jm.service.JmImageService;
import com.zhangy.skyeye.publics.consts.FileTypeEnum;
import com.zhangy.skyeye.publics.utils.LocalLockUtil;
import com.zhangy.skyeye.publics.utils.OpenCVUtil;
import org.opencv.core.Core;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.io.File;
import java.net.ConnectException;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.zhangy.skyeye.publics.consts.CacheKey.SAR_IMAGE_UPLOAD_LOCK;
@ -33,12 +27,12 @@ import static com.zhangy.skyeye.publics.consts.CacheKey.SAR_IMAGE_UPLOAD_LOCK;
@RestController
@RequestMapping("/sar/image")
public class JmImageController {
@Autowired
private JmImageService sarImageService;
@Autowired
private RedisTemplate redisTemplate;
private LocalLockUtil localLockUtil;
/**
* 分页查询
@ -47,16 +41,16 @@ public class JmImageController {
public Object selectPage(@Valid @RequestBody JmImagePageDTO param) {
return sarImageService.selectPage(param);
}
/**
/**
* 按任务查询低精度图像
*/
@GetMapping("/queryByJob")
public Object selectList(@RequestParam Long jobId) {
return sarImageService.selectByJob(FileTypeEnum.SAR_IMAGE_LOW, jobId);
}
/**
/**
* 查询详情
*/
@GetMapping("/detail")
@ -80,31 +74,19 @@ public class JmImageController {
@PostMapping("/addHigh")
public Object addHighImage(@Valid @RequestBody JmJobImageDTO dto) {
String lockKey = SAR_IMAGE_UPLOAD_LOCK;
String requestId = UUID.randomUUID().toString(); // 唯一标识当前请求
// 尝试 60 秒获取锁比原来 1 分钟更灵活
boolean locked = localLockUtil.tryLock(lockKey, 60);
if (!locked) {
throw ServiceException.noLog("正在上传其它图像,请稍后重试!");
}
try {
// 原子操作获取锁设置过期时间30秒
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
1,
TimeUnit.MINUTES
);
if (Boolean.FALSE.equals(locked)) {
throw ServiceException.noLog(MessageUtils.message("sar.image.add_highres.eagain"));
}
// 执行高精度图像添加逻辑
sarImageService.addHighImage(dto);
} finally {
// 仅删除自己的锁Lua脚本保证原子性
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ "return redis.call('del', KEYS[1]) "
+ "else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
requestId
);
// 释放锁
localLockUtil.unlock(lockKey);
}
return MessageUtils.message("sar.image.add_highres.success");
return "操作完成";
}
/**
@ -138,6 +120,7 @@ public class JmImageController {
.map(e -> BeanUtil.copyProperties(e, JmImageKtyDTO.class))
.collect(Collectors.toList());
}
static {
//System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
OpenCVUtil.loadNativeDylib();

View File

@ -1,5 +1,6 @@
package com.zhangy.skyeye.jm.service.impl;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.common.extend.util.MessageUtils;
import com.zhangy.skyeye.jm.dto.*;
@ -14,7 +15,6 @@ import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.publics.consts.ExecStatusEnum;
import com.zhangy.skyeye.publics.consts.WebSocketKey;
import com.zhangy.skyeye.publics.utils.CoordUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.sar.listen.SarImageUdpProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -46,7 +46,7 @@ public class JmJobStatusServiceImpl implements JmJobStatusService {
private SarImageUdpProcessor imageProcessService;
@Autowired
private RedisUtil redisUtil;
private SarCache sarCache;
// @Autowired
// private ISmpSubscriptService subscriptService;
@ -183,10 +183,9 @@ public class JmJobStatusServiceImpl implements JmJobStatusService {
}
for (JmJobPayload payload : newUav.getPayloadList()) {
// 校验雷达连接状态
if (!redisUtil.hasKey(CacheKey.getSarConnect(payload.getIp()))) {
if (!sarCache.isConnected(payload.getIp())) {
throw ServiceException.warnLog(MessageUtils.message("device.sar.offline", payload.getPayloadName()));
}
if (runningPayloadIds.contains(payload.getPayloadId())) {
throw ServiceException.warnLog(MessageUtils.message("sar.control.turnon.esarinexec"));
}
@ -295,11 +294,11 @@ public class JmJobStatusServiceImpl implements JmJobStatusService {
}
endAirline(jobId, jobVo, uavVo, airlineVo, wsVo, nextAirline == null);
} else if (isBoot == 1 && aStatus == ExecStatusEnum.PROCESSING && overButSending) {
// 前一航线的图片没传完就开始了新航线
endAirline(jobId, jobVo, uavVo, airlineVo, wsVo, nextAirline == null);
if (nextAirline != null) {
startAirline(nextAirline, wsVo);
}
// 前一航线的图片没传完就开始了新航线
endAirline(jobId, jobVo, uavVo, airlineVo, wsVo, nextAirline == null);
if (nextAirline != null) {
startAirline(nextAirline, wsVo);
}
} // else {
// return;
// }

View File

@ -1,27 +1,22 @@
package com.zhangy.skyeye.jm.task;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.device.entity.SkyeyePayload;
import com.zhangy.skyeye.device.service.IPayloadService;
import com.zhangy.skyeye.jm.dto.JmJobStatusDTO;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.jm.dto.JmSarStatusDTO;
import com.zhangy.skyeye.publics.service.ISysLoginService;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.sar.exception.SarConnectException;
import com.zhangy.skyeye.sar.service.ISarControlService;
// import com.zhangy.skyeye.smp.dto.SmpUavStatusWsDTO;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* 任务管理定时任务
@ -34,18 +29,6 @@ public class JmTaskScheduler {
@Setter
private boolean isDebug;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JmJobStatusService jobStatusService;
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Autowired
private ISysLoginService loginService;
@ -56,7 +39,7 @@ public class JmTaskScheduler {
private IPayloadService payloadService;
@Autowired
private ISarControlService sarControlService;
private SarCache sarCache; // 直接用领域缓存
/**
* 每1秒向前端推送雷达状态信息断开连接则所有数据置0返回
@ -108,28 +91,31 @@ public class JmTaskScheduler {
/**
* 每10秒进行载荷连接判断
* 1. 如果有用户登录
* 1未执行连接指令进行连接
* 2已经执行连接指令但没有获取到sar状态信息进行重连
* 1未执行连接指令进行连接
* 2已经执行连接指令但没有获取到sar状态信息进行重连
* 2. 没有用户在线
* 因为断开连接会先结束任务不能断开连接等硬件断电
* 因为断开连接会先结束任务不能断开连接等硬件断电
*/
@Scheduled(fixedRate = 10000)
public void connectSar() {
if (!loginService.hasLogged() || isDebug) { // 断开
/*Map<Object, Object> map = redisUtil.hmget(CacheKey.SAR_CONNECTED);
if (map != null && map.size() > 0) {
map.keySet().forEach(ip -> controlInfoService.sendUdp((String) ip, SarControlTypeEnum.DISCONNECT));
}*/
} else { // 连接
if (!loginService.hasLogged() || isDebug) {
// 断开逻辑如果需要可遍历 permanentCache 或其他方式获取 ip 列表
// 但当前注释掉了保持原样或删除
} else {
List<SkyeyePayload> sarList = payloadService.getSar(null);
sarList.forEach(sar -> {
if (!redisUtil.hHasKey(CacheKey.SAR_CONNECTED, sar.getIp()) ||
!redisUtil.hasKey(CacheKey.getSarConnect(sar.getIp()))) {
String ip = sar.getIp();
// 判断是否已连接 shortCache 里是否有该 ip 的状态最近有状态包
JmSarStatusDTO status = sarCache.getLatestStatus(ip);
// 判断是否已执行连接指令 permanentCache 里是否有该 ip 的连接标志
boolean hasConnectedFlag = sarCache.isConnected(ip);
// 如果缺少状态 缺少连接标志则尝试连接
if (!hasConnectedFlag || Objects.isNull(status)) {
try {
controlInfoService.sendUdp(sar.getIp(), SarControlTypeEnum.CONNECT);
controlInfoService.sendUdp(ip, SarControlTypeEnum.CONNECT);
log.info("尝试连接 SAR IP: {}", ip);
} catch (SarConnectException ex) {
// sar可能没通电连接失败不做处理
log.warn("连接雷达[" + sar.getIp() + "]失败:" + ex.getMessage());
log.warn("连接雷达[{}]失败:{}", ip, ex.getMessage());
}
}
});

View File

@ -1,16 +1,18 @@
package com.zhangy.skyeye.publics.advice;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhangy.skyeye.cache.publics.UserTokenCache;
import com.zhangy.skyeye.common.extend.anno.IgnoreAuth;
import com.zhangy.skyeye.common.extend.exception.AuthException;
import com.zhangy.skyeye.common.extend.util.JsonUtil;
import com.zhangy.skyeye.common.utils.JwtUtil;
import com.zhangy.skyeye.common.utils.SpringUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.publics.utils.SecurityUtil;
import com.zhangy.skyeye.publics.dto.UserDTO;
import com.zhangy.skyeye.publics.utils.SecurityUtil;
import com.zhangy.skyeye.publics.utils.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
@ -32,10 +34,9 @@ import java.util.Objects;
* 6. 续期 Redis 中的 token 有效期
*/
@Slf4j
@Component
public class AuthInterceptor implements AsyncHandlerInterceptor {
private final RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行 CORS 预检请求OPTIONS由后续 Filter 统一处理跨域头
@ -57,9 +58,13 @@ public class AuthInterceptor implements AsyncHandlerInterceptor {
if (method.isAnnotationPresent(IgnoreAuth.class)) {
return true;
}
// 注入 UserTokenCache
UserTokenCache userTokenCache = SpringContextUtil.getBean(UserTokenCache.class);
// 获取并校验 token
String token = SecurityUtil.getToken(request);
if (token == null || token.trim().isEmpty()) {
if (StrUtil.isBlank(token)) {
throw new AuthException("请求未授权:缺少 token");
}
// 解析 token 中的用户名信息
@ -81,25 +86,24 @@ public class AuthInterceptor implements AsyncHandlerInterceptor {
log.error("token 中的用户信息解析失败: {}", usernamePayload, e);
throw new AuthException("token 格式错误,请重新登录");
}
// 获取该用户在 Redis 中存储的 token
String redisKey = userDTO.getTokenKey();
if (redisKey == null || redisKey.trim().isEmpty()) {
// 获取该用户在 CaffeineCache 中存储的 token
String tokenKey = userDTO.getTokenKey();
if (StrUtil.isBlank(tokenKey)) {
throw new AuthException("用户信息异常,请重新登录");
}
Object storedToken = redisUtil.get(redisKey);
// Redis 中不存在 登录已失效
// CaffeineCache 中的 token 与当前请求不一致 多设备登录被踢
String storedToken = userTokenCache.getToken(tokenKey);
if (storedToken == null) {
// 显式处理 null token 已失效或被移除
throw new AuthException("登录状态已失效,请重新登录");
}
// Redis 中的 token 与当前请求不一致 多设备登录被踢
if (!Objects.equals(token, storedToken.toString())) {
log.warn("检测到多端登录冲突,用户: {}, 当前token: {}, redisToken: {}",
usernamePayload, token.substring(0, 10) + "...",
storedToken.toString().substring(0, 10) + "...");
if (!Objects.equals(token, storedToken)) {
log.warn("检测到多端登录冲突,用户: {}, 当前token: {}, storedToken: {}",
usernamePayload, token, storedToken);
throw new AuthException("该账号已在其他设备登录,请重新登录");
}
// 校验通过 续期 Redis token 有效时间
redisUtil.expire(redisKey, JwtUtil.EXPIRE_TIME / 1000);
// 续期Caffeine expireAfterWrite 是从最后写入开始算所以 put 一下即可续期
userTokenCache.storeToken(tokenKey, storedToken);
return true;
}
}

View File

@ -1,112 +1,58 @@
package com.zhangy.skyeye.publics.consts;
// import com.zhangy.skyeye.smp.dto.SmpWayPointResDTO;
public class CacheKey {
/*==========================================
publics 模块用 skyeye: 前缀
==========================================*/
/**
* 用户token30分钟
*/
/*==================== publics 模块 ====================*/
// 用户token30分钟
private static final String USER_TOEKN = "skyeye:user:token:%s@%s";
/**
* 获取token key
*/
public static String getToekn(Long userId, String userName) {
// 获取token key
public static String getToken(Long userId, String userName) {
return String.format(CacheKey.USER_TOEKN, userId, userName);
}
/*==========================================
分布式锁用 skyeye:lock: 前缀
==========================================*/
/**
* 上传sar图像锁
*/
/*==================== 分布式锁 ====================*/
// 上传sar图像锁
public static final String SAR_IMAGE_UPLOAD_LOCK = "skyeye:lock:image";
/*==========================================
sar 模块用 skyeye:sar: 前缀
==========================================*/
/**
* 控制回包1秒
*/
/*==================== SAR 模块 ====================*/
// 控制回包1秒
private static final String SAR_CONTROL_BACK = "skyeye:sar:control";
/**
* sar状态1秒
*/
private static final String SAR_STATUS = "skyeye:sar:status:";
/**
* 已执行连接命令的sar永久
*/
// sar状态1秒
private static final String SAR_STATUS = "skyeye:sar:status";
// 已执行连接命令的sar永久
public static final String SAR_CONNECTED = "skyeye:sar:connected";
/**
* 获取控制回包key加ip
*/
// 获取控制回包key加ip
public static String getSarControlBack(String ip) {
return SAR_CONTROL_BACK + ip;
return SAR_CONTROL_BACK + ":" + ip;
}
// 获取状态key加ip
public static String getSarStatus(String ip) {
return SAR_STATUS + ":" + ip;
}
// 获取已执行连接命令的sar-key加ip
public static String getSarConnected(String ip) {
return SAR_CONNECTED + ":" + ip;
}
/**
* 获取状态key加ip
*/
public static String getSarConnect(String ip) {
return SAR_STATUS + ip;
}
/*==========================================
device 模块用 skyeye:device: 前缀
==========================================*/
/**
* sar载荷永久
*/
/*==================== Device 模块 ====================*/
// sar载荷永久
public static final String DEVICE_SAR = "skyeye:device:sar";
/**
* 无人机永久
*/
// 无人机永久
public static final String DEVICE_UAV = "skyeye:device:uav";
/*==========================================
smp 模块用 skyeye:smp: 前缀
==========================================*/
/**
* 运动规划响应1秒
*/
//public static final String SMP_WAYPOINT_RES = "skyeye:smp:waypoint:";
/**
* 飞行控制响应1秒
*/
//public static final String SMP_FLIGHT_RES = "skyeye:smp:flight:";
/**
* 云台控制响应1秒
*/
//public static final String SMP_GIMBALMGR_RES = "skyeye:smp:gimbalmgr:";
/**
* 数据订阅响应1秒
*/
//public static final String SMP_SUBSCRIPT_RES = "skyeye:smp:subscript:";
/**
* 数据订阅回传数据1秒
*/
//public static final String SMP_SUBSCRIPT_DATA = "skyeye:smp:subscript:";
/**
* 相机视频流响应1秒
*/
//public static final String SMP_VIDEO_RES = "skyeye:smp:video:";
/*==================== SMP 模块 ====================*/
// 运动规划响应1秒
public static final String SMP_WAYPOINT_RES = "skyeye:smp:waypoint:";
// 飞行控制响应1秒
public static final String SMP_FLIGHT_RES = "skyeye:smp:flight:";
// 云台控制响应1秒
public static final String SMP_GIMBALMGR_RES = "skyeye:smp:gimbalmgr:";
// 数据订阅响应1秒
public static final String SMP_SUBSCRIPT_RES = "skyeye:smp:subscript:";
// 数据订阅回传数据1秒
public static final String SMP_SUBSCRIPT_DATA = "skyeye:smp:subscript:";
// 相机视频流响应1秒
public static final String SMP_VIDEO_RES = "skyeye:smp:video:";
}

View File

@ -2,6 +2,7 @@ package com.zhangy.skyeye.publics.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.zhangy.skyeye.cache.publics.UserTokenCache;
import com.zhangy.skyeye.common.extend.anno.IgnoreAuth;
import com.zhangy.skyeye.common.extend.anno.OperationLog;
import com.zhangy.skyeye.common.extend.dto.PageDTO;
@ -10,7 +11,6 @@ import com.zhangy.skyeye.common.extend.util.MessageUtils;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
import com.zhangy.skyeye.common.pojo.result.Result;
import com.zhangy.skyeye.common.utils.JwtUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.publics.dto.RegisterDTO;
import com.zhangy.skyeye.publics.dto.SysUserPwdDTO;
@ -19,7 +19,7 @@ import com.zhangy.skyeye.publics.entity.SysLog;
import com.zhangy.skyeye.publics.entity.SysUser;
import com.zhangy.skyeye.publics.service.ISysUserService;
import com.zhangy.skyeye.publics.utils.SecurityUtil;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
@ -30,125 +30,124 @@ import java.util.List;
@Validated
@RestController
@AllArgsConstructor
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private RedisUtil redisUtil;
private final UserTokenCache userTokenCache;
/** token失效时间 */
private final int TOKEN_EXPIRE = 60 * 60 * 24;
/**
* 登录接口
*
* @return
*/
@IgnoreAuth
@OperationLog(value = "登录", type = SysLog.TYPE_USER)
@PostMapping("/login")
public Result login(@RequestBody RegisterDTO registerDTO) {
SysUser user = sysUserService.selectByAccount(registerDTO.getUsername(), registerDTO.getPassword().toLowerCase());
if (user == null) {
throw ServiceException.noLog(MessageUtils.message("user.login.error"));
}
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
String payload = JSONUtil.toJsonStr(userDTO); // 保持原逻辑
String accessToken = JwtUtil.sign(payload, user.getPassword());
String tokenKey = userDTO.getTokenKey(); // skyeye:user:token:id@account
userTokenCache.storeToken(tokenKey, accessToken);
userDTO.setToken(accessToken);
return Result.successData(MessageUtils.message("user.login.success"), userDTO);
}
/**
* 登录接口
* @return
*/
@IgnoreAuth
@OperationLog(value = "登录", type = SysLog.TYPE_USER)
@PostMapping("/login")
public Result login(@RequestBody RegisterDTO registerDTO) {
SysUser user = sysUserService.selectByAccount(registerDTO.getUsername(), registerDTO.getPassword().toLowerCase());
if (user == null) {
throw ServiceException.noLog(MessageUtils.message("user.login.error"));
}
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
String key = JSONUtil.toJsonStr(userDTO);
String accessToken = JwtUtil.sign(key, user.getPassword());
redisUtil.set(userDTO.getTokenKey(), accessToken, TOKEN_EXPIRE);
userDTO.setToken(accessToken);
return Result.successData(MessageUtils.message("user.login.success"), userDTO);
}
/**
* 修改密码
*
* @param dto
* @return
*/
@PostMapping("/updpwd")
public Object updatePwd(@Valid SysUserPwdDTO dto) {
List<SysUser> list = sysUserService.selectById(dto.getUserId());
if (ObjectUtil.isEmpty(list)) {
throw ServiceException.noLog(MessageUtils.message("user.updpwd.error"));
}
SysUser user = list.get(0);
if (user.getPassword().equals(dto.getOldPwd())) {
throw ServiceException.noLog(MessageUtils.message("user.updpwd.nochg"));
}
user.setPassword(dto.getNewPwd());
sysUserService.update(user);
return MessageUtils.message("user.updpwd.success");
}
/**
* 修改密码
* @param dto
* @return
*/
@PostMapping("/updpwd")
public Object updatePwd(@Valid SysUserPwdDTO dto) {
List<SysUser> list = sysUserService.selectById(dto.getUserId());
if (ObjectUtil.isEmpty(list)) {
throw ServiceException.noLog(MessageUtils.message("user.updpwd.error"));
}
SysUser user = list.get(0);
if (user.getPassword().equals(dto.getOldPwd())) {
throw ServiceException.noLog(MessageUtils.message("user.updpwd.nochg"));
}
user.setPassword(dto.getNewPwd());
sysUserService.update(user);
return MessageUtils.message("user.updpwd.success");
}
/**
* 退出接口
*/
@IgnoreAuth
@OperationLog(value = "退出登录", type = SysLog.TYPE_USER)
@RequestMapping("/logout")
@GetMapping
public Result logout() {
UserDTO userDTO = SecurityUtil.getUser();
if (userDTO != null) {
String key = CacheKey.getToekn(userDTO.getId(), userDTO.getAccount());
redisUtil.del(key);
}
return Result.status(true);
}
/**
* 退出接口
*/
@IgnoreAuth
@OperationLog(value = "退出登录", type = SysLog.TYPE_USER)
@RequestMapping("/logout")
@GetMapping
public Result logout() {
UserDTO userDTO = SecurityUtil.getUser();
if (userDTO != null) {
String tokenKey = CacheKey.getToken(userDTO.getId(), userDTO.getAccount());
userTokenCache.evictToken(tokenKey);
}
return Result.status(true);
}
/**
* 分页查询
*/
@RequestMapping("/page")
public Object selectPage(@Valid @RequestBody PageDTO param) {
return sysUserService.selectPage(param);
}
/**
* 分页查询
*/
@RequestMapping("/page")
public Object selectPage(@Valid @RequestBody PageDTO param) {
return sysUserService.selectPage(param);
}
/**
* 列表查询
*/
@RequestMapping("/list")
public Object selectList(@Valid @RequestBody PageDTO param) {
return sysUserService.selectList(param);
}
/**
* 列表查询
*/
@RequestMapping("/list")
public Object selectList(@Valid @RequestBody PageDTO param) {
return sysUserService.selectList(param);
}
/**
* 查询详情
*/
@GetMapping("/detail")
public Object selectById(@RequestParam Long... id) {
return sysUserService.selectById(id);
}
/**
* 查询详情
*/
@GetMapping("/detail")
public Object selectById(@RequestParam Long... id) {
return sysUserService.selectById(id);
}
/**
* 新增
*/
@PostMapping("/save")
@OperationLog(value = "用户新增", type = SysLog.TYPE_USER)
public Object insert(@RequestBody SysUser e) {
return sysUserService.insert(e);
}
/**
* 新增
*/
@PostMapping("/save")
@OperationLog(value = "用户新增", type = SysLog.TYPE_USER)
public Object insert(@RequestBody SysUser e) {
return sysUserService.insert(e);
}
/**
* 修改
*/
@PostMapping("/update")
@OperationLog(value = "用户修改", type = SysLog.TYPE_USER)
public Object update(@RequestBody SysUser e) {
return sysUserService.update(e);
}
/**
* 修改
*/
@PostMapping("/update")
@OperationLog(value = "用户修改", type = SysLog.TYPE_USER)
public Object update(@RequestBody SysUser e) {
return sysUserService.update(e);
}
/**
* 删除
*/
@OperationLog(value = "用户删除", type = SysLog.TYPE_USER)
@PostMapping("/remove")
public Object delete(@RequestBody Long... id) {
return sysUserService.delete(id);
}
/**
* 删除
*/
@OperationLog(value = "用户删除", type = SysLog.TYPE_USER)
@PostMapping("/remove")
public Object delete(@RequestBody Long... id) {
return sysUserService.delete(id);
}
}

View File

@ -10,26 +10,37 @@ public class UserDTO {
private Long id;
/** 用户名 */
/**
* 用户名
*/
private String account;
/** 状态 0禁用 1正常 */
/**
* 状态 0禁用 1正常
*/
private Integer status;
/** 用户所有的权限信息 */
/**
* 用户所有的权限信息
*/
private Set<String> authorities;
/** 角色ID */
/**
* 角色ID
*/
private Integer roleId;
/** Token */
/**
* Token
*/
private String token;
/**
* 获取token的缓存键
*
* @return
*/
public String getTokenKey() {
return CacheKey.getToekn(id, account);
return CacheKey.getToken(id, account);
}
}

View File

@ -1,57 +1,25 @@
package com.zhangy.skyeye.publics.service.impl;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.cache.publics.UserTokenCache;
import com.zhangy.skyeye.publics.service.ISysLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import com.zhangy.skyeye.publics.utils.CacheUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
@RequiredArgsConstructor
public class SysLoginServiceImpl implements ISysLoginService {
@Autowired
private RedisTemplate redisTemplate;
private final UserTokenCache userTokenCache;
@Override
public boolean hasLogged() {
return Boolean.TRUE.equals(redisTemplate.execute((RedisCallback<Boolean>) connection -> {
try (Cursor<byte[]> cursor = connection.scan(
ScanOptions.scanOptions()
.match("skyeye:user:token:*")
.count(1) // 只需要找到1个匹配项
.build())) {
return cursor.hasNext();
// } catch (IOException ex) {
} catch (Exception ex) {
throw new ServiceException("查询当前登录人数失败:" + ex.getMessage());
}
}));
// 获取底层 Caffeine Cache
return CacheUtil.size(userTokenCache) > 0;
}
@Override
public int countLogged() {
return (int) redisTemplate.execute((RedisCallback<Integer>) connection -> {
int count = 0;
ScanOptions options = ScanOptions.scanOptions()
.match("skyeye:user:token:*")
.count(100) // 合理的COUNT值
.build();
try (Cursor<byte[]> cursor = connection.scan(options)) {
while (cursor.hasNext()) {
count++;
cursor.next();
}
// } catch (IOException ex) {
} catch (Exception ex) {
}
return count;
});
return (int) CacheUtil.size(userTokenCache);
}
}
}

View File

@ -0,0 +1,74 @@
package com.zhangy.skyeye.publics.utils;
import com.zhangy.skyeye.cache.publics.UserTokenCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 缓存工具
* @AUTHOR: GuanCheng Long
* @DATE: 2026/1/30 13:34
*/
@Component
@Slf4j
public class CacheUtil {
@SuppressWarnings("unchecked")
public static <T> T get(Cache cache, Object key, Class<T> clazz) {
if (cache == null) return null;
Cache.ValueWrapper wrapper = cache.get(key);
if (wrapper == null) return null;
Object value = wrapper.get();
if (clazz.isInstance(value)) {
return (T) value;
}
// 可选log.warn("缓存类型不匹配,期望 {},实际 {}", clazz.getName(), value.getClass().getName());
return null;
}
// CacheUtil.java 中新增
@SuppressWarnings("unchecked")
public static <T> List<T> getAll(Cache cache, Class<T> clazz) {
if (cache == null) return Collections.emptyList();
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
(com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache.getNativeCache();
return nativeCache.asMap().values().stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.collect(Collectors.toList());
}
public static long size(Cache springCache) {
if (springCache == null) {
return 0;
}
try {
com.github.benmanes.caffeine.cache.Cache<?, ?> nativeCache =
(com.github.benmanes.caffeine.cache.Cache<?, ?>) springCache.getNativeCache();
return nativeCache.estimatedSize(); // nativeCache.asMap().size()
} catch (Exception e) {
log.warn("无法获取缓存大小,可能是非 Caffeine 实现", e);
return 0;
}
}
public static long size(UserTokenCache tokenCache) {
if (tokenCache == null) return 0;
Object nativeCache = tokenCache.getNativeCache();
if (nativeCache instanceof com.github.benmanes.caffeine.cache.Cache) {
com.github.benmanes.caffeine.cache.Cache<?, ?> caffeineCache =
(com.github.benmanes.caffeine.cache.Cache<?, ?>) nativeCache;
return caffeineCache.estimatedSize();
}
return 0;
}
}

View File

@ -0,0 +1,52 @@
package com.zhangy.skyeye.publics.utils;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @PROJECT_NAME: skyeyesystem_online
* @DESCRIPTION: 本地内存锁工具替代 Redis 分布式锁适用于单实例部署
* @AUTHOR: GuanCheng Long
* @DATE: 2026/1/30 13:14
*/
@Component
public class LocalLockUtil {
// Caffeine 缓存key=锁名, value=ReentrantLock
private static final Cache<String, ReentrantLock> LOCK_CACHE = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES) // 锁闲置 5 分钟自动清理
.maximumSize(100) // 最多 100 个锁
.build();
/**
* 获取或创建锁并尝试获取
*
* @param lockKey 锁的 key
* @param timeoutSeconds 尝试获取锁的超时时间
* @return true=获取成功false=获取失败被占用
*/
public boolean tryLock(String lockKey, long timeoutSeconds) {
ReentrantLock lock = LOCK_CACHE.get(lockKey, k -> new ReentrantLock());
try {
return lock.tryLock(timeoutSeconds, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 释放锁仅释放自己的
*/
public void unlock(String lockKey) {
ReentrantLock lock = LOCK_CACHE.getIfPresent(lockKey);
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

View File

@ -0,0 +1,25 @@
package com.zhangy.skyeye.publics.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
log.info("SpringContextUtil 初始化完成");
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}

View File

@ -1,20 +1,16 @@
package com.zhangy.skyeye.sar.context;
import com.zhangy.skyeye.cache.uav.UavCache;
import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO;
import com.zhangy.skyeye.jm.dto.JmUavStatusDTO;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
*/
/**
* @PROJECT_NAME: skyeyesystem
* @DESCRIPTION: SAR 任务上下文提供者实现基于 Redis 缓存避免循环依赖
* - JmJobStatusServiceImpl 在更新状态时主动把 JmUavStatusDTO 推送到 Redis
* - 此类只从 Redis 读取不注入 JmJobStatusService
* - JmJobStatusServiceImpl 在更新状态时主动把 JmUavStatusDTO 推送到 Redis
* - 此类只从 Redis 读取不注入 JmJobStatusService
* @AUTHOR: GuanCheng Long
* @DATE: 2026/1/21 1:10
*/
@ -22,14 +18,11 @@ import org.springframework.stereotype.Component;
@Component
public class SarTaskContextProviderImpl implements SarTaskContextProvider {
@Autowired
private RedisUtil redisUtil;
private final UavCache uavCache;
// Redis 键前缀
private static final String UAV_STATUS_KEY_PREFIX = "sar:context:uav:";
// 缓存过期时间
private static final long CACHE_EXPIRE_SECONDS = 600; // 10 分钟
public SarTaskContextProviderImpl(UavCache uavCache) {
this.uavCache = uavCache;
}
@Override
public JmUavStatusDTO getCurrentUav(String payloadIp) {
@ -37,16 +30,8 @@ public class SarTaskContextProviderImpl implements SarTaskContextProvider {
log.warn("获取 uav 状态失败payloadIp 为空");
return null;
}
String key = UAV_STATUS_KEY_PREFIX + payloadIp;
Object obj = redisUtil.get(key);
if (obj instanceof JmUavStatusDTO) {
return (JmUavStatusDTO) obj;
}
log.debug("Redis 中未找到 IP={} 的 uav 状态", payloadIp);
return null;
String key = "sar:context:uav:" + payloadIp; // 保持原 key 格式
return uavCache.getShort(key, JmUavStatusDTO.class);
}
@Override
@ -80,11 +65,10 @@ public class SarTaskContextProviderImpl implements SarTaskContextProvider {
return;
}
String key = UAV_STATUS_KEY_PREFIX + payloadIp;
String key = "sar:context:uav:" + payloadIp;
try {
// 设置值 + 过期时间
redisUtil.set(key, uavStatus, CACHE_EXPIRE_SECONDS);
log.debug("已更新 Redis 缓存key={}, jobExecId={}", key, uavStatus.getJobExecId());
uavCache.putShort(key, uavStatus);// 自动过期 10 分钟
log.debug("已更新缓存key={}, jobExecId={}", key, uavStatus.getJobExecId());
} catch (Exception e) {
log.error("更新 SAR uav 状态缓存失败ip={}", payloadIp, e);
}
@ -97,8 +81,8 @@ public class SarTaskContextProviderImpl implements SarTaskContextProvider {
if (payloadIp == null) {
return;
}
String key = UAV_STATUS_KEY_PREFIX + payloadIp;
redisUtil.del(key);
log.debug("SAR uav 缓存已标记清理(或自然过期){}", key);
String key = "sar:context:uav:" + payloadIp;
uavCache.evictShort(key);
log.debug("SAR uav 缓存已清理:{}", key);
}
}

View File

@ -1,21 +1,20 @@
package com.zhangy.skyeye.sar.control;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.sar.dto.SarControlDTO;
import com.zhangy.skyeye.sar.dto.SarControlParamDTO;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* 控制连接
* 控制建立连接
* 连接成功后在回传状态时会将状态信息放入缓存这里只记录连接标志
*/
@Slf4j
@Order(1)
@ -23,12 +22,11 @@ import java.util.List;
@RequiredArgsConstructor
public class SarControlConnectStrategy implements ISarControlStrategy {
private final RedisUtil redisUtil;
private final SarCache sarCache;
@Override
public boolean supports(SarControlParamDTO param) {
SarControlTypeEnum controlType = param.getControlType();
return controlType == SarControlTypeEnum.CONNECT;
return param.getControlType() == SarControlTypeEnum.CONNECT;
}
@Override
@ -40,15 +38,13 @@ public class SarControlConnectStrategy implements ISarControlStrategy {
@Override
public void sendPost(SarControlDTO sar) {
// 建立连接后回传状态时会将状态信息放入缓存
redisUtil.hset(CacheKey.SAR_CONNECTED, sar.getIp(), new Date());
// 建立连接后记录连接标志
sarCache.markConnected(sar.getIp());
log.info("SAR [{}] 已标记为连接状态", sar.getIp());
}
@Override
public int retryType() {
return 0;
}
}
}

View File

@ -1,8 +1,8 @@
package com.zhangy.skyeye.sar.control;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.sar.dto.SarControlDTO;
import com.zhangy.skyeye.sar.dto.SarControlPackDTO;
@ -14,7 +14,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@ -59,7 +58,7 @@ public class SarControlContext {
private final int POLLING_INTERVAL = 100;
@Autowired
private RedisUtil redisUtil;
private SarCache sarCache;
/**
* 应答超时
@ -74,6 +73,7 @@ public class SarControlContext {
/**
* 执行
*
* @param param
*/
public void execute(SarControlParamDTO param) {
@ -89,12 +89,12 @@ public class SarControlContext {
/**
* 执行发送失败则递归重试
*
* @param param 控制参数
* @param param 控制参数
* @param matchedStrategy 控制策略
*/
private void sendControl(SarControlParamDTO param, ISarControlStrategy matchedStrategy) {
List<SarControlDTO> controls = matchedStrategy.handle(param);
for(SarControlDTO control: controls) {
for (SarControlDTO control : controls) {
control.setIp(param.getIp());
sendUdp(control);
matchedStrategy.sendPost(control);
@ -103,60 +103,92 @@ public class SarControlContext {
/**
* 发送未收到回执则重发
*
* @param control
*/
private void sendUdp(SarControlDTO control) {
SarControlTypeEnum controlType = control.getControlType();
String ip = control.getIp();
log.debug("开始发送雷达控制指令[" + controlType + "]----------------------");
//System.out.println(control);
String targetIp = control.getIp();
log.info("准备发送雷达控制指令 | 类型:{} | 目标IP:{}", controlType, targetIp);
String cacheKey = CacheKey.getSarControlBack(targetIp);
byte[] payload;
try {
payload = pack(control);
} catch (Exception e) {
log.error("打包控制指令失败 | 类型:{} | ip:{} | {}", controlType, targetIp, e.getMessage(), e);
throw new ServiceException("控制指令打包失败:" + e.getMessage());
}
try (DatagramSocket socket = new DatagramSocket()) {
byte[] content = pack(control);
socket.connect(new InetSocketAddress(ip, PORT));
DatagramPacket packet = new DatagramPacket(content, content.length);
int failCount = 0;
SarErrorDTO info = null;
while (info == null && failCount < RETRY_MAX) { // 失败重试
socket.send(packet);
// 每0.1秒取回执1秒后超时
long startTime = System.currentTimeMillis();
String cacheKey = CacheKey.getSarControlBack(ip);
while (System.currentTimeMillis() - startTime < ANSWER_TIMEOUT) {
if (redisUtil.hasKey(cacheKey)) {
info = (SarErrorDTO) redisUtil.get(cacheKey);
redisUtil.del(cacheKey);
break;
} else {
try {
Thread.sleep(POLLING_INTERVAL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
InetSocketAddress address = new InetSocketAddress(targetIp, PORT);
socket.connect(address);
DatagramPacket packet = new DatagramPacket(payload, payload.length);
SarErrorDTO response = null;
int retryCount = 0;
while (response == null && retryCount < RETRY_MAX) {
// 发送
try {
socket.send(packet);
log.info("UDP指令已发送 | 第{}次尝试 | 类型:{} | ip:{}",
retryCount + 1, controlType, targetIp);
} catch (IOException e) {
log.warn("发送UDP失败 | 第{}次尝试 | {}", retryCount + 1, e.getMessage());
retryCount++;
continue;
}
if (info == null) { // 超时未收到回执
failCount++;
} else if (info.getExecStatus() == 1) {
// 只有发送的数据结构错误时才会返回错误状态并不会因为业务不允许返回错误
throw new ServiceException("控制指令[" + controlType + "]执行状态错误,请重试");
// 等待回执轮询方式
response = waitForResponse(cacheKey, targetIp, controlType);
if (response == null) {
retryCount++;
log.warn("第{}次发送未收到回执 | 类型:{} | ip:{}", retryCount, controlType, targetIp);
}
}
if (info == null) {
throw new SarConnectException("控制指令[" + controlType + "]发送失败,雷达[" + ip + "]无应答,请重试");
} else {
log.info("雷达控制指令[" + controlType + "]发送完毕----------------------");
// 最终判断
if (response == null) {
String msg = String.format("雷达[%s]控制指令[%s]发送失败:超时无应答", targetIp, controlType);
log.error(msg);
throw new SarConnectException(msg + ",请重试");
}
} catch (RedisConnectionFailureException ex) {
throw ServiceException.errorLog("无法连接到Redis服务");
} catch (IOException ex) {
throw ServiceException.errorLog("控制指令[" + controlType + "]发送失败 " + ex.getMessage());
// 业务状态判断
if (response.getExecStatus() == 1) {
String msg = String.format("雷达[%s]控制指令[%s]执行失败:状态码=1数据结构错误",
targetIp, controlType);
log.error(msg);
throw new ServiceException(msg + ",请检查指令内容后重试");
}
log.info("雷达控制指令发送成功 | 类型:{} | ip:{} | 状态:{}", controlType, targetIp, response.getExecStatus());
} catch (IOException e) {
String msg = String.format("雷达控制指令[%s]网络异常 | ip:%s | %s", controlType, targetIp, e.getMessage());
log.error(msg, e);
throw ServiceException.errorLog(msg);
}
}
/**
* 轮询等待缓存中的回执带超时
*/
private SarErrorDTO waitForResponse(String cacheKey, String ip, SarControlTypeEnum controlType) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < ANSWER_TIMEOUT) {
SarErrorDTO dto = sarCache.getControlBack(ip); // 如果 SarCache 封装了泛型方法可以直接用
if (dto != null) {
sarCache.saveStatus(cacheKey, null); // 清空 shortCache 对应的 key
log.info("收到雷达回执 | ip:{} | 控制类型:{} | 状态:{}", ip, controlType, dto.getExecStatus());
return dto;
}
try {
Thread.sleep(POLLING_INTERVAL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("等待雷达回执时被中断 | ip:{}", ip);
return null;
}
}
return null; // 超时
}
/**
* 封包
*
* @param sarControl
* @return
*/

View File

@ -1,10 +1,9 @@
package com.zhangy.skyeye.sar.control;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.sar.dto.SarControlDTO;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.sar.dto.SarControlParamDTO;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
@ -22,7 +21,7 @@ import java.util.List;
@RequiredArgsConstructor
public class SarControlDisconnectStrategy implements ISarControlStrategy {
private final RedisUtil redisUtil;
private final SarCache sarCache;
@Override
public boolean supports(SarControlParamDTO param) {
@ -37,15 +36,12 @@ public class SarControlDisconnectStrategy implements ISarControlStrategy {
return Collections.singletonList(connect);
}
/**
* 删除缓存键
* @param sar
*/
@Override
public void sendPost(SarControlDTO sar) {
String connectKey = CacheKey.getSarConnect(sar.getIp());
redisUtil.del(connectKey);
redisUtil.hdel(CacheKey.SAR_CONNECTED, sar.getIp());
// 清理短时状态
sarCache.removeConnection(sar.getIp()); // permanentCache 移除连接标志
sarCache.saveStatus(sar.getIp(), null); // 等同于清空 shortCache 中的状态
log.info("已断开 SAR IP: {},缓存清理完成", sar.getIp());
}
@Override

View File

@ -42,14 +42,12 @@ public class SarControlController {
*/
@RequestMapping("/turnon")
public Result turnOn(@RequestBody String ip) {
String ipVal = "";
String ipVal;
try {
// 1. 使用 ObjectMapper JSON 字符串解析成一个 JsonNode
JsonNode rootNode = objectMapper.readTree(ip);
// 2. 从树中获取 "payloadId" 节点并将其值转换为文本
ipVal = rootNode.path("payloadId").asText();
// 检查是否成功获取
if (ipVal == null || ipVal.isEmpty()) {
// 根据你的业务逻辑返回错误例如

View File

@ -1,15 +1,15 @@
package com.zhangy.skyeye.sar.listen;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.jm.dto.JmSarStatusDTO;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.sar.dto.SarErrorDTO;
import com.zhangy.skyeye.sar.dto.SarStatusPackDTO;
import com.zhangy.skyeye.sar.enums.SarErrorTypeEnum;
import com.zhangy.skyeye.sar.task.CircularBufferQueue;
import com.zhangy.skyeye.sar.task.DiscardOldestPolicyWithLog;
import com.zhangy.skyeye.sar.task.PriorityThreadFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit;
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SarStatusListener extends SarAbstractListener {
/**
@ -49,8 +50,7 @@ public class SarStatusListener extends SarAbstractListener {
@Value("${skyeye.sar.udp.status.connect-timeout:15}")
private int connectTimeout;
@Autowired
private RedisUtil redisUtil;
private final SarCache sarCache;
@Autowired
private JmJobStatusService sarJobStatusService;
@ -70,38 +70,34 @@ public class SarStatusListener extends SarAbstractListener {
@Override
protected void processData(DatagramPacket packet) throws IOException {
socket.receive(packet);
// 过滤无效数据包
if (packet.getLength() != 100) {
return;
}
log.debug("接收到状态包----------------------");
String ip = packet.getAddress().getHostAddress();
// 处理接收到的数据
SarStatusPackDTO packDTO = SarStatusPackDTO.parse(ip, packet.getData());
if (packDTO == null) {
if (running)
log.warn("[" + packDTO.getPayloadIp() + "]状态包校验失败,已丢弃。错误包=" +
packDTO.getErrorPacketStatus() + ",状态包=" + packDTO.getDevicePacketStatus());
log.warn("状态包校验失败,已丢弃。");
return;
}
// 错误包结果放入缓存超时1秒
// 错误包 回执
int errorStatus = packDTO.getErrorPacketStatus();
if (errorStatus == 1) {
SarErrorDTO info = packDTO.getDeviceErrorInfo();
if (info.getErrorPacketType() == SarErrorTypeEnum.RESULT) {
log.debug("收到回执包:" + ip);
redisUtil.set(CacheKey.getSarControlBack(ip), info, answerTimeout, TimeUnit.SECONDS);
log.debug("收到回执包:{}", ip);
sarCache.saveControlBack(ip, info); // 使用 SarCache类型安全自动短期缓存自动 2s 过期
}
}
// 状态包
int deviceStatus = packDTO.getDevicePacketStatus();
if (deviceStatus == 1) {
JmSarStatusDTO info = packDTO.getDeviceStatusInfo();
log.debug("sar开机状态" + info.getIsBoot());
log.debug("sar开机状态{}", info.getIsBoot());
sarJobStatusService.update(ip, info);
redisUtil.set(CacheKey.getSarConnect(ip), info, answerTimeout, TimeUnit.SECONDS);
//System.out.println(info);
sarCache.saveStatus(ip, info); // 使用 SarCache类型安全自动短期缓存自动过期
}
log.debug("----------------------状态包解析完毕");
log.debug("状态包解析完毕");
}
}

View File

@ -5,16 +5,13 @@ import com.zhangy.skyeye.common.extend.util.JsonUtil;
import com.zhangy.skyeye.jm.dto.JmJobDTO;
import com.zhangy.skyeye.jm.dto.JmSarStatusDTO;
import com.zhangy.skyeye.jm.entity.JmJobPayload;
import com.zhangy.skyeye.jm.entity.JmJobUav;
import com.zhangy.skyeye.publics.consts.CacheKey;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.cache.sar.SarCache;
import com.zhangy.skyeye.sar.control.SarControlContext;
import com.zhangy.skyeye.sar.dto.SarControlParamDTO;
import com.zhangy.skyeye.sar.enums.SarControlTypeEnum;
import com.zhangy.skyeye.sar.service.ISarControlService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@ -30,9 +27,7 @@ import static com.zhangy.skyeye.jm.consts.JmJobModeEnum.CRUISE;
public class SarControlServiceImpl implements ISarControlService {
private final SarControlContext udpSendContext;
@Autowired
private RedisUtil redisUtil;
private final SarCache sarCache;
@Override
public void sendUdp(JmJobDTO job) {
@ -67,7 +62,6 @@ public class SarControlServiceImpl implements ISarControlService {
});
}
@Override
public void sendUdp(SarControlParamDTO param) {
udpSendContext.execute(param);
@ -87,28 +81,30 @@ public class SarControlServiceImpl implements ISarControlService {
@Override
public void turnOn(String ip) {
if (!redisUtil.hHasKey(CacheKey.SAR_CONNECTED, ip) || !redisUtil.hasKey(CacheKey.getSarConnect(ip))) {
// 用领域缓存判断连接和状态
boolean connected = sarCache.isConnected(ip);
JmSarStatusDTO status = sarCache.getLatestStatus(ip);
if (!connected || status == null) {
throw new ServiceException("请先加电并连接sar");
}
SarControlParamDTO param = new SarControlParamDTO(ip, SarControlTypeEnum.TURNON);
udpSendContext.execute(param);
}
@Override
public void endAll(String ip) {
if (!redisUtil.hHasKey(CacheKey.SAR_CONNECTED, ip) || !redisUtil.hasKey(CacheKey.getSarConnect(ip))) {
// 用领域缓存判断连接和状态
boolean connected = sarCache.isConnected(ip);
JmSarStatusDTO status = sarCache.getLatestStatus(ip);
if (!connected || status == null) {
throw new ServiceException("请先加电并连接sar");
}
SarControlParamDTO param = new SarControlParamDTO(ip, SarControlTypeEnum.ENDALL);
udpSendContext.execute(param);
}
@Override
public JmSarStatusDTO getLatestStatus(String ip) {
String connectKey = CacheKey.getSarConnect(ip);
// 从缓存取载荷状态信息
return (JmSarStatusDTO) redisUtil.get(connectKey);
return sarCache.getLatestStatus(ip);
}
}

View File

@ -1,9 +1,9 @@
package com.zhangy.skyeye.sar.service.impl;
import com.zhangy.skyeye.cache.image.ImageCache;
import com.zhangy.skyeye.common.extend.util.MathUtil;
import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO;
import com.zhangy.skyeye.jm.dto.JmImageRotateDTO;
import com.zhangy.skyeye.jm.dto.JmUavStatusDTO;
import com.zhangy.skyeye.jm.entity.JmImage;
@ -16,7 +16,6 @@ import com.zhangy.skyeye.publics.utils.OpenCVUtil;
import com.zhangy.skyeye.sar.dto.SarBackImageFrameDTO;
import com.zhangy.skyeye.sar.service.ISarImageService;
import com.zhangy.skyeye.sar.service.SarWsAsyncService;
import com.zhangy.skyeye.sar.util.RadarDisplayOptions;
import com.zhangy.skyeye.sar.util.SarImageToneAdjuster;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -46,7 +45,7 @@ public class SarImageServiceImpl implements ISarImageService {
private SarWsAsyncService sarWsAsyncService;
@Autowired
private RedisUtil redisUtil;
private ImageCache imageCache;
// 图片最大宽度前端说和电脑有关4096保险点一般是4096 8192 16384
@Value("${skyeye.sar.image.max:4096}")
@ -56,43 +55,43 @@ public class SarImageServiceImpl implements ISarImageService {
private final String CACHE_FIELD_START_FRAME_NO = "startFrameNo";
// 当前帧号
private final String CACHE_FIELD_CURR_FRAME_NO = "currFrameNo";
// 缓存超时
private final long CACHE_EXPIRE_SECOND = 24 * 3600;
/**
* 获取基准图像信息
*
* @param airlineId 航线执行ID
* @param airlineId 航线执行ID
* @param singleWidth 单条图片宽度
* @param frameNo 当前帧号
* @param frameNo 当前帧号
* @return 返回非空的图像信息其字段 imageNo 一定有值
*/
private JmImage getBaseImage(Long airlineId, int singleWidth, int frameNo) {IMG_MAX_WITH=1;
private JmImage getBaseImage(Long airlineId, int singleWidth, int frameNo) {
List<JmImage> imageList = imageService.selectLowByAirline(airlineId);
String cacheKey = "jmImgJoin-" + airlineId;
JmImage base = null;
String cachePrefix = "jmImgJoin-" + airlineId;
JmImage base;
// 情况1航线第一张图
if (ObjectUtil.isEmpty(imageList)) {
base = new JmImage();
base.setImageNo(1);
redisUtil.hset(cacheKey, CACHE_FIELD_START_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND);
// 存起始帧号
imageCache.put(cachePrefix + ":" + CACHE_FIELD_START_FRAME_NO, frameNo);
return base;
}
// 情况2如果最后一张还能拼图则直接返回继续拼
JmImage last = imageList.get(imageList.size() - 1);
Integer startFrameNo = (Integer) redisUtil.hget(cacheKey, CACHE_FIELD_START_FRAME_NO);
int currWidth = startFrameNo == null ? 0 : singleWidth * (frameNo - startFrameNo + 1); // 图宽当前图+基准图
Integer startFrameNo = imageCache.get(cachePrefix + ":" + CACHE_FIELD_START_FRAME_NO, Integer.class);
int currWidth = startFrameNo == null ? 0 : singleWidth * (frameNo - startFrameNo + 1);
int surplusNum = (IMG_MAX_WITH - currWidth) / singleWidth; // 还可以拼图片数
Integer baseNo = (Integer) redisUtil.hget("jmImgJoin-" + airlineId, CACHE_FIELD_CURR_FRAME_NO);
Integer baseNo = imageCache.get(cachePrefix + ":" + CACHE_FIELD_CURR_FRAME_NO, Integer.class);
if (startFrameNo == null || currWidth < IMG_MAX_WITH ||
baseNo == null || (frameNo - baseNo + 1 <= surplusNum)) { // 当前图+填充 不能超过允许拼接数
log.info("当前宽度:" + currWidth + " < " + IMG_MAX_WITH + " 可以继续拼接");
baseNo == null || (frameNo - baseNo + 1 <= surplusNum)) {
log.info("当前宽度:{} < {} 可以继续拼接", currWidth, IMG_MAX_WITH);
return last;
}
// 情况3已经拼接到最大数量或者当前图+填充数量超过允许拼接数量创建新图像文件
log.info("当前宽度:" + currWidth + " > " + IMG_MAX_WITH + " 重新拼接,当前帧号" + frameNo + "作为首帧");
log.info("当前宽度:{} > {} 重新拼接,当前帧号{}作为首帧", currWidth, IMG_MAX_WITH, frameNo);
base = new JmImage();
redisUtil.hset(cacheKey, CACHE_FIELD_START_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND);
imageCache.put(cachePrefix + ":" + CACHE_FIELD_START_FRAME_NO, frameNo);
base.setImageNo(last.getImageNo() + 1);
return base;
}
@ -107,47 +106,12 @@ public class SarImageServiceImpl implements ISarImageService {
}
// 使用前一张图的右侧坐标作为后一张图的左侧前提是没丢图
if (!isFirst && !lostImage) {
/*imageFrame.setLon1(before[0]);
imageFrame.setLat1(before[1]);
imageFrame.setLon4(before[2]);
imageFrame.setLat4(before[3]);*/
// 注释部分保持原样
}
before[0] = imageFrame.getLon5();
before[1] = imageFrame.getLat5();
before[2] = imageFrame.getLon8();
before[3] = imageFrame.getLat8();
/*switch (rotateDTO.getType()) {
case 0:
case 1:
// 使用前一张图的右侧坐标作为后一张图的左侧前提是没丢图
if (!isFirst && !lostImage) {
imageFrame.setLon1(before[0]);
imageFrame.setLat1(before[1]);
imageFrame.setLon4(before[2]);
imageFrame.setLat4(before[3]);
} else if (before == null) {
before = new Double[4];
currAirline.setBeforeRight(before);
}
before[0] = imageFrame.getLon5();
before[1] = imageFrame.getLat5();
before[2] = imageFrame.getLon8();
before[3] = imageFrame.getLat8();
break;
case 2:
case 3:
if (!isFirst && !lostImage) {
imageFrame.setLon5(before[0]);
imageFrame.setLat5(before[1]);
imageFrame.setLon8(before[2]);
imageFrame.setLat8(before[3]);
}
before[0] = imageFrame.getLon1();
before[1] = imageFrame.getLat1();
before[2] = imageFrame.getLon4();
before[3] = imageFrame.getLat4();
break;
} */
}
/**
@ -155,8 +119,8 @@ public class SarImageServiceImpl implements ISarImageService {
*
* @param sourceIp
* @param airlineExecId
* @param frameData 图像帧数据
* @param imageFrame 图像帧
* @param frameData 图像帧数据
* @param imageFrame 图像帧
*/
@Override
public JmImage parseImage(String sourceIp, Long airlineExecId, byte[] frameData, SarBackImageFrameDTO imageFrame) {
@ -187,28 +151,28 @@ public class SarImageServiceImpl implements ISarImageService {
}
// 3.保存图像png用航线ID+序号命名
JmImage base = getBaseImage(airlineExecId, currImage.width(), imageFrame.getFrameNo());
String imageName = airlineExecId + "-" + base.getImageNo() +".png";
JmImage base = getBaseImage(airlineExecId, currImage.width(), frameNo);
String imageName = airlineExecId + "-" + base.getImageNo() + ".png";
String[] imagePath = sysFileTypeService.getFilePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId, imageName);
String currPath = imagePath[0];
System.out.println("帧:" + frameNo);
// 4.保存基准图同步用于下次拼接
Integer baseNo = (Integer) redisUtil.hget("jmImgJoin-" + airlineExecId, CACHE_FIELD_CURR_FRAME_NO);
Integer baseNo = imageCache.get("jmImgJoin-" + airlineExecId + ":" + CACHE_FIELD_CURR_FRAME_NO, Integer.class);
boolean lostImage = baseNo != null && (frameNo - baseNo > 1); // 判断是否丢图
String basePath = sysFileTypeService.getAbsolutePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId,
airlineExecId + "-" + base.getImageNo() +"-base.png");
airlineExecId + "-" + base.getImageNo() + "-base.png");
Mat baseMat = generateBaseMat(base, currImage, frameNo, imageFrame.getMax(), basePath, baseNo);
if (baseMat == null) { // 拼接失败 基准图生成失败则跳过按丢图处理
return null;
}
if (lostImage) {
log.warn("丢图"+(frameNo - baseNo)+"张!当前帧" + frameNo + ",前帧" + baseNo);
log.warn("丢图{}张!当前帧{},前帧{}", (frameNo - baseNo), frameNo, baseNo);
}
//modCoord(imageFrame, lostImage, currAirline);
redisUtil.hset("jmImgJoin-" + airlineExecId, CACHE_FIELD_CURR_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND);// 更新帧号
imageCache.put("jmImgJoin-" + airlineExecId + ":" + CACHE_FIELD_CURR_FRAME_NO, frameNo);
// ### 亮度调整用于可靠udp版本图像固定使用系数0.5
// 拆分多张图片去掉自适应调整
if (IMG_MAX_WITH > 20000) {
@ -224,15 +188,15 @@ public class SarImageServiceImpl implements ISarImageService {
// 6.更新基准图坐标和帧号
JmImage imageInfo = saveImage(uav, airlineExecId, base, imagePath, imageFrame, frameNo, lostImage);
long end = System.currentTimeMillis();
log.info("生成" + imageFrame.getImageBitDeep()+"位雷达回传图像:帧序号" + frameNo + "," +
imageInfo.getRelativePath() + ",耗时" + (end - start)/1000 + "");
log.info("生成{}位雷达回传图像:帧序号{}{},耗时{}秒",
imageFrame.getImageBitDeep(), frameNo, imageInfo.getRelativePath(), (end - start) / 1000);
return imageInfo;
}
/**
* 加载回传图像先转置再转为Mat
*
* @param frameData 图像帧数据包含参数信息和未转置的8位灰度图像数据
* @param frameData 图像帧数据包含参数信息和未转置的8位灰度图像数据
* @param imageFrame 图像帧对象
* @return 转置后的4通道图像Mat
*/
@ -265,10 +229,10 @@ public class SarImageServiceImpl implements ISarImageService {
/**
* 生成基准图将原有基准图与新图拼接
*
* @param base 基准图信息
* @param base 基准图信息
* @param currImage 当前图
* @param currNo 当前图的帧号
* @param currMax 当前图最大值
* @param currNo 当前图的帧号
* @param currMax 当前图最大值
* @param imagePath 基准图路径
* @return
*/
@ -281,7 +245,7 @@ public class SarImageServiceImpl implements ISarImageService {
if (baseMax < currMax) {
OpenCVUtil.multiply(baseImage, baseMax / currMax);
} else if (baseMax > currMax) {
OpenCVUtil.multiply(currImage , currMax / baseMax);
OpenCVUtil.multiply(currImage, currMax / baseMax);
}
}
Mat baseMat = ImageUtil.join(baseImage, baseNo, currImage, currNo); // 会释放 currImage baseImage 资源
@ -295,9 +259,9 @@ public class SarImageServiceImpl implements ISarImageService {
* 生成后处理图将拼接好的基准图转置并调整对比度
*
* @param currAirline 当前航线信息
* @param baseMat 原基准图处理后释放资源
* @param imagePath 后处理图路径每条航线对应一张图每次生成会覆盖
* @param imageLight 图像亮度倍数0是不调整
* @param baseMat 原基准图处理后释放资源
* @param imagePath 后处理图路径每条航线对应一张图每次生成会覆盖
* @param imageLight 图像亮度倍数0是不调整
*/
private void generateAfterMat(JmAirlineStatusDTO currAirline, Mat baseMat, String imagePath, int imageLight) {
// 后处理参数

View File

@ -49,11 +49,11 @@ spring:
date-format: yyyy-MM-dd'T'HH:mm:ss'Z'
#redis 配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: 'P@ssw0rd'
#redis:
#database: 0
#host: 127.0.0.1
#port: 6379
#password: 'P@ssw0rd'
mybatis-plus:
mapper-locations: classpath*:mapping/**/*Mapping.xml

View File

@ -22,10 +22,10 @@
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!-- </dependency>-->
</dependencies>

View File

@ -1,70 +0,0 @@
package com.zhangy.skyeye.redis.config;
import com.zhangy.skyeye.redis.utils.RedisUtil;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
/**
* Redis 配置
*/
@EnableCaching
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class DTRedisTemplateConfiguration {
/**
* value 序列化
* @return RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(RedisSerializer.class)
public RedisSerializer<Object> redisSerializer() {
return new JdkSerializationRedisSerializer();
}
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(RedisTemplate.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
RedisKeySerializer redisKeySerializer = new RedisKeySerializer();
// key 序列化
redisTemplate.setKeySerializer(redisKeySerializer);
redisTemplate.setHashKeySerializer(redisKeySerializer);
// value 序列化
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1));
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
@Bean(name = "redisUtil")
@ConditionalOnBean(RedisTemplate.class)
public RedisUtil redisUtils(RedisTemplate<String, Object> redisTemplate) {
return new RedisUtil(redisTemplate);
}
}

View File

@ -1,55 +1,55 @@
package com.zhangy.skyeye.redis.config;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 将redis key序列化为字符串
* spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题
*/
public class RedisKeySerializer implements RedisSerializer<Object> {
private final Charset charset;
private final ConversionService converter;
public RedisKeySerializer() {
this(StandardCharsets.UTF_8);
}
public RedisKeySerializer(Charset charset) {
Objects.requireNonNull(charset, "Charset must not be null");
this.charset = charset;
this.converter = DefaultConversionService.getSharedInstance();
}
@Override
public Object deserialize(byte[] bytes) {
// redis keys 会用到反序列化
if (bytes == null) {
return null;
}
return new String(bytes, charset);
}
@Override
@Nullable
public byte[] serialize(Object object) {
Objects.requireNonNull(object, "redis key is null");
String key;
if (object instanceof SimpleKey) {
key = "";
} else if (object instanceof String) {
key = (String) object;
} else {
key = converter.convert(object, String.class);
}
return key.getBytes(this.charset);
}
}
//package com.zhangy.skyeye.redis.config;
//
//import org.springframework.cache.interceptor.SimpleKey;
//import org.springframework.core.convert.ConversionService;
//import org.springframework.core.convert.support.DefaultConversionService;
//import org.springframework.data.redis.serializer.RedisSerializer;
//import org.springframework.lang.Nullable;
//
//import java.nio.charset.Charset;
//import java.nio.charset.StandardCharsets;
//import java.util.Objects;
//
///**
// * 将redis key序列化为字符串
// * spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题
// */
//public class RedisKeySerializer implements RedisSerializer<Object> {
// private final Charset charset;
// private final ConversionService converter;
//
// public RedisKeySerializer() {
// this(StandardCharsets.UTF_8);
// }
//
// public RedisKeySerializer(Charset charset) {
// Objects.requireNonNull(charset, "Charset must not be null");
// this.charset = charset;
// this.converter = DefaultConversionService.getSharedInstance();
// }
//
// @Override
// public Object deserialize(byte[] bytes) {
// // redis keys 会用到反序列化
// if (bytes == null) {
// return null;
// }
// return new String(bytes, charset);
// }
//
// @Override
// @Nullable
// public byte[] serialize(Object object) {
// Objects.requireNonNull(object, "redis key is null");
// String key;
// if (object instanceof SimpleKey) {
// key = "";
// } else if (object instanceof String) {
// key = (String) object;
// } else {
// key = converter.convert(object, String.class);
// }
// return key.getBytes(this.charset);
// }
//
//}

View File

@ -1,2 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zhangy.skyeye.redis.config.DTRedisTemplateConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=