192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
import { and, count, desc, eq, inArray, lte, sql } from "drizzle-orm";
|
||
import { getDb } from "../config/database.js";
|
||
import {
|
||
notifications,
|
||
type NewNotification,
|
||
type Notification,
|
||
} from "./notifications.schema.js";
|
||
|
||
/**
|
||
* NotificationsRepository —— 通知数据访问层。
|
||
*
|
||
* 职责:封装 MySQL 读写,与业务逻辑解耦。
|
||
* 仲裁依据 G10:使用 getDb() 函数式获取 db 实例。
|
||
*/
|
||
|
||
export async function insertNotification(
|
||
row: NewNotification,
|
||
): Promise<Notification> {
|
||
const db = getDb();
|
||
await db.insert(notifications).values(row);
|
||
return row as Notification;
|
||
}
|
||
|
||
export async function insertNotifications(
|
||
rows: NewNotification[],
|
||
): Promise<void> {
|
||
if (rows.length === 0) return;
|
||
const db = getDb();
|
||
await db.insert(notifications).values(rows);
|
||
}
|
||
|
||
export async function findById(id: string): Promise<Notification | undefined> {
|
||
const db = getDb();
|
||
const [row] = await db
|
||
.select()
|
||
.from(notifications)
|
||
.where(eq(notifications.id, id))
|
||
.limit(1);
|
||
return row;
|
||
}
|
||
|
||
export async function findByEventId(
|
||
eventId: string,
|
||
): Promise<Notification | undefined> {
|
||
const db = getDb();
|
||
const [row] = await db
|
||
.select()
|
||
.from(notifications)
|
||
.where(eq(notifications.eventId, eventId))
|
||
.limit(1);
|
||
return row;
|
||
}
|
||
|
||
/**
|
||
* 批量查询已存在的 eventId(用于 sendBatch 幂等过滤)。
|
||
* 返回已存在的 eventId 集合。
|
||
*/
|
||
export async function findExistingEventIds(
|
||
eventIds: string[],
|
||
): Promise<Set<string>> {
|
||
if (eventIds.length === 0) return new Set();
|
||
const db = getDb();
|
||
const rows = await db
|
||
.select({ eventId: notifications.eventId })
|
||
.from(notifications)
|
||
.where(
|
||
and(
|
||
inArray(notifications.eventId, eventIds),
|
||
sql`${notifications.eventId} IS NOT NULL`,
|
||
),
|
||
);
|
||
return new Set(
|
||
rows
|
||
.map((r) => r.eventId)
|
||
.filter((id): id is string => id !== null && id !== undefined),
|
||
);
|
||
}
|
||
|
||
export async function listByUser(
|
||
userId: string,
|
||
options: {
|
||
onlyUnread?: boolean;
|
||
type?: string;
|
||
page: number;
|
||
pageSize: number;
|
||
},
|
||
): Promise<{ items: Notification[]; total: number }> {
|
||
const db = getDb();
|
||
const conditions = [eq(notifications.userId, userId)];
|
||
|
||
if (options.onlyUnread) {
|
||
conditions.push(eq(notifications.isRead, false));
|
||
}
|
||
if (options.type) {
|
||
conditions.push(
|
||
eq(notifications.type, options.type as Notification["type"]),
|
||
);
|
||
}
|
||
|
||
const where = and(...conditions);
|
||
const offset = (options.page - 1) * options.pageSize;
|
||
|
||
const items = await db
|
||
.select()
|
||
.from(notifications)
|
||
.where(where)
|
||
.orderBy(desc(notifications.createdAt))
|
||
.limit(options.pageSize)
|
||
.offset(offset);
|
||
|
||
const [countRow] = await db
|
||
.select({ value: count() })
|
||
.from(notifications)
|
||
.where(where);
|
||
|
||
return { items, total: countRow?.value ?? 0 };
|
||
}
|
||
|
||
export async function getUnreadCount(userId: string): Promise<number> {
|
||
const db = getDb();
|
||
const [row] = await db
|
||
.select({ value: count() })
|
||
.from(notifications)
|
||
.where(
|
||
and(eq(notifications.userId, userId), eq(notifications.isRead, false)),
|
||
);
|
||
return row?.value ?? 0;
|
||
}
|
||
|
||
export async function markAsRead(id: string, userId: string): Promise<void> {
|
||
const db = getDb();
|
||
await db
|
||
.update(notifications)
|
||
.set({ isRead: true, readAt: new Date(), status: "read" })
|
||
.where(and(eq(notifications.id, id), eq(notifications.userId, userId)));
|
||
}
|
||
|
||
export async function batchMarkAsRead(
|
||
ids: string[],
|
||
userId: string,
|
||
): Promise<void> {
|
||
const db = getDb();
|
||
await db
|
||
.update(notifications)
|
||
.set({ isRead: true, readAt: new Date(), status: "read" })
|
||
.where(
|
||
and(inArray(notifications.id, ids), eq(notifications.userId, userId)),
|
||
);
|
||
}
|
||
|
||
export async function markAllAsRead(
|
||
userId: string,
|
||
before?: number,
|
||
): Promise<number> {
|
||
const db = getDb();
|
||
const conditions = [
|
||
eq(notifications.userId, userId),
|
||
eq(notifications.isRead, false),
|
||
];
|
||
if (before !== undefined) {
|
||
conditions.push(lte(notifications.createdAt, new Date(before)));
|
||
}
|
||
const result = await db
|
||
.update(notifications)
|
||
.set({ isRead: true, readAt: new Date(), status: "read" })
|
||
.where(and(...conditions));
|
||
|
||
// mysql2 affectedRows
|
||
return (result as unknown as { affectedRows?: number }).affectedRows ?? 0;
|
||
}
|
||
|
||
export async function recallByGroup(groupId: string): Promise<number> {
|
||
const db = getDb();
|
||
const result = await db
|
||
.update(notifications)
|
||
.set({ status: "recalled" })
|
||
.where(eq(notifications.groupId, groupId));
|
||
|
||
return (result as unknown as { affectedRows?: number }).affectedRows ?? 0;
|
||
}
|
||
|
||
export async function deleteById(id: string): Promise<void> {
|
||
const db = getDb();
|
||
await db.delete(notifications).where(eq(notifications.id, id));
|
||
}
|
||
|
||
/** 用于测试/重置 */
|
||
export async function deleteAllByUserId(userId: string): Promise<void> {
|
||
const db = getDb();
|
||
await db.delete(notifications).where(eq(notifications.userId, userId));
|
||
}
|