From 2e316a710466b08015530030929200db498809b8 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Fri, 17 Jul 2026 13:55:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E6=97=A7=E9=80=9A?= =?UTF-8?q?=E9=81=93=E6=8A=A5=E5=BA=9F=E5=82=AC=E5=8A=9E=E6=8A=95=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 57 +++++++++++++++++++ .../legacyReminderRecoveryMigration.test.ts | 39 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 backend/prisma/migrations/20260717_recover_legacy_disposal_reminders/migration.sql create mode 100644 backend/src/utils/__tests__/legacyReminderRecoveryMigration.test.ts diff --git a/backend/prisma/migrations/20260717_recover_legacy_disposal_reminders/migration.sql b/backend/prisma/migrations/20260717_recover_legacy_disposal_reminders/migration.sql new file mode 100644 index 0000000..f54ca52 --- /dev/null +++ b/backend/prisma/migrations/20260717_recover_legacy_disposal_reminders/migration.sql @@ -0,0 +1,57 @@ +-- 统一通知中心切换前,旧 work_notice 催办任务可能在零次投递时就把报废单标记为已催办。 +-- 保留旧任务用于审计,但仅对“仍待审批、从未尝试/发送、且尚无新通道任务”的记录开放一次重新催办。 +START TRANSACTION; + +UPDATE `disposal_record` AS `disposal` +SET `disposal`.`reminded_at` = NULL +WHERE `disposal`.`status` = 'pending' + AND `disposal`.`reminded_at` IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM `dingtalk_delivery` AS `legacy_delivery` + WHERE `legacy_delivery`.`business_type` = 'disposal' + AND `legacy_delivery`.`business_id` = `disposal`.`id` + AND `legacy_delivery`.`event_type` = 'disposal_overdue_reminder' + AND `legacy_delivery`.`provider_type` = 'work_notice' + AND `legacy_delivery`.`status` IN ('pending', 'retrying', 'sending') + AND `legacy_delivery`.`attempt_count` = 0 + AND `legacy_delivery`.`dingtalk_task_id` IS NULL + AND `legacy_delivery`.`sent_at` IS NULL + AND `legacy_delivery`.`read_at` IS NULL + ) + AND NOT EXISTS ( + SELECT 1 + FROM `dingtalk_delivery` AS `current_delivery` + WHERE `current_delivery`.`business_type` = 'disposal' + AND `current_delivery`.`business_id` = `disposal`.`id` + AND `current_delivery`.`event_type` = 'disposal_overdue_reminder' + AND `current_delivery`.`provider_type` = 'notification_center' + ); + +UPDATE `dingtalk_delivery` AS `legacy_delivery` +JOIN `disposal_record` AS `disposal` + ON `disposal`.`id` = `legacy_delivery`.`business_id` +LEFT JOIN `dingtalk_delivery` AS `current_delivery` + ON `current_delivery`.`business_type` = 'disposal' + AND `current_delivery`.`business_id` = `legacy_delivery`.`business_id` + AND `current_delivery`.`event_type` = 'disposal_overdue_reminder' + AND `current_delivery`.`provider_type` = 'notification_center' +SET `legacy_delivery`.`status` = 'failed', + `legacy_delivery`.`next_attempt_at` = NOW(3), + `legacy_delivery`.`lease_owner` = NULL, + `legacy_delivery`.`lease_expires_at` = NULL, + `legacy_delivery`.`last_error_code` = 'LEGACY_CHANNEL_RETIRED', + `legacy_delivery`.`last_error_message` = '历史工作通知通道未实际投递,已开放由统一通知中心重新生成', + `legacy_delivery`.`failure_notified_at` = NOW(3) +WHERE `legacy_delivery`.`business_type` = 'disposal' + AND `legacy_delivery`.`event_type` = 'disposal_overdue_reminder' + AND `legacy_delivery`.`provider_type` = 'work_notice' + AND `legacy_delivery`.`status` IN ('pending', 'retrying', 'sending') + AND `legacy_delivery`.`attempt_count` = 0 + AND `legacy_delivery`.`dingtalk_task_id` IS NULL + AND `legacy_delivery`.`sent_at` IS NULL + AND `legacy_delivery`.`read_at` IS NULL + AND `disposal`.`status` = 'pending' + AND `current_delivery`.`id` IS NULL; + +COMMIT; diff --git a/backend/src/utils/__tests__/legacyReminderRecoveryMigration.test.ts b/backend/src/utils/__tests__/legacyReminderRecoveryMigration.test.ts new file mode 100644 index 0000000..c3d33ef --- /dev/null +++ b/backend/src/utils/__tests__/legacyReminderRecoveryMigration.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import test from 'node:test' + +const migrationPath = path.resolve( + __dirname, + '../../../prisma/migrations/20260717_recover_legacy_disposal_reminders/migration.sql' +) + +test('旧通道零次投递的报废催办可重新生成统一通知中心任务', () => { + const sql = readFileSync(migrationPath, 'utf8') + + assert.match(sql, /START TRANSACTION;/i) + assert.match(sql, /UPDATE `disposal_record` AS `disposal`/i) + assert.match(sql, /SET `disposal`\.`reminded_at` = NULL/i) + assert.match(sql, /`disposal`\.`status` = 'pending'/i) + assert.match(sql, /`legacy_delivery`\.`provider_type` = 'work_notice'/i) + assert.match(sql, /`legacy_delivery`\.`attempt_count` = 0/i) + assert.match(sql, /`legacy_delivery`\.`sent_at` IS NULL/i) + assert.match(sql, /`legacy_delivery`\.`read_at` IS NULL/i) + assert.match(sql, /`current_delivery`\.`provider_type` = 'notification_center'/i) + assert.match(sql, /SET `legacy_delivery`\.`status` = 'failed'/i) + assert.match(sql, /LEGACY_CHANNEL_RETIRED/i) + assert.match(sql, /`legacy_delivery`\.`failure_notified_at` = NOW\(3\)/i) + assert.match(sql, /LEFT JOIN `dingtalk_delivery` AS `current_delivery`/i) + assert.match(sql, /`current_delivery`\.`id` IS NULL/i) + assert.match(sql, /COMMIT;/i) + assert.doesNotMatch(sql, /DELETE\s+FROM\s+`?dingtalk_delivery`?/i) +}) + +test('已发送、已读或已有统一通知中心任务的催办不会被迁移重放', () => { + const sql = readFileSync(migrationPath, 'utf8') + + assert.match(sql, /`legacy_delivery`\.`dingtalk_task_id` IS NULL/i) + assert.match(sql, /NOT EXISTS \([\s\S]*`current_delivery`\.`event_type` = 'disposal_overdue_reminder'[\s\S]*`current_delivery`\.`provider_type` = 'notification_center'/i) + assert.doesNotMatch(sql, /`sent_at`\s*=\s*NULL/i) + assert.doesNotMatch(sql, /`read_at`\s*=\s*NULL/i) +})