feat(server): 站内通知基础设施与关键事件触发(F14,F30)
- 新增 notifications 表与 API(list/mark-read) - submit/approve/reject/return/transfer/pay/forgot-password 写入站内通知 - 通知失败降级,不阻塞主流程
这个提交包含在:
@@ -0,0 +1,56 @@
|
||||
function uniq(values) {
|
||||
return Array.from(new Set((values ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
async function resolveRecipientsForAssignee(tx, params) {
|
||||
if (params.assigneeId) {
|
||||
return [params.assigneeId];
|
||||
}
|
||||
|
||||
if (params.assigneeRole) {
|
||||
const rows = await tx.employeeRole.findMany({
|
||||
where: {
|
||||
role_code: params.assigneeRole,
|
||||
employee: {
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
select: {
|
||||
employee_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => row.employee_id);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function createNotifications(tx, input) {
|
||||
const recipientIds = uniq(input.recipientEmployeeIds);
|
||||
if (recipientIds.length === 0) {
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
const title = String(input.title ?? "").slice(0, 200);
|
||||
const body = input.body != null ? String(input.body).slice(0, 5000) : null;
|
||||
const type = String(input.type ?? "").slice(0, 64);
|
||||
const targetType = input.targetType != null ? String(input.targetType).slice(0, 64) : null;
|
||||
|
||||
return tx.notification.createMany({
|
||||
data: recipientIds.map((recipientId) => ({
|
||||
recipient_employee_id: recipientId,
|
||||
type,
|
||||
title,
|
||||
body,
|
||||
target_type: targetType,
|
||||
target_id: input.targetId ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createNotifications,
|
||||
resolveRecipientsForAssignee,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户