feat: 完成P6-P8后端能力与P9路由重构(V1-V2,D1,D3,B1-B2,B4,I1-I4,P9A2-P9A3)

这个提交包含在:
2026-02-10 11:28:28 +08:00
父节点 bec21e1b38
当前提交 6bc09f557b
共修改 33 个文件,包含 3467 行新增和 13 行删除
+125 -5
查看文件
@@ -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,
};