2026-04-02 23:35:53 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:05:29 +08:00
|
|
|
|
// 内存存储容量常量
|
|
|
|
|
|
const MaxEvents = 100000
|
|
|
|
|
|
|
2026-04-02 23:35:53 +08:00
|
|
|
|
// 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()
|
|
|
|
|
|
|
2026-04-03 09:05:29 +08:00
|
|
|
|
// 检查容量,超过上限时清理旧事件
|
|
|
|
|
|
if len(s.events) >= MaxEvents {
|
|
|
|
|
|
s.cleanupOldEvents(MaxEvents / 10)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:35:53 +08:00
|
|
|
|
// 生成事件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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:05:29 +08:00
|
|
|
|
// cleanupOldEvents 清理旧事件,保留最近的 events
|
|
|
|
|
|
func (s *InMemoryAuditStore) cleanupOldEvents(removeCount int) {
|
|
|
|
|
|
if removeCount <= 0 {
|
|
|
|
|
|
removeCount = MaxEvents / 10
|
|
|
|
|
|
}
|
|
|
|
|
|
if removeCount >= len(s.events) {
|
|
|
|
|
|
removeCount = len(s.events) - 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保留最近的事件,删除旧事件
|
|
|
|
|
|
remaining := len(s.events) - removeCount
|
|
|
|
|
|
s.events = s.events[remaining:]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:35:53 +08:00
|
|
|
|
// 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
|
2026-04-03 09:05:29 +08:00
|
|
|
|
idempotencyMu sync.Mutex // 保护幂等性检查的互斥锁
|
2026-04-02 23:35:53 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:05:29 +08:00
|
|
|
|
// 处理幂等性 - 使用互斥锁保护检查和插入之间的时间窗口
|
2026-04-02 23:35:53 +08:00
|
|
|
|
if event.IdempotencyKey != "" {
|
2026-04-03 09:05:29 +08:00
|
|
|
|
s.idempotencyMu.Lock()
|
2026-04-02 23:35:53 +08:00
|
|
|
|
existing, err := s.store.GetByIdempotencyKey(ctx, event.IdempotencyKey)
|
|
|
|
|
|
if err == nil && existing != nil {
|
2026-04-03 09:05:29 +08:00
|
|
|
|
s.idempotencyMu.Unlock()
|
2026-04-02 23:35:53 +08:00
|
|
|
|
// 检查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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-03 09:05:29 +08:00
|
|
|
|
s.idempotencyMu.Unlock()
|
2026-04-02 23:35:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 首次创建 - 返回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
|
|
|
|
|
|
}
|
2026-04-03 09:39:32 +08:00
|
|
|
|
if a.ActionDetail != b.ActionDetail {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-04-02 23:35:53 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-03 09:39:32 +08:00
|
|
|
|
if a.ResultMessage != b.ResultMessage {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
// 比较Extensions
|
|
|
|
|
|
if !compareExtensions(a.Extensions, b.Extensions) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// compareExtensions 比较两个map是否相等
|
|
|
|
|
|
func compareExtensions(a, b map[string]any) bool {
|
|
|
|
|
|
if len(a) != len(b) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
for k, v1 := range a {
|
|
|
|
|
|
v2, ok := b[k]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
// 简单的值比较,不处理嵌套map的情况
|
|
|
|
|
|
if v1 != v2 {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 23:35:53 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|