From a7c3b61680e91eb78a7340e65ceb7d4a5bfac4c6 Mon Sep 17 00:00:00 2001 From: guoshengxiong <1923636941@qq.com> Date: Wed, 26 Nov 2025 16:52:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=AE=9E=E4=BD=93=E7=B1=BB?= =?UTF-8?q?=E6=98=AF=E5=90=A6=E5=B0=91=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validator/BulkEntityValidator.java | 500 ++++++++++++++++++ .../modules/validator/EntityFieldInfo.java | 15 + .../validator/EntityValidatorController.java | 62 +++ .../modules/validator/ValidationResult.java | 40 ++ .../modules/validator/ValidatorConfig.java | 15 + 5 files changed, 632 insertions(+) create mode 100644 src/main/java/com/zhgd/xmgl/modules/validator/BulkEntityValidator.java create mode 100644 src/main/java/com/zhgd/xmgl/modules/validator/EntityFieldInfo.java create mode 100644 src/main/java/com/zhgd/xmgl/modules/validator/EntityValidatorController.java create mode 100644 src/main/java/com/zhgd/xmgl/modules/validator/ValidationResult.java create mode 100644 src/main/java/com/zhgd/xmgl/modules/validator/ValidatorConfig.java diff --git a/src/main/java/com/zhgd/xmgl/modules/validator/BulkEntityValidator.java b/src/main/java/com/zhgd/xmgl/modules/validator/BulkEntityValidator.java new file mode 100644 index 000000000..3e86fa7cb --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/validator/BulkEntityValidator.java @@ -0,0 +1,500 @@ +package com.zhgd.xmgl.modules.validator; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class BulkEntityValidator { + + @Autowired + @Qualifier("db1DataSource") + private DataSource dataSource; + + private final Map> tableColumnsCache = new ConcurrentHashMap<>(); + private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver); + + /** + * 应用启动时自动验证所有实体类 + */ + @PostConstruct + public void autoValidateOnStartup() { + log.info("开始自动验证实体类与数据库表映射..."); + try { + Map results = validateAllEntitiesInProject(); + printValidationSummary(results); + } catch (Exception e) { + log.error("实体类映射验证失败", e); + } + } + + /** + * 验证项目中所有实体类 - 返回验证结果 + */ + public Map validateAllEntitiesInProject() { + // 常见的实体类包路径,可以根据实际情况调整 + String[] commonEntityPackages = { + "com.zhgd.xmgl.modules.**.entity" + }; + + Map allResults = new LinkedHashMap<>(); + + for (String basePackage : commonEntityPackages) { + try { + Map packageResults = validatePackageEntities(basePackage); + allResults.putAll(packageResults); + } catch (Exception e) { + log.warn("扫描包 {} 失败: {}", basePackage, e.getMessage()); + allResults.put(basePackage, ValidationResult.error(basePackage, "扫描包失败: " + e.getMessage())); + } + } + + return allResults; + } + + /** + * 验证指定包下的所有实体类 + */ + public Map validatePackageEntities(String basePackage) { + log.info("开始扫描包: {}", basePackage); + Map results = new LinkedHashMap<>(); + + Set> entityClasses = findEntitiesInPackage(basePackage); + log.info("在包 {} 中找到 {} 个实体类", basePackage, entityClasses.size()); + + for (Class entityClass : entityClasses) { + try { + ValidationResult result = validateSingleEntity(entityClass); + results.put(entityClass.getSimpleName(), result); + } catch (Exception e) { + log.error("验证实体类 {} 失败: {}", entityClass.getSimpleName(), e.getMessage()); + results.put(entityClass.getSimpleName(), + ValidationResult.error(entityClass.getSimpleName(), e.getMessage())); + } + } + + return results; + } + + /** + * 验证单个实体类 + */ + public ValidationResult validateSingleEntity(Class entityClass) { + TableName tableAnnotation = entityClass.getAnnotation(TableName.class); + if (tableAnnotation == null) { + return ValidationResult.error(entityClass.getSimpleName(), "缺少 @TableName 注解"); + } + + String tableName = tableAnnotation.value(); + if (tableName.isEmpty()) { + return ValidationResult.error(entityClass.getSimpleName(), "@TableName 注解值为空"); + } + + // 检查表是否存在 + if (!isTableExists(tableName)) { + return ValidationResult.error(entityClass.getSimpleName(), + "数据库表不存在: " + tableName); + } + + List dbColumns = getTableColumns(tableName); + List entityFields = getEntityFields(entityClass); + + // 对比字段映射 + List missingInEntity = findMissingInEntity(dbColumns, entityFields); + List missingInDb = findMissingInDatabase(entityFields, dbColumns); + List typeMismatches = findTypeMismatches(entityClass, tableName, entityFields, dbColumns); + + return ValidationResult.of(entityClass.getSimpleName(), tableName, + missingInEntity, missingInDb, typeMismatches); + } + + /** + * 扫描包下的所有实体类 + */ + private Set> findEntitiesInPackage(String basePackage) { + Set> entities = new LinkedHashSet<>(); + try { + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(basePackage) + "/**/*.class"; + + Resource[] resources = resourcePatternResolver.getResources(packageSearchPath); + + for (Resource resource : resources) { + if (resource.isReadable()) { + try { + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource); + String className = metadataReader.getClassMetadata().getClassName(); + + // 过滤内部类、匿名类等 + if (className.contains("$")) { + continue; + } + + Class clazz = Class.forName(className); + if (clazz.isAnnotationPresent(TableName.class)) { + entities.add(clazz); + } + } catch (ClassNotFoundException e) { + log.warn("无法加载类: {}", resource.getFilename()); + } + } + } + } catch (Exception e) { + log.error("扫描包 {} 失败: {}", basePackage, e.getMessage()); + } + return entities; + } + + /** + * 获取数据库表的所有字段 + */ + private List getTableColumns(String tableName) { + return tableColumnsCache.computeIfAbsent(tableName, this::fetchTableColumnsFromDb); + } + + private List fetchTableColumnsFromDb(String tableName) { + List columns = new ArrayList<>(); + try (Connection conn = dataSource.getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + + // 尝试不同的表名匹配策略 + ResultSet rs = metaData.getColumns(null, null, tableName, null); + if (!rs.next()) { + // 尝试小写 + rs = metaData.getColumns(null, null, tableName.toLowerCase(), null); + } + if (!rs.next()) { + // 尝试大写 + rs = metaData.getColumns(null, null, tableName.toUpperCase(), null); + } + + // 重置游标 + rs.beforeFirst(); + + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + // 清理列名中的特殊字符 + columnName = cleanColumnName(columnName); + columns.add(columnName.toLowerCase()); + } + } catch (SQLException e) { + log.error("获取表 {} 字段失败: {}", tableName, e.getMessage()); + } + return columns; + } + + /** + * 检查表是否存在 + */ + private boolean isTableExists(String tableName) { + try (Connection conn = dataSource.getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + ResultSet rs = metaData.getTables(null, null, tableName, null); + if (rs.next()) return true; + + // 尝试小写 + rs = metaData.getTables(null, null, tableName.toLowerCase(), null); + if (rs.next()) return true; + + // 尝试大写 + rs = metaData.getTables(null, null, tableName.toUpperCase(), null); + return rs.next(); + + } catch (SQLException e) { + log.error("检查表是否存在失败: {}", e.getMessage()); + return false; + } + } + + /** + * 获取实体类的所有字段信息 + */ + private List getEntityFields(Class entityClass) { + List fields = new ArrayList<>(); + Class currentClass = entityClass; + + // 遍历所有父类直到 Object + while (currentClass != null && currentClass != Object.class) { + Field[] declaredFields = currentClass.getDeclaredFields(); + + for (Field field : declaredFields) { + // 忽略静态字段、序列化字段等 + if (shouldIgnoreField(field)) { + continue; + } + + EntityFieldInfo fieldInfo = extractFieldInfo(field); + if (fieldInfo != null) { + fields.add(fieldInfo); + } + } + + currentClass = currentClass.getSuperclass(); + } + + return fields; + } + + /** + * 判断是否应该忽略该字段 + */ + private boolean shouldIgnoreField(Field field) { + int modifiers = field.getModifiers(); + + // 忽略静态字段 + if (Modifier.isStatic(modifiers)) { + return true; + } + + // 忽略序列化字段 + if ("serialVersionUID".equals(field.getName())) { + return true; + } + + // 忽略被 @TableField(exist = false) 标记的字段 + TableField tableField = field.getAnnotation(TableField.class); + if (tableField != null && !tableField.exist()) { + return true; + } + + return false; + } + + /** + * 提取字段信息 + */ + private EntityFieldInfo extractFieldInfo(Field field) { + String fieldName = field.getName(); + String columnName; + + TableField tableField = field.getAnnotation(TableField.class); + if (tableField != null && !tableField.value().isEmpty()) { + columnName = tableField.value(); + } else { + columnName = StrUtil.toUnderlineCase(fieldName); + } + + // 清理列名中的特殊字符 + columnName = cleanColumnName(columnName); + + return new EntityFieldInfo( + fieldName, + columnName.toLowerCase(), + field.getType().getSimpleName() + ); + } + /** + * 清理列名,处理各种特殊情况 + */ + private String cleanColumnName(String columnName) { + if (columnName == null || columnName.isEmpty()) { + return columnName; + } + + // 去掉反引号、单引号、双引号等 + columnName = columnName.replace("`", "") + .replace("'", "") + .replace("\"", "") + .trim(); + + // 如果列名被方括号包围(SQL Server风格),去掉方括号 + if (columnName.startsWith("[") && columnName.endsWith("]")) { + columnName = columnName.substring(1, columnName.length() - 1); + } + + return columnName; + } + /** + * 找出数据库中有的但实体类缺少的字段 + */ + private List findMissingInEntity(List dbColumns, List entityFields) { + Set entityColumnNames = entityFields.stream() + .map(EntityFieldInfo::getColumnName) + .collect(Collectors.toSet()); + + return dbColumns.stream() + .filter(column -> !entityColumnNames.contains(column)) + .collect(Collectors.toList()); + } + + /** + * 找出实体类有的但数据库缺少的字段 + */ + private List findMissingInDatabase(List entityFields, List dbColumns) { + Set dbColumnSet = new HashSet<>(dbColumns); + + return entityFields.stream() + .map(EntityFieldInfo::getColumnName) + .filter(columnName -> !dbColumnSet.contains(columnName)) + .collect(Collectors.toList()); + } + + /** + * 找出类型不匹配的字段(基础实现) + */ + private List findTypeMismatches(Class entityClass, String tableName, + List entityFields, List dbColumns) { + // 这里可以实现更复杂的类型匹配检查 + // 目前返回空列表,可以根据需要扩展 + return new ArrayList<>(); + } + + /** + * 打印验证摘要 + */ + public void printValidationSummary(Map results) { + if (results.isEmpty()) { + log.info("🎉 没有找到需要验证的实体类!"); + return; + } + + long errorCount = results.values().stream() + .filter(ValidationResult::hasIssues) + .count(); + long successCount = results.size() - errorCount; + + log.info("验证完成!总计验证 {} 个实体类,成功: {},存在问题: {}", + results.size(), successCount, errorCount); + + if (errorCount == 0) { + log.info("🎉 所有实体类映射验证通过!"); + return; + } + + log.warn("⚠️ 发现 {} 个实体类存在映射问题:", errorCount); + + results.forEach((className, result) -> { + if (result.hasIssues()) { + log.warn("\n=========================================="); + log.warn("实体类: {} -> 表: {}", className, result.getTableName()); + + if (!result.getMissingInEntity().isEmpty()) { + log.warn("❌ 数据库有但实体缺少: {}", result.getMissingInEntity()); + } + + if (!result.getMissingInDb().isEmpty()) { + log.warn("❌ 实体有但数据库缺少: {}", result.getMissingInDb()); + } + + if (!result.getTypeMismatches().isEmpty()) { + log.warn("⚠️ 类型不匹配: {}", result.getTypeMismatches()); + } + + if (result.getError() != null) { + log.warn("💥 验证错误: {}", result.getError()); + } + } + }); + + log.warn("\n=========================================="); + } + + /** + * 生成详细的验证报告 + */ + public String generateDetailedReport() { + Map results = validateAllEntitiesInProject(); + + StringBuilder report = new StringBuilder(); + report.append("实体类映射验证报告\n"); + report.append("生成时间: ").append(new Date()).append("\n\n"); + + if (results.isEmpty()) { + report.append("✅ 没有找到需要验证的实体类\n"); + return report.toString(); + } + + long errorCount = results.values().stream() + .filter(ValidationResult::hasIssues) + .count(); + long successCount = results.size() - errorCount; + + report.append("验证统计: 总计 ").append(results.size()) + .append(" 个实体类,成功: ").append(successCount) + .append(",存在问题: ").append(errorCount).append("\n\n"); + + if (errorCount == 0) { + report.append("✅ 所有实体类映射正确\n"); + return report.toString(); + } + + report.append("发现 ").append(errorCount).append(" 个问题:\n\n"); + + results.forEach((className, result) -> { + if (result.hasIssues()) { + report.append("实体类: ").append(className) + .append(" -> 表: ").append(result.getTableName()).append("\n"); + + if (!result.getMissingInEntity().isEmpty()) { + report.append(" 数据库有但实体缺少: ").append(result.getMissingInEntity()).append("\n"); + } + + if (!result.getMissingInDb().isEmpty()) { + report.append(" 实体有但数据库缺少: ").append(result.getMissingInDb()).append("\n"); + } + + if (!result.getTypeMismatches().isEmpty()) { + report.append(" 类型不匹配: ").append(result.getTypeMismatches()).append("\n"); + } + + if (result.getError() != null) { + report.append(" 错误: ").append(result.getError()).append("\n"); + } + + report.append("\n"); + } + }); + + return report.toString(); + } + + + /** + * 获取验证统计信息 + */ + public Map getValidationStatistics() { + Map results = validateAllEntitiesInProject(); + + long total = results.size(); + long errorCount = results.values().stream() + .filter(ValidationResult::hasIssues) + .count(); + long successCount = total - errorCount; + + Map stats = new HashMap<>(); + stats.put("totalEntities", total); + stats.put("successCount", successCount); + stats.put("errorCount", errorCount); + stats.put("successRate", total > 0 ? (successCount * 100.0 / total) : 0); + stats.put("lastValidated", new Date()); + + return stats; + } + +} diff --git a/src/main/java/com/zhgd/xmgl/modules/validator/EntityFieldInfo.java b/src/main/java/com/zhgd/xmgl/modules/validator/EntityFieldInfo.java new file mode 100644 index 000000000..055033990 --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/validator/EntityFieldInfo.java @@ -0,0 +1,15 @@ +package com.zhgd.xmgl.modules.validator; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 实体字段信息类 + */ +@Data +@AllArgsConstructor +class EntityFieldInfo { + private String fieldName; + private String columnName; + private String fieldType; +} diff --git a/src/main/java/com/zhgd/xmgl/modules/validator/EntityValidatorController.java b/src/main/java/com/zhgd/xmgl/modules/validator/EntityValidatorController.java new file mode 100644 index 000000000..ede100d66 --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/validator/EntityValidatorController.java @@ -0,0 +1,62 @@ +package com.zhgd.xmgl.modules.validator; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +@RestController +@RequestMapping("/xmgl/entity-validator") +public class EntityValidatorController { + + @Autowired + private BulkEntityValidator validator; + + @GetMapping("/validate-all") + public Map validateAll() { + Map results = validator.validateAllEntitiesInProject(); + + long errorCount = results.values().stream() + .filter(ValidationResult::hasIssues) + .count(); + + Map response = new HashMap<>(); + response.put("totalEntities", results.size()); + response.put("errorCount", errorCount); + response.put("successCount", results.size() - errorCount); + response.put("results", results); + response.put("report", validator.generateDetailedReport()); + + return response; + } + + @PostMapping("/validate-package") + public Map validatePackage(@RequestParam String packageName) { + Map results = validator.validatePackageEntities(packageName); + + long errorCount = results.values().stream() + .filter(ValidationResult::hasIssues) + .count(); + + Map response = new HashMap<>(); + response.put("package", packageName); + response.put("totalEntities", results.size()); + response.put("errorCount", errorCount); + response.put("successCount", results.size() - errorCount); + response.put("results", results); + response.put("hasIssues", errorCount > 0); + + return response; + } + + @GetMapping("/status") + public Map getValidationStatus() { + return validator.getValidationStatistics(); + } + + @GetMapping("/report") + public String getDetailedReport() { + return validator.generateDetailedReport(); + } +} diff --git a/src/main/java/com/zhgd/xmgl/modules/validator/ValidationResult.java b/src/main/java/com/zhgd/xmgl/modules/validator/ValidationResult.java new file mode 100644 index 000000000..03fd04bec --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/validator/ValidationResult.java @@ -0,0 +1,40 @@ +package com.zhgd.xmgl.modules.validator; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Collections; +import java.util.List; + +/** + * 验证结果类 + */ +@Data +@AllArgsConstructor +class ValidationResult { + private String className; + private String tableName; + private List missingInEntity; + private List missingInDb; + private List typeMismatches; + private String error; + + public static ValidationResult of(String className, String tableName, + List missingInEntity, List missingInDb, + List typeMismatches) { + return new ValidationResult(className, tableName, missingInEntity, missingInDb, typeMismatches, null); + } + + public static ValidationResult error(String className, String error) { + return new ValidationResult(className, null, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), error); + } + + public boolean hasIssues() { + return error != null || + !missingInEntity.isEmpty() || + !missingInDb.isEmpty() || + !typeMismatches.isEmpty(); + } +} + diff --git a/src/main/java/com/zhgd/xmgl/modules/validator/ValidatorConfig.java b/src/main/java/com/zhgd/xmgl/modules/validator/ValidatorConfig.java new file mode 100644 index 000000000..3f740e988 --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/validator/ValidatorConfig.java @@ -0,0 +1,15 @@ +package com.zhgd.xmgl.modules.validator; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ValidatorConfig { + + @Bean + @ConditionalOnMissingBean + public BulkEntityValidator bulkEntityValidator() { + return new BulkEntityValidator(); + } +}