Files
lijiaoqiao/supply-api/internal/audit/service/audit_service.go
Your Name 89104bd0db feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档
- 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规范
2026-04-02 23:35:53 +08:00

308 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}