Files
lislgosms/api/tools/seed-signature-quality-demo.ts
T

260 lines
9.7 KiB
TypeScript

import { PrismaService } from '../src/prisma/prisma.service';
const databaseUrl = process.env.DATABASE_URL
?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public';
const databaseHost = new URL(databaseUrl).hostname;
if (process.env.NODE_ENV === 'production') {
throw new Error('Refusing to seed signature-quality demo data in production mode');
}
if (process.env.ALLOW_LOCAL_SIGNATURE_QUALITY_DEMO !== 'true') {
throw new Error('Set ALLOW_LOCAL_SIGNATURE_QUALITY_DEMO=true explicitly to seed local demo data');
}
if (!['localhost', '127.0.0.1', '::1'].includes(databaseHost)) {
throw new Error(`Refusing to seed a non-local database host: ${databaseHost}`);
}
const prisma = new PrismaService();
async function main() {
const date = shanghaiDateKey();
const messagePrefix = `LOCAL-SIGSTAT-${date.replaceAll('-', '')}-`;
const existing = await prisma.smsMessageRecord.count({
where: { messageId: { startsWith: messagePrefix } },
});
if (existing > 0) {
console.log(`Local signature-quality demo already exists for ${date}: ${existing} messages`);
return;
}
const tenant = await prisma.tenant.upsert({
where: { code: 'LOCAL-SIG-QUALITY-DEMO' },
update: { name: '本地统计演示企业', status: 'active' },
create: {
id: 'local-signature-quality-tenant',
code: 'LOCAL-SIG-QUALITY-DEMO',
name: '本地统计演示企业',
status: 'active',
certificationStatus: 'approved',
},
});
const application = await prisma.smsApplication.upsert({
where: { cmppAccount: 'LOCAL_SIG_QUALITY_DEMO' },
update: { name: '本地短信统计演示应用', status: 'active', interfaceEnabled: false },
create: {
id: 'local-signature-quality-application',
tenantId: tenant.id,
name: '本地短信统计演示应用',
cmppAccount: 'LOCAL_SIG_QUALITY_DEMO',
cmppEnterpriseCode: 'LOCAL',
secretHash: 'local-demo-not-for-authentication',
interfaceEnabled: false,
status: 'active',
},
});
const channelDefinitions = [
{ id: 'local-signature-quality-channel-fulong', code: 'LOCAL-DEMO-FULONG', name: '本地演示-富泷' },
{ id: 'local-signature-quality-channel-tiebushan', code: 'LOCAL-DEMO-TIEBUSHAN', name: '本地演示-铁布衫' },
{ id: 'local-signature-quality-channel-relay', code: 'LOCAL-DEMO-RELAY', name: '本地演示-行业中转' },
];
const channels = await Promise.all(channelDefinitions.map((channel) => prisma.smsChannel.upsert({
where: { code: channel.code },
update: { name: channel.name, status: 'inactive' },
create: {
...channel,
carrier: null,
gatewayHost: '127.0.0.1',
gatewayPort: 65535,
account: channel.code,
passwordCipher: 'local-demo',
srcId: '10690000',
status: 'inactive',
},
})));
const signatureDefinitions = [
{ id: 'local-signature-quality-signature-property', name: '【本地演示物业】', count: 24 },
{ id: 'local-signature-quality-signature-aerospace', name: '【本地演示航信】', count: 18 },
{ id: 'local-signature-quality-signature-member', name: '【本地会员服务】', count: 12 },
];
const signatures = await Promise.all(signatureDefinitions.map((signature) => prisma.smsSignature.upsert({
where: { id: signature.id },
update: {
name: signature.name,
tenantId: tenant.id,
applicationId: application.id,
auditStatus: 'approved',
},
create: {
id: signature.id,
tenantId: tenant.id,
applicationId: application.id,
name: signature.name,
purpose: '本地数据统计页面演示',
auditStatus: 'approved',
reportStatus: 'approved',
pendingReport: false,
},
})));
const messages: Array<Record<string, unknown>> = [];
const submits: Array<Record<string, unknown>> = [];
const receipts: Array<Record<string, unknown>> = [];
const dayStart = new Date(`${date}T00:00:00+08:00`);
const carriers = ['mobile', 'unicom', 'telecom'];
let sequence = 0;
signatureDefinitions.forEach((definition, signatureIndex) => {
for (let index = 0; index < definition.count; index += 1) {
sequence += 1;
const key = `${messagePrefix}${String(sequence).padStart(3, '0')}`;
const messageRecordId = `local-signature-quality-message-${date}-${sequence}`;
const carrier = carriers[(index + signatureIndex) % carriers.length];
const primaryChannel = channels[(index + signatureIndex) % channels.length];
const retryChannel = channels[(index + signatureIndex + 1) % channels.length];
const queuedAt = new Date(dayStart.getTime() + (8 * 60 + sequence * 7) * 60_000);
const submittedAt = new Date(queuedAt.getTime() + 500);
const isRetry = index % 11 === 0;
const isSubmitFailure = !isRetry && index % 9 === 0;
const isUnknown = !isRetry && !isSubmitFailure && index % 7 === 0;
const isFailure = !isRetry && !isSubmitFailure && !isUnknown && index % 5 === 0;
const deliveredAt = isRetry || (!isSubmitFailure && !isUnknown && !isFailure)
? new Date(submittedAt.getTime() + 1_400 + (index % 8) * 650)
: null;
const finalChannel = isRetry ? retryChannel : primaryChannel;
const messageStatus = isSubmitFailure
? 'submit_failed'
: isUnknown
? 'submitted'
: isFailure
? 'failed'
: 'delivered';
messages.push({
id: messageRecordId,
tenantId: tenant.id,
applicationId: application.id,
signatureId: signatures[signatureIndex].id,
messageId: key,
phoneNumber: `1390000${String(sequence).padStart(4, '0')}`,
carrier,
province: '上海',
content: `${definition.name}本地数据统计页面演示短信`,
channelId: finalChannel.id,
status: messageStatus,
submitStatus: isSubmitFailure ? 'rejected' : 'accepted',
receiptStatus: isFailure ? 'undelivered' : deliveredAt ? 'delivered' : null,
queuedAt,
submittedAt,
deliveredAt,
});
const addAttempt = ({
attempt,
channelId,
submitStatus,
receiptStatus,
attemptSubmittedAt,
attemptDeliveredAt,
}: {
attempt: number;
channelId: string;
submitStatus: string;
receiptStatus?: 'delivered' | 'undelivered';
attemptSubmittedAt: Date;
attemptDeliveredAt?: Date;
}) => {
const submitRecordId = `local-signature-quality-submit-${date}-${sequence}-${attempt}`;
const submitId = `LOCAL-SUB-${date.replaceAll('-', '')}-${sequence}-${attempt}`;
const gatewayMessageId = `LOCAL-GW-${date.replaceAll('-', '')}-${sequence}-${attempt}`;
submits.push({
id: submitRecordId,
tenantId: tenant.id,
messageRecordId,
channelId,
submitId,
gatewayMessageId,
submitStatus,
submittedAt: attemptSubmittedAt,
createdAt: attemptSubmittedAt,
});
if (receiptStatus && attemptDeliveredAt) {
receipts.push({
id: `local-signature-quality-receipt-${date}-${sequence}-${attempt}`,
tenantId: tenant.id,
messageRecordId,
receiptKey: `LOCAL-RECEIPT-${date.replaceAll('-', '')}-${sequence}-${attempt}`,
channelId,
messageId: key,
gatewayMessageId,
phoneNumber: `1390000${String(sequence).padStart(4, '0')}`,
receiptStatus,
rawStatus: receiptStatus === 'delivered' ? 'DELIVRD' : 'UNDELIV',
deliveredAt: attemptDeliveredAt,
createdAt: attemptDeliveredAt,
});
}
};
if (isRetry) {
addAttempt({
attempt: 0,
channelId: primaryChannel.id,
submitStatus: 'accepted',
receiptStatus: 'undelivered',
attemptSubmittedAt: submittedAt,
attemptDeliveredAt: new Date(submittedAt.getTime() + 2_100),
});
const retrySubmittedAt = new Date(submittedAt.getTime() + 2_500);
addAttempt({
attempt: 1,
channelId: retryChannel.id,
submitStatus: 'accepted',
receiptStatus: 'delivered',
attemptSubmittedAt: retrySubmittedAt,
attemptDeliveredAt: deliveredAt ?? new Date(retrySubmittedAt.getTime() + 2_000),
});
} else {
addAttempt({
attempt: 0,
channelId: primaryChannel.id,
submitStatus: isSubmitFailure ? 'rejected' : 'accepted',
receiptStatus: isFailure ? 'undelivered' : deliveredAt ? 'delivered' : undefined,
attemptSubmittedAt: submittedAt,
attemptDeliveredAt: isFailure ? new Date(submittedAt.getTime() + 3_300) : deliveredAt ?? undefined,
});
}
}
});
await prisma.$transaction([
prisma.smsMessageRecord.createMany({ data: messages as never[] }),
prisma.smsSubmitRecord.createMany({ data: submits as never[] }),
prisma.smsReceiptRecord.createMany({ data: receipts as never[] }),
]);
console.log(JSON.stringify({
date,
tenant: tenant.name,
application: application.name,
signatures: signatures.map((signature) => signature.name),
messages: messages.length,
submitAttempts: submits.length,
receipts: receipts.length,
}, null, 2));
}
function shanghaiDateKey(value = new Date()) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(value);
const byType = new Map(parts.map((part) => [part.type, part.value]));
return `${byType.get('year')}-${byType.get('month')}-${byType.get('day')}`;
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());