最近项目中有个需求, 要记录新注册用户的次日登录情况, 于是写出了如下代码:
$create_time = '用户注册时间'; //格式 Y-m-d H:i:s $time = time(); $lasttime = date('Y-m-d H:i:s', $time); $current_day = floor($time / 86400); $create_day = floor( strtotime($create_time) / 86400 ); $days = $current_day - $create_day; switch ($days) { case 1: $values['2day'] = 1; break; //次日登陆 case 6: $values['7day'] = 1; break; //七日登陆 case 14: $values['15day'] = 1; break; //十五日登陆 } //执行SQL修改数据库相关字段
这段代码放到线上后, 出现了奇怪的BUG, 明明是当天注册的用户, 却出现了有次日登录的情况. 排查代码没有发现问题, 于是暂时搁置去忙其它事情. 然后在第6天时, 竟然又出现了有七日登陆的数据. 于是开始和同事正式解决这个问题, 最终发现是由于函数的时区原因导致, 具体如下:
time() 返回自从 Unix 纪元(格林威治时间 1970 年 1 月 1 日 00:00:00)到当前时间的秒数.
上面是 time() 函数在手册中的说明, 重点是格林威治时间, time() 始终返回的是格林威治时间的时间戳. 当PHP设置过时区后, date() 在格式化时间的操作中会将 (当前时区的时间 - 格林威治时间) 的偏移量自动添加进去, 按东八区的时间算也就是8小时. strtotime() 同样会自动将时区的偏移量加入处理操作中. 所以这时上面代码中 strtotime($create_time) 得到的同样是格林威治时间. $current_day 与 $create_day 现在都是按照格林威治时间计算的天数, 而BUG也就出现在这里.
比如当前时间为 2015-02-02 07:00:00 那么格林威治时间为 2015-02-01 23:00:00 (当前时间减去8小时)
当前时间为 2015-02-02 09:00:00 格林威治时间为 2015-02-02 01:00:00
再通过 floor() 处理后, 就相当于格林威治时间的 2015-02-02 与 2015-02-01, 中间相差一天.
所以如果用户在7点多注册, 而在9点再次登录的情况下, $current_day - $create_day = 1.
测试代码如下:
//date_default_timezone_set('UTC'); //设置为格林威治时间 date_default_timezone_set('Asia/Shanghai'); //设置为东八区上海时间 $a = floor( strtotime('2015-02-02 07:00:00') / 86400 ); $b = floor( strtotime('2015-02-02 09:00:00') / 86400 ); echo $b - $a; // 结果 1 //将格林威治时间打开, 注释掉上海时间, 结果输出为 0.
最终解决BUG后的代码如下:
$create_time = '用户注册时间'; //格式 Y-m-d H:i:s $time = time(); $lasttime = date('Y-m-d H:i:s', $time); //时间戳总是获取的格林威治时间, strtotime()会自动添加当前时区的偏移量, 这里因时区问题导致天数计算出现一天的误差, 所以在处理时间戳时增加时区的偏移量 $current_day = floor( ($time + date('Z')) / 86400 ); $create_day = floor( (strtotime($create_time) + date('Z')) / 86400 ); $days = $current_day - $create_day; switch ($days) { case 1: $values['2day'] = 1; break; //次日登陆 case 6: $values['7day'] = 1; break; //七日登陆 case 14: $values['15day'] = 1; break; //十五日登陆 } //执行SQL修改数据库相关字段