布同: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目录下一般会有已经有几个文件了,你可以拷贝其中一个自己建一个子站,稍加修改,如:
// 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文件,其中可以加入如下测试代码:
// 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函数即可。
// 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为主键。
建表如下:
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类再包一下。例如:
// 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访问数据库的基本函数也可以封装下,将错误记录到执行的文件中去。例如:
// 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,将数组转换的麻烦。例如:
// 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类开发
// 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的信息,可以做如下的代码,例如:
// 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部分需要自动发送请求到服务器的某个接口去询问是否发生了变化,用来判断页面是否应该刷新。服务器的接口可以为:
// 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表中将最近更新的一行的时间获取到,并返回。如果吐出页面的时间和后台数据最近更新的时间不一致,那么就需要刷新页面。例如:
// 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的脚本中了,例如:
// 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等。
【总结】
到这里,所有我想讲述的技术点都在这里了。我并没有把所有页面和接口的开发都统统列举,只是点到即止。如有疑问,请留言即可。这里是其中部分脚本: