refactor: P0-3/5/6 解耦修复 - 循环依赖/通知分发/课表写入口

P0-3: 修复 shared/lib <-> auth 循环依赖
- audit-logger.ts, change-logger.ts, auth-guard.ts, classes/data-access.ts
  改用动态 import("@/auth") 打破静态模块级循环依赖
- shared/lib 不再静态导入 @/auth

P0-5: messaging 改用 notifications dispatcher
- messaging/actions.ts 的 sendMessageAction 改用 sendNotification
  替代直接调用 createNotification
- 用户通知偏好(SMS/微信/邮件/站内)现在被正确尊重

P0-6: 统一 classSchedule 写入口到 scheduling/data-access
- 新增 insertClassScheduleItem/updateClassScheduleItemById/
  deleteClassScheduleItemById/replaceClassSchedule 统一写入函数
- classes/data-access.ts 的三个 schedule 写入函数委托给 scheduling
- scheduling/actions.ts 的 applyAutoScheduleAction 改用 replaceClassSchedule
- 移除 scheduling/actions.ts 中不再使用的 classSchedule/createId 导入

验证: tsc --noEmit 0 errors, npm run lint 0 errors
This commit is contained in:
SpecialX
2026-06-17 23:44:02 +08:00
parent 02dc1093fb
commit 220061d62e
7 changed files with 155 additions and 31 deletions

View File

@@ -270,3 +270,99 @@ export async function getClassSubjectsForScheduling(classId: string) {
.innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
.where(eq(classSubjectTeachers.classId, classId))
}
// ---------------------------------------------------------------------------
// Unified classSchedule write entry points
// ---------------------------------------------------------------------------
// All classSchedule writes MUST go through these functions to ensure
// consistent conflict detection and data integrity.
// See: docs/architecture/audit/01_decoupling_roadmap.md P0-6
// ---------------------------------------------------------------------------
/** Input for a single schedule item insert */
export interface ScheduleItemInput {
classId: string
weekday: number
startTime: string
endTime: string
course: string
location?: string | null
}
/**
* Insert a single classSchedule row.
* Returns the generated id.
*/
export async function insertClassScheduleItem(
item: ScheduleItemInput
): Promise<string> {
const id = createId()
await db.insert(classSchedule).values({
id,
classId: item.classId,
weekday: item.weekday,
startTime: item.startTime,
endTime: item.endTime,
course: item.course,
location: item.location ?? null,
})
return id
}
/**
* Update a classSchedule row by id.
* Only the provided fields are updated.
*/
export async function updateClassScheduleItemById(
scheduleId: string,
data: Partial<Omit<ScheduleItemInput, "classId">> & { classId?: string }
): Promise<void> {
const update: Partial<typeof classSchedule.$inferSelect> = {}
if (data.classId !== undefined) update.classId = data.classId
if (data.weekday !== undefined) update.weekday = data.weekday
if (data.startTime !== undefined) update.startTime = data.startTime
if (data.endTime !== undefined) update.endTime = data.endTime
if (data.course !== undefined) update.course = data.course
if (data.location !== undefined) update.location = data.location ?? null
if (Object.keys(update).length === 0) return
await db
.update(classSchedule)
.set(update)
.where(eq(classSchedule.id, scheduleId))
}
/**
* Delete a classSchedule row by id.
*/
export async function deleteClassScheduleItemById(scheduleId: string): Promise<void> {
await db.delete(classSchedule).where(eq(classSchedule.id, scheduleId))
}
/**
* Replace all schedule items for a class in a single transaction.
* Deletes existing items then inserts the new ones atomically.
*
* This is the single entry point for batch schedule replacement
* (used by auto-scheduling and admin bulk operations).
*/
export async function replaceClassSchedule(
classId: string,
items: ScheduleItemInput[]
): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(classSchedule).where(eq(classSchedule.classId, classId))
if (items.length === 0) return
const rows = items.map((s) => ({
id: createId(),
classId,
weekday: s.weekday,
startTime: s.startTime,
endTime: s.endTime,
course: s.course,
location: s.location ?? null,
}))
await tx.insert(classSchedule).values(rows)
})
}