feat: add carrier-aware signature retirement alerts
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
ALTER TABLE "SmsChannel"
|
||||
ADD COLUMN "carriers" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
UPDATE "SmsChannel"
|
||||
SET "carriers" = CASE
|
||||
WHEN "carrier" = 'mobile' THEN ARRAY['mobile']::TEXT[]
|
||||
WHEN "carrier" = 'unicom' THEN ARRAY['unicom']::TEXT[]
|
||||
WHEN "carrier" = 'telecom' THEN ARRAY['telecom']::TEXT[]
|
||||
WHEN "carrier" = 'all' THEN ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
|
||||
-- 旧页面和旧发送链对空/未知carrier一直按移动处理,迁移保持原业务语义且保证至少一项。
|
||||
ELSE ARRAY['mobile']::TEXT[]
|
||||
END;
|
||||
|
||||
ALTER TABLE "SmsChannel"
|
||||
ADD CONSTRAINT "SmsChannel_carriers_supported_check"
|
||||
CHECK (
|
||||
cardinality("carriers") BETWEEN 1 AND 3
|
||||
AND "carriers" <@ ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
|
||||
);
|
||||
|
||||
ALTER TABLE "ChannelSignatureReportTask"
|
||||
ADD COLUMN "carrier" TEXT,
|
||||
ADD COLUMN "approvedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "approvalScope" TEXT NOT NULL DEFAULT 'legacy_channel';
|
||||
|
||||
-- The current approved timestamp is reconstructed from the latest transition
|
||||
-- into approved. updatedAt is deliberately not used because unrelated edits
|
||||
-- can change it and would incorrectly restart the grace period.
|
||||
UPDATE "ChannelSignatureReportTask" task
|
||||
SET "approvedAt" = approved_record."approvedAt"
|
||||
FROM (
|
||||
SELECT "taskId", MAX("createdAt") AS "approvedAt"
|
||||
FROM "ChannelSignatureReportRecord"
|
||||
WHERE "statusAfter" = 'approved'
|
||||
GROUP BY "taskId"
|
||||
) approved_record
|
||||
WHERE task.id = approved_record."taskId"
|
||||
AND task.status = 'approved';
|
||||
|
||||
CREATE INDEX "ChannelSignatureReportTask_signatureId_channelId_carrier_idx"
|
||||
ON "ChannelSignatureReportTask"("signatureId", "channelId", "carrier");
|
||||
|
||||
-- 旧索引把运营商排除在唯一维度外,会阻止同一签名/通道建立多运营商事实。
|
||||
-- 拆成三类条件索引,在升级维度的同时继续保护历史任务和引流任务不重复。
|
||||
DROP INDEX IF EXISTS "ChannelSignatureReportTask_target_key";
|
||||
|
||||
CREATE UNIQUE INDEX "ChannelSignatureReportTask_signature_channel_carrier_key"
|
||||
ON "ChannelSignatureReportTask"("signatureId", "channelId", "carrier")
|
||||
WHERE "reportType" = 'signature' AND "carrier" IS NOT NULL AND "drainageItemId" IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "ChannelSignatureReportTask_legacy_signature_channel_key"
|
||||
ON "ChannelSignatureReportTask"("signatureId", "channelId")
|
||||
WHERE "reportType" = 'signature' AND "carrier" IS NULL AND "drainageItemId" IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "ChannelSignatureReportTask_drainage_target_key"
|
||||
ON "ChannelSignatureReportTask"("signatureId", "drainageItemId", "channelId")
|
||||
WHERE "reportType" = 'drainage' AND "drainageItemId" IS NOT NULL;
|
||||
|
||||
CREATE TABLE "SignatureRetirementRule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ruleType" TEXT NOT NULL,
|
||||
"targetId" TEXT,
|
||||
"targetKey" TEXT NOT NULL DEFAULT '',
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"mobileWindowDays" INTEGER NOT NULL DEFAULT 30,
|
||||
"mobileThreshold" INTEGER NOT NULL DEFAULT 1,
|
||||
"unicomWindowDays" INTEGER NOT NULL DEFAULT 30,
|
||||
"unicomThreshold" INTEGER NOT NULL DEFAULT 1,
|
||||
"telecomWindowDays" INTEGER NOT NULL DEFAULT 30,
|
||||
"telecomThreshold" INTEGER NOT NULL DEFAULT 1,
|
||||
"messageTemplate" TEXT,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SignatureRetirementRule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "SignatureRetirementRule_ruleType_targetKey_key" ON "SignatureRetirementRule"("ruleType", "targetKey");
|
||||
CREATE INDEX "SignatureRetirementRule_ruleType_enabled_idx" ON "SignatureRetirementRule"("ruleType", "enabled");
|
||||
|
||||
CREATE TABLE "SignatureRetirementWebhook" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"platform" TEXT NOT NULL,
|
||||
"urlEncrypted" TEXT NOT NULL,
|
||||
"urlMasked" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SignatureRetirementWebhook_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "SignatureRetirementWebhook_status_createdAt_idx" ON "SignatureRetirementWebhook"("status", "createdAt");
|
||||
|
||||
CREATE TABLE "SignatureRetirementCycle" (
|
||||
"id" TEXT NOT NULL,
|
||||
"dimensionType" TEXT NOT NULL,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"channelKey" TEXT NOT NULL DEFAULT '',
|
||||
"carrier" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'open',
|
||||
"startedOn" DATE NOT NULL,
|
||||
"lastDetectedOn" DATE NOT NULL,
|
||||
"resolvedOn" DATE,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SignatureRetirementCycle_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "SignatureRetirementCycle_dimension_status_idx" ON "SignatureRetirementCycle"("dimensionType", "signatureId", "channelKey", "carrier", "status");
|
||||
CREATE INDEX "SignatureRetirementCycle_status_lastDetectedOn_idx" ON "SignatureRetirementCycle"("status", "lastDetectedOn");
|
||||
-- 同一监控维度只能存在一个开放周期,数据库约束用于兜住并发检测实例。
|
||||
CREATE UNIQUE INDEX "SignatureRetirementCycle_open_dimension_key"
|
||||
ON "SignatureRetirementCycle"("dimensionType", "signatureId", "channelKey", "carrier")
|
||||
WHERE "status" = 'open';
|
||||
|
||||
CREATE TABLE "SignatureRetirementDetection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"detectionDate" DATE NOT NULL,
|
||||
"dimensionType" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"channelKey" TEXT NOT NULL DEFAULT '',
|
||||
"carrier" TEXT NOT NULL,
|
||||
"windowDays" INTEGER NOT NULL,
|
||||
"threshold" INTEGER NOT NULL,
|
||||
"submittedAttempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"acceptedBusinessCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"deliveredBusinessCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"approvedAt" TIMESTAMP(3) NOT NULL,
|
||||
"ruleId" TEXT,
|
||||
"ruleVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"status" TEXT NOT NULL,
|
||||
"cycleId" TEXT,
|
||||
"suppressed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"notificationTitle" TEXT,
|
||||
"notificationContent" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SignatureRetirementDetection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "SignatureRetirementDetection_dimension_key" ON "SignatureRetirementDetection"("detectionDate", "dimensionType", "signatureId", "channelKey", "carrier");
|
||||
CREATE INDEX "SignatureRetirementDetection_date_type_status_idx" ON "SignatureRetirementDetection"("detectionDate", "dimensionType", "status");
|
||||
CREATE INDEX "SignatureRetirementDetection_signature_carrier_date_idx" ON "SignatureRetirementDetection"("signatureId", "carrier", "detectionDate");
|
||||
CREATE INDEX "SignatureRetirementDetection_channel_carrier_date_idx" ON "SignatureRetirementDetection"("channelId", "carrier", "detectionDate");
|
||||
|
||||
CREATE TABLE "SignatureRetirementMessage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"detectionId" TEXT NOT NULL,
|
||||
"cycleId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"isRead" BOOLEAN NOT NULL DEFAULT false,
|
||||
"suppressed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"readAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SignatureRetirementMessage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "SignatureRetirementMessage_detectionId_key" ON "SignatureRetirementMessage"("detectionId");
|
||||
CREATE INDEX "SignatureRetirementMessage_created_read_suppressed_idx" ON "SignatureRetirementMessage"("createdAt", "isRead", "suppressed");
|
||||
CREATE INDEX "SignatureRetirementMessage_tenant_createdAt_idx" ON "SignatureRetirementMessage"("tenantId", "createdAt");
|
||||
|
||||
CREATE TABLE "SignatureRetirementSuppression" (
|
||||
"id" TEXT NOT NULL,
|
||||
"dimensionType" TEXT NOT NULL,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"channelKey" TEXT NOT NULL DEFAULT '',
|
||||
"carrier" TEXT NOT NULL,
|
||||
"mode" TEXT NOT NULL,
|
||||
"muteUntil" DATE,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"reason" TEXT,
|
||||
"operatorId" TEXT,
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"cancelledById" TEXT,
|
||||
"cancelReason" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SignatureRetirementSuppression_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "SignatureRetirementSuppression_dimension_key" ON "SignatureRetirementSuppression"("dimensionType", "signatureId", "channelKey", "carrier");
|
||||
CREATE INDEX "SignatureRetirementSuppression_active_muteUntil_idx" ON "SignatureRetirementSuppression"("active", "muteUntil");
|
||||
|
||||
CREATE TABLE "SignatureRetirementWebhookDelivery" (
|
||||
"id" TEXT NOT NULL,
|
||||
"detectionDate" DATE NOT NULL,
|
||||
"webhookId" TEXT NOT NULL,
|
||||
"groupKey" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"attemptCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextRetryAt" TIMESTAMP(3),
|
||||
"lastHttpStatus" INTEGER,
|
||||
"lastError" TEXT,
|
||||
"deliveredAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SignatureRetirementWebhookDelivery_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "SignatureRetirementWebhookDelivery_key" ON "SignatureRetirementWebhookDelivery"("webhookId", "detectionDate", "groupKey");
|
||||
CREATE INDEX "SignatureRetirementWebhookDelivery_status_retry_idx" ON "SignatureRetirementWebhookDelivery"("status", "nextRetryAt");
|
||||
@@ -806,6 +806,7 @@ model SmsChannel {
|
||||
code String @unique
|
||||
name String
|
||||
carrier String?
|
||||
carriers String[] @default([])
|
||||
sendRegion String @default("全国")
|
||||
protocol String @default("CMPP")
|
||||
gatewayHost String
|
||||
@@ -1059,6 +1060,9 @@ model ChannelSignatureReportTask {
|
||||
tenantId String
|
||||
signatureId String
|
||||
channelId String
|
||||
carrier String?
|
||||
approvedAt DateTime?
|
||||
approvalScope String @default("legacy_channel")
|
||||
reportType String @default("signature")
|
||||
drainageItemId String?
|
||||
status String @default("pending")
|
||||
@@ -1078,10 +1082,152 @@ model ChannelSignatureReportTask {
|
||||
@@index([tenantId, status])
|
||||
@@index([status, createdAt])
|
||||
@@index([signatureId, channelId])
|
||||
@@index([signatureId, channelId, carrier])
|
||||
@@index([signatureId, drainageItemId, channelId])
|
||||
@@index([reportType, status])
|
||||
}
|
||||
|
||||
model SignatureRetirementRule {
|
||||
id String @id @default(cuid())
|
||||
ruleType String
|
||||
targetId String?
|
||||
targetKey String @default("")
|
||||
enabled Boolean @default(true)
|
||||
mobileWindowDays Int @default(30)
|
||||
mobileThreshold Int @default(1)
|
||||
unicomWindowDays Int @default(30)
|
||||
unicomThreshold Int @default(1)
|
||||
telecomWindowDays Int @default(30)
|
||||
telecomThreshold Int @default(1)
|
||||
messageTemplate String?
|
||||
version Int @default(1)
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([ruleType, targetKey])
|
||||
@@index([ruleType, enabled])
|
||||
}
|
||||
|
||||
model SignatureRetirementWebhook {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
platform String
|
||||
urlEncrypted String
|
||||
urlMasked String
|
||||
status String @default("active")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, createdAt])
|
||||
}
|
||||
|
||||
model SignatureRetirementCycle {
|
||||
id String @id @default(cuid())
|
||||
dimensionType String
|
||||
signatureId String
|
||||
channelId String?
|
||||
channelKey String @default("")
|
||||
carrier String
|
||||
status String @default("open")
|
||||
startedOn DateTime @db.Date
|
||||
lastDetectedOn DateTime @db.Date
|
||||
resolvedOn DateTime? @db.Date
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([dimensionType, signatureId, channelKey, carrier, status])
|
||||
@@index([status, lastDetectedOn])
|
||||
}
|
||||
|
||||
model SignatureRetirementDetection {
|
||||
id String @id @default(cuid())
|
||||
detectionDate DateTime @db.Date
|
||||
dimensionType String
|
||||
tenantId String
|
||||
applicationId String?
|
||||
signatureId String
|
||||
channelId String?
|
||||
channelKey String @default("")
|
||||
carrier String
|
||||
windowDays Int
|
||||
threshold Int
|
||||
submittedAttempts Int @default(0)
|
||||
acceptedBusinessCount Int @default(0)
|
||||
deliveredBusinessCount Int @default(0)
|
||||
approvedAt DateTime
|
||||
ruleId String?
|
||||
ruleVersion Int @default(1)
|
||||
status String
|
||||
cycleId String?
|
||||
suppressed Boolean @default(false)
|
||||
notificationTitle String?
|
||||
notificationContent String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([detectionDate, dimensionType, signatureId, channelKey, carrier])
|
||||
@@index([detectionDate, dimensionType, status])
|
||||
@@index([signatureId, carrier, detectionDate])
|
||||
@@index([channelId, carrier, detectionDate])
|
||||
}
|
||||
|
||||
model SignatureRetirementMessage {
|
||||
id String @id @default(cuid())
|
||||
detectionId String @unique
|
||||
cycleId String
|
||||
tenantId String
|
||||
title String
|
||||
content String
|
||||
isRead Boolean @default(false)
|
||||
suppressed Boolean @default(false)
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([createdAt, isRead, suppressed])
|
||||
@@index([tenantId, createdAt])
|
||||
}
|
||||
|
||||
model SignatureRetirementSuppression {
|
||||
id String @id @default(cuid())
|
||||
dimensionType String
|
||||
signatureId String
|
||||
channelId String?
|
||||
channelKey String @default("")
|
||||
carrier String
|
||||
mode String
|
||||
muteUntil DateTime? @db.Date
|
||||
active Boolean @default(true)
|
||||
reason String?
|
||||
operatorId String?
|
||||
cancelledAt DateTime?
|
||||
cancelledById String?
|
||||
cancelReason String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([dimensionType, signatureId, channelKey, carrier])
|
||||
@@index([active, muteUntil])
|
||||
}
|
||||
|
||||
model SignatureRetirementWebhookDelivery {
|
||||
id String @id @default(cuid())
|
||||
detectionDate DateTime @db.Date
|
||||
webhookId String
|
||||
groupKey String
|
||||
payload Json
|
||||
status String @default("pending")
|
||||
attemptCount Int @default(0)
|
||||
nextRetryAt DateTime?
|
||||
lastHttpStatus Int?
|
||||
lastError String?
|
||||
deliveredAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([webhookId, detectionDate, groupKey])
|
||||
@@index([status, nextRetryAt])
|
||||
}
|
||||
|
||||
model ChannelSignatureReportRecord {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
|
||||
Reference in New Issue
Block a user