• 布同: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="10%;">队伍编号</td>
    	<td style="40%;">名字</td>
    	<td style="20%;">时间</td>
    	<td style="10%;">打分状态</td>
    	<td style="10%;">管理</td>
    	<td style="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等。

    【总结】

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

    代码下载

  • 相关阅读:
    使用 yo 命令行向导给 SAP UI5 应用添加一个新的视图
    SAP Fiori Elements 应用的 manifest.json 文件运行时如何被解析的
    SAP UI5 标准应用的多语言支持
    微软 Excel 365 里如何设置下拉菜单和自动高亮成指定颜色
    SAP Fiori Elements 应用里的 Title 显示的内容是从哪里来的
    本地开发好的 SAP Fiori Elements 应用,如何部署到 ABAP 服务器上?
    如何在 Cypress 测试代码中屏蔽(Suppress)来自应用代码报出的错误消息
    教你一招:让集群慢节点无处可藏
    应用架构步入“无服务器”时代 Serverless技术迎来新发展
    MySQL数据库事务隔离性的实现
  • 原文地址:https://www.cnblogs.com/Hiker/p/dance.html
Copyright © 2020-2023  润新知