# ExternalService 三层架构设计详解

## 概述

ExternalService 架构采用 **JSON 存储 + Schema 验证 + Struct 类型转换** 三层设计，实现了灵活、可扩展的外部服务管理体系。

```
┌─────────────────────────────────────────────────────────┐
│              ExternalService 三层架构                    │
│                                                         │
│  第一层：JSON 存储层（Database Layer）                  │
│  ┌─────────────────────────────────────────────────┐   │
│  │ external_services.Config (TEXT)                 │   │
│  │                                                 │   │
│  │ 优势：                                          │   │
│  │ ✅ 一张表容纳所有异构配置                        │   │
│  │ ✅ 不改表结构，支持无限扩展                      │   │
│  │ ✅ 向后兼容，旧数据不受影响                      │   │
│  └─────────────────────────────────────────────────┘   │
│                        ↓                                │
│  第二层：Schema 验证层（Validation Layer）              │
│  ┌─────────────────────────────────────────────────┐   │
│  │ JSON Schema                                     │   │
│  │                                                 │   │
│  │ 作用：                                          │   │
│  │ ✅ 前端动态表单渲染                              │   │
│  │ ✅ 输入验证（必填、格式、枚举、正则）            │   │
│  │ ✅ 前后端统一验证规则                            │   │
│  │ ✅ 零代码新增服务类型                            │   │
│  └─────────────────────────────────────────────────┘   │
│                        ↓                                │
│  第三层：Struct 类型转换层（Type Safety Layer）         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Go Struct + ValidateConfig() + BuildConfig()    │   │
│  │                                                 │   │
│  │ 作用：                                          │   │
│  │ ✅ 编译期类型检查                                │   │
│  │ ✅ 业务逻辑验证（比 Schema 更复杂）               │   │
│  │ ✅ 设置默认值                                    │   │
│  │ ✅ 返回标准接口（TransportConfig 等）             │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
```

---

## 第一层：JSON 存储层（Database Layer）

### 设计思想

传统的数据库设计要求每个服务类型单独建表：
- `stun_servers` 表
- `turn_servers` 表
- `ddns_configs` 表
- ...

**问题**：
1. 表越来越多，难以维护
2. 每次新增服务都需要改表结构
3. 历史数据迁移困难
4. 无法支持动态扩展

### 解决方案

使用一个 TEXT 字段存储 JSON：

```go
type ExternalService struct {
    ID            string     `gorm:"primaryKey;type:varchar(36)" json:"id"`
    Category      string     `gorm:"type:varchar(32);not null;index" json:"category"`
    ServiceType   string     `gorm:"type:varchar(64);not null;index" json:"serviceType"`
    Name          string     `gorm:"type:varchar(64);not null" json:"name"`
    Config        string     `gorm:"type:text;not null" json:"config"`  // ← JSON 字符串
    // ... 其他字段
}
```

### 示例数据

**FRP 穿透服务**：
```json
{
  "category": "networking",
  "serviceType": "frp_server",
  "name": "我的 FRP 服务器",
  "config": "{\"server_addr\":\"frp.example.com\",\"token\":\"xxx\"}"
}
```

**SSL ACME 证书**：
```json
{
  "category": "security",
  "serviceType": "ssl_acme",
  "name": "Let's Encrypt 证书",
  "config": "{\"ca_provider\":\"letsencrypt\",\"email\":\"admin@example.com\",\"domains\":[\"*.example.com\"]}"
}
```

**TURN 服务器（长期凭证）**：
```json
{
  "category": "networking",
  "serviceType": "turn_server",
  "name": "Coturn 服务器",
  "config": "{\"server_addr\":\"turn.example.com\",\"auth_type\":\"long_term\",\"long_term\":{\"username\":\"user\",\"password\":\"***\"}}"
}
```

### 优势总结

| 维度 | 传统方式 | JSON 存储 |
|------|---------|-----------|
| **表数量** | N 张表（每类服务一张） | 1 张表 |
| **扩展性** | ❌ 需要 ALTER TABLE | ✅ 无需改表 |
| **数据迁移** | ❌ 复杂且危险 | ✅ 无迁移成本 |
| **向后兼容** | ❌ 可能破坏旧数据 | ✅ 完全兼容 |

---

## 第二层：Schema 验证层（Validation Layer）

### 设计思想

如果只有 JSON 存储，用户可能会乱填数据。如何防止？

**错误做法**：后端手动校验每个字段
```go
// ❌ 不推荐
if config["server_addr"] == "" {
    return errors.New("服务器地址不能为空")
}
if config["token"] == "" {
    return errors.New("token 不能为空")
}
// ... 需要写几十个这样的校验
```

**正确做法**：JSON Schema 自动验证

### JSON Schema 是什么？

JSON Schema 是一种描述 JSON 数据结构的标准（RFC Draft），用于：
1. 验证 JSON 数据格式
2. 自动生成文档
3. 生成前端表单

### FRP 服务器配置 Schema

```go
func (p *FRPServerProvider) ConfigSchema() string {
    return `{
        "type": "object",
        "required": ["server_addr", "token"],
        "properties": {
            "server_addr": {
                "type": "string",
                "title": "FRP 服务器地址",
                "description": "FRP 服务器的域名或 IP 地址"
            },
            "server_port": {
                "type": "integer",
                "title": "服务器端口",
                "default": 7000,
                "minimum": 1,
                "maximum": 65535
            },
            "token": {
                "type": "string",
                "title": "认证令牌",
                "format": "password",
                "minLength": 8
            },
            "protocol": {
                "type": "string",
                "enum": ["tcp", "kcp", "quic"],
                "default": "tcp",
                "title": "传输协议"
            }
        }
    }`
}
```

### Schema 验证规则说明

| 关键字 | 作用 | 示例 |
|--------|------|------|
| `type` | 数据类型 | `"string"`, `"integer"`, `"boolean"`, `"array"`, `"object"` |
| `required` | 必填字段 | `["server_addr", "token"]` |
| `minimum` / `maximum` | 数值范围 | `minimum: 1, maximum: 65535` |
| `minLength` / `maxLength` | 字符串长度 | `minLength: 8` |
| `enum` | 枚举值 | `["tcp", "kcp", "quic"]` |
| `format` | 特殊格式 | `"email"`, `"uri"`, `"password"` |
| `pattern` | 正则表达式 | `"pattern": "^[a-z0-9.-]+$"` |
| `default` | 默认值 | `"default": 7000` |

### 前端动态表单渲染

**Vue 3 + Element Plus 实现**：

```vue
<template>
  <el-form :model="formData" label-width="120px">
    <!-- 根据 Schema 动态渲染字段 -->
    
    <!-- server_addr: string -->
    <el-form-item label="服务器地址" required>
      <el-input v-model="formData.server_addr" />
    </el-form-item>
    
    <!-- server_port: integer (带范围) -->
    <el-form-item label="服务器端口">
      <el-input-number 
        v-model="formData.server_port" 
        :min="1" 
        :max="65535" 
      />
    </el-form-item>
    
    <!-- token: string (密码格式) -->
    <el-form-item label="认证令牌" required>
      <el-input 
        v-model="formData.token" 
        type="password" 
        show-password 
      />
    </el-form-item>
    
    <!-- protocol: enum (下拉框) -->
    <el-form-item label="传输协议">
      <el-select v-model="formData.protocol">
        <el-option label="TCP" value="tcp" />
        <el-option label="KCP" value="kcp" />
        <el-option label="QUIC" value="quic" />
      </el-select>
    </el-form-item>
  </el-form>
</template>
```

### 自动验证流程

```javascript
// 前端验证（基于 Schema）
import Ajv from 'ajv'

const ajv = new Ajv()
const validate = ajv.compile(schema)

const valid = validate(formData)
if (!valid) {
  console.error(validate.errors)
  // [
  //   {
  //     instancePath: "/server_addr",
  //     message: "is required"
  //   },
  //   {
  //     instancePath: "/token",
  //     message: "must NOT have fewer than 8 characters"
  //   }
  // ]
}
```

### 优势总结

| 维度 | 手动验证 | Schema 验证 |
|------|---------|-------------|
| **代码量** | ❌ 每个服务写几十行验证 | ✅ 声明式定义 |
| **一致性** | ❌ 容易遗漏或矛盾 | ✅ 前后端统一 |
| **可维护性** | ❌ 分散在各处 | ✅ 集中管理 |
| **前端表单** | ❌ 硬编码每个表单 | ✅ 动态渲染 |
| **新增服务** | ❌ 前后端都要改 | ✅ 零代码 |

---

## 第三层：Struct 类型转换层（Type Safety Layer）

### 设计思想

虽然 JSON 很灵活，但 Go 是强类型语言。如何在业务逻辑中安全使用？

**错误做法**：全程使用 `map[string]interface{}`
```go
// ❌ 不推荐
func UseTURN(config map[string]interface{}) error {
    addr := config["server_addr"].(string)  // 类型断言，可能 panic
    port := config["server_port"].(int)     // 可能是 float64！
}
```

**正确做法**：转换为 Go Struct

### 定义配置结构体

```go
// internal/service_impl/networking/frp_server.go
type FRPConfig struct {
    ServerAddr string `json:"server_addr"`
    ServerPort int    `json:"server_port,omitempty"`
    Token      string `json:"token"`
    Protocol   string `json:"protocol,omitempty"` // tcp/kcp/quic
}
```

### ValidateConfig() 业务验证

```go
type FRPServerProvider struct{}

func (p *FRPServerProvider) ValidateConfig(configJSON string) error {
    var cfg FRPConfig
    if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
        return fmt.Errorf("配置解析失败：%w", err)
    }
    
    // 业务逻辑校验（比 Schema 更复杂）
    if cfg.ServerAddr == "" {
        return errors.New("服务器地址不能为空")
    }
    if cfg.Token == "" {
        return errors.New("认证令牌不能为空")
    }
    if len(cfg.Token) < 8 {
        return errors.New("认证令牌长度至少 8 位")
    }
    if cfg.Protocol != "" && !isValidProtocol(cfg.Protocol) {
        return fmt.Errorf("不支持的协议：%s", cfg.Protocol)
    }
    
    // 检查服务器是否可达
    conn, err := net.DialTimeout("tcp", cfg.ServerAddr+":7000", 5*time.Second)
    if err != nil {
        return fmt.Errorf("服务器不可达：%w", err)
    }
    conn.Close()
    
    return nil
}
```

### BuildConfig() 构建对象 + 设置默认值

```go
func (p *FRPServerProvider) BuildConfig(configJSON string) (*FRPConfig, error) {
    var cfg FRPConfig
    if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
        return nil, err
    }
    
    // 设置默认值
    if cfg.ServerPort == 0 {
        cfg.ServerPort = 7000  // 默认 7000
    }
    if cfg.Protocol == "" {
        cfg.Protocol = "tcp"   // 默认 TCP
    }
    
    return &cfg, nil
}
```

### 在业务逻辑中使用

```go
// internal/service/network.go
func (s *NetworkService) CreateRelay(config *FRPConfig) error {
    // 现在可以安全地使用强类型
    fmt.Printf("Connecting to %s:%d\n", config.ServerAddr, config.ServerPort)
    fmt.Printf("Using protocol: %s\n", config.Protocol)
    
    // 类型安全，编译器会检查
    tunnel := frp.NewTunnel(config.ServerAddr, config.ServerPort, config.Protocol)
    return tunnel.Connect(config.Token)
}
```

### 复杂场景：TURN 多种认证方式

```go
type TURNConfig struct {
    ServerAddr string `json:"server_addr"`
    Realm      string `json:"realm,omitempty"`
    
    // 认证方式（互斥）
    AuthType string          `json:"auth_type"` // long_term | short_term | auth_secret
    LongTerm *LongTermAuth   `json:"long_term,omitempty"`
    ShortTerm *ShortTermAuth `json:"short_term,omitempty"`
}

type LongTermAuth struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type ShortTermAuth struct {
    Username  string `json:"username"`
    AuthSecret string `json:"auth_secret"`
    ExpiresIn int    `json:"expires_in,omitempty"`
}

// GetCredentials 动态获取凭证
func (p *TURNServerProvider) GetCredentials(configJSON string) (Credentials, error) {
    var cfg TURNConfig
    json.Unmarshal([]byte(configJSON), &cfg)
    
    switch cfg.AuthType {
    case "long_term":
        // 返回固定的用户名密码
        return &LongTermCredentials{
            Username: cfg.LongTerm.Username,
            Password: cfg.LongTerm.Password,
        }, nil
        
    case "short_term":
        // 动态生成短期凭证（HMAC-SHA1）
        now := time.Now()
        expiry := now.Add(time.Duration(cfg.ShortTerm.ExpiresIn) * time.Second)
        
        hmac := hmac.New(sha1.New, []byte(cfg.ShortTerm.AuthSecret))
        hmac.Write([]byte(cfg.ShortTerm.Username))
        hmac.Write([]byte(now.Format(time.RFC3339)))
        password := base64.StdEncoding.EncodeToString(hmac.Sum(nil))
        
        return &ShortTermCredentials{
            Username:  cfg.ShortTerm.Username,
            Password:  password,
            ExpiresAt: expiry,
        }, nil
        
    default:
        return nil, errors.New("不支持的认证方式")
    }
}
```

### 优势总结

| 维度 | map[string]interface{} | Go Struct |
|------|------------------------|-----------|
| **类型安全** | ❌ 运行时才能发现错误 | ✅ 编译期检查 |
| **IDE 支持** | ❌ 没有自动补全 | ✅ 完整的智能提示 |
| **重构友好** | ❌ 容易遗漏 | ✅ 自动更新所有引用 |
| **文档化** | ❌ 字段含义不明确 | ✅ 注释即文档 |
| **默认值** | ❌ 需要手动处理 | ✅ 统一设置 |

---

## 完整使用流程示例

### 场景：创建 FRP 穿透服务

#### 步骤 1：用户选择服务类型

前端 UI：
```
请选择服务类型：
○ STUN 服务器
● FRP 穿透服务器
○ SSL 证书
○ 阿里云 DDNS
```

#### 步骤 2：前端请求 Schema

```javascript
// GET /services/schema/frp_server
const response = await fetch('/api/services/schema/frp_server')
const schema = await response.json()
// schema = {
//   "type": "object",
//   "required": ["server_addr", "token"],
//   "properties": {...}
// }
```

#### 步骤 3：前端动态渲染表单

```vue
<DynamicForm :schema="schema" v-model="formData" />
```

渲染结果：
```
┌─────────────────────────────────┐
│ FRP 服务器地址：[____________]   │
│ 服务器端口：    [7000      ]   │
│ 认证令牌：      [••••••••]     │
│ 传输协议：      [TCP ▼     ]   │
└─────────────────────────────────┘
```

#### 步骤 4：用户填写并提交

```javascript
formData = {
  server_addr: "frp.example.com",
  server_port: 7000,
  token: "mytoken123",
  protocol: "tcp"
}
```

#### 步骤 5：前端 Schema 验证

```javascript
const valid = validate(formData)
if (!valid) {
  showError(validate.errors)
  return
}
```

#### 步骤 6：发送到后端

```javascript
POST /api/services
{
  "category": "networking",
  "serviceType": "frp_server",
  "name": "我的 FRP 服务器",
  "config": formData
}
```

#### 步骤 7：后端 ValidateConfig()

```go
provider := registry.Get("frp_server")
err := provider.ValidateConfig(configJSON)
if err != nil {
    return err  // 返回 400 错误
}
```

#### 步骤 8：保存到数据库

```go
service := &ExternalService{
    Category:    "networking",
    ServiceType: "frp_server",
    Name:        "我的 FRP 服务器",
    Config:      configJSON,  // JSON 字符串
}
db.Create(service)
```

#### 步骤 9：业务逻辑使用

```go
// 后续使用时，通过 BuildConfig() 获取类型安全的对象
config, _ := provider.BuildConfig(service.Config)
fmt.Printf("FRP Server: %s:%d\n", config.ServerAddr, config.ServerPort)
// 输出：FRP Server: frp.example.com:7000
```

---

## 架构优势对比

### 新增 FRP 穿透服务

**传统方式（3 天）**：

1. 创建 `frp_servers` 表
   ```sql
   CREATE TABLE frp_servers (
     id VARCHAR(36) PRIMARY KEY,
     server_addr VARCHAR(255) NOT NULL,
     server_port INT DEFAULT 7000,
     token VARCHAR(255) NOT NULL,
     protocol VARCHAR(16) DEFAULT 'tcp',
     created_at TIMESTAMP,
     updated_at TIMESTAMP
   );
   ```

2. 编写 CRUD Handler
   ```go
   type FRPServerHandler struct {
       db *gorm.DB
   }
   
   func (h *FRPServerHandler) Create(c *gin.Context) {
       var req FRPServerRequest
       c.ShouldBindJSON(&req)
       
       server := &FRPServer{
           ServerAddr: req.ServerAddr,
           // ...
       }
       h.db.Create(server)
   }
   ```

3. 开发前端管理页面
   - `FRPList.vue` - 列表页
   - `FRPCreate.vue` - 创建页
   - `FRPEdit.vue` - 编辑页

4. 编写表单验证逻辑
   ```vue
   const rules = {
     server_addr: [{ required: true, message: '请输入服务器地址' }],
     token: [
       { required: true, message: '请输入认证令牌' },
       { min: 8, message: '长度至少 8 位' }
     ],
     // ...
   }
   ```

5. 测试 + 修改 Bug（约半天）

**总计**：约 15-20 小时

---

**三层架构（30 分钟）**：

1. 实现 `FRPServerProvider`
   ```go
   type FRPServerProvider struct{}
   
   func (p *FRPServerProvider) ConfigSchema() string {
       return `{...}`  // JSON Schema
   }
   
   func (p *FRPServerProvider) ValidateConfig(configJSON string) error {
       // 业务验证逻辑
   }
   ```

2. 注册到 Registry
   ```go
   func init() {
       DefaultRegistry.Register(&FRPServerProvider{})
   }
   ```

3. 完成！

**前端自动适配**：
- ✅ 自动获取 Schema
- ✅ 自动渲染表单
- ✅ 自动验证输入

**总计**：约 30 分钟

---

### 效果对比总结

| 维度 | 传统方式 | 三层架构 | 提升 |
|------|---------|----------|------|
| **开发时间** | 3 天 | 30 分钟 | **12 倍** |
| **数据库变更** | ✅ 需要 | ❌ 不需要 | - |
| **前端开发** | ✅ 需要 | ❌ 自动 | - |
| **代码复用** | ❌ 低 | ✅ 高 | - |
| **维护成本** | ❌ 高 | ✅ 低 | - |
| **扩展难度** | ❌ 困难 | ✅ 简单 | - |

---

## 总结

**三层架构的核心价值**：

1. **JSON 存储层** → 解决**灵活性**问题
   - 一张表容纳所有异构配置
   - 支持无限扩展，不改表结构

2. **Schema 验证层** → 解决**规范性**问题
   - 前后端统一验证规则
   - 动态表单渲染，零代码新增

3. **Struct 类型转换层** → 解决**安全性**问题
   - 编译期类型检查
   - 业务逻辑验证，默认值处理

**最终效果**：
- ✅ **开发效率提升 12 倍**
- ✅ **零代码新增服务类型**
- ✅ **前后端自动适配**
- ✅ **类型安全 + 业务验证**

这就是为什么我们需要 **JSON 存储 + Schema 验证 + Struct 类型转换** 三层架构！
