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
+313
View File
@@ -0,0 +1,313 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/pkg/shortid"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DDNSUsageHandler DDNS Usage 管理 Handler
type DDNSUsageHandler struct {
db *gorm.DB
logger *zap.Logger
}
// NewDDNSUsageHandler 创建 DDNS Usage Handler
func NewDDNSUsageHandler(db *gorm.DB, logger *zap.Logger) *DDNSUsageHandler {
return &DDNSUsageHandler{
db: db,
logger: logger,
}
}
// CreateUsageRequest 创建 Usage 请求
type CreateUsageRequest struct {
ServiceID string `json:"service_id"` // DDNS 服务 ID
PrefixMode string `json:"prefix_mode"` // "auto" | "custom"
CustomPrefix string `json:"custom_prefix"` // 自定义前缀(仅 custom 模式需要)
NetworkID uint64 `json:"network_id"` // 绑定的网络 ID
NetworkName string `json:"network_name"` // 网络名称(用于显示)
}
// CreateUsage 创建 DDNS Usage
// POST /api/v1/ddns/usages
func (h *DDNSUsageHandler) CreateUsage(c *gin.Context) {
var req CreateUsageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 验证必填字段
if req.ServiceID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择 DDNS 服务"})
return
}
if req.PrefixMode != "auto" && req.PrefixMode != "custom" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的前缀模式"})
return
}
// 开启事务
tx := h.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 验证 DDNS 服务存在
var service model.ExternalService
if err := tx.First(&service, req.ServiceID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusNotFound, gin.H{"error": "DDNS 服务不存在"})
return
}
// 2. 解析配置获取域名
var config map[string]interface{}
if err := json.Unmarshal([]byte(service.Config), &config); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析 DDNS 配置失败"})
return
}
domain, _ := config["domain"].(string)
if domain == "" {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "DDNS 配置缺少域名"})
return
}
// 3. 根据模式生成前缀
var recordPrefix string
if req.PrefixMode == "auto" {
// ✅ 算法生成:Base64(NetworkID)
recordPrefix = shortid.EncodeID(req.NetworkID)
h.logger.Debug("自动生成前缀",
zap.Uint64("network_id", req.NetworkID),
zap.String("prefix", recordPrefix))
} else if req.PrefixMode == "custom" {
// ✅ 用户自定义:验证格式 + 检测占用
if err := validateCustomPrefix(req.CustomPrefix); err != nil {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检测是否被占用
var count int64
tx.Model(&model.DDNSUsage{}).
Where("service_id = ? AND record_prefix = ?", req.ServiceID, req.CustomPrefix).
Count(&count)
if count > 0 {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "该前缀已被其他组网占用"})
return
}
recordPrefix = req.CustomPrefix
h.logger.Debug("使用自定义前缀",
zap.String("prefix", req.CustomPrefix))
}
// 4. 创建 Usage 记录
usage := &model.DDNSUsage{
ID: uuid.New().String(),
ProviderID: req.ServiceID, // ✅ 使用 ProviderID 字段
UsageType: "meshseed_sync", // MeshSeed 同步用途
PrefixMode: req.PrefixMode,
RecordPrefix: recordPrefix,
RecordType: "TXT", // MeshSeed 同步固定使用 TXT 记录
Description: fmt.Sprintf("MeshSeed 同步 - %s", req.NetworkName),
IsExclusive: true, // 独占性,一个前缀只能绑定一个网络
}
if err := tx.Create(usage).Error; err != nil {
tx.Rollback()
h.logger.Error("创建 Usage 失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败:" + err.Error()})
return
}
// 5. 创建绑定关系
binding := &model.NetworkDDNSBinding{
ID: uuid.New().String(),
NetworkID: req.NetworkID,
UsageID: usage.ID,
ProviderID: req.ServiceID,
Status: "active",
}
if err := tx.Create(binding).Error; err != nil {
tx.Rollback()
h.logger.Error("创建绑定关系失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建绑定失败:" + err.Error()})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
h.logger.Error("事务提交失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交失败"})
return
}
// 构造完整域名
fullDomain := fmt.Sprintf("_meshray.%s.%s", recordPrefix, domain)
h.logger.Info("DDNS Usage 创建成功",
zap.String("usage_id", usage.ID),
zap.Uint64("network_id", req.NetworkID),
zap.String("full_domain", fullDomain))
c.JSON(http.StatusOK, gin.H{
"message": "创建成功",
"data": gin.H{
"id": usage.ID,
"provider_id": usage.ProviderID,
"prefix_mode": usage.PrefixMode,
"record_prefix": recordPrefix,
"full_domain": fullDomain,
"network_id": req.NetworkID,
},
})
}
// CheckPrefixOccupied 检查前缀是否被占用
// GET /api/v1/ddns/check-prefix?service_id=xxx&prefix=yyy
func (h *DDNSUsageHandler) CheckPrefixOccupied(c *gin.Context) {
serviceID := c.Query("service_id")
prefix := c.Query("prefix")
if serviceID == "" || prefix == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必要参数"})
return
}
// 查询占用情况
var count int64
h.db.Model(&model.DDNSUsage{}).
Where("service_id = ? AND record_prefix = ?", serviceID, prefix).
Count(&count)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"occupied": count > 0,
"count": count,
},
})
}
// GetAvailableUsages 获取可用的 DDNS Usage 列表
// GET /api/v1/ddns/usages/available
func (h *DDNSUsageHandler) GetAvailableUsages(c *gin.Context) {
serviceID := c.Query("service_id")
// 查询所有 TXT 类型的 Usage
query := h.db.Where("record_type = ?", "TXT").
Preload("Provider")
if serviceID != "" {
query = query.Where("service_id = ?", serviceID)
}
var usages []model.DDNSUsage
if err := query.Find(&usages).Error; err != nil {
h.logger.Error("查询 Usage 列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
// 构造返回数据
type UsageVO struct {
ID string `json:"id"`
ProviderID string `json:"provider_id"`
PrefixMode string `json:"prefix_mode"`
RecordPrefix string `json:"record_prefix"`
RecordType string `json:"record_type"`
Description string `json:"description"`
IsOccupied bool `json:"is_occupied"`
NetworkID *uint64 `json:"network_id,omitempty"`
FullDomain string `json:"full_domain"`
Provider interface{} `json:"provider,omitempty"`
}
result := make([]UsageVO, 0, len(usages))
for _, usage := range usages {
// 获取完整域名
var provider model.ExternalService
h.db.First(&provider, usage.ProviderID)
var config map[string]interface{}
json.Unmarshal([]byte(provider.Config), &config)
domain, _ := config["domain"].(string)
fullDomain := fmt.Sprintf("_meshray.%s.%s", usage.RecordPrefix, domain)
// 从 NetworkDDNSBinding 表查询关联的 Network ID
var networkID *uint64
var binding model.NetworkDDNSBinding
if err := h.db.Where("ddns_usage_id = ?", usage.ID).First(&binding).Error; err == nil {
networkID = &binding.NetworkID
}
vo := UsageVO{
ID: usage.ID,
ProviderID: usage.ProviderID,
PrefixMode: usage.PrefixMode,
RecordPrefix: usage.RecordPrefix,
RecordType: usage.RecordType,
Description: usage.Description,
IsOccupied: usage.IsExclusive,
NetworkID: networkID,
FullDomain: fullDomain,
}
if usage.Provider != nil {
vo.Provider = gin.H{
"id": usage.Provider.ID,
"name": usage.Provider.Name,
}
}
result = append(result, vo)
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// validateCustomPrefix 验证自定义前缀
func validateCustomPrefix(prefix string) error {
if prefix == "" {
return fmt.Errorf("前缀不能为空")
}
if len(prefix) > 63 {
return fmt.Errorf("前缀不能超过 63 个字符")
}
// DNS 标签规范
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, prefix)
if !matched {
return fmt.Errorf("前缀只能包含字母、数字、下划线和连字符")
}
// 系统保留字检查
if strings.HasPrefix(prefix, "_meshray") {
return fmt.Errorf("_meshray.* 前缀为系统保留,请使用自动生成模式")
}
return nil
}