2026-02-28 16:41:39 +08:00
|
|
|
|
# 技术选型与项目初始化
|
|
|
|
|
|
|
|
|
|
|
|
> 创建时间:2025-02-04
|
|
|
|
|
|
> 基于之前的设计文档,确定技术栈并搭建项目框架
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、技术选型确认
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 前端技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 技术 | 选择 | 版本 | 说明 |
|
|
|
|
|
|
|-----|------|------|------|
|
|
|
|
|
|
| 框架 | Vue 3 | ^3.4.0 | 渐进式框架,生态完善 |
|
|
|
|
|
|
| 构建工具 | Vite | ^5.0.0 | 快速的开发服务器 |
|
|
|
|
|
|
| UI组件库 | Ant Design Vue | ^4.0.0 | 企业级UI组件 |
|
|
|
|
|
|
| 状态管理 | Pinia | ^2.1.0 | Vue官方推荐 |
|
|
|
|
|
|
| 路由 | Vue Router | ^4.2.0 | 官方路由管理 |
|
|
|
|
|
|
| HTTP客户端 | Axios | ^1.6.0 | Promise-based HTTP |
|
|
|
|
|
|
| 工具库 | Lodash-es | ^4.17.0 | 实用工具函数 |
|
|
|
|
|
|
| 日期处理 | Day.js | ^1.11.0 | 轻量级日期库 |
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 后端技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 技术 | 选择 | 版本 | 说明 |
|
|
|
|
|
|
|-----|------|------|------|
|
|
|
|
|
|
| 框架 | NestJS | ^10.0.0 | Node.js企业级框架 |
|
|
|
|
|
|
| 语言 | TypeScript | ^5.0.0 | 类型安全 |
|
|
|
|
|
|
| ORM | Prisma | ^5.0.0 | 现代化ORM |
|
|
|
|
|
|
| 数据库 | PostgreSQL | ^15.0.0 | 开源关系数据库 |
|
|
|
|
|
|
| 缓存 | Redis | ^7.0.0 | 内存数据库 |
|
|
|
|
|
|
| 认证 | JWT + Passport | - | Token认证 |
|
|
|
|
|
|
| 文件存储 | 阿里云OSS / MinIO | - | 对象存储 |
|
|
|
|
|
|
| 验证 | class-validator | - | 数据验证 |
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 开发工具
|
|
|
|
|
|
|
|
|
|
|
|
| 工具 | 用途 |
|
|
|
|
|
|
|-----|------|
|
|
|
|
|
|
| VS Code | 代码编辑器 |
|
|
|
|
|
|
| Git | 版本控制 |
|
|
|
|
|
|
| Docker | 容器化部署 |
|
|
|
|
|
|
| Postman | API测试 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、项目目录结构
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 整体结构
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
reading-platform/
|
|
|
|
|
|
├── frontend/ # 前端项目(三端统一)
|
|
|
|
|
|
│ ├── src/
|
|
|
|
|
|
│ │ ├── views/
|
|
|
|
|
|
│ │ │ ├── admin/ # 超管端页面
|
|
|
|
|
|
│ │ │ ├── school/ # 学校端页面
|
|
|
|
|
|
│ │ │ ├── teacher/ # 教师端页面
|
|
|
|
|
|
│ │ │ └── auth/ # 认证页面
|
|
|
|
|
|
│ │ ├── components/ # 公共组件
|
|
|
|
|
|
│ │ ├── stores/ # Pinia状态管理
|
|
|
|
|
|
│ │ ├── api/ # API请求
|
|
|
|
|
|
│ │ ├── router/ # 路由配置
|
|
|
|
|
|
│ │ ├── utils/ # 工具函数
|
|
|
|
|
|
│ │ └── types/ # TypeScript类型
|
|
|
|
|
|
│ ├── package.json
|
|
|
|
|
|
│ └── vite.config.ts
|
|
|
|
|
|
│
|
|
|
|
|
|
├── backend/ # 后端项目
|
|
|
|
|
|
│ ├── src/
|
|
|
|
|
|
│ │ ├── modules/
|
|
|
|
|
|
│ │ │ ├── auth/ # 认证模块
|
|
|
|
|
|
│ │ │ ├── admin/ # 超管端模块
|
|
|
|
|
|
│ │ │ ├── school/ # 学校端模块
|
|
|
|
|
|
│ │ │ ├── teacher/ # 教师端模块
|
|
|
|
|
|
│ │ │ ├── course/ # 课程包模块
|
|
|
|
|
|
│ │ │ ├── tenant/ # 租户模块
|
|
|
|
|
|
│ │ │ ├── common/ # 公共模块
|
|
|
|
|
|
│ │ │ └── upload/ # 文件上传模块
|
|
|
|
|
|
│ │ ├── common/
|
|
|
|
|
|
│ │ │ ├── decorators/ # 装饰器
|
|
|
|
|
|
│ │ │ ├── guards/ # 守卫
|
|
|
|
|
|
│ │ │ ├── filters/ # 过滤器
|
|
|
|
|
|
│ │ │ └── interceptors/ # 拦截器
|
|
|
|
|
|
│ │ ├── config/ # 配置
|
|
|
|
|
|
│ │ └── database/ # 数据库相关
|
|
|
|
|
|
│ ├── prisma/
|
|
|
|
|
|
│ │ └── schema.prisma # Prisma模型
|
|
|
|
|
|
│ ├── package.json
|
|
|
|
|
|
│ └── nest-cli.json
|
|
|
|
|
|
│
|
|
|
|
|
|
├── docs/ # 文档
|
|
|
|
|
|
│ └── ... # 已有的设计文档
|
|
|
|
|
|
│
|
|
|
|
|
|
├── docker-compose.yml # Docker编排
|
|
|
|
|
|
└── README.md
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、数据库模型(Prisma Schema)
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 核心模型定义
|
|
|
|
|
|
|
|
|
|
|
|
```prisma
|
|
|
|
|
|
// prisma/schema.prisma
|
|
|
|
|
|
|
|
|
|
|
|
generator client {
|
|
|
|
|
|
provider = "prisma-client-js"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
datasource db {
|
|
|
|
|
|
provider = "postgresql"
|
|
|
|
|
|
url = env("DATABASE_URL")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 租户相关 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Tenant {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
name String
|
|
|
|
|
|
address String?
|
|
|
|
|
|
contactPerson String? @map("contact_person")
|
|
|
|
|
|
contactPhone String? @map("contact_phone")
|
|
|
|
|
|
logoUrl String? @map("logo_url")
|
|
|
|
|
|
|
|
|
|
|
|
packageType PackageType @map("package_type")
|
|
|
|
|
|
teacherQuota Int @default(20) @map("teacher_quota")
|
|
|
|
|
|
studentQuota Int @default(200) @map("student_quota")
|
|
|
|
|
|
storageQuota BigInt @default(5368709120) @map("storage_quota") // 5GB
|
|
|
|
|
|
|
|
|
|
|
|
startDate DateTime @map("start_date") @db.Date
|
|
|
|
|
|
expireDate DateTime @map("expire_date") @db.Date
|
|
|
|
|
|
|
|
|
|
|
|
teacherCount Int @default(0) @map("teacher_count")
|
|
|
|
|
|
studentCount Int @default(0) @map("student_count")
|
|
|
|
|
|
storageUsed BigInt @default(0) @map("storage_used")
|
|
|
|
|
|
|
|
|
|
|
|
status TenantStatus @default(ACTIVE)
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
teachers Teacher[]
|
|
|
|
|
|
students Student[]
|
|
|
|
|
|
classes Class[]
|
|
|
|
|
|
lessons Lesson[]
|
|
|
|
|
|
tenantCourses TenantCourse[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum PackageType {
|
|
|
|
|
|
BASIC
|
|
|
|
|
|
STANDARD
|
|
|
|
|
|
ADVANCED
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum TenantStatus {
|
|
|
|
|
|
ACTIVE
|
|
|
|
|
|
SUSPENDED
|
|
|
|
|
|
EXPIRED
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 教师相关 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Teacher {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
tenantId BigInt @map("tenant_id")
|
|
|
|
|
|
name String
|
|
|
|
|
|
phone String
|
|
|
|
|
|
email String?
|
|
|
|
|
|
|
|
|
|
|
|
loginAccount String @unique @map("login_account")
|
|
|
|
|
|
passwordHash String @map("password_hash")
|
|
|
|
|
|
classIds Json? @map("class_ids") // 存储负责的班级ID列表
|
|
|
|
|
|
|
|
|
|
|
|
status TeacherStatus @default(ACTIVE)
|
|
|
|
|
|
|
|
|
|
|
|
lessonCount Int @default(0) @map("lesson_count")
|
|
|
|
|
|
feedbackCount Int @default(0) @map("feedback_count")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
|
|
|
|
|
|
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
lessons Lesson[]
|
|
|
|
|
|
feedbacks LessonFeedback[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("teachers")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum TeacherStatus {
|
|
|
|
|
|
ACTIVE
|
|
|
|
|
|
SUSPENDED
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 班级相关 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Class {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
tenantId BigInt @map("tenant_id")
|
|
|
|
|
|
name String
|
|
|
|
|
|
grade Grade
|
|
|
|
|
|
|
|
|
|
|
|
teacherId BigInt? @map("teacher_id")
|
|
|
|
|
|
|
|
|
|
|
|
studentCount Int @default(0) @map("student_count")
|
|
|
|
|
|
lessonCount Int @default(0) @map("lesson_count")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
teacher Teacher? @relation(fields: [teacherId], references: [id])
|
|
|
|
|
|
students Student[]
|
|
|
|
|
|
lessons Lesson[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("classes")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum Grade {
|
|
|
|
|
|
SMALL // 小班
|
|
|
|
|
|
MIDDLE // 中班
|
|
|
|
|
|
BIG // 大班
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 学生相关 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Student {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
tenantId BigInt @map("tenant_id")
|
|
|
|
|
|
classId BigInt @map("class_id")
|
|
|
|
|
|
name String
|
|
|
|
|
|
gender Gender?
|
|
|
|
|
|
birthDate DateTime? @map("birth_date") @db.Date
|
|
|
|
|
|
parentPhone String? @map("parent_phone")
|
|
|
|
|
|
parentName String? @map("parent_name")
|
|
|
|
|
|
|
|
|
|
|
|
readingCount Int @default(0) @map("reading_count")
|
|
|
|
|
|
lessonCount Int @default(0) @map("lesson_count")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
records StudentRecord[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("students")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum Gender {
|
|
|
|
|
|
MALE
|
|
|
|
|
|
FEMALE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 课程包相关 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Course {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
name String
|
|
|
|
|
|
description String? @db.Text
|
|
|
|
|
|
|
|
|
|
|
|
pictureBookId BigInt? @map("picture_book_id")
|
|
|
|
|
|
pictureBookName String? @map("picture_book_name")
|
|
|
|
|
|
|
|
|
|
|
|
gradeTags Json @map("grade_tags") // ["small", "middle"]
|
|
|
|
|
|
domainTags Json @map("domain_tags") // [{"level1":"语言", "level2":"阅读与书写准备"}]
|
|
|
|
|
|
|
|
|
|
|
|
duration Int @default(25) // 分钟
|
|
|
|
|
|
|
|
|
|
|
|
status CourseStatus @default(DRAFT)
|
|
|
|
|
|
version String @default("1.0")
|
|
|
|
|
|
|
|
|
|
|
|
usageCount Int @default(0) @map("usage_count")
|
|
|
|
|
|
teacherCount Int @default(0) @map("teacher_count")
|
|
|
|
|
|
avgRating Decimal @default(0) @map("avg_rating") @db.Decimal(3, 2)
|
|
|
|
|
|
|
|
|
|
|
|
createdBy BigInt? @map("created_by")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
publishedAt DateTime? @map("published_at")
|
|
|
|
|
|
|
|
|
|
|
|
resources CourseResource[]
|
|
|
|
|
|
scripts CourseScript[]
|
|
|
|
|
|
activities CourseActivity[]
|
|
|
|
|
|
lessons Lesson[]
|
|
|
|
|
|
tenantCourses TenantCourse[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("courses")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum CourseStatus {
|
|
|
|
|
|
DRAFT
|
|
|
|
|
|
REVIEWING
|
|
|
|
|
|
PUBLISHED
|
|
|
|
|
|
ARCHIVED
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 课程资源 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model CourseResource {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
courseId BigInt @map("course_id")
|
|
|
|
|
|
resourceType ResourceType @map("resource_type")
|
|
|
|
|
|
resourceName String @map("resource_name")
|
|
|
|
|
|
fileUrl String @map("file_url")
|
|
|
|
|
|
fileSize BigInt? @map("file_size")
|
|
|
|
|
|
mimeType String? @map("mime_type")
|
|
|
|
|
|
metadata Json? // 其他元数据(时长、分辨率等)
|
|
|
|
|
|
|
|
|
|
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
|
|
|
|
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
|
|
|
|
|
|
@@map("course_resources")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum ResourceType {
|
|
|
|
|
|
EBOOK
|
|
|
|
|
|
AUDIO
|
|
|
|
|
|
VIDEO
|
|
|
|
|
|
PPT
|
|
|
|
|
|
ANIMATION
|
|
|
|
|
|
INTERACTIVE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 课程脚本 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model CourseScript {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
courseId BigInt @map("course_id")
|
|
|
|
|
|
stepIndex Int @map("step_index")
|
|
|
|
|
|
stepName String @map("step_name")
|
|
|
|
|
|
stepType StepType @map("step_type")
|
|
|
|
|
|
|
|
|
|
|
|
duration Int // 分钟
|
|
|
|
|
|
objective String? @db.Text
|
|
|
|
|
|
teacherScript String? @map("teacher_script") @db.Text
|
|
|
|
|
|
interactionPoints Json? @map("interaction_points")
|
|
|
|
|
|
|
|
|
|
|
|
resourceIds Json? @map("resource_ids") // 关联的资源ID列表
|
|
|
|
|
|
|
|
|
|
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
pages CourseScriptPage[]
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([courseId, stepIndex])
|
|
|
|
|
|
@@map("course_scripts")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum StepType {
|
|
|
|
|
|
INTRO // 导入
|
|
|
|
|
|
GUIDE // 引导观察
|
|
|
|
|
|
READING // 师幼共读
|
|
|
|
|
|
ACTIVITY // 延伸活动
|
|
|
|
|
|
ENDING // 活动结束
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 逐页配置 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model CourseScriptPage {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
scriptId BigInt @map("script_id")
|
|
|
|
|
|
pageNumber Int @map("page_number")
|
|
|
|
|
|
questions Json? // 提问列表
|
|
|
|
|
|
interactionComponent Json? @map("interaction_component")
|
|
|
|
|
|
teacherNotes String? @map("teacher_notes") @db.Text
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
script CourseScript @relation(fields: [scriptId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([scriptId, pageNumber])
|
|
|
|
|
|
@@map("course_script_pages")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 延伸活动 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model CourseActivity {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
courseId BigInt @map("course_id")
|
|
|
|
|
|
name String
|
|
|
|
|
|
|
|
|
|
|
|
domain String? // 所属领域
|
|
|
|
|
|
domainTagId BigInt? @map("domain_tag_id")
|
|
|
|
|
|
activityType ActivityType @map("activity_type")
|
|
|
|
|
|
duration Int? // 分钟
|
|
|
|
|
|
|
|
|
|
|
|
onlineMaterials Json? @map("online_materials")
|
|
|
|
|
|
offlineMaterials String? @map("offline_materials") @db.Text
|
|
|
|
|
|
activityGuide String? @map("activity_guide") @db.Text
|
|
|
|
|
|
objectives String? @db.Text
|
|
|
|
|
|
|
|
|
|
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
|
|
|
|
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
|
|
|
|
|
|
@@map("course_activities")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum ActivityType {
|
|
|
|
|
|
HANDS_ON // 动手操作
|
|
|
|
|
|
COMMUNICATION // 交流讨论
|
|
|
|
|
|
GAME // 游戏互动
|
|
|
|
|
|
OTHER
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 授课记录 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Lesson {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
tenantId BigInt @map("tenant_id")
|
|
|
|
|
|
teacherId BigInt @map("teacher_id")
|
|
|
|
|
|
classId BigInt @map("class_id")
|
|
|
|
|
|
courseId BigInt @map("course_id")
|
|
|
|
|
|
|
|
|
|
|
|
plannedDatetime DateTime? @map("planned_datetime")
|
|
|
|
|
|
startDatetime DateTime? @map("start_datetime")
|
|
|
|
|
|
endDatetime DateTime? @map("end_datetime")
|
|
|
|
|
|
actualDuration Int? @map("actual_duration") // 分钟
|
|
|
|
|
|
|
|
|
|
|
|
status LessonStatus @default(PLANNED)
|
|
|
|
|
|
|
|
|
|
|
|
overallRating OverallRating? @map("overall_rating")
|
|
|
|
|
|
participationRating ParticipationRating? @map("participation_rating")
|
|
|
|
|
|
completionNote CompletionNote? @map("completion_note")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
|
|
|
|
teacher Teacher @relation(fields: [teacherId], references: [id])
|
|
|
|
|
|
class Class @relation(fields: [classId], references: [id])
|
|
|
|
|
|
course Course @relation(fields: [courseId], references: [id])
|
|
|
|
|
|
feedbacks LessonFeedback[]
|
|
|
|
|
|
records StudentRecord[]
|
|
|
|
|
|
|
|
|
|
|
|
@@map("lessons")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum LessonStatus {
|
|
|
|
|
|
PLANNED
|
|
|
|
|
|
IN_PROGRESS
|
|
|
|
|
|
COMPLETED
|
|
|
|
|
|
CANCELLED
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum OverallRating {
|
|
|
|
|
|
EXCELLENT
|
|
|
|
|
|
GOOD
|
|
|
|
|
|
AVERAGE
|
|
|
|
|
|
NEEDS_IMPROVEMENT
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum ParticipationRating {
|
|
|
|
|
|
VERY_HIGH
|
|
|
|
|
|
HIGH
|
|
|
|
|
|
MEDIUM
|
|
|
|
|
|
LOW
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum CompletionNote {
|
|
|
|
|
|
ALL_COMPLETED
|
|
|
|
|
|
ADJUSTED
|
|
|
|
|
|
PARTIAL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 课程反馈 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model LessonFeedback {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
lessonId BigInt @map("lesson_id")
|
|
|
|
|
|
teacherId BigInt @map("teacher_id")
|
|
|
|
|
|
|
|
|
|
|
|
designQuality Int? @map("design_quality") // 1-5
|
|
|
|
|
|
participation Int? // 1-5
|
|
|
|
|
|
goalAchievement Int? @map("goal_achievement") // 1-5
|
|
|
|
|
|
|
|
|
|
|
|
stepFeedbacks Json? @map("step_feedbacks")
|
|
|
|
|
|
|
|
|
|
|
|
pros String? @db.Text
|
|
|
|
|
|
suggestions String? @db.Text
|
|
|
|
|
|
|
|
|
|
|
|
activitiesDone Json? @map("activities_done")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
teacher Teacher @relation(fields: [teacherId], references: [id])
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([lessonId, teacherId])
|
|
|
|
|
|
@@map("lesson_feedbacks")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 学生测评记录 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model StudentRecord {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
lessonId BigInt @map("lesson_id")
|
|
|
|
|
|
studentId BigInt @map("student_id")
|
|
|
|
|
|
|
|
|
|
|
|
focus Int? // 1-5
|
|
|
|
|
|
participation Int?
|
|
|
|
|
|
interest Int?
|
|
|
|
|
|
understanding Int?
|
|
|
|
|
|
|
|
|
|
|
|
domainAchievements Json? @map("domain_achievements")
|
|
|
|
|
|
|
|
|
|
|
|
notes String? @db.Text
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
student Student @relation(fields: [studentId], references: [id])
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([lessonId, studentId])
|
|
|
|
|
|
@@map("student_records")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 标签体系 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model Tag {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
level Int // 1-4
|
|
|
|
|
|
code String @unique
|
|
|
|
|
|
name String
|
|
|
|
|
|
parentId BigInt? @map("parent_id")
|
|
|
|
|
|
|
|
|
|
|
|
description String? @db.Text
|
|
|
|
|
|
metadata Json?
|
|
|
|
|
|
|
|
|
|
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
|
|
|
|
|
|
@@index([parentId])
|
|
|
|
|
|
@@index([level])
|
|
|
|
|
|
@@map("tags")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 租户课程授权 ====================
|
|
|
|
|
|
|
|
|
|
|
|
model TenantCourse {
|
|
|
|
|
|
id BigInt @id @default(autoincrement())
|
|
|
|
|
|
tenantId BigInt @map("tenant_id")
|
|
|
|
|
|
courseId BigInt @map("course_id")
|
|
|
|
|
|
|
|
|
|
|
|
authorized Boolean @default(true)
|
|
|
|
|
|
authorizedAt DateTime? @map("authorized_at")
|
|
|
|
|
|
|
|
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
|
|
|
|
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([tenantId, courseId])
|
|
|
|
|
|
@@map("tenant_courses")
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、后端核心模块
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 项目初始化
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 创建后端项目
|
|
|
|
|
|
mkdir reading-platform-backend
|
|
|
|
|
|
cd reading-platform-backend
|
|
|
|
|
|
npm init -y
|
|
|
|
|
|
|
|
|
|
|
|
# 安装依赖
|
|
|
|
|
|
npm install @nestjs/common@^10.0.0 @nestjs/core@^10.0.0 @nestjs/platform-express@^10.0.0
|
|
|
|
|
|
npm install @nestjs/config@^3.0.0 @nestjs/jwt@^10.0.0 @nestjs/passport@^10.0.0
|
|
|
|
|
|
npm install @prisma/client@^5.0.0 passport@^0.6.0 passport-jwt@^4.0.0
|
|
|
|
|
|
npm install class-validator@^0.14.0 class-transformer@^0.5.1
|
|
|
|
|
|
npm install bcrypt@^5.1.0
|
|
|
|
|
|
npm install @nestjs/throttler@^5.0.0 # 限流
|
|
|
|
|
|
npm install -D @nestjs/cli@^10.0.0
|
|
|
|
|
|
npm install -D prisma@^5.0.0
|
|
|
|
|
|
npm install -D typescript@^5.0.0 @types/node@^20.0.0 @types/passport-jwt@^4.0.0 @types/bcrypt@^5.0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 安装额外依赖
|
|
|
|
|
|
npm install @nestjs/swagger@^7.0.0 # API文档
|
|
|
|
|
|
npm install rxjs@^7.8.0
|
|
|
|
|
|
npm install reflect-metadata@^0.1.0
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 NestJS主模块
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// src/main.ts
|
|
|
|
|
|
import { NestFactory } from '@nestjs/core';
|
|
|
|
|
|
import { ValidationPipe } from '@nestjs/common';
|
|
|
|
|
|
import { AppModule } from './app.module';
|
|
|
|
|
|
|
|
|
|
|
|
async function bootstrap() {
|
|
|
|
|
|
const app = await NestFactory.create(AppModule);
|
|
|
|
|
|
|
|
|
|
|
|
// 全局验证管道
|
|
|
|
|
|
app.useGlobalPipes(
|
|
|
|
|
|
new ValidationPipe({
|
|
|
|
|
|
whitelist: true,
|
|
|
|
|
|
forbidNonWhitelisted: true,
|
|
|
|
|
|
transform: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// CORS
|
|
|
|
|
|
app.enableCors({
|
|
|
|
|
|
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
|
|
|
|
|
credentials: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// API前缀
|
|
|
|
|
|
app.setGlobalPrefix('api/v1');
|
|
|
|
|
|
|
|
|
|
|
|
const port = process.env.PORT || 3000;
|
|
|
|
|
|
await app.listen(port);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🚀 Application is running on: http://localhost:${port}`);
|
|
|
|
|
|
console.log(`📚 API documentation: http://localhost:${port}/api/docs`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bootstrap();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// src/app.module.ts
|
|
|
|
|
|
import { Module } from '@nestjs/common';
|
|
|
|
|
|
import { ConfigModule } from '@nestjs/config';
|
|
|
|
|
|
import { ThrottlerModule } from '@nestjs/throttler';
|
|
|
|
|
|
import { PrismaModule } from './database/prisma.module';
|
|
|
|
|
|
import { AuthModule } from './modules/auth/auth.module';
|
|
|
|
|
|
import { AdminModule } from './modules/admin/admin.module';
|
|
|
|
|
|
import { SchoolModule } from './modules/school/school.module';
|
|
|
|
|
|
import { TeacherModule } from './modules/teacher/teacher.module';
|
|
|
|
|
|
import { CourseModule } from './modules/course/course.module';
|
|
|
|
|
|
import { TenantModule } from './modules/tenant/tenant.module';
|
|
|
|
|
|
import { CommonModule } from './modules/common/common.module';
|
|
|
|
|
|
import { UploadModule } from './modules/upload/upload.module';
|
|
|
|
|
|
|
|
|
|
|
|
@Module({
|
|
|
|
|
|
imports: [
|
|
|
|
|
|
ConfigModule.forRoot({
|
|
|
|
|
|
isGlobal: true,
|
|
|
|
|
|
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
|
|
|
|
|
|
}),
|
|
|
|
|
|
ThrottlerModule.forRoot([{
|
|
|
|
|
|
ttl: 60000, // 60秒
|
|
|
|
|
|
limit: 100, // 最多100个请求
|
|
|
|
|
|
}]),
|
|
|
|
|
|
PrismaModule,
|
|
|
|
|
|
AuthModule,
|
|
|
|
|
|
AdminModule,
|
|
|
|
|
|
SchoolModule,
|
|
|
|
|
|
TeacherModule,
|
|
|
|
|
|
CourseModule,
|
|
|
|
|
|
TenantModule,
|
|
|
|
|
|
CommonModule,
|
|
|
|
|
|
UploadModule,
|
|
|
|
|
|
],
|
|
|
|
|
|
})
|
|
|
|
|
|
export class AppModule {}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 五、前端项目初始化
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 项目创建
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 使用Vite创建Vue3项目
|
|
|
|
|
|
npm create vite@latest reading-platform-frontend -- --template vue-ts
|
|
|
|
|
|
cd reading-platform-frontend
|
|
|
|
|
|
|
|
|
|
|
|
# 安装依赖
|
|
|
|
|
|
npm install
|
|
|
|
|
|
npm install vue-router@^4.2.0 pinia@^2.1.0
|
|
|
|
|
|
npm install ant-design-vue@^4.0.0
|
|
|
|
|
|
npm install axios@^1.6.0 dayjs@^1.11.0 lodash-es@^4.17.0
|
|
|
|
|
|
npm install @ant-design/icons-vue@^7.0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 开发依赖
|
|
|
|
|
|
npm install -D @types/lodash-es@^4.17.0 @types/node@^20.0.0
|
|
|
|
|
|
npm install -D unplugin-vue-components@^0.26.0 # 组件自动导入
|
|
|
|
|
|
npm install -D unplugin-auto-import@^0.17.0 # API自动导入
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 Vite配置
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// vite.config.ts
|
|
|
|
|
|
import { defineConfig } from 'vite';
|
|
|
|
|
|
import vue from '@vitejs/plugin-vue';
|
|
|
|
|
|
import { resolve } from 'path';
|
|
|
|
|
|
import AutoImport from 'unplugin-auto-import/vite';
|
|
|
|
|
|
import Components from 'unplugin-vue-components/vite';
|
|
|
|
|
|
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
|
|
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
|
plugins: [
|
|
|
|
|
|
vue(),
|
|
|
|
|
|
AutoImport({
|
|
|
|
|
|
imports: [
|
|
|
|
|
|
'vue',
|
|
|
|
|
|
'vue-router',
|
|
|
|
|
|
'pinia',
|
|
|
|
|
|
{
|
|
|
|
|
|
'ant-design-vue': [
|
|
|
|
|
|
'message',
|
|
|
|
|
|
'notification',
|
|
|
|
|
|
'Modal',
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
dts: 'src/auto-imports.d.ts',
|
|
|
|
|
|
}),
|
|
|
|
|
|
Components({
|
|
|
|
|
|
resolvers: [
|
|
|
|
|
|
AntDesignVueResolver({
|
|
|
|
|
|
importStyle: false,
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
dts: 'src/components.d.ts',
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
resolve: {
|
|
|
|
|
|
alias: {
|
|
|
|
|
|
'@': resolve(__dirname, 'src'),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
server: {
|
|
|
|
|
|
port: 5173,
|
|
|
|
|
|
proxy: {
|
|
|
|
|
|
'/api': {
|
|
|
|
|
|
target: 'http://localhost:3000',
|
|
|
|
|
|
changeOrigin: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 路由配置
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// src/router/index.ts
|
|
|
|
|
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
|
|
|
|
|
|
|
|
|
|
|
const routes: RouteRecordRaw[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/login',
|
|
|
|
|
|
name: 'Login',
|
|
|
|
|
|
component: () => import('@/views/auth/LoginView.vue'),
|
|
|
|
|
|
meta: { requiresAuth: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/admin',
|
|
|
|
|
|
name: 'Admin',
|
|
|
|
|
|
component: () => import('@/views/admin/LayoutView.vue'),
|
|
|
|
|
|
meta: { requiresAuth: true, role: 'admin' },
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '',
|
|
|
|
|
|
redirect: '/admin/dashboard',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'dashboard',
|
|
|
|
|
|
name: 'AdminDashboard',
|
|
|
|
|
|
component: () => import('@/views/admin/DashboardView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'courses',
|
|
|
|
|
|
name: 'AdminCourses',
|
|
|
|
|
|
component: () => import('@/views/admin/courses/CourseListView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
// ... 其他超管端路由
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/school',
|
|
|
|
|
|
name: 'School',
|
|
|
|
|
|
component: () => import('@/views/school/LayoutView.vue'),
|
|
|
|
|
|
meta: { requiresAuth: true, role: 'school' },
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '',
|
|
|
|
|
|
redirect: '/school/dashboard',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'dashboard',
|
|
|
|
|
|
name: 'SchoolDashboard',
|
|
|
|
|
|
component: () => import('@/views/school/DashboardView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
// ... 其他学校端路由
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/teacher',
|
|
|
|
|
|
name: 'Teacher',
|
|
|
|
|
|
component: () => import('@/views/teacher/LayoutView.vue'),
|
|
|
|
|
|
meta: { requiresAuth: true, role: 'teacher' },
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '',
|
|
|
|
|
|
redirect: '/teacher/dashboard',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'dashboard',
|
|
|
|
|
|
name: 'TeacherDashboard',
|
|
|
|
|
|
component: () => import('@/views/teacher/DashboardView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'courses',
|
|
|
|
|
|
name: 'TeacherCourses',
|
|
|
|
|
|
component: () => import('@/views/teacher/courses/CourseListView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'courses/:id',
|
|
|
|
|
|
name: 'TeacherCourseDetail',
|
|
|
|
|
|
component: () => import('@/views/teacher/courses/CourseDetailView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'courses/:id/prepare',
|
|
|
|
|
|
name: 'TeacherCoursePrepare',
|
|
|
|
|
|
component: () => import('@/views/teacher/courses/PrepareModeView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'lessons/:id',
|
|
|
|
|
|
name: 'TeacherLesson',
|
|
|
|
|
|
component: () => import('@/views/teacher/lessons/LessonView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
// ... 其他教师端路由
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/:pathMatch(.*)*',
|
|
|
|
|
|
name: 'NotFound',
|
|
|
|
|
|
component: () => import('@/views/NotFoundView.vue'),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
|
routes,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 路由守卫
|
|
|
|
|
|
router.beforeEach((to, from, next) => {
|
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
|
|
|
|
|
|
|
if (to.meta.requiresAuth && !token) {
|
|
|
|
|
|
next('/login');
|
|
|
|
|
|
} else if (to.path === '/login' && token) {
|
|
|
|
|
|
// 根据用户角色跳转
|
|
|
|
|
|
const role = localStorage.getItem('role');
|
|
|
|
|
|
next(`/${role}/dashboard`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default router;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 六、Docker配置
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 docker-compose.yml
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
version: '3.8'
|
|
|
|
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
|
postgres:
|
|
|
|
|
|
image: postgres:15-alpine
|
|
|
|
|
|
container_name: reading-platform-db
|
|
|
|
|
|
environment:
|
|
|
|
|
|
POSTGRES_USER: admin
|
|
|
|
|
|
POSTGRES_PASSWORD: password
|
|
|
|
|
|
POSTGRES_DB: reading_platform
|
|
|
|
|
|
ports:
|
|
|
|
|
|
- "5432:5432"
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
- postgres_data:/var/lib/postgresql/data
|
|
|
|
|
|
|
|
|
|
|
|
redis:
|
|
|
|
|
|
image: redis:7-alpine
|
|
|
|
|
|
container_name: reading-platform-redis
|
|
|
|
|
|
ports:
|
|
|
|
|
|
- "6379:6379"
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
- redis_data:/data
|
|
|
|
|
|
|
|
|
|
|
|
backend:
|
|
|
|
|
|
build:
|
|
|
|
|
|
context: ./backend
|
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
|
container_name: reading-platform-backend
|
|
|
|
|
|
environment:
|
|
|
|
|
|
NODE_ENV: development
|
|
|
|
|
|
DATABASE_URL: postgresql://admin:password@postgres:5432/reading_platform
|
|
|
|
|
|
REDIS_URL: redis://redis:6379
|
|
|
|
|
|
JWT_SECRET: your-secret-key
|
|
|
|
|
|
ports:
|
|
|
|
|
|
- "3000:3000"
|
|
|
|
|
|
depends_on:
|
|
|
|
|
|
- postgres
|
|
|
|
|
|
- redis
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
- ./backend:/app
|
|
|
|
|
|
- /app/node_modules
|
|
|
|
|
|
command: npm run start:dev
|
|
|
|
|
|
|
|
|
|
|
|
frontend:
|
|
|
|
|
|
build:
|
|
|
|
|
|
context: ./frontend
|
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
|
container_name: reading-platform-frontend
|
|
|
|
|
|
ports:
|
|
|
|
|
|
- "5173:5173"
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
- ./frontend:/app
|
|
|
|
|
|
- /app/node_modules
|
|
|
|
|
|
command: npm run dev
|
|
|
|
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
postgres_data:
|
|
|
|
|
|
redis_data:
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 七、环境变量配置
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 后端环境变量
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# backend/.env.development
|
|
|
|
|
|
NODE_ENV=development
|
|
|
|
|
|
PORT=3000
|
|
|
|
|
|
|
|
|
|
|
|
# 数据库
|
|
|
|
|
|
DATABASE_URL="postgresql://admin:password@localhost:5432/reading_platform"
|
|
|
|
|
|
|
|
|
|
|
|
# Redis
|
|
|
|
|
|
REDIS_URL="redis://localhost:6379"
|
|
|
|
|
|
|
|
|
|
|
|
# JWT
|
|
|
|
|
|
JWT_SECRET="your-super-secret-jwt-key-change-in-production"
|
|
|
|
|
|
JWT_EXPIRES_IN="7d"
|
|
|
|
|
|
|
|
|
|
|
|
# 文件上传
|
|
|
|
|
|
UPLOAD_DIR="./uploads"
|
|
|
|
|
|
MAX_FILE_SIZE=104857600 # 100MB
|
|
|
|
|
|
|
|
|
|
|
|
# OSS (阿里云)
|
|
|
|
|
|
OSS_REGION="oss-cn-hangzhou"
|
|
|
|
|
|
OSS_ACCESS_KEY_ID="your-access-key"
|
|
|
|
|
|
OSS_ACCESS_KEY_SECRET="your-secret"
|
|
|
|
|
|
OSS_BUCKET="reading-platform"
|
|
|
|
|
|
|
|
|
|
|
|
# 前端URL
|
|
|
|
|
|
FRONTEND_URL="http://localhost:5173"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 7.2 前端环境变量
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# frontend/.env.development
|
|
|
|
|
|
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
2026-03-26 12:02:20 +08:00
|
|
|
|
VITE_APP_TITLE=少儿智慧阅读
|
2026-02-28 16:41:39 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 八、下一步开发计划
|
|
|
|
|
|
|
|
|
|
|
|
> **更新时间**: 2026-02-13
|
|
|
|
|
|
> **项目状态**: 开发中 - 超管端课程包核心功能开发中
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 1: 基础框架(第1-2周)✅ 已完成
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 技术选型确认
|
|
|
|
|
|
- [x] 项目初始化
|
|
|
|
|
|
- [x] 数据库设计与迁移
|
|
|
|
|
|
- [x] 认证系统实现
|
|
|
|
|
|
- [x] 权限管理实现
|
|
|
|
|
|
- [x] 标签体系初始化
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 2: 超管端核心(第3-6周)🔄 进行中
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 课程包制作工作台
|
|
|
|
|
|
- [x] 课程包管理(草稿/发布/审核)
|
|
|
|
|
|
- [x] 课程包审核流程
|
|
|
|
|
|
- [x] 租户管理(基础功能)
|
|
|
|
|
|
- [ ] 资源库管理(第二阶段开发)
|
|
|
|
|
|
- [ ] 数据统计看板
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 3: 教师端核心(第7-10周)🔄 进行中
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 课程中心
|
|
|
|
|
|
- [x] 备课模式
|
|
|
|
|
|
- [x] 上课模式
|
|
|
|
|
|
- [ ] 班级管理
|
|
|
|
|
|
- [ ] 反馈记录
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 4: 学校端(第11-12周)🔄 框架完成
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 教师管理
|
|
|
|
|
|
- [ ] 学生管理
|
|
|
|
|
|
- [ ] 课程授权查看
|
|
|
|
|
|
- [ ] 数据报表
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 5: 反馈与数据(第13-14周)⬜ 待开发
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 教师反馈收集
|
|
|
|
|
|
- [ ] 测评记录
|
|
|
|
|
|
- [ ] 数据统计优化
|
|
|
|
|
|
- [ ] 性能优化
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 九、资源库开发策略(2026-02-13 确认)
|
|
|
|
|
|
|
|
|
|
|
|
根据MVP优先级,资源库功能采用三阶段开发策略:
|
|
|
|
|
|
|
|
|
|
|
|
| 阶段 | 内容 | 状态 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| **第一阶段** | 完成课程包核心功能,关联绘本使用文本输入 | ✅ 进行中 |
|
|
|
|
|
|
| **第二阶段** | 开发资源库完整管理功能 | ⬜ 待开发 |
|
|
|
|
|
|
| **第三阶段** | 重构资源选择逻辑,整合课程包与资源库 | ⬜ 待开发 |
|
|
|
|
|
|
|
|
|
|
|
|
详见 `开发进展记录.md` 资源库开发策略部分。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
现在让我开始创建具体的项目文件...
|