百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

手把手教您使用Python+Flutter开发在线教育系统(上),纯干货!

suiw9 2025-03-14 20:32 16 浏览 0 评论

一、系统架构设计

复制

在线教育系统架构:
Flutter前端 (Mobile/Web) <-> Django REST API <-> PostgreSQL数据库
                   ↑
                (JWT认证)

二、后端开发(Python Django)

  1. 项目初始化

bash

复制

pip install django djangorestframework djangorestframework-simplejwt
django-admin startproject edu_platform
cd edu_platform
python manage.py startapp courses
  1. 数据库模型 (courses/models.py)

python

复制

from django.db import models
from django.contrib.auth.models import User

class CourseCategory(models.Model):
    """课程分类模型"""
    name = models.CharField(max_length=100)
    description = models.TextField()

    def __str__(self):
        return self.name

class Course(models.Model):
    """课程主模型"""
    LEVEL_CHOICES = [
        ('B', '初级'),
        ('I', '中级'),
        ('A', '高级')
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField()
    category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE)
    teacher = models.ForeignKey(User, on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    duration = models.IntegerField(help_text="课程总时长(分钟)")
    level = models.CharField(max_length=1, choices=LEVEL_CHOICES)
    created_at = models.DateTimeField(auto_now_add=True)
    cover_image = models.ImageField(upload_to='course_covers/')

    def __str__(self):
        return self.title

class Lesson(models.Model):
    """课程章节模型"""
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    order = models.PositiveIntegerField()
    video_url = models.URLField()
    duration = models.IntegerField(help_text="章节时长(分钟)")

    class Meta:
        ordering = ['order']

    def __str__(self):
        return f"{self.course.title} - {self.title}"

class Enrollment(models.Model):
    """学生选课模型"""
    student = models.ForeignKey(User, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    enrolled_at = models.DateTimeField(auto_now_add=True)
    completed = models.BooleanField(default=False)

    class Meta:
        unique_together = ('student', 'course')

    def __str__(self):
        return f"{self.student.username} - {self.course.title}"
  1. 序列化器 (courses/serializers.py)

python

复制

from rest_framework import serializers
from .models import Course, Lesson, Enrollment, CourseCategory
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseCategory
        fields = '__all__'

class LessonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Lesson
        fields = ['id', 'title', 'order', 'duration', 'video_url']

class CourseSerializer(serializers.ModelSerializer):
    teacher = UserSerializer(read_only=True)
    lessons = LessonSerializer(many=True, read_only=True)
    category = CourseCategorySerializer()
    
    class Meta:
        model = Course
        fields = [
            'id', 'title', 'description', 'category',
            'price', 'duration', 'level', 'cover_image',
            'teacher', 'lessons', 'created_at'
        ]

class EnrollmentSerializer(serializers.ModelSerializer):
    student = UserSerializer(read_only=True)
    course = CourseSerializer(read_only=True)

    class Meta:
        model = Enrollment
        fields = ['id', 'student', 'course', 'enrolled_at', 'completed']
        read_only_fields = ['enrolled_at']
  1. 视图 (courses/views.py)

python

复制

from rest_framework import viewsets, permissions, status
from rest_framework.response import Response
from rest_framework.decorators import action
from .models import Course, Enrollment
from .serializers import CourseSerializer, EnrollmentSerializer
from django.contrib.auth import get_user_model

User = get_user_model()

class CourseViewSet(viewsets.ModelViewSet):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    @action(detail=True, methods=['post'])
    def enroll(self, request, pk=None):
        """学生选课"""
        course = self.get_object()
        student = request.user
        
        if Enrollment.objects.filter(student=student, course=course).exists():
            return Response(
                {'error': 'Already enrolled'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        enrollment = Enrollment.objects.create(
            student=student,
            course=course
        )
        serializer = EnrollmentSerializer(enrollment)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

class EnrollmentViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = EnrollmentSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        return Enrollment.objects.filter(student=self.request.user)
  1. 认证配置 (edu_platform/settings.py)

python

复制

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}

三、Flutter前端开发

  1. 项目初始化

bash

复制

flutter create edu_app
cd edu_app
flutter pub add provider http cached_network_image video_player dio flutter_secure_storage
  1. 核心模型 (lib/models/course.dart)

dart

复制

class Course {
  final int id;
  final String title;
  final String description;
  final double price;
  final String coverImage;
  final List lessons;

  Course({
    required this.id,
    required this.title,
    required this.description,
    required this.price,
    required this.coverImage,
    required this.lessons,
  });

  factory Course.fromJson(Map json) {
    return Course(
      id: json['id'],
      title: json['title'],
      description: json['description'],
      price: json['price'].toDouble(),
      coverImage: json['cover_image'],
      lessons: (json['lessons'] as List)
          .map((lesson) => Lesson.fromJson(lesson))
          .toList(),
    );
  }
}

class Lesson {
  final int id;
  final String title;
  final String videoUrl;
  final int duration;

  Lesson({
    required this.id,
    required this.title,
    required this.videoUrl,
    required this.duration,
  });

  factory Lesson.fromJson(Map json) {
    return Lesson(
      id: json['id'],
      title: json['title'],
      videoUrl: json['video_url'],
      duration: json['duration'],
    );
  }
}
  1. API服务 (lib/services/api_service.dart)

dart

复制

import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class ApiService {
  final Dio _dio = Dio();
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  ApiService() {
    _dio.options.baseUrl = 'http://your-api-domain.com/api/';
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: _addAuthorizationHeader,
    ));
  }

  Future _addAuthorizationHeader(
      RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await _storage.read(key: 'access_token');
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    return handler.next(options);
  }

  Future login(String username, String password) async {
    try {
      final response = await _dio.post('login/', data: {
        'username': username,
        'password': password,
      });
      
      await _storage.write(
          key: 'access_token', value: response.data['access']);
      await _storage.write(
          key: 'refresh_token', value: response.data['refresh']);
    } on DioError catch (e) {
      throw _handleError(e);
    }
  }

  Future<List> getCourses() async {
    try {
      final response = await _dio.get('courses/');
      return (response.data as List)
          .map((course) => Course.fromJson(course))
          .toList();
    } on DioError catch (e) {
      throw _handleError(e);
    }
  }

  Future enrollCourse(int courseId) async {
    try {
      await _dio.post('courses/$courseId/enroll/');
    } on DioError catch (e) {
      throw _handleError(e);
    }
  }

  String _handleError(DioError e) {
    if (e.response != null) {
      return e.response!.data['error'] ?? 'Unknown error';
    } else {
      return 'Network error, please check your connection';
    }
  }
}
  1. 状态管理 (lib/providers/course_provider.dart)

dart

复制

import 'package:flutter/foundation.dart';
import '../models/course.dart';
import '../services/api_service.dart';

class CourseProvider with ChangeNotifier {
  final ApiService _apiService;
  List _courses = [];
  bool _isLoading = false;
  String _errorMessage = '';

  CourseProvider(this._apiService);

  List get courses => _courses;
  bool get isLoading => _isLoading;
  String get errorMessage => _errorMessage;

  Future loadCourses() async {
    _isLoading = true;
    notifyListeners();

    try {
      _courses = await _apiService.getCourses();
      _errorMessage = '';
    } catch (e) {
      _errorMessage = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  Future enrollCourse(int courseId) async {
    try {
      await _apiService.enrollCourse(courseId);
      // 更新本地状态
      final index = _courses.indexWhere((c) => c.id == courseId);
      if (index != -1) {
        // 在实际应用中可能需要更复杂的状态更新
        notifyListeners();
      }
    } catch (e) {
      throw e.toString();
    }
  }
}
  1. 核心界面 (lib/screens/course_detail_screen.dart)

dart

复制

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/course.dart';
import '../providers/course_provider.dart';
import 'video_player_screen.dart';

class CourseDetailScreen extends StatelessWidget {
  final Course course;

  const CourseDetailScreen({super.key, required this.course});

  @override
  Widget build(BuildContext context) {
    final courseProvider = Provider.of(context);

    return Scaffold(
      appBar: AppBar(title: Text(course.title)),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Image.network(
              course.coverImage,
              height: 200,
              fit: BoxFit.cover,
            ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    course.description,
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    '价格: yen${course.price.toStringAsFixed(2)}',
                    style: const TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.red,
                    ),
                  ),
                  const SizedBox(height: 24),
                  ElevatedButton(
                    onPressed: () async {
                      try {
                        await courseProvider.enrollCourse(course.id);
                        // 导航到课程学习页面
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (_) => VideoPlayerScreen(
                              lessons: course.lessons,
                            ),
                          ),
                        );
                      } catch (e) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text(e.toString())),
                        );
                      }
                    },
                    child: const Text('立即报名'),
                  ),
                  const SizedBox(height: 24),
                  const Text(
                    '课程章节',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  ...course.lessons.map(
                    (lesson) => ListTile(
                      leading: const Icon(Icons.play_circle_outline),
                      title: Text(lesson.title),
                      subtitle: Text('时长: ${lesson.duration}分钟'),
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (_) => VideoPlayerScreen(
                              lessons: course.lessons,
                              initialIndex: course.lessons.indexOf(lesson),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  1. 视频播放组件 (lib/screens/video_player_screen.dart)

dart

复制

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../models/course.dart';

class VideoPlayerScreen extends StatefulWidget {
  final List lessons;
  final int initialIndex;

  const VideoPlayerScreen({
    super.key,
    required this.lessons,
    this.initialIndex = 0,
  });

  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State {
  late VideoPlayerController _controller;
  late int _currentIndex;

  @override
  void initState() {
    super.initState();
    _currentIndex = widget.initialIndex;
    _initializeVideo(widget.lessons[_currentIndex].videoUrl);
  }

  void _initializeVideo(String url) {
    _controller = VideoPlayerController.network(url)
      ..initialize().then((_) {
        setState(() {});
        _controller.play();
      });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _switchVideo(int index) {
    if (index == _currentIndex) return;
    setState(() {
      _currentIndex = index;
      _controller.dispose();
      _initializeVideo(widget.lessons[index].videoUrl);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.lessons[_currentIndex].title)),
      body: Column(
        children: [
          AspectRatio(
            aspectRatio: _controller.value.aspectRatio,
            child: VideoPlayer(_controller),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: widget.lessons.length,
              itemBuilder: (context, index) => ListTile(
                title: Text(widget.lessons[index].title),
                selected: index == _currentIndex,
                onTap: () => _switchVideo(index),
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _controller.value.isPlaying
                ? _controller.pause()
                : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
}

四、支付系统深度实现

1. 支付模块架构设计

bash

复制

支付流程:
Flutter发起支付请求 -> Django创建支付订单 -> 调用支付网关 -> 用户完成支付 -> 异步通知处理 -> 更新订单状态

2. 数据库模型扩展

python

复制

# payments/models.py
from django.db import models
from courses.models import Course
from django.contrib.auth import get_user_model

User = get_user_model()

class PaymentOrder(models.Model):
    ORDER_STATUS = [
        ('created', '已创建'),
        ('paid', '支付成功'),
        ('failed', '支付失败'),
        ('refunded', '已退款')
    ]
    
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    order_number = models.CharField(max_length=64, unique=True)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20, choices=ORDER_STATUS, default='created')
    created_at = models.DateTimeField(auto_now_add=True)
    paid_at = models.DateTimeField(null=True)
    payment_channel = models.CharField(max_length=20)  # wechat/alipay
    transaction_id = models.CharField(max_length=128, null=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['order_number']),
            models.Index(fields=['user', 'status'])
        ]

class RefundRecord(models.Model):
    order = models.ForeignKey(PaymentOrder, on_delete=models.CASCADE)
    refund_amount = models.DecimalField(max_digits=10, decimal_places=2)
    refund_reason = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    refund_status = models.CharField(max_length=20)

3. 微信支付集成实现

python

复制

# payments/wechat_pay.py
import hashlib
import xml.etree.ElementTree as ET
from datetime import datetime
import requests

class WeChatPay:
    def __init__(self, appid, mch_id, api_key):
        self.appid = appid
        self.mch_id = mch_id
        self.api_key = api_key
    
    def generate_sign(self, params):
        sorted_params = sorted(params.items())
        sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
        sign_str += f'&key={self.api_key}'
        return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()

    def create_unified_order(self, order_params):
        base_params = {
            'appid': self.appid,
            'mch_id': self.mch_id,
            'nonce_str': hashlib.md5(str(datetime.now()).encode()).hexdigest(),
            'notify_url': 'https://yourdomain.com/payments/wechat/notify/',
            'trade_type': 'APP'
        }
        all_params = {**base_params, **order_params}
        all_params['sign'] = self.generate_sign(all_params)
        
        xml_data = self._dict_to_xml(all_params)
        response = requests.post(
            'https://api.mch.weixin.qq.com/pay/unifiedorder',
            data=xml_data
        )
        return self._parse_xml_response(response.content)

    def _dict_to_xml(self, params):
        xml = ["<xml>"]
        for k, v in params.items():
            xml.append(f"<{k}>{v}")
        xml.append("</xml>")
        return ''.join(xml)

    def _parse_xml_response(self, content):
        root = ET.fromstring(content)
        return {child.tag: child.text for child in root}

4. 支付视图实现

python

复制

# payments/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import PaymentOrder
from .serializers import PaymentOrderSerializer
from .wechat_pay import WeChatPay
from django.conf import settings

class PaymentAPI(APIView):
    def post(self, request):
        user = request.user
        course_id = request.data.get('course_id')
        
        try:
            course = Course.objects.get(pk=course_id)
            order = PaymentOrder.objects.create(
                user=user,
                course=course,
                amount=course.price,
                order_number=self._generate_order_number(user),
                payment_channel='wechat'
            )
            
            wechat_pay = WeChatPay(
                appid=settings.WECHAT_APPID,
                mch_id=settings.WECHAT_MCH_ID,
                api_key=settings.WECHAT_API_KEY
            )
            
            order_params = {
                'body': course.title,
                'out_trade_no': order.order_number,
                'total_fee': int(course.price * 100),
                'spbill_create_ip': request.META.get('REMOTE_ADDR')
            }
            
            result = wechat_pay.create_unified_order(order_params)
            if result['return_code'] == 'SUCCESS':
                return Response({
                    'appid': result['appid'],
                    'partnerid': result['mch_id'],
                    'prepayid': result['prepay_id'],
                    'package': 'Sign=WXPay',
                    'noncestr': result['nonce_str'],
                    'timestamp': str(int(time.time())),
                    'sign': result['sign']
                })
            else:
                order.status = 'failed'
                order.save()
                return Response({'error': result['return_msg']}, 
                              status=status.HTTP_400_BAD_REQUEST)
                
        except Course.DoesNotExist:
            return Response({'error': '课程不存在'}, 
                          status=status.HTTP_400_BAD_REQUEST)

    def _generate_order_number(self, user):
        timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
        return f"{user.id}_{timestamp}"

五、直播系统完整实现

1. Agora集成架构

bash

复制

实时通信流程:
Flutter端初始化Agora SDK -> 获取频道Token -> 加入频道 -> 处理音视频流 -> 离开频道

2. 频道管理实现

python

复制

# live/models.py
from django.db import models
from courses.models import Course
from django.contrib.auth import get_user_model

User = get_user_model()

class LiveChannel(models.Model):
    course = models.OneToOneField(Course, on_delete=models.CASCADE)
    channel_name = models.CharField(max_length=255)
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()
    is_active = models.BooleanField(default=False)
    max_attendees = models.IntegerField(default=1000)
    
class LiveParticipant(models.Model):
    channel = models.ForeignKey(LiveChannel, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    join_time = models.DateTimeField(auto_now_add=True)
    role = models.CharField(max_length=10, choices=[
        ('host', '主播'), 
        ('audience', '观众')
    ])

3. Token生成服务

python

复制

# live/utils.py
import hmac
import hashlib
import base64
import time
from datetime import timedelta

class AgoraTokenGenerator:
    def __init__(self, app_id, app_certificate):
        self.app_id = app_id
        self.app_certificate = app_certificate

    def generate_token(self, channel_name, user_id, role=1, expire=3600):
        expire_time = int(time.time()) + expire
        
        token_params = {
            "app_id": self.app_id,
            "app_certificate": self.app_certificate,
            "channel_name": channel_name,
            "uid": user_id,
            "privilege": {
                1: expire_time,  # Join channel
                2: expire_time   # Publish audio/video
            }
        }
        
        return self._build_token(token_params)

    def _build_token(self, params):
        content = self._pack_uint32(params['uid']) + \
                 self._pack_uint32(params['privilege'][1]) + \
                 self._pack_uint32(params['privilege'][2])
        
        signature = hmac.new(
            params['app_certificate'].encode('utf-8'),
            content,
            hashlib.sha256
        ).digest()
        
        version = b'\x01'
        content = version + signature + content
        return params['app_id'] + ':' + base64.b64encode(content).decode('utf-8')

    @staticmethod
    def _pack_uint32(num):
        return num.to_bytes(4, byteorder='big')

4. Flutter直播组件

dart

复制

// lib/widgets/live_broadcast.dart
import 'package:agora_rtc_engine/agora_rtc_engine.dart';

class LiveBroadcast extends StatefulWidget {
  final String channelName;
  final bool isHost;

  const LiveBroadcast({
    super.key,
    required this.channelName,
    required this.isHost
  });

  @override
  _LiveBroadcastState createState() => _LiveBroadcastState();
}

class _LiveBroadcastState extends State {
  late final RtcEngine _engine;
  List _remoteUids = [];
  bool _isMuted = false;
  bool _isVideoEnabled = true;

  @override
  void initState() {
    super.initState();
    _initAgora();
  }

  Future _initAgora() async {
    _engine = createAgoraRtcEngine();
    await _engine.initialize(const RtcEngineContext(
      appId: APP_ID,
      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
    ));

    _engine.registerEventHandler(
      RtcEngineEventHandler(
        onJoinChannelSuccess: (connection, elapsed) {
          debugPrint('成功加入频道: ${connection.channelId}');
        },
        onUserJoined: (connection, remoteUid, elapsed) {
          setState(() => _remoteUids.add(remoteUid));
        },
        onUserOffline: (connection, remoteUid, reason) {
          setState(() => _remoteUids.remove(remoteUid));
        },
      ),
    );

    await _engine.setClientRole(
      role: widget.isHost 
          ? ClientRoleType.clientRoleBroadcaster
          : ClientRoleType.clientRoleAudience,
    );

    await _engine.joinChannel(
      token: await _getToken(),
      channelId: widget.channelName,
      uid: 0,
      options: const ChannelMediaOptions(),
    );

    if (widget.isHost) {
      await _engine.startPreview();
      await _engine.enableVideo();
    }
  }

  Future _getToken() async {
    // 调用后端API获取动态Token
    final response = await http.post(
      Uri.parse('$API_URL/live/token/'),
      body: {'channel': widget.channelName}
    );
    return jsonDecode(response.body)['token'];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          _renderVideoViews(),
          _buildControlPanel(),
        ],
      ),
    );
  }

  Widget _renderVideoViews() {
    return widget.isHost
        ? AgoraVideoView(
            controller: VideoViewController(
              rtcEngine: _engine,
              canvas: const VideoCanvas(uid: 0),
            ),
          )
        : ListView.builder(
            itemCount: _remoteUids.length,
            itemBuilder: (context, index) => AgoraVideoView(
              controller: VideoViewController.remote(
                rtcEngine: _engine,
                canvas: VideoCanvas(uid: _remoteUids[index]),
                connection: const RtcConnection(channelId: ''),
              ),
            ),
          );
  }

  Widget _buildControlPanel() {
    return Positioned(
      bottom: 20,
      child: Row(
        children: [
          IconButton(
            icon: Icon(_isMuted ? Icons.mic_off : Icons.mic),
            onPressed: _toggleMic,
          ),
          IconButton(
            icon: Icon(_isVideoEnabled 
                ? Icons.videocam 
                : Icons.videocam_off),
            onPressed: _toggleVideo,
          ),
        ],
      ),
    );
  }

  void _toggleMic() {
    setState(() {
      _isMuted = !_isMuted;
      _engine.muteLocalAudioStream(_isMuted);
    });
  }

  void _toggleVideo() {
    setState(() {
      _isVideoEnabled = !_isVideoEnabled;
      _engine.muteLocalVideoStream(!_isVideoEnabled);
    });
  }

  @override
  void dispose() {
    super.dispose();
    _engine.leaveChannel();
    _engine.release();
  }
}

六、学习进度跟踪系统

1. 进度模型设计

python

复制

# progress/models.py
from django.db import models
from courses.models import Lesson
from django.contrib.auth import get_user_model

User = get_user_model()

class LearningProgress(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE)
    last_position = models.FloatField(default=0)  # 最后观看位置(秒)
    is_completed = models.BooleanField(default=False)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        unique_together = ('user', 'lesson')
        indexes = [
            models.Index(fields=['user', 'is_completed']),
            models.Index(fields=['lesson', 'is_completed'])
        ]

2. 进度同步API

python

复制

# progress/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import LearningProgress
from .serializers import ProgressSerializer

class ProgressAPI(APIView):
    def post(self, request):
        serializer = ProgressSerializer(data=request.data)
        if serializer.is_valid():
            lesson_id = serializer.validated_data['lesson_id']
            position = serializer.validated_data['position']
            completed = serializer.validated_data['completed']
            
            progress, created = LearningProgress.objects.update_or_create(
                user=request.user,
                lesson_id=lesson_id,
                defaults={
                    'last_position': position,
                    'is_completed': completed
                }
            )
            return Response({'status': 'success'})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

3. Flutter进度管理

dart

复制

// lib/widgets/video_progress.dart
class VideoProgressTracker extends StatefulWidget {
  final String lessonId;
  final VideoPlayerController controller;

  const VideoProgressTracker({
    super.key,
    required this.lessonId,
    required this.controller,
  });

  @override
  _VideoProgressTrackerState createState() => _VideoProgressTrackerState();
}

class _VideoProgressTrackerState extends State {
  Timer? _saveTimer;
  bool _isCompleted = false;

  @override
  void initState() {
    super.initState();
    _loadProgress();
    widget.controller.addListener(_handleVideoProgress);
  }

  Future _loadProgress() async {
    final response = await http.get(
      Uri.parse('$API_URL/progress/?lesson_id=${widget.lessonId}'),
      headers: await _getAuthHeaders(),
    );
    
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      widget.controller.seekTo(Duration(seconds: data['last_position'].toInt()));
      setState(() => _isCompleted = data['is_completed']);
    }
  }

  void _handleVideoProgress() {
    final position = widget.controller.value.position.inSeconds;
    final duration = widget.controller.value.duration.inSeconds;
    
    if (position > 0 && duration > 0) {
      // 每15秒保存一次进度
      if (position % 15 == 0) {
        _saveProgress(position);
      }
      
      // 完成状态判断(观看超过95%)
      if (position >= duration * 0.95 && !_isCompleted) {
        setState(() => _isCompleted = true);
        _saveProgress(position, completed: true);
      }
    }
  }

  Future _saveProgress(int position, {bool completed = false}) async {
    await http.post(
      Uri.parse('$API_URL/progress/'),
      headers: await _getAuthHeaders(),
      body: jsonEncode({
        'lesson_id': widget.lessonId,
        'position': position,
        'completed': completed || _isCompleted,
      }),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: widget.controller,
      builder: (context, value, child) {
        final position = value.position;
        final duration = value.duration;
        return LinearProgressIndicator(
          value: duration.inSeconds > 0 
              ? position.inSeconds / duration.inSeconds
              : 0,
          backgroundColor: Colors.grey[200],
          valueColor: AlwaysStoppedAnimation(
            _isCompleted ? Colors.green : Theme.of(context).primaryColor),
        );
      },
    );
  }

  @override
  void dispose() {
    _saveTimer?.cancel();
    widget.controller.removeListener(_handleVideoProgress);
    super.dispose();
  }
}

(未完待续,下一篇文章继续!)

相关推荐

「底层技术原理体系」深入探索Java服务器性能监控Metrics框架

承接上文承接上文中的【深度挖掘Java性能调优】「底层技术原理体系」深入探索Java服务器性能监控Metrics框架的实现原理分析(Counter篇),我们知道和了解了对应的Counter计数器的作用...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061...

6款可替代dreamweaver的工具(替代director的软件)

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

宇宙厂:Vue3.0 为何用 Proxy 替代 defineProperty?

大家好,很高兴又见面了,我是"...

专为Vue打造的开源表单验证框架,Github star7k+——VeeValidate

介绍vee-validate是Vue.js的基于模板的验证框架,可以验证输入并显示错误。基于模板,只需为每个输入值更改时指定应使用哪种验证器。系统会在支持40多种语言环境的情况下自动生成错误。现成的规...

自定义 Redis Starter 实现与扩展全解析

一、引言在SpringBoot开发中,Redis凭借其高性能和丰富的数据结构,成为缓存、分布式锁、消息队列等场景的首选解决方案。为了提升开发效率,避免重复配置和代码编写,我们可以创建一个自定义...

OpenAI Operator 的开源替代方案:Nanobrowser

Nanobrowser:OpenAIOperator的开源替代方案Nanobrowser[1]是一款开源的AI网页自动化工具,官方号称是OpenAIOperator的开源替代品。...

微软开始测试Edge AI历史搜索 为Windows 11提供更好的安全性

微软Edge浏览器正在获得更多与AI相关的功能。此外,Edge似乎正在试验WebPushAPI,这可以改善通过浏览器处理Windows11通知的方式。另一个值得注意的变化是与下载...

「Postman」测试(Tests)脚本编写和断言详解

测试确认您的API按预期工作,服务之间的集成运行可靠,并且新开发没有破坏任何现有功能。您可以使用JavaScript为PostmanAPI请求编写测试脚本。当您的API项目出现问题时...

NestJs 详细介绍及使用示例(nestjs视频教程)

NestJS详细介绍及使用示例如果觉得不错欢迎点赞、关注、转发、收藏,您的支持是我最大的动力,谢谢!...

angularjs应用prerender.io 搜索引擎优化实践

上一篇博文(http://www.cnblogs.com/ideal-lx/p/5625428.html)介绍了单页面搜索引擎优化的原理,以及介绍了两个开源框架的优劣。prerender框架的工作原理...

微软Edge浏览器测试“AI历史搜索”,口语化寻找历史标签页

IT之家3月4日消息,科技媒体WindowsLatest昨日(3月3日)发布博文,报道称微软正邀请Canary频道用户,在MicrosoftEdge浏览器中,测试AI...

async-validator 源码学习(一):文档翻译

async-validator是一个表单异步校验库,阿里旗下的Ant-design和Element组件库中的表单验证使用的都是async-validator,目前版本已更新到4.0.7...

HTML表单验证库SMValidator(html中表单验证)

SMValidator是一个易用、轻量且强大的表单验证工具。支持html和javascript两种配置方式,可以立即或手动触发验证,独立显示每条规则的信息,可自定义表单或信息容器的样式。目前minif...

Spring Boot 3.4+Wasm:让Java后端逻辑在浏览器中“狂奔”

在传统Web架构中,Java后端逻辑始终被禁锢在服务器端,依赖“请求-响应”模式与前端交互。这种架构在低并发场景下表现尚可,但在高并发、低延迟的业务场景中(如电商秒杀、实时竞价),其性能瓶颈日益凸显:...

取消回复欢迎 发表评论: