feat(ai-image): AI生图开发

This commit is contained in:
2026-09-27 22:40:52 +08:00
parent e670d06ca6
commit a29dd92676
105 changed files with 9943 additions and 361 deletions
@@ -0,0 +1,28 @@
SET search_path TO atlas, public;
-- 派生任务独立保存源图和结果,避免编辑草稿确认前修改原图或其他 SKU 的选图。
CREATE TABLE media_aspect_ratio_derivatives (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
source_asset_id UUID NOT NULL,
output_asset_id UUID,
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'succeeded', 'failed')),
error_code TEXT,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
FOREIGN KEY (workspace_id, source_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
FOREIGN KEY (workspace_id, output_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
UNIQUE (workspace_id, id),
CHECK ((status = 'succeeded') = (output_asset_id IS NOT NULL)),
CHECK (status <> 'failed' OR error_code IS NOT NULL)
);
CREATE INDEX ix_media_aspect_ratio_derivatives_source
ON media_aspect_ratio_derivatives (workspace_id, source_asset_id, created_at DESC);
ALTER TABLE media_aspect_ratio_derivatives ENABLE ROW LEVEL SECURITY;
ALTER TABLE media_aspect_ratio_derivatives FORCE ROW LEVEL SECURITY;
CREATE POLICY workspace_isolation ON media_aspect_ratio_derivatives
USING (workspace_id = app_current_workspace_id())
WITH CHECK (workspace_id = app_current_workspace_id());
@@ -0,0 +1,44 @@
SET search_path TO atlas, public;
-- 精确坐标按水印在预览区域内的可移动范围归一化;旧九宫格数据保留原位置。
ALTER TABLE watermark_template_versions
ADD COLUMN IF NOT EXISTS position_x NUMERIC(5,4),
ADD COLUMN IF NOT EXISTS position_y NUMERIC(5,4);
-- 迁移角色受强制 RLS 限制,回填期间临时关闭并在同一事务内恢复。
ALTER TABLE watermark_template_versions NO FORCE ROW LEVEL SECURITY;
ALTER TABLE watermark_template_versions DISABLE ROW LEVEL SECURITY;
UPDATE watermark_template_versions
SET position_x = CASE
WHEN position IN ('top_left', 'middle_left', 'bottom_left') THEN 0
WHEN position IN ('top_center', 'center', 'bottom_center') THEN 0.5
ELSE 1
END,
position_y = CASE
WHEN position IN ('top_left', 'top_center', 'top_right') THEN 0
WHEN position IN ('middle_left', 'center', 'middle_right') THEN 0.5
ELSE 1
END
WHERE position_x IS NULL OR position_y IS NULL;
ALTER TABLE watermark_template_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE watermark_template_versions FORCE ROW LEVEL SECURITY;
ALTER TABLE watermark_template_versions
ALTER COLUMN position_x SET NOT NULL,
ALTER COLUMN position_y SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'atlas.watermark_template_versions'::regclass
AND conname = 'watermark_template_versions_position_coordinates_check'
) THEN
ALTER TABLE watermark_template_versions
ADD CONSTRAINT watermark_template_versions_position_coordinates_check
CHECK (position_x BETWEEN 0 AND 1 AND position_y BETWEEN 0 AND 1);
END IF;
END
$$;
@@ -0,0 +1,48 @@
SET search_path TO atlas, public;
ALTER TABLE watermark_template_versions
ADD COLUMN IF NOT EXISTS image_size_px SMALLINT,
ADD COLUMN IF NOT EXISTS border_radius_px SMALLINT NOT NULL DEFAULT 0;
-- 旧图片模板的比例按原 240px 预览框中的显示宽度换算,保留可编辑的像素尺寸。
ALTER TABLE watermark_template_versions NO FORCE ROW LEVEL SECURITY;
ALTER TABLE watermark_template_versions DISABLE ROW LEVEL SECURITY;
UPDATE watermark_template_versions
SET image_size_px = LEAST(200, GREATEST(20, ROUND(scale_ratio * 144)::integer))
WHERE kind = 'image' AND image_size_px IS NULL;
ALTER TABLE watermark_template_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE watermark_template_versions FORCE ROW LEVEL SECURITY;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'atlas.watermark_template_versions'::regclass
AND conname = 'watermark_template_versions_image_size_check'
) THEN
ALTER TABLE watermark_template_versions
ADD CONSTRAINT watermark_template_versions_image_size_check
CHECK (image_size_px IS NULL OR image_size_px BETWEEN 20 AND 200);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'atlas.watermark_template_versions'::regclass
AND conname = 'watermark_template_versions_image_size_required_check'
) THEN
ALTER TABLE watermark_template_versions
ADD CONSTRAINT watermark_template_versions_image_size_required_check
CHECK (kind <> 'image' OR image_size_px IS NOT NULL);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'atlas.watermark_template_versions'::regclass
AND conname = 'watermark_template_versions_border_radius_check'
) THEN
ALTER TABLE watermark_template_versions
ADD CONSTRAINT watermark_template_versions_border_radius_check
CHECK (border_radius_px BETWEEN 0 AND 100);
END IF;
END
$$;
@@ -0,0 +1,32 @@
SET search_path TO atlas, public;
-- 水印派生引用不可变模板版本;编辑草稿确认前不改写原图或 SKU 选图。
CREATE TABLE media_watermark_derivatives (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
source_asset_id UUID NOT NULL,
watermark_template_id UUID NOT NULL,
watermark_template_version INTEGER NOT NULL CHECK (watermark_template_version > 0),
output_asset_id UUID,
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'succeeded', 'failed')),
error_code TEXT,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
FOREIGN KEY (workspace_id, source_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
FOREIGN KEY (workspace_id, output_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
FOREIGN KEY (workspace_id, watermark_template_id, watermark_template_version)
REFERENCES watermark_template_versions(workspace_id, template_id, version) ON DELETE RESTRICT,
UNIQUE (workspace_id, id),
CHECK ((status = 'succeeded') = (output_asset_id IS NOT NULL)),
CHECK (status <> 'failed' OR error_code IS NOT NULL)
);
CREATE INDEX ix_media_watermark_derivatives_source
ON media_watermark_derivatives (workspace_id, source_asset_id, created_at DESC);
ALTER TABLE media_watermark_derivatives ENABLE ROW LEVEL SECURITY;
ALTER TABLE media_watermark_derivatives FORCE ROW LEVEL SECURITY;
CREATE POLICY workspace_isolation ON media_watermark_derivatives
USING (workspace_id = app_current_workspace_id())
WITH CHECK (workspace_id = app_current_workspace_id());
@@ -0,0 +1,9 @@
SET search_path TO atlas, public;
-- 图片水印最小最长边从 20px 调整为 10px;已有模板数据保持不变。
ALTER TABLE watermark_template_versions
DROP CONSTRAINT IF EXISTS watermark_template_versions_image_size_check;
ALTER TABLE watermark_template_versions
ADD CONSTRAINT watermark_template_versions_image_size_check
CHECK (image_size_px IS NULL OR image_size_px BETWEEN 10 AND 200);
@@ -0,0 +1,8 @@
SET search_path TO atlas, public;
ALTER TABLE watermark_templates
ADD COLUMN IF NOT EXISTS is_default BOOLEAN NOT NULL DEFAULT false;
CREATE UNIQUE INDEX IF NOT EXISTS ux_watermark_templates_workspace_default
ON watermark_templates (workspace_id)
WHERE is_default AND status = 'active' AND deleted_at IS NULL;
@@ -0,0 +1,32 @@
SET search_path TO atlas, public;
-- 单张 AI 编辑保存素材 ID 和编辑参数;供应商任务 ID 只在工作空间内可见。
CREATE TABLE media_ai_edits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
source_asset_id UUID NOT NULL,
reference_asset_ids UUID[] NOT NULL DEFAULT '{}',
settings JSONB NOT NULL,
provider_task_id TEXT,
output_asset_id UUID,
status TEXT NOT NULL DEFAULT 'queued'
CHECK (status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')),
error_code TEXT,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
FOREIGN KEY (workspace_id, source_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
FOREIGN KEY (workspace_id, output_asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
UNIQUE (workspace_id, id),
CHECK (cardinality(reference_asset_ids) <= 2),
CHECK ((status = 'succeeded') = (output_asset_id IS NOT NULL)),
CHECK (status <> 'failed' OR error_code IS NOT NULL)
);
CREATE INDEX ix_media_ai_edits_source ON media_ai_edits (workspace_id, source_asset_id, created_at DESC);
ALTER TABLE media_ai_edits ENABLE ROW LEVEL SECURITY;
ALTER TABLE media_ai_edits FORCE ROW LEVEL SECURITY;
CREATE POLICY workspace_isolation ON media_ai_edits
USING (workspace_id = app_current_workspace_id())
WITH CHECK (workspace_id = app_current_workspace_id());
@@ -0,0 +1,10 @@
SET search_path TO atlas, public;
-- AI 生图记录按商品归档;旧任务若没有关联商品,不出现在商品素材库中。
ALTER TABLE media_ai_edits
ADD COLUMN product_id UUID,
ADD CONSTRAINT media_ai_edits_product_fk
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT;
CREATE INDEX ix_media_ai_edits_product
ON media_ai_edits (workspace_id, product_id, completed_at DESC)
WHERE status = 'succeeded';
@@ -0,0 +1,89 @@
SET search_path TO atlas, public;
CREATE TYPE platform_listing_source AS ENUM ('atlas_published', 'external_sync');
CREATE TYPE platform_sync_status AS ENUM ('queued', 'running', 'succeeded', 'failed', 'cancelled');
CREATE TABLE platform_product_sync_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
store_id UUID NOT NULL,
platform_id UUID NOT NULL,
status platform_sync_status NOT NULL DEFAULT 'queued',
trigger_source TEXT NOT NULL CHECK (trigger_source IN ('manual', 'scheduled', 'recovery')),
cursor_started TEXT,
cursor_finished TEXT,
listed_count INTEGER NOT NULL DEFAULT 0 CHECK (listed_count >= 0),
upserted_count INTEGER NOT NULL DEFAULT 0 CHECK (upserted_count >= 0),
failed_count INTEGER NOT NULL DEFAULT 0 CHECK (failed_count >= 0),
error_payload JSONB,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (workspace_id, store_id, platform_id) REFERENCES stores(workspace_id, id, platform_id) ON DELETE RESTRICT,
UNIQUE (workspace_id, id)
);
CREATE INDEX ix_platform_product_sync_runs_store ON platform_product_sync_runs (workspace_id, store_id, created_at DESC, id DESC);
CREATE UNIQUE INDEX ux_platform_product_sync_runs_active ON platform_product_sync_runs (workspace_id, store_id) WHERE status IN ('queued', 'running');
CREATE TABLE platform_listings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
store_id UUID NOT NULL,
platform_id UUID NOT NULL,
remote_product_id TEXT NOT NULL,
offer_id TEXT NOT NULL,
remote_sku TEXT,
name TEXT,
currency_code CHAR(3) REFERENCES currencies(code),
selling_price NUMERIC(18,4) CHECK (selling_price IS NULL OR selling_price >= 0),
list_price NUMERIC(18,4) CHECK (list_price IS NULL OR list_price >= 0),
visible BOOLEAN,
remote_state TEXT,
moderation_status TEXT,
validation_status TEXT,
stock_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
image_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
error_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
visibility_details JSONB NOT NULL DEFAULT '{}'::jsonb,
source platform_listing_source NOT NULL DEFAULT 'external_sync',
atlas_variant_listing_id UUID,
last_sync_run_id UUID,
last_synced_at TIMESTAMPTZ NOT NULL,
missing_since TIMESTAMPTZ,
archived_at TIMESTAMPTZ,
raw_payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (workspace_id, store_id, platform_id) REFERENCES stores(workspace_id, id, platform_id) ON DELETE RESTRICT,
FOREIGN KEY (workspace_id, atlas_variant_listing_id) REFERENCES product_variant_platform_listings(workspace_id, id) ON DELETE SET NULL,
FOREIGN KEY (workspace_id, last_sync_run_id) REFERENCES platform_product_sync_runs(workspace_id, id) ON DELETE SET NULL,
UNIQUE (workspace_id, id),
UNIQUE (workspace_id, store_id, platform_id, remote_product_id),
UNIQUE (workspace_id, store_id, platform_id, offer_id)
);
CREATE INDEX ix_platform_listings_store_list ON platform_listings (workspace_id, store_id, updated_at DESC, id DESC);
CREATE INDEX ix_platform_listings_store_visibility ON platform_listings (workspace_id, store_id, visible, updated_at DESC);
CREATE INDEX ix_platform_listings_atlas_mapping ON platform_listings (workspace_id, atlas_variant_listing_id) WHERE atlas_variant_listing_id IS NOT NULL;
CREATE TABLE platform_listing_sync_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
platform_listing_id UUID NOT NULL,
sync_run_id UUID NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (workspace_id, platform_listing_id) REFERENCES platform_listings(workspace_id, id) ON DELETE CASCADE,
FOREIGN KEY (workspace_id, sync_run_id) REFERENCES platform_product_sync_runs(workspace_id, id) ON DELETE CASCADE,
UNIQUE (workspace_id, platform_listing_id, sync_run_id)
);
-- 商品镜像、同步运行和响应快照均强制租户隔离,Worker 也必须在租户事务中访问。
DO $$
DECLARE table_name TEXT;
BEGIN
FOREACH table_name IN ARRAY ARRAY['platform_product_sync_runs', 'platform_listings', 'platform_listing_sync_snapshots'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
EXECUTE format('CREATE POLICY workspace_isolation ON %I USING (workspace_id = app_current_workspace_id()) WITH CHECK (workspace_id = app_current_workspace_id())', table_name);
END LOOP;
END $$;
@@ -0,0 +1,18 @@
SET search_path TO atlas, public;
-- 派生记录绑定商品,供选图弹窗找回未加入 SKU 草稿的处理结果。
ALTER TABLE media_aspect_ratio_derivatives
ADD COLUMN product_id UUID,
ADD CONSTRAINT media_aspect_ratio_derivatives_product_fk
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT;
CREATE INDEX ix_media_aspect_ratio_derivatives_product
ON media_aspect_ratio_derivatives (workspace_id, product_id, completed_at DESC)
WHERE status = 'succeeded';
ALTER TABLE media_watermark_derivatives
ADD COLUMN product_id UUID,
ADD CONSTRAINT media_watermark_derivatives_product_fk
FOREIGN KEY (workspace_id, product_id) REFERENCES products(workspace_id, id) ON DELETE RESTRICT;
CREATE INDEX ix_media_watermark_derivatives_product
ON media_watermark_derivatives (workspace_id, product_id, completed_at DESC)
WHERE status = 'succeeded';