1. 概述

在本系列的 Spring 构建 Reddit 应用案例 中,我们将实现一个实用功能:根据用户的时区来安排帖子发布时间

处理时区问题向来是开发中的“踩坑重灾区”,技术方案五花八门。我们首先要解决的是:如何按照用户的(可配置)时区显示时间。其次,还需要明确:日期在数据库中应以何种格式存储

我们的策略是:

✅ 服务器统一使用 UTC 存储和传输时间
✅ 用户时区由前端配置,服务端保存
✅ 写入时转换为 UTC,读取时由前端转回用户时区

这样既保证了数据一致性,又实现了个性化展示。


2. 新增用户偏好设置 — timezone

我们先在已有的 Preference 实体中添加一个字段 timezone

@Entity
public class Preference {
    ...
    private String timezone;
}

然后在用户的“偏好设置页面”中提供时区选择功能。这里我们使用一个轻量级但非常实用的 jQuery 插件 Easy Timezone Picker

<select id="timezone" name="timezone"></select>
<script>
    $(function() {
        $('#timezone').timezones();
    });
</script>

⚠️ 注意:系统默认时区为服务器时区,即 UTC。新用户未设置时将使用此默认值。

该插件会自动生成一个带时区偏移的下拉列表(如 UTC+08:00America/New_York),用户选择后保存到其 Preference 中即可。


3. 控制器层:写入时的时区转换

核心逻辑在于:用户提交的时间是其本地时间,需转换为 UTC 存入数据库

我们来看 ScheduledPostRestController 的实现:

@Controller
@RequestMapping(value = "/api/scheduledPosts")
public class ScheduledPostRestController {
    private static final SimpleDateFormat dateFormat = 
      new SimpleDateFormat("yyyy-MM-dd HH:mm");
     
    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void schedule(
      @RequestBody Post post, 
      @RequestParam(value = "date") String date) throws ParseException 
    {
        post.setSubmissionDate(
          calculateSubmissionDate(date, getCurrentUser().getPreference().getTimezone()));
        // 保存到数据库
        postService.save(post);
    }
     
    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updatePost(
      @RequestBody Post post, 
      @RequestParam(value = "date") String date) throws ParseException 
    {
        post.setSubmissionDate(
          calculateSubmissionDate(date, getCurrentUser().getPreference().getTimezone()));
        postService.update(post);
    }
    
    private synchronized Date calculateSubmissionDate(String dateString, String userTimeZone) 
      throws ParseException {
        dateFormat.setTimeZone(TimeZone.getTimeZone(userTimeZone));
        return dateFormat.parse(dateString);
    }
}

关键点说明:

  • calculateSubmissionDate() 将用户提交的字符串时间(如 "2025-04-05 10:00")按其时区解析为 Date 对象
  • ⚠️ SimpleDateFormat 非线程安全,此处加 synchronized 保护(生产环境建议使用 DateTimeFormatter
  • ❌ 服务器返回给前端的仍是 UTC 时间戳,转换责任交给前端

为什么读操作不转换?
因为我们的前端是 SPA 架构,使用 Moment.js 统一处理时区显示,服务端无需介入,解耦更清晰。


4. 前端实现:基于 Moment.js 的时区渲染

4.1 展示帖子列表

在表格头部显示当前用户时区:

<table>
  <thead>
    <tr>
      <th>Post title</th>
      <th>Submission Date 
        (<span id="timezone" sec:authentication="principal.preference.timezone">UTC</span>)
      </th>
    </tr>
  </thead>
</table>

在加载数据后,调用 convertDate() 转换时间显示:

function loadPage(page){
    // 假设 posts 是从接口获取的 UTC 时间数组
    $('.table').append('<tr><td>'+post.title+'</td><td>'+
      convertDate(post.submissionDate)+'</td></tr>');
}

function convertDate(date){
    var serverTimezone = [[${#dates.format(#calendars.createToday(), 'z')}]]; // 输出 "UTC"
    var serverDate = moment.tz(date, serverTimezone);
    var clientDate = serverDate.clone().tz($("#timezone").html());
    var myformat = "YYYY-MM-DD HH:mm";
    return clientDate.format(myformat);
}

📌 核心逻辑:

  1. moment.tz(date, "UTC"):将 UTC 时间字符串解析为 Moment 对象
  2. .clone().tz(userTimezone):克隆并转换到用户时区
  3. .format():格式化输出

✅ 使用 clone() 是为了避免修改原对象,避免副作用。


4.2 发布新帖子

表单中提示用户输入其本地时间:

Submission Date (<span sec:authentication="principal.preference.timezone">UTC</span>)
<input id="date" name="date" />

<script type="text/javascript">
function schedulePost(){
    var data = {};
    $('form').serializeArray().map(function(x){data[x.name] = x.value;});
    
    $.ajax({
        url: 'api/scheduledPosts?date='+$("#date").val(),
        data: JSON.stringify(data),
        type: 'POST',
        contentType:'application/json',
        success: function(result) {
            window.location.href="scheduledPosts";
        },
        error: function(error) {
            alert(error.responseText);
        }   
    }); 
}
</script>

📌 注意:date 参数作为查询参数传入,值为用户本地时间字符串(如 2025-04-05 10:00),由后端按用户时区解析。


4.3 编辑已有帖子

编辑时需将数据库中的 UTC 时间“还原”为用户的本地时间,方便修改:

$(function() {
    var serverTimezone = [[${#dates.format(#calendars.createToday(), 'z')}]]; // "UTC"
    var serverDate = moment.tz($("#date").val(), serverTimezone);
    var clientDate = serverDate.clone().tz($("#timezone").html());
    var myformat = "YYYY-MM-DD HH:mm";
    $("#date").val(clientDate.format(myformat));
});

这样用户看到的就是自己熟悉的本地时间,无需手动换算。


5. 总结

我们为这个 Reddit 风格的应用增加了一个看似简单但极其实用的功能:全站时间按用户时区显示

在此之前,所有时间都以 UTC 展示,对非 UTC 时区用户极不友好——这正是我使用过程中最大的痛点之一。现在,通过以下设计,问题迎刃而解:

  • ✅ 用户可配置 timezone 偏好
  • ✅ 写入时由服务端转为 UTC 存储
  • ✅ 读取时由前端 Moment.js 转回本地时区
  • ✅ 数据库统一使用 UTC,避免混乱

这套方案简单粗暴但非常有效,适用于大多数 Web 应用。如果你也在处理时区问题,不妨参考这个模式,少走弯路。


原始标题:Displaying Dates in the Timezone of the User

« 上一篇: Baeldung周报第27期
» 下一篇: Baeldung周报第28期