R18-4/R21-2: 收口企微脱敏通知与 Portal 搜索摘要
这个提交包含在:
@@ -28,6 +28,15 @@ export type PortalHomePayload = {
|
|||||||
weeklyReport: { weekStart: string; status: "draft" | "submitted"; needsHelp: boolean; href: string } | null;
|
weeklyReport: { weekStart: string; status: "draft" | "submitted"; needsHelp: boolean; href: string } | null;
|
||||||
weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null;
|
weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null;
|
||||||
todoSummary: { openCount: number; highPriorityCount: number; href: string } | null;
|
todoSummary: { openCount: number; highPriorityCount: number; href: string } | null;
|
||||||
|
projectSummary:
|
||||||
|
| {
|
||||||
|
myCount: number;
|
||||||
|
overdueMilestonesCount: number;
|
||||||
|
highPriorityTaskCount: number;
|
||||||
|
openRiskCount: number;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null;
|
peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null;
|
||||||
adoptionSummary:
|
adoptionSummary:
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export type GlobalSearchHit = {
|
|||||||
| "purchase_request"
|
| "purchase_request"
|
||||||
| "travel_request"
|
| "travel_request"
|
||||||
| "asset"
|
| "asset"
|
||||||
|
| "project"
|
||||||
|
| "contract_registry"
|
||||||
|
| "confidential_doc_ref"
|
||||||
| "weekly_report"
|
| "weekly_report"
|
||||||
| "todo";
|
| "todo";
|
||||||
entityId: string;
|
entityId: string;
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ function entityLabel(entityType: string) {
|
|||||||
if (entityType === "purchase_request") return "采购";
|
if (entityType === "purchase_request") return "采购";
|
||||||
if (entityType === "travel_request") return "差旅";
|
if (entityType === "travel_request") return "差旅";
|
||||||
if (entityType === "asset") return "资产";
|
if (entityType === "asset") return "资产";
|
||||||
|
if (entityType === "project") return "项目";
|
||||||
|
if (entityType === "contract_registry") return "合同台账";
|
||||||
|
if (entityType === "confidential_doc_ref") return "资料引用";
|
||||||
return entityType;
|
return entityType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
|||||||
});
|
});
|
||||||
|
|
||||||
const message = useMemo(() => {
|
const message = useMemo(() => {
|
||||||
if (!trimmed) return "输入姓名、公告、资产、请假、报销、周报或待办关键字开始搜索。";
|
if (!trimmed) return "输入姓名、公告、项目、合同、资产、请假、报销、周报或待办关键字开始搜索。";
|
||||||
if (trimmed.length < 2) return "至少输入 2 个字符。";
|
if (trimmed.length < 2) return "至少输入 2 个字符。";
|
||||||
if (searchQuery.isError) {
|
if (searchQuery.isError) {
|
||||||
return isApiError(searchQuery.error)
|
return isApiError(searchQuery.error)
|
||||||
@@ -51,7 +54,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="搜索人员、公告、资产、请假、报销、周报、待办"
|
placeholder="搜索人员、项目、合同、公告、资产、请假、报销、周报、待办"
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(event) => setKeyword(event.target.value)}
|
onChange={(event) => setKeyword(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function PortalHomePage() {
|
|||||||
<Link
|
<Link
|
||||||
key={card.key}
|
key={card.key}
|
||||||
to={card.href}
|
to={card.href}
|
||||||
|
aria-label={`portal-card-${card.key}`}
|
||||||
className={
|
className={
|
||||||
card.severity === "high"
|
card.severity === "high"
|
||||||
? "card-std border-red-200 bg-red-50 p-5"
|
? "card-std border-red-200 bg-red-50 p-5"
|
||||||
@@ -184,6 +185,11 @@ export function PortalHomePage() {
|
|||||||
我的待办:未完成 {payload.todoSummary.openCount},高优先级 {payload.todoSummary.highPriorityCount}
|
我的待办:未完成 {payload.todoSummary.openCount},高优先级 {payload.todoSummary.highPriorityCount}
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
|
{payload.projectSummary ? (
|
||||||
|
<Link to={payload.projectSummary.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
|
||||||
|
项目摘要:我的项目 {payload.projectSummary.myCount},逾期里程碑 {payload.projectSummary.overdueMilestonesCount},风险 {payload.projectSummary.openRiskCount}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
{payload.peopleLifecycle ? (
|
{payload.peopleLifecycle ? (
|
||||||
<Link to={payload.peopleLifecycle.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
|
<Link to={payload.peopleLifecycle.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
|
||||||
人员闭环:入职 {payload.peopleLifecycle.onboardingCount},离职 {payload.peopleLifecycle.offboardingCount},资产待归还 {payload.peopleLifecycle.assetReturnPendingCount}
|
人员闭环:入职 {payload.peopleLifecycle.onboardingCount},离职 {payload.peopleLifecycle.offboardingCount},资产待归还 {payload.peopleLifecycle.assetReturnPendingCount}
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ function buildQuickLinks({ request, featureEnabled }) {
|
|||||||
if (featureEnabled.todoBoard) {
|
if (featureEnabled.todoBoard) {
|
||||||
items.push({ key: "todo", title: "待办", href: "/todos" });
|
items.push({ key: "todo", title: "待办", href: "/todos" });
|
||||||
}
|
}
|
||||||
|
if (featureEnabled.projectDomain) {
|
||||||
|
items.push({ key: "projects", title: "项目", href: "/projects" });
|
||||||
|
}
|
||||||
if (request.employee.permissionCodes.includes("system.users") || request.employee.permissionCodes.includes("system.settings")) {
|
if (request.employee.permissionCodes.includes("system.users") || request.employee.permissionCodes.includes("system.settings")) {
|
||||||
items.push({ key: "people", title: "人员管理", href: "/people" });
|
items.push({ key: "people", title: "人员管理", href: "/people" });
|
||||||
items.push({ key: "workspace", title: "工作区设置", href: "/system/workspace" });
|
items.push({ key: "workspace", title: "工作区设置", href: "/system/workspace" });
|
||||||
@@ -186,6 +189,7 @@ async function portalRoutes(fastify) {
|
|||||||
employeeLifecycle: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.employeeLifecycle),
|
employeeLifecycle: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.employeeLifecycle),
|
||||||
todoBoard: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.todoBoard),
|
todoBoard: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.todoBoard),
|
||||||
paymentBatch: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.paymentBatch),
|
paymentBatch: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.paymentBatch),
|
||||||
|
projectDomain: await evaluateOptionalFlag(fastify, request, FEATURE_FLAG_KEYS.projectDomain),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -202,6 +206,10 @@ async function portalRoutes(fastify) {
|
|||||||
assetReturnPendingCount,
|
assetReturnPendingCount,
|
||||||
activeEmployeeCount,
|
activeEmployeeCount,
|
||||||
login7dCount,
|
login7dCount,
|
||||||
|
myProjectCount,
|
||||||
|
overdueProjectMilestoneCount,
|
||||||
|
highPriorityProjectTaskCount,
|
||||||
|
openProjectRiskCount,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fastify.prisma.workflowTask.count({ where: { status: "pending", OR: assigneeFilters } }),
|
fastify.prisma.workflowTask.count({ where: { status: "pending", OR: assigneeFilters } }),
|
||||||
fastify.prisma.reimbursementForm.count({ where: { OR: [{ owner_id: request.employee.id }, { submitter_id: request.employee.id }] } }),
|
fastify.prisma.reimbursementForm.count({ where: { OR: [{ owner_id: request.employee.id }, { submitter_id: request.employee.id }] } }),
|
||||||
@@ -260,6 +268,69 @@ async function portalRoutes(fastify) {
|
|||||||
distinct: ["employee_id"],
|
distinct: ["employee_id"],
|
||||||
}).then((rows) => rows.length)
|
}).then((rows) => rows.length)
|
||||||
: Promise.resolve(0),
|
: Promise.resolve(0),
|
||||||
|
featureEnabled.projectDomain
|
||||||
|
? fastify.prisma.project.count({
|
||||||
|
where: fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "*")
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ owner_employee_id: request.employee.id },
|
||||||
|
{ manager_employee_id: request.employee.id },
|
||||||
|
{ members: { some: { employee_id: request.employee.id, left_at: null } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
|
featureEnabled.projectDomain
|
||||||
|
? fastify.prisma.projectMilestone.count({
|
||||||
|
where: {
|
||||||
|
status: { in: ["pending", "in_progress", "delayed"] },
|
||||||
|
due_date: { lt: new Date() },
|
||||||
|
project: fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "*")
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ owner_employee_id: request.employee.id },
|
||||||
|
{ manager_employee_id: request.employee.id },
|
||||||
|
{ members: { some: { employee_id: request.employee.id, left_at: null } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
|
featureEnabled.projectDomain
|
||||||
|
? fastify.prisma.projectTask.count({
|
||||||
|
where: {
|
||||||
|
status: { in: ["todo", "doing", "blocked"] },
|
||||||
|
priority: { in: ["high", "urgent"] },
|
||||||
|
project: fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "*")
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ owner_employee_id: request.employee.id },
|
||||||
|
{ manager_employee_id: request.employee.id },
|
||||||
|
{ members: { some: { employee_id: request.employee.id, left_at: null } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
|
featureEnabled.projectDomain
|
||||||
|
? fastify.prisma.projectRisk.count({
|
||||||
|
where: {
|
||||||
|
status: { not: "closed" },
|
||||||
|
project: fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "*")
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ owner_employee_id: request.employee.id },
|
||||||
|
{ manager_employee_id: request.employee.id },
|
||||||
|
{ members: { some: { employee_id: request.employee.id, left_at: null } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const todayPresence = featureEnabled.presenceBoard
|
const todayPresence = featureEnabled.presenceBoard
|
||||||
@@ -328,6 +399,10 @@ async function portalRoutes(fastify) {
|
|||||||
href: "/workflow/tasks",
|
href: "/workflow/tasks",
|
||||||
severity: pendingTaskCount > 0 ? "warning" : "normal",
|
severity: pendingTaskCount > 0 ? "warning" : "normal",
|
||||||
});
|
});
|
||||||
|
if (featureEnabled.projectDomain) {
|
||||||
|
cards.push({ key: "project_milestones", title: "逾期里程碑", value: overdueProjectMilestoneCount, href: "/projects", severity: overdueProjectMilestoneCount > 0 ? "warning" : "normal" });
|
||||||
|
cards.push({ key: "project_risks", title: "项目风险", value: openProjectRiskCount, href: "/projects", severity: openProjectRiskCount > 0 ? "high" : "normal" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cards.push({
|
cards.push({
|
||||||
key: "my_todos",
|
key: "my_todos",
|
||||||
@@ -353,6 +428,9 @@ async function portalRoutes(fastify) {
|
|||||||
href: "/weekly-reports",
|
href: "/weekly-reports",
|
||||||
severity: myWeeklyReport?.status === "submitted" ? "normal" : "warning",
|
severity: myWeeklyReport?.status === "submitted" ? "normal" : "warning",
|
||||||
});
|
});
|
||||||
|
if (featureEnabled.projectDomain) {
|
||||||
|
cards.push({ key: "my_projects", title: "我的项目", value: myProjectCount, href: "/projects", severity: highPriorityProjectTaskCount > 0 ? "warning" : "normal" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureEnabled.paymentBatch && riskCount > 0) {
|
if (featureEnabled.paymentBatch && riskCount > 0) {
|
||||||
@@ -423,6 +501,15 @@ async function portalRoutes(fastify) {
|
|||||||
href: "/todos",
|
href: "/todos",
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
projectSummary: featureEnabled.projectDomain
|
||||||
|
? {
|
||||||
|
myCount: myProjectCount,
|
||||||
|
overdueMilestonesCount: overdueProjectMilestoneCount,
|
||||||
|
highPriorityTaskCount: highPriorityProjectTaskCount,
|
||||||
|
openRiskCount: openProjectRiskCount,
|
||||||
|
href: "/projects",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
peopleLifecycle: featureEnabled.employeeLifecycle && isManagerView
|
peopleLifecycle: featureEnabled.employeeLifecycle && isManagerView
|
||||||
? {
|
? {
|
||||||
onboardingCount,
|
onboardingCount,
|
||||||
|
|||||||
+104
-6
@@ -13,12 +13,15 @@ const ENTITY_PRIORITY = {
|
|||||||
employee: 0,
|
employee: 0,
|
||||||
announcement: 1,
|
announcement: 1,
|
||||||
asset: 2,
|
asset: 2,
|
||||||
leave: 3,
|
project: 3,
|
||||||
reimbursement: 4,
|
contract_registry: 4,
|
||||||
weekly_report: 5,
|
confidential_doc_ref: 5,
|
||||||
todo: 6,
|
leave: 6,
|
||||||
purchase_request: 7,
|
reimbursement: 7,
|
||||||
travel_request: 8,
|
weekly_report: 8,
|
||||||
|
todo: 9,
|
||||||
|
purchase_request: 10,
|
||||||
|
travel_request: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildValidationError(details, statusCode = 400) {
|
function buildValidationError(details, statusCode = 400) {
|
||||||
@@ -75,6 +78,9 @@ async function searchRoutes(fastify) {
|
|||||||
announcements: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.announcements),
|
announcements: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.announcements),
|
||||||
weeklyReport: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.weeklyReport),
|
weeklyReport: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.weeklyReport),
|
||||||
todoBoard: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.todoBoard),
|
todoBoard: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.todoBoard),
|
||||||
|
projectDomain: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.projectDomain),
|
||||||
|
projectLinking: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.projectLinking),
|
||||||
|
confidentialDocRefs: await isFlagEnabled(fastify, request, FEATURE_FLAG_KEYS.confidentialDocRefs),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
@@ -261,6 +267,98 @@ async function searchRoutes(fastify) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (featureEnabled.projectDomain && (fastify.hasPermission(request.employee, "project.view_own") || fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage"))) {
|
||||||
|
tasks.push(
|
||||||
|
fastify.prisma.project.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || isAdmin
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ owner_employee_id: request.employee.id },
|
||||||
|
{ manager_employee_id: request.employee.id },
|
||||||
|
{ members: { some: { employee_id: request.employee.id, left_at: null } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ project_no: { contains: q, mode: "insensitive" } },
|
||||||
|
{ name: { contains: q, mode: "insensitive" } },
|
||||||
|
{ client_name: { contains: q, mode: "insensitive" } },
|
||||||
|
{ client_alias: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
orderBy: { updated_at: "desc" },
|
||||||
|
select: { id: true, project_no: true, name: true, client_name: true, client_alias: true, is_confidential: true, updated_at: true },
|
||||||
|
}).then((rows) => rows.map((item) => ({
|
||||||
|
entityType: "project",
|
||||||
|
entityId: item.id,
|
||||||
|
title: `${item.project_no} · ${item.name}`,
|
||||||
|
subtitle: item.is_confidential ? "涉密项目" : item.client_alias || item.client_name || null,
|
||||||
|
status: null,
|
||||||
|
href: `/projects/${item.id}`,
|
||||||
|
matchedBy: matchByValue(q, item.project_no, item.name, item.client_name, item.client_alias),
|
||||||
|
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featureEnabled.projectLinking && (fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "system.settings") || isAdmin)) {
|
||||||
|
tasks.push(
|
||||||
|
fastify.prisma.contractRegistry.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ contract_no: { contains: q, mode: "insensitive" } },
|
||||||
|
{ counterparty_name: { contains: q, mode: "insensitive" } },
|
||||||
|
{ summary: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
orderBy: { updated_at: "desc" },
|
||||||
|
select: { id: true, contract_no: true, counterparty_name: true, confidentiality_level: true, summary: true, updated_at: true },
|
||||||
|
}).then((rows) => rows.map((item) => ({
|
||||||
|
entityType: "contract_registry",
|
||||||
|
entityId: item.id,
|
||||||
|
title: item.contract_no,
|
||||||
|
subtitle: ["confidential", "restricted"].includes(item.confidentiality_level) ? "涉密合同台账" : item.counterparty_name,
|
||||||
|
status: item.confidentiality_level,
|
||||||
|
href: "/contracts-registry",
|
||||||
|
matchedBy: matchByValue(q, item.contract_no, item.counterparty_name, item.summary),
|
||||||
|
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featureEnabled.confidentialDocRefs && (fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "system.settings") || isAdmin)) {
|
||||||
|
tasks.push(
|
||||||
|
fastify.prisma.confidentialDocumentRef.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: q, mode: "insensitive" } },
|
||||||
|
{ document_no: { contains: q, mode: "insensitive" } },
|
||||||
|
{ offline_archive_no: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
orderBy: { updated_at: "desc" },
|
||||||
|
select: { id: true, title: true, document_no: true, confidentiality_level: true, updated_at: true },
|
||||||
|
}).then((rows) => rows.map((item) => ({
|
||||||
|
entityType: "confidential_doc_ref",
|
||||||
|
entityId: item.id,
|
||||||
|
title: item.document_no || item.title,
|
||||||
|
subtitle: ["confidential", "restricted"].includes(item.confidentiality_level) ? "涉密资料引用" : item.title,
|
||||||
|
status: item.confidentiality_level,
|
||||||
|
href: "/contracts-registry",
|
||||||
|
matchedBy: matchByValue(q, item.title, item.document_no),
|
||||||
|
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (featureEnabled.todoBoard) {
|
if (featureEnabled.todoBoard) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
fastify.prisma.todoItem.findMany({
|
fastify.prisma.todoItem.findMany({
|
||||||
|
|||||||
@@ -75,6 +75,32 @@ function buildImDedupeKey({ notificationType, targetType, targetId, targetPath,
|
|||||||
].join(':');
|
].join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redactImMessage(config, payload) {
|
||||||
|
const effectiveType = String(payload?.entityType ?? payload?.targetType ?? '').trim();
|
||||||
|
const rawTitle = String(payload?.title ?? '系统通知').slice(0, 200);
|
||||||
|
const rawBody = payload?.body == null ? '' : String(payload.body);
|
||||||
|
|
||||||
|
if (!config?.publicSecurityHardeningEnabled) {
|
||||||
|
return { title: rawTitle, body: rawBody.slice(0, 4000) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveType === 'project') {
|
||||||
|
return {
|
||||||
|
title: rawTitle || '项目待处理事项',
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['contract_registry', 'confidential_doc_ref'].includes(effectiveType)) {
|
||||||
|
return {
|
||||||
|
title: '有待处理事项',
|
||||||
|
body: `事项编号:${String(payload?.targetId ?? payload?.entityId ?? '-').slice(0, 80)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { title: rawTitle, body: rawBody.slice(0, 4000) };
|
||||||
|
}
|
||||||
|
|
||||||
async function enqueueImNotifications(tx, params) {
|
async function enqueueImNotifications(tx, params) {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
@@ -154,6 +180,14 @@ async function enqueueImNotifications(tx, params) {
|
|||||||
|
|
||||||
let queued = 0;
|
let queued = 0;
|
||||||
let duplicated = 0;
|
let duplicated = 0;
|
||||||
|
const redactedMessage = redactImMessage(config, {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
targetType,
|
||||||
|
targetId,
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
});
|
||||||
for (const channel of matchedChannels) {
|
for (const channel of matchedChannels) {
|
||||||
const result = await enqueueIntegrationEvent(tx, {
|
const result = await enqueueIntegrationEvent(tx, {
|
||||||
eventType: 'notification.im',
|
eventType: 'notification.im',
|
||||||
@@ -171,8 +205,8 @@ async function enqueueImNotifications(tx, params) {
|
|||||||
channelType: normalizeChannelType(channel.channel_type),
|
channelType: normalizeChannelType(channel.channel_type),
|
||||||
notificationType,
|
notificationType,
|
||||||
domainScope,
|
domainScope,
|
||||||
title: String(title ?? '').slice(0, 200),
|
title: redactedMessage.title,
|
||||||
body: body == null ? '' : String(body).slice(0, 4000),
|
body: redactedMessage.body,
|
||||||
targetType: targetType ?? null,
|
targetType: targetType ?? null,
|
||||||
targetId: targetId ?? null,
|
targetId: targetId ?? null,
|
||||||
targetPath: targetPath ?? null,
|
targetPath: targetPath ?? null,
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户