引言

微课堂(MicroClass)是一个广泛使用的在线教育平台,其1.4.9版本是一个稳定且功能丰富的版本。本文将深入解析该版本的源码结构,并提供实战应用指南,帮助开发者理解其核心机制并进行二次开发或定制化部署。

1. 源码结构解析

1.1 项目目录概览

微课堂1.4.9版本的源码通常包含以下主要目录:

microclass/
├── app/                # 应用核心代码
│   ├── controllers/    # 控制器层
│   ├── models/         # 数据模型层
│   ├── views/          # 视图层
│   └── services/       # 业务逻辑层
├── config/             # 配置文件
├── public/             # 静态资源(CSS、JS、图片)
├── database/           # 数据库迁移和种子文件
├── tests/              # 测试用例
├── vendor/             # 依赖库(Composer)
├── .env                # 环境配置文件
├── composer.json       # PHP依赖管理
└── package.json        # 前端依赖管理(如果使用Node.js)

1.2 核心文件详解

1.2.1 控制器(Controllers)

控制器处理用户请求,调用模型和视图。例如,CourseController.php 负责课程相关操作:

<?php
// app/controllers/CourseController.php

namespace App\Controllers;

use App\Models\Course;
use App\Services\CourseService;

class CourseController extends BaseController
{
    public function show($id)
    {
        // 从模型获取课程数据
        $course = Course::find($id);
        
        // 使用服务层处理业务逻辑
        $courseService = new CourseService();
        $courseData = $courseService->formatCourseData($course);
        
        // 返回视图
        return view('courses.show', ['course' => $courseData]);
    }
    
    public function create()
    {
        // 验证用户权限
        if (!auth()->user()->can('create_course')) {
            return redirect()->back()->with('error', '无权限创建课程');
        }
        
        // 处理表单提交
        if (request()->isMethod('post')) {
            $data = request()->all();
            
            // 验证数据
            $validated = $this->validate($data, [
                'title' => 'required|string|max:255',
                'description' => 'required|string',
                'price' => 'numeric|min:0'
            ]);
            
            // 创建课程
            $course = Course::create($validated);
            
            // 重定向到课程详情页
            return redirect()->route('courses.show', $course->id)
                           ->with('success', '课程创建成功');
        }
        
        return view('courses.create');
    }
}

1.2.2 数据模型(Models)

模型定义数据结构和业务规则。例如,Course.php 模型:

<?php
// app/models/Course.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Course extends Model
{
    use SoftDeletes; // 软删除
    
    // 可批量赋值的字段
    protected $fillable = [
        'title', 'description', 'price', 'user_id', 'status'
    ];
    
    // 日期字段
    protected $dates = ['deleted_at'];
    
    // 关联用户(创建者)
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    // 关联课程章节
    public function chapters()
    {
        return $this->hasMany(Chapter::class)->orderBy('order');
    }
    
    // 关联课程评论
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
    
    // 获取已购买该课程的用户
    public function enrolledUsers()
    {
        return $this->belongsToMany(User::class, 'enrollments')
                    ->withPivot('enrolled_at', 'expires_at');
    }
    
    // 作用域:获取已发布的课程
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }
    
    // 访问器:获取格式化价格
    public function getFormattedPriceAttribute()
    {
        return number_format($this->price, 2) . '元';
    }
    
    // 修改器:设置价格时自动保留两位小数
    public function setPriceAttribute($value)
    {
        $this->attributes['price'] = round($value, 2);
    }
}

1.2.3 服务层(Services)

服务层封装复杂业务逻辑,使控制器保持简洁。例如,CourseService.php

<?php
// app/services/CourseService.php

namespace App\Services;

use App\Models\Course;
use App\Models\Enrollment;
use App\Models\User;

class CourseService
{
    /**
     * 格式化课程数据,用于前端展示
     */
    public function formatCourseData(Course $course)
    {
        return [
            'id' => $course->id,
            'title' => $course->title,
            'description' => $course->description,
            'price' => $course->formatted_price,
            'instructor' => $course->user->name,
            'chapter_count' => $course->chapters->count(),
            'enrollment_count' => $course->enrolledUsers->count(),
            'average_rating' => $this->calculateAverageRating($course),
            'is_enrolled' => $this->isUserEnrolled(auth()->id(), $course->id)
        ];
    }
    
    /**
     * 计算课程平均评分
     */
    private function calculateAverageRating(Course $course)
    {
        $ratings = $course->comments->pluck('rating');
        
        if ($ratings->isEmpty()) {
            return 0;
        }
        
        return round($ratings->sum() / $ratings->count(), 1);
    }
    
    /**
     * 检查用户是否已报名课程
     */
    public function isUserEnrolled($userId, $courseId)
    {
        return Enrollment::where('user_id', $userId)
                        ->where('course_id', $courseId)
                        ->where('expires_at', '>', now())
                        ->exists();
    }
    
    /**
     * 创建课程并处理相关逻辑
     */
    public function createCourse(array $data, $userId)
    {
        // 开启数据库事务
        DB::beginTransaction();
        
        try {
            // 创建课程
            $course = Course::create([
                'title' => $data['title'],
                'description' => $data['description'],
                'price' => $data['price'],
                'user_id' => $userId,
                'status' => 'draft'
            ]);
            
            // 如果有章节数据,创建章节
            if (isset($data['chapters']) && is_array($data['chapters'])) {
                foreach ($data['chapters'] as $chapterData) {
                    $course->chapters()->create([
                        'title' => $chapterData['title'],
                        'content' => $chapterData['content'],
                        'order' => $chapterData['order'] ?? 0
                    ]);
                }
            }
            
            // 提交事务
            DB::commit();
            
            return $course;
            
        } catch (\Exception $e) {
            // 回滚事务
            DB::rollBack();
            throw $e;
        }
    }
}

1.3 数据库设计

微课堂1.4.9版本使用MySQL数据库,主要表结构如下:

1.3.1 课程表(courses)

CREATE TABLE `courses` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL COMMENT '创建者ID',
  `title` varchar(255) NOT NULL COMMENT '课程标题',
  `description` text COMMENT '课程描述',
  `price` decimal(8,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
  `status` enum('draft','published','archived') NOT NULL DEFAULT 'draft' COMMENT '状态',
  `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图',
  `difficulty` enum('beginner','intermediate','advanced') DEFAULT 'beginner' COMMENT '难度',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `courses_user_id_index` (`user_id`),
  KEY `courses_status_index` (`status`),
  FULLTEXT KEY `courses_title_description_fulltext` (`title`,`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

1.3.2 章节表(chapters)

CREATE TABLE `chapters` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `course_id` bigint(20) unsigned NOT NULL COMMENT '课程ID',
  `title` varchar(255) NOT NULL COMMENT '章节标题',
  `content` text COMMENT '章节内容',
  `order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
  `video_url` varchar(255) DEFAULT NULL COMMENT '视频链接',
  `duration` int(11) DEFAULT NULL COMMENT '时长(秒)',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `chapters_course_id_index` (`course_id`),
  KEY `chapters_order_index` (`order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

1.3.3 报名表(enrollments)

CREATE TABLE `enrollments` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
  `course_id` bigint(20) unsigned NOT NULL COMMENT '课程ID',
  `enrolled_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间',
  `expires_at` timestamp NULL DEFAULT NULL COMMENT '过期时间',
  `payment_status` enum('pending','paid','failed') NOT NULL DEFAULT 'pending' COMMENT '支付状态',
  `amount` decimal(8,2) NOT NULL DEFAULT '0.00' COMMENT '支付金额',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `enrollments_user_course_unique` (`user_id`,`course_id`),
  KEY `enrollments_user_id_index` (`user_id`),
  KEY `enrollments_course_id_index` (`course_id`),
  KEY `enrollments_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

2. 核心功能实现解析

2.1 用户认证系统

微课堂使用JWT(JSON Web Token)进行API认证,以下是认证流程的详细实现:

2.1.1 JWT认证中间件

<?php
// app/Http/Middleware/JwtAuth.php

namespace App\Http\Middleware;

use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Models\User;

class JwtAuth
{
    public function handle($request, Closure $next)
    {
        // 从请求头获取token
        $token = $request->header('Authorization');
        
        if (!$token) {
            return response()->json(['error' => '未提供认证令牌'], 401);
        }
        
        // 移除"Bearer "前缀
        $token = str_replace('Bearer ', '', $token);
        
        try {
            // 解码JWT
            $decoded = JWT::decode($token, new Key(config('app.jwt_secret'), 'HS256'));
            
            // 查找用户
            $user = User::find($decoded->sub);
            
            if (!$user) {
                return response()->json(['error' => '用户不存在'], 401);
            }
            
            // 将用户信息附加到请求
            $request->auth = $user;
            
        } catch (\Exception $e) {
            return response()->json(['error' => '无效的令牌'], 401);
        }
        
        return $next($request);
    }
}

2.1.2 登录控制器

<?php
// app/controllers/AuthController.php

namespace App\Controllers;

use App\Models\User;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class AuthController extends BaseController
{
    public function login(Request $request)
    {
        // 验证请求数据
        $validator = Validator::make($request->all(), [
            'email' => 'required|email',
            'password' => 'required|string'
        ]);
        
        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }
        
        // 查找用户
        $user = User::where('email', $request->email)->first();
        
        if (!$user || !Hash::check($request->password, $user->password)) {
            return response()->json(['error' => '邮箱或密码错误'], 401);
        }
        
        // 生成JWT令牌
        $payload = [
            'sub' => $user->id,
            'iat' => time(),
            'exp' => time() + (60 * 60 * 24) // 24小时过期
        ];
        
        $token = JWT::encode($payload, config('app.jwt_secret'), 'HS256');
        
        // 返回令牌和用户信息
        return response()->json([
            'token' => $token,
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
                'role' => $user->role
            ]
        ]);
    }
    
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:8|confirmed'
        ]);
        
        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }
        
        // 创建用户
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'role' => 'student' // 默认角色
        ]);
        
        // 生成JWT令牌
        $payload = [
            'sub' => $user->id,
            'iat' => time(),
            'exp' => time() + (60 * 60 * 24)
        ];
        
        $token = JWT::encode($payload, config('app.jwt_secret'), 'HS256');
        
        return response()->json([
            'token' => $token,
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
                'role' => $user->role
            ]
        ], 201);
    }
}

2.2 课程支付系统

微课堂支持多种支付方式,以下是支付宝支付集成的示例:

2.2.1 支付宝支付控制器

<?php
// app/controllers/PaymentController.php

namespace App\Controllers;

use App\Models\Course;
use App\Models\Enrollment;
use App\Services\PaymentService;
use Illuminate\Http\Request;

class PaymentController extends BaseController
{
    public function createPayment(Request $request, $courseId)
    {
        // 验证用户是否已报名
        $course = Course::findOrFail($courseId);
        $userId = $request->auth->id;
        
        if ($this->isUserEnrolled($userId, $courseId)) {
            return response()->json(['error' => '您已报名该课程'], 400);
        }
        
        // 创建报名记录(待支付状态)
        $enrollment = Enrollment::create([
            'user_id' => $userId,
            'course_id' => $courseId,
            'payment_status' => 'pending',
            'amount' => $course->price
        ]);
        
        // 调用支付服务生成支付链接
        $paymentService = new PaymentService();
        $paymentResult = $paymentService->createAlipayOrder($enrollment);
        
        return response()->json([
            'enrollment_id' => $enrollment->id,
            'payment_url' => $paymentResult['url'],
            'order_no' => $paymentResult['order_no']
        ]);
    }
    
    public function paymentCallback(Request $request)
    {
        // 支付宝回调处理
        $paymentService = new PaymentService();
        $result = $paymentService->handleAlipayCallback($request);
        
        if ($result['success']) {
            // 更新报名状态
            $enrollment = Enrollment::find($result['enrollment_id']);
            $enrollment->update([
                'payment_status' => 'paid',
                'expires_at' => now()->addYear() // 一年有效期
            ]);
            
            // 发送通知
            $this->sendEnrollmentNotification($enrollment);
            
            return response()->json(['status' => 'success']);
        }
        
        return response()->json(['status' => 'failed'], 400);
    }
}

2.2.2 支付宝支付服务

<?php
// app/services/PaymentService.php

namespace App\Services;

use App\Models\Enrollment;
use Illuminate\Support\Facades\Http;

class PaymentService
{
    private $alipayConfig;
    
    public function __construct()
    {
        $this->alipayConfig = [
            'app_id' => config('alipay.app_id'),
            'merchant_private_key' => config('alipay.merchant_private_key'),
            'alipay_public_key' => config('alipay.alipay_public_key'),
            'gateway' => 'https://openapi.alipay.com/gateway.do',
            'notify_url' => url('/api/payments/alipay/callback'),
            'return_url' => url('/api/payments/alipay/return')
        ];
    }
    
    /**
     * 创建支付宝订单
     */
    public function createAlipayOrder(Enrollment $enrollment)
    {
        // 构建请求参数
        $bizContent = [
            'out_trade_no' => 'MC' . time() . $enrollment->id,
            'product_code' => 'FAST_INSTANT_TRADE_PAY',
            'total_amount' => $enrollment->amount,
            'subject' => '微课堂课程购买:' . $enrollment->course->title,
            'body' => '课程ID:' . $enrollment->course_id,
            'timeout_express' => '30m'
        ];
        
        // 生成签名
        $params = [
            'app_id' => $this->alipayConfig['app_id'],
            'method' => 'alipay.trade.page.pay',
            'charset' => 'utf-8',
            'sign_type' => 'RSA2',
            'timestamp' => date('Y-m-d H:i:s'),
            'version' => '1.0',
            'biz_content' => json_encode($bizContent),
            'notify_url' => $this->alipayConfig['notify_url'],
            'return_url' => $this->alipayConfig['return_url']
        ];
        
        // 签名(简化示例,实际应使用SDK)
        $sign = $this->generateSign($params);
        $params['sign'] = $sign;
        
        // 构建支付URL
        $url = $this->alipayConfig['gateway'] . '?' . http_build_query($params);
        
        return [
            'url' => $url,
            'order_no' => $bizContent['out_trade_no']
        ];
    }
    
    /**
     * 处理支付宝回调
     */
    public function handleAlipayCallback($request)
    {
        // 验证签名
        if (!$this->verifySign($request->all())) {
            return ['success' => false, 'error' => '签名验证失败'];
        }
        
        // 解析回调数据
        $tradeStatus = $request->get('trade_status');
        $outTradeNo = $request->get('out_trade_no');
        
        // 提取报名ID(从订单号中)
        $enrollmentId = substr($outTradeNo, -10);
        
        if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
            return [
                'success' => true,
                'enrollment_id' => $enrollmentId,
                'trade_no' => $request->get('trade_no'),
                'amount' => $request->get('total_amount')
            ];
        }
        
        return ['success' => false, 'error' => '支付状态异常'];
    }
    
    /**
     * 生成签名(简化版,实际应使用支付宝SDK)
     */
    private function generateSign($params)
    {
        // 按参数名排序
        ksort($params);
        
        // 拼接字符串
        $stringToSign = '';
        foreach ($params as $key => $value) {
            if ($key !== 'sign') {
                $stringToSign .= $key . '=' . $value . '&';
            }
        }
        $stringToSign = rtrim($stringToSign, '&');
        
        // 使用私钥签名
        $privateKey = $this->alipayConfig['merchant_private_key'];
        openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA256);
        
        return base64_encode($sign);
    }
    
    /**
     * 验证签名
     */
    private function verifySign($params)
    {
        // 提取签名
        $sign = $params['sign'];
        unset($params['sign']);
        
        // 按参数名排序
        ksort($params);
        
        // 拼接字符串
        $stringToSign = '';
        foreach ($params as $key => $value) {
            $stringToSign .= $key . '=' . $value . '&';
        }
        $stringToSign = rtrim($stringToSign, '&');
        
        // 使用支付宝公钥验证
        $publicKey = $this->alipayConfig['alipay_public_key'];
        $signDecoded = base64_decode($sign);
        
        return openssl_verify($stringToSign, $signDecoded, $publicKey, OPENSSL_ALGO_SHA256) === 1;
    }
}

2.3 视频播放与进度跟踪

微课堂支持视频播放和学习进度跟踪,以下是前端和后端的实现:

2.3.1 前端视频播放器(Vue.js示例)

<!-- resources/js/components/VideoPlayer.vue -->
<template>
  <div class="video-player-container">
    <video 
      ref="videoPlayer"
      :src="videoUrl"
      @timeupdate="onTimeUpdate"
      @ended="onVideoEnded"
      @play="onPlay"
      @pause="onPause"
      controls
      style="width: 100%; max-height: 500px;"
    >
      您的浏览器不支持视频标签。
    </video>
    
    <div class="progress-info">
      <div class="progress-bar">
        <div 
          class="progress-fill" 
          :style="{ width: progressPercentage + '%' }"
        ></div>
      </div>
      <div class="progress-text">
        已观看:{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
        ({{ progressPercentage }}%)
      </div>
    </div>
    
    <div class="controls">
      <button @click="togglePlay" class="btn">
        {{ isPlaying ? '暂停' : '播放' }}
      </button>
      <button @click="saveProgress" class="btn btn-primary">
        保存进度
      </button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    videoUrl: String,
    chapterId: Number,
    initialProgress: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      currentTime: 0,
      duration: 0,
      isPlaying: false,
      progressPercentage: 0,
      saveInterval: null
    };
  },
  mounted() {
    // 初始化视频播放器
    this.$nextTick(() => {
      const video = this.$refs.videoPlayer;
      
      // 设置初始进度
      if (this.initialProgress > 0) {
        video.currentTime = this.initialProgress;
      }
      
      // 监听视频事件
      video.addEventListener('loadedmetadata', () => {
        this.duration = video.duration;
      });
      
      // 自动保存进度(每30秒)
      this.saveInterval = setInterval(() => {
        if (this.isPlaying) {
          this.saveProgress();
        }
      }, 30000);
    });
  },
  beforeDestroy() {
    // 清理定时器
    if (this.saveInterval) {
      clearInterval(this.saveInterval);
    }
    // 退出时保存进度
    this.saveProgress();
  },
  methods: {
    onTimeUpdate(event) {
      this.currentTime = event.target.currentTime;
      this.progressPercentage = Math.round(
        (this.currentTime / this.duration) * 100
      );
    },
    onVideoEnded() {
      this.isPlaying = false;
      this.progressPercentage = 100;
      this.saveProgress();
      // 视频结束,标记章节完成
      this.markChapterCompleted();
    },
    onPlay() {
      this.isPlaying = true;
    },
    onPause() {
      this.isPlaying = false;
    },
    togglePlay() {
      const video = this.$refs.videoPlayer;
      if (video.paused) {
        video.play();
      } else {
        video.pause();
      }
    },
    async saveProgress() {
      try {
        const response = await axios.post('/api/learning/progress', {
          chapter_id: this.chapterId,
          progress_seconds: this.currentTime,
          progress_percentage: this.progressPercentage
        });
        
        if (response.data.success) {
          console.log('进度已保存');
        }
      } catch (error) {
        console.error('保存进度失败:', error);
      }
    },
    async markChapterCompleted() {
      try {
        const response = await axios.post('/api/learning/complete', {
          chapter_id: this.chapterId
        });
        
        if (response.data.success) {
          this.$emit('chapter-completed', this.chapterId);
        }
      } catch (error) {
        console.error('标记完成失败:', error);
      }
    },
    formatTime(seconds) {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    }
  }
};
</script>

<style scoped>
.video-player-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
}

.progress-info {
  margin: 15px 0;
}

.progress-bar {
  height: 8px;
  background: #e9ecef;
  border-radius: 4px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #4CAF50, #45a049);
  transition: width 0.3s ease;
}

.progress-text {
  margin-top: 5px;
  font-size: 14px;
  color: #666;
}

.controls {
  display: flex;
  gap: 10px;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: #6c757d;
  color: white;
}

.btn-primary {
  background: #007bff;
}
</style>

2.3.2 后端进度跟踪API

<?php
// app/controllers/LearningController.php

namespace App\Controllers;

use App\Models\ChapterProgress;
use App\Models\Chapter;
use Illuminate\Http\Request;

class LearningController extends BaseController
{
    /**
     * 保存学习进度
     */
    public function saveProgress(Request $request)
    {
        $validated = $request->validate([
            'chapter_id' => 'required|exists:chapters,id',
            'progress_seconds' => 'required|numeric|min:0',
            'progress_percentage' => 'required|numeric|min:0|max:100'
        ]);
        
        $userId = $request->auth->id;
        
        // 查找或创建进度记录
        $progress = ChapterProgress::updateOrCreate(
            [
                'user_id' => $userId,
                'chapter_id' => $validated['chapter_id']
            ],
            [
                'progress_seconds' => $validated['progress_seconds'],
                'progress_percentage' => $validated['progress_percentage'],
                'last_viewed_at' => now()
            ]
        );
        
        // 如果进度达到100%,标记为已完成
        if ($validated['progress_percentage'] >= 100) {
            $progress->update(['is_completed' => true]);
        }
        
        return response()->json([
            'success' => true,
            'progress' => $progress
        ]);
    }
    
    /**
     * 标记章节完成
     */
    public function markCompleted(Request $request)
    {
        $validated = $request->validate([
            'chapter_id' => 'required|exists:chapters,id'
        ]);
        
        $userId = $request->auth->id;
        
        // 更新进度记录
        ChapterProgress::updateOrCreate(
            [
                'user_id' => $userId,
                'chapter_id' => $validated['chapter_id']
            ],
            [
                'is_completed' => true,
                'completed_at' => now(),
                'progress_percentage' => 100,
                'progress_seconds' => Chapter::find($validated['chapter_id'])->duration ?? 0
            ]
        );
        
        // 检查课程是否全部完成
        $this->checkCourseCompletion($userId, $validated['chapter_id']);
        
        return response()->json(['success' => true]);
    }
    
    /**
     * 获取章节进度
     */
    public function getChapterProgress($chapterId)
    {
        $userId = request()->auth->id;
        
        $progress = ChapterProgress::where('user_id', $userId)
                                  ->where('chapter_id', $chapterId)
                                  ->first();
        
        return response()->json([
            'progress' => $progress ? [
                'progress_seconds' => $progress->progress_seconds,
                'progress_percentage' => $progress->progress_percentage,
                'is_completed' => $progress->is_completed,
                'last_viewed_at' => $progress->last_viewed_at
            ] : null
        ]);
    }
    
    /**
     * 检查课程是否全部完成
     */
    private function checkCourseCompletion($userId, $chapterId)
    {
        $chapter = Chapter::with('course')->find($chapterId);
        $course = $chapter->course;
        
        // 获取课程所有章节
        $allChapters = $course->chapters;
        
        // 获取用户已完成的章节
        $completedChapters = ChapterProgress::where('user_id', $userId)
                                           ->where('chapter_id', $allChapters->pluck('id'))
                                           ->where('is_completed', true)
                                           ->pluck('chapter_id');
        
        // 如果所有章节都已完成
        if ($completedChapters->count() === $allChapters->count()) {
            // 标记课程完成
            $this->markCourseCompleted($userId, $course->id);
        }
    }
    
    /**
     * 标记课程完成
     */
    private function markCourseCompleted($userId, $courseId)
    {
        // 更新报名记录
        \DB::table('enrollments')
           ->where('user_id', $userId)
           ->where('course_id', $courseId)
           ->update(['completed_at' => now()]);
        
        // 发送完成通知
        $this->sendCourseCompletionNotification($userId, $courseId);
    }
}

3. 实战应用指南

3.1 本地开发环境搭建

3.1.1 环境要求

  • PHP 7.4+ (推荐8.0+)
  • MySQL 5.7+ 或 MariaDB 10.2+
  • Composer
  • Node.js 14+ (用于前端构建)
  • Nginx/Apache

3.1.2 安装步骤

# 1. 克隆代码仓库
git clone https://github.com/microclass/microclass-1.4.9.git
cd microclass-1.4.9

# 2. 安装PHP依赖
composer install --no-dev

# 3. 复制环境配置文件
cp .env.example .env

# 4. 生成应用密钥
php artisan key:generate

# 5. 配置数据库
# 编辑 .env 文件,设置数据库连接信息
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=microclass
# DB_USERNAME=root
# DB_PASSWORD=

# 6. 运行数据库迁移
php artisan migrate

# 7. 运行数据库种子(可选,用于测试数据)
php artisan db:seed

# 8. 安装前端依赖
npm install

# 9. 构建前端资源
npm run production

# 10. 启动开发服务器
php artisan serve --host=0.0.0.0 --port=8000

3.1.3 配置文件示例

.env 文件配置:

# 应用配置
APP_NAME=微课堂
APP_ENV=local
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
APP_DEBUG=true
APP_URL=http://localhost:8000

# 数据库配置
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=microclass
DB_USERNAME=root
DB_PASSWORD=

# JWT配置
JWT_SECRET=your_jwt_secret_key_here

# 支付宝配置
ALIPAY_APP_ID=your_app_id
ALIPAY_MERCHANT_PRIVATE_KEY=your_private_key
ALIPAY_PUBLIC_KEY=your_public_key

# 文件存储
FILESYSTEM_DISK=local
UPLOAD_MAX_SIZE=10485760  # 10MB

# 邮件配置(用于通知)
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your_email@gmail.com
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=notify@microclass.com
MAIL_FROM_NAME="${APP_NAME}"

3.2 二次开发指南

3.2.1 添加新功能模块

假设需要添加一个“直播课程”功能:

  1. 创建模型
// app/models/LiveCourse.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LiveCourse extends Model
{
    protected $fillable = [
        'course_id', 'live_url', 'start_time', 'end_time', 
        'max_participants', 'current_participants'
    ];
    
    public function course()
    {
        return $this->belongsTo(Course::class);
    }
    
    public function participants()
    {
        return $this->belongsToMany(User::class, 'live_course_participants')
                    ->withPivot('joined_at');
    }
}
  1. 创建控制器
// app/controllers/LiveCourseController.php
namespace App\Controllers;

use App\Models\LiveCourse;
use Illuminate\Http\Request;

class LiveCourseController extends BaseController
{
    public function create(Request $request)
    {
        // 验证权限
        if (!auth()->user()->can('create_live_course')) {
            return response()->json(['error' => '无权限'], 403);
        }
        
        // 验证数据
        $validated = $request->validate([
            'course_id' => 'required|exists:courses,id',
            'live_url' => 'required|url',
            'start_time' => 'required|date',
            'end_time' => 'required|date|after:start_time',
            'max_participants' => 'required|integer|min:1'
        ]);
        
        // 创建直播课程
        $liveCourse = LiveCourse::create($validated);
        
        return response()->json([
            'success' => true,
            'live_course' => $liveCourse
        ]);
    }
    
    public function join($liveCourseId)
    {
        $userId = auth()->id();
        $liveCourse = LiveCourse::findOrFail($liveCourseId);
        
        // 检查是否已报名
        if ($liveCourse->participants->contains($userId)) {
            return response()->json(['error' => '已报名'], 400);
        }
        
        // 检查人数限制
        if ($liveCourse->current_participants >= $liveCourse->max_participants) {
            return response()->json(['error' => '人数已满'], 400);
        }
        
        // 检查时间
        if (now() < $liveCourse->start_time || now() > $liveCourse->end_time) {
            return response()->json(['error' => '不在直播时间'], 400);
        }
        
        // 加入直播
        DB::transaction(function () use ($liveCourse, $userId) {
            $liveCourse->participants()->attach($userId, [
                'joined_at' => now()
            ]);
            
            $liveCourse->increment('current_participants');
        });
        
        return response()->json(['success' => true]);
    }
}
  1. 添加路由
// routes/api.php
Route::group(['middleware' => ['auth:api']], function () {
    Route::post('/live-courses', [LiveCourseController::class, 'create']);
    Route::post('/live-courses/{id}/join', [LiveCourseController::class, 'join']);
    Route::get('/live-courses/{id}', [LiveCourseController::class, 'show']);
});

3.2.2 扩展用户角色系统

微课堂默认有学生和讲师角色,可以扩展更多角色:

  1. 修改用户模型
// app/models/User.php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    // 添加角色常量
    const ROLE_STUDENT = 'student';
    const ROLE_INSTRUCTOR = 'instructor';
    const ROLE_ADMIN = 'admin';
    const ROLE_MODERATOR = 'moderator';
    
    protected $fillable = [
        'name', 'email', 'password', 'role'
    ];
    
    // 角色验证方法
    public function hasRole($role)
    {
        return $this->role === $role;
    }
    
    public function hasAnyRole($roles)
    {
        return in_array($this->role, (array)$roles);
    }
    
    // 权限检查(简化版,实际可使用权限包)
    public function can($permission)
    {
        // 根据角色定义权限
        $permissions = [
            self::ROLE_STUDENT => ['enroll_course', 'view_content'],
            self::ROLE_INSTRUCTOR => ['create_course', 'edit_course', 'view_analytics'],
            self::ROLE_ADMIN => ['manage_users', 'manage_courses', 'view_reports'],
            self::ROLE_MODERATOR => ['moderate_comments', 'manage_content']
        ];
        
        return in_array($permission, $permissions[$this->role] ?? []);
    }
}
  1. 创建权限中间件
// app/Http/Middleware/CheckPermission.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckPermission
{
    public function handle($request, Closure $next, $permission)
    {
        $user = $request->auth;
        
        if (!$user || !$user->can($permission)) {
            return response()->json(['error' => '无权限访问'], 403);
        }
        
        return $next($request);
    }
}
  1. 使用权限中间件
// routes/api.php
Route::group(['middleware' => ['auth:api']], function () {
    // 只有讲师可以创建课程
    Route::post('/courses', [CourseController::class, 'create'])
         ->middleware('permission:create_course');
    
    // 只有管理员可以管理用户
    Route::get('/users', [UserController::class, 'index'])
         ->middleware('permission:manage_users');
});

3.3 性能优化建议

3.3.1 数据库优化

  1. 添加索引
-- 为常用查询字段添加索引
ALTER TABLE `courses` ADD INDEX `idx_user_status` (`user_id`, `status`);
ALTER TABLE `enrollments` ADD INDEX `idx_user_course` (`user_id`, `course_id`);
ALTER TABLE `chapters` ADD INDEX `idx_course_order` (`course_id`, `order`);
  1. 使用Eloquent优化查询
// 避免N+1查询问题
$course = Course::with(['chapters', 'user', 'enrolledUsers'])->find($id);

// 使用select只获取需要的字段
$users = User::select('id', 'name', 'email')->get();

// 使用whereHas进行条件查询
$courses = Course::whereHas('enrolledUsers', function ($query) {
    $query->where('user_id', auth()->id());
})->get();

3.3.2 缓存策略

// 使用Redis缓存课程数据
use Illuminate\Support\Facades\Cache;

class CourseService
{
    public function getCourseWithCache($courseId)
    {
        $cacheKey = "course:{$courseId}";
        
        return Cache::remember($cacheKey, 3600, function () use ($courseId) {
            return Course::with(['chapters', 'user', 'enrolledUsers'])
                        ->find($courseId);
        });
    }
    
    public function clearCourseCache($courseId)
    {
        Cache::forget("course:{$courseId}");
    }
}

3.3.3 队列处理

对于耗时操作(如发送邮件、生成报表),使用队列:

// 创建队列任务
php artisan make:job SendEnrollmentNotification

// app/Jobs/SendEnrollmentNotification.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Models\Enrollment;

class SendEnrollmentNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $enrollment;
    
    public function __construct(Enrollment $enrollment)
    {
        $this->enrollment = $enrollment;
    }
    
    public function handle()
    {
        // 发送邮件通知
        Mail::to($this->enrollment->user->email)
            ->send(new EnrollmentConfirmation($this->enrollment));
        
        // 发送短信通知(如果配置了短信服务)
        // $this->sendSmsNotification();
    }
}

4. 常见问题与解决方案

4.1 部署问题

4.1.1 权限问题

问题:上传文件失败,提示权限不足。

解决方案

# 设置存储目录权限
chmod -R 775 storage/
chmod -R 775 bootstrap/cache/
chown -R www-data:www-data storage/ bootstrap/cache/

4.1.2 内存限制

问题:处理大文件或大量数据时内存不足。

解决方案

// 在php.ini中增加内存限制
memory_limit = 256M

// 或者在代码中临时增加
ini_set('memory_limit', '256M');

// 使用生成器处理大数据集
public function processLargeDataset()
{
    $query = User::where('status', 'active')->cursor();
    
    foreach ($query as $user) {
        // 处理每个用户
        $this->processUser($user);
    }
}

4.2 安全问题

4.2.1 SQL注入防护

问题:如何防止SQL注入攻击?

解决方案

// 错误的做法(直接拼接SQL)
$users = DB::select("SELECT * FROM users WHERE email = '{$email}'");

// 正确的做法(使用参数绑定)
$users = DB::select("SELECT * FROM users WHERE email = ?", [$email]);

// 或者使用Eloquent
$users = User::where('email', $email)->get();

4.2.2 XSS攻击防护

问题:如何防止跨站脚本攻击?

解决方案

// 输出到HTML时进行转义
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// 在Blade模板中自动转义
{{ $userInput }}

// 如果需要输出原始HTML(谨慎使用)
{!! $safeHtml !!}

4.3 性能问题

4.3.1 页面加载慢

问题:课程列表页面加载缓慢。

解决方案

// 1. 使用分页
$courses = Course::with('user')->paginate(20);

// 2. 延迟加载关联数据
$courses = Course::with(['user' => function ($query) {
    $query->select('id', 'name');
}])->paginate(20);

// 3. 使用缓存
$courseList = Cache::remember('course_list_page_' . $page, 300, function () use ($page) {
    return Course::with('user')->paginate(20, ['*'], 'page', $page);
});

5. 总结

微课堂1.4.9版本是一个功能完善、架构清晰的在线教育平台。通过本文的源码解析和实战指南,开发者可以:

  1. 理解核心架构:掌握MVC分层结构、服务层设计、数据库关系
  2. 实现核心功能:用户认证、课程管理、支付系统、视频播放
  3. 进行二次开发:添加新功能模块、扩展角色权限系统
  4. 优化性能:数据库优化、缓存策略、队列处理
  5. 解决常见问题:部署、安全、性能问题

在实际开发中,建议:

  • 遵循PSR编码规范
  • 编写单元测试和集成测试
  • 使用版本控制(Git)管理代码
  • 定期更新依赖库以修复安全漏洞
  • 监控系统性能并及时优化

通过深入理解微课堂的源码结构和设计思想,开发者可以构建出更加稳定、高效、可扩展的在线教育平台。