布同:web版比赛实时算分系统的设计

【需求分析】

  表演期间,需要展示当前节目的基本信息。

  表演完毕,需要所有评审对当前表演者进行打分。打分可能分为多项,多个考核点,最后加和算是最后给分。

  打分完毕,需要计算平均分,可能会有去掉最高分和最低分的操作。

  最后展示,所有队伍的排名。

【需求点】

  1.管理权限登录

  评审需要进行登录,登录后才能够打分。每个评审帐号只能够被登陆一次。

  2.管理评审的基本信息

  可以提前进行录入,也可以根据评委登录的时候填写,不过会比较麻烦。还是建议提前录入,分配帐号密码,短信告知,便于减少会场上忙乱中的失误。这里可以提供实时的编辑功能,因为如果评审名字出现错别字,那么及时修正是对评审尊重的表现,也是系统基本的工作之一,算是提高鲁棒性。

  3.表演者信息管理

  后台最好有个表演者列表,可以管理表演顺序,控制评审的对分对象,临时队伍退赛也可以及时剔除,避免影响。还可以统计分数,便于排名。

  4.大屏幕信息展示管理

  可能需要展示的有,评审列表,节目列表,单个节目的详细介绍,单个节目的分数列表,排名列表。这些都需要大屏幕实时相应去变化。

【技术方案】

  1.LAMP结构搭建后台

  可以找一个性能较强的笔记本,上面只需要安装一个wampserver就可以具有一个apache+mysql+php的结构。对于五十位评审来说,每秒最多需要25个进程就可以应付,所以负载还是能够承受的。之所以选择这样的架构,是因为web开发app周期短,效率高。如果使用桌面程序,开发周期较长,成本高。另外,web app一般能够和mysql混搭,可以方便的修改数据,依赖mysql,可以进行对数据丰富方式的查找和管理。另外,apache搭建的web app可以开放给局域网中的其他终端访问,例如使用平板电脑打开浏览器就能够通过形如http://192.168.0.1/admin/的地址形式去访问位于192.168.0.1机器上的web程序,当然这个网址一般是网关地址,实际中的地址可能是内网地址中的任何一个。

  php脚本类似c++语法,对于c/C++程序员来说入手很快。wampserver搭建的apache几乎不需要任何配置,写好php代码管理好数据即可。同时,wampserver安装完成之后,可以在文档目录下找到几个已经建好的子站点,文档目录一般是c:/wamp。所以整个技术还是比较容易入手的。

  2.建立页面缓存

  如果用php去动态打印页面代码是很累的,这里一般使用比较成熟的smarty模版语言。smarty是利用php进行封装之后的一个类,用来将一定格式的网页模版翻译为可以供浏览器执行的页面文件。这个页面文件可以保存在本地目录中,供快速调用。如果模版文件被修改,生成的缓存页面也会被修改,所以开发完成后,调用的速度是很快的。

  3.免刷新控制显示

  对于评审已经打开的评分页面,如果关闭评分,这这个页面也需要将提交入口关闭。但是服务器是不能控制浏览器的,只能利用Javascript代码来判断什么时候可以评分,什么时候不能评分。这里可以用setInterval函数来设置一个定时器,这个定时器每过一段时间就问服务器一次,是否还可以评分,如果请求返回结束,则关闭入口,或者将提交按钮置为无效即可。如果页面需要刷新,则Javascript代码让页面刷新,重新从服务器返回新的数据即可。所以,其实也不是完全不刷新,只是不用用户手动刷新而已。

【操作过程】

1.安装wampserver

这个程序是免费的,网上可以下载到,也可以直接通过QQ管家的程序管理功能搜索这款软件并下载,这样省去了网上去到处查找的麻烦。

可以选择安装在D盘,都是一样的,安装之后会在D:/wamp目录下能看到alias和apps目录。

2.添加文档目录配置

在alias目录下一般会有已经有几个文件了,你可以拷贝其中一个自己建一个子站,稍加修改,如:

1
2
3
4
5
6
7
8
9
// count.conf
Alias /count "D:/wamp/apps/count/"
 
<Directory "D:/wamp/apps/count/">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride all
        Order Deny,Allow
    Allow from all
</Directory>

 其中D:/wamp/apps/count就是我建好的名字为count子站的子站了,文件名可以使用count.conf,加以区分。

3.添加网站入口文件

在刚才count.conf文件中填好的目录下,如:D:/wamp/apps/count/,添加index.php文件,其中可以加入如下测试代码:

1
2
3
// index.php
<?php
     echo "welcome to count.";

 原则上讲,php文件应该有个?>作为结束符,不过没有也是可以的,系统会自己找到结束符。所以直接不添加了。而且在html文件中,可以添加php代码,这个时候php代码段的最后位置如果有大量的空白内容也许会打印到页面文件中,造成意外的格式,反倒是不好的。所以不用结束符是更好的方式。

有了上面这两步就可以在任务栏restart wampserver来使刚才的修改生效。在浏览器中国输入http://localhost/count即可,如果显示welcom to count则说明修改正确。

如果什么都没有出现,也许是php脚本语法有误,但是错误提示被关闭,这个时候可以打开apache中的php.ini文件,打开error_reporting设置,这样就可以调试php代码,当然也可以在php脚本中开启这个设置,相关查阅error_reporting函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Turn off all error reporting
error_reporting(0);
 
// Report simple running errors
error_reporting(E_ERROR | E_WARNING | E_PARSE);
 
// Reporting E_NOTICE can be good too (to report uninitialized
// variables or catch variable name misspellings ...)
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
 
// Report all errors except E_NOTICE
// This is the default value set in php.ini
error_reporting(E_ALL ^ E_NOTICE);
 
// Report all PHP errors (bitwise 63 may be used in PHP 3)
error_reporting(E_ALL);
 
// Same as error_reporting(E_ALL);
ini_set('error_reporting', E_ALL);

看到了welcom to count之后,就要开始进入全面开发阶段了。

4.搭建网站框架

一个好的网站应该有自己的网站架构来管理自己的代码和功能,便于维护和升级,也可以使得结构清晰,便于理解。这里我们可以使用经典的MVC架构。

在apps/count目录下创建module,controller,view三个文件夹,其中view放置页面文件,很多人喜欢把js和css文件也放在view文件加下,这是可行的,放在和view同级目录也行,根据个人习惯即可,count目录是子站的入口目录,只要获取js和css文件的路径是方便的,都是可以的。

module目录放置功能前端功能类,controller放置调用前端功能类,决定什么接口展示什么页面,view是放置页面的地方,还可以放置smarty模版。

我们还可以再建一个library目录,用来放置插件,其他扩展的功能类,例如smarty类,gearman,DB类,memcache类等,当然我们这里也不是都能用到。

5.设计数据存储结构

我们可以建三个表格,t_client表,用来存放评审人员基本信息(他们相关密码帐号也可以一并存储),主键为cid,评审id。t_group表,各个参赛队伍的基本信息,主键为gid,参赛队伍id。t_score表,评审对队伍的打分表,包含所有的打分细项,以gid+cid为主键。

建表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CREATE TABLE `t_group` (
  `gid` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `name` varchar(512) DEFAULT '',
  `active` int(11) unsigned DEFAULT '0',
  PRIMARY KEY (`gid`),
  UNIQUE KEY `name` (`name`)
);
 
CREATE TABLE `t_client` (
  `cid` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL DEFAULT '',
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `clientip` varchar(20) DEFAULT '',
  `type` varchar(20) DEFAULT '',
  PRIMARY KEY (`cid`),
  UNIQUE KEY `name` (`name`)
);
 
CREATE TABLE `t_score` (
  `cid` int(11) unsigned NOT NULL DEFAULT '0',
  `gid` int(11) unsigned NOT NULL DEFAULT '0',
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `yishu` varchar(20) DEFAULT '',
  `jishu` varchar(20) DEFAULT '',
  `total` varchar(20) DEFAULT '',
  primary KEY (`cid`,`gid`)
)

 这里gid和cid都使用自增id,这样方便区分和索引,如果添加完成队伍基本信息之后发生了队伍变化,一方面可以添加到脚本,清空数据库之后重新导入,另一方面也可以就此修改数据库,用update来更新。这里因为gid和cid都不止一个表格在用,如果重新定义了id,必须将所有表都修改,保证一致性。直接修改主键是个危险的操作,可能会打破原来的关联关系。所以这里如果id可能修改的情况下,可以再增加一个index字段,用来专门定义展示的顺序或者编号,尽量不修改主键,将主键对用户隐藏。

6.基本工具

这里的基本工具包括,smarty类,可以放到library下,也可以自己定义个类,简单的将smarty类再包一下。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Module.php
    function smartyOut( $view, $out = null )
    {
        $file = BASE_PATH.'/View/'.$view.'.html';
        if( ! is_file( $file ) )
        {
            throw new Exception("can't find [{$file}].");
        }
        require_once BASE_PATH.'/Library/Smarty/Smarty.class.php';       
        $smarty = Smarty::getInstance();
        $smarty->left_delimiter = '<!--{';
        $smarty->right_delimiter = '}-->';
        $out['time'] = date('Y-m-d H:i:s');
        $smarty->assign( 'out', $out );
        $smarty->display( $file );
    }

 这里经过这样的简单包装之后,只需要传入一个view名字,变量名,就完成了对smarty的调用。

另外,对于DB访问数据库的基本函数也可以封装下,将错误记录到执行的文件中去。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// DB.php
class DB extends Module
{
    function __construct()
    {
        $this->_link = mysql_connect( 'localhost','root','' );
        $this->_db = 'test';
        mysql_select_db( $this->_db );
    }
    function __destruct()
    {
        mysql_close( $this->_link );
    }
     
    static $_instance = false;
    static function getInstance()
    {
        if( self::$_instance == false )
        {
            self::$_instance = new self();
        }
         
        return self::$_instance;
    }
 
    private function _query( $sql, &$resource )
    {
        $ret = array( 'ret'=>true, 'info'=>'成功.' );
        $resource = mysql_query( $sql, $this->_link );
        if( $resource === false )
        {
            $ret['info'] = mysql_errno()." : ".mysql_error();
            $ret['ret'] = false;
        }
        return $ret;
    }
     
     
    function select( $sql, &$results )
    {
        $resource = '';
        $ret = $this->_query( $sql, $resource );
        if( !$ret['ret'] ) return $ret;
        while ( $row = mysql_fetch_array( $resource, MYSQL_ASSOC ) ) {
            $results[] = $row;
        }
        mysql_free_result( $resource );
        return $ret;
    }
     
    function update( $sql )
    {
        $resource = '';
        $ret = $this->_query( $sql, $resource );
        if( !$ret['ret'] ) return $ret;
        $ret['nupdate'] = mysql_affected_rows( $this->_link );
        return $ret;
    }
     
    function insert( $sql )
    {
        return $this->update( $sql );
    }
    function delete( $sql )
    {
        return $this->update( $sql );
    }

 这里封装了update,select,insert,delete操作,其中select将查询到的结构直接返回,这样避免了每次新建连接都去判断是否新建数据库链接成功与否,同时这里也可以将DB部分的日志收集到一个单独的文件中去。

日志工具可以按照几个等级和类型进行封装,这里我为了方便,只封装了info提示信息函数。我将info的参数多类型化,这样传入字符串和数组都能够很好的处理。避免外部时而json_encode,时而serialize,将数组转换的麻烦。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Logs.php
class Logs
{
    static $_dir = '';
    static $_file = '';
    static function init( $pre )
    {
        self::$_dir = BASE_PATH.'/Log/';
        self::$_file = self::$_dir. $pre."_".date("Ymd");
    }
     
    static function infoStr( $obj )
    {
        if( !self::$_file )
        {
            throw new Exception( "log file not init." );
        }
         
        if( is_array( $obj ) )
        {
            $str = '';
            foreach( $obj as $one )
            {
                $str .= self::infoStr( $one );
            }
        }
        else
        {
            $str = $obj;
        }
         
        return $str;
    }
     
    static function info( $obj )
    {
        $str = '['.date('Y-m-d H:i:s').']'. self::infoStr( $obj );
        file_put_contents( self::$_file, $str, FILE_APPEND );
    }
}

 7.module类开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// module.php
class Group extends Module
{
    function __construct()
    {
        Logs::init( 'log' );
    }
     
    function display()
    {
        $obj = DB::getInstance();
        $sql = "select * from t_group";
        $results = array();
        $ret = $obj->select( $sql, $results );
        $ret['num'] = count( $results );
        if( $ret['ret'] && $results )
        {
            $ret['info'] = "查询成功.";
        }
        else
        {
            $ret['ret'] = false;
            $ret['info'] = "查询失败.";
        }
         
        $ret['data'] = $results;
        parent::smartyOut( 'grouplist', $ret );
    }

 上面是一个我定义的展示表演队伍列表的类,将数据获取到之后,再加上页面文件名字grouplist.html,传递给父类Module,这样就可以将变量传到grouplist文件中,这个文件其实是一个smarty模版,就可以通过out变量访问到results变量中的队伍列表了。

8.view开发

根据上面的grouplist.html的信息,可以做如下的代码,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// grouplist.php
<p>参赛队伍: <input type="button" value="添加" id="b_add"/><input type="button" value="刷新" id="b_update"/></p>
<table id="setscore">
<tr>
    <td style="width:10%;">队伍编号</td>
    <td style="width:40%;">名字</td>
    <td style="width:20%;">时间</td>
    <td style="width:10%;">打分状态</td>
    <td style="width:10%;">管理</td>
    <td style="width:10%;">大屏幕显示</td>
</tr>
<!--{section name=t loop=$out.data}-->
<tr>
    <td id="id" gid="<!--{$out.data[t].id}-->"><!--{$out.data[t].id}--></td>
    <td id="name" gid="<!--{$out.data[t].id}-->"><!--{$out.data[t].name}--></td>
    <td><!--{$out.data[t].time}--></td>
    <td id="td_active"><div><!--{if $out.data[t].active == 1}-->正打分
    <!--{elseif $out.data[t].active == 2}-->已打分
    <!--{elseif $out.data[t].active == 0}-->未打分
    <!--{/if}--></div><select id="s_active" style="display:none" onchange="choose(this, <!--{$out.data[t].id}-->)">
    <option value='0'>未打分</option>
    <option value='1'>正打分</option>
    <option value='2'>已打分</option>
    </select></td>
    <td id="admin" gname="<!--{$out.data[t].name}-->" gid="<!--{$out.data[t].id}-->">
        <input type="button" value="删除" id="b_del"/>
        <input type="button" value="分数" id="b_score" onclick="goScoreHtml(<!--{$out.data[t].id}-->)"/></td>
    <td id="admin" gname="<!--{$out.data[t].name}-->" gid="<!--{$out.data[t].id}-->">
        <input type="button" value="信息" id="b_del"/>
        <input type="button" value="打分" id="b_score" onclick="goScoreHtml(<!--{$out.data[t].id}-->)"/></td></tr>
<!--{/section}-->
 
</table>

 通过上面的smartyOut函数,我们已经可以看到,通过<!--和-->符号包裹的部分将会被smarty模版替换,$out下的节点包括ret,info,data,data已经在上面复制为队伍数组了,这里利用section循环来将每一组队伍信息打印到tr标签中。smarty模版支持section循环,if条件判断,操作非常方便灵活。

最后我们既可以看到类似如此的效果:

  这里我们看到的页面还是一个比较死的页面,要让他自动感知后台数据的变化,需要做以下工作。

9.页面自动刷新

js部分需要自动发送请求到服务器的某个接口去询问是否发生了变化,用来判断页面是否应该刷新。服务器的接口可以为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Client.php
    function getUpdateTime()
    {
        $gid = isset($_POST['gid']) ? $_POST['gid'] : '';
        DB::filter( $gid );
         
        $sql = "select * from t_score where gid='{$gid}' order by time desc limit 1";
        $db = DB::getInstance();
        $results = array();
        $ret = $db->select( $sql, $results );
        if( $ret['ret'] && $results )
        {
            $ret['time'] = 's'.$results[0]['time'];
        }
        else
        {
            $ret['ret'] = false;
            $ret['info'] = "还没有该队伍的打分记录.";
            echo json_encode( $ret );
            return;
        }
         
        $ret['info'] = '获取最大更新时间成功.';
        echo json_encode( $ret );
        return;
    }

 这个接口从t_score表中将最近更新的一行的时间获取到,并返回。如果吐出页面的时间和后台数据最近更新的时间不一致,那么就需要刷新页面。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// grouplist.html
function updatePage()
{
    var url = '?m=Client&a=getUpdateTime';
    var data = {};
    $.ajax({type: "POST", url: url, data: data, success: function(str){
        var info = "var result=" + str +';';
        try{
            eval(info);
        } catch(exception) {
            alert(info);
            return;
        }  
         
        if( result['ret'] && g_time < result['stime'] ){
            window.location.href = window.location.href;
        }
    }});
}

 这里url中的getUpdateTime指向的服务函数就是上面php脚本中的getUpdateTime函数了。关于如何将url定位访问到服务器的某个脚本函数,这是一个很基础的问题。我还是简单介绍下吧。在index.php函数中一般可以拿到浏览器向服务器发送的请求url,获取到url中的任何信息。我这里将m定位为服务器上的某个类名,例如Client类,a参数定义为类中的函数名,那么服务器上只需要在入口文件中加入如下代码,就可以定位到php的脚本中了,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Module.php
    function parse()
    {
        $module = isset($_GET['m']) ? $_GET['m'] : '';
        $action = isset($_GET['a']) ? $_GET['a'] : '';
        $this->_rute = array(
                            'module' => $module,
                            'action' => $action,
                        );
                         
        if( !$module )
        {
            return false;
        }
 
        $file = BASE_PATH.'/Module/'.$module.'.php';
        if( ! is_file( $file ) )
        {
            throw New Exception( "can't find module [{$file}]." ) ;
            exit;
        }
             
        require_once $file;
         
        if( ! class_exists( $module ) )
        {
            throw New Exception( "can't find class [{$module}]." ) ;
            exit;
        }
         
        if( ! method_exists( $module, $action ) )
        {
            throw New Exception( "can't find action [{$action}] in class [{$module}]." ) ;
            exit;
        }
         
        $obj = new $module();
        $obj->$action();
         
        return true;
    }

 php是动态脚本,随时都可以从字符串中决定调用什么类,什么函数。这也正是脚本的最突出的特点之一。

  9.其他注意点

wampserver如果默认没有开启online模式,那么局域网中的其他机器是不能访问到count子站。是否开启了这个模式,可以将鼠标move over任务栏上的wampserver图标,将会显示这个信息,如果没有可以鼠标左键单击,开启online模式。

如果后台的脚本使用POST方式去请求,那么从页面上要调试后台脚本时,如果发现不能访问到,可以按照这样的步骤去进行:先看apache日志是否有捕获到这个请求,如果未捕获到,可以重启apache,或者重启所有服务(只是稍慢几秒);如果日志中显示异常,可能是apache的配置不正确,请你检查count.conf文件,如果没有错误,那么还要注意allow all的配置,不要将所有请求都deny,如果要限制本机,也是在这里设置的,例如:deny from localhost,127.0.0.1等。

【总结】

到这里,所有我想讲述的技术点都在这里了。我并没有把所有页面和接口的开发都统统列举,只是点到即止。如有疑问,请留言即可。这里是其中部分脚本:

代码下载

posted @   布同  阅读(1949)  评论(2编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示