feat: complete reporting and filing workflows
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
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());
|
||||
Reference in New Issue
Block a user