## 设计文档 - multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO) - audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO) - routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO) - sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO) - compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO) ## TDD开发成果 - IAM模块: supply-api/internal/iam/ (111个测试) - 审计日志模块: supply-api/internal/audit/ (40+测试) - 路由策略模块: gateway/internal/router/ (33+测试) - 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/ ## 规范文档 - parallel_agent_output_quality_standards: 并行Agent产出质量规范 - project_experience_summary: 项目经验总结 (v2) - 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划 ## 评审报告 - 5个CONDITIONAL GO设计文档评审报告 - fix_verification_report: 修复验证报告 - full_verification_report: 全面质量验证报告 - tdd_module_quality_verification: TDD模块质量验证 - tdd_execution_summary: TDD执行总结 依据: Superpowers执行框架 + TDD规范
308 lines
7.5 KiB
Go
308 lines
7.5 KiB
Go
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
|
||
} |