CREATE TABLE enterprise_site_connections ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES tenants(id), workspace_id BIGINT NOT NULL REFERENCES workspaces(id), created_by_user_id BIGINT NOT NULL REFERENCES users(id), name VARCHAR(160) NOT NULL, site_url TEXT NOT NULL, cms_type VARCHAR(32) NOT NULL, auth_type VARCHAR(32) NOT NULL DEFAULT 'native_api', appid VARCHAR(160) NOT NULL, credential_ciphertext TEXT NOT NULL, default_acode VARCHAR(32), default_mcode VARCHAR(32), default_scode VARCHAR(64), status VARCHAR(20) NOT NULL DEFAULT 'draft', plugin_version VARCHAR(40), site_name VARCHAR(160), last_ping_at TIMESTAMPTZ, last_sync_at TIMESTAMPTZ, last_publish_at TIMESTAMPTZ, last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT ck_enterprise_site_connections_cms_type CHECK (cms_type IN ('pbootcms', 'wordpress', 'dedecms', 'phpcms')), CONSTRAINT ck_enterprise_site_connections_status CHECK (status IN ('draft', 'connected', 'error', 'disabled')) ); CREATE UNIQUE INDEX uk_enterprise_site_connections_active_site ON enterprise_site_connections (tenant_id, workspace_id, lower(site_url), cms_type) WHERE deleted_at IS NULL; CREATE INDEX idx_enterprise_site_connections_tenant_workspace ON enterprise_site_connections (tenant_id, workspace_id, status, created_at DESC) WHERE deleted_at IS NULL; CREATE TABLE enterprise_site_categories ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES tenants(id), workspace_id BIGINT NOT NULL REFERENCES workspaces(id), connection_id BIGINT NOT NULL REFERENCES enterprise_site_connections(id) ON DELETE CASCADE, remote_id VARCHAR(128) NOT NULL, parent_remote_id VARCHAR(128), name VARCHAR(160) NOT NULL, slug VARCHAR(160), raw_payload_json JSONB, synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uk_enterprise_site_categories_remote UNIQUE (connection_id, remote_id) ); CREATE INDEX idx_enterprise_site_categories_connection ON enterprise_site_categories (connection_id, remote_id); ALTER TABLE publish_records ALTER COLUMN platform_account_id DROP NOT NULL; ALTER TABLE publish_records ADD COLUMN target_type VARCHAR(32) NOT NULL DEFAULT 'platform_account', ADD COLUMN target_connection_id BIGINT REFERENCES enterprise_site_connections(id); ALTER TABLE publish_records ADD CONSTRAINT ck_publish_records_target_type CHECK (target_type IN ('platform_account', 'enterprise_site')); ALTER TABLE publish_records ADD CONSTRAINT ck_publish_records_target_ref CHECK ( (target_type = 'platform_account' AND platform_account_id IS NOT NULL AND target_connection_id IS NULL) OR (target_type = 'enterprise_site' AND platform_account_id IS NULL AND target_connection_id IS NOT NULL) ); CREATE UNIQUE INDEX uk_publish_records_batch_enterprise_site ON publish_records (publish_batch_id, target_connection_id) WHERE target_type = 'enterprise_site'; CREATE INDEX idx_publish_records_enterprise_site_active ON publish_records (tenant_id, article_id, target_connection_id, updated_at DESC, id DESC) WHERE target_type = 'enterprise_site'; INSERT INTO media_platforms (platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order) VALUES ('pbootcms', 'PBootCMS', 'enterprise', 'P', '#1d4ed8', NULL, NULL, 'active', 900) ON CONFLICT (platform_id) DO UPDATE SET name = EXCLUDED.name, category = EXCLUDED.category, short_name = EXCLUDED.short_name, accent_color = EXCLUDED.accent_color, status = EXCLUDED.status, sort_order = EXCLUDED.sort_order, updated_at = NOW();