This commit is contained in:
@@ -10,11 +10,14 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
|
||||
@@ -57,7 +60,6 @@ type Completion struct {
|
||||
StateChanged bool
|
||||
ResourceID *string
|
||||
ResourceKey *string
|
||||
AuditEventID *uint
|
||||
RecoveryStrategy string
|
||||
}
|
||||
|
||||
@@ -75,17 +77,19 @@ type InboundAttempt struct {
|
||||
ContentType string
|
||||
RequestID *string
|
||||
CorrelationID *string
|
||||
AuditEventID *uint
|
||||
}
|
||||
|
||||
// Repository 负责创建稳定尝试及受控地进入终态。
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
now func() time.Time
|
||||
db *gorm.DB
|
||||
now func() time.Time
|
||||
audit *audit.Writer
|
||||
}
|
||||
|
||||
// NewRepository 创建 Integration Log Repository。
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db, now: time.Now}
|
||||
return &Repository{db: db, now: time.Now, audit: audit.NewWriter(audit.NewRegistry(), nil)}
|
||||
}
|
||||
|
||||
// Start 在实际调用外部系统前持久化尝试事实。
|
||||
@@ -107,6 +111,12 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati
|
||||
if input.IntegrationID == "" {
|
||||
input.IntegrationID = uuid.NewString()
|
||||
}
|
||||
if input.AuditEventID == nil && input.TriggerSeries != nil {
|
||||
input.AuditEventID = r.auditEventIDForSeries(ctx, *input.TriggerSeries)
|
||||
}
|
||||
if input.AuditEventID == nil {
|
||||
input.AuditEventID = r.recordAuditEvent(ctx, constants.AuditActionIntegrationAttemptStarted, input.IntegrationID, input.Provider, input.Direction, input.Operation, input.ResourceType, input.ResourceID, input.ResourceKey, input.CorrelationID)
|
||||
}
|
||||
autoAttempt := input.Attempt <= 0 && input.TriggerSeries != nil
|
||||
if input.Attempt <= 0 {
|
||||
input.Attempt = 1
|
||||
@@ -193,9 +203,6 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet
|
||||
} else if completion.ProviderMessage != "" {
|
||||
updates["provider_message"] = sanitizer.TextSummary(completion.ProviderMessage)
|
||||
}
|
||||
if completion.AuditEventID != nil {
|
||||
updates["audit_event_id"] = completion.AuditEventID
|
||||
}
|
||||
if completion.ResourceID != nil {
|
||||
updates["resource_id"] = completion.ResourceID
|
||||
}
|
||||
@@ -236,6 +243,9 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
|
||||
if input.IntegrationID == "" {
|
||||
input.IntegrationID = uuid.NewString()
|
||||
}
|
||||
if input.AuditEventID == nil {
|
||||
input.AuditEventID = r.recordAuditEvent(ctx, constants.AuditActionIntegrationInboundReceived, input.IntegrationID, input.Provider, constants.IntegrationDirectionInbound, input.Operation, input.ResourceType, input.ResourceID, input.ResourceKey, input.CorrelationID)
|
||||
}
|
||||
triggerSeries := input.IntegrationID
|
||||
hash := sha256.Sum256(input.RawPayload)
|
||||
summary, err := marshalSummary(map[string]any{
|
||||
@@ -255,6 +265,7 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
|
||||
TriggerSeries: &triggerSeries,
|
||||
Result: constants.IntegrationResultPending, RequestSummary: summary,
|
||||
ContentHash: hex.EncodeToString(hash[:]), RequestID: input.RequestID, CorrelationID: input.CorrelationID,
|
||||
AuditEventID: input.AuditEventID,
|
||||
}
|
||||
result := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "provider"}, {Name: "operation"}, {Name: "idempotency_key"}},
|
||||
@@ -278,6 +289,80 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
|
||||
return &existing, false, nil
|
||||
}
|
||||
|
||||
// recordAuditEvent 为新的外部交互建立稳定审计关联;审计失败不丢失外部事实。
|
||||
func (r *Repository) recordAuditEvent(ctx context.Context, actionCode, integrationID, provider, direction, operation, resourceType string, resourceID, resourceKey, correlationID *string) *uint {
|
||||
if r == nil || r.db == nil || r.audit == nil {
|
||||
zap.L().Warn("Integration Log 缺少审计关联", zap.String("integration_id", integrationID), zap.String("reason", "审计 Writer 未配置"))
|
||||
return nil
|
||||
}
|
||||
value := auditcontext.From(ctx)
|
||||
if !validAuditOrigin(value.ActorKind, value.ActorID, value.Source) {
|
||||
value.ActorKind = constants.AuditActorSystemTask
|
||||
value.ActorID = "integration_log"
|
||||
value.Source = constants.AuditSourceWorker
|
||||
}
|
||||
event, err := r.audit.AppendAndGet(ctx, r.db, audit.AppendInput{
|
||||
EventID: "integration:" + integrationID,
|
||||
ActionCode: actionCode, Summary: "记录外部交互审计关联",
|
||||
Actor: audit.ActorInput{Kind: value.ActorKind, ID: value.ActorID, Name: value.ActorName, ShopID: value.ActorShopID, EnterpriseID: value.ActorEnterpriseID},
|
||||
Source: value.Source, ScopeType: constants.AuditScopePlatform,
|
||||
Result: constants.AuditResultSuccess, RequestID: textValue(correlationID), CorrelationID: textValue(correlationID),
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIntegrationLog, Key: integrationID, DisplayName: operation,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleWorkerIntegration,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"integration_id": integrationID, "provider": provider, "direction": direction, "operation": operation,
|
||||
"resource_type": resourceType, "resource_id": textValue(resourceID), "resource_key": textValue(resourceKey), "correlation_id": textValue(correlationID),
|
||||
},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
zap.L().Warn("Integration Log 缺少审计关联", zap.String("integration_id", integrationID), zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
return &event.ID
|
||||
}
|
||||
|
||||
func (r *Repository) auditEventIDForSeries(ctx context.Context, triggerSeries string) *uint {
|
||||
if r == nil || r.db == nil || triggerSeries == "" {
|
||||
return nil
|
||||
}
|
||||
var log model.IntegrationLog
|
||||
if err := r.db.WithContext(ctx).Where("trigger_series = ? AND audit_event_id IS NOT NULL", triggerSeries).Order("attempt DESC").First(&log).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return log.AuditEventID
|
||||
}
|
||||
|
||||
func validAuditOrigin(actorKind, actorID, source string) bool {
|
||||
if actorID == "" {
|
||||
return false
|
||||
}
|
||||
switch source {
|
||||
case constants.AuditSourceAdminAPI:
|
||||
return actorKind == constants.AuditActorAccount
|
||||
case constants.AuditSourcePersonalAPI:
|
||||
return actorKind == constants.AuditActorPersonalCustomer
|
||||
case constants.AuditSourceOpenAPI:
|
||||
return actorKind == constants.AuditActorOpenAPI
|
||||
case constants.AuditSourceCallback:
|
||||
return actorKind == constants.AuditActorExternalSystem
|
||||
case constants.AuditSourceScheduler:
|
||||
return actorKind == constants.AuditActorScheduledJob
|
||||
case constants.AuditSourceWorker:
|
||||
return actorKind == constants.AuditActorSystemTask
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func textValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
// ClaimExpiredInboundPending 原子认领已超过处理租约的入站 pending 记录。
|
||||
func (r *Repository) ClaimExpiredInboundPending(ctx context.Context, integrationID string, lease time.Duration) (bool, error) {
|
||||
if r == nil || r.db == nil {
|
||||
|
||||
Reference in New Issue
Block a user