Files
MES/yawei-mes/doc/元数据自动适配系统技术方案.md

1950 lines
60 KiB
Markdown
Raw Normal View History

2026-04-02 10:38:23 +08:00
# 元数据自动适配系统技术方案
## 📋 文档信息
- **文档版本**: 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<String> tables = getBusinessTables();
for (String tableName : tables) {
// 采集表元数据
TableMetadata tableMetadata = collectTableMetadata(tableName);
// 采集字段元数据
List<ColumnMetadata> 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 <T> List<T> selectList(String tableName, Map<String, Object> params, Class<T> resultType) {
// 获取表元数据
TableMetadata tableMetadata = metadataService.getTableMetadata(tableName);
List<ColumnMetadata> columns = metadataService.getColumnMetadata(tableName);
// 动态构建SQL
StringBuilder sql = new StringBuilder("SELECT ");
// 构建查询字段
List<String> 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<String, Object> data) {
List<ColumnMetadata> columns = metadataService.getColumnMetadata(tableName);
StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append(" (");
StringBuilder values = new StringBuilder(" VALUES (");
List<String> fieldNames = new ArrayList<>();
List<String> 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<String, Object> data) {
List<ColumnMetadata> columns = metadataService.getColumnMetadata(tableName);
StringBuilder sql = new StringBuilder("UPDATE ").append(tableName).append(" SET ");
List<String> 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<Map<String, Object>> selectList(String tableName, Map<String, Object> params) {
// 验证表是否支持动态适配
if (!metadataService.isDynamicSupport(tableName)) {
throw new ServiceException("表[" + tableName + "]未启用动态适配");
}
return dynamicMapper.selectList(tableName, params, Map.class);
}
/**
* 根据ID查询详情
*/
public Map<String, Object> selectById(String tableName, Long id) {
Map<String, Object> params = new HashMap<>();
params.put("id", id);
List<Map<String, Object>> list = selectList(tableName, params);
return list.isEmpty() ? null : list.get(0);
}
/**
* 新增数据
*/
public int insert(String tableName, Map<String, Object> data) {
// 数据验证
validateData(tableName, data);
return dynamicMapper.insert(tableName, data);
}
/**
* 更新数据
*/
public int update(String tableName, Map<String, Object> 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<String, Object> params = new HashMap<>();
params.put("id", id);
return sqlSessionTemplate.delete(sql, params);
}
/**
* 数据验证
*/
private void validateData(String tableName, Map<String, Object> data) {
List<ColumnMetadata> 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<String, Object> params) {
startPage();
List<Map<String, Object>> 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<String, Object> data = dynamicService.selectById(tableName, id);
return AjaxResult.success(data);
}
/**
* 新增
* POST /dynamic/{tableName}
*/
@PostMapping("/{tableName}")
public AjaxResult add(@PathVariable String tableName, @RequestBody Map<String, Object> data) {
int result = dynamicService.insert(tableName, data);
return toAjax(result);
}
/**
* 修改
* PUT /dynamic/{tableName}
*/
@PutMapping("/{tableName}")
public AjaxResult edit(@PathVariable String tableName, @RequestBody Map<String, Object> 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> columnMetadata = metadataService.getColumnMetadata(tableName);
Map<String, Object> metadata = new HashMap<>();
metadata.put("table", tableMetadata);
metadata.put("columns", columnMetadata);
return AjaxResult.success(metadata);
}
}
```
### 2.4 前端动态渲染引擎
#### 2.4.1 动态表单生成器
```vue
<!-- DynamicForm.vue - 动态表单组件 -->
<template>
<el-form ref="dynamicForm" :model="formData" :rules="formRules" label-width="120px">
<el-row :gutter="20">
<el-col
v-for="column in formColumns"
:key="column.columnName"
:span="column.formSpan || 12"
>
<el-form-item :label="column.columnComment" :prop="column.javaFieldName">
<!-- 文本输入框 -->
<el-input
v-if="column.vueFormType === 'el-input'"
v-model="formData[column.javaFieldName]"
:placeholder="'请输入' + column.columnComment"
:maxlength="column.characterMaximumLength"
/>
<!-- 文本域 -->
<el-input
v-else-if="column.vueFormType === 'el-input[textarea]'"
v-model="formData[column.javaFieldName]"
type="textarea"
:rows="3"
:placeholder="'请输入' + column.columnComment"
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if="column.vueFormType === 'el-input-number'"
v-model="formData[column.javaFieldName]"
:placeholder="'请输入' + column.columnComment"
:precision="column.numericScale"
/>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="column.vueFormType === 'el-date-picker[date]'"
v-model="formData[column.javaFieldName]"
type="date"
value-format="yyyy-MM-dd"
:placeholder="'请选择' + column.columnComment"
/>
<!-- 日期时间选择器 -->
<el-date-picker
v-else-if="column.vueFormType === 'el-date-picker[datetime]'"
v-model="formData[column.javaFieldName]"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
:placeholder="'请选择' + column.columnComment"
/>
<!-- 下拉选择器 -->
<el-select
v-else-if="column.vueFormType === 'el-select'"
v-model="formData[column.javaFieldName]"
:placeholder="'请选择' + column.columnComment"
>
<el-option
v-for="dict in getDictOptions(column.dictType)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<!-- 开关 -->
<el-switch
v-else-if="column.vueFormType === 'el-switch'"
v-model="formData[column.javaFieldName]"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
export default {
name: 'DynamicForm',
props: {
tableName: {
type: String,
required: true
},
formData: {
type: Object,
default: () => ({})
}
},
data() {
return {
// 表单字段元数据
formColumns: [],
// 表单验证规则
formRules: {}
}
},
created() {
this.loadMetadata()
},
methods: {
/**
* 加载元数据
*/
async loadMetadata() {
const { data } = await this.$http.get(`/dynamic/${this.tableName}/metadata`)
// 过滤出需要在表单显示的字段
this.formColumns = data.columns.filter(col => col.isFormShow === 1)
// 动态生成验证规则
this.generateRules(data.columns)
},
/**
* 生成表单验证规则
*/
generateRules(columns) {
const rules = {}
columns.forEach(column => {
const fieldRules = []
// 必填验证
if (column.isRequired === 1) {
fieldRules.push({
required: true,
message: column.columnComment + '不能为空',
trigger: 'blur'
})
}
// 长度验证
if (column.characterMaximumLength) {
fieldRules.push({
max: column.characterMaximumLength,
message: `长度不能超过${column.characterMaximumLength}个字符`,
trigger: 'blur'
})
}
// 数字范围验证
if (column.dataType === 'int' || column.dataType === 'bigint') {
fieldRules.push({
type: 'number',
message: '请输入数字',
trigger: 'blur'
})
}
if (fieldRules.length > 0) {
rules[column.javaFieldName] = fieldRules
}
})
this.formRules = rules
},
/**
* 获取字典选项
*/
getDictOptions(dictType) {
if (!dictType) return []
// 从Vuex或缓存中获取字典数据
return this.$store.getters.getDictData(dictType)
},
/**
* 表单验证
*/
validate() {
return this.$refs.dynamicForm.validate()
},
/**
* 重置表单
*/
resetForm() {
this.$refs.dynamicForm.resetFields()
}
}
}
</script>
```
#### 2.4.2 动态表格生成器
```vue
<!-- DynamicTable.vue - 动态表格组件 -->
<template>
<div>
<!-- 查询表单 -->
<el-form v-if="queryColumns.length > 0" :model="queryParams" :inline="true" size="small">
<el-form-item
v-for="column in queryColumns"
:key="column.columnName"
:label="column.columnComment"
>
<!-- 文本查询 -->
<el-input
v-if="column.queryType === 'LIKE'"
v-model="queryParams[column.javaFieldName]"
:placeholder="'请输入' + column.columnComment"
clearable
/>
<!-- 精确查询 -->
<el-select
v-else-if="column.queryType === 'EQ' && column.dictType"
v-model="queryParams[column.javaFieldName]"
:placeholder="'请选择' + column.columnComment"
clearable
>
<el-option
v-for="dict in getDictOptions(column.dictType)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<!-- 范围查询 -->
<el-date-picker
v-else-if="column.queryType === 'BETWEEN'"
v-model="queryParams[column.javaFieldName + 'Range']"
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="dataList" border stripe>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<!-- 动态生成列 -->
<el-table-column
v-for="column in tableColumns"
:key="column.columnName"
:label="column.columnComment"
:prop="column.javaFieldName"
:width="column.columnWidth"
:align="column.columnAlign || 'center'"
:show-overflow-tooltip="true"
>
<template slot-scope="scope">
<!-- 字典翻译 -->
<span v-if="column.dictType">
{{ getDictLabel(column.dictType, scope.row[column.javaFieldName]) }}
</span>
<!-- 日期格式化 -->
<span v-else-if="column.dataType === 'datetime' || column.dataType === 'timestamp'">
{{ parseTime(scope.row[column.javaFieldName]) }}
</span>
<!-- 普通字段 -->
<span v-else>
{{ scope.row[column.javaFieldName] }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
export default {
name: 'DynamicTable',
props: {
tableName: {
type: String,
required: true
}
},
data() {
return {
loading: false,
dataList: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10
},
// 表格列元数据
tableColumns: [],
// 查询字段元数据
queryColumns: []
}
},
created() {
this.loadMetadata()
},
methods: {
/**
* 加载元数据
*/
async loadMetadata() {
const { data } = await this.$http.get(`/dynamic/${this.tableName}/metadata`)
// 过滤出需要在列表显示的字段
this.tableColumns = data.columns.filter(col => col.isListShow === 1)
// 过滤出可查询的字段
this.queryColumns = data.columns.filter(col => col.isQuery === 1)
// 加载数据
this.getList()
},
/**
* 查询列表
*/
async getList() {
this.loading = true
try {
const { data } = await this.$http.get(`/dynamic/${this.tableName}/list`, {
params: this.queryParams
})
this.dataList = data.rows
this.total = data.total
} finally {
this.loading = false
}
},
/**
* 搜索
*/
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/**
* 重置查询
*/
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10
}
this.getList()
},
/**
* 修改
*/
handleUpdate(row) {
this.$emit('edit', row)
},
/**
* 删除
*/
handleDelete(row) {
this.$confirm('确认删除该数据吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await this.$http.delete(`/dynamic/${this.tableName}/${row.id}`)
this.$message.success('删除成功')
this.getList()
})
},
/**
* 获取字典标签
*/
getDictLabel(dictType, value) {
const dict = this.$store.getters.getDictData(dictType)
const item = dict.find(d => d.value === value)
return item ? item.label : value
},
/**
* 获取字典选项
*/
getDictOptions(dictType) {
if (!dictType) return []
return this.$store.getters.getDictData(dictType)
}
}
}
</script>
```
#### 2.4.3 元数据管理页面
```vue
<!-- metadata/index.vue - 元数据管理页面 -->
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>元数据管理</span>
<el-button
style="float: right;"
type="primary"
size="small"
icon="el-icon-refresh"
@click="handleRefreshAll"
>刷新全部元数据</el-button>
</div>
<!-- 表列表 -->
<el-table :data="tableList" border stripe>
<el-table-column label="表名" prop="tableName" width="200" />
<el-table-column label="表注释" prop="tableComment" show-overflow-tooltip />
<el-table-column label="业务模块" prop="businessModule" width="120" />
<el-table-column label="字段数" prop="columnCount" width="100" align="center" />
<el-table-column label="动态适配" width="100" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.isDynamicSupport"
:active-value="1"
:inactive-value="0"
@change="handleSwitchChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="版本" prop="version" width="80" align="center" />
<el-table-column label="更新时间" prop="updateTime" width="160" />
<el-table-column label="操作" width="280" align="center" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewColumns(scope.row)"
>字段详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-refresh"
@click="handleRefresh(scope.row)"
>刷新</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-time"
@click="handleViewHistory(scope.row)"
>变更历史</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 字段详情对话框 -->
<el-dialog title="字段详情" :visible.sync="columnDialogVisible" width="80%">
<el-table :data="columnList" border>
<el-table-column label="字段名" prop="columnName" width="150" />
<el-table-column label="字段注释" prop="columnComment" show-overflow-tooltip />
<el-table-column label="数据类型" prop="columnType" width="120" />
<el-table-column label="Java类型" prop="javaFieldType" width="100" />
<el-table-column label="Vue组件" prop="vueFormType" width="180" />
<el-table-column label="必填" width="60" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.isRequired ? 'danger' : 'info'" size="small">
{{ scope.row.isRequired ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="列表显示" width="80" align="center">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.isListShow" :true-label="1" :false-label="0" />
</template>
</el-table-column>
<el-table-column label="表单显示" width="80" align="center">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.isFormShow" :true-label="1" :false-label="0" />
</template>
</el-table-column>
<el-table-column label="可查询" width="80" align="center">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.isQuery" :true-label="1" :false-label="0" />
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSaveColumns">保 存</el-button>
<el-button @click="columnDialogVisible = false">取 消</el-button>
</div>
</el-dialog>
<!-- 变更历史对话框 -->
<el-dialog title="变更历史" :visible.sync="historyDialogVisible" width="70%">
<el-timeline>
<el-timeline-item
v-for="item in changeHistory"
:key="item.id"
:timestamp="item.changeTime"
placement="top"
>
<el-card>
<h4>{{ item.changeType }} - {{ item.changeObject }}</h4>
<p>对象名称: {{ item.objectName }}</p>
<p>变更描述: {{ item.changeDesc }}</p>
<el-tag v-if="item.isSynced" type="success">已同步</el-tag>
<el-tag v-else type="warning">未同步</el-tag>
</el-card>
</el-timeline-item>
</el-timeline>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'MetadataManagement',
data() {
return {
tableList: [],
columnList: [],
changeHistory: [],
columnDialogVisible: false,
historyDialogVisible: false,
currentTable: null
}
},
created() {
this.getTableList()
},
methods: {
/**
* 获取表列表
*/
async getTableList() {
const { data } = await this.$http.get('/metadata/table/list')
this.tableList = data
},
/**
* 刷新单个表元数据
*/
async handleRefresh(row) {
await this.$http.post('/metadata/collector/refresh', { tableName: row.tableName })
this.$message.success('刷新成功')
this.getTableList()
},
/**
* 刷新全部元数据
*/
async handleRefreshAll() {
this.$confirm('确认刷新全部表的元数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await this.$http.post('/metadata/collector/refreshAll')
this.$message.success('刷新成功')
this.getTableList()
})
},
/**
* 查看字段详情
*/
async handleViewColumns(row) {
this.currentTable = row
const { data } = await this.$http.get(`/metadata/column/list/${row.id}`)
this.columnList = data
this.columnDialogVisible = true
},
/**
* 保存字段配置
*/
async handleSaveColumns() {
await this.$http.put('/metadata/column/batchUpdate', this.columnList)
this.$message.success('保存成功')
this.columnDialogVisible = false
},
/**
* 查看变更历史
*/
async handleViewHistory(row) {
const { data } = await this.$http.get(`/metadata/changeLog/list/${row.id}`)
this.changeHistory = data
this.historyDialogVisible = true
},
/**
* 动态适配开关变更
*/
async handleSwitchChange(row) {
await this.$http.put('/metadata/table/updateDynamicSupport', {
id: row.id,
isDynamicSupport: row.isDynamicSupport
})
this.$message.success('设置成功')
}
}
}
</script>
```
---
## 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
<template>
<div class="app-container">
<!-- 使用动态表格 -->
<dynamic-table
table-name="iot_config"
@edit="handleEdit"
/>
<!-- 使用动态表单 -->
<el-dialog title="编辑" :visible.sync="dialogVisible">
<dynamic-form
table-name="iot_config"
:form-data="formData"
ref="dynamicForm"
/>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import DynamicTable from '@/components/DynamicTable'
import DynamicForm from '@/components/DynamicForm'
export default {
components: {
DynamicTable,
DynamicForm
},
data() {
return {
dialogVisible: false,
formData: {}
}
},
methods: {
handleEdit(row) {
this.formData = { ...row }
this.dialogVisible = true
},
async submitForm() {
const valid = await this.$refs.dynamicForm.validate()
if (valid) {
// 调用通用API
await this.$http.put('/dynamic/iot_config', this.formData)
this.$message.success('保存成功')
this.dialogVisible = false
}
}
}
}
</script>
```
#### 方式2: 元数据驱动传统页面
```vue
<template>
<el-form :model="form" ref="form">
<!-- 动态生成表单项 -->
<el-form-item
v-for="field in formFields"
:key="field.javaFieldName"
:label="field.columnComment"
:prop="field.javaFieldName"
:rules="getFieldRules(field)"
>
<component
:is="getFormComponent(field)"
v-model="form[field.javaFieldName]"
v-bind="getComponentProps(field)"
/>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
form: {},
formFields: []
}
},
async created() {
// 获取元数据
const { data } = await this.$http.get('/dynamic/iot_config/metadata')
this.formFields = data.columns.filter(col => col.isFormShow === 1)
},
methods: {
getFormComponent(field) {
const componentMap = {
'el-input': 'el-input',
'el-input-number': 'el-input-number',
'el-select': 'el-select',
'el-date-picker[datetime]': 'el-date-picker'
}
return componentMap[field.vueFormType] || 'el-input'
},
getFieldRules(field) {
const rules = []
if (field.isRequired === 1) {
rules.push({
required: true,
message: field.columnComment + '不能为空',
trigger: 'blur'
})
}
return rules
},
getComponentProps(field) {
const props = {}
if (field.vueFormType === 'el-date-picker[datetime]') {
props.type = 'datetime'
props.valueFormat = 'yyyy-MM-dd HH:mm:ss'
} else if (field.vueFormType === 'el-input-number') {
props.precision = field.numericScale || 0
}
return props
}
}
}
</script>
```
---
## 4. 高级特性
### 4.1 元数据版本管理
```java
/**
* 元数据版本服务
*/
@Service
public class MetadataVersionService {
/**
* 创建元数据快照
*/
public void createSnapshot(String tableName, String description) {
TableMetadata tableMetadata = metadataService.getTableMetadata(tableName);
List<ColumnMetadata> 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<ColumnMetadata> columns = JSON.parseArray(snapshot.getColumnMetadata(), ColumnMetadata.class);
// 更新当前元数据
metadataService.updateTableMetadata(tableMetadata);
metadataService.batchUpdateColumnMetadata(columns);
}
/**
* 版本对比
*/
public Map<String, Object> compareVersions(String tableName, Integer version1, Integer version2) {
MetadataSnapshot snap1 = metadataSnapshotMapper.selectByTableAndVersion(tableName, version1);
MetadataSnapshot snap2 = metadataSnapshotMapper.selectByTableAndVersion(tableName, version2);
List<ColumnMetadata> columns1 = JSON.parseArray(snap1.getColumnMetadata(), ColumnMetadata.class);
List<ColumnMetadata> columns2 = JSON.parseArray(snap2.getColumnMetadata(), ColumnMetadata.class);
Map<String, Object> 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<String, DataSource> 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<ColumnMetadata> 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
<!-- 使用虚拟滚动处理大量字段 -->
<el-table
:data="dataList"
height="500"
:virtual-scroll="true"
>
</el-table>
<!-- 字段懒加载 -->
<el-tab-pane lazy>
</el-tab-pane>
```
### 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<String> issues = new ArrayList<>();
// 检查是否有未同步的变更
List<MetadataChangeLog> unsynced = metadataChangeLogMapper.selectUnsynced();
if (!unsynced.isEmpty()) {
issues.add("发现 " + unsynced.size() + " 条未同步的元数据变更");
}
// 检查是否有缺失映射的字段
List<ColumnMetadata> unmapped = metadataColumnMapper.selectUnmapped();
if (!unmapped.isEmpty()) {
issues.add("发现 " + unmapped.size() + " 个字段缺少类型映射");
}
// 检查是否有配置异常的字段
List<ColumnMetadata> 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 联系方式
- **技术支持**:
- **文档维护**:
- **问题反馈**:
---
**文档结束**