Module Update
This commit is contained in:
112
docs/architecture/001_database_schema_design.md
Normal file
112
docs/architecture/001_database_schema_design.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 架构决策记录 (ADR): 数据库 Schema 设计方案 v1.0
|
||||
|
||||
**状态**: 已实施 (IMPLEMENTED)
|
||||
**日期**: 2025-12-23
|
||||
**作者**: 首席系统架构师
|
||||
**背景**: Next_Edu 平台 - K12 智慧教育管理系统
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述 (Overview)
|
||||
|
||||
本文档详细记录了 Next_Edu 平台的数据库 Schema 架构设计。本设计优先考虑 **可扩展性 (Scalability)**、**灵活性 (Flexibility)**(针对复杂的嵌套内容)以及 **严格的类型安全 (Strict Type Safety)**,并完全符合 PRD 中规定的领域驱动设计 (DDD) 原则。
|
||||
|
||||
## 2. 技术栈决策 (Technology Stack Decisions)
|
||||
|
||||
| 组件 | 选择 | 理由 |
|
||||
| :--- | :--- | :--- |
|
||||
| **数据库** | MySQL 8.0+ | 强大的关系型支持,完善的 JSON 能力,行业标准。 |
|
||||
| **ORM** | Drizzle ORM | 轻量级,零运行时开销 (Zero-runtime overhead),业界一流的 TypeScript 类型推断。 |
|
||||
| **ID 策略** | CUID2 | 分布式友好 (k-sortable),安全(防连续猜测攻击),比 UUID 更短。 |
|
||||
| **认证方案** | Auth.js v5 | 标准化的 OAuth 流程,支持自定义数据库适配器。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心 Schema 领域模型 (Core Schema Domains)
|
||||
|
||||
物理文件位于 `src/shared/db/schema.ts`,逻辑上分为三大领域。
|
||||
|
||||
### 3.1 身份与访问管理 (IAM)
|
||||
|
||||
我们采用了 **Auth.js 标准表** 与 **自定义 RBAC** 相结合的混合模式。
|
||||
|
||||
* **标准表**: `users`, `accounts` (OAuth), `sessions`, `verificationTokens`。
|
||||
* **RBAC 扩展**:
|
||||
* `roles`: 定义系统角色(例如:`grade_head` 年级主任, `teacher` 老师)。
|
||||
* `users_to_roles`: 多对多关联表。
|
||||
* **设计目标**: 解决“一人多职”问题(例如:一个老师同时也是年级主任),避免在 `users` 表中堆砌字段。
|
||||
|
||||
### 3.2 智能题库中心 (Intelligent Question Bank) - 核心
|
||||
|
||||
这是最复杂的领域,需要支持无限层级嵌套和富文本内容。
|
||||
|
||||
#### 实体定义:
|
||||
1. **`questions` (题目表)**:
|
||||
* `id`: CUID2。
|
||||
* `content`: **JSON 类型**。存储结构化内容(如 SlateJS 节点),支持富文本、图片和公式混排。
|
||||
* `parentId`: **自引用 (Self-Reference)**。
|
||||
* *若为 NULL*: 独立题目 或 “大题干” (Parent)。
|
||||
* *若有值*: 子题目 (Child)(例如:一篇阅读理解下的第1小题)。
|
||||
* `type`: 枚举 (`single_choice`, `text`, `composite` 等)。
|
||||
2. **`knowledge_points` (知识点表)**:
|
||||
* 通过 `parentId` 实现树状结构。
|
||||
* 支持无限层级 (学科 -> 章 -> 节 -> 知识点)。
|
||||
3. **`questions_to_knowledge_points`**:
|
||||
* 多对多关联。一道题可考察多个知识点;一个知识点可关联数千道题。
|
||||
|
||||
### 3.3 教务教学流 (Academic Teaching Flow)
|
||||
|
||||
将物理世界的教学过程映射为数字实体。
|
||||
|
||||
* **`textbooks` & `chapters`**: 标准的教材大纲映射。`chapters` 同样支持通过 `parentId` 进行嵌套。
|
||||
* **`exams`**: 考试/作业的聚合实体。
|
||||
* **`exam_submissions`**: 代表一名学生的单次答题记录。
|
||||
* **`submission_answers`**: 细粒度的答题详情,记录每道题的答案,支持自动评分 (`score`) 和人工反馈 (`feedback`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键设计模式 (Key Design Patterns)
|
||||
|
||||
### 4.1 无限嵌套 ("Composite" Pattern)
|
||||
我们没有为“题干”和“题目”创建单独的表,而是在 `questions` 表上使用 **自引用 (Self-Referencing)** 模式。
|
||||
|
||||
* **优点**:
|
||||
* 统一的查询接口 (`db.query.questions.findFirst({ with: { children: true } })`)。
|
||||
* 递归逻辑可统一应用。
|
||||
* 当内容结构变化时,迁移更简单。
|
||||
* **缺点**:
|
||||
* 需要处理递归查询逻辑(已通过 Drizzle Relations 解决)。
|
||||
|
||||
### 4.2 CUID2 优于 自增 ID
|
||||
* **安全性**: 防止 ID 枚举攻击(猜测下一个用户 ID)。
|
||||
* **分布式**: 支持在客户端或多服务器节点生成,无碰撞风险。
|
||||
* **性能**: `k-sortable` 特性保证了比随机 UUID v4 更好的索引局部性。
|
||||
|
||||
### 4.3 JSON 存储内容
|
||||
* 教育内容不仅仅是“文本”。它包含格式、LaTeX 公式和图片引用。
|
||||
* 使用 `JSON` 存储允许前端 (Next.js) 直接渲染富组件,无需解析复杂的 HTML 字符串。
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全与索引策略 (Security & Indexing Strategy)
|
||||
|
||||
### 索引 (Indexes)
|
||||
* **外键**: 所有外键列 (`author_id`, `parent_id` 等) 均显式建立索引。
|
||||
* **性能**:
|
||||
* `parent_id_idx`: 对树形结构的遍历性能至关重要。
|
||||
* `email_idx`: 登录查询的核心索引。
|
||||
|
||||
### 类型安全 (Type Safety)
|
||||
* 严格的 TypeScript 定义直接从 `src/shared/db/schema.ts` 导出。
|
||||
* Zod Schema (待生成) 将与这些 Drizzle 定义保持 1:1 对齐。
|
||||
|
||||
---
|
||||
|
||||
## 6. 目录结构 (Directory Structure)
|
||||
|
||||
```bash
|
||||
src/shared/db/
|
||||
├── index.ts # 单例数据库连接池
|
||||
├── schema.ts # 物理表结构定义
|
||||
└── relations.ts # 逻辑 Drizzle 关系定义
|
||||
```
|
||||
52
docs/architecture/002_exam_structure_migration.md
Normal file
52
docs/architecture/002_exam_structure_migration.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Database Schema Change Request: Exam Structure Support
|
||||
|
||||
## 1. Table: `exams`
|
||||
|
||||
### Change
|
||||
**Add Column**: `structure`
|
||||
|
||||
### Details
|
||||
- **Type**: `JSON`
|
||||
- **Nullable**: `TRUE` (Default: `NULL`)
|
||||
|
||||
### Reason
|
||||
To support hierarchical exam structures (e.g., Sections/Groups containing Questions). The existing flat `exam_questions` table only supports a simple list of questions and is insufficient for complex exam layouts (e.g., "Part A: Reading", "Part B: Writing").
|
||||
|
||||
### Before vs After
|
||||
|
||||
**Before**:
|
||||
`exams` table only stores metadata (`title`, `description`, etc.). Question ordering relies solely on `exam_questions.order`.
|
||||
|
||||
**After**:
|
||||
`exams` table includes `structure` column to store the full tree representation:
|
||||
```json
|
||||
[
|
||||
{ "id": "uuid-1", "type": "group", "title": "Section A", "children": [...] },
|
||||
{ "id": "uuid-2", "type": "question", "questionId": "q1", "score": 10 }
|
||||
]
|
||||
```
|
||||
*Note: `exam_questions` table is retained for relational integrity and efficient querying of question usage, but the presentation order/structure is now driven by this new JSON column.*
|
||||
|
||||
---
|
||||
|
||||
## 2. Table: `questions_to_knowledge_points`
|
||||
|
||||
### Change
|
||||
**Rename Foreign Key Constraints**
|
||||
|
||||
### Details
|
||||
- Rename constraint for `question_id` to `q_kp_qid_fk`
|
||||
- Rename constraint for `knowledge_point_id` to `q_kp_kpid_fk`
|
||||
|
||||
### Reason
|
||||
The default generated foreign key names (e.g., `questions_to_knowledge_points_knowledge_point_id_knowledge_points_id_fk`) exceed the MySQL identifier length limit (64 characters), causing migration failures.
|
||||
|
||||
### Before vs After
|
||||
|
||||
**Before**:
|
||||
- FK Name: `questions_to_knowledge_points_question_id_questions_id_fk` (Too long)
|
||||
- FK Name: `questions_to_knowledge_points_knowledge_point_id_knowledge_points_id_fk` (Too long)
|
||||
|
||||
**After**:
|
||||
- FK Name: `q_kp_qid_fk`
|
||||
- FK Name: `q_kp_kpid_fk`
|
||||
115
docs/architecture/002_role_based_routing.md
Normal file
115
docs/architecture/002_role_based_routing.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Architecture RFC: Role-Based Routing & Directory Structure
|
||||
|
||||
**Status**: PROPOSED
|
||||
**Date**: 2025-12-23
|
||||
**Context**: Next_Edu supports multiple roles (Admin, Teacher, Student, Parent) with distinct UI/UX requirements.
|
||||
|
||||
## 1. 核心问题 (The Problem)
|
||||
目前项目结构中,页面与角色耦合不清。
|
||||
* `/dashboard` 目前硬编码为教师视图。
|
||||
* 不同角色的功能模块(如“课程”)可能有完全不同的视图和逻辑(教师管理课程 vs 学生学习课程)。
|
||||
* 缺乏统一的路由规范指导开发人员“把页面放哪里”。
|
||||
|
||||
## 2. 解决方案:基于角色的路由策略 (Role-Based Routing Strategy)
|
||||
|
||||
我们采用 **"Hybrid Routing" (混合路由)** 策略:
|
||||
1. **Explicit Dashboard Routing**: Dashboards are separated by role in the file structure (e.g., `/teacher/dashboard`).
|
||||
2. **Explicit Role Scopes (显式角色域)**: 具体的业务功能页面放入 `/teacher`, `/student`, `/admin` 专属路径下。
|
||||
|
||||
### 2.1 目录结构 (Directory Structure)
|
||||
|
||||
```
|
||||
src/app/(dashboard)/
|
||||
├── layout.tsx # Shared App Shell (Sidebar, Header)
|
||||
├── dashboard/
|
||||
│ └── page.tsx # [Redirector] Redirects to role-specific dashboard
|
||||
│
|
||||
├── teacher/ # 教师专属路由域
|
||||
│ ├── dashboard/ # Teacher Dashboard
|
||||
│ │ └── page.tsx
|
||||
│ ├── classes/
|
||||
│ │ └── page.tsx
|
||||
│ ├── exams/
|
||||
│ │ └── page.tsx
|
||||
│ └── textbooks/
|
||||
│ └── page.tsx
|
||||
│
|
||||
├── student/ # 学生专属路由域
|
||||
│ ├── dashboard/ # Student Dashboard
|
||||
│ │ └── page.tsx
|
||||
│ ├── learning/
|
||||
│ │ └── page.tsx
|
||||
│ └── schedule/
|
||||
│ └── page.tsx
|
||||
│
|
||||
└── admin/ # 管理员专属路由域
|
||||
├── dashboard/ # Admin Dashboard
|
||||
│ └── page.tsx
|
||||
├── users/
|
||||
└── school/
|
||||
```
|
||||
|
||||
### 2.2 Dashboard Routing Logic
|
||||
|
||||
`/dashboard` 页面现在作为一个 **Portal (入口)** 或 **Redirector (重定向器)**,而不是直接渲染内容。
|
||||
|
||||
**Example: `src/app/(dashboard)/dashboard/page.tsx`**
|
||||
|
||||
```tsx
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const session = await auth();
|
||||
const role = session?.user?.role;
|
||||
|
||||
switch (role) {
|
||||
case "teacher":
|
||||
redirect("/teacher/dashboard");
|
||||
case "student":
|
||||
redirect("/student/dashboard");
|
||||
case "admin":
|
||||
redirect("/admin/dashboard");
|
||||
default:
|
||||
redirect("/login");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 模块化组织 (Module Organization)
|
||||
|
||||
为了避免 `src/app` 变得臃肿,具体的**业务组件**必须存放在 `src/modules` 中。
|
||||
|
||||
```
|
||||
src/modules/
|
||||
├── teacher/ # 教师业务领域
|
||||
│ ├── components/ # 仅教师使用的组件 (e.g., GradebookTable)
|
||||
│ ├── hooks/
|
||||
│ └── actions.ts
|
||||
│
|
||||
├── student/ # 学生业务领域
|
||||
│ ├── components/ # (e.g., CourseProgressCard)
|
||||
│ └── ...
|
||||
│
|
||||
├── shared/ # 跨角色共享
|
||||
│ ├── components/ # (e.g., CourseCard - if generic)
|
||||
```
|
||||
|
||||
## 4. 路由与导航配置 (Navigation Config)
|
||||
|
||||
`src/modules/layout/config/navigation.ts` 已经配置好了基于角色的菜单。
|
||||
* 当用户访问 `/dashboard` 时,根据角色看到不同的 Dashboard 组件。
|
||||
* 点击侧边栏菜单(如 "Exams")时,跳转到显式路径 `/teacher/exams`。
|
||||
|
||||
## 5. 优势 (Benefits)
|
||||
1. **Security**: 可以在 Middleware 或 Layout 层级轻松对 `/admin/*` 路径实施权限控制。
|
||||
2. **Clarity**: 开发者清楚知道“教师的试卷列表页”应该放在 `src/app/(dashboard)/teacher/exams/page.tsx`。
|
||||
3. **Decoupling**: 教师端和学生端的逻辑完全解耦,互不影响。
|
||||
|
||||
---
|
||||
|
||||
## 6. Action Items (执行计划)
|
||||
|
||||
1. **Refactor Dashboard**: 将 `src/app/(dashboard)/dashboard/page.tsx` 重构为 Dispatcher。
|
||||
2. **Create Role Directories**: 在 `src/app/(dashboard)` 下创建 `teacher`, `student`, `admin` 目录。
|
||||
3. **Move Components**: 确保 `src/modules` 结构清晰。
|
||||
39
docs/db/schema-changelog.md
Normal file
39
docs/db/schema-changelog.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Database Schema Changelog
|
||||
|
||||
## v1.1.0 - Exam Structure & Performance Optimization
|
||||
**Date:** 2025-12-29
|
||||
**Migration ID:** `0001_flawless_texas_twister`
|
||||
**Author:** Principal Database Architect
|
||||
|
||||
### 1. Summary
|
||||
This release introduces support for hierarchical exam structures (Sectioning/Grouping) and optimizes database constraint naming for better compatibility with MySQL environments.
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Table: `exams`
|
||||
* **Action**: `ADD COLUMN`
|
||||
* **Field**: `structure` (JSON)
|
||||
* **Reason**: To support nested exam layouts (e.g., "Part I: Listening", "Section A").
|
||||
* *Architecture Note*: This JSON field is strictly for **Presentation Layer** ordering and grouping. The `exam_questions` table remains the **Source of Truth** for relational integrity and scoring logic.
|
||||
* **Schema Definition**:
|
||||
```typescript
|
||||
type ExamStructure = Array<
|
||||
| { type: 'group', title: string, children: ExamStructure }
|
||||
| { type: 'question', questionId: string, score: number }
|
||||
>
|
||||
```
|
||||
|
||||
#### 2.2 Table: `questions_to_knowledge_points`
|
||||
* **Action**: `RENAME FOREIGN KEY`
|
||||
* **Details**:
|
||||
* Old: `questions_to_knowledge_points_question_id_questions_id_fk` -> New: `q_kp_qid_fk`
|
||||
* Old: `questions_to_knowledge_points_knowledge_point_id_knowledge_points_id_fk` -> New: `q_kp_kpid_fk`
|
||||
* **Reason**: Previous names exceeded MySQL's 64-character identifier limit, causing potential migration failures in production environments.
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration. The script includes `ALTER TABLE ... DROP FOREIGN KEY` followed by `ADD CONSTRAINT`.
|
||||
* **Down**: Revert `structure` column. Note that FK names can be kept short as they are implementation details.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Negligible. JSON parsing is done client-side or at application layer.
|
||||
* **Data Integrity**: High. Existing data is unaffected. New `structure` field defaults to `NULL`.
|
||||
75
docs/db/seed-data.md
Normal file
75
docs/db/seed-data.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Database Seeding Strategy
|
||||
|
||||
**Status**: Implemented
|
||||
**Script Location**: [`scripts/seed.ts`](../../scripts/seed.ts)
|
||||
**Command**: `npm run db:seed`
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
The seed script is designed to populate the database with a **representative set of data** that covers all core business scenarios. It serves two purposes:
|
||||
1. **Development**: Provides a consistent baseline for developers.
|
||||
2. **Validation**: Verifies complex schema relationships (e.g., recursive trees, JSON structures).
|
||||
|
||||
## 2. Seed Data Topology
|
||||
|
||||
### 2.1 Identity & Access Management (IAM)
|
||||
We strictly follow the RBAC model defined in `docs/architecture/001_database_schema_design.md`.
|
||||
|
||||
* **Roles**:
|
||||
* `admin`: System Administrator.
|
||||
* `teacher`: Academic Instructor.
|
||||
* `student`: Learner.
|
||||
* `grade_head`: Head of Grade Year (Demonstrates multi-role capability).
|
||||
* **Users**:
|
||||
* `admin@next-edu.com` (Role: Admin)
|
||||
* `math@next-edu.com` (Role: Teacher + Grade Head)
|
||||
* `alice@next-edu.com` (Role: Student)
|
||||
|
||||
### 2.2 Knowledge Graph
|
||||
Generates a hierarchical structure to test recursive queries (`parentId`).
|
||||
* **Math** (Level 0)
|
||||
* └── **Algebra** (Level 1)
|
||||
* └── **Linear Equations** (Level 2)
|
||||
|
||||
### 2.3 Question Bank
|
||||
Includes rich content and nested structures.
|
||||
1. **Simple Single Choice**: "What is 2 + 2?"
|
||||
2. **Composite Question (Reading Comprehension)**:
|
||||
* **Parent**: A reading passage.
|
||||
* **Child 1**: Single Choice question about the passage.
|
||||
* **Child 2**: Open-ended text question.
|
||||
|
||||
### 2.4 Exams
|
||||
Demonstrates the new **JSON Structure** field (`exams.structure`).
|
||||
* **Title**: "Algebra Mid-Term 2025"
|
||||
* **Structure**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "group",
|
||||
"title": "Part 1: Basics",
|
||||
"children": [{ "type": "question", "questionId": "...", "score": 10 }]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"title": "Part 2: Reading",
|
||||
"children": [{ "type": "question", "questionId": "...", "score": 20 }]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 3. How to Run
|
||||
|
||||
### Prerequisites
|
||||
Ensure your `.env` file contains a valid `DATABASE_URL`.
|
||||
|
||||
### Execution
|
||||
Run the following command in the project root:
|
||||
|
||||
```bash
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### Reset Behavior
|
||||
**WARNING**: The script currently performs a **TRUNCATE** on all core tables before seeding. This ensures a clean state but will **WIPE EXISTING DATA**.
|
||||
76
docs/design/001_auth_ui_implementation.md
Normal file
76
docs/design/001_auth_ui_implementation.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Auth UI Implementation Details
|
||||
|
||||
**Date**: 2025-12-23
|
||||
**Author**: Senior Frontend Engineer
|
||||
**Module**: Auth (`src/modules/auth`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述 (Overview)
|
||||
|
||||
本文档记录了登录 (`/login`) 和注册 (`/register`) 页面的前端实现细节。遵循 Vertical Slice Architecture 和 Pixel-Perfect UI 规范。
|
||||
|
||||
## 2. 架构设计 (Architecture)
|
||||
|
||||
### 2.1 目录结构
|
||||
所有认证相关的业务逻辑和组件均封装在 `src/modules/auth` 下,保持了高内聚。
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ └── (auth)/ # 路由层 (Server Components)
|
||||
│ ├── layout.tsx # 统一的 AuthLayout 容器
|
||||
│ ├── login/page.tsx
|
||||
│ └── register/page.tsx
|
||||
│
|
||||
├── modules/
|
||||
│ └── auth/ # 业务模块
|
||||
│ └── components/ # 模块私有组件
|
||||
│ ├── auth-layout.tsx # 左右分屏布局
|
||||
│ ├── login-form.tsx # 登录表单 (Client Component)
|
||||
│ └── register-form.tsx # 注册表单 (Client Component)
|
||||
```
|
||||
|
||||
### 2.2 渲染策略
|
||||
* **Server Components**: 页面入口 (`page.tsx`) 和布局 (`layout.tsx`) 默认为服务端组件,负责元数据 (`Metadata`) 和静态结构渲染。
|
||||
* **Client Components**: 表单组件 (`*-form.tsx`) 标记为 `'use client'`,处理交互逻辑(状态管理、表单提交、Loading 状态)。
|
||||
|
||||
## 3. UI/UX 细节
|
||||
|
||||
### 3.1 布局 (Layout)
|
||||
采用 **Split Screen (分屏)** 设计:
|
||||
* **左侧 (Desktop Only)**:
|
||||
* 深色背景 (`bg-zinc-900`),强调品牌沉浸感。
|
||||
* 包含 Logo (`Next_Edu`) 和用户证言 (`Blockquote`)。
|
||||
* 使用 `hidden lg:flex` 实现响应式显隐。
|
||||
* **右侧**:
|
||||
* 居中对齐的表单容器。
|
||||
* 移动端优先 (`w-full`),桌面端限制最大宽度 (`sm:w-[350px]`)。
|
||||
|
||||
### 3.2 交互 (Interactions)
|
||||
* **Loading State**: 表单提交时按钮进入 `disabled` 状态并显示 `Loader2` 旋转动画。
|
||||
* **Micro-animations**:
|
||||
* 按钮 Hover 效果。
|
||||
* 链接 Hover 下划线 (`hover:underline`).
|
||||
* **Feedback**: 模拟了 3 秒的异步请求延迟,用于演示加载状态。
|
||||
|
||||
## 4. 错误处理 (Error Handling)
|
||||
|
||||
### 4.1 模块级错误边界
|
||||
* **Scoped Error Boundary**: `src/app/(auth)/error.tsx` 仅处理 Auth 模块内的运行时错误。
|
||||
* 显示友好的 "Authentication Error" 提示。
|
||||
* 提供 "Try again" 按钮重置状态。
|
||||
|
||||
### 4.2 模块级 404
|
||||
* **Scoped Not Found**: `src/app/(auth)/not-found.tsx` 处理 Auth 模块内的无效路径。
|
||||
* 引导用户返回 `/login` 页面,防止用户迷失。
|
||||
|
||||
## 5. 组件复用
|
||||
* 使用了 `src/shared/components/ui` 中的标准 Shadcn 组件:
|
||||
* `Button`, `Input`, `Label` (新增).
|
||||
* 图标库统一使用 `lucide-react`.
|
||||
|
||||
## 5. 后续计划 (Next Steps)
|
||||
* [ ] 集成 `next-auth` (Auth.js) 进行实际的身份验证。
|
||||
* [ ] 添加 Zod Schema 进行前端表单验证。
|
||||
* [ ] 对接后端 API (`src/modules/auth/actions.ts`).
|
||||
100
docs/design/002_teacher_dashboard_implementation.md
Normal file
100
docs/design/002_teacher_dashboard_implementation.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 教师仪表盘实现与 Hydration 修复记录
|
||||
|
||||
**日期**: 2025-12-23
|
||||
**作者**: 资深前端工程师 (Senior Frontend Engineer)
|
||||
**状态**: 已实现
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档详细说明了教师仪表盘 (Teacher Dashboard) 的实现细节,该实现严格遵循 Next_Edu 设计系统 v1.3.0。文档还记录了开发过程中遇到的 Hydration 错误及其解决方案。
|
||||
|
||||
## 2. 组件架构
|
||||
|
||||
仪表盘采用垂直切片架构 (Vertical Slice Architecture),代码位于 `src/modules/dashboard`。
|
||||
|
||||
### 2.1 文件结构
|
||||
```
|
||||
src/modules/dashboard/
|
||||
└── components/
|
||||
├── teacher-stats.tsx # 核心指标 (学生数, 课程数, 待批改作业)
|
||||
├── teacher-schedule.tsx # 今日日程列表
|
||||
├── recent-submissions.tsx # 最近的学生提交记录
|
||||
└── teacher-quick-actions.tsx # 常用操作 (创建作业等)
|
||||
```
|
||||
|
||||
### 2.2 设计系统集成
|
||||
所有组件严格遵循 v1.3.0 规范:
|
||||
- **排版 (Typography)**: 使用 `Geist Sans`,数据展示开启 `tabular-nums`。
|
||||
- **色彩 (Colors)**: 使用语义化 HSL 变量 (`muted`, `primary`, `destructive`)。
|
||||
- **图标 (Icons)**: 使用 `lucide-react` (如 `Users`, `BookOpen`, `Inbox`)。
|
||||
- **状态 (States)**:
|
||||
- **Loading**: 使用自定义骨架屏 (Skeleton),拒绝全屏 Spinner。
|
||||
- **Empty**: 使用 `EmptyState` 组件处理无数据场景。
|
||||
|
||||
## 3. 组件详情
|
||||
|
||||
### 3.1 TeacherStats (教师统计)
|
||||
- **用途**: 展示教师当前状态的高层概览。
|
||||
- **特性**:
|
||||
- 在响应式网格中展示 4 个关键指标。
|
||||
- 支持 `isLoading` 属性以渲染骨架屏。
|
||||
- 使用 `Card` 组件作为容器。
|
||||
|
||||
### 3.2 TeacherSchedule (教师日程)
|
||||
- **用途**: 展示今日课程安排。
|
||||
- **特性**:
|
||||
- 列出课程时间及地点。
|
||||
- 使用 Badge 区分 "Lecture" (讲座) 和 "Workshop" (研讨会)。
|
||||
- **空状态**: 当无日程时显示 "No Classes Today"。
|
||||
|
||||
### 3.3 RecentSubmissions (最近提交)
|
||||
- **用途**: 追踪最新的学生活动。
|
||||
- **特性**:
|
||||
- 展示学生头像、姓名、作业名称及时间。
|
||||
- "Late" (迟交) 状态指示器。
|
||||
- **空状态**: 当列表为空时显示 "No New Submissions"。
|
||||
|
||||
### 3.4 EmptyState Component (空状态组件)
|
||||
- **位置**: `src/shared/components/ui/empty-state.tsx`
|
||||
- **规范**:
|
||||
- 虚线边框容器。
|
||||
- 居中图标 (Muted 背景)。
|
||||
- 清晰的标题和描述。
|
||||
- 可选的操作按钮插槽。
|
||||
|
||||
## 4. Hydration 错误修复
|
||||
|
||||
### 4.1 问题描述
|
||||
开发过程中观察到 "Hydration failed" 错误,原因是 HTML 嵌套无效。具体来说,是 `p` 标签内包含了块级元素(或 React 在 hydration 检查期间视为块级的元素)。
|
||||
|
||||
### 4.2 根本原因分析
|
||||
React 的 hydration 过程对 HTML 有效性要求极高。将 `div` 放入 `p` 标签中违反了 HTML5 标准,但浏览器通常会自动修正 DOM 结构,导致实际 DOM 与 React 基于虚拟 DOM 预期的结构不一致。
|
||||
|
||||
### 4.3 实施的修复
|
||||
将所有仪表盘组件中存在风险的 `p` 标签替换为 `div` 标签,以确保嵌套结构的健壮性。
|
||||
|
||||
**示例 (RecentSubmissions):**
|
||||
|
||||
*修改前 (有风险):*
|
||||
```tsx
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{item.studentName}
|
||||
</p>
|
||||
```
|
||||
|
||||
*修改后 (安全):*
|
||||
```tsx
|
||||
<div className="text-sm font-medium leading-none">
|
||||
{item.studentName}
|
||||
</div>
|
||||
```
|
||||
|
||||
**受影响的组件:**
|
||||
1. `recent-submissions.tsx`
|
||||
2. `teacher-stats.tsx`
|
||||
3. `teacher-schedule.tsx`
|
||||
|
||||
## 5. 下一步计划
|
||||
- 将 Mock Data 对接到真实的 API 端点 (React Server Actions)。
|
||||
- 实现 "Quick Actions" (快捷操作) 的具体功能。
|
||||
- 为 Submissions 和 Schedule 添加 "View All" (查看全部) 跳转导航。
|
||||
98
docs/design/003_textbooks_module_implementation.md
Normal file
98
docs/design/003_textbooks_module_implementation.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Textbooks Module Implementation Details
|
||||
|
||||
**Date**: 2025-12-23
|
||||
**Author**: DevOps Architect
|
||||
**Module**: Textbooks (`src/modules/textbooks`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述 (Overview)
|
||||
|
||||
本文档记录了教材模块 (`Textbooks Module`) 的全栈实现细节。该模块负责教材、章节结构及知识点映射的数字化管理,采用了 **Vertical Slice Architecture** 和 **Immersive Workbench** 交互设计。
|
||||
|
||||
## 2. 架构设计 (Architecture)
|
||||
|
||||
### 2.1 目录结构
|
||||
所有教材相关的业务逻辑、数据访问和组件均封装在 `src/modules/textbooks` 下,实现了高度的模块化和隔离。
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ └── (dashboard)/
|
||||
│ └── teacher/
|
||||
│ └── textbooks/ # 路由层 (Server Components)
|
||||
│ ├── page.tsx # 列表页
|
||||
│ ├── loading.tsx # 列表骨架屏
|
||||
│ └── [id]/ # 详情页
|
||||
│ ├── page.tsx
|
||||
│ └── loading.tsx
|
||||
│
|
||||
├── modules/
|
||||
│ └── textbooks/ # 业务模块
|
||||
│ ├── actions.ts # Server Actions (增删改)
|
||||
│ ├── data-access.ts # 数据访问层 (Mock/DB)
|
||||
│ ├── types.ts # 类型定义 (Schema-aligned)
|
||||
│ └── components/ # 模块私有组件
|
||||
│ ├── textbook-content-layout.tsx # [核心] 三栏布局工作台
|
||||
│ ├── chapter-sidebar-list.tsx # 递归章节树
|
||||
│ ├── knowledge-point-panel.tsx # 知识点管理面板
|
||||
│ ├── create-chapter-dialog.tsx # 章节创建弹窗
|
||||
│ └── ... (其他交互组件)
|
||||
```
|
||||
|
||||
### 2.2 渲染策略
|
||||
* **Server Components**: 页面入口 (`page.tsx`) 负责初始数据获取 (Data Fetching),利用 `Promise.all` 并行拉取教材、章节和知识点数据,实现 "Render-as-you-fetch"。
|
||||
* **Client Components**: 工作台布局 (`textbook-content-layout.tsx`) 标记为 `'use client'`,接管后续的所有交互逻辑(状态管理、局部更新、弹窗控制),提供类似 SPA 的流畅体验。
|
||||
|
||||
## 3. UI/UX 细节
|
||||
|
||||
### 3.1 布局 (Layout)
|
||||
采用 **Immersive Workbench (沉浸式工作台)** 三栏设计:
|
||||
* **左侧 (Navigation)**:
|
||||
* 展示递归的章节树 (`Recursive Tree`)。
|
||||
* 支持折叠/展开,清晰展示教材结构。
|
||||
* 顶部提供 "+" 按钮快速创建新章节。
|
||||
* **中间 (Content)**:
|
||||
* **阅读模式**: 渲染 Markdown 格式的章节正文。
|
||||
* **编辑模式**: 提供 `Textarea` 进行内容创作,支持实时保存。
|
||||
* **右侧 (Context)**:
|
||||
* **上下文感知**: 仅显示当前选中章节的关联知识点。
|
||||
* 提供知识点的快速添加 (`Dialog`) 和删除操作。
|
||||
|
||||
### 3.2 交互 (Interactions)
|
||||
* **Selection State**: 点击左侧章节,中间和右侧区域即时更新,无需页面跳转。
|
||||
* **Optimistic UI**: 虽然使用 Server Actions,但通过本地状态 (`useState`) 实现了操作的即时反馈(如保存正文后立即退出编辑模式)。
|
||||
* **Feedback**: 使用 `sonner` (`toast`) 提供操作成功或失败的提示。
|
||||
|
||||
## 4. 数据流与逻辑 (Data Flow)
|
||||
|
||||
### 4.1 Server Actions
|
||||
所有数据变更操作均通过 `src/modules/textbooks/actions.ts` 定义的 Server Actions 处理:
|
||||
* `createChapterAction`: 创建章节(支持嵌套)。
|
||||
* `updateChapterContentAction`: 更新正文内容。
|
||||
* `createKnowledgePointAction`: 创建知识点并自动关联当前章节。
|
||||
* `updateTextbookAction`: 更新教材元数据(Title, Subject, Grade, Publisher)。
|
||||
* `deleteTextbookAction`: 删除教材及其关联数据。
|
||||
* `delete...Action`: 处理删除逻辑。
|
||||
|
||||
### 4.2 数据访问层 (Data Access)
|
||||
* **Mock Implementation**: 目前在 `data-access.ts` 中使用内存数组模拟数据库操作,并人为增加了延迟 (`setTimeout`) 以测试 Loading 状态。
|
||||
* **Type Safety**: 定义了严格的 TypeScript 类型 (`Chapter`, `KnowledgePoint`, `UpdateTextbookInput`),确保前后端数据契约一致。
|
||||
|
||||
## 5. 组件复用
|
||||
* 使用了 `src/shared/components/ui` 中的 Shadcn 组件:
|
||||
* `Dialog`, `ScrollArea`, `Card`, `Button`, `Input`, `Textarea`, `Select`.
|
||||
* `Collapsible` 用于实现递归章节树。
|
||||
* 图标库统一使用 `lucide-react`.
|
||||
|
||||
## 6. Settings 功能实现 (New)
|
||||
* **入口**: 详情页右上角的 "Settings" 按钮。
|
||||
* **组件**: `TextbookSettingsDialog`。
|
||||
* **功能**:
|
||||
* **Edit**: 修改教材的基本信息。
|
||||
* **Delete**: 提供红色删除按钮,二次确认后执行删除并跳转回列表页。
|
||||
|
||||
## 7. 后续计划 (Next Steps)
|
||||
* [ ] **富文本编辑器**: 集成 Tiptap 替换现有的 Markdown Textarea,支持更丰富的格式。
|
||||
* [ ] **拖拽排序**: 实现章节树的拖拽排序 (`dnd-kit`)。
|
||||
* [ ] **数据库对接**: 将 `data-access.ts` 中的 Mock 逻辑替换为真实的 `drizzle-orm` 数据库调用。
|
||||
87
docs/design/004_question_bank_implementation.md
Normal file
87
docs/design/004_question_bank_implementation.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Question Bank Module Implementation
|
||||
|
||||
## 1. Overview
|
||||
The Question Bank module (`src/modules/questions`) is a core component for teachers to manage their examination resources. It implements a comprehensive CRUD interface with advanced filtering and batch operations.
|
||||
|
||||
**Status**: IMPLEMENTED
|
||||
**Date**: 2025-12-23
|
||||
**Author**: Senior Frontend Engineer
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture & Tech Stack
|
||||
|
||||
### 2.1 Vertical Slice Architecture
|
||||
Following the project's architectural guidelines, all question-related logic is encapsulated within `src/modules/questions`:
|
||||
- `components/`: UI components (Data Table, Dialogs, Filters)
|
||||
- `actions.ts`: Server Actions for data mutation
|
||||
- `data-access.ts`: Database query logic
|
||||
- `schema.ts`: Zod schemas for validation
|
||||
- `types.ts`: TypeScript interfaces
|
||||
|
||||
### 2.2 Key Technologies
|
||||
- **Data Grid**: `@tanstack/react-table` for high-performance rendering.
|
||||
- **State Management**: `nuqs` for URL-based state (filters, search).
|
||||
- **Forms**: `react-hook-form` + `zod` + `shadcn/ui` form components.
|
||||
- **Validation**: Strict server-side and client-side validation using Zod schemas.
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Design
|
||||
|
||||
### 3.1 QuestionDataTable (`question-data-table.tsx`)
|
||||
- **Features**: Pagination, Sorting, Row Selection.
|
||||
- **Performance**: Uses `React.memo` compatible patterns where possible (though `useReactTable` itself is not memoized).
|
||||
- **Responsiveness**: Mobile-first design with horizontal scroll for complex columns.
|
||||
|
||||
### 3.2 QuestionColumns (`question-columns.tsx`)
|
||||
Custom cell renderers for rich data display:
|
||||
- **Type Badge**: Color-coded badges for different question types (Single Choice, Multiple Choice, etc.).
|
||||
- **Difficulty**: Visual indicator with color (Green -> Red) and numerical value.
|
||||
- **Actions**: Dropdown menu for Edit, Delete, View Details, and Copy ID.
|
||||
|
||||
### 3.3 Create/Edit Dialog (`create-question-dialog.tsx`)
|
||||
A unified dialog component for both creating and editing questions.
|
||||
- **Dynamic Fields**: Shows/hides "Options" field based on question type.
|
||||
- **Interactive Options**: Allows adding/removing/reordering options for choice questions.
|
||||
- **Optimistic UI**: Shows loading states during submission.
|
||||
|
||||
### 3.4 Filters (`question-filters.tsx`)
|
||||
- **URL Sync**: All filter states (Search, Type, Difficulty) are synced to URL parameters.
|
||||
- **Debounce**: Search input uses debounce to prevent excessive requests.
|
||||
- **Server Filtering**: Filtering logic is executed on the server side (currently simulated in `page.tsx`, ready for DB integration).
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Details
|
||||
|
||||
### 4.1 Data Flow
|
||||
1. **Read**: `page.tsx` (Server Component) fetches data based on `searchParams`.
|
||||
2. **Write**: Client components invoke Server Actions (simulated) -> Revalidate Path -> UI Updates.
|
||||
3. **Filter**: User interaction -> Update URL -> Server Component Re-render -> New Data.
|
||||
|
||||
### 4.2 Type Safety
|
||||
A shared `Question` interface ensures consistency across the stack:
|
||||
```typescript
|
||||
export interface Question {
|
||||
id: string;
|
||||
content: any; // Rich text structure
|
||||
type: QuestionType;
|
||||
difficulty: number;
|
||||
// ... relations
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 UI/UX Standards
|
||||
- **Empty States**: Custom `EmptyState` component when no data matches.
|
||||
- **Loading States**: Skeleton screens for table loading.
|
||||
- **Feedback**: `Sonner` toasts for success/error notifications.
|
||||
- **Confirmation**: `AlertDialog` for destructive actions (Delete).
|
||||
|
||||
---
|
||||
|
||||
## 5. Next Steps
|
||||
- [ ] Integrate with real Database (replace Mock Data).
|
||||
- [ ] Implement Rich Text Editor (Slate.js / Tiptap) for question content.
|
||||
- [ ] Add "Batch Import" functionality.
|
||||
- [ ] Implement "Tags" management for Knowledge Points.
|
||||
90
docs/design/005_exam_module_implementation.md
Normal file
90
docs/design/005_exam_module_implementation.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 考试模块实现设计文档
|
||||
|
||||
## 1. 概述
|
||||
考试模块提供了一个完整的评估管理生命周期,使教师能够创建考试、组卷(支持嵌套分组)、发布评估以及对学生的提交进行评分。
|
||||
|
||||
## 2. 数据架构
|
||||
|
||||
### 2.1 核心实体
|
||||
- **Exams**: 根实体,包含元数据(标题、时间安排)和结构信息。
|
||||
- **ExamQuestions**: 关系链接,用于查询题目的使用情况(扁平化表示)。
|
||||
- **ExamSubmissions**: 学生的考试尝试记录。
|
||||
- **SubmissionAnswers**: 链接到特定题目的单个答案。
|
||||
|
||||
### 2.2 `structure` 字段
|
||||
为了支持层级布局(如章节/分组),我们在 `exams` 表中引入了一个 JSON 列 `structure`。这作为考试布局的“单一事实来源”(Source of Truth)。
|
||||
|
||||
**JSON Schema:**
|
||||
```typescript
|
||||
type ExamNode = {
|
||||
id: string; // 节点的唯一 UUID
|
||||
type: 'group' | 'question';
|
||||
title?: string; // 'group' 类型必填
|
||||
questionId?: string; // 'question' 类型必填
|
||||
score?: number; // 在此考试上下文中的分值
|
||||
children?: ExamNode[]; // 'group' 类型的递归子节点
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 组件架构
|
||||
|
||||
### 3.1 组卷(构建器)
|
||||
位于 `/teacher/exams/[id]/build`。
|
||||
|
||||
- **`ExamAssembly` (客户端组件)**
|
||||
- 管理 `structure` 状态树。
|
||||
- 处理“添加题目”、“添加章节”、“移除”和“重新排序”操作。
|
||||
- 实时计算总分和进度。
|
||||
- **`StructureEditor` (客户端组件)**
|
||||
- 基于 `@dnd-kit` 构建。
|
||||
- 提供嵌套的可排序(Sortable)界面。
|
||||
- 支持在组内/组间拖拽题目(当前优化为 2 层深度)。
|
||||
- **`QuestionBankList`**
|
||||
- 可搜索/筛选的可用题目列表。
|
||||
- “添加”操作将节点追加到结构树中。
|
||||
|
||||
### 3.2 阅卷界面
|
||||
位于 `/teacher/exams/grading/[submissionId]`。
|
||||
|
||||
- **`GradingView` (客户端组件)**
|
||||
- **左侧面板**: 只读视图,显示学生的答案与题目内容。
|
||||
- **右侧面板**: 评分和反馈的输入字段。
|
||||
- **状态**: 在提交前管理本地更改。
|
||||
- **Actions**: `gradeSubmissionAction` 更新 `submissionAnswers` 并将总分聚合到 `examSubmissions`。
|
||||
|
||||
## 4. 关键工作流
|
||||
|
||||
### 4.1 创建与构建考试
|
||||
1. **创建**: 教师输入基本信息(标题、科目)。数据库创建记录(草稿状态)。
|
||||
2. **构建**:
|
||||
- 教师打开“构建”页面。
|
||||
- 服务器从数据库 Hydrate(注水)`initialStructure`。
|
||||
- 教师从题库拖拽题目到结构树。
|
||||
- 教师创建章节(分组)。
|
||||
- **保存**: 同时提交 `questionsJson`(扁平化,用于索引)和 `structureJson`(树状,用于布局)到 `updateExamAction`。
|
||||
3. **发布**: 状态变更为 `published`。
|
||||
|
||||
### 4.2 阅卷流程
|
||||
1. **列表**: 教师查看 `submission-data-table`。
|
||||
2. **评分**: 打开特定提交。
|
||||
3. **审查**: 遍历题目。
|
||||
- 系统显示学生答案。
|
||||
- 教师输入分数(上限为满分)和反馈。
|
||||
4. **提交**: 服务器更新单个答案记录并重新计算提交总分。
|
||||
|
||||
## 5. 技术决策
|
||||
|
||||
### 5.1 混合存储策略
|
||||
我们在存储考试题目时采用了 **混合方法**:
|
||||
- **关系型 (`exam_questions`)**: 用于“查找所有使用题目 X 的考试”查询和外键约束。
|
||||
- **文档型 (`exams.structure`)**: 用于渲染嵌套 UI 和保留任意排序/分组。
|
||||
*理由*: 这结合了 SQL 的完整性和 NoSQL 在 UI 布局上的灵活性。
|
||||
|
||||
### 5.2 拖拽功能
|
||||
使用 `@dnd-kit` 代替旧库,因为:
|
||||
- 更好的无障碍支持(键盘支持)。
|
||||
- 模块化架构(Sensors, Modifiers)。
|
||||
- 面向未来(现代 React Hooks 模式)。
|
||||
|
||||
### 5.3 Server Actions
|
||||
所有变更操作(保存草稿、发布、评分)均使用 Next.js Server Actions,以确保类型安全并自动重新验证缓存。
|
||||
258
docs/design/design_system.md
Normal file
258
docs/design/design_system.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Next_Edu Design System Specs
|
||||
|
||||
**Version**: 1.4.0 (Updated)
|
||||
**Status**: ACTIVE
|
||||
**Role**: Chief Creative Director
|
||||
**Philosophy**: "Data as Art" - Clean, Minimalist, Information-Dense.
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心理念 (Core Philosophy)
|
||||
|
||||
Next_Edu 旨在对抗教育系统常见的信息过载。我们的设计风格深受 **International Typographic Style (国际主义设计风格)** 影响。
|
||||
|
||||
* **Precision (精准)**: 每一个像素的留白都有其目的。
|
||||
* **Clarity (清晰)**: 通过排版和对比度区分层级,而非装饰性的色块。
|
||||
* **Efficiency (效率)**: 专为高密度数据操作优化,减少视觉噪音。
|
||||
|
||||
---
|
||||
|
||||
## 2. 视觉基础 (Visual Foundation)
|
||||
|
||||
### 2.1 色彩系统 (Color System)
|
||||
|
||||
我们放弃 Hex 直选,全面采用 **HSL 语义化变量** 以支持完美的多主题适配。
|
||||
|
||||
#### **Base (基调)**
|
||||
* **Neutral**: Zinc (锌色). 冷峻、纯净,无偏色。
|
||||
* **Brand**: Deep Indigo (深靛蓝).
|
||||
* `Primary`: 专业、权威,避免幼稚的高饱和蓝。
|
||||
* 语义: `hsl(var(--primary))`
|
||||
|
||||
#### **Functional (功能色)**
|
||||
| 语义 | 色系 | 用途 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Destructive** | Red | 删除、危险操作、系统错误 |
|
||||
| **Warning** | Amber | 需注意的状态、非阻断性警告 |
|
||||
| **Success** | Emerald | 操作成功、状态正常 |
|
||||
| **Info** | Blue | 一般性提示、帮助信息 |
|
||||
|
||||
#### **Surface Hierarchy (层级)**
|
||||
1. **Background**: 应用底层背景。
|
||||
2. **Card**: 内容承载容器,轻微提升层级。
|
||||
3. **Popover**: 悬浮层、下拉菜单,最高层级。
|
||||
4. **Muted**: 用于次级信息或禁用状态背景。
|
||||
|
||||
### 2.2 排版 (Typography)
|
||||
|
||||
* **Font Family**: `Geist Sans` > `Inter` > `System UI`.
|
||||
* *Requirement*: 必须开启 `tabular-nums` (等宽数字) 特性,确保表格数据对齐。
|
||||
* **Scale**:
|
||||
* **Base Body**: 14px (0.875rem) - 提升信息密度。
|
||||
* **H1 (Page Title)**: 24px, Tracking -0.02em, Weight 600.
|
||||
* **H2 (Section Title)**: 20px, Tracking -0.01em, Weight 600.
|
||||
* **H3 (Card Title)**: 16px, Weight 600.
|
||||
* **Tiny/Caption**: 12px, Text-Muted-Foreground.
|
||||
|
||||
### 2.3 质感 (Look & Feel)
|
||||
|
||||
* **Borders**: `1px solid var(--border)`. 界面骨架,以此分割区域而非背景色块。
|
||||
* **Radius**:
|
||||
* `sm` (4px): Badges, Checkboxes.
|
||||
* `md` (8px): **Default**. Buttons, Inputs, Cards.
|
||||
* `lg` (12px): Modals, Dialogs.
|
||||
* **Shadows**:
|
||||
* Default: None (Flat).
|
||||
* Hover: `shadow-sm` (仅用于可交互元素).
|
||||
* Dropdown/Popover: `shadow-md`.
|
||||
* *Ban*: 禁止使用大面积弥散阴影。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心布局 (App Shell)
|
||||
|
||||
### 3.1 架构 (Architecture)
|
||||
我们采用了 `SidebarProvider` + `AppSidebar` 的组合模式,确保了布局的灵活性和移动端的完美适配。
|
||||
|
||||
* **Provider**: `SidebarProvider` (src/modules/layout/components/sidebar-provider.tsx)
|
||||
* 管理侧边栏状态 (`expanded`, `isMobile`).
|
||||
* 负责在移动端渲染 Sheet (Drawer)。
|
||||
* 负责在桌面端渲染 Sticky Sidebar。
|
||||
* **Key Prop**: `sidebar` (显式传递侧边栏组件)。
|
||||
|
||||
### 3.2 布局结构
|
||||
```
|
||||
+-------------------------------------------------------+
|
||||
| Sidebar | Header (Sticky) |
|
||||
| |-------------------------------------------|
|
||||
| (Collap- | Main Content |
|
||||
| sible) | |
|
||||
| | +-------------------------------------+ |
|
||||
| | | Card | |
|
||||
| | | +---------------------------------+ | |
|
||||
| | | | Data Table | | |
|
||||
| | | +---------------------------------+ | |
|
||||
| | +-------------------------------------+ |
|
||||
| | |
|
||||
+-------------------------------------------------------+
|
||||
```
|
||||
|
||||
### 3.3 详细规范
|
||||
|
||||
#### **Sidebar (侧边栏)**
|
||||
* **Width**: Expanded `260px` | Collapsed `64px` | Mobile `Sheet (Drawer)`.
|
||||
* **Behavior**:
|
||||
* Desktop: 固定左侧,支持折叠。
|
||||
* Mobile: 默认隐藏,点击汉堡菜单从左侧滑出。
|
||||
* **Navigation Item**:
|
||||
* Height: `36px` (Compact).
|
||||
* State:
|
||||
* `Inactive`: `text-muted-foreground hover:text-foreground`.
|
||||
* `Active`: `bg-sidebar-accent text-sidebar-accent-foreground font-medium`.
|
||||
|
||||
#### **Header (顶栏)**
|
||||
* **Height**: `64px` (h-16).
|
||||
* **Layout**: `flex items-center justify-between px-6 border-b`.
|
||||
* **Components**:
|
||||
1. **Breadcrumb**: 显示当前路径,层级清晰。
|
||||
2. **Global Search**: `Cmd+K` 触发,居中或靠右。
|
||||
3. **User Nav**: 头像 + 下拉菜单。
|
||||
|
||||
#### **Main Content (内容区)**
|
||||
* **Padding**: `p-6` (Desktop) / `p-4` (Mobile).
|
||||
* **Max Width**: `max-w-[1600px]` (默认) 或 `w-full` (针对超宽报表)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 导航与角色系统 (Navigation & Roles)
|
||||
|
||||
Next_Edu 支持多角色(Multi-Tenant / Role-Based)导航系统。
|
||||
|
||||
### 4.1 配置文件
|
||||
导航结构已从 UI 组件中解耦,统一配置在:
|
||||
`src/modules/layout/config/navigation.ts`
|
||||
|
||||
### 4.2 支持的角色 (Roles)
|
||||
系统内置支持以下角色,每个角色拥有独立的侧边栏菜单结构:
|
||||
* **Admin**: 系统管理员,拥有所有管理权限 (School Management, User Management, Finance)。
|
||||
* **Teacher**: 教师,关注班级管理 (My Classes)、成绩录入 (Gradebook) 和日程。
|
||||
* **Student**: 学生,关注课程学习 (My Learning) 和作业提交。
|
||||
* **Parent**: 家长,关注子女动态和学费缴纳。
|
||||
|
||||
### 4.3 开发与调试
|
||||
* **View As (Dev Mode)**: 在开发环境下,侧边栏顶部提供 "View As" 下拉菜单,允许开发者实时切换角色视角,预览不同角色的导航结构。
|
||||
* **Implementation**: `AppSidebar` 组件通过读取 `NAV_CONFIG[currentRole]` 动态渲染菜单项。
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理与边界情况 (Error Handling & Boundaries)
|
||||
|
||||
系统必须优雅地处理错误和边缘情况,避免白屏或无反馈。
|
||||
|
||||
### 5.1 全局错误边界 (Global Error Boundary)
|
||||
* **Scope**: 捕获渲染期间的未处理异常。
|
||||
* **UI**: 显示友好的错误页面(非技术堆栈信息),提供 "Try Again" 按钮重置状态。
|
||||
* **Implementation**: 使用 React `ErrorBoundary` 或 Next.js `error.tsx`。
|
||||
|
||||
### 5.2 404 Not Found
|
||||
* **Design**: 必须保留 App Shell (Sidebar + Header),仅在 Main Content 区域显示 404 提示。
|
||||
* **Content**: "Page not found" 文案 + 返回 Dashboard 的主操作按钮。
|
||||
|
||||
### 5.3 空状态 (Empty States)
|
||||
当列表或表格无数据时,**严禁**只显示空白。
|
||||
* **Component**: `EmptyState`。
|
||||
* **Composition**:
|
||||
1. **Icon**: 线性风格图标 (muted foreground).
|
||||
2. **Title**: 简短说明 (e.g., "No students found").
|
||||
3. **Description**: 解释原因或下一步操作 (e.g., "Add a student to get started").
|
||||
4. **Action**: (可选) "Create New" 按钮。
|
||||
|
||||
### 5.4 加载状态 (Loading States)
|
||||
* **Initial Load**: 使用 `Skeleton` 骨架屏,模拟内容布局,避免 CLS (Content Layout Shift)。禁止使用全屏 Spinner。
|
||||
* **Action Loading**: 按钮点击后进入 `disabled` + `spinner` 状态。
|
||||
* **Table Loading**: 表格内容区域显示 3-5 行 Skeleton Rows。
|
||||
|
||||
### 5.5 表单验证 (Form Validation)
|
||||
* **Style**: 错误信息显示在输入框下方,字号 `text-xs`,颜色 `text-destructive`。
|
||||
* **Input**: 边框变红 (`border-destructive`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 职责边界与协作 (Responsibility Boundaries)
|
||||
|
||||
**[IMPORTANT] 严禁越界修改 (Strict No-Modification Policy)**
|
||||
|
||||
为了维护大型项目的可维护性,UI 工程师和开发人员必须遵守以下边界规则:
|
||||
|
||||
### 6.1 模块化原则 (Modularity)
|
||||
* **Scope**: 开发者仅应对分配给自己的模块负责。例如,负责 "Dashboard" 的开发者**不应**修改 "Sidebar" 或 "Auth" 模块的代码。
|
||||
* **Dependencies**: 如果你的模块依赖于其他模块的变更,**必须**先与该模块的负责人沟通,或在 PR 中明确标注。
|
||||
|
||||
### 6.2 共享组件 (Shared Components)
|
||||
* **Immutable Core**: `src/shared/components/ui` 下的基础组件(如 Button, Card)视为**核心库**。
|
||||
* **Extension**: 如果基础组件不能满足需求,优先考虑组合(Composition)或创建新的业务组件,而不是修改核心组件的源码。
|
||||
* **Modification Request**: 只有在发现严重 Bug 或需要全局样式调整时,才允许修改核心组件,且必须经过 Design Lead 审批。
|
||||
|
||||
### 6.3 样式一致性 (Consistency)
|
||||
* **Global CSS**: `globals.css` 定义了系统的物理法则。严禁在局部组件中随意覆盖全局 CSS 变量。
|
||||
* **Tailwind Config**: 禁止随意在组件中添加任意值(Arbitrary Values, e.g., `w-[123px]`),必须使用 Design Token。
|
||||
|
||||
---
|
||||
|
||||
## 7. 组件设计规范 (Component Specs)
|
||||
|
||||
### 7.1 Card (卡片)
|
||||
卡片是信息组织的基本单元。
|
||||
* **Class**: `bg-card text-card-foreground border rounded-lg shadow-none`.
|
||||
* **Header**: `p-6 pb-2`. Title (`font-semibold leading-none tracking-tight`).
|
||||
* **Content**: `p-6 pt-0`.
|
||||
|
||||
### 7.2 Data Table (数据表格)
|
||||
教务系统的核心组件。
|
||||
* **Density**:
|
||||
* `Default`: Row Height `48px` (h-12).
|
||||
* `Compact`: Row Height `36px` (h-9).
|
||||
* **Header**: `bg-muted/50 text-muted-foreground text-xs uppercase font-medium`.
|
||||
* **Stripes**: 默认关闭。仅在列数 > 8 时开启 `even:bg-muted/50`。
|
||||
* **Actions**: 行操作按钮应默认隐形 (`opacity-0`),Hover 时显示 (`group-hover:opacity-100`),减少视觉干扰。
|
||||
|
||||
### 7.3 Feedback (反馈与通知)
|
||||
* **Toast**: 使用 `Sonner` 组件。
|
||||
* 位置: 默认右下角 (Bottom Right).
|
||||
* 样式: 极简黑白风格 (跟随主题),支持撤销操作。
|
||||
* 调用: `toast("Event has been created", { description: "Sunday, December 03, 2023 at 9:00 AM" })`.
|
||||
* **Skeleton**: 加载状态必须使用 Skeleton 骨架屏,禁止使用全屏 Spinner。
|
||||
* **Badge**: 状态指示器。
|
||||
* `default`: 主要状态 (Primary).
|
||||
* `secondary`: 次要状态 (Neutral).
|
||||
* `destructive`: 错误/警告状态 (Error).
|
||||
* `outline`: 描边风格 (Subtle).
|
||||
|
||||
---
|
||||
|
||||
## 8. 开发指南 (Developer Guide)
|
||||
|
||||
### 8.1 CSS Variables
|
||||
所有颜色和圆角均通过 CSS 变量控制,定义在 `globals.css` 中。禁止在代码中 Hardcode 颜色值 (如 `#FFFFFF`, `rgb(0,0,0)` )。
|
||||
|
||||
### 8.2 Tailwind Utility 优先
|
||||
优先使用 Tailwind Utility Classes。
|
||||
* ✅ `text-sm text-muted-foreground`
|
||||
* ❌ `.custom-text-class { font-size: 14px; color: #666; }`
|
||||
|
||||
### 8.3 Dark Mode
|
||||
设计系统原生支持深色模式。只要正确使用语义化颜色变量(如 `bg-background`, `text-foreground`),Dark Mode 将自动完美适配,无需额外编写 `dark:` 修饰符(除非为了特殊调整)。
|
||||
|
||||
### 8.4 组件库引用
|
||||
所有 UI 组件位于 `src/shared/components/ui`。
|
||||
* `Button`: 基础按钮
|
||||
* `Input`: 输入框
|
||||
* `Select`: 下拉选择器 (New)
|
||||
* `Sheet`: 侧边栏/抽屉
|
||||
* `Sonner`: Toast 通知
|
||||
* `Badge`: 徽章/标签
|
||||
* `Skeleton`: 加载占位符
|
||||
* `DropdownMenu`: 下拉菜单
|
||||
* `Avatar`: 头像
|
||||
* `Label`: 表单标签
|
||||
* `EmptyState`: 空状态占位 (New)
|
||||
180
docs/product_requirements.md
Normal file
180
docs/product_requirements.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Next_Edu 产品需求文档 (PRD) - K12 智慧教学管理系统
|
||||
|
||||
**版本**: 2.0.0 (K12 Enterprise Edition)
|
||||
**状态**: 规划中
|
||||
**最后更新**: 2025-12-22
|
||||
**作者**: Senior EdTech Product Manager
|
||||
**适用范围**: 全校级教学管理 (教-考-练-评)
|
||||
|
||||
---
|
||||
|
||||
## 1. 角色与权限矩阵 (Complex Role Matrix)
|
||||
|
||||
本系统采用基于 RBAC (Role-Based Access Control) 的多维权限设计,并结合 **行级安全 (Row-Level Security, RLS)** 策略,确保数据隔离与行政管理的精确匹配。
|
||||
|
||||
### 1.1 角色定义与核心职责
|
||||
|
||||
| 角色 | 核心职责 | 权限特征 (Scope) |
|
||||
| :--- | :--- | :--- |
|
||||
| **系统管理员 (Admin)** | 基础数据维护、账号管理、学期设置 | 全局系统配置,不可触碰教学业务数据内容(隐私保护)。 |
|
||||
| **校长 (Principal)** | 全校教学概况监控、宏观统计报表 | **全校可见**。查看所有年级、学科的统计数据(平均分、作业完成率),无修改具体的题目/作业权限。 |
|
||||
| **年级主任 (Grade Head)** | 本年级行政管理、班级均衡度分析 | **年级可见**。管理本年级所有行政班级;查看本年级跨学科对比;无权干涉其他年级。 |
|
||||
| **教研组长 (Subject Head)** | 学科资源建设、命题质量把控 | **学科可见**。管理本学科公共题库、教案模板;查看全校该学科教学质量;无权查看其他学科详情。 |
|
||||
| **班主任 (Class Teacher)** | 班级学生管理、家校通知、综合评价 | **行政班可见**。查看本班所有学生的跨学科成绩、考勤;发布班级公告。 |
|
||||
| **任课老师 (Teacher)** | 备课、出卷、批改、个别辅导 | **教学班可见**。仅能操作自己所教班级的该学科作业/考试;私有题库管理。 |
|
||||
| **学生 (Student)** | 完成作业、参加考试、查看错题本 | **个人可见**。仅能访问分配给自己的任务;查看个人成长档案。 |
|
||||
|
||||
### 1.2 关键权限辨析:年级主任 vs 教研组长
|
||||
|
||||
* **维度差异**:
|
||||
* **年级主任 (横向管理)**: 关注的是 **"人" (People & Administration)**。例如:高一(3)班的整体纪律如何?高一年级整体是否在期中考试中达标?他们需要跨学科的数据视图(如:某学生是否偏科)。
|
||||
* **教研组长 (纵向管理)**: 关注的是 **"内容" (Content & Pedagogy)**。例如:英语科目的“阅读理解”题型得分率全校是否偏低?公共题库的题目质量如何?他们需要跨年级但单学科的深度视图。
|
||||
|
||||
* **数据可见性 (RLS 策略)**:
|
||||
* `GradeHead_View`: `WHERE class.grade_id = :current_user_grade_id`
|
||||
* `SubjectHead_View`: `WHERE course.subject_id = :current_user_subject_id` (可能跨所有年级)
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心功能模块深度拆解
|
||||
|
||||
### 2.1 智能题库中心 (Smart Question Bank)
|
||||
|
||||
这是系统的核心资产库,必须支持高复杂度的题目结构。
|
||||
|
||||
* **多层嵌套题目结构 (Nested Questions)**:
|
||||
* **场景**: 英语完形填空、语文现代文阅读、理综大题。
|
||||
* **逻辑**: 引入 **"题干 (Stem)"** 与 **"子题 (Sub-question)"** 的概念。
|
||||
* **父题 (Parent)**: 承载公共题干(如一篇 500 字的文章、一张物理实验图表)。通常不直接设分,或者设总分。
|
||||
* **子题 (Child)**: 依附于父题,是具体的答题点(选择、填空、简答)。每个子题有独立的分值、答案和解析。
|
||||
* **交互**: 组卷时,拖动“父题”,所有“子题”必须作为一个原子整体跟随移动,不可拆分。
|
||||
|
||||
* **知识点图谱 (Knowledge Graph)**:
|
||||
* **结构**: 树状结构 (Tree)。例如:`数学 -> 代数 -> 函数 -> 二次函数 -> 二次函数的图像`。
|
||||
* **关联**:
|
||||
* **多对多 (Many-to-Many)**: 一道题可能考察多个知识点(综合题)。
|
||||
* **权重**: (可选高级需求) 标记主要考点与次要考点。
|
||||
|
||||
### 2.2 课本与大纲映射 (Textbook & Curriculum)
|
||||
|
||||
* **课本数字化**:
|
||||
* 系统预置主流教材版本 (如人教版、北师大版)。
|
||||
* **核心映射**: `Textbook Chapter` (课本章节) <--> `Knowledge Point` (知识点)。
|
||||
* **价值**: 老师备课时,只需选择“必修一 第一章”,系统自动推荐关联的“集合”相关题目,无需手动去海量题库搜索。
|
||||
|
||||
### 2.3 试卷/作业组装引擎 (Assembly Engine)
|
||||
|
||||
* **智能筛选**: 支持交集筛选(同时包含“力学”和“三角函数”的题目)。
|
||||
* **AB 卷生成**: 针对防作弊场景,支持题目乱序或选项乱序(Shuffle)。
|
||||
* **作业分层**: 支持“必做题”与“选做题”设置,满足分层教学需求。
|
||||
|
||||
### 2.4 消息通知中心 (Notification System)
|
||||
|
||||
分级分策略的消息分发:
|
||||
|
||||
* **强提醒 (High Priority)**: 系统公告、考试开始提醒。通过站内信 + 弹窗 + (集成)短信/微信模板消息。
|
||||
* **业务流 (Medium Priority)**: 作业发布、成绩出炉。站内红点 + 列表推送。
|
||||
* **弱提醒 (Low Priority)**: 错题本更新、周报生成。仅在进入相关模块时提示。
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据实体关系推演 (Data Entity Relationships)
|
||||
|
||||
基于 MySQL 关系型数据库的设计方案。
|
||||
|
||||
### 3.1 核心实体模型 (ER Draft)
|
||||
|
||||
1. **SysUser**: `id`, `username`, `role`, `school_id`
|
||||
2. **TeacherProfile**: `user_id`, `is_grade_head`, `is_subject_head`
|
||||
3. **Class**: `id`, `grade_level` (e.g., 10), `class_name` (e.g., "3班"), `homeroom_teacher_id`
|
||||
4. **Subject**: `id`, `name` (e.g., "Math")
|
||||
5. **Course**: `id`, `class_id`, `subject_id`, `teacher_id` (核心教学关系表: 谁教哪个班的哪门课)
|
||||
|
||||
### 3.2 题库与知识点设计 (关键难点)
|
||||
|
||||
#### Table: `knowledge_points` (知识点树)
|
||||
* `id`: UUID
|
||||
* `subject_id`: FK
|
||||
* `name`: VARCHAR
|
||||
* `parent_id`: UUID (Self-reference, Root is NULL)
|
||||
* `level`: INT (1, 2, 3...)
|
||||
* `code`: VARCHAR (e.g., "M-ALG-01-02" 用于快速检索)
|
||||
|
||||
#### Table: `questions` (支持嵌套)
|
||||
* `id`: UUID
|
||||
* `content`: TEXT (HTML/Markdown, store images as URLs)
|
||||
* `type`: ENUM ('SINGLE', 'MULTI', 'FILL', 'ESSAY', 'COMPOSITE')
|
||||
* `parent_id`: UUID (Self-reference, **核心设计**)
|
||||
* If `NULL`: 这是一道独立题目 OR 复合题的大题干。
|
||||
* If `NOT NULL`: 这是一个子题目,属于 `parent_id` 对应的题干。
|
||||
* `difficulty`: INT (1-5)
|
||||
* `answer`: TEXT (JSON structure for structured answers)
|
||||
* `analysis`: TEXT (解析)
|
||||
* `created_by`: FK (Teacher)
|
||||
* `scope`: ENUM ('PUBLIC', 'PRIVATE')
|
||||
|
||||
#### Table: `question_knowledge` (题目-知识点关联)
|
||||
* `question_id`: FK
|
||||
* `knowledge_point_id`: FK
|
||||
* **Primary Key**: (`question_id`, `knowledge_point_id`)
|
||||
|
||||
### 3.3 课本映射设计
|
||||
|
||||
#### Table: `textbooks`
|
||||
* `id`: UUID
|
||||
* `name`: VARCHAR
|
||||
* `grade_level`: INT
|
||||
* `subject_id`: FK
|
||||
|
||||
#### Table: `textbook_chapters`
|
||||
* `id`: UUID
|
||||
* `textbook_id`: FK
|
||||
* `name`: VARCHAR
|
||||
* `parent_id`: UUID (Sections within Chapters)
|
||||
* `content`: TEXT (Rich text content of the chapter/section) -- [ADDED for Content Viewing]
|
||||
* `order`: INT
|
||||
|
||||
#### Table: `chapter_knowledge_mapping`
|
||||
* `chapter_id`: FK
|
||||
* `knowledge_point_id`: FK
|
||||
* *解释*: 这张表是连接“教学进度”与“底层知识”的桥梁。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键业务流程 (User Flows)
|
||||
|
||||
### 4.1 智能组卷与发布流程 (Exam Creation Flow)
|
||||
|
||||
这是一个高频且复杂的路径,需要极高的流畅度。
|
||||
|
||||
1. **启动组卷**:
|
||||
* 老师进入 [教学工作台] -> 点击 [新建试卷/作业]。
|
||||
* 输入基本信息(名称、考试时长、总分)。
|
||||
|
||||
2. **设定范围 (锚定课本)**:
|
||||
* 老师选择教材版本:`人教版高中数学必修一`。
|
||||
* 选择章节:勾选 `第一章 集合` 和 `第二章 函数概念`。
|
||||
* *系统动作*: 后台查询 `chapter_knowledge_mapping`,提取出这几章对应的所有 `knowledge_points`。
|
||||
|
||||
3. **筛选题目**:
|
||||
* 系统展示题目列表,默认过滤条件为上述提取的知识点。
|
||||
* 老师增加筛选:`难度: 中等`, `题型: 选择题`。
|
||||
* **处理嵌套题**: 如果筛选结果包含一个“完形填空”的子题,系统在 UI 上必须**强制展示**其对应的父题干,并提示老师“需整体添加”。
|
||||
|
||||
4. **加入试题篮 (Cart)**:
|
||||
* 老师点击“+”号。
|
||||
* 试题篮动态更新:`当前题目数: 15, 预计总分: 85`。
|
||||
|
||||
5. **试卷精修 (Refine)**:
|
||||
* 进入“试卷预览”模式。
|
||||
* 调整题目顺序 (Drag & drop)。
|
||||
* 修改某道题的分值(覆盖默认分值)。
|
||||
|
||||
6. **发布设置**:
|
||||
* 选择发布对象:`高一(3)班`, `高一(5)班` (基于 `Course` 表权限)。
|
||||
* 设置时间:`开始时间`, `截止时间`。
|
||||
* 发布模式:`在线作答` 或 `线下答题卡` (若线下,系统生成 PDF 和答题卡样式)。
|
||||
|
||||
7. **完成**:
|
||||
* 学生端收到 `Notifications` 推送。
|
||||
* `Exams` 表生成记录,`ExamAllocations` 表为每个班级/学生生成状态记录。
|
||||
35
docs/scripts/reset-db.ts
Normal file
35
docs/scripts/reset-db.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import "dotenv/config"
|
||||
import { db } from "@/shared/db"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
async function reset() {
|
||||
console.log("🔥 Resetting database...")
|
||||
|
||||
// Disable foreign key checks
|
||||
await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`)
|
||||
|
||||
// Get all table names
|
||||
const tables = await db.execute(sql`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE();
|
||||
`)
|
||||
|
||||
// Drop each table
|
||||
for (const row of (tables[0] as unknown as any[])) {
|
||||
const tableName = row.TABLE_NAME || row.table_name
|
||||
console.log(`Dropping table: ${tableName}`)
|
||||
await db.execute(sql.raw(`DROP TABLE IF EXISTS \`${tableName}\`;`))
|
||||
}
|
||||
|
||||
// Re-enable foreign key checks
|
||||
await db.execute(sql`SET FOREIGN_KEY_CHECKS = 1;`)
|
||||
|
||||
console.log("✅ Database reset complete.")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
reset().catch((err) => {
|
||||
console.error("❌ Reset failed:", err)
|
||||
process.exit(1)
|
||||
})
|
||||
346
docs/scripts/seed-exams.ts
Normal file
346
docs/scripts/seed-exams.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import "dotenv/config"
|
||||
import { db } from "@/shared/db"
|
||||
import { users, exams, questions, knowledgePoints, examSubmissions, examQuestions, submissionAnswers } from "@/shared/db/schema"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { faker } from "@faker-js/faker"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
/**
|
||||
* Seed Script for Next_Edu
|
||||
*
|
||||
* Usage:
|
||||
* 1. Ensure DATABASE_URL is set in .env
|
||||
* 2. Run with tsx: npx tsx docs/scripts/seed-exams.ts
|
||||
*/
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "English", "Chemistry", "Biology"]
|
||||
const GRADES = ["Grade 10", "Grade 11", "Grade 12"]
|
||||
const DIFFICULTY = [1, 2, 3, 4, 5]
|
||||
|
||||
async function seed() {
|
||||
console.log("🌱 Starting seed process...")
|
||||
|
||||
// 1. Create a Teacher User if not exists
|
||||
const teacherEmail = "teacher@example.com"
|
||||
let teacherId = "user_teacher_123"
|
||||
|
||||
const existingTeacher = await db.query.users.findFirst({
|
||||
where: eq(users.email, teacherEmail)
|
||||
})
|
||||
|
||||
if (!existingTeacher) {
|
||||
console.log("Creating teacher user...")
|
||||
await db.insert(users).values({
|
||||
id: teacherId,
|
||||
name: "Senior Teacher",
|
||||
email: teacherEmail,
|
||||
role: "teacher",
|
||||
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Teacher",
|
||||
})
|
||||
} else {
|
||||
teacherId = existingTeacher.id
|
||||
console.log("Teacher user exists:", teacherId)
|
||||
}
|
||||
|
||||
// 1b. Create Students
|
||||
console.log("Creating students...")
|
||||
const studentIds: string[] = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const sId = createId()
|
||||
studentIds.push(sId)
|
||||
await db.insert(users).values({
|
||||
id: sId,
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
role: "student",
|
||||
image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${sId}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Create Knowledge Points
|
||||
console.log("Creating knowledge points...")
|
||||
const kpIds: string[] = []
|
||||
for (const subject of SUBJECTS) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const kpId = createId()
|
||||
kpIds.push(kpId)
|
||||
await db.insert(knowledgePoints).values({
|
||||
id: kpId,
|
||||
name: `${subject} - ${faker.science.unit()}`,
|
||||
description: faker.lorem.sentence(),
|
||||
level: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create Questions
|
||||
console.log("Creating questions...")
|
||||
const questionIds: string[] = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const qId = createId()
|
||||
questionIds.push(qId)
|
||||
|
||||
const type = faker.helpers.arrayElement(["single_choice", "multiple_choice", "text", "judgment"])
|
||||
|
||||
await db.insert(questions).values({
|
||||
id: qId,
|
||||
content: {
|
||||
text: faker.lorem.paragraph(),
|
||||
options: type.includes("choice") ? [
|
||||
{ id: "A", text: faker.lorem.sentence(), isCorrect: true },
|
||||
{ id: "B", text: faker.lorem.sentence(), isCorrect: false },
|
||||
{ id: "C", text: faker.lorem.sentence(), isCorrect: false },
|
||||
{ id: "D", text: faker.lorem.sentence(), isCorrect: false },
|
||||
] : undefined
|
||||
},
|
||||
type: type as any,
|
||||
difficulty: faker.helpers.arrayElement(DIFFICULTY),
|
||||
authorId: teacherId,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Create Exams & Submissions
|
||||
console.log("Creating exams and submissions...")
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const examId = createId()
|
||||
const subject = faker.helpers.arrayElement(SUBJECTS)
|
||||
const grade = faker.helpers.arrayElement(GRADES)
|
||||
const status = faker.helpers.arrayElement(["draft", "published", "archived"])
|
||||
|
||||
const scheduledAt = faker.date.soon({ days: 30 })
|
||||
|
||||
const meta = {
|
||||
subject,
|
||||
grade,
|
||||
difficulty: faker.helpers.arrayElement(DIFFICULTY),
|
||||
totalScore: 100,
|
||||
durationMin: faker.helpers.arrayElement([45, 60, 90, 120]),
|
||||
questionCount: faker.number.int({ min: 10, max: 30 }),
|
||||
tags: [faker.word.sample(), faker.word.sample()],
|
||||
scheduledAt: scheduledAt.toISOString()
|
||||
}
|
||||
|
||||
await db.insert(exams).values({
|
||||
id: examId,
|
||||
title: `${subject} ${faker.helpers.arrayElement(["Midterm", "Final", "Quiz", "Unit Test"])}`,
|
||||
description: JSON.stringify(meta),
|
||||
creatorId: teacherId,
|
||||
startTime: scheduledAt,
|
||||
status: status as any,
|
||||
})
|
||||
|
||||
// Link some questions to this exam (random 5 questions)
|
||||
const selectedQuestions = faker.helpers.arrayElements(questionIds, 5)
|
||||
await db.insert(examQuestions).values(
|
||||
selectedQuestions.map((qId, idx) => ({
|
||||
examId,
|
||||
questionId: qId,
|
||||
score: 20, // 5 * 20 = 100
|
||||
order: idx
|
||||
}))
|
||||
)
|
||||
|
||||
// Create submissions for published exams
|
||||
if (status === "published") {
|
||||
const submittingStudents = faker.helpers.arrayElements(studentIds, faker.number.int({ min: 1, max: 3 }))
|
||||
for (const studentId of submittingStudents) {
|
||||
const submissionId = createId()
|
||||
const submissionStatus = faker.helpers.arrayElement(["submitted", "graded"])
|
||||
|
||||
await db.insert(examSubmissions).values({
|
||||
id: submissionId,
|
||||
examId,
|
||||
studentId,
|
||||
score: submissionStatus === "graded" ? faker.number.int({ min: 60, max: 100 }) : null,
|
||||
status: submissionStatus,
|
||||
submittedAt: faker.date.recent(),
|
||||
})
|
||||
|
||||
// Generate answers for this submission
|
||||
for (const qId of selectedQuestions) {
|
||||
await db.insert(submissionAnswers).values({
|
||||
id: createId(),
|
||||
submissionId: submissionId,
|
||||
questionId: qId,
|
||||
answerContent: { answer: faker.lorem.sentence() }, // Mock answer
|
||||
score: submissionStatus === "graded" ? faker.number.int({ min: 0, max: 20 }) : null,
|
||||
feedback: submissionStatus === "graded" ? faker.lorem.sentence() : null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create a specific Primary School Chinese Exam (小学语文)
|
||||
console.log("Creating Primary School Chinese Exam...")
|
||||
const chineseExamId = createId()
|
||||
const chineseQuestions = []
|
||||
|
||||
// 5a. Pinyin Questions
|
||||
const pinyinQ1 = createId()
|
||||
const pinyinQ2 = createId()
|
||||
chineseQuestions.push({ id: pinyinQ1, score: 5 }, { id: pinyinQ2, score: 5 })
|
||||
|
||||
await db.insert(questions).values([
|
||||
{
|
||||
id: pinyinQ1,
|
||||
content: { text: "看拼音写词语:chūn tiān ( )" },
|
||||
type: "text",
|
||||
difficulty: 1,
|
||||
authorId: teacherId,
|
||||
},
|
||||
{
|
||||
id: pinyinQ2,
|
||||
content: { text: "看拼音写词语:huā duǒ ( )" },
|
||||
type: "text",
|
||||
difficulty: 1,
|
||||
authorId: teacherId,
|
||||
}
|
||||
])
|
||||
|
||||
// 5b. Vocabulary Questions
|
||||
const vocabQ1 = createId()
|
||||
const vocabQ2 = createId()
|
||||
chineseQuestions.push({ id: vocabQ1, score: 5 }, { id: vocabQ2, score: 5 })
|
||||
|
||||
await db.insert(questions).values([
|
||||
{
|
||||
id: vocabQ1,
|
||||
content: {
|
||||
text: "选词填空:今天天气真( )。",
|
||||
options: [
|
||||
{ id: "A", text: "美好", isCorrect: false },
|
||||
{ id: "B", text: "晴朗", isCorrect: true },
|
||||
{ id: "C", text: "快乐", isCorrect: false }
|
||||
]
|
||||
},
|
||||
type: "single_choice",
|
||||
difficulty: 2,
|
||||
authorId: teacherId,
|
||||
},
|
||||
{
|
||||
id: vocabQ2,
|
||||
content: {
|
||||
text: "下列词语中,书写正确的是( )。",
|
||||
options: [
|
||||
{ id: "A", text: "漂扬", isCorrect: false },
|
||||
{ id: "B", text: "飘扬", isCorrect: true },
|
||||
{ id: "C", text: "票扬", isCorrect: false }
|
||||
]
|
||||
},
|
||||
type: "single_choice",
|
||||
difficulty: 2,
|
||||
authorId: teacherId,
|
||||
}
|
||||
])
|
||||
|
||||
// 5c. Reading Comprehension Questions
|
||||
const readingQ1 = createId()
|
||||
const readingQ2 = createId()
|
||||
chineseQuestions.push({ id: readingQ1, score: 10 }, { id: readingQ2, score: 10 })
|
||||
|
||||
await db.insert(questions).values([
|
||||
{
|
||||
id: readingQ1,
|
||||
content: {
|
||||
text: "阅读短文《小兔子乖乖》,回答问题:\n\n小兔子乖乖,把门儿开开...\n\n文中提到的动物是?",
|
||||
options: [
|
||||
{ id: "A", text: "大灰狼", isCorrect: false },
|
||||
{ id: "B", text: "小兔子", isCorrect: true },
|
||||
{ id: "C", text: "小花猫", isCorrect: false }
|
||||
]
|
||||
},
|
||||
type: "single_choice",
|
||||
difficulty: 3,
|
||||
authorId: teacherId,
|
||||
},
|
||||
{
|
||||
id: readingQ2,
|
||||
content: { text: "请用一句话形容小兔子。" },
|
||||
type: "text",
|
||||
difficulty: 3,
|
||||
authorId: teacherId,
|
||||
}
|
||||
])
|
||||
|
||||
// 5d. Construct Exam Structure
|
||||
const chineseExamStructure = [
|
||||
{
|
||||
id: createId(),
|
||||
type: "group",
|
||||
title: "第一部分:基础知识",
|
||||
children: [
|
||||
{
|
||||
id: createId(),
|
||||
type: "group",
|
||||
title: "一、看拼音写词语",
|
||||
children: [
|
||||
{ id: createId(), type: "question", questionId: pinyinQ1, score: 5 },
|
||||
{ id: createId(), type: "question", questionId: pinyinQ2, score: 5 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: "group",
|
||||
title: "二、词语积累",
|
||||
children: [
|
||||
{ id: createId(), type: "question", questionId: vocabQ1, score: 5 },
|
||||
{ id: createId(), type: "question", questionId: vocabQ2, score: 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: "group",
|
||||
title: "第二部分:阅读理解",
|
||||
children: [
|
||||
{
|
||||
id: createId(),
|
||||
type: "group",
|
||||
title: "三、短文阅读",
|
||||
children: [
|
||||
{ id: createId(), type: "question", questionId: readingQ1, score: 10 },
|
||||
{ id: createId(), type: "question", questionId: readingQ2, score: 10 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
await db.insert(exams).values({
|
||||
id: chineseExamId,
|
||||
title: "小学语文三年级上册期末考试",
|
||||
description: JSON.stringify({
|
||||
subject: "Chinese",
|
||||
grade: "Grade 3",
|
||||
difficulty: 3,
|
||||
totalScore: 40,
|
||||
durationMin: 90,
|
||||
questionCount: 6,
|
||||
tags: ["期末", "语文", "三年级"]
|
||||
}),
|
||||
structure: chineseExamStructure,
|
||||
creatorId: teacherId,
|
||||
status: "published",
|
||||
startTime: new Date(),
|
||||
})
|
||||
|
||||
// Link questions to exam
|
||||
await db.insert(examQuestions).values(
|
||||
chineseQuestions.map((q, idx) => ({
|
||||
examId: chineseExamId,
|
||||
questionId: q.id,
|
||||
score: q.score,
|
||||
order: idx
|
||||
}))
|
||||
)
|
||||
|
||||
console.log("✅ Seed completed successfully!")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
seed().catch((err) => {
|
||||
console.error("❌ Seed failed:", err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user