国际化时间字段处理说明
国际化时间字段处理说明
目录
强制约定
入参:前端按客户端时区转 UTC 时间戳提交。落库:后端统一按北京时间(
Asia/Shanghai)写入,便于维护与 SQL 查询。出参:统一返回 UTC 的 ISO 8601;前端再转成客户端本地时区的Y-m-d H:i:s。
- 请求时间参数(如列表筛选的起止时间):一律由前端按客户端所在时区换算为 UTC Unix 时间戳(秒或毫秒)后提交;后端按时间戳理解绝对时刻,不再接收「无时区的本地时间字符串」作为权威入参。
- 后端落库:统一按北京时间(
Asia/Shanghai)逻辑落库(库内常见为Y-m-d H:i:s/datetime(6)墙钟时间)。运维排查、对账、手工 SQL 均以北京时间为准,降低心智负担。 - 响应时间字段:后端统一返回 UTC 的 ISO 8601(推荐形如
2025-01-01T04:00:00.000000Z);禁止在 Resource 里再格式化成业务方本地Y-m-d H:i:s。 - 前端展示:收到 ISO 8601 后,自动按客户端时区转为
Y-m-d H:i:s(或产品约定的本地展示格式)再展示。
参考实现以 user 服务中权限组列表筛选、通讯录相关时间筛选及 Resource 出参为准。
全链路概览
客户端本地时间(用户所见)
→ 前端按客户端时区转为 UTC 时间戳(秒或毫秒)
→ 请求体提交 start_time / end_time 等 numeric 字段
→ 后端将时间戳换算为北京时间后落库 / 按北京时间字段查询
→ 响应中时间字段为 UTC ISO 8601(…Z)
→ 前端按客户端时区格式化为 Y-m-d H:i:s 展示核心原则:传输用 UTC(时间戳进、ISO 8601 出);库内按北京时间存储与查询,便于维护;本地展示交给前端。
请求:前端 → 后端
约定
| 项 | 说明 |
|---|---|
| 字段示例 | start_time、end_time、invite_start_time、apply_start_time 等 |
| 类型 | 数字:秒级(约 10 位)或毫秒级(约 13 位及以上) |
| 语义 | UTC 绝对时刻对应的 Unix 时间戳(由客户端本地时间换算而来) |
| 校验 | Request 使用 integer / numeric,不要用「本地时间字符串」强校验 |
前端职责
- 用户在日历/日期组件选中的是本地时区的时间点或区间。
- 提交前转为 UTC 时间戳(与团队前端工具库约定秒或毫秒,同一接口内保持一致)。
- 不要提交
2025-01-01 12:00:00这类无偏移量的字符串作为权威时间入参。
后端职责
- 将入参 UTC 时间戳解析为绝对时刻后,**换算为北京时间(
Asia/Shanghai)**再用于落库与where条件(与库内字段对齐)。 - 同时兼容秒、毫秒时,按位数或阈值判断(例如 ≥ 1e12 视为毫秒)。
- 可用基础包
TgkwAdc\Helper\TimeHelper::parseDateOrTimestamp()/toCarbon()做统一解析,再setTimezone('Asia/Shanghai')后写入或比较。
响应:后端 → 前端
约定
| 项 | 说明 |
|---|---|
| 格式 | UTC ISO 8601,带 Z 后缀 |
| 示例 | 2025-01-01T04:00:00.000000Z |
| Resource | 直接透出模型时间,或 formatDate()(当前实现为原样字符串透传,不再服务端转本地字符串) |
| 前端 | 解析 ISO 8601 → 客户端时区 → 展示为 Y-m-d H:i:s |
说明
BaseResource::formatDate() 已约定:后端不再把时间格式化成业务本地字符串,统一由前端处理。Resource 中写 'created_at' => $this->created_at 或 'created_at' => $this->formatDate($this->created_at) 均可,只要最终 JSON 中是 UTC ISO 8601。
后端接入要点
- 落库(强制):业务时间字段统一按北京时间写入数据库,便于运维查看、对账与 SQL 排查;不要在库内混存 UTC 墙钟字符串与北京时间墙钟字符串。
- 列表筛选:Request 声明
start_time/end_time为数字;Service 将 UTC 时间戳转为北京时间后再与库字段比较。 - 详情 / 列表出参:时间字段走 Resource,把库内北京时间转换为 UTC ISO 8601 返回(展示本地化交给前端)。
- 不要在 Resource 中按客户端时区或随意时区格式化成
Y-m-d H:i:s再回传前端。 - API 边界:统一遵守「时间戳进 → 北京时间落库/查询 → ISO 8601 出」。
样例代码
1. Request:时间筛选入参为时间戳
// 列表筛选场景
'start_time' => 'sometimes|integer', // UTC 秒级或按约定的毫秒时间戳
'end_time' => 'sometimes|integer',
// 或兼容秒/毫秒浮点数字符串
'invite_start_time' => 'sometimes|numeric',
'invite_end_time' => 'sometimes|numeric',2. Service:时间戳 → 北京时间后查询 / 落库
use TgkwAdc\Helper\TimeHelper;
// 入参为 UTC 时间戳 → 转为北京时间字符串,与库内字段对齐
if (! empty($filters['start_time'])) {
$start = TimeHelper::parseDateOrTimestamp($filters['start_time'])
->setTimezone('Asia/Shanghai')
->format('Y-m-d H:i:s');
$query->where('created_at', '>=', $start);
}
if (! empty($filters['end_time'])) {
$end = TimeHelper::parseDateOrTimestamp($filters['end_time'])
->setTimezone('Asia/Shanghai')
->format('Y-m-d H:i:s');
$query->where('created_at', '<=', $end);
}
// 落库同理:绝对时刻换算为北京时间后再写入
// $model->xxx_at = TimeHelper::parseDateOrTimestamp($ts)
// ->setTimezone('Asia/Shanghai')
// ->format('Y-m-d H:i:s');3. Resource:时间字段透出(由前端转本地展示)
return [
'id' => (string) $this->id,
'name' => $this->name,
// 返回 UTC ISO 8601,例如 2025-01-01T04:00:00.000000Z
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];4. 前端(示意)
提交:本地 Date → getTime() / 秒级时间戳 → 请求 start_time、end_time
展示:响应 created_at(ISO 8601 Z)→ 按客户端时区 format 为 YYYY-MM-DD HH:mm:ss禁止清单
| 禁止行为 | 正确做法 |
|---|---|
前端提交无时区的 Y-m-d H:i:s 字符串作为权威时间 | 提交 UTC 时间戳 |
| 库内按 UTC 墙钟字符串落库,或与北京时间混存 | 统一按北京时间落库,便于维护与查询 |
后端 Resource 返回本地 / 北京时间 Y-m-d H:i:s 给前端展示 | 返回 UTC ISO 8601 |
| 出参按客户端时区格式化 | 出参保持 UTC,展示交给前端 |
| 同一接口混用「有的字段时间戳、有的字段本地字符串」且无约定 | 时间类型参数统一时间戳 |
| 忽略秒/毫秒差异导致差 1000 倍 | 统一约定,或兼容两种并用阈值判断 |
FAQ
秒和毫秒用哪一种?
接口层可同时兼容;实现上建议用位数或 >= 1e12 判断毫秒。前后端同一业务约定一种即可,避免混传。
为什么出参不用时间戳?
ISO 8601(UTC)可读、可自描述时区(Z),前端日期库解析稳定;展示格式由客户端时区决定。
库里存的是什么?
北京时间(Asia/Shanghai)墙钟时间,便于运维维护、对账与直接 SQL 查询。API 边界仍遵守:入参 UTC 时间戳、出参 UTC ISO 8601;读写时在「时间戳 / ISO」与「北京时间库字段」之间显式换算。
为什么落库用北京时间而不是 UTC?
团队日常排障、报表、手工查库多以国内业务日为基准;库内统一北京时间可减少「库里一眼看不懂」的成本。对外接口仍用 UTC,避免与多时区客户端耦合。
旧接口还返回 Y-m-d H:i:s 怎么办?
新接口与改造中的接口按本文执行;存量接口按排期切换,避免同一资源有的字段已是 ISO、有的仍是本地字符串且无文档说明。