Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+689
View File
@@ -0,0 +1,689 @@
# 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 类型转换** 三层架构!