refactor(actions): 组 C 模块迁移至 invalidateFor(lesson-preparation/elective/announcements/messaging/notifications/settings/rbac)

This commit is contained in:
SpecialX
2026-07-05 22:53:26 +08:00
parent 7cfd85d0f5
commit 8fff820e1d
22 changed files with 133 additions and 135 deletions

View File

@@ -1,6 +1,6 @@
"use server"
import { revalidatePath } from "next/cache"
import { invalidateFor } from "@/shared/lib/cache"
import { createId } from "@paralleldrive/cuid2"
import { getTranslations } from "next-intl/server"
@@ -149,8 +149,7 @@ export async function createAnnouncementAction(
}
}
revalidatePath("/admin/announcements")
revalidatePath("/announcements")
await invalidateFor("announcements.create", { id })
void trackEvent({
event: isPublished ? "announcement.published" : "announcement.created",
@@ -226,9 +225,7 @@ export async function updateAnnouncementAction(
}
}
revalidatePath("/admin/announcements")
revalidatePath(`/admin/announcements/${id}`)
revalidatePath("/announcements")
await invalidateFor("announcements.update", { id })
void trackEvent({
event: isPublished && !wasPublished ? "announcement.published" : "announcement.updated",
@@ -257,8 +254,7 @@ export async function deleteAnnouncementAction(id: string): Promise<ActionState<
await deleteAnnouncementById(id)
revalidatePath("/admin/announcements")
revalidatePath("/announcements")
await invalidateFor("announcements.delete", { id })
void trackEvent({
event: "announcement.deleted",
@@ -289,9 +285,7 @@ export async function publishAnnouncementAction(id: string): Promise<ActionState
// 发布成功后触发通知(失败不阻塞)
await notifyAnnouncementPublished(existing)
revalidatePath("/admin/announcements")
revalidatePath(`/admin/announcements/${id}`)
revalidatePath("/announcements")
await invalidateFor("announcements.publish", { id })
void trackEvent({
event: "announcement.published",
@@ -316,9 +310,7 @@ export async function archiveAnnouncementAction(id: string): Promise<ActionState
await archiveAnnouncementById(id)
revalidatePath("/admin/announcements")
revalidatePath(`/admin/announcements/${id}`)
revalidatePath("/announcements")
await invalidateFor("announcements.archive", { id })
void trackEvent({
event: "announcement.archived",
@@ -348,8 +340,7 @@ export async function toggleAnnouncementPinAction(id: string): Promise<ActionSta
if (!existing) return { success: false, message: t("messages.notFound") }
await toggleAnnouncementPin(id)
revalidatePath("/admin/announcements")
revalidatePath("/announcements")
await invalidateFor("announcements.togglePin", { id })
void trackEvent({
event: "announcement.pin_toggled",

View File

@@ -1,6 +1,6 @@
"use server"
import { revalidatePath } from "next/cache"
import { invalidateFor } from "@/shared/lib/cache"
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
@@ -32,19 +32,6 @@ import {
} from "./data-access-operations"
import { COURSE_SELECTION_STATUS_LABEL_KEYS } from "./constants"
const revalidateElectivePaths = (id?: string) => {
revalidatePath("/admin/elective")
revalidatePath("/teacher/elective")
revalidatePath("/student/elective")
revalidatePath("/parent/elective")
if (id) {
revalidatePath(`/admin/elective/${id}`)
revalidatePath(`/admin/elective/${id}/edit`)
revalidatePath(`/teacher/elective/${id}`)
revalidatePath(`/teacher/elective/${id}/edit`)
}
}
const requireCourseId = (formData: FormData): string => {
const id = String(formData.get("courseId") ?? "")
if (!id) throw new Error("Course ID is required")
@@ -119,7 +106,7 @@ export async function createElectiveCourseAction(
}
}
const id = await createElectiveCourse(parsed.data, ctx.userId)
revalidateElectivePaths(id)
await invalidateFor("elective.create", { id })
await trackEvent({
event: "elective.course_created",
userId: ctx.userId,
@@ -175,7 +162,7 @@ export async function updateElectiveCourseAction(
}
}
await updateElectiveCourse(id, parsed.data)
revalidateElectivePaths(id)
await invalidateFor("elective.update", { id })
await trackEvent({
event: "elective.course_updated",
userId: ctx.userId,
@@ -206,7 +193,7 @@ export async function deleteElectiveCourseAction(
}
await deleteElectiveCourse(id)
revalidateElectivePaths()
await invalidateFor("elective.delete", { id })
await trackEvent({
event: "elective.course_deleted",
userId: ctx.userId,
@@ -237,7 +224,7 @@ export async function openSelectionAction(
}
await openSelection(courseId)
revalidateElectivePaths(courseId)
await invalidateFor("elective.openSelection", { id: courseId })
await trackEvent({
event: "elective.selection_opened",
userId: ctx.userId,
@@ -268,7 +255,7 @@ export async function closeSelectionAction(
}
await closeSelection(courseId)
revalidateElectivePaths(courseId)
await invalidateFor("elective.closeSelection", { id: courseId })
await trackEvent({
event: "elective.selection_closed",
userId: ctx.userId,
@@ -308,7 +295,7 @@ export async function runLotteryAction(
}
const result = await runLottery(parsed.data.courseId)
revalidateElectivePaths(parsed.data.courseId)
await invalidateFor("elective.runLottery", { id: parsed.data.courseId })
await trackEvent({
event: "elective.lottery_completed",
userId: ctx.userId,
@@ -348,7 +335,7 @@ export async function selectCourseAction(
}
}
const result = await selectCourse(parsed.data.courseId, ctx.userId, parsed.data.priority)
revalidateElectivePaths(parsed.data.courseId)
await invalidateFor("elective.selectCourse", { id: parsed.data.courseId })
await trackEvent({
event: "elective.course_selected",
userId: ctx.userId,
@@ -385,7 +372,7 @@ export async function dropCourseAction(
}
}
await dropCourse(parsed.data.courseId, ctx.userId, parsed.data.dropReason)
revalidateElectivePaths(parsed.data.courseId)
await invalidateFor("elective.dropCourse", { id: parsed.data.courseId })
await trackEvent({
event: "elective.course_dropped",
userId: ctx.userId,

View File

@@ -4,7 +4,7 @@
"use server";
import { getTranslations } from "next-intl/server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { z } from "zod";
import {
getAttachmentsByPlanId,
@@ -80,7 +80,7 @@ export async function createLessonPlanAttachmentAction(
}
await createAttachment(parseResult.data, auth.userId);
const attachment = await getAttachmentById(parseResult.data.planId);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.attachment.create", { id: parseResult.data.planId });
return { success: true, data: { attachment } };
} catch (e) {
return handleActionError(e);

View File

@@ -3,7 +3,7 @@
*/
"use server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { z } from "zod";
import {
getCommentsByPlanId,
@@ -73,7 +73,7 @@ export async function createLessonPlanCommentAction(
return { success: false, message: "Unauthorized" };
}
const comment = await createComment(parseResult.data, auth.userId);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.comment.create", { id: parseResult.data.planId });
return { success: true, data: { comment } };
} catch (e) {
return handleActionError(e);

View File

@@ -4,7 +4,7 @@
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import {
getFormativeItemsByPlanId,
createFormativeItem,
@@ -65,7 +65,7 @@ export async function createFormativeItemAction(
return { success: false, message: "Invalid input" };
}
const item = await createFormativeItem(parseResult.data);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.formative.create", { id: parseResult.data.planId });
return { success: true, data: { item } };
} catch (e) {
return handleActionError(e);

View File

@@ -1,6 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { getTranslations } from "next-intl/server";
import {
requirePermission,
@@ -57,8 +57,7 @@ export async function publishLessonPlanHomeworkAction(input: {
homeworkTitle,
homeworkDescription,
});
revalidatePath("/teacher/lesson-plans");
revalidatePath("/teacher/homework");
await invalidateFor("lesson-preparation.publishHomework");
return {
success: true,
data: {

View File

@@ -4,7 +4,7 @@
"use server";
import { getTranslations } from "next-intl/server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import {
submitLessonPlanForReviewSchema,
reviewLessonPlanSchema,
@@ -40,7 +40,7 @@ export async function submitLessonPlanForReviewAction(
return { success: false, message: t("error.unauthorized") };
}
const status = await submitForReview(parseResult.data.planId, auth.userId);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.review.submit", { id: parseResult.data.planId });
return { success: true, data: { status } };
} catch (e) {
return handleActionError(e);
@@ -70,7 +70,7 @@ export async function reviewLessonPlanAction(
parseResult.data.decision,
parseResult.data.reviewComment,
);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.review.review", { id: parseResult.data.planId });
return { success: true, data: { newStatus, record } };
} catch (e) {
return handleActionError(e);
@@ -120,7 +120,7 @@ export async function withdrawSubmissionAction(
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const status = await withdrawSubmission(planId);
revalidatePath(`/teacher/lesson-plans/${planId}/edit`);
await invalidateFor("lesson-preparation.review.withdraw", { id: planId });
return { success: true, data: { status } };
} catch (e) {
return handleActionError(e);

View File

@@ -4,7 +4,7 @@
"use server";
import { getTranslations } from "next-intl/server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { z } from "zod";
import {
getSchedulesByPlanId,
@@ -64,8 +64,7 @@ export async function createLessonPlanScheduleAction(
createdBy: auth.userId,
});
revalidatePath("/teacher/lesson-plans");
revalidatePath("/teacher/lesson-plans/calendar");
await invalidateFor("lesson-preparation.schedule.create");
return { success: true, data: { schedule } };
} catch (e) {
return handleActionError(e);
@@ -78,8 +77,7 @@ export async function deleteLessonPlanScheduleAction(
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
await deleteSchedule(scheduleId);
revalidatePath("/teacher/lesson-plans");
revalidatePath("/teacher/lesson-plans/calendar");
await invalidateFor("lesson-preparation.schedule.delete");
return { success: true, data: null };
} catch (e) {
return handleActionError(e);

View File

@@ -4,7 +4,7 @@
"use server";
import { getTranslations } from "next-intl/server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { z } from "zod";
import {
getSubstitutesByPlanId,
@@ -105,7 +105,7 @@ export async function createSubstituteAction(
},
auth.userId,
);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
await invalidateFor("lesson-preparation.substitute.create", { id: parseResult.data.planId });
return { success: true, data: { substitute } };
} catch (e) {
return handleActionError(e);

View File

@@ -1,6 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
import { invalidateFor } from "@/shared/lib/cache";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { requirePermission } from "@/shared/lib/auth-guard";
@@ -125,7 +125,7 @@ export async function createLessonPlanAction(
return key;
},
});
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.create");
return { success: true, data: { planId } };
} catch (e) {
if (e instanceof LessonPlanDataError && e.code === "TEMPLATE_NOT_FOUND")
@@ -154,7 +154,7 @@ export async function updateLessonPlanAction(input: {
...(parsed.data.title ? { title: parsed.data.title } : {}),
content,
});
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.update");
return { success: true };
} catch (e) {
return handleActionError(e);
@@ -235,7 +235,7 @@ export async function revertLessonPlanVersionAction(input: {
revertLabel,
);
if (!result) return { success: false, message: t("error.versionNotFound") };
revalidatePath(`/teacher/lesson-plans/${parsed.data.planId}/edit`);
await invalidateFor("lesson-preparation.revertVersion", { id: parsed.data.planId });
return { success: true, data: { newVersionNo: result.newVersionNo } };
} catch (e) {
return handleActionError(e);
@@ -249,7 +249,7 @@ export async function deleteLessonPlanAction(
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
await softDeleteLessonPlan(planId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.delete");
return { success: true };
} catch (e) {
return handleActionError(e);
@@ -265,8 +265,7 @@ export async function publishLessonPlanAction(
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_PUBLISH);
await publishLessonPlan(planId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
revalidatePath(`/teacher/lesson-plans/${planId}/edit`);
await invalidateFor("lesson-preparation.publish", { id: planId });
return { success: true, message: t("action.publishPlanSuccess") };
} catch (e) {
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
@@ -284,8 +283,7 @@ export async function unpublishLessonPlanAction(
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_PUBLISH);
await unpublishLessonPlan(planId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
revalidatePath(`/teacher/lesson-plans/${planId}/edit`);
await invalidateFor("lesson-preparation.unpublish", { id: planId });
return { success: true, message: t("action.unpublishPlanSuccess") };
} catch (e) {
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
@@ -307,7 +305,7 @@ export async function duplicateLessonPlanAction(
ctx.userId,
t("error.duplicateSuffix"),
);
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.duplicate");
return { success: true, data: { newPlanId } };
} catch (e) {
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
@@ -324,7 +322,7 @@ export async function duplicateLessonPlanFormAction(
if (!planId) return;
const result = await duplicateLessonPlanAction(planId);
if (result.success && result.data) {
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.duplicate");
redirect(`/teacher/lesson-plans/${result.data.newPlanId}/edit`);
}
}
@@ -376,7 +374,7 @@ export async function deleteTemplateAction(
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
await deletePersonalTemplate(templateId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
await invalidateFor("lesson-preparation.template.delete");
return { success: true };
} catch (e) {
return handleActionError(e);

View File

@@ -1,6 +1,6 @@
"use server"
import { revalidatePath } from "next/cache"
import { invalidateFor } from "@/shared/lib/cache"
import { getTranslations } from "next-intl/server"
import { PermissionDeniedError, requirePermission } from "@/shared/lib/auth-guard"
import { trackEvent } from "@/shared/lib/track-event"
@@ -129,8 +129,7 @@ export async function sendMessageAction(
metadata: { messageType: "message", messageId: id },
})
revalidatePath("/messages")
revalidatePath(`/messages/${id}`)
await invalidateFor("messaging.send", { id })
void trackEvent({
event: "message.sent",
@@ -170,8 +169,7 @@ export async function markMessageAsReadAction(messageId: string): Promise<Action
}
await markMessageAsRead(parsed.data.messageId, ctx.userId)
revalidatePath("/messages")
revalidatePath(`/messages/${parsed.data.messageId}`)
await invalidateFor("messaging.markRead", { id: parsed.data.messageId })
void trackEvent({
event: "message.marked_read",
@@ -198,8 +196,7 @@ export async function deleteMessageAction(messageId: string): Promise<ActionStat
}
await deleteMessage(parsed.data.messageId, ctx.userId)
revalidatePath("/messages")
revalidatePath(`/messages/${parsed.data.messageId}`)
await invalidateFor("messaging.delete", { id: parsed.data.messageId })
void trackEvent({
event: "message.deleted",
@@ -310,8 +307,7 @@ export async function toggleMessageStarAction(messageId: string): Promise<Action
}
await toggleMessageStar(parsed.data.messageId, ctx.userId)
revalidatePath("/messages")
revalidatePath(`/messages/${parsed.data.messageId}`)
await invalidateFor("messaging.toggleStar", { id: parsed.data.messageId })
void trackEvent({
event: "message.star_toggled",
@@ -422,7 +418,7 @@ export async function deleteMessageDraftAction(draftId: string): Promise<ActionS
const ctx = await requirePermission(Permissions.MESSAGE_SEND)
await deleteMessageDraft(draftId, ctx.userId)
revalidatePath("/messages/compose")
await invalidateFor("messaging.draft.delete")
return { success: true, message: "Draft deleted" }
} catch (e) {
@@ -448,7 +444,7 @@ export async function bulkMarkMessagesAsReadAction(
}
const updated = await bulkMarkMessagesAsRead(parsed.data.ids, ctx.userId)
revalidatePath("/messages")
await invalidateFor("messaging.bulkMarkRead")
void trackEvent({
event: "message.marked_read",
@@ -477,7 +473,7 @@ export async function bulkDeleteMessagesAction(
}
await bulkDeleteMessages(parsed.data.ids, ctx.userId)
revalidatePath("/messages")
await invalidateFor("messaging.bulkDelete")
void trackEvent({
event: "message.deleted",
@@ -506,7 +502,7 @@ export async function bulkToggleMessagesStarAction(
}
await bulkToggleMessagesStar(parsed.data.ids, ctx.userId)
revalidatePath("/messages")
await invalidateFor("messaging.bulkToggleStar")
void trackEvent({
event: "message.star_toggled",
@@ -550,8 +546,7 @@ export async function recallMessageAction(
return { success: false, message: "Recall window expired" }
}
revalidatePath("/messages")
revalidatePath(`/messages/${parsed.data.messageId}`)
await invalidateFor("messaging.recall", { id: parsed.data.messageId })
void trackEvent({
event: "message.recalled",
@@ -632,7 +627,7 @@ export async function sendGroupMessageAction(
)
)
revalidatePath("/messages")
await invalidateFor("messaging.sendGroup")
void trackEvent({
event: "message.group_sent",
@@ -713,7 +708,7 @@ export async function saveMessageTemplateAction(
content: input.content,
sortOrder: input.sortOrder,
})
revalidatePath("/messages/compose")
await invalidateFor("messaging.template.save", { id: input.templateId })
return { success: true, message: "Template updated", data: input.templateId }
}
@@ -724,7 +719,7 @@ export async function saveMessageTemplateAction(
sortOrder: input.sortOrder,
})
revalidatePath("/messages/compose")
await invalidateFor("messaging.template.save", { id })
void trackEvent({
event: "message.template_created",
@@ -751,7 +746,7 @@ export async function deleteMessageTemplateAction(templateId: string): Promise<A
}
await deleteMessageTemplate(parsed.data.messageId, ctx.userId)
revalidatePath("/messages/compose")
await invalidateFor("messaging.template.delete", { id: parsed.data.messageId })
return { success: true, message: "Template deleted" }
} catch (e) {
@@ -891,7 +886,7 @@ export async function blockUserAction(
return { success: false, message: "User already blocked" }
}
revalidatePath("/messages")
await invalidateFor("messaging.block", { id: input.blockedId })
void trackEvent({
event: "message.user_blocked",
@@ -915,7 +910,7 @@ export async function unblockUserAction(blockedId: string): Promise<ActionState<
try {
const ctx = await requirePermission(Permissions.MESSAGE_SEND)
await unblockUser(ctx.userId, blockedId)
revalidatePath("/messages")
await invalidateFor("messaging.unblock", { id: blockedId })
void trackEvent({
event: "message.user_unblocked",

View File

@@ -13,7 +13,7 @@
* 站内通知读取/标记已读复用 MESSAGE_READ 权限(任何能读消息的用户都能查看通知)。
*/
import { revalidatePath } from "next/cache"
import { invalidateFor } from "@/shared/lib/cache"
import { z } from "zod"
import { PermissionDeniedError, requirePermission } from "@/shared/lib/auth-guard"
import { trackEvent } from "@/shared/lib/track-event"
@@ -232,7 +232,7 @@ export async function markNotificationAsReadAction(
}
await markNotificationAsRead(parsed.data, ctx.userId)
revalidatePath("/messages")
await invalidateFor("notifications.markRead", { id: parsed.data })
void trackEvent({
event: "notification.marked_read",
@@ -256,7 +256,7 @@ export async function markAllNotificationsAsReadAction(): Promise<ActionState<st
try {
const ctx = await requirePermission(Permissions.MESSAGE_READ)
await markAllNotificationsAsRead(ctx.userId)
revalidatePath("/messages")
await invalidateFor("notifications.markAllRead")
void trackEvent({
event: "notification.marked_all_read",
@@ -287,7 +287,7 @@ export async function archiveNotificationAction(
}
await archiveNotification(parsed.data, ctx.userId)
revalidatePath("/messages")
await invalidateFor("notifications.archive", { id: parsed.data })
void trackEvent({
event: "notification.archived",
@@ -372,7 +372,7 @@ export async function updateNotificationPreferencesAction(
return { success: false, message: "Failed to update notification preferences" }
}
revalidatePath("/settings")
await invalidateFor("notifications.updatePreferences")
return { success: true, message: "Notification preferences updated", data: updated }
} catch (e) {

View File

@@ -1,6 +1,5 @@
"use server"
import { revalidatePath } from "next/cache"
import { after } from "next/server"
import type { ActionState } from "@/shared/types/action-state"
@@ -8,6 +7,7 @@ import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guar
import { Permissions, type Permission } from "@/shared/types/permissions"
import { logAudit } from "@/shared/lib/audit-logger"
import { logDataChange } from "@/shared/lib/change-logger"
import { invalidateFor } from "@/shared/lib/cache"
import {
AssignUserRolesSchema,
@@ -71,7 +71,7 @@ export async function createRoleAction(
})
)
revalidatePath("/admin/roles")
await invalidateFor("rbac.role.create", { id: role.id })
return { success: true, data: role, message: "Role created" }
} catch (error) {
if (error instanceof PermissionDeniedError) {
@@ -131,8 +131,7 @@ export async function updateRoleAction(
})
)
revalidatePath("/admin/roles")
revalidatePath(`/admin/roles/${roleId}`)
await invalidateFor("rbac.role.update", { id: roleId })
return { success: true, data: role, message: "Role updated" }
} catch (error) {
if (error instanceof PermissionDeniedError) {
@@ -177,7 +176,7 @@ export async function deleteRoleAction(
})
)
revalidatePath("/admin/roles")
await invalidateFor("rbac.role.delete", { id: roleId })
return { success: true, message: "Role deleted" }
} catch (error) {
if (error instanceof PermissionDeniedError) {
@@ -212,8 +211,7 @@ export async function toggleRoleEnabledAction(
})
)
revalidatePath("/admin/roles")
revalidatePath(`/admin/roles/${roleId}`)
await invalidateFor("rbac.role.toggleEnabled", { id: roleId })
return { success: true, data: role, message: enabled ? "Role enabled" : "Role disabled" }
} catch (error) {
if (error instanceof PermissionDeniedError) {
@@ -283,8 +281,7 @@ export async function setRolePermissionsAction(
})
)
revalidatePath("/admin/roles")
revalidatePath(`/admin/roles/${parsed.data.roleId}`)
await invalidateFor("rbac.role.setPermissions", { id: parsed.data.roleId })
return { success: true, data: permissions, message: "Permissions updated" }
} catch (error) {
if (error instanceof PermissionDeniedError) {
@@ -348,7 +345,7 @@ export async function assignUserRolesAction(
})
)
revalidatePath("/admin/users")
await invalidateFor("rbac.assignUserRoles", { id: parsed.data.userId })
return {
success: true,
data: parsed.data.roleNames,

View File

@@ -1,7 +1,5 @@
"use server"
import { revalidatePath } from "next/cache"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
@@ -11,6 +9,7 @@ import {
deleteFileAttachment,
getFileByUrl,
} from "@/modules/files/data-access"
import { invalidateFor } from "@/shared/lib/cache"
/**
* 清理旧头像文件(磁盘 + DB 记录)
@@ -62,8 +61,7 @@ export async function updateUserAvatarAction(
await cleanupOldAvatarFile(oldImageUrl)
}
revalidatePath("/profile")
revalidatePath("/settings")
await invalidateFor("settings.avatar.update", { id: ctx.userId })
return { success: true, data: { image: updated.image ?? imageUrl } }
} catch (error) {
@@ -94,8 +92,7 @@ export async function removeUserAvatarAction(): Promise<ActionState<null>> {
await cleanupOldAvatarFile(oldImageUrl)
}
revalidatePath("/profile")
revalidatePath("/settings")
await invalidateFor("settings.avatar.remove", { id: ctx.userId })
return { success: true, data: null }
} catch (error) {

View File

@@ -1,12 +1,12 @@
"use server"
import { z } from "zod"
import { revalidatePath } from "next/cache"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getSession } from "@/shared/lib/session"
import { invalidateFor } from "@/shared/lib/cache"
import { getBrandConfig, saveBrandConfig } from "./data-access-brand"
import type { BrandConfig } from "./brand-config"
@@ -67,9 +67,7 @@ export async function saveBrandConfigAction(
}
await saveBrandConfig(config, session?.user?.id)
revalidatePath("/admin/settings")
revalidatePath("/(auth)/login")
revalidatePath("/(auth)/register")
await invalidateFor("settings.brand.save")
return { success: true, data: config, message: "Brand configuration saved" }
} catch (e) {

View File

@@ -1,6 +1,5 @@
"use server"
import { revalidatePath } from "next/cache"
import { compare, hash } from "bcryptjs"
import { z } from "zod"
@@ -11,6 +10,7 @@ import { validatePassword } from "@/shared/lib/password-policy"
import { rateLimit, rateLimitKey, RATE_LIMIT_RULES } from "@/shared/lib/rate-limit"
import { normalizeBcryptHash } from "@/shared/lib/bcrypt-utils"
import { checkBreachedPassword } from "@/shared/lib/breached-password"
import { invalidateFor } from "@/shared/lib/cache"
import {
getPasswordSecurityByUserId,
@@ -103,7 +103,7 @@ export async function changePasswordAction(
await updateUserPassword(userId, newHash, now)
await upsertPasswordSecurityOnPasswordChange(userId, now, existingSecurity)
revalidatePath("/settings")
await invalidateFor("settings.password.change")
return { success: true, message: "Password changed successfully", data: null }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }

View File

@@ -1,6 +1,5 @@
"use server"
import { revalidatePath } from "next/cache"
import { desc, eq } from "drizzle-orm"
import type { ActionState } from "@/shared/types/action-state"
@@ -11,6 +10,7 @@ import { loginLogs } from "@/shared/db/schema"
import { logLoginEvent } from "@/shared/lib/login-logger"
import { trackAuthEvent } from "@/shared/lib/track-event"
import { getUserProfile } from "@/modules/users/data-access"
import { invalidateFor } from "@/shared/lib/cache"
import {
getSystemSettingsByCategory,
@@ -236,7 +236,7 @@ export async function verifyTwoFactorAction(
await setTwoFactorEnabled(ctx.userId, true)
await setTwoFactorEnabledAt(ctx.userId, now)
revalidatePath("/settings")
await invalidateFor("settings.security.verify2FA", { id: ctx.userId })
const status = await getTwoFactorStatus(ctx.userId)
@@ -301,7 +301,7 @@ export async function disableTwoFactorAction(
await deleteTotpSecret(ctx.userId)
await deleteBackupCodes(ctx.userId)
revalidatePath("/settings")
await invalidateFor("settings.security.disable2FA", { id: ctx.userId })
const status = await getTwoFactorStatus(ctx.userId)
@@ -348,7 +348,7 @@ export async function regenerateBackupCodesAction(
const hashedJson = await hashBackupCodes(backupCodes)
await setBackupCodesHashed(ctx.userId, hashedJson)
revalidatePath("/settings")
await invalidateFor("settings.security.regenerateBackupCodes", { id: ctx.userId })
const status = await getTwoFactorStatus(ctx.userId)
return {
@@ -397,7 +397,7 @@ export async function revokeAllOtherSessionsAction(): Promise<
"Remote logout requested (JWT-based, no active DB sessions to revoke)",
})
revalidatePath("/settings")
await invalidateFor("settings.security.revokeSessions", { id: ctx.userId })
return { success: true, data: { revokedCount } }
} catch (error) {

View File

@@ -9,8 +9,6 @@
* 的函数必须是 "use server" 标记的 Server Action
*/
import { revalidatePath } from "next/cache"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
@@ -23,6 +21,7 @@ import type {
NotificationPreferences,
UpdateNotificationPreferencesInput,
} from "@/modules/notifications/types"
import { invalidateFor } from "@/shared/lib/cache"
/**
* 更新用户资料Server Action wrapper
@@ -52,7 +51,7 @@ export async function updateNotificationPreferencesAction(
return { success: false, message: "Failed to update notification preferences" }
}
revalidatePath("/settings")
await invalidateFor("settings.notifications.updatePreferences")
return { success: true, message: "Notification preferences updated", data: updated }
} catch (e) {

View File

@@ -1,11 +1,11 @@
"use server"
import { z } from "zod"
import { revalidatePath } from "next/cache"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { invalidateFor } from "@/shared/lib/cache"
import {
getAllSystemSettings,
@@ -157,7 +157,7 @@ export async function saveAdminSystemSettingsAction(
await upsertSystemSettings(items, ctx.userId)
revalidatePath("/admin/settings")
await invalidateFor("settings.systemSettings.save")
return { success: true, data: null }
} catch (error) {

View File

@@ -1,7 +1,6 @@
"use server"
import { z } from "zod"
import { revalidatePath } from "next/cache"
import { createId } from "@paralleldrive/cuid2"
import type { ActionState } from "@/shared/types/action-state"
@@ -12,6 +11,7 @@ import {
} from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"
import { invalidateFor } from "@/shared/lib/cache"
import {
countDefaultAiProviders,
@@ -181,8 +181,7 @@ export async function upsertAiProviderAction(
payload.isDefault === true
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
await invalidateFor("settings.aiProvider.upsert", { id })
return { success: true, message: "AI provider updated", data: id }
}
@@ -214,8 +213,7 @@ export async function upsertAiProviderAction(
shouldMakeDefault
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
await invalidateFor("settings.aiProvider.upsert", { id })
return { success: true, message: "AI provider created", data: id }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
@@ -281,8 +279,7 @@ export async function deleteAiProviderAction(
parsed.data.id,
user.isAdmin ? undefined : user.id
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
await invalidateFor("settings.aiProvider.delete", { id: parsed.data.id })
return { success: true, message: "AI provider deleted", data: null }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }

View File

@@ -152,6 +152,13 @@ export const CLIENT_INVALIDATION_MAP = {
"lesson-preparation.substitute.create": { queryKeys: [["lesson-preparation", "substitutes"]] },
"lesson-preparation.substitute.updateStatus": { queryKeys: [["lesson-preparation", "substitutes"]] },
"lesson-preparation.substitute.delete": { queryKeys: [["lesson-preparation", "substitutes"]] },
"lesson-preparation.attachment.create": { queryKeys: [["lesson-preparation", "detail"]] },
"lesson-preparation.review.submit": { queryKeys: [["lesson-preparation", "detail"]] },
"lesson-preparation.review.review": { queryKeys: [["lesson-preparation", "detail"]] },
"lesson-preparation.review.withdraw": { queryKeys: [["lesson-preparation", "detail"]] },
"lesson-preparation.schedule.create": { queryKeys: [["lesson-preparation", "schedules"]] },
"lesson-preparation.schedule.delete": { queryKeys: [["lesson-preparation", "schedules"]] },
"lesson-preparation.publishHomework": { queryKeys: [["lesson-preparation", "list"], ["homework", "list"]] },
// ===== messaging 模块 =====
"messaging.send": { queryKeys: [["messaging", "list"]] },

View File

@@ -685,6 +685,41 @@ export const INVALIDATION_MAP = {
queryKeys: [["lesson-preparation", "substitutes"]],
paths: ["/teacher/lesson-plans"],
},
"lesson-preparation.attachment.create": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "detail"]],
paths: ["/teacher/lesson-plans"],
},
"lesson-preparation.review.submit": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "detail"]],
paths: ["/teacher/lesson-plans"],
},
"lesson-preparation.review.review": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "detail"]],
paths: ["/teacher/lesson-plans"],
},
"lesson-preparation.review.withdraw": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "detail"]],
paths: ["/teacher/lesson-plans"],
},
"lesson-preparation.schedule.create": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "schedules"]],
paths: ["/teacher/lesson-plans", "/teacher/lesson-plans/calendar"],
},
"lesson-preparation.schedule.delete": {
tags: ["lesson-preparation"],
queryKeys: [["lesson-preparation", "schedules"]],
paths: ["/teacher/lesson-plans", "/teacher/lesson-plans/calendar"],
},
"lesson-preparation.publishHomework": {
tags: ["lesson-preparation", "homework"],
queryKeys: [["lesson-preparation", "list"], ["homework", "list"]],
paths: ["/teacher/lesson-plans", "/teacher/homework"],
},
// ===== messaging 模块 =====
"messaging.send": {