编程

Laravel 中高效的用户时区处理

1269 2024-03-24 12:35:00

当今世界,web 应用经常用来满足来自不同时区的用户的需求。无论是事件调度、显示准确的时间戳还是管理用户交互,正确处理时区对于提供无缝的用户体验至关重要。

然而,管理时区可能是一项复杂的任务,尤其是在处理不同地区、夏令时变化和不同用户偏好时。

在构建应用时,几乎可以肯定地你会存储 DateTime 和其他与时间戳相关的数据。默认情况下,Laravel 在每个迁移文件中创建一个 created_atupdated_at 字段,并处理将它们正确添加到数据库中的过程。

我们大多数人都在不同的时区,因此,我们需要将所有日期存储在 UTC 中。这是世界调节时钟和时间的主要时间标准。它的有用之处在于,它被普遍接受为默认时间标准,因为它位于 0° 经度。

大多数应用都以这种格式存储日期和时间。虽然这是好事,但它带来了另一系列问题。由于大多数人都在不同的时区,我们可能需要参考 UTC 时区中的本地时间。

这在许多应用中没有什么不同。我们需要考虑到我们的应用的用户位于不同的时区,因此我们需要根据他们的时区在前端为他们显示数据。你可以在这里找到所有时区的列表

早些时候,我创建了一篇关于日期查询范围(Date Scope)的文章,允许我们使用日期查询范围从数据库中获取数据。这导致 Laravel 使用默认的应用时区(UTC)从数据库中获取数据。我们可能需要更改逻辑以考虑用户的时区。例如,澳大利亚悉尼的用户处于 UTC+10 时区,这意味着他们比 UTC 提前了 10 小时。因此,如果我们想正确地显示他们的记录,那么仅基于 UTC 的提取对他们来说没有任何价值,因为今天对他们来说可能是 UTC 的前一天。

我们如何根据用户所在的时区获取数据?本文中,我们将探讨如何在 Laravel 中正确管理用户时区。Laravel 使用 Carbon 库为日期和时间操作提供了强大的支持。Carbon 可用于将日期和时间值转换为用户的正确时区。

以 UTC 存储日期

在软件开发中,一致而准确地存储日期和时间是构建可靠应用的一个基本方面。一种被广泛采用的最佳做法是将协调世界时(UTC)的日期存储在应用的数据库中。将日期以 UTC 存储带来了许多好处,并有助于克服与时区和夏令时更改相关的复杂性。

当日期以 UTC 存储时,可以为不同时区的所有操作建立一个通用参考点。UTC 是一个标准化的全球时间,不受夏令时调整或地区时区差异等当地变化的影响。这种一致性确保了无论用户的位置如何,存储的日期和时间值都将保持不变。

通过使用 UTC 作为数据库中日期的内部表示,可以消除在本地时区存储日期时出现的歧义。假设一个用户在本地时区创建约会的场景。如果在没有转换的情况下按原样存储日期和时间,当不同时区的用户访问和解释这些信息时,可能会出现复杂情况。将日期以 UTC 存储可以为计算和比较提供通用参考,从而缓解这一问题。

Carbon 来拯救

正如我们所看到的,在处理多个时区时会出现许多复杂性。我们已经确定,以 UTC 存储日期和时间是最好的方法,因为它提供了一个中心参考点。这不仅有助于我们保持应用程序的一致性,而且在冬季或夏季等不同季节不会受到时间调整的影响。

Carbon 库是一个强大的工具,可以简化 Laravel 中日期时间值的操作和管理。它提供了多个实用函数,如 Carbon::now()Carbon::today(),这有助于处理日期和时间。

Carbon 帮我们解析数据并转换成各种格式,这样你就可以将日期对象转换成人类可读的格式了。

Carbon 的主要优势之一是其对时区工作的无缝支持。它通过提供各种设置 Carbon 对象时区的方式简化了时区转换。

第一种方式是使用 setTimezone() 方法。该方法允许你将 Carbon 对象转换成不同的时区,同时保留了底层的 datetime 值。

Carbon::now()->setTimezone(‘Australia/Sydney’);

上述代码将获取 UTC 的当前时间转换成悉尼本地时间

另一种方式是直接在对象实例化时设置时区

Carbon::now(‘Australia/Sydney’);

此方法会自动将新创建的 Carbon 对象的时区设置为指定的时区。这意味着 Carbon 将不会保留基本的日期时间值,因为它是用该时区的 Datetime 实例化的,在这个例子中,该时区是获取悉尼的当前本地时间。

你应该在什么时候使用它们呢?

now($timezone)

当你在指定时区中使用当前 datetime 并且随后不需要任何时区转换时,now($timezone) 是一个理想的选择。它以所需的时区创建 Carbon 对象,确保在该对象的后续操作都在指定时区内。这种方法简洁方便,尤其是在需要隔离特定时区内的日期时间操作的情况下。

now()->setTimezone($timezone)

当你已经有一个 Carbon 对象并且需要在不同时区之间执行转换时,使用 now()->setTimezone($timezone) 是合适的。例如,如果有一个 Carbon 对象表示应用时区中的当前日期时间,则可以使用 now()->setTimezone('Australia/Sydney') 将其转换为 “Australian/Sdney“ 时区。

当使用许多时区,并且需要以 UTC 实例化日期对象,然后将其转换为其他时区时,这一点非常有用。

数据库同步(MYSQL).

如果我们没有配置数据库服务器以知悉各种时区,那么所有这些工作都将是无用的。

我将使用 MYSQL 作为一个例子,但应该有其他数据库引擎的指南。

为了有效地管理 Laravel 应用中的时区,访问准确全面的时区数据至关重要。MySQL 是广泛使用的数据库管理系统,提供了对时区数据的内置支持。通过将适当的时区数据导入 MySQL 服务器,可以确保 DateTime 值的准确转换和一致处理。

理解 MySQL 中的时区数据

MySQL 使用时区数据库,其中包含大量时区信息,包括地理位置、夏令时规则、历史时区变化等。这些数据对于准确的时区转换和计算至关重要。

导入时区数据到 MySQL

要将时区数据导入到 MySQL 服务器中,你可以使用 MySQL 提供的 mysql_tzinfo_to_sql 功能。该功能将通常位于 MySQL 安装目录的时区数据文件,转换成可以执行的 SQL 语句,填充MySQL 数据库中的 time_zone 表格,

你可以在命令行中运行 mysql_tzinfo_to_sql 命令,指定时区数据文件路径及输出文件路径。

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql 

该命令在填充 MySQL 数据库中的 time_zone 表格,使得时区数据可以在 MySQL 中使用。

现在,我们可以使用原生 SQL ,将各种字段转换成用户的时区。

SELECT CONVERT_TZ('2030-05-15 12:00:00','UTC','Australia/Sydney');

然后你就可以准备能够以正确时区获取数据的 Eloquent 查询。

推测/请求用户时区

我们已经看到了为用户提供个性化体验的好处,并将此功能提供给他们,让他们看到你的应用为他们提供的价值。虽然后端逻辑可能已经完成,但我们仍然需要处理前端方面的事情。

幸运的是,我们可以通过各种方式请求用户的时区。

用户偏好

这是一种简单的方法,因为用户可以在其帐户设置或配置文件中明确提供他们的时区偏好。这允许他们明确定义他们想要的时区,确保在整个应用中准确地表示日期和时间。

可以以下拉列表的形式,要求用户设置他们喜欢的时区。然后,你可以将此时区存储在数据库中或作为 cookie 存储。

客户端时区推测

有时,第一种方法可能会受到限制,尤其是当你的应用正在处理团队支持时。例如,用户可能会雇佣承包商,并邀请他们作为团队成员进入他们的帐户。在这种情况下,承包商可能与用户处于不同的时区,因此,如果他们被雇佣来提供各种数据的见解,第一种方法可能会受到限制,这取决于他们的应用用例。

我们可以使用某些技术,包括尝试猜测当前用户的时区。有一些库,如 moment-timezone,可以帮助做到这一点。

import moment from 'moment-timezone';

const timezone = moment.tz.guess();

虽然不精确,但此库会尝试猜测时区,你可以在向后端发出请求时将时区附加为标头或正文。这有助于为系统中的所有用户提供个性化的方法。

IP Geolocation

这可能是我最不推荐的方法。它设计获取用户的 ip,并使用它来尝试获取用户的时区。我最不建议这样做,因为使用用户的 IP 可能不会产生正确的结果,因为用户可能正在使用 VPN,因此可能导致扭曲的结果。

我会把它作为后备方案,以防前两种方法没有生成好的结果。

if ($request->hasHeader('X-Timezone')) {
     $timezone = $request->header('X-Timezone');
} else {         
    $ip = $request->ip();
    $url = "http://ip-api.com/json/$ip";

    $tz = file_get_contents($url);
    $timezone = json_decode($tz, true)['timezone'];
  }
}

这样,你就可以获得用户的时区,可以在后端使用该时区来获取正确的结果。

联结起来

一旦我们处理了前端和后端的逻辑,我们就可以将其联结起来。

php artisan make:controller AnalyticsController -r

我将时区设置为 header,因此,我将使用 Axios 拦截器为用户设置时区。

//customAxios.js

import axios from "axios";
import moment from 'moment-timezone';

const instance = axios.create();

instance.interceptors.request.use( function (config) {
    const timezone = moment.tz.guess();
    config.headers['X-Timezone'] = timezone;
    return config;
});

export default instance;

来自前端的每个请求都应该有 X-Timezone 头,我们可以在后端检索它。

//App/Http/Controllers/AnalyticsController

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Carbon;
use App\Models\Analytics;
use Illuminate\Http\Request;

class AnalyticsController extends Controller
{
  public function index(Request $request)
    {
        if ($request->hasHeader('X-Timezone')) {
            $timezone = $request->header('X-Timezone');
        } else {
            $ip = $request->ip();
            $url = "http://ip-api.com/json/$ip";
            $tz = file_get_contents($url);
            $timezone = json_decode($tz, true)['timezone'];
        }

        $startDate = $request->query('start');
        $endDate = $request->query('end');

        $start = Carbon::parse($startDate, $timezone)->startOfDay();
        $end = Carbon::parse($endDate, $timezone)->endOfDay();

        $query = DB::raw("CONVERT_TZ(`created_at`, 'UTC', '$timezone')");

        $analytics = Analytics::whereBetween($query, [$start, $end])
->get();

        return $analytics;
    }
}

在这种情况下,我们将使用日期范围来获取特定范围之间的记录。

差不多就是这样。

结论

在 Laravel 中管理用户时区是构建提供准确和个性化体验的应用的一个关键方面。通过利用 Laravel 和 Carbon 库的强大功能,可以有效地处理与时间相关的操作,并确保不同时区的日期和时间的一致表示。

在本文中,我们探讨了各种概念和技术,以帮助您有效地管理 Laravel 应用中的用户时区。希望这篇文章对你有用。感谢阅读并欢迎留言探讨!