feat(P19IM1-3,P19IM1-4): 接入业务事件与通知渠道后台
这个提交包含在:
@@ -3,6 +3,10 @@ const { z } = require("zod");
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { buildAuditLogData } = require("../utils/audit");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { createNotifications } = require("../services/notifications");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
|
||||
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
const NOTE_SCHEMA = z.string().trim().max(500).nullable().optional();
|
||||
@@ -72,6 +76,10 @@ function normalizeLocation(value) {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function uniq(values) {
|
||||
return Array.from(new Set((values ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
function ensureMovementAllowed(asset) {
|
||||
if (BLOCKED_MOVEMENT_STATUSES.has(asset.status)) {
|
||||
throw buildValidationError({ status: "movement_not_allowed" }, 409);
|
||||
@@ -195,6 +203,7 @@ async function assetMovementsRoutes(fastify) {
|
||||
const assetId = parseUuid("id", request.params?.id);
|
||||
const body = parseBody(ASSIGN_BODY_SCHEMA, request.body);
|
||||
const note = normalizeNote(body.note);
|
||||
const emailTasks = [];
|
||||
|
||||
const result = await fastify.prisma.$transaction(async (tx) => {
|
||||
const asset = await ensureAssetExists(tx, assetId);
|
||||
@@ -249,9 +258,52 @@ async function assetMovementsRoutes(fastify) {
|
||||
}),
|
||||
});
|
||||
|
||||
const recipients = uniq([employee.id]);
|
||||
const title = `【资产领用】资产卡片 ${asset.asset_no}`;
|
||||
const bodyText = `资产卡片 ${asset.asset_no} 已分配给你,请在资产台账中查看。`;
|
||||
await createNotifications(tx, {
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetAssigned,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
});
|
||||
await enqueueImNotifications(tx, {
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetAssigned,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
targetPath: "/assets",
|
||||
entityType: "asset_card",
|
||||
entityId: asset.id,
|
||||
actorId: request.employee.id,
|
||||
});
|
||||
emailTasks.push({
|
||||
type: NOTIFICATION_TYPES.assetAssigned,
|
||||
recipientEmployeeIds: recipients,
|
||||
subject: title,
|
||||
body: bodyText,
|
||||
targetPath: "/assets",
|
||||
});
|
||||
|
||||
return { updatedAsset, assignmentId: assignment.id };
|
||||
});
|
||||
|
||||
for (const task of emailTasks) {
|
||||
void sendNotificationEmailSafe({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
...task,
|
||||
});
|
||||
}
|
||||
|
||||
return sendData(reply, request, {
|
||||
assetId: result.updatedAsset.id,
|
||||
assetNo: result.updatedAsset.asset_no,
|
||||
@@ -273,6 +325,7 @@ async function assetMovementsRoutes(fastify) {
|
||||
const assetId = parseUuid("id", request.params?.id);
|
||||
const body = parseBody(RETURN_BODY_SCHEMA, request.body);
|
||||
const note = normalizeNote(body.note);
|
||||
const emailTasks = [];
|
||||
|
||||
const result = await fastify.prisma.$transaction(async (tx) => {
|
||||
const asset = await ensureAssetExists(tx, assetId);
|
||||
@@ -312,9 +365,52 @@ async function assetMovementsRoutes(fastify) {
|
||||
}),
|
||||
});
|
||||
|
||||
const recipients = uniq([asset.assignee_employee_id]);
|
||||
const title = `【资产归还】资产卡片 ${asset.asset_no}`;
|
||||
const bodyText = `资产卡片 ${asset.asset_no} 已完成归还。`;
|
||||
await createNotifications(tx, {
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetReturned,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
});
|
||||
await enqueueImNotifications(tx, {
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetReturned,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
targetPath: "/assets",
|
||||
entityType: "asset_card",
|
||||
entityId: asset.id,
|
||||
actorId: request.employee.id,
|
||||
});
|
||||
emailTasks.push({
|
||||
type: NOTIFICATION_TYPES.assetReturned,
|
||||
recipientEmployeeIds: recipients,
|
||||
subject: title,
|
||||
body: bodyText,
|
||||
targetPath: "/assets",
|
||||
});
|
||||
|
||||
return { updatedAsset, assignmentId: closedAssignment?.id ?? null };
|
||||
});
|
||||
|
||||
for (const task of emailTasks) {
|
||||
void sendNotificationEmailSafe({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
...task,
|
||||
});
|
||||
}
|
||||
|
||||
return sendData(reply, request, {
|
||||
assetId: result.updatedAsset.id,
|
||||
assetNo: result.updatedAsset.asset_no,
|
||||
@@ -339,6 +435,7 @@ async function assetMovementsRoutes(fastify) {
|
||||
const hasEmployeeField = Object.prototype.hasOwnProperty.call(body, "toEmployeeId");
|
||||
const hasLocationField = Object.prototype.hasOwnProperty.call(body, "toLocation");
|
||||
const nextLocation = hasLocationField ? normalizeLocation(body.toLocation) : undefined;
|
||||
const emailTasks = [];
|
||||
|
||||
const result = await fastify.prisma.$transaction(async (tx) => {
|
||||
const asset = await ensureAssetExists(tx, assetId);
|
||||
@@ -428,9 +525,55 @@ async function assetMovementsRoutes(fastify) {
|
||||
}),
|
||||
});
|
||||
|
||||
const recipients = uniq([
|
||||
currentEmployeeId,
|
||||
employeeChange ? requestedEmployeeId : currentEmployeeId,
|
||||
]);
|
||||
const title = `【资产调拨】资产卡片 ${asset.asset_no}`;
|
||||
const bodyText = `资产卡片 ${asset.asset_no} 已完成调拨,请在资产台账中查看最新归属与位置。`;
|
||||
await createNotifications(tx, {
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetTransferred,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
});
|
||||
await enqueueImNotifications(tx, {
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
recipientEmployeeIds: recipients,
|
||||
type: NOTIFICATION_TYPES.assetTransferred,
|
||||
title,
|
||||
body: bodyText,
|
||||
targetType: "asset_card",
|
||||
targetId: asset.id,
|
||||
targetPath: "/assets",
|
||||
entityType: "asset_card",
|
||||
entityId: asset.id,
|
||||
actorId: request.employee.id,
|
||||
});
|
||||
emailTasks.push({
|
||||
type: NOTIFICATION_TYPES.assetTransferred,
|
||||
recipientEmployeeIds: recipients,
|
||||
subject: title,
|
||||
body: bodyText,
|
||||
targetPath: "/assets",
|
||||
});
|
||||
|
||||
return { updatedAsset, transferId: transfer.id, assignmentId };
|
||||
});
|
||||
|
||||
for (const task of emailTasks) {
|
||||
void sendNotificationEmailSafe({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
requestId: request.id,
|
||||
...task,
|
||||
});
|
||||
}
|
||||
|
||||
return sendData(reply, request, {
|
||||
assetId: result.updatedAsset.id,
|
||||
assetNo: result.updatedAsset.asset_no,
|
||||
|
||||
在新工单中引用
屏蔽一个用户