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:
@@ -1,35 +1,45 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
users,
|
||||
import {
|
||||
users,
|
||||
accounts,
|
||||
sessions,
|
||||
roles,
|
||||
usersToRoles,
|
||||
questions,
|
||||
knowledgePoints,
|
||||
questionsToKnowledgePoints,
|
||||
textbooks,
|
||||
chapters,
|
||||
roles,
|
||||
usersToRoles,
|
||||
questions,
|
||||
knowledgePoints,
|
||||
questionsToKnowledgePoints,
|
||||
textbooks,
|
||||
chapters,
|
||||
schools,
|
||||
grades,
|
||||
classes,
|
||||
classEnrollments,
|
||||
classSchedule,
|
||||
subjects,
|
||||
exams,
|
||||
examQuestions,
|
||||
examSubmissions,
|
||||
exams,
|
||||
examQuestions,
|
||||
examSubmissions,
|
||||
submissionAnswers,
|
||||
homeworkAssignments,
|
||||
homeworkAssignmentQuestions,
|
||||
homeworkAssignmentTargets,
|
||||
homeworkSubmissions,
|
||||
homeworkAnswers
|
||||
homeworkAnswers,
|
||||
announcements,
|
||||
coursePlans,
|
||||
coursePlanItems,
|
||||
gradeRecords,
|
||||
messages,
|
||||
messageNotifications,
|
||||
parentStudentRelations,
|
||||
attendanceRecords,
|
||||
attendanceRules,
|
||||
passwordSecurity,
|
||||
} from "./schema";
|
||||
|
||||
// --- Users & Roles Relations ---
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
export const usersRelations = relations(users, ({ many, one }) => ({
|
||||
accounts: many(accounts),
|
||||
sessions: many(sessions),
|
||||
usersToRoles: many(usersToRoles),
|
||||
@@ -40,6 +50,11 @@ export const usersRelations = relations(users, ({ many }) => ({
|
||||
submissions: many(examSubmissions),
|
||||
homeworkSubmissions: many(homeworkSubmissions),
|
||||
authoredQuestions: many(questions),
|
||||
authoredAnnouncements: many(announcements),
|
||||
sentMessages: many(messages, { relationName: "message_sender" }),
|
||||
receivedMessages: many(messages, { relationName: "message_receiver" }),
|
||||
notifications: many(messageNotifications),
|
||||
passwordSecurity: one(passwordSecurity),
|
||||
}));
|
||||
|
||||
export const accountsRelations = relations(accounts, ({ one }) => ({
|
||||
@@ -314,3 +329,158 @@ export const homeworkAnswersRelations = relations(homeworkAnswers, ({ one }) =>
|
||||
references: [questions.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Announcements Relations ---
|
||||
|
||||
export const announcementsRelations = relations(announcements, ({ one }) => ({
|
||||
author: one(users, {
|
||||
fields: [announcements.authorId],
|
||||
references: [users.id],
|
||||
}),
|
||||
targetGrade: one(grades, {
|
||||
fields: [announcements.targetGradeId],
|
||||
references: [grades.id],
|
||||
}),
|
||||
targetClass: one(classes, {
|
||||
fields: [announcements.targetClassId],
|
||||
references: [classes.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Course Plans Relations ---
|
||||
|
||||
export const coursePlansRelations = relations(coursePlans, ({ one, many }) => ({
|
||||
class: one(classes, {
|
||||
fields: [coursePlans.classId],
|
||||
references: [classes.id],
|
||||
}),
|
||||
subject: one(subjects, {
|
||||
fields: [coursePlans.subjectId],
|
||||
references: [subjects.id],
|
||||
}),
|
||||
teacher: one(users, {
|
||||
fields: [coursePlans.teacherId],
|
||||
references: [users.id],
|
||||
relationName: "course_plan_teacher",
|
||||
}),
|
||||
createdByUser: one(users, {
|
||||
fields: [coursePlans.createdBy],
|
||||
references: [users.id],
|
||||
relationName: "course_plan_creator",
|
||||
}),
|
||||
items: many(coursePlanItems),
|
||||
}));
|
||||
|
||||
export const coursePlanItemsRelations = relations(coursePlanItems, ({ one }) => ({
|
||||
plan: one(coursePlans, {
|
||||
fields: [coursePlanItems.planId],
|
||||
references: [coursePlans.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Grade Records Relations ---
|
||||
|
||||
export const gradeRecordsRelations = relations(gradeRecords, ({ one }) => ({
|
||||
student: one(users, {
|
||||
fields: [gradeRecords.studentId],
|
||||
references: [users.id],
|
||||
relationName: "grade_records_student",
|
||||
}),
|
||||
class: one(classes, {
|
||||
fields: [gradeRecords.classId],
|
||||
references: [classes.id],
|
||||
}),
|
||||
subject: one(subjects, {
|
||||
fields: [gradeRecords.subjectId],
|
||||
references: [subjects.id],
|
||||
}),
|
||||
exam: one(exams, {
|
||||
fields: [gradeRecords.examId],
|
||||
references: [exams.id],
|
||||
}),
|
||||
recorder: one(users, {
|
||||
fields: [gradeRecords.recordedBy],
|
||||
references: [users.id],
|
||||
relationName: "grade_records_recorder",
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Messages Relations ---
|
||||
|
||||
export const messagesRelations = relations(messages, ({ one, many }) => ({
|
||||
sender: one(users, {
|
||||
fields: [messages.senderId],
|
||||
references: [users.id],
|
||||
relationName: "message_sender",
|
||||
}),
|
||||
receiver: one(users, {
|
||||
fields: [messages.receiverId],
|
||||
references: [users.id],
|
||||
relationName: "message_receiver",
|
||||
}),
|
||||
parent: one(messages, {
|
||||
fields: [messages.parentMessageId],
|
||||
references: [messages.id],
|
||||
relationName: "message_thread",
|
||||
}),
|
||||
replies: many(messages, { relationName: "message_thread" }),
|
||||
}));
|
||||
|
||||
// --- Message Notifications Relations ---
|
||||
|
||||
export const messageNotificationsRelations = relations(messageNotifications, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [messageNotifications.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Parent-Student Relations ---
|
||||
|
||||
export const parentStudentRelationsRelations = relations(parentStudentRelations, ({ one }) => ({
|
||||
parent: one(users, {
|
||||
fields: [parentStudentRelations.parentId],
|
||||
references: [users.id],
|
||||
relationName: "parent_relations",
|
||||
}),
|
||||
student: one(users, {
|
||||
fields: [parentStudentRelations.studentId],
|
||||
references: [users.id],
|
||||
relationName: "student_relations",
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Attendance Relations ---
|
||||
|
||||
export const attendanceRecordsRelations = relations(attendanceRecords, ({ one }) => ({
|
||||
student: one(users, {
|
||||
fields: [attendanceRecords.studentId],
|
||||
references: [users.id],
|
||||
relationName: "attendance_records_student",
|
||||
}),
|
||||
class: one(classes, {
|
||||
fields: [attendanceRecords.classId],
|
||||
references: [classes.id],
|
||||
}),
|
||||
recorder: one(users, {
|
||||
fields: [attendanceRecords.recordedBy],
|
||||
references: [users.id],
|
||||
relationName: "attendance_records_recorder",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const attendanceRulesRelations = relations(attendanceRules, ({ one }) => ({
|
||||
class: one(classes, {
|
||||
fields: [attendanceRules.classId],
|
||||
references: [classes.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Password Security Relations ---
|
||||
|
||||
export const passwordSecurityRelations = relations(passwordSecurity, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [passwordSecurity.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
json,
|
||||
mysqlEnum,
|
||||
boolean,
|
||||
foreignKey
|
||||
foreignKey,
|
||||
date,
|
||||
datetime,
|
||||
decimal,
|
||||
bigint
|
||||
} from "drizzle-orm/mysql-core";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type { AdapterAccountType } from "next-auth/adapters";
|
||||
@@ -35,6 +39,13 @@ export const users = mysqlTable("users", {
|
||||
gradeId: varchar("grade_id", { length: 128 }),
|
||||
departmentId: varchar("department_id", { length: 128 }),
|
||||
onboardedAt: timestamp("onboarded_at", { mode: "date" }),
|
||||
|
||||
// 未成年人信息保护
|
||||
birthDate: date("birth_date"),
|
||||
guardianName: varchar("guardian_name", { length: 255 }),
|
||||
guardianPhone: varchar("guardian_phone", { length: 20 }),
|
||||
guardianRelation: varchar("guardian_relation", { length: 50 }),
|
||||
consentAcceptedAt: datetime("consent_accepted_at"),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
@@ -623,7 +634,452 @@ export const aiProviders = mysqlTable("ai_providers", {
|
||||
defaultIdx: index("ai_provider_default_idx").on(table.isDefault),
|
||||
}));
|
||||
|
||||
// Re-export old courses table if needed or deprecate it.
|
||||
// --- 7. Announcements ---
|
||||
|
||||
export const announcementTypeEnum = mysqlEnum("type", ["school", "grade", "class"]);
|
||||
export const announcementStatusEnum = mysqlEnum("status", ["draft", "published", "archived"]);
|
||||
|
||||
export const announcements = mysqlTable("announcements", {
|
||||
id: id("id").primaryKey(),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
content: text("content").notNull(),
|
||||
type: announcementTypeEnum.default("school").notNull(),
|
||||
status: announcementStatusEnum.default("draft").notNull(),
|
||||
targetGradeId: varchar("target_grade_id", { length: 128 }),
|
||||
targetClassId: varchar("target_class_id", { length: 128 }),
|
||||
authorId: varchar("author_id", { length: 128 }).notNull().references(() => users.id),
|
||||
publishedAt: datetime("published_at", { mode: "date" }),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
authorIdx: index("announcements_author_idx").on(table.authorId),
|
||||
statusIdx: index("announcements_status_idx").on(table.status),
|
||||
typeIdx: index("announcements_type_idx").on(table.type),
|
||||
targetGradeIdx: index("announcements_target_grade_idx").on(table.targetGradeId),
|
||||
targetClassIdx: index("announcements_target_class_idx").on(table.targetClassId),
|
||||
}));
|
||||
|
||||
// --- 8. Audit & Login Logs ---
|
||||
|
||||
export const auditLogStatusEnum = mysqlEnum("status", ["success", "failure"]);
|
||||
|
||||
export const auditLogs = mysqlTable("audit_logs", {
|
||||
id: id("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 128 }).notNull(),
|
||||
userName: varchar("user_name", { length: 255 }).notNull(),
|
||||
action: varchar("action", { length: 255 }).notNull(),
|
||||
module: varchar("module", { length: 128 }).notNull(),
|
||||
targetId: varchar("target_id", { length: 128 }),
|
||||
targetType: varchar("target_type", { length: 128 }),
|
||||
detail: text("detail"),
|
||||
ipAddress: varchar("ip_address", { length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 512 }),
|
||||
status: auditLogStatusEnum.default("success").notNull(),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdIdx: index("audit_logs_user_id_idx").on(table.userId),
|
||||
moduleIdx: index("audit_logs_module_idx").on(table.module),
|
||||
actionIdx: index("audit_logs_action_idx").on(table.action),
|
||||
statusIdx: index("audit_logs_status_idx").on(table.status),
|
||||
createdAtIdx: index("audit_logs_created_at_idx").on(table.createdAt),
|
||||
}));
|
||||
|
||||
export const loginLogActionEnum = mysqlEnum("action", ["signin", "signout", "signup"]);
|
||||
export const loginLogStatusEnum = mysqlEnum("status", ["success", "failure"]);
|
||||
|
||||
export const loginLogs = mysqlTable("login_logs", {
|
||||
id: id("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 128 }),
|
||||
userEmail: varchar("user_email", { length: 255 }).notNull(),
|
||||
action: loginLogActionEnum.notNull(),
|
||||
status: loginLogStatusEnum.default("success").notNull(),
|
||||
ipAddress: varchar("ip_address", { length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 512 }),
|
||||
errorMessage: text("error_message"),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdIdx: index("login_logs_user_id_idx").on(table.userId),
|
||||
userEmailIdx: index("login_logs_user_email_idx").on(table.userEmail),
|
||||
actionIdx: index("login_logs_action_idx").on(table.action),
|
||||
statusIdx: index("login_logs_status_idx").on(table.status),
|
||||
createdAtIdx: index("login_logs_created_at_idx").on(table.createdAt),
|
||||
}));
|
||||
|
||||
// --- 8b. Data Change Logs (数据变更日志) ---
|
||||
|
||||
export const dataChangeLogActionEnum = mysqlEnum("action", ["create", "update", "delete"]);
|
||||
|
||||
export const dataChangeLogs = mysqlTable("data_change_logs", {
|
||||
id: varchar("id", { length: 128 }).primaryKey(),
|
||||
tableName: varchar("table_name", { length: 128 }).notNull(),
|
||||
recordId: varchar("record_id", { length: 128 }).notNull(),
|
||||
action: dataChangeLogActionEnum.notNull(),
|
||||
oldValue: text("old_value"),
|
||||
newValue: text("new_value"),
|
||||
changedBy: varchar("changed_by", { length: 128 }).notNull(),
|
||||
changedByName: varchar("changed_by_name", { length: 255 }).notNull(),
|
||||
ipAddress: varchar("ip_address", { length: 45 }),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
tableNameIdx: index("data_change_logs_table_name_idx").on(table.tableName),
|
||||
recordIdIdx: index("data_change_logs_record_id_idx").on(table.recordId),
|
||||
actionIdx: index("data_change_logs_action_idx").on(table.action),
|
||||
changedByIdx: index("data_change_logs_changed_by_idx").on(table.changedBy),
|
||||
createdAtIdx: index("data_change_logs_created_at_idx").on(table.createdAt),
|
||||
}));
|
||||
|
||||
// Re-export old courses table if needed or deprecate it.
|
||||
// Assuming we are replacing the old simple schema with this robust one.
|
||||
// But if there were existing tables, we might keep them or comment them out.
|
||||
// For this task, I will overwrite completely as this is a "System Architect" redesign.
|
||||
|
||||
// --- 9. Grade Records (成绩录入) ---
|
||||
|
||||
export const gradeRecordTypeEnum = mysqlEnum("type", ["exam", "quiz", "homework", "other"]);
|
||||
export const gradeRecordSemesterEnum = mysqlEnum("semester", ["1", "2"]);
|
||||
|
||||
export const gradeRecords = mysqlTable("grade_records", {
|
||||
id: id("id").primaryKey(),
|
||||
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
classId: varchar("class_id", { length: 128 }).notNull().references(() => classes.id, { onDelete: "cascade" }),
|
||||
subjectId: varchar("subject_id", { length: 128 }).notNull().references(() => subjects.id, { onDelete: "cascade" }),
|
||||
examId: varchar("exam_id", { length: 128 }),
|
||||
academicYearId: varchar("academic_year_id", { length: 128 }),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
score: decimal("score", { precision: 6, scale: 2 }).notNull(),
|
||||
fullScore: decimal("full_score", { precision: 6, scale: 2 }).default("100").notNull(),
|
||||
type: gradeRecordTypeEnum.default("exam").notNull(),
|
||||
semester: gradeRecordSemesterEnum.default("1").notNull(),
|
||||
recordedBy: varchar("recorded_by", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
remark: text("remark"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
studentIdx: index("grade_records_student_idx").on(table.studentId),
|
||||
classIdx: index("grade_records_class_idx").on(table.classId),
|
||||
subjectIdx: index("grade_records_subject_idx").on(table.subjectId),
|
||||
examIdx: index("grade_records_exam_idx").on(table.examId),
|
||||
classSubjectIdx: index("grade_records_class_subject_idx").on(table.classId, table.subjectId),
|
||||
recordedByIdx: index("grade_records_recorded_by_idx").on(table.recordedBy),
|
||||
classFk: foreignKey({
|
||||
columns: [table.classId],
|
||||
foreignColumns: [classes.id],
|
||||
name: "gr_c_fk",
|
||||
}).onDelete("cascade"),
|
||||
studentFk: foreignKey({
|
||||
columns: [table.studentId],
|
||||
foreignColumns: [users.id],
|
||||
name: "gr_s_fk",
|
||||
}).onDelete("cascade"),
|
||||
subjectFk: foreignKey({
|
||||
columns: [table.subjectId],
|
||||
foreignColumns: [subjects.id],
|
||||
name: "gr_sub_fk",
|
||||
}).onDelete("cascade"),
|
||||
recordedByFk: foreignKey({
|
||||
columns: [table.recordedBy],
|
||||
foreignColumns: [users.id],
|
||||
name: "gr_rb_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 10. File Attachments (文件附件) ---
|
||||
|
||||
export const fileAttachments = mysqlTable("file_attachments", {
|
||||
id: varchar("id", { length: 128 }).primaryKey(),
|
||||
filename: varchar("filename", { length: 255 }).notNull(),
|
||||
originalName: varchar("original_name", { length: 255 }).notNull(),
|
||||
mimeType: varchar("mime_type", { length: 128 }).notNull(),
|
||||
size: bigint("size", { mode: "number" }).notNull(),
|
||||
storagePath: varchar("storage_path", { length: 512 }).notNull(),
|
||||
url: varchar("url", { length: 512 }),
|
||||
uploaderId: varchar("uploader_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
targetType: varchar("target_type", { length: 128 }),
|
||||
targetId: varchar("target_id", { length: 128 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
uploaderIdx: index("file_attachments_uploader_idx").on(table.uploaderId),
|
||||
targetIdx: index("file_attachments_target_idx").on(table.targetType, table.targetId),
|
||||
createdAtIdx: index("file_attachments_created_at_idx").on(table.createdAt),
|
||||
}));
|
||||
|
||||
// --- 11. Course Plans (课程计划) ---
|
||||
|
||||
export const coursePlanStatusEnum = mysqlEnum("status", ["planning", "active", "completed", "paused"]);
|
||||
export const coursePlanSemesterEnum = mysqlEnum("semester", ["1", "2"]);
|
||||
|
||||
export const coursePlans = mysqlTable("course_plans", {
|
||||
id: id("id").primaryKey(),
|
||||
classId: varchar("class_id", { length: 128 }).notNull().references(() => classes.id, { onDelete: "cascade" }),
|
||||
subjectId: varchar("subject_id", { length: 128 }).notNull().references(() => subjects.id, { onDelete: "cascade" }),
|
||||
teacherId: varchar("teacher_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
academicYearId: varchar("academic_year_id", { length: 128 }),
|
||||
semester: coursePlanSemesterEnum.default("1").notNull(),
|
||||
totalHours: int("total_hours").default(0).notNull(),
|
||||
completedHours: int("completed_hours").default(0).notNull(),
|
||||
weeklyHours: int("weekly_hours").default(0).notNull(),
|
||||
startDate: date("start_date"),
|
||||
endDate: date("end_date"),
|
||||
syllabus: text("syllabus"),
|
||||
objectives: text("objectives"),
|
||||
status: coursePlanStatusEnum.default("planning").notNull(),
|
||||
createdBy: varchar("created_by", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
classIdx: index("course_plans_class_idx").on(table.classId),
|
||||
teacherIdx: index("course_plans_teacher_idx").on(table.teacherId),
|
||||
subjectIdx: index("course_plans_subject_idx").on(table.subjectId),
|
||||
statusIdx: index("course_plans_status_idx").on(table.status),
|
||||
classSubjectIdx: index("course_plans_class_subject_idx").on(table.classId, table.subjectId),
|
||||
classFk: foreignKey({
|
||||
columns: [table.classId],
|
||||
foreignColumns: [classes.id],
|
||||
name: "cp_c_fk",
|
||||
}).onDelete("cascade"),
|
||||
subjectFk: foreignKey({
|
||||
columns: [table.subjectId],
|
||||
foreignColumns: [subjects.id],
|
||||
name: "cp_s_fk",
|
||||
}).onDelete("cascade"),
|
||||
teacherFk: foreignKey({
|
||||
columns: [table.teacherId],
|
||||
foreignColumns: [users.id],
|
||||
name: "cp_t_fk",
|
||||
}).onDelete("cascade"),
|
||||
createdByFk: foreignKey({
|
||||
columns: [table.createdBy],
|
||||
foreignColumns: [users.id],
|
||||
name: "cp_cb_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
export const coursePlanItems = mysqlTable("course_plan_items", {
|
||||
id: id("id").primaryKey(),
|
||||
planId: varchar("plan_id", { length: 128 }).notNull().references(() => coursePlans.id, { onDelete: "cascade" }),
|
||||
week: int("week").notNull(),
|
||||
topic: varchar("topic", { length: 255 }).notNull(),
|
||||
content: text("content"),
|
||||
hours: int("hours").default(2).notNull(),
|
||||
textbookChapter: varchar("textbook_chapter", { length: 255 }),
|
||||
notes: text("notes"),
|
||||
isCompleted: boolean("is_completed").default(false).notNull(),
|
||||
completedAt: date("completed_at"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
planIdx: index("course_plan_items_plan_idx").on(table.planId),
|
||||
planWeekIdx: index("course_plan_items_plan_week_idx").on(table.planId, table.week),
|
||||
planFk: foreignKey({
|
||||
columns: [table.planId],
|
||||
foreignColumns: [coursePlans.id],
|
||||
name: "cpi_p_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 13. Messages (站内消息) ---
|
||||
|
||||
export const messages = mysqlTable("messages", {
|
||||
id: id("id").primaryKey(),
|
||||
senderId: varchar("sender_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
receiverId: varchar("receiver_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
subject: varchar("subject", { length: 255 }),
|
||||
content: text("content").notNull(),
|
||||
isRead: boolean("is_read").default(false).notNull(),
|
||||
readAt: timestamp("read_at", { mode: "date" }),
|
||||
parentMessageId: varchar("parent_message_id", { length: 128 }), // 回复链
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
senderIdx: index("messages_sender_idx").on(table.senderId),
|
||||
receiverIdx: index("messages_receiver_idx").on(table.receiverId),
|
||||
isReadIdx: index("messages_is_read_idx").on(table.isRead),
|
||||
parentIdx: index("messages_parent_idx").on(table.parentMessageId),
|
||||
receiverReadIdx: index("messages_receiver_read_idx").on(table.receiverId, table.isRead),
|
||||
}));
|
||||
|
||||
// --- 14. Message Notifications (消息通知) ---
|
||||
|
||||
export const messageNotifications = mysqlTable("message_notifications", {
|
||||
id: id("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
type: varchar("type", { length: 128 }).notNull(), // "message", "announcement", "homework", "grade"
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
content: text("content"),
|
||||
link: varchar("link", { length: 512 }),
|
||||
isRead: boolean("is_read").default(false).notNull(),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdx: index("message_notifications_user_idx").on(table.userId),
|
||||
isReadIdx: index("message_notifications_is_read_idx").on(table.isRead),
|
||||
userReadIdx: index("message_notifications_user_read_idx").on(table.userId, table.isRead),
|
||||
createdAtIdx: index("message_notifications_created_at_idx").on(table.createdAt),
|
||||
}));
|
||||
|
||||
// --- 14b. Notification Preferences (通知偏好) ---
|
||||
|
||||
export const notificationPreferences = mysqlTable("notification_preferences", {
|
||||
id: varchar("id", { length: 128 }).primaryKey(),
|
||||
userId: varchar("user_id", { length: 128 }).notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||
emailEnabled: boolean("email_enabled").default(false).notNull(),
|
||||
smsEnabled: boolean("sms_enabled").default(false).notNull(),
|
||||
pushEnabled: boolean("push_enabled").default(true).notNull(),
|
||||
homeworkNotifications: boolean("homework_notifications").default(true).notNull(),
|
||||
gradeNotifications: boolean("grade_notifications").default(true).notNull(),
|
||||
announcementNotifications: boolean("announcement_notifications").default(true).notNull(),
|
||||
messageNotifications: boolean("message_notifications").default(true).notNull(),
|
||||
attendanceNotifications: boolean("attendance_notifications").default(true).notNull(),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdIdx: index("notification_preferences_user_idx").on(table.userId),
|
||||
userFk: foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [users.id],
|
||||
name: "np_u_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 12. Parent-Student Relations (家长-子女关联) ---
|
||||
|
||||
export const parentStudentRelations = mysqlTable("parent_student_relations", {
|
||||
id: varchar("id", { length: 128 }).primaryKey().$defaultFn(() => createId()),
|
||||
parentId: varchar("parent_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
relation: varchar("relation", { length: 50 }), // 父亲/母亲/其他
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
parentIdx: index("parent_student_relations_parent_idx").on(table.parentId),
|
||||
studentIdx: index("parent_student_relations_student_idx").on(table.studentId),
|
||||
parentFk: foreignKey({
|
||||
columns: [table.parentId],
|
||||
foreignColumns: [users.id],
|
||||
name: "psr_p_fk",
|
||||
}).onDelete("cascade"),
|
||||
studentFk: foreignKey({
|
||||
columns: [table.studentId],
|
||||
foreignColumns: [users.id],
|
||||
name: "psr_s_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 15. Attendance (考勤管理) ---
|
||||
|
||||
export const attendanceStatusEnum = mysqlEnum("status", ["present", "absent", "late", "early_leave", "excused"]);
|
||||
|
||||
export const attendanceRecords = mysqlTable("attendance_records", {
|
||||
id: id("id").primaryKey(),
|
||||
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
classId: varchar("class_id", { length: 128 }).notNull().references(() => classes.id, { onDelete: "cascade" }),
|
||||
scheduleId: varchar("schedule_id", { length: 128 }),
|
||||
date: date("date").notNull(),
|
||||
status: attendanceStatusEnum.notNull(),
|
||||
remark: text("remark"),
|
||||
recordedBy: varchar("recorded_by", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
studentIdx: index("attendance_records_student_idx").on(table.studentId),
|
||||
classIdx: index("attendance_records_class_idx").on(table.classId),
|
||||
dateIdx: index("attendance_records_date_idx").on(table.date),
|
||||
classDateIdx: index("attendance_records_class_date_idx").on(table.classId, table.date),
|
||||
studentDateIdx: index("attendance_records_student_date_idx").on(table.studentId, table.date),
|
||||
scheduleIdx: index("attendance_records_schedule_idx").on(table.scheduleId),
|
||||
recordedByIdx: index("attendance_records_recorded_by_idx").on(table.recordedBy),
|
||||
classFk: foreignKey({
|
||||
columns: [table.classId],
|
||||
foreignColumns: [classes.id],
|
||||
name: "ar_c_fk",
|
||||
}).onDelete("cascade"),
|
||||
studentFk: foreignKey({
|
||||
columns: [table.studentId],
|
||||
foreignColumns: [users.id],
|
||||
name: "ar_s_fk",
|
||||
}).onDelete("cascade"),
|
||||
recordedByFk: foreignKey({
|
||||
columns: [table.recordedBy],
|
||||
foreignColumns: [users.id],
|
||||
name: "ar_rb_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
export const attendanceRules = mysqlTable("attendance_rules", {
|
||||
id: id("id").primaryKey(),
|
||||
classId: varchar("class_id", { length: 128 }).references(() => classes.id, { onDelete: "cascade" }),
|
||||
lateThresholdMinutes: int("late_threshold_minutes").default(15),
|
||||
earlyLeaveThresholdMinutes: int("early_leave_threshold_minutes").default(15),
|
||||
enableAutoMark: boolean("enable_auto_mark").default(false),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
classIdx: index("attendance_rules_class_idx").on(table.classId),
|
||||
classFk: foreignKey({
|
||||
columns: [table.classId],
|
||||
foreignColumns: [classes.id],
|
||||
name: "atr_c_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 17. Password Security (密码安全策略) ---
|
||||
|
||||
export const passwordSecurity = mysqlTable("password_security", {
|
||||
id: varchar("id", { length: 128 }).primaryKey().$defaultFn(() => createId()),
|
||||
userId: varchar("user_id", { length: 128 }).notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||
failedLoginAttempts: int("failed_login_attempts").default(0).notNull(),
|
||||
lockedUntil: timestamp("locked_until", { mode: "date" }),
|
||||
passwordChangedAt: timestamp("password_changed_at").defaultNow().notNull(),
|
||||
mustChangePassword: boolean("must_change_password").default(false).notNull(),
|
||||
lastPasswordChange: timestamp("last_password_change", { mode: "date" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdIdx: index("password_security_user_idx").on(table.userId),
|
||||
userFk: foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [users.id],
|
||||
name: "ps_u_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
// --- 16. Scheduling Rules & Schedule Changes (排课规则与调课) ---
|
||||
|
||||
export const schedulingRules = mysqlTable("scheduling_rules", {
|
||||
id: id("id").primaryKey(),
|
||||
classId: varchar("class_id", { length: 128 }), // null=全局规则
|
||||
maxDailyHours: int("max_daily_hours").default(8),
|
||||
maxContinuousHours: int("max_continuous_hours").default(2),
|
||||
lunchBreakStart: varchar("lunch_break_start", { length: 10 }).default("12:00"),
|
||||
lunchBreakEnd: varchar("lunch_break_end", { length: 10 }).default("13:00"),
|
||||
morningStart: varchar("morning_start", { length: 10 }).default("08:00"),
|
||||
afternoonEnd: varchar("afternoon_end", { length: 10 }).default("17:00"),
|
||||
avoidBackToBack: boolean("avoid_back_to_back").default(false),
|
||||
balancedSubjects: boolean("balanced_subjects").default(true),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
classIdx: index("scheduling_rules_class_idx").on(table.classId),
|
||||
}));
|
||||
|
||||
export const scheduleChangeStatusEnum = mysqlEnum("status", ["pending", "approved", "rejected", "completed"]);
|
||||
|
||||
export const scheduleChanges = mysqlTable("schedule_changes", {
|
||||
id: id("id").primaryKey(),
|
||||
originalScheduleId: varchar("original_schedule_id", { length: 128 }),
|
||||
classId: varchar("class_id", { length: 128 }).notNull(),
|
||||
originalTeacherId: varchar("original_teacher_id", { length: 128 }),
|
||||
substituteTeacherId: varchar("substitute_teacher_id", { length: 128 }),
|
||||
originalDate: date("original_date"),
|
||||
newDate: date("new_date"),
|
||||
newStartTime: varchar("new_start_time", { length: 10 }),
|
||||
newEndTime: varchar("new_end_time", { length: 10 }),
|
||||
reason: text("reason"),
|
||||
status: scheduleChangeStatusEnum.default("pending").notNull(),
|
||||
requestedBy: varchar("requested_by", { length: 128 }).notNull(),
|
||||
approvedBy: varchar("approved_by", { length: 128 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
classIdx: index("schedule_changes_class_idx").on(table.classId),
|
||||
statusIdx: index("schedule_changes_status_idx").on(table.status),
|
||||
requestedByIdx: index("schedule_changes_requested_by_idx").on(table.requestedBy),
|
||||
originalScheduleIdx: index("schedule_changes_original_schedule_idx").on(table.originalScheduleId),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user