SuperAPI AI Directory

cc-time-zone-safety

Security & auditing

View repository

Updated 10-15 21:11

About this skill

Timezone Safety Guidelines

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_ZONE constant 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 hardcode now()

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 carrying BIZ_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 TIMESTAMP performs store/read conversion according to the session/server timezone;DATETIME is a naked string
  • JDBC URL serverTimezone=UTC , when Java writes 2026-05-14 10:00 BIZ it is actually stored as UTC, and read out and converted again
  • If the JVM uses UTC but the DB session uses +08:00, "today's data" will be in WHERE created_at >= today not 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 TZ environment variable
  • Whether the DB field type choice (DATETIME vs TIMESTAMP) 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, not LocalDateTime for 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_LOCATION constant 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 TZ environment 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` - [章节名]