perf(cmpp): decouple supplier result callbacks

This commit is contained in:
hectorzhao
2026-08-20 14:19:42 +08:00
parent 485af688d2
commit 67b760a599
27 changed files with 1000 additions and 66 deletions
@@ -58,6 +58,7 @@ export interface GatewayInboundSingleSubmitResult {
}
export interface GatewaySubmitResultDto {
eventId?: string;
traceId?: string;
messageId: string;
channelId: string;
@@ -81,6 +82,7 @@ export interface GatewaySubmitResultDto {
}
export interface GatewaySubmitSegmentResultDto {
eventId?: string;
traceId?: string;
messageId: string;
channelId: string;
@@ -2216,6 +2216,46 @@ describe('SendChainService', () => {
});
});
it('treats a redelivered Outbox aggregate event as idempotent', async () => {
const { service, prisma, billing } = createService();
const baseSubmit = {
id: 'submit-1',
messageRecordId: 'record-1',
channelId: 'channel-1',
submitId: 'SUB-1',
submitStatus: 'accepted',
};
prisma.smsSubmitRecord.findUnique
.mockResolvedValueOnce({ ...baseSubmit, resultEventId: null })
.mockResolvedValueOnce({ ...baseSubmit, resultEventId: 'submit:SUB-1:aggregate' });
const event = {
eventId: 'submit:SUB-1:aggregate',
messageId: 'MSG-1',
channelId: 'channel-1',
submitId: 'SUB-1',
sequenceId: 7,
gatewayMessageId: 'GW-1',
submitStatus: 'accepted' as const,
submittedAt: '2026-07-01T10:00:00.000Z',
};
await service.handleSubmitResult(event);
await service.handleSubmitResult(event);
expect(billing.charge).toHaveBeenCalledTimes(1);
expect(billing.release).toHaveBeenCalledTimes(1);
expect(prisma.smsSubmitRecord.updateMany).toHaveBeenCalledWith({
where: {
id: 'submit-1',
OR: [{ resultEventId: null }, { resultEventId: 'submit:SUB-1:aggregate' }],
},
data: {
resultEventId: 'submit:SUB-1:aggregate',
resultProcessedAt: new Date('2026-07-01T10:00:00.000Z'),
},
});
});
it('rejects a legacy aggregate SubmitResult when it cannot match one submit attempt uniquely', async () => {
const { service, prisma } = createService();
prisma.smsSubmitRecord.findMany.mockResolvedValue([
@@ -128,6 +128,12 @@ export class SendGatewayResultService {
async handleSubmitResult(data: GatewaySubmitResultDto) {
const message = await this.facade.requireMessageByGatewayEvent(data.messageId, data.gatewayMessageId);
const submitRecord = await this.facade.resolveSubmitRecordForGatewayResult(message.id, data);
if (data.eventId && submitRecord.resultEventId) {
if (submitRecord.resultEventId !== data.eventId) {
throw new BadRequestException('Gateway SubmitResult eventId conflicts with the submit attempt');
}
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
const effectiveData = { ...data, submitId: submitRecord.submitId };
const batchTask = message.batchTaskId
? await this.prisma.smsBatchTask.findUnique({ where: { id: message.batchTaskId }, select: { sourceType: true } })
@@ -147,6 +153,7 @@ export class SendGatewayResultService {
await this.facade.recordSubmitSegments(message, effectiveData, submittedAt);
const isStandaloneChannelTest = !message.tenantId && !message.batchTaskId;
if (message.submitId && effectiveData.submitId !== message.submitId) {
await this.markSubmitResultProcessed(submitRecord.id, data.eventId, submittedAt);
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
const status = data.submitStatus === 'accepted' ? 'submitted' : data.submitStatus === 'timeout' ? 'timeout' : 'submit_failed';
@@ -166,6 +173,7 @@ export class SendGatewayResultService {
);
if (retried) {
await this.facade.refreshTaskProgress(businessMessage.batchTaskId);
await this.markSubmitResultProcessed(submitRecord.id, data.eventId, submittedAt);
return retried;
}
await this.facade.releaseMessageReservation(businessMessage, data.submitStatus === 'timeout' ? '提交超时释放冻结' : '提交失败释放冻结');
@@ -218,9 +226,29 @@ export class SendGatewayResultService {
if (message.batchTaskId) {
await this.facade.refreshTaskProgress(message.batchTaskId);
}
await this.markSubmitResultProcessed(submitRecord.id, data.eventId, submittedAt);
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
private async markSubmitResultProcessed(submitRecordId: string, eventId: string | undefined, processedAt: Date) {
if (!eventId) {
return;
}
const updated = await this.prisma.smsSubmitRecord.updateMany({
where: {
id: submitRecordId,
OR: [{ resultEventId: null }, { resultEventId: eventId }],
},
data: {
resultEventId: eventId,
resultProcessedAt: processedAt,
},
});
if (updated.count === 0) {
throw new BadRequestException('Gateway SubmitResult eventId conflicts with the submit attempt');
}
}
async resolveSubmitRecordForGatewayResult(messageRecordId: string, data: GatewaySubmitResultDto) {
if (data.submitId) {
const exact = await this.prisma.smsSubmitRecord.findUnique({ where: { submitId: data.submitId } });