布同: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等。
【总结】
到这里,所有我想讲述的技术点都在这里了。我并没有把所有页面和接口的开发都统统列举,只是点到即止。如有疑问,请留言即可。这里是其中部分脚本:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .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 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义