feat: 实现发送质量监控与报备状态消息通知
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-06 19:22:49 +08:00
parent 69e3d7368d
commit 457319e627
66 changed files with 6992 additions and 489 deletions
@@ -0,0 +1,110 @@
CREATE TABLE "ReportReadinessState" (
"objectKey" TEXT PRIMARY KEY, "mask" INTEGER NOT NULL, "armed" BOOLEAN NOT NULL,
"cycle" INTEGER NOT NULL DEFAULT 0, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
);
CREATE TABLE "ReportNotificationHour" (
"id" TEXT PRIMARY KEY, "tenantId" TEXT NOT NULL, "tenantName" TEXT NOT NULL,
"hour" TIMESTAMP(3) NOT NULL, "revision" INTEGER NOT NULL DEFAULT 1,
"signatureCount" INTEGER NOT NULL DEFAULT 0, "drainageCount" INTEGER NOT NULL DEFAULT 0,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
UNIQUE ("tenantId", "hour")
);
CREATE INDEX "ReportNotificationHour_hour_idx" ON "ReportNotificationHour" ("hour" DESC);
CREATE TABLE "ReportReadinessEvent" (
"id" TEXT PRIMARY KEY, "hourId" TEXT NOT NULL REFERENCES "ReportNotificationHour"("id"),
"objectKey" TEXT NOT NULL, "cycle" INTEGER NOT NULL, "tenantId" TEXT NOT NULL,
"reportType" TEXT NOT NULL, "signatureId" TEXT NOT NULL, "drainageItemId" TEXT,
"applicationId" TEXT, "applicationName" TEXT, "signatureName" TEXT NOT NULL,
"targetName" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
UNIQUE ("objectKey", "cycle")
);
CREATE INDEX "ReportReadinessEvent_hour_idx" ON "ReportReadinessEvent" ("hourId", "createdAt", "id");
CREATE TABLE "ReportNotificationRead" (
"userId" TEXT NOT NULL, "hourId" TEXT NOT NULL REFERENCES "ReportNotificationHour"("id"),
"revision" INTEGER NOT NULL, PRIMARY KEY ("userId", "hourId")
);
CREATE FUNCTION cmpp_report_ready_mask(kind TEXT, signature_id TEXT, drainage_id TEXT) RETURNS INTEGER
LANGUAGE sql STABLE AS $$
SELECT COALESCE(sum(bit),0)::integer FROM (VALUES ('mobile',1),('unicom',2),('telecom',4)) AS carriers(name,bit)
WHERE EXISTS (
SELECT 1 FROM "SmsChannel" c
WHERE c.status='active' AND c."sendRegion"='全国'
AND (CASE WHEN cardinality(c.carriers)>0 THEN carriers.name=ANY(c.carriers)
ELSE c.carrier IN (carriers.name,'all') END)
AND (SELECT t.status FROM "ChannelSignatureReportTask" t
WHERE t."channelId"=c.id AND t."signatureId"=signature_id AND t."reportType"=kind
AND (kind='signature' OR t."drainageItemId"=drainage_id)
AND (t.carrier=carriers.name OR (t.carrier IS NULL AND t."approvalScope"='legacy_channel'))
ORDER BY (t.carrier=carriers.name) DESC NULLS LAST, t."updatedAt" DESC, t.id DESC LIMIT 1)='approved'
);
$$;
-- Seed only state. Existing successes must never become historical unread notices.
INSERT INTO "ReportReadinessState" ("objectKey",mask,armed)
SELECT 'signature:'||id, mask, mask=0 FROM "SmsSignature" s
CROSS JOIN LATERAL (SELECT cmpp_report_ready_mask('signature',s.id,NULL) AS mask) m;
INSERT INTO "ReportReadinessState" ("objectKey",mask,armed)
SELECT 'drainage:'||id, mask, mask=0 FROM "SmsDrainageInfo" d
CROSS JOIN LATERAL (SELECT cmpp_report_ready_mask('drainage',d."signatureId",d.id) AS mask) m;
CREATE FUNCTION cmpp_report_readiness_before() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE r "ChannelSignatureReportTask"; k TEXT; m INTEGER;
BEGIN
r := CASE WHEN TG_OP='DELETE' THEN OLD ELSE NEW END;
IF TG_OP='UPDATE' AND (OLD."signatureId",OLD."drainageItemId",OLD."reportType") IS DISTINCT FROM
(NEW."signatureId",NEW."drainageItemId",NEW."reportType") THEN
RAISE EXCEPTION 'Reporting task identity is immutable';
END IF;
k := r."reportType"||':'||CASE WHEN r."reportType"='drainage' THEN r."drainageItemId" ELSE r."signatureId" END;
IF k IS NULL THEN RETURN r; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(k, 20260906));
m := cmpp_report_ready_mask(r."reportType",r."signatureId",r."drainageItemId");
INSERT INTO "ReportReadinessState" ("objectKey",mask,armed) VALUES(k,m,m=0) ON CONFLICT DO NOTHING;
-- A configuration change can remove eligibility without changing a reporting task.
UPDATE "ReportReadinessState" SET mask=m, armed=armed OR m=0 WHERE "objectKey"=k;
RETURN r;
END;
$$;
CREATE FUNCTION cmpp_report_readiness_after() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE r "ChannelSignatureReportTask"; k TEXT; m INTEGER; st "ReportReadinessState";
sig "SmsSignature"; tenant_name TEXT; app_name TEXT; target_name TEXT;
app_id TEXT; hour_value TIMESTAMP(3); hour_id TEXT; next_cycle INTEGER;
BEGIN
r := CASE WHEN TG_OP='DELETE' THEN OLD ELSE NEW END;
k := r."reportType"||':'||CASE WHEN r."reportType"='drainage' THEN r."drainageItemId" ELSE r."signatureId" END;
IF k IS NULL THEN RETURN r; END IF;
SELECT * INTO st FROM "ReportReadinessState" WHERE "objectKey"=k FOR UPDATE;
m := cmpp_report_ready_mask(r."reportType",r."signatureId",r."drainageItemId");
IF m=7 AND st.armed THEN
SELECT * INTO sig FROM "SmsSignature" WHERE id=r."signatureId";
IF sig.id IS NOT NULL THEN
SELECT name INTO tenant_name FROM "Tenant" WHERE id=sig."tenantId";
app_id:=sig."applicationId"; target_name:=sig.name;
IF r."reportType"='drainage' THEN
SELECT COALESCE(d."applicationId",sig."applicationId"), d.url INTO app_id,target_name
FROM "SmsDrainageInfo" d WHERE id=r."drainageItemId";
END IF;
SELECT name INTO app_name FROM "SmsApplication" WHERE id=app_id;
hour_value:=date_trunc('hour',timezone('UTC',statement_timestamp()));
hour_id:=md5(sig."tenantId"||':'||hour_value::text);
next_cycle:=st.cycle+1;
INSERT INTO "ReportNotificationHour" (id,"tenantId","tenantName",hour,"signatureCount","drainageCount")
VALUES(hour_id,sig."tenantId",tenant_name,hour_value,(r."reportType"='signature')::integer,(r."reportType"='drainage')::integer)
ON CONFLICT ("tenantId",hour) DO UPDATE SET revision="ReportNotificationHour".revision+1,
"signatureCount"="ReportNotificationHour"."signatureCount"+EXCLUDED."signatureCount",
"drainageCount"="ReportNotificationHour"."drainageCount"+EXCLUDED."drainageCount",
"updatedAt"=timezone('UTC',statement_timestamp()) RETURNING id INTO hour_id;
INSERT INTO "ReportReadinessEvent" (id,"hourId","objectKey",cycle,"tenantId","reportType","signatureId","drainageItemId","applicationId","applicationName","signatureName","targetName")
VALUES(md5(k||':'||next_cycle),hour_id,k,next_cycle,sig."tenantId",r."reportType",sig.id,r."drainageItemId",app_id,app_name,sig.name,target_name);
UPDATE "ReportReadinessState" SET cycle=next_cycle,armed=false WHERE "objectKey"=k;
END IF;
END IF;
UPDATE "ReportReadinessState" SET mask=m,armed=armed OR m=0,"updatedAt"=timezone('UTC',statement_timestamp()) WHERE "objectKey"=k;
RETURN r;
END;
$$;
CREATE TRIGGER report_readiness_before BEFORE INSERT OR UPDATE OR DELETE ON "ChannelSignatureReportTask"
FOR EACH ROW EXECUTE FUNCTION cmpp_report_readiness_before();
CREATE TRIGGER report_readiness_after AFTER INSERT OR UPDATE OR DELETE ON "ChannelSignatureReportTask"
FOR EACH ROW EXECUTE FUNCTION cmpp_report_readiness_after();