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

389 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TURN 配置设计说明
**更新时间**: 2026-03-24
**设计模式**: **内嵌式设计**(非独立表)
---
## 📋 **TURN 配置存储位置**
### ❌ **误解**
之前认为存在独立的 `TURNConfig` 表,实际上**没有这个独立模型**。
### ✅ **实际设计**
**TURN 配置内嵌在 `SystemSetting` 模型中**,作为系统全局设置的一部分。
**文件**: [`internal/model/models.go`](file://e:\Project\MeshRay\internal\model\models.go#L103-L122)
```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 等一样,属于基础设施配置
**对比独立表的劣势**:
```sql
-- ❌ 如果设计为独立表
CREATE TABLE turn_configs (
id INTEGER PRIMARY KEY,
url VARCHAR(255),
username VARCHAR(128),
password VARCHAR(128)
);
-- 问题:永远只会有一条记录,浪费表结构
```
**内嵌设计的优势**:
```sql
-- ✅ 实际设计
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. **数据库迁移**
**自动迁移时会自动创建字段**:
```go
// store.go:36-56
func (s *Store) AutoMigrate() error {
return s.db.AutoMigrate(
&model.Network{},
&model.Device{},
// ... 其他模型
&model.SystemSetting{}, // ✅ 包含 TURN 配置字段
&model.ExternalService{}, // ✅ 也包含 TURN 服务器配置
)
}
```
**生成的数据库表结构**:
```sql
-- 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 配置**
```go
// 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 配置**
```go
// 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. **前端调用**
```vue
<!-- 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. **为什么不用独立表?**
**独立表的问题**:
```go
// ❌ 假设的独立表设计
type TURNConfig struct {
ID uint `gorm:"primaryKey"`
URL string
Username string
Password string
}
// 问题:
// 1. 永远只会有一条记录(浪费表结构)
// 2. 需要额外的 JOIN 查询
// 3. 增加数据库连接负担
// 4. 不符合"全局唯一配置"的语义
```
**内嵌设计的优势**:
```go
// ✅ 实际的内嵌设计
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) |
| **管理方式** | 系统设置页面 | 外部服务管理页面 |
**示例**:
```go
// 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` 会初始化默认值
---
### 🎯 **最佳实践**
**读取配置**:
```go
var setting model.SystemSetting
s.store.DB().First(&setting, 1)
// 直接使用 TURN 配置
if setting.TURNMode == "manual" {
useCustomTURN(setting.TURNURL)
}
```
**更新配置**:
```go
// 先获取,再修改,最后保存
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)
```
**前端展示**:
```javascript
// 注意密码不会返回(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`](file://e:\Project\MeshRay\internal\model\models.go#L110-L113)
*MeshRay - 清晰的架构设计!* 🏗️✨