Files
Meshray-Manager/internal/service/service.go
T
zkcoi e9ca2f7d70 fix: rename manager module to git.zkcoi.com/zkcoi/Meshray-Manager
- 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
2026-07-15 16:25:46 +08:00

285 lines
7.5 KiB
Go

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
}