R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关
这个提交包含在:
@@ -19,6 +19,13 @@ const LIST_QUERY_SCHEMA = z.object({
|
||||
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||
unreadOnly: z.enum(['0', '1', 'true', 'false']).optional(),
|
||||
});
|
||||
const DELIVERY_STATUS_VALUES = Object.freeze(['pending', 'success', 'failed']);
|
||||
const DELIVERY_QUERY_SCHEMA = z.object({
|
||||
notificationId: z.string().uuid().optional(),
|
||||
channelId: z.string().uuid().optional(),
|
||||
status: z.enum(DELIVERY_STATUS_VALUES).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
});
|
||||
|
||||
const CHANNEL_ID_SCHEMA = z.string().uuid();
|
||||
const ROLE_CODE_VALUES = Object.freeze(Object.keys(ROLE_PERMISSIONS));
|
||||
@@ -141,6 +148,20 @@ function parseListQuery(query) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseDeliveryQuery(query) {
|
||||
const parsed = DELIVERY_QUERY_SCHEMA.safeParse(query ?? {});
|
||||
if (!parsed.success) {
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
|
||||
return {
|
||||
notificationId: parsed.data.notificationId ?? null,
|
||||
channelId: parsed.data.channelId ?? null,
|
||||
status: parsed.data.status ?? null,
|
||||
limit: parsed.data.limit ?? 200,
|
||||
};
|
||||
}
|
||||
|
||||
function parseNotificationId(value) {
|
||||
const parsed = CHANNEL_ID_SCHEMA.safeParse(value);
|
||||
if (!parsed.success) {
|
||||
@@ -249,6 +270,54 @@ function mapNotificationChannel(record, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapNotificationDeliverySummary(record) {
|
||||
return {
|
||||
notificationId: record.notification_id,
|
||||
channelId: record.channel_id,
|
||||
channelName: record.channel?.name ?? '-',
|
||||
channelType: record.channel?.channel_type ?? 'wecom',
|
||||
notificationType: record.notification?.type ?? '',
|
||||
notificationTitle: record.notification?.title ?? '',
|
||||
status: record.status,
|
||||
attemptCount: 1,
|
||||
latestAttemptAt: record.attempted_at?.toISOString?.() ?? null,
|
||||
resultCode: record.result_code ?? null,
|
||||
errorMessage: record.error_message ?? null,
|
||||
responseBody: record.response_body ?? null,
|
||||
notificationCreatedAt: record.notification?.created_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeDeliveryAttempts(items, query) {
|
||||
const summaries = [];
|
||||
const summaryMap = new Map();
|
||||
const skippedKeys = new Set();
|
||||
|
||||
for (const item of items) {
|
||||
const key = `${item.notification_id}:${item.channel_id}`;
|
||||
if (skippedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const current = summaryMap.get(key);
|
||||
if (!current) {
|
||||
if (query.status && item.status !== query.status) {
|
||||
skippedKeys.add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const summary = mapNotificationDeliverySummary(item);
|
||||
summaryMap.set(key, summary);
|
||||
summaries.push(summary);
|
||||
continue;
|
||||
}
|
||||
|
||||
current.attemptCount += 1;
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
function handleNotificationChannelWriteError(error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
@@ -412,6 +481,51 @@ async function notificationsRoutes(fastify) {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/notifications/deliveries',
|
||||
{
|
||||
preHandler: fastify.requirePermission('notification.channel.manage'),
|
||||
schema: {
|
||||
tags: ['notifications'],
|
||||
summary: 'List Notification Deliveries',
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const query = parseDeliveryQuery(request.query);
|
||||
const where = {
|
||||
...(query.notificationId ? { notification_id: query.notificationId } : {}),
|
||||
...(query.channelId ? { channel_id: query.channelId } : {}),
|
||||
};
|
||||
|
||||
const items = await fastify.prisma.notificationDelivery.findMany({
|
||||
where,
|
||||
orderBy: [{ attempted_at: 'desc' }, { attempt_no: 'desc' }],
|
||||
take: query.limit,
|
||||
include: {
|
||||
channel: {
|
||||
select: {
|
||||
id: true,
|
||||
channel_type: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
notification: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
title: true,
|
||||
created_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return sendData(reply, request, {
|
||||
items: summarizeDeliveryAttempts(items, query),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/notifications/channels',
|
||||
{
|
||||
|
||||
在新工单中引用
屏蔽一个用户