260 lines
9.7 KiB
TypeScript
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());
|