R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关

这个提交包含在:
2026-04-05 11:21:39 +08:00
父节点 ff0a677bf3
当前提交 47e10e6acc
共修改 24 个文件,包含 1701 行新增和 197 行删除
+30 -25
查看文件
@@ -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);
},