-- ============================================================================= -- 镭射卡重构 v3 — 业务表迁移(Phase 1) -- 文档:docs/specs/2026-05-25-laser-card-refactor-design.md §6.5–6.11 -- 创建日期:2026-05-27 -- -- 说明: -- 1. 本期新增 laser_card_templates / laser_card_instances / laser_card_operation_logs -- 2. 不对 assets / asset_registry 做 ALTER(品类+工艺走 assets.tags 约定,见 §6.3.1) -- 3. 依赖:public.assets、public.mint_orders、public.materials 已存在 -- 执行:psql -U -d -f migrate_laser_card_v3_tables.sql -- 备注:§4 含全部 COMMENT ON(表/字段/索引/约束),可重复执行;已建表无备注时可单独跑 migrate_laser_card_v3_comments_only.sql -- ============================================================================= BEGIN; -- ----------------------------------------------------------------------------- -- 1. laser_card_templates -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.laser_card_templates ( id BIGSERIAL PRIMARY KEY, template_code VARCHAR(64) NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, status VARCHAR(20) NOT NULL DEFAULT 'published', version INT NOT NULL DEFAULT 1, thumbnail_oss_key VARCHAR(255), backdrop_options JSONB NOT NULL DEFAULT '[]'::jsonb, render_config JSONB NOT NULL DEFAULT '{}'::jsonb, engine_min_version VARCHAR(32) NOT NULL DEFAULT 'compositor-1.0.0', sort_order INT NOT NULL DEFAULT 0, star_id BIGINT NOT NULL DEFAULT 0, created_by BIGINT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT ); CREATE UNIQUE INDEX IF NOT EXISTS uk_lct_code_version ON public.laser_card_templates (template_code, version) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_lct_status_star ON public.laser_card_templates (status, star_id) WHERE deleted_at IS NULL; -- ----------------------------------------------------------------------------- -- 2. laser_card_instances -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.laser_card_instances ( id BIGSERIAL PRIMARY KEY, instance_no VARCHAR(32) NOT NULL, instance_ulid VARCHAR(40) NOT NULL, template_id BIGINT NOT NULL, template_code VARCHAR(64) NOT NULL, template_version INT NOT NULL, owner_user_id BIGINT NOT NULL, star_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'rendered', client_request_id VARCHAR(64), render_config JSONB, materials_snapshot JSONB NOT NULL DEFAULT '[]'::jsonb, composite_oss_key VARCHAR(255), composite_material_id BIGINT, asset_id BIGINT, mint_order_id VARCHAR(100), idempotency_key VARCHAR(64), version INT NOT NULL DEFAULT 1, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT, CONSTRAINT fk_lci_template FOREIGN KEY (template_id) REFERENCES public.laser_card_templates (id), CONSTRAINT fk_lci_asset FOREIGN KEY (asset_id) REFERENCES public.assets (id) ON DELETE SET NULL, CONSTRAINT fk_lci_mint_order FOREIGN KEY (mint_order_id) REFERENCES public.mint_orders (order_id) ON DELETE SET NULL, CONSTRAINT fk_lci_composite_material FOREIGN KEY (composite_material_id) REFERENCES public.materials (id) ON DELETE SET NULL, CONSTRAINT chk_lci_status CHECK (status IN ('rendered', 'minting', 'minted')) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_lci_instance_no ON public.laser_card_instances (instance_no); CREATE UNIQUE INDEX IF NOT EXISTS uk_lci_instance_ulid ON public.laser_card_instances (instance_ulid); CREATE UNIQUE INDEX IF NOT EXISTS uk_lci_user_client_req ON public.laser_card_instances (owner_user_id, client_request_id) WHERE deleted_at IS NULL AND client_request_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_lci_owner_status ON public.laser_card_instances (owner_user_id, status) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_lci_asset ON public.laser_card_instances (asset_id) WHERE asset_id IS NOT NULL AND deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_lci_star_created ON public.laser_card_instances (star_id, created_at DESC) WHERE deleted_at IS NULL; -- ----------------------------------------------------------------------------- -- 3. laser_card_operation_logs -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.laser_card_operation_logs ( id BIGSERIAL PRIMARY KEY, instance_id BIGINT NOT NULL, instance_no VARCHAR(32) NOT NULL, operator_user_id BIGINT NOT NULL, action VARCHAR(50) NOT NULL, status_before VARCHAR(20), status_after VARCHAR(20), request_id VARCHAR(64), payload_json JSONB, result_json JSONB, ip_address VARCHAR(45), user_agent VARCHAR(255), latency_ms INT, err_code VARCHAR(32), created_at BIGINT NOT NULL, CONSTRAINT fk_lclog_instance FOREIGN KEY (instance_id) REFERENCES public.laser_card_instances (id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_lclog_instance_time ON public.laser_card_operation_logs (instance_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_lclog_operator_time ON public.laser_card_operation_logs (operator_user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_lclog_action_time ON public.laser_card_operation_logs (action, created_at DESC); -- ----------------------------------------------------------------------------- -- 4. 表 / 字段 / 索引 / 约束备注(COMMENT,可重复执行) -- ----------------------------------------------------------------------------- \ir migrate_laser_card_v3_comments.sql -- ----------------------------------------------------------------------------- -- 5. 种子数据:5 套预设(与 frontend/utils/laser-card/laserPresets.js 对齐) -- ----------------------------------------------------------------------------- INSERT INTO public.laser_card_templates ( template_code, name, status, version, backdrop_options, render_config, engine_min_version, sort_order, star_id, created_by, created_at, updated_at ) SELECT v.template_code, v.name, 'published', 1, v.backdrop_options::jsonb, v.render_config::jsonb, 'compositor-1.0.0', v.sort_order, 0, 0, (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint, (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint FROM ( VALUES ( 'dream', '梦幻', 0, '[{"id":"liquidBlue","label":"液态蓝","oss_key":"static/laser-bg/laser-bg-1.png"}]', '{"style":"dream","angle":36,"beam":"prism","backdrop":"liquidBlue","laserStrength":78,"diffractionDensity":68,"marbleFlow":52,"beamIntensity":92,"grainDensity":82}' ), ( 'classic', '经典', 1, '[{"id":"liquidLavender","label":"液态紫","oss_key":"static/laser-bg/laser-bg-2.png"}]', '{"style":"classic","angle":24,"beam":"aurora","backdrop":"liquidLavender","laserStrength":85,"diffractionDensity":74,"marbleFlow":38,"beamIntensity":96,"grainDensity":76}' ), ( 'holoFull', '全息', 2, '[{"id":"liquidPearl","label":"液态珍珠","oss_key":"static/laser-bg/laser-bg-3.png"}]', '{"style":"holoFull","angle":58,"beam":"neon","backdrop":"liquidPearl","laserStrength":90,"diffractionDensity":80,"marbleFlow":58,"beamIntensity":100,"grainDensity":88}' ), ( 'ice', '冰蓝', 3, '[{"id":"liquidBlue","label":"液态蓝","oss_key":"static/laser-bg/laser-bg-1.png"}]', '{"style":"ice","angle":46,"beam":"white","backdrop":"liquidBlue","laserStrength":72,"diffractionDensity":60,"marbleFlow":44,"beamIntensity":88,"grainDensity":74}' ), ( 'sunset', '晚霞', 4, '[{"id":"liquidLavender","label":"液态紫","oss_key":"static/laser-bg/laser-bg-2.png"}]', '{"style":"sunset","angle":30,"beam":"sunset","backdrop":"liquidLavender","laserStrength":82,"diffractionDensity":66,"marbleFlow":50,"beamIntensity":94,"grainDensity":84}' ) ) AS v (template_code, name, sort_order, backdrop_options, render_config) WHERE NOT EXISTS ( SELECT 1 FROM public.laser_card_templates t WHERE t.template_code = v.template_code AND t.version = 1 AND t.deleted_at IS NULL ); COMMIT;