feat(edit): 编辑上报细节优化
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- 历史记录无法可靠推断归属,保留空值但不再作为任何商品的候选图片。
|
||||
ALTER TABLE media_upload_intents ADD COLUMN product_id UUID;
|
||||
ALTER TABLE media_upload_intents ADD CONSTRAINT media_upload_intents_product_fk
|
||||
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT;
|
||||
ALTER TABLE media_upload_intents ADD CONSTRAINT media_upload_intents_product_purpose_check
|
||||
CHECK (product_id IS NULL OR purpose = 'product_image');
|
||||
CREATE INDEX ix_media_upload_intents_product_ready
|
||||
ON media_upload_intents (workspace_id, product_id, completed_at DESC)
|
||||
WHERE purpose = 'product_image' AND status = 'ready';
|
||||
|
||||
ALTER TABLE product_image_imports ADD COLUMN product_id UUID;
|
||||
ALTER TABLE product_image_imports ADD CONSTRAINT product_image_imports_product_fk
|
||||
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT;
|
||||
ALTER TABLE product_image_imports DROP CONSTRAINT product_image_imports_workspace_id_source_url_key;
|
||||
ALTER TABLE product_image_imports ADD CONSTRAINT product_image_imports_product_url_unique
|
||||
UNIQUE (workspace_id, product_id, source_url);
|
||||
CREATE INDEX ix_product_image_imports_product_recent
|
||||
ON product_image_imports (workspace_id, product_id, updated_at DESC, id DESC);
|
||||
@@ -0,0 +1,63 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- 素材保留审计关系,删除后释放内容摘要,重新上传会创建独立素材和对象路径。
|
||||
ALTER TABLE media_assets ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
ALTER TABLE media_assets ADD COLUMN deleted_checksum_sha256 TEXT;
|
||||
ALTER TABLE media_assets ALTER COLUMN checksum_sha256 DROP NOT NULL;
|
||||
ALTER TABLE media_assets ADD CONSTRAINT media_assets_deletion_shape
|
||||
CHECK ((deleted_at IS NULL AND checksum_sha256 IS NOT NULL)
|
||||
OR (deleted_at IS NOT NULL AND checksum_sha256 IS NULL));
|
||||
ALTER TABLE collection_asset_transfers ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
|
||||
CREATE TABLE product_image_deletions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL,
|
||||
product_id UUID NOT NULL,
|
||||
asset_id UUID,
|
||||
collection_image_id UUID,
|
||||
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'succeeded', 'failed')),
|
||||
objects JSONB NOT NULL DEFAULT '[]',
|
||||
error_code TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (workspace_id, asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (collection_image_id) REFERENCES collection_asset_transfers(id) ON DELETE RESTRICT,
|
||||
UNIQUE (workspace_id, id),
|
||||
UNIQUE (workspace_id, asset_id),
|
||||
UNIQUE (workspace_id, collection_image_id),
|
||||
CHECK (asset_id IS NOT NULL OR collection_image_id IS NOT NULL)
|
||||
);
|
||||
ALTER TABLE product_image_deletions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE product_image_deletions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY workspace_isolation ON product_image_deletions
|
||||
USING (workspace_id = app_current_workspace_id()) WITH CHECK (workspace_id = app_current_workspace_id());
|
||||
ALTER TABLE collection_asset_transfers ADD CONSTRAINT collection_asset_transfers_product_identity UNIQUE (workspace_id, id, product_id);
|
||||
ALTER TABLE product_image_deletions ADD CONSTRAINT product_image_deletions_collection_scope
|
||||
FOREIGN KEY (workspace_id, collection_image_id, product_id) REFERENCES collection_asset_transfers(workspace_id, id, product_id);
|
||||
|
||||
-- 引用写入锁定素材,与删除事务的排他锁串行;历史引用保留,但不能重新选择已删除素材。
|
||||
CREATE FUNCTION reject_deleted_media_reference() RETURNS trigger LANGUAGE plpgsql SET search_path = atlas, public AS $$
|
||||
DECLARE column_name TEXT; asset UUID; removed TIMESTAMPTZ;
|
||||
BEGIN
|
||||
FOREACH column_name IN ARRAY TG_ARGV LOOP
|
||||
FOR asset IN SELECT value::uuid FROM jsonb_array_elements_text(
|
||||
CASE WHEN jsonb_typeof(to_jsonb(NEW)->column_name) = 'array' THEN to_jsonb(NEW)->column_name
|
||||
ELSE jsonb_build_array(to_jsonb(NEW)->column_name) END) value WHERE value IS NOT NULL LOOP
|
||||
SELECT deleted_at INTO removed FROM media_assets WHERE id = asset FOR KEY SHARE;
|
||||
IF removed IS NOT NULL THEN RAISE EXCEPTION 'media asset has been deleted' USING ERRCODE = '23514'; END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
DO $$ DECLARE rel RECORD; BEGIN
|
||||
FOR rel IN SELECT c.conrelid::regclass AS tab, a.attname AS col
|
||||
FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.contype = 'f' AND c.confrelid = 'atlas.media_assets'::regclass
|
||||
AND a.attname <> 'workspace_id' AND c.conrelid <> 'atlas.product_image_deletions'::regclass LOOP
|
||||
EXECUTE format('CREATE TRIGGER %I BEFORE INSERT OR UPDATE OF %I ON %s FOR EACH ROW EXECUTE FUNCTION reject_deleted_media_reference(%L)',
|
||||
'guard_deleted_' || rel.col, rel.col, rel.tab, rel.col);
|
||||
END LOOP;
|
||||
END $$;
|
||||
CREATE TRIGGER guard_deleted_ai_references BEFORE INSERT OR UPDATE OF reference_asset_ids ON media_ai_edits
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_deleted_media_reference('reference_asset_ids');
|
||||
@@ -0,0 +1,21 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- 删除保护也落在数据库,禁止直接更新绕过 SKU、头像、水印和活跃任务的引用校验。
|
||||
CREATE FUNCTION guard_media_asset_deletion() RETURNS trigger LANGUAGE plpgsql SET search_path = atlas, public AS $$
|
||||
BEGIN
|
||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||
PERFORM 1 FROM media_assets WHERE id = OLD.id FOR UPDATE;
|
||||
IF EXISTS (SELECT 1 FROM product_variant_listing_media WHERE asset_id = OLD.id)
|
||||
OR EXISTS (SELECT 1 FROM watermark_template_versions WHERE image_asset_id = OLD.id)
|
||||
OR EXISTS (SELECT 1 FROM users WHERE avatar_asset_id = OLD.id)
|
||||
OR EXISTS (SELECT 1 FROM media_ai_edits WHERE status IN ('queued','running') AND (source_asset_id = OLD.id OR OLD.id = ANY(reference_asset_ids)))
|
||||
OR EXISTS (SELECT 1 FROM media_aspect_ratio_derivatives WHERE status IN ('queued','running') AND source_asset_id = OLD.id)
|
||||
OR EXISTS (SELECT 1 FROM media_watermark_derivatives WHERE status IN ('queued','running') AND source_asset_id = OLD.id)
|
||||
OR EXISTS (SELECT 1 FROM product_image_suite_references r JOIN product_image_suites s ON s.id = r.suite_id WHERE r.asset_id = OLD.id AND s.status IN ('queued','running')) THEN
|
||||
RAISE EXCEPTION 'media asset is in use' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER guard_media_asset_deletion BEFORE UPDATE OF deleted_at ON media_assets
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_media_asset_deletion();
|
||||
Reference in New Issue
Block a user