Files
aiops/backend/service/service.go
T
Logan.Li 83662487cb fix: 接通服务器V2排序链路
- 将排序参数从V2 Handler传递至Service与Repository

- 增加仓储分页排序与参数白名单测试
2026-07-26 23:38:06 +08:00

682 lines
17 KiB
Go

// Package service 提供业务逻辑层 — 编排 repository、处理加密/校验/转换
// Handler 不直接调用 repository,通过 service 调用
package service
import (
"encoding/json"
"fmt"
"math"
"ezops/dto"
"ezops/model"
"ezops/pkg/crypto"
ezredis "ezops/pkg/redis"
"ezops/repository"
"gorm.io/gorm"
)
// ━━━ Server Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
type ServerService struct {
repo *repository.ServerRepository
cache ezredis.Cache
crypt *crypto.SecretBox
}
func NewServerService(repo *repository.ServerRepository, cache ezredis.Cache, crypt *crypto.SecretBox) *ServerService {
return &ServerService{repo: repo, cache: cache, crypt: crypt}
}
// ListServers 获取服务器列表
func (s *ServerService) ListServers(keyword, status, regionID, sortField, sortOrder string, page, pageSize int) ([]model.Server, int64, error) {
list, total, err := s.repo.List(keyword, status, regionID, sortField, sortOrder, page, pageSize)
if err != nil {
return nil, 0, err
}
// 填充计算字段
for i := range list {
list[i].HasAuth = list[i].Password != "" || list[i].SSHKey != ""
}
return list, total, nil
}
// GetServer 获取服务器详情
func (s *ServerService) GetServer(id uint) (*model.Server, error) {
server, err := s.repo.GetByID(id)
if err != nil {
return nil, err
}
server.HasAuth = server.Password != "" || server.SSHKey != ""
return server, nil
}
// CreateServer 创建服务器 — 加密凭证
func (s *ServerService) CreateServer(req *dto.CreateServerRequest) (*model.Server, error) {
srv := &model.Server{
Hostname: req.Hostname,
IP: req.IP,
PublicIP: req.PublicIP,
Port: req.Port,
Username: req.Username,
AuthType: req.AuthType,
OSType: req.OSType,
Protocol: req.Protocol,
RDPPort: req.RDPPort,
OS: req.OS,
OSVersion: req.OSVersion,
Arch: req.Arch,
CPU: req.CPU,
Memory: req.Memory,
Disk: req.Disk,
RegionID: req.RegionID,
CloudID: req.CloudID,
Tags: req.Tags,
Description: req.Description,
}
if req.Port == 0 {
srv.Port = 22
}
if req.Username == "" {
srv.Username = "root"
}
if req.AuthType == "" {
srv.AuthType = "password"
}
if srv.OSType == "" {
srv.OSType = "linux"
}
if srv.Protocol == "" {
srv.Protocol = "ssh"
}
if srv.Protocol == "rdp" {
srv.OSType = "windows"
if srv.RDPPort == 0 {
srv.RDPPort = 3389
}
}
// 加密密码和 SSH 密钥
if req.Password != "" {
enc, err := s.crypt.Encrypt(req.Password)
if err != nil {
return nil, fmt.Errorf("加密密码失败: %w", err)
}
srv.Password = enc
}
if req.SSHKey != "" {
enc, err := s.crypt.Encrypt(req.SSHKey)
if err != nil {
return nil, fmt.Errorf("加密 SSH 密钥失败: %w", err)
}
srv.SSHKey = enc
}
if req.CredentialID != nil && *req.CredentialID > 0 {
if err := s.applyCredentialToServer(*req.CredentialID, srv); err != nil {
return nil, err
}
}
if err := s.repo.Create(srv); err != nil {
return nil, err
}
return srv, nil
}
// UpdateServer 部分更新服务器
func (s *ServerService) UpdateServer(id uint, req *dto.UpdateServerRequest) error {
updates := make(map[string]interface{})
if req.Hostname != nil {
updates["hostname"] = *req.Hostname
}
if req.IP != nil {
updates["ip"] = *req.IP
}
if req.PublicIP != nil {
updates["public_ip"] = *req.PublicIP
}
if req.Port != nil {
updates["port"] = *req.Port
}
if req.Username != nil {
updates["username"] = *req.Username
}
if req.AuthType != nil {
updates["auth_type"] = *req.AuthType
}
if req.OSType != nil {
updates["os_type"] = *req.OSType
}
if req.Protocol != nil {
updates["protocol"] = *req.Protocol
}
if req.RDPPort != nil {
updates["rdp_port"] = *req.RDPPort
}
if req.OS != nil {
updates["os"] = *req.OS
}
if req.OSVersion != nil {
updates["os_version"] = *req.OSVersion
}
if req.Arch != nil {
updates["arch"] = *req.Arch
}
if req.CPU != nil {
updates["cpu"] = *req.CPU
}
if req.Memory != nil {
updates["memory"] = *req.Memory
}
if req.Disk != nil {
updates["disk"] = *req.Disk
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.RegionID != nil {
updates["region_id"] = *req.RegionID
}
if req.CloudID != nil {
updates["cloud_id"] = *req.CloudID
}
if req.Tags != nil {
updates["tags"] = *req.Tags
}
if req.Description != nil {
updates["description"] = *req.Description
}
// 加密凭据字段
if req.Password != nil && *req.Password != "" {
enc, err := s.crypt.Encrypt(*req.Password)
if err != nil {
return fmt.Errorf("加密密码失败: %w", err)
}
updates["password"] = enc
}
if req.SSHKey != nil && *req.SSHKey != "" {
enc, err := s.crypt.Encrypt(*req.SSHKey)
if err != nil {
return fmt.Errorf("加密 SSH 密钥失败: %w", err)
}
updates["ssh_key"] = enc
}
if req.CredentialID != nil && *req.CredentialID > 0 {
srv, err := s.repo.GetByID(id)
if err != nil {
return err
}
if err := s.applyCredentialToServer(*req.CredentialID, srv); err != nil {
return err
}
updates["username"] = srv.Username
updates["password"] = srv.Password
updates["ssh_key"] = srv.SSHKey
updates["auth_type"] = srv.AuthType
updates["port"] = srv.Port
updates["rdp_port"] = srv.RDPPort
updates["protocol"] = srv.Protocol
updates["os_type"] = srv.OSType
}
if len(updates) == 0 {
return nil
}
return s.repo.UpdateFields(id, updates)
}
func (s *ServerService) applyCredentialToServer(credentialID uint, srv *model.Server) error {
cred, err := s.credentialByID(credentialID)
if err != nil {
return err
}
plain, err := s.crypt.Decrypt(cred.Content)
if err != nil {
return fmt.Errorf("凭据解密失败: %w", err)
}
switch cred.SecretType {
case "ssh":
var sshc model.SSHSecretContent
if json.Unmarshal([]byte(plain), &sshc) == nil {
if sshc.SSHUser != "" {
srv.Username = sshc.SSHUser
}
if sshc.SSHPort > 0 {
srv.Port = sshc.SSHPort
}
if sshc.PrivateKey != "" {
srv.SSHKey = sshc.PrivateKey
srv.Password = ""
srv.AuthType = "key"
} else if sshc.Password != "" {
srv.Password = sshc.Password
srv.SSHKey = ""
srv.AuthType = "password"
}
}
srv.Protocol = "ssh"
if srv.OSType == "" {
srv.OSType = "linux"
}
case "rdp":
var rdpc model.RDPSecretContent
if err := json.Unmarshal([]byte(plain), &rdpc); err != nil {
return fmt.Errorf("RDP 凭据格式错误: %w", err)
}
srv.Username = rdpc.Username
srv.Password = plain
srv.SSHKey = ""
srv.AuthType = "password"
srv.Protocol = "rdp"
srv.OSType = "windows"
if srv.RDPPort == 0 {
srv.RDPPort = 3389
}
default:
return fmt.Errorf("服务器仅支持关联 ssh/rdp 凭据")
}
return nil
}
func (s *ServerService) credentialByID(id uint) (*model.Credential, error) {
var cred model.Credential
if err := s.repo.DB().First(&cred, id).Error; err != nil {
return nil, err
}
return &cred, nil
}
// DeleteServer 删除服务器
func (s *ServerService) DeleteServer(id uint) error {
return s.repo.Delete(id)
}
// GetDashboardStats 获取仪表盘统计
func (s *ServerService) GetDashboardStats() (*model.DashboardStats, error) {
var stats model.DashboardStats
stats.ServerStats.Total = s.repo.CountByStatus("")
stats.ServerStats.Running = s.repo.CountByStatus("running")
stats.ServerStats.Stopped = s.repo.CountByStatus("stopped")
stats.ServerStats.Maintenance = s.repo.CountByStatus("maintenance")
stats.ServerStats.Error = s.repo.CountByStatus("error")
stats.ServerStats.AvgCPU = math.Round(s.repo.AvgMetric("cpu_usage")*100) / 100
stats.ServerStats.AvgMemory = math.Round(s.repo.AvgMetric("memory_usage")*100) / 100
stats.ServerStats.AvgDisk = math.Round(s.repo.AvgMetric("disk_usage")*100) / 100
return &stats, nil
}
// ━━━ Credential Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
type CredentialService struct {
repo *repository.CredentialRepository
crypt *crypto.SecretBox
}
func NewCredentialService(repo *repository.CredentialRepository, crypt *crypto.SecretBox) *CredentialService {
return &CredentialService{repo: repo, crypt: crypt}
}
// List 分页查询
func (s *CredentialService) List(keyword, secretType string, page, pageSize int) ([]model.Credential, int64, error) {
return s.repo.List(keyword, secretType, page, pageSize)
}
// GetByID 获取详情
func (s *CredentialService) GetByID(id uint) (*model.Credential, error) {
return s.repo.GetByID(id)
}
// buildCredentialContent 根据请求构建加密内容
func (s *CredentialService) buildCredentialContent(req *dto.CreateCredentialRequest) (string, string, error) {
switch req.SecretType {
case "ssh":
data := map[string]interface{}{
"private_key": req.SSHPrivateKey,
"public_key": req.SSHPublicKey,
"ssh_user": req.SSHUser,
"ssh_port": req.SSHPort,
"password": req.SSHPassword,
}
b, _ := json.Marshal(data)
return string(b), fmt.Sprintf("SSH %s@%d", req.SSHUser, req.SSHPort), nil
case "rdp":
security := req.RDPSecurity
if security == "" {
security = "nla"
}
data := map[string]string{
"username": req.RDPUsername,
"password": req.RDPPassword,
"domain": req.RDPDomain,
"security": security,
}
b, _ := json.Marshal(data)
return string(b), fmt.Sprintf("RDP %s", req.RDPUsername), nil
case "docker":
data := map[string]string{
"server": req.DockerServer,
"username": req.DockerUsername,
"password": req.DockerPassword,
}
b, _ := json.Marshal(data)
return string(b), req.DockerServer, nil
case "tls":
data := map[string]string{"cert": req.TLSCert, "key": req.TLSKey, "ca": req.TLSCA}
b, _ := json.Marshal(data)
return string(b), "TLS 证书已配置", nil
case "password":
return req.PlainPassword, "密码", nil
case "env":
return req.EnvVars, "环境变量", nil
default:
return "", "", fmt.Errorf("不支持的凭据类型: %s", req.SecretType)
}
}
// Create 创建凭据
func (s *CredentialService) Create(req *dto.CreateCredentialRequest) (*model.Credential, error) {
content, usage, err := s.buildCredentialContent(req)
if err != nil {
return nil, err
}
encContent, err := s.crypt.Encrypt(content)
if err != nil {
return nil, fmt.Errorf("加密失败: %w", err)
}
cred := &model.Credential{
Name: req.Name,
SecretType: req.SecretType,
Content: encContent,
Summary: usage,
Description: req.Description,
}
if err := s.repo.Create(cred); err != nil {
return nil, err
}
return cred, nil
}
// buildUpdateContent 构建更新凭据的加密内容
func (s *CredentialService) buildUpdateContent(req *dto.UpdateCredentialRequest, existingSecretType string) (string, string, error) {
st := existingSecretType
switch st {
case "ssh":
data := map[string]interface{}{}
if req.SSHPrivateKey != nil {
data["private_key"] = *req.SSHPrivateKey
}
if req.SSHPublicKey != nil {
data["public_key"] = *req.SSHPublicKey
}
if req.SSHUser != nil {
data["ssh_user"] = *req.SSHUser
}
if req.SSHPort != nil {
data["ssh_port"] = *req.SSHPort
}
if req.SSHPassword != nil {
data["password"] = *req.SSHPassword
}
if len(data) == 0 {
return "", "", nil
}
b, _ := json.Marshal(data)
return string(b), "", nil
case "rdp":
data := map[string]string{}
if req.RDPUsername != nil {
data["username"] = *req.RDPUsername
}
if req.RDPPassword != nil {
data["password"] = *req.RDPPassword
}
if req.RDPDomain != nil {
data["domain"] = *req.RDPDomain
}
if req.RDPSecurity != nil {
data["security"] = *req.RDPSecurity
}
if len(data) == 0 {
return "", "", nil
}
b, _ := json.Marshal(data)
return string(b), "", nil
case "docker":
data := map[string]string{}
if req.DockerServer != nil {
data["server"] = *req.DockerServer
}
if req.DockerUsername != nil {
data["username"] = *req.DockerUsername
}
if req.DockerPassword != nil {
data["password"] = *req.DockerPassword
}
b, _ := json.Marshal(data)
return string(b), "", nil
case "tls":
data := map[string]string{}
if req.TLSCert != nil {
data["cert"] = *req.TLSCert
}
if req.TLSKey != nil {
data["key"] = *req.TLSKey
}
if req.TLSCA != nil {
data["ca"] = *req.TLSCA
}
b, _ := json.Marshal(data)
return string(b), "", nil
case "password":
if req.PlainPassword != nil {
return *req.PlainPassword, "", nil
}
return "", "", nil
case "env":
if req.EnvVars != nil {
return *req.EnvVars, "", nil
}
return "", "", nil
}
return "", "", nil
}
// Update 更新凭据
func (s *CredentialService) Update(id uint, req *dto.UpdateCredentialRequest) error {
existing, err := s.repo.GetByID(id)
if err != nil {
return err
}
content, _, err := s.buildUpdateContent(req, existing.SecretType)
if err != nil {
return err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if content != "" {
enc, err := s.crypt.Encrypt(content)
if err != nil {
return fmt.Errorf("加密失败: %w", err)
}
updates["secret_data"] = enc
}
if len(updates) == 0 {
return nil
}
return s.repo.UpdateFields(id, updates)
}
// Delete 删除凭据
func (s *CredentialService) Delete(id uint) error {
return s.repo.Delete(id)
}
// ━━━ Alert Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
type AlertContactService struct {
repo *repository.AlertContactRepository
}
func NewAlertContactService(repo *repository.AlertContactRepository) *AlertContactService {
return &AlertContactService{repo: repo}
}
func (s *AlertContactService) List(keyword string, page, pageSize int) ([]model.AlertContact, int64, error) {
return s.repo.List(keyword, page, pageSize)
}
func (s *AlertContactService) GetByID(id uint) (*model.AlertContact, error) {
return s.repo.GetByID(id)
}
func (s *AlertContactService) Create(req *dto.CreateAlertContactRequest) (*model.AlertContact, error) {
c := &model.AlertContact{
Name: req.Name,
ChannelType: req.ChannelType,
WebhookURL: req.WebhookURL,
Phone: req.Phone,
Email: req.Email,
}
if err := s.repo.Create(c); err != nil {
return nil, err
}
return c, nil
}
func (s *AlertContactService) Update(id uint, req *dto.UpdateAlertContactRequest) error {
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.ChannelType != nil {
updates["channel_type"] = *req.ChannelType
}
if req.WebhookURL != nil {
updates["webhook_url"] = *req.WebhookURL
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.Email != nil {
updates["email"] = *req.Email
}
if len(updates) == 0 {
return nil
}
// AlertContact 没有 UpdateFields,直接 Save
c, err := s.repo.GetByID(id)
if err != nil {
return err
}
if req.Name != nil {
c.Name = *req.Name
}
if req.ChannelType != nil {
c.ChannelType = *req.ChannelType
}
if req.WebhookURL != nil {
c.WebhookURL = *req.WebhookURL
}
if req.Phone != nil {
c.Phone = *req.Phone
}
if req.Email != nil {
c.Email = *req.Email
}
return s.repo.Update(c)
}
func (s *AlertContactService) Delete(id uint) error {
return s.repo.Delete(id)
}
// AlertContactGroup Service
type AlertContactGroupService struct {
repo *repository.AlertContactGroupRepository
}
func NewAlertContactGroupService(repo *repository.AlertContactGroupRepository) *AlertContactGroupService {
return &AlertContactGroupService{repo: repo}
}
func (s *AlertContactGroupService) List(keyword string, page, pageSize int) ([]model.AlertContactGroup, int64, error) {
return s.repo.List(keyword, page, pageSize)
}
func (s *AlertContactGroupService) GetByID(id uint) (*model.AlertContactGroup, error) {
return s.repo.GetByID(id)
}
func (s *AlertContactGroupService) Create(req *dto.CreateAlertContactGroupRequest) (*model.AlertContactGroup, error) {
g := &model.AlertContactGroup{GroupName: req.GroupName, Notes: req.Notes}
if len(req.ContactIDs) > 0 {
g.ContactIDs = toJSONString(req.ContactIDs)
}
if err := s.repo.Create(g); err != nil {
return nil, err
}
return g, nil
}
func (s *AlertContactGroupService) Update(id uint, req *dto.UpdateAlertContactGroupRequest) error {
updates := make(map[string]interface{})
if req.GroupName != nil {
updates["group_name"] = *req.GroupName
}
if req.Notes != nil {
updates["notes"] = *req.Notes
}
if req.ContactIDs != nil {
updates["contact_ids"] = toJSONString(*req.ContactIDs)
}
if len(updates) == 0 {
return nil
}
return s.repo.UpdateFields(id, updates)
}
func (s *AlertContactGroupService) Delete(id uint) error {
return s.repo.Delete(id)
}
func toJSONString(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
}
// ━━━ Config Helper — 管理所有 Service 的依赖注入 ━━━━
type Services struct {
Server *ServerService
Credential *CredentialService
AlertContact *AlertContactService
AlertContactGroup *AlertContactGroupService
}
// NewServices 初始化所有服务
func NewServices(db *gorm.DB, cache ezredis.Cache, masterKey string) *Services {
crypt := crypto.NewSecretBox(masterKey)
return &Services{
Server: NewServerService(repository.NewServerRepository(db), cache, crypt),
Credential: NewCredentialService(repository.NewCredentialRepository(db), crypt),
AlertContact: NewAlertContactService(repository.NewAlertContactRepository(db)),
AlertContactGroup: NewAlertContactGroupService(repository.NewAlertContactGroupRepository(db)),
}
}