package memorylifecycle import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/database" "data-terminal-cloud/internal/outbox" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) const deletedMemoryMarker = "[已按用户请求删除]" type DeleteProcessor struct { pool *pgxpool.Pool audit *audit.Repository } func NewDeleteProcessor(pool *pgxpool.Pool) (*DeleteProcessor, error) { if pool == nil { return nil, errors.New("database pool is required") } return &DeleteProcessor{pool: pool, audit: audit.NewRepository()}, nil } func (p *DeleteProcessor) Handle(ctx context.Context, event outbox.Event) error { return database.Transact(ctx, p.pool, func(tx pgx.Tx) error { var tenantID, memoryPublicID, status string err := tx.QueryRow(ctx, `SELECT tenant_id::text,public_id::text,status FROM curated_memory WHERE id=$1::uuid FOR UPDATE`, event.AggregateID).Scan(&tenantID, &memoryPublicID, &status) if errors.Is(err, pgx.ErrNoRows) || status == "DELETED" { return nil } if err != nil { return fmt.Errorf("load memory deletion: %w", err) } if status != "PENDING_DELETE" { return nil } var unresolvedProviderRefs int if err := tx.QueryRow(ctx, `SELECT count(*) FROM memory_index_entry WHERE memory_id=$1::uuid AND index_type='provider' AND provider_ref IS NOT NULL AND status<>'DELETED'`, event.AggregateID).Scan(&unresolvedProviderRefs); err != nil { return fmt.Errorf("check provider memory deletion: %w", err) } if unresolvedProviderRefs > 0 { return outbox.Retryable(errors.New("provider memory deletion has not reconciled")) } if _, err := tx.Exec(ctx, `UPDATE memory_index_entry SET embedding=NULL,status='DELETED',provider_ref=NULL,indexed_at=NULL,last_error=NULL,updated_at=now() WHERE memory_id=$1::uuid`, event.AggregateID); err != nil { return fmt.Errorf("delete memory indexes: %w", err) } digest := sha256.Sum256([]byte(deletedMemoryMarker)) if _, err := tx.Exec(ctx, `UPDATE memory_version SET content=$2,content_digest=$3,change_reason=CASE WHEN COALESCE(change_reason,'')='' THEN 'user requested deletion' ELSE change_reason || '; user requested deletion' END WHERE memory_id=$1::uuid`, event.AggregateID, deletedMemoryMarker, hex.EncodeToString(digest[:])); err != nil { return fmt.Errorf("redact memory versions: %w", err) } if _, err := tx.Exec(ctx, `UPDATE curated_memory SET status='DELETED',deleted_at=now(),valid_to=COALESCE(valid_to,now()),version=version+1,updated_at=now() WHERE id=$1::uuid`, event.AggregateID); err != nil { return fmt.Errorf("complete memory deletion: %w", err) } return p.audit.Record(ctx, tx, audit.Event{TenantID: &tenantID, ActorType: "system", Action: "memory.delete.complete", ResourceType: "memory", ResourceID: &memoryPublicID, Result: "success", Detail: map[string]any{"local_index": "deleted", "database_content": "redacted"}}) }) }