feat(server): 站内通知基础设施与关键事件触发(F14,F30)

- 新增 notifications 表与 API(list/mark-read)
- submit/approve/reject/return/transfer/pay/forgot-password 写入站内通知
- 通知失败降级,不阻塞主流程
这个提交包含在:
2026-02-09 17:45:17 +08:00
父节点 8bd068f2a3
当前提交 5faf843cee
共修改 8 个文件,包含 495 行新增和 1 行删除
@@ -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,
};