feat(server): 完成 F15 邮件通知(SMTP/Nodemailer)\n\n- 新增 email 服务(EMAIL_ENABLED + SMTP_* 配置)\n- 待办/审批/付款/密码重置触发可选邮件,失败降级不阻塞\n- 更新 server/README 邮件配置说明,并推进 CSV 状态为 DONE
这个提交包含在:
@@ -8,6 +8,7 @@ const {
|
||||
createNotifications,
|
||||
resolveRecipientsForAssignee,
|
||||
} = require("../services/notifications");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const {
|
||||
buildWorkflowStepsFromApprovalRule,
|
||||
resolveActionableStep,
|
||||
@@ -1141,6 +1142,7 @@ async function reimbursementRoutes(fastify) {
|
||||
async (request, reply) => {
|
||||
const reimbursementId = parseReimbursementId(request.params?.id);
|
||||
let submitResult = null;
|
||||
const emailTasks = [];
|
||||
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
const reimbursementRows = await tx.$queryRaw`
|
||||
@@ -1365,6 +1367,14 @@ async function reimbursementRoutes(fastify) {
|
||||
targetType: "workflow_task",
|
||||
targetId: workflowTask.id,
|
||||
});
|
||||
|
||||
emailTasks.push({
|
||||
type: "workflow.task_assigned",
|
||||
recipientEmployeeIds: recipients,
|
||||
subject: `【待办】报销单 ${formNumber} 待审批`,
|
||||
body: `报销单 ${formNumber} 已提交,请及时处理。`,
|
||||
targetPath: `/workflow/tasks/${workflowTask.id}`,
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error(
|
||||
{ err: error, reimbursementId, taskId: workflowTask.id },
|
||||
@@ -1397,6 +1407,16 @@ async function reimbursementRoutes(fastify) {
|
||||
};
|
||||
});
|
||||
|
||||
for (const task of emailTasks) {
|
||||
void sendNotificationEmailSafe({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
...task,
|
||||
});
|
||||
}
|
||||
|
||||
return sendData(reply, request, submitResult);
|
||||
},
|
||||
);
|
||||
@@ -1546,6 +1566,7 @@ async function reimbursementRoutes(fastify) {
|
||||
const reimbursementId = parseReimbursementId(request.params?.id);
|
||||
const payInput = parsePayBody(request.body);
|
||||
let response = null;
|
||||
const emailTasks = [];
|
||||
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
const reimbursementRows = await tx.$queryRaw`
|
||||
@@ -1635,6 +1656,14 @@ async function reimbursementRoutes(fastify) {
|
||||
targetType: "reimbursement_form",
|
||||
targetId: reimbursementId,
|
||||
});
|
||||
|
||||
emailTasks.push({
|
||||
type: "reimbursement.paid",
|
||||
recipientEmployeeIds: recipients,
|
||||
subject: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
|
||||
body: `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`,
|
||||
targetPath: `/reimbursements/${reimbursementId}`,
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error(
|
||||
{ err: error, reimbursementId },
|
||||
@@ -1675,6 +1704,16 @@ async function reimbursementRoutes(fastify) {
|
||||
};
|
||||
});
|
||||
|
||||
for (const task of emailTasks) {
|
||||
void sendNotificationEmailSafe({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
...task,
|
||||
});
|
||||
}
|
||||
|
||||
return sendData(reply, request, response);
|
||||
},
|
||||
);
|
||||
|
||||
在新工单中引用
屏蔽一个用户