# 表结构查看功能实现说明

## 功能概述

表结构查看功能已完成，用户可以查看 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
<template>
  <a-modal
    v-model:visible="visible"
    :title="title"
    width="900px"
    :footer="false"
    @cancel="handleClose"
  >
    <a-tabs v-model:active-tab>
      <!-- MySQL 表结构 -->
      <a-tab-pane key="mysql" title="表结构">
        <a-table
          :data="mysqlColumns"
          :columns="mysqlColumnDefs"
          :pagination="false"
          size="small"
          :bordered="{cell:true}"
        >
          <template #columns>
            <a-table-column title="字段名" data-index="Field" width="150"/>
            <a-table-column title="类型" data-index="Type" width="120"/>
            <a-table-column title="是否NULL" data-index="Null" width="80"/>
            <a-table-column title="主键" data-index="Key" width="80"/>
            <a-table-column title="默认值" data-index="Default" width="120"/>
            <a-table-column title="额外信息" data-index="Extra" width="200"/>
          </template>
        </a-table>

        <a-divider>索引信息</a-divider>

        <a-table
          :data="indexes"
          :columns="indexColumnDefs"
          :pagination="false"
          size="small"
          :bordered="{cell:true}"
        >
          <template #columns>
            <a-table-column title="索引名" data-index="name" width="150"/>
            <a-table-column title="唯一" data-index="unique" width="80">
              <template #cell="{ record }">
                {{ record.unique ? '是' : '否' }}
              </template>
            </a-table-column>
            <a-table-column title="字段" data-index="keys" width="200"/>
          </template>
        </a-table>
      </a-tab-pane>

      <!-- MongoDB 集合结构 -->
      <a-tab-pane key="mongo" title="集合结构">
        <a-statistic-group :data="mongoStats" direction="row" style="margin-bottom: 16px;">
          <a-statistic-item title="文档总数" :value="structure.documentCount"/>
          <a-statistic-item title="字段数" :value="Object.keys(structure.fieldStats).length"/>
          <a-statistic-item title="索引数" :value="structure.indexes.length"/>
        </a-statistic-group>

        <a-divider>文档示例</a-divider>

        <a-table
          :data="structure.sampleDocs"
          :columns="mongoColumnDefs"
          :pagination="false"
          size="small"
          :bordered="{cell:true}"
        >
        </a-table>
      </a-tab-pane>

      <!-- Redis Key 信息 -->
      <a-tab-pane key="redis" title="Key 信息">
        <a-descriptions :data="redisInfo" :column="1" size="small">
          <a-descriptions-item label="Key 名" :value="structure.key"/>
          <a-descriptions-item label="数据类型" :value="structure.info.type"/>
          <a-descriptions-item label="数据长度" :value="structure.info.length"/>
          <a-descriptions-item label="TTL（秒）">
            {{ structure.info.ttl }}
            <a-tag v-if="structure.info.ttl > 0" color="red">即将过期</a-tag>
          </a-descriptions-item>
          <a-descriptions-item label="值预览">
            <pre style="max-height: 150px; overflow: auto;">{{ structure.info.value }}</pre>
          </a-descriptions-item>
        </a-descriptions>
      </a-tab-pane>
    </a-tabs>
  </a-modal>
</template>

<script setup>
import {computed, onMounted, ref} from 'vue'
import {Message} from '@arco-design/web-vue'

// Props
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  connectionId: {
    type: Number,
    default: null
  },
  database: {
    type: String,
    default: ''
  },
  tableName: {
    type: String,
    default: ''
  }
})

// 状态
const loading = ref(false)
const structure = ref({})
const indexes = ref([])

// 计算属性
const title = computed(() => {
  return `${props.tableName} - 结构`
})

const activeTab = computed(() => {
  if (!props.database) {
    return 'mysql'
  }
  // 根据 database 判断数据库类型（简化处理）
  return 'mysql'
})

// MySQL 列定义
const mysqlColumnDefs = [
  { title: '字段名', dataIndex: 'Field', width: 150 },
  { title: '类型', dataIndex: 'Type', width: 120 },
  { title: '是否NULL', dataIndex: 'Null', width: 80 },
  { title: '主键', dataIndex: 'Key', width: 80 },
  { title: '默认值', dataIndex: 'Default', width: 120 },
  { title: '额外信息', dataIndex: 'Extra', width: 200 }
]

const mysqlColumns = computed(() => {
  return structure.value.columns || []
})

// 索引列定义
const indexColumnDefs = [
  { title: '索引名', dataIndex: 'name', width: 150 },
  { title: '唯一', dataIndex: 'unique', width: 80 },
  { title: '字段', dataIndex: 'keys', width: 200 }
]

// MongoDB 统计数据
const mongoStats = computed(() => {
  return [
    { label: '文档总数', value: structure.value.documentCount || 0 },
    { label: '字段数', value: Object.keys(structure.value.fieldStats || {}).length },
    { label: '索引数', value: structure.value.indexes?.length || 0 }
  ]
})

const mongoColumnDefs = computed(() => {
  const columns = []
  if (structure.value.sampleDocs && structure.value.sampleDocs.length > 0) {
    const firstDoc = structure.value.sampleDocs[0]
    Object.keys(firstDoc).forEach(key => {
      columns.push({ title: key, dataIndex: key, width: 150 })
    })
  }
  return columns
})

const mongoSampleDocs = computed(() => {
  return structure.value.sampleDocs || []
})

// Redis 信息
const redisInfo = computed(() => {
  return structure.value.info || {}
})

// 加载表结构
const loadStructure = async () => {
  if (!props.connectionId || !props.database || !props.tableName) {
    Message.warning('参数不完整')
    return
  }

  loading.value = true
  try {
    if (!window.go?.main?.App?.GetTableStructure) {
      throw new Error('Go 后端未就绪')
    }

    const result = await window.go.main.App.GetTableStructure(
      props.connectionId,
      props.database,
      props.tableName
    )

    console.log('GetTableStructure 返回结果:', result)

    structure.value = result
  } catch (error) {
    console.error('加载表结构失败:', error)
    Message.error('加载表结构失败: ' + (error.message || error))
  } finally {
    loading.value = false
  }
}

// 关闭对话框
const handleClose = () => {
  emit('update:visible', false)
}

onMounted(() => {
  if (props.visible) {
    loadStructure()
  }
})
</script>

<style scoped>
.arco-table {
  font-size: 13px;
}

.arco-table :deep(.arco-table-cell) {
  padding: 8px 12px;
}
</style>
```

#### 集成到主页面
**文件**: `go-desk/frontend/src/views/db-cli/index.vue`

```vue
<!-- 表结构对话框 -->
<TableStructure
    v-model:visible="showTableStructure"
    :connection-id="currentConnection?.id"
    :database="currentDatabase"
    :table-name="currentTableName"
/>

<!-- 连接树组件更新 -->
<ConnectionTree
    :current-connection-id="currentConnection?.id"
    @connection-select="handleConnectionSelect"
    @connection-edit="handleConnectionEdit"
    @connection-delete="handleConnectionDelete"
    @table-select="handleTableSelect"
    @table-structure="handleTableStructure"
    @show-bookmarks="handleShowBookmarks"
    @show-templates="handleShowTemplates"
    @new-connection="handleNewConnection"
    ref="connectionTreeRef"
/>
```

### 数据流程

```
用户点击表名
    ↓
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
**状态**: ✅ 已完成
**测试状态**: ⏳ 待用户测试

