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 行删除
+33
查看文件
@@ -4,6 +4,7 @@ const { buildAuditLogData } = require("../utils/audit");
const { hashPassword, randomRefreshToken, sha256Hex, verifyPassword } = require("../utils/crypto");
const { validatePassword } = require("../utils/passwordPolicy");
const { sendData } = require("../utils/reply");
const { createNotifications } = require("../services/notifications");
function getHeaderString(request, name) {
const value = request.headers[name];
@@ -661,6 +662,22 @@ async function authRoutes(fastify) {
}),
});
try {
await createNotifications(fastify.prisma, {
recipientEmployeeIds: [employee.id],
type: "auth.password_reset_requested",
title: "【安全】收到密码重置请求",
body: "如非本人操作,请尽快联系管理员核查账号安全。",
targetType: "employee",
targetId: employee.id,
});
} catch (error) {
fastify.log.error(
{ err: error, employeeId: employee.id, requestId: request.id },
"Failed to create password reset requested notifications",
);
}
if (fastify.config.devShowPasswordResetCode) {
fastify.log.info(
{ employeeId: employee.id, email, code, requestId: request.id },
@@ -763,6 +780,22 @@ async function authRoutes(fastify) {
extra: {},
}),
});
try {
await createNotifications(tx, {
recipientEmployeeIds: [employee.id],
type: "auth.password_reset_succeeded",
title: "【安全】密码已重置",
body: "你的账号密码已重置成功。如非本人操作,请立即联系管理员。",
targetType: "employee",
targetId: employee.id,
});
} catch (error) {
fastify.log.error(
{ err: error, employeeId: employee.id, requestId: request.id },
"Failed to create password reset succeeded notifications",
);
}
});
reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config));