我想写一个脚本,每天下午2点向用户的手机发送推送通知。但有来自世界各地的用户,所以我需要根据特定用户的时区随时发送通知,我将其保存在DB中,如"GMT+4"。
我需要通过克朗的工作来做到这一点。我从来没有用过它,所以我不知道如何编写这个脚本并用PHP实现它。有人能帮我吗?
一个非常简单的方法是让cron作业每1分钟调用一个PHP脚本,并让它检查数据库中需要做的事情。如果现在需要做什么(),那么就去做。
在crontab中,您可以使用类似的东西,
* * * * * /usr/bin/php /path/to/my/script.php
script.php可以是这样的
<?php
$ts = time();
// Run for up to 50 seconds
while($ts + 50 > time())
{
... SELECT id, time FROM Table WHERE time <= timeFromGMT(GMTvalue) ...
if(returns a row){
Send notification;
Set a flag that notification is send;
}
sleep(5);
}
注意:使用一些锁定机制来防止同时有多个进程
简介
跳到";解决方案摘要";如果您只对实际答案/解决方案感兴趣,请参阅下面的部分
我意识到这是一个老问题,有一个公认的答案和长期的闲置(坦率地说,截至本文撰写之时,没有那么多观点),但我认为这应该得到一个更好的答案,因为:
- 这个问题目前被接受的答案并没有深入到实质。答案并没有详细说明命名的(伪代码?)mysql函数";timeFromGMT";例如,以及需要什么才能使原始问题中描述的程序真正正常工作
- 这个问题几乎涵盖了我的确切情况,但它可以用很多不同的方式被问到。有很多现有的问答;A的主题相似(与时区相关),但到目前为止,我还没有发现像这一个这样合适;许多答案都假设了不同的条件(例如用户输入的日期/时间)
- 我相信这个问题的答案涵盖了比OP描述的情况更广泛的领域。我认为可以肯定地说,对很多人来说,使用时区也不是一项微不足道的任务
前面的一些东西
我重新表述了这个问题以扩大范围:
如何让一个PHP脚本(cron作业)在一组用户的本地时间做一些事情,只给定用户的时区(id)
-
;仅给定用户的时区id";部分是必不可少的。例如,在用户输入计划任务的网络应用程序中,您需要使用时间戳。但是如果用户例如只被呈现一个选项;每天给我发一份通知";我们没有这些信息。因此,我们可能拥有的只是用户的时区(id),以及他们是否启用了";每天做点什么"背景
-
这个问题的原始海报提到;"时区";以诸如";GMT+4";,这是否与夏令时有关——没有提及(或存储?)——我建议避免这样做。我建议存储用户的PHP时区(命名)id(例如"欧洲/阿姆斯特丹")。这些id可以用于PHP的所有日期&时间函数。
-
处理时区时的另一个一般建议是从UTC开始计算所有内容,因为夏令时不会影响它。因此,如果您有一个web应用程序,用户在特定的日期/时间输入任务,则存储UTC日期/时间(或从time()返回的PHP的UNIX时间戳,该时间戳也始终为UTC,即使与"设置的时区无关;date_default_timezone_set()"例如)。
-
为了覆盖所有不同的时区,cron作业必须至少每15分钟运行一次php脚本。例如,时区";Pacific/Chatham";撰写本文时为UTC+12:45。创建cron作业本身在许多现有文章中都有介绍,应该很容易搜索和设置。
解决方案摘要
为了让cron作业在用户本地时间的特定时间做一些事情,我们首先必须知道它当前在哪个时区。
如果(像最初的问题一样)我们想在当地用户时间每天下午2点做一些事情,我们可以在所有可能的时区上循环(我们可以使用DateTimeZone::listIdentifiers()检索),然后检查当前每个时区的时间,如果它与给定时间(下午2点,或本例中的"14:00")匹配,我们知道设置了该特定时区(并且启用了"每天做点什么"设置)的用户是我们需要从数据库中选择的用户。为了找到当前为给定时间的时区,我创建了一个自定义函数get_timezones_where_time_is()。
根据上面的要求,cron作业需要在给定时间的每个可能的时区中运行,以找到所有可能的匹配时区。即至少每15分钟一次,并且至少在指定的时间(本例中为下午2点)
这是一种边缘情况,但必须在指定时间的同一分钟内调用get_timezones_where_time_is()才能获得预期结果。例如,如果cron作业将在服务器时间12:00运行,但get_timezones_where_time_is()只在12:01的一分钟后执行,那么它当然不会再在世界上找到任何此时为14:00的时区(因为此时某个地方已经是14:01了)
解决方案
这是一个使用cron作业运行的示例脚本,有关更多详细信息,请参阅代码中的注释。它应该在PHP 5>=中工作5.2.0.
<?php
/* Just an example, since inluding files in a cron job can be counter intuitive */
set_include_path('/var/www/vhosts/example.com/');// this must be an absolute path, I recommended to the parent directory of your web/document root.
require_once 'config/config.php';// could contain the mysql login details
// Takes a time (hour and minutes) in the 24 hour clock format WITH LEADING ZERO'S, e.g. "14:00" or "04:00",
// Returns an array of timezone id's (from DateTimeZone::listIdentifiers()) where it currently is that given time, or an empty array.
// Throws exception on false $time input parameter
function get_timezones_where_time_is(string $time)
{
$time = explode(":", $time);
if(!isset($time[0]) || !isset($time[1]))
{
throw new Exception('Function get_timezones_where_time_is() expects a time paramter in 24 hour format with leading zero's, such as "04:00".');
}
$ret = array();
$timeHours = $time[0];
$timeMinutes = $time[1];
$timezoneIds = DateTimeZone::listIdentifiers();
foreach($timezoneIds as $key => $tzId)
{
$now = new DateTime(null, new DateTimeZone($tzId));
if($timeHours == $now->format('H') && $timeMinutes == $now->format('i'))
{
$ret[] = $tzId;
}
}
return $ret;
}
try
{
// get timezone id's where it is currently the given time of the day
$timezoneMatches = get_timezones_where_time_is('14:00');
// if no timezones found where it is currently the given time, we have nothing to do
if(!$timezoneMatches)
{
exit;
}
// we have atleast 1 found timezone where it is the given time of the day, so prepare and query the database
$mysqli = new mysqli(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_NAME);
if ($mysqli->connect_errno)
{
throw new Exception('mysqli connection error: ' . $mysqli->connect_error);
}
// just an example query, ofcourse
$query = "SELECT * FROM users WHERE timezone IN ('".implode("', '", $timezoneMatches)."') AND send_daily_notification = 1";
$selectUsers = $mysqli->query($query);
if(!$selectUsers)
{
throw new Exception("There was probably an error in the query: ".$query);
}
while($user = $selectUsers->fetch_assoc())
{
// do something for each user, such as sending a notification
}
$selectUsers->close();
}
catch (Exception $e)
{
// logs to php ini defined error log file
error_log('Cron job "send notification" failed with error message: '.$e->getMessage());
// output error here for easier debugging when running script directly (e.g. from command line)
echo 'Failed with error: '.$e->getMessage();
}
?>
后记
- mysqli的错误处理和选择当然是个人偏好,我尽我所能将最佳实践包括在内,并将其针对我认为访问StackOverflow的受众。例如,如果您认为要处理不同的数据库系统,那么您应该/可能希望使用PHPPDO
- 在我的测试/开发服务器上,DateTimeZone::listIdentifiers()包含425个时区;当找到50个时区时,具有给定时间的(自定义)函数get_timezones_where_time_is()将在大约20毫秒内完成。在给定的场景中,函数ony需要调用一次
- 使用DateTime对象(在函数get_timezones_where_time_is()的循环中)检查给定时区中的实际当前时间意味着我们还考虑了夏令时。例如,PHP ini时区设置或脚本中对date_default_timezone_set()的早期调用不应该影响结果
- 我建议避免选择可能在夏令时切换期间的时间。例如,get_timezones_where_time_is('02:00')可能会做一些意想不到的事情,如果时钟从03:00回到02:00,脚本很可能会"再次运行",因为实际上又是那个时间(我还没有测试)
- DateTimeZone::list缩写()可能看起来像是一个解决方案的一部分,但它包含历史时区数据,因此同一时区id可以以不同的偏移量多次出现在其中(请参阅链接的php手册页上当前投票最多的注释),这使得不适合通过偏移量获取时区id
- 我没有实现时区ID,因为我认为它们是可信的输入。如果引入了带引号的时区id,则显示的示例数据库查询将中断
- 如果像OP一样,您只存储了格式为"0"的用户时区;GMT+4";,如果没有夏令时,这就变成了一场猜谜游戏。我可能会在get_timezones_where_time_is()函数返回的时区id上循环,为每个时区创建一个DateTimeZone对象,并使用DateTimeZone::getOffset()获取GMT偏移量(以秒为单位)并更正dst。将该偏移量转换为小时,然后使用该偏移量来查询数据库中具有时区的用户;GMT+4";。如果可能的话,最好解决数据问题