F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
540 lines
14 KiB
Go
540 lines
14 KiB
Go
// Package service 提供业务逻辑层 — 编排 repository、处理加密/校验/转换
|
|
// Handler 不直接调用 repository,通过 service 调用
|
|
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
|
|
"ezops/dto"
|
|
"ezops/model"
|
|
"ezops/repository"
|
|
"ezops/pkg/crypto"
|
|
ezredis "ezops/pkg/redis"
|
|
|
|
"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 string, page, pageSize int) ([]model.Server, int64, error) {
|
|
list, total, err := s.repo.List(keyword, status, regionID, 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,
|
|
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"
|
|
}
|
|
|
|
// 加密密码和 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 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.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 len(updates) == 0 {
|
|
return nil
|
|
}
|
|
return s.repo.UpdateFields(id, updates)
|
|
}
|
|
|
|
// 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 "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 "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.Name}
|
|
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.Name != nil {
|
|
updates["group_name"] = *req.Name
|
|
}
|
|
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)),
|
|
}
|
|
}
|