10

高校教务系统课程表的简单实现方式


零、需求分析:抛开需求谈教程就是耍流氓


在许多大学的教务系统或者第三方教务软件中,都有一个重要的、不可或缺的功能——课程表,作为整个教务软件中使用频率最高的功能,课程表的好坏直接决定了这个系统的用户体验。
然而对于刚刚初学软件开发的我来说,无论是前端界面还是后端的代码似乎都有一些难度。想着给和我一样的又恰好也在开发教务系统相关软件的小伙伴们一些思路,这篇博客就诞生了。

如图,就是某款教务软件的课表界面:
课程表.png


一、E-R——数据应该怎么存(本文以MySQL为例)


1、第一个问题:数据表

一门课是一个实体,因为一门课中含有很多节课,所以会有许多条数据,如果只有一张数据表,把每节课都从作为一条单独的数据写入表中,无疑,会产生许多冗余的数据(因为对于某一门课中的每一节课来说,他们的课程名称、上课班级、任课教师都是相同的),
图片.png
并且还会出现其他问题,比如,一门课程是一个对象,同这种方式储存的数据,一门课程被拆分成了多个对象,在数据的存取时很不方便。
因此,应该怎么写呢?——一门课程建一张数据表(命名为Course表),一节课建一张数据表(命名为Course_info表)。这样,一门课程与一节课就是多对一的实体关系。

2、第二个问题:课程信息有哪些字段

首先声明一下关于时间表示的关键字段:

  • 周次(第一周、第二周...)week,int型
  • 星期(星期一、星期二...)weekday,int型
  • 上课时间(第一节课、第二节课...)begin,int型

对于同一门课程来说,不同的周次、不同的时段,上课的教室都会不一样,也就是说,对于同一门课程的每节课,周次、时段、教室这三个值中任意两个随第三个值的变化而变化,并且每节课这三个值都至少会有一个改变;而对于同一门课程来说,课程名、任课教师、班级这三个值都是一样的。
所以,这两张数据表该分别储存什么内容,就显而易见了——把不变量储存进Course表,变化量储存到Course_info表中。
Course表的字段:图片.png
Course_info表的字段:图片.png
Course表数据:图片.png
Course_info表数据:图片.png

可见,只要course_id这个字段正确指向Course表的对象,无论课程的时间、教室怎么变,课程都还是那个课程,教师都还是那个教师。


3、第三个问题:课程的大小节问题

大学的课程,一大节课,会连续持续几小节(请看本文的第一张图片),所以数据库中,每一节课存大节、还是小节,是个问题。

  • 一开始的思路是,全部存为大节,每大节两小节,把一天的时间分为五大节,可随之而来的问题就是,如果一节课持续三个小节,该怎么办?
  • 于是,把思路改成存小节,把一节课分成11个小节,课这样有出现了问题:一大节是一个对象,如果存小节,就硬生生的把一个大节拆成了两个或三个对象。或许你认为这样也无伤大雅,但是,试想,一旦需要记录签到状况,三个小节是三个对象。这样,在第二节课的时候,第一节课的签到信息就失效了,如果再想看第二节课的签到信息,就需要重新签到,影响用户体验。
  • 因此,一种合理的方案是这样,把一天分成11个小节,对于每节课,储存一个起点(启示的小节是第几节课)和一个长度(这节课持续几个小节)。这样就解决了上述的所有问题:一节课就是一个对象,同时也让时间段足够细化。

对Course_info表做如下修改,加入持续长度:图片.png
修改后的数据:图片.png


二、后端


有了合理的数据库做支撑,后端的查询方法就十分的好写了:

  • 从session中取出此学生的登陆信息
  • 取出当前的时间信息:周次、星期、小节数
  • 根据登陆信息从学生与课程的关联表中(Score表,本文没有提及)取得此学生的所有课程的id,是一个int类型的数组
  • 根据得到的数组,在Course表中取出此学生所上的所有课程
  • 循环,对于每一门课程,在Course_in表中,找到这个学生在本周次(week)、本星期(weekday)的所有课程
  • 新建一个二维数组,作为课程表,因为每周7天,每天有11小节,所以二维数组的大小是7*11
  • 此时,刚刚查询出的每一节课,都是此学生这周(week)的课程,所以只需要根据星期(weekday)和起始小节(begin)把数据填入课程表数组的对应位置就可以了,然后就可以把这个课程表传入前端了。

(代码以ThinkPHP为例)

        // 获取本学生id
        $studentId = session('studentId');

        //查询本学期本本学生的所有课程,(Score表就是学生与课程、成绩的关联表)
        $score = new Score();
        $getScore =  $score->where(['course_id'=>$courseIds, 'student_id'=>$studentId])->select();
        
        //建立课程表数组
        $coursetable = array('1' => array('0','0','0','0','0','0','0','0') , 
        '2' => array('0','0','0','0','0','0','0','0') , 
        '3' => array('0','0','0','0','0','0','0','0') , 
        '4' => array('0','0','0','0','0','0','0','0') , 
        '5' => array('0','0','0','0','0','0','0','0') , 
        '6' => array('0','0','0','0','0','0','0','0') , 
        '7' => array('0','0','0','0','0','0','0','0') , 
        '8' => array('0','0','0','0','0','0','0','0') , 
        '9' => array('0','0','0','0','0','0','0','0') , 
        '10' => array('0','0','0','0','0','0','0','0') , 
        '11' => array('0','0','0','0','0','0','0','0')) ;
        
        //储存本学期本学生所有课程的course_id
        $scoreIds = [];
        foreach ($getScore as $score) {
            array_push($scoreIds, $score->course_id);
        }
        
        //对于每一门课程,查询所有的课(单位:节)
        $courseinfos = Courseinfo::where(['course_id'=>$scoreIds, 'week'=>$week])->select();
        
        //把每节课填入课表
        foreach ($courseinfos as $key => $acourseinfo) {
            {
                $coursetable[$acourseinfo->begin][$acourseinfo->weekday] = $acourseinfo;
            }
        }
        
        //传入课程表
        $this->assign('coursetable',$coursetable);   

三、前端


1、已知:后端传入一个7*11的数组,在合适的位置有对应的课程,在没有课程的位置,值为零。

因此,只需要在前端也加一个7*11的循环即可

//通过循环生成表格
//$i为表格的行,值为小节(begin),$j为表格的列,值为星期几(weekday)

for ($i=0; $i < 11; $i++) { 
    for ($j=0; $j < 7; $j++) { 
        if (课程表中存在 weekday==$j 并且 begin==$i 的值) {
            此单元格中显示此课程的信息
        }
    }
}


2、解决单元格合并的问题

课程表.png
还是这张图,可以看到,当课程持续两小节或者三小节时,连续两、三个单元格都会显示这节课的信息,而实际上,数据库中只存了第一小节的信息。因此需要合并单元格。利用HTML表格的rowspan标签,可以很方便的合并单元格。
合并前代码:

<table border="1">
  <tr>
    <th>星期一</th>
    <th>星期二</th>
    <th rowspan="1">合并前</th>
  </tr>
  <tr>
    <td>123</td>
    <td>456</td>
    <th>合并前</th>
  </tr>
  <tr>
    <td>789</td>
    <td>012</td>
    <th>合并前</th>
  </tr>
</table>

当rowspan=1时不进行合并,相当于不写。效果如下图片.png

合并后代码:

<table border="1">
  <tr>
    <th>星期一</th>
    <th>星期二</th>
    <th rowspan="3">合并前</th>
  </tr>
  <tr>
    <td>123</td>
    <td>456</td>

  </tr>
  <tr>
    <td>789</td>
    <td>012</td>

  </tr>
</table>

而当rowspan被改成3的时候,就可以发现,最右面的单元格被合并了。图片.png
然而,其实说合并,这只是一种表面的说法,细心的你一定发现了,在第二行、第三行分别少了一个<td>标签图片.png
与其说rowspan是合并单元格,不如说:rowspan改变了原有单元格的高度。
那么问题来了,为什么下面的行要少一个<td>?如果在上一行使用了rowspan的情况下,下面几行仍然保持不变,那么会出现什么现象呢?

<table border="1">
  <tr>
    <th>星期一</th>
    <th>星期二</th>
    <th rowspan="3">合并后</th>
  </tr>
  <tr>
    <td>123</td>
    <td>456</td>
    <td>234</td>
  </tr>
  <tr>
    <td>789</td>
    <td>012</td>
    <td>567</td>
  </tr>
</table>

图片.png

单元格就是这么强硬,一旦启用了rowspan,上面的单元格就会把下面的单元格强硬的“挤走”——使其向右移动一个单位。但是这在课程表中是绝对不能出现的——不可能因为第一节课有两节,就把本来是周三的课显示到了周四吧?!

所以,就需要动态的判断了。
我们知道,一个单元格有两种状态:显示、不显示(不显示可以不写这一行,也可以用CSS来定义一个样式“disappear”,当class等于这个样式时,单元格就不显示了。而一节课有三种状态:持续2小节、3小节、4小节。所以对于每一个单元格,都需要判断它上面的单元格有没有长度等于2的课、它上面的上面的单元格有没有长度为3的课、它上面的上面的上面的单元格有没有长度为四的课——以上三种情况有任意一种满足,这个单元格就是隐藏状态,反之为显示状态
但还有一种情况需要考虑,那就是数组下标不能超出范围。比如,对于是第一行的单元格(第一小节),如果请求它上方的单元格(第零小节),会超出数组范围,导致报错。所以,第一节、第二节、第三节需要单独对待,其他的小节由于确保不会超出索引,所以可以使用循环。
由于本人的实际项目中的代码比较凌乱,就不贴出来了,仅提供实现此功能的逻辑,供大家参考。


四、总结


在我看来,最大的困难,一是在于数据库方面,数据如何储存、如何建立实体关系、一节课按大节还是按小节储存...这些都是需要考虑的问题。而数据库的结构是一个系统的基石,不合理的结构会大大增加开发的难度,甚至可能由于数据库结构不合理导致项目失败。
另一个问题就是前端输出信息时的判断了,为了防止使用rowspan导致单元格错误的问题,需要根据前几行的数据来判断当前单元格是否显示,如果上面有课,就不显示,这样就完美解决了问题。唯一的不足就是,如果不借助后端语言,仅仅用HTML来写循环和判断,会非常非常麻烦,这是我没有列出代码的原因——如果能使用JS或者在前端直接使用PHP代码来循环,就能减轻复杂度了。
或许在大佬面前这种功能简单的不值一提,可作为一个菜鸡初学者,实现这个功能的时候确实花费了很多精力,思考了很长的时间,大概想了四五天才想出这个方案。

路途漫漫,道阻且长,我们都在成长。

终有一天,当我们回望过去的自己时,一定会发现,这段时间自己在不断的进步,脸上流露出欣慰的笑容。

图片.png


LYX6666
1.6k 声望75 粉丝

一个正在茁壮成长的零基础小白