- go.mod module path matches repo zkcoi/Meshray-Manager (case-distinct from zkcoi/Meshray/core)
- rewrite internal imports meshray/{internal,web,pkg} -> Meshray-Manager/... (core refs kept)
- sync README.md / install.sh repo URLs; add CHANGELOG entry
314 lines
8.6 KiB
Go
314 lines
8.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
|
|
"git.zkcoi.com/zkcoi/Meshray-Manager/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
|
|
}
|