Files
Meshray-Manager/docs/TURN 配置设计说明.md
T
2026-06-30 15:14:37 +08:00

11 KiB
Raw Blame History

TURN 配置设计说明

更新时间: 2026-03-24
设计模式: 内嵌式设计(非独立表)


📋 TURN 配置存储位置

误解

之前认为存在独立的 TURNConfig 表,实际上没有这个独立模型

实际设计

TURN 配置内嵌在 SystemSetting 模型中,作为系统全局设置的一部分。

文件: internal/model/models.go

// SystemSetting 系统设置模型 - 单例模式,全局只有一条记录
type SystemSetting struct {
    ID              uint      `gorm:"primaryKey;type:bigint" json:"id"`                     // 固定为 1
    ServerIP        string    `gorm:"type:varchar(45)" json:"server_ip"`                    // 服务端公网 IP
    ServerPort      int       `gorm:"default:51820" json:"server_port"`                     // WireGuard 监听端口
    ServerPublicKey string    `gorm:"type:varchar(64)" json:"server_public_key,omitempty"`  // WireGuard 服务端公钥
    DDNSDomain      string    `gorm:"type:varchar(255)" json:"ddns_domain"`                 // DDNS 域名
    TURNMode        string    `gorm:"type:varchar(16);default:'auto'" json:"turn_mode"`     // auto/manual
    TURNURL         string    `gorm:"type:varchar(255)" json:"turn_url"`                    // TURN 服务器 URL
    TURNUsername    string    `gorm:"type:varchar(128)" json:"turn_username,omitempty"`     // TURN 用户名
    TURNPassword    string    `gorm:"type:"size:varchar(128)" json:"-"`                     // TURN 密码(加密存储)
    LogLevel        string    `gorm:"type:varchar(16);default:'info'" json:"log_level"`     // debug/info/warn/error
    LogFormat       string    `gorm:"type:varchar(16);default:'console'" json:"log_format"` // console/json
    MaxBackups      int       `gorm:"default:7" json:"max_backups"`                         // 日志最大保留份数
    MaxAge          int       `gorm:"default:30" json:"max_age"`                            // 日志最大保留天数
    Theme           string    `gorm:"type:varchar(32);default:'light'" json:"theme"`        // light/dark/auto
    Language        string    `gorm:"type:varchar(16);default:'zh-CN'" json:"language"`     // zh-CN/en-US
    CreatedAt       time.Time `gorm:"autoCreateTime" json:"createdAt"`
    UpdatedAt       time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
}

🎯 为什么采用内嵌设计?

1. 单例模式的合理性

原因: TURN 配置是全局唯一的系统级配置

  • 整个系统只需要一套 TURN 配置
  • 不需要为不同网络创建不同的 TURN 配置
  • 与 ServerIP、ServerPort 等一样,属于基础设施配置

对比独立表的劣势:

-- ❌ 如果设计为独立表
CREATE TABLE turn_configs (
    id INTEGER PRIMARY KEY,
    url VARCHAR(255),
    username VARCHAR(128),
    password VARCHAR(128)
);

-- 问题:永远只会有一条记录,浪费表结构

内嵌设计的优势:

-- ✅ 实际设计
CREATE TABLE system_settings (
    id INTEGER PRIMARY KEY,
    server_ip VARCHAR(45),
    server_port INTEGER,
    turn_mode VARCHAR(16),      -- 内嵌 TURN 配置
    turn_url VARCHAR(255),      -- 内嵌 TURN 配置
    turn_username VARCHAR(128), -- 内嵌 TURN 配置
    turn_password VARCHAR(128)  -- 内嵌 TURN 配置
    -- ... 其他系统配置
);

-- 优势:所有系统配置集中在一张表,便于管理和备份

2. 数据库迁移

自动迁移时会自动创建字段:

// store.go:36-56
func (s *Store) AutoMigrate() error {
    return s.db.AutoMigrate(
        &model.Network{},
        &model.Device{},
        // ... 其他模型
        &model.SystemSetting{},     // ✅ 包含 TURN 配置字段
        &model.ExternalService{},   // ✅ 也包含 TURN 服务器配置
    )
}

生成的数据库表结构:

-- SQLite 表结构
CREATE TABLE IF NOT EXISTS "system_settings" (
    "id" integer PRIMARY KEY,
    "server_ip" varchar(45),
    "server_port" integer DEFAULT 51820,
    "server_public_key" varchar(64),
    "ddns_domain" varchar(255),
    "turn_mode" varchar(16) DEFAULT 'auto',      -- ← TURN 配置
    "turn_url" varchar(255),                     -- ← TURN 配置
    "turn_username" varchar(128),                -- ← TURN 配置
    "turn_password" varchar(128),                -- ← TURN 配置
    "log_level" varchar(16) DEFAULT 'info',
    "log_format" varchar(16) DEFAULT 'console',
    "max_backups" integer DEFAULT 7,
    "max_age" integer DEFAULT 30,
    "theme" varchar(32) DEFAULT 'light',
    "language" varchar(16) DEFAULT 'zh-CN',
    "created_at" datetime,
    "updated_at" datetime
);

📊 TURN 配置字段说明

字段名 类型 默认值 说明
turn_mode string "auto" TURN 模式:auto/manual
turn_url string - TURN 服务器 URL(手动模式)
turn_username string - TURN 用户名(可选)
turn_password string - TURN 密码(加密存储,JSON 不返回)

🔧 使用方式

1. 读取 TURN 配置

// service/device.go
var setting model.SystemSetting
err := s.store.DB().First(&setting, 1).Error

if err != nil {
    return nil, err
}

// 使用 TURN 配置
turnMode := setting.TURNMode      // "auto" 或 "manual"
turnURL := setting.TURNURL        // "turn:stun.example.com:3478"
turnUser := setting.TURNUsername  // "myuser"
// turnPass 不会通过 JSON 返回,但可以从数据库读取

2. 更新 TURN 配置

// api/handler/settings.go
func (h *SettingsHandler) UpdateSystemSettings(c *gin.Context) {
    var req struct {
        TURNMode     string `json:"turn_mode"`
        TURNURL      string `json:"turn_url"`
        TURNUsername string `json:"turn_username"`
        TURNPassword string `json:"turn_password"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // 获取现有设置
    var setting model.SystemSetting
    h.store.DB().First(&setting, 1)
    
    // 更新 TURN 配置
    setting.TURNMode = req.TURNMode
    setting.TURNURL = req.TURNURL
    setting.TURNUsername = req.TURNUsername
    if req.TURNPassword != "" {
        // 加密后存储
        encrypted, _ := encrypt(req.TURNPassword)
        setting.TURNPassword = encrypted
    }
    
    // 保存到数据库
    h.store.DB().Save(&setting)
    
    c.JSON(200, gin.H{"message": "更新成功"})
}

3. 前端调用

<!-- Settings.vue -->
<template>
  <el-form :model="settings">
    <el-form-item label="TURN 模式">
      <el-radio-group v-model="settings.turn_mode">
        <el-radio label="auto">自动</el-radio>
        <el-radio label="manual">手动</el-radio>
      </el-radio-group>
    </el-form-item>
    
    <el-form-item label="TURN 服务器" v-if="settings.turn_mode === 'manual'">
      <el-input v-model="settings.turn_url" placeholder="turn:example.com:3478" />
    </el-form-item>
    
    <el-form-item label="用户名">
      <el-input v-model="settings.turn_username" />
    </el-form-item>
    
    <el-form-item label="密码">
      <el-input v-model="form.turn_password" type="password" />
    </el-form-item>
  </el-form>
</template>

<script setup>
const loadSettings = async () => {
  const res = await request.get('/settings/system')
  settings.value = res.data
  
  // 注意:密码不会返回(json:"-"
  // form.turn_password 为空,需要用户手动输入
}
</script>

🏗️ 架构设计考量

1. 为什么不用独立表?

独立表的问题:

// ❌ 假设的独立表设计
type TURNConfig struct {
    ID       uint   `gorm:"primaryKey"`
    URL      string
    Username string
    Password string
}

// 问题:
// 1. 永远只会有一条记录(浪费表结构)
// 2. 需要额外的 JOIN 查询
// 3. 增加数据库连接负担
// 4. 不符合"全局唯一配置"的语义

内嵌设计的优势:

// ✅ 实际的内嵌设计
type SystemSetting struct {
    // ... 其他系统配置
    TURNMode     string  // 直接访问,无需 JOIN
    TURNURL      string
    TURNUsername string
    TURNPassword string
}

// 优势:
// 1. 一次查询获取所有系统配置
// 2. 符合"全局唯一"的语义
// 3. 简化数据库设计
// 4. 便于备份和迁移

2. 与 ExternalService 的区别

问题: 既然有 ExternalService 表,为什么还需要内嵌 TURN 配置?

答案: 两者用途不同

特性 SystemSetting (内嵌) ExternalService (独立表)
用途 主用 TURN 服务器 备用 TURN 服务器池
数量 1 个(全局唯一) N 个(可多个)
场景 默认配置 多区域/多服务商
访问速度 快(一次查询) 慢(需要 JOIN
管理方式 系统设置页面 外部服务管理页面

示例:

// SystemSetting: 主用 TURN
turn_mode: "auto"
turn_url: "turn:primary.example.com:3478"
turn_username: "main_user"

// ExternalService: 备用 TURN 池
[
  {
    service_type: "turn_server",
    name: "美国节点",
    config: {"url": "turn:us.example.com:3478", ...}
  },
  {
    service_type: "turn_server",
    name: "欧洲节点",
    config: {"url": "turn:eu.example.com:3478", ...}
  }
]

📝 总结

关键结论

  1. TURN 配置没有被删除

    • 内嵌在 SystemSetting 模型中(第 110-113 行)
    • 作为系统全局配置的一部分
  2. 为什么采用内嵌设计?

    • 符合"全局唯一配置"的语义
    • 简化数据库设计
    • 提高查询性能(无需 JOIN
    • 便于管理和备份
  3. 字段完整性

    • turn_mode: 自动/手动模式
    • turn_url: TURN 服务器 URL
    • turn_username: 用户名
    • turn_password: 密码(加密存储)
  4. 数据库迁移

    • AutoMigrate 会自动创建所有字段
    • initDefaultSettings 会初始化默认值

🎯 最佳实践

读取配置:

var setting model.SystemSetting
s.store.DB().First(&setting, 1)

// 直接使用 TURN 配置
if setting.TURNMode == "manual" {
    useCustomTURN(setting.TURNURL)
}

更新配置:

// 先获取,再修改,最后保存
var setting model.SystemSetting
s.store.DB().First(&setting, 1)

setting.TURNMode = "manual"
setting.TURNURL = "turn:new-server.com:3478"
s.store.DB().Save(&setting)

前端展示:

// 注意密码不会返回(json: "-"
loadSettings().then(data => {
    console.log(data.turn_mode)     // ✅ 有值
    console.log(data.turn_url)      // ✅ 有值
    console.log(data.turn_username) // ✅ 有值
    console.log(data.turn_password) // ❌ undefined(加密存储)
})

状态: TURN 配置完整保留
设计: 内嵌在 SystemSetting 模型中
位置: internal/model/models.go:110-113

MeshRay - 清晰的架构设计! 🏗️