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> = []; const submits: Array> = []; const receipts: Array> = []; 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());