R35-2 审批通知补结构化摘要

这个提交包含在:
2026-03-25 11:38:00 +08:00
父节点 a962f4b074
当前提交 6fba13573e
共修改 12 个文件,包含 1243 行新增和 266 行删除
+95 -123
查看文件
@@ -16,6 +16,14 @@ const {
createNotifications,
resolveRecipientsForAssignee,
} = require("../services/notifications");
const {
buildApprovedCopy,
buildPendingTaskCopy,
buildRejectedCopy,
buildReturnedCopy,
buildWorkflowEntityMeta,
normalizeWorkflowEntityType,
} = require("../services/notifications/workflowCopy");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { sendNotificationEmailSafe } = require("../services/email");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
@@ -423,92 +431,19 @@ async function loadReimbursementNotifyContext(tx, reimbursementId) {
select: {
id: true,
form_number: true,
total_amount: true,
owner_id: true,
submitter_id: true,
owner: {
select: {
id: true,
name: true,
},
},
},
});
}
function getWorkflowEntityMeta(entityType, entityRecord, entityId) {
const normalizedEntityType = entityType === "procurement" ? "purchase_request" : entityType;
if (normalizedEntityType === "reimbursement") {
return {
label: "报销单",
reference: entityRecord?.form_number ?? entityId,
targetType: "reimbursement_form",
detailPath: `/reimbursements/${entityId}`,
};
}
if (normalizedEntityType === "leave") {
return {
label: "请假单",
reference: entityId,
targetType: "leave_request",
detailPath: "/leave-requests",
};
}
if (normalizedEntityType === "purchase_request") {
return {
label: "采购申请",
reference: entityRecord?.request_no ?? entityId,
targetType: "purchase_request",
detailPath: "/purchase-requests",
};
}
if (normalizedEntityType === "travel_request") {
return {
label: "差旅申请",
reference: entityRecord?.request_no ?? entityId,
targetType: "travel_request",
detailPath: `/travel-requests?id=${entityId}`,
};
}
return {
label: "审批单",
reference: entityId,
targetType: normalizedEntityType,
detailPath: `/workflow/tasks/${entityId}`,
};
}
function buildPendingTaskCopy(entityMeta, { transfer = false } = {}) {
return {
title: `【待办】${entityMeta.label} ${entityMeta.reference} ${transfer ? "转审待处理" : "待审批"}`,
body: transfer
? `${entityMeta.label} ${entityMeta.reference} 已转审给你,请及时处理。`
: `${entityMeta.label} ${entityMeta.reference} 已流转到你,请及时处理。`,
};
}
function buildApprovedCopy(entityMeta) {
return {
title: `【已批准】${entityMeta.label} ${entityMeta.reference}`,
body:
entityMeta.targetType === "reimbursement_form"
? `${entityMeta.label} ${entityMeta.reference} 已审批通过,等待财务付款。`
: `${entityMeta.label} ${entityMeta.reference} 已审批通过。`,
};
}
function buildRejectedCopy(entityMeta, comment) {
return {
title: `【已驳回】${entityMeta.label} ${entityMeta.reference}`,
body: `${entityMeta.label} ${entityMeta.reference} 已被驳回:${comment}`,
};
}
function buildReturnedCopy(entityMeta, comment) {
return {
title: `【已退回】${entityMeta.label} ${entityMeta.reference}`,
body: `${entityMeta.label} ${entityMeta.reference} 已退回:${comment}`,
};
}
function uniq(values) {
return Array.from(new Set((values ?? []).filter(Boolean)));
}
@@ -519,6 +454,22 @@ async function loadLeaveNotifyContext(tx, leaveRequestId) {
select: {
id: true,
owner_id: true,
start_date: true,
end_date: true,
requested_days: true,
leave_type: {
select: {
id: true,
code: true,
name: true,
},
},
owner: {
select: {
id: true,
name: true,
},
},
},
});
}
@@ -529,8 +480,15 @@ async function loadPurchaseNotifyContext(tx, purchaseRequestId) {
select: {
id: true,
request_no: true,
total_amount: true,
applicant_id: true,
submitter_id: true,
applicant: {
select: {
id: true,
name: true,
},
},
},
});
}
@@ -541,14 +499,21 @@ async function loadTravelNotifyContext(tx, travelRequestId) {
select: {
id: true,
request_no: true,
estimated_amount: true,
applicant_employee_id: true,
submitter_id: true,
applicant: {
select: {
id: true,
name: true,
},
},
},
});
}
async function loadWorkflowNotifyContext(tx, entityType, entityId) {
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
if (normalizedEntityType === 'reimbursement') {
const record = await loadReimbursementNotifyContext(tx, entityId);
@@ -589,7 +554,7 @@ async function loadWorkflowNotifyContext(tx, entityType, entityId) {
}
function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
if (outcome === 'approved') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementApproved;
@@ -673,7 +638,7 @@ async function queueWorkflowOutcomeNotifications({
outcome,
comment = null,
}) {
const normalizedEntityType = workflowInstance.entity_type === 'procurement' ? 'purchase_request' : workflowInstance.entity_type;
const normalizedEntityType = normalizeWorkflowEntityType(workflowInstance.entity_type);
const notificationType = resolveWorkflowOutcomeNotificationType(normalizedEntityType, outcome);
if (!notificationType) {
return;
@@ -685,7 +650,7 @@ async function queueWorkflowOutcomeNotifications({
return;
}
const entityMeta = getWorkflowEntityMeta(normalizedEntityType, context.record, workflowInstance.entity_id);
const entityMeta = buildWorkflowEntityMeta(normalizedEntityType, context.record, workflowInstance.entity_id);
const copy =
outcome === 'approved'
? buildApprovedCopy(entityMeta)
@@ -1476,42 +1441,41 @@ async function workflowRoutes(fastify) {
},
});
if (workflowInstance.entity_type === "reimbursement") {
try {
const reimbursement = await loadReimbursementNotifyContext(
tx,
workflowInstance.entity_id,
);
const entityMeta = getWorkflowEntityMeta(
workflowInstance.entity_type,
reimbursement,
workflowInstance.entity_id,
);
const pendingCopy = buildPendingTaskCopy(entityMeta);
const recipients = await resolveRecipientsForAssignee(tx, {
assigneeId: actionable.assigneeId,
assigneeRole: actionable.assigneeRole,
});
try {
const notificationContext = await loadWorkflowNotifyContext(
tx,
workflowInstance.entity_type,
workflowInstance.entity_id,
);
const entityMeta = buildWorkflowEntityMeta(
workflowInstance.entity_type,
notificationContext.record,
workflowInstance.entity_id,
);
const pendingCopy = buildPendingTaskCopy(entityMeta);
const recipients = await resolveRecipientsForAssignee(tx, {
assigneeId: actionable.assigneeId,
assigneeRole: actionable.assigneeRole,
});
await queueTaskAssignedNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
recipientEmployeeIds: recipients,
title: pendingCopy.title,
body: pendingCopy.body,
taskId: nextTask.id,
entityType: workflowInstance.entity_type,
entityId: workflowInstance.entity_id,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId: nextTask.id },
"Failed to create task assigned notifications",
);
}
await queueTaskAssignedNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
recipientEmployeeIds: recipients,
title: pendingCopy.title,
body: pendingCopy.body,
taskId: nextTask.id,
entityType: workflowInstance.entity_type,
entityId: workflowInstance.entity_id,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId: nextTask.id },
"Failed to create task assigned notifications",
);
}
result = {
@@ -2138,10 +2102,14 @@ async function workflowRoutes(fastify) {
});
try {
const reimbursement = await loadReimbursementNotifyContext(tx, workflowInstance.entity_id);
const entityMeta = getWorkflowEntityMeta(
const notificationContext = await loadWorkflowNotifyContext(
tx,
workflowInstance.entity_type,
reimbursement,
workflowInstance.entity_id,
);
const entityMeta = buildWorkflowEntityMeta(
workflowInstance.entity_type,
notificationContext.record,
workflowInstance.entity_id,
);
const pendingCopy = buildPendingTaskCopy(entityMeta, { transfer: true });
@@ -2195,8 +2163,12 @@ async function workflowRoutes(fastify) {
module.exports = workflowRoutes;
module.exports.__testables = {
buildApprovedCopy,
buildPendingTaskCopy,
buildRejectedCopy,
buildReturnedCopy,
buildWorkflowEntityMeta,
handlePurchaseApprovalErpSync,
listAllowedActionsForTask,
getWorkflowEntityMeta,
resolveWorkflowOutcomeNotificationType,
};