慕学在线教育平台项目总结
项目简介
项目描述
- 项目名称:慕学在线教育平台项目
- 架构:前后端不分离MVT
- 成果:在线教育平台的后台管理,包括:搜索、排序、过滤、导出、自定义页面等功能,前台功能
- 开发周期:2019/02/04-2020/03/30
- 参考链接:Django+xadmin打造在线教育平台
- 所用技术:
Python3.7
Django2.2
MySQL5.7
xadmin
redis - 收获:
需求分析
- 在线教育系统
数据库设计
需求分析
django的app设计
自定义userprofile覆盖django自带的user表
课程相关表设计
课程机构相关表结构设计
用户操作相关表结构设计登录和注册
图片验证码
账号和密码登录
手机动态验证码登录
手机号码注册
表单验证
通过session和cookie讲解登录原理课程机构管理模块
课程机构列表页展示
课程机构数据分页
课程机构筛选
课程机构排序
ajax+modelform实现异步数据提交
课程机构收藏
课程机构详情页展示
机构经典课程展示
热门机构推荐课程管理模块
课程列表页
课程数据分页
课程筛选
热门课程推荐
课程详情页展示
课程收藏
相关课程推荐
学习过该课程的同学还学习过的其他课程
课程评论
view的权限控制
视频播放页面
通过阿里云oss管理视频文件
课程章节信息展示讲师管理模块
讲师列表页
讲师详情展示
讲师的课程展示
讲师分享功能个人中心管理模块
个人信息展示
个人信息修改
头像修改
密码修改
手机号码修改
学习的课程展示
我的收藏 - 课程机构、讲师、课程
全局消息和个人消息首页和全局管理模块
首页信息展示
首页轮播图展示
全局搜索功能
自定义用户验证接口
自定义全局错误页面(404, 403, 500) - 后台管理系统(xadmin)
表的增、删、改、查和过滤的快速配置
自定义xadmin的全局信息
自定义xadmin的各种主题功能
自定义详情页面的布局
django自带的组和权限管理配置
自定义编辑和修改页面的表单
不同用户进入列表页面的数据过滤
重载数据修改后的逻辑控制
同一张表的不同数据使用不同的管理器管理
将图片显示在列表页
配置只读,排除和默认排序字段
修改不同的表的图标展示
使用inline在一个表显示多个表的数据
让讲师可以登录后台系统并过滤数据
集成ueditor富文本编辑功能到xadmin中
配置xadmin的导出功能(可以只导出选择的数据)
配置xadmin的导入功能(可以导入各种格式的数据)
开发过程
创建虚拟环境
- 方式1
makevirtualenv -p C:\Users\caoyang\AppData\Local\Programs\Python\Python37\python.exe mxonline
(报错)
新建的虚拟环境默认放在C:Users/caoyang/Envs
- 方式2
python -m venv mxonline
在哪个文件夹下创建虚拟环境就在哪个文件夹下,我用的这种方式,但是后来发现上面的方式更好 - 安装Django
pip install django==2.2 -i https://pypi.douban.com/simple
Django会默认依赖一些第三方的包,如sqlparse,pytz
新建项目
-
直接在在pycharm中新建Django项目
- 使用命令新建项目和应用
新建项目django-admin startproject MxOnline
新建应用python manage.py startapp courses
新建应用后需要在settings.py文件中的INSTALLED_APPS添加'apps.courses.apps.CoursesConfig',
-
目录结构
其实最好不要最外层目录,把文档和环境直接放到项目中去,否则不能debug
-
包文件
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt Django 2.2 mysqlclient pillow
配置一个HTML页面显示的步骤
- 配置url
- 配置对应的views逻辑
- 拆分静态文件(css,js,image)放入到static,html放到template之下
可以放到对应的app下面static
也可以放入全局的static(推荐这种) -
配置全局的static文件访问路径
STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ]
数据库相关操作
- 新建数据库mxonline
字符集utf8 -- UTF-8 Unicode
排序规则utf8_general_ci
-
配置mysql引擎
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': "mxonline", 'USER': "root", 'PASSWORD': "Mm123456", 'HOST': "localhost" } }
- 安装mysql驱动
访问Unofficial Windows Binaries for Python Extension Packages搜mysqlclient
,下载python3.7 64位版本(这个版本是python的版本,不是操作系统的版本)
进入下载目录,进入虚拟环境pip install mysqlclient-1.4.2-cp37-cp37m-win_amd64.whl
(其实可以直接安) - 生成Django自带的表
makemigration
迁移文件
migrate
生成数据表
只要models.py改了就要重新迁移 - 在models.py设计表
__str__()
函数是当我们print这个类的对象的时候,字符串的描述是什么,return的值必须是存在的
像性别这种可选范围的可以使用choices参数
upload_to="course/%Y/%m"
参数可以设置文件保存路径
设置默认当前时间default=datetime.now
,now后面一定不能有括号,否则就直接调用了
所有表中都有的字段可以放到BaseModel(放在最底层users的model类中)中去,为了不让BaseModel生成一张表,要在它的Meta中设置abstract = True
时长要精确到分钟数
外键必须指明on_delete,on_delete=models.CASCADE
级联删除,on_delete=models.SET_NULL
设为空值(必须有null=True,blank=True
)
from django.contrib.auth import get_user_model
可以得到UserProfile来自哪一个class(自定义的还是Django自带的)
多对多关系先设计成一对多的 - 数据库操作
queryset:1.进行for循环 2.进行切片
queryset本身并没有执行SQL操作,delete进行删除操作
print(查询结果.query)
可以查对应的SQL语句
get()方法返回的是一个对象,不是queryset,进行数据操作,数据不存在或有多条数据存在会抛出异常xxx matching query dose not exist
save()存在则更新,不存在则保存 -
从前端提取数据并保存到数据库中
<form action="" method="post"> <input type="text" name="message"> <input type="submit" class="button" value="提交"/> { % c s r f _ t o k e n % } </form>
if request.method == "POST": message_text = request.POST.get("message","") # message是表单里的name message = Message() # message是数据库对象,变量名不能与对象名重复 message.message = message_text # 第二个message是数据库中的字段 message.save() return render(request,"message_form.html",{ "message":message })
-
Django的template数据展示
if request.method == "GET": var_dict = {} all_messages = Message.objects.filter() if all_messages: message = all_messages[0] var_dict = { "message":message } return render(request,"message_form.html",var_dict)
后台开发
自定义UserProfile覆盖Django默认的user表
from django.contrib.auth.models import AbstractUser
导入django自带的user类- 在settings.py文件中设置
AUTH_USER_MODEL="users.UserProfile"
,此时还需要安装 pip install pillow
通过admin搭建后台管理系统
- settings.py的
INSTALLED_APPS
中一定要有'django.contrib.admin',
- 总路由中的
urlpatterns
中加入path('admin/', admin.site.urls),
-
修改语言设置
LANGUAGE_CODE = 'zh-Hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False
-
在users的admin.py中注册表
from django.contrib import admin from django.contrib.auth.admin import UserAdmin # 密码加密用的 from apps.users.models import UserProfile class UserProfileAdmin(admin.ModelAdmin): pass #用户表和用户管理表关联注册 #admin.site.register(UserProfile, UserAdmin)
-
在users的apps.py中把app的名称改成中文
from django.apps import AppConfig class UsersConfig(AppConfig): name = 'apps.users' verbose_name = "用户"
xadmin的配置步骤
- 下载xadmin源码,放到项目根目录之下
- 在settings的
INSTALLED_APPS
中添加crispy_forms
和xadmin.apps.XAdminConfig
- 安装xadmin的依赖包(在xadmin的requirements.txt里面)
- 通过migrate生成xadmin需要的表
- 在urls中配置访问地址
path('xadmin/', xadmin.site.urls),
xadmin的使用
-
xadmin的全局配置
class GlobalSettings(object): site_title = "慕学后台管理系统" site_footer = "慕学在线网" #menu_style = "accordion" # 侧边栏收起 class BaseSettings(object): #切换换主题的 enable_themes = True use_bootswatch = True xadmin.site.register(xadmin.views.CommAdminView, GlobalSettings) xadmin.site.register(xadmin.views.BaseAdminView, BaseSettings)
-
xadmin搭建后台管理系统
import xadmin from apps.courses.models import Course from xadmin.layout import Fieldset, Main, Side, Row class LessonInline(object): model = Lesson #章节表 style = "tab" #左右切换 extra = 0 #不自动新建 exclude = ["add_time"] #去除 class CourseResourceInline(object): model = CourseResource style = "tab" extra = 1 #自动新建一个tab #自定义课程,还需要在INSTALLED_APPS中加入import_export,把xadmin/plugins/__init__.py中的'export'注释掉 from import_export import resources class MyResource(resources.ModelResource): class Meta: model = Course #fields = ('name', 'description',) #exclude = () class NewCourseAdmin(object): #设置列表页样式 import_export_args = {'import_resource_class': MyResource, 'export_resource_class': MyResource} #数据的导入和导出 list_display = ["name", "desc", "show_image", "go_to", "detail", "degree", "learn_times", "students"] #列表页显示的字段 search_fields = ["name", "desc", "detail", "degree", "students"] #搜索字段 list_filter = ["name", "teacher__name", "desc", "detail", "degree", "learn_times", "students"] #过滤字段,对于外键的过滤用双下划线 list_editable = ["desc", "degree"] #列表页可编辑字段 readonly_fields = ["click_nums", "students", "add_time"] # 只读字段 #exclude = [ "fav_nums"] # 去除字段,和其他字段不要冲突 ordering = ["-click_nums"] # 列表页默认排序 model_icon = 'fa fa-paper-plane' # 图标去http://www.fontawesome.com.cn/faicons/ inlines = [LessonInline, CourseResourceInline] #新增课程,下面可直接新增章节和资源 style_fields = { "detail":"ueditor" } #只返回当前用户的数据 #在Teacher表中添加一个字段 #from apps.users.models import UserProfile #class Teacher(BaseModel): # user = models.OneToOneField(UserProfile,on_delete=models.SET_NULL,null=True,blank=True,verbose_name="用户") def queryset(self): qs = super().queryset() #默认所有数据 if not self.request.user.is_superuser: qs = qs.filter(teacher=self.request.user.teacher) return qs #自定义页面布局 def get_form_layout(self): if self.org_obj: # 设置编辑页面是自定义样式,新增是默认的 self.form_layout = ( Main( #一个Fieldset就是一块 Fieldset("讲师信息", 'teacher', 'course_org', css_class='unsort no_title' ), Fieldset("基本信息", 'name', 'desc', Row('learn_times', 'degree'), Row('category', 'tag'), 'youneed_know', 'teacher_tell', 'detail' ), ), Side( Fieldset("访问信息", 'fav_nums', 'click_nums', 'students', 'add_time' ) ), Side( Fieldset("选择信息", 'is_banner', 'is_classics' ) ), ) return super(NewCourseAdmin, self).get_form_layout() xadmin.site.register(Course, NewCourseAdmin)
-
重载save_models方法控制保存和修改数据的逻辑
class UserCourseAdmin(object): list_display = ["user", "course", "add_time"] search_fields = ["user", "course"] list_filter = ["user", "course", "add_time"] #用户学习一门课程,课程的学习人数加一 def save_models(self): obj = self.new_obj #新增一条记录是会生成一个对象,当前并没有保存到数据库中 if not obj.id: #没有id的时候是新增 obj.save() course = obj.course course.students += 1 course.save()
-
同一张表的不同数据使用不同的管理器进行管理
#在models.py中新建轮播课程的model类 class BannerCourse(Course): class Meta: verbose_name = "轮播课程" verbose_name_plural = verbose_name proxy = True # 多个管理器管理一张表,不新增一张表 #在adminx.py中管理轮播课程 from apps.courses.models import BannerCourse class BannerCourseAdmin(object): list_display = ["name", "desc", "detail", "degree", "learn_times", "students"] search_fields = ["name", "desc", "detail", "degree", "students"] list_filter = ["name", "teacher__name", "desc", "detail", "degree", "learn_times", "students"] list_editable = ["desc", "degree"] def queryset(self): qs = super().queryset() qs = qs.filter(is_banner=True) return qs xadmin.site.register(BannerCourse, BannerCourseAdmin)
-
通过在model中定义方法将图片显示在列表页
#显示图片而不是路径,list_display中加入show_image def show_image(self): from django.utils.safestring import mark_safe return mark_safe("<img src='{}'>".format(self.image.url)) show_image.short_description = "图片" #添加链接,list_display中加入go_to def go_to(self): from django.utils.safestring import mark_safe return mark_safe("<a href='/course/{}'>跳转</a>".format(self.id)) go_to.short_description = "跳转"
DjangoUediter富文本编辑器
- 将djangoueditor源码拷贝到项目根目录下
- INSTALLED_APPS 中配置 ‘DjangoUeditor’
- 配置相关的url:
url(r’^ueditor/’,include(‘DjangoUeditor.urls’)), - 修改相关models.py
detail = UEditorField(verbose_name="课程详情", width=600, height=300, imagePath="courses/ueditor/images/",filePath="courses/ueditor/files/", default="")
- 下载ueditor插件并放置到xadmin源码的plugins目录下
- 将editor文件名配置到plugins目录下的__init__.py文件的PLUGINS变量中
-
在对应的model的管理器adminx.py中配置:
style_fields = { "detail":"ueditor" }
detail表示model中富文本的字段
- 前端接收时用
{ % autoescape off % }{ { course.detail } }{ % endautoescape % }
- 文档看 https://github.com/zhangfisher/DjangoUeditor
前台开发
通过Django内置的login和logout完成账号登录和退出登录
-
登录逻辑
-
views.py代码实现
from django.shortcuts import render #返回HTML页面 from django.views.generic.base import View #所有视图函数都继承View from django.contrib.auth import authenticate, login, logout #authenticate判断用户是否存在 from django.http import HttpResponseRedirect, JsonResponse #返回HTML页面 from django.urls import reverse #重定向 from apps.users.forms import LoginForm #登录注册表单验证 #退出登录 class LogoutView(View): def get(self, request, *args, **kwargs): logout(request) return HttpResponseRedirect(reverse("index")) class LoginView(View): def get(self, request, *args, **kwargs): #判断用户是否登录 if request.user.is_authenticated: return HttpResponseRedirect(reverse("index")) next = request.GET.get("next", "") def post(self, request, *args, **kwargs): #表单验证,实例化LoginForm login_form = LoginForm(request.POST) banners = Banner.objects.all()[:3] if login_form.is_valid(): #获取用户名和密码 #user_name = request.POST.get("username", "") #password = request.POST.get("password", "") #验证字段,放到forms.py中避免代码冗余 user_name = login_form.cleaned_data["username"] password = login_form.cleaned_data["password"] #用于通过用户名和密码查询用户是否存在 user = authenticate(username=user_name, password=password) #1.只通过用户名查找用户不对,还得密码校验 #2.需要先加密再通过加密后的密码查询,扩展性不好 #from apps.users.models import UserProfile #user = UserProfile.objects.get(username=user_name, password=password) if user is not None: #查询到用户 login(request, user) #登录成功之后应该怎么返回页面 next = request.GET.get("next", "") if next: return HttpResponseRedirect(next) return HttpResponseRedirect(reverse("index")) #reverse重定向,如果用render的话路由还是login else: #未查询到用户 return render(request, "login.html", {"msg": "用户名或密码错误", "login_form": login_form, "banners": banners}) else: return render(request, "login.html", {"login_form": login_form, "banners": banners})
-
forms.py代码实现
from django import forms #所有表单都需要继承form里的Form类 class LoginForm(forms.Form): #账号密码登录 #required=True是否为必填字段,字段名必须与input的name属性保持一致 username = forms.CharField(required=True, min_length=2) password = forms.CharField(required=True, min_length=3)
动态验证码登录
常见web攻击
SQL注入攻击
- SQL注入攻击的危害
非法读取、篡改、删除数据库中的数据
盗取用户的各类敏感信息,获取利益
通过修改数据库来修改网页上的内容
注入木马 -
SQL注入攻击与防范示例
import MySQLdb username = "' OR 1=1 #" #井号在SQL语句中是注释的意思,相当于username='' or 1=1,所以无论密码是什么都可以查到数据 password = "pbkdf2_sha256$150000$rHVW33UGSs2H$Ld3gPBExyTeglCWFxTbDYlNs0E98Dp8jStusgZQak1s=" conn = MySQLdb.connect(host="127.0.0.1", user="root", passwd="Mm123456", db="mxonline", charset="utf8") #建立连接 cursor = conn.cursor() sql = "select * from users_userprofile where username='{}' and password='{}'".format(username,password) #print(sql) cursor.execute(sql) #执行SQL语句 for row in cursor.fetchall(): print(row)
- 防护措施
表单验证
查询用户的逻辑
django的orm会对特殊字符转义, orm会确保用户输入数据的安全性
xss攻击原理及防范
- xss跨站脚本攻击(Cross Site Scripting)的危害
盗用各类用户账号,如用户网银账号/各类管理员账号
盗窃企业重要的具有商业价值的资料
非法转账
控制受害者机器向其他网站发起攻击,注入木马等 -
xss攻击的流程
- 防护措施
首先代码里对用户输入的地方和变量都需要仔细检查长度和对<>;'
等支付做过滤
避免直接在cookie中泄露用户隐私,例如email/密码登,通过是cookie和系统IP绑定来降低cookie泄露后的危险
尽量采用POST而非GET提交表单
csrf攻击与防范
- csrf跨站请求伪造(Cross-site request forgery)的危害
以你名义发送邮件
盗用你的账号
购买商品
虚拟货币转账 -
csrf攻击原理
- 防护措施:在form里加上
{ % c s r f _ t o k e n % }
难点
-
数据库设计,避免循环引用 分层设计:上层可以引用下一层,下层不能引用上一层,层级之间可以互相引用
常识
- xadmin中方法前有个
@filter_hook
的都可以重载 - 文件重命名时选中
Search for references
其他引用的地方都会相应修改 -
from django.views.generic import TemplateView
,调用它的as_view()方法可以不用写后台的方法直接返回页面from django.urls import path from django.views.generic import TemplateView # 不用写后台方法的类 urlpatterns = [ #所有的View类都有一个as_view()方法,调用这个方法之后就会产生一个def,就像我们自己定义的方法, 想返回一个HTML的话就配置个template_name="index.html"参数 path('', TemplateView.as_view(template_name="index.html"), name="index"),
-
配置静态文件
#在settings.py中配置 STATIC_URL = '/static/' #自定义 不能和STATIC_ROOT同时存在 STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ] #上传文件都放到media里 MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #STATIC_ROOT = os.path.join(BASE_DIR, 'static') #上线时取消注释 #一定要配置总路由 from Mxonline.settings import MEDIA_ROOT from django.views.static import serve urlpatterns = [ # 配置上传文件的访问url url(r'^media/(?P<path>.*)$', serve, {"document_root": MEDIA_ROOT}), ] 在HTML文件中导入静态文件用`"{ % static 'css/style.css' % }"`或`"/static/css/style.css"`,推荐第一种 导入服务器上传的静态文件用`{ { course.image.url } }`或`{ { MEDIA_URL } }{ { course.image } }`,推荐第一种
{ % if user.is_authenticated % }
判断用户是否已经登录
settings.py中TEMPLATES -> ‘OPTIONS’ -> ‘context_processors’里面是所有页面中都应传入的全局变量,所以HTML中可以直接拿到request和user等
通过看有没有sessionid判断登录状态
bug
Django版本问题
批注掉就完了
django2.2/mysql ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you have 0.9.3
django报错:render() got an unexpected keyword argument ‘renderer’
注销模型
错误原因:UserProfile模型跟系统的冲突了,但是我自定义了显示的列,所以就要重新关联,关联之前要把系统的注销掉,如果类内部用默认的可以不用注册
解决方案:‘The model %s is already registered’ % model.name) xadmin.sites.AlreadyRegistered: The model Us
import xadmin
from .models import UserProfile
#xadmin中这里是继承object,不再是继承admin
class UserProfileAdmin(object):
'''
管理员信息
'''
#显示的列
list_display = ['nick_name', 'birthday', 'gender', 'address','mobile','image','add_time']
#搜索的字段,不要添加时间搜索
search_fields = ['nick_name', 'birthday', 'gender', 'address','mobile']
#过滤
list_filter = ['nick_name', 'birthday', 'gender', 'address','mobile']
#将管理器与model进行注册关联
xadmin.site.unregister(UserProfile)
xadmin.site.register(UserProfile, UserProfileAdmin)
安装mysql驱动
解决“raise ValueError(“Dependency on app with no migrations: %s” % key[0]) ValueError: Dependency on ”
windows系统Django字符集错误
UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xa6 in position 9737: ill….
admin密码是明文
注册的时候加个它from django.contrib.auth.admin import UserAdmin # 密码加密用的
,然后用户表和用户管理表关联注册admin.site.register(UserProfile, UserAdmin)
get方法访问对象报xxx matching query dose not exist
原因:数据不存在或有多条数据
解决方法:
try:
message = Message.objects.get(name="bobby1")
except Message.DoesNotExist as e:
pass
except Message.MultipleObjectsReturned as e:
pass
The view xxx didn’t return an HttpResponse object.’
view中任何一个代码分支下都要render一个页面,POST方法也得返回
新增用户报错Duplicate entry ‘’ for key ‘mobile’
原因: django自带的新增用户只有用户名,密码和确认密码,但是用户表里mobile列设置了唯一约束,默认为空,创建超级用户的时候手机号默认为空了,后面再创建用户手机号就不能为空了
解决方法:把唯一约束去掉,或者修改django源码(不推荐)
导入静态文件路径static
前必须有/
如<link rel="stylesheet" type="text/css" href="/static/css/style.css">
/static
和settings.py
文件中的STATIC_URL = '/static/'
是对应的
如果不加/
,浏览器就会把/static/css/style.css
当成路由去解析成http://127.0.0.1:8000/static/css/style.css
就会报404
图片验证码
Redis缓存
下载redis-latest.zip后解压,进入文件夹运行redis-server.exe
启动服务,再打开一个命令行运行redis-cli.exe
连接服务
redis.exceptions.ConnectionError: Error 10061 connecting to 127.0.0.1:6379. 由于目标计算机积极拒绝,无法连接。 Redis服务没开
不能根据add_time排序
add_time统一写到user的models.py里面了,需要导入并继承BaseModel
视频播放不了
可能是因为model类中视频路径字段长度不够,给截断了
405错误
浏览器请求的方法错误,检查视图函数中get方法和post方法使用是否正确
其他错误
解决“raise ValueError(“Dependency on app with no migrations: %s” % key[0]) ValueError: Dependency on ”
UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xa6 in position 9737: ill….