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:00
、America/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);
}
📌 核心逻辑:
moment.tz(date, "UTC")
:将 UTC 时间字符串解析为 Moment 对象.clone().tz(userTimezone)
:克隆并转换到用户时区.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 应用。如果你也在处理时区问题,不妨参考这个模式,少走弯路。