Time Zone Safety logo

Time Zone Safety

CommunityPopular
doccker
time-zone-safety

当代码出现 LocalDate.now() / LocalDateTime.now() / Instant.now() / new Date() / Date.now() / time.Now() 等"当前时间"调用,或涉及周/月/日起点、跨时区比较、数据库时间字段时触发。防止生产环境时区漂移导致的统计错乱。

Overview

Publisherdoccker
Repositorycc-use-exp
Skill nametime-zone-safety
Stars
1.1K
Forks
116
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by doccker on GitHub. Read the source before you install it.

Installation

Install the Time Zone Safety AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/doccker/cc-use-exp/tree/main/.claude/skills/time-zone-safety
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/doccker/cc-use-exp.git /tmp/cc-use-exp
mkdir -p .claude/skills
cp -r /tmp/cc-use-exp/.claude/skills/time-zone-safety .claude/skills/time-zone-safety
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Time Zone Safety in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Time Zone Safety on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Time Zone Safety is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

时区安全规范

服务端代码里,"当前时间"几乎从不应该裸调。一个 docker 镜像默认 UTC、一个生产机配置 Asia/Shanghai,同一份代码跑出来的"本周起点"就差 8 小时。

配套:query-performance-safety(性能) / multi-tenant-safety(隔离)


陷阱 #1: 裸调 .now() 依赖 JVM 默认时区

场景: LocalDate.now() / LocalDateTime.now() / Instant.now() / new Date() 不传时区

问题根因

  • LocalDate.now() 在 UTC 服务器上返回的"今天"比业务时区少 1 天(凌晨场景)
  • LocalDateTime.now() 拿到的是 JVM 默认时区时间,docker base image 经常默认 UTC
  • 同一段代码在开发机(Asia/Shanghai)和生产(UTC)跑出不同结果,无法本地复现
  • 数据库时间通常按业务时区入库,比较时一边是业务时区一边是 UTC → 偏差 8h

错误示例

java
// ❌ 错误:依赖 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();

正确做法

项目级业务时区常量 + 所有 .now() 显式传入:

java
// ✅ 项目常量类(如 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 时间戳,无歧义

嗅探信号

bash
# 任何裸 .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/

检查清单

  • 项目内是否有统一的 BIZ_ZONE 常量
  • Service / Handler 层禁止裸 .now(),必须显式时区
  • Instant.now() 可以裸用(UTC 时间戳无歧义)
  • 测试用 Clock.fixed(instant, zone) 注入,不要硬编码 now()

陷阱 #2: 周/月/日起点计算缺失时区

场景: 计算"本周起点 / 本月初 / 今日 00:00"用于统计聚合

问题根因

跨日/跨周/跨月切换时间点最敏感。如果起点用 UTC 算,业务时区下"本周一 0 点"对应 UTC 周日 16:00,导致统计错位。

错误示例

java
// ❌ 错误:周起点未指定时区
LocalDateTime weekStart = LocalDate.now()
        .with(DayOfWeek.MONDAY)
        .atStartOfDay();

// ❌ 错误:月起点未指定时区
LocalDateTime monthStart = LocalDate.now().withDayOfMonth(1).atStartOfDay();

正确做法

java
// ✅ 周起点(业务周一 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);

如果 DB 存的是 UTC TIMESTAMP / Instant,必须先把 BIZ 起点转 UTC:

java
Instant todayStartUtc = LocalDate.now(BIZ_ZONE)
        .atStartOfDay(BIZ_ZONE)
        .toInstant();

检查清单

  • 所有 with(DayOfWeek.X).atStartOfDay() 链路前是否带 BIZ_ZONE
  • 周起点定义是否符合业务(周一 vs 周日 vs ISO 周)
  • 数据库时间字段单位与查询参数单位一致(同为 LocalDateTime 或同为 Instant)

陷阱 #3: 数据库与应用层时区不一致

场景: JDBC 默认 serverTimezone 与 JVM 不一致;MySQL TIMESTAMP 自动转换与 DATETIME 不同步

问题根因

  • MySQL TIMESTAMP 会按 session/server 时区做存读转换;DATETIME 是裸字符串
  • JDBC URL serverTimezone=UTC 时,Java 写入 2026-05-14 10:00 BIZ 实际存为 UTC,读出来又转一次
  • 若 JVM 用 UTC 但 DB session 用 +08:00,会出现"今天的数据"在 WHERE created_at >= today 查不到

正确做法

固定时区策略并文档化(团队内统一选一种):

策略DB 列类型JDBC URLJava 字段适用场景
A. 业务时区全栈DATETIMEserverTimezone=Asia/ShanghaiLocalDateTime单一时区业务(推荐 90% 中国场景)
B. UTC 存储 + 业务展示TIMESTAMPserverTimezone=UTCInstant / OffsetDateTime跨时区 SaaS、需要全球部署

强烈禁止混用:DB 列用 DATETIME 又把 serverTimezone=UTC,最难排查。

yaml
# ✅ 策略 A 的连接串示例
spring:
  datasource:
    url: jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false
dockerfile
# ✅ 部署层固定 JVM 时区,避免 base image 飘
ENV TZ=Asia/Shanghai
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

检查清单

  • JDBC URL 是否显式设置 serverTimezone
  • Dockerfile 是否固定 TZ 环境变量
  • DB 字段类型选择(DATETIME vs TIMESTAMP)与策略一致
  • 团队 wiki 是否文档化了时区策略

陷阱 #4: 跨时区比较的隐式陷阱

场景: 用户上传"2026-05-14 10:00"字符串,后端转换时丢失时区信息

错误示例

java
// ❌ 错误:字符串直接 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()))) { ... }

正确做法

java
// ✅ 明确字符串是业务时区
LocalDateTime userLocal = LocalDateTime.parse(request.getTimeStr());
Instant userInstant = userLocal.atZone(BIZ_ZONE).toInstant();

// ✅ 比较时都转 Instant
if (userInstant.isAfter(order.getCreatedInstant())) { ... }

检查清单

  • 用户输入的时间字符串是否定义了时区语义(HTTP API 文档应声明)
  • 跨时区比较一律用 Instant,不用 LocalDateTime 直接比
  • 日志输出时间是否带时区(yyyy-MM-dd HH:mm:ss z)

多语言示例

Go

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

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

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();

总检查清单

应用层:

  • 项目内有统一 BIZ_ZONE / BIZ_LOCATION 常量
  • 业务代码无裸 LocalDate.now() / LocalDateTime.now() / new Date() / time.Now()
  • 周/月/日起点计算显式带时区
  • 测试用 Clock.fixed() / Mock,不依赖真实时间

数据层:

  • JDBC URL 显式设置 serverTimezone
  • DB 字段类型与时区策略一致
  • 跨时区比较一律用 Instant / UTC 时间戳

部署层:

  • Docker / k8s 设置 TZ 环境变量
  • CI 测试容器与生产时区一致

接口层:

  • API 文档声明时间参数的时区语义(ISO 8601 + 时区偏移)
  • 响应时间字段带时区(2026-05-14T10:00:00+08:00)

规则溯源

> 📋 本回复遵循:`time-zone-safety` - [章节名]

Frequently asked questions

What does the Time Zone Safety AI skill do?

当代码出现 LocalDate.now() / LocalDateTime.now() / Instant.now() / new Date() / Date.now() / time.Now() 等"当前时间"调用,或涉及周/月/日起点、跨时区比较、数据库时间字段时触发。防止生产环境时区漂移导致的统计错乱。

Why use Time Zone Safety on TypingMind?

Because you install it once and use it with any model. Time Zone Safety is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Time Zone Safety in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/doccker/cc-use-exp/tree/main/.claude/skills/time-zone-safety. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Time Zone Safety?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Time Zone Safety?

As many as you like. As long as a model supports skills, you can use Time Zone Safety with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Time Zone Safety AI skill free?

It is published on GitHub by doccker. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇