阅读主题
日程规则与当地时间解析
用户在预约表单中选好日期,填上上午九点,按下确认。要生成提醒,系统必须把“哪天几点”转换成一个确定的瞬间;如果设置的是“每个工作日九点”,还要先找出符合条件的日期。表单上的时间看起来已经完整,计划的生成规则却还没有全部确定。
上一节为业务日求出区间,本节为日程生成执行瞬间。我们先确定预约要保持什么,再根据时区规则解析输入,最后讨论生成结果怎样用于执行。
1.6 日程规则与当地时间解析
1.6.1 固定瞬间与保持当地日程
一场线上会议已经确认在 2027-07-10T13:00:00Z 开始。按该日期的时区规则,纽约参与者看到上午九点,北京参与者看到晚上九点。若产品约定保持这个已确认的瞬间,参与者旅行只会改变界面显示,会议仍在原来的瞬间开始;要改变它,需要明确改期。
另一种预约承诺的是“2027 年 7 月 10 日,纽约门店当地时间上午九点到店”。系统同样可以先算出 2027-07-10T13:00:00Z,但若产品要求始终保持门店当地九点,原始日期时间和地区时区就仍是计算依据。假如赴约前该地区修改了时区规则,九点对应的瞬间可能变化,预约与提醒就需要重新检查。
两种安排在创建时可能得到同一个时间点,区别在于条件变化后保持什么。固定瞬间的计划以已确认时间点为准,保持当地日程的计划以当地输入和地区规则为准。 线上或到店只是帮助理解的例子,具体采用哪一种承诺,仍由产品约定,不能由数据库类型或场景名称自动决定。时区规则确实会更新,实际变更可见 IANA 规则更新记录。
重复日程更需要保留生成依据。假设纽约门店每天九点开门,2025 年 3 月 8 日的九点对应 UTC 十四点,次日九点却对应 UTC 十三点。如果从第一次的瞬间加 24 小时,得到的是 3 月 9 日 UTC 十四点,也就是门店当地十点。计算保持了固定间隔,却改变了开门钟点。
因此,“每天九点”的生成过程应是:
text
日期规则选出营业日期 D
→ 将 D 与当地时刻 09:00 组合
→ 查询门店时区在该日期时间的候选瞬间
→ 按解析策略确定结果
→ 生成这一次营业计划如果规则是“每个工作日九点”,第一步就要使用业务工作日历。星期一到星期五只是每周的基本安排,还需要结合适用的公共假日、补休、调休和机构自己的营业安排,才能判断某一天是否生成计划。
这里必须说明采用哪个国家、地区以及哪家机构的日历。一个国家内部也未必只有一份假日安排,例如英国政府分别列出英格兰和威尔士、苏格兰、北爱尔兰的银行假日日历。英国政府假日日历 同一个时区可以服务于多份不同的业务日历,因此不能从 ZoneId 推断公共假日,也不能从用户界面的语言推断适用地区。
公共假日也不自动等于门店休业日。假设门店约定通常周一至周五营业、公共假日休息,但允许临时增开或停业,就应先形成这家门店的有效营业日历:明确哪些公共假日安排适用,再处理指定日期的例外,并规定冲突时以哪项决定为准。例如,一个普通周三因适用的公共假日而不生成计划,一个周六则因明确的补班或营业安排而生成计划。这样,日期选择是在查询业务约定,而不是简单判断星期几。
业务日历决定哪天安排,时区规则决定当天几点对应哪个瞬间。 对“每个工作日九点”,非工作日直接不生成那一次计划;对“每月一日九点,遇假日顺延”,则要先查日历找到下一营业日,再解析九点。如果日历尚未覆盖目标年份,应明确暂缓生成或标记待确认,不能把“没有假日数据”当成“这一天营业”。
采用外部假日数据时,应记录来源、适用地区、覆盖年份和版本,并明确由谁更新。日历更新后,先比较哪些未来日期的营业状态发生了变化,再按变更政策重算未执行计划;已有用户确认的预约还需处理通知或重新确认,不能直接无声删除,已执行记录则保留原事实。日期规则、业务日历、当地时刻、地区时区和解析策略共同决定计划,今天九点的一个 UTC 值无法替代这些依据。
1.6.2 从当地输入寻找候选瞬间
日期选定后,先检查输入的日期、时刻和时区标识是否合法,再查询它们能对应哪些瞬间。这两步不能合并:2025-03-09T02:30 是格式和日历字段都合法的当地日期时间,但纽约当天跳过了两点到三点,那里没有出现过这个读数。
相反,纽约 2025-11-02T01:30 出现了两次。查询地区规则,可以得到两个有效偏移量,再分别换算成瞬间:
text
当地输入:2025-11-02T01:30,America/New_York
候选一:-04:00 → 2025-11-02T05:30:00Z(第一次一点半)
候选二:-05:00 → 2025-11-02T06:30:00Z(第二次一点半)Java 的 ZoneRules.getValidOffsets 可以查询有效偏移量。普通情况下有一个;缺口中没有;重叠时通常有两个。为每个有效偏移量计算出的瞬间,才是符合当地输入与地区规则的候选。Java 地区规则 API
规则查询给出候选,业务策略决定如何处理候选。 本例预约采用“缺口拒绝,重叠要求用户选择”:没有候选时,请用户重新选时间;有一个时可以直接确定;有多个时,展示第一次、第二次等选项,让用户确认。界面可以用自然语言解释两次一点半,不必要求用户理解偏移量的数值含义。
其他业务可以采用不同策略。例如定时巡检允许跳过不存在的那一次;也可以约定调整到其他时刻,但“顺延”必须说清。纽约春季的两点半,若按一小时的跳变长度平移,会变成三点半;若使用缺口结束后的第一个有效时刻,则是三点整。两种结果都合理,却是不同的计划,不能只用“自动处理夏令时”概括。
本例还需校验用户提交的偏移量。它必须属于本次查到的候选集合,即使当前只有一个候选也一样。纽约夏季九点采用 -04:00;若请求错误地传来 -05:00,直接按它换算会得到 UTC 十四点,再换回纽约就是十点。数学换算成功,却没有兑现九点的预约。
反过来,服务端也不能忽略用户选择:用户确认了第二次一点半,再默认取第一次,会让预约提前一小时。一个满足本例规则的解析流程可以写成:
text
resolve(local_start, time_zone, selected_offset):
offsets = 查询该当地日期时间在地区规则中的有效偏移量
if offsets 为空:
返回“该时间不存在,请重新选择”
if selected_offset 已提交:
if selected_offset 不属于 offsets:
返回“所选偏移量与该日期的时区规则不符”
chosen_offset = selected_offset
else if offsets 只有一个:
chosen_offset = 唯一偏移量
else:
返回候选,等待用户选择
return local_start 按 chosen_offset 换算得到的瞬间图 1-4:本例预约根据候选集合处理缺口、唯一结果与重叠。提交的偏移量需要重新校验,完整实现见预约解析实验。
Java 的 ZonedDateTime.of(localDateTime, zone) 有自己的便利行为:缺口中按跳变长度向后调整,重叠时默认采用较早的偏移。因此,纽约上述两点半会变成三点半,一点半则默认选第一次。这是该构造方法的明确约定,不能代替本例要求的拒绝与用户选择。Java ZonedDateTime.of
1.6.3 协议保存意图,计划保存结果
接口要让服务端拿到上述解析输入。本例接收当地日期时间、地区时区,以及用户必要时选择的偏移量:
json
{
"local_start": "2027-07-10T09:00:00",
"time_zone": "America/New_York",
"selected_offset": null
}这里的 null 表示尚未选择偏移量,不表示 UTC 或服务器默认时区。本例日期只有 -04:00 一个有效偏移量,因此服务端可以直接创建预约,返回确定的 starts_at = 2027-07-10T13:00:00Z。若输入是纽约 2025 年 11 月 2 日的一点半,则应先返回两个候选,等待选择,不能在此时宣称预约已经创建。
用户选择第二次后,再提交 "selected_offset": "-05:00",服务端重新查询并校验,成功后得到 2025-11-02T06:30:00Z。客户端之前展示过候选,并不能代替提交时的校验:输入可能已经变更,客户端也可能沿用了旧数据。
对于保持当地日程的预约,创建成功后应保留当地输入、地区时区、解析策略和最终采用的偏移量,同时保存用于执行的 starts_at。二者有计算关系,不能独立修改。原始输入用于编辑和重新计算,时间点用于明确当前计划在哪一刻执行;保存了计算结果,不等于以后应始终以这个结果为准。
改期时,应按新日期重新解析,旧偏移量不能自动作为新日期的选择。 例如把纽约九点的预约从七月改到十二月,仍沿用七月的 -04:00 就会与十二月规则冲突。表单应清除或重新验证旧选择,服务端也必须再次查询候选。改期后,依赖开始时间的提醒应随之更新。
时区规则更新与用户改期还需要分开处理。对于固定瞬间的计划,规则更新可能改变当地显示,但不应擅自改变执行瞬间。对于保持当地日程的计划,应检查受影响的未执行预约,用新规则重新解析;如果出现新的缺口、重叠或旧选择失效,应按约定处理或要求重新确认。已经执行的记录保留原事实,不应被新规则改写。
若要求重现过去一次解析,还要能取得当时使用的时区规则与解析策略。可以记录版本并保留对应规则数据;只存一个会持续更新的时区名称,或只存一个无法再取得数据的版本号,都不足以重跑旧计算。
只接收带偏移量时间戳的接口也可以成立,它能定位一次确定的瞬间。但同一个偏移量可能用于多个地区,无法据此推断未来日期应使用哪套规则。这样的接口应明确只接收已确定的时间点,不能同时承诺保持用户未提交的当地日程。
1.6.4 生成计划与执行计划的边界
得到 starts_at,意味着这次计划已有明确的执行时间。它并不证明执行器会准时运行。例如预约提醒计划在上午八点发送,执行器八点十分才恢复服务;时间解析再精确,也无法决定这条提醒此时该补发还是作废。
生成器决定何时应该执行,执行器处理届时实际发生的事。 提醒迟到是否补发、失败是否重试,以及任务是否已经完成,都要由执行规则说明。可以规定开课前迟到的提醒仍然发送,开课后则跳过;这只是一个业务选择,并非时间类型自带的保证。
改期也会留下执行问题。门店将未来的开门时间从九点改为十点,已经排入队列的九点计划不会自行消失。生成结果需要关联原日程和版本,执行时才能识别已失效的旧计划。检查版本与真正执行之间的并发变化,还需执行层协调;单次版本比较不等于整个操作已经安全。完整的计划身份、补跑和改期处理见日报与计划执行。
验证时应分别检查这两层。生成与解析测试覆盖正常日期、缺口、重叠的两次选择、无效偏移,以及改期后重新解析;重复日程还应检查跨越偏移变化后是否仍保持约定的当地钟点,以及公共假日被排除、补班日被纳入、不同地区日历产生不同日期、数据覆盖不足和日历更新时的处理。执行测试则模拟迟到、重复领取和旧版本计划,检查各自约定的行为。不能用“创建时得到一个正确时间戳”代替这些验证。
日程现在既有用户的原始选择,也有计算出的执行瞬间。下一节讨论这些信息怎样经过接口和数据库,才能在读取、展示和再次编辑时保持原来的含义。