# 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 ``` ### 自动验证流程 ```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 ``` 渲染结果: ``` ┌─────────────────────────────────┐ │ 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 类型转换** 三层架构!