Files
lijiaoqiao/supply-api/internal/audit/service/audit_service.go

308 lines
7.5 KiB
Go
Raw Normal View History

package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"lijiaoqiao/supply-api/internal/audit/model"
)
// 错误定义
var (
ErrInvalidInput = errors.New("invalid input: event is nil")
ErrMissingEventName = errors.New("invalid input: event name is required")
ErrEventNotFound = errors.New("event not found")
ErrIdempotencyConflict = errors.New("idempotency key conflict")
)
// CreateEventResult 事件创建结果
type CreateEventResult struct {
EventID string `json:"event_id"`
StatusCode int `json:"status_code"`
Status string `json:"status"`
OriginalCreatedAt *time.Time `json:"original_created_at,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
RetryAfterMs int64 `json:"retry_after_ms,omitempty"`
}
// EventFilter 事件查询过滤器
type EventFilter struct {
TenantID int64
Category string
EventName string
ObjectType string
ObjectID int64
StartTime time.Time
EndTime time.Time
Success *bool
Limit int
Offset int
}
// AuditStoreInterface 审计存储接口
type AuditStoreInterface interface {
Emit(ctx context.Context, event *model.AuditEvent) error
Query(ctx context.Context, filter *EventFilter) ([]*model.AuditEvent, int64, error)
GetByIdempotencyKey(ctx context.Context, key string) (*model.AuditEvent, error)
}
// InMemoryAuditStore 内存审计存储
type InMemoryAuditStore struct {
mu sync.RWMutex
events []*model.AuditEvent
nextID int64
idempotencyKeys map[string]*model.AuditEvent
}
// NewInMemoryAuditStore 创建内存审计存储
func NewInMemoryAuditStore() *InMemoryAuditStore {
return &InMemoryAuditStore{
events: make([]*model.AuditEvent, 0),
nextID: 1,
idempotencyKeys: make(map[string]*model.AuditEvent),
}
}
// Emit 发送事件
func (s *InMemoryAuditStore) Emit(ctx context.Context, event *model.AuditEvent) error {
s.mu.Lock()
defer s.mu.Unlock()
// 生成事件ID
if event.EventID == "" {
event.EventID = generateEventID()
}
event.CreatedAt = time.Now()
s.events = append(s.events, event)
// 如果有幂等键,记录映射
if event.IdempotencyKey != "" {
s.idempotencyKeys[event.IdempotencyKey] = event
}
return nil
}
// Query 查询事件
func (s *InMemoryAuditStore) Query(ctx context.Context, filter *EventFilter) ([]*model.AuditEvent, int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var result []*model.AuditEvent
for _, e := range s.events {
// 按租户过滤
if filter.TenantID > 0 && e.TenantID != filter.TenantID {
continue
}
// 按类别过滤
if filter.Category != "" && e.EventCategory != filter.Category {
continue
}
// 按事件名称过滤
if filter.EventName != "" && e.EventName != filter.EventName {
continue
}
// 按对象类型过滤
if filter.ObjectType != "" && e.ObjectType != filter.ObjectType {
continue
}
// 按对象ID过滤
if filter.ObjectID > 0 && e.ObjectID != filter.ObjectID {
continue
}
// 按时间范围过滤
if !filter.StartTime.IsZero() && e.Timestamp.Before(filter.StartTime) {
continue
}
if !filter.EndTime.IsZero() && e.Timestamp.After(filter.EndTime) {
continue
}
// 按成功状态过滤
if filter.Success != nil && e.Success != *filter.Success {
continue
}
result = append(result, e)
}
total := int64(len(result))
// 分页
if filter.Offset > 0 {
if filter.Offset >= len(result) {
return []*model.AuditEvent{}, total, nil
}
result = result[filter.Offset:]
}
if filter.Limit > 0 && filter.Limit < len(result) {
result = result[:filter.Limit]
}
return result, total, nil
}
// GetByIdempotencyKey 根据幂等键获取事件
func (s *InMemoryAuditStore) GetByIdempotencyKey(ctx context.Context, key string) (*model.AuditEvent, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if event, ok := s.idempotencyKeys[key]; ok {
return event, nil
}
return nil, ErrEventNotFound
}
// generateEventID 生成事件ID
func generateEventID() string {
now := time.Now()
return now.Format("20060102150405.000000") + fmt.Sprintf("%03d", now.Nanosecond()%1000000/1000) + "-evt"
}
// AuditService 审计服务
type AuditService struct {
store AuditStoreInterface
processingDelay time.Duration
}
// NewAuditService 创建审计服务
func NewAuditService(store AuditStoreInterface) *AuditService {
return &AuditService{
store: store,
}
}
// SetProcessingDelay 设置处理延迟(用于模拟异步处理)
func (s *AuditService) SetProcessingDelay(delay time.Duration) {
s.processingDelay = delay
}
// CreateEvent 创建审计事件
func (s *AuditService) CreateEvent(ctx context.Context, event *model.AuditEvent) (*CreateEventResult, error) {
// 输入验证
if event == nil {
return nil, ErrInvalidInput
}
if event.EventName == "" {
return nil, ErrMissingEventName
}
// 设置时间戳
if event.Timestamp.IsZero() {
event.Timestamp = time.Now()
}
if event.TimestampMs == 0 {
event.TimestampMs = event.Timestamp.UnixMilli()
}
// 如果没有事件ID生成一个
if event.EventID == "" {
event.EventID = generateEventID()
}
// 处理幂等性
if event.IdempotencyKey != "" {
existing, err := s.store.GetByIdempotencyKey(ctx, event.IdempotencyKey)
if err == nil && existing != nil {
// 检查payload是否相同
if isSamePayload(existing, event) {
// 重放同参 - 返回200
return &CreateEventResult{
EventID: existing.EventID,
StatusCode: 200,
Status: "duplicate",
OriginalCreatedAt: &existing.CreatedAt,
}, nil
} else {
// 重放异参 - 返回409
return &CreateEventResult{
StatusCode: 409,
Status: "conflict",
ErrorCode: "IDEMPOTENCY_PAYLOAD_MISMATCH",
ErrorMessage: "Idempotency key reused with different payload",
}, nil
}
}
}
// 首次创建 - 返回201
err := s.store.Emit(ctx, event)
if err != nil {
return nil, err
}
return &CreateEventResult{
EventID: event.EventID,
StatusCode: 201,
Status: "created",
}, nil
}
// ListEvents 列出事件(带分页)
func (s *AuditService) ListEvents(ctx context.Context, tenantID int64, offset, limit int) ([]*model.AuditEvent, int64, error) {
filter := &EventFilter{
TenantID: tenantID,
Offset: offset,
Limit: limit,
}
return s.store.Query(ctx, filter)
}
// ListEventsWithFilter 列出事件(带过滤器)
func (s *AuditService) ListEventsWithFilter(ctx context.Context, filter *EventFilter) ([]*model.AuditEvent, int64, error) {
return s.store.Query(ctx, filter)
}
// HashIdempotencyKey 计算幂等键的哈希值
func (s *AuditService) HashIdempotencyKey(key string) string {
hash := sha256.Sum256([]byte(key))
return hex.EncodeToString(hash[:])
}
// isSamePayload 检查两个事件的payload是否相同
func isSamePayload(a, b *model.AuditEvent) bool {
// 比较关键字段
if a.EventName != b.EventName {
return false
}
if a.EventCategory != b.EventCategory {
return false
}
if a.OperatorID != b.OperatorID {
return false
}
if a.TenantID != b.TenantID {
return false
}
if a.ObjectType != b.ObjectType {
return false
}
if a.ObjectID != b.ObjectID {
return false
}
if a.Action != b.Action {
return false
}
if a.CredentialType != b.CredentialType {
return false
}
if a.SourceType != b.SourceType {
return false
}
if a.SourceIP != b.SourceIP {
return false
}
if a.Success != b.Success {
return false
}
if a.ResultCode != b.ResultCode {
return false
}
return true
}