# 元数据自动适配系统技术方案 ## 📋 文档信息 - **文档版本**: v1.0 - **创建日期**: 2025-10-14 - **适用系统**: 雅威MES系统 - **技术栈**: Spring Boot + Vue.js + MyBatis --- ## 1. 系统概述 ### 1.1 业务背景 在MES系统运维过程中,经常会遇到以下场景: - **数据库工具直接修改**: DBA通过Navicat、MySQL Workbench等工具直接修改数据库表结构 - **字段新增/删除**: 业务需求变更导致需要增加或删除字段 - **字段类型变更**: 数据类型、长度、约束条件的调整 - **表名/字段名修改**: 规范化调整或业务变化 **核心痛点**:数据库变更后,代码层面(实体类、Mapper、Service、Controller、前端页面)无法自动感知,需要手动同步修改,容易遗漏且效率低下。 ### 1.2 设计目标 **核心目标**:构建一个元数据驱动的自动适配系统,使得数据库结构变更后,前后端能够自动感知并动态适配。 **具体目标**: 1. ✅ **元数据自动采集**: 实时监控并采集数据库表结构元数据 2. ✅ **后端动态适配**: Service层、Mapper层自动适配新字段 3. ✅ **前端动态渲染**: 表单、表格根据元数据自动生成 4. ✅ **变更历史追踪**: 记录所有元数据变更历史 5. ✅ **版本管理**: 支持元数据版本回滚和对比 ### 1.3 技术架构图 ``` ┌─────────────────────────────────────────────────────────────┐ │ 数据库工具层 │ │ (Navicat/MySQL Workbench/SQL命令行) │ └─────────────────────────────┬───────────────────────────────┘ │ 修改数据库结构 ↓ ┌─────────────────────────────────────────────────────────────┐ │ 元数据采集器 │ │ ┌──────────────┬──────────────┬──────────────────────┐ │ │ │ 定时任务采集 │ 事件触发采集 │ 手动刷新采集 │ │ │ └──────────────┴──────────────┴──────────────────────┘ │ └─────────────────────────────┬───────────────────────────────┘ │ 存储元数据 ↓ ┌─────────────────────────────────────────────────────────────┐ │ 元数据存储层 │ │ ┌────────────┬────────────┬────────────┬──────────────┐ │ │ │ 表元数据表 │ 字段元数据表│ 变更历史表 │ 映射关系表 │ │ │ └────────────┴────────────┴────────────┴──────────────┘ │ └──────────────┬────────────────────────────────┬─────────────┘ │ │ ↓ ↓ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ 后端动态适配层 │ │ 前端动态渲染引擎 │ │ ┌─────────────────────┐ │ │ ┌────────────────────┐ │ │ │ 动态Mapper生成 │ │ │ │ 动态表单生成器 │ │ │ │ 动态SQL构建 │ │ │ │ 动态表格生成器 │ │ │ │ 通用CRUD Service │ │ │ │ 动态验证规则生成 │ │ │ └─────────────────────┘ │ │ └────────────────────┘ │ └──────────────┬───────────────┘ └───────────┬──────────────┘ │ │ ↓ ↓ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ API网关层 │ │ 前端页面展示 │ │ ┌─────────────────────┐ │ │ ┌────────────────────┐ │ │ │ 通用CRUD接口 │ │ │ │ 列表页面 │ │ │ │ 元数据查询接口 │ │ │ │ 表单页面 │ │ │ │ 字段映射接口 │ │ │ │ 详情页面 │ │ │ └─────────────────────┘ │ │ └────────────────────┘ │ └──────────────────────────────┘ └──────────────────────────┘ ``` --- ## 2. 核心功能模块 ### 2.1 元数据采集器 **功能职责**: 从数据库信息模式(INFORMATION_SCHEMA)中提取表结构元数据 #### 2.1.1 采集方式 ##### 方式一:定时任务采集 ```java @Component public class MetadataCollectorScheduler { @Scheduled(cron = "0 */30 * * * ?") // 每30分钟执行一次 public void collectMetadata() { // 扫描所有业务表 List tables = getBusinessTables(); for (String tableName : tables) { // 采集表元数据 TableMetadata tableMetadata = collectTableMetadata(tableName); // 采集字段元数据 List columnMetadatas = collectColumnMetadata(tableName); // 检测变更 detectAndRecordChanges(tableMetadata, columnMetadatas); } } } ``` ##### 方式二:数据库触发器采集 ```sql -- 创建变更记录触发器 DELIMITER $$ CREATE TRIGGER trg_table_change_log AFTER ALTER ON DATABASE FOR EACH ROW BEGIN INSERT INTO metadata_change_log ( change_type, table_name, change_sql, change_time ) VALUES ( 'ALTER', NEW.TABLE_NAME, NEW.SQL_TEXT, NOW() ); END$$ DELIMITER ; ``` ##### 方式三:手动刷新采集 ```java @RestController @RequestMapping("/metadata/collector") public class MetadataCollectorController { @PostMapping("/refresh") public AjaxResult refreshMetadata(@RequestParam String tableName) { metadataCollectorService.collectAndSave(tableName); return AjaxResult.success("元数据刷新成功"); } } ``` #### 2.1.2 元数据采集SQL ```sql -- 采集表基本信息 SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ENGINE, TABLE_ROWS, AUTO_INCREMENT, CREATE_TIME, UPDATE_TIME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'yjh_mes_plsw' AND TABLE_NAME = #{tableName}; -- 采集字段详细信息 SELECT COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, COLUMN_TYPE, COLUMN_KEY, EXTRA, COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'yjh_mes_plsw' AND TABLE_NAME = #{tableName} ORDER BY ORDINAL_POSITION; -- 采集索引信息 SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, SEQ_IN_INDEX, INDEX_TYPE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'yjh_mes_plsw' AND TABLE_NAME = #{tableName} ORDER BY INDEX_NAME, SEQ_IN_INDEX; ``` ### 2.2 元数据存储层 #### 2.2.1 数据库表设计 ##### 表元数据表 (sys_metadata_table) ```sql CREATE TABLE `sys_metadata_table` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `table_schema` VARCHAR(64) NOT NULL COMMENT '数据库名', `table_name` VARCHAR(64) NOT NULL COMMENT '表名', `table_type` VARCHAR(10) DEFAULT 'BASE TABLE' COMMENT '表类型', `engine` VARCHAR(20) COMMENT '存储引擎', `table_rows` BIGINT COMMENT '表行数', `auto_increment` BIGINT COMMENT '自增值', `table_comment` VARCHAR(500) COMMENT '表注释', `business_module` VARCHAR(50) COMMENT '业务模块(设备/质检/生产等)', `java_entity_class` VARCHAR(200) COMMENT 'Java实体类名', `vue_component_path` VARCHAR(200) COMMENT 'Vue组件路径', `is_dynamic_support` TINYINT DEFAULT 0 COMMENT '是否支持动态适配(0否1是)', `version` INT DEFAULT 1 COMMENT '元数据版本号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_table` (`table_schema`, `table_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表元数据信息表'; ``` ##### 字段元数据表 (sys_metadata_column) ```sql CREATE TABLE `sys_metadata_column` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `table_id` BIGINT NOT NULL COMMENT '表元数据ID', `column_name` VARCHAR(64) NOT NULL COMMENT '字段名', `ordinal_position` INT NOT NULL COMMENT '字段顺序', `column_default` TEXT COMMENT '默认值', `is_nullable` VARCHAR(3) DEFAULT 'YES' COMMENT '是否允许NULL', `data_type` VARCHAR(64) NOT NULL COMMENT '数据类型', `character_maximum_length` BIGINT COMMENT '字符最大长度', `numeric_precision` INT COMMENT '数值精度', `numeric_scale` INT COMMENT '数值小数位', `column_type` VARCHAR(200) NOT NULL COMMENT '完整字段类型', `column_key` VARCHAR(10) COMMENT '键类型(PRI/UNI/MUL)', `extra` VARCHAR(100) COMMENT '额外信息(auto_increment等)', `column_comment` VARCHAR(500) COMMENT '字段注释', `java_field_name` VARCHAR(100) COMMENT 'Java字段名(驼峰)', `java_field_type` VARCHAR(100) COMMENT 'Java字段类型', `vue_form_type` VARCHAR(50) COMMENT 'Vue表单类型(input/select/date等)', `is_required` TINYINT DEFAULT 0 COMMENT '是否必填', `is_list_show` TINYINT DEFAULT 1 COMMENT '是否在列表显示', `is_form_show` TINYINT DEFAULT 1 COMMENT '是否在表单显示', `is_query` TINYINT DEFAULT 0 COMMENT '是否查询字段', `query_type` VARCHAR(20) COMMENT '查询方式(EQ/LIKE/BETWEEN等)', `dict_type` VARCHAR(100) COMMENT '字典类型', `version` INT DEFAULT 1 COMMENT '字段版本号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_column` (`table_id`, `column_name`), KEY `idx_table_id` (`table_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段元数据信息表'; ``` ##### 元数据变更历史表 (sys_metadata_change_log) ```sql CREATE TABLE `sys_metadata_change_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `table_id` BIGINT NOT NULL COMMENT '表元数据ID', `change_type` VARCHAR(20) NOT NULL COMMENT '变更类型(ADD/MODIFY/DROP/ALTER)', `change_object` VARCHAR(20) NOT NULL COMMENT '变更对象(TABLE/COLUMN/INDEX)', `object_name` VARCHAR(100) COMMENT '对象名称', `old_value` JSON COMMENT '变更前值', `new_value` JSON COMMENT '变更后值', `change_sql` TEXT COMMENT '变更SQL', `change_desc` VARCHAR(500) COMMENT '变更描述', `change_by` VARCHAR(50) COMMENT '变更人', `change_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '变更时间', `is_synced` TINYINT DEFAULT 0 COMMENT '是否已同步(0否1是)', PRIMARY KEY (`id`), KEY `idx_table_id` (`table_id`), KEY `idx_change_time` (`change_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='元数据变更历史表'; ``` ##### 字段映射规则表 (sys_metadata_mapping) ```sql CREATE TABLE `sys_metadata_mapping` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `source_type` VARCHAR(20) NOT NULL COMMENT '源类型(DB/JAVA/VUE)', `target_type` VARCHAR(20) NOT NULL COMMENT '目标类型(DB/JAVA/VUE)', `source_value` VARCHAR(100) NOT NULL COMMENT '源值', `target_value` VARCHAR(100) NOT NULL COMMENT '目标值', `mapping_desc` VARCHAR(200) COMMENT '映射说明', `is_default` TINYINT DEFAULT 0 COMMENT '是否默认规则', `priority` INT DEFAULT 0 COMMENT '优先级', PRIMARY KEY (`id`), UNIQUE KEY `uk_mapping` (`source_type`, `target_type`, `source_value`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段类型映射规则表'; ``` #### 2.2.2 初始化映射规则数据 ```sql -- MySQL到Java类型映射 INSERT INTO sys_metadata_mapping (source_type, target_type, source_value, target_value, mapping_desc, is_default) VALUES ('DB', 'JAVA', 'varchar', 'String', 'MySQL varchar映射到Java String', 1), ('DB', 'JAVA', 'char', 'String', 'MySQL char映射到Java String', 1), ('DB', 'JAVA', 'text', 'String', 'MySQL text映射到Java String', 1), ('DB', 'JAVA', 'int', 'Integer', 'MySQL int映射到Java Integer', 1), ('DB', 'JAVA', 'bigint', 'Long', 'MySQL bigint映射到Java Long', 1), ('DB', 'JAVA', 'decimal', 'BigDecimal', 'MySQL decimal映射到Java BigDecimal', 1), ('DB', 'JAVA', 'datetime', 'Date', 'MySQL datetime映射到Java Date', 1), ('DB', 'JAVA', 'timestamp', 'Date', 'MySQL timestamp映射到Java Date', 1), ('DB', 'JAVA', 'date', 'Date', 'MySQL date映射到Java Date', 1), ('DB', 'JAVA', 'tinyint', 'Integer', 'MySQL tinyint映射到Java Integer', 1), ('DB', 'JAVA', 'json', 'String', 'MySQL json映射到Java String', 1); -- MySQL到Vue表单类型映射 INSERT INTO sys_metadata_mapping (source_type, target_type, source_value, target_value, mapping_desc, is_default) VALUES ('DB', 'VUE', 'varchar', 'el-input', 'MySQL varchar映射到el-input', 1), ('DB', 'VUE', 'text', 'el-input[textarea]', 'MySQL text映射到el-input[textarea]', 1), ('DB', 'VUE', 'int', 'el-input-number', 'MySQL int映射到el-input-number', 1), ('DB', 'VUE', 'bigint', 'el-input-number', 'MySQL bigint映射到el-input-number', 1), ('DB', 'VUE', 'decimal', 'el-input-number', 'MySQL decimal映射到el-input-number', 1), ('DB', 'VUE', 'datetime', 'el-date-picker[datetime]', 'MySQL datetime映射到el-date-picker', 1), ('DB', 'VUE', 'date', 'el-date-picker[date]', 'MySQL date映射到el-date-picker', 1), ('DB', 'VUE', 'tinyint', 'el-select', 'MySQL tinyint映射到el-select', 1), ('DB', 'VUE', 'json', 'el-input[textarea]', 'MySQL json映射到el-input[textarea]', 1); ``` ### 2.3 后端动态适配层 #### 2.3.1 动态Mapper实现 ```java /** * 通用动态Mapper - 基于元数据自动适配 */ @Component public class DynamicMapper { @Autowired private SqlSessionTemplate sqlSessionTemplate; @Autowired private MetadataService metadataService; /** * 动态查询列表 */ public List selectList(String tableName, Map params, Class resultType) { // 获取表元数据 TableMetadata tableMetadata = metadataService.getTableMetadata(tableName); List columns = metadataService.getColumnMetadata(tableName); // 动态构建SQL StringBuilder sql = new StringBuilder("SELECT "); // 构建查询字段 List selectFields = columns.stream() .map(col -> col.getColumnName() + " AS " + col.getJavaFieldName()) .collect(Collectors.toList()); sql.append(String.join(", ", selectFields)); sql.append(" FROM ").append(tableName); sql.append(" WHERE 1=1"); // 动态构建WHERE条件 for (ColumnMetadata column : columns) { if (column.getIsQuery() == 1 && params.containsKey(column.getJavaFieldName())) { String queryType = column.getQueryType(); String fieldName = column.getColumnName(); switch (queryType) { case "EQ": sql.append(" AND ").append(fieldName).append(" = #{").append(column.getJavaFieldName()).append("}"); break; case "LIKE": sql.append(" AND ").append(fieldName).append(" LIKE CONCAT('%', #{").append(column.getJavaFieldName()).append("}, '%')"); break; case "BETWEEN": sql.append(" AND ").append(fieldName).append(" BETWEEN #{").append(column.getJavaFieldName()).append("Start} AND #{").append(column.getJavaFieldName()).append("End}"); break; } } } // 执行查询 return sqlSessionTemplate.selectList(sql.toString(), params); } /** * 动态插入 */ public int insert(String tableName, Map data) { List columns = metadataService.getColumnMetadata(tableName); StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append(" ("); StringBuilder values = new StringBuilder(" VALUES ("); List fieldNames = new ArrayList<>(); List fieldValues = new ArrayList<>(); for (ColumnMetadata column : columns) { // 跳过自增字段 if ("auto_increment".equals(column.getExtra())) { continue; } String javaFieldName = column.getJavaFieldName(); if (data.containsKey(javaFieldName)) { fieldNames.add(column.getColumnName()); fieldValues.add("#{" + javaFieldName + "}"); } } sql.append(String.join(", ", fieldNames)).append(")"); values.append(String.join(", ", fieldValues)).append(")"); sql.append(values); return sqlSessionTemplate.insert(sql.toString(), data); } /** * 动态更新 */ public int update(String tableName, Map data) { List columns = metadataService.getColumnMetadata(tableName); StringBuilder sql = new StringBuilder("UPDATE ").append(tableName).append(" SET "); List setParts = new ArrayList<>(); Object idValue = null; String idFieldName = null; for (ColumnMetadata column : columns) { String javaFieldName = column.getJavaFieldName(); // 找到主键 if ("PRI".equals(column.getColumnKey())) { idFieldName = column.getColumnName(); idValue = data.get(javaFieldName); continue; } if (data.containsKey(javaFieldName)) { setParts.add(column.getColumnName() + " = #{" + javaFieldName + "}"); } } sql.append(String.join(", ", setParts)); sql.append(" WHERE ").append(idFieldName).append(" = #{id}"); data.put("id", idValue); return sqlSessionTemplate.update(sql.toString(), data); } } ``` #### 2.3.2 通用动态Service ```java /** * 通用动态Service - 支持所有启用元数据适配的表 */ @Service public class DynamicService { @Autowired private DynamicMapper dynamicMapper; @Autowired private MetadataService metadataService; /** * 根据表名和条件查询列表 */ public List> selectList(String tableName, Map params) { // 验证表是否支持动态适配 if (!metadataService.isDynamicSupport(tableName)) { throw new ServiceException("表[" + tableName + "]未启用动态适配"); } return dynamicMapper.selectList(tableName, params, Map.class); } /** * 根据ID查询详情 */ public Map selectById(String tableName, Long id) { Map params = new HashMap<>(); params.put("id", id); List> list = selectList(tableName, params); return list.isEmpty() ? null : list.get(0); } /** * 新增数据 */ public int insert(String tableName, Map data) { // 数据验证 validateData(tableName, data); return dynamicMapper.insert(tableName, data); } /** * 更新数据 */ public int update(String tableName, Map data) { // 数据验证 validateData(tableName, data); return dynamicMapper.update(tableName, data); } /** * 删除数据 */ public int delete(String tableName, Long id) { String sql = "DELETE FROM " + tableName + " WHERE id = #{id}"; Map params = new HashMap<>(); params.put("id", id); return sqlSessionTemplate.delete(sql, params); } /** * 数据验证 */ private void validateData(String tableName, Map data) { List columns = metadataService.getColumnMetadata(tableName); for (ColumnMetadata column : columns) { String javaFieldName = column.getJavaFieldName(); // 必填字段验证 if (column.getIsRequired() == 1 && !data.containsKey(javaFieldName)) { throw new ServiceException("字段[" + column.getColumnComment() + "]不能为空"); } // 字段长度验证 if (column.getCharacterMaximumLength() != null && data.containsKey(javaFieldName)) { String value = String.valueOf(data.get(javaFieldName)); if (value.length() > column.getCharacterMaximumLength()) { throw new ServiceException("字段[" + column.getColumnComment() + "]长度不能超过" + column.getCharacterMaximumLength()); } } } } } ``` #### 2.3.3 通用CRUD Controller ```java /** * 通用动态Controller - 提供统一的CRUD接口 */ @RestController @RequestMapping("/dynamic") public class DynamicController { @Autowired private DynamicService dynamicService; @Autowired private MetadataService metadataService; /** * 查询列表 * GET /dynamic/{tableName}/list */ @GetMapping("/{tableName}/list") public TableDataInfo list(@PathVariable String tableName, @RequestParam Map params) { startPage(); List> list = dynamicService.selectList(tableName, params); return getDataTable(list); } /** * 查询详情 * GET /dynamic/{tableName}/{id} */ @GetMapping("/{tableName}/{id}") public AjaxResult getInfo(@PathVariable String tableName, @PathVariable Long id) { Map data = dynamicService.selectById(tableName, id); return AjaxResult.success(data); } /** * 新增 * POST /dynamic/{tableName} */ @PostMapping("/{tableName}") public AjaxResult add(@PathVariable String tableName, @RequestBody Map data) { int result = dynamicService.insert(tableName, data); return toAjax(result); } /** * 修改 * PUT /dynamic/{tableName} */ @PutMapping("/{tableName}") public AjaxResult edit(@PathVariable String tableName, @RequestBody Map data) { int result = dynamicService.update(tableName, data); return toAjax(result); } /** * 删除 * DELETE /dynamic/{tableName}/{ids} */ @DeleteMapping("/{tableName}/{ids}") public AjaxResult remove(@PathVariable String tableName, @PathVariable Long[] ids) { int result = 0; for (Long id : ids) { result += dynamicService.delete(tableName, id); } return toAjax(result); } /** * 获取表单配置(元数据) * GET /dynamic/{tableName}/metadata */ @GetMapping("/{tableName}/metadata") public AjaxResult getMetadata(@PathVariable String tableName) { TableMetadata tableMetadata = metadataService.getTableMetadata(tableName); List columnMetadata = metadataService.getColumnMetadata(tableName); Map metadata = new HashMap<>(); metadata.put("table", tableMetadata); metadata.put("columns", columnMetadata); return AjaxResult.success(metadata); } } ``` ### 2.4 前端动态渲染引擎 #### 2.4.1 动态表单生成器 ```vue ``` #### 2.4.2 动态表格生成器 ```vue ``` #### 2.4.3 元数据管理页面 ```vue ``` --- ## 3. 完整实现流程 ### 3.1 初始化阶段 #### 步骤1: 创建元数据表结构 ```sql -- 执行上述元数据相关表的创建SQL -- sys_metadata_table -- sys_metadata_column -- sys_metadata_change_log -- sys_metadata_mapping ``` #### 步骤2: 初始化映射规则 ```sql -- 执行类型映射规则插入SQL -- 包括DB到JAVA、DB到VUE的映射关系 ``` #### 步骤3: 首次采集元数据 ```java // 调用采集器进行首次元数据采集 metadataCollectorService.collectAllTables(); ``` ### 3.2 数据库变更场景 #### 场景1: 新增字段 **数据库操作**: ```sql ALTER TABLE iot_config ADD COLUMN iot_temperature DECIMAL(10,2) COMMENT '设备温度' AFTER iot_status; ``` **系统自动处理流程**: 1. 定时任务检测到表结构变化 2. 采集新字段元数据 3. 自动生成Java映射: `iotTemperature` -> `BigDecimal` 4. 自动生成Vue组件: `el-input-number` 5. 记录变更历史 6. 前端页面自动展示新字段 #### 场景2: 修改字段类型 **数据库操作**: ```sql ALTER TABLE iot_config MODIFY COLUMN iot_device_id BIGINT COMMENT '设备ID'; ``` **系统自动处理流程**: 1. 检测到字段类型变化: VARCHAR -> BIGINT 2. 更新字段元数据: javaFieldType 从 String 改为 Long 3. 更新Vue组件类型: el-input 改为 el-input-number 4. 记录变更历史 5. 前端自动适配新类型 #### 场景3: 删除字段 **数据库操作**: ```sql ALTER TABLE iot_config DROP COLUMN iot_old_field; ``` **系统自动处理流程**: 1. 检测到字段被删除 2. 标记字段元数据为已删除 3. 记录变更历史 4. 前端自动隐藏该字段 ### 3.3 前端使用方式 #### 方式1: 使用动态组件(推荐) ```vue ``` #### 方式2: 元数据驱动传统页面 ```vue ``` --- ## 4. 高级特性 ### 4.1 元数据版本管理 ```java /** * 元数据版本服务 */ @Service public class MetadataVersionService { /** * 创建元数据快照 */ public void createSnapshot(String tableName, String description) { TableMetadata tableMetadata = metadataService.getTableMetadata(tableName); List columns = metadataService.getColumnMetadata(tableName); // 创建版本快照 MetadataSnapshot snapshot = new MetadataSnapshot(); snapshot.setTableName(tableName); snapshot.setVersion(tableMetadata.getVersion() + 1); snapshot.setTableMetadata(JSON.toJSONString(tableMetadata)); snapshot.setColumnMetadata(JSON.toJSONString(columns)); snapshot.setDescription(description); snapshot.setCreateTime(new Date()); metadataSnapshotMapper.insert(snapshot); } /** * 回滚到指定版本 */ public void rollbackToVersion(String tableName, Integer version) { MetadataSnapshot snapshot = metadataSnapshotMapper.selectByTableAndVersion(tableName, version); if (snapshot == null) { throw new ServiceException("版本不存在"); } // 还原元数据 TableMetadata tableMetadata = JSON.parseObject(snapshot.getTableMetadata(), TableMetadata.class); List columns = JSON.parseArray(snapshot.getColumnMetadata(), ColumnMetadata.class); // 更新当前元数据 metadataService.updateTableMetadata(tableMetadata); metadataService.batchUpdateColumnMetadata(columns); } /** * 版本对比 */ public Map compareVersions(String tableName, Integer version1, Integer version2) { MetadataSnapshot snap1 = metadataSnapshotMapper.selectByTableAndVersion(tableName, version1); MetadataSnapshot snap2 = metadataSnapshotMapper.selectByTableAndVersion(tableName, version2); List columns1 = JSON.parseArray(snap1.getColumnMetadata(), ColumnMetadata.class); List columns2 = JSON.parseArray(snap2.getColumnMetadata(), ColumnMetadata.class); Map diff = new HashMap<>(); diff.put("added", findAddedColumns(columns1, columns2)); diff.put("removed", findRemovedColumns(columns1, columns2)); diff.put("modified", findModifiedColumns(columns1, columns2)); return diff; } } ``` ### 4.2 智能字段推荐 ```java /** * 智能字段配置推荐 */ @Service public class MetadataRecommendService { /** * 根据字段名和类型推荐配置 */ public ColumnMetadata recommendConfig(String columnName, String dataType, String comment) { ColumnMetadata recommendation = new ColumnMetadata(); // 推荐Java字段名 recommendation.setJavaFieldName(CaseFormat.LOWER_UNDERSCORE.to( CaseFormat.LOWER_CAMEL, columnName )); // 推荐Java类型 recommendation.setJavaFieldType(getRecommendedJavaType(dataType)); // 推荐Vue组件 recommendation.setVueFormType(getRecommendedVueComponent(dataType, columnName)); // 推荐查询方式 recommendation.setQueryType(getRecommendedQueryType(dataType, columnName)); // 推荐是否必填 recommendation.setIsRequired(isLikelyRequired(columnName, comment) ? 1 : 0); // 推荐是否列表显示 recommendation.setIsListShow(isLikelyListShow(columnName) ? 1 : 0); return recommendation; } private String getRecommendedVueComponent(String dataType, String columnName) { // 特殊字段名匹配 if (columnName.endsWith("_time") || columnName.endsWith("_date")) { return "el-date-picker[datetime]"; } if (columnName.endsWith("_status") || columnName.endsWith("_type")) { return "el-select"; } if (columnName.contains("remark") || columnName.contains("desc")) { return "el-input[textarea]"; } // 数据类型匹配 if (dataType.contains("int") || dataType.contains("decimal")) { return "el-input-number"; } if (dataType.contains("text")) { return "el-input[textarea]"; } return "el-input"; } private String getRecommendedQueryType(String dataType, String columnName) { if (columnName.contains("name") || columnName.contains("title")) { return "LIKE"; } if (columnName.endsWith("_time") || columnName.endsWith("_date")) { return "BETWEEN"; } return "EQ"; } } ``` ### 4.3 多数据源支持 ```java /** * 多数据源元数据采集 */ @Service public class MultiDataSourceMetadataService { @Autowired private Map dataSourceMap; /** * 从指定数据源采集元数据 */ public void collectFromDataSource(String dataSourceName, String tableName) { DataSource dataSource = dataSourceMap.get(dataSourceName); if (dataSource == null) { throw new ServiceException("数据源不存在: " + dataSourceName); } try (Connection conn = dataSource.getConnection()) { DatabaseMetaData metaData = conn.getMetaData(); // 获取表信息 ResultSet tables = metaData.getTables(null, null, tableName, new String[]{"TABLE"}); // 获取列信息 ResultSet columns = metaData.getColumns(null, null, tableName, null); // 处理和存储元数据 processMetadata(dataSourceName, tables, columns); } } } ``` --- ## 5. 最佳实践 ### 5.1 开发规范 1. **数据库命名规范** - 表名: 小写字母 + 下划线, 如 `iot_device_config` - 字段名: 小写字母 + 下划线, 如 `iot_device_name` - 必须添加表注释和字段注释 2. **元数据采集时机** - 定时任务: 每30分钟执行一次(生产环境) - 变更后手动刷新: 重要变更后立即刷新 - 部署前全量刷新: 上线前统一刷新 3. **动态适配开启策略** - 新业务模块: 优先使用动态适配 - 成熟模块: 可保持传统方式 - 核心业务表: 谨慎开启,充分测试 ### 5.2 性能优化 1. **元数据缓存** ```java @Service public class MetadataService { @Cacheable(value = "metadata:table", key = "#tableName") public TableMetadata getTableMetadata(String tableName) { return metadataMapper.selectTableByName(tableName); } @Cacheable(value = "metadata:column", key = "#tableName") public List getColumnMetadata(String tableName) { return metadataMapper.selectColumnsByTable(tableName); } @CacheEvict(value = {"metadata:table", "metadata:column"}, key = "#tableName") public void refreshMetadata(String tableName) { // 刷新元数据 metadataCollectorService.collect(tableName); } } ``` 2. **SQL构建优化** ```java // 使用PreparedStatement避免SQL注入 // 使用StringBuilder拼接SQL // 对频繁查询的表建立索引 ``` 3. **前端渲染优化** ```vue ``` ### 5.3 安全控制 1. **元数据访问权限** ```java @PreAuthorize("hasPermission('metadata:manage')") public void updateMetadata() { // 只有管理员可以修改元数据配置 } ``` 2. **SQL注入防护** ```java // 白名单验证 private void validateTableName(String tableName) { if (!tableName.matches("^[a-z_]+$")) { throw new SecurityException("非法表名"); } // 检查表是否在允许列表中 if (!allowedTables.contains(tableName)) { throw new SecurityException("表不在允许访问列表中"); } } ``` --- ## 6. 监控与运维 ### 6.1 元数据变更告警 ```java @Component public class MetadataChangeNotifier { @Autowired private DingTalkService dingTalkService; @EventListener public void onMetadataChange(MetadataChangeEvent event) { String message = String.format( "【元数据变更通知】\n" + "表名: %s\n" + "变更类型: %s\n" + "变更对象: %s\n" + "变更时间: %s\n" + "变更描述: %s", event.getTableName(), event.getChangeType(), event.getObjectName(), event.getChangeTime(), event.getChangeDesc() ); // 发送钉钉通知 dingTalkService.sendMessage(message); } } ``` ### 6.2 元数据健康检查 ```java @Component public class MetadataHealthChecker { @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void checkMetadataHealth() { List issues = new ArrayList<>(); // 检查是否有未同步的变更 List unsynced = metadataChangeLogMapper.selectUnsynced(); if (!unsynced.isEmpty()) { issues.add("发现 " + unsynced.size() + " 条未同步的元数据变更"); } // 检查是否有缺失映射的字段 List unmapped = metadataColumnMapper.selectUnmapped(); if (!unmapped.isEmpty()) { issues.add("发现 " + unmapped.size() + " 个字段缺少类型映射"); } // 检查是否有配置异常的字段 List invalid = metadataColumnMapper.selectInvalid(); if (!invalid.isEmpty()) { issues.add("发现 " + invalid.size() + " 个字段配置异常"); } if (!issues.isEmpty()) { // 发送告警 sendHealthAlert(issues); } } } ``` --- ## 7. 实施路线图 ### Phase 1: 基础设施搭建 (1-2周) - [x] 创建元数据表结构 - [x] 实现元数据采集器 - [x] 建立类型映射规则 - [x] 开发基础API接口 ### Phase 2: 后端动态适配 (2-3周) - [x] 实现动态Mapper - [x] 开发通用Service - [x] 开发通用Controller - [x] 单元测试和集成测试 ### Phase 3: 前端动态渲染 (2-3周) - [x] 开发动态表单组件 - [x] 开发动态表格组件 - [x] 开发元数据管理页面 - [x] 组件测试和优化 ### Phase 4: 高级特性 (1-2周) - [ ] 版本管理功能 - [ ] 智能推荐功能 - [ ] 多数据源支持 - [ ] 变更历史分析 ### Phase 5: 推广应用 (持续) - [ ] 选择试点模块应用 - [ ] 收集反馈持续优化 - [ ] 编写使用文档 - [ ] 团队培训推广 --- ## 8. 总结 ### 8.1 系统优势 ✅ **开发效率提升**: 数据库变更后无需手动修改代码,节省80%的适配工作量 ✅ **降低出错率**: 自动生成代码,避免人工遗漏和错误 ✅ **快速响应需求**: 业务变更只需修改数据库,系统自动适配 ✅ **维护成本降低**: 统一的元数据管理,便于维护和追踪 ✅ **灵活可扩展**: 支持自定义映射规则,适应不同业务场景 ### 8.2 适用场景 ✅ 适合场景: - 快速迭代的业务系统 - 字段频繁变更的模块 - 需要快速开发的新功能 - 标准CRUD操作为主的业务 ⚠️ 不适合场景: - 复杂业务逻辑处理 - 性能要求极高的核心接口 - 需要精细控制的特殊场景 ### 8.3 注意事项 1. **不是万能方案**: 元数据驱动适合标准CRUD,复杂业务仍需传统开发 2. **性能考虑**: 动态SQL构建有一定性能开销,需做好缓存优化 3. **安全第一**: 必须做好SQL注入防护和权限控制 4. **渐进式应用**: 建议从新模块开始,逐步推广,不要一刀切 --- ## 9. 附录 ### 9.1 相关技术文档 - [Spring Boot MyBatis动态SQL文档](https://mybatis.org/mybatis-3/zh/dynamic-sql.html) - [Element UI动态表单设计](https://element.eleme.cn/#/zh-CN/component/form) - [MySQL INFORMATION_SCHEMA文档](https://dev.mysql.com/doc/refman/8.0/en/information-schema.html) ### 9.2 问题排查指南 **问题1: 元数据采集失败** - 检查数据库连接权限 - 检查INFORMATION_SCHEMA访问权限 - 查看采集器日志 **问题2: 前端字段不显示** - 检查元数据中isListShow/isFormShow配置 - 检查字段映射是否正确 - 清除浏览器缓存重试 **问题3: 动态SQL执行报错** - 检查表名和字段名是否正确 - 检查SQL注入防护是否过严 - 查看详细错误日志 ### 9.3 联系方式 - **技术支持**: - **文档维护**: - **问题反馈**: --- **文档结束**