一、开发环境的安装与配置
1.1 Python3.10以及PyCharm的安装
浏览器输入Python官方网址:https://www.python.org/,点击下载进入下载界面,选择相对应的版本,本项目采用的是win64位的Python3.10版本。安装PyCharm步骤与上述一致,仅网址不同,本项目采用的是Pycharm2021专业版。
1.2MySQL8.0的安装
MySQL的安装与上述如出一辙,在浏览器地址栏输入MySQL官方网址,进入下载界面,选择相对应版本。本次项目采用的是win64位的MySQL8.0版本。
二.设计任务及理解
2.1设计任务
本项目的任务是基于flask模拟雨课堂教学系统的实现,前端页面的设计需参考雨课堂的界面,设计实现课程班级模块中的“我听的课”功能,能够创建课程信息,并提交。设计实现资源库中上传课件的功能,并显示课程上传时间,以及右上角数量汇总信息,同时可以实现删除的效果。
2.2 理解
对于本次项目,我对设计任务的理解的内容有:首先是本项目要基于flask框架,其次就是前端的页面,即使设计任务里没有提及到关于用户登录和注册的功能,为了设计的严谨性,这一功能是要添加上的;紧接着就是实现主页中的功能,可以连接MySQL,在MySQL中创建数据库,并与后端相连,再将数据传入前端页面中,可将用户名和班级信息进行绑定,从而达到设计的严谨性。
三、所用第三方库简介
3.1 Flask框架简介及所需要的库
Flask是一个非常轻量级的框架,提供了搭建Web服务的必要组件,Flask具有良好的扩展性,可以使用其他开源的Flask扩展插件。Flask框架里主要包含Jinja2模板引擎、路由、视图、静态文件和蓝图等。在本次项目中用到的有render_template、request、session、redirect、Jinja2模板引擎、路由、视图以及静态文件。
3.2 pymysql库
pymysql是在pycharm上使用的第三方包,在安装pymysql之前,首先必须要确保电脑上安装了MySQL。pymysql是作为MySQL客户端来操作数据,它的操作流程如图1所示。
图1 pymysql操作流程
四、设计方案与实现
4.1 项目结构
本次软件设计我们的项目结构如图2所示,static文件夹分别存放CSS文件、JS文件、图片以及上传的课件。templates文件夹存放模块文件,模板即在Flask中允许响应给用户看的网页,Flask中的模板是依赖于Jinja2的模板系统。默认情况下,Flask会在程序文件夹中的templates的子文件夹中搜索模板,我们需要手动创建templates文件夹。本次项目中,templates中共有4个模板文件,用于登录界面、注册界面、主界面以及上传界面的展示。app.py文件为后端的接口,里面配置了视图函数和路由。userdata.py文件为业务逻辑模块,用于连接数据库以及编写了相对应的方法。
图2 项目结构图
4.2 userdata.py与app.py设计方案
userdata模块中我们定义了三个类。User类(父类)主要用于用户的登录和注册,并将注册的数据保存到数据库中,以及查询用户所在的班级;Course_class类(子类)主要用于查询同一班级下的班级成员以及用户加入班级;Course类(子类)主要用于查询用户听的可以及添加课程。app.py模块中,我们主要定义了7个接口即视图函数,具体如图3所示,每个视图函数还会配置对应的路由。同时为了保护用户的隐私安全,我们还在该模块中设置了秘钥,否则将会抛出异常。
图3 app.py视图函数
4.3 数据库的设计
在本次项目中,我们新建了用于存储用户数据的数据库“yeketang”,该数据库如下图4所示,图中有三张表分别为user用户表、class班级表、以及course用户课程表。user表中有username、password字段;class表中有username、classid字段;course表中有username、courseid、coursename字段。
图4 数据库搭建
4.4 登录和注册的功能实现
接下来我们对登录和注册功能的实现,首先如图5所示为userdata模块下的User类中的登录和注册的流程图。在登录login()方法中,我们通过执行SQL语句来实现此功能。我们在用fetchone()函数获取数据库读取到的结果,返回的数据类型为元组类型,随后添加if条件语句,判断用户输入的密码是否在这个元组中,如果在元组中则返回1。如图6所示为app接口模块下的登录注册流程图。在HTTP中,常见的请求方法有GET和POST。GET请求中的参数包含在URL里面,数据可以在URL中看到,而POST的请求的URL不会包含这些数据;同时GET请求提交的数据最多只有1024字节,而POST方式没有限制。因此为了防止用户输入的密码泄露,我们在login()接口中,我们添加了if条件语句进行判断,如果是GET方法,我们就展示页面;如果是POST方法,就通过request.form.get()函数获取到前端页面输入的值,接着创建实例化对象并调用登录login()方法,流程如图8所示。最后我们添加一个if条件判断语句,判断login()方法返回的值是否为1,如果为1则说明登录成功,我们便将页面重定向至index主界面,否则就返回“账号密码错误,请重新登录”。对于注册功能的实现,与上述登录功能基本类似。首先在接口模块中先判断HTTP的请求方法。GET方法就展示页面,在POST方法下,获取到前端注册页面的username、password,接着创建实例化对象,并将username、password传入对象中,再调用register()方法执行SQL语句,将username、password存入数据库中,最后再将页面重定向至登录界面。
图5 userdata模块登录注册流程图
图6 app模块登录注册流程
userdata.py
# 定义用户类class User: def __init__(self, username, password): self.username = username self.password = password # 登录账号 def login(self): # ping()使用该方法 ping(reconnect=True) ,那么可以在每次连接之前,会检查当前连接是否已关闭,如果连接关闭则会重新进行连接。 db.ping(reconnect=True) # 编写sql语句,用来查询前端输入的用户名与数据库中user表对应的密码 sql = "select password from user where username='" + self.username + "'" # 执行sql语句 cursor.execute(sql) # 将数据从数据库读出 类型为元组类型 results = cursor.fetchone() # 判断前端输入的密码是否与数据库密码一致,一致则返回1 if self.password in results: return 1 # 关闭数据库 db.close() # 注册账号 def register(self): # ping()使用该方法 ping(reconnect=True) ,那么可以在每次连接之前,会检查当前连接是否已关闭,如果连接关闭则会重新进行连接。 db.ping(reconnect=True) # 插入sql语句 sql_0 = "INSERT INTO user(username,password) VALUES(%s,%s)" sql = sql_0 % (repr(self.username), repr(self.password)) # 执行sql语句 cursor.execute(sql) # 提交到数据库执行 db.commit() # 关闭数据库 db.close()
app.py
# 登录接口@app.route('/login', methods=['GET', 'POST'])def login(): if request.method == 'GET': return render_template('login.html') if request.method == 'POST': username = request.form.get('username') # 接收来自前台的账号 password = request.form.get('password') # 接收来自前台的密码 user = ud.User(username, password) logins = user.login() if logins == 1: # 将用户名存储至用户会话中,用户会话是一种私有存储,默认情况下,会保存在cookie中。 session['username'] = username return redirect('/index') else: return '账户密码错误,请重新登录'# 注册接口@app.route('/register', methods=['GET', 'POST'])def register(): # 判断是get请求还是post请求 if request.method == 'GET': return render_template('register.html') if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = ud.User(username, password) user.register() return redirect('/login')
4.5 展示用户班级及班级成员功能的实现
接着我们对展示用户班级及班级成员的功能进行实现,具体流程如图7所示。首先我们需要通过用户会话来获取到用户在登录界面输入的用户名,调用show_user()方法时,还需要判断用户是否已经加入班级,定义一个变量results用于接收查询到的结果。如果数据库查不到记录,我们将results赋值为None,并返回results;反之我们将查询到的记录赋值给results并转为字符串,将结果返回,展示班级成员的方法与上述类似,这里就不展开说明了。最后在主接口中,判断返回的结果是否为None,是则添加班级,否则展示。接着再将后端数据传入前端数据。同时我们前端页面运用Jinja2模板进行判断,根据结果是否为None,展示不同的内容。
图7 用户班级流程图
userdata.py
# 查询用户所在班级的方法 def show_user(self): # ping()使用该方法 ping(reconnect=True) ,那么可以在每次连接之前,会检查当前连接是否已关闭,如果连接关闭则会重新进行连接。 db.ping(reconnect=True) # 插入sql语句 sql = "SELECT classid FROM class WHERE username='" + self.username + "'" #执行sql语句,对该用户在class表中没有记录抛出异常,并将返回的结果赋值为None try: # 执行sql语句 cursor.execute(sql) # 将数据从数据库读出 类型为元组类型 转成字符串 results = ''.join(cursor.fetchone()) #关闭数据库 db.close() return results except: results=None db.close() return results# 定义课程班级类 班级类继承用户类class Course_class(User): def __init__(self, username, password, class_id): super().__init__(username, password) self.class_id = class_id # 查询用户所在的班级成员的方法 def show_class_member(self): # ping()使用该方法 ping(reconnect=True) ,那么可以在每次连接之前,会检查当前连接是否已关闭,如果连接关闭则会重新进行连接。 db.ping(reconnect=True) if self.class_id==None: results1=[] else: # 插入sql语句 sql = "SELECT username FROM class WHERE classid='" + self.class_id + "'" # 执行sql语句 cursor.execute(sql) # 将数据从数据库读出 类型为多个元组 results = cursor.fetchall() # 定义一个空列表 results1 = [] # 遍历元组中每个元素,将每个元素转化为字符串并添加至列表中 for item in results: results1.append(''.join(item)) # 关闭数据库 db.close() # 返回结果 return results1 #加入班级 def add_class(self): # ping()使用该方法 ping(reconnect=True) db.ping(reconnect=True) #编写sql语句 sql_0 = "INSERT INTO class(username,classid) VALUES(%s,%s)" sql = sql_0 % (repr(self.username), repr(self.class_id)) cursor.execute(sql) # 提交到数据库执行 db.commit() # 关闭数据库 db.close()
app.py代码见下一部分
4.6 展示用户课程及添加课程功能实现
接下来我们对用户课程及添加课程功能进行实现。该功能实现要比上述展示用户班级功能要简单的多。因为课程可以重复添加多次。用户可以学很多的课程,用户也可以不学任何的课程;而对于班级而言,用户仅可以添加一次。所以图中对于展示用户课程以及添加课程功能并没有对此进行判断。由于我们需要在前端页面展示用户的课程号以及课程名,我们将会数据库中返回的元组转成字典,再将字典添加到列表中,最后再模板文件中用Jinja2模板,通过for循环遍历列表,展示到前端页面中。
app.py
# 主界面接口(显示用户登录的用户名即个人信息)@app.route('/index', methods=['GET', 'POST']) # 这里写入的是接口名称,即URL地址def main_interface(): # 从用户会话中获取到用户名 username = session.get('username') # 获取用户在前端输入的课程号 course_id = request.form.get('course_id') # 获取用户在前端输入的课程名 course_name = request.form.get('course_name') # 创建实例化对象user user = ud.User(username, '') # 调用方法,获取用户的班级号 class_id = user.show_user() # 创建Course_class类中的实例化对象 course_class = ud.Course_class(username, '', class_id) # 对班级号进行判断,如果班级号为空,并且是POST请求,则将前端数据赋值给该用户 if class_id == None: if request.method == 'POST': class_id1 = request.form.get('class-id') # 赋值完成后,重新创建Course_class类实例化对象 course_class1 = ud.Course_class(username, '', class_id1) # 调用方法,将班级号加到数据库中 course_class1.add_class() return redirect('/index') # 调用展示班级成员的方法 class_member = course_class.show_class_member() # 创建Course类中的实例化对象 course = ud.Course(username, '', course_id, course_name) if request.method == 'POST': course.add_course() course_list = course.search_course() # 将用户名 班级号 班级成员 课程列表传入前端 return render_template('index.html', username=username, class_id=class_id, class_member=class_member, course_list=course_list)
userdata.py
# 定义课程类 课程类继承于用户类class Course(User): def __init__(self, username, password, course_id, course_name): super().__init__(username, password) self.course_id = course_id self.course_name = course_name # 定义查询课程的方法 def search_course(self): # ping()使用该方法 ping(reconnect=True) ,那么可以在每次连接之前,会检查当前连接是否已关闭,如果连接关闭则会重新进行连接。 db.ping(reconnect=True) # 插入sql语句 sql = "SELECT courseid,coursename FROM course WHERE username='" + self.username + "'" # 执行sql语句 cursor.execute(sql) # 将数据从数据库读出 类型为多个元组 results = cursor.fetchall() # 定义一个空列表 course_list = [] # 将返回的元组通过遍历转成字典,最后再将字典存入列表中 for items in results: results1 = [items] for item in results1: course_dict = {item[0]: item[1]} course_list.append(course_dict) # 关闭数据库 db.close() return course_list # 添加课程并加入到数据库 def add_course(self): # ping()使用该方法 ping(reconnect=True) db.ping(reconnect=True) # 插入sql语句 sql_0 = "INSERT INTO course(username,courseid,coursename) VALUES(%s,%s,%s)" sql = sql_0 % (repr(self.username), repr(self.course_id), repr(self.course_name)) # 执行sql语句 cursor.execute(sql) # 提交到数据库执行 db.commit() # 关闭数据库 db.close()
4.7 上传文件及删除文件功能实现
最后我们对上传文件及删除文件功能进行实现,该功能大致流程示意图如图13所示。首先前端页面选择本地磁盘下需要上传的文件,点击上传按钮。在上传接口中我们通过request.file获取到相应的文件,将其存放在static文件夹的uploads文件夹中。接着通过os.walk()函数遍历uploads文件夹下的所有文件,将其存放到列表中,遍历该列表,通过os.path.getctime来获取每个文件的创建时间即上传时间,将其保存至列表中。最后将两个列表通过zip()函数打包至前端页面,前端页面再将其遍历,右上角的数量汇总信息则通过列表的长度来显示。删除文件是在展示上传文件时,每条记录后面添加一个“删除”的a标签,该标签的URL地址为每条记录的文件名。在app接口模块添加相关的视图函数,路由的URL为该文件名。获取需要删除的文件名调用os.remove()函数将其删除,删除完之后再重新,流程如图8所示。至此,本次项目所有功能都已经实现。
图8 上传文件与删除文件
app.py
@app.route('/upload', methods=['POST'])def upload_file(): file_list = [] file_time = [] if request.method == 'POST': # 获得前端上传的文件 f = request.files['file'] # 将其保存在static下的uploads文件夹中 f.save(path.join(app.config['uploads'], f.filename)) # 对uploads文件夹下所有的文件进行遍历 for item in os.walk(app.config['uploads']): # os.walk函数执行后,会产生(root,dirs,files)的三元组 # root所指的是当前正在遍历的这个文件夹的本身的地址 # dirs是一个 list,内容是该文件夹中所有的目录的名字(不包括子目录) # files 同样是 list,内容是该文件夹中所有的文件(不包括子目录) for items in item[2]: file_list.append(items) for items1 in file_list: # 获取上传文件时的时间,并对其格式化 a = os.path.getctime(app.config['uploads'] + f'/{items1}') b = time.localtime(a) c = time.strftime("%Y-%m-%d %H:%M:%S", b) file_time.append(c) # 将两个列表打包,方便前端遍历展示 zip_list = zip(file_list, file_time) return render_template('upload.html', zip_list=zip_list, file_list=file_list)# 删除上传的文件@app.route("/delete/<file>")def delete_file(file): file_list = [] file_time = [] # 需要删除的文件路径 path = app.config['uploads'] + f"/{file}" # 删除文件 os.remove(path) for item in os.walk(app.config['uploads']): for items in item[2]: file_list.append(items) for items1 in file_list: a = os.path.getctime(app.config['uploads'] + f'/{items1}') b = time.localtime(a) c = time.strftime("%Y-%m-%d %H:%M:%S", b) file_time.append(c) zip_list = zip(file_list, file_time) return render_template('upload.html', zip_list=zip_list, file_list=file_list)if __name__ == '__main__': app.run()
upload.html
<table border=1> <tr> <td colspan=3 style="text-align: center;">资源库</td> <tr> <td>文件名</td> <td>上传时间</td> <td>操作</td> </tr> {% for item,items in zip_list %} <tr> <td>{{ item }}</td> <td>{{ items }}</td> <td><a href="/delete/{{ item }}">删除</a></td> </tr> {% endfor %} </table>
五、设计结果分析
在上面的叙述所我们已经完成了对该项目所有功能的实现,现在让我们运行程序,查看实际的效果。首先我们点击运行,打开网址进入登录界面,为了运行结果的严谨性,我们选择注册账号,输入账号1404和密码123123,点击注册跳转至登录界面,数据库user表中刚刚注册的账号已被加入数据库中,接着输入我们注册的账号和密码并点击登录进入主界面。班级号显示为None,页面显示没有加入相应的班级。我们点击加入班级,输入班级号1002,并点击添加课程输入课程号B01以及课程名Python程序设计,点击提交。相关的信息都已被添加至页面中,接着我们点击侧边栏资源库页面,点击上传文件。上传5个文件并点击删除,将其全部删除,项目运行如图9所示。从图9可以看出此次运行,所有功能都已实现,运行效果很好。
图9 项目运行图