Files
creator-hub/internal/creator/deletion.go
T

176 lines
5.7 KiB
Go

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
}