package audit import ( "encoding/json" "fmt" "time" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "go.uber.org/zap" "gorm.io/datatypes" "gorm.io/gorm" ) func Record(db *gorm.DB, principal auth.Principal, action, resource string, resourceID uint64, payload interface{}) error { fields := []zap.Field{ zap.Uint64("tenant_id", principal.TenantID), zap.Uint64("user_id", principal.UserID), zap.String("action", action), zap.String("resource", resource), zap.Uint64("resource_id", resourceID), } if err := db.Transaction(func(tx *gorm.DB) error { return RecordTx(tx, principal, action, resource, resourceID, payload) }); err != nil { zap.L().Error("persist business event", append(fields, zap.Error(err))...) return err } logBusinessEvent(fields) return nil } // RecordTx persists the audit entry and its projection event in the caller's // transaction. Callers that mutate business data should use this function so // the mutation and Outbox event cannot diverge. func RecordTx(tx *gorm.DB, principal auth.Principal, action, resource string, resourceID uint64, payload interface{}) error { data, err := json.Marshal(payload) if err != nil { return err } entry := model.AuditLog{TenantID: principal.TenantID, UserID: principal.UserID, Action: action, Resource: resource, ResourceID: resourceID, Payload: datatypes.JSON(data)} if err := tx.Create(&entry).Error; err != nil { return err } if !isProjected(resource, action) { return nil } auditID := entry.ID outbox := model.MultiTableOutbox{TenantID: principal.TenantID, AuditLogID: &auditID, Resource: resource, ResourceID: resourceID, Action: action, Payload: datatypes.JSON(data), DedupeKey: fmt.Sprintf("audit:%d", entry.ID), Status: "pending", AvailableAt: time.Now()} return tx.Create(&outbox).Error } func isProjected(resource, action string) bool { switch resource { case "scenario": return action == "create" || action == "update" || action == "archive" case "scenario_field": return action == "create" || action == "update" || action == "delete" case "sop": return action == "publish" || action == "offline" case "scenario_rule", "knowledge_item", "knowledge_relation": return action == "create" || action == "update" || action == "delete" || action == "archive" case "sop_run": return action == "start" || action == "finish" || action == "feedback" default: return false } } func logBusinessEvent(fields []zap.Field) { zap.L().Info("business event", fields...) }