feat: harden CMPP delivery and platform workflows

This commit is contained in:
hectorzhao
2026-07-20 18:07:29 +08:00
parent 80fb5a8f53
commit f02c33cbb7
61 changed files with 1834 additions and 281 deletions
+56
View File
@@ -0,0 +1,56 @@
import { PrismaService } from '../src/prisma/prisma.service';
const prisma = new PrismaService();
const mode = process.argv[2];
const channelA = 'it-receipt-channel-a';
const channelB = 'it-receipt-channel-b';
const recordA = 'it-receipt-record-a';
const recordB = 'it-receipt-record-b';
async function cleanup() {
await prisma.smsReceiptRecord.deleteMany({ where: { messageRecordId: { in: [recordA, recordB] } } });
await prisma.smsSubmitRecord.deleteMany({ where: { messageRecordId: { in: [recordA, recordB] } } });
await prisma.smsMessageRecord.deleteMany({ where: { id: { in: [recordA, recordB] } } });
await prisma.smsChannel.deleteMany({ where: { id: { in: [channelA, channelB] } } });
}
async function main() {
if (mode === 'seed') {
await cleanup();
await prisma.smsChannel.createMany({ data: [
{ id: channelA, code: 'IT-RECEIPT-A', name: '回执验证通道A', gatewayHost: '127.0.0.1', gatewayPort: 17890, account: 'shared-account', passwordCipher: 'test-password-a', srcId: '10690000' },
{ id: channelB, code: 'IT-RECEIPT-B', name: '回执验证通道B', gatewayHost: '127.0.0.1', gatewayPort: 17891, account: 'shared-account', passwordCipher: 'test-password-b', srcId: '10690001' },
] });
await prisma.smsMessageRecord.createMany({ data: [
{ id: recordA, messageId: 'IT-RECEIPT-MSG-A', phoneNumber: '13800000001', content: '回执验证A', channelId: channelA, submitId: 'IT-RECEIPT-SUB-A', gatewayMessageId: 'SHARED-UPSTREAM-MSG-ID', status: 'submitted', submitStatus: 'accepted', submittedAt: new Date('2026-07-20T08:00:00.000Z') },
{ id: recordB, messageId: 'IT-RECEIPT-MSG-B', phoneNumber: '13800000002', content: '回执验证B', channelId: channelB, submitId: 'IT-RECEIPT-SUB-B', gatewayMessageId: 'SHARED-UPSTREAM-MSG-ID', status: 'submitted', submitStatus: 'accepted', submittedAt: new Date('2026-07-20T08:00:00.000Z') },
] });
await prisma.smsSubmitRecord.createMany({ data: [
{ id: 'it-receipt-submit-a', messageRecordId: recordA, channelId: channelA, submitId: 'IT-RECEIPT-SUB-A', gatewayMessageId: 'SHARED-UPSTREAM-MSG-ID', submitStatus: 'accepted', submittedAt: new Date('2026-07-20T08:00:00.000Z') },
{ id: 'it-receipt-submit-b', messageRecordId: recordB, channelId: channelB, submitId: 'IT-RECEIPT-SUB-B', gatewayMessageId: 'SHARED-UPSTREAM-MSG-ID', submitStatus: 'accepted', submittedAt: new Date('2026-07-20T08:00:00.000Z') },
] });
console.log(JSON.stringify({ ok: true, mode }));
return;
}
if (mode === 'verify') {
const [a, b, receipts] = await Promise.all([
prisma.smsMessageRecord.findUnique({ where: { id: recordA } }),
prisma.smsMessageRecord.findUnique({ where: { id: recordB } }),
prisma.smsReceiptRecord.findMany({ where: { messageRecordId: { in: [recordA, recordB] } } }),
]);
const evidence = { a, b, receiptCount: receipts.length, receipts };
const valid = a?.status === 'submitted' && a.receiptStatus === null
&& b?.status === 'delivered' && b.receiptStatus === 'delivered' && b.channelId === channelB
&& b.receiptRawStatus === 'DELIVRD' && receipts.length === 1 && receipts[0]?.messageRecordId === recordB;
console.log(JSON.stringify({ ok: valid, evidence }, (_key, value) => typeof value === 'bigint' ? value.toString() : value));
await cleanup();
if (!valid) throw new Error('receipt API identity assertion failed');
return;
}
throw new Error('usage: verify-receipt-api.ts seed|verify');
}
void main().finally(() => prisma.$disconnect()).catch((error) => {
console.error(error);
process.exitCode = 1;
});
+82
View File
@@ -0,0 +1,82 @@
import { PrismaService } from '../src/prisma/prisma.service';
import { ReportsService } from '../src/reports/reports.service';
const ids = {
tenant: 'it-report-tenant', application: 'it-report-app', channel: 'it-report-channel',
batch: 'it-report-batch', successMessage: 'it-report-message-success', failedMessage: 'it-report-message-failed',
};
async function main() {
const prisma = new PrismaService();
const reports = new ReportsService(prisma);
const queuedAt = new Date('2026-07-18T02:00:00.000Z');
try {
await prisma.tenant.create({ data: { id: ids.tenant, name: '报表集成验证企业', code: 'IT-REPORT-TENANT' } });
await prisma.smsApplication.create({ data: {
id: ids.application, tenantId: ids.tenant, name: '报表集成验证应用', cmppAccount: 'it-report-account',
cmppEnterpriseCode: 'ITREPORT', secretHash: 'not-a-real-secret',
} });
await prisma.smsChannel.create({ data: {
id: ids.channel, code: 'IT-REPORT-CHANNEL', name: '报表集成验证通道', gatewayHost: '127.0.0.1',
gatewayPort: 17890, account: 'it-report-upstream', passwordCipher: 'not-a-real-password', srcId: '10690000', unitPrice: 200n,
} });
await prisma.smsBatchTask.create({ data: {
id: ids.batch, tenantId: ids.tenant, applicationId: ids.application, taskNo: 'IT-REPORT-BATCH', sourceType: 'api',
content: '报表集成验证', phoneTotal: 2, status: 'finished', createdAt: queuedAt,
} });
const baseMessage = {
tenantId: ids.tenant, batchTaskId: ids.batch, applicationId: ids.application, content: '报表集成验证',
billingUnits: 1, unitPrice: 352n, amountCents: 352n, channelId: ids.channel, submittedAt: queuedAt, queuedAt,
};
await prisma.smsMessageRecord.createMany({ data: [
{ ...baseMessage, id: ids.successMessage, messageId: 'IT-REPORT-MSG-SUCCESS', phoneNumber: '13800000001', submitId: 'IT-REPORT-SUB-SUCCESS', gatewayMessageId: 'IT-REPORT-GW-SUCCESS', status: 'delivered', submitStatus: 'accepted', receiptStatus: 'delivered', receiptRawStatus: 'DELIVRD', deliveredAt: new Date(queuedAt.getTime() + 5_000) },
{ ...baseMessage, id: ids.failedMessage, messageId: 'IT-REPORT-MSG-FAILED', phoneNumber: '13800000002', submitId: 'IT-REPORT-SUB-FAILED', gatewayMessageId: 'IT-REPORT-GW-FAILED', status: 'failed', submitStatus: 'accepted', receiptStatus: 'undelivered', receiptRawStatus: 'UNDELIV', deliveredAt: new Date(queuedAt.getTime() + 8_000) },
] });
await prisma.smsSubmitRecord.createMany({ data: [
{ id: 'it-report-submit-success', tenantId: ids.tenant, batchTaskId: ids.batch, messageRecordId: ids.successMessage, channelId: ids.channel, submitId: 'IT-REPORT-SUB-SUCCESS', gatewayMessageId: 'IT-REPORT-GW-SUCCESS', submitStatus: 'accepted', costUnitPrice: 200n, costAmountCents: 200n, submittedAt: queuedAt },
{ id: 'it-report-submit-failed', tenantId: ids.tenant, batchTaskId: ids.batch, messageRecordId: ids.failedMessage, channelId: ids.channel, submitId: 'IT-REPORT-SUB-FAILED', gatewayMessageId: 'IT-REPORT-GW-FAILED', submitStatus: 'accepted', costUnitPrice: 200n, costAmountCents: 200n, submittedAt: queuedAt },
] });
await prisma.smsReceiptRecord.createMany({ data: [
{ id: 'it-report-receipt-success', receiptKey: 'it-report-receipt-key-success', tenantId: ids.tenant, batchTaskId: ids.batch, messageRecordId: ids.successMessage, channelId: ids.channel, messageId: 'IT-REPORT-MSG-SUCCESS', gatewayMessageId: 'IT-REPORT-GW-SUCCESS', receiptStatus: 'delivered', rawStatus: 'DELIVRD', deliveredAt: new Date(queuedAt.getTime() + 5_000) },
{ id: 'it-report-receipt-failed', receiptKey: 'it-report-receipt-key-failed', tenantId: ids.tenant, batchTaskId: ids.batch, messageRecordId: ids.failedMessage, channelId: ids.channel, messageId: 'IT-REPORT-MSG-FAILED', gatewayMessageId: 'IT-REPORT-GW-FAILED', receiptStatus: 'undelivered', rawStatus: 'UNDELIV', deliveredAt: new Date(queuedAt.getTime() + 8_000) },
] });
await prisma.smsBillingRecord.createMany({ data: [
{ id: 'it-report-billing-success', tenantId: ids.tenant, applicationId: ids.application, taskId: ids.batch, messageId: 'IT-REPORT-MSG-SUCCESS', phoneNumber: '13800000001', contentLength: 8, billingUnits: 1, unitPrice: 352n, amountCents: 352n, billingStatus: 'charged', createdAt: queuedAt },
{ id: 'it-report-billing-refunded', tenantId: ids.tenant, applicationId: ids.application, taskId: ids.batch, messageId: 'IT-REPORT-MSG-FAILED', phoneNumber: '13800000002', contentLength: 8, billingUnits: 1, unitPrice: 352n, amountCents: 352n, billingStatus: 'refunded', createdAt: queuedAt },
] });
await reports.refreshRollingWindow(new Date('2026-07-20T05:00:00.000Z'));
const [reconciliation, applicationProfit, channelProfit, applicationQuality, channelQuality] = await Promise.all([
prisma.dailyReconciliationReport.findUnique({ where: { reportDate_tenantId_applicationId: { reportDate: new Date('2026-07-18'), tenantId: ids.tenant, applicationId: ids.application } } }),
prisma.dailyProfitReport.findUnique({ where: { reportDate_dimensionType_dimensionId: { reportDate: new Date('2026-07-18'), dimensionType: 'application', dimensionId: ids.application } } }),
prisma.dailyProfitReport.findUnique({ where: { reportDate_dimensionType_dimensionId: { reportDate: new Date('2026-07-18'), dimensionType: 'channel', dimensionId: ids.channel } } }),
prisma.dailyQualityReport.findUnique({ where: { reportDate_dimensionType_dimensionId: { reportDate: new Date('2026-07-18'), dimensionType: 'application', dimensionId: ids.application } } }),
prisma.dailyQualityReport.findUnique({ where: { reportDate_dimensionType_dimensionId: { reportDate: new Date('2026-07-18'), dimensionType: 'channel', dimensionId: ids.channel } } }),
]);
const evidence = JSON.parse(JSON.stringify({ reconciliation, applicationProfit, channelProfit, applicationQuality, channelQuality }, (_key, value) => typeof value === 'bigint' ? value.toString() : value));
const valid = reconciliation?.sentUnits === 2 && reconciliation.successUnits === 1 && reconciliation.failedUnits === 1
&& applicationProfit?.revenueCents === 352n && applicationProfit.refundCents === 352n && applicationProfit.costCents === 400n && applicationProfit.profitCents === -48n
&& channelProfit?.successUnits === 1 && channelProfit.failedUnits === 1
&& applicationQuality?.avgArrivalMs === 5_000 && channelQuality?.avgArrivalMs === 5_000;
if (!valid) throw new Error(`report recalculation assertion failed: ${JSON.stringify(evidence)}`);
console.log(JSON.stringify({ ok: true, evidence }));
} finally {
await prisma.dailyQualityReport.deleteMany({ where: { OR: [{ tenantId: ids.tenant }, { channelId: ids.channel }] } });
await prisma.dailyProfitReport.deleteMany({ where: { OR: [{ tenantId: ids.tenant }, { channelId: ids.channel }] } });
await prisma.dailyReconciliationReport.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsBillingRecord.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsReceiptRecord.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsSubmitRecord.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsMessageRecord.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsBatchTask.deleteMany({ where: { tenantId: ids.tenant } });
await prisma.smsChannel.deleteMany({ where: { id: ids.channel } });
await prisma.smsApplication.deleteMany({ where: { id: ids.application } });
await prisma.tenant.deleteMany({ where: { id: ids.tenant } });
await prisma.$disconnect();
}
}
void main().catch((error) => {
console.error(error);
process.exitCode = 1;
});