feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013
## P1 功能(20 项) - 站内消息系统、家长仪表盘、学生考勤管理 - Excel 导入导出、用户批量导入、成绩导出 - 排课规则+自动排课+课表调整 - 成绩趋势+对比分析、密码安全策略、速率限制 - 数据变更日志、文件预览+存储策略、全文检索 - 依赖审计集成 CI、数据库定时备份、E2E 测试完善 - 通知偏好管理 ## 基础设施修复 - src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求) - .env: MySQL 端口从 13002 切换至 14013 - scripts/create-db.ts: 新增数据库初始化脚本 ## 架构文档同步 - 004_architecture_impact_map.md 和 005_architecture_data.json 完整记录所有新增表、模块、路由、权限、依赖关系
This commit is contained in:
242
src/modules/announcements/actions.ts
Normal file
242
src/modules/announcements/actions.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { requireAuth, requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { db } from "@/shared/db"
|
||||
import { announcements } from "@/shared/db/schema"
|
||||
import type { ActionState } from "@/shared/types/action-state"
|
||||
|
||||
import { CreateAnnouncementSchema, UpdateAnnouncementSchema } from "./schema"
|
||||
import { getAnnouncements, getAnnouncementById } from "./data-access"
|
||||
import type { GetAnnouncementsParams, Announcement } from "./types"
|
||||
|
||||
export async function createAnnouncementAction(
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.ANNOUNCEMENT_MANAGE)
|
||||
|
||||
const parsed = CreateAnnouncementSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
content: formData.get("content"),
|
||||
type: formData.get("type") || undefined,
|
||||
status: formData.get("status") || undefined,
|
||||
targetGradeId: formData.get("targetGradeId") || undefined,
|
||||
targetClassId: formData.get("targetClassId") || undefined,
|
||||
publishedAt: formData.get("publishedAt") || undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const input = parsed.data
|
||||
const isPublished = input.status === "published"
|
||||
const publishedAt = isPublished
|
||||
? input.publishedAt
|
||||
? new Date(input.publishedAt)
|
||||
: new Date()
|
||||
: input.publishedAt
|
||||
? new Date(input.publishedAt)
|
||||
: null
|
||||
|
||||
const id = createId()
|
||||
await db.insert(announcements).values({
|
||||
id,
|
||||
title: input.title,
|
||||
content: input.content,
|
||||
type: input.type,
|
||||
status: input.status,
|
||||
targetGradeId: input.targetGradeId,
|
||||
targetClassId: input.targetClassId,
|
||||
authorId: ctx.userId,
|
||||
publishedAt,
|
||||
})
|
||||
|
||||
revalidatePath("/admin/announcements")
|
||||
revalidatePath("/announcements")
|
||||
|
||||
return { success: true, message: "Announcement created", data: id }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAnnouncementAction(
|
||||
id: string,
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ANNOUNCEMENT_MANAGE)
|
||||
|
||||
const existing = await getAnnouncementById(id)
|
||||
if (!existing) return { success: false, message: "Announcement not found" }
|
||||
|
||||
const parsed = UpdateAnnouncementSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
content: formData.get("content"),
|
||||
type: formData.get("type") || undefined,
|
||||
status: formData.get("status") || undefined,
|
||||
targetGradeId: formData.get("targetGradeId") || undefined,
|
||||
targetClassId: formData.get("targetClassId") || undefined,
|
||||
publishedAt: formData.get("publishedAt") || undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const input = parsed.data
|
||||
const wasPublished = existing.status === "published"
|
||||
const isPublished = input.status === "published"
|
||||
const publishedAt = isPublished
|
||||
? existing.publishedAt
|
||||
? new Date(existing.publishedAt)
|
||||
: new Date()
|
||||
: input.publishedAt
|
||||
? new Date(input.publishedAt)
|
||||
: null
|
||||
|
||||
await db
|
||||
.update(announcements)
|
||||
.set({
|
||||
title: input.title,
|
||||
content: input.content,
|
||||
type: input.type,
|
||||
status: input.status,
|
||||
targetGradeId: input.targetGradeId,
|
||||
targetClassId: input.targetClassId,
|
||||
publishedAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(announcements.id, id))
|
||||
|
||||
revalidatePath("/admin/announcements")
|
||||
revalidatePath(`/admin/announcements/${id}`)
|
||||
revalidatePath("/announcements")
|
||||
void wasPublished
|
||||
|
||||
return { success: true, message: "Announcement updated", data: id }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAnnouncementAction(id: string): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ANNOUNCEMENT_MANAGE)
|
||||
|
||||
const existing = await getAnnouncementById(id)
|
||||
if (!existing) return { success: false, message: "Announcement not found" }
|
||||
|
||||
await db.delete(announcements).where(eq(announcements.id, id))
|
||||
|
||||
revalidatePath("/admin/announcements")
|
||||
revalidatePath("/announcements")
|
||||
|
||||
return { success: true, message: "Announcement deleted" }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishAnnouncementAction(id: string): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ANNOUNCEMENT_MANAGE)
|
||||
|
||||
const existing = await getAnnouncementById(id)
|
||||
if (!existing) return { success: false, message: "Announcement not found" }
|
||||
|
||||
await db
|
||||
.update(announcements)
|
||||
.set({
|
||||
status: "published",
|
||||
publishedAt: existing.publishedAt ? new Date(existing.publishedAt) : new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(announcements.id, id))
|
||||
|
||||
revalidatePath("/admin/announcements")
|
||||
revalidatePath(`/admin/announcements/${id}`)
|
||||
revalidatePath("/announcements")
|
||||
|
||||
return { success: true, message: "Announcement published" }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function archiveAnnouncementAction(id: string): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ANNOUNCEMENT_MANAGE)
|
||||
|
||||
const existing = await getAnnouncementById(id)
|
||||
if (!existing) return { success: false, message: "Announcement not found" }
|
||||
|
||||
await db
|
||||
.update(announcements)
|
||||
.set({
|
||||
status: "archived",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(announcements.id, id))
|
||||
|
||||
revalidatePath("/admin/announcements")
|
||||
revalidatePath(`/admin/announcements/${id}`)
|
||||
revalidatePath("/announcements")
|
||||
|
||||
return { success: true, message: "Announcement archived" }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAnnouncementsAction(
|
||||
params?: GetAnnouncementsParams
|
||||
): Promise<ActionState<Announcement[]>> {
|
||||
try {
|
||||
await requireAuth()
|
||||
const data = await getAnnouncements(params)
|
||||
return { success: true, data }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user