package service import ( "context" "fmt" "log" "sync" "time" "gorm.io/gorm" "oci-portal/internal/model" ) // 系统日志保留策略与异步写入超时。 const ( systemLogRetention = 90 * 24 * time.Hour // 过期删除阈值 systemLogMaxRows = 50000 // 总量兜底上限,超出删最旧 systemLogCleanupTick = 24 * time.Hour // 周期清理间隔 systemLogWriteWait = 3 * time.Second // 单条异步写入超时 ) // 系统日志查询分页默认值与上限。 const ( systemLogDefaultPageSize = 20 systemLogMaxPageSize = 100 ) // SystemLogService 记录并查询面板自身的操作日志(区别于 OCI 云端审计事件)。 type SystemLogService struct { db *gorm.DB wg sync.WaitGroup } // NewSystemLogService 组装依赖;调用 StartCleanup 后开始周期清理。 func NewSystemLogService(db *gorm.DB) *SystemLogService { return &SystemLogService{db: db} } // Record 异步落一条日志,不阻塞业务请求;写入失败只记日志。 func (s *SystemLogService) Record(entry model.SystemLog) { s.wg.Add(1) go func() { defer s.wg.Done() ctx, cancel := context.WithTimeout(context.Background(), systemLogWriteWait) defer cancel() if err := s.record(ctx, entry); err != nil { log.Printf("system log record: %v", err) } }() } // record 同步落库,供 Record 内部与测试直调。 func (s *SystemLogService) record(ctx context.Context, entry model.SystemLog) error { if entry.CreatedAt.IsZero() { entry.CreatedAt = time.Now() } if err := s.db.WithContext(ctx).Create(&entry).Error; err != nil { return fmt.Errorf("create system log: %w", err) } return nil } // Wait 等待在途异步写入与清理 goroutine 退出,供进程收尾与测试同步。 func (s *SystemLogService) Wait() { s.wg.Wait() } // SystemLogQuery 是日志查询参数;Keyword 对路径与用户名模糊匹配。 type SystemLogQuery struct { Page int PageSize int Keyword string } // normalize 补分页默认值并钳制上限。 func (q SystemLogQuery) normalize() SystemLogQuery { if q.Page < 1 { q.Page = 1 } if q.PageSize < 1 { q.PageSize = systemLogDefaultPageSize } if q.PageSize > systemLogMaxPageSize { q.PageSize = systemLogMaxPageSize } return q } // List 按时间倒序分页查询日志,返回当前页与总数。 func (s *SystemLogService) List(ctx context.Context, q SystemLogQuery) ([]model.SystemLog, int64, error) { q = q.normalize() tx := s.db.WithContext(ctx).Model(&model.SystemLog{}) if q.Keyword != "" { kw := "%" + q.Keyword + "%" tx = tx.Where("path LIKE ? OR username LIKE ?", kw, kw) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("count system logs: %w", err) } items := make([]model.SystemLog, 0, q.PageSize) err := tx.Order("created_at DESC, id DESC"). Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize). Find(&items).Error if err != nil { return nil, 0, fmt.Errorf("list system logs: %w", err) } return items, total, nil } // StartCleanup 启动周期清理:启动即清一次,之后每 24h 一次,随 ctx 取消退出。 func (s *SystemLogService) StartCleanup(ctx context.Context) { s.wg.Add(1) go func() { defer s.wg.Done() s.cleanupOnce(ctx) ticker := time.NewTicker(systemLogCleanupTick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: s.cleanupOnce(ctx) } } }() } // cleanupOnce 执行一轮清理,失败只记日志、不中断周期调度。 func (s *SystemLogService) cleanupOnce(ctx context.Context) { if err := s.cleanup(ctx, systemLogRetention, systemLogMaxRows); err != nil { log.Printf("system log cleanup: %v", err) } } // cleanup 先删过期记录,再对超量部分删最旧;阈值参数化便于测试。 func (s *SystemLogService) cleanup(ctx context.Context, retention time.Duration, maxRows int) error { cutoff := time.Now().Add(-retention) if err := s.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(&model.SystemLog{}).Error; err != nil { return fmt.Errorf("delete expired system logs: %w", err) } var total int64 if err := s.db.WithContext(ctx).Model(&model.SystemLog{}).Count(&total).Error; err != nil { return fmt.Errorf("count system logs: %w", err) } overflow := int(total) - maxRows if overflow <= 0 { return nil } oldest := s.db.Model(&model.SystemLog{}).Select("id"). Order("created_at ASC, id ASC").Limit(overflow) if err := s.db.WithContext(ctx).Where("id IN (?)", oldest).Delete(&model.SystemLog{}).Error; err != nil { return fmt.Errorf("trim system logs over cap: %w", err) } return nil }