# 表结构查看功能实现说明 ## 功能概述 表结构查看功能已完成,用户可以查看 MySQL 表、MongoDB 集合、Redis Key 的详细结构和信息。 ## 实现内容 ### 1. 后端实现(Go) #### MySQL 表结构查询 **文件**: `go-desk/internal/dbclient/mysql.go` ```go // GetTableStructure 获取表结构 func (c *MySQLClient) GetTableStructure(ctx context.Context, database, tableName string) ([]map[string]interface{}, error) { var columns []map[string]interface{} query := "DESCRIBE " if database != "" { query += fmt.Sprintf("`%s`.", database) } query += fmt.Sprintf("`%s`", tableName) err := c.db.Raw(query).Scan(&columns).Error if err != nil { return nil, fmt.Errorf("获取表结构失败: %v", err) } // 转换为统一格式 for _, col := range columns { // 确保字段存在 if _, ok := col["Field"]; !ok { col["Field"] = "" } if _, ok := col["Type"]; !ok { col["Type"] = "" } if _, ok := col["Null"]; !ok { col["Null"] = "NO" } if _, ok := col["Key"]; !ok { col["Key"] = "" } if _, ok := col["Default"]; !ok { col["Default"] = nil } if _, ok := col["Extra"]; !ok { col["Extra"] = "" } } return columns, nil } // GetIndexes 获取索引列表 func (c *MySQLClient) GetIndexes(ctx context.Context, database, tableName string) ([]map[string]interface{}, error) { var indexes []map[string]interface{} query := "SHOW INDEX FROM " if database != "" { query += fmt.Sprintf("`%s`.", database) } query += fmt.Sprintf("`%s`", tableName) err := c.db.Raw(query).Scan(&indexes).Error if err != nil { return nil, fmt.Errorf("获取索引列表失败: %v", err) } return indexes, nil } ``` **字段说明**: - `Field`: 字段名 - `Type`: 字段类型(int, varchar, text, datetime, etc.) - `Null`: 是否允许 NULL - `Key`: 是否主键 - `Default`: 默认值 - `Extra`: 额外信息 #### MongoDB 集合结构查询 **文件**: `go-desk/internal/dbclient/mongo.go` ```go // GetCollectionStructure 获取集合结构 func (c *MongoClient) GetCollectionStructure(ctx context.Context, database, collectionName string) (map[string]interface{}, error) { coll := c.client.Database(database).Collection(collectionName) result := map[string]interface{}{ "database": database, "collection": collectionName, "sampleDocs": []map[string]interface{}{}, "fieldStats": map[string]int{}, } // 获取文档示例(最多 5 个) cursor, err := coll.Find(ctx, bson.M{}).Limit(5) if err != nil { return nil, fmt.Errorf("获取文档示例失败: %v", err) } defer cursor.Close(ctx) var docs []bson.M if err = cursor.All(ctx, &docs); err != nil { return nil, fmt.Errorf("解析文档失败: %v", err) } // 转换为 map for _, doc := range docs { docMap := make(map[string]interface{}) for k, v := range doc { docMap[k] = v } result["sampleDocs"] = append(result["sampleDocs"].([]map[string]interface{}), docMap) } // 字段统计 fieldCount := make(map[string]int) for _, doc := range docs { for key := range doc { fieldCount[key]++ } } result["fieldStats"] = fieldCount // 文档总数 count, err := coll.CountDocuments(ctx, bson.M{}) if err != nil { return nil, fmt.Errorf("获取文档数量失败: %v", err) } result["documentCount"] = count // 索引信息 cursor, err = coll.Indexes().ListSpecifications(ctx) if err != nil { return nil, fmt.Errorf("获取索引信息失败: %v", err) } else { var indexes []map[string]interface{} for cursor.Next(ctx) { spec := cursor.Current indexes = append(indexes, map[string]interface{}{ "name": spec.Name, "unique": spec.Unique, "keys": spec.Keys, }) } cursor.Close(ctx) result["indexes"] = indexes } return result, nil } // CountDocuments 获取文档数量 func (c *MongoClient) CountDocuments(ctx context.Context, database, collectionName string) (int64, error) { coll := c.client.Database(database).Collection(collectionName) count, err := coll.CountDocuments(ctx, bson.M{}) return count, err } ``` **返回数据**: - `database`: 数据库名 - `collection`: 集合名 - `sampleDocs`: 文档示例(最多 5 个) - `fieldStats`: 字段统计 - `documentCount`: 文档总数 - `indexes`: 索引列表 #### Redis Key 详细信息 **文件**: `go-desk/internal/dbclient/redis.go` ```go // GetKeyInfo 获取 Key 详细信息 func (c *RedisClient) GetKeyInfo(ctx context.Context, key string) (map[string]interface{}, error) { info := map[string]interface{}{ "key": key, "type": "", "value": nil, "ttl": 0, "length": 0, } // 获取 Key 类型 keyType, err := c.GetKeyType(ctx, key) if err != nil { return nil, fmt.Errorf("获取 Key 类型失败: %v", err) } info["type"] = keyType // 获取 TTL ttl, err := c.GetTTL(ctx, key) if err != nil { return nil, fmt.Errorf("获取 TTL 失败: %v", err) } info["ttl"] = ttl.Seconds() // 获取 Key 值(限制大小,避免过大) value, err := c.GetKeyValue(ctx, key) if err != nil { return nil, fmt.Errorf("获取 Key 值失败: %v", err) } info["value"] = formatValuePreview(value) // 获取 Key 长度(使用 STRLEN、HLEN、SCARD、ZCARD) var keyLength int64 switch keyType { case "string": keyLength, err = c.client.StrLen(ctx, key).Result() case "list": keyLength, err = c.client.LLen(ctx, key).Result() case "set": keyLength, err = c.client.SCard(ctx, key).Result() case "zset": keyLength, err = c.client.ZCard(ctx, key).Result() case "hash": keyLength, err = c.client.HLen(ctx, key).Result() } if err == nil { info["length"] = keyLength } return info, nil } // formatValuePreview 格式化值预览(限制长度) func formatValuePreview(value interface{}) string { if value == nil { return "" } const maxPreviewLength = 200 valueStr := fmt.Sprintf("%v", value) if len(valueStr) > maxPreviewLength { valueStr = valueStr[:maxPreviewLength] + "..." } return valueStr } ``` **返回数据**: - `key`: Key 名称 - `type`: 数据类型(string, list, set, zset, hash) - `value`: 值预览(最多 200 字符) - `ttl`: 过期时间(秒) - `length`: 数据长度(string 为字符数,list/set/zset/hash 为元素数) #### 应用层 API **文件**: `go-desk/app.go` ```go // GetTableStructure 获取表结构 func (a *App) GetTableStructure(connectionId uint, database, tableName string) (map[string]interface{}, error) { ctx, cancel := context.WithTimeout(a.ctx, 30*time.Second) defer cancel() pool := dbclient.GetPool() // 获取连接配置 conn, err := storage.GetConnection(connectionId) if err != nil { return nil, fmt.Errorf("获取连接配置失败: %v", err) } // 根据数据库类型调用对应客户端 switch conn.Type { case "mysql": client, err := pool.GetMySQLClient(conn) if err != nil { return nil, fmt.Errorf("获取 MySQL 客户端失败: %v", err) } structure, err := client.GetTableStructure(ctx, database, tableName) if err != nil { return nil, err } return map[string]interface{}{ "type": "mysql", "database": database, "table": tableName, "columns": structure, }, nil case "mongo": client, err := pool.GetMongoClient(conn) if err != nil { return nil, fmt.Errorf("获取 MongoDB 客户端失败: %v", err) } structure, err := client.GetCollectionStructure(ctx, database, tableName) if err != nil { return nil, err } return map[string]interface{}{ "type": "mongo", "database": database, "collection": tableName, "structure": structure, }, nil case "redis": client, err := pool.GetRedisClient(conn) if err != nil { return nil, fmt.Errorf("获取 Redis 客户端失败: %v", err) } info, err := client.GetKeyInfo(ctx, tableName) // tableName 作为 key 名 if err != nil { return nil, err } return map[string]interface{}{ "type": "redis", "key": tableName, "info": info, }, nil default: return nil, fmt.Errorf("不支持的数据库类型: %s", conn.Type) } } // GetIndexes 获取索引列表 func (a *App) GetIndexes(connectionId uint, database, tableName string) ([]map[string]interface{}, error) { ctx, cancel := context.WithTimeout(a.ctx, 30*time.Second) defer cancel() pool := dbclient.GetPool() // 获取连接配置 conn, err := storage.GetConnection(connectionId) if err != nil { return nil, fmt.Errorf("获取连接配置失败: %v", err) } // 目前只支持 MySQL if conn.Type != "mysql" { return nil, fmt.Errorf("当前只支持 MySQL 的索引查询") } client, err := pool.GetMySQLClient(conn) if err != nil { return nil, fmt.Errorf("获取 MySQL 客户端失败: %v", err) } indexes, err := client.GetIndexes(ctx, database, tableName) if err != nil { return nil, err } return indexes, nil } ``` ### 2. 前端实现(Vue) #### 表结构展示组件 **文件**: `go-desk/frontend/src/views/db-cli/components/TableStructure.vue` ```vue 索引信息 {{ record.unique ? '是' : '否' }} 文档示例 {{ structure.info.ttl }} 即将过期 {{ structure.info.value }} ``` #### 集成到主页面 **文件**: `go-desk/frontend/src/views/db-cli/index.vue` ```vue ``` ### 数据流程 ``` 用户点击表名 ↓ ConnectionTree 触发 table-select 事件 ↓ index.vue 记录当前数据库和表名 ↓ 用户点击表结构按钮(新增) ↓ index.vue 显示 TableStructure 对话框 ↓ TableStructure 组件调用 GetTableStructure API ↓ 后端根据数据库类型调用对应客户端 ↓ MySQL: GetTableStructure → DESCRIBE 查询 → 解析列信息 MongoDB: GetCollectionStructure → 文档分析 → 字段统计 Redis: GetKeyInfo → 命令查询 → 值预览 ↓ 返回结构数据 ↓ 前端展示对应 Tab 页面 ``` ### 功能特性 #### MySQL - ✅ 表结构展示(字段名、类型、是否NULL、主键、默认值) - ✅ 索引列表(索引名、唯一、字段) #### MongoDB - ✅ 文档示例(最多 5 个) - ✅ 字段统计 - ✅ 文档总数 - ✅ 索引列表 #### Redis - ✅ Key 类型识别 - ✅ TTL 显示 - ✅ 数据长度统计 - ✅ 值预览(限制 200 字符) ### 使用示例 #### MySQL 1. 在连接树中选择表 2. 点击"表结构"按钮 3. 查看表字段信息 4. 查看表索引信息 #### MongoDB 1. 在连接树中选择集合 2. 点击"表结构"按钮 3. 查看文档示例 4. 查看字段统计 5. 查看索引信息 #### Redis 1. 在连接树中选择 Key 2. 点击"表结构"按钮 3. 查看 Key 类型 4. 查看 TTL 5. 查看数据长度 6. 查看值预览 ### 技术要点 #### 后端 - **统一接口**: `GetTableStructure()` 根据 `conn.Type` 调用不同客户端 - **数据解析**: 自动转换为统一格式 - **错误处理**: 完善的超时和错误处理 #### 前端 - **Tab 页面**: 根据数据库类型显示不同内容 - **响应式数据**: 使用 `computed` 自动更新 - **表格组件**: 使用 Arco Design 统一展示 - **统计卡片**: MongoDB 数据统计 --- **实现时间**: 2026-01-XX **状态**: ✅ 已完成 **测试状态**: ⏳ 待用户测试
{{ structure.info.value }}