fix: decouple channel test sms records
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||||
|
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||||
|
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE "SmsMessageSegmentAudit" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE "SmsReceiptRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||||
+12
-12
@@ -859,8 +859,8 @@ model SmsApiRequest {
|
|||||||
|
|
||||||
model SmsMessageRecord {
|
model SmsMessageRecord {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tenantId String
|
tenantId String?
|
||||||
batchTaskId String
|
batchTaskId String?
|
||||||
applicationId String?
|
applicationId String?
|
||||||
templateId String?
|
templateId String?
|
||||||
messageId String @unique
|
messageId String @unique
|
||||||
@@ -884,8 +884,8 @@ model SmsMessageRecord {
|
|||||||
timeoutAt DateTime?
|
timeoutAt DateTime?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||||
batchTask SmsBatchTask @relation(fields: [batchTaskId], references: [id], onDelete: Cascade)
|
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id], onDelete: Cascade)
|
||||||
application SmsApplication? @relation(fields: [applicationId], references: [id])
|
application SmsApplication? @relation(fields: [applicationId], references: [id])
|
||||||
template SmsTemplate? @relation(fields: [templateId], references: [id])
|
template SmsTemplate? @relation(fields: [templateId], references: [id])
|
||||||
channel SmsChannel? @relation(fields: [channelId], references: [id])
|
channel SmsChannel? @relation(fields: [channelId], references: [id])
|
||||||
@@ -921,8 +921,8 @@ model CmppSubmitSession {
|
|||||||
|
|
||||||
model SmsSubmitRecord {
|
model SmsSubmitRecord {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tenantId String
|
tenantId String?
|
||||||
batchTaskId String
|
batchTaskId String?
|
||||||
messageRecordId String
|
messageRecordId String
|
||||||
channelId String
|
channelId String
|
||||||
sessionId String?
|
sessionId String?
|
||||||
@@ -936,8 +936,8 @@ model SmsSubmitRecord {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||||
batchTask SmsBatchTask @relation(fields: [batchTaskId], references: [id], onDelete: Cascade)
|
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id], onDelete: Cascade)
|
||||||
messageRecord SmsMessageRecord @relation(fields: [messageRecordId], references: [id], onDelete: Cascade)
|
messageRecord SmsMessageRecord @relation(fields: [messageRecordId], references: [id], onDelete: Cascade)
|
||||||
channel SmsChannel @relation(fields: [channelId], references: [id])
|
channel SmsChannel @relation(fields: [channelId], references: [id])
|
||||||
session CmppSubmitSession? @relation(fields: [sessionId], references: [id])
|
session CmppSubmitSession? @relation(fields: [sessionId], references: [id])
|
||||||
@@ -950,7 +950,7 @@ model SmsSubmitRecord {
|
|||||||
|
|
||||||
model SmsMessageSegmentAudit {
|
model SmsMessageSegmentAudit {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tenantId String
|
tenantId String?
|
||||||
batchTaskId String?
|
batchTaskId String?
|
||||||
messageRecordId String
|
messageRecordId String
|
||||||
submitRecordId String?
|
submitRecordId String?
|
||||||
@@ -972,7 +972,7 @@ model SmsMessageSegmentAudit {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||||
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id])
|
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id])
|
||||||
messageRecord SmsMessageRecord @relation(fields: [messageRecordId], references: [id], onDelete: Cascade)
|
messageRecord SmsMessageRecord @relation(fields: [messageRecordId], references: [id], onDelete: Cascade)
|
||||||
submitRecord SmsSubmitRecord? @relation(fields: [submitRecordId], references: [id])
|
submitRecord SmsSubmitRecord? @relation(fields: [submitRecordId], references: [id])
|
||||||
@@ -988,7 +988,7 @@ model SmsMessageSegmentAudit {
|
|||||||
|
|
||||||
model SmsReceiptRecord {
|
model SmsReceiptRecord {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tenantId String
|
tenantId String?
|
||||||
batchTaskId String?
|
batchTaskId String?
|
||||||
messageRecordId String?
|
messageRecordId String?
|
||||||
channelId String?
|
channelId String?
|
||||||
@@ -1001,7 +1001,7 @@ model SmsReceiptRecord {
|
|||||||
deliveredAt DateTime
|
deliveredAt DateTime
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||||
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id])
|
batchTask SmsBatchTask? @relation(fields: [batchTaskId], references: [id])
|
||||||
messageRecord SmsMessageRecord? @relation(fields: [messageRecordId], references: [id])
|
messageRecord SmsMessageRecord? @relation(fields: [messageRecordId], references: [id])
|
||||||
channel SmsChannel? @relation(fields: [channelId], references: [id])
|
channel SmsChannel? @relation(fields: [channelId], references: [id])
|
||||||
|
|||||||
@@ -596,20 +596,12 @@ describe('ChannelsService', () => {
|
|||||||
channelId: 'channel-1',
|
channelId: 'channel-1',
|
||||||
status: 'submit_queued',
|
status: 'submit_queued',
|
||||||
submitted: 1,
|
submitted: 1,
|
||||||
batchTaskId: 'batch-1',
|
testNo: expect.stringMatching(/^CHTEST-/),
|
||||||
}));
|
}));
|
||||||
expect(prisma.smsBatchTask.create).toHaveBeenCalledWith({
|
expect(prisma.tenant.findFirst).not.toHaveBeenCalled();
|
||||||
data: expect.objectContaining({
|
expect(prisma.smsBatchTask.create).not.toHaveBeenCalled();
|
||||||
tenantId: 'tenant-1',
|
|
||||||
sourceType: 'admin_channel_test',
|
|
||||||
phoneTotal: 1,
|
|
||||||
status: 'submit_queued',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledWith({
|
expect(prisma.smsMessageRecord.create).toHaveBeenCalledWith({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
tenantId: 'tenant-1',
|
|
||||||
batchTaskId: 'batch-1',
|
|
||||||
phoneNumber: '18821203795',
|
phoneNumber: '18821203795',
|
||||||
channelId: 'channel-1',
|
channelId: 'channel-1',
|
||||||
status: 'submit_queued',
|
status: 'submit_queued',
|
||||||
@@ -618,8 +610,6 @@ describe('ChannelsService', () => {
|
|||||||
});
|
});
|
||||||
expect(prisma.smsSubmitRecord.create).toHaveBeenCalledWith({
|
expect(prisma.smsSubmitRecord.create).toHaveBeenCalledWith({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
tenantId: 'tenant-1',
|
|
||||||
batchTaskId: 'batch-1',
|
|
||||||
channelId: 'channel-1',
|
channelId: 'channel-1',
|
||||||
sessionId: 'session-1',
|
sessionId: 'session-1',
|
||||||
submitStatus: 'queued',
|
submitStatus: 'queued',
|
||||||
@@ -645,6 +635,7 @@ describe('ChannelsService', () => {
|
|||||||
action: 'sms_channel.test_submit',
|
action: 'sms_channel.test_submit',
|
||||||
resource: 'sms_channel',
|
resource: 'sms_channel',
|
||||||
resourceId: 'channel-1',
|
resourceId: 'channel-1',
|
||||||
|
detail: expect.objectContaining({ testNo: expect.stringMatching(/^CHTEST-/) }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -432,29 +432,8 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
throw new BadRequestException('通道当前没有可用 CMPP 连接,请先连接成功后再测试发送');
|
throw new BadRequestException('通道当前没有可用 CMPP 连接,请先连接成功后再测试发送');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenant = await this.prisma.tenant.findFirst({ orderBy: { createdAt: 'asc' } });
|
|
||||||
if (!tenant) {
|
|
||||||
throw new BadRequestException('未找到可归属测试短信的客户租户');
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const taskNo = `CHTEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
const testNo = `CHTEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
||||||
const batchTask = await this.prisma.smsBatchTask.create({
|
|
||||||
data: {
|
|
||||||
tenantId: tenant.id,
|
|
||||||
taskNo,
|
|
||||||
sourceType: 'admin_channel_test',
|
|
||||||
content,
|
|
||||||
category: 'channel_test',
|
|
||||||
phoneTotal: phoneNumbers.length,
|
|
||||||
status: 'submit_queued',
|
|
||||||
auditStatus: 'approved',
|
|
||||||
progressTotal: phoneNumbers.length,
|
|
||||||
submittedTotal: 0,
|
|
||||||
createdById: data.operatorId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const [index, phoneNumber] of phoneNumbers.entries()) {
|
for (const [index, phoneNumber] of phoneNumbers.entries()) {
|
||||||
const messageId = `MSG-TEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
const messageId = `MSG-TEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
||||||
@@ -466,14 +445,12 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
const messageRecord = await this.prisma.smsMessageRecord.create({
|
const messageRecord = await this.prisma.smsMessageRecord.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId: tenant.id,
|
|
||||||
batchTaskId: batchTask.id,
|
|
||||||
messageId,
|
messageId,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
content,
|
content,
|
||||||
billingUnits: calculateBillingUnits(content),
|
billingUnits: calculateBillingUnits(content),
|
||||||
unitPrice: channel.unitPrice,
|
unitPrice: 0,
|
||||||
amountCents: channel.unitPrice * calculateBillingUnits(content),
|
amountCents: 0,
|
||||||
queuePriority: 'normal',
|
queuePriority: 'normal',
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
submitId,
|
submitId,
|
||||||
@@ -484,8 +461,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
await this.prisma.smsSubmitRecord.create({
|
await this.prisma.smsSubmitRecord.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId: tenant.id,
|
|
||||||
batchTaskId: batchTask.id,
|
|
||||||
messageRecordId: messageRecord.id,
|
messageRecordId: messageRecord.id,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@@ -499,8 +474,7 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
phoneNumber,
|
phoneNumber,
|
||||||
messageId,
|
messageId,
|
||||||
submitId,
|
submitId,
|
||||||
batchTaskId: batchTask.id,
|
testNo,
|
||||||
tenantId: tenant.id,
|
|
||||||
attempt: index,
|
attempt: index,
|
||||||
accessNo: data.accessNo,
|
accessNo: data.accessNo,
|
||||||
});
|
});
|
||||||
@@ -514,10 +488,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.smsBatchTask.update({
|
|
||||||
where: { id: batchTask.id },
|
|
||||||
data: { submittedTotal: phoneNumbers.length },
|
|
||||||
});
|
|
||||||
await this.prisma.operationLog.create({
|
await this.prisma.operationLog.create({
|
||||||
data: {
|
data: {
|
||||||
userId: data.operatorId,
|
userId: data.operatorId,
|
||||||
@@ -525,7 +495,7 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
resource: 'sms_channel',
|
resource: 'sms_channel',
|
||||||
resourceId: channel.id,
|
resourceId: channel.id,
|
||||||
detail: {
|
detail: {
|
||||||
batchTaskId: batchTask.id,
|
testNo,
|
||||||
phoneTotal: phoneNumbers.length,
|
phoneTotal: phoneNumbers.length,
|
||||||
messageRecordIds: results.map((item) => item.messageRecordId),
|
messageRecordIds: results.map((item) => item.messageRecordId),
|
||||||
connectionId: connectedState.connectionId,
|
connectionId: connectedState.connectionId,
|
||||||
@@ -536,8 +506,7 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return {
|
return {
|
||||||
channelId,
|
channelId,
|
||||||
status: 'submit_queued',
|
status: 'submit_queued',
|
||||||
batchTaskId: batchTask.id,
|
testNo,
|
||||||
taskNo: batchTask.taskNo,
|
|
||||||
submitted: results.length,
|
submitted: results.length,
|
||||||
messages: results,
|
messages: results,
|
||||||
queuedAt: createdAt,
|
queuedAt: createdAt,
|
||||||
@@ -1265,8 +1234,7 @@ function buildChannelTestSubmitCommand({
|
|||||||
phoneNumber,
|
phoneNumber,
|
||||||
messageId,
|
messageId,
|
||||||
submitId,
|
submitId,
|
||||||
batchTaskId,
|
testNo,
|
||||||
tenantId,
|
|
||||||
attempt,
|
attempt,
|
||||||
accessNo,
|
accessNo,
|
||||||
}: {
|
}: {
|
||||||
@@ -1286,8 +1254,7 @@ function buildChannelTestSubmitCommand({
|
|||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
submitId: string;
|
submitId: string;
|
||||||
batchTaskId: string;
|
testNo: string;
|
||||||
tenantId: string;
|
|
||||||
attempt: number;
|
attempt: number;
|
||||||
accessNo?: string;
|
accessNo?: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -1299,9 +1266,9 @@ function buildChannelTestSubmitCommand({
|
|||||||
messageId,
|
messageId,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
tenantId,
|
tenantId: 'platform-channel-test',
|
||||||
applicationId: 'admin-channel-test',
|
applicationId: 'admin-channel-test',
|
||||||
taskId: batchTaskId,
|
taskId: testNo,
|
||||||
submitId,
|
submitId,
|
||||||
queuePriority: 'normal',
|
queuePriority: 'normal',
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
|
|||||||
@@ -13,8 +13,15 @@ export class AdminSendChainController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('messages')
|
@Get('messages')
|
||||||
listMessages(@Query('taskId') taskId?: string, @Query('phoneNumber') phoneNumber?: string) {
|
listMessages(
|
||||||
return this.sendChain.listMessages(taskId, phoneNumber);
|
@Query('tenantId') tenantId?: string,
|
||||||
|
@Query('applicationId') applicationId?: string,
|
||||||
|
@Query('channelId') channelId?: string,
|
||||||
|
@Query('taskId') taskId?: string,
|
||||||
|
@Query('phoneNumber') phoneNumber?: string,
|
||||||
|
@Query('status') status?: string,
|
||||||
|
) {
|
||||||
|
return this.sendChain.listMessages({ tenantId, applicationId, channelId, taskId, phoneNumber, status });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('submit-records')
|
@Get('submit-records')
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class ClientSendChainController {
|
|||||||
|
|
||||||
@Get('batch-tasks/:id/messages')
|
@Get('batch-tasks/:id/messages')
|
||||||
listTaskMessages(@Param('id') taskId: string) {
|
listTaskMessages(@Param('id') taskId: string) {
|
||||||
return this.sendChain.listMessages(taskId);
|
return this.sendChain.listMessages({ taskId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('batch-tasks/:id/cancel')
|
@Post('batch-tasks/:id/cancel')
|
||||||
|
|||||||
@@ -556,7 +556,24 @@ describe('SendChainService', () => {
|
|||||||
|
|
||||||
it('updates admin channel test message status without business retry routing', async () => {
|
it('updates admin channel test message status without business retry routing', async () => {
|
||||||
const { service, prisma, billing } = createService();
|
const { service, prisma, billing } = createService();
|
||||||
prisma.smsBatchTask.findUnique.mockResolvedValue({ id: 'task-1', tenantId: 'tenant-1', sourceType: 'admin_channel_test' });
|
prisma.smsMessageRecord.findFirst.mockResolvedValueOnce({
|
||||||
|
id: 'record-1',
|
||||||
|
tenantId: null,
|
||||||
|
batchTaskId: null,
|
||||||
|
applicationId: null,
|
||||||
|
templateId: null,
|
||||||
|
messageId: 'MSG-1',
|
||||||
|
phoneNumber: '13800000001',
|
||||||
|
content: 'hello',
|
||||||
|
billingUnits: 1,
|
||||||
|
unitPrice: 0,
|
||||||
|
amountCents: 0,
|
||||||
|
status: 'submit_queued',
|
||||||
|
queuePriority: 'normal',
|
||||||
|
submitId: 'SUB-1',
|
||||||
|
gatewayMessageId: null,
|
||||||
|
channelId: 'channel-1',
|
||||||
|
});
|
||||||
|
|
||||||
await service.handleSubmitResult({
|
await service.handleSubmitResult({
|
||||||
messageId: 'MSG-1',
|
messageId: 'MSG-1',
|
||||||
@@ -572,6 +589,7 @@ describe('SendChainService', () => {
|
|||||||
|
|
||||||
expect(prisma.channelRouteRule.findFirst).not.toHaveBeenCalled();
|
expect(prisma.channelRouteRule.findFirst).not.toHaveBeenCalled();
|
||||||
expect(billing.release).not.toHaveBeenCalled();
|
expect(billing.release).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.smsBatchTask.update).not.toHaveBeenCalled();
|
||||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith({
|
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith({
|
||||||
where: { id: 'record-1' },
|
where: { id: 'record-1' },
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
|
|||||||
@@ -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({
|
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' },
|
orderBy: { queuedAt: 'desc' },
|
||||||
take: 200,
|
take: 500,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,17 +610,21 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
if (!message || message.status !== 'queued') {
|
if (!message || message.status !== 'queued') {
|
||||||
return { skipped: true };
|
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 {
|
try {
|
||||||
const routed = await this.selectChannelForMessage(message);
|
const routed = await this.selectChannelForMessage(businessMessage);
|
||||||
return await this.submitMessageToGateway(message, routed, 0);
|
return await this.submitMessageToGateway(businessMessage, routed, 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = error instanceof Error ? error.message : '无可用通道组或通道';
|
const reason = error instanceof Error ? error.message : '无可用通道组或通道';
|
||||||
await this.prisma.smsMessageRecord.update({
|
await this.prisma.smsMessageRecord.update({
|
||||||
where: { id: message.id },
|
where: { id: message.id },
|
||||||
data: { status: 'failed', errorMessage: reason },
|
data: { status: 'failed', errorMessage: reason },
|
||||||
});
|
});
|
||||||
await this.releaseMessageReservation(message, reason);
|
await this.releaseMessageReservation(businessMessage, reason);
|
||||||
await this.refreshTaskProgress(message.batchTaskId);
|
await this.refreshTaskProgress(businessMessage.batchTaskId);
|
||||||
return { submitted: false, messageRecordId: message.id, status: 'failed', reason };
|
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);
|
await this.recordSubmitSegments(message, data, submittedAt);
|
||||||
const batchTask = await this.prisma.smsBatchTask.findUnique({
|
const isStandaloneChannelTest = !message.tenantId && !message.batchTaskId;
|
||||||
where: { id: message.batchTaskId },
|
|
||||||
select: { sourceType: true },
|
|
||||||
});
|
|
||||||
const isAdminChannelTest = batchTask?.sourceType === 'admin_channel_test';
|
|
||||||
if (data.submitId && message.submitId && data.submitId !== message.submitId) {
|
if (data.submitId && message.submitId && data.submitId !== message.submitId) {
|
||||||
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
||||||
}
|
}
|
||||||
const status = data.submitStatus === 'accepted' ? 'submitted' : data.submitStatus === 'timeout' ? 'timeout' : 'submit_failed';
|
const status = data.submitStatus === 'accepted' ? 'submitted' : data.submitStatus === 'timeout' ? 'timeout' : 'submit_failed';
|
||||||
if (data.submitStatus === 'accepted') {
|
if (data.submitStatus === 'accepted' && !isStandaloneChannelTest && message.tenantId && message.batchTaskId) {
|
||||||
await this.chargeAcceptedMessage(message);
|
const businessMessage = message as typeof message & { tenantId: string; batchTaskId: string };
|
||||||
} else if (!isAdminChannelTest) {
|
await this.chargeAcceptedMessage(businessMessage);
|
||||||
const retried = await this.retryMessageIfAllowed(message, data.submitStatus === 'timeout' ? '提交超时补发' : '提交失败补发');
|
} 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) {
|
if (retried) {
|
||||||
await this.refreshTaskProgress(message.batchTaskId);
|
await this.refreshTaskProgress(businessMessage.batchTaskId);
|
||||||
return retried;
|
return retried;
|
||||||
}
|
}
|
||||||
await this.releaseMessageReservation(message, data.submitStatus === 'timeout' ? '提交超时释放冻结' : '提交失败释放冻结');
|
await this.releaseMessageReservation(businessMessage, data.submitStatus === 'timeout' ? '提交超时释放冻结' : '提交失败释放冻结');
|
||||||
}
|
}
|
||||||
await this.prisma.smsMessageRecord.update({
|
await this.prisma.smsMessageRecord.update({
|
||||||
where: { id: message.id },
|
where: { id: message.id },
|
||||||
@@ -664,7 +687,9 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
resolvedStatus: data.submitStatus,
|
resolvedStatus: data.submitStatus,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (message.batchTaskId) {
|
||||||
await this.refreshTaskProgress(message.batchTaskId);
|
await this.refreshTaskProgress(message.batchTaskId);
|
||||||
|
}
|
||||||
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
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')) {
|
if (!isCurrentAttempt || (status === 'failed' && message.status === 'delivered')) {
|
||||||
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
||||||
}
|
}
|
||||||
if (status === 'failed') {
|
const isStandaloneChannelTest = !message.tenantId && !message.batchTaskId;
|
||||||
const retried = await this.retryMessageIfAllowed(message, '回执失败补发');
|
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) {
|
if (retried) {
|
||||||
await this.refreshTaskProgress(message.batchTaskId);
|
await this.refreshTaskProgress(businessMessage.batchTaskId);
|
||||||
return retried;
|
return retried;
|
||||||
}
|
}
|
||||||
await this.refundMessage(message, '最终失败退款');
|
await this.refundMessage(businessMessage, '最终失败退款');
|
||||||
}
|
}
|
||||||
await this.prisma.smsMessageRecord.update({
|
await this.prisma.smsMessageRecord.update({
|
||||||
where: { id: message.id },
|
where: { id: message.id },
|
||||||
@@ -725,6 +752,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
deliveredAt,
|
deliveredAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!isStandaloneChannelTest && message.tenantId && message.applicationId) {
|
||||||
await this.queueAndTryDownstreamDelivery({
|
await this.queueAndTryDownstreamDelivery({
|
||||||
tenantId: message.tenantId,
|
tenantId: message.tenantId,
|
||||||
applicationId: message.applicationId,
|
applicationId: message.applicationId,
|
||||||
@@ -741,7 +769,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
deliveredAt: deliveredAt.toISOString(),
|
deliveredAt: deliveredAt.toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (message.batchTaskId) {
|
||||||
await this.refreshTaskProgress(message.batchTaskId);
|
await this.refreshTaskProgress(message.batchTaskId);
|
||||||
|
}
|
||||||
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
return this.prisma.smsMessageRecord.findUnique({ where: { id: message.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,7 +1296,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}> {
|
}> {
|
||||||
if (data.messageId) {
|
if (data.messageId) {
|
||||||
const message = await this.prisma.smsMessageRecord.findUnique({ where: { messageId: data.messageId } });
|
const message = await this.prisma.smsMessageRecord.findUnique({ where: { messageId: data.messageId } });
|
||||||
if (message) {
|
if (message?.tenantId) {
|
||||||
return {
|
return {
|
||||||
tenantId: message.tenantId,
|
tenantId: message.tenantId,
|
||||||
applicationId: message.applicationId ?? undefined,
|
applicationId: message.applicationId ?? undefined,
|
||||||
@@ -1324,30 +1355,31 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
const recentMessages = await this.prisma.smsMessageRecord.findMany({
|
const recentMessages = await this.prisma.smsMessageRecord.findMany({
|
||||||
where: {
|
where: {
|
||||||
phoneNumber: data.phoneNumber,
|
phoneNumber: data.phoneNumber,
|
||||||
|
tenantId: { not: null },
|
||||||
applicationId: { not: null },
|
applicationId: { not: null },
|
||||||
submittedAt: { gte: since },
|
submittedAt: { gte: since },
|
||||||
},
|
},
|
||||||
orderBy: { submittedAt: 'desc' },
|
orderBy: { submittedAt: 'desc' },
|
||||||
take: 2,
|
take: 2,
|
||||||
});
|
});
|
||||||
if (recentMessages.length === 1) {
|
const matchableRecentMessages = recentMessages.filter((message) => message.tenantId && message.applicationId);
|
||||||
|
if (matchableRecentMessages.length === 1) {
|
||||||
return {
|
return {
|
||||||
tenantId: recentMessages[0].tenantId,
|
tenantId: matchableRecentMessages[0].tenantId ?? undefined,
|
||||||
applicationId: recentMessages[0].applicationId ?? undefined,
|
applicationId: matchableRecentMessages[0].applicationId ?? undefined,
|
||||||
messageRecordId: recentMessages[0].id,
|
messageRecordId: matchableRecentMessages[0].id,
|
||||||
matchStatus: 'matched',
|
matchStatus: 'matched',
|
||||||
matchReason: `手机号 ${windowHours} 小时窗口唯一匹配`,
|
matchReason: `手机号 ${windowHours} 小时窗口唯一匹配`,
|
||||||
candidates: [],
|
candidates: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (recentMessages.length > 1) {
|
if (matchableRecentMessages.length > 1) {
|
||||||
return {
|
return {
|
||||||
matchStatus: 'ambiguous',
|
matchStatus: 'ambiguous',
|
||||||
matchReason: `手机号 ${windowHours} 小时窗口匹配多条下发记录`,
|
matchReason: `手机号 ${windowHours} 小时窗口匹配多条下发记录`,
|
||||||
candidates: recentMessages
|
candidates: matchableRecentMessages
|
||||||
.filter((message) => message.applicationId)
|
|
||||||
.map((message) => ({
|
.map((message) => ({
|
||||||
tenantId: message.tenantId,
|
tenantId: String(message.tenantId),
|
||||||
applicationId: String(message.applicationId),
|
applicationId: String(message.applicationId),
|
||||||
messageRecordId: message.id,
|
messageRecordId: message.id,
|
||||||
matchSource: 'phone_window',
|
matchSource: 'phone_window',
|
||||||
@@ -1433,11 +1465,11 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const message = await this.prisma.smsMessageRecord.findUnique({ where: { id: candidate.id } });
|
const message = await this.prisma.smsMessageRecord.findUnique({ where: { id: candidate.id } });
|
||||||
if (message) {
|
if (message?.tenantId) {
|
||||||
await this.refundMessage(message, '72小时未收到明确回执,自动超时退款');
|
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);
|
await this.refreshTaskProgress(batchTaskId);
|
||||||
}
|
}
|
||||||
return { timeout: candidates.length };
|
return { timeout: candidates.length };
|
||||||
@@ -1959,8 +1991,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
private async recordSubmitSegments(
|
private async recordSubmitSegments(
|
||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
tenantId: string;
|
tenantId?: string | null;
|
||||||
batchTaskId: string;
|
batchTaskId?: string | null;
|
||||||
channelId?: string | null;
|
channelId?: string | null;
|
||||||
submitId?: string | null;
|
submitId?: string | null;
|
||||||
billingUnits?: number | null;
|
billingUnits?: number | null;
|
||||||
@@ -2048,8 +2080,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
private async recordReceiptSegment(
|
private async recordReceiptSegment(
|
||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
tenantId: string;
|
tenantId?: string | null;
|
||||||
batchTaskId: string;
|
batchTaskId?: string | null;
|
||||||
channelId?: string | null;
|
channelId?: string | null;
|
||||||
submitId?: string | null;
|
submitId?: string | null;
|
||||||
billingUnits?: number | null;
|
billingUnits?: number | null;
|
||||||
|
|||||||
@@ -188,7 +188,8 @@
|
|||||||
|
|
||||||
1. Gateway 必须按运营端通道配置连接上游 SMSC,使用通道的 `gatewayHost/gatewayPort/account/passwordCipher/srcId/cmppVersion` 完成 CMPP 2.0/3.0 connect/login。
|
1. Gateway 必须按运营端通道配置连接上游 SMSC,使用通道的 `gatewayHost/gatewayPort/account/passwordCipher/srcId/cmppVersion` 完成 CMPP 2.0/3.0 connect/login。
|
||||||
- 运营端通道创建/编辑必须提供 CMPP 2.0/3.0 版本选项,默认 CMPP 2.0;保存后 Gateway 连接与 SubmitCommand 均必须使用真实保存的 `cmppVersion`。
|
- 运营端通道创建/编辑必须提供 CMPP 2.0/3.0 版本选项,默认 CMPP 2.0;保存后 Gateway 连接与 SubmitCommand 均必须使用真实保存的 `cmppVersion`。
|
||||||
- 运营端通道“发送测试”必须走真实闭环:前端提交手机号和短信内容到 NestJS API,后端校验通道在线后创建 `SmsBatchTask`、`SmsMessageRecord`、`SmsSubmitRecord`,并向 Redis Stream `gateway.submit.commands` 写入真实 `SubmitCommand`;短信记录页面必须能查询到测试短信,不允许只返回占位成功或只关闭弹窗。
|
- 运营端通道“发送测试”必须走真实闭环:前端提交手机号和短信内容到 NestJS API,后端校验通道在线后创建独立 `SmsMessageRecord`、`SmsSubmitRecord`,并向 Redis Stream `gateway.submit.commands` 写入真实 `SubmitCommand`;短信记录页面必须能查询到测试短信,不允许只返回占位成功或只关闭弹窗。
|
||||||
|
- 通道测试短信是运营侧上游通道连通性验证,不绑定企业、企业应用或 `SmsBatchTask` 发送任务;submit result 和 receipt 只记录在短信记录、提交记录、回执记录和分片审计中,不触发客户侧 Deliver 推送、企业账务、任务进度或业务补发。
|
||||||
2. Gateway 必须校验上游 connect/login 返回码,区分 connected、auth_failed、connect_timeout、network_error、protocol_error 等状态,并回写 NestJS 真实连接状态。
|
2. Gateway 必须校验上游 connect/login 返回码,区分 connected、auth_failed、connect_timeout、network_error、protocol_error 等状态,并回写 NestJS 真实连接状态。
|
||||||
3. Gateway 必须支持每个通道配置期望连接数,建立多条长连接,并按连接维度维护 currentConnections、lastConnectedAt、lastHeartbeatAt、lastError、reconnectCount。
|
3. Gateway 必须支持每个通道配置期望连接数,建立多条长连接,并按连接维度维护 currentConnections、lastConnectedAt、lastHeartbeatAt、lastError、reconnectCount。
|
||||||
- `desiredConnections`、`windowSize` 是平台对上游通道连接池和提交窗口的运行配置,必须通过运营端通道配置页面保存到真实后端;它们不是 CMPP 标准 PDU 字段,也不是 gocmpp 的原生配置字段。
|
- `desiredConnections`、`windowSize` 是平台对上游通道连接池和提交窗口的运行配置,必须通过运营端通道配置页面保存到真实后端;它们不是 CMPP 标准 PDU 字段,也不是 gocmpp 的原生配置字段。
|
||||||
|
|||||||
@@ -247,7 +247,8 @@
|
|||||||
- 通道协议默认为 CMPP,CMPP 版本默认 2.0,且可选择 2.0 或 3.0。
|
- 通道协议默认为 CMPP,CMPP 版本默认 2.0,且可选择 2.0 或 3.0。
|
||||||
- 通道限速保存正确。
|
- 通道限速保存正确。
|
||||||
- 通道真实保存 `desiredConnections/windowSize`,后续 Gateway `ConnectChannel` 与 `SubmitCommand.upstream` 使用该配置。
|
- 通道真实保存 `desiredConnections/windowSize`,后续 Gateway `ConnectChannel` 与 `SubmitCommand.upstream` 使用该配置。
|
||||||
- 通道测试短信必须调用真实 NestJS API,创建 `SmsBatchTask`、`SmsMessageRecord`、`SmsSubmitRecord`,并向 Redis Stream `gateway.submit.commands` 写入 `SubmitCommand`;短信记录页面能查询到该测试短信。
|
- 通道测试短信必须调用真实 NestJS API,创建独立 `SmsMessageRecord` 和 `SmsSubmitRecord`,并向 Redis Stream `gateway.submit.commands` 写入 `SubmitCommand`;短信记录页面能查询到该测试短信。
|
||||||
|
- 通道测试短信不得绑定企业、企业应用或 `SmsBatchTask` 发送任务;submit result 和 receipt 只更新短信记录、提交记录、回执记录和分片审计,不向客户侧推送 Deliver。
|
||||||
- 若通道未启用或没有在线 CMPP 连接,测试短信 API 返回明确错误,不能只在前端假提示成功。
|
- 若通道未启用或没有在线 CMPP 连接,测试短信 API 返回明确错误,不能只在前端假提示成功。
|
||||||
- 停用通道不会被路由选中。
|
- 停用通道不会被路由选中。
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
## 2026-07-09 运营端通道测试短信闭环修复
|
## 2026-07-09 运营端通道测试短信闭环修复
|
||||||
|
|
||||||
- 生产验证发现运营端通道“短信测试”弹窗仅关闭页面,未调用后端;`POST /api/admin/channels/:id/test` 仍返回 phase-4 placeholder,不创建 `SmsMessageRecord/SmsSubmitRecord`,也不写入 Gateway SubmitCommand,因此短信记录页面无记录。
|
- 生产验证发现运营端通道“短信测试”弹窗仅关闭页面,未调用后端;`POST /api/admin/channels/:id/test` 仍返回 phase-4 placeholder,不创建 `SmsMessageRecord/SmsSubmitRecord`,也不写入 Gateway SubmitCommand,因此短信记录页面无记录。
|
||||||
- 已修复为真实链路:前端提交手机号、内容和可选接入号;NestJS 校验通道 active 且存在在线 CMPP 连接后,创建 `SmsBatchTask`、`SmsMessageRecord`、`SmsSubmitRecord` 和操作日志,并向 BullMQ `gateway.submit.queue` 与 Redis Stream `gateway.submit.commands` 写入真实 `SubmitCommand`。
|
- 已修复为真实链路:前端提交手机号、内容和可选接入号;NestJS 校验通道 active 且存在在线 CMPP 连接后,创建独立 `SmsMessageRecord`、`SmsSubmitRecord` 和操作日志,并向 BullMQ `gateway.submit.queue` 与 Redis Stream `gateway.submit.commands` 写入真实 `SubmitCommand`。通道测试不绑定企业、企业应用或 `SmsBatchTask` 发送任务。
|
||||||
- 测试口径同步:`TC-ADMIN-003` 增加通道测试短信闭环要求,必须能从页面/API 发起真实测试短信,短信记录页面可查询到对应记录,Gateway submit worker 按通道真实 CMPP 配置消费发送。
|
- 测试口径同步:`TC-ADMIN-003` 增加通道测试短信闭环要求,必须能从页面/API 发起真实测试短信,短信记录页面可查询到对应记录,Gateway submit worker 按通道真实 CMPP 配置消费发送。
|
||||||
- 已执行:`npm --prefix api test -- channels.service.spec.ts --runInBand`、`npm --prefix api run build`、`npm run build`。待生产部署后用指定号码做一次真实发送验证,并回查 DB/短信记录。
|
- 已执行:`npm --prefix api test -- channels.service.spec.ts --runInBand`、`npm --prefix api run build`、`npm run build`。待生产部署后用指定号码做一次真实发送验证,并回查 DB/短信记录。
|
||||||
|
- 2026-07-09 追加:按产品边界收窄通道测试短信,`SmsMessageRecord/SmsSubmitRecord/SmsReceiptRecord/SmsMessageSegmentAudit` 支持 `tenantId/batchTaskId` 为空;通道测试只记录短信、提交结果和回执,不进入企业账务、发送任务进度、客户侧 Deliver 推送或业务补发。
|
||||||
|
|
||||||
## 2026-07-09 线上通道 CMPP 版本修复
|
## 2026-07-09 线上通道 CMPP 版本修复
|
||||||
|
|
||||||
|
|||||||
+32
-4
@@ -93,8 +93,7 @@ export type ChannelConnectionLogResponse = {
|
|||||||
export type ChannelTestResponse = {
|
export type ChannelTestResponse = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
status: string;
|
status: string;
|
||||||
batchTaskId: string;
|
testNo: string;
|
||||||
taskNo: string;
|
|
||||||
submitted: number;
|
submitted: number;
|
||||||
messages: Array<{
|
messages: Array<{
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
@@ -344,6 +343,7 @@ export type SmsMessageRecord = {
|
|||||||
amountCents: number;
|
amountCents: number;
|
||||||
status: string;
|
status: string;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
|
errorCode?: string | null;
|
||||||
queuedAt: string;
|
queuedAt: string;
|
||||||
submittedAt?: string | null;
|
submittedAt?: string | null;
|
||||||
deliveredAt?: string | null;
|
deliveredAt?: string | null;
|
||||||
@@ -352,8 +352,36 @@ export type SmsMessageRecord = {
|
|||||||
channel?: AdminChannel | null;
|
channel?: AdminChannel | null;
|
||||||
tenant?: TenantOption | null;
|
tenant?: TenantOption | null;
|
||||||
application?: { id: string; name: string };
|
application?: { id: string; name: string };
|
||||||
submitRecords?: Array<Record<string, unknown>>;
|
submitRecords?: SmsSubmitRecord[];
|
||||||
receiptRecords?: Array<Record<string, unknown>>;
|
receiptRecords?: SmsReceiptRecord[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SmsSubmitRecord = {
|
||||||
|
id: string;
|
||||||
|
channelId: string;
|
||||||
|
submitId: string;
|
||||||
|
sequenceId?: number | null;
|
||||||
|
gatewayMessageId?: string | null;
|
||||||
|
submitStatus: string;
|
||||||
|
errorCode?: string | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
submittedAt?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
channel?: AdminChannel | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SmsReceiptRecord = {
|
||||||
|
id: string;
|
||||||
|
channelId?: string | null;
|
||||||
|
messageId: string;
|
||||||
|
gatewayMessageId: string;
|
||||||
|
sequenceId?: number | null;
|
||||||
|
receiptStatus: string;
|
||||||
|
rawStatus: string;
|
||||||
|
errorCode?: string | null;
|
||||||
|
deliveredAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
channel?: AdminChannel | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SmsMessageSegmentAudit = {
|
export type SmsMessageSegmentAudit = {
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ function SmsTestModal({
|
|||||||
content,
|
content,
|
||||||
accessNo: accessNo.trim() || undefined,
|
accessNo: accessNo.trim() || undefined,
|
||||||
});
|
});
|
||||||
setResult(`已提交 ${response.submitted} 条测试短信,任务号 ${response.taskNo}`);
|
setResult(`已提交 ${response.submitted} 条测试短信,测试流水号 ${response.testNo}`);
|
||||||
} catch (failure) {
|
} catch (failure) {
|
||||||
setError(failure instanceof Error ? failure.message : '测试短信发送失败');
|
setError(failure instanceof Error ? failure.message : '测试短信发送失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user