mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-03 09:54:11 +05:30
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { requireAdmin } from "@/lib/require-auth";
|
|
import { actorFromSession, recordAuditLog } from "@/services/audit";
|
|
import {
|
|
deleteAnnouncementNotifications,
|
|
dispatchAnnouncementNotifications,
|
|
syncAnnouncementNotifications,
|
|
} from "@/services/announcements";
|
|
|
|
const announcementSchema = z.object({
|
|
title: z.string().trim().min(1, "标题不能为空"),
|
|
body: z.string().trim().min(1, "内容不能为空"),
|
|
audience: z.enum(["PUBLIC", "USERS", "ADMINS", "SPECIFIC_USER"]),
|
|
displayType: z.enum(["INLINE", "BIG", "POPUP"]).default("INLINE"),
|
|
targetUserId: z.string().optional(),
|
|
dismissible: z.string().optional(),
|
|
sendNotification: z.string().optional(),
|
|
startAt: z.string().optional(),
|
|
endAt: z.string().optional(),
|
|
});
|
|
|
|
function revalidateAnnouncementViews() {
|
|
revalidatePath("/admin/announcements");
|
|
revalidatePath("/dashboard");
|
|
revalidatePath("/account");
|
|
revalidatePath("/notifications");
|
|
revalidatePath("/login");
|
|
}
|
|
|
|
function parseAnnouncementInput(formData: FormData) {
|
|
const data = announcementSchema.parse(Object.fromEntries(formData));
|
|
|
|
if (data.audience === "SPECIFIC_USER" && !data.targetUserId) {
|
|
throw new Error("定向消息必须选择用户");
|
|
}
|
|
|
|
const startAt = data.startAt ? new Date(data.startAt) : null;
|
|
const endAt = data.endAt ? new Date(data.endAt) : null;
|
|
if (startAt && Number.isNaN(startAt.getTime())) {
|
|
throw new Error("开始时间格式无效");
|
|
}
|
|
if (endAt && Number.isNaN(endAt.getTime())) {
|
|
throw new Error("结束时间格式无效");
|
|
}
|
|
if (startAt && endAt && endAt <= startAt) {
|
|
throw new Error("结束时间必须晚于开始时间");
|
|
}
|
|
|
|
return {
|
|
title: data.title,
|
|
body: data.body,
|
|
audience: data.audience,
|
|
displayType: data.displayType,
|
|
targetUserId: data.audience === "SPECIFIC_USER" ? data.targetUserId ?? null : null,
|
|
dismissible: data.dismissible === "true",
|
|
sendNotification: data.sendNotification === "true",
|
|
startAt,
|
|
endAt,
|
|
};
|
|
}
|
|
|
|
export async function createAnnouncement(formData: FormData) {
|
|
const session = await requireAdmin();
|
|
const data = parseAnnouncementInput(formData);
|
|
|
|
const announcement = await prisma.announcement.create({
|
|
data: {
|
|
title: data.title,
|
|
body: data.body,
|
|
audience: data.audience,
|
|
displayType: data.displayType,
|
|
targetUserId: data.targetUserId,
|
|
createdById: session.user.id,
|
|
isActive: true,
|
|
dismissible: data.dismissible,
|
|
sendNotification: data.sendNotification,
|
|
startAt: data.startAt,
|
|
endAt: data.endAt,
|
|
},
|
|
});
|
|
|
|
await dispatchAnnouncementNotifications(announcement.id);
|
|
await recordAuditLog({
|
|
actor: actorFromSession(session),
|
|
action: "announcement.create",
|
|
targetType: "Announcement",
|
|
targetId: announcement.id,
|
|
targetLabel: announcement.title,
|
|
message: `创建公告/消息 ${announcement.title}`,
|
|
});
|
|
|
|
revalidateAnnouncementViews();
|
|
}
|
|
|
|
export async function updateAnnouncement(id: string, formData: FormData) {
|
|
const session = await requireAdmin();
|
|
const data = parseAnnouncementInput(formData);
|
|
|
|
const announcement = await prisma.announcement.update({
|
|
where: { id },
|
|
data: {
|
|
title: data.title,
|
|
body: data.body,
|
|
audience: data.audience,
|
|
displayType: data.displayType,
|
|
targetUserId: data.targetUserId,
|
|
dismissible: data.dismissible,
|
|
sendNotification: data.sendNotification,
|
|
startAt: data.startAt,
|
|
endAt: data.endAt,
|
|
},
|
|
});
|
|
|
|
await syncAnnouncementNotifications(announcement.id);
|
|
await recordAuditLog({
|
|
actor: actorFromSession(session),
|
|
action: "announcement.update",
|
|
targetType: "Announcement",
|
|
targetId: announcement.id,
|
|
targetLabel: announcement.title,
|
|
message: `更新公告/消息 ${announcement.title}`,
|
|
});
|
|
|
|
revalidateAnnouncementViews();
|
|
}
|
|
|
|
export async function toggleAnnouncement(id: string, isActive: boolean) {
|
|
const session = await requireAdmin();
|
|
const announcement = await prisma.announcement.update({
|
|
where: { id },
|
|
data: { isActive },
|
|
});
|
|
|
|
await recordAuditLog({
|
|
actor: actorFromSession(session),
|
|
action: isActive ? "announcement.enable" : "announcement.disable",
|
|
targetType: "Announcement",
|
|
targetId: announcement.id,
|
|
targetLabel: announcement.title,
|
|
message: `${isActive ? "启用" : "停用"}公告/消息 ${announcement.title}`,
|
|
});
|
|
|
|
if (isActive) {
|
|
await dispatchAnnouncementNotifications(announcement.id);
|
|
}
|
|
|
|
revalidateAnnouncementViews();
|
|
}
|
|
|
|
export async function deleteAnnouncement(id: string) {
|
|
const session = await requireAdmin();
|
|
const announcement = await prisma.announcement.findUnique({
|
|
where: { id },
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
},
|
|
});
|
|
|
|
if (!announcement) {
|
|
throw new Error("公告不存在");
|
|
}
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
await deleteAnnouncementNotifications(announcement.id, tx);
|
|
await tx.announcement.delete({
|
|
where: {
|
|
id: announcement.id,
|
|
},
|
|
});
|
|
await recordAuditLog(
|
|
{
|
|
actor: actorFromSession(session),
|
|
action: "announcement.delete",
|
|
targetType: "Announcement",
|
|
targetId: announcement.id,
|
|
targetLabel: announcement.title,
|
|
message: `删除公告/消息 ${announcement.title}`,
|
|
},
|
|
tx,
|
|
);
|
|
});
|
|
|
|
revalidateAnnouncementViews();
|
|
}
|