- 迁移 043 一次到位:runtime_instance/environment_binding 并入 browser_env(账号即环境)、 credential_reference 并入 social_account、work_source/metric_plan 并入 creator_work、 sync 租约并入 creator_collection_checkpoint(kind='sync')、audit_event 瘦列、 creator_settings 去转写列、TRUNCATE 账号域(竞品采集数据无损保留) - 统一迁移 runner:environment.Open 串起 001 账号基座 + hub 链 + 038 creator 版本回填 (v+1000) + creator 1017-1042 + 043;account/creator Open 不再迁移 - 三域 store SQL 全量适配:CreateBoundEnv 就绪门禁、SetRuntimeCleanupPending fence 语义(未知代 MissingRuntimeID 允许登记并释放活跃实例)、metric_plan 列并入 work、 checkpoint sync 租约、deletion 链路重写 - api 适配:envView 去 runtime_instance_id、audit filter 去任务列、browser_env 单表查询 - 测试:迁移 043 形态探针、迁移不可变映射新路径、legacy 迁移重放子测试重写、 fence/出口生命周期/单来源 upsert 语义覆盖;CREATORHUB_POSTGRES_TEST_URL 全绿, environment 70%/creator 67.5% 覆盖率,tsc 通过
175 lines
5.4 KiB
Go
175 lines
5.4 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 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 secret_reference_id, secret_key
|
|
FROM creator_account_password
|
|
WHERE 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 WHERE account_id = $1`, accountID); err != nil {
|
|
return databaseError(err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_profile WHERE 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 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 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 id FROM social_account WHERE 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()))
|
|
)`).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
|
|
}
|