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,149 @@
import { z } from "zod"
export const CreateCoursePlanSchema = z
.object({
classId: z.string().trim().min(1),
subjectId: z.string().trim().min(1),
teacherId: z.string().trim().min(1),
academicYearId: z.string().trim().optional().nullable(),
semester: z.enum(["1", "2"]).optional(),
totalHours: z.coerce.number().int().min(0).optional(),
weeklyHours: z.coerce.number().int().min(0).optional(),
startDate: z.string().trim().optional().nullable(),
endDate: z.string().trim().optional().nullable(),
syllabus: z.string().trim().optional().nullable(),
objectives: z.string().trim().optional().nullable(),
status: z.enum(["planning", "active", "completed", "paused"]).optional(),
})
.transform((v) => ({
classId: v.classId,
subjectId: v.subjectId,
teacherId: v.teacherId,
academicYearId: v.academicYearId && v.academicYearId.length > 0 ? v.academicYearId : null,
semester: v.semester ?? "1",
totalHours: v.totalHours ?? 0,
weeklyHours: v.weeklyHours ?? 0,
startDate: v.startDate && v.startDate.length > 0 ? v.startDate : null,
endDate: v.endDate && v.endDate.length > 0 ? v.endDate : null,
syllabus: v.syllabus && v.syllabus.length > 0 ? v.syllabus : null,
objectives: v.objectives && v.objectives.length > 0 ? v.objectives : null,
status: v.status ?? "planning",
}))
export type CreateCoursePlanInput = z.infer<typeof CreateCoursePlanSchema>
export const UpdateCoursePlanSchema = z
.object({
classId: z.string().trim().min(1).optional(),
subjectId: z.string().trim().min(1).optional(),
teacherId: z.string().trim().min(1).optional(),
academicYearId: z.string().trim().optional().nullable(),
semester: z.enum(["1", "2"]).optional(),
totalHours: z.coerce.number().int().min(0).optional(),
completedHours: z.coerce.number().int().min(0).optional(),
weeklyHours: z.coerce.number().int().min(0).optional(),
startDate: z.string().trim().optional().nullable(),
endDate: z.string().trim().optional().nullable(),
syllabus: z.string().trim().optional().nullable(),
objectives: z.string().trim().optional().nullable(),
status: z.enum(["planning", "active", "completed", "paused"]).optional(),
})
.transform((v) => ({
...v,
academicYearId:
v.academicYearId !== undefined
? v.academicYearId && v.academicYearId.length > 0
? v.academicYearId
: null
: undefined,
startDate:
v.startDate !== undefined
? v.startDate && v.startDate.length > 0
? v.startDate
: null
: undefined,
endDate:
v.endDate !== undefined
? v.endDate && v.endDate.length > 0
? v.endDate
: null
: undefined,
syllabus:
v.syllabus !== undefined
? v.syllabus && v.syllabus.length > 0
? v.syllabus
: null
: undefined,
objectives:
v.objectives !== undefined
? v.objectives && v.objectives.length > 0
? v.objectives
: null
: undefined,
}))
export type UpdateCoursePlanInput = z.infer<typeof UpdateCoursePlanSchema>
export const CreateCoursePlanItemSchema = z
.object({
planId: z.string().trim().min(1),
week: z.coerce.number().int().min(1),
topic: z.string().trim().min(1).max(255),
content: z.string().trim().optional().nullable(),
hours: z.coerce.number().int().min(1).optional(),
textbookChapter: z.string().trim().optional().nullable(),
notes: z.string().trim().optional().nullable(),
})
.transform((v) => ({
planId: v.planId,
week: v.week,
topic: v.topic,
content: v.content && v.content.length > 0 ? v.content : null,
hours: v.hours ?? 2,
textbookChapter:
v.textbookChapter && v.textbookChapter.length > 0 ? v.textbookChapter : null,
notes: v.notes && v.notes.length > 0 ? v.notes : null,
}))
export type CreateCoursePlanItemInput = z.infer<typeof CreateCoursePlanItemSchema>
export const UpdateCoursePlanItemSchema = z
.object({
week: z.coerce.number().int().min(1).optional(),
topic: z.string().trim().min(1).max(255).optional(),
content: z.string().trim().optional().nullable(),
hours: z.coerce.number().int().min(1).optional(),
textbookChapter: z.string().trim().optional().nullable(),
notes: z.string().trim().optional().nullable(),
isCompleted: z.boolean().optional(),
completedAt: z.string().trim().optional().nullable(),
})
.transform((v) => ({
...v,
content:
v.content !== undefined
? v.content && v.content.length > 0
? v.content
: null
: undefined,
textbookChapter:
v.textbookChapter !== undefined
? v.textbookChapter && v.textbookChapter.length > 0
? v.textbookChapter
: null
: undefined,
notes:
v.notes !== undefined
? v.notes && v.notes.length > 0
? v.notes
: null
: undefined,
completedAt:
v.completedAt !== undefined
? v.completedAt && v.completedAt.length > 0
? v.completedAt
: null
: undefined,
}))
export type UpdateCoursePlanItemInput = z.infer<typeof UpdateCoursePlanItemSchema>