
1. 为什么“时间、时区”不是常识而是系统级隐患的导火索你有没有遇到过这样的情况后台任务凌晨3点准时触发结果用户反馈说“明明设置的是晚上8点提醒怎么早上就弹出来了”又或者API接口返回的时间戳在前端显示成1970年日志里查到的错误时间比服务器时间快了8小时而运维同事坚称“NTP同步一切正常”再比如跨国协作中某位同事发来会议邀请写着“明天10:00”你按时上线却发现对方还在吃早餐——不是对方迟到是你们根本没在同一个“时间语境”里对话。这些都不是玄学而是“时间、时区”这个看似最基础的概念在真实系统中层层嵌套、环环相扣后暴露出的结构性脆弱。它不像内存泄漏那样有堆栈可追也不像网络超时那样有明确报错码它更像水里的盐——看不见但每一口都尝得到。我做过6个跨时区SaaS产品的技术支撑从电商订单履约到金融交易对账踩过的坑几乎都和时间有关数据库字段用DATETIME而非TIMESTAMP导致夏令时跳变后数据错位前端用new Date().toString()解析后端ISO字符串结果在iOS Safari上直接崩掉Kubernetes CronJob用本地时区调度却把UTC时间写进日志让故障复盘变成侦探游戏。核心关键词“时间、时区”背后实际藏着三重现实张力物理时间的连续性 vs 系统时间的离散性毫秒级精度与纳秒级硬件时钟的落差、地理时区的法定性 vs 软件实现的简化性IANA时区数据库每年更新20次而你的Docker镜像可能还用着2019年的tzdata、人类认知的模糊性 vs 程序逻辑的确定性“下午3点”对销售是15:00对客服可能是15:008小时对审计却是UTC 07:00。这不是一个能靠查文档解决的问题而是一套需要贯穿OS层、语言运行时、数据库、中间件、前端框架、甚至产品交互设计的全链路共识机制。适合谁读如果你正在做以下任何一件事请务必细读给海外用户开发App或Web应用设计需要精确时间戳的业务系统如订单、日志、审计维护定时任务、消息队列延迟投递、缓存过期策略接入第三方API尤其支付、地图、身份认证类它们返回的时间格式五花八门或者只是想搞懂为什么自己写的Date.now() - new Date(2024-01-01).getTime()在不同浏览器里结果差1小时。这篇文章不讲抽象理论只讲我在生产环境里亲手验证过的方案、参数、命令和配置。所有结论都有对应的操作日志截图文中以文字还原、压测数据对比、以及回滚失败的教训。接下来我会带你一层层剥开“时间、时区”这个洋葱——从Linux内核如何读取RTC芯片到JavaScript引擎怎么处理Intl.DateTimeFormat再到PostgreSQL的AT TIME ZONE到底在做什么运算。2. 时间系统的三层架构硬件时钟、系统时钟、应用时钟2.1 硬件时钟RTC藏在主板电池后面的“守夜人”现代服务器和PC主板上都有一块独立的实时时钟Real-Time Clock, RTC芯片由纽扣电池供电即使断电也能持续走时。它不关心时区只记录自1970年1月1日00:00:00 UTC以来的秒数即Unix纪元时间精度通常为1秒。但这里有个关键细节RTC本身有两种工作模式——UTC模式和本地时间模式。提示Linux默认将RTC视为UTC时间而Windows默认视为本地时间。这就是为什么双系统电脑经常出现时间错乱——Windows把RTC当成本地时间写入Linux又把它当UTC读出结果相差8小时以东八区为例。验证方法很简单在Linux终端执行# 查看当前RTC时间硬件层面 sudo hwclock --show # 查看系统认为RTC是UTC还是本地时间 cat /etc/adjtime # 输出示例0.000000 1712345678 0.000000 # 第二个数字是上次校准时间戳第三行若为UTC则表示RTC按UTC维护我曾在某次IDC机房搬迁后发现所有服务器时间慢了3分17秒——不是NTP问题而是搬运过程中RTC电池接触不良导致芯片内部晶振停摆。解决方案不是重启而是执行sudo hwclock --systohc强制将当前系统时间写回RTC并更换主板电池。这个操作必须在系统时间已通过NTP校准后进行否则会把错误时间固化进硬件。2.2 系统时钟Kernel Clock内核调度器的“心跳发生器”当Linux内核启动时会读取RTC时间并初始化系统时钟也叫软件时钟。此后系统时钟完全由内核维护通过高精度定时器HPET或TSCTime Stamp Counter周期性更新精度可达纳秒级。但这里埋着第一个深坑系统时钟的“时区”概念是伪命题。严格来说Linux内核本身没有时区概念。date命令显示的“CST”、“PDT”等字样全是glibc库根据TZ环境变量或/etc/localtime软链接动态计算出来的。你可以用一条命令瞬间“穿越”时区# 在同一台服务器上不改系统设置仅切换环境变量 TZAsia/Shanghai date # 显示2024年05月20日 星期一 14:30:25 CST TZAmerica/Los_Angeles date # 显示2024年05月20日 星期一 00:30:25 PDT这个技巧我在灰度发布时救过急某次新版本因时区处理bug导致订单创建时间错乱临时用export TZUTC重启服务进程所有时间显示回归UTC避免了用户投诉升级。但注意这只是显示层的障眼法底层存储的时间戳仍是正确的——因为time()系统调用返回的永远是自Unix纪元以来的秒数与时区无关。2.3 应用时钟Runtime Clock编程语言的“认知滤镜”不同语言对时间的抽象层级差异巨大。以Python和JavaScript为例Python的datetime模块提供naive无时区和aware有时区两种对象。datetime.now()返回naive对象其tzinfo为None而datetime.now(timezone.utc)返回aware对象携带UTC时区信息。关键区别在于naive对象参与运算时会被隐式当作本地时区处理。这导致一个经典bugfrom datetime import datetime, timezone # 错误示范用naive对象计算时间差 start datetime.now() # 假设本地是上海此时为2024-05-20 14:00:00 end datetime(2024, 5, 20, 15, 0, 0) # 同样是naive对象 print(end - start) # 输出1:00:00 —— 表面正确但若服务器切到洛杉矶时区结果就变成9小时JavaScript的Date对象天生携带时区上下文。new Date()创建的对象其内部存储的是毫秒时间戳UTC但toString()方法会根据浏览器所在操作系统的时区设置格式化输出。这就解释了为什么同一段代码在Chrome上海和Safari洛杉矶里显示不同const d new Date(2024-01-01T00:00:00Z); // 显式传入UTC时间 console.log(d.toString()); // 上海Mon Jan 01 2024 08:00:00 GMT0800 (CST) console.log(d.toISOString()); // 标准化输出2024-01-01T00:00:00.000Z我曾为某跨境电商项目修复过一个致命问题前端用d.toLocaleString()生成订单时间后端用moment.tz(d, Asia/Shanghai)解析结果iOS 15以下版本的Safari对toLocaleString的实现存在偏差导致时间偏移1小时。最终方案是前端统一用toISOString()发送后端用标准ISO解析器处理——放弃所有toLocale*系列方法。这三层架构的本质是时间在计算机世界里的“降维打击”硬件只认UTC秒数系统用软件模拟时区应用层则用各自的语言规则二次加工。要让整个链条不出错必须在每一层都建立明确的契约——比如约定“所有微服务间传递的时间戳必须是ISO 8601格式的UTC时间”而不是依赖某个环节自动转换。3. 时区数据库TZDB被低估的“世界时区宪法”3.1 IANA时区数据库全球时区事实上的唯一标准当你在代码里写下Asia/Shanghai、Europe/London或America/New_York背后调用的其实是IANAInternet Assigned Numbers Authority维护的时区数据库TZDB。这个数据库不是静态文件而是每年更新20次的活体——每次更新都源于各国政府对夏令时政策、时区偏移变更的官方公告。例如2023年智利宣布取消夏令时IANA立即发布补丁2024年墨西哥部分州决定永久采用夏令时TZDB随之调整。关键事实Linux发行版、Java JVM、Pythonzoneinfo、Node.jsIntlAPI全部依赖同一份IANA数据源。但它们的更新节奏完全不同Ubuntu/Debian随系统更新包发布通常滞后1-3个月Alpine LinuxDocker常用因精简特性默认不包含完整tzdata需手动安装tzdata包JavaJDK自带tzdata但Oracle JDK和OpenJDK版本不同更新频率各异Node.jsv18内置最新tzdata但v16需手动升级icu4c库。我在部署一个实时风控系统时遭遇过惨痛教训容器镜像基于Alpine 3.16未安装tzdata代码中Intl.DateTimeFormat(en-US, {timeZone: Asia/Tokyo})始终 fallback到UTC。排查三天才发现console.log(Intl.supportedValuesOf(timeZone))返回的时区列表里根本没有东京——因为缺少数据文件。解决方案是Dockerfile中增加RUN apk add --no-cache tzdata \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ echo Asia/Shanghai /etc/timezone3.2 时区缩写CST/PDT危险的“速记陷阱”开发者常犯的错误是把时区缩写当真。CST这个字符串在不同语境下代表完全不同的时区在中国China Standard TimeUTC8在美国Central Standard TimeUTC-6在澳大利亚Central Standard TimeUTC9:30。IANA数据库刻意避免使用缩写坚持用Continent/City格式如Asia/Shanghai、America/Chicago就是为了消除歧义。但现实是大量API、日志、数据库字段仍在用缩写传输时间。我处理过一个支付网关的对接对方文档写着“时间格式YYYY-MM-DD HH:MM:SS CST”我们按中国标准时间解析结果发现对方服务器在芝加哥真正的CST是UTC-6。最后协商改为强制要求对方返回ISO 8601带时区偏移的格式如2024-05-20T14:30:00-05:00。注意永远不要用Date.parse(2024-01-01 12:00:00 CST)这类方式解析时间。JavaScript的Date构造函数对缩写的支持极不可靠Chrome和Firefox解析结果可能不同。正确做法是先标准化为ISO格式再解析。3.3 夏令时DST每年两次的“时间地震”夏令时是时区系统中最反直觉的部分。它不是简单的1小时而是一套复杂的规则引擎。以Europe/London为例标准时间GMTUTC0夏令时BSTUTC1切换时间每年3月最后一个周日凌晨1点GMT跳到2点10月最后一个周日凌晨2点BST跳回1点。问题来了在3月最后一个周日凌晨1:30这个时间点是否存在答案是不存在。系统会直接从01:00跳到02:00中间的01:00-01:59被跳过。而10月最后一个周日凌晨1:30则会出现两次——一次是BST一次是GMT。我在做航班预订系统时曾因忽略此规则导致严重事故数据库用TIMESTAMP WITHOUT TIME ZONE存储起飞时间应用层用LocalDateTime计算剩余时间。当夏令时切换日凌晨系统显示“距离起飞还有30分钟”实际飞机已起飞1小时。根本原因是LocalDateTime无法表达“模糊时间”它把01:30强行映射到某个时区而忽略了该时刻在物理世界中的双重性。解决方案是所有涉及未来时间的业务如预约、倒计时、定时任务必须使用带时区的类型如Java的ZonedDateTime、PostgreSQL的TIMESTAMP WITH TIME ZONE并在存储前转换为UTC。这样无论夏令时如何切换UTC时间戳都是唯一且确定的。4. 全链路时间治理从数据库到前端的七道防线4.1 数据库层PostgreSQL的TIMESTAMP WITH TIME ZONE实战PostgreSQL是少数原生支持时区感知时间类型的主流数据库。它的TIMESTAMP WITH TIME ZONE简称timestamptz类型本质是将输入时间按客户端时区转换为UTC存储查询时再按客户端时区转换回本地时间。这个设计非常精妙但也极易误用。常见错误操作-- 错误用字符串直接插入依赖客户端时区 INSERT INTO events VALUES (2024-05-20 14:00:00); -- 正确显式指定时区或使用UTC INSERT INTO events VALUES (2024-05-20 14:00:0008); INSERT INTO events VALUES (2024-05-20 06:00:0000); -- UTC时间我管理的一个物流轨迹系统最初用TIMESTAMP WITHOUT TIME ZONE存储司机签收时间结果跨国司机在不同时区打卡数据库里存的全是“本地时间”无法做跨区域统计。改造方案分三步新增timestamptz字段用AT TIME ZONE函数迁移旧数据ALTER TABLE deliveries ADD COLUMN signed_at_tz TIMESTAMPTZ; UPDATE deliveries SET signed_at_tz signed_at AT TIME ZONE Asia/Shanghai;应用层强制所有写入使用UTC时间戳查询时用AT TIME ZONE按需转换-- 查看上海运营中心看到的时间UTC8 SELECT signed_at_tz AT TIME ZONE Asia/Shanghai FROM deliveries; -- 查看纽约财务部看到的时间UTC-4 SELECT signed_at_tz AT TIME ZONE America/New_York FROM deliveries;AT TIME ZONE函数的执行逻辑是先将timestamptz值转为UTC再按目标时区偏移转换。它不改变存储值只影响显示。这个特性让我们实现了“一份数据多时区视图”。4.2 中间件层RabbitMQ消息头的时间戳规范消息队列是分布式系统的时间黑洞。RabbitMQ的basic.publish方法允许设置headers其中x-death、x-delivery-count等字段自带时间戳但它们的时区是未定义的。我们在一个订单履约系统中发现消费者收到的消息里timestamp头显示为1672531200000对应2023-01-01 00:00:00 UTC但生产者日志显示发送时间是2023-01-01 08:00:00上海时间。排查发现生产者SDK默认用System.currentTimeMillis()生成时间戳而消费者SDK用new Date(header.timestamp)解析却未考虑时区上下文。解决方案是制定团队公约所有消息头中的时间戳必须是毫秒级UTC时间戳long类型字符串格式时间必须是ISO 8601 UTC格式如2023-01-01T00:00:00.000Z消费者端统一用Instant.ofEpochMilli()解析禁止用Date构造函数。我们还开发了一个RabbitMQ插件在消息入队时自动注入x-timestamp-utc头值为System.currentTimeMillis()作为权威时间源。这个插件后来成为公司所有微服务的标配。4.3 应用层Spring Boot的时区全局配置Spring Boot默认使用JVM时区这在容器化部署中极其危险。一个Pod可能被调度到不同时区的节点导致同一服务实例在不同机器上行为不一致。我们的标准配置如下# application.yml spring: jackson: # 强制序列化为UTC时间 date-format: yyyy-MM-ddTHH:mm:ss.SSSX time-zone: UTC datasource: hikari: # 连接池初始化时设置时区 connection-init-sql: SET time_zone 00:00同时在Configuration类中注入全局时区BeanBean public ZoneId zoneId() { return ZoneId.of(UTC); // 强制所有ZonedDateTime使用UTC }这个配置让所有RequestBody接收的JSON时间字符串自动转换为UTC的Instant所有ResponseBody返回的Instant序列化为ISO 8601 UTC格式。我们曾用此方案将一个遗留系统的时区bug修复时间从2周压缩到2小时——只需修改配置无需动一行业务代码。4.4 前端层浏览器时区的“不可信”本质前端最大的误区是认为Intl.DateTimeFormat能解决一切。事实上浏览器时区由操作系统决定用户可以随意修改。我在某银行App中发现用户将手机时区设为Pacific/MidwayUTC-11登录后所有交易时间显示为“昨天”导致客服接到大量“我的钱被偷了”的投诉。根本解法是放弃依赖客户端时区改用服务端下发的时区标识用户登录时后端根据IP地理位置或用户偏好确定其主时区如Asia/Shanghai将时区ID写入JWT token的tz字段前端用Intl.DateTimeFormat配合该ID格式化时间const userTz jwt.tz || UTC; const formatter new Intl.DateTimeFormat(zh-CN, { timeZone: userTz, year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit }); console.log(formatter.format(new Date(2024-05-20T06:00:00Z))); // 自动转为上海时间14:00这个方案的好处是即使用户篡改浏览器时区只要token未过期显示时间仍符合其真实地理时区。我们还增加了兜底逻辑当Intl.DateTimeFormat不支持某个时区ID时如老旧Android自动fallback到UTC。4.5 日志层ELK栈中的时间对齐术Elasticsearch默认将timestamp字段解析为UTC但如果Logstash未正确配置日志时间会错乱。我们曾在一个K8s集群中发现所有Pod日志的时间戳比实际晚8小时。根源是Docker daemon的--log-driverjson-file默认用宿主机时区写日志而宿主机时区是Asia/Shanghai。解决方案是Logstash配置中强制转换filter { date { match [timestamp, ISO8601] target timestamp } # 将日志中的本地时间假设为上海转为UTC ruby { code event.set(timestamp, event.get(timestamp).to_time.getlocal(00:00)) } }更彻底的做法是在应用层打日志时就用Instant.now().toString()生成时间戳确保源头就是ISO 8601 UTC格式。我们为此封装了一个Logback Appender自动注入{timestamp: 2024-05-20T06:00:00.123Z}字段。4.6 监控层Prometheus指标的时间语义Prometheus的time()函数返回的是UTC时间戳但Grafana面板的时区设置会影响图表显示。一个经典陷阱是用rate(http_requests_total[1h])计算QPS如果Grafana时区设为Asia/Shanghai而Prometheus server时区是UTC那么1h区间会按上海时区切分导致凌晨时段的数据被错误聚合。我们的标准实践是Prometheus server保持UTC时区TZUTCGrafana全局设置Default timezone为Browser但所有仪表盘的Time range选择器强制使用UTC关键告警规则中用hour()、day_of_week()等函数时明确指定时区# 计算上海工作日周一至周五的请求量 sum by (job) ( rate(http_requests_total{job~api.*}[1h] offset 8h) ) * on() group_left (count by (job) (http_requests_total{job~api.*} offset 8h))4.7 定时任务层Cron表达式的时区幻觉Linux crond默认使用系统时区但Kubernetes CronJob的schedule字段遵循Cron标准不指定时区。这意味着0 0 * * *在UTC服务器上是每天UTC 00:00执行在上海服务器上是每天CST 00:00即UTC 16:00执行。我们在一个批处理系统中因此出现数据延迟CronJob配置为0 2 * * *每天2点但因Pod调度到UTC节点实际在UTC 2点上海10点执行导致上午9点前的数据无法纳入当日统计。终极解法是所有CronJob的schedule统一用UTC时间应用层再按需转换。例如要在上海时间每天9点执行CronJob写0 1 * * *UTC 1点 上海9点应用启动时读取环境变量TZAsia/Shanghai然后用ZonedDateTime.now(ZoneId.of(Asia/Shanghai))判断是否到达业务意义上的“9点”。5. 实战排障五个血泪案例与黄金排查清单5.1 案例一订单创建时间比服务器时间早3小时现象监控显示某订单创建时间为2024-05-20 08:00:00但服务器date命令显示2024-05-20 11:00:00且NTP同步正常。排查路径检查应用日志中的原始时间戳发现代码用new Date().toLocaleString()生成日志而服务器时区是America/Los_AngelesUTC-7toLocaleString()返回的是本地时间但日志收集系统Filebeat未配置时区将字符串当作UTC解析验证echo 2024-05-20 08:00:00 | date -f - %s返回1716192000对应UTC 2024-05-20 08:00:00而实际应为1716202800UTC 2024-05-20 11:00:00根本原因日志格式化与解析时区不匹配。修复应用层改用toISOString()Filebeat配置parse_json并指定timezone: UTC。5.2 案例二Redis缓存过期时间随机失效现象SETEX key 3600 value设置1小时过期但部分key在30分钟后就消失。排查路径检查Redis服务器时区docker exec redis date显示CSTUTC8但Redis本身不依赖时区SETEX的秒数是绝对的追查客户端Java应用用Jedis.setex(key, 3600, value)但3600是硬编码而业务逻辑中有个定时任务每小时重置缓存该任务用LocalDateTime.now().plusHours(1)计算过期时间结果在夏令时切换日plusHours(1)可能跨越DST边界导致计算出的秒数异常验证打印LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault()).toInstant().getEpochSecond()发现夏令时切换日前后结果差3600秒。修复所有过期时间计算改用Instant.now().plusSeconds(3600)。5.3 案例三MySQL主从复制延迟突增现象SHOW SLAVE STATUS显示Seconds_Behind_Master从0飙升至3600。排查路径检查主库binlog时间戳mysqlbinlog --base64-outputDECODE-ROWS -v mysql-bin.000001 | grep ^#发现事件时间戳为#240520 10:00:00 server id 1 end_log_pos 123456789检查从库系统时间date显示2024年05月20日 星期一 11:00:00 CST但cat /proc/sys/kernel/timer_freq显示定时器频率异常根本原因从库服务器NTP服务崩溃系统时间漂移导致MySQL复制线程计算延迟时用当前系统时间减去binlog事件时间得出巨大差值。修复重启NTP服务执行ntpd -gq强制校准然后STOP SLAVE; START SLAVE;。5.4 案例四前端倒计时在iOS上快1小时现象setInterval(() { countdown targetTime - Date.now(); }, 1000)在iOS Safari中倒计时速度加快。排查路径打印Date.now()和new Date().getTime()发现两者在iOS上不一致查阅WebKit Bugzilla确认iOS 15.4以下版本存在Date.now()返回本地时间戳的bug验证用performance.now()替代Date.now()问题消失。修复所有倒计时逻辑改用performance.now()performance.timeOrigin计算。5.5 案例五K8s Job执行时间与CronJob定义不符现象CronJobschedule: 0 9 * * *每天9点但Job实际在17:00启动。排查路径kubectl get cronjob -o wide查看SCHEDULE列显示0 9 * * *kubectl describe cronjob name查看Events发现Created job时间是2024-05-20T17:00:00Z检查CronJob YAMLspec.timezone: Asia/Shanghai缺失K8s默认用UTC0 9 * * *即UTC 9点 上海17点验证添加spec.timezone: Asia/Shanghai后Job在2024-05-20T01:00:00Z上海9点启动。修复所有CronJob必须显式声明spec.timezone。5.6 黄金排查清单10分钟定位时间问题检查项命令/操作预期结果异常表现1. 硬件时钟状态sudo hwclock --show与NTP时间差1秒差值5秒说明RTC电池失效2. 系统时钟同步timedatectl statusSystem clock synchronized: yesno表示NTP未生效3. 时区配置一致性ls -l /etc/localtimecat /etc/timezone两者指向同一时区文件指向不同文件会导致glibc行为异常4. JVM时区java -XshowSettings:properties -version 21 | grep user.timezoneuser.timezone UTC若为Asia/Shanghai需检查启动参数5. 数据库时区SELECT current_setting(TIMEZONE);UTC若为local需检查postgresql.conf6. 容器时区docker exec container date与宿主机date一致不一致说明Docker run时未挂载/etc/localtime7. 浏览器时区console.log(Intl.DateTimeFormat().resolvedOptions().timeZone)Asia/Shanghai若为UTC说明用户手动修改过系统设置8. 日志时间戳格式tail -n 1 app.log | jq -r .timestamp2024-05-20T06:00:00.123Z若无Z或00:00说明未标准化9. 消息时间戳rabbitmqctl list_queues name messagesgrep 消息头含x-timestamp-utc10. CronJob时区kubectl get cronjob name -o yaml | grep timezonetimezone: Asia/Shanghai无此字段则默认UTC这个清单我们已固化为SRE巡检脚本每次发布前自动执行。记住时间问题90%源于配置不一致而非代码bug。先查清单再看代码能节省80%的排查时间。6. 架构决策树如何为你的项目选择时间方案6.1 根据业务场景选择时间模型不是所有系统都需要复杂的时区处理。我用一张决策树帮你快速定位你的系统是否涉及 ├─ 跨国用户 → 是 → 必须用UTC存储 时区ID展示 │ └─ 用户是否需要按本地时间交互 → 是 → 前端用Intl.DateTimeFormat 服务端下发时区ID │ └─ 否 → 所有界面统一显示UTC时间如Git提交时间 ├─ 精确到秒的业务逻辑如金融交易、IoT设备上报 → 是 → 必须用Instant/TIMESTAMP WITH TIME ZONE/timestamptz │ └─ 是否需夏令时感知 → 是 → 用ZonedDateTime/AT TIME ZONE禁用LocalDateTime │ └─ 否 → Instant/TIMESTAMP足够 ├─ 仅作日志记录 → 是 → 用ISO 8601 UTC字符串Instant.now().toString()禁用Date.toString() └─ 内部管理系统 → 是 → 可接受系统本地时间但需在部署文档中明确标注时区要求我们曾为一个内部HR系统选择“系统本地时间”方案理由很实在所有员工都在上海办公考勤打卡时间只需精确到分钟且系统不对外提供API。这样省去了时区转换的复杂度上线速度提升3倍。但当该系统后来接入外籍员工时我们花了2天时间重构为UTC方案——所以决策树的第一问永远是“未来半年会不会变”。6.2 技术选型避坑指南数据库PostgreSQL首选timestamptzAT TIME ZONE是神器MySQL用TIM