1950 lines
60 KiB
Markdown
1950 lines
60 KiB
Markdown
|
|
# 元数据自动适配系统技术方案
|
|||
|
|
|
|||
|
|
## 📋 文档信息
|
|||
|
|
|
|||
|
|
- **文档版本**: 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 联系方式
|
|||
|
|
|
|||
|
|
- **技术支持**:
|
|||
|
|
- **文档维护**:
|
|||
|
|
- **问题反馈**:
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档结束**
|
|||
|
|
|
|||
|
|
|
|||
|
|
|