From a37c7248abc57dcc6ccd3ed43ed581a7cc6c5471 Mon Sep 17 00:00:00 2001 From: tangwei Date: Tue, 30 Jun 2026 17:38:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9D=99=E6=80=81=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E5=92=8C=E5=8A=A8=E6=80=81=E5=AD=97=E5=85=B8=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/SdHbrvDicServiceImpl.java | 31 ++ .../platform/utils/CodeToNameMetadataBo.java | 94 +++++ .../utils/DictCodeToNameConverter.java | 350 ++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 backend/src/main/java/com/yfd/platform/utils/CodeToNameMetadataBo.java create mode 100644 backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdHbrvDicServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdHbrvDicServiceImpl.java index 67dc9c00..5d58e2fe 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdHbrvDicServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdHbrvDicServiceImpl.java @@ -16,6 +16,8 @@ import com.yfd.platform.qgc_base.mapper.SdHbrvDicMapper; import com.yfd.platform.qgc_base.mapper.SdHydrobaseMapper; import com.yfd.platform.qgc_base.service.ISdHbrvDicService; import com.yfd.platform.system.service.IAdminAuthService; +import com.yfd.platform.utils.CodeToNameMetadataBo; +import com.yfd.platform.utils.DictCodeToNameConverter; import com.yfd.platform.utils.QgcQueryWrapperUtil; import com.yfd.platform.utils.SecurityUtils; import jakarta.annotation.Resource; @@ -51,6 +53,35 @@ public class SdHbrvDicServiceImpl extends ServiceImpl + * 每个模块的 ServiceImpl 中可按需定义自己的 codeToNameMetadataBoList, + * 在查询方法返回前调用 dictCodeToNameConverter.convertCodeToName(voList, codeToNameMetadataBoList) + * 即可自动将 code 字段转为对应的 name 字段。 + *

+ * + *
+     * 使用示例:
+     * // 在查询方法末尾调用:
+     * dictCodeToNameConverter.convertCodeToName(voList, CODE_TO_NAME_META_LIST);
+     * 
+ */ + private static final List CODE_TO_NAME_META_LIST = new ArrayList<>(); + +// static { +// +// CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("grd").modifyProperty("grdName").dictType("STATIC").dictSource("TEST").build()); +// CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("baseid").modifyProperty("basename").dictType("DYNAMIC").dictSource("SD_HYDROBASE").codeColumn("BASEID").nameColumn("BASENAME").filter("NVL(IS_DELETED, 0) = 0 ").build()); +// } + + @Override public Page queryPageList(Page page, String hbrvnm, String baseid) { return this.page(page, this.lambdaQuery() diff --git a/backend/src/main/java/com/yfd/platform/utils/CodeToNameMetadataBo.java b/backend/src/main/java/com/yfd/platform/utils/CodeToNameMetadataBo.java new file mode 100644 index 00000000..5f8ba742 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/CodeToNameMetadataBo.java @@ -0,0 +1,94 @@ +package com.yfd.platform.utils; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 代码转名称元数据配置 + *

+ * 用于配置 VO 中某个 code 字段如何转换为对应的 name 字段, + * 支持静态字典(SYS_DICTIONARY / SYS_DICTIONARY_ITEMS)和动态字典(业务表)。 + *

+ * + *
+ * 使用示例:
+ * // 静态字典:将 vo.typeCode 转为 vo.typeName,字典编码为 "PROJECT_TYPE"
+ * CodeToNameMetadataBo.builder()
+ *     .codeProperty("typeCode")
+ *     .modifyProperty("typeName")
+ *     .dictType("STATIC")
+ *     .dictSource("PROJECT_TYPE")
+ *     .build();
+ *
+ * // 动态字典:将 vo.engCode 转为 vo.engName,查 SD_ENGINFO_B_H 表
+ * CodeToNameMetadataBo.builder()
+ *     .codeProperty("engCode")
+ *     .modifyProperty("engName")
+ *     .dictType("DYNAMIC")
+ *     .dictSource("SD_ENGINFO_B_H")
+ *     .codeColumn("STCD")
+ *     .nameColumn("ENNM")
+ *     .filter("NVL(IS_DELETED, 0) = 0")
+ *     .build();
+ * 
+ * + * @author Generated + * @since 2025-05-18 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CodeToNameMetadataBo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 编码属性名(VO 中存放 code 的字段名) + */ + private String codeProperty; + + /** + * 名称属性名(VO 中需要设置 name 的字段名) + */ + private String modifyProperty; + + /** + * 字典类型:STATIC(静态字典)或 DYNAMIC(动态字典) + */ + private String dictType; + + /** + * 字典来源: + * - 静态字典:SYS_DICTIONARY 表中的 DICTCODE + * - 动态字典:业务表名 + */ + private String dictSource; + + /** + * 动态字典:表中编码列名(静态字典不需要) + */ + private String codeColumn; + + /** + * 动态字典:表中名称列名(静态字典不需要) + */ + private String nameColumn; + + /** + * 额外过滤条件(可选) + * - 静态字典:SYS_DICTIONARY_ITEMS 的额外 WHERE 条件 + * - 动态字典:业务表的额外 WHERE 条件 + */ + private String filter; + + /** + * 是否多选(code 值为逗号分隔的多个编码) + */ + @Builder.Default + private boolean isMulti = false; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java b/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java new file mode 100644 index 00000000..a7db357d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/DictCodeToNameConverter.java @@ -0,0 +1,350 @@ +package com.yfd.platform.utils; + +import cn.hutool.core.util.StrUtil; +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.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 字典代码转名称转换器 + *

+ * 支持静态字典(SYS_DICTIONARY / SYS_DICTIONARY_ITEMS)和动态字典(业务表查询), + * 将 VO 中的 code 字段值转换为对应的 name 字段值。 + *

+ * + *

使用方式

+ *
+ * // 1. 在 ServiceImpl 中注入
+ * @Resource
+ * private DictCodeToNameConverter dictCodeToNameConverter;
+ *
+ * // 2. 定义元数据列表(通常放在 static 静态代码块中)
+ * private static final List<CodeToNameMetadataBo> CODE_TO_NAME_META_LIST = List.of(
+ *     CodeToNameMetadataBo.builder()
+ *         .codeProperty("stationType")
+ *         .modifyProperty("stationTypeName")
+ *         .dictType("STATIC")
+ *         .dictSource("STATION_TYPE")
+ *         .build(),
+ *     CodeToNameMetadataBo.builder()
+ *         .codeProperty("engCode")
+ *         .modifyProperty("engName")
+ *         .dictType("DYNAMIC")
+ *         .dictSource("SD_ENGINFO_B_H")
+ *         .codeColumn("STCD")
+ *         .nameColumn("ENNM")
+ *         .filter("NVL(IS_DELETED, 0) = 0")
+ *         .build()
+ * );
+ *
+ * // 3. 在查询方法中调用转换
+ * public DataSourceResult<MyVo> queryList(DataSourceRequest request) {
+ *     // ... 查询数据得到 voList ...
+ *     dictCodeToNameConverter.convertCodeToName(voList, CODE_TO_NAME_META_LIST);
+ *     // ... 返回结果 ...
+ * }
+ * 
+ * + * @author Generated + * @since 2025-05-18 + */ +@Slf4j +@Component +public class DictCodeToNameConverter { + + @Resource + private SysDictionaryMapper sysDictionaryMapper; + + @Resource + private SysDictionaryItemsMapper sysDictionaryItemsMapper; + + @Resource + private MicroservicDynamicSQLMapper microservicDynamicSQLMapper; + + /** + * 静态字典缓存:dictCode -> Map + */ + private final Map> staticDictCache = new ConcurrentHashMap<>(); + + /** + * 动态字典缓存:缓存key -> Map + */ + private final Map> dynamicDictCache = new ConcurrentHashMap<>(); + + /** + * 批量转换:根据元数据配置,将 VO 列表中的 code 字段转换为对应的 name 字段 + * + * @param voList VO 对象列表 + * @param metadataList 元数据配置列表 + * @param VO 类型 + */ + public void convertCodeToName(List voList, List metadataList) { + if (voList == null || voList.isEmpty() || metadataList == null || metadataList.isEmpty()) { + return; + } + + for (CodeToNameMetadataBo metadata : metadataList) { + try { + Map codeNameMap = loadCodeNameMap(metadata); + if (codeNameMap.isEmpty()) { + continue; + } + applyCodeToName(voList, metadata, codeNameMap); + } catch (Exception e) { + log.error("字典转换失败: codeProperty={}, dictSource={}", metadata.getCodeProperty(), metadata.getDictSource(), e); + } + } + } + + /** + * 加载静态字典映射 + * + * @param dictCode 字典编码 + * @return Map + */ + public Map loadStaticDictMap(String dictCode) { + return staticDictCache.computeIfAbsent(dictCode, key -> { + Map map = new LinkedHashMap<>(); + try { + SysDictionary dict = sysDictionaryMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SysDictionary::getDictCode, key) + ); + if (dict == null) { + log.warn("静态字典未找到: dictCode={}", key); + return map; + } + List items = sysDictionaryItemsMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SysDictionaryItems::getDictId, dict.getId()) + .orderByAsc(SysDictionaryItems::getOrderNo) + ); + for (SysDictionaryItems item : items) { + if (StrUtil.isNotBlank(item.getItemCode())) { + map.put(item.getItemCode(), StrUtil.nullToDefault(item.getDictName(), item.getItemCode())); + } + } + } catch (Exception e) { + log.error("加载静态字典失败: dictCode={}", key, e); + } + return map; + }); + } + + /** + * 加载动态字典映射(从业务表查询) + * + * @param tableName 表名 + * @param codeColumn 编码列名 + * @param nameColumn 名称列名 + * @param filter 额外过滤条件 + * @return Map + */ + public Map loadDynamicDictMap(String tableName, String codeColumn, String nameColumn, String filter) { + String cacheKey = tableName + "|" + codeColumn + "|" + nameColumn + "|" + StrUtil.nullToDefault(filter, ""); + return dynamicDictCache.computeIfAbsent(cacheKey, key -> { + Map map = new LinkedHashMap<>(); + try { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT ").append(codeColumn).append(" AS code, ") + .append(nameColumn).append(" AS name ") + .append("FROM ").append(tableName) + .append(" WHERE 1=1 "); + if (StrUtil.isNotBlank(filter)) { + sql.append("AND ").append(filter).append(" "); + } + Map params = new HashMap<>(); + List> rows = microservicDynamicSQLMapper.getAllList(sql.toString(), params); + for (Map row : rows) { + String code = Objects.toString(row.get("CODE"), null); + String name = Objects.toString(row.get("NAME"), null); + if (StrUtil.isNotBlank(code)) { + map.put(code, StrUtil.nullToDefault(name, code)); + } + } + } catch (Exception e) { + log.error("加载动态字典失败: tableName={}", tableName, e); + } + return map; + }); + } + + /** + * 根据元数据加载 code->name 映射 + */ + private Map loadCodeNameMap(CodeToNameMetadataBo metadata) { + if ("STATIC".equalsIgnoreCase(metadata.getDictType())) { + return loadStaticDictMap(metadata.getDictSource()); + } else if ("DYNAMIC".equalsIgnoreCase(metadata.getDictType())) { + return loadDynamicDictMap( + metadata.getDictSource(), + metadata.getCodeColumn(), + metadata.getNameColumn(), + metadata.getFilter() + ); + } + log.warn("未知字典类型: dictType={}", metadata.getDictType()); + return Collections.emptyMap(); + } + + /** + * 将 code->name 映射应用到 VO 列表 + */ + private void applyCodeToName(List voList, CodeToNameMetadataBo metadata, Map codeNameMap) { + for (T vo : voList) { + try { + String codeValue = getFieldValue(vo, metadata.getCodeProperty()); + if (StrUtil.isBlank(codeValue)) { + continue; + } + + String nameValue; + if (metadata.isMulti()) { + // 多选:逗号分隔的多个code + nameValue = Arrays.stream(codeValue.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .map(c -> codeNameMap.getOrDefault(c, c)) + .collect(Collectors.joining(",")); + } else { + nameValue = codeNameMap.getOrDefault(codeValue.trim(), codeValue); + } + setFieldValue(vo, metadata.getModifyProperty(), nameValue); + } catch (Exception e) { + log.debug("字段转换跳过: vo={}, codeProperty={}", vo.getClass().getSimpleName(), metadata.getCodeProperty(), e); + } + } + } + + /** + * 清除所有缓存 + */ + 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; + } + field.setAccessible(true); + Object value = field.get(obj); + return value != null ? value.toString() : null; + } catch (Exception e) { + return null; + } + } + + /** + * 通过反射设置对象的字段值,支持驼峰命名 + */ + private void setFieldValue(T obj, String fieldName, String value) { + try { + Field field = findField(obj.getClass(), fieldName); + 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; + } + // 先精确匹配 + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().equals(fieldName)) { + return field; + } + } + // 忽略大小写匹配 + for (Field field : clazz.getDeclaredFields()) { + 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; + } + } + return findField(clazz.getSuperclass(), fieldName); + } + + private String underlineToCamel(String 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; + } + } + return sb.toString(); + } + + private String camelToUnderline(String 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); + } + } + return sb.toString(); + } +} \ No newline at end of file