# 技术选型与项目初始化

> 创建时间：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
VITE_APP_TITLE=少儿智慧阅读
```

---

## 八、下一步开发计划

> **更新时间**: 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` 资源库开发策略部分。

---

现在让我开始创建具体的项目文件...
