-- Phase 6: reviewed public content and durable user notifications. ALTER TABLE `files` MODIFY `business_type` ENUM('SERVICE_RECORD', 'PROJECT_COVER', 'ACTIVITY_COVER', 'CONTENT_COVER') NOT NULL; CREATE TABLE `content_posts` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `public_id` CHAR(36) NOT NULL, `client_request_id` CHAR(36) NOT NULL, `request_fingerprint` CHAR(64) NOT NULL, `type` ENUM('ORGANIZATION_INTRO','PUBLIC_WELFARE_NEWS','SERVICE_STORY','NOTICE','FAQ') NOT NULL, `title` VARCHAR(200) NOT NULL, `summary` VARCHAR(500) NOT NULL, `content` TEXT NOT NULL, `cover_file_id` BIGINT UNSIGNED NULL, `status` ENUM('DRAFT','PENDING_REVIEW','PUBLISHED','ARCHIVED') NOT NULL DEFAULT 'DRAFT', `revision` INTEGER UNSIGNED NOT NULL DEFAULT 1, `privacy_reviewed_at` DATETIME(3) NULL, `reviewed_by` BIGINT UNSIGNED NULL, `reviewed_at` DATETIME(3) NULL, `published_at` DATETIME(3) NULL, `created_by` BIGINT UNSIGNED NOT NULL, `updated_by` BIGINT UNSIGNED NOT NULL, `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` DATETIME(3) NOT NULL, UNIQUE INDEX `content_posts_public_id_key` (`public_id`), UNIQUE INDEX `content_posts_cover_file_id_key` (`cover_file_id`), UNIQUE INDEX `content_posts_creator_request_key` (`created_by`, `client_request_id`), INDEX `content_posts_status_type_published_idx` (`status`, `type`, `published_at`), INDEX `content_posts_creator_status_updated_idx` (`created_by`, `status`, `updated_at`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `content_post_status_history` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `content_post_id` BIGINT UNSIGNED NOT NULL, `request_id` CHAR(36) NOT NULL, `request_fingerprint` CHAR(64) NOT NULL, `from_status` ENUM('DRAFT','PENDING_REVIEW','PUBLISHED','ARCHIVED') NULL, `to_status` ENUM('DRAFT','PENDING_REVIEW','PUBLISHED','ARCHIVED') NOT NULL, `operator_user_id` BIGINT UNSIGNED NOT NULL, `reason_code` VARCHAR(100) NOT NULL, `note_encrypted` TEXT NULL, `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), UNIQUE INDEX `content_post_status_history_request_id_key` (`request_id`), INDEX `content_status_history_post_created_idx` (`content_post_id`, `created_at`), INDEX `content_status_history_operator_created_idx` (`operator_user_id`, `created_at`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `notifications` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `public_id` CHAR(36) NOT NULL, `user_id` BIGINT UNSIGNED NOT NULL, `event_key` VARCHAR(160) NOT NULL, `type` ENUM('APPLICATION_SUBMITTED','APPLICATION_ACCEPTED','SERVICE_ARRANGED','SERVICE_COMPLETED','FOLLOW_UP_PENDING','ACTIVITY_REGISTRATION_RESULT') NOT NULL, `title` VARCHAR(120) NOT NULL, `body` VARCHAR(500) NOT NULL, `resource_type` VARCHAR(80) NULL, `resource_public_id` CHAR(36) NULL, `read_at` DATETIME(3) NULL, `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), UNIQUE INDEX `notifications_public_id_key` (`public_id`), UNIQUE INDEX `notifications_user_event_key` (`user_id`, `event_key`), INDEX `notifications_user_read_created_idx` (`user_id`, `read_at`, `created_at`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `notification_preferences` ( `user_id` BIGINT UNSIGNED NOT NULL, `type` ENUM('APPLICATION_SUBMITTED','APPLICATION_ACCEPTED','SERVICE_ARRANGED','SERVICE_COMPLETED','FOLLOW_UP_PENDING','ACTIVITY_REGISTRATION_RESULT') NOT NULL, `in_app_enabled` BOOLEAN NOT NULL DEFAULT true, `wechat_enabled` BOOLEAN NOT NULL DEFAULT false, `authorized_at` DATETIME(3) NULL, `updated_at` DATETIME(3) NOT NULL, INDEX `notification_preferences_type_wechat_idx` (`type`, `wechat_enabled`), PRIMARY KEY (`user_id`, `type`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `notification_deliveries` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `public_id` CHAR(36) NOT NULL, `notification_id` BIGINT UNSIGNED NOT NULL, `channel` ENUM('WECHAT_SUBSCRIPTION') NOT NULL, `status` ENUM('PENDING','PROCESSING','SENT','FAILED','DEAD') NOT NULL DEFAULT 'PENDING', `attempt_count` INTEGER UNSIGNED NOT NULL DEFAULT 0, `next_attempt_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `last_attempt_at` DATETIME(3) NULL, `provider_message_id` VARCHAR(160) NULL, `error_code` VARCHAR(100) NULL, `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` DATETIME(3) NOT NULL, UNIQUE INDEX `notification_deliveries_public_id_key` (`public_id`), UNIQUE INDEX `notification_deliveries_notification_channel_key` (`notification_id`, `channel`), INDEX `notification_deliveries_status_next_idx` (`status`, `next_attempt_at`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `content_posts` ADD CONSTRAINT `content_posts_cover_file_id_fkey` FOREIGN KEY (`cover_file_id`) REFERENCES `files` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `content_posts` ADD CONSTRAINT `content_posts_created_by_fkey` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `content_posts` ADD CONSTRAINT `content_posts_updated_by_fkey` FOREIGN KEY (`updated_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `content_posts` ADD CONSTRAINT `content_posts_reviewed_by_fkey` FOREIGN KEY (`reviewed_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `content_post_status_history` ADD CONSTRAINT `content_status_history_post_fkey` FOREIGN KEY (`content_post_id`) REFERENCES `content_posts` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `content_post_status_history` ADD CONSTRAINT `content_status_history_operator_fkey` FOREIGN KEY (`operator_user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `notifications` ADD CONSTRAINT `notifications_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `notification_preferences` ADD CONSTRAINT `notification_preferences_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `notification_deliveries` ADD CONSTRAINT `notification_deliveries_notification_id_fkey` FOREIGN KEY (`notification_id`) REFERENCES `notifications` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; INSERT INTO `permissions` (`code`, `name`, `description`) VALUES ('content:read', '查看内容', '查看内容草稿、审核状态及历史'), ('content:manage', '管理内容', '创建和维护内容草稿'), ('content:review', '审核内容', '审核、发布、退回和归档内容'), ('notifications:manage', '管理通知', '查看通知投递状态并安全重试'); INSERT INTO `role_permissions` (`role_id`, `permission_id`) SELECT r.id, p.id FROM `roles` r CROSS JOIN `permissions` p WHERE r.code IN ('ADMIN','SUPER_ADMIN') AND p.code IN ('content:read','content:manage','content:review','notifications:manage'); INSERT INTO `role_permissions` (`role_id`, `permission_id`) SELECT r.id, p.id FROM `roles` r CROSS JOIN `permissions` p WHERE r.code IN ('STAFF','PROJECT_MANAGER') AND p.code = 'content:read';