当静态字典或动态字典数据发生变更时发布此事件,通知 + * {@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} 在字典数据变更时即时清除缓存。 *
* *
+ * 动态字典缓存:缓存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();
}