Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
auditLogQueueCapacity = 4096
|
||||
auditLogBatchSize = 100
|
||||
auditLogFlushInterval = time.Second
|
||||
|
||||
auditRetentionCheckInterval = 24 * time.Hour
|
||||
auditRetentionStartupDelay = 5 * time.Minute
|
||||
auditRetentionBatchSize = 5000
|
||||
)
|
||||
|
||||
// AuditLogService 管理面操作审计日志服务。
|
||||
// 写入端为非阻塞异步批量落库(不拖慢管理请求);
|
||||
// 读取端提供分页查询;清空端点由 handler 层做 TOTP 强校验后调用 ClearAll。
|
||||
type AuditLogService struct {
|
||||
repo AuditLogRepository
|
||||
settingService *SettingService
|
||||
|
||||
queue chan *AuditLog
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
droppedCount uint64
|
||||
writeFailed uint64
|
||||
writtenCount uint64
|
||||
}
|
||||
|
||||
func NewAuditLogService(repo AuditLogRepository, settingService *SettingService) *AuditLogService {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &AuditLogService{
|
||||
repo: repo,
|
||||
settingService: settingService,
|
||||
queue: make(chan *AuditLog, auditLogQueueCapacity),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动异步写入与保留期清理协程。
|
||||
func (s *AuditLogService) Start() {
|
||||
if s == nil || s.repo == nil {
|
||||
return
|
||||
}
|
||||
s.wg.Add(2)
|
||||
go s.runWriter()
|
||||
go s.runRetentionLoop()
|
||||
}
|
||||
|
||||
// Stop 停止服务并尽量落盘队列中剩余记录。
|
||||
func (s *AuditLogService) Stop() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.cancel()
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
// Record 非阻塞入队一条审计记录;队列打满时丢弃并计数(管理面流量下几乎不可能发生)。
|
||||
func (s *AuditLogService) Record(entry *AuditLog) {
|
||||
if s == nil || entry == nil {
|
||||
return
|
||||
}
|
||||
if entry.CreatedAt.IsZero() {
|
||||
entry.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case s.queue <- entry:
|
||||
default:
|
||||
atomic.AddUint64(&s.droppedCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// List 分页查询审计日志。
|
||||
func (s *AuditLogService) List(ctx context.Context, filter *AuditLogFilter) (*AuditLogList, error) {
|
||||
return s.repo.List(ctx, filter)
|
||||
}
|
||||
|
||||
// GetByID 查询单条详情。
|
||||
func (s *AuditLogService) GetByID(ctx context.Context, id int64) (*AuditLog, error) {
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// ClearAll 全量清空审计日志并写入留痕记录。
|
||||
// 调用方(handler)必须先完成 TOTP 验证;本方法负责:
|
||||
// 1. 统计并清空全表
|
||||
// 2. 同步写入一条 "audit_log.clear" 留痕记录(绕过异步队列,保证落库)
|
||||
func (s *AuditLogService) ClearAll(ctx context.Context, trace *AuditLog) (int64, error) {
|
||||
deleted, err := s.repo.Count(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("count audit logs: %w", err)
|
||||
}
|
||||
if err := s.repo.TruncateAll(ctx); err != nil {
|
||||
return 0, fmt.Errorf("truncate audit logs: %w", err)
|
||||
}
|
||||
|
||||
if trace != nil {
|
||||
trace.Action = AuditActionAuditLogClear
|
||||
if trace.CreatedAt.IsZero() {
|
||||
trace.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
if trace.Extra == nil {
|
||||
trace.Extra = map[string]any{}
|
||||
}
|
||||
trace.Extra["deleted_rows"] = deleted
|
||||
if err := s.repo.Insert(ctx, trace); err != nil {
|
||||
// 留痕失败必须显式暴露:清空已发生,但审计链断裂。
|
||||
return deleted, fmt.Errorf("audit logs cleared (%d rows) but failed to persist clear-trace record: %w", deleted, err)
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (s *AuditLogService) runWriter() {
|
||||
defer s.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(auditLogFlushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
batch := make([]*AuditLog, 0, auditLogBatchSize)
|
||||
flush := func() {
|
||||
if len(batch) == 0 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
inserted, err := s.repo.BatchInsert(ctx, batch)
|
||||
cancel()
|
||||
if err != nil {
|
||||
atomic.AddUint64(&s.writeFailed, uint64(len(batch)))
|
||||
_, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log flush failed\" err=%v batch=%d\n",
|
||||
time.Now().Format(time.RFC3339Nano), err, len(batch))
|
||||
} else {
|
||||
atomic.AddUint64(&s.writtenCount, uint64(inserted))
|
||||
}
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
// 停机前排空队列。
|
||||
for {
|
||||
select {
|
||||
case item := <-s.queue:
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
batch = append(batch, item)
|
||||
if len(batch) >= auditLogBatchSize {
|
||||
flush()
|
||||
}
|
||||
default:
|
||||
flush()
|
||||
return
|
||||
}
|
||||
}
|
||||
case item := <-s.queue:
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
batch = append(batch, item)
|
||||
if len(batch) >= auditLogBatchSize {
|
||||
flush()
|
||||
}
|
||||
case <-ticker.C:
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runRetentionLoop 按保留期定期删除过期审计日志。
|
||||
// 删除操作幂等,多实例并发执行无害,因此无需选主。
|
||||
func (s *AuditLogService) runRetentionLoop() {
|
||||
defer s.wg.Done()
|
||||
|
||||
startupTimer := time.NewTimer(auditRetentionStartupDelay)
|
||||
defer startupTimer.Stop()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-startupTimer.C:
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(auditRetentionCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
s.runRetentionOnce()
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.runRetentionOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuditLogService) runRetentionOnce() {
|
||||
ctx, cancel := context.WithTimeout(s.ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
days := 0
|
||||
if s.settingService != nil {
|
||||
days = s.settingService.GetAuditLogRetentionDays(ctx)
|
||||
}
|
||||
if days <= 0 {
|
||||
return // 0 或负值表示永久保留,仅支持手动清空
|
||||
}
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -days)
|
||||
for {
|
||||
deleted, err := s.repo.DeleteBefore(ctx, cutoff, auditRetentionBatchSize)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log retention cleanup failed\" err=%v\n",
|
||||
time.Now().Format(time.RFC3339Nano), err)
|
||||
return
|
||||
}
|
||||
if deleted == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user