feat: 支持通知 outbox 手动重发
This commit is contained in:
@@ -147,6 +147,22 @@ export class NotificationOutboxService {
|
||||
return messages.map((message) => this.toSummary(message));
|
||||
}
|
||||
|
||||
async getMessage(
|
||||
id: string,
|
||||
): Promise<NotificationOutboxMessage | undefined> {
|
||||
const prisma = this.databaseClient();
|
||||
|
||||
if (!prisma) {
|
||||
return this.memoryMessages.find((message) => message.id === id);
|
||||
}
|
||||
|
||||
const message = await prisma.outboxMessage.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return message ? this.toSummary(message) : undefined;
|
||||
}
|
||||
|
||||
async listRetryableMessages(
|
||||
input: ListRetryableNotificationOutboxInput,
|
||||
): Promise<NotificationOutboxMessage[]> {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 通知 outbox 手动重发入口只允许超级管理员触发真实 provider 投递。
|
||||
*/
|
||||
import { Controller, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminOnlyMessage } from '../../auth/admin-only-message.decorator';
|
||||
import { AuthGuard } from '../../auth/auth.guard';
|
||||
import { AuthenticatedUser } from '../../auth/auth.types';
|
||||
import { CurrentUser } from '../../auth/current-user.decorator';
|
||||
import { PasswordChangeGuard } from '../../auth/password-change.guard';
|
||||
import { SuperAdminGuard } from '../../auth/super-admin.guard';
|
||||
import { NotificationOutboxMessage } from '../outbox/notification-outbox.types';
|
||||
import { NotificationOutboxManualRetryService } from './notification-outbox-manual-retry.service';
|
||||
|
||||
@ApiTags('通知 outbox')
|
||||
@Controller('notification-outbox')
|
||||
@UseGuards(AuthGuard, PasswordChangeGuard, SuperAdminGuard)
|
||||
@AdminOnlyMessage('无权限操作通知 outbox')
|
||||
export class NotificationOutboxManualRetryController {
|
||||
constructor(
|
||||
private readonly manualRetryService: NotificationOutboxManualRetryService,
|
||||
) {}
|
||||
|
||||
@Post(':id/retry')
|
||||
@ApiOkResponse({ description: '手动重发失败或已终止的通知 outbox。' })
|
||||
async retryMessage(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthenticatedUser,
|
||||
): Promise<NotificationOutboxMessage> {
|
||||
return this.manualRetryService.retryMessage(id, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 通知 outbox 手动重发服务用于管理员在配置 webhook 后补投历史失败消息。
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuditService } from '../../audit/audit.service';
|
||||
import { AuthenticatedUser } from '../../auth/auth.types';
|
||||
import { AppError } from '../../common/errors/app-error';
|
||||
import { redactSensitive } from '../../common/security/redact-sensitive';
|
||||
import { NotificationOutboxService } from '../outbox/notification-outbox.service';
|
||||
import { NotificationOutboxMessage } from '../outbox/notification-outbox.types';
|
||||
import { DeployNotificationService } from './deploy-notification.service';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationOutboxManualRetryService {
|
||||
constructor(
|
||||
private readonly notificationOutboxService: NotificationOutboxService,
|
||||
private readonly deployNotificationService: DeployNotificationService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
async retryMessage(
|
||||
id: string,
|
||||
operator: AuthenticatedUser,
|
||||
): Promise<NotificationOutboxMessage> {
|
||||
const message = await this.notificationOutboxService.getMessage(id);
|
||||
|
||||
if (!message) {
|
||||
throw new AppError('RESOURCE_NOT_FOUND', '通知 outbox 记录不存在', 404, {
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
if (message.status !== 'failed' && message.status !== 'dead') {
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'只有失败或已终止的通知 outbox 可以手动重发',
|
||||
400,
|
||||
{
|
||||
id,
|
||||
status: message.status,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result =
|
||||
await this.deployNotificationService.retryOutboxMessage(message);
|
||||
const updated =
|
||||
(await this.notificationOutboxService.getMessage(id)) ?? message;
|
||||
|
||||
await this.auditService.record({
|
||||
action: `NOTIFICATION_OUTBOX_MANUAL_RETRY_${result.status.toUpperCase()}`,
|
||||
resourceType: 'notification_outbox',
|
||||
resourceId: id,
|
||||
actorId: operator.id,
|
||||
actorName: operator.account,
|
||||
before: this.auditSnapshot(message),
|
||||
after: {
|
||||
...this.auditSnapshot(updated),
|
||||
channel: result.channel,
|
||||
reason: result.reason,
|
||||
},
|
||||
});
|
||||
|
||||
return updated;
|
||||
} catch (error) {
|
||||
const messageText =
|
||||
error instanceof Error ? error.message : '通知 outbox 手动重发失败';
|
||||
|
||||
await this.auditService.record({
|
||||
action: 'NOTIFICATION_OUTBOX_MANUAL_RETRY_FAILED',
|
||||
resourceType: 'notification_outbox',
|
||||
resourceId: id,
|
||||
actorId: operator.id,
|
||||
actorName: operator.account,
|
||||
before: this.auditSnapshot(message),
|
||||
after: {
|
||||
message: redactSensitive({ message: messageText }).message,
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private auditSnapshot(
|
||||
message: NotificationOutboxMessage,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
status: message.status,
|
||||
attemptCount: message.attemptCount,
|
||||
nextAttemptAt: message.nextAttemptAt,
|
||||
lastError: message.lastError,
|
||||
channel: message.channel,
|
||||
template: message.template,
|
||||
deployRunId: message.deployRunId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NotificationOutboxModule } from '../outbox/notification-outbox.module';
|
||||
import { DeployNotificationService } from './deploy-notification.service';
|
||||
import { NotificationOutboxManualRetryController } from './notification-outbox-manual-retry.controller';
|
||||
import { NotificationOutboxManualRetryService } from './notification-outbox-manual-retry.service';
|
||||
import { NotificationOutboxRetryService } from './notification-outbox-retry.service';
|
||||
import { WeComClient } from './wecom.client';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationOutboxModule],
|
||||
controllers: [NotificationOutboxManualRetryController],
|
||||
providers: [
|
||||
WeComClient,
|
||||
DeployNotificationService,
|
||||
NotificationOutboxManualRetryService,
|
||||
NotificationOutboxRetryService,
|
||||
],
|
||||
exports: [WeComClient, DeployNotificationService],
|
||||
|
||||
Reference in New Issue
Block a user