feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现

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 ✅
This commit is contained in:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10050 -4065

No files matched your search

+539
View File
@@ -0,0 +1,539 @@
// 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)),
}
}