fix: harden real backend admin workflows and ui

This commit is contained in:
hectorzhao
2026-07-03 19:29:56 +08:00
parent dd09d91c1e
commit 8cca361441
71 changed files with 5111 additions and 4439 deletions
@@ -37,6 +37,11 @@ export class AdminSendChainController {
return this.sendChain.enqueueBatchTask(taskId);
}
@Post('batch-tasks/:id/terminate')
terminateTask(@Param('id') taskId: string) {
return this.sendChain.terminateBatchTask(taskId);
}
@Post('scheduled/dispatch-due')
dispatchDueScheduledTasks() {
return this.sendChain.dispatchDueScheduledTasks();
@@ -244,6 +244,23 @@ describe('SendChainService', () => {
});
});
it('terminates non-final tasks by canceling unsubmitted messages', async () => {
const { service, prisma } = createService();
prisma.smsBatchTask.findUnique.mockResolvedValue({ id: 'task-1', status: 'sending' });
service['refreshTaskProgress'] = jest.fn().mockResolvedValue(undefined);
await service.terminateBatchTask('task-1');
expect(prisma.smsMessageRecord.updateMany).toHaveBeenCalledWith({
where: { batchTaskId: 'task-1', status: { in: ['ready', 'queued', 'scheduled', 'submit_queued'] } },
data: { status: 'canceled', errorMessage: '运营终止任务,未提交号码停止发送' },
});
expect(prisma.smsBatchTask.update).toHaveBeenCalledWith({
where: { id: 'task-1' },
data: { status: 'canceled', canceledAt: expect.any(Date), rejectReason: '运营终止任务' },
});
});
it('blocks sending when enterprise certification is not approved', async () => {
const { service, prisma } = createService();
prisma.tenant.findUnique.mockResolvedValue({ id: 'tenant-1', status: 'active', certificationStatus: 'rejected' });
+26 -1
View File
@@ -237,7 +237,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
listBatchTasks(tenantId?: string, status?: string) {
return this.prisma.smsBatchTask.findMany({
where: { tenantId, status },
include: { apiRequests: true },
include: {
tenant: true,
application: true,
template: true,
apiRequests: true,
messages: { include: { channel: true }, orderBy: { queuedAt: 'asc' }, take: 100000 },
},
orderBy: { createdAt: 'desc' },
take: 100,
});
@@ -388,6 +394,25 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
});
}
async terminateBatchTask(taskId: string) {
const task = await this.prisma.smsBatchTask.findUnique({ where: { id: taskId } });
if (!task) {
throw new NotFoundException('SMS batch task not found');
}
if (['finished', 'completed', 'failed', 'canceled', 'rejected'].includes(task.status)) {
throw new BadRequestException('SMS batch task is already final');
}
await this.prisma.smsMessageRecord.updateMany({
where: { batchTaskId: taskId, status: { in: ['ready', 'queued', 'scheduled', 'submit_queued'] } },
data: { status: 'canceled', errorMessage: '运营终止任务,未提交号码停止发送' },
});
await this.refreshTaskProgress(taskId);
return this.prisma.smsBatchTask.update({
where: { id: taskId },
data: { status: 'canceled', canceledAt: new Date(), rejectReason: '运营终止任务' },
});
}
async dispatchDueScheduledTasks(now = new Date()) {
const tasks = await this.prisma.smsBatchTask.findMany({
where: { status: 'scheduled', scheduledAt: { lte: now } },