月度归档:2012年12月

一天一笔记

上班也有三年多了,大概是从10年时候有一个对当天工作情况记录的习惯;都是记录在笔记本上面的,去年写完了一本,今年也买了一个新的笔记本,在9月份的时候突然笔没有墨水了,又多次搞忘记了买笔芯;所以转念一想为什么不自己搭建一个网上笔记记录的平台呢?

目前部署在sina sae平台上面想看的请点http://daynote.sinaapp.com

首页(图片都还是本地测试数据)

于是就计划开始做了,要最快的做出来;所以使用的是同事@ljyf5593用kohanaframework写的一个cms,后台功能很完善,用户、权限、后台工具等等都直接采用,也就是只需要做下前台以及数据的展示就ok了。

要说记笔记这样的事情,其实有很多现成的软件可用;各种云平台,云笔记等等,同事就是用的盛大麦库云笔记,如果你还想极客一点可以使用各种云平台的开放api开发一个自己的云笔记应用。不过我觉得目前自己用的这个就挺好;直接web页面访问,也不用安装各种客户端,也不想去折腾云平台api了。所以就先用着呗。

要说自己认为的亮点,我觉得有这么几个

#标记管理

不用搞成每一个note笔记前都放置一个checkbox,在建一个checkbox submit进行删除、导出、发送等其他操作。且用户点击note即选择,取消则再点击一次就ok。结果集操作nav可以随着用户的结果记录是否存在而自动的显示与隐藏。js很简单,看源码ok。

#导出,email发送

在用户选择标记后,note集合可以用来删除,导出,email发送。其中导出就是导出为excel到线下。

导出

本来想把同一天的所有不同时段内容进行分组的,不过我觉得phpexcel进行数据处理真的太难搞了。所以就直接一条笔记对应一条记录。kohana 使用phpexcel真的很方便,直接加module就好了。github上就有,开源的贡献者真伟大。

email发送

标记标签后发送到邮箱,就是着过样子啦。。。ps: title 上面已经修改好了striptags

吐槽:sae其实真的很强大了,但是也为了安全运维等因素导致sae环境与lamp环境还是有区别的。比如禁止本地io,.htaccess采用了config.yaml。所以导致一些类库代码在 sae上面跑不起来,也没有一个具体的详细错误,对于排除起来真的好麻烦。所以目前显线上sae平台上面,导出,email发送都是空白空白。。。还得找个时间单独的把phpexcel与email模块进行sae测试下,进行具体的代码排查。

=== 2013年1月4日21:34:54 更新===

phpexcel在sae上能正常数据导出了,原因是这样的,phpexcel默认是采用Excel2007进行数据导出的,所以也是采用的Excel2007实现类。而Excel2007实现类需要zip扩展来实现的(也就是zipArchive类,不过可以用zlib来实现对于zip读写,不过成本就太大了)。官方也考虑到了可能不会支持zip扩展的情况,于是就使用xml实现了PHPExcel_Settings::PCLZIP的PHPExcel_Shared_ZipArchive类,不过在sae环境上测试时提示”XMLWriter class  not found “也就是需要xmlwriter的扩展支持(PHP 5 >= 5.1.2, PECL xmlwriter >= 0.1.0) sae上面没有支持。

//writer/Excel2007.php
$zipClass = PHPExcel_Settings::getZipClass();
$objZip = new $zipClass();

本来想替换另一种实现或者是直接csv导出的,又看到phpexcel里面还可用Excel5进行数据导出;抱着试一试的态度看看,测试正常数据导出!这说明Excel5没有采用xml进行excel创建,详细还没看代码。

改为Excel5数据导出很简单


$objWriter = PHPExcel_IOFactory::createWriter($this->_spreadsheet, 'Excel5'); // 第二参数把Excel2007改为Excel5就行
$objWriter->save('php://output');
exit;

#支持markdown语法

markdown很流行,开源平台,大公司各种支持。不过markdown听闻版本分化严重啊,希望能有一个统一标准。确实很好用,如果没有一个格式体现,全是一段段文字看起来很累。https://github.com/michelf/php-markdown

#笔记note编写自动保存

这个很简单,直接setIntval 15秒自动保存,就是要区分创建与修改的动作。如果是创建则自动写入数据库,把note创建成功的noteid返回到表单,这时点击发布时实际上市修改了。如果你没有点击发布而是刷新了页面,那么这时还只是草稿,草稿数则+1;

#note笔记归档

用户笔记可以按照月份、年份进行排序实现。便于用户进行自我梳理查找。我觉得这里的作用应该大于笔记搜索;让用户可以看看自我的成长经历。个人就这挫设计,其实有更好的ui 设计,比如现在很流行的时间线。

其他还想实现的功能,比如可以让textarea支持table的(tabIndent.js)不过有时候我经常在touch,移动设备上面写,table这时现的很多余所以暂时没有加上去。还有同步发送到微博,不过貌似笔记是比较私密的事情,当然也有可以公开的暂时也没有加。

有喜欢的可以注册用下哟。

一个有关当前时间的sql排序实现

在项目中遇到了这样的一个需求

“在用户参与的一个活动列表中,获取当前时间中刚刚开始了(也就是刚才开始的,不是最先开始)的一条活动;如果当前时间没有活动开始,则获取将要开始的一条活动记录”且必须在一次数据库查询操作中完成。

也就是必须要在数据库中做活动的状态区分。

现在具象化一点的理解这个需求,有这样一个表t_time

里面只有三个字段

id,starttime,endtime

我们假定现在每一条记录就是一个活动。

现在可以把需求抽象的分析为“当前时间大于starttime且小于endtime的按desc到序排列获取第一条记录,当前时间小于starttime且小于endtime的按asc正序排列获取第一条记录”且同样是要在一条sql语句中完成。

于是开始进行sql编写,在sql要完成两种状态的辨别,那就肯定是需要条件分支类似的语句控制。

mysql中的条件语句分支,常用的就是if ,case when 语句。

在找到if then 的用法后,开始第一次if then语句的测试


if (SELECT * FROM `t_time` where starttime < UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP())
then ( SELECT * FROM `t_time` where starttime < UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP() order by starttime desc )
else if(SELECT * FROM t_time WHERE starttime > UNIX_TIMESTAMP() and endtime > UNIX_TIMESTAMP())
then ( SELECT * FROM `t_time` where starttime > UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP() order by starttime asc )
end if;

结果是;华丽丽的报错啊!!!

Err] 1064 – You have an error in your SQL syntax;

考虑到语法没有问题啊。mysql真是有待学习啊,在网上搜索后了解到“if then endif只能在procedure或是function里用”,手册也没看,就乱来。而且不能为了这么一个处理还单独去创建一个存储过程或函数。

 

再查看case when语句,这次是能在sql中直接执行的。


select * from t_time where endtime > UNIX_TIMESTAMP()
and (case when starttime < UNIX_TIMESTAMP() then starttime < UNIX_TIMESTAMP() else starttime > UNIX_TIMESTAMP() end) order by starttime desc;

结果

但是还是没有达到预期效果,case when 没有起到作用,加上case when语句别名后的结果

可以看到case when 返回的是boolean结果;再次无效

 

于是继续查看手册还有没有办法,if()函数也能进行分支判断,可以一试。

IF(expr1,expr2,expr3)
如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。IF() 的返回值为数字值或字符串值,具体情况视其所在语境而定。

if()函数可以返回数值或者是字符


SELECT *,if(starttime < UNIX_TIMESTAMP(), 'starttime < UNIX_TIMESTAMP()', 'starttime > UNIX_TIMESTAMP()') as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() and t order by starttime desc;

 

 

结果还是语法错误,[Err] 1054 – Unknown column ‘t’ in ‘where clause’

把 and t去掉;sql语句是能正常执行的。结果如下

if()函数确实是执行了,但是返回的字符 别名为t,sql语句还是不认识。这个没搞明白。
无奈,只好求教于公司同事w;在简单说明后,他说项目里面有一处地方用到了这个,已经有了一个sql语句实现了;原来这个项目里面已经处理过这样的业务需求了,他说那时候第一次研究出来的,因为这个项目我是半路进来的,前面的开发都没参与;但隐约记得那一段时间他们组里的人都在说“写出一个牛x的sql排序了”,不过因为各种原因,我没去了解。哎,总归还是遇上了。

于是请w去讲解下这个处理方式。

在查看到项目源代码后,他说简而言之就是一个排序的实现,sql里面还是要进行两个状态的判断;

就是通过if()函数构建一个线性的排序


SELECT *,if( starttime < UNIX_TIMESTAMP(), starttime - UNIX_TIMESTAMP(), (starttime - UNIX_TIMESTAMP()) + 2000000) as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() order by t asc;

分析if()函数的作用。

为true
也就是已经开始记录的排序,执行 starttime – UNIX_TIMESTAMP() 按得到的值;starttime asc排序这样就得到了“最先开始的活动记录,也就是按starttime asc排序”列表
为false
执行 (starttime – UNIX_TIMESTAMP())+20000000
starttime – UNIX_TIMESTAMP()这样得到了 “starttime距离现在最近的,也就是马上将要开始的starttime”记录列表;由于 “未开始记录的”starttime – UNIX_TIMESTAMP() 得到的值可能小于“已开始记录的”starttime – UNIX_TIMESTAMP() 的值,这种情况下排序就混乱了,所以要加上一个很大的值20000000避免这种情况,这样就能保证未开始记录的starttime永远大于已开始记录的 starttime值,这样未开始的值记录排在了已开始记录之后了。

( 图上的时间是假定的,就是为了说明已经开始的记录是哪几条。其实执行的时间也是算得出来的:-) )

这里我们的需求就快要实现了,可以看到已开始记录是按starttime  asc排序的。也就是得到的是“最先开始的活动记录”,而我们要的是“刚刚开始的活动记录”,实现这个其实很简单了

starttime < UNIX_TIMESTAMP() ,那么starttime – UNIX_TIMESTAMP() 则是一定为负数的;starttime越大,得出的绝对值越小。反之starttime越小,得出的绝对值则越大,但加上负号,则是starttime越小的,得出的负值越小了,最后sql语句都是asc排序的。结果集里面已开始的记录也就是asc排序的。

 

所以如果要starttime 正序desc排序,那就是 UNIX_TIMESTAMP() – starttime 表达式了,starttime越大的,得到的值越小;starttime越小的,得到值越大;因为最后sql语句是asc排序,结果集里面已开始的记录从排列上来说是desc倒序的。


SELECT *,if( starttime < UNIX_TIMESTAMP(), UNIX_TIMESTAMP() - starttime, (starttime - UNIX_TIMESTAMP()) + 20000000) as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() order by t asc;

可以看到id为2的记录排到第一了。

 

这个业务需求在与当前时间有关且记录是否过期进行排序的列表中常应用到,一次请求后数据处理过程中;可以用endtime与当前时间进行对比,对于已过期的记录进行标记,这样显示时可以很明了了。

 

这个之前在ppc上面提问题了的,查看可以点击这里

 

Apache Solr使用简介

在一个项目中需要使用全文检索功能,架构w推荐了Apache Solr一个独立的企业级别搜索服务器。于是就顺势了解下这个solr搜索服务器。

简介

官网地址:http://lucene.apache.org/solr/

solr是基于lucene的一个搜索服务器,lucene也是Apache的一个开源项目;

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,即它不是一个完整的全文检

索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。

Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起

完整的全文检索引擎。–来自百度百科

可以简单的说Lucene就是一个搜索引擎架构,而Solr则是一个搜索服务器的实现。

solr是使用java开发实现,目前已经到4.0版本了,运行需要java1.6或以上版本;基于http get进行xml/json/cvs/binary等格式类型的数据传输。solr的数据都是以document对象进行存储,每一个document都由许多field组成,且每一个document都有一个默认的uniqueid,默认的是id。

solr的安装很简单,下载完成后解压,进入solr/example目录

user:~/solr/example java -jar start.jar

输入http://ip:8983/solr/即可看到solr服务器的管理界面。

solr需要一个java服务器,solr服务器里面自带了jetty服务器所以不需要另立服务器,如果有tomcat也可以直接新开一个端口用来运行。

Dashboard是solr服务器的搭建路径,版本,运行环境,机器的物理内存,File Descriptor Count ,jvm内存使用情况等

Logging 则是运行的记录,包括错误,警告等

core admin则是 Schema 的管理界面,可以查看某个Schema的情况,同事还能新建,删除,优化等

java properties 即运行solr的java的所有详细参数

thread dump 运行线程的详细情况,各种数据跟踪

collection1 则是建立的Schema,可以查看Schema的记录数,版本数;还有如ping,query,config等管理操作

solr与php的结合实现

php的solr接口实现已现有代码实现了。项目地址:http://code.google.com/p/solr-php-client/

不过这里需要注意的是因为solr现在是4.0版本了,该php client实现里面还有几个需要注意的,如service.php 中实现add接口的addDocument方法,4.0版本的add属性参数已有改变,废弃了其中的allowDups,overwritePending,overwriteCommitted这几个参数所以需要将这几个参数去掉,否则solr服务器会报错。

一个很简单的例子

<?php

require_once 'Apache/Solr/Service.php';

$transportIntance = new Apache_Solr_HttpTransport_Curl();
//$transportIntance = new Apache_Solr_HttpTransport_CurlNoReuse();
// 如果这里有不同的Schema如ASchema与BSchema,则传入的第三个参数就是'/solr/ASchema/'与'/solr/BSchema/'
$solr = new Apache_Solr_Service('192.168.1.10', '8983', '/solr/');

// 检测solr服务是否可用
if($solr->ping()){
$solr->deleteByQuery('*:*');

//
// Create two documents to represent two auto parts.
// In practice, documents would likely be assembled from a
//   database query.
// 这里需要注意的是,使用的field必须存在于Schema中,否则document写入不到solr服务器,且检索时会产生
  // org.apache.solr.common.SolrException: ERROR: [doc=50cdce141f792] unknown field 'partno'的错误
  // 这是需要将partno等schema里面没有的field全部都加入到apache-solr-4.0.0/example/solr/collection1/conf/schema.xml <fields>里面
  // 写入到schema的 field 需要注意name与attribute,详细请看http://wiki.apache.org/solr/SchemaXml
  // 保存完成后重启服务器
$parts = array(
'spark_plug' => array(
'partno' => 1,
'name' => 'Spark plugdw',
//      'model' => array( 'Boxster', '924' ),
'year' => array( 1999, 2000 ),
'price' => 25.00,
'inStock' => true,
),
'windshield' => array(
'partno' => 2,
'name' => 'Windshield',
//      'model' => array('911', '111'), //multiple values encountered for non multiValued field model:
'year' => array( 1999, 2000 ),
'price' => 15.00,
'inStock' => false,
)
);

$documents = array();

foreach ( $parts as $item => $fields ) {
$part = new Apache_Solr_Document();
$part->id = uniqid();
foreach ( $fields as $key => $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $datum ) {
$part->setMultiValue( $key, $datum );
}
}
else {
$part->$key = $value;
}
}

$documents[] = $part;
}

//var_dump($documents);

// Load the documents into the index

try {
$solr->addDocuments( $documents );
    // 这里需要注意在加入document后必须要commit提交到检索索引里面,否则最新写入的内容不会被检索到
$solr->commit();
   // optimize索引优化这里会重建索引,每次adddocument时不必要进行,应该放到用户搜索很少的时刻定时执行
   // 如每周末的24点执行,crontab也是一个选择
$solr->optimize();
}
catch ( Exception $e ) {
echo $e->getMessage();
}

// Run some queries. Provide the raw path, a starting offset
//   for result documents, and the maximum number of result
//   documents to return. You can also use a fourth parameter
//   to control how results are sorted and highlighted,
//   among other options.
//
$offset = 0;
$limit = 10;

$queries = array(
'partno:1',
'name:*w*'
);

foreach ( $queries as $query ) {
$response = $solr->search( $query, $offset, $limit );

if ( $response->getHttpStatus() == 200 ) {
// print_r( $response->getRawResponse() );

if ( $response->response->numFound > 0 ) {
echo "$query <br /><br/>";

foreach ( $response->response->docs as $doc ) {
echo "$doc->partno $doc->name <br />";
}

echo '<br />';
}
}
else {
echo $response->getHttpStatusMessage();
}
}

}else {
die('no connection solr service...');
}

?>

solr的query语法

根据不同的需要进行选择。

/**
* query语法
* 查询语法 http://lucene.apache.org/core/3_6_0/queryparsersyntax.html
* 1) title:”The Right Way” AND text:go // 标题的值匹配,AND text:go 表示对title默认的值为go的项进行筛选,但不是必须的
* 2) mod_date:[20020101 TO 20030101] //mod_date日期在20020101与20030101之间的
* 3) title:{Aida TO Carmen}     // 标题在Aida 与Carmen之间的集合,但不包括Aida Carmen
* 4) content:”jakarta apache” AND “Apache Lucene” // 搜索的字段必须包含 jakarta apache与Apache Lucene;可以使用&&代替AND
* 5) content:+jakarta lucene     // 必须包含jakarta 或可能包含lucene
* 6) content:”jakarta apache” jakarta // 结果中包含jakarta apache或jakarta ;或使用content:”jakarta apache” OR jakarta。可以使用||代替OR
* 7) content:”jakarta apache” NOT “Apache Lucene” // 包含jakarta apache,但不包含Apache Lucene;可以使用! 代替NOT
* 8) content:”jakarta apache” -“Apache Lucene” // 包含jakarta apache,但不包括Apache Lucene;与NOT区别还没明白
* 9) content:(jakarta OR apache) AND website // 使用括号实现子查询,包含jakarta OR apache以及website
* 10) title:(+return +”pink panther”)        // 使用括号将多个子句合并为一个字段,标题中必须包含单词return以及短语”pink panther”
* 11) title:tes? // 使用? * 来实现内容的搜索
* 12) 支持转移字符的查询,需转移字符包括+ – && || ! ( ) { } [ ] ^ ” ~ * ? : \ 可以使用\来转义字符,如title:\(1\+1\)\:2
* 13) title:roam~ // ~表示模糊查询,romam~将查找到foam或roams得单词;roam~0.8表示将得到roam 80%的相似度单词
* 14) content:”jakarta apache”~10 // 临近搜索,检索相隔10个单词的”apache”和”jakarta”
*/

solr的搜索是自动进行分词的,当然这是指英文的空格分词;如果中文需要分词怎么办呢?当然也有有心人已经做出来了,就是这个mmseg4j 作者博客上的简介 作者对于lucene solr很有研究,值得一看。

更多阅读

Solr Tutorial

Apache Solr Wiki

淘宝网综合业务平台团队博客 solr文章

Lucene/Solr Optimize相关总结

Solr监控

js顺序执行与input吸引浏览器焦点的体现

在电子商务的充值、支付等动作时;用户确认订单后,执行提交动作,我们把这个页面称为“订单提交页面”,订单打开一个新的页面跳转至支付平台进行订单的处理,这个页面即为“支付平台处理页面”,而订单提交页面则显示一个遮罩层,提示用户在新页面完成订单且有“已完成支付”与“支付出现问题”两个按钮。

原型图片xx

而现在我就是要实现这样的一个业务流程。待整个业务流程完成后,使用firefox进行测试充值等,一切良好(当然架构,还有开发都没那么快与容易,详细可以再写一篇了)。完成后内部测试,使用各种浏览器,firefox,chrome,ie,360等浏览器进行操作。同事Y提醒在使用ie进行购买时,业务上也没有特别大的问题,但就是在进行确认订单提交动作后,form新页面打开,浏览器焦点在“支付平台处理页面”上面,不过这时“支付平台处理页面”突然最小化失去浏览器焦点;浏览器焦点又再次回到“订单提交页面”的弹出层上面。

很显然功能确实不受影响能正常使用,但是对于用户的体验不好,用户在完成订单提交后,浏览器焦点应该直接在“支付平台处理页面”上,用户可以直接进行支付平台处理操作。而现在用户则必须再次点击“支付平台处理页面”重新获取浏览器的焦点。

只能再次检查代码,这一流程使用的是很简单很原始的逻辑。下面的代码可以直接在IE8及其以下的浏览器(浏览器新打开页面必须是在新窗口的形式,如果是以新标签打开页面则没有这样的问题)中打开点击submit按钮即可出现这样的问题。


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>input test</title>

<script language="javascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script language="javascript" src="http://malsup.github.com/jquery.blockUI.js"></script>
<style type="text/css">

.loginform { width: 400px; height: 200px; border: 1px solid #ccc; margin: 0 auto;}
.message { width: 400px; height: 160px; border: 2px solid #ccc;  display: none; background:#fff;}
.message .i{ width: 80px; height: 20px; margin:10px; display:block; border: 1px solid #ccc;}
h4 { text-align: center; }
</style>
</head>

<body>

<div>
<form action="http://baidu.com" target="_blank" id="goform">
<h4>username:<input type="text" name="name" /></h4>
<h4>password:<input type="text" name="pwd" /></h4>
<h4><input type="button" id="go" name="go" value="submit" /></h4>
</form>

</div>

<div id="message">
<h4>提示信息</h4>
<h4><input type="button" value="ok" /><input type="button" value="no" /></h4>
<!--- <h4><a>ok</a><a>no</a></h4>--> <!-- 解决方法-->
</div>

<script language="javascript">

$(document).ready(function() {

$('#go').click(function(){

$.blockUI({
message: $("#message"),
timeout: false,
onUnblock: false,
css: {
top: '20%',
left: ($(window).width()-400)/2,
cursor: 'pointer',
margin: '0 auto',
background: 'none',
border: 'none'}
});
$('.blockUI').attr('title','点击关闭').click( $.unblockUI );
$('#goform').submit();
});
});
</script>

</body>
</html>

订单form提交的动作主要就是由

$(‘#go’).click(function(){
// 遮罩处理

// 表单提交
$(‘#goform’).submit();
});

这样的代码完成的,以js至上而下的执行流程。首先完成遮罩的处理,然后进行form订单的提交动作。左思右想应该是没什么问题的啊,但是还是如此。

于是开始考虑延时执行form表单提交的动作,意思就是让遮罩处理的动作完成后,在过几秒进行执行。

setTimeout(function(){$(‘#goform’).submit();  }, 10);

再次执行,弹出层正常执行,但因为IE提示安全警告拦截了新页面的弹出。因为IE不认为表单的提交是用户的手动操作,如要顺利弹出需要用户将该站加入允许弹出新页面的列表,这样更加不可取。

进而开始考虑实现浏览器的sleep,即休眠暂停当前线程;确实真的找到了有人实现sleep的东西,这里查看 。不过sleep的核心实现都是循环执行其他代码或者是指定一个时间如果时间到了,则跳出

<script type="text/javascript">
function sleep(milliSeconds){
var startTime = new Date().getTime(); // get the current time
while(new Date().getTime() < startTime + milliSeconds); // hog cpu
}
</script>

不过上面的代码其实并没有让脚本解释器sleep下来,而且有让CPU迅速上到高负荷的附作用。浏览器甚至会在该段时间内处于假死状态。不可取。

在迷惑了半天还是不知道为什么,于是只好请教大牛同事w了。问题简要的说明后,w看过代码后表示按顺序执行是完全没问题的,也进入了我之前的思维,使用setTimeout,更改动作逻辑。同样行不通,最后w注意到弹出层的内容,看到里面有两个input标签;于是就是直接把它改为a标签。再次尝试果然就好了,w说input会自动获取焦点的,而a标签则不会。

在把input替换为a标签后,没有出现这样的情况了。这是为什么呢。在对网页用table选择不同标签时,页面标签都能正常获取到焦点。浏览器加载网页时a与input的焦点获取原理是怎样的?

discuzx2系列之群组积分排行

很长一段时间内都在忙着一个用discuzX2二次开发的项目。刚开始接触discuzx,查看里面的代码的时候感觉就像渣渣一样。虽然功能很强大,但是觉得代码真是太混乱了。不过随着后来项目定制的深入以及慢慢对discuzx的了解,discuzx真的很强大。尽管有些代码不是很规范,比如全局变量的滥用,函数里面require文件,输入数据的检测,模板里面的逻辑混乱可阅读性不高,整体架构只是类mvc还是很传统的函数编程等等还有很多。不过discuzx确实是一款成功的论坛类cms的系统,是一款很好的产品,可扩展性高,数据承载量大,功能强大,快速高效。现在就介绍下遇到的问题以及对这些问题的处理方法。

discuzx的资料:

discuzx的技术文库 http://dev.discuz.org/wiki

discuzx主站 http://www.discuz.net/

现在discuzx已经更新到2.5了。听说3.0现在正在研发中,2.5版本的整体架构变更比较大,很多之前直接写sql的以及用函数的地方都使用类库来替代了,不过代码还没有细看详细了解自行看源码。

这次要记录的内容是discuzx群组模块里面的一个功能扩展,这个功能就是群组的积分排行以及群组的当月积分排行。

可以看出群组排行可以按两项排行,一是总积分排行,二是当月积分排行。

一群组总积分排行,这个其实很简单。

我们知道用户创建的群组数据都存储在forum_forum表中,我们通过查看数据字典可以很轻松的知道该表中的字段所代表的意义。

如上图commoncredits就是“群组公共积分”,那么现在就可以很轻松的按这个字段的值进行desc排序了。不过群组的公共积分是怎样计算的呢?下面在详细了解。

二群组当月积分的总排行

这个会比较麻烦,因为discuzx木有这个功能啊(侧面说明discuzx很强大,很多功能都不需要进行改动直接拿来用)。不过我们可以马上进行扩展。

首先就是要搞明白群组的“公共积分”是怎么得来的,不管是总积分排行还是当月积分排行,数据更新的来源都是同一处。不过现在无处下手不知道积分增加的代码在哪,不过积分增加一定是与用户在群组内的操作有关的,比如发帖,回复,回帖等。所以只好监控表数据,通过回帖操作,发现总积分commoncredits数据+1,不过第二次操作时数据就没有变化了,那就说明回帖的时候一定进行了数据的更新。

可以查找到帖子回复的代码文件是source/include/post/post_newreply.php

不过再次查找就是麻烦了,一段一段的读代码时间也太长了,所以简单的办法来了,直接source搜索commoncredits,commoncredits数据变更一定是有“commoncredits=commoncredits+1”的sql语句了,于是搜之就出现结果了,就是function_grouplog.php这个文件里面,只有这一个函数。然后再用“updategroupcreditlog”在post_newreply.php里面搜索是在哪调用的就知道了

我们还可以具体了解到updategroupcreditlog是在哪些的动作中调用的。

可以看到发帖、回帖、发布商品动作都会对群组的公共积分进行增加。

那么接下来要做的事就是了解updategroupcreditlog函数了。

查看源代码可以了解到这个函数对于增加积分的行为。使用cookie可以避免用户每次回帖或发帖都检测数据库,同时在服务器端forum_groupcreditslog中保存用户今天在该论坛是否回帖的数据是很有必要的。因为这样可以规避用户更改客户端时间或删除cookie信息来为增加群组的积分。接下来就是通过检查$_G是否存在当前群组的信息;然后再检测群组的等级,判断群组是否足够积分进行升级。


<?php

/**
 *      [Discuz!] (C)2001-2099 Comsenz Inc.
 *      This is NOT a freeware, use is subject to license terms
 *
 *      $Id: function_grouplog.php 16644 2010-09-11 03:33:30Z monkey $
 */

if(!defined('IN_DISCUZ')) {
 exit('Access Denied');
 }

function updategroupcreditlog($fid, $uid) {
 global $_G;
 if(empty($fid) || empty($uid)) {
 return false;
 }
 $today = date('Ymd', TIMESTAMP);
 $month = date('Ym', TIMESTAMP);

$updategroupcredit = getcookie('groupcredit_'.$fid);
 if($updategroupcredit < $today) {
 $status = DB::result_first("SELECT logdate FROM ".DB::table('forum_groupcreditslog')." WHERE fid='$fid' AND uid='$uid' AND logdate='$today'");
 if(empty($status)) {

DB::query("UPDATE ".DB::table('forum_forum')." SET commoncredits=commoncredits+1 WHERE fid='$fid'");
 DB::query("REPLACE INTO ".DB::table('forum_groupcreditslog')." (fid, uid, logdate) VALUES ('$fid', '$uid', '$today')");
 if(empty($_G['forum']) || empty($_G['forum']['level'])) {
 $forum = DB::fetch_first("SELECT name, level, commoncredits FROM ".DB::table('forum_forum')." WHERE fid='$fid'");
 } else {
 $_G['forum']['commoncredits'] ++;
 $forum = &$_G['forum'];
 }
 if(empty($_G['grouplevels'])) {
 loadcache('grouplevels');
 }
 $grouplevel = $_G['grouplevels'][$forum['level']];

if($grouplevel['type'] == 'default' && !($forum['commoncredits'] >= $grouplevel['creditshigher'] && $forum['commoncredits'] < $grouplevel['creditslower'])) {
 $levelid = DB::result_first("SELECT levelid FROM ".DB::table('forum_grouplevel')." WHERE type='default' AND creditshigher<='$forum[commoncredits]' AND creditslower>'$forum[commoncredits]' LIMIT 1");
 if(!empty($levelid)) {
 DB::query("UPDATE ".DB::table('forum_forum')." SET level='$levelid' WHERE fid='$fid'");
 $groupfounderuid = DB::result_first("SELECT founderuid FROM ".DB::table('forum_forumfield')." WHERE fid='$fid' LIMIT 1");
 notification_add($groupfounderuid, 'system', 'grouplevel_update', array(
 'groupname' => '<a href="forum.php?mod=group&fid='.$fid.'">'.$forum['name'].'</a>',
 'newlevel' => $_G['grouplevels'][$levelid]['leveltitle']
 ));
 }
 }
// 当月积分排行的处理
// 当前月份财富记录判断
 $monthstatus = DB::result_first("SELECT *  FROM ".DB::table('forum_groupmonthcredits')." WHERE fid='$fid' AND month='$month'");
 // 已存在当月财富记录
 if( $monthstatus){
 DB::query("UPDATE ".DB::table('forum_forum')." SET monthcommoncredits=monthcommoncredits+1 WHERE fid='$fid'");
 DB::query("UPDATE ".DB::table('forum_groupmonthcredits')." SET credit=credit+1 WHERE fid='$fid' AND month = {$month}");
 }else {
 // 需创建新的记录
 DB::query("INSERT INTO ".DB::table('forum_groupmonthcredits')." (fid, month, credit) VALUES ('$fid', '$month', '1')");
 DB::query("UPDATE ".DB::table('forum_forum')." SET monthcommoncredits=1,creditmonth = {$month} WHERE fid='$fid'");
 }

}
 dsetcookie('groupcredit_'.$fid, $today, 86400);
 }
 }


那么我们的目的明确了,接下来就是要分析处理了。

记录群组每月的增长财富值,并按当前月份的财富值进行排行。

群组表forum_forum新加入字段
monthcommoncredits // 当前月份财富值,积分值
creditmonth          // 财富值的当前月份,便于群组排序是与当前月份进行匹配

第一步
用户每天发帖,群组财富积分会增长。

第二部
借此机会可以实现每月的财富积分记录
新建一个数据表“群组每月的财富积分记录”

groupmonthcredits
fid  // 群组id
month // 月份
credit // 当月积分
可以用来记录统计某个群组的每个月的积分,对应可以扩展实现群组的当月积分趋势
普通用户if(empty($status)) 今天第一次进行发帖时
群组总积分+1
判断 groupmonthcredits 表的当前月份的财富积分记录是否存在?
如存在 则当前月份财富积分 credit 继续+1,群组表中的公共积分 monthcommoncredits 继续+1
否则不存在,说明现在进入新的月份,在 groupmonthcredits 创建新的当前月财富积分记录,credit默认为1(因为用户今天在群组内的第一次操作有财富积分+1奖励)。
同时forum_forum表中的当月财富积分 monthcommoncredits 更新为1,将当前财富月份 creditmonth 更新为当前的月份
第三步
群组按当前月份财富额排行,可以直接使用群组表 forum_forum 中的monthcommoncredits 进行desc排序。
但这里会存在一个bug,就是某群组8月的“当月财富积分”为1000,排名第一,这时候进入9月
但群组中的任何一位成员都没有进行发帖、回复等操作。于是该群组的财富积分还是 1000 ,还是高居榜首,原因就是第二步中的所有当月财富积分的判断都需要在用户进行发帖,回复等操作下进行处理。这时候要解决这个问题就需要用到 creditmonth 财富值月份字段了。只查询与当前月份匹配的群组记录

这样就可以避免了。尽管这样的可能性比较小,但是还需要做考虑。
对于财富积分的增长,始终放在用户手中。