171 lines
6.7 KiB
Markdown
171 lines
6.7 KiB
Markdown
# 后端时间设计说明
|
||
|
||
## 1. 文档定位
|
||
|
||
本文集中记录本项目的时间存储、接口返回和页面展示规则,避免把数据库里的 UTC 时间误当成酒店当地展示时间。
|
||
|
||
适用范围:
|
||
|
||
- 后端数据库时间字段设计。
|
||
- 后端 API 时间字段返回。
|
||
- 前端展示和按日期筛选。
|
||
- AgentBus / SuperAgent 等外部系统时间字段映射。
|
||
|
||
如本文与具体业务 PRD 冲突,以业务 PRD 的特殊说明为准;没有特殊说明时,统一遵守本文。
|
||
|
||
## 2. 总体原则
|
||
|
||
一句话原则:
|
||
|
||
数据库存 UTC 事实时间,接口返回带 `Z` 的 UTC 时间,页面按酒店或用户时区展示;酒店业务日期不做 UTC 时间点换算。
|
||
|
||
落地规则:
|
||
|
||
- 数据库中的时间点字段统一按 UTC 理解。
|
||
- Java Entity 中的 `LocalDateTime` 默认表示 UTC 时间点,不表示服务器本地时间。
|
||
- API 返回时间点字段必须是带 `Z` 的 ISO 8601 UTC 时间,例如 `2026-07-09T05:56:56Z`。
|
||
- 前端展示时间点时,再按用户或酒店时区格式化;当前默认酒店时区是 `Asia/Bangkok`。
|
||
- 入住日期、离店日期、酒店营业日等本地业务日期,不得和 UTC 时间点混用。
|
||
|
||
## 3. 时间字段分类
|
||
|
||
| 类型 | 示例字段 | 含义 | 数据库语义 | API 语义 | 页面展示 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 系统审计时间 | `created_at`、`updated_at`、`occurred_at`、`confirmed_at`、`completed_at` | 本系统内部动作发生时间 | UTC 时间点 | 带 `Z` 的 UTC 时间 | 按酒店或用户时区展示 |
|
||
| 外部来源事件时间 | SourceMessage `received_at`、`source_sent_at` | 外部系统提供的邮件接收 / 发送时间 | UTC 时间点 | 带 `Z` 的 UTC 时间 | 按酒店或用户时区展示 |
|
||
| 外部认证时间 | SuperAgent HMAC timestamp | 外部调用签名时间 | UTC 时间点 | ISO 8601 UTC | 不作为业务展示字段 |
|
||
| 酒店本地业务日期 | 入住日期、离店日期、营业日 | 酒店当地日历日期 | `LocalDate` 或明确本地日期语义 | 日期字符串 | 直接按酒店本地日期展示 |
|
||
|
||
## 4. 数据库存储规则
|
||
|
||
MySQL 时间点字段当前主要使用 `DATETIME(6)`。
|
||
|
||
注意:`DATETIME(6)` 本身不带时区。本项目约定,所有表示时间点的 `DATETIME(6)` 都按 UTC 解释。
|
||
|
||
示例:
|
||
|
||
```text
|
||
数据库值:2026-07-09 05:56:56
|
||
真实含义:2026-07-09T05:56:56Z
|
||
泰国展示:2026-07-09 12:56:56
|
||
```
|
||
|
||
后端连接 MySQL 时,JDBC URL 应明确 `serverTimezone=UTC`。部署容器和 JVM 也应优先使用 UTC,或至少保证入库时间都通过 UTC 生成。
|
||
|
||
## 5. Java 类型规则
|
||
|
||
后端内部约定:
|
||
|
||
- Entity / Snapshot 中从数据库读取的 `LocalDateTime` 默认表示 UTC 时间点。
|
||
- API Response 中的时间点字段优先使用 `OffsetDateTime`。
|
||
- 数据库 `LocalDateTime` 输出到 API 时,使用 `UtcTimeFormatter.toUtcOffsetDateTime(...)` 统一补 UTC offset。
|
||
- 当前 UTC 时间统一使用 `LocalDateTime.now(ZoneOffset.UTC)` 或 `OffsetDateTime.now(ZoneOffset.UTC)`。
|
||
- 酒店本地日期使用 `LocalDate`,不要使用 `LocalDateTime` 假装表示日期。
|
||
|
||
示例:
|
||
|
||
```java
|
||
OffsetDateTime apiTime = UtcTimeFormatter.toUtcOffsetDateTime(entity.getCreatedAt());
|
||
```
|
||
|
||
## 6. SourceMessage 邮件时间规则
|
||
|
||
`platform_source_message_inbox.received_at` 的语义已经调整为邮件来源接收时间:
|
||
|
||
1. 优先取 AgentBus payload 或 source 中的 `received_at`。
|
||
2. 如果 AgentBus 没有提供或格式无法解析,回退为本系统捕获该消息的 UTC 时间。
|
||
3. `created_at` / `updated_at` 仍表示本系统记录创建 / 更新时间,不随邮件来源时间变化。
|
||
4. `source_sent_at` 表示来源系统提供的邮件发送时间,缺失时为空。
|
||
|
||
因此:
|
||
|
||
- `received_at` 用于邮件会话排序和业务展示。
|
||
- `created_at` / `updated_at` 用于系统审计和排查链路处理时间。
|
||
- 不应把 `received_at` 和 `created_at` 强行要求相等。
|
||
|
||
## 7. API 返回规则
|
||
|
||
后端返回给前端的时间点字段统一是 UTC 字符串:
|
||
|
||
```json
|
||
{
|
||
"source_received_at": "2026-07-09T05:56:56Z",
|
||
"created_at": "2026-07-09T05:57:00Z"
|
||
}
|
||
```
|
||
|
||
接口字段名保持业务语义:
|
||
|
||
- `received_at` / `source_received_at`:邮件来源接收时间,UTC。
|
||
- `source_sent_at`:邮件来源发送时间,UTC。
|
||
- `created_at`:本系统记录创建时间,UTC。
|
||
- `updated_at`:本系统记录更新时间,UTC。
|
||
|
||
前端不能把这些带 `Z` 的时间直接当泰国本地时间显示。
|
||
|
||
## 8. 页面展示规则
|
||
|
||
页面展示时间点时,应按酒店或用户时区格式化。当前酒店默认时区是 `Asia/Bangkok`。
|
||
|
||
示例:
|
||
|
||
```text
|
||
API 返回:2026-07-09T05:56:56Z
|
||
泰国时间:2026-07-09 12:56:56
|
||
```
|
||
|
||
前端可以使用 `Intl.DateTimeFormat`:
|
||
|
||
```ts
|
||
new Intl.DateTimeFormat("zh-CN", {
|
||
timeZone: "Asia/Bangkok",
|
||
dateStyle: "short",
|
||
timeStyle: "medium",
|
||
}).format(new Date("2026-07-09T05:56:56Z"));
|
||
```
|
||
|
||
业务人员最终应看页面展示时间;数据库只用于排查 UTC 事实时间。
|
||
|
||
## 9. 按酒店本地日期筛选
|
||
|
||
如果用户按酒店本地日期筛选,例如查询泰国当地 `2026-07-09` 的邮件,后端查询数据库前必须把本地日期范围转换成 UTC 范围。
|
||
|
||
泰国时区示例:
|
||
|
||
```text
|
||
Asia/Bangkok 2026-07-09 00:00:00
|
||
-> UTC 2026-07-08 17:00:00
|
||
|
||
Asia/Bangkok 2026-07-10 00:00:00
|
||
-> UTC 2026-07-09 17:00:00
|
||
```
|
||
|
||
查询建议使用左闭右开:
|
||
|
||
```sql
|
||
received_at >= '2026-07-08 17:00:00'
|
||
AND received_at < '2026-07-09 17:00:00'
|
||
```
|
||
|
||
不要直接用数据库 UTC 日期的 `2026-07-09 00:00:00 ~ 23:59:59` 当作泰国当地一天。
|
||
|
||
## 10. 排查规则
|
||
|
||
排查时间问题时,先确认要看的是什么:
|
||
|
||
| 场景 | 应看字段 | 判断方式 |
|
||
| --- | --- | --- |
|
||
| 业务人员看到邮件接收时间 | API `received_at` 或页面展示值 | API 是 UTC,页面转酒店时区 |
|
||
| 判断 AgentBus 是否传了邮件接收时间 | 原始 payload `received_at` | 应是 ISO 8601 UTC,例如 `2026-07-09T05:56:56Z` |
|
||
| 判断本系统什么时候写入记录 | `created_at` | UTC 系统审计时间 |
|
||
| 判断链路处理延迟 | `created_at - received_at` | 两者都按 UTC 理解后再比较 |
|
||
| 判断入住 / 离店日期 | 业务日期字段 | 不做 UTC 时间点换算 |
|
||
|
||
## 11. 禁止事项
|
||
|
||
- 不要把数据库 `DATETIME(6)` 直接当酒店当地时间展示。
|
||
- 不要在数据库里同时保存一份 UTC 时间和一份泰国展示时间。
|
||
- 不要把入住日期、离店日期、营业日用 UTC 时间点自动前后偏移。
|
||
- 不要在前端用中文或英文展示文案判断时间语义,应按接口字段名和文档约定处理。
|
||
- 不要在日志里输出包含客户敏感信息的完整邮件正文或附件 URL;时间字段可以输出,但要带上下文说明其 UTC 语义。
|