一、系统架构设计
复制
在线教育系统架构:
Flutter前端 (Mobile/Web) <-> Django REST API <-> PostgreSQL数据库
↑
(JWT认证)
二、后端开发(Python Django)
- 项目初始化
bash
复制
pip install django djangorestframework djangorestframework-simplejwt
django-admin startproject edu_platform
cd edu_platform
python manage.py startapp courses
- 数据库模型 (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}"
- 序列化器 (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']
- 视图 (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)
- 认证配置 (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前端开发
- 项目初始化
bash
复制
flutter create edu_app
cd edu_app
flutter pub add provider http cached_network_image video_player dio flutter_secure_storage
- 核心模型 (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'],
);
}
}
- 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';
}
}
}
- 状态管理 (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();
}
}
}
- 核心界面 (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),
),
),
);
},
),
),
],
),
),
],
),
),
);
}
}
- 视频播放组件 (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}{k}>")
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();
}
}
(未完待续,下一篇文章继续!)