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:
271
src/modules/attendance/actions.ts
Normal file
271
src/modules/attendance/actions.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import type { ActionState } from "@/shared/types/action-state"
|
||||
|
||||
import {
|
||||
RecordAttendanceSchema,
|
||||
BatchRecordAttendanceSchema,
|
||||
UpdateAttendanceSchema,
|
||||
AttendanceRuleSchema,
|
||||
} from "./schema"
|
||||
import {
|
||||
createAttendanceRecord,
|
||||
batchCreateAttendanceRecords,
|
||||
updateAttendanceRecord,
|
||||
deleteAttendanceRecord,
|
||||
getAttendanceRecords,
|
||||
getClassAttendanceForDate,
|
||||
getAttendanceRules,
|
||||
upsertAttendanceRules,
|
||||
} from "./data-access"
|
||||
import {
|
||||
getStudentAttendanceSummary,
|
||||
getClassAttendanceStats,
|
||||
} from "./data-access-stats"
|
||||
import type { AttendanceQueryParams, AttendanceListItem } from "./types"
|
||||
|
||||
export async function recordAttendanceAction(
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.ATTENDANCE_MANAGE)
|
||||
|
||||
const parsed = RecordAttendanceSchema.safeParse({
|
||||
studentId: formData.get("studentId"),
|
||||
classId: formData.get("classId"),
|
||||
date: formData.get("date"),
|
||||
status: formData.get("status"),
|
||||
remark: formData.get("remark") || undefined,
|
||||
scheduleId: formData.get("scheduleId") || undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const id = await createAttendanceRecord(parsed.data, ctx.userId)
|
||||
revalidatePath("/teacher/attendance")
|
||||
return { success: true, message: "Attendance recorded", 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 batchRecordAttendanceAction(
|
||||
prevState: ActionState<number> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<number>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.ATTENDANCE_MANAGE)
|
||||
|
||||
const recordsJson = formData.get("recordsJson")
|
||||
if (typeof recordsJson !== "string" || recordsJson.length === 0) {
|
||||
return { success: false, message: "Missing records data" }
|
||||
}
|
||||
|
||||
const parsed = BatchRecordAttendanceSchema.safeParse({
|
||||
records: JSON.parse(recordsJson),
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const count = await batchCreateAttendanceRecords(parsed.data, ctx.userId)
|
||||
revalidatePath("/teacher/attendance")
|
||||
return { success: true, message: `Recorded attendance for ${count} students`, data: count }
|
||||
} 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 updateAttendanceAction(
|
||||
id: string,
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_MANAGE)
|
||||
|
||||
const parsed = UpdateAttendanceSchema.safeParse({
|
||||
status: formData.get("status") || undefined,
|
||||
remark: formData.get("remark") || undefined,
|
||||
scheduleId: formData.get("scheduleId") || undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
await updateAttendanceRecord(id, parsed.data)
|
||||
revalidatePath("/teacher/attendance")
|
||||
return { success: true, message: "Attendance updated" }
|
||||
} 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 deleteAttendanceAction(
|
||||
id: string
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_MANAGE)
|
||||
await deleteAttendanceRecord(id)
|
||||
revalidatePath("/teacher/attendance")
|
||||
return { success: true, message: "Attendance record 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 getAttendanceAction(
|
||||
params: AttendanceQueryParams
|
||||
): Promise<ActionState<{ items: AttendanceListItem[]; total: number; page: number; pageSize: number; totalPages: number }>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const result = await getAttendanceRecords({
|
||||
...params,
|
||||
scope: ctx.dataScope,
|
||||
currentUserId: ctx.userId,
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
}
|
||||
} 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 getStudentAttendanceAction(
|
||||
studentId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<ActionState<Awaited<ReturnType<typeof getStudentAttendanceSummary>>>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
|
||||
if (ctx.dataScope.type === "class_members" && ctx.userId !== studentId) {
|
||||
return { success: false, message: "Can only view your own attendance" }
|
||||
}
|
||||
if (ctx.dataScope.type === "children" && !ctx.dataScope.childrenIds.includes(studentId)) {
|
||||
return { success: false, message: "Can only view your children's attendance" }
|
||||
}
|
||||
|
||||
const summary = await getStudentAttendanceSummary(studentId, startDate, endDate)
|
||||
return { success: true, data: summary }
|
||||
} 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 getClassAttendanceStatsAction(
|
||||
classId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<ActionState<Awaited<ReturnType<typeof getClassAttendanceStats>>>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const result = await getClassAttendanceStats(classId, startDate, endDate)
|
||||
return { success: true, data: result }
|
||||
} 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 getClassAttendanceForDateAction(
|
||||
classId: string,
|
||||
date: string
|
||||
): Promise<ActionState<AttendanceListItem[]>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const records = await getClassAttendanceForDate(classId, date)
|
||||
return { success: true, data: records }
|
||||
} 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 saveAttendanceRulesAction(
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_MANAGE)
|
||||
|
||||
const parsed = AttendanceRuleSchema.safeParse({
|
||||
classId: formData.get("classId"),
|
||||
lateThresholdMinutes: formData.get("lateThresholdMinutes") || undefined,
|
||||
earlyLeaveThresholdMinutes: formData.get("earlyLeaveThresholdMinutes") || undefined,
|
||||
enableAutoMark: formData.get("enableAutoMark") === "true",
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const id = await upsertAttendanceRules(parsed.data)
|
||||
revalidatePath("/teacher/attendance")
|
||||
return { success: true, message: "Attendance rules saved", 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 getAttendanceRulesAction(
|
||||
classId?: string
|
||||
): Promise<ActionState<Awaited<ReturnType<typeof getAttendanceRules>>>> {
|
||||
try {
|
||||
await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const rules = await getAttendanceRules(classId)
|
||||
return { success: true, data: rules }
|
||||
} 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