Compare commits
183
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ad8e61793 | ||
|
|
57b58f1c40 | ||
|
|
530a65de80 | ||
|
|
3357ace7e1 | ||
|
|
94e997ec4d | ||
|
|
ecc3d7a504 | ||
|
|
37ffce40b2 | ||
|
|
4b9127e1ab | ||
|
|
461a65f810 | ||
|
|
ca4f591a13 | ||
|
|
0af671b4ed | ||
|
|
c0a4317a7e | ||
|
|
500f43f673 | ||
|
|
751c03d421 | ||
|
|
fbacb74544 | ||
|
|
b8560372cc | ||
|
|
87c5b1eccc | ||
|
|
95c052e9ea | ||
|
|
d8e5319647 | ||
|
|
99c8c7c68b | ||
|
|
352a6293b4 | ||
|
|
7c1a0287a0 | ||
|
|
df70b336a0 | ||
|
|
9891ffee23 | ||
|
|
94aeacd3a2 | ||
|
|
04a497c791 | ||
|
|
e0f6eed0d4 | ||
|
|
04f78695ed | ||
|
|
0857de09d8 | ||
|
|
059b38e8fe | ||
|
|
2ce682c3fc | ||
|
|
b461532075 | ||
|
|
54617c927e | ||
|
|
5018167696 | ||
|
|
53736c96e8 | ||
|
|
c78ed1cc32 | ||
|
|
e12bdf010e | ||
|
|
91f04f5288 | ||
|
|
2ee39056eb | ||
|
|
ca12f14b00 | ||
|
|
3997349c21 | ||
|
|
0bfeb0839e | ||
|
|
4f4fa5fcb7 | ||
|
|
39d2cc5909 | ||
|
|
afd3c96070 | ||
|
|
2f781ebb8a | ||
|
|
b29576fcd1 | ||
|
|
f186aee00b | ||
|
|
e28288f691 | ||
|
|
344003e034 | ||
|
|
55019443c0 | ||
|
|
66bc230d7e | ||
|
|
a09036c67b | ||
|
|
cd085d2712 | ||
|
|
0f223f7f91 | ||
|
|
ef957f7daa | ||
|
|
f02c33cbb7 | ||
|
|
80fb5a8f53 | ||
|
|
23a1f6fa15 | ||
|
|
faa716b8d0 | ||
|
|
9d5c507007 | ||
|
|
dcb6162dcf | ||
|
|
4f07b331e5 | ||
|
|
85ff037647 | ||
|
|
7091a8bed4 | ||
|
|
cf9f4ce4cd | ||
|
|
a7a4e8d9f6 | ||
|
|
28fb8e9038 | ||
|
|
3e114ee729 | ||
|
|
27c464246b | ||
|
|
8c3336600e | ||
|
|
16311546af | ||
|
|
a758672436 | ||
|
|
9bbfb72be6 | ||
|
|
e47432bc9d | ||
|
|
00b6d95752 | ||
|
|
c1a17699db | ||
|
|
3e3b7a9d1a | ||
|
|
28dad93e3e | ||
|
|
f35691f185 | ||
|
|
1ce02ef206 | ||
|
|
135b4fd24e | ||
|
|
8c03663f24 | ||
|
|
3d37adcc9f | ||
|
|
f08a73e191 | ||
|
|
ab29e60ae1 | ||
|
|
7099147871 | ||
|
|
6421671259 | ||
|
|
dec2430b7e | ||
|
|
ff3e56070a | ||
|
|
6eaa4157e8 | ||
|
|
551b99cbcd | ||
|
|
567e4da0c9 | ||
|
|
19d47b45eb | ||
|
|
d4b2df82da | ||
|
|
344b2afe3b | ||
|
|
551edfd8ca | ||
|
|
ade06058f5 | ||
|
|
7703873346 | ||
|
|
eab059583f | ||
|
|
3f3ff8a793 | ||
|
|
87ae4a2063 | ||
|
|
053bcca990 | ||
|
|
8b6ec92f4f | ||
|
|
c8b6d0aa7c | ||
|
|
390b970032 | ||
|
|
24cb632a45 | ||
|
|
7b8424d9bc | ||
|
|
37048a4c9a | ||
|
|
2fea15ef25 | ||
|
|
76e0417ccf | ||
|
|
2ecbb920a6 | ||
|
|
6236547871 | ||
|
|
bb4992f0f9 | ||
|
|
a4d42cf702 | ||
|
|
20f89d14aa | ||
|
|
208a6c23f8 | ||
|
|
709ac97764 | ||
|
|
09b78377e6 | ||
|
|
d5e668a2e6 | ||
|
|
ff0756ff9c | ||
|
|
767f0ca9aa | ||
|
|
57a4c86a79 | ||
|
|
3ed5f51bae | ||
|
|
dc7d015219 | ||
|
|
20757203d9 | ||
|
|
64bb3fb430 | ||
|
|
268392ee9c | ||
|
|
06e6be3c39 | ||
|
|
2a65b41c4e | ||
|
|
e0579857c6 | ||
|
|
c58a010951 | ||
|
|
a4a638208e | ||
|
|
8144f08652 | ||
|
|
cc628d0214 | ||
|
|
ad3b86ba0a | ||
|
|
3777ef95e8 | ||
|
|
a08dd7f58d | ||
|
|
7ac364b407 | ||
|
|
3781e07015 | ||
|
|
07b0c89df6 | ||
|
|
fb15e1568f | ||
|
|
5a856e0b84 | ||
|
|
994dadb84e | ||
|
|
a35ba4886a | ||
|
|
ac859d5607 | ||
|
|
9fee6fa81b | ||
|
|
4c8f7dd73f | ||
|
|
72f2c010ce | ||
|
|
b5132d7f4e | ||
|
|
8cca361441 | ||
|
|
dd09d91c1e | ||
|
|
a890b03163 | ||
|
|
131f344ac4 | ||
|
|
06562e42dd | ||
|
|
e26d8324c3 | ||
|
|
9c639d54b9 | ||
|
|
321cf716f2 | ||
|
|
ab421cf8a7 | ||
|
|
f8c9b78c21 | ||
|
|
8ba4ef8a13 | ||
|
|
df3df3a771 | ||
|
|
118a4179e8 | ||
|
|
251989e318 | ||
|
|
9e03920e61 | ||
|
|
26757cb3b3 | ||
|
|
7fa91675b8 | ||
|
|
bbbbe269a8 | ||
|
|
1466a28a44 | ||
|
|
7f834b2905 | ||
|
|
21d1dd2839 | ||
|
|
7ff6d7eac3 | ||
|
|
50d75f1cf3 | ||
|
|
db27ace069 | ||
|
|
19ff6531d1 | ||
|
|
a31beeac0d | ||
|
|
91d1e38a09 | ||
|
|
924457a48e | ||
|
|
27bb2d798a | ||
|
|
56f835a00f | ||
|
|
ee926fea04 | ||
|
|
824a8b334f | ||
|
|
2675fafb28 |
@@ -0,0 +1,42 @@
|
||||
# Web
|
||||
VITE_API_BASE_URL=http://localhost:3000/api
|
||||
|
||||
# API
|
||||
API_PORT=3000
|
||||
DATABASE_URL=postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
HTTP_API_MASTER_KEY=replace-with-at-least-32-random-characters
|
||||
# Customer-facing HTTP API origin returned by the real backend and shown in copied integration parameters.
|
||||
HTTP_API_PUBLIC_ORIGIN=https://api.example.com
|
||||
API_ENABLE_SEND_WORKER=true
|
||||
API_SEND_WORKER_CONCURRENCY=50
|
||||
ADMIN_SESSION_IDLE_TIMEOUT_MS=3600000
|
||||
CLIENT_SESSION_IDLE_TIMEOUT_MS=7200000
|
||||
SESSION_LOCK_RECOVERY_MS=14400000
|
||||
SESSION_ABSOLUTE_TIMEOUT_MS=43200000
|
||||
SESSION_RECENT_AUTH_MS=1800000
|
||||
OPERATION_LOG_ARCHIVE_ENABLED=true
|
||||
OPERATION_LOG_RETENTION_DAYS=180
|
||||
OPERATION_LOG_ARCHIVE_BATCH_SIZE=1000
|
||||
OPERATION_LOG_ARCHIVE_MAX_BATCHES=20
|
||||
OPERATION_LOG_ARCHIVE_INTERVAL_MS=86400000
|
||||
SMS_RECEIPT_TIMEOUT_SCAN_ENABLED=true
|
||||
SMS_RECEIPT_TIMEOUT_HOURS=72
|
||||
SMS_RECEIPT_TIMEOUT_SCAN_INTERVAL_MS=300000
|
||||
# Local HTTP development only. Production must use HTTPS and true.
|
||||
SESSION_COOKIE_SECURE=false
|
||||
MINIO_ENDPOINT=localhost:9000
|
||||
MINIO_ACCESS_KEY=cmpp_minio
|
||||
MINIO_SECRET_KEY=cmpp_minio_password
|
||||
MINIO_BUCKET=cmpp-platform
|
||||
|
||||
# Gateway
|
||||
GATEWAY_HEALTH_ADDR=:8090
|
||||
GATEWAY_REDIS_ADDR=127.0.0.1:6379
|
||||
GATEWAY_CMPP_VERSION=3.0
|
||||
GATEWAY_CMPP_ADDR=127.0.0.1:7890
|
||||
GATEWAY_CMPP_USER=900001
|
||||
GATEWAY_CMPP_PASSWORD=888888
|
||||
CMPP_DOWNSTREAM_ACK_TIMEOUT_SECONDS=30
|
||||
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -2,12 +2,18 @@ node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.local
|
||||
.local-tools/
|
||||
.local-data/
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
logs/
|
||||
gateway/gateway.exe
|
||||
dump.rdb
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# CMPP Platform API Spike
|
||||
|
||||
阶段 0 的 `api/` 目录用于 NestJS + BullMQ 通信 Spike。当前只定义边界,不展开业务模块。
|
||||
|
||||
## 阶段 0 职责
|
||||
|
||||
- 生产 `cmpp.submit.commands` 队列消息。
|
||||
- 消费 `cmpp.submit.results`、`cmpp.receipt.events`、`cmpp.uplink.events`。
|
||||
- 使用 `traceId`、`messageId`、`channelId` 记录最小链路状态。
|
||||
- 为 500 条/秒 Spike 提供批量入队脚本或测试入口。
|
||||
|
||||
## 不在阶段 0 实现
|
||||
|
||||
- 认证、权限、租户完整模型。
|
||||
- 企业、应用、签名、模板 CRUD。
|
||||
- 风控、审核、报备、计费真实业务规则。
|
||||
- PostgreSQL/Prisma 正式 schema。
|
||||
|
||||
## 队列契约
|
||||
|
||||
以 `docs/contracts/gateway-queue-messages.schema.json` 和 `docs/contracts/examples/` 为准。
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
clearMocks: true,
|
||||
};
|
||||
Generated
+9533
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "cmpp-platform-api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "jest --runInBand",
|
||||
"test:watch": "jest --watch",
|
||||
"start": "node dist/main.js",
|
||||
"start:dev": "ts-node src/main.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate:dev": "prisma migrate dev",
|
||||
"prisma:migrate:deploy": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.1.28",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.28",
|
||||
"@nestjs/platform-express": "^11.1.28",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"@prisma/adapter-pg": "^7.9.0",
|
||||
"@prisma/client": "^7.9.0",
|
||||
"bullmq": "^5.79.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"brace-expansion": "file:vendor/brace-expansion-compat",
|
||||
"exceljs": "^4.4.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"minio": "^8.0.7",
|
||||
"pg": "^8.22.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.9.3",
|
||||
"jest": "^30.4.2",
|
||||
"prisma": "^7.9.0",
|
||||
"ts-jest": "^29.4.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "$brace-expansion",
|
||||
"exceljs": {
|
||||
"uuid": "11.1.1"
|
||||
},
|
||||
"find-my-way": "9.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'prisma/config';
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url:
|
||||
process.env.DATABASE_URL ??
|
||||
'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public',
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "SmsBatchTask" ADD COLUMN "scheduledAt" TIMESTAMP(3);
|
||||
ALTER TABLE "SmsBatchTask" ADD COLUMN "canceledAt" TIMESTAMP(3);
|
||||
CREATE INDEX "SmsBatchTask_status_scheduledAt_idx" ON "SmsBatchTask"("status", "scheduledAt");
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
ALTER TABLE "Tenant" ADD COLUMN "certificationStatus" TEXT NOT NULL DEFAULT 'approved';
|
||||
|
||||
CREATE TABLE "EnterpriseCertification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"companyName" TEXT NOT NULL,
|
||||
"licenseNo" TEXT,
|
||||
"contactName" TEXT,
|
||||
"contactPhone" TEXT,
|
||||
"materials" JSONB,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"rejectReason" TEXT,
|
||||
"reviewerId" TEXT,
|
||||
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "EnterpriseCertification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "CmppConnectionState" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'disconnected',
|
||||
"desiredConnections" INTEGER NOT NULL DEFAULT 1,
|
||||
"currentConnections" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastConnectedAt" TIMESTAMP(3),
|
||||
"lastDisconnectedAt" TIMESTAMP(3),
|
||||
"lastHeartbeatAt" TIMESTAMP(3),
|
||||
"reconnectCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastError" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "CmppConnectionState_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "EnterpriseCertification_tenantId_status_createdAt_idx" ON "EnterpriseCertification"("tenantId", "status", "createdAt");
|
||||
CREATE UNIQUE INDEX "CmppConnectionState_channelId_connectionId_key" ON "CmppConnectionState"("channelId", "connectionId");
|
||||
CREATE INDEX "CmppConnectionState_tenantId_status_idx" ON "CmppConnectionState"("tenantId", "status");
|
||||
CREATE INDEX "CmppConnectionState_channelId_status_idx" ON "CmppConnectionState"("channelId", "status");
|
||||
|
||||
ALTER TABLE "EnterpriseCertification" ADD CONSTRAINT "EnterpriseCertification_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE "User"
|
||||
ADD COLUMN "email" TEXT,
|
||||
ADD COLUMN "phone" TEXT,
|
||||
ADD COLUMN "failedLoginCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "lockedUntil" TIMESTAMP(3),
|
||||
ADD COLUMN "lastLoginAt" TIMESTAMP(3),
|
||||
ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Channel-group routing rules for first-version send-chain failover.
|
||||
ALTER TABLE "SmsChannel" ADD COLUMN "sendRegion" TEXT NOT NULL DEFAULT '全国';
|
||||
|
||||
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryEnabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryTimeLimitHours" INTEGER NOT NULL DEFAULT 72;
|
||||
|
||||
CREATE TABLE "PhoneCarrierRule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"carrier" TEXT NOT NULL,
|
||||
"pattern" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 100,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"remark" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PhoneCarrierRule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "PhoneCarrierRule_status_priority_idx" ON "PhoneCarrierRule"("status", "priority");
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "SmsChannelGroup" ADD COLUMN "carrier" TEXT NOT NULL DEFAULT 'mobile';
|
||||
|
||||
CREATE UNIQUE INDEX "SmsChannelGroupItem_groupId_province_key" ON "SmsChannelGroupItem"("groupId", "province");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "SmsApplication" ADD COLUMN "customerUnitPrice" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Make CMPP connection states application-scoped for enterprise application status.
|
||||
ALTER TABLE "CmppConnectionState" ADD COLUMN "applicationId" TEXT;
|
||||
|
||||
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
DROP INDEX IF EXISTS "CmppConnectionState_channelId_connectionId_key";
|
||||
CREATE UNIQUE INDEX "CmppConnectionState_applicationId_channelId_connectionId_key"
|
||||
ON "CmppConnectionState"("applicationId", "channelId", "connectionId");
|
||||
CREATE INDEX "CmppConnectionState_applicationId_status_idx"
|
||||
ON "CmppConnectionState"("applicationId", "status");
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryTimeLimitMinutes" INTEGER NOT NULL DEFAULT 720;
|
||||
|
||||
UPDATE "SmsChannelGroup"
|
||||
SET "retryTimeLimitMinutes" = LEAST(GREATEST("retryTimeLimitHours", 1), 72) * 60;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "SmsApplication" ADD COLUMN "queuePriority" TEXT NOT NULL DEFAULT 'normal';
|
||||
ALTER TABLE "SmsMessageRecord" ADD COLUMN "queuePriority" TEXT NOT NULL DEFAULT 'normal';
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE "SmsApplication" ADD COLUMN "cmppAccount" TEXT;
|
||||
|
||||
WITH numbered AS (
|
||||
SELECT id, row_number() OVER (ORDER BY "createdAt", id) AS rn
|
||||
FROM "SmsApplication"
|
||||
)
|
||||
UPDATE "SmsApplication" app
|
||||
SET "cmppAccount" = lpad(((100000 + numbered.rn) % 1000000)::text, 6, '0')
|
||||
FROM numbered
|
||||
WHERE app.id = numbered.id;
|
||||
|
||||
ALTER TABLE "SmsApplication" ALTER COLUMN "cmppAccount" SET NOT NULL;
|
||||
CREATE UNIQUE INDEX "SmsApplication_cmppAccount_key" ON "SmsApplication"("cmppAccount");
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
-- Add customer-side downstream delivery persistence and uplink matching fields.
|
||||
|
||||
ALTER TABLE "SmsUplinkMessage"
|
||||
ADD COLUMN "applicationId" TEXT,
|
||||
ADD COLUMN "messageRecordId" TEXT,
|
||||
ADD COLUMN "matchStatus" TEXT NOT NULL DEFAULT 'unmatched',
|
||||
ADD COLUMN "matchReason" TEXT;
|
||||
|
||||
ALTER TABLE "SmsUplinkMessage"
|
||||
ADD CONSTRAINT "SmsUplinkMessage_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsUplinkMessage"
|
||||
ADD CONSTRAINT "SmsUplinkMessage_messageRecordId_fkey"
|
||||
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "SmsUplinkMessage_applicationId_receivedAt_idx" ON "SmsUplinkMessage"("applicationId", "receivedAt");
|
||||
CREATE INDEX "SmsUplinkMessage_messageRecordId_idx" ON "SmsUplinkMessage"("messageRecordId");
|
||||
CREATE INDEX "SmsUplinkMessage_matchStatus_idx" ON "SmsUplinkMessage"("matchStatus");
|
||||
|
||||
CREATE TABLE "CmppDownstreamDelivery" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"messageRecordId" TEXT,
|
||||
"messageId" TEXT,
|
||||
"deliveryType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"payload" JSONB NOT NULL,
|
||||
"retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextRetryAt" TIMESTAMP(3),
|
||||
"deliveredAt" TIMESTAMP(3),
|
||||
"lastError" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CmppDownstreamDelivery_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD CONSTRAINT "CmppDownstreamDelivery_tenantId_fkey"
|
||||
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD CONSTRAINT "CmppDownstreamDelivery_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD CONSTRAINT "CmppDownstreamDelivery_messageRecordId_fkey"
|
||||
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "CmppDownstreamDelivery_tenantId_status_createdAt_idx" ON "CmppDownstreamDelivery"("tenantId", "status", "createdAt");
|
||||
CREATE INDEX "CmppDownstreamDelivery_applicationId_status_createdAt_idx" ON "CmppDownstreamDelivery"("applicationId", "status", "createdAt");
|
||||
CREATE INDEX "CmppDownstreamDelivery_messageId_idx" ON "CmppDownstreamDelivery"("messageId");
|
||||
CREATE INDEX "CmppDownstreamDelivery_messageRecordId_idx" ON "CmppDownstreamDelivery"("messageRecordId");
|
||||
CREATE INDEX "CmppDownstreamDelivery_deliveryType_status_idx" ON "CmppDownstreamDelivery"("deliveryType", "status");
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "SmsApplication"
|
||||
ADD COLUMN "cmppMaxConnections" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "cmppWindowSize" INTEGER NOT NULL DEFAULT 16;
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE "GatewaySubmitDeadLetter" (
|
||||
"id" TEXT NOT NULL,
|
||||
"streamMessageId" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"traceId" TEXT,
|
||||
"messageId" TEXT,
|
||||
"submitId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"failureCode" TEXT NOT NULL,
|
||||
"failureMessage" TEXT NOT NULL,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 1,
|
||||
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
|
||||
"commandPayload" JSONB,
|
||||
"rawPayload" TEXT,
|
||||
"manualRetryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastRetryStreamId" TEXT,
|
||||
"lastRetriedAt" TIMESTAMP(3),
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"resolvedStatus" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GatewaySubmitDeadLetter_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "GatewaySubmitDeadLetter_streamMessageId_key" ON "GatewaySubmitDeadLetter"("streamMessageId");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("status", "createdAt");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_tenantId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("tenantId", "status", "createdAt");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_applicationId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("applicationId", "status", "createdAt");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_channelId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("channelId", "status", "createdAt");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_messageId_idx" ON "GatewaySubmitDeadLetter"("messageId");
|
||||
CREATE INDEX "GatewaySubmitDeadLetter_submitId_idx" ON "GatewaySubmitDeadLetter"("submitId");
|
||||
|
||||
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE "GatewayDownstreamRecoveryStatus" (
|
||||
"id" TEXT NOT NULL,
|
||||
"account" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"gatewayInstanceId" TEXT,
|
||||
"state" TEXT NOT NULL,
|
||||
"lastAttemptAt" TIMESTAMP(3),
|
||||
"lastSuccessAt" TIMESTAMP(3),
|
||||
"lastFailureAt" TIMESTAMP(3),
|
||||
"nextRetryAt" TIMESTAMP(3),
|
||||
"attemptCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastError" TEXT,
|
||||
"lastSkipReason" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GatewayDownstreamRecoveryStatus_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "GatewayDownstreamRecoveryStatus_account_key" ON "GatewayDownstreamRecoveryStatus"("account");
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_tenantId_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("tenantId", "state", "updatedAt");
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_applicationId_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("applicationId", "state", "updatedAt");
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("state", "updatedAt");
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_nextRetryAt_idx" ON "GatewayDownstreamRecoveryStatus"("nextRetryAt");
|
||||
|
||||
ALTER TABLE "GatewayDownstreamRecoveryStatus"
|
||||
ADD CONSTRAINT "GatewayDownstreamRecoveryStatus_tenantId_fkey"
|
||||
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "GatewayDownstreamRecoveryStatus"
|
||||
ADD CONSTRAINT "GatewayDownstreamRecoveryStatus_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "GatewayDownstreamRecoveryStatus"
|
||||
ADD COLUMN "failureCategory" TEXT;
|
||||
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_failureCategory_updatedAt_idx"
|
||||
ON "GatewayDownstreamRecoveryStatus"("failureCategory", "updatedAt");
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "GatewayDownstreamRecoveryStatus"
|
||||
ADD COLUMN "lockOwner" TEXT,
|
||||
ADD COLUMN "lockExpiresAt" TIMESTAMP(3);
|
||||
|
||||
CREATE INDEX "GatewayDownstreamRecoveryStatus_lockOwner_lockExpiresAt_idx"
|
||||
ON "GatewayDownstreamRecoveryStatus"("lockOwner", "lockExpiresAt");
|
||||
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE "SmsMessageSegmentAudit" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"batchTaskId" TEXT,
|
||||
"messageRecordId" TEXT NOT NULL,
|
||||
"submitRecordId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"submitId" TEXT NOT NULL,
|
||||
"attempt" INTEGER NOT NULL DEFAULT 0,
|
||||
"segmentTotal" INTEGER NOT NULL DEFAULT 1,
|
||||
"segmentIndex" INTEGER NOT NULL DEFAULT 1,
|
||||
"sequenceId" INTEGER,
|
||||
"gatewayMessageId" TEXT,
|
||||
"submitStatus" TEXT NOT NULL DEFAULT 'queued',
|
||||
"receiptStatus" TEXT,
|
||||
"rawStatus" TEXT,
|
||||
"compensationType" TEXT,
|
||||
"errorCode" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"submittedAt" TIMESTAMP(3),
|
||||
"deliveredAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SmsMessageSegmentAudit_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SmsMessageSegmentAudit_messageRecordId_submitId_segmentIndex_key"
|
||||
ON "SmsMessageSegmentAudit"("messageRecordId", "submitId", "segmentIndex");
|
||||
|
||||
CREATE INDEX "SmsMessageSegmentAudit_tenantId_createdAt_idx"
|
||||
ON "SmsMessageSegmentAudit"("tenantId", "createdAt");
|
||||
|
||||
CREATE INDEX "SmsMessageSegmentAudit_messageRecordId_segmentIndex_idx"
|
||||
ON "SmsMessageSegmentAudit"("messageRecordId", "segmentIndex");
|
||||
|
||||
CREATE INDEX "SmsMessageSegmentAudit_submitRecordId_idx"
|
||||
ON "SmsMessageSegmentAudit"("submitRecordId");
|
||||
|
||||
CREATE INDEX "SmsMessageSegmentAudit_gatewayMessageId_idx"
|
||||
ON "SmsMessageSegmentAudit"("gatewayMessageId");
|
||||
|
||||
CREATE INDEX "SmsMessageSegmentAudit_submitStatus_receiptStatus_idx"
|
||||
ON "SmsMessageSegmentAudit"("submitStatus", "receiptStatus");
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit"
|
||||
ADD CONSTRAINT "SmsMessageSegmentAudit_tenantId_fkey"
|
||||
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit"
|
||||
ADD CONSTRAINT "SmsMessageSegmentAudit_batchTaskId_fkey"
|
||||
FOREIGN KEY ("batchTaskId") REFERENCES "SmsBatchTask"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit"
|
||||
ADD CONSTRAINT "SmsMessageSegmentAudit_messageRecordId_fkey"
|
||||
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit"
|
||||
ADD CONSTRAINT "SmsMessageSegmentAudit_submitRecordId_fkey"
|
||||
FOREIGN KEY ("submitRecordId") REFERENCES "SmsSubmitRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit"
|
||||
ADD CONSTRAINT "SmsMessageSegmentAudit_channelId_fkey"
|
||||
FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,45 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SmsUplinkMatchCandidate" (
|
||||
"id" TEXT NOT NULL,
|
||||
"uplinkMessageId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"messageRecordId" TEXT,
|
||||
"matchSource" TEXT NOT NULL,
|
||||
"confidence" INTEGER NOT NULL DEFAULT 50,
|
||||
"reason" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"claimedAt" TIMESTAMP(3),
|
||||
"claimedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SmsUplinkMatchCandidate_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SmsUplinkMatchCandidate_uplinkMessageId_applicationId_messageRecordId_key" ON "SmsUplinkMatchCandidate"("uplinkMessageId", "applicationId", "messageRecordId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SmsUplinkMatchCandidate_uplinkMessageId_status_idx" ON "SmsUplinkMatchCandidate"("uplinkMessageId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SmsUplinkMatchCandidate_tenantId_status_createdAt_idx" ON "SmsUplinkMatchCandidate"("tenantId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SmsUplinkMatchCandidate_applicationId_status_createdAt_idx" ON "SmsUplinkMatchCandidate"("applicationId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SmsUplinkMatchCandidate_messageRecordId_idx" ON "SmsUplinkMatchCandidate"("messageRecordId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_uplinkMessageId_fkey" FOREIGN KEY ("uplinkMessageId") REFERENCES "SmsUplinkMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_messageRecordId_fkey" FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "SmsChannel" ALTER COLUMN "cmppVersion" SET DEFAULT '2.0';
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "SmsMessageSegmentAudit" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "SmsReceiptRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SmsApplication"
|
||||
ADD COLUMN "interfaceEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "interfaceType" TEXT NOT NULL DEFAULT 'cmpp20';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE "SmsApplication" ADD COLUMN "cmppEnterpriseCode" TEXT;
|
||||
|
||||
UPDATE "SmsApplication" AS app
|
||||
SET "cmppEnterpriseCode" = tenant."code",
|
||||
"secretHash" = substring(md5(random()::text || clock_timestamp()::text || app."id") from 1 for 16)
|
||||
FROM "Tenant" AS tenant
|
||||
WHERE app."tenantId" = tenant."id";
|
||||
|
||||
ALTER TABLE "SmsApplication" ALTER COLUMN "cmppEnterpriseCode" SET NOT NULL;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
ALTER TABLE "EnterpriseBlacklist" ADD COLUMN "applicationId" TEXT;
|
||||
|
||||
UPDATE "EnterpriseBlacklist" AS blacklist
|
||||
SET "applicationId" = (
|
||||
SELECT "id"
|
||||
FROM "SmsApplication"
|
||||
WHERE "tenantId" = blacklist."tenantId"
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
DELETE FROM "EnterpriseBlacklist" WHERE "applicationId" IS NULL;
|
||||
|
||||
DROP INDEX IF EXISTS "EnterpriseBlacklist_tenantId_phoneNumber_key";
|
||||
|
||||
ALTER TABLE "EnterpriseBlacklist" ALTER COLUMN "applicationId" SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "EnterpriseBlacklist_applicationId_phoneNumber_key" ON "EnterpriseBlacklist"("applicationId", "phoneNumber");
|
||||
|
||||
ALTER TABLE "EnterpriseBlacklist" ADD CONSTRAINT "EnterpriseBlacklist_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "User" ADD COLUMN "sessionVersion" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,29 @@
|
||||
CREATE TABLE "CmppDownstreamConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"account" TEXT NOT NULL,
|
||||
"enterpriseCode" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"remoteIp" TEXT,
|
||||
"protocol" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'connected',
|
||||
"connectedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastHeartbeatAt" TIMESTAMP(3),
|
||||
"lastSubmitAt" TIMESTAMP(3),
|
||||
"lastDeliverAt" TIMESTAMP(3),
|
||||
"disconnectedAt" TIMESTAMP(3),
|
||||
"lastError" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CmppDownstreamConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "CmppDownstreamConnection_connectionId_key" ON "CmppDownstreamConnection"("connectionId");
|
||||
CREATE INDEX "CmppDownstreamConnection_applicationId_status_updatedAt_idx" ON "CmppDownstreamConnection"("applicationId", "status", "updatedAt");
|
||||
CREATE INDEX "CmppDownstreamConnection_tenantId_status_updatedAt_idx" ON "CmppDownstreamConnection"("tenantId", "status", "updatedAt");
|
||||
CREATE INDEX "CmppDownstreamConnection_account_status_idx" ON "CmppDownstreamConnection"("account", "status");
|
||||
|
||||
ALTER TABLE "CmppDownstreamConnection" ADD CONSTRAINT "CmppDownstreamConnection_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "CmppDownstreamConnection" ADD CONSTRAINT "CmppDownstreamConnection_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "carrier" TEXT,
|
||||
ADD COLUMN "province" TEXT;
|
||||
|
||||
UPDATE "SmsMessageRecord" AS message
|
||||
SET "province" = segment."province"
|
||||
FROM "PhoneSegment" AS segment
|
||||
WHERE segment."prefix" = left(message."phoneNumber", 7);
|
||||
|
||||
UPDATE "SmsMessageRecord" AS message
|
||||
SET "carrier" = COALESCE((
|
||||
SELECT rule."carrier"
|
||||
FROM "PhoneCarrierRule" AS rule
|
||||
WHERE rule."status" = 'active'
|
||||
AND message."phoneNumber" ~ rule."pattern"
|
||||
ORDER BY rule."priority" ASC, rule."createdAt" ASC
|
||||
LIMIT 1
|
||||
), 'mobile');
|
||||
|
||||
CREATE INDEX "SmsMessageRecord_carrier_province_idx"
|
||||
ON "SmsMessageRecord"("carrier", "province");
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE "SmsSendTask"
|
||||
ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'risk',
|
||||
ADD COLUMN "aggregationKey" TEXT,
|
||||
ADD COLUMN "contentHash" TEXT,
|
||||
ADD COLUMN "windowStartedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "windowEndsAt" TIMESTAMP(3);
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "signatureId" TEXT,
|
||||
ADD COLUMN "reviewTaskId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "SmsSendTask_aggregationKey_key" ON "SmsSendTask"("aggregationKey");
|
||||
CREATE INDEX "SmsMessageRecord_reviewTaskId_status_idx" ON "SmsMessageRecord"("reviewTaskId", "status");
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD CONSTRAINT "SmsMessageRecord_signatureId_fkey"
|
||||
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD CONSTRAINT "SmsMessageRecord_reviewTaskId_fkey"
|
||||
FOREIGN KEY ("reviewTaskId") REFERENCES "SmsSendTask"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,33 @@
|
||||
ALTER TABLE "ChannelReportField"
|
||||
ADD COLUMN "drainageFieldId" TEXT,
|
||||
ADD COLUMN "reportType" TEXT NOT NULL DEFAULT 'both';
|
||||
|
||||
CREATE INDEX "ChannelReportField_drainageFieldId_reportType_idx"
|
||||
ON "ChannelReportField"("drainageFieldId", "reportType");
|
||||
|
||||
ALTER TABLE "ChannelReportField"
|
||||
ADD CONSTRAINT "ChannelReportField_drainageFieldId_fkey"
|
||||
FOREIGN KEY ("drainageFieldId") REFERENCES "DrainageField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
CREATE TABLE "DrainageReportMaterial" (
|
||||
"id" TEXT NOT NULL,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"drainageItemId" TEXT NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"fieldCode" TEXT NOT NULL,
|
||||
"fieldValue" TEXT,
|
||||
"fileObjectId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "DrainageReportMaterial_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "DrainageReportMaterial_signatureId_drainageItemId_channelId_fieldCode_key"
|
||||
ON "DrainageReportMaterial"("signatureId", "drainageItemId", "channelId", "fieldCode");
|
||||
CREATE INDEX "DrainageReportMaterial_channelId_fieldCode_idx"
|
||||
ON "DrainageReportMaterial"("channelId", "fieldCode");
|
||||
|
||||
ALTER TABLE "DrainageReportMaterial" ADD CONSTRAINT "DrainageReportMaterial_signatureId_fkey"
|
||||
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "DrainageReportMaterial" ADD CONSTRAINT "DrainageReportMaterial_channelId_fkey"
|
||||
FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
UPDATE "DrainageField"
|
||||
SET "fieldType" = 'string'
|
||||
WHERE "fieldType" NOT IN ('string', 'image', 'file');
|
||||
|
||||
UPDATE "ChannelReportField"
|
||||
SET "fieldType" = CASE
|
||||
WHEN "fieldType" IN ('string', 'image', 'file') THEN "fieldType"
|
||||
ELSE 'string'
|
||||
END;
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE "ChannelSignatureReportTask"
|
||||
ADD COLUMN "reportType" TEXT NOT NULL DEFAULT 'signature',
|
||||
ADD COLUMN "drainageItemId" TEXT;
|
||||
|
||||
CREATE INDEX "ChannelSignatureReportTask_signatureId_drainageItemId_channelId_idx"
|
||||
ON "ChannelSignatureReportTask"("signatureId", "drainageItemId", "channelId");
|
||||
|
||||
CREATE INDEX "ChannelSignatureReportTask_reportType_status_idx"
|
||||
ON "ChannelSignatureReportTask"("reportType", "status");
|
||||
@@ -0,0 +1,45 @@
|
||||
INSERT INTO "ChannelSignatureReportTask" (
|
||||
"id", "tenantId", "signatureId", "channelId", "reportType", "drainageItemId", "status", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
'drainage_' || md5(random()::text || clock_timestamp()::text || material."id"),
|
||||
signature."tenantId",
|
||||
material."signatureId",
|
||||
material."channelId",
|
||||
'drainage',
|
||||
material."drainageItemId",
|
||||
'pending',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT DISTINCT ON ("signatureId", "drainageItemId", "channelId")
|
||||
"id", "signatureId", "drainageItemId", "channelId"
|
||||
FROM "DrainageReportMaterial"
|
||||
ORDER BY "signatureId", "drainageItemId", "channelId", "createdAt"
|
||||
) material
|
||||
JOIN "SmsSignature" signature ON signature."id" = material."signatureId"
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ChannelSignatureReportTask" task
|
||||
WHERE task."signatureId" = material."signatureId"
|
||||
AND task."channelId" = material."channelId"
|
||||
AND task."reportType" = 'drainage'
|
||||
AND task."drainageItemId" = material."drainageItemId"
|
||||
);
|
||||
|
||||
INSERT INTO "ChannelSignatureReportRecord" (
|
||||
"id", "taskId", "channelId", "action", "statusAfter", "reason", "createdAt"
|
||||
)
|
||||
SELECT
|
||||
'drainage_record_' || md5(random()::text || clock_timestamp()::text || task."id"),
|
||||
task."id",
|
||||
task."channelId",
|
||||
'migration_backfill',
|
||||
task."status",
|
||||
'从已有引流报备材料回填任务',
|
||||
CURRENT_TIMESTAMP
|
||||
FROM "ChannelSignatureReportTask" task
|
||||
WHERE task."reportType" = 'drainage'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "ChannelSignatureReportRecord" record WHERE record."taskId" = task."id"
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE UNIQUE INDEX "ChannelSignatureReportTask_target_key"
|
||||
ON "ChannelSignatureReportTask"(
|
||||
"signatureId",
|
||||
"channelId",
|
||||
"reportType",
|
||||
COALESCE("drainageItemId", '')
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
CREATE TABLE "SmsDrainageInfo" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"applicationId" TEXT,
|
||||
"siteName" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"remark" TEXT,
|
||||
"reportValues" JSONB,
|
||||
"auditStatus" TEXT NOT NULL DEFAULT 'pending',
|
||||
"rejectReason" TEXT,
|
||||
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SmsDrainageInfo_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TEMP TABLE drainage_backfill_map AS
|
||||
SELECT
|
||||
signature."id" AS "signatureId",
|
||||
COALESCE(NULLIF(item.value->>'id', ''), item.ordinality::text) AS "oldId",
|
||||
'drain_' || md5(signature."id" || ':' || COALESCE(NULLIF(item.value->>'id', ''), item.ordinality::text)) AS "newId",
|
||||
signature."tenantId" AS "tenantId",
|
||||
signature."applicationId" AS "applicationId",
|
||||
item.value AS item
|
||||
FROM "SmsSignature" signature
|
||||
CROSS JOIN LATERAL jsonb_array_elements(COALESCE(signature."drainageInfo"->'links', '[]'::jsonb)) WITH ORDINALITY AS item(value, ordinality);
|
||||
|
||||
INSERT INTO "SmsDrainageInfo" (
|
||||
"id", "tenantId", "signatureId", "applicationId", "siteName", "url", "remark", "reportValues",
|
||||
"auditStatus", "submittedAt", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
"newId",
|
||||
"tenantId",
|
||||
"signatureId",
|
||||
"applicationId",
|
||||
COALESCE(NULLIF(item->>'siteName', ''), '未命名站点'),
|
||||
COALESCE(item->>'url', ''),
|
||||
NULLIF(item->>'remark', ''),
|
||||
COALESCE(item->'reportValues', '{}'::jsonb),
|
||||
CASE WHEN item->>'auditStatus' IN ('pending', 'approved', 'rejected') THEN item->>'auditStatus' ELSE 'pending' END,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM drainage_backfill_map;
|
||||
|
||||
UPDATE "ChannelSignatureReportTask" task
|
||||
SET "drainageItemId" = map."newId"
|
||||
FROM drainage_backfill_map map
|
||||
WHERE task."signatureId" = map."signatureId"
|
||||
AND task."reportType" = 'drainage'
|
||||
AND task."drainageItemId" = map."oldId";
|
||||
|
||||
UPDATE "DrainageReportMaterial" material
|
||||
SET "drainageItemId" = map."newId"
|
||||
FROM drainage_backfill_map map
|
||||
WHERE material."signatureId" = map."signatureId"
|
||||
AND material."drainageItemId" = map."oldId";
|
||||
|
||||
DROP TABLE drainage_backfill_map;
|
||||
|
||||
CREATE INDEX "SmsDrainageInfo_tenantId_auditStatus_updatedAt_idx"
|
||||
ON "SmsDrainageInfo"("tenantId", "auditStatus", "updatedAt");
|
||||
CREATE INDEX "SmsDrainageInfo_signatureId_auditStatus_idx"
|
||||
ON "SmsDrainageInfo"("signatureId", "auditStatus");
|
||||
CREATE INDEX "SmsDrainageInfo_applicationId_idx"
|
||||
ON "SmsDrainageInfo"("applicationId");
|
||||
|
||||
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_tenantId_fkey"
|
||||
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_signatureId_fkey"
|
||||
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "ChannelSignatureReportTask" ADD CONSTRAINT "ChannelSignatureReportTask_drainageItemId_fkey"
|
||||
FOREIGN KEY ("drainageItemId") REFERENCES "SmsDrainageInfo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "ChannelSignatureReportRecord"
|
||||
ADD COLUMN "sourceEntry" TEXT NOT NULL DEFAULT 'system';
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE "ChannelSignatureReportRecord"
|
||||
SET "sourceEntry" = 'legacy'
|
||||
WHERE "action" = 'manual_status_change'
|
||||
AND "sourceEntry" = 'system';
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
ALTER TABLE "SmsApplication"
|
||||
ADD COLUMN "downstreamReceiptRetryEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "downstreamUplinkRetryEnabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD COLUMN "retryEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "sentAt" TIMESTAMP(3),
|
||||
ADD COLUMN "acknowledgedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "ackDeadlineAt" TIMESTAMP(3),
|
||||
ADD COLUMN "ackResult" INTEGER,
|
||||
ADD COLUMN "ackSequenceId" TEXT,
|
||||
ADD COLUMN "ackMessageId" TEXT,
|
||||
ADD COLUMN "connectionId" TEXT;
|
||||
|
||||
UPDATE "CmppDownstreamDelivery"
|
||||
SET
|
||||
"status" = 'unconfirmed',
|
||||
"sentAt" = "deliveredAt",
|
||||
"deliveredAt" = NULL,
|
||||
"lastError" = '历史记录仅确认 Gateway 已写出,未留存 CMPP_DELIVER_RESP'
|
||||
WHERE "status" = 'delivered';
|
||||
|
||||
UPDATE "CmppDownstreamDelivery"
|
||||
SET "retryEnabled" = CASE
|
||||
WHEN "deliveryType" = 'uplink' THEN app."downstreamUplinkRetryEnabled"
|
||||
ELSE app."downstreamReceiptRetryEnabled"
|
||||
END
|
||||
FROM "SmsApplication" app
|
||||
WHERE app.id = "CmppDownstreamDelivery"."applicationId";
|
||||
|
||||
CREATE INDEX "CmppDownstreamDelivery_status_ackDeadlineAt_idx"
|
||||
ON "CmppDownstreamDelivery"("status", "ackDeadlineAt");
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "cmppSubmitSequenceId" TEXT;
|
||||
|
||||
UPDATE "CmppDownstreamDelivery"
|
||||
SET
|
||||
"status" = 'unconfirmed',
|
||||
"deliveredAt" = NULL,
|
||||
"lastError" = 'CMPP_DELIVER_RESP Msg_Id=0,仅确认协议收包,未确认业务回执关联'
|
||||
WHERE "deliveryType" = 'receipt'
|
||||
AND "status" = 'delivered'
|
||||
AND COALESCE("ackMessageId", '0') = '0';
|
||||
@@ -0,0 +1,37 @@
|
||||
CREATE INDEX "OperationLog_createdAt_idx"
|
||||
ON "OperationLog"("createdAt");
|
||||
|
||||
CREATE INDEX "OperationLog_resource_createdAt_idx"
|
||||
ON "OperationLog"("resource", "createdAt");
|
||||
|
||||
CREATE TABLE "OperationLogArchive" (
|
||||
"originalId" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"userId" TEXT,
|
||||
"action" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"resourceId" TEXT,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"detail" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL,
|
||||
"archiveMonth" TEXT NOT NULL,
|
||||
"archivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OperationLogArchive_pkey" PRIMARY KEY ("originalId")
|
||||
);
|
||||
|
||||
CREATE INDEX "OperationLogArchive_tenantId_createdAt_idx"
|
||||
ON "OperationLogArchive"("tenantId", "createdAt");
|
||||
|
||||
CREATE INDEX "OperationLogArchive_userId_createdAt_idx"
|
||||
ON "OperationLogArchive"("userId", "createdAt");
|
||||
|
||||
CREATE INDEX "OperationLogArchive_createdAt_idx"
|
||||
ON "OperationLogArchive"("createdAt");
|
||||
|
||||
CREATE INDEX "OperationLogArchive_resource_createdAt_idx"
|
||||
ON "OperationLogArchive"("resource", "createdAt");
|
||||
|
||||
CREATE INDEX "OperationLogArchive_archiveMonth_idx"
|
||||
ON "OperationLogArchive"("archiveMonth");
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE "RechargeOrder" DROP CONSTRAINT IF EXISTS "RechargeOrder_planId_fkey";
|
||||
|
||||
DROP TABLE IF EXISTS "BillingPlan";
|
||||
|
||||
ALTER TABLE "TenantAccount"
|
||||
DROP COLUMN "smsUnits",
|
||||
DROP COLUMN "creditCents";
|
||||
|
||||
ALTER TABLE "AccountTransaction"
|
||||
DROP COLUMN "smsUnits";
|
||||
|
||||
ALTER TABLE "RechargeOrder"
|
||||
DROP COLUMN "planId",
|
||||
DROP COLUMN "smsUnits";
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "TenantAccount"
|
||||
ADD COLUMN "creditCents" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD COLUMN "manualRetryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "lastRetriedAt" TIMESTAMP(3);
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ADD COLUMN "costUnitPrice" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "costAmountCents" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE "SmsSubmitRecord" submit
|
||||
SET
|
||||
"costUnitPrice" = channel."unitPrice",
|
||||
"costAmountCents" = channel."unitPrice" * message."billingUnits"
|
||||
FROM "SmsChannel" channel, "SmsMessageRecord" message
|
||||
WHERE submit."channelId" = channel.id
|
||||
AND submit."messageRecordId" = message.id;
|
||||
|
||||
CREATE TABLE "DailyReconciliationReport" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reportDate" DATE NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"tenantName" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"applicationName" TEXT NOT NULL,
|
||||
"sentUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"successUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "DailyReconciliationReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "DailyProfitReport" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reportDate" DATE NOT NULL,
|
||||
"dimensionType" TEXT NOT NULL,
|
||||
"dimensionId" TEXT NOT NULL,
|
||||
"dimensionName" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"tenantName" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"sentUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"successUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"revenueCents" INTEGER NOT NULL DEFAULT 0,
|
||||
"costCents" INTEGER NOT NULL DEFAULT 0,
|
||||
"profitCents" INTEGER NOT NULL DEFAULT 0,
|
||||
"profitRateBps" INTEGER NOT NULL DEFAULT 0,
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "DailyProfitReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "DailyReconciliationReport_reportDate_tenantId_applicationId_key"
|
||||
ON "DailyReconciliationReport"("reportDate", "tenantId", "applicationId");
|
||||
CREATE INDEX "DailyReconciliationReport_reportDate_idx" ON "DailyReconciliationReport"("reportDate");
|
||||
CREATE INDEX "DailyReconciliationReport_tenantId_reportDate_idx" ON "DailyReconciliationReport"("tenantId", "reportDate");
|
||||
CREATE INDEX "DailyReconciliationReport_applicationId_reportDate_idx" ON "DailyReconciliationReport"("applicationId", "reportDate");
|
||||
|
||||
CREATE UNIQUE INDEX "DailyProfitReport_reportDate_dimensionType_dimensionId_key"
|
||||
ON "DailyProfitReport"("reportDate", "dimensionType", "dimensionId");
|
||||
CREATE INDEX "DailyProfitReport_dimensionType_reportDate_idx" ON "DailyProfitReport"("dimensionType", "reportDate");
|
||||
CREATE INDEX "DailyProfitReport_tenantId_reportDate_idx" ON "DailyProfitReport"("tenantId", "reportDate");
|
||||
CREATE INDEX "DailyProfitReport_applicationId_reportDate_idx" ON "DailyProfitReport"("applicationId", "reportDate");
|
||||
CREATE INDEX "DailyProfitReport_channelId_reportDate_idx" ON "DailyProfitReport"("channelId", "reportDate");
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Downstream connection rows represent current live sessions only.
|
||||
-- Remove historical disconnected/timeout rows left by the previous persistence model.
|
||||
DELETE FROM "CmppDownstreamConnection"
|
||||
WHERE "status" <> 'connected';
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE "CommonReportField" (
|
||||
"id" TEXT NOT NULL,
|
||||
"drainageFieldId" TEXT NOT NULL,
|
||||
"reportType" TEXT NOT NULL,
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 100,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CommonReportField_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "CommonReportField_drainageFieldId_reportType_key"
|
||||
ON "CommonReportField"("drainageFieldId", "reportType");
|
||||
|
||||
CREATE INDEX "CommonReportField_reportType_status_sortOrder_idx"
|
||||
ON "CommonReportField"("reportType", "status", "sortOrder");
|
||||
|
||||
ALTER TABLE "CommonReportField"
|
||||
ADD CONSTRAINT "CommonReportField_drainageFieldId_fkey"
|
||||
FOREIGN KEY ("drainageFieldId") REFERENCES "DrainageField"("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,65 @@
|
||||
ALTER TABLE "SmsMessageRecord" ADD COLUMN "drainageInfoId" TEXT;
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD CONSTRAINT "SmsMessageRecord_drainageInfoId_fkey"
|
||||
FOREIGN KEY ("drainageInfoId") REFERENCES "SmsDrainageInfo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "SmsMessageRecord_drainageInfoId_queuedAt_idx"
|
||||
ON "SmsMessageRecord"("drainageInfoId", "queuedAt");
|
||||
|
||||
WITH drainage_matches AS (
|
||||
SELECT
|
||||
message.id AS "messageRecordId",
|
||||
drainage.id AS "drainageInfoId",
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY message.id
|
||||
ORDER BY LENGTH(drainage.url) DESC, drainage."updatedAt" DESC, drainage.id
|
||||
) AS match_rank,
|
||||
COUNT(*) OVER (
|
||||
PARTITION BY message.id, LENGTH(drainage.url)
|
||||
) AS same_length_matches
|
||||
FROM "SmsMessageRecord" message
|
||||
JOIN "SmsDrainageInfo" drainage
|
||||
ON drainage."signatureId" = message."signatureId"
|
||||
AND drainage."auditStatus" = 'approved'
|
||||
AND LENGTH(BTRIM(drainage.url)) > 0
|
||||
AND POSITION(BTRIM(drainage.url) IN message.content) > 0
|
||||
WHERE message."drainageInfoId" IS NULL
|
||||
)
|
||||
UPDATE "SmsMessageRecord" message
|
||||
SET "drainageInfoId" = matched."drainageInfoId"
|
||||
FROM drainage_matches matched
|
||||
WHERE message.id = matched."messageRecordId"
|
||||
AND matched.match_rank = 1
|
||||
AND matched.same_length_matches = 1;
|
||||
|
||||
CREATE TABLE "DailyQualityReport" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reportDate" DATE NOT NULL,
|
||||
"dimensionType" TEXT NOT NULL,
|
||||
"dimensionId" TEXT NOT NULL,
|
||||
"dimensionName" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"tenantName" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"signatureId" TEXT,
|
||||
"drainageInfoId" TEXT,
|
||||
"sentUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"successUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"successRateBps" INTEGER NOT NULL DEFAULT 0,
|
||||
"avgArrivalMs" INTEGER,
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "DailyQualityReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "DailyQualityReport_reportDate_dimensionType_dimensionId_key"
|
||||
ON "DailyQualityReport"("reportDate", "dimensionType", "dimensionId");
|
||||
CREATE INDEX "DailyQualityReport_dimensionType_reportDate_sentUnits_idx"
|
||||
ON "DailyQualityReport"("dimensionType", "reportDate", "sentUnits");
|
||||
CREATE INDEX "DailyQualityReport_tenantId_reportDate_idx" ON "DailyQualityReport"("tenantId", "reportDate");
|
||||
CREATE INDEX "DailyQualityReport_applicationId_reportDate_idx" ON "DailyQualityReport"("applicationId", "reportDate");
|
||||
CREATE INDEX "DailyQualityReport_channelId_reportDate_idx" ON "DailyQualityReport"("channelId", "reportDate");
|
||||
CREATE INDEX "DailyQualityReport_signatureId_reportDate_idx" ON "DailyQualityReport"("signatureId", "reportDate");
|
||||
CREATE INDEX "DailyQualityReport_drainageInfoId_reportDate_idx" ON "DailyQualityReport"("drainageInfoId", "reportDate");
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
ALTER TABLE "SmsApplication"
|
||||
ADD COLUMN "cmppApplicationExtension" TEXT,
|
||||
ADD COLUMN "cmppAccessNumberFillEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "cmppAccessNumberFillPrefix" TEXT,
|
||||
ADD COLUMN "cmppClientSrcId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "SmsApplication_cmppClientSrcId_key"
|
||||
ON "SmsApplication"("cmppClientSrcId");
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "clientSrcId" TEXT,
|
||||
ADD COLUMN "applicationExtension" TEXT;
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
ALTER TABLE "SmsSignature"
|
||||
ADD COLUMN "materialVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "pendingReport" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "reportChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE "SmsDrainageInfo"
|
||||
ADD COLUMN "materialVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "pendingReport" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "reportChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE "ChannelReportField"
|
||||
ADD COLUMN "exportName" TEXT,
|
||||
ADD COLUMN "columnWidth" INTEGER NOT NULL DEFAULT 18,
|
||||
ADD COLUMN "imageWidth" INTEGER NOT NULL DEFAULT 120,
|
||||
ADD COLUMN "imageHeight" INTEGER NOT NULL DEFAULT 80,
|
||||
ADD COLUMN "defaultValue" TEXT,
|
||||
ADD COLUMN "transform" TEXT;
|
||||
|
||||
ALTER TABLE "ReportExportFile"
|
||||
ALTER COLUMN "taskId" DROP NOT NULL,
|
||||
ADD COLUMN "batchId" TEXT,
|
||||
ADD COLUMN "channelId" TEXT;
|
||||
|
||||
CREATE TABLE "ReportMaterialBatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"batchNo" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'generating',
|
||||
"createdById" TEXT,
|
||||
"selectedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"channelCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"fileCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
CONSTRAINT "ReportMaterialBatch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ReportMaterialBatchItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"batchId" TEXT NOT NULL,
|
||||
"signatureId" TEXT NOT NULL,
|
||||
"drainageItemId" TEXT,
|
||||
"reportType" TEXT NOT NULL,
|
||||
"materialVersion" INTEGER NOT NULL,
|
||||
"snapshot" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "ReportMaterialBatchItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ReportExportFileItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"exportFileId" TEXT NOT NULL,
|
||||
"batchItemId" TEXT NOT NULL,
|
||||
"taskId" TEXT NOT NULL,
|
||||
"rowNumber" INTEGER NOT NULL,
|
||||
CONSTRAINT "ReportExportFileItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ReportMaterialImportProfile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"reportType" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"sheetName" TEXT,
|
||||
"headerRowCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"dataStartRow" INTEGER NOT NULL DEFAULT 2,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "ReportMaterialImportProfile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ReportMaterialImportProfileColumn" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"sourceHeader" TEXT NOT NULL,
|
||||
"sourceHeaderPath" TEXT,
|
||||
"sourceColumnIndex" INTEGER NOT NULL,
|
||||
"targetFieldCode" TEXT NOT NULL,
|
||||
"targetKind" TEXT NOT NULL DEFAULT 'dynamic',
|
||||
"fieldType" TEXT NOT NULL DEFAULT 'string',
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"transform" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 100,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "ReportMaterialImportProfileColumn_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ReportMaterialImportBatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT,
|
||||
"profileId" TEXT,
|
||||
"fileObjectId" TEXT NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"reportType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'analyzed',
|
||||
"sheetName" TEXT NOT NULL,
|
||||
"headerRowCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"dataStartRow" INTEGER NOT NULL DEFAULT 2,
|
||||
"mapping" JSONB NOT NULL,
|
||||
"preview" JSONB,
|
||||
"result" JSONB,
|
||||
"rowCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"successCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"failedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
CONSTRAINT "ReportMaterialImportBatch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "ReportMaterialBatch_batchNo_key" ON "ReportMaterialBatch"("batchNo");
|
||||
CREATE INDEX "ReportMaterialBatch_status_createdAt_idx" ON "ReportMaterialBatch"("status", "createdAt");
|
||||
CREATE INDEX "ReportMaterialBatchItem_batchId_reportType_idx" ON "ReportMaterialBatchItem"("batchId", "reportType");
|
||||
CREATE INDEX "ReportMaterialBatchItem_signatureId_drainageItemId_idx" ON "ReportMaterialBatchItem"("signatureId", "drainageItemId");
|
||||
CREATE UNIQUE INDEX "ReportExportFileItem_exportFileId_batchItemId_key" ON "ReportExportFileItem"("exportFileId", "batchItemId");
|
||||
CREATE INDEX "ReportExportFileItem_taskId_idx" ON "ReportExportFileItem"("taskId");
|
||||
CREATE INDEX "ReportExportFile_batchId_channelId_idx" ON "ReportExportFile"("batchId", "channelId");
|
||||
CREATE INDEX "ReportMaterialImportProfile_reportType_status_idx" ON "ReportMaterialImportProfile"("reportType", "status");
|
||||
CREATE INDEX "ReportMaterialImportProfile_tenantId_applicationId_idx" ON "ReportMaterialImportProfile"("tenantId", "applicationId");
|
||||
CREATE UNIQUE INDEX "ReportMaterialImportProfileColumn_profileId_sourceColumnIndex_key" ON "ReportMaterialImportProfileColumn"("profileId", "sourceColumnIndex");
|
||||
CREATE INDEX "ReportMaterialImportProfileColumn_profileId_sortOrder_idx" ON "ReportMaterialImportProfileColumn"("profileId", "sortOrder");
|
||||
CREATE INDEX "ReportMaterialImportBatch_tenantId_createdAt_idx" ON "ReportMaterialImportBatch"("tenantId", "createdAt");
|
||||
CREATE INDEX "ReportMaterialImportBatch_status_createdAt_idx" ON "ReportMaterialImportBatch"("status", "createdAt");
|
||||
|
||||
ALTER TABLE "ReportExportFile" ADD CONSTRAINT "ReportExportFile_batchId_fkey" FOREIGN KEY ("batchId") REFERENCES "ReportMaterialBatch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_batchId_fkey" FOREIGN KEY ("batchId") REFERENCES "ReportMaterialBatch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_signatureId_fkey" FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_drainageItemId_fkey" FOREIGN KEY ("drainageItemId") REFERENCES "SmsDrainageInfo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_exportFileId_fkey" FOREIGN KEY ("exportFileId") REFERENCES "ReportExportFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_batchItemId_fkey" FOREIGN KEY ("batchItemId") REFERENCES "ReportMaterialBatchItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "ChannelSignatureReportTask"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ReportMaterialImportProfileColumn" ADD CONSTRAINT "ReportMaterialImportProfileColumn_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "ReportMaterialImportProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
DROP INDEX IF EXISTS "ChannelReportField_channelId_code_key";
|
||||
|
||||
INSERT INTO "ChannelReportField" (
|
||||
"id", "channelId", "drainageFieldId", "reportType", "code", "name", "exportName", "fieldType",
|
||||
"required", "description", "sortOrder", "columnWidth", "imageWidth", "imageHeight", "defaultValue",
|
||||
"transform", "status", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
"id" || '_drainage', "channelId", "drainageFieldId", 'drainage', "code", "name", "exportName", "fieldType",
|
||||
"required", "description", "sortOrder", "columnWidth", "imageWidth", "imageHeight", "defaultValue",
|
||||
"transform", "status", "createdAt", "updatedAt"
|
||||
FROM "ChannelReportField"
|
||||
WHERE "reportType" = 'both';
|
||||
|
||||
UPDATE "ChannelReportField" SET "reportType" = 'signature' WHERE "reportType" = 'both';
|
||||
|
||||
CREATE UNIQUE INDEX "ChannelReportField_channelId_code_reportType_key"
|
||||
ON "ChannelReportField"("channelId", "code", "reportType");
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Existing rows predate the pending-material workflow and must not be treated as newly changed.
|
||||
-- New rows continue to use the schema default (true), while later edits explicitly set true.
|
||||
UPDATE "SmsSignature" SET "pendingReport" = false;
|
||||
UPDATE "SmsDrainageInfo" SET "pendingReport" = false;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "SmsChannelGroupItem" DROP COLUMN "rateLimitPerSecond";
|
||||
@@ -0,0 +1,165 @@
|
||||
CREATE TABLE "SmsApplicationHttpConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sendEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"messageQueryEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"receiptWebhookEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"uplinkWebhookEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"uplinkQueryEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"credentialSelfServiceEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"qpsLimit" INTEGER NOT NULL DEFAULT 10,
|
||||
"timestampToleranceSeconds" INTEGER NOT NULL DEFAULT 300,
|
||||
"maxCredentialCount" INTEGER NOT NULL DEFAULT 2,
|
||||
"uplinkRetentionDays" INTEGER NOT NULL DEFAULT 90,
|
||||
"maxQueryRangeDays" INTEGER NOT NULL DEFAULT 31,
|
||||
"maxPageSize" INTEGER NOT NULL DEFAULT 100,
|
||||
"receiptDeliveryMode" TEXT NOT NULL DEFAULT 'cmpp',
|
||||
"uplinkDeliveryMode" TEXT NOT NULL DEFAULT 'cmpp',
|
||||
"webhookRetryEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"webhookMaxAttempts" INTEGER NOT NULL DEFAULT 7,
|
||||
"webhookTimeoutSeconds" INTEGER NOT NULL DEFAULT 10,
|
||||
"requireHttps" BOOLEAN NOT NULL DEFAULT true,
|
||||
"allowClientManualRetry" BOOLEAN NOT NULL DEFAULT true,
|
||||
"allowClientTest" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SmsApplicationHttpConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "SmsApplicationHttpIpAllowlist" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"ipCidr" TEXT NOT NULL,
|
||||
"remark" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SmsApplicationHttpIpAllowlist_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "HttpApiCredential" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"accessKey" TEXT NOT NULL,
|
||||
"secretEncrypted" TEXT NOT NULL,
|
||||
"secretLast4" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"lastUsedAt" TIMESTAMP(3),
|
||||
"lastUsedIp" TEXT,
|
||||
"createdById" TEXT,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "HttpApiCredential_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "OpenApiRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"credentialId" TEXT NOT NULL,
|
||||
"requestId" TEXT NOT NULL,
|
||||
"idempotencyKey" TEXT NOT NULL,
|
||||
"bodyHash" TEXT NOT NULL,
|
||||
"clientMessageId" TEXT,
|
||||
"messageRecordId" TEXT,
|
||||
"httpStatus" INTEGER,
|
||||
"businessCode" TEXT,
|
||||
"responseBody" JSONB,
|
||||
"sourceIp" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"durationMs" INTEGER,
|
||||
"status" TEXT NOT NULL DEFAULT 'processing',
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "OpenApiRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "HttpWebhookEndpoint" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"secretEncrypted" TEXT NOT NULL,
|
||||
"secretLast4" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"lastTestAt" TIMESTAMP(3),
|
||||
"lastTestStatus" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "HttpWebhookEndpoint_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "HttpWebhookEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"eventId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"messageRecordId" TEXT,
|
||||
"messageId" TEXT,
|
||||
"uplinkMessageId" TEXT,
|
||||
"payload" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "HttpWebhookEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "HttpWebhookDelivery" (
|
||||
"id" TEXT NOT NULL,
|
||||
"eventId" TEXT NOT NULL,
|
||||
"endpointId" TEXT 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 "HttpWebhookDelivery_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "HttpWebhookAttempt" (
|
||||
"id" TEXT NOT NULL,
|
||||
"deliveryId" TEXT NOT NULL,
|
||||
"attemptNo" INTEGER NOT NULL,
|
||||
"requestHeaders" JSONB,
|
||||
"responseStatus" INTEGER,
|
||||
"responseSummary" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"durationMs" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "HttpWebhookAttempt_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "SmsMessageRecord" ADD COLUMN "clientMessageId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "SmsApplicationHttpConfig_applicationId_key" ON "SmsApplicationHttpConfig"("applicationId");
|
||||
CREATE UNIQUE INDEX "SmsApplicationHttpIpAllowlist_applicationId_ipCidr_key" ON "SmsApplicationHttpIpAllowlist"("applicationId", "ipCidr");
|
||||
CREATE UNIQUE INDEX "HttpApiCredential_accessKey_key" ON "HttpApiCredential"("accessKey");
|
||||
CREATE INDEX "HttpApiCredential_applicationId_status_idx" ON "HttpApiCredential"("applicationId", "status");
|
||||
CREATE UNIQUE INDEX "OpenApiRequest_requestId_key" ON "OpenApiRequest"("requestId");
|
||||
CREATE UNIQUE INDEX "OpenApiRequest_applicationId_idempotencyKey_key" ON "OpenApiRequest"("applicationId", "idempotencyKey");
|
||||
CREATE INDEX "OpenApiRequest_applicationId_createdAt_idx" ON "OpenApiRequest"("applicationId", "createdAt");
|
||||
CREATE UNIQUE INDEX "HttpWebhookEndpoint_applicationId_eventType_key" ON "HttpWebhookEndpoint"("applicationId", "eventType");
|
||||
CREATE UNIQUE INDEX "HttpWebhookEvent_eventId_key" ON "HttpWebhookEvent"("eventId");
|
||||
CREATE INDEX "HttpWebhookEvent_applicationId_createdAt_idx" ON "HttpWebhookEvent"("applicationId", "createdAt");
|
||||
CREATE UNIQUE INDEX "HttpWebhookDelivery_eventId_endpointId_key" ON "HttpWebhookDelivery"("eventId", "endpointId");
|
||||
CREATE INDEX "HttpWebhookDelivery_status_nextRetryAt_idx" ON "HttpWebhookDelivery"("status", "nextRetryAt");
|
||||
CREATE UNIQUE INDEX "HttpWebhookAttempt_deliveryId_attemptNo_key" ON "HttpWebhookAttempt"("deliveryId", "attemptNo");
|
||||
CREATE UNIQUE INDEX "SmsMessageRecord_applicationId_clientMessageId_key" ON "SmsMessageRecord"("applicationId", "clientMessageId");
|
||||
|
||||
ALTER TABLE "SmsApplicationHttpConfig" ADD CONSTRAINT "SmsApplicationHttpConfig_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsApplicationHttpIpAllowlist" ADD CONSTRAINT "SmsApplicationHttpIpAllowlist_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpApiCredential" ADD CONSTRAINT "HttpApiCredential_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "HttpApiCredential"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookEndpoint" ADD CONSTRAINT "HttpWebhookEndpoint_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookEvent" ADD CONSTRAINT "HttpWebhookEvent_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookEvent" ADD CONSTRAINT "HttpWebhookEvent_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookDelivery" ADD CONSTRAINT "HttpWebhookDelivery_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "HttpWebhookEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookDelivery" ADD CONSTRAINT "HttpWebhookDelivery_endpointId_fkey" FOREIGN KEY ("endpointId") REFERENCES "HttpWebhookEndpoint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "HttpWebhookAttempt" ADD CONSTRAINT "HttpWebhookAttempt_deliveryId_fkey" FOREIGN KEY ("deliveryId") REFERENCES "HttpWebhookDelivery"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
-- Monetary values were historically stored as integer cents. The platform now
|
||||
-- stores integer units of 0.0001 yuan. Convert existing values by 100 while
|
||||
-- widening columns to bigint so balances and report aggregates do not inherit
|
||||
-- PostgreSQL integer's ~214,748 yuan ceiling at the new scale.
|
||||
|
||||
ALTER TABLE "TenantAccount"
|
||||
ALTER COLUMN "balanceCents" TYPE BIGINT USING ("balanceCents"::BIGINT * 100),
|
||||
ALTER COLUMN "creditCents" TYPE BIGINT USING ("creditCents"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "AccountTransaction"
|
||||
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100),
|
||||
ALTER COLUMN "balanceAfter" TYPE BIGINT USING ("balanceAfter"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "BillingRule"
|
||||
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "RechargeOrder"
|
||||
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "SmsBillingRecord"
|
||||
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100),
|
||||
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "SmsApplication"
|
||||
ALTER COLUMN "customerUnitPrice" TYPE BIGINT USING ("customerUnitPrice"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "SmsChannel"
|
||||
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100),
|
||||
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ALTER COLUMN "costUnitPrice" TYPE BIGINT USING ("costUnitPrice"::BIGINT * 100),
|
||||
ALTER COLUMN "costAmountCents" TYPE BIGINT USING ("costAmountCents"::BIGINT * 100);
|
||||
|
||||
ALTER TABLE "DailyProfitReport"
|
||||
ALTER COLUMN "revenueCents" TYPE BIGINT USING ("revenueCents"::BIGINT * 100),
|
||||
ALTER COLUMN "costCents" TYPE BIGINT USING ("costCents"::BIGINT * 100),
|
||||
ALTER COLUMN "profitCents" TYPE BIGINT USING ("profitCents"::BIGINT * 100);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "cmppSubmitGroupMessageId" TEXT;
|
||||
@@ -0,0 +1,26 @@
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "receiptRawStatus" TEXT;
|
||||
|
||||
ALTER TABLE "SmsReceiptRecord"
|
||||
ADD COLUMN "receiptKey" TEXT,
|
||||
ADD COLUMN "errorMessage" TEXT;
|
||||
|
||||
UPDATE "SmsReceiptRecord"
|
||||
SET "receiptKey" = MD5(
|
||||
COALESCE("channelId", '') || E'\u0001' ||
|
||||
"gatewayMessageId" || E'\u0001' ||
|
||||
"receiptStatus" || E'\u0001' ||
|
||||
"rawStatus"
|
||||
) || MD5("id");
|
||||
|
||||
ALTER TABLE "SmsReceiptRecord"
|
||||
ALTER COLUMN "receiptKey" SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "SmsReceiptRecord_receiptKey_key"
|
||||
ON "SmsReceiptRecord"("receiptKey");
|
||||
|
||||
CREATE INDEX "SmsSubmitRecord_channelId_gatewayMessageId_idx"
|
||||
ON "SmsSubmitRecord"("channelId", "gatewayMessageId");
|
||||
|
||||
CREATE INDEX "SmsReceiptRecord_channelId_gatewayMessageId_idx"
|
||||
ON "SmsReceiptRecord"("channelId", "gatewayMessageId");
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE "DailyReconciliationReport"
|
||||
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "DailyProfitReport"
|
||||
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "refundCents" BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "DailyQualityReport"
|
||||
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE "SmsReceiptRecord"
|
||||
ADD COLUMN "phoneNumber" TEXT;
|
||||
|
||||
UPDATE "SmsReceiptRecord" receipt
|
||||
SET "phoneNumber" = message."phoneNumber"
|
||||
FROM "SmsMessageRecord" message
|
||||
WHERE receipt."messageRecordId" = message.id
|
||||
AND receipt."phoneNumber" IS NULL;
|
||||
|
||||
CREATE INDEX "SmsReceiptRecord_channelId_gatewayMessageId_phoneNumber_idx"
|
||||
ON "SmsReceiptRecord"("channelId", "gatewayMessageId", "phoneNumber");
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
-- Historical receipts created before channel-aware matching could be attached to
|
||||
-- the right message but retain the wrong channel when two channels shared the
|
||||
-- same supplier account. Only repair rows that have exactly one submit record
|
||||
-- for the same message, upstream message id and destination phone number.
|
||||
WITH unique_receipt_submit AS (
|
||||
SELECT
|
||||
receipt.id AS receipt_id,
|
||||
MIN(submit."channelId") AS correct_channel_id
|
||||
FROM "SmsReceiptRecord" receipt
|
||||
JOIN "SmsMessageRecord" message
|
||||
ON message.id = receipt."messageRecordId"
|
||||
JOIN "SmsSubmitRecord" submit
|
||||
ON submit."messageRecordId" = message.id
|
||||
AND submit."gatewayMessageId" = receipt."gatewayMessageId"
|
||||
WHERE receipt."receiptStatus" = 'delivered'
|
||||
AND receipt."phoneNumber" = message."phoneNumber"
|
||||
GROUP BY receipt.id
|
||||
HAVING COUNT(submit.id) = 1
|
||||
)
|
||||
UPDATE "SmsReceiptRecord" receipt
|
||||
SET "channelId" = matched.correct_channel_id
|
||||
FROM unique_receipt_submit matched
|
||||
WHERE receipt.id = matched.receipt_id
|
||||
AND receipt."channelId" IS DISTINCT FROM matched.correct_channel_id;
|
||||
|
||||
-- Aggregate the corrected successful receipt onto the message master record.
|
||||
-- DISTINCT ON chooses the first successful receipt for segmented messages while
|
||||
-- preserving the original submit/gateway identity of the current attempt.
|
||||
WITH delivered_match AS (
|
||||
SELECT DISTINCT ON (message.id)
|
||||
message.id AS message_record_id,
|
||||
submit."channelId" AS channel_id,
|
||||
receipt."gatewayMessageId" AS gateway_message_id,
|
||||
receipt."rawStatus" AS raw_status,
|
||||
receipt."errorCode" AS error_code,
|
||||
receipt."errorMessage" AS error_message,
|
||||
receipt."deliveredAt" AS delivered_at
|
||||
FROM "SmsMessageRecord" message
|
||||
JOIN "SmsReceiptRecord" receipt
|
||||
ON receipt."messageRecordId" = message.id
|
||||
AND receipt."receiptStatus" = 'delivered'
|
||||
AND receipt."phoneNumber" = message."phoneNumber"
|
||||
JOIN "SmsSubmitRecord" submit
|
||||
ON submit."messageRecordId" = message.id
|
||||
AND submit."gatewayMessageId" = receipt."gatewayMessageId"
|
||||
AND submit."channelId" = receipt."channelId"
|
||||
WHERE message."gatewayMessageId" = receipt."gatewayMessageId"
|
||||
ORDER BY message.id, receipt."deliveredAt" ASC, receipt.id ASC
|
||||
)
|
||||
UPDATE "SmsMessageRecord" message
|
||||
SET
|
||||
status = 'delivered',
|
||||
"receiptStatus" = 'delivered',
|
||||
"receiptRawStatus" = matched.raw_status,
|
||||
"channelId" = matched.channel_id,
|
||||
"gatewayMessageId" = matched.gateway_message_id,
|
||||
"errorCode" = matched.error_code,
|
||||
"errorMessage" = matched.error_message,
|
||||
"deliveredAt" = matched.delivered_at,
|
||||
"timeoutAt" = NULL,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
FROM delivered_match matched
|
||||
WHERE message.id = matched.message_record_id
|
||||
AND (
|
||||
message.status IS DISTINCT FROM 'delivered'
|
||||
OR message."receiptStatus" IS DISTINCT FROM 'delivered'
|
||||
OR message."deliveredAt" IS DISTINCT FROM matched.delivered_at
|
||||
OR message."channelId" IS DISTINCT FROM matched.channel_id
|
||||
);
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
-- Enforce a real per-application daily send quota and make 100,000 the platform default.
|
||||
UPDATE "SmsApplication"
|
||||
SET "dailyLimit" = 100000
|
||||
WHERE "dailyLimit" IS NULL;
|
||||
|
||||
ALTER TABLE "SmsApplication"
|
||||
ALTER COLUMN "dailyLimit" SET DEFAULT 100000,
|
||||
ALTER COLUMN "dailyLimit" SET NOT NULL;
|
||||
|
||||
CREATE TABLE "SmsApplicationDailyUsage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"usageDate" DATE NOT NULL,
|
||||
"usedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SmsApplicationDailyUsage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SmsApplicationDailyUsage_applicationId_usageDate_key"
|
||||
ON "SmsApplicationDailyUsage"("applicationId", "usageDate");
|
||||
CREATE INDEX "SmsApplicationDailyUsage_usageDate_idx"
|
||||
ON "SmsApplicationDailyUsage"("usageDate");
|
||||
ALTER TABLE "SmsApplicationDailyUsage"
|
||||
ADD CONSTRAINT "SmsApplicationDailyUsage_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Preserve sends already accepted earlier on the deployment day, so deploying at
|
||||
-- midday cannot grant an extra full quota. The business day is Asia/Shanghai.
|
||||
INSERT INTO "SmsApplicationDailyUsage" (
|
||||
"id", "applicationId", "usageDate", "usedCount", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
md5(message."applicationId" || ':' || ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date)::text),
|
||||
message."applicationId",
|
||||
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date,
|
||||
COUNT(*)::integer,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM "SmsMessageRecord" message
|
||||
WHERE message."applicationId" IS NOT NULL
|
||||
AND message."queuedAt" >= ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date::timestamp AT TIME ZONE 'Asia/Shanghai')
|
||||
AND message."queuedAt" < (((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai')
|
||||
GROUP BY message."applicationId";
|
||||
|
||||
-- HTTP applications created before the UI default fix retained CMPP delivery modes.
|
||||
-- Existing enabled webhook capabilities are migrated to the HTTP transport they expose.
|
||||
UPDATE "SmsApplicationHttpConfig"
|
||||
SET "receiptDeliveryMode" = 'http'
|
||||
WHERE "enabled" = TRUE
|
||||
AND "receiptWebhookEnabled" = TRUE
|
||||
AND "receiptDeliveryMode" = 'cmpp';
|
||||
|
||||
UPDATE "SmsApplicationHttpConfig"
|
||||
SET "uplinkDeliveryMode" = 'http'
|
||||
WHERE "enabled" = TRUE
|
||||
AND "uplinkWebhookEnabled" = TRUE
|
||||
AND "uplinkDeliveryMode" = 'cmpp';
|
||||
|
||||
ALTER TABLE "SmsApplicationHttpConfig"
|
||||
ALTER COLUMN "sendEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "messageQueryEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "receiptWebhookEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "uplinkWebhookEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "uplinkQueryEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "credentialSelfServiceEnabled" SET DEFAULT TRUE,
|
||||
ALTER COLUMN "receiptDeliveryMode" SET DEFAULT 'http',
|
||||
ALTER COLUMN "uplinkDeliveryMode" SET DEFAULT 'http';
|
||||
|
||||
-- Rollback guidance:
|
||||
-- Drop SmsApplicationDailyUsage and its FK/indexes, restore SmsApplication.dailyLimit
|
||||
-- to nullable/no-default, and restore HTTP column defaults to FALSE/'cmpp'. Data migrated
|
||||
-- from cmpp to http is intentionally not auto-reversed because explicit edits after upgrade
|
||||
-- cannot be distinguished safely.
|
||||
@@ -0,0 +1,20 @@
|
||||
-- SMS signature names are canonical business identifiers and must contain
|
||||
-- exactly one complete pair of Chinese black brackets: 【signature】.
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
id,
|
||||
btrim(
|
||||
regexp_replace(
|
||||
regexp_replace(btrim(name), '^[【\[]+', ''),
|
||||
'[】\]]+$',
|
||||
''
|
||||
)
|
||||
) AS inner_name
|
||||
FROM "SmsSignature"
|
||||
)
|
||||
UPDATE "SmsSignature" AS signature
|
||||
SET name = '【' || normalized.inner_name || '】'
|
||||
FROM normalized
|
||||
WHERE signature.id = normalized.id
|
||||
AND normalized.inner_name <> ''
|
||||
AND signature.name IS DISTINCT FROM '【' || normalized.inner_name || '】';
|
||||
@@ -0,0 +1,19 @@
|
||||
-- The previous bracket-normalization migration ran against a legacy SQL_ASCII
|
||||
-- database. PostgreSQL treated the UTF-8 bracket regexp as individual bytes and
|
||||
-- removed the trailing 0x91 byte from the final Chinese character in two known
|
||||
-- signatures. Restore only the rows whose primary key and corrupt byte sequence
|
||||
-- both match the pre-migration backup, so later user edits cannot be overwritten.
|
||||
UPDATE "SmsSignature"
|
||||
SET name = convert_from(
|
||||
decode('e38090e888aae5a4a9e4bfa1e681afe4bfa1e8afbae7bd91e38091', 'hex'),
|
||||
'SQL_ASCII'
|
||||
)
|
||||
WHERE id IN (
|
||||
'cmrj2ci1r00544pnkt64wza4e',
|
||||
'cmrobgqp7002nwenke5wh09fr'
|
||||
)
|
||||
AND encode(convert_to(name, 'SQL_ASCII'), 'hex') =
|
||||
'e38090e888aae5a4a9e4bfa1e681afe4bfa1e8afbae7bde38091';
|
||||
|
||||
-- Rollback guidance: do not restore the invalid byte sequence. If rollback is
|
||||
-- required, keep the repaired UTF-8 values and only roll back application code.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
CREATE TABLE "CmppInboundLongMessage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"groupKey" TEXT NOT NULL,
|
||||
"account" TEXT NOT NULL,
|
||||
"srcId" TEXT,
|
||||
"phoneNumbers" JSONB NOT NULL,
|
||||
"concatReference" INTEGER NOT NULL,
|
||||
"segmentTotal" INTEGER NOT NULL,
|
||||
"msgFmt" INTEGER NOT NULL,
|
||||
"messageId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'collecting',
|
||||
"response" JSONB,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CmppInboundLongMessage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "CmppInboundLongMessageSegment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"groupId" TEXT NOT NULL,
|
||||
"segmentIndex" INTEGER NOT NULL,
|
||||
"sequenceId" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"contentHash" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CmppInboundLongMessageSegment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "CmppInboundLongMessage_messageId_key"
|
||||
ON "CmppInboundLongMessage"("messageId");
|
||||
|
||||
CREATE INDEX "CmppInboundLongMessage_groupKey_status_createdAt_idx"
|
||||
ON "CmppInboundLongMessage"("groupKey", "status", "createdAt");
|
||||
|
||||
CREATE INDEX "CmppInboundLongMessage_applicationId_status_expiresAt_idx"
|
||||
ON "CmppInboundLongMessage"("applicationId", "status", "expiresAt");
|
||||
|
||||
CREATE UNIQUE INDEX "CmppInboundLongMessageSegment_groupId_segmentIndex_key"
|
||||
ON "CmppInboundLongMessageSegment"("groupId", "segmentIndex");
|
||||
|
||||
CREATE INDEX "CmppInboundLongMessageSegment_sequenceId_idx"
|
||||
ON "CmppInboundLongMessageSegment"("sequenceId");
|
||||
|
||||
ALTER TABLE "CmppInboundLongMessage"
|
||||
ADD CONSTRAINT "CmppInboundLongMessage_tenantId_fkey"
|
||||
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "CmppInboundLongMessage"
|
||||
ADD CONSTRAINT "CmppInboundLongMessage_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "CmppInboundLongMessageSegment"
|
||||
ADD CONSTRAINT "CmppInboundLongMessageSegment_groupId_fkey"
|
||||
FOREIGN KEY ("groupId") REFERENCES "CmppInboundLongMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE "CmppConnectionState"
|
||||
ADD COLUMN "lastReconnectAttemptAt" TIMESTAMP(3),
|
||||
ADD COLUMN "nextReconnectAt" TIMESTAMP(3),
|
||||
ADD COLUMN "lastErrorCategory" TEXT;
|
||||
|
||||
CREATE INDEX "CmppConnectionState_status_nextReconnectAt_idx"
|
||||
ON "CmppConnectionState"("status", "nextReconnectAt");
|
||||
|
||||
-- Rollback:
|
||||
-- DROP INDEX "CmppConnectionState_status_nextReconnectAt_idx";
|
||||
-- ALTER TABLE "CmppConnectionState"
|
||||
-- DROP COLUMN "lastErrorCategory",
|
||||
-- DROP COLUMN "nextReconnectAt",
|
||||
-- DROP COLUMN "lastReconnectAttemptAt";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "SmsApplication"
|
||||
ALTER COLUMN "maxPhonesPerTask" SET DEFAULT 10000;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
ALTER TABLE "DailyReconciliationReport"
|
||||
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "DailyProfitReport"
|
||||
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "DailyQualityReport"
|
||||
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE "DailyReconciliationReport"
|
||||
SET "submittedUnits" = "sentUnits",
|
||||
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
|
||||
|
||||
UPDATE "DailyProfitReport"
|
||||
SET "submittedUnits" = "sentUnits",
|
||||
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
|
||||
|
||||
UPDATE "DailyQualityReport"
|
||||
SET "submittedUnits" = "sentUnits",
|
||||
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "channelId", "connectionId"
|
||||
ORDER BY "updatedAt" DESC, "createdAt" DESC, id DESC
|
||||
) AS row_no
|
||||
FROM "CmppConnectionState"
|
||||
WHERE "applicationId" IS NULL
|
||||
)
|
||||
DELETE FROM "CmppConnectionState" state
|
||||
USING ranked
|
||||
WHERE state.id = ranked.id
|
||||
AND ranked.row_no > 1;
|
||||
|
||||
CREATE UNIQUE INDEX "CmppConnectionState_supplier_channel_connection_key"
|
||||
ON "CmppConnectionState"("channelId", "connectionId")
|
||||
WHERE "applicationId" IS NULL;
|
||||
|
||||
-- Rollback:
|
||||
-- DROP INDEX "CmppConnectionState_supplier_channel_connection_key";
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE "ProtocolInteractionLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"protocol" TEXT NOT NULL,
|
||||
"direction" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"account" TEXT,
|
||||
"messageId" TEXT,
|
||||
"gatewayMessageId" TEXT,
|
||||
"traceId" TEXT,
|
||||
"requestId" TEXT,
|
||||
"phoneMasked" TEXT,
|
||||
"resultCode" TEXT,
|
||||
"durationMs" INTEGER,
|
||||
"payloadBytes" INTEGER,
|
||||
"retryCount" INTEGER,
|
||||
"detail" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ProtocolInteractionLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "ProtocolInteractionLog_createdAt_idx" ON "ProtocolInteractionLog"("createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_protocol_direction_createdAt_idx" ON "ProtocolInteractionLog"("protocol", "direction", "createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_eventType_createdAt_idx" ON "ProtocolInteractionLog"("eventType", "createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_status_createdAt_idx" ON "ProtocolInteractionLog"("status", "createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_channelId_createdAt_idx" ON "ProtocolInteractionLog"("channelId", "createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_applicationId_createdAt_idx" ON "ProtocolInteractionLog"("applicationId", "createdAt");
|
||||
CREATE INDEX "ProtocolInteractionLog_messageId_idx" ON "ProtocolInteractionLog"("messageId");
|
||||
CREATE INDEX "ProtocolInteractionLog_gatewayMessageId_idx" ON "ProtocolInteractionLog"("gatewayMessageId");
|
||||
CREATE INDEX "ProtocolInteractionLog_requestId_idx" ON "ProtocolInteractionLog"("requestId");
|
||||
@@ -0,0 +1,17 @@
|
||||
UPDATE "SmsApplicationHttpConfig" config
|
||||
SET
|
||||
"receiptDeliveryMode" = CASE
|
||||
WHEN application."interfaceEnabled" AND config.enabled THEN 'both'
|
||||
WHEN application."interfaceEnabled" THEN 'cmpp'
|
||||
WHEN config.enabled THEN 'http'
|
||||
ELSE 'none'
|
||||
END,
|
||||
"uplinkDeliveryMode" = CASE
|
||||
WHEN application."interfaceEnabled" AND config.enabled THEN 'both'
|
||||
WHEN application."interfaceEnabled" THEN 'cmpp'
|
||||
WHEN config.enabled THEN 'http'
|
||||
ELSE 'none'
|
||||
END,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
FROM "SmsApplication" application
|
||||
WHERE application.id = config."applicationId";
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
CREATE TABLE "CmppDownstreamDeliveryAttempt" (
|
||||
"id" TEXT NOT NULL,
|
||||
"deliveryId" TEXT NOT NULL,
|
||||
"attemptKey" TEXT NOT NULL,
|
||||
"attemptNo" INTEGER NOT NULL,
|
||||
"connectionId" TEXT,
|
||||
"sequenceId" TEXT,
|
||||
"messageId" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"ackDeadlineAt" TIMESTAMP(3),
|
||||
"acknowledgedAt" TIMESTAMP(3),
|
||||
"ackResult" INTEGER,
|
||||
"failureType" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CmppDownstreamDeliveryAttempt_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "UpstreamReceiptInbox" (
|
||||
"id" TEXT NOT NULL,
|
||||
"receiptKey" TEXT NOT NULL,
|
||||
"incomingChannelId" TEXT NOT NULL,
|
||||
"incomingConnectionId" TEXT,
|
||||
"upstreamAccount" TEXT NOT NULL,
|
||||
"upstreamHost" TEXT NOT NULL,
|
||||
"upstreamPort" INTEGER NOT NULL,
|
||||
"protocol" TEXT NOT NULL,
|
||||
"protocolVersion" TEXT NOT NULL,
|
||||
"provisionalMessageId" TEXT,
|
||||
"sequenceId" INTEGER,
|
||||
"gatewayMessageId" TEXT NOT NULL,
|
||||
"phoneNumber" TEXT,
|
||||
"receiptStatus" TEXT NOT NULL,
|
||||
"rawStatus" TEXT NOT NULL,
|
||||
"errorCode" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"deliveredAt" TIMESTAMP(3) NOT NULL,
|
||||
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"matchedMessageRecordId" TEXT,
|
||||
"matchedSubmitRecordId" TEXT,
|
||||
"matchedChannelId" TEXT,
|
||||
"attemptCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextRetryAt" TIMESTAMP(3),
|
||||
"lastError" TEXT,
|
||||
"processedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UpstreamReceiptInbox_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "CmppDownstreamDeliveryAttempt_attemptKey_key"
|
||||
ON "CmppDownstreamDeliveryAttempt"("attemptKey");
|
||||
|
||||
CREATE INDEX "CmppDownstreamDeliveryAttempt_deliveryId_attemptNo_idx"
|
||||
ON "CmppDownstreamDeliveryAttempt"("deliveryId", "attemptNo");
|
||||
|
||||
CREATE INDEX "CmppDownstreamDeliveryAttempt_status_ackDeadlineAt_idx"
|
||||
ON "CmppDownstreamDeliveryAttempt"("status", "ackDeadlineAt");
|
||||
|
||||
CREATE INDEX "CmppDownstreamDeliveryAttempt_connectionId_sequenceId_idx"
|
||||
ON "CmppDownstreamDeliveryAttempt"("connectionId", "sequenceId");
|
||||
|
||||
CREATE UNIQUE INDEX "UpstreamReceiptInbox_receiptKey_key"
|
||||
ON "UpstreamReceiptInbox"("receiptKey");
|
||||
|
||||
CREATE INDEX "UpstreamReceiptInbox_status_nextRetryAt_receivedAt_idx"
|
||||
ON "UpstreamReceiptInbox"("status", "nextRetryAt", "receivedAt");
|
||||
|
||||
CREATE INDEX "UpstreamReceiptInbox_gatewayMessageId_phoneNumber_idx"
|
||||
ON "UpstreamReceiptInbox"("gatewayMessageId", "phoneNumber");
|
||||
|
||||
CREATE INDEX "UpstreamReceiptInbox_incomingChannelId_receivedAt_idx"
|
||||
ON "UpstreamReceiptInbox"("incomingChannelId", "receivedAt");
|
||||
|
||||
CREATE INDEX "UpstreamReceiptInbox_matchedMessageRecordId_idx"
|
||||
ON "UpstreamReceiptInbox"("matchedMessageRecordId");
|
||||
|
||||
ALTER TABLE "CmppDownstreamDeliveryAttempt"
|
||||
ADD CONSTRAINT "CmppDownstreamDeliveryAttempt_deliveryId_fkey"
|
||||
FOREIGN KEY ("deliveryId") REFERENCES "CmppDownstreamDelivery"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,15 @@
|
||||
DROP INDEX IF EXISTS "User_username_key";
|
||||
DROP INDEX IF EXISTS "User_email_key";
|
||||
DROP INDEX IF EXISTS "User_phone_key";
|
||||
|
||||
CREATE UNIQUE INDEX "User_active_username_key"
|
||||
ON "User" ("username")
|
||||
WHERE "deletedAt" IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "User_active_email_key"
|
||||
ON "User" ("email")
|
||||
WHERE "deletedAt" IS NULL AND "email" IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "User_active_phone_key"
|
||||
ON "User" ("phone")
|
||||
WHERE "deletedAt" IS NULL AND "phone" IS NOT NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE "SmsApplication"
|
||||
ADD COLUMN "disablingAt" TIMESTAMP(3),
|
||||
ADD COLUMN "autoDisableAt" TIMESTAMP(3),
|
||||
ADD COLUMN "disableReason" TEXT;
|
||||
|
||||
CREATE INDEX "SmsApplication_status_autoDisableAt_idx"
|
||||
ON "SmsApplication"("status", "autoDisableAt");
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Risk thresholds now inherit from global rules and may be overridden per application.
|
||||
-- Existing maxPhonesPerTask values are intentionally discarded because this environment
|
||||
-- contains test-only application data and the product owner explicitly declined migration.
|
||||
ALTER TABLE "RiskRule" ADD COLUMN "applicationId" TEXT;
|
||||
|
||||
-- Retain historical rules and hit records for audit, but remove them from the
|
||||
-- effective rule set and configuration page.
|
||||
UPDATE "RiskRule"
|
||||
SET "status" = 'deleted'
|
||||
WHERE "code" IN (
|
||||
'DUPLICATE_PHONE_RATIO',
|
||||
'ILLEGAL_PHONE_RATIO',
|
||||
'BLACKLIST_HIT_RATIO',
|
||||
'TEMPLATE_VARIABLE_ANOMALY'
|
||||
);
|
||||
|
||||
UPDATE "RiskRule"
|
||||
SET "status" = 'deleted'
|
||||
WHERE "tenantId" IS NOT NULL AND "applicationId" IS NULL;
|
||||
|
||||
DROP INDEX IF EXISTS "RiskRule_tenantId_code_key";
|
||||
DROP INDEX IF EXISTS "RiskRule_tenantId_status_priority_idx";
|
||||
|
||||
CREATE UNIQUE INDEX "RiskRule_applicationId_code_key"
|
||||
ON "RiskRule"("applicationId", "code");
|
||||
|
||||
CREATE UNIQUE INDEX "RiskRule_global_code_key"
|
||||
ON "RiskRule"("code")
|
||||
WHERE "applicationId" IS NULL AND "status" <> 'deleted';
|
||||
|
||||
CREATE INDEX "RiskRule_tenantId_applicationId_status_priority_idx"
|
||||
ON "RiskRule"("tenantId", "applicationId", "status", "priority");
|
||||
|
||||
ALTER TABLE "RiskRule"
|
||||
ADD CONSTRAINT "RiskRule_applicationId_fkey"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "SmsApplication" DROP COLUMN "maxPhonesPerTask";
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ADD COLUMN "retryOfSubmitRecordId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "SmsSubmitRecord_retryOfSubmitRecordId_key"
|
||||
ON "SmsSubmitRecord"("retryOfSubmitRecordId");
|
||||
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ADD CONSTRAINT "SmsSubmitRecord_retryOfSubmitRecordId_fkey"
|
||||
FOREIGN KEY ("retryOfSubmitRecordId") REFERENCES "SmsSubmitRecord"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AccountTransaction"
|
||||
ADD COLUMN "idempotencyKey" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "AccountTransaction_idempotencyKey_key"
|
||||
ON "AccountTransaction"("idempotencyKey");
|
||||
|
||||
ALTER TABLE "CmppDownstreamDelivery"
|
||||
ADD COLUMN "dedupeKey" TEXT;
|
||||
|
||||
WITH ranked_receipts AS (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "messageRecordId", "deliveryType"
|
||||
ORDER BY "createdAt", id
|
||||
) AS row_number
|
||||
FROM "CmppDownstreamDelivery"
|
||||
WHERE "messageRecordId" IS NOT NULL
|
||||
AND "deliveryType" = 'receipt'
|
||||
)
|
||||
UPDATE "CmppDownstreamDelivery" AS delivery
|
||||
SET "dedupeKey" = 'receipt:' || delivery."messageRecordId"
|
||||
FROM ranked_receipts
|
||||
WHERE delivery.id = ranked_receipts.id
|
||||
AND ranked_receipts.row_number = 1;
|
||||
|
||||
CREATE UNIQUE INDEX "CmppDownstreamDelivery_dedupeKey_key"
|
||||
ON "CmppDownstreamDelivery"("dedupeKey");
|
||||
@@ -0,0 +1,37 @@
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ADD COLUMN "channelGroupId" TEXT,
|
||||
ADD COLUMN "channelGroupName" TEXT;
|
||||
|
||||
ALTER TABLE "SmsSubmitRecord"
|
||||
ADD CONSTRAINT "SmsSubmitRecord_channelGroupId_fkey"
|
||||
FOREIGN KEY ("channelGroupId") REFERENCES "SmsChannelGroup"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "SmsSubmitRecord_channelGroupId_idx"
|
||||
ON "SmsSubmitRecord"("channelGroupId");
|
||||
|
||||
UPDATE "SmsSubmitRecord" submit
|
||||
SET
|
||||
"channelGroupId" = matched."groupId",
|
||||
"channelGroupName" = matched."groupName"
|
||||
FROM (
|
||||
SELECT DISTINCT ON (submit_record.id)
|
||||
submit_record.id,
|
||||
route."groupId",
|
||||
channel_group.name AS "groupName"
|
||||
FROM "SmsSubmitRecord" submit_record
|
||||
JOIN "SmsMessageRecord" message ON message.id = submit_record."messageRecordId"
|
||||
JOIN "ChannelRouteRule" route
|
||||
ON route."tenantId" = message."tenantId"
|
||||
AND route."applicationId" = message."applicationId"
|
||||
AND route.carrier = message.carrier
|
||||
AND route."channelId" IS NULL
|
||||
AND route.province IS NULL
|
||||
JOIN "SmsChannelGroupItem" group_item
|
||||
ON group_item."groupId" = route."groupId"
|
||||
AND group_item."channelId" = submit_record."channelId"
|
||||
JOIN "SmsChannelGroup" channel_group ON channel_group.id = route."groupId"
|
||||
WHERE route.status = 'active'
|
||||
ORDER BY submit_record.id, route.priority ASC, route."createdAt" ASC
|
||||
) matched
|
||||
WHERE submit.id = matched.id;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
ALTER TABLE "ReportMaterialImportBatch"
|
||||
ADD COLUMN "reviewedById" TEXT,
|
||||
ADD COLUMN "reviewedAt" TIMESTAMP(3);
|
||||
|
||||
CREATE TABLE "ReportMaterialImportItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"batchId" TEXT NOT NULL,
|
||||
"rowNumber" INTEGER NOT NULL,
|
||||
"reportType" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"targetId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending_review',
|
||||
"payload" JSONB NOT NULL,
|
||||
"originalSnapshot" JSONB,
|
||||
"errorMessage" TEXT,
|
||||
"reviewReason" TEXT,
|
||||
"reviewedById" TEXT,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ReportMaterialImportItem_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "ReportMaterialImportItem_batchId_fkey"
|
||||
FOREIGN KEY ("batchId") REFERENCES "ReportMaterialImportBatch"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "ReportMaterialImportItem_batchId_rowNumber_key"
|
||||
ON "ReportMaterialImportItem"("batchId", "rowNumber");
|
||||
CREATE INDEX "ReportMaterialImportItem_batchId_status_idx"
|
||||
ON "ReportMaterialImportItem"("batchId", "status");
|
||||
CREATE INDEX "ReportMaterialImportItem_reportType_status_createdAt_idx"
|
||||
ON "ReportMaterialImportItem"("reportType", "status", "createdAt");
|
||||
CREATE INDEX "ReportMaterialImportItem_targetId_idx"
|
||||
ON "ReportMaterialImportItem"("targetId");
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE INDEX "RechargeOrder_payMethod_createdAt_idx" ON "RechargeOrder"("payMethod", "createdAt");
|
||||
CREATE INDEX "SmsApplication_status_createdAt_idx" ON "SmsApplication"("status", "createdAt");
|
||||
CREATE INDEX "SmsSignature_tenantId_updatedAt_idx" ON "SmsSignature"("tenantId", "updatedAt");
|
||||
CREATE INDEX "SmsTemplate_tenantId_auditStatus_createdAt_idx" ON "SmsTemplate"("tenantId", "auditStatus", "createdAt");
|
||||
CREATE INDEX "SmsChannel_status_createdAt_idx" ON "SmsChannel"("status", "createdAt");
|
||||
CREATE INDEX "ChannelSignatureReportTask_status_createdAt_idx" ON "ChannelSignatureReportTask"("status", "createdAt");
|
||||
CREATE INDEX "ChannelSignatureReportRecord_createdAt_idx" ON "ChannelSignatureReportRecord"("createdAt");
|
||||
CREATE INDEX "SmsMessageRecord_queuedAt_idx" ON "SmsMessageRecord"("queuedAt");
|
||||
CREATE INDEX "SmsUplinkMessage_receivedAt_idx" ON "SmsUplinkMessage"("receivedAt");
|
||||
@@ -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;
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE "DrainageDetectionRule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"pattern" TEXT NOT NULL,
|
||||
"flags" TEXT NOT NULL DEFAULT 'giu',
|
||||
"priority" INTEGER NOT NULL DEFAULT 100,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"description" TEXT,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "DrainageDetectionRule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "DrainageDetectionRule_code_key" ON "DrainageDetectionRule"("code");
|
||||
CREATE INDEX "DrainageDetectionRule_status_priority_idx" ON "DrainageDetectionRule"("status", "priority");
|
||||
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "hasDrainageContent" BOOLEAN,
|
||||
ADD COLUMN "drainageDetection" JSONB,
|
||||
ADD COLUMN "drainageDetectionVersion" TEXT,
|
||||
ADD COLUMN "drainageEvaluatedAt" TIMESTAMP(3);
|
||||
|
||||
CREATE INDEX "SmsMessageRecord_hasDrainageContent_queuedAt_idx"
|
||||
ON "SmsMessageRecord"("hasDrainageContent", "queuedAt");
|
||||
|
||||
INSERT INTO "DrainageDetectionRule"
|
||||
("id", "code", "name", "category", "pattern", "flags", "priority", "status", "description", "version", "updatedAt")
|
||||
VALUES
|
||||
('drainage-rule-url', 'URL', 'URL及裸域名', 'url', $regex$(?:https?:\/\/)?(?:www\.)?(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:\/[^\s,,;;!!??<>《》]*)?$regex$, 'giu', 10, 'active', '识别协议链接、裸域名、短链接及IP地址链接;邮箱区间由检测器排除', 1, CURRENT_TIMESTAMP),
|
||||
('drainage-rule-mobile', 'MOBILE', '手机号码', 'mobile', $regex$(?:^|[^0-9])((?:\+?86)?1[3-9][0-9]{9})(?:$|[^0-9])$regex$, 'giu', 20, 'active', '规范化后识别+86、空格、短横线及中文标点拆分手机号', 1, CURRENT_TIMESTAMP),
|
||||
('drainage-rule-landline', 'LANDLINE', '固定电话号码', 'landline', $regex$(?:^|[^0-9])((?:\+?86)?(?:\(0[0-9]{2,3}\)|0[0-9]{2,3})-?[0-9]{7,8}(?:(?:转|分机|ext)[0-9]{1,6})?)(?:$|[^0-9])$regex$, 'giu', 30, 'active', '识别区号括号、分隔符和分机号', 1, CURRENT_TIMESTAMP);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
-- PostgreSQL standard-conforming strings preserve backslashes literally. The initial
|
||||
-- seed used JavaScript-style escaping, so already-migrated databases need their three
|
||||
-- built-in patterns normalized to the single backslashes expected by RegExp.
|
||||
UPDATE "DrainageDetectionRule"
|
||||
SET
|
||||
"pattern" = $regex$(?:https?:\/\/)?(?:www\.)?(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:\/[^\s,,;;!!??<>《》]*)?$regex$,
|
||||
"version" = "version" + 1,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "code" = 'URL';
|
||||
|
||||
UPDATE "DrainageDetectionRule"
|
||||
SET
|
||||
"pattern" = $regex$(?:^|[^0-9])((?:\+?86)?1[3-9][0-9]{9})(?:$|[^0-9])$regex$,
|
||||
"version" = "version" + 1,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "code" = 'MOBILE';
|
||||
|
||||
UPDATE "DrainageDetectionRule"
|
||||
SET
|
||||
"pattern" = $regex$(?:^|[^0-9])((?:\+?86)?(?:\(0[0-9]{2,3}\)|0[0-9]{2,3})-?[0-9]{7,8}(?:(?:转|分机|ext)[0-9]{1,6})?)(?:$|[^0-9])$regex$,
|
||||
"version" = "version" + 1,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "code" = 'LANDLINE';
|
||||
@@ -0,0 +1,12 @@
|
||||
ALTER TABLE "SmsMessageRecord"
|
||||
ADD COLUMN "cmppRegisteredDelivery" BOOLEAN,
|
||||
ADD COLUMN "timeoutReceiptQueuedAt" TIMESTAMP(3);
|
||||
|
||||
ALTER TABLE "CmppInboundLongMessageSegment"
|
||||
ADD COLUMN "registeredDelivery" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- Historical CMPP submissions were accepted before Registered_Delivery was
|
||||
-- persisted. Preserve their existing receipt-enabled behavior.
|
||||
UPDATE "SmsMessageRecord"
|
||||
SET "cmppRegisteredDelivery" = true
|
||||
WHERE "cmppSubmitSequenceId" IS NOT NULL;
|
||||
@@ -0,0 +1,43 @@
|
||||
CREATE TABLE "SmsReceiptAnomaly" (
|
||||
"id" TEXT NOT NULL,
|
||||
"anomalyKey" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"applicationId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"messageRecordId" TEXT,
|
||||
"submitRecordId" TEXT,
|
||||
"receiptRecordId" TEXT,
|
||||
"anomalyType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"previousStatus" TEXT,
|
||||
"incomingStatus" TEXT,
|
||||
"rawStatus" TEXT,
|
||||
"errorCode" TEXT,
|
||||
"detail" JSONB,
|
||||
"occurrenceCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"firstOccurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastOccurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"resolutionNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SmsReceiptAnomaly_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SmsReceiptAnomaly_anomalyKey_key" ON "SmsReceiptAnomaly"("anomalyKey");
|
||||
CREATE INDEX "SmsReceiptAnomaly_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("status", "lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_anomalyType_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("anomalyType", "lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_tenantId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("tenantId", "status", "lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_applicationId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("applicationId", "status", "lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_channelId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("channelId", "status", "lastOccurredAt");
|
||||
CREATE INDEX "SmsReceiptAnomaly_messageRecordId_idx" ON "SmsReceiptAnomaly"("messageRecordId");
|
||||
CREATE INDEX "SmsReceiptAnomaly_submitRecordId_idx" ON "SmsReceiptAnomaly"("submitRecordId");
|
||||
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_messageRecordId_fkey" FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_submitRecordId_fkey" FOREIGN KEY ("submitRecordId") REFERENCES "SmsSubmitRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_receiptRecordId_fkey" FOREIGN KEY ("receiptRecordId") REFERENCES "SmsReceiptRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuditModule } from './audit/audit.module';
|
||||
import { ManualOperationAuditMiddleware } from './audit/manual-operation-audit.middleware';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { SessionValidationMiddleware } from './auth/session-validation.middleware';
|
||||
import { RequestContextMiddleware } from './common/request-context.middleware';
|
||||
import { BillingModule } from './billing/billing.module';
|
||||
import { ChannelsModule } from './channels/channels.module';
|
||||
import { CertificationModule } from './certification/certification.module';
|
||||
import { DictionariesModule } from './dictionaries/dictionaries.module';
|
||||
import { DeletionGovernanceModule } from './deletion-governance/deletion-governance.module';
|
||||
import { FilesModule } from './files/files.module';
|
||||
import { HealthController } from './health.controller';
|
||||
import { OperationsModule } from './operations/operations.module';
|
||||
import { OpenApiModule } from './open-api/open-api.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { ProtocolLogsModule } from './protocol-logs/protocol-logs.module';
|
||||
import { RiskReviewModule } from './risk-review/risk-review.module';
|
||||
import { ReportsModule } from './reports/reports.module';
|
||||
import { ReportMaterialsModule } from './report-materials/report-materials.module';
|
||||
import { SendChainModule } from './send-chain/send-chain.module';
|
||||
import { SmsConfigModule } from './sms-config/sms-config.module';
|
||||
import { TenantsModule } from './tenants/tenants.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
}),
|
||||
PrismaModule,
|
||||
ProtocolLogsModule,
|
||||
AuthModule,
|
||||
TenantsModule,
|
||||
UsersModule,
|
||||
AuditModule,
|
||||
FilesModule,
|
||||
DictionariesModule,
|
||||
DeletionGovernanceModule,
|
||||
BillingModule,
|
||||
CertificationModule,
|
||||
SmsConfigModule,
|
||||
ChannelsModule,
|
||||
RiskReviewModule,
|
||||
ReportsModule,
|
||||
ReportMaterialsModule,
|
||||
SendChainModule,
|
||||
OperationsModule,
|
||||
OpenApiModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [RequestContextMiddleware, SessionValidationMiddleware, ManualOperationAuditMiddleware],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(RequestContextMiddleware, SessionValidationMiddleware, ManualOperationAuditMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { TenantId } from '../common/tenant-id.decorator';
|
||||
import { AuditService, CreateOperationLogDto } from './audit.service';
|
||||
|
||||
@ApiTags('audit')
|
||||
@Controller('admin/operation-logs')
|
||||
export class AuditController {
|
||||
constructor(private readonly audit: AuditService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@TenantId() tenantId?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.audit.list(tenantId, Number(page), Number(pageSize));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateOperationLogDto) {
|
||||
return this.audit.create(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuditController } from './audit.controller';
|
||||
import { AuditService } from './audit.service';
|
||||
import { OperationLogRetentionService } from './operation-log-retention.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AuditController],
|
||||
providers: [AuditService, OperationLogRetentionService],
|
||||
exports: [AuditService],
|
||||
})
|
||||
export class AuditModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
describe('AuditService', () => {
|
||||
it('returns bounded paginated operation logs', async () => {
|
||||
const prisma = {
|
||||
operationLog: {
|
||||
findMany: jest.fn().mockResolvedValue([{ id: 'log-1' }]),
|
||||
count: jest.fn().mockResolvedValue(1),
|
||||
},
|
||||
};
|
||||
const service = new AuditService(prisma as never);
|
||||
|
||||
await expect(service.list('tenant-1', 2, 1_000)).resolves.toEqual({
|
||||
items: [{ id: 'log-1' }],
|
||||
total: 1,
|
||||
page: 2,
|
||||
pageSize: 100,
|
||||
});
|
||||
expect(prisma.operationLog.findMany).toHaveBeenCalledWith({
|
||||
where: { tenantId: 'tenant-1' },
|
||||
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
|
||||
skip: 100,
|
||||
take: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export interface CreateOperationLogDto {
|
||||
tenantId?: string;
|
||||
userId?: string;
|
||||
action: string;
|
||||
resource: string;
|
||||
resourceId?: string;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(tenantId?: string, pageInput?: number, pageSizeInput?: number) {
|
||||
const page = positiveInteger(pageInput, 1);
|
||||
const pageSize = Math.min(100, positiveInteger(pageSizeInput, 20));
|
||||
const where = tenantId ? { tenantId } : undefined;
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.operationLog.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.operationLog.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
create(data: CreateOperationLogDto) {
|
||||
const createData: Prisma.OperationLogUncheckedCreateInput = {
|
||||
tenantId: data.tenantId,
|
||||
userId: data.userId,
|
||||
action: data.action,
|
||||
resource: data.resource,
|
||||
resourceId: data.resourceId,
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
detail: data.detail as Prisma.InputJsonValue | undefined,
|
||||
};
|
||||
return this.prisma.operationLog.create({ data: createData });
|
||||
}
|
||||
}
|
||||
|
||||
function positiveInteger(value: number | undefined, fallback: number) {
|
||||
const normalized = Number(value);
|
||||
return Number.isInteger(normalized) && normalized > 0 ? normalized : fallback;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ManualOperationAuditMiddleware } from './manual-operation-audit.middleware';
|
||||
|
||||
describe('ManualOperationAuditMiddleware', () => {
|
||||
it('records a successful authenticated mutation without storing request bodies', async () => {
|
||||
const prisma = { operationLog: { create: jest.fn().mockResolvedValue({ id: 'log-1' }) } };
|
||||
const middleware = new ManualOperationAuditMiddleware(prisma as never);
|
||||
let finish: (() => void) | undefined;
|
||||
const response = { statusCode: 200, once: (_event: 'finish', listener: () => void) => { finish = listener; } };
|
||||
const next = jest.fn();
|
||||
|
||||
middleware.use({
|
||||
method: 'PUT',
|
||||
originalUrl: '/api/admin/enterprise-applications/c12345678901234567890?view=full',
|
||||
sessionUserId: 'user-1',
|
||||
header: () => 'jest',
|
||||
}, response, next);
|
||||
finish?.();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(prisma.operationLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
userId: 'user-1',
|
||||
action: 'manual_operation.put',
|
||||
resource: 'enterprise-applications',
|
||||
resourceId: 'c12345678901234567890',
|
||||
detail: { method: 'PUT', path: '/api/admin/enterprise-applications/c12345678901234567890', statusCode: 200 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not record unauthenticated, read-only, or failed requests', () => {
|
||||
const prisma = { operationLog: { create: jest.fn() } };
|
||||
const middleware = new ManualOperationAuditMiddleware(prisma as never);
|
||||
const response = { statusCode: 500, once: (_event: 'finish', listener: () => void) => listener() };
|
||||
middleware.use({ method: 'POST', originalUrl: '/api/admin/test', sessionUserId: 'user-1', header: () => undefined }, response, jest.fn());
|
||||
middleware.use({ method: 'GET', originalUrl: '/api/admin/test', sessionUserId: 'user-1', header: () => undefined }, response, jest.fn());
|
||||
middleware.use({ method: 'POST', originalUrl: '/api/admin/test', header: () => undefined }, response, jest.fn());
|
||||
expect(prisma.operationLog.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { requestContext } from '../common/request-context';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
type AuditRequest = {
|
||||
method?: string;
|
||||
originalUrl?: string;
|
||||
url?: string;
|
||||
sessionUserId?: string;
|
||||
header(name: string): string | undefined;
|
||||
};
|
||||
|
||||
type AuditResponse = {
|
||||
statusCode?: number;
|
||||
once(event: 'finish', listener: () => void): void;
|
||||
};
|
||||
|
||||
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
@Injectable()
|
||||
export class ManualOperationAuditMiddleware implements NestMiddleware {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
use(request: AuditRequest, response: AuditResponse, next: () => void) {
|
||||
const method = (request.method ?? '').toUpperCase();
|
||||
const userId = request.sessionUserId;
|
||||
const path = (request.originalUrl ?? request.url ?? '').split('?')[0];
|
||||
if (!userId || !MUTATING_METHODS.has(method)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { resource, resourceId } = operationResource(path);
|
||||
const ipAddress = requestContext.getStore()?.ipAddress;
|
||||
const userAgent = request.header('user-agent');
|
||||
response.once('finish', () => {
|
||||
const statusCode = response.statusCode ?? 200;
|
||||
if (statusCode >= 400) return;
|
||||
void this.prisma.operationLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action: `manual_operation.${method.toLowerCase()}`,
|
||||
resource,
|
||||
resourceId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
detail: { method, path, statusCode },
|
||||
},
|
||||
}).catch(() => undefined);
|
||||
});
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function operationResource(path: string) {
|
||||
const segments = path.split('/').filter(Boolean).filter((segment) => !['api', 'admin', 'client'].includes(segment));
|
||||
const resource = segments[0] ?? 'manual_operation';
|
||||
const resourceId = segments.find((segment, index) => index > 0 && looksLikeResourceId(segment));
|
||||
return { resource, resourceId };
|
||||
}
|
||||
|
||||
function looksLikeResourceId(value: string) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(value) || /^c[a-z0-9]{20,}$/i.test(value);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OperationLogRetentionService } from './operation-log-retention.service';
|
||||
|
||||
describe('OperationLogRetentionService', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
it('archives expired logs in bounded batches using the configured retention window', async () => {
|
||||
process.env.OPERATION_LOG_RETENTION_DAYS = '90';
|
||||
process.env.OPERATION_LOG_ARCHIVE_BATCH_SIZE = '2';
|
||||
process.env.OPERATION_LOG_ARCHIVE_MAX_BATCHES = '3';
|
||||
const prisma = {
|
||||
$executeRaw: jest.fn()
|
||||
.mockResolvedValueOnce(2)
|
||||
.mockResolvedValueOnce(1),
|
||||
};
|
||||
const service = new OperationLogRetentionService(prisma as never);
|
||||
|
||||
await expect(service.archiveExpiredLogs(new Date('2026-07-14T00:00:00.000Z'))).resolves.toEqual({
|
||||
archived: 3,
|
||||
cutoff: new Date('2026-04-15T00:00:00.000Z'),
|
||||
retentionDays: 90,
|
||||
});
|
||||
expect(prisma.$executeRaw).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user