R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关
这个提交包含在:
@@ -5,17 +5,17 @@ const { buildAuditLogData } = require("../utils/audit");
|
||||
const { currentDateString, parseDateOnlyString } = require("../utils/date");
|
||||
const { findActiveDelegationForDelegate } = require("../utils/delegation");
|
||||
const {
|
||||
createNotifications,
|
||||
createNotificationsWithRecords,
|
||||
resolveRecipientsForAssignee,
|
||||
} = require("../services/notifications");
|
||||
const {
|
||||
buildPendingTaskCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
} = require("../services/notifications/workflowCopy");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { enqueueReimbursementJournalExport } = require("../services/integrations/erpJournal");
|
||||
const { dispatchNotificationBatch, flushNotificationDispatchTasks } = require("../services/notificationDispatcher");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
const { ensureCutoverAllowed } = require("../services/cutover");
|
||||
const { resolveApplicableBudgetPoolByContext } = require("../services/budgetPool");
|
||||
@@ -450,8 +450,8 @@ function buildSkippedNotificationResult(reason) {
|
||||
return { skipped: true, reason };
|
||||
}
|
||||
|
||||
function buildSkippedImResult(reason) {
|
||||
return { queued: 0, duplicated: 0, matchedChannels: [], skippedReason: reason };
|
||||
function buildSkippedDispatchResult(reason) {
|
||||
return { deliveredChannels: [], skippedReason: reason, notificationCount: 0, eligibleRecipientCount: 0, matchedChannelCount: 0 };
|
||||
}
|
||||
|
||||
function buildSkippedEmailResult(reason, failed = 0) {
|
||||
@@ -488,8 +488,8 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
actorId,
|
||||
loadSummary = loadReimbursementSubmitSummary,
|
||||
resolveRecipients = resolveRecipientsForAssignee,
|
||||
createAppNotifications = createNotifications,
|
||||
enqueueIm = enqueueImNotifications,
|
||||
createAppNotifications = createNotificationsWithRecords,
|
||||
dispatchNotifications = dispatchNotificationBatch,
|
||||
sendEmailSafe = sendNotificationEmailSafe,
|
||||
}) {
|
||||
let copy = buildPendingTaskCopy(
|
||||
@@ -540,7 +540,7 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
return {
|
||||
recipientEmployeeIds: [],
|
||||
notification: buildSkippedNotificationResult("recipient_resolution_failed"),
|
||||
im: buildSkippedImResult("recipient_resolution_failed"),
|
||||
dispatch: buildSkippedDispatchResult("recipient_resolution_failed"),
|
||||
email: buildSkippedEmailResult("recipient_resolution_failed"),
|
||||
};
|
||||
}
|
||||
@@ -549,14 +549,15 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
return {
|
||||
recipientEmployeeIds: [],
|
||||
notification: buildSkippedNotificationResult("no_recipients"),
|
||||
im: buildSkippedImResult("no_recipients"),
|
||||
dispatch: buildSkippedDispatchResult("no_recipients"),
|
||||
email: buildSkippedEmailResult("no_recipients"),
|
||||
};
|
||||
}
|
||||
|
||||
let notification = buildSkippedNotificationResult("not_attempted");
|
||||
let notificationRecords = [];
|
||||
try {
|
||||
notification = await createAppNotifications(prisma, {
|
||||
notificationRecords = await createAppNotifications(prisma, {
|
||||
recipientEmployeeIds,
|
||||
type: NOTIFICATION_TYPES.workflowTaskAssigned,
|
||||
title,
|
||||
@@ -564,6 +565,7 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
targetType: "workflow_task",
|
||||
targetId: workflowTaskId,
|
||||
});
|
||||
notification = { count: notificationRecords.length };
|
||||
} catch (error) {
|
||||
logger?.error?.(
|
||||
{
|
||||
@@ -578,20 +580,20 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
notification = buildSkippedNotificationResult("notification_failed");
|
||||
}
|
||||
|
||||
let im = buildSkippedImResult("not_attempted");
|
||||
let dispatch = buildSkippedDispatchResult("not_attempted");
|
||||
try {
|
||||
im = await enqueueIm(prisma, {
|
||||
dispatch = await dispatchNotifications({
|
||||
prisma,
|
||||
config,
|
||||
logger,
|
||||
recipientEmployeeIds,
|
||||
requestId,
|
||||
notifications: notificationRecords,
|
||||
type: NOTIFICATION_TYPES.workflowTaskAssigned,
|
||||
title,
|
||||
body,
|
||||
targetType: "workflow_task",
|
||||
targetId: workflowTaskId,
|
||||
targetPath,
|
||||
entityType: "reimbursement",
|
||||
entityId: reimbursementId,
|
||||
actorId,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -603,9 +605,9 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
workflowTaskId,
|
||||
recipients: recipientEmployeeIds.length,
|
||||
},
|
||||
"Failed to enqueue reimbursement submit IM notifications",
|
||||
"Failed to dispatch reimbursement submit notifications",
|
||||
);
|
||||
im = buildSkippedImResult("enqueue_im_failed");
|
||||
dispatch = buildSkippedDispatchResult("dispatch_failed");
|
||||
}
|
||||
|
||||
let email = buildSkippedEmailResult("not_attempted");
|
||||
@@ -639,7 +641,7 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
return {
|
||||
recipientEmployeeIds,
|
||||
notification,
|
||||
im,
|
||||
dispatch,
|
||||
email,
|
||||
};
|
||||
}
|
||||
@@ -2042,6 +2044,7 @@ async function reimbursementRoutes(fastify) {
|
||||
const payInput = parsePayBody(request.body);
|
||||
let response = null;
|
||||
const emailTasks = [];
|
||||
const dispatchTasks = [];
|
||||
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
const reimbursementRows = await tx.$queryRaw`
|
||||
@@ -2196,7 +2199,7 @@ async function reimbursementRoutes(fastify) {
|
||||
const recipients = Array.from(
|
||||
new Set([reimbursement.owner_id, reimbursement.submitter_id].filter(Boolean)),
|
||||
);
|
||||
await createNotifications(tx, {
|
||||
const notifications = await createNotificationsWithRecords(tx, {
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.reimbursementPaid,
|
||||
title: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
|
||||
@@ -2204,19 +2207,14 @@ async function reimbursementRoutes(fastify) {
|
||||
targetType: "reimbursement_form",
|
||||
targetId: reimbursementId,
|
||||
});
|
||||
|
||||
await enqueueImNotifications(tx, {
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
recipientEmployeeIds: recipients,
|
||||
dispatchTasks.push({
|
||||
notifications,
|
||||
type: NOTIFICATION_TYPES.reimbursementPaid,
|
||||
title: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
|
||||
body: `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`,
|
||||
targetType: "reimbursement_form",
|
||||
targetId: reimbursementId,
|
||||
targetPath: `/reimbursements/${reimbursementId}`,
|
||||
entityType: "reimbursement",
|
||||
entityId: reimbursementId,
|
||||
actorId: request.employee.id,
|
||||
});
|
||||
|
||||
@@ -2276,6 +2274,13 @@ async function reimbursementRoutes(fastify) {
|
||||
...task,
|
||||
});
|
||||
}
|
||||
await flushNotificationDispatchTasks({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
tasks: dispatchTasks,
|
||||
});
|
||||
|
||||
return sendData(reply, request, response);
|
||||
},
|
||||
|
||||
在新工单中引用
屏蔽一个用户