CREATE TABLE IF NOT EXISTS qipai_notification_templates ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NULL, scope_store_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(store_id, 0)) STORED, template_code VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, event_type VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, channel VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, title_template VARCHAR(256) NOT NULL, body_template VARCHAR(2000) NOT NULL, external_template_id VARCHAR(128) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', version INT UNSIGNED NOT NULL DEFAULT 1, created_by BIGINT UNSIGNED NULL, updated_by BIGINT UNSIGNED NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_notification_template_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_notification_template_store FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id), CONSTRAINT fk_qipai_notification_template_creator FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id), CONSTRAINT fk_qipai_notification_template_updater FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_notification_template_scope (tenant_id, scope_store_id, template_code), UNIQUE KEY uq_qipai_notification_template_scope_id (tenant_id, id), KEY idx_qipai_notification_template_event (tenant_id, event_type, channel, status), CONSTRAINT chk_qipai_notification_template_channel CHECK (channel IN ('IN_APP', 'WECHAT_SUBSCRIBE', 'WE_COM', 'WEBHOOK', 'CLOUD_SPEAKER')), CONSTRAINT chk_qipai_notification_template_status CHECK (status IN ('ACTIVE', 'DISABLED')), CONSTRAINT chk_qipai_notification_template_version CHECK (version >= 1) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_notification_routes ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NULL, scope_store_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(store_id, 0)) STORED, event_type VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, template_id BIGINT UNSIGNED NOT NULL, recipient_type VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, recipient_value VARCHAR(191) NOT NULL, quiet_start TIME NULL, quiet_end TIME NULL, status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', version INT UNSIGNED NOT NULL DEFAULT 1, created_by BIGINT UNSIGNED NULL, updated_by BIGINT UNSIGNED NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_notification_route_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_notification_route_store FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id), CONSTRAINT fk_qipai_notification_route_template FOREIGN KEY (tenant_id, template_id) REFERENCES qipai_notification_templates(tenant_id, id), CONSTRAINT fk_qipai_notification_route_creator FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id), CONSTRAINT fk_qipai_notification_route_updater FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_notification_route_scope_id (tenant_id, id), UNIQUE KEY uq_qipai_notification_route (tenant_id, scope_store_id, event_type, template_id, recipient_type, recipient_value), KEY idx_qipai_notification_route_match (tenant_id, event_type, store_id, status), CONSTRAINT chk_qipai_notification_route_recipient CHECK (recipient_type IN ('CUSTOMER', 'USER', 'ROLE', 'STORE_WEBHOOK')), CONSTRAINT chk_qipai_notification_route_status CHECK (status IN ('ACTIVE', 'DISABLED')), CONSTRAINT chk_qipai_notification_route_quiet CHECK ((quiet_start IS NULL AND quiet_end IS NULL) OR (quiet_start IS NOT NULL AND quiet_end IS NOT NULL)), CONSTRAINT chk_qipai_notification_route_version CHECK (version >= 1) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_notification_subscriptions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, channel VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, template_code VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, status VARCHAR(16) NOT NULL, authorized_at DATETIME(3) NOT NULL, revoked_at DATETIME(3) NULL, updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_notification_subscription_user FOREIGN KEY (tenant_id, user_id) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_notification_subscription (tenant_id, user_id, channel, template_code), CONSTRAINT chk_qipai_notification_subscription_channel CHECK (channel IN ('WECHAT_SUBSCRIBE')), CONSTRAINT chk_qipai_notification_subscription_status CHECK (status IN ('AUTHORIZED', 'REVOKED')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_notification_deliveries ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NULL, outbox_event_id BIGINT UNSIGNED NOT NULL, route_id BIGINT UNSIGNED NOT NULL, template_id BIGINT UNSIGNED NOT NULL, event_type VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, channel VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, recipient_type VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, recipient_id VARCHAR(191) NOT NULL, recipient_masked VARCHAR(191) NOT NULL, title_snapshot VARCHAR(256) NOT NULL, body_snapshot VARCHAR(2000) NOT NULL, payload_snapshot JSON NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', attempts INT UNSIGNED NOT NULL DEFAULT 0, max_attempts INT UNSIGNED NOT NULL DEFAULT 8, available_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), sent_at DATETIME(3) NULL, read_at DATETIME(3) NULL, last_error_code VARCHAR(64) NOT NULL DEFAULT '', last_error_message VARCHAR(512) NOT NULL DEFAULT '', provider_message_id VARCHAR(191) NOT NULL DEFAULT '', manual_retry_count INT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_notification_delivery_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_notification_delivery_store FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id), CONSTRAINT fk_qipai_notification_delivery_outbox FOREIGN KEY (outbox_event_id) REFERENCES qipai_outbox_events(id), CONSTRAINT fk_qipai_notification_delivery_route FOREIGN KEY (tenant_id, route_id) REFERENCES qipai_notification_routes(tenant_id, id), CONSTRAINT fk_qipai_notification_delivery_template FOREIGN KEY (tenant_id, template_id) REFERENCES qipai_notification_templates(tenant_id, id), UNIQUE KEY uq_qipai_notification_delivery_scope_id (tenant_id, id), UNIQUE KEY uq_qipai_notification_delivery_idempotency (tenant_id, outbox_event_id, channel, recipient_type, recipient_id, template_id), KEY idx_qipai_notification_delivery_dispatch (status, available_at, id), KEY idx_qipai_notification_delivery_management (tenant_id, store_id, status, created_at, id), KEY idx_qipai_notification_delivery_inbox (tenant_id, recipient_type, recipient_id, channel, read_at, created_at), CONSTRAINT chk_qipai_notification_delivery_status CHECK (status IN ('PENDING', 'PROCESSING', 'SENT', 'RETRY', 'FAILED', 'SUPPRESSED')), CONSTRAINT chk_qipai_notification_delivery_attempts CHECK (attempts <= max_attempts AND max_attempts BETWEEN 1 AND 20) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_notification_attempts ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, delivery_id BIGINT UNSIGNED NOT NULL, attempt_no INT UNSIGNED NOT NULL, trigger_type VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, status VARCHAR(16) NOT NULL, error_code VARCHAR(64) NOT NULL DEFAULT '', error_message VARCHAR(512) NOT NULL DEFAULT '', provider_message_id VARCHAR(191) NOT NULL DEFAULT '', operator_id BIGINT UNSIGNED NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_notification_attempt_delivery FOREIGN KEY (tenant_id, delivery_id) REFERENCES qipai_notification_deliveries(tenant_id, id), CONSTRAINT fk_qipai_notification_attempt_operator FOREIGN KEY (tenant_id, operator_id) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_notification_attempt (tenant_id, delivery_id, attempt_no), CONSTRAINT chk_qipai_notification_attempt_trigger CHECK (trigger_type IN ('AUTO', 'MANUAL')), CONSTRAINT chk_qipai_notification_attempt_status CHECK (status IN ('SENT', 'FAILED', 'SUPPRESSED')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; DROP TRIGGER IF EXISTS qipai_notification_attempts_no_update; CREATE TRIGGER qipai_notification_attempts_no_update BEFORE UPDATE ON qipai_notification_attempts FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'NOTIFICATION_ATTEMPT_IMMUTABLE'; DROP TRIGGER IF EXISTS qipai_notification_attempts_no_delete; CREATE TRIGGER qipai_notification_attempts_no_delete BEFORE DELETE ON qipai_notification_attempts FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'NOTIFICATION_ATTEMPT_IMMUTABLE'; INSERT IGNORE INTO qipai_permissions (code, name, category) VALUES ('notification.read', '查看通知中心', 'notification'), ('notification.manage', '管理通知与补发', 'notification'); INSERT IGNORE INTO qipai_role_permissions (tenant_id, role_id, permission_id) SELECT r.tenant_id, r.id, p.id FROM qipai_roles r INNER JOIN qipai_permissions p ON p.code IN ('notification.read', 'notification.manage') WHERE r.code IN ('STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN') AND r.status = 'ACTIVE' AND r.deleted_at IS NULL; INSERT IGNORE INTO qipai_schema_migrations (version, name) VALUES ('2026081110', 'm10a_notification_center');