import { and, desc, eq, inArray, sql } from "drizzle-orm"; import { getDatabase } from "@/modules/core/server/db"; import { accounts, noticeReadReceipts, notices } from "@/modules/core/server/schema"; import type { Notice, NoticeCenterData, NoticeInput } from "@/modules/notices/types"; export type NoticeMutationFailure = { success: false; reason: string; status: number; }; type ReadCountRow = { noticeId: string; count: number; }; export function isNoticeMutationFailure(value: unknown): value is NoticeMutationFailure { return ( typeof value === "object" && value !== null && "success" in value && (value as { success?: unknown }).success === false ); } function iso(value: Date): string { return value.toISOString(); } function optionalIso(value: Date | null): string | undefined { return value ? iso(value) : undefined; } function toNotice( row: typeof notices.$inferSelect, accountNameById: Map, readCountByNoticeId: Map, readNoticeIds: Set, ): Notice { const createdByAccountId = row.createdByAccountId ?? undefined; const updatedByAccountId = row.updatedByAccountId ?? undefined; return { id: row.id, organizationId: row.organizationId, title: row.title, content: row.content, audience: row.audience, status: row.status, publishedAt: optionalIso(row.publishedAt), createdByAccountId, createdByName: createdByAccountId ? accountNameById.get(createdByAccountId) : undefined, updatedByAccountId, updatedByName: updatedByAccountId ? accountNameById.get(updatedByAccountId) : undefined, readCount: readCountByNoticeId.get(row.id) ?? 0, hasRead: readNoticeIds.has(row.id), createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt), }; } export async function listNoticeCenterData(organizationId: string, accountId: string): Promise { const database = getDatabase(); const noticeRows = await database.select().from(notices).where(eq(notices.organizationId, organizationId)).orderBy(desc(notices.updatedAt)); const noticeIds = noticeRows.map((notice) => notice.id); const accountIds = Array.from( new Set( noticeRows .flatMap((notice) => [notice.createdByAccountId, notice.updatedByAccountId]) .filter((item): item is string => typeof item === "string"), ), ); const [accountRows, readCountRows, readRows] = await Promise.all([ accountIds.length > 0 ? database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(inArray(accounts.id, accountIds)) : [], noticeIds.length > 0 ? database .select({ noticeId: noticeReadReceipts.noticeId, count: sql`count(*)::int` }) .from(noticeReadReceipts) .where(and(eq(noticeReadReceipts.organizationId, organizationId), inArray(noticeReadReceipts.noticeId, noticeIds))) .groupBy(noticeReadReceipts.noticeId) : ([] as ReadCountRow[]), noticeIds.length > 0 ? database .select({ noticeId: noticeReadReceipts.noticeId }) .from(noticeReadReceipts) .where( and( eq(noticeReadReceipts.organizationId, organizationId), eq(noticeReadReceipts.accountId, accountId), inArray(noticeReadReceipts.noticeId, noticeIds), ), ) : [], ]); const readCountByNoticeId = new Map(readCountRows.map((row) => [row.noticeId, Number(row.count)])); const readNoticeIds = new Set(readRows.map((row) => row.noticeId)); const noticeData = noticeRows.map((notice) => toNotice( notice, new Map(accountRows.map((account) => [account.id, account.name])), readCountByNoticeId, readNoticeIds, ), ); return { metrics: { published: noticeRows.filter((notice) => notice.status === "published").length, drafts: noticeRows.filter((notice) => notice.status === "draft").length, unread: noticeRows.filter((notice) => notice.status === "published" && !readNoticeIds.has(notice.id)).length, retracted: noticeRows.filter((notice) => notice.status === "retracted").length, }, notices: noticeData, }; } export async function createNotice(input: NoticeInput & { accountId: string; organizationId: string }): Promise { const database = getDatabase(); const now = new Date(); const rows = await database .insert(notices) .values({ organizationId: input.organizationId, title: input.title, content: input.content, audience: input.audience, status: input.status, publishedAt: input.status === "published" ? now : undefined, createdByAccountId: input.accountId, updatedByAccountId: input.accountId, }) .returning(); const notice = rows[0]; return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告创建失败", status: 500 }; } export async function updateNotice(input: NoticeInput & { accountId: string; id: string; organizationId: string }): Promise { const database = getDatabase(); const now = new Date(); const rows = await database .update(notices) .set({ title: input.title, content: input.content, audience: input.audience, status: input.status, publishedAt: input.status === "published" ? now : null, updatedByAccountId: input.accountId, updatedAt: now, }) .where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId))) .returning(); const notice = rows[0]; return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告不存在", status: 404 }; } export async function deleteNotice(input: { id: string; organizationId: string }): Promise { const database = getDatabase(); const rows = await database.delete(notices).where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId))).returning(); const notice = rows[0]; return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告不存在", status: 404 }; } export async function markNoticeRead(input: { accountId: string; id: string; organizationId: string }): Promise { const database = getDatabase(); const rows = await database.select().from(notices).where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId))); const notice = rows[0]; if (!notice) { return { success: false, reason: "公告不存在", status: 404 }; } await database .insert(noticeReadReceipts) .values({ organizationId: input.organizationId, noticeId: input.id, accountId: input.accountId }) .onConflictDoUpdate({ target: [noticeReadReceipts.noticeId, noticeReadReceipts.accountId], set: { readAt: new Date() }, }); return toNotice(notice, new Map(), new Map([[notice.id, 1]]), new Set([notice.id])); }