Initial commit

This commit is contained in:
JetSprow
2026-04-29 05:12:39 +10:00
commit 27dbca9cbf
379 changed files with 43486 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
"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();
}