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:
320
src/modules/course-plans/data-access.ts
Normal file
320
src/modules/course-plans/data-access.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import "server-only"
|
||||
|
||||
import { cache } from "react"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { and, asc, desc, eq, inArray, type SQL } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
classes,
|
||||
coursePlanItems,
|
||||
coursePlans,
|
||||
subjects,
|
||||
users,
|
||||
} from "@/shared/db/schema"
|
||||
import type {
|
||||
CoursePlan,
|
||||
CoursePlanItem,
|
||||
CoursePlanListItem,
|
||||
CoursePlanStatus,
|
||||
CoursePlanWithItems,
|
||||
GetCoursePlansParams,
|
||||
ReorderCoursePlanItemInput,
|
||||
} from "./types"
|
||||
import type {
|
||||
CreateCoursePlanInput,
|
||||
CreateCoursePlanItemInput,
|
||||
UpdateCoursePlanInput,
|
||||
UpdateCoursePlanItemInput,
|
||||
} from "./schema"
|
||||
|
||||
const toIso = (d: Date | null | undefined): string | null =>
|
||||
d ? d.toISOString() : null
|
||||
|
||||
const toIsoRequired = (d: Date): string => d.toISOString()
|
||||
|
||||
const mapPlanRow = (
|
||||
row: {
|
||||
id: string
|
||||
classId: string
|
||||
subjectId: string
|
||||
teacherId: string
|
||||
academicYearId: string | null
|
||||
semester: "1" | "2"
|
||||
totalHours: number
|
||||
completedHours: number
|
||||
weeklyHours: number
|
||||
startDate: Date | null
|
||||
endDate: Date | null
|
||||
syllabus: string | null
|
||||
objectives: string | null
|
||||
status: CoursePlanStatus
|
||||
createdBy: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
className: string | null
|
||||
subjectName: string | null
|
||||
teacherName: string | null
|
||||
}
|
||||
): CoursePlanListItem => ({
|
||||
id: row.id,
|
||||
classId: row.classId,
|
||||
subjectId: row.subjectId,
|
||||
teacherId: row.teacherId,
|
||||
academicYearId: row.academicYearId,
|
||||
semester: row.semester,
|
||||
totalHours: Number(row.totalHours),
|
||||
completedHours: Number(row.completedHours),
|
||||
weeklyHours: Number(row.weeklyHours),
|
||||
startDate: toIso(row.startDate),
|
||||
endDate: toIso(row.endDate),
|
||||
syllabus: row.syllabus,
|
||||
objectives: row.objectives,
|
||||
status: row.status,
|
||||
createdBy: row.createdBy,
|
||||
createdAt: toIsoRequired(row.createdAt),
|
||||
updatedAt: toIsoRequired(row.updatedAt),
|
||||
className: row.className,
|
||||
subjectName: row.subjectName,
|
||||
teacherName: row.teacherName,
|
||||
})
|
||||
|
||||
const mapItemRow = (
|
||||
row: {
|
||||
id: string
|
||||
planId: string
|
||||
week: number
|
||||
topic: string
|
||||
content: string | null
|
||||
hours: number
|
||||
textbookChapter: string | null
|
||||
notes: string | null
|
||||
isCompleted: boolean
|
||||
completedAt: Date | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
): CoursePlanItem => ({
|
||||
id: row.id,
|
||||
planId: row.planId,
|
||||
week: Number(row.week),
|
||||
topic: row.topic,
|
||||
content: row.content,
|
||||
hours: Number(row.hours),
|
||||
textbookChapter: row.textbookChapter,
|
||||
notes: row.notes,
|
||||
isCompleted: Boolean(row.isCompleted),
|
||||
completedAt: toIso(row.completedAt),
|
||||
createdAt: toIsoRequired(row.createdAt),
|
||||
updatedAt: toIsoRequired(row.updatedAt),
|
||||
})
|
||||
|
||||
const buildPlanSelect = () =>
|
||||
db
|
||||
.select({
|
||||
id: coursePlans.id,
|
||||
classId: coursePlans.classId,
|
||||
subjectId: coursePlans.subjectId,
|
||||
teacherId: coursePlans.teacherId,
|
||||
academicYearId: coursePlans.academicYearId,
|
||||
semester: coursePlans.semester,
|
||||
totalHours: coursePlans.totalHours,
|
||||
completedHours: coursePlans.completedHours,
|
||||
weeklyHours: coursePlans.weeklyHours,
|
||||
startDate: coursePlans.startDate,
|
||||
endDate: coursePlans.endDate,
|
||||
syllabus: coursePlans.syllabus,
|
||||
objectives: coursePlans.objectives,
|
||||
status: coursePlans.status,
|
||||
createdBy: coursePlans.createdBy,
|
||||
createdAt: coursePlans.createdAt,
|
||||
updatedAt: coursePlans.updatedAt,
|
||||
className: classes.name,
|
||||
subjectName: subjects.name,
|
||||
teacherName: users.name,
|
||||
})
|
||||
.from(coursePlans)
|
||||
.leftJoin(classes, eq(classes.id, coursePlans.classId))
|
||||
.leftJoin(subjects, eq(subjects.id, coursePlans.subjectId))
|
||||
.leftJoin(users, eq(users.id, coursePlans.teacherId))
|
||||
|
||||
export const getCoursePlans = cache(
|
||||
async (params?: GetCoursePlansParams): Promise<CoursePlanListItem[]> => {
|
||||
try {
|
||||
const conditions: SQL[] = []
|
||||
if (params?.classId) conditions.push(eq(coursePlans.classId, params.classId))
|
||||
if (params?.teacherId) conditions.push(eq(coursePlans.teacherId, params.teacherId))
|
||||
if (params?.subjectId) conditions.push(eq(coursePlans.subjectId, params.subjectId))
|
||||
if (params?.status)
|
||||
conditions.push(eq(coursePlans.status, params.status as CoursePlanStatus))
|
||||
|
||||
const query = buildPlanSelect()
|
||||
const rows = await (conditions.length > 0
|
||||
? query.where(and(...conditions))
|
||||
: query
|
||||
).orderBy(desc(coursePlans.createdAt))
|
||||
|
||||
return rows.map(mapPlanRow)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const getCoursePlanById = cache(
|
||||
async (id: string): Promise<CoursePlanWithItems | null> => {
|
||||
try {
|
||||
const [planRow] = await buildPlanSelect()
|
||||
.where(eq(coursePlans.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!planRow) return null
|
||||
|
||||
const itemRows = await db
|
||||
.select()
|
||||
.from(coursePlanItems)
|
||||
.where(eq(coursePlanItems.planId, id))
|
||||
.orderBy(asc(coursePlanItems.week), asc(coursePlanItems.createdAt))
|
||||
|
||||
return {
|
||||
...mapPlanRow(planRow),
|
||||
items: itemRows.map(mapItemRow),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export async function createCoursePlan(
|
||||
data: CreateCoursePlanInput,
|
||||
createdBy: string
|
||||
): Promise<string> {
|
||||
const id = createId()
|
||||
await db.insert(coursePlans).values({
|
||||
id,
|
||||
classId: data.classId,
|
||||
subjectId: data.subjectId,
|
||||
teacherId: data.teacherId,
|
||||
academicYearId: data.academicYearId,
|
||||
semester: data.semester,
|
||||
totalHours: data.totalHours,
|
||||
completedHours: 0,
|
||||
weeklyHours: data.weeklyHours,
|
||||
startDate: data.startDate ? new Date(data.startDate) : null,
|
||||
endDate: data.endDate ? new Date(data.endDate) : null,
|
||||
syllabus: data.syllabus,
|
||||
objectives: data.objectives,
|
||||
status: data.status,
|
||||
createdBy,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
export async function updateCoursePlan(
|
||||
id: string,
|
||||
data: Partial<UpdateCoursePlanInput>
|
||||
): Promise<void> {
|
||||
const update: Partial<typeof coursePlans.$inferSelect> = {}
|
||||
if (data.classId !== undefined) update.classId = data.classId
|
||||
if (data.subjectId !== undefined) update.subjectId = data.subjectId
|
||||
if (data.teacherId !== undefined) update.teacherId = data.teacherId
|
||||
if (data.academicYearId !== undefined) update.academicYearId = data.academicYearId
|
||||
if (data.semester !== undefined) update.semester = data.semester
|
||||
if (data.totalHours !== undefined) update.totalHours = data.totalHours
|
||||
if (data.completedHours !== undefined) update.completedHours = data.completedHours
|
||||
if (data.weeklyHours !== undefined) update.weeklyHours = data.weeklyHours
|
||||
if (data.startDate !== undefined)
|
||||
update.startDate = data.startDate ? new Date(data.startDate) : null
|
||||
if (data.endDate !== undefined)
|
||||
update.endDate = data.endDate ? new Date(data.endDate) : null
|
||||
if (data.syllabus !== undefined) update.syllabus = data.syllabus
|
||||
if (data.objectives !== undefined) update.objectives = data.objectives
|
||||
if (data.status !== undefined) update.status = data.status
|
||||
|
||||
if (Object.keys(update).length === 0) return
|
||||
|
||||
await db.update(coursePlans).set(update).where(eq(coursePlans.id, id))
|
||||
}
|
||||
|
||||
export async function deleteCoursePlan(id: string): Promise<void> {
|
||||
await db.delete(coursePlans).where(eq(coursePlans.id, id))
|
||||
}
|
||||
|
||||
export async function createCoursePlanItem(
|
||||
data: CreateCoursePlanItemInput
|
||||
): Promise<string> {
|
||||
const id = createId()
|
||||
await db.insert(coursePlanItems).values({
|
||||
id,
|
||||
planId: data.planId,
|
||||
week: data.week,
|
||||
topic: data.topic,
|
||||
content: data.content,
|
||||
hours: data.hours,
|
||||
textbookChapter: data.textbookChapter,
|
||||
notes: data.notes,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
export async function updateCoursePlanItem(
|
||||
id: string,
|
||||
data: Partial<UpdateCoursePlanItemInput>
|
||||
): Promise<void> {
|
||||
const update: Partial<typeof coursePlanItems.$inferSelect> = {}
|
||||
if (data.week !== undefined) update.week = data.week
|
||||
if (data.topic !== undefined) update.topic = data.topic
|
||||
if (data.content !== undefined) update.content = data.content
|
||||
if (data.hours !== undefined) update.hours = data.hours
|
||||
if (data.textbookChapter !== undefined) update.textbookChapter = data.textbookChapter
|
||||
if (data.notes !== undefined) update.notes = data.notes
|
||||
if (data.isCompleted !== undefined) update.isCompleted = data.isCompleted
|
||||
if (data.completedAt !== undefined)
|
||||
update.completedAt = data.completedAt ? new Date(data.completedAt) : null
|
||||
|
||||
if (Object.keys(update).length === 0) return
|
||||
|
||||
await db.update(coursePlanItems).set(update).where(eq(coursePlanItems.id, id))
|
||||
}
|
||||
|
||||
export async function deleteCoursePlanItem(id: string): Promise<void> {
|
||||
await db.delete(coursePlanItems).where(eq(coursePlanItems.id, id))
|
||||
}
|
||||
|
||||
export async function reorderCoursePlanItems(
|
||||
planId: string,
|
||||
items: ReorderCoursePlanItemInput[]
|
||||
): Promise<void> {
|
||||
if (items.length === 0) return
|
||||
|
||||
const itemIds = items.map((i) => i.id)
|
||||
const [existing] = await db
|
||||
.select({ id: coursePlanItems.id })
|
||||
.from(coursePlanItems)
|
||||
.where(and(eq(coursePlanItems.planId, planId), inArray(coursePlanItems.id, itemIds)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) return
|
||||
|
||||
for (const item of items) {
|
||||
await db
|
||||
.update(coursePlanItems)
|
||||
.set({ week: item.week })
|
||||
.where(eq(coursePlanItems.id, item.id))
|
||||
}
|
||||
}
|
||||
|
||||
export type { CoursePlan, CoursePlanItem, CoursePlanWithItems }
|
||||
|
||||
export const getSubjectOptions = cache(async (): Promise<{ id: string; name: string }[]> => {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ id: subjects.id, name: subjects.name })
|
||||
.from(subjects)
|
||||
.orderBy(asc(subjects.order), asc(subjects.name))
|
||||
return rows.map((r) => ({ id: r.id, name: r.name }))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user