# DDNS Usage 前缀定义策略

**设计时间**: 2026-03-26  
**核心问题**: 如何定义和管理 TXT 记录前缀  

---

## 🎯 问题背景

### 当前需求

```
同一个 DDNS 配置（如 example.com）需要支持多个组网同步:
├─ 组网 A → _meshray._mesh.network-a.example.com
├─ 组网 B → _meshray._mesh.network-b.example.com
└─ 组网 C → _custom.prefix.network-c.example.com

问题：前缀 (_meshray._mesh) 如何定义？谁来决定？
```

---

## ✅ 三种设计方案

### 方案一：系统预设固定前缀（推荐）⭐

**设计思路**:
```
系统内置标准前缀，用户不可自定义
├─ MeshSeed 同步专用：_meshray._mesh
├─ 未来扩展 1: _meshray.config (配置同步)
└─ 未来扩展 2: _meshray.device (设备注册)
```

**优点**:
- ✅ 标准化，避免混乱
- ✅ 安全性高（防止恶意前缀）
- ✅ 实现简单

**缺点**:
- ❌ 灵活性较低
- ❌ 无法适配特殊场景

**数据库设计**:
```go
// DDNSUsage 模型 - 前缀字段枚举化
type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 A：预设前缀类型（枚举）
    PrefixType   string     `gorm:"type:varchar(32);not null"` 
    /*
    可选值:
    - "MESHSEED_SYNC" → 对应 "_meshray._mesh"
    - "CONFIG_SYNC"   → 对应 "_meshray.config"
    - "DEVICE_REG"    → 对应 "_meshray.device"
    */
    
    RecordPrefix string     `gorm:"-"` // 计算字段，不存储
    FullDomain   string     // 自动生成
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 方法：获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
    switch u.PrefixType {
    case "MESHSEED_SYNC":
        return "_meshray._mesh"
    case "CONFIG_SYNC":
        return "_meshray.config"
    case "DEVICE_REG":
        return "_meshray.device"
    default:
        panic("未知的前缀类型：" + u.PrefixType)
    }
}
```

**前端实现**:
```vue
<!-- 创建 Usage 时只能选择预设类型 -->
<el-form-item label="用途类型" prop="prefix_type">
  <el-select v-model="formData.prefix_type" placeholder="请选择">
    <el-option 
      label="MeshSeed 同步 (_meshray._mesh)" 
      value="MESHSEED_SYNC" 
    />
    <el-option 
      label="配置同步 (_meshray.config)" 
      value="CONFIG_SYNC"
      disabled 
    />
    <el-option 
      label="设备注册 (_meshray.device)" 
      value="DEVICE_REG"
      disabled 
    />
  </el-select>
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    当前仅支持 MeshSeed 同步，其他功能开发中
  </div>
</el-form-item>
```

---

### 方案二：管理员自定义前缀（灵活）🔧

**设计思路**:
```
管理员创建 Usage 时自由填写前缀
├─ 示例 1: _meshray._mesh
├─ 示例 2: _custom.prefix
└─ 示例 3: anything.you.want
```

**优点**:
- ✅ 灵活性极高
- ✅ 适配各种场景

**缺点**:
- ❌ 容易冲突（需要验证唯一性）
- ❌ 安全性风险（可能输入恶意前缀）
- ❌ 用户学习成本高

**数据库设计**:
```go
type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 B：完全自定义前缀
    RecordPrefix string     `gorm:"type:varchar(255);not null"`
    /*
    示例:
    - "_meshray._mesh"
    - "_custom.test"
    - "anything"
    */
    
    // 格式验证
    validator func(string) error
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 验证函数
func validateRecordPrefix(prefix string) error {
    if prefix == "" {
        return fmt.Errorf("前缀不能为空")
    }
    
    // DNS 标签规则
    if len(prefix) > 253 {
        return fmt.Errorf("前缀过长（最大 253 字符）")
    }
    
    // 只能包含字母、数字、连字符、点
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, prefix)
    if !matched {
        return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
    }
    
    // 不能以特殊字符开头
    if strings.HasPrefix(prefix, "_") && !strings.HasPrefix(prefix, "_meshray") {
        return fmt.Errorf("下划线前缀仅限系统使用")
    }
    
    return nil
}
```

**前端实现**:
```vue
<el-form-item label="TXT 记录前缀" prop="record_prefix">
  <el-input
    v-model="formData.record_prefix"
    placeholder="_meshray._mesh"
    maxlength="253"
  />
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    完整记录名：{{ formData.record_prefix }}.网络名称。域名
  </div>
  <div class="form-tip">
    <el-icon><WarningFilled /></el-icon>
    只能包含字母、数字、点、下划线和连字符
  </div>
</el-form-item>
```

---

### 方案三：混合模式（最佳实践）🏆

**设计思路**:
```
系统预设 + 管理员自定义组合
├─ 预设类型：快速选择，安全可靠
└─ 自定义：高级选项，满足特殊需求
```

**数据库设计**:
```go
type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 C：混合模式
    PrefixMode   string     `gorm:"type:varchar(16);not null;default:'preset'"`
    /*
    - "preset"    → 使用预设类型
    - "custom"    → 使用自定义前缀
    */
    
    PrefixType   string     `gorm:"type:varchar(32)"`          // preset 模式下使用
    RecordPrefix string     `gorm:"type:varchar(255)"`         // custom 模式下使用
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 方法：获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
    if u.PrefixMode == "preset" {
        return u.GetPresetPrefix()
    } else {
        return u.RecordPrefix
    }
}

func (u *DDNSUsage) GetPresetPrefix() string {
    switch u.PrefixType {
    case "MESHSEED_SYNC":
        return "_meshray._mesh"
    case "CONFIG_SYNC":
        return "_meshray.config"
    default:
        return "_meshray._mesh" // 默认回退
    }
}
```

**前端实现**:
```vue
<el-form-item label="前缀模式" prop="prefix_mode">
  <el-radio-group v-model="formData.prefix_mode">
    <el-radio label="preset">系统预设</el-radio>
    <el-radio label="custom">自定义</el-radio>
  </el-radio-group>
</el-form-item>

<!-- 预设模式 -->
<el-form-item v-if="formData.prefix_mode === 'preset'" label="预设类型">
  <el-select v-model="formData.prefix_type" placeholder="请选择">
    <el-option 
      label="✨ MeshSeed 同步 (_meshray._mesh)" 
      value="MESHSEED_SYNC" 
    />
    <el-option 
      label="🔧 配置同步 (_meshray.config)" 
      value="CONFIG_SYNC"
      disabled
    />
  </el-select>
</el-form-item>

<!-- 自定义模式 -->
<el-form-item v-else label="TXT 记录前缀">
  <el-input
    v-model="formData.record_prefix"
    placeholder="例如：my.custom.prefix"
    maxlength="253"
  />
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    需符合 DNS 命名规范
  </div>
</el-form-item>
```

---

## 🎯 推荐方案：混合模式

### 理由

1. **兼顾安全与灵活** ✅
   - 默认使用预设，避免错误
   - 允许高级用户自定义

2. **渐进式扩展** ✅
   - 初期只有 MeshSeed 同步
   - 后续可增加其他用途

3. **用户体验好** ✅
   - 普通用户选预设即可
   - 专家用户可以深度定制

---

## 🔧 完整实现（混合模式）

### 后端验证逻辑

```go
// internal/service/ddns_usage.go

// CreateUsage 创建 DDNS 使用方式
func (s *DDNSService) CreateUsage(ctx context.Context, req CreateUsageRequest) (*model.DDNSUsage, error) {
    // 1. 验证 DDNS 服务存在
    var service model.ExternalService
    if err := s.db.First(&service, req.ServiceID).Error; err != nil {
        return nil, fmt.Errorf("DDNS 服务不存在")
    }
    
    // 2. 根据模式验证前缀
    var recordPrefix string
    if req.PrefixMode == "preset" {
        // 预设模式：验证类型合法性
        switch req.PrefixType {
        case "MESHSEED_SYNC":
            recordPrefix = "_meshray._mesh"
        default:
            return nil, fmt.Errorf("未知的预设类型")
        }
    } else if req.PrefixMode == "custom" {
        // 自定义模式：严格验证格式
        if err := validateCustomPrefix(req.RecordPrefix); err != nil {
            return nil, err
        }
        recordPrefix = req.RecordPrefix
    } else {
        return nil, fmt.Errorf("无效的前缀模式")
    }
    
    // 3. 解析域名
    var config map[string]interface{}
    json.Unmarshal([]byte(service.Config), &config)
    domain, _ := config["domain"].(string)
    if domain == "" {
        return nil, fmt.Errorf("DDNS 配置缺少域名")
    }
    
    // 4. 创建 Usage
    usage := &model.DDNSUsage{
        ID:           generateUUID(),
        ServiceID:    req.ServiceID,
        PrefixMode:   req.PrefixMode,
        PrefixType:   req.PrefixType,
        RecordPrefix: recordPrefix,
        FullDomain:   fmt.Sprintf("%s.%s", recordPrefix, domain),
        Enabled:      true,
    }
    
    // 5. 检查是否重复（同一域名 + 前缀组合）
    var count int64
    s.db.Model(&model.DDNSUsage{}).
        Where("service_id = ? AND record_prefix = ? AND network_id IS NOT NULL",
            req.ServiceID, recordPrefix).
        Count(&count)
    
    if count > 0 {
        return nil, fmt.Errorf("该前缀已被其他组网占用")
    }
    
    // 6. 保存
    if err := s.db.Create(usage).Error; err != nil {
        return nil, fmt.Errorf("创建失败：%w", err)
    }
    
    return usage, nil
}

// 自定义前缀验证
func validateCustomPrefix(prefix string) error {
    if prefix == "" {
        return fmt.Errorf("前缀不能为空")
    }
    
    if len(prefix) > 253 {
        return fmt.Errorf("前缀过长")
    }
    
    // DNS 标签规范
    if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(prefix) {
        return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
    }
    
    // 保留前缀检查
    if strings.HasPrefix(prefix, "_meshray.") && prefix != "_meshray._mesh" {
        return fmt.Errorf("_meshray.* 前缀为系统保留")
    }
    
    return nil
}
```

---

### 前端完整表单

```vue
<!-- CreateUsageDialog.vue -->
<template>
  <el-dialog title="创建 DDNS 使用方式" v-model="visible">
    <el-form :model="form" label-width="120px">
      
      <!-- 选择 DDNS 配置 -->
      <el-form-item label="DDNS 服务" required>
        <el-select v-model="form.service_id" filterable style="width: 100%">
          <el-option
            v-for="svc in ddnsServices"
            :key="svc.id"
            :label="`${svc.name} (${svc.config.domain})`"
            :value="svc.id"
          />
        </el-select>
      </el-form-item>
      
      <!-- 前缀模式 -->
      <el-form-item label="前缀模式" required>
        <el-radio-group v-model="form.prefix_mode">
          <el-radio label="preset">
            ✨ 系统预设
            <span class="radio-desc">推荐使用，安全可靠</span>
          </el-radio>
          <el-radio label="custom">
            🔧 自定义
            <span class="radio-desc">高级选项，需谨慎填写</span>
          </el-radio>
        </el-radio-group>
      </el-form-item>
      
      <!-- 预设类型 -->
      <el-form-item v-if="form.prefix_mode === 'preset'" label="预设类型" required>
        <el-select v-model="form.prefix_type" style="width: 100%">
          <el-option 
            label="✨ MeshSeed 同步 (_meshray._mesh)" 
            value="MESHSEED_SYNC" 
          >
            <div style="display: flex; justify-content: space-between;">
              <span>MeshSeed 同步</span>
              <el-tag size="small" type="success">推荐</el-tag>
            </div>
            <div class="option-desc">用于组网配置加密同步到 DNS TXT 记录</div>
          </el-option>
          <el-option 
            label="🔧 配置同步 (_meshray.config)" 
            value="CONFIG_SYNC"
            disabled
          >
            <div class="option-desc">即将支持，敬请期待</div>
          </el-option>
        </el-select>
      </el-form-item>
      
      <!-- 自定义前缀 -->
      <el-form-item v-else label="TXT 记录前缀" required>
        <el-input
          v-model="form.record_prefix"
          placeholder="例如：my.custom.prefix"
          maxlength="253"
          show-word-limit
        />
        <div class="form-tip">
          <el-icon><InfoFilled /></el-icon>
          完整记录名：{{ form.record_prefix }}.网络名称。域名
        </div>
        <div class="form-tip">
          <el-icon><WarningFilled /></el-icon>
          只能包含字母、数字、点、下划线和连字符
        </div>
        <div class="form-tip">
          <el-icon><WarningFilled /></el-icon>
          _meshray.* 前缀为系统保留，不能使用
        </div>
      </el-form-item>
      
      <!-- 用途说明 -->
      <el-form-item label="用途说明">
        <el-input
          v-model="form.description"
          type="textarea"
          :rows="2"
          placeholder="描述这个用途，如：办公网络 MeshSeed 同步"
        />
      </el-form-item>
      
    </el-form>
    
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit">创建</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
const form = ref({
  service_id: '',
  prefix_mode: 'preset',
  prefix_type: 'MESHSEED_SYNC',
  record_prefix: '',
  description: ''
})

const handleSubmit = async () => {
  try {
    await createDDNSUsage(form.value)
    ElMessage.success('创建成功')
    emit('success')
    visible.value = false
  } catch (error: any) {
    ElMessage.error('创建失败：' + error.message)
  }
}
</script>
```

---

## 📊 实际应用示例

### 场景 1: 标准企业用户（使用预设）

```
公司 IT 管理员配置:
1. 添加 DDNS 服务
   └─ Cloudflare + mesh.company.com
   
2. 创建 Usage（预设模式）
   └─ 类型：MeshSeed 同步 (_meshray._mesh)
   
3. 创建组网 A
   └─ 选择 Usage → _meshray._mesh.office.mesh.company.com
   
4. 创建组网 B
   └─ 选择 Usage → _meshray._mesh.dev.mesh.company.com
   
结果:
✅ 自动分配不同子域名
✅ 不会冲突
✅ 管理简单
```

---

### 场景 2: 高级用户（自定义前缀）

```
技术专家配置:
1. 添加 DDNS 服务
   └─ Cloudflare + example.com
   
2. 创建 Usage（自定义模式）
   └─ 前缀：prod.meshray.sync
   
3. 创建生产环境组网
   └─ 选择 Usage → prod.meshray.sync.prod-net.example.com
   
4. 创建第二个 Usage
   └─ 前缀：test.meshray.sync
   
5. 创建测试环境组网
   └─ 选择 Usage → test.meshray.sync.test-net.example.com
   
结果:
✅ 环境隔离清晰
✅ 命名规范自主定义
✅ 灵活性极高
```

---

## ✅ 最终建议

**采用混合模式**:

1. **默认引导用户使用预设** (90% 场景)
   - MeshSeed 同步专用前缀：`_meshray._mesh`
   - 安全、标准、无需思考

2. **提供自定义入口** (10% 高级场景)
   - 严格验证格式
   - 保留系统前缀
   - 防止冲突

3. **未来扩展预留**
   - `_meshray.config` - 配置同步
   - `_meshray.device` - 设备注册
   - `_meshray.log` - 日志投递

这样既保证了大多数用户的易用性，又满足了高级用户的灵活性需求！🎯

需要我立即开始实现吗？
