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:
SpecialX
2026-06-17 13:44:37 +08:00
parent 125f7ec54c
commit 3b6272c99d
195 changed files with 27274 additions and 416 deletions

View 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" }
}
}