package creator import ( "context" "database/sql" "errors" "time" ) func (s *Store) CheckOwnedAccountDeletion(ctx context.Context, accountID string) error { if accountID == "" { return ErrInvalid } var exists bool if err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM social_account WHERE account_id = $1)`, accountID).Scan(&exists); err != nil { return databaseError(err) } if !exists { return ErrNotFound } var active bool if err := s.db.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'owned' AND source_id = $1 AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now())) )`, accountID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } return nil } func (s *Store) DeleteOwnedAccountData(ctx context.Context, accountID string) error { if accountID == "" { return ErrInvalid } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return errors.New("begin creator account deletion") } defer tx.Rollback() if err := lockCreatorAccount(ctx, tx, accountID); err != nil { return err } if err := checkOwnedAccountDeletionTx(ctx, tx, accountID); err != nil { return err } var secretID, secretKey string var hasSecret bool if err := tx.QueryRowContext(ctx, ` SELECT p.secret_reference_id, p.secret_key FROM creator_account_password p JOIN social_account account ON account.id = p.account_id WHERE account.account_id = $1`, accountID).Scan(&secretID, &secretKey); err == nil { hasSecret = true } else if !errors.Is(err, sql.ErrNoRows) { return databaseError(err) } if hasSecret && s.secrets == nil { return errors.New("creator secret bridge is unavailable") } if err := deleteCreatorSourceTx(ctx, tx, SourceOwned, accountID); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_password p USING social_account account WHERE account.id = p.account_id AND account.account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_profile p USING social_account account WHERE account.id = p.account_id AND account.account_id = $1`, accountID); err != nil { return databaseError(err) } if err := tx.Commit(); err != nil { return errors.New("commit creator account deletion") } if hasSecret { if err := s.secrets.Delete(context.WithoutCancel(ctx), SecretReference{ID: secretID, Provider: "os_keyring"}, secretKey); err != nil { return errors.Join(errors.New("delete creator account password"), err) } } return nil } func (s *Store) DeleteCompetitor(ctx context.Context, competitorID string) error { if competitorID == "" { return ErrInvalid } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return errors.New("begin competitor deletion") } defer tx.Rollback() var syncStatus string var syncLease sql.NullTime if err := tx.QueryRowContext(ctx, ` SELECT sync_status, sync_lease_until FROM creator_competitor WHERE competitor_id = $1 FOR UPDATE`, competitorID).Scan(&syncStatus, &syncLease); err != nil { return rowError(err) } if syncStatus == "running" || (syncLease.Valid && syncLease.Time.After(time.Now())) { return ErrConflict } var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'competitor' AND source_id = $1 AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now())) )`, competitorID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } if err := deleteCreatorSourceTx(ctx, tx, SourceCompetitor, competitorID); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_competitor WHERE competitor_id = $1`, competitorID); err != nil { return databaseError(err) } if err := tx.Commit(); err != nil { return errors.New("commit competitor deletion") } return nil } func lockCreatorAccount(ctx context.Context, tx *sql.Tx, accountID string) error { var lockedID string if err := tx.QueryRowContext(ctx, `SELECT account_id FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID).Scan(&lockedID); err != nil { return rowError(err) } return nil } func checkOwnedAccountDeletionTx(ctx context.Context, tx *sql.Tx, accountID string) error { var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'owned' AND source_id = $1 AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now())) )`, accountID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } return nil } func deleteCreatorSourceTx(ctx context.Context, tx *sql.Tx, sourceType, sourceID string) error { var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_collection_checkpoint WHERE source_type = $1 AND source_id = $2 AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now())) )`, sourceType, sourceID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_collection_checkpoint WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } // 单来源内联模型:来源切换已由 upsert 覆盖内联列完成,这里删除仍归属该来源的作品(级联清 metric/comment/cover)。 if _, err := tx.ExecContext(ctx, `DELETE FROM creator_work WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } return nil }