### 1.可行性分析报告: #### 1.1引言: ​ 随着时代的不断发展,互联网已进入千家万户,社会进入网络时代,计算机网络已经成为社会发展的强大动力。网络不仅给我们带来无穷的信息,也带来了许多便利。不仅企业、政府的正常工作离不开网络,教育事业同样需要网络。远程教育已经成为现代教育技术的发展的主要方向之一,在线习题测试作为远程教育的一个子系统也成为一个重要研究领域。 ​ 网络技术的发展使得学生习题的技术手段和载体发生了革命性的变化,网络的开放性、分布性和基于网络的巨大计算能力使学生做题和测试突破了空间和时间的限制。基于网络的习题测试系统正成为人们的研究热点之一。与传统习题测试相比,网络在线测试系统不仅能节约大量时间、人力财力。在线习题测试系统产生的背景正是当今教育信息化的趋势及我国高等教育信息化建设;目的是充分利用学校现有的计算机软硬件资源和网络资源实现无纸化习题测试以避免传统测试的不足。 #### 1.2项目背景: ​ 项目名称:在线习题测试系统(灯塔) ​ 项目的开发者:唐三萌、何鹏瞩、郑培炤、张舜尧、磨文龙、陈德聪 ​ 用户:各高校师生 #### 1.3参考资料: 1、https://www.djangoproject.com/ 2、https://blog.csdn.net/weixin_45110404/article/details/90758243 3、《跟老齐学Python:Django实战(第2版)》作者: 齐伟 电子工业出版社 出版年: 2019-1 4、https://docs.djangoproject.com/zh-hans/3.0/ 5、学长项目:https://github.com/EnJoy-git/OnlineExerciseTest ### 2.可行性研究的前提: #### 2.1要求: 1、前端设计和后台设计(#1、前端设计和后台设计) 2、角色划分设计 3、题库管理:习题分类(单选题、多选题、填空题,教师可在线录入也可以excel或word文件方式上传试题,提供给教师题库模板 4、自动判题计分功能 5、其他特色功能 #### 2.2目标: ​ 1.设计出简洁优美的前端用户交汇界面,首页应包括用户登录、注册两大必备功能。在此基础上,可添加如验证码、记住密码等额外选项。 ​ 2.软件用户分为教师和学生,可建立相应数据库对用户信息进行保存。 ​ 3.学生端登入可进行文件的上传、习题的练习、查看老师发布的测试信息并参与,系统可自动对学生提交的选择题答案进行判断正误,并将学生最终成绩进行一个排名处理。 ​ 4.教师服务端教师服务端 ​ ### 3.对目前系统的分析: ​ 1、后端数据库: ​ 导入django中的models包,并用migrations中的CreateModel函数建立数据库的表,其属性包括:身份、姓名、学号/教师编号、手机号、账号、密码。其代码如下: ```python name='UserInfo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('identity', models.CharField(max_length=4, verbose_name='身份')), ('name', models.CharField(max_length=32, verbose_name='姓名')), ('id_number', models.CharField(max_length=32, verbose_name='学号/教师编号')), ('phone', models.CharField(max_length=32, unique=True, verbose_name='手机')), ('username', models.CharField(max_length=32, unique=True, verbose_name='账号')), ('password', models.CharField(max_length=32, verbose_name='密码')), ], ``` ​ 2、登入主界面: ​ 主界面包括注册和登录,注册时使用request.POST来接收前端传过来的数据,经过与数据库现有数据比对后确定用户名和手机号未被注册,即可将数据导入数据库完成注册,代码如下: ```python identity = request.POST.get("identity") name = request.POST.get("name") id_number = request.POST.get("id_number") phone = request.POST.get("phone") username = request.POST.get("username") password = request.POST.get("password") # 添加到数据库 UserInfo.objects.create(identity=identity, name=name, id_number=id_number, phone=phone, username=username, password=password) ``` ​ 到此完成注册,并跳转到登录界面,登录时采用request.COOKIES.get方法从cookie中获取用户名和密码。在登录过程中采用了验证码技术,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试。 ​ 验证码的实现,先从PIL导入Image, ImageDraw, ImageFont, ImageFilter包,可定义两函数如下,生成随机的字符串和RGB颜色 ```python def random_str(length=4): return ''.join(random.sample(string.ascii_letters, length)) #随机字符串 默认长度 4 ``` ​ ```python def random_color(s=1, e=255): return random.randint(s, e), random.randint(s, e), random.randint(s, e) #随机 RGB 颜色 ``` ​ 接着创建Image、Draw、Font对象,调用Draw中的point方法令随机颜色填充每个像素,并调用text方法将随机颜色验证码写到图片上,具体实现代码如下: ```python image = Image.new('RGB', (width, height), (255, 255, 255)) # 创建Font对象 file = os.path.dirname(os.path.abspath(__file__)) font = ImageFont.truetype(f'{file}/FZSTK.ttf', size) # 创建Draw对象 draw = ImageDraw.Draw(image) # 随机颜色填充每个像素 for x in range(0, width, 2): for y in range(height): draw.point((x, y), fill=random_color(64, 255)) # 验证码 code = random_str(length) # 随机颜色验证码写到图片上 for t in range(length): draw.text((40 * t + 5, 5), code[t], font=font, fill=random_color(32, 127)) return image, code ``` ​ 当用户完成用户名、密码和验证码的输入后,显然要进行一系列的判断,这个过程使用dologin函数实现,首先使用HTTP协议请求方法中的POST方法获取验证码,再利用会话控制session对象获取保存的code。判断验证码和输入的code与session对象中保存的一致后,再使用HTTP协议POST方法从登录表单中获取用户名和密码及是否勾选了记住密码(rember即为判断是否勾选了记住密码的标志),并将返回的响应赋给response。完成上述操作后再进行数据库的遍历对比,找出符合输入用户名和密码的数据,并将其存入到session对象中,若 勾选了记住用户名和密码,将用户名和密码保存到cookie中,否则 删除cookie中的之前保存用户名和密码。之后再进行登入用户身份的判断并跳转到不同的端口(学生端或教师服务端)。 ​ 若用户密码或验证码输入不匹配,则将输入的用户名和密码清空,若密码错误则返回“用户名或密码错误”后重新返回登录界面,验证码错误则返回“验证码错误”后重新返回登录界面。 ```python def dologin(request): # 获取表单中提交的验证码 check_code = request.POST.get('check_code') # 获取session会话中保存的code session_checkcode = request.session.get('check_code') if check_code and check_code.lower() == session_checkcode.lower(): # 从登录表单中获取用户名和密码及是否勾选了记住密码 username = request.POST.get('username') password = request.POST.get('password') rember = request.POST.get('rember') response = HttpResponse() temp_RawQuerySet = UserInfo.objects.raw('select * from login_userinfo') for temp in temp_RawQuerySet: if temp.username == username and temp.password == password: # ## 存入基本信息到session ## # request.session['username'] = username request.session['password'] = password request.session['id_number'] = temp.id_number request.session['name'] = temp.name if rember == 'rember': # 勾选了记住用户名和密码 # 将用户名和密码保存到cookie中 response.set_cookie('username', username, max_age=3 * 24 * 3600) response.set_signed_cookie('pwd', password, salt='pwdsalt', max_age=3 * 24 * 3600) else: # 删除cookie中的之前保存用户名和密码 response.delete_cookie('username') response.delete_cookie('pwd') if temp.identity == '老师': return redirect('teacher_client:teacher_client') # return render(request, 'teacher_client.html', {'username': temp.username}) else: return redirect('student_client:student_client') # 用户密码不匹配 response.delete_cookie('username') response.delete_cookie('pwd') error_msg = '用户名或密码错误' return render(request, 'login.html', {'error_msg1': error_msg}) else: error_msg = '验证码错误' return render(request, 'login.html', {'error_msg': error_msg}) ``` ​ 3.学生端口:成功登录后跳转学生端口,对于学生和老师端口的设计,首先要设计相关的数据库来存储相关的数据,包括姓名、学号/教师编号、加课码、课程名、题目信息、A、B、C、D选项、答案、所属班级、存入时间、考试时间等数据信息。 ```py class teacherClass(models.Model): """记录班级加课码""" name = models.CharField(max_length=32, verbose_name="姓名", blank=False, null=False) id_number = models.CharField(max_length=32, verbose_name="学号/教师编号", blank=False, null=False) classCode = models.CharField(max_length=20, verbose_name="加课码", default='') classname = models.CharField(max_length=20, verbose_name="课程名", default='') class classTable(models.Model): """班级表""" name = models.CharField(max_length=32, verbose_name="姓名", blank=False, null=False) id_number = models.CharField(max_length=32, verbose_name="学号/教师编号", blank=False, null=False) phone = models.CharField(max_length=32, verbose_name="手机", unique=True, blank=False, null=False) classCode = models.CharField(max_length=6, verbose_name="加课码", default='') class questionBank(models.Model): """用于保存用户上传提交的题目""" qus_imfomation = models.CharField(max_length=1000, verbose_name="题目信息", blank=False, null=False) qus_A = models.CharField(max_length=1000, verbose_name="选项A", blank=False, null=False) qus_B = models.CharField(max_length=1000, verbose_name="选项B", blank=False, null=False) qus_C = models.CharField(max_length=1000, verbose_name="选项C", blank=False, null=False) qus_D = models.CharField(max_length=1000, verbose_name="选项D") qus_ans = models.CharField(max_length=1000, verbose_name="答案", blank=False, null=False) que_classcode = models.CharField(max_length=1000, verbose_name="所属班级", blank=False, null=False) que_time = models.CharField(max_length=200, verbose_name="存入时间", default="") class testQuestionBank(models.Model): """考试信息表""" qus_imfomation = models.CharField(max_length=1000, verbose_name="题目信息", blank=False, null=False) qus_A = models.CharField(max_length=1000, verbose_name="选项A", blank=False, null=False) qus_B = models.CharField(max_length=1000, verbose_name="选项B", blank=False, null=False) qus_C = models.CharField(max_length=1000, verbose_name="选项C", blank=False, null=False) qus_D = models.CharField(max_length=1000, verbose_name="选项D") qus_ans = models.CharField(max_length=1000, verbose_name="答案") test_code = models.CharField(max_length=1000, verbose_name="所属班级", blank=False, null=False) test_time = models.CharField(max_length=200, verbose_name="考试时间", default='') ``` ​ 进入学生端口后,要先定义一系列如下变量及列表如下:以变量记录 总题目数、单选题题目数、多选题题目数、填空题题目数、正确的题目数量、错误的题目数量,列表记录单选题标准答案、多选题标准答案、填空题标准答案、单选题、多选题、填空题。 ```python total_number = 0 # 总题目数 ans = [] single_choice_number = 0 # 单选题题目数 multiple_choice_number = 0 # 多选题题目数 gap_filling_number = 0 # 填空题题目数 single_choice_ans = [] # 单选题标准答案 multiple_choice_ans = [] # 多选题标准答案 gap_filling_ans = [] # 填空题标准答案 right_num = 0 # 正确的题目数量 wrong_num = 0 # 错误的题目数量 single_choice = [] # 单选题 multiple_choice = [] # 多选题 gap_filling = [] # 填空题 ``` ​ 接着若收到GET请求,则将session对象中的“username”和“password”赋给username和password,若username和password同时不为空,则将username返回前端,否则返回登录界面重新登录。 ```python def student_client(request): """学生端口""" if request.method == "GET": username = request.session.get('username') password = request.session.get('password') if username and password: StudentID = request.session.get('id_number') StudentName = request.session.get('classcode') return rernde(request, "student_client.html", {"username": username}) else: return redirect("http://127.0.0.1:8000/") ``` ​ 通过构造函数test_join来接受加入测试的请求,前面的if判断同上,即接着若收到GET请求,则将session对象中的“username”和“password”赋给username和password,若username和password同时不为空,则将username返回前端,否则返回登录界面重新登录。接着将获得的testcode赋给test_code,如果test_code存在,则将test_code存入session对象中并跳转test_client界面进入测试。 ```python def test_join(request): """加入测试""" if request.method == "GET": username = request.session.get('username') password = request.session.get('password') if username and password: StudentID= request.session.get('id_number') StudentName = request.session.get('classcode') else: return redirect("http://127.0.0.1:8000/") test_code = request.POST.get("testcode") if test_code: request.session['test_code'] = test_code return redirect("student_client:test_client") ``` ​ 在进入test_client考试界面后,可从数据库中获取题目,并将获取的题目赋给 temp_RawQuerySetquestion, ```python questionSelect = "select id, qus_imfomation, " \ "qus_A, qus_B, qus_C, qus_D, " \ "qus_ans," \ "test_time " \ "from teacher_client_testquestionbank " \ "where test_code = " + str(test_code) temp_RawQuerySetquestion = models.questionBank.objects.raw(questionSelect) ``` ​ 由于得到的题目中包括单选题、多选题及填空题,可通过循环来判断分别都是哪些题型,若选项为空,则为填空题,否则若选项只有一个,则为单选题,否则则为多选。详细代码如下: ```python for temp in temp_RawQuerySetquestion: temp_testtime = temp.test_time ans.append(temp.qus_ans) if temp.qus_ans is None: gap_filling.append(temp) gap_filling_ans.append(temp.qus_A) elif len(temp.qus_ans) == 1: single_choice.append(temp) single_choice_ans.append(temp.qus_ans) else: multiple_choice.append(temp) multiple_choice_ans.append(temp.qus_ans) ``` ​ 完成后对总题数、单选题题数、多选题题数、填空题题数进行一次统计: ```python total_number = len(gap_filling) + len(multiple_choice) + len(single_choice) total_list = [x + 1 for x in range(total_number)] single_choice_number = len(single_choice) multiple_choice_number = len(multiple_choice) gap_filling_number = len(gap_filling) ``` ​ 完成后将获得的相应信息返回到前端: ```python return render(request, "test1.html", {"total_num": total_number, "total_list": total_list, "single_choice": single_choice, "multiple_choice": multiple_choice, "gap_filling": gap_filling, "test_time": temp_testtime, "single_choice_number": single_choice_number, "multiple_choice_number": multiple_choice_number, "gap_filling_number": gap_filling_number }) ``` ​ 定义三个列表分别保存用户输入的单选题、多选题、填空题答案,再从前端获得输入的答案后存入相应的列表中: ```python single_choice_answer = [] # 用户输入的单选题答案 multiple_choice_answer = [] # 用户输入的多选题答案 gap_filling_answer = [] # 用户输入的填空题答案 for temp in range(total_number): pass # 获取前端传递的单选题答案 for temp in range(1, single_choice_number+1): single_str = "choose" + str(temp) temp_single_choice_answer = request.POST.get(single_str) single_choice_answer.append(temp_single_choice_answer) # 获取前端传递的多选题答案 for temp in range(1, multiple_choice_number+1): multiple_str = "chooseA" + str(temp) temp_multiple_choice_answer = request.POST.getlist(multiple_str) multiple_choice_answer.append(temp_multiple_choice_answer) # 获取前端传递的填空题答案 for temp in range(1, gap_filling_number+1): gap_filling_str = "chooseB" + str(temp) temp_gap_filling_answer = request.POST.getlist(gap_filling_str) gap_filling_answer.append(temp_gap_filling_answer) ``` ​ 接着进入判题阶段,将前端获得的答案与数据库中的答案进行一一比对,由于有填空题、单选题、多选题三种不同题型,故用三个循环来比对,答案相同则right_num+1,否则wrong_num+1。 ```python for ans, answer in zip(single_choice_ans, single_choice_ans): if answer == ans: right_num += 1 else: wrong_num += 1 # 判断多选题 ans:标准答案 answer:用户答案 for ans, answer in zip(multiple_choice_ans, multiple_choice_answer): if answer == ans: right_num += 1 else: wrong_num += 1 # 判断填空题 ans:标准答案 answer:用户答案 for ans, answer in zip(gap_filling_ans, gap_filling_answer): if answer == ans: right_num += 1 else: wrong_num += 1 ``` ​ 4.教师端口:开始与学生端一致,收到来自前端的GET请求后,则将session会话中的“username”和“password”赋给username和password,若username和password同时不为空,则将username返回前端,否则返回登录界面重新登录。 ​ ​ 接着在数据库中通过用户名检索出该名老师的信息并放入temp_RawQuerySetClass中: ```python select_teacher_client_teacherclass = 'select id, id_number, classname, classcode ' \ 'from teacher_client_teacherclass ' \ 'where id_number = ' + str(teacherID) temp_RawQuerySetClass = teacherClass.objects.raw(select_teacher_client_teachercla ``` ​ 定义两个列表classname、classcode来存放一名老师的班级及相应加课码,并对temp_RawQuerySetClass进行循环遍历,将其班级和加课码放入列表中。 ​ 之后再通过加课码classcode进行数据库检索找出加入课堂的相应学生并定义相应列表Sname、Sno、Sphone 存放学生姓名、学号、手机号。 ​ 将以上数据返回到前端teacher_client.html ```python return render(request, 'teacher_client.html', {'classname': json.dumps(classname), 'res': json.dumps(res), 'classcode': classcode, "student": temp_RawQuerySetStudent, 'username': username, "class": temp_RawQuerySetClass}, ) ``` ​ 若需要创建一个班级,则在收到一个GET请求后,跳转返回到createClass.html,在该页面拿到班级名字classname后将该classname存入session会话中。 ```python if request.method == "GET": return render(request, "createClass.html") classname = request.POST.get('classname') request.session['classname'] = classname ``` ​ 利用if判断获得的classname不为空,利用get_code获得相应的加课码,并将code存入session对象中,之后再从session对象中获得username和id_number,并将这些信息一并存入数据库中,之后可跳转到老师客户端。 ```python if classname != '': # global CLASS_ID # CLASS_ID += 1 code = get_code() request.session['code'] = code username = request.session.get('username') id_number = request.session.get('id_number') teacherClass.objects.create(name=username, id_number=id_number, classCode=code, classname=classname) return redirect("teacher_client:teacher_client") ``` ​ 要实现教师端文件的上传,可利用request.FILES.get('exc')拿到从前端获取的Excel文件,通过循环遍历,将题目的A、B、C、D、标准答案、班级码存入Excel的每一行中,并将这些信息存入数据库中,完成后跳转教师客户端。 ```python qus_imfomation = row[0].value qus_A = row[1].value qus_B = row[2].value qus_C = row[3].value qus_D = row[4].value qus_ans = row[5].value que_classcode = row[6].value questionBank.objects.create(qus_imfomation=qus_imfomation, qus_A=qus_A, qus_B=qus_B, qus_C=qus_C, qus_D=qus_D, qus_ans=qus_ans, que_classcode=que_classcode, que_time=now_time) ``` ​ 定义一函数test_release用于习题发布,由于教师发布习题默认发布最近上传的习题,显然在从数据库中拿出各个习题发布时间后还要对其进行排序查找的操作来确定应该发布哪些习题,可定义一列表time_list,将各习题发布时间存入该列表,对该列表进行一次基数排序,确认排序完成根据与目前系统时间比较后进行一次二分查找,在找到离目前系统时间最近的一次发布时间target_time后,以此为根据在数据库拿出题目信息、选项和答案。再利用三个列表分别存入单选题、多选题、填空题,区分单选、多选、填空的方法与之前学生端一致。完成后记录总题数、单选题数、多选题数、填空题数,并将以上所有信息返回给前端中。 ​ 在题目成功在前端显示后,需要教师设置考试时间即可。 ```python time_list = [] time_select = "select id, que_time " \ "from teacher_client_questionbank" time_res = questionBank.objects.raw(time_select) for temp in time_res: if temp.que_time not in time_list: time_list.append(temp.que_time) time_list = [float(x) for x in time_list] time_list = radix_sort(time_list) print(time_list) target_time = binary_search(time_list, time.time()) questionSelect = "select id, qus_imfomation, " \ "qus_A, qus_B, qus_C, qus_D, " \ "qus_ans, " \ "que_classcode " \ "from teacher_client_questionbank " \ "where que_time =" + str(target_time) temp_RawQuerySetquestion = questionBank.objects.raw(questionSelect) single_choice = [] multiple_choice = [] gap_filling = [] for temp in temp_RawQuerySetquestion: temp_classcode = temp.que_classcode if temp.qus_ans is None: gap_filling.append(temp) elif len(temp.qus_ans) == 1: single_choice.append(temp) else: multiple_choice.append(temp) # testQuestionBank.objects.create(qus_imfomation=temp.qus_imfomation, qus_A=temp.qus_A, qus_B=temp.qus_B, # qus_C=temp.qus_C, qus_D=temp.qus_D, qus_ans=temp.qus_ans, # test_code=TEST_CODE, test_time=test_time) total_number = len(gap_filling) + len(multiple_choice) + len(single_choice) total_list = [x + 1 for x in range(total_number)] single_choice_number = len(single_choice) multiple_choice_number = len(multiple_choice) gap_filling_number = len(gap_filling) return render(request, "test.html", {"total_num": total_number, "total_list": total_list, "single_choice": single_choice, "multiple_choice": multiple_choice, "gap_filling": gap_filling, "ClassCode": temp_classcode, "TEST_CODE": TEST_CODE, "single_choice_number": single_choice_number, "multiple_choice_number": multiple_choice_number, "gap_filling_number": gap_filling_number }) ```