【问题标题】:send cron job notification by timezone php通过时区php发送cron作业通知
【发布时间】:2015-09-24 14:12:59
【问题描述】:

我想编写一个脚本,每天下午 2 点向用户的手机发送推送通知。但是有来自世界各地的用户,所以我需要根据特定用户的时区及时发送该通知,我将其保存在数据库中,例如“GMT+4”。

我需要通过 Cron 工作来完成这项工作。我从未使用过它,所以我不知道如何编写该脚本并使其在 PHP 中工作。任何人都可以帮助我吗?

【问题讨论】:

  • 我猜,每小时运行一次 cronjob 并使用 mysqlquery 过滤脚本中的正确用户

标签: php cron timezone timezone-offset


【解决方案1】:

简介

如果您只对实际答案/解决方案感兴趣,请跳至下面的“解决方案摘要”部分。

我意识到这是一个老问题,答案已被接受且长期不活跃(坦率地说,截至撰写本文时,观点并不多),但我认为这值得更好的答案,因为:

  • 目前接受的对这个问题的回答没有深入细节。例如,答案没有详细说明命名(伪代码?)mysql 函数“timeFromGMT”,以及使原始问题中描述的程序真正按预期正常工作所需的条件。
  • 这个问题几乎涵盖了我的确切情况,但可以通过多种不同的方式提出。现有的问答有很多类似(时区相关)主题,但到目前为止我发现没有一个像这个一样合适;许多答案都假设了不同的条件(例如用户输入的日期/时间)。
  • 我相信这个问题的答案涵盖了比 OP 所描述的情况更广泛的情况。我认为可以肯定地说,对于很多人来说,使用时区也不是一件容易的事。

前面的一些事情

我对问题进行了一些重新表述以扩大范围:

如何让 PHP 脚本(cron 作业)在设定的用户本地时间做某事,只给定用户的时区 (id)?

  • “仅给定用户的时区 ID”部分是必不可少的。例如,在用户输入计划任务的 Web 应用程序中,您需要使用时间戳。 但是,例如,如果仅向用户显示“每天向我发送通知”之类的选项,我们就没有该信息。因此,我们可能只有用户的时区 (id),以及他们是否启用了“每天做某事”设置。

  • 这个问题的原始发布者提到让用户“时区”以“GMT +4”等格式存储,无论是否考虑到夏令时,都没有提到(或存储?) -我建议避免这种情况。 我建议改为存储用户的 PHP timezone (named) id(例如“Europe/Amsterdam”)。这些 id 可以在所有PHP's date & time functions 中使用。

  • 处理时区时的另一个一般建议是从 UTC 计算所有内容,因为夏令时不会影响它。 因此,如果您确实有一个用户在特定日期/时间输入任务的 Web 应用程序,请存储 UTC 日期/时间(或从 time() 返回的 PHP 的 UNIX 时间戳,也始终采用 UTC,即使不考虑使用“date_default_timezone_set()”设置的时区,例如)。

  • 为了覆盖所有不同的时区,cron 作业必须至少每 15 分钟运行一次 php 脚本。例如。在撰写本文时,“太平洋/查塔姆”时区为 UTC +12:45。许多现有文章都介绍了创建 cron 作业本身,并且应该易于搜索和设置。

解决方案总结

为了让 cron 作业在用户本地时间的特定时间执行某些操作,我们首先必须知道当前给定时间位于哪个时区。

如果(就像在原始问题中一样)我们想在本地用户时间每天下午 2 点做某事,我们可以遍历所有可能的时区(我们可以使用 DateTimeZone::listIdentifiers() 检索),然后检查现在是几点当前的每个时区,如果它与给定时间(本例中为下午 2 点,或“14:00”)匹配,我们知道设置了该特定时区(以及启用“每天做某事”设置)的用户是那些我们需要从数据库中进行选择。为了找到当前给定时间的时区,我创建了一个自定义函数 get_timezones_where_time_is()。

根据上述要求,cron 作业需要在给定时间的每个可能的时区偏移中运行,以找到所有可能的匹配时区。 IE。至少每 15 分钟一次,并且至少在指定的时间(本例中为下午 2 点)。

这是一种极端情况,但必须在指定时间的同一分钟内调用 get_timezones_where_time_is() 才能获得预期结果。例如,如果 cron 作业将在服务器时间 12:00 运行,但 get_timezones_where_time_is() 仅在 12:01 的一分钟后执行,它当然不会再在世界上的 14:00 找到任何时区时刻(因为那时已经是 14:01 了)。

解决方案

这是一个使用 cron 作业运行的示例脚本,有关更多详细信息,请参阅代码中的 cmets。它应该在 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 的观众。例如,如果您认为自己要处理不同的数据库系统,您应该/可能想要使用 PHP PDO
  • 在我的测试/开发服务器上DateTimeZone::listIdentifiers() 包含 425 个时区;当找到 50 个时区时,具有给定时间的(自定义)函数 get_timezones_where_time_is() 在大约 20 毫秒内完成。在给定的场景中,函数只需要调用一次。
  • 使用DateTime 对象(在函数get_timezones_where_time_is() 的循环中)检查给定时区的实际当前时间意味着我们还要考虑夏令时。例如,PHP ini 时区设置或更早在脚本中调用 date_default_timezone_set() 不应影响结果。
  • 我建议避免选择可以在夏令时切换期间的时间。例如。 get_timezones_where_time_is('02:00') 可能会做意想不到的事情,如果时钟从 03:00 回到 02:00,脚本很可能会“再次运行”,因为它实际上又是那个时间(我没有测试过) .
  • DateTimeZone::listAbbreviations() 可能看起来像是(部分)解决方案,但它包含历史时区数据,因此相同的时区 id 可以以不同的偏移量多次出现在其中(请参阅链接的 php 手册页上当前最受好评的评论),即例如,不适合通过偏移量获取时区 ID。
  • 我没有real_escape_string 时区ID,因为我认为它们是受信任的输入。如果曾经引入带引号的时区 ID,则显示的示例数据库查询将中断。
  • 如果像 OP 一样,您只以“GMT +4”格式存储用户时区,并且无论是否包括夏令时,这都变成了一个猜谜游戏。我可能会遍历我的 get_timezones_where_time_is() 函数返回的时区 ID,为每个时区创建一个 DateTimeZone 对象并使用 DateTimeZone::getOffset() 以秒为单位获取 GMT 偏移量并更正 dst。将该偏移量转换为小时,然后使用它在数据库中查询具有“GMT +4”格式的时区的用户。如果可能,最好解决数据问题。

【讨论】:

    【解决方案2】:

    一个非常简单的方法是让 cron-job 每 1 分钟调用一次 PHP 脚本,并让它检查数据库中需要做的事情。如果有什么事情需要做now(),那就去做吧。

    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);       
    } 
    

    注意:使用一些锁定机制来防止多个进程同时进行。

    【讨论】:

      猜你喜欢
      • 2014-07-29
      • 2012-06-16
      • 1970-01-01
      • 2021-02-17
      • 1970-01-01
      • 1970-01-01
      • 2013-08-31
      • 1970-01-01
      • 2015-04-05
      相关资源
      最近更新 更多