From 2dcde94d4be159f07d313ba5392a443840667a61 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 30 Jul 2026 17:10:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E9=9D=99=E6=80=81?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E7=BC=93=E5=AD=98=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 6 + .../yfd/platform/config/SecurityConfig.java | 1 + .../controller/SdFpssrlRController.java | 3 + .../platform/qgc_base/domain/SdAiboxBH.java | 3 + .../qgc_base/domain/SdFpssrlRAiRequest.java | 5 + .../service/impl/SdAiboxBHServiceImpl.java | 2 + .../controller/DictCacheManageController.java | 89 +++++++ .../controller/SysDictionaryController.java | 12 + .../SysDictionaryItemsController.java | 48 ++++ .../utils/DictCacheInvalidateEvent.java | 58 +++++ .../utils/DictCodeToNameConverter.java | 241 +++++++++--------- 11 files changed, 353 insertions(+), 115 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/system/controller/DictCacheManageController.java create mode 100644 backend/src/main/java/com/yfd/platform/utils/DictCacheInvalidateEvent.java diff --git a/backend/pom.xml b/backend/pom.xml index ec6855c2..c92b7eed 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -95,6 +95,12 @@ spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + org.springframework.boot diff --git a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java index a1178822..3c457b73 100644 --- a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java @@ -64,6 +64,7 @@ public class SecurityConfig { .requestMatchers("/tempFile/**").permitAll() .requestMatchers("/system/user/auditUser").permitAll() .requestMatchers("/api/oauth2/oauth/token").permitAll() + .requestMatchers("/dict/cache/**").permitAll() .requestMatchers("/sys/psbmodulelbb/**").permitAll() .requestMatchers("/base/operationLog/**").permitAll() // .requestMatchers("/eng/**").permitAll() diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdFpssrlRController.java b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdFpssrlRController.java index ae195913..cb49968c 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdFpssrlRController.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdFpssrlRController.java @@ -141,6 +141,9 @@ public class SdFpssrlRController { if (req.getStcd() == null || req.getStcd().isEmpty()) { return ResponseResult.error("第" + (i + 1) + "条过鱼设施编码(stcd)不能为空"); } + if (req.getFwdx() == null || req.getFwdx().isEmpty()) { + return ResponseResult.error("第" + (i + 1) + "条服务对象(fwdx)不能为空"); + } if (req.getTm() == null) { return ResponseResult.error("第" + (i + 1) + "条识别时间(tm)不能为空"); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAiboxBH.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAiboxBH.java index 601289bf..3933e989 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAiboxBH.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAiboxBH.java @@ -104,6 +104,9 @@ public class SdAiboxBH implements Serializable { @FieldChinese("是否启用") private Integer usfl; + @TableField(exist = false) + private String usflName; + /** * 数据是否接入 */ diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdFpssrlRAiRequest.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdFpssrlRAiRequest.java index 7b844d05..e533018e 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdFpssrlRAiRequest.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdFpssrlRAiRequest.java @@ -38,6 +38,11 @@ public class SdFpssrlRAiRequest { */ private String fsz; + /** + * 服务对象 + */ + private String fwdx; + /** * 鱼长度,单位:cm */ diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAiboxBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAiboxBHServiceImpl.java index 4ab0272d..17e35771 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAiboxBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAiboxBHServiceImpl.java @@ -56,6 +56,8 @@ public class SdAiboxBHServiceImpl extends ServiceImpl提供手动刷新字典缓存的功能,用于紧急情况或运维场景。 + * + *

使用方式

+ *
    + *
  • 清除全部缓存:POST /dict/cache/clearAll
  • + *
  • 清除指定静态字典缓存:POST /dict/cache/clearStatic?dictCode=xxx
  • + *
  • 清除指定动态字典缓存:POST /dict/cache/clearDynamic?tableName=xxx&codeColumn=xxx&nameColumn=xxx
  • + *
  • 查看缓存统计:GET /dict/cache/stats
  • + *
+ */ +@RestController +@RequestMapping("/dict/cache") +@Tag(name = "字典缓存管理") +public class DictCacheManageController { + + @Resource + private DictCodeToNameConverter dictCodeToNameConverter; + + /** + * 清除所有字典缓存 + */ + @PostMapping("/clearAll") + @Operation(summary = "清除所有字典缓存") + public ResponseResult clearAllCache() { + dictCodeToNameConverter.clearCache(); + return ResponseResult.success("所有字典缓存已清除"); + } + + /** + * 清除指定静态字典缓存 + * + * @param dictCode 字典编码 + */ + @PostMapping("/clearStatic") + @Operation(summary = "清除指定静态字典缓存") + public ResponseResult clearStaticCache(@RequestParam String dictCode) { + if (StrUtil.isBlank(dictCode)) { + return ResponseResult.error("dictCode 不能为空"); + } + dictCodeToNameConverter.clearStaticDictCache(dictCode); + return ResponseResult.success("静态字典缓存已清除: " + dictCode); + } + + /** + * 清除指定动态字典缓存 + * + * @param tableName 数据库表名 + * @param codeColumn 编码列名 + * @param nameColumn 名称列名 + * @param filter 额外过滤条件(可选) + */ + @PostMapping("/clearDynamic") + @Operation(summary = "清除指定动态字典缓存") + public ResponseResult clearDynamicCache(@RequestParam String tableName, + @RequestParam String codeColumn, + @RequestParam String nameColumn, + @RequestParam(required = false) String filter) { + if (StrUtil.isBlank(tableName) || StrUtil.isBlank(codeColumn) || StrUtil.isBlank(nameColumn)) { + return ResponseResult.error("tableName、codeColumn、nameColumn 不能为空"); + } + dictCodeToNameConverter.clearDynamicDictCache(tableName, codeColumn, nameColumn, filter); + return ResponseResult.success("动态字典缓存已清除: " + tableName); + } + + /** + * 查看缓存统计信息 + */ + @GetMapping("/stats") + @Operation(summary = "查看字典缓存统计信息") + public ResponseResult getCacheStats() { + Map stats = dictCodeToNameConverter.getCacheStats(); + return ResponseResult.successData(stats); + } +} diff --git a/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryController.java b/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryController.java index b91f785c..e6c688a3 100644 --- a/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryController.java +++ b/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryController.java @@ -8,8 +8,10 @@ import com.yfd.platform.system.domain.SysDictionaryItems; import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; import com.yfd.platform.system.service.ISysDictionaryService; import com.yfd.platform.system.service.ISysDictionaryItemsService; +import com.yfd.platform.utils.DictCacheInvalidateEvent; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @@ -36,6 +38,9 @@ public class SysDictionaryController { @Resource private ISysDictionaryItemsService sysDictionaryItemsService; + @Resource + private ApplicationEventPublisher eventPublisher; + /********************************** * 用途说明: 获取数据字典列表 * 参数说明 dictType 字典类型 @@ -61,8 +66,15 @@ public class SysDictionaryController { @PostMapping("/deleteById") @Operation(summary = "根据ID删除字典") public ResponseResult deleteDictById(@RequestParam String id) { + // 删除前获取 dictCode,用于后续清除缓存 + SysDictionary dict = sysDictionaryService.getById(id); + String dictCode = dict != null ? dict.getDictCode() : null; boolean ok = sysDictionaryService.deleteDictById(id); if (ok) { + // 发布静态字典缓存失效事件 + if (StrUtil.isNotBlank(dictCode)) { + eventPublisher.publishEvent(new DictCacheInvalidateEvent(this, dictCode)); + } return ResponseResult.success(); } else { return ResponseResult.error(); diff --git a/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryItemsController.java b/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryItemsController.java index bf85d306..80627cd4 100644 --- a/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryItemsController.java +++ b/backend/src/main/java/com/yfd/platform/system/controller/SysDictionaryItemsController.java @@ -5,11 +5,15 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yfd.platform.annotation.Log; import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysDictionary; import com.yfd.platform.system.domain.SysDictionaryItems; import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; import com.yfd.platform.system.service.ISysDictionaryItemsService; +import com.yfd.platform.system.service.ISysDictionaryService; +import com.yfd.platform.utils.DictCacheInvalidateEvent; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; @@ -33,6 +37,31 @@ public class SysDictionaryItemsController { @Resource private ISysDictionaryItemsService sysDictionaryItemsService; + @Resource + private ISysDictionaryService sysDictionaryService; + + @Resource + private ApplicationEventPublisher eventPublisher; + + /** + * 根据 dictId 查询 dictCode,用于发布缓存失效事件 + */ + private String getDictCodeByDictId(String dictId) { + if (StrUtil.isBlank(dictId)) return null; + SysDictionary dict = sysDictionaryService.getById(dictId); + return dict != null ? dict.getDictCode() : null; + } + + /** + * 发布静态字典缓存失效事件 + */ + private void invalidateStaticCache(String dictId) { + String dictCode = getDictCodeByDictId(dictId); + if (StrUtil.isNotBlank(dictCode)) { + eventPublisher.publishEvent(new DictCacheInvalidateEvent(this, dictCode)); + } + } + /********************************** * 用途说明: 分页查询字典项信息 * 参数说明 dictID 字典ID ItemName 字典项名称 pageNum 当前页 @@ -77,6 +106,8 @@ public class SysDictionaryItemsController { boolean ok = sysDictionaryItemsService.addDictionaryItem(sysDictionaryItems); if (ok) { + // 发布静态字典缓存失效事件 + invalidateStaticCache(sysDictionaryItems.getDictId()); return ResponseResult.success(); } else { return ResponseResult.error(); @@ -98,6 +129,8 @@ public class SysDictionaryItemsController { boolean ok = sysDictionaryItemsService.updateById(sysDictionaryItems); if (ok) { + // 发布静态字典缓存失效事件 + invalidateStaticCache(sysDictionaryItems.getDictId()); return ResponseResult.success(); } else { return ResponseResult.error(); @@ -132,8 +165,13 @@ public class SysDictionaryItemsController { if (StrUtil.isBlank(id)) { return ResponseResult.error("参数为空"); } + // 删除前获取 dictId,用于清除缓存 + SysDictionaryItems item = sysDictionaryItemsService.getById(id); + String dictId = item != null ? item.getDictId() : null; boolean ok = sysDictionaryItemsService.removeById(id); if (ok) { + // 发布静态字典缓存失效事件 + invalidateStaticCache(dictId); return ResponseResult.success(); } else { return ResponseResult.error(); @@ -155,8 +193,18 @@ public class SysDictionaryItemsController { String[] splitIds = id.split(","); // 数组转集合 List ids = Arrays.asList(splitIds); + // 批量删除前获取所有相关的 dictId,去重后逐个清除缓存 + List items = sysDictionaryItemsService.listByIds(ids); boolean ok = sysDictionaryItemsService.removeByIds(ids); if (ok) { + // 发布静态字典缓存失效事件 + if (items != null) { + items.stream() + .map(SysDictionaryItems::getDictId) + .filter(StrUtil::isNotBlank) + .distinct() + .forEach(dictId -> invalidateStaticCache(dictId)); + } return ResponseResult.success(); } else { return ResponseResult.error(); diff --git a/backend/src/main/java/com/yfd/platform/utils/DictCacheInvalidateEvent.java b/backend/src/main/java/com/yfd/platform/utils/DictCacheInvalidateEvent.java new file mode 100644 index 00000000..6cb6635d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/DictCacheInvalidateEvent.java @@ -0,0 +1,58 @@ +package com.yfd.platform.utils; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * 字典缓存失效事件 + * + *

当静态字典或动态字典数据发生变更时发布此事件,通知 + * {@link DictCodeToNameConverter} 清除对应缓存。 + */ +@Getter +public class DictCacheInvalidateEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + /** 缓存类型:STATIC-静态字典, DYNAMIC-动态字典 */ + private final String cacheType; + + /** 静态字典:dictCode;动态字典:表名 */ + private final String dictSource; + + /** 以下仅动态字典需要 */ + private String codeColumn; + private String nameColumn; + private String filter; + + /** + * 静态字典缓存失效事件 + * + * @param source 事件源 + * @param dictCode 字典编码 + */ + public DictCacheInvalidateEvent(Object source, String dictCode) { + super(source); + this.cacheType = "STATIC"; + this.dictSource = dictCode; + } + + /** + * 动态字典缓存失效事件 + * + * @param source 事件源 + * @param tableName 数据库表名 + * @param codeColumn 编码列名 + * @param nameColumn 名称列名 + * @param filter 额外过滤条件 + */ + public DictCacheInvalidateEvent(Object source, String tableName, + String codeColumn, String nameColumn, String filter) { + super(source); + this.cacheType = "DYNAMIC"; + this.dictSource = tableName; + this.codeColumn = codeColumn; + this.nameColumn = nameColumn; + this.filter = filter; + } +} diff --git a/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java b/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java index 363d4f92..94dd29f2 100644 --- a/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java +++ b/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java @@ -2,25 +2,29 @@ package com.yfd.platform.utils; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import com.yfd.platform.common.MicroservicDynamicSQLMapper; import com.yfd.platform.system.domain.SysDictionary; import com.yfd.platform.system.domain.SysDictionaryItems; import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; import com.yfd.platform.system.mapper.SysDictionaryMapper; +import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * 字典代码转名称转换器 *

- * 支持静态字典(SYS_DICTIONARY / SYS_DICTIONARY_ITEMS)和动态字典(业务表查询), - * 将 VO 中的 code 字段值转换为对应的 name 字段值。 + * 使用 Caffeine Cache 存储字典映射,TTL=1小时自动过期(兜底), + * 并通过监听 {@link DictCacheInvalidateEvent} 在字典数据变更时即时清除缓存。 *

* *

使用方式

@@ -56,13 +60,17 @@ import java.util.stream.Collectors; * } * * - * @author Generated * @since 2025-05-18 */ @Slf4j @Component public class DictCodeToNameConverter { + /** 默认缓存 TTL:1 小时 */ + private static final long CACHE_TTL_HOURS = 1; + /** 最大缓存条目数 */ + private static final long MAX_CACHE_SIZE = 500; + @Resource private SysDictionaryMapper sysDictionaryMapper; @@ -73,14 +81,60 @@ public class DictCodeToNameConverter { private MicroservicDynamicSQLMapper microservicDynamicSQLMapper; /** - * 静态字典缓存:dictCode -> Map + * 静态字典缓存:dictCode -> Map<itemCode, dictName> + * TTL=1小时,最大 500 条目 */ - private final Map> staticDictCache = new ConcurrentHashMap<>(); + private Cache> staticDictCache; /** - * 动态字典缓存:缓存key -> Map + * 动态字典缓存:缓存key -> Map<code, name> + * TTL=1小时,最大 500 条目 */ - private final Map> dynamicDictCache = new ConcurrentHashMap<>(); + private Cache> dynamicDictCache; + + @PostConstruct + public void init() { + staticDictCache = Caffeine.newBuilder() + .expireAfterWrite(CACHE_TTL_HOURS, TimeUnit.HOURS) + .maximumSize(MAX_CACHE_SIZE) + .removalListener((key, value, cause) -> + log.debug("静态字典缓存失效: dictCode={}, cause={}", key, cause)) + .build(); + + dynamicDictCache = Caffeine.newBuilder() + .expireAfterWrite(CACHE_TTL_HOURS, TimeUnit.HOURS) + .maximumSize(MAX_CACHE_SIZE) + .removalListener((key, value, cause) -> + log.debug("动态字典缓存失效: key={}, cause={}", key, cause)) + .build(); + + log.info("DictCodeToNameConverter 初始化完成, TTL={}小时, maxSize={}", CACHE_TTL_HOURS, MAX_CACHE_SIZE); + } + + // ==================== 缓存失效事件监听 ==================== + + /** + * 监听字典缓存失效事件,即时清除对应缓存 + */ + @EventListener + public void onDictCacheInvalidate(DictCacheInvalidateEvent event) { + if ("STATIC".equalsIgnoreCase(event.getCacheType())) { + String dictCode = event.getDictSource(); + if (StrUtil.isNotBlank(dictCode)) { + staticDictCache.invalidate(dictCode); + log.info("静态字典缓存已清除: dictCode={}", dictCode); + } + } else if ("DYNAMIC".equalsIgnoreCase(event.getCacheType())) { + String cacheKey = event.getDictSource() + "|" + + event.getCodeColumn() + "|" + + event.getNameColumn() + "|" + + StrUtil.nullToDefault(event.getFilter(), ""); + dynamicDictCache.invalidate(cacheKey); + log.info("动态字典缓存已清除: key={}", cacheKey); + } + } + + // ==================== 对外转换方法 ==================== /** * 批量转换:根据元数据配置,将 VO 列表中的 code 字段转换为对应的 name 字段 @@ -108,17 +162,7 @@ public class DictCodeToNameConverter { } /** - * 分页结果转换:将 Page 中所有记录的 code 字段转换为对应的 name 字段 - * - *
-     * 使用示例:
-     * Page<SdHbrvDic> page = hbrvDicService.queryPageList(...);
-     * dictCodeToNameConverter.convertCodeToName(page, CODE_TO_NAME_META_LIST);
-     * 
- * - * @param page 分页对象 - * @param metadataList 元数据配置列表 - * @param VO 类型 + * 分页结果转换 */ public void convertCodeToName(Page page, List metadataList) { if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) { @@ -128,17 +172,7 @@ public class DictCodeToNameConverter { } /** - * 单个对象转换:将单个对象的 code 字段转换为对应的 name 字段 - * - *
-     * 使用示例:
-     * SdHbrvDic hbrvDic = hbrvDicService.getById(hbrvcd);
-     * dictCodeToNameConverter.convertCodeToName(hbrvDic, CODE_TO_NAME_META_LIST);
-     * 
- * - * @param obj 单个对象 - * @param metadataList 元数据配置列表 - * @param VO 类型 + * 单个对象转换 */ public void convertCodeToName(T obj, List metadataList) { if (obj == null || metadataList == null || metadataList.isEmpty()) { @@ -147,14 +181,13 @@ public class DictCodeToNameConverter { convertCodeToName(Collections.singletonList(obj), metadataList); } + // ==================== 缓存管理方法 ==================== + /** - * 加载静态字典映射 - * - * @param dictCode 字典编码 - * @return Map + * 加载静态字典映射(从 Caffeine Cache 获取,缓存未命中则查库) */ public Map loadStaticDictMap(String dictCode) { - return staticDictCache.computeIfAbsent(dictCode, key -> { + return staticDictCache.get(dictCode, key -> { Map map = new LinkedHashMap<>(); try { SysDictionary dict = sysDictionaryMapper.selectOne( @@ -175,6 +208,7 @@ public class DictCodeToNameConverter { map.put(item.getItemCode(), StrUtil.nullToDefault(item.getDictName(), item.getItemCode())); } } + log.debug("静态字典已加载: dictCode={}, size={}", key, map.size()); } catch (Exception e) { log.error("加载静态字典失败: dictCode={}", key, e); } @@ -183,17 +217,11 @@ public class DictCodeToNameConverter { } /** - * 加载动态字典映射(从业务表查询) - * - * @param tableName 表名 - * @param codeColumn 编码列名 - * @param nameColumn 名称列名 - * @param filter 额外过滤条件 - * @return Map + * 加载动态字典映射(从 Caffeine Cache 获取,缓存未命中则查库) */ public Map loadDynamicDictMap(String tableName, String codeColumn, String nameColumn, String filter) { String cacheKey = tableName + "|" + codeColumn + "|" + nameColumn + "|" + StrUtil.nullToDefault(filter, ""); - return dynamicDictCache.computeIfAbsent(cacheKey, key -> { + return dynamicDictCache.get(cacheKey, key -> { Map map = new LinkedHashMap<>(); try { StringBuilder sql = new StringBuilder(); @@ -213,6 +241,7 @@ public class DictCodeToNameConverter { map.put(code, StrUtil.nullToDefault(name, code)); } } + log.debug("动态字典已加载: tableName={}, size={}", tableName, map.size()); } catch (Exception e) { log.error("加载动态字典失败: tableName={}", tableName, e); } @@ -220,6 +249,48 @@ public class DictCodeToNameConverter { }); } + /** + * 清除所有缓存 + */ + public void clearCache() { + staticDictCache.invalidateAll(); + dynamicDictCache.invalidateAll(); + log.info("所有字典缓存已清除"); + } + + /** + * 清除指定静态字典缓存 + */ + public void clearStaticDictCache(String dictCode) { + if (StrUtil.isNotBlank(dictCode)) { + staticDictCache.invalidate(dictCode); + log.info("静态字典缓存已清除: dictCode={}", dictCode); + } + } + + /** + * 清除指定动态字典缓存 + */ + public void clearDynamicDictCache(String tableName, String codeColumn, String nameColumn, String filter) { + String cacheKey = tableName + "|" + codeColumn + "|" + nameColumn + "|" + StrUtil.nullToDefault(filter, ""); + dynamicDictCache.invalidate(cacheKey); + log.info("动态字典缓存已清除: key={}", cacheKey); + } + + /** + * 获取缓存统计信息 + */ + public Map getCacheStats() { + Map stats = new LinkedHashMap<>(); + stats.put("staticDictSize", staticDictCache.estimatedSize()); + stats.put("dynamicDictSize", dynamicDictCache.estimatedSize()); + stats.put("ttlHours", CACHE_TTL_HOURS); + stats.put("maxSize", MAX_CACHE_SIZE); + return stats; + } + + // ==================== 私有方法 ==================== + /** * 根据元数据加载 code->name 映射 */ @@ -251,7 +322,6 @@ public class DictCodeToNameConverter { String nameValue; if (metadata.isMulti()) { - // 多选:逗号分隔的多个code nameValue = Arrays.stream(codeValue.split(",")) .map(String::trim) .filter(StrUtil::isNotBlank) @@ -267,40 +337,12 @@ public class DictCodeToNameConverter { } } - /** - * 清除所有缓存 - */ - public void clearCache() { - staticDictCache.clear(); - dynamicDictCache.clear(); - } - - /** - * 清除指定静态字典缓存 - */ - public void clearStaticDictCache(String dictCode) { - staticDictCache.remove(dictCode); - } - - /** - * 清除指定动态字典缓存 - */ - public void clearDynamicDictCache(String tableName, String codeColumn, String nameColumn, String filter) { - String cacheKey = tableName + "|" + codeColumn + "|" + nameColumn + "|" + StrUtil.nullToDefault(filter, ""); - dynamicDictCache.remove(cacheKey); - } - // ==================== 反射工具方法 ==================== - /** - * 通过反射获取对象的字段值(转为字符串),支持驼峰命名 - */ private String getFieldValue(T obj, String fieldName) { try { Field field = findField(obj.getClass(), fieldName); - if (field == null) { - return null; - } + if (field == null) return null; field.setAccessible(true); Object value = field.get(obj); return value != null ? value.toString() : null; @@ -309,82 +351,51 @@ public class DictCodeToNameConverter { } } - /** - * 通过反射设置对象的字段值,支持驼峰命名 - */ private void setFieldValue(T obj, String fieldName, String value) { try { Field field = findField(obj.getClass(), fieldName); - if (field == null) { - return; - } + if (field == null) return; field.setAccessible(true); field.set(obj, value); } catch (Exception ignored) { - // 忽略设置失败 } } - /** - * 查找字段(忽略大小写,支持驼峰和下划线变体),递归查找父类 - */ private Field findField(Class clazz, String fieldName) { - if (clazz == null || clazz == Object.class) { - return null; - } - // 先精确匹配 + if (clazz == null || clazz == Object.class) return null; for (Field field : clazz.getDeclaredFields()) { - if (field.getName().equals(fieldName)) { - return field; - } + if (field.getName().equals(fieldName)) return field; } - // 忽略大小写匹配 for (Field field : clazz.getDeclaredFields()) { - if (field.getName().equalsIgnoreCase(fieldName)) { - return field; - } + if (field.getName().equalsIgnoreCase(fieldName)) return field; } - // 驼峰-下划线互转匹配 String camelName = underlineToCamel(fieldName); String underlineName = camelToUnderline(fieldName); for (Field field : clazz.getDeclaredFields()) { String fName = field.getName(); - if (fName.equalsIgnoreCase(camelName) || fName.equalsIgnoreCase(underlineName)) { - return field; - } + if (fName.equalsIgnoreCase(camelName) || fName.equalsIgnoreCase(underlineName)) return field; } return findField(clazz.getSuperclass(), fieldName); } private String underlineToCamel(String str) { - if (StrUtil.isBlank(str) || !str.contains("_")) { - return str; - } + if (StrUtil.isBlank(str) || !str.contains("_")) return str; StringBuilder sb = new StringBuilder(); boolean upper = false; for (char c : str.toCharArray()) { - if (c == '_') { - upper = true; - } else { - sb.append(upper ? Character.toUpperCase(c) : c); - upper = false; - } + if (c == '_') { upper = true; } + else { sb.append(upper ? Character.toUpperCase(c) : c); upper = false; } } return sb.toString(); } private String camelToUnderline(String str) { - if (StrUtil.isBlank(str)) { - return str; - } + if (StrUtil.isBlank(str)) return str; StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); - if (Character.isUpperCase(c)) { - sb.append('_').append(Character.toLowerCase(c)); - } else { - sb.append(c); - } + if (Character.isUpperCase(c)) sb.append('_').append(Character.toLowerCase(c)); + else sb.append(c); } return sb.toString(); }