feat: add phone frequency controls and modularize codebase

This commit is contained in:
hectorzhao
2026-07-31 22:25:23 +08:00
parent 0af671b4ed
commit ca4f591a13
216 changed files with 41579 additions and 23694 deletions
@@ -0,0 +1,131 @@
CREATE TABLE "PhoneFrequencyHit" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"ruleId" TEXT,
"ruleCode" TEXT NOT NULL,
"ruleName" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"thresholdValue" INTEGER NOT NULL,
"actualValue" INTEGER NOT NULL,
"windowStartedAt" TIMESTAMP(3) NOT NULL,
"windowEndsAt" TIMESTAMP(3) NOT NULL,
"generation" INTEGER NOT NULL DEFAULT 0,
"action" TEXT NOT NULL DEFAULT 'block',
"sourceType" TEXT,
"releasedAt" TIMESTAMP(3),
"releasedById" TEXT,
"releaseReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PhoneFrequencyHit_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "PhoneFrequencyState" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"ruleId" TEXT,
"ruleCode" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"windowStartedAt" TIMESTAMP(3) NOT NULL,
"windowEndsAt" TIMESTAMP(3) NOT NULL,
"count" INTEGER NOT NULL DEFAULT 0,
"generation" INTEGER NOT NULL DEFAULT 0,
"activeHitId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PhoneFrequencyState_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PhoneFrequencyHit_applicationId_ruleCode_phoneNumber_windowStartedAt_generation_key"
ON "PhoneFrequencyHit"("applicationId", "ruleCode", "phoneNumber", "windowStartedAt", "generation");
CREATE INDEX "PhoneFrequencyHit_tenantId_applicationId_createdAt_idx"
ON "PhoneFrequencyHit"("tenantId", "applicationId", "createdAt");
CREATE INDEX "PhoneFrequencyHit_applicationId_phoneNumber_createdAt_idx"
ON "PhoneFrequencyHit"("applicationId", "phoneNumber", "createdAt");
CREATE INDEX "PhoneFrequencyHit_windowEndsAt_releasedAt_idx"
ON "PhoneFrequencyHit"("windowEndsAt", "releasedAt");
CREATE INDEX "PhoneFrequencyHit_ruleId_idx" ON "PhoneFrequencyHit"("ruleId");
CREATE INDEX "PhoneFrequencyHit_releasedById_idx" ON "PhoneFrequencyHit"("releasedById");
CREATE UNIQUE INDEX "PhoneFrequencyState_activeHitId_key" ON "PhoneFrequencyState"("activeHitId");
CREATE UNIQUE INDEX "PhoneFrequencyState_applicationId_ruleCode_phoneNumber_key"
ON "PhoneFrequencyState"("applicationId", "ruleCode", "phoneNumber");
CREATE INDEX "PhoneFrequencyState_tenantId_applicationId_windowEndsAt_idx"
ON "PhoneFrequencyState"("tenantId", "applicationId", "windowEndsAt");
CREATE INDEX "PhoneFrequencyState_ruleId_idx" ON "PhoneFrequencyState"("ruleId");
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_ruleId_fkey"
FOREIGN KEY ("ruleId") REFERENCES "RiskRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_releasedById_fkey"
FOREIGN KEY ("releasedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_ruleId_fkey"
FOREIGN KEY ("ruleId") REFERENCES "RiskRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_activeHitId_fkey"
FOREIGN KEY ("activeHitId") REFERENCES "PhoneFrequencyHit"("id") ON DELETE SET NULL ON UPDATE CASCADE;
INSERT INTO "RiskRule" (
"id", "tenantId", "applicationId", "code", "name", "description", "metric",
"thresholdValue", "action", "status", "priority", "config", "createdAt", "updatedAt"
)
SELECT
'default-phone-frequency-24h', NULL, NULL, 'PHONE_FREQUENCY_24H',
'单号码24小时发送频次', '同一企业应用下,单个号码在北京时间自然日内最多允许10条业务短信。',
'phoneFrequencyCount', 10, 'block', 'active', 40,
'{"periodSeconds":86400,"timeZone":"Asia/Shanghai","alignment":"fixed"}'::jsonb,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM "RiskRule"
WHERE "applicationId" IS NULL
AND "code" = 'PHONE_FREQUENCY_24H'
AND "status" <> 'deleted'
);
INSERT INTO "RiskRule" (
"id", "tenantId", "applicationId", "code", "name", "description", "metric",
"thresholdValue", "action", "status", "priority", "config", "createdAt", "updatedAt"
)
SELECT
'default-phone-frequency-5m', NULL, NULL, 'PHONE_FREQUENCY_5M',
'单号码5分钟发送频次', '同一企业应用下,单个号码在固定5分钟周期内最多允许5条业务短信。',
'phoneFrequencyCount', 5, 'block', 'active', 50,
'{"periodSeconds":300,"timeZone":"Asia/Shanghai","alignment":"fixed"}'::jsonb,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM "RiskRule"
WHERE "applicationId" IS NULL
AND "code" = 'PHONE_FREQUENCY_5M'
AND "status" <> 'deleted'
);
@@ -0,0 +1,34 @@
CREATE TABLE "PhoneFrequencyWhitelist" (
"id" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"reason" TEXT NOT NULL,
"remark" TEXT,
"createdById" TEXT NOT NULL,
"updatedById" TEXT NOT NULL,
"deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PhoneFrequencyWhitelist_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PhoneFrequencyWhitelist_phoneNumber_key"
ON "PhoneFrequencyWhitelist"("phoneNumber");
CREATE INDEX "PhoneFrequencyWhitelist_status_updatedAt_idx"
ON "PhoneFrequencyWhitelist"("status", "updatedAt");
CREATE INDEX "PhoneFrequencyWhitelist_createdById_idx"
ON "PhoneFrequencyWhitelist"("createdById");
CREATE INDEX "PhoneFrequencyWhitelist_updatedById_idx"
ON "PhoneFrequencyWhitelist"("updatedById");
ALTER TABLE "PhoneFrequencyWhitelist"
ADD CONSTRAINT "PhoneFrequencyWhitelist_createdById_fkey"
FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyWhitelist"
ADD CONSTRAINT "PhoneFrequencyWhitelist_updatedById_fkey"
FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+98 -10
View File
@@ -32,6 +32,8 @@ model Tenant {
riskRules RiskRule[]
smsSendTasks SmsSendTask[]
riskHitRecords RiskHitRecord[]
phoneFrequencyStates PhoneFrequencyState[]
phoneFrequencyHits PhoneFrequencyHit[]
smsBatchTasks SmsBatchTask[]
smsMessageRecords SmsMessageRecord[]
smsApiRequests SmsApiRequest[]
@@ -88,13 +90,16 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant? @relation(fields: [tenantId], references: [id])
roles UserRole[]
operationLogs OperationLog[]
auditRecords AuditRecord[]
createdSmsSendTasks SmsSendTask[] @relation("SmsSendTaskCreator")
reviewedSmsSendTasks SmsSendTask[] @relation("SmsSendTaskReviewer")
createdSmsBatchTasks SmsBatchTask[] @relation("SmsBatchTaskCreator")
tenant Tenant? @relation(fields: [tenantId], references: [id])
roles UserRole[]
operationLogs OperationLog[]
auditRecords AuditRecord[]
createdSmsSendTasks SmsSendTask[] @relation("SmsSendTaskCreator")
reviewedSmsSendTasks SmsSendTask[] @relation("SmsSendTaskReviewer")
createdSmsBatchTasks SmsBatchTask[] @relation("SmsBatchTaskCreator")
releasedPhoneFrequencyHits PhoneFrequencyHit[] @relation("PhoneFrequencyHitReleaser")
createdPhoneFrequencyWhitelistEntries PhoneFrequencyWhitelist[] @relation("PhoneFrequencyWhitelistCreator")
updatedPhoneFrequencyWhitelistEntries PhoneFrequencyWhitelist[] @relation("PhoneFrequencyWhitelistUpdater")
}
model Role {
@@ -440,6 +445,8 @@ model SmsApplication {
dailyUsages SmsApplicationDailyUsage[]
inboundLongMessages CmppInboundLongMessage[]
riskRules RiskRule[]
phoneFrequencyStates PhoneFrequencyState[]
phoneFrequencyHits PhoneFrequencyHit[]
@@index([tenantId, status])
@@index([status, createdAt])
@@ -1268,9 +1275,11 @@ model RiskRule {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant? @relation(fields: [tenantId], references: [id])
application SmsApplication? @relation(fields: [applicationId], references: [id], onDelete: Cascade)
hits RiskHitRecord[]
tenant Tenant? @relation(fields: [tenantId], references: [id])
application SmsApplication? @relation(fields: [applicationId], references: [id], onDelete: Cascade)
hits RiskHitRecord[]
phoneFrequencyStates PhoneFrequencyState[]
phoneFrequencyHits PhoneFrequencyHit[]
@@unique([applicationId, code])
@@index([tenantId, applicationId, status, priority])
@@ -1339,6 +1348,85 @@ model RiskHitRecord {
@@index([ruleCode])
}
model PhoneFrequencyState {
id String @id @default(cuid())
tenantId String
applicationId String
ruleId String?
ruleCode String
phoneNumber String
windowStartedAt DateTime
windowEndsAt DateTime
count Int @default(0)
generation Int @default(0)
activeHitId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
application SmsApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)
rule RiskRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
activeHit PhoneFrequencyHit? @relation("ActivePhoneFrequencyHit", fields: [activeHitId], references: [id], onDelete: SetNull)
@@unique([applicationId, ruleCode, phoneNumber])
@@index([tenantId, applicationId, windowEndsAt])
@@index([ruleId])
}
model PhoneFrequencyHit {
id String @id @default(cuid())
tenantId String
applicationId String
ruleId String?
ruleCode String
ruleName String
phoneNumber String
thresholdValue Int
actualValue Int
windowStartedAt DateTime
windowEndsAt DateTime
generation Int @default(0)
action String @default("block")
sourceType String?
releasedAt DateTime?
releasedById String?
releaseReason String?
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
application SmsApplication @relation(fields: [applicationId], references: [id])
rule RiskRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
releasedBy User? @relation("PhoneFrequencyHitReleaser", fields: [releasedById], references: [id])
activeForState PhoneFrequencyState? @relation("ActivePhoneFrequencyHit")
@@unique([applicationId, ruleCode, phoneNumber, windowStartedAt, generation])
@@index([tenantId, applicationId, createdAt])
@@index([applicationId, phoneNumber, createdAt])
@@index([windowEndsAt, releasedAt])
@@index([ruleId])
@@index([releasedById])
}
model PhoneFrequencyWhitelist {
id String @id @default(cuid())
phoneNumber String @unique
status String @default("active")
reason String
remark String?
createdById String
updatedById String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation("PhoneFrequencyWhitelistCreator", fields: [createdById], references: [id])
updatedBy User @relation("PhoneFrequencyWhitelistUpdater", fields: [updatedById], references: [id])
@@index([status, updatedAt])
@@index([createdById])
@@index([updatedById])
}
model SmsBatchTask {
id String @id @default(cuid())
tenantId String