416 lines
24 KiB
JavaScript
416 lines
24 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
|
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
|
|
|
|
const databaseUrl = process.env.DATABASE_URL
|
|
?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public';
|
|
const prisma = new PrismaClient({ adapter: new PrismaPg(databaseUrl) });
|
|
|
|
const PREFIX = 'screenshot-seed-20260822';
|
|
const tenantId = `${PREFIX}-tenant`;
|
|
const applicationIds = [`${PREFIX}-app-notice`, `${PREFIX}-app-marketing`];
|
|
const channelIds = [`${PREFIX}-channel-mobile`, `${PREFIX}-channel-unicom`, `${PREFIX}-channel-telecom`];
|
|
const signatureIds = [`${PREFIX}-signature-service`, `${PREFIX}-signature-member`, `${PREFIX}-signature-cloud`];
|
|
const drainageIds = [`${PREFIX}-drainage-mall`, `${PREFIX}-drainage-event`];
|
|
|
|
const tenantName = '星河智联科技有限公司';
|
|
const applicationNames = ['星河通知中心', '星河会员营销'];
|
|
const channelNames = ['华东移动一号通道', '华北联通优质通道', '全国电信高速通道'];
|
|
const signatureNames = ['【星河服务】', '【星河会员】', '【星河云】'];
|
|
|
|
const now = new Date();
|
|
const dayMs = 86_400_000;
|
|
|
|
function id(suffix) {
|
|
return `${PREFIX}-${suffix}`;
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function dateDaysAgo(days, hour = 10, minute = 0) {
|
|
const value = new Date(now.getTime() - days * dayMs);
|
|
value.setHours(hour, minute, 0, 0);
|
|
return value;
|
|
}
|
|
|
|
function dateKeyDaysAgo(days) {
|
|
const value = dateDaysAgo(days, 0, 0);
|
|
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
|
|
}
|
|
|
|
function reportDate(days) {
|
|
return new Date(`${dateKeyDaysAgo(days)}T00:00:00.000Z`);
|
|
}
|
|
|
|
async function clearPreviousSeed() {
|
|
await prisma.protocolInteractionLog.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.smsMessageRecord.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.smsBatchTask.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.smsSendTask.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.dailyReconciliationReport.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.dailyProfitReport.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
await prisma.dailyQualityReport.deleteMany({ where: { id: { startsWith: PREFIX } } });
|
|
}
|
|
|
|
async function ensureDimensions() {
|
|
await prisma.tenant.upsert({
|
|
where: { code: 'SCREENSHOT-DEMO' },
|
|
update: { name: tenantName, status: 'active', certificationStatus: 'approved' },
|
|
create: { id: tenantId, code: 'SCREENSHOT-DEMO', name: tenantName, status: 'active', certificationStatus: 'approved' },
|
|
});
|
|
|
|
await prisma.tenantAccount.upsert({
|
|
where: { tenantId },
|
|
update: { balanceCents: 268_560_00n, creditCents: 50_000_00n, status: 'active' },
|
|
create: { id: id('tenant-account'), tenantId, balanceCents: 268_560_00n, creditCents: 50_000_00n, status: 'active' },
|
|
});
|
|
|
|
for (let index = 0; index < applicationIds.length; index += 1) {
|
|
await prisma.smsApplication.upsert({
|
|
where: { cmppAccount: `SCREENSHOT_APP_${index + 1}` },
|
|
update: {
|
|
tenantId,
|
|
name: applicationNames[index],
|
|
status: 'active',
|
|
interfaceEnabled: true,
|
|
customerUnitPrice: BigInt(index === 0 ? 8 : 10),
|
|
},
|
|
create: {
|
|
id: applicationIds[index],
|
|
tenantId,
|
|
name: applicationNames[index],
|
|
scene: index === 0 ? '订单、验证码与服务通知' : '会员权益、节日活动与营销触达',
|
|
cmppAccount: `SCREENSHOT_APP_${index + 1}`,
|
|
cmppEnterpriseCode: `SG${String(index + 1).padStart(4, '0')}`,
|
|
cmppApplicationExtension: String(21 + index),
|
|
cmppClientSrcId: `1069008899${index + 1}`,
|
|
secretHash: sha256(`screenshot-local-secret-${index + 1}`),
|
|
interfaceEnabled: true,
|
|
interfaceType: index === 0 ? 'cmpp30' : 'http',
|
|
cmppMaxConnections: 3,
|
|
cmppWindowSize: 32,
|
|
dailyLimit: 500_000,
|
|
customerUnitPrice: BigInt(index === 0 ? 8 : 10),
|
|
queuePriority: index === 0 ? 'high' : 'normal',
|
|
status: 'active',
|
|
},
|
|
});
|
|
}
|
|
|
|
const channelMeta = [
|
|
{ code: 'SCREENSHOT-MOBILE', carrier: 'mobile', carriers: ['mobile'], price: 5 },
|
|
{ code: 'SCREENSHOT-UNICOM', carrier: 'unicom', carriers: ['unicom'], price: 5 },
|
|
{ code: 'SCREENSHOT-TELECOM', carrier: 'telecom', carriers: ['telecom'], price: 6 },
|
|
];
|
|
for (let index = 0; index < channelIds.length; index += 1) {
|
|
const meta = channelMeta[index];
|
|
await prisma.smsChannel.upsert({
|
|
where: { code: meta.code },
|
|
update: { name: channelNames[index], carrier: meta.carrier, carriers: meta.carriers, status: 'inactive', unitPrice: BigInt(meta.price) },
|
|
create: {
|
|
id: channelIds[index], code: meta.code, name: channelNames[index], carrier: meta.carrier,
|
|
carriers: meta.carriers, sendRegion: '全国', protocol: 'CMPP', gatewayHost: '127.0.0.1', gatewayPort: 17890 + index,
|
|
enterpriseCode: `SGCH${index + 1}`, account: `screenshot_channel_${index + 1}`,
|
|
passwordCipher: 'local-screenshot-seed-only', srcId: `10690088${index + 1}`, cmppVersion: '3.0',
|
|
rateLimitPerSecond: 300 + index * 100, unitPrice: BigInt(meta.price), status: 'inactive',
|
|
},
|
|
});
|
|
}
|
|
|
|
for (let index = 0; index < signatureIds.length; index += 1) {
|
|
await prisma.smsSignature.upsert({
|
|
where: { id: signatureIds[index] },
|
|
update: { name: signatureNames[index], auditStatus: 'approved', reportStatus: 'approved', pendingReport: false },
|
|
create: {
|
|
id: signatureIds[index], tenantId, applicationId: applicationIds[index % 2], name: signatureNames[index],
|
|
purpose: index === 1 ? '会员营销通知' : '交易与服务通知', auditStatus: 'approved', reportStatus: 'approved',
|
|
pendingReport: false, reportChangedAt: dateDaysAgo(45), createdAt: dateDaysAgo(60),
|
|
},
|
|
});
|
|
}
|
|
|
|
const drainageMeta = [
|
|
{ siteName: '星河优选商城', url: 'https://mall.example.test/benefits', remark: '会员积分兑换与新品活动页' },
|
|
{ siteName: '星河夏日活动', url: 'https://events.example.test/summer', remark: '夏日专属优惠活动落地页' },
|
|
];
|
|
for (let index = 0; index < drainageIds.length; index += 1) {
|
|
await prisma.smsDrainageInfo.upsert({
|
|
where: { id: drainageIds[index] },
|
|
update: { ...drainageMeta[index], auditStatus: 'approved', pendingReport: false },
|
|
create: {
|
|
id: drainageIds[index], tenantId, signatureId: signatureIds[1], applicationId: applicationIds[1],
|
|
...drainageMeta[index], reportValues: { ICP备案号: '浙ICP备20260088号', 业务类型: '会员权益活动' },
|
|
auditStatus: 'approved', pendingReport: false, reviewedAt: dateDaysAgo(35), submittedAt: dateDaysAgo(40),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function seedSendTasks() {
|
|
const statuses = ['approved', 'pending_review', 'approved', 'rejected', 'approved', 'pending_review'];
|
|
const decisions = ['allow', 'manual_review', 'allow', 'reject', 'allow', 'manual_review'];
|
|
const contents = [
|
|
'【星河服务】您的订单已发货,物流单号已更新,请注意查收。',
|
|
'【星河会员】您的会员积分将于本月底到期,可登录官网查看权益。',
|
|
'【星河云】验证码 726418,5 分钟内有效,请勿告知他人。',
|
|
'【星河会员】夏日优选活动已开启,会员可享限时积分兑换权益。',
|
|
];
|
|
const rows = Array.from({ length: 24 }, (_, index) => {
|
|
const status = statuses[index % statuses.length];
|
|
return {
|
|
id: id(`send-task-${String(index + 1).padStart(3, '0')}`), tenantId,
|
|
applicationId: applicationIds[index % 2], taskNo: `ST-SHOT-${dateKeyDaysAgo(index % 8).replaceAll('-', '')}-${String(index + 1).padStart(4, '0')}`,
|
|
sourceType: index % 3 === 0 ? 'client' : 'risk', content: contents[index % contents.length],
|
|
category: index % 4 === 2 ? '验证码' : index % 2 === 0 ? '行业通知' : '会员营销',
|
|
phoneTotal: 180 + index * 37, uniquePhoneTotal: 176 + index * 35,
|
|
duplicateRatio: Number(((index % 5) * 0.012).toFixed(3)), illegalRatio: index % 7 === 0 ? 0.006 : 0,
|
|
blacklistHitRatio: index % 6 === 0 ? 0.009 : 0, variableIssues: index % 5 === 0 ? { missing: 2, extra: 0 } : null,
|
|
status, riskDecision: decisions[index % decisions.length],
|
|
reviewReason: status === 'pending_review' ? '命中大批量营销内容人工复核阈值' : null,
|
|
rejectReason: status === 'rejected' ? '营销内容缺少有效退订说明' : null,
|
|
reviewedAt: status === 'approved' || status === 'rejected' ? dateDaysAgo(index % 8, 11, index) : null,
|
|
createdAt: dateDaysAgo(index % 8, 9, index), updatedAt: dateDaysAgo(index % 8, 11, index),
|
|
};
|
|
});
|
|
await prisma.smsSendTask.createMany({ data: rows });
|
|
return rows;
|
|
}
|
|
|
|
function messageStatus(index, batchIndex) {
|
|
if (batchIndex === 10) return index < 7 ? 'submitted' : index < 12 ? 'queued' : 'scheduled';
|
|
if (batchIndex === 11) return index < 5 ? 'submit_failed' : index < 11 ? 'failed' : 'rejected';
|
|
const slot = (index * 7 + batchIndex * 3) % 20;
|
|
if (slot < 14) return 'delivered';
|
|
if (slot < 16) return 'submitted';
|
|
if (slot < 18) return 'failed';
|
|
if (slot === 18) return 'timeout';
|
|
return 'rejected';
|
|
}
|
|
|
|
async function seedBatchesAndMessages(sendTasks) {
|
|
const provinces = ['浙江', '江苏', '广东', '北京', '上海', '四川', '湖北', '山东'];
|
|
const carriers = ['mobile', 'unicom', 'telecom'];
|
|
const contents = [
|
|
'【星河服务】您的订单 XH20260822001 已完成支付,感谢您的使用。',
|
|
'【星河服务】您的快递已到达服务站,请凭取件码 8216 及时领取。',
|
|
'【星河云】登录验证码 726418,5 分钟内有效。',
|
|
'【星河会员】本周会员日权益已到账,点击活动页可查看详情。',
|
|
'【星河会员】夏日优选活动进行中,会员专享积分兑换已开启。',
|
|
];
|
|
const batchRows = [];
|
|
const messageRows = [];
|
|
for (let batchIndex = 0; batchIndex < 12; batchIndex += 1) {
|
|
const createdAt = dateDaysAgo(batchIndex % 7, 8 + (batchIndex % 9), batchIndex * 3);
|
|
const statuses = Array.from({ length: 20 }, (_, index) => messageStatus(index, batchIndex));
|
|
const submittedTotal = statuses.filter((value) => !['queued', 'scheduled', 'rejected'].includes(value)).length;
|
|
const successTotal = statuses.filter((value) => value === 'delivered').length;
|
|
const failedTotal = statuses.filter((value) => ['failed', 'submit_failed', 'rejected'].includes(value)).length;
|
|
const timeoutTotal = statuses.filter((value) => value === 'timeout').length;
|
|
const unknownTotal = statuses.filter((value) => value === 'submitted').length;
|
|
const batchStatus = batchIndex < 8 ? 'completed' : batchIndex < 10 ? 'sending' : batchIndex === 10 ? 'scheduled' : 'failed';
|
|
const batchId = id(`batch-${String(batchIndex + 1).padStart(3, '0')}`);
|
|
batchRows.push({
|
|
id: batchId, tenantId, applicationId: applicationIds[batchIndex % 2],
|
|
taskNo: `BT-SHOT-${dateKeyDaysAgo(batchIndex % 7).replaceAll('-', '')}-${String(batchIndex + 1).padStart(4, '0')}`,
|
|
sourceType: batchIndex % 3 === 0 ? 'http' : 'client', content: contents[batchIndex % contents.length],
|
|
category: batchIndex % 2 === 0 ? '行业通知' : '营销通知', phoneTotal: 20, status: batchStatus,
|
|
auditStatus: batchIndex === 11 ? 'rejected' : 'approved', progressTotal: 20, submittedTotal,
|
|
successTotal, failedTotal, unknownTotal, timeoutTotal,
|
|
scheduledAt: batchIndex === 10 ? new Date(now.getTime() + 2 * 60 * 60 * 1000) : null,
|
|
rejectReason: batchIndex === 11 ? '模板变量与受众字段不匹配' : null,
|
|
createdAt, updatedAt: new Date(createdAt.getTime() + 18 * 60 * 1000),
|
|
});
|
|
|
|
for (let index = 0; index < 20; index += 1) {
|
|
const status = statuses[index];
|
|
const carrier = carriers[(index + batchIndex) % carriers.length];
|
|
const queuedAt = new Date(createdAt.getTime() + index * 41_000);
|
|
const submittedAt = ['queued', 'scheduled', 'rejected'].includes(status) ? null : new Date(queuedAt.getTime() + 900 + (index % 5) * 240);
|
|
const deliveredAt = status === 'delivered' ? new Date(submittedAt.getTime() + 1800 + (index % 9) * 720) : null;
|
|
const units = index % 6 === 0 ? 2 : 1;
|
|
const hasDrainage = batchIndex % 2 === 1 && index % 4 === 0;
|
|
const messageContent = hasDrainage
|
|
? `${contents[batchIndex % contents.length]} 活动地址 https://events.example.test/summer`
|
|
: contents[batchIndex % contents.length];
|
|
messageRows.push({
|
|
id: id(`message-${String(batchIndex + 1).padStart(3, '0')}-${String(index + 1).padStart(3, '0')}`),
|
|
tenantId, batchTaskId: batchId, applicationId: applicationIds[batchIndex % 2],
|
|
signatureId: signatureIds[batchIndex % signatureIds.length], drainageInfoId: hasDrainage ? drainageIds[(batchIndex + index) % 2] : null,
|
|
reviewTaskId: sendTasks[(batchIndex * 2 + index) % sendTasks.length].id,
|
|
messageId: `MSG-SHOT-${String(batchIndex + 1).padStart(3, '0')}-${String(index + 1).padStart(4, '0')}`,
|
|
clientMessageId: `CLIENT-SHOT-${String(batchIndex + 1).padStart(3, '0')}-${String(index + 1).padStart(4, '0')}`,
|
|
phoneNumber: `1380013${String(8000 + batchIndex * 20 + index).slice(-4)}`,
|
|
carrier, province: provinces[(index + batchIndex * 2) % provinces.length], content: messageContent,
|
|
hasDrainageContent: hasDrainage,
|
|
drainageDetection: hasDrainage ? { matches: [{ start: messageContent.indexOf('https://'), end: messageContent.length, type: 'url' }], version: 'screenshot-v1' } : { matches: [], version: 'screenshot-v1' },
|
|
drainageDetectionVersion: 'screenshot-v1', drainageEvaluatedAt: queuedAt,
|
|
billingUnits: units, unitPrice: BigInt(batchIndex % 2 === 0 ? 8 : 10), amountCents: BigInt(units * (batchIndex % 2 === 0 ? 8 : 10)),
|
|
queuePriority: batchIndex % 3 === 0 ? 'high' : 'normal', channelId: channelIds[(index + batchIndex) % channelIds.length],
|
|
submitId: submittedAt ? `SUBMIT-SHOT-${batchIndex + 1}-${index + 1}` : null,
|
|
gatewayMessageId: submittedAt ? String(8_600_000_000_000 + batchIndex * 100 + index) : null,
|
|
status, submitStatus: status === 'rejected' ? 'rejected' : submittedAt ? 'accepted' : null,
|
|
receiptStatus: status === 'delivered' ? 'delivered' : status === 'failed' ? 'undelivered' : null,
|
|
receiptRawStatus: status === 'delivered' ? 'DELIVRD' : status === 'failed' ? 'UNDELIV' : null,
|
|
errorCode: status === 'submit_failed' ? 'CMPP-8' : status === 'failed' ? 'YX:0003' : status === 'timeout' ? 'RECEIPT_TIMEOUT' : status === 'rejected' ? 'RISK_REJECTED' : null,
|
|
errorMessage: status === 'submit_failed' ? '供应商通道暂时拒绝' : status === 'failed' ? '号码空号或停机' : status === 'timeout' ? '72 小时未收到最终回执' : status === 'rejected' ? '命中营销风控规则' : null,
|
|
queuedAt, submittedAt, deliveredAt, timeoutAt: status === 'timeout' ? new Date(queuedAt.getTime() + 72 * 60 * 60 * 1000) : null,
|
|
updatedAt: deliveredAt ?? submittedAt ?? queuedAt,
|
|
});
|
|
}
|
|
}
|
|
await prisma.smsBatchTask.createMany({ data: batchRows });
|
|
await prisma.smsMessageRecord.createMany({ data: messageRows });
|
|
return { batchRows, messageRows };
|
|
}
|
|
|
|
async function seedProtocolLogs(messages) {
|
|
const eventTypes = ['submit', 'submit_resp', 'deliver_receipt', 'deliver_resp', 'send_request', 'receipt_webhook', 'connect'];
|
|
const directions = ['client_to_platform', 'platform_to_channel', 'channel_to_platform', 'platform_to_client'];
|
|
const statuses = ['success', 'accepted', 'success', 'received', 'success', 'retrying', 'failed'];
|
|
const rows = Array.from({ length: 180 }, (_, index) => {
|
|
const message = messages[index % messages.length];
|
|
const eventType = eventTypes[index % eventTypes.length];
|
|
const protocol = index % 5 === 0 || index % 5 === 4 ? 'http' : 'cmpp';
|
|
const direction = directions[index % directions.length];
|
|
const status = statuses[index % statuses.length];
|
|
const daysAgo = index < 112 ? 0 : 1 + (index % 6);
|
|
const createdAt = dateDaysAgo(daysAgo, 8 + (index % 12), (index * 7) % 60);
|
|
return {
|
|
id: id(`protocol-${String(index + 1).padStart(4, '0')}`), protocol, direction, eventType, status,
|
|
tenantId, applicationId: message.applicationId, channelId: message.channelId,
|
|
account: protocol === 'http' ? `AK-SHOT-${(index % 2) + 1}` : `SCREENSHOT_APP_${(index % 2) + 1}`,
|
|
messageId: message.messageId, gatewayMessageId: message.gatewayMessageId,
|
|
traceId: `TRACE-SHOT-${String(index + 1).padStart(6, '0')}`,
|
|
requestId: protocol === 'http' ? `REQ-SHOT-${String(index + 1).padStart(6, '0')}` : null,
|
|
phoneNumber: message.phoneNumber, resultCode: status === 'failed' ? (protocol === 'http' ? '429' : '8') : status === 'retrying' ? 'RETRY-1' : '0',
|
|
durationMs: 18 + ((index * 37) % 680), payloadBytes: 96 + ((index * 53) % 1200), retryCount: status === 'retrying' ? 1 + (index % 3) : 0,
|
|
detail: { gateway: `local-gateway-${(index % 2) + 1}`, windowSize: 32, segmentCount: index % 9 === 0 ? 2 : 1, result: status }, createdAt,
|
|
};
|
|
});
|
|
await prisma.protocolInteractionLog.createMany({ data: rows });
|
|
return rows;
|
|
}
|
|
|
|
async function seedReports() {
|
|
const reconciliation = [];
|
|
const profit = [];
|
|
const quality = [];
|
|
for (let days = 1; days <= 30; days += 1) {
|
|
const date = reportDate(days);
|
|
for (let appIndex = 0; appIndex < applicationIds.length; appIndex += 1) {
|
|
const submitted = 8200 + ((days * 977 + appIndex * 2231) % 12_000);
|
|
const failed = 70 + ((days * 41 + appIndex * 67) % 260);
|
|
const unknown = 18 + ((days * 17 + appIndex * 13) % 95);
|
|
const success = submitted - failed - unknown;
|
|
const sent = success + failed + unknown;
|
|
reconciliation.push({
|
|
id: id(`recon-${days}-${appIndex}`), reportDate: date, tenantId, tenantName,
|
|
applicationId: applicationIds[appIndex], applicationName: applicationNames[appIndex],
|
|
submittedUnits: submitted, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
generatedAt: dateDaysAgo(days - 1, 2, 12), updatedAt: dateDaysAgo(days - 1, 2, 12),
|
|
});
|
|
const revenue = BigInt(success * (appIndex === 0 ? 8 : 10));
|
|
const cost = BigInt(success * (appIndex === 0 ? 5 : 6));
|
|
const appProfit = revenue - cost;
|
|
profit.push({
|
|
id: id(`profit-app-${days}-${appIndex}`), reportDate: date, dimensionType: 'application', dimensionId: applicationIds[appIndex], dimensionName: applicationNames[appIndex],
|
|
tenantId, tenantName, applicationId: applicationIds[appIndex], submittedUnits: submitted, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
revenueCents: revenue, refundCents: 0n, costCents: cost, profitCents: appProfit, profitRateBps: Number(appProfit * 10_000n / revenue),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 18), updatedAt: dateDaysAgo(days - 1, 2, 18),
|
|
});
|
|
quality.push({
|
|
id: id(`quality-app-${days}-${appIndex}`), reportDate: date, dimensionType: 'application', dimensionId: applicationIds[appIndex], dimensionName: applicationNames[appIndex],
|
|
tenantId, tenantName, applicationId: applicationIds[appIndex], submittedUnits: submitted, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
successRateBps: Math.round(success * 10_000 / sent), avgArrivalMs: 1800 + ((days * 173 + appIndex * 641) % 5200),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 25), updatedAt: dateDaysAgo(days - 1, 2, 25),
|
|
});
|
|
}
|
|
|
|
for (let channelIndex = 0; channelIndex < channelIds.length; channelIndex += 1) {
|
|
const submitted = 4600 + ((days * 587 + channelIndex * 1871) % 8500);
|
|
const failed = 35 + ((days * 29 + channelIndex * 43) % 180);
|
|
const unknown = 12 + ((days * 11 + channelIndex * 7) % 72);
|
|
const success = submitted - failed - unknown;
|
|
const sent = submitted;
|
|
const revenue = BigInt(success * 9);
|
|
const cost = BigInt(success * (channelIndex === 2 ? 6 : 5));
|
|
const channelProfit = revenue - cost;
|
|
profit.push({
|
|
id: id(`profit-channel-${days}-${channelIndex}`), reportDate: date, dimensionType: 'channel', dimensionId: channelIds[channelIndex], dimensionName: channelNames[channelIndex],
|
|
channelId: channelIds[channelIndex], submittedUnits: submitted, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
revenueCents: revenue, refundCents: 0n, costCents: cost, profitCents: channelProfit, profitRateBps: Number(channelProfit * 10_000n / revenue),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 20), updatedAt: dateDaysAgo(days - 1, 2, 20),
|
|
});
|
|
quality.push({
|
|
id: id(`quality-channel-${days}-${channelIndex}`), reportDate: date, dimensionType: 'channel', dimensionId: channelIds[channelIndex], dimensionName: channelNames[channelIndex],
|
|
channelId: channelIds[channelIndex], submittedUnits: submitted, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
successRateBps: Math.round(success * 10_000 / sent), avgArrivalMs: 1450 + ((days * 137 + channelIndex * 991) % 4800),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 27), updatedAt: dateDaysAgo(days - 1, 2, 27),
|
|
});
|
|
}
|
|
|
|
for (let signatureIndex = 0; signatureIndex < signatureIds.length; signatureIndex += 1) {
|
|
const sent = 3300 + ((days * 431 + signatureIndex * 1201) % 6200);
|
|
const failed = 28 + ((days * 23 + signatureIndex * 31) % 130);
|
|
const unknown = 8 + ((days * 7 + signatureIndex * 5) % 45);
|
|
const success = sent - failed - unknown;
|
|
quality.push({
|
|
id: id(`quality-signature-${days}-${signatureIndex}`), reportDate: date, dimensionType: 'signature', dimensionId: signatureIds[signatureIndex], dimensionName: signatureNames[signatureIndex],
|
|
tenantId, tenantName, applicationId: applicationIds[signatureIndex % 2], signatureId: signatureIds[signatureIndex],
|
|
submittedUnits: sent, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
successRateBps: Math.round(success * 10_000 / sent), avgArrivalMs: 1900 + ((days * 149 + signatureIndex * 733) % 5100),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 29), updatedAt: dateDaysAgo(days - 1, 2, 29),
|
|
});
|
|
}
|
|
|
|
for (let drainageIndex = 0; drainageIndex < drainageIds.length; drainageIndex += 1) {
|
|
const sent = 1250 + ((days * 277 + drainageIndex * 881) % 3600);
|
|
const failed = 18 + ((days * 13 + drainageIndex * 19) % 85);
|
|
const unknown = 5 + ((days * 5 + drainageIndex * 3) % 28);
|
|
const success = sent - failed - unknown;
|
|
quality.push({
|
|
id: id(`quality-drainage-${days}-${drainageIndex}`), reportDate: date, dimensionType: 'drainage', dimensionId: drainageIds[drainageIndex], dimensionName: drainageIndex === 0 ? '星河优选商城' : '星河夏日活动',
|
|
tenantId, tenantName, applicationId: applicationIds[1], signatureId: signatureIds[1], drainageInfoId: drainageIds[drainageIndex],
|
|
submittedUnits: sent, sentUnits: sent, unknownUnits: unknown, successUnits: success, failedUnits: failed,
|
|
successRateBps: Math.round(success * 10_000 / sent), avgArrivalMs: 2300 + ((days * 181 + drainageIndex * 521) % 5800),
|
|
generatedAt: dateDaysAgo(days - 1, 2, 31), updatedAt: dateDaysAgo(days - 1, 2, 31),
|
|
});
|
|
}
|
|
}
|
|
await prisma.dailyReconciliationReport.createMany({ data: reconciliation, skipDuplicates: true });
|
|
await prisma.dailyProfitReport.createMany({ data: profit, skipDuplicates: true });
|
|
await prisma.dailyQualityReport.createMany({ data: quality, skipDuplicates: true });
|
|
return { reconciliation, profit, quality };
|
|
}
|
|
|
|
async function main() {
|
|
await clearPreviousSeed();
|
|
await ensureDimensions();
|
|
const sendTasks = await seedSendTasks();
|
|
const { batchRows, messageRows } = await seedBatchesAndMessages(sendTasks);
|
|
const protocolRows = await seedProtocolLogs(messageRows);
|
|
const reports = await seedReports();
|
|
console.log(JSON.stringify({
|
|
seed: PREFIX,
|
|
tenant: tenantName,
|
|
applications: applicationNames.length,
|
|
channels: channelNames.length,
|
|
sendTasks: sendTasks.length,
|
|
batchTasks: batchRows.length,
|
|
messages: messageRows.length,
|
|
protocolLogs: protocolRows.length,
|
|
reconciliationReports: reports.reconciliation.length,
|
|
profitReports: reports.profit.length,
|
|
qualityReports: reports.quality.length,
|
|
}, null, 2));
|
|
}
|
|
|
|
main()
|
|
.finally(async () => prisma.$disconnect())
|
|
.catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|