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

690 lines
20 KiB
Markdown
Raw 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.
# 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 类型转换** 三层架构!