Claude Code 日期与时间处理实战指南
用 Claude Code 安全处理时区、DST、Intl 格式化、测试和数据库时间保存策略的实战指南。
为什么日期时间不能只交给 Claude Code 随手实现
日期与时间的 bug 很容易在代码评审中漏掉,却会在生产环境造成很大影响。页面在中国或日本看起来正常,不代表它能正确处理美国夏令时、欧洲月底结算、印度半小时时差,或服务器与浏览器的时区差异。如果只是对 Claude Code 说“帮我实现日期处理”,它可能会生成漂亮的格式化代码,但不会自动补齐保存策略、API 契约和边界测试。
更稳妥的做法,是把 Claude Code 当成一起梳理日期时间规格的结对工程师,而不只是代码生成器。本文面向预约、账单、通知、客服 SLA、管理后台等跨时区 Web 应用。测试整体设计可参考Claude Code 测试策略,数据库变更可配合数据库迁移指南阅读。
实现前先看官方资料。显示格式以MDN Intl.DateTimeFormat为基准,Temporal 的状态与概念参考TC39 Temporal proposal和MDN Temporal,PostgreSQL 的时间保存行为参考PostgreSQL Date/Time Types,需要让 Claude Code 编辑后自动跑检查时则参考Claude Code hooks。
先固定术语和保存策略
在让 Claude Code 写代码之前,团队必须先统一语言。否则评审时很容易出现“我们存 UTC,所以没问题”“产品只面向本地用户,所以固定本地时区就好”这类过度简化的判断。
| 术语 | 直观含义 | 保存原则 |
|---|---|---|
| instant | 全球唯一的时间点,例如 2026-06-02T00:00:00Z | 以 UTC 基准的 timestamp 保存 |
| local date | 用户日历上的日期,例如生日、截止日、营业日 | 用 YYYY-MM-DD 保存,不和时刻混在一起 |
| wall clock time | 用户在钟表上看到的时间,例如 09:00 会议 | 必须和 IANA 时区 ID 一起保存 |
| IANA timezone | Asia/Shanghai、America/New_York 这样的地区名 | 不要用固定 offset 替代 |
| DST | 夏令时;某些本地日期可能只有 23 或 25 小时 | 必须测试切换日 |
真实项目中,建议把下面三条写进 AGENTS.md 或 CLAUDE.md。
- API 中已经发生的时间点必须是带
Z或显式 offset 的 ISO 8601 字符串。 - 未来预约、通知、计划任务要分开保存
localDate、localTime、timeZone。 - UI 显示必须给
Intl.DateTimeFormat同时传入locale和timeZone,不要依赖运行环境默认值。
边界关系可以这样理解。
flowchart LR
A["Client: local date/time input"] --> B["API contract: localDate + localTime + timeZone"]
B --> C["Server: validate and convert when needed"]
C --> D["Database: instant in timestamptz + original timeZone"]
D --> E["UI: format with Intl.DateTimeFormat"]
E --> A
用 Intl.DateTimeFormat 打好安全基础
如果需求只是展示,优先从标准 API Intl.DateTimeFormat 开始。MDN 将它说明为用于语言敏感日期时间格式化的内置 API,并且可以显式指定 timeZone。给 Claude Code 的约束也要写清楚:不要无参数调用 toLocaleString(),不要把代表本地日历日的 YYYY-MM-DD 直接交给 Date 解析。
下面是一个无依赖的基础模块,可以放在 src/lib/date-policy.ts,供 UI、API 和测试共用。
// src/lib/date-policy.ts
export const TIME_POLICY = {
defaultLocale: 'zh-CN',
defaultTimeZone: 'Asia/Shanghai',
} as const;
type FormatOptions = {
locale?: string;
timeZone?: string;
includeWeekday?: boolean;
};
function toDate(input: string | Date): Date {
const date = input instanceof Date ? input : new Date(input);
if (!Number.isFinite(date.getTime())) {
throw new Error(`Invalid date value: ${String(input)}`);
}
return date;
}
export function toUtcIso(input: string | Date): string {
if (typeof input === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(input)) {
throw new Error('Timestamp must include Z or an explicit UTC offset.');
}
return toDate(input).toISOString();
}
function dateParts(date: Date, timeZone: string): Record<string, string> {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(date);
return Object.fromEntries(
parts
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value]),
);
}
export function dayKeyInTimeZone(
input: string | Date,
timeZone = TIME_POLICY.defaultTimeZone,
): string {
const parts = dateParts(toDate(input), timeZone);
return `${parts.year}-${parts.month}-${parts.day}`;
}
export function formatInstant(
input: string | Date,
{
locale = TIME_POLICY.defaultLocale,
timeZone = TIME_POLICY.defaultTimeZone,
includeWeekday = true,
}: FormatOptions = {},
): string {
return new Intl.DateTimeFormat(locale, {
timeZone,
weekday: includeWeekday ? 'short' : undefined,
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZoneName: 'short',
}).format(toDate(input));
}
这段代码的目的不是把所有日期操作都藏进工具函数,而是把三件事分清:全球唯一时间点、本地日期 key、展示标签。以后 Claude Code 想新增工具函数时,先让它说明新需求属于哪一类。
未来的本地时间要显式转换
Intl.DateTimeFormat 擅长输出,但不适合把 2026-11-01 01:30 America/New_York 这种本地时间安全转换成 UTC instant。这个场景应该在项目层选择一个库,并确认当前官方文档。Luxon和它的API docs提供明确的时区 API。date-fns适合函数式日期工具,但涉及时区时要确认最新官方能力。Temporal 在 TC39 已是 Stage 4,但仍要检查目标浏览器、Node 版本和 polyfill 策略。
预约表单中,用户输入的是本地日期和本地时间,服务端需要转换成数据库保存的 instant。
// src/lib/schedule-time.ts
import { DateTime } from 'luxon';
type LocalScheduleInput = {
localDate: string; // YYYY-MM-DD
localTime: string; // HH:mm
timeZone: string; // IANA time zone, for example "America/New_York"
};
export function scheduleToUtcIso(input: LocalScheduleInput): string {
const rawLocal = `${input.localDate}T${input.localTime}`;
const local = DateTime.fromISO(rawLocal, { zone: input.timeZone });
if (!local.isValid) {
throw new Error(local.invalidExplanation ?? `Invalid local time: ${rawLocal}`);
}
const roundTrip = local.setZone(input.timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
if (roundTrip !== rawLocal) {
throw new Error(`Nonexistent local time in ${input.timeZone}: ${rawLocal}`);
}
const iso = local.toUTC().toISO({ suppressMilliseconds: true });
if (!iso) {
throw new Error(`Could not convert local time to UTC: ${rawLocal}`);
}
return iso;
}
注意,这个函数并没有自动解决夏令时结束时重复出现的 01:00。产品必须决定:选择较早 offset、选择较晚 offset,还是让用户确认。让 Claude Code 写测试时,要明确要求覆盖不存在的本地时刻、重复时刻、月底和闰日。
分开处理客户端、服务端和数据库边界
最常见的失败,是没有决定一个值到底由浏览器时区还是服务器时区解释,就直接传给 Date。浏览器输入、API payload、数据库列、邮件、CSV 导出都是不同边界。
评审设计时,至少用下面三个用例检查。
- 预约系统:输入采用场地本地时间,保存采用 UTC instant,展示采用浏览者或场地时区。
- 账单截止:合同中的“月底 23:59”属于客户合同时区的 local date 规则,不要用加 24 小时拼出下个月。
- 客服 SLA:营业日、假日、办公时间按地区不同,既要保存截止 instant,也要保存便于人理解的本地上下文。
PostgreSQL 官方文档说明,timestamp with time zone 输入会转换为 UTC,原始时区不会保留。因此,只靠 timestamptz 无法知道用户当时选择的是 America/New_York。产品需要这个信息时要另存一列。
create table scheduled_events (
id uuid primary key,
title text not null,
starts_at timestamptz not null,
original_time_zone text not null check (original_time_zone <> ''),
local_date date not null,
local_time time not null,
created_at timestamptz not null default now()
);
create index scheduled_events_starts_at_idx
on scheduled_events (starts_at);
不要以为把 2026-06-02T09:00:00+09:00 插入 timestamp without time zone 就安全。PostgreSQL 先确定类型;如果值已经被判断为 without time zone,时区信息会被忽略。把“日期时间列类型是否匹配 API 契约”加入 Claude Code 的评审清单。
用固定 instant 测试 DST 和边界日
测试不应依赖“今天”、当前时钟或开发者机器时区。把输入固定为 UTC instant,再断言目标时区中的 local date 或显示结果。使用 Vitest 时,可以从下面的测试开始。更完整的策略可参考Claude Code Vitest 进阶。
// src/lib/date-policy.test.ts
import { describe, expect, it } from 'vitest';
import { dayKeyInTimeZone, formatInstant, toUtcIso } from './date-policy';
describe('date/time policy', () => {
it('requires an explicit offset for API timestamps', () => {
expect(() => toUtcIso('2026-06-02T09:00:00')).toThrow(/offset/);
expect(toUtcIso('2026-06-02T09:00:00+09:00')).toBe('2026-06-02T00:00:00.000Z');
});
it('calculates a local day key across the UTC date boundary', () => {
expect(dayKeyInTimeZone('2026-03-31T15:01:00Z', 'Asia/Tokyo')).toBe('2026-04-01');
expect(dayKeyInTimeZone('2026-04-01T00:30:00Z', 'America/Los_Angeles')).toBe('2026-03-31');
});
it('formats a DST transition instant in the requested time zone', () => {
const label = formatInstant('2026-03-08T07:30:00Z', {
locale: 'en-US',
timeZone: 'America/New_York',
});
expect(label).toMatch(/03:30|3:30/);
expect(label).toMatch(/EDT|GMT-4/);
});
});
这里故意允许 EDT 或 GMT-4,因为 ICU 数据在不同 CI 环境可能不同。local date key 和 UTC 转换结果属于业务逻辑,应该严格断言。
用提示词和 hooks 防止回归
给 Claude Code 的约束要在实现前写清楚。提示词可以长一点,重点是列出不能破坏的边界。
Before implementing date/time changes, read date-policy.ts and the database schema.
Constraints:
- API timestamps for completed events must be ISO strings with Z or an explicit offset
- Future bookings must keep localDate, localTime, and timeZone separate
- Intl.DateTimeFormat must always receive locale and timeZone
- Do not use new Date('YYYY-MM-DD') for local calendar dates
- Add Vitest cases for DST start, DST end, month end, and leap day
- Explain the PostgreSQL timestamp/timestamptz difference in the review notes
Done when:
- npm test -- --run date-time passes
- Updated API response examples are added to README
团队使用时,可以用 Claude Code hooks 在编辑后自动运行日期时间测试。官方文档说明,hooks 可以在 Claude Code 生命周期事件中执行 shell 命令。完整流程可参考Claude Code hooks 指南。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm test -- --run date-time"
}
]
}
]
}
}
按时间类型选择库
不要只按流行度选日期库,而要按产品处理的时间类型选择。Intl.DateTimeFormat 适合本地化显示,date-fns 适合轻量日期工具,Luxon 适合包含 IANA 时区的预约和转换,Temporal 适合想在类型层面区分 Instant、PlainDate、ZonedDateTime 的长期设计。即使 Temporal 已是 Stage 4,也要先确认目标运行环境和 polyfill 方针。
| 选择 | 适合场景 | 注意点 |
|---|---|---|
| Intl | 带明确时区的本地化显示 | 不是本地时间转 instant 的工具 |
| date-fns | 纯日期计算、函数级导入、小工具 | 时区需求要看当前官方文档 |
| Luxon | IANA 时区预约、相对时间、ISO 转换 | DST 重复时刻仍需产品规则 |
| Temporal | 清晰区分 Instant、PlainDate、ZonedDateTime | 要确认运行环境与 polyfill |
从内容变现角度看,单纯列库没有足够价值。读者真正需要的是:该存什么、在哪里转换、测什么。如果团队想把这套规则放进 CLAUDE.md、hooks、PR review 和 migration 流程,可以查看Claude Code 培训与咨询。个人开发者则可以把本文提示词加入免费速查表作为重复使用的防线。
实际验证记录
Masa 的验证记录:最有用的检查,是固定一个 UTC instant,分别用东京、纽约、洛杉矶显示,再断言跨日期边界的 local date key。最容易复现的 bug,是把 YYYY-MM-DD 当成用户本地日期转成 Date,然后在另一个时区显示成前一天。本文的策略让 local date 保持字符串,只对保存的 instant 和展示标签做显式转换,因此 Claude Code 生成的 diff 更容易评审。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。