feat: add carrier-aware signature retirement alerts
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
||||
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { SignatureRetirementService } = require('../../api/dist/signature-retirement/signature-retirement.service.js');
|
||||
const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg(process.env.DATABASE_URL ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public'),
|
||||
});
|
||||
const service = new SignatureRetirementService(prisma);
|
||||
const todayKey = dateKey(new Date());
|
||||
const results = [];
|
||||
|
||||
try {
|
||||
for (let offset = 30; offset >= 0; offset -= 1) {
|
||||
const detectionDate = dateKey(new Date(Date.now() - offset * 86_400_000));
|
||||
results.push(await service.runDetection(detectionDate));
|
||||
}
|
||||
const notifications = [];
|
||||
for (let offset = 4; offset >= 0; offset -= 1) {
|
||||
const notificationDate = dateKey(new Date(Date.now() - offset * 86_400_000));
|
||||
const notification = await service.publishNotifications(notificationDate);
|
||||
const detections = await prisma.signatureRetirementDetection.findMany({
|
||||
where: { detectionDate: new Date(`${notificationDate}T00:00:00.000Z`), signatureId: { startsWith: 'qa-retirement-' } },
|
||||
select: { id: true },
|
||||
});
|
||||
await prisma.signatureRetirementMessage.updateMany({
|
||||
where: { detectionId: { in: detections.map((item) => item.id) } },
|
||||
data: { createdAt: new Date(`${notificationDate}T08:00:00+08:00`) },
|
||||
});
|
||||
notifications.push({ notificationDate, ...notification });
|
||||
}
|
||||
const unreported = await service.unreportedSignatures({ date: todayKey, page: 1, pageSize: 10 });
|
||||
const todayMessages = await service.listMessages({ dateFrom: todayKey, dateTo: todayKey, page: 1, pageSize: 10 });
|
||||
const filteredHistory = await service.listMessages({ dateFrom: dateKey(new Date(Date.now() - 4 * 86_400_000)), dateTo: todayKey, tenantId: 'qa-retirement-tenant-b', signatureKeyword: '跨通道', page: 1, pageSize: 10 });
|
||||
console.log(JSON.stringify({
|
||||
todayKey,
|
||||
detectionDays: results.length,
|
||||
detectionRows: results.reduce((sum, item) => sum + item.alerted + item.healthy, 0),
|
||||
today: results.at(-1),
|
||||
notifications,
|
||||
unreported,
|
||||
todayMessages: { total: todayMessages.total, page: todayMessages.page, pageSize: todayMessages.pageSize, items: todayMessages.items.length },
|
||||
filteredHistory: { total: filteredHistory.total, items: filteredHistory.items.length, applicationNames: [...new Set(filteredHistory.items.map((item) => item.applicationName))] },
|
||||
}));
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
function dateKey(value) {
|
||||
return new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(value);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
||||
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg(process.env.DATABASE_URL ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public'),
|
||||
});
|
||||
|
||||
const signatureIds = ['qa-retirement-signature-steady', 'qa-retirement-signature-low', 'qa-retirement-signature-zero', 'qa-retirement-signature-retry', 'qa-retirement-signature-unreported', 'qa-retirement-signature-partial'];
|
||||
const taskIds = ['qa-retirement-task-steady-mobile', 'qa-retirement-task-steady-unicom', 'qa-retirement-task-low-unicom', 'qa-retirement-task-zero-telecom', 'qa-retirement-task-retry-primary', 'qa-retirement-task-retry-backup', 'qa-retirement-task-partial-mobile'];
|
||||
const todayKey = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
|
||||
const todayNoon = new Date(`${todayKey}T12:00:00+08:00`);
|
||||
const approvedAt = new Date(todayNoon.getTime() - 100 * 86_400_000);
|
||||
|
||||
try {
|
||||
await cleanup();
|
||||
|
||||
const tenantA = await prisma.tenant.upsert({
|
||||
where: { code: 'qa-retirement-a' },
|
||||
update: { name: '清退验收企业A', status: 'active', certificationStatus: 'approved' },
|
||||
create: { id: 'qa-retirement-tenant-a', code: 'qa-retirement-a', name: '清退验收企业A', status: 'active', certificationStatus: 'approved' },
|
||||
});
|
||||
const tenantB = await prisma.tenant.upsert({
|
||||
where: { code: 'qa-retirement-b' },
|
||||
update: { name: '清退验收企业B', status: 'active', certificationStatus: 'approved' },
|
||||
create: { id: 'qa-retirement-tenant-b', code: 'qa-retirement-b', name: '清退验收企业B', status: 'active', certificationStatus: 'approved' },
|
||||
});
|
||||
const appA = await prisma.smsApplication.upsert({
|
||||
where: { cmppAccount: 'qa_retirement_a' },
|
||||
update: { tenantId: tenantA.id, name: '清退验收通知应用', status: 'active' },
|
||||
create: { id: 'qa-retirement-app-a', tenantId: tenantA.id, name: '清退验收通知应用', cmppAccount: 'qa_retirement_a', cmppEnterpriseCode: 'QA_RET_A', secretHash: 'local-seed-only', status: 'active' },
|
||||
});
|
||||
const appB = await prisma.smsApplication.upsert({
|
||||
where: { cmppAccount: 'qa_retirement_b' },
|
||||
update: { tenantId: tenantB.id, name: '清退验收营销应用', status: 'active' },
|
||||
create: { id: 'qa-retirement-app-b', tenantId: tenantB.id, name: '清退验收营销应用', cmppAccount: 'qa_retirement_b', cmppEnterpriseCode: 'QA_RET_B', secretHash: 'local-seed-only', status: 'active' },
|
||||
});
|
||||
|
||||
const channelAll = await upsertChannel('qa-retirement-all', '验收通道-华东三网', ['mobile', 'unicom', 'telecom'], 42000n);
|
||||
const channelMu = await upsertChannel('qa-retirement-mu', '验收通道-移动联通', ['mobile', 'unicom'], 39000n);
|
||||
const channelTelecom = await upsertChannel('qa-retirement-telecom', '验收通道-电信专线', ['telecom'], 41000n);
|
||||
|
||||
const steady = await upsertSignature(signatureIds[0], tenantA.id, appA.id, '【验收稳定活跃】');
|
||||
const low = await upsertSignature(signatureIds[1], tenantA.id, appA.id, '【验收低量预警】');
|
||||
const zero = await upsertSignature(signatureIds[2], tenantB.id, appB.id, '【验收零量预警】');
|
||||
const retry = await upsertSignature(signatureIds[3], tenantB.id, appB.id, '【验收跨通道补发】');
|
||||
const unreported = await upsertSignature(signatureIds[4], tenantA.id, appA.id, '【验收完全未报备】', 'waiting_material');
|
||||
const partial = await upsertSignature(signatureIds[5], tenantB.id, appB.id, '【验收仅移动已报备】');
|
||||
|
||||
await createTask(taskIds[0], tenantA.id, steady.id, channelAll.id, 'mobile');
|
||||
await createTask(taskIds[1], tenantA.id, steady.id, channelAll.id, 'unicom');
|
||||
await createTask(taskIds[2], tenantA.id, low.id, channelMu.id, 'unicom');
|
||||
await createTask(taskIds[3], tenantB.id, zero.id, channelTelecom.id, 'telecom');
|
||||
await createTask(taskIds[4], tenantB.id, retry.id, channelAll.id, 'mobile');
|
||||
await createTask(taskIds[5], tenantB.id, retry.id, channelMu.id, 'mobile');
|
||||
await createTask(taskIds[6], tenantB.id, partial.id, channelAll.id, 'mobile');
|
||||
|
||||
await prisma.signatureRetirementRule.upsert({
|
||||
where: { ruleType_targetKey: { ruleType: 'enterprise_global', targetKey: '' } },
|
||||
update: { enabled: true, mobileWindowDays: 30, mobileThreshold: 15, unicomWindowDays: 30, unicomThreshold: 15, telecomWindowDays: 30, telecomThreshold: 15, version: { increment: 1 } },
|
||||
create: { ruleType: 'enterprise_global', targetKey: '', enabled: true, mobileWindowDays: 30, mobileThreshold: 15, unicomWindowDays: 30, unicomThreshold: 15, telecomWindowDays: 30, telecomThreshold: 15 },
|
||||
});
|
||||
await prisma.signatureRetirementRule.upsert({
|
||||
where: { ruleType_targetKey: { ruleType: 'channel_global', targetKey: '' } },
|
||||
update: { enabled: true, mobileWindowDays: 30, mobileThreshold: 15, unicomWindowDays: 30, unicomThreshold: 15, telecomWindowDays: 30, telecomThreshold: 15, version: { increment: 1 } },
|
||||
create: { ruleType: 'channel_global', targetKey: '', enabled: true, mobileWindowDays: 30, mobileThreshold: 15, unicomWindowDays: 30, unicomThreshold: 15, telecomWindowDays: 30, telecomThreshold: 15 },
|
||||
});
|
||||
|
||||
for (let day = 1; day <= 30; day += 1) {
|
||||
await addAcceptedMessage({ key: `steady-${day}`, tenantId: tenantA.id, applicationId: appA.id, signatureId: steady.id, carrier: 'mobile', day, channelId: channelAll.id, delivered: day % 7 !== 0 });
|
||||
}
|
||||
for (const day of [3, 10, 20, 29]) {
|
||||
await addAcceptedMessage({ key: `low-${day}`, tenantId: tenantA.id, applicationId: appA.id, signatureId: low.id, carrier: 'unicom', day, channelId: channelMu.id, delivered: day !== 20 });
|
||||
}
|
||||
for (const day of [2, 6, 10, 14, 18, 22, 26, 29]) {
|
||||
await addRetryMessage({ key: `retry-${day}`, tenantId: tenantB.id, applicationId: appB.id, signatureId: retry.id, day, failedChannelId: channelAll.id, acceptedChannelId: channelMu.id });
|
||||
}
|
||||
for (let index = 1; index <= 6; index += 1) {
|
||||
await addAcceptedMessage({ key: `unreported-${index}`, tenantId: tenantA.id, applicationId: appA.id, signatureId: unreported.id, carrier: 'mobile', day: 0, channelId: channelAll.id, delivered: index <= 5 });
|
||||
}
|
||||
for (let index = 1; index <= 4; index += 1) {
|
||||
await addAcceptedMessage({ key: `partial-telecom-${index}`, tenantId: tenantB.id, applicationId: appB.id, signatureId: partial.id, carrier: 'telecom', day: 0, channelId: channelTelecom.id, delivered: true });
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
todayKey,
|
||||
tenants: 2,
|
||||
applications: 2,
|
||||
channels: 3,
|
||||
signatures: 6,
|
||||
carrierReportTasks: 7,
|
||||
messageRecords: 52,
|
||||
scenarios: ['稳定活跃', '低量预警', '零量预警', '跨通道补发', '完全未报备', '仅其他运营商已报备'],
|
||||
}));
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
const detections = await prisma.signatureRetirementDetection.findMany({ where: { signatureId: { in: signatureIds } }, select: { id: true } });
|
||||
const detectionIds = detections.map((item) => item.id);
|
||||
await prisma.signatureRetirementMessage.deleteMany({ where: { detectionId: { in: detectionIds } } });
|
||||
await prisma.signatureRetirementSuppression.deleteMany({ where: { signatureId: { in: signatureIds } } });
|
||||
await prisma.signatureRetirementDetection.deleteMany({ where: { id: { in: detectionIds } } });
|
||||
await prisma.signatureRetirementCycle.deleteMany({ where: { signatureId: { in: signatureIds } } });
|
||||
const messages = await prisma.smsMessageRecord.findMany({ where: { messageId: { startsWith: 'QA-RETIREMENT-' } }, select: { id: true } });
|
||||
const messageIds = messages.map((item) => item.id);
|
||||
await prisma.smsReceiptRecord.deleteMany({ where: { messageRecordId: { in: messageIds } } });
|
||||
await prisma.smsMessageSegmentAudit.deleteMany({ where: { messageRecordId: { in: messageIds } } });
|
||||
await prisma.smsSubmitRecord.deleteMany({ where: { messageRecordId: { in: messageIds } } });
|
||||
await prisma.smsMessageRecord.deleteMany({ where: { id: { in: messageIds } } });
|
||||
await prisma.channelSignatureReportRecord.deleteMany({ where: { taskId: { in: taskIds } } });
|
||||
await prisma.channelSignatureReportTask.deleteMany({ where: { id: { in: taskIds } } });
|
||||
}
|
||||
|
||||
async function upsertChannel(code, name, carriers, unitPrice) {
|
||||
return prisma.smsChannel.upsert({
|
||||
where: { code },
|
||||
update: { name, carrier: carriers.length === 3 ? 'all' : carriers.length === 1 ? carriers[0] : 'multi', carriers, unitPrice, status: 'disabled' },
|
||||
create: { code, name, carrier: carriers.length === 3 ? 'all' : carriers.length === 1 ? carriers[0] : 'multi', carriers, sendRegion: '全国', protocol: 'CMPP', gatewayHost: '127.0.0.1', gatewayPort: 17890, enterpriseCode: 'QA_LOCAL', account: code, passwordCipher: 'local-seed-only', srcId: '10690000', cmppVersion: '3.0', rateLimitPerSecond: 100, unitPrice, status: 'disabled' },
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertSignature(id, tenantId, applicationId, name, reportStatus = 'approved') {
|
||||
return prisma.smsSignature.upsert({
|
||||
where: { id },
|
||||
update: { tenantId, applicationId, name, auditStatus: 'approved', reportStatus, pendingReport: reportStatus !== 'approved' },
|
||||
create: { id, tenantId, applicationId, name, purpose: '本地签名清退验收', auditStatus: 'approved', reportStatus, pendingReport: reportStatus !== 'approved' },
|
||||
});
|
||||
}
|
||||
|
||||
async function createTask(id, tenantId, signatureId, channelId, carrier) {
|
||||
await prisma.channelSignatureReportTask.create({ data: { id, tenantId, signatureId, channelId, carrier, approvedAt, approvalScope: 'carrier_specific', reportType: 'signature', status: 'approved', reason: '本地签名清退验收数据' } });
|
||||
await prisma.channelSignatureReportRecord.create({ data: { taskId: id, channelId, action: 'local_qa_approved', statusBefore: 'reporting', statusAfter: 'approved', reason: '本地签名清退验收数据', sourceEntry: 'system' } });
|
||||
}
|
||||
|
||||
async function addAcceptedMessage({ key, tenantId, applicationId, signatureId, carrier, day, channelId, delivered }) {
|
||||
const at = new Date(todayNoon.getTime() - day * 86_400_000);
|
||||
const record = await prisma.smsMessageRecord.create({ data: { id: `qa-retirement-message-${key}`, tenantId, applicationId, signatureId, messageId: `QA-RETIREMENT-${key.toUpperCase()}`, phoneNumber: carrier === 'mobile' ? '13800138000' : '18600186000', carrier, content: `【本地验收】${key}`, channelId, status: delivered ? 'delivered' : 'submitted', submitStatus: 'accepted', receiptStatus: delivered ? 'delivered' : 'failed', queuedAt: at, submittedAt: at, deliveredAt: delivered ? at : null } });
|
||||
const submit = await prisma.smsSubmitRecord.create({ data: { id: `qa-retirement-submit-${key}`, tenantId, messageRecordId: record.id, channelId, submitId: `QA-SUBMIT-${key.toUpperCase()}`, gatewayMessageId: `QA-GW-${key.toUpperCase()}`, submitStatus: 'accepted', submittedAt: at, createdAt: at } });
|
||||
await prisma.smsMessageSegmentAudit.create({ data: { id: `qa-retirement-segment-${key}`, tenantId, messageRecordId: record.id, submitRecordId: submit.id, channelId, submitId: submit.submitId, gatewayMessageId: submit.gatewayMessageId, submitStatus: 'accepted', receiptStatus: delivered ? 'delivered' : 'failed', rawStatus: delivered ? 'DELIVRD' : 'UNDELIV', submittedAt: at, deliveredAt: delivered ? at : null, createdAt: at } });
|
||||
}
|
||||
|
||||
async function addRetryMessage({ key, tenantId, applicationId, signatureId, day, failedChannelId, acceptedChannelId }) {
|
||||
const at = new Date(todayNoon.getTime() - day * 86_400_000);
|
||||
const record = await prisma.smsMessageRecord.create({ data: { id: `qa-retirement-message-${key}`, tenantId, applicationId, signatureId, messageId: `QA-RETIREMENT-${key.toUpperCase()}`, phoneNumber: '13800138001', carrier: 'mobile', content: `【本地验收】${key}`, channelId: acceptedChannelId, status: 'delivered', submitStatus: 'accepted', receiptStatus: 'delivered', queuedAt: at, submittedAt: at, deliveredAt: at } });
|
||||
const failed = await prisma.smsSubmitRecord.create({ data: { id: `qa-retirement-submit-${key}-failed`, tenantId, messageRecordId: record.id, channelId: failedChannelId, submitId: `QA-SUBMIT-${key.toUpperCase()}-FAILED`, submitStatus: 'failed', errorCode: 'LOCAL_TIMEOUT', errorMessage: '本地模拟通道超时,仅用于历史统计', submittedAt: at, createdAt: at } });
|
||||
await prisma.smsMessageSegmentAudit.create({ data: { id: `qa-retirement-segment-${key}-failed`, tenantId, messageRecordId: record.id, submitRecordId: failed.id, channelId: failedChannelId, submitId: failed.submitId, submitStatus: 'failed', errorCode: 'LOCAL_TIMEOUT', submittedAt: at, createdAt: at } });
|
||||
const accepted = await prisma.smsSubmitRecord.create({ data: { id: `qa-retirement-submit-${key}-accepted`, tenantId, messageRecordId: record.id, channelId: acceptedChannelId, retryOfSubmitRecordId: failed.id, submitId: `QA-SUBMIT-${key.toUpperCase()}-ACCEPTED`, gatewayMessageId: `QA-GW-${key.toUpperCase()}`, submitStatus: 'accepted', submittedAt: new Date(at.getTime() + 60_000), createdAt: new Date(at.getTime() + 60_000) } });
|
||||
await prisma.smsMessageSegmentAudit.create({ data: { id: `qa-retirement-segment-${key}-accepted`, tenantId, messageRecordId: record.id, submitRecordId: accepted.id, channelId: acceptedChannelId, submitId: accepted.submitId, gatewayMessageId: accepted.gatewayMessageId, submitStatus: 'accepted', receiptStatus: 'delivered', rawStatus: 'DELIVRD', submittedAt: accepted.submittedAt, deliveredAt: new Date(at.getTime() + 120_000), createdAt: accepted.createdAt } });
|
||||
}
|
||||
Reference in New Issue
Block a user