布同: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等。

【总结】

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

代码下载

posted @ 2012-11-03 17:15  布同  阅读(1933)  评论(2编辑  收藏  举报