Files
Meshray-Manager/docs/ExternalService 三层架构设计.md
T
2026-06-30 15:14:37 +08:00

20 KiB
Raw Blame History

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:

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 穿透服务

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

SSL ACME 证书

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

TURN 服务器(长期凭证)

{
  "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 存储,用户可能会乱填数据。如何防止?

错误做法:后端手动校验每个字段

// ❌ 不推荐
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

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 实现

<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>

自动验证流程

// 前端验证(基于 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{}

// ❌ 不推荐
func UseTURN(config map[string]interface{}) error {
    addr := config["server_addr"].(string)  // 类型断言,可能 panic
    port := config["server_port"].(int)     // 可能是 float64
}

正确做法:转换为 Go Struct

定义配置结构体

// 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() 业务验证

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() 构建对象 + 设置默认值

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
}

在业务逻辑中使用

// 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 多种认证方式

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

// 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:前端动态渲染表单

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

渲染结果:

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

步骤 4:用户填写并提交

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

步骤 5:前端 Schema 验证

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

步骤 6:发送到后端

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

步骤 7:后端 ValidateConfig()

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

步骤 8:保存到数据库

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

步骤 9:业务逻辑使用

// 后续使用时,通过 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

    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

    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. 编写表单验证逻辑

    const rules = {
      server_addr: [{ required: true, message: '请输入服务器地址' }],
      token: [
        { required: true, message: '请输入认证令牌' },
        { min: 8, message: '长度至少 8 位' }
      ],
      // ...
    }
    
  5. 测试 + 修改 Bug(约半天)

总计:约 15-20 小时


三层架构(30 分钟)

  1. 实现 FRPServerProvider

    type FRPServerProvider struct{}
    
    func (p *FRPServerProvider) ConfigSchema() string {
        return `{...}`  // JSON Schema
    }
    
    func (p *FRPServerProvider) ValidateConfig(configJSON string) error {
        // 业务验证逻辑
    }
    
  2. 注册到 Registry

    func init() {
        DefaultRegistry.Register(&FRPServerProvider{})
    }
    
  3. 完成!

前端自动适配

  • 自动获取 Schema
  • 自动渲染表单
  • 自动验证输入

总计:约 30 分钟


效果对比总结

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

总结

三层架构的核心价值

  1. JSON 存储层 → 解决灵活性问题

    • 一张表容纳所有异构配置
    • 支持无限扩展,不改表结构
  2. Schema 验证层 → 解决规范性问题

    • 前后端统一验证规则
    • 动态表单渲染,零代码新增
  3. Struct 类型转换层 → 解决安全性问题

    • 编译期类型检查
    • 业务逻辑验证,默认值处理

最终效果

  • 开发效率提升 12 倍
  • 零代码新增服务类型
  • 前后端自动适配
  • 类型安全 + 业务验证

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