关于系统跨日处理

很多业务系统的营业时间并不遵循自然日(0:00-24:00),比如酒吧、KTV、便利店、夜班生产等场景。这类系统面临的共同挑战是:**如何定义”一天”**?

本文以酒吧下单系统为例,梳理跨日处理的常见方案和设计要点。

场景描述

某酒吧的营业时间为每日 21:00 至次日 05:00,且每个营业日归属于开始日期

举个例子:

  • 10月1日营业日:10月1日 21:00 ~ 10月2日 05:00
  • 10月2日营业日:10月2日 21:00 ~ 10月3日 05:00

系统按营业日划分,而非自然日。这带来了一系列问题。

核心问题:时间归属

假设客人在凌晨 2:00(即10月2日 02:00)下单,预定”当天晚上 10 点”的桌台。

这里”当天”究竟指什么?

1
2
3
4
5
6
时间轴:
10/1 21:00 ──────── 10/2 00:00 ──────── 10/2 05:00 ──── ... ──── 10/2 21:00
│ │ │ │
营业日开始 自然日切换 营业日结束 下个营业日开始

客人凌晨2点下单

分析:

  1. 凌晨 2:00 仍属于10月1日营业日(因为尚未到 05:00 结束时间)
  2. 但”晚上 10 点”(22:00)属于10月2日营业日(21:00 之后即进入新营业日)

所以”当天晚上 10 点” → 实际指向下一个营业日的时段。

通用设计方案

处理跨日业务的核心是引入”营业日偏移“概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 计算某个时间点所属的营业日
function getBusinessDay(date: Date, openHour: number, closeHour: number): string {
const hour = date.getHours();
// 如果当前时间在营业结束时间之前(跨午夜后的时段)
if (hour < closeHour) {
// 归属于前一天的营业日
const prevDay = new Date(date);
prevDay.setDate(prevDay.getDate() - 1);
return formatDate(prevDay);
}
// 否则属于当天的营业日
return formatDate(date);
}

// 获取营业日的自然时间范围
function getBusinessDayRange(businessDay: string, openHour: number, closeHour: number) {
const start = new Date(`${businessDay}T${String(openHour).padStart(2, '0')}:00:00`);
const end = new Date(start);
end.setDate(end.getDate() + 1);
end.setHours(closeHour, 0, 0, 0);
return { start, end };
}

// 示例
const date = new Date('2024-10-02T02:00:00');
console.log(getBusinessDay(date, 21, 5)); // "2024-10-01"
console.log(getBusinessDayRange('2024-10-01', 21, 5));
// { start: 2024-10-01 21:00, end: 2024-10-02 05:00 }

预定场景的完整逻辑

回到酒吧预定问题,完整的处理流程如下:

  1. 判断当前营业日:凌晨 2:00 → 属于 10月1日营业日
  2. **解析”晚上10点”**:22:00 已超过 21:00 → 属于 10月2日营业日
  3. 存储预定:将记录保存到 10月2日营业日的桌台状态中
  4. 前端展示:用户选择 10月2日时,显示 22:00 的预定信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 预定处理
function handleReservation(
reserveTime: Date, // 下单时间,如 10/2 02:00
targetTimeStr: string // 预定时间段,如 "22:00"
) {
const currentBizDay = getBusinessDay(reserveTime, 21, 5); // "10-01"
const targetHour = parseInt(targetTimeStr.split(':')[0]);

// 确定目标营业日
let targetBizDay: string;
if (targetHour >= 21) {
// 晚上9点后属于下一个营业日
targetBizDay = getNextBusinessDay(currentBizDay);
} else {
// 凌晨时段,可能是当前营业日的后半段
targetBizDay = currentBizDay;
}

// 保存到目标营业日
saveReservation(targetBizDay, targetTimeStr, reserveTime);

// 返回给前端的提示
return {
message: `预定成功,请在 ${targetBizDay} 日查看您在 ${targetTimeStr} 的预定`,
displayDay: targetBizDay
};
}

常见坑点

1. 数据库字段设计

永远不要只用自然日期当唯一标识,应该同时存储:

字段示例说明
business_day2024-10-01营业日(业务维度)
created_at2024-10-02 02:00:00实际创建时间(自然时间)
reserve_at2024-10-02 22:00:00预定目标时间

2. 时区问题

如果系统涉及时区,跨日问题会叠加时区偏移,建议统一使用 UTC 存储,展示层再转换。

3. 夏令时

部分国家有夏令时调整,一年中有两天不是 24 小时。如果营业时间接近凌晨 2 点,要注意可能跳过的时段。

4. 数据统计

日报、月报的统计维度应以营业日为准,而非自然日。不做区分会导致账对不上。

总结

跨日处理的核心不是算法复杂,而是定义清晰。关键在于:

  1. 明确”一天”在业务语境下的起止时间
  2. 所有时间相关的逻辑统一使用营业日概念
  3. 数据库同时保留营业日和自然时间两个维度
  4. 前端展示时做好营业日到自然日期的映射

把这几条理清楚,跨日问题基本就解决了。