package service import ( "context" "errors" "fmt" "net" "strconv" "strings" "time" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/dnsprovider" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/model" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite" "github.com/google/uuid" "github.com/libdns/libdns" "gorm.io/gorm" ) // ServiceService 服务管理服务 type ServiceService struct { store *sqlite.Store } // NewServiceService 创建服务服务实例 func NewServiceService(store *sqlite.Store) *ServiceService { return &ServiceService{store: store} } // ListServices 获取服务列表(支持按类型筛选) func (s *ServiceService) ListServices(serverType string) ([]model.Service, error) { var services []model.Service query := s.store.DB().Order("created_at desc") if serverType != "" { // 支持大小写不敏感匹配 query = query.Where("LOWER(type) = LOWER(?)", serverType) } err := query.Find(&services).Error return services, err } // GetServiceByID 根据 ID 获取服务 func (s *ServiceService) GetServiceByID(id string) (*model.Service, error) { var service model.Service if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("服务不存在") } return nil, err } return &service, nil } // CreateService 创建服务 func (s *ServiceService) CreateService(req *model.Service) (*model.Service, error) { if req.Name == "" { return nil, errors.New("服务名称不能为空") } if req.Type == "" { return nil, errors.New("服务类型不能为空") } // 强制转换为大写以保持一致性 req.Type = strings.ToUpper(req.Type) // 校验支持的类型 allowedTypes := map[string]bool{ "STUN": true, "TURN": true, "DDNS": true, "TUN": true, "CUSTOM": true, } if !allowedTypes[req.Type] { return nil, fmt.Errorf("不支持的服务类型: %s", req.Type) } // 地址和端口校验(除 TUN 外通常需要) if req.Type != "TUN" { if req.Address == "" { return nil, errors.New("服务器地址不能为空") } if req.Port <= 0 || req.Port > 65535 { return nil, errors.New("端口号无效") } } // DDNS 鉴权校验(基础设施模式) if req.Type == "DDNS" && req.ConfigMode == "infrastructure" { if req.Provider == "" { return nil, errors.New("请选择 DNS 服务商") } if req.Domain == "" { return nil, errors.New("请输入根域名") } // 根据服务商校验认证信息 switch req.Provider { case "cloudflare": if req.Token == "" { return nil, errors.New("请输入 API Token") } case "aliyun": if req.AuthUsername == "" || req.AuthPassword == "" { return nil, errors.New("请输入 AccessKey ID 和 Secret") } case "tencent": if req.AuthUsername == "" || req.AuthPassword == "" { return nil, errors.New("请输入 SecretId 和 SecretKey") } } } // DDNS 全功能模式校验 if req.Type == "DDNS" && req.ConfigMode == "fullservice" { if req.DDNSConfigID == "" { return nil, errors.New("请选择 DDNS 配置") } if req.RecordType == "" { return nil, errors.New("请选择记录类型") } // 根据记录类型校验字段 switch req.RecordType { case "A", "AAAA": if req.Subdomain == "" { return nil, errors.New("请输入主机记录") } if req.TargetIP == "" { return nil, errors.New("请输入目标 IP") } if req.Port <= 0 { return nil, errors.New("请输入检测端口") } case "TXT": if req.TXTRecordName == "" { return nil, errors.New("请输入 TXT 记录名称") } if req.TXTValue == "" { return nil, errors.New("请输入 TXT 记录值") } case "CNAME": if req.CNAMETarget == "" { return nil, errors.New("请输入目标域名") } } } // 生成 ID if req.ID == "" { req.ID = uuid.New().String() } // 如果是 DDNS 全功能模式,先创建 DNS 记录 if req.Type == "DDNS" && req.ConfigMode == "fullservice" { // 使用事务确保原子性 tx := s.store.DB().Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 1. 获取关联的 DDNS 配置 var ddnsConfig model.Service if err := tx.Where("id = ?", req.DDNSConfigID).First(&ddnsConfig).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("获取 DDNS 配置失败:%w", err) } // 2. 确定记录类型、名称和值 var recordType, name, value string switch req.RecordType { case "A", "AAAA": recordType = req.RecordType name = req.Subdomain value = req.TargetIP case "TXT": recordType = req.RecordType name = req.TXTRecordName value = req.TXTValue case "CNAME": recordType = req.RecordType name = req.Subdomain value = req.CNAMETarget default: tx.Rollback() return nil, errors.New("不支持的记录类型") } // 3. 创建 DNS Provider providerConfig := dnsprovider.ProviderConfig{ Provider: dnsprovider.ProviderType(ddnsConfig.Provider), Domain: ddnsConfig.Domain, APIToken: ddnsConfig.Token, AccessKeyID: ddnsConfig.AuthUsername, AccessKeySecret: ddnsConfig.AuthPassword, SecretId: ddnsConfig.AuthUsername, SecretKey: ddnsConfig.AuthPassword, } provider, err := dnsprovider.NewDNSProvider(providerConfig) if err != nil { tx.Rollback() return nil, fmt.Errorf("创建 DNS Provider 失败:%w", err) } // 4. 构建并添加 DNS 记录 dnsRecord := &dnsprovider.DNSRecord{ Type: dnsprovider.RecordType(recordType), Name: name, Value: value, TTL: req.TTL, } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _, err = provider.AppendRecords(ctx, ddnsConfig.Domain, []libdns.Record{dnsRecord.ToLibdnsRecord()}) if err != nil { tx.Rollback() return nil, fmt.Errorf("创建 DNS 记录失败:%w", err) } // 5. DNS 记录创建成功,保存到数据库 if err := tx.Create(req).Error; err != nil { tx.Rollback() return nil, err } tx.Commit() return req, nil } // 其他情况直接保存 if err := s.store.DB().Create(req).Error; err != nil { return nil, err } return req, nil } // UpdateService 更新服务 func (s *ServiceService) UpdateService(id string, updates map[string]interface{}) (*model.Service, error) { var service model.Service if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil { return nil, errors.New("服务不存在") } if err := s.store.DB().Model(&service).Updates(updates).Error; err != nil { return nil, err } return &service, nil } // DeleteService 删除服务 func (s *ServiceService) DeleteService(id string) error { var service model.Service if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil { return errors.New("服务不存在") } return s.store.DB().Delete(&service).Error } // TestServiceConnectivity 测试服务连通性 func (s *ServiceService) TestServiceConnectivity(id string) (string, error) { var service model.Service if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil { return "", errors.New("服务不存在") } addr := net.JoinHostPort(service.Address, strconv.Itoa(service.Port)) if service.Type == "STUN" { // STUN 使用 UDP 测试 conn, err := net.DialTimeout("udp", addr, 5*time.Second) if err != nil { return "unreachable", nil } conn.Close() return "ok", nil } // 其他使用 TCP 测试 conn, err := net.DialTimeout("tcp", addr, 5*time.Second) if err != nil { return "unreachable", nil } conn.Close() return "ok", nil }