In server-side code, "current time" should almost never be called naked. A Docker image defaults to UTC, a production machine is configured with Asia/Shanghai, and the "start of this week" produced by the same code differs by 8 hours.
Companion:
query-performance-safety(performance) /multi-tenant-safety(isolation)
Trap #1: Naked call to .now() depends on the JVM default timezone
Scenario: LocalDate.now() / LocalDateTime.now() / Instant.now() / new Date() without passing a timezone
Root cause
LocalDate.now()On a UTC server, the returned "today" is 1 day less than the business timezone (early morning scenario)LocalDateTime.now()What is obtained is the JVM default timezone time; docker base images often default to UTC- The same code produces different results on the dev machine (Asia/Shanghai) and production (UTC), and cannot be reproduced locally
- Database times are usually stored in the business timezone, and when comparing, one side is the business timezone and the other is UTC → 8h deviation
Incorrect example
// ❌ 错误:依赖 JVM 默认时区
LocalDateTime weekStart = LocalDate.now()
.with(DayOfWeek.MONDAY)
.atStartOfDay();
// ❌ 错误:跨日切换的统计依赖默认时区
LocalDate today = LocalDate.now();
List<Order> todayOrders = orderRepository.findByCreatedAtAfter(today.atStartOfDay());
// ❌ 错误:Date 用默认时区
Date now = new Date();
Correct approach
Project-level business timezone constant + all .now() explicitly passed in:
// ✅ 项目常量类(如 TimeConstants.java)
public final class TimeConstants {
/** 系统业务时区(中国大陆) */
public static final ZoneId BIZ_ZONE = ZoneId.of("Asia/Shanghai");
private TimeConstants() {}
}
// ✅ 业务代码统一传入
LocalDateTime weekStart = LocalDate.now(BIZ_ZONE)
.with(DayOfWeek.MONDAY)
.atStartOfDay();
LocalDate today = LocalDate.now(BIZ_ZONE);
LocalDateTime nowBiz = LocalDateTime.now(BIZ_ZONE);
Instant instantNow = Instant.now(); // Instant 本身是 UTC 时间戳,无歧义
Sniffing signal
# 任何裸 .now() 在业务代码中都该 review
grep -rnE "LocalDate\.now\(\)|LocalDateTime\.now\(\)|LocalTime\.now\(\)" src/main/java/ \
| grep -v "TimeConstants\|@Configuration"
# Date 系列在新代码中应淘汰
grep -rnE "new Date\(\)|new GregorianCalendar\(\)" src/main/java/
# Go
grep -rnE "time\.Now\(\)" --include="*.go" .
# JS / TS
grep -rnE "new Date\(\)|Date\.now\(\)" --include="*.ts" --include="*.js" src/
Checklist
- Is there a unified
BIZ_ZONEconstant within the project - Service / Handler layers are prohibited from naked
.now(), a timezone must be explicit -
Instant.now()can be used naked (UTC timestamp is unambiguous) - Tests use
Clock.fixed(instant, zone)injection, do not hardcodenow()
Trap #2: Week/month/day start calculations missing timezone
Scenario: Calculating "start of this week / start of this month / today 00:00" for statistical aggregation
Root cause
The switch points across days/weeks/months are the most sensitive. If the start is calculated in UTC, "Monday 0:00 this week" in the business timezone corresponds to Sunday 16:00 UTC, causing statistical misalignment.
Incorrect example
// ❌ 错误:周起点未指定时区
LocalDateTime weekStart = LocalDate.now()
.with(DayOfWeek.MONDAY)
.atStartOfDay();
// ❌ 错误:月起点未指定时区
LocalDateTime monthStart = LocalDate.now().withDayOfMonth(1).atStartOfDay();
Correct approach
// ✅ 周起点(业务周一 00:00)
LocalDateTime weekStart = LocalDate.now(BIZ_ZONE)
.with(DayOfWeek.MONDAY)
.atStartOfDay();
// ✅ 月起点
LocalDateTime monthStart = LocalDate.now(BIZ_ZONE).withDayOfMonth(1).atStartOfDay();
// ✅ 今日 00:00(业务时区)
LocalDateTime todayStart = LocalDate.now(BIZ_ZONE).atStartOfDay();
// ✅ 与数据库时间字段比较(假设 DB 存的也是业务时区 LocalDateTime)
List<Order> todayOrders = orderRepository.findByCreatedAtAfter(todayStart);
If the DB stores UTC TIMESTAMP / Instant, the BIZ start must first be converted to UTC:
Instant todayStartUtc = LocalDate.now(BIZ_ZONE)
.atStartOfDay(BIZ_ZONE)
.toInstant();
Checklist
- All
with(DayOfWeek.X).atStartOfDay()before the chain, is it carryingBIZ_ZONE - Does the week start definition conform to the business (Monday vs Sunday vs ISO week)
- The unit of the database time field is consistent with the unit of the query parameter (both LocalDateTime or both Instant)
Trap #3: Inconsistent timezones between database and application layer
Scenario: JDBC default serverTimezone is inconsistent with the JVM; MySQL TIMESTAMP automatic conversion and DATETIME are not synchronized
Root cause
- MySQL
TIMESTAMPperforms store/read conversion according to the session/server timezone;DATETIMEis a naked string - JDBC URL
serverTimezone=UTC, when Java writes2026-05-14 10:00 BIZit is actually stored as UTC, and read out and converted again - If the JVM uses
UTCbut the DB session uses+08:00, "today's data" will be inWHERE created_at >= todaynot found
Correct approach
Fix the timezone strategy and document it (the team uniformly chooses one):
| Strategy | DB column type | JDBC URL | Java field | Applicable scenario |
|---|---|---|---|---|
| A. Business timezone full stack | DATETIME |
serverTimezone=Asia/Shanghai |
LocalDateTime |
Single timezone business (recommended for 90% of China scenarios) |
| B. UTC storage + business display | TIMESTAMP |
serverTimezone=UTC |
Instant / OffsetDateTime |
Cross-timezone SaaS, requires global deployment |
Strongly prohibit mixing: DB columns use DATETIME but again serverTimezone=UTC, the hardest to troubleshoot.
# ✅ 策略 A 的连接串示例
spring:
datasource:
url: jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false
# ✅ 部署层固定 JVM 时区,避免 base image 飘
ENV TZ=Asia/Shanghai
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
Checklist
- Whether the JDBC URL explicitly sets
serverTimezone - Whether the Dockerfile pins the
TZenvironment variable - Whether the DB field type choice (
DATETIMEvsTIMESTAMP) is consistent with the strategy - Whether the team wiki documents the time zone strategy
Pitfall #4: The implicit trap of cross-time-zone comparison
Scenario: The user uploads the string "2026-05-14 10:00", and the backend loses the time zone information during conversion
Incorrect example
// ❌ 错误:字符串直接 parse 成 LocalDateTime,时区信息丢失
LocalDateTime userTime = LocalDateTime.parse(request.getTimeStr());
// ❌ 错误:与 Instant 比较时隐式用 JVM 时区
Instant dbTime = order.getCreatedInstant();
LocalDateTime userTime = LocalDateTime.parse(request.getTimeStr());
if (userTime.isAfter(LocalDateTime.ofInstant(dbTime, ZoneId.systemDefault()))) { ... }
Correct approach
// ✅ 明确字符串是业务时区
LocalDateTime userLocal = LocalDateTime.parse(request.getTimeStr());
Instant userInstant = userLocal.atZone(BIZ_ZONE).toInstant();
// ✅ 比较时都转 Instant
if (userInstant.isAfter(order.getCreatedInstant())) { ... }
Checklist
- Whether the time string entered by the user defines time zone semantics (this should be declared in the HTTP API documentation)
- Always use
Instantfor cross-time-zone comparison, notLocalDateTimefor direct comparison - Whether log output times include the time zone (
yyyy-MM-dd HH:mm:ss z)
Multilingual examples
Go
// ❌ 依赖系统时区
now := time.Now()
// ✅ 业务时区
var bizZone = time.FixedZone("Asia/Shanghai", 8*3600)
// 或加载 IANA
bizZone, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(bizZone)
weekStart := now.AddDate(0, 0, -int(now.Weekday()-time.Monday)).
Truncate(24 * time.Hour)
Python
# ❌ naive datetime
from datetime import datetime
now = datetime.now()
# ✅ aware datetime
from datetime import datetime
from zoneinfo import ZoneInfo
BIZ_ZONE = ZoneInfo("Asia/Shanghai")
now = datetime.now(tz=BIZ_ZONE)
week_start = (now - timedelta(days=now.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
Node.js / TypeScript
// ❌ Date 默认本地时区
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// ✅ 用 luxon 或 date-fns-tz 显式时区
import { DateTime } from "luxon";
const BIZ_ZONE = "Asia/Shanghai";
const now = DateTime.now().setZone(BIZ_ZONE);
const weekStart = now.startOf("week"); // luxon 默认 Monday
const todayStart = now.startOf("day");
// 落库前转 UTC ISO
const utcIso = now.toUTC().toISO();
Overall checklist
Application layer:
- A unified
BIZ_ZONE/BIZ_LOCATIONconstant exists in the project - Business code contains no bare
LocalDate.now()/LocalDateTime.now()/new Date()/time.Now() - Week/month/day start calculations explicitly include the time zone
- Tests use
Clock.fixed()/ Mock, not relying on real time
Data layer:
- The JDBC URL explicitly sets
serverTimezone - DB field types are consistent with the time zone strategy
- Always use
Instant/ UTC timestamps for cross-time-zone comparison
Deployment layer:
- Docker / k8s sets the
TZenvironment variable - The time zone of CI test containers is consistent with production
Interface layer:
- The API documentation declares the time zone semantics of time parameters (ISO 8601 + time zone offset)
- Response time fields include the time zone (
2026-05-14T10:00:00+08:00)
Rule provenance
> 📋 本回复遵循:`cc-time-zone-safety` - [章节名]