fix: decouple channel test sms records

This commit is contained in:
hectorzhao
2026-07-09 12:23:43 +08:00
parent c58a010951
commit e0579857c6
13 changed files with 190 additions and 135 deletions
+87 -55
View File
@@ -352,11 +352,32 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
});
}
listMessages(taskId?: string, phoneNumber?: string) {
listMessages(query: {
tenantId?: string;
applicationId?: string;
channelId?: string;
taskId?: string;
phoneNumber?: string;
status?: string;
} = {}) {
return this.prisma.smsMessageRecord.findMany({
where: { batchTaskId: taskId, phoneNumber },
where: {
tenantId: query.tenantId,
applicationId: query.applicationId,
channelId: query.channelId,
batchTaskId: query.taskId,
phoneNumber: query.phoneNumber,
status: query.status,
},
include: {
tenant: true,
application: true,
channel: true,
submitRecords: { include: { channel: true }, orderBy: { createdAt: 'asc' } },
receiptRecords: { include: { channel: true }, orderBy: { createdAt: 'asc' } },
},
orderBy: { queuedAt: 'desc' },
take: 200,
take: 500,
});
}
@@ -589,17 +610,21 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
if (!message || message.status !== 'queued') {
return { skipped: true };
}
if (!message.tenantId || !message.batchTaskId) {
return { skipped: true, reason: 'standalone channel test message' };
}
const businessMessage = message as typeof message & { tenantId: string; batchTaskId: string };
try {
const routed = await this.selectChannelForMessage(message);
return await this.submitMessageToGateway(message, routed, 0);
const routed = await this.selectChannelForMessage(businessMessage);
return await this.submitMessageToGateway(businessMessage, routed, 0);
} catch (error) {
const reason = error instanceof Error ? error.message : '无可用通道组或通道';
await this.prisma.smsMessageRecord.update({
where: { id: message.id },
data: { status: 'failed', errorMessage: reason },
});
await this.releaseMessageReservation(message, reason);
await this.refreshTaskProgress(message.batchTaskId);
await this.releaseMessageReservation(businessMessage, reason);
await this.refreshTaskProgress(businessMessage.batchTaskId);
return { submitted: false, messageRecordId: message.id, status: 'failed', reason };
}
}
@@ -619,24 +644,22 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
},
});
await this.recordSubmitSegments(message, data, submittedAt);
const batchTask = await this.prisma.smsBatchTask.findUnique({
where: { id: message.batchTaskId },
select: { sourceType: true },
});
const isAdminChannelTest = batchTask?.sourceType === 'admin_channel_test';
const isStandaloneChannelTest = !message.tenantId && !message.batchTaskId;
if (data.submitId && message.submitId && data.submitId !== message.submitId) {
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
const status = data.submitStatus === 'accepted' ? 'submitted' : data.submitStatus === 'timeout' ? 'timeout' : 'submit_failed';
if (data.submitStatus === 'accepted') {
await this.chargeAcceptedMessage(message);
} else if (!isAdminChannelTest) {
const retried = await this.retryMessageIfAllowed(message, data.submitStatus === 'timeout' ? '提交超时补发' : '提交失败补发');
if (data.submitStatus === 'accepted' && !isStandaloneChannelTest && message.tenantId && message.batchTaskId) {
const businessMessage = message as typeof message & { tenantId: string; batchTaskId: string };
await this.chargeAcceptedMessage(businessMessage);
} else if (data.submitStatus !== 'accepted' && !isStandaloneChannelTest && message.tenantId && message.batchTaskId) {
const businessMessage = message as typeof message & { tenantId: string; batchTaskId: string };
const retried = await this.retryMessageIfAllowed(businessMessage, data.submitStatus === 'timeout' ? '提交超时补发' : '提交失败补发');
if (retried) {
await this.refreshTaskProgress(message.batchTaskId);
await this.refreshTaskProgress(businessMessage.batchTaskId);
return retried;
}
await this.releaseMessageReservation(message, data.submitStatus === 'timeout' ? '提交超时释放冻结' : '提交失败释放冻结');
await this.releaseMessageReservation(businessMessage, data.submitStatus === 'timeout' ? '提交超时释放冻结' : '提交失败释放冻结');
}
await this.prisma.smsMessageRecord.update({
where: { id: message.id },
@@ -664,7 +687,9 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
resolvedStatus: data.submitStatus,
},
});
await this.refreshTaskProgress(message.batchTaskId);
if (message.batchTaskId) {
await this.refreshTaskProgress(message.batchTaskId);
}
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
@@ -708,13 +733,15 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
if (!isCurrentAttempt || (status === 'failed' && message.status === 'delivered')) {
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
if (status === 'failed') {
const retried = await this.retryMessageIfAllowed(message, '回执失败补发');
const isStandaloneChannelTest = !message.tenantId && !message.batchTaskId;
if (status === 'failed' && !isStandaloneChannelTest && message.tenantId && message.batchTaskId) {
const businessMessage = message as typeof message & { tenantId: string; batchTaskId: string };
const retried = await this.retryMessageIfAllowed(businessMessage, '回执失败补发');
if (retried) {
await this.refreshTaskProgress(message.batchTaskId);
await this.refreshTaskProgress(businessMessage.batchTaskId);
return retried;
}
await this.refundMessage(message, '最终失败退款');
await this.refundMessage(businessMessage, '最终失败退款');
}
await this.prisma.smsMessageRecord.update({
where: { id: message.id },
@@ -725,23 +752,27 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
deliveredAt,
},
});
await this.queueAndTryDownstreamDelivery({
tenantId: message.tenantId,
applicationId: message.applicationId,
messageRecordId: message.id,
messageId: message.messageId,
deliveryType: 'receipt',
payload: {
if (!isStandaloneChannelTest && message.tenantId && message.applicationId) {
await this.queueAndTryDownstreamDelivery({
tenantId: message.tenantId,
applicationId: message.applicationId,
messageRecordId: message.id,
messageId: message.messageId,
gatewayMessageId: data.gatewayMessageId,
phoneNumber: message.phoneNumber,
receiptStatus: data.receiptStatus,
rawStatus: data.rawStatus,
errorCode: data.errorCode,
deliveredAt: deliveredAt.toISOString(),
},
});
await this.refreshTaskProgress(message.batchTaskId);
deliveryType: 'receipt',
payload: {
messageId: message.messageId,
gatewayMessageId: data.gatewayMessageId,
phoneNumber: message.phoneNumber,
receiptStatus: data.receiptStatus,
rawStatus: data.rawStatus,
errorCode: data.errorCode,
deliveredAt: deliveredAt.toISOString(),
},
});
}
if (message.batchTaskId) {
await this.refreshTaskProgress(message.batchTaskId);
}
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
}
@@ -1265,7 +1296,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
}> {
if (data.messageId) {
const message = await this.prisma.smsMessageRecord.findUnique({ where: { messageId: data.messageId } });
if (message) {
if (message?.tenantId) {
return {
tenantId: message.tenantId,
applicationId: message.applicationId ?? undefined,
@@ -1324,30 +1355,31 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const recentMessages = await this.prisma.smsMessageRecord.findMany({
where: {
phoneNumber: data.phoneNumber,
tenantId: { not: null },
applicationId: { not: null },
submittedAt: { gte: since },
},
orderBy: { submittedAt: 'desc' },
take: 2,
});
if (recentMessages.length === 1) {
const matchableRecentMessages = recentMessages.filter((message) => message.tenantId && message.applicationId);
if (matchableRecentMessages.length === 1) {
return {
tenantId: recentMessages[0].tenantId,
applicationId: recentMessages[0].applicationId ?? undefined,
messageRecordId: recentMessages[0].id,
tenantId: matchableRecentMessages[0].tenantId ?? undefined,
applicationId: matchableRecentMessages[0].applicationId ?? undefined,
messageRecordId: matchableRecentMessages[0].id,
matchStatus: 'matched',
matchReason: `手机号 ${windowHours} 小时窗口唯一匹配`,
candidates: [],
};
}
if (recentMessages.length > 1) {
if (matchableRecentMessages.length > 1) {
return {
matchStatus: 'ambiguous',
matchReason: `手机号 ${windowHours} 小时窗口匹配多条下发记录`,
candidates: recentMessages
.filter((message) => message.applicationId)
candidates: matchableRecentMessages
.map((message) => ({
tenantId: message.tenantId,
tenantId: String(message.tenantId),
applicationId: String(message.applicationId),
messageRecordId: message.id,
matchSource: 'phone_window',
@@ -1433,11 +1465,11 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
});
for (const candidate of candidates) {
const message = await this.prisma.smsMessageRecord.findUnique({ where: { id: candidate.id } });
if (message) {
await this.refundMessage(message, '72小时未收到明确回执,自动超时退款');
if (message?.tenantId) {
await this.refundMessage(message as typeof message & { tenantId: string }, '72小时未收到明确回执,自动超时退款');
}
}
for (const batchTaskId of new Set(candidates.map((candidate) => candidate.batchTaskId))) {
for (const batchTaskId of new Set(candidates.map((candidate) => candidate.batchTaskId).filter((value): value is string => Boolean(value)))) {
await this.refreshTaskProgress(batchTaskId);
}
return { timeout: candidates.length };
@@ -1959,8 +1991,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
private async recordSubmitSegments(
message: {
id: string;
tenantId: string;
batchTaskId: string;
tenantId?: string | null;
batchTaskId?: string | null;
channelId?: string | null;
submitId?: string | null;
billingUnits?: number | null;
@@ -2048,8 +2080,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
private async recordReceiptSegment(
message: {
id: string;
tenantId: string;
batchTaskId: string;
tenantId?: string | null;
batchTaskId?: string | null;
channelId?: string | null;
submitId?: string | null;
billingUnits?: number | null;