feat: 完成P6-P8后端能力与P9路由重构(V1-V2,D1,D3,B1-B2,B4,I1-I4,P9A2-P9A3)
这个提交包含在:
@@ -1,5 +1,7 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
const { sendInstantMessageByChannel } = require("../notifications/channels");
|
||||
|
||||
function resolveEmailBaseUrl(config) {
|
||||
const explicit = String(config.emailBaseUrl ?? "").trim();
|
||||
if (explicit) {
|
||||
@@ -51,9 +53,21 @@ function getTransporter(config) {
|
||||
return cachedTransporter;
|
||||
}
|
||||
|
||||
function uniq(values) {
|
||||
return Array.from(new Set((values ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
async function sendNotificationEmailSafe(params) {
|
||||
try {
|
||||
return await sendNotificationEmail(params);
|
||||
const [emailResult, imResult] = await Promise.all([
|
||||
sendNotificationEmail(params),
|
||||
sendNotificationIm(params),
|
||||
]);
|
||||
|
||||
return {
|
||||
email: emailResult,
|
||||
im: imResult,
|
||||
};
|
||||
} catch (error) {
|
||||
params.logger?.error?.(
|
||||
{
|
||||
@@ -62,9 +76,12 @@ async function sendNotificationEmailSafe(params) {
|
||||
notificationType: params.type,
|
||||
recipients: params.recipientEmployeeIds?.length ?? 0,
|
||||
},
|
||||
"Failed to send notification email",
|
||||
"Failed to send notification",
|
||||
);
|
||||
return { skipped: false, sent: 0, failed: params.recipientEmployeeIds?.length ?? 0 };
|
||||
return {
|
||||
email: { skipped: false, sent: 0, failed: params.recipientEmployeeIds?.length ?? 0 },
|
||||
im: { skipped: false, sent: 0, failed: params.recipientEmployeeIds?.length ?? 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +106,7 @@ async function sendNotificationEmail({
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const recipients = Array.from(new Set((recipientEmployeeIds ?? []).filter(Boolean)));
|
||||
const recipients = uniq(recipientEmployeeIds);
|
||||
if (!recipients.length) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
@@ -158,8 +175,111 @@ async function sendNotificationEmail({
|
||||
return { skipped: false, sent, failed };
|
||||
}
|
||||
|
||||
async function sendNotificationIm({
|
||||
prisma,
|
||||
config,
|
||||
logger,
|
||||
requestId,
|
||||
recipientEmployeeIds,
|
||||
subject,
|
||||
body,
|
||||
type = "notification",
|
||||
targetPath,
|
||||
}) {
|
||||
if (!config.imNotifierEnabled) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const notificationType = String(type ?? "notification");
|
||||
const allowedTypePrefixes = ["reimbursement."];
|
||||
const allowedTypes = new Set(["workflow.task_assigned"]);
|
||||
|
||||
const allowed =
|
||||
allowedTypes.has(notificationType) ||
|
||||
allowedTypePrefixes.some((prefix) => notificationType.startsWith(prefix));
|
||||
|
||||
if (!allowed) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const recipients = uniq(recipientEmployeeIds);
|
||||
if (!recipients.length) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const channels = await prisma.notificationChannel.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
channel_type: {
|
||||
in: ["dingtalk", "wechat", "wechat_work", "wecom"],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
channel_type: true,
|
||||
name: true,
|
||||
enabled: true,
|
||||
config_json: true,
|
||||
template_json: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channels.length) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const safeSubject = String(subject ?? "").trim().slice(0, 200) || "系统通知";
|
||||
const safeBody = String(body ?? "").trim();
|
||||
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const channel of channels) {
|
||||
try {
|
||||
const result = await sendInstantMessageByChannel({
|
||||
config,
|
||||
channel,
|
||||
payload: {
|
||||
title: safeSubject,
|
||||
body: safeBody,
|
||||
targetType: type,
|
||||
targetId: targetPath ?? null,
|
||||
recipientCount: recipients.length,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
sent += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
logger?.warn?.(
|
||||
{
|
||||
requestId,
|
||||
channelId: channel.id,
|
||||
channelType: channel.channel_type,
|
||||
resultCode: result.code,
|
||||
},
|
||||
"IM notification skipped",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
logger?.error?.(
|
||||
{
|
||||
err: error,
|
||||
requestId,
|
||||
channelId: channel.id,
|
||||
channelType: channel.channel_type,
|
||||
},
|
||||
"IM notification send failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { skipped: false, sent, failed };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationEmailSafe,
|
||||
sendNotificationEmail,
|
||||
};
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户