Files
ephron-ren-prd/prd-audit-timezone-fix.md

216 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计页面时间显示问题
> **版本**: v1.0
> **日期**: 2026-05-06
> **状态**: 📝 待评审
---
## 一、问题描述
### 1.1 现象
审计页面 (`/admin/audit`) 显示的时间比实际时间少 8 小时。
### 1.2 复现步骤
1. 登录 auth.ephron.ren 管理后台
2. 访问 `/admin/audit`
3. 观察「时间」列
**预期行为**显示北京时间UTC+8
**实际行为**:显示 UTC 时间(比北京时间少 8 小时)
### 1.3 影响范围
- 影响所有审计日志的时间显示
- 影响时间筛选功能(筛选条件也是 UTC 时间)
---
## 二、根因分析
### 2.1 数据库表结构
```sql
CREATE TABLE IF NOT EXISTS audit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
```
### 2.2 问题根因
1. SQLite 的 `CURRENT_TIMESTAMP` 返回 **UTC 时间**,格式为 `YYYY-MM-DD HH:MM:SS`
2. 模板直接显示 `{{ event.created_at }}`,没有转换
3. 服务器时区可能是 UTC或者 Python 没有正确处理时区转换
### 2.3 代码位置
**模板**`auth/templates/admin/audit.html` 第 272 行
```html
<td>{{ event.created_at or "-" }}</td>
```
**查询函数**`shared/audit_events.py` 第 146 行
```python
def query_audit_events(...) -> list[dict[str, Any]]:
# 直接返回数据库原始值,没有时区转换
```
---
## 三、解决方案
### 3.1 方案对比
| 方案 | 实现位置 | 优点 | 缺点 |
|------|---------|------|------|
| A. 后端转换 | Python 查询函数 | 统一处理,前端无需改动 | 需要修改查询函数 |
| B. 前端转换 | JavaScript | 灵活,可适配用户时区 | 需要 JS 代码 |
| C. 模板过滤器 | Jinja2 过滤器 | 简单 | 需要自定义过滤器 |
### 3.2 推荐方案:后端转换
`query_audit_events()` 函数中,将 `created_at` 从 UTC 转换为北京时间。
**实现**
```python
from datetime import datetime, timezone, timedelta
def _utc_to_beijing(utc_str: str | None) -> str | None:
"""将 UTC 时间字符串转换为北京时间字符串"""
if not utc_str:
return None
try:
# 解析 UTC 时间
utc_dt = datetime.strptime(utc_str, "%Y-%m-%d %H:%M:%S")
utc_dt = utc_dt.replace(tzinfo=timezone.utc)
# 转换为北京时间
beijing_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
return beijing_dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return utc_str
def query_audit_events(...) -> list[dict[str, Any]]:
# ... 查询逻辑 ...
events: list[dict[str, Any]] = []
for row in rows:
event = dict(row)
# 转换时间
event["created_at"] = _utc_to_beijing(event["created_at"])
# ... 其他处理 ...
events.append(event)
return events
```
### 3.3 备选方案:模板过滤器
在模板中使用 Jinja2 过滤器:
```python
# 在路由中注册过滤器
def format_datetime(utc_str):
if not utc_str:
return "-"
try:
utc_dt = datetime.strptime(utc_str, "%Y-%m-%d %H:%M:%S")
utc_dt = utc_dt.replace(tzinfo=timezone.utc)
beijing_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
return beijing_dt.strftime("%Y-%m-%d %H:%M:%S")
except:
return utc_str
templates.env.filters["format_datetime"] = format_datetime
```
模板中使用:
```html
<td>{{ event.created_at | format_datetime }}</td>
```
---
## 四、实现细节
### 4.1 修改文件
- `shared/audit_events.py`:添加时区转换函数,修改 `query_audit_events()`
### 4.2 注意事项
1. **时间筛选**:筛选条件(`start_time``end_time`)也需要转换为 UTC 再查询
2. **兼容性**:确保旧数据(可能已经是北京时间)不会被重复转换
3. **性能**:时区转换是轻量操作,不会影响查询性能
### 4.3 筛选条件处理
```python
def _beijing_to_utc(beijing_str: str | None) -> str | None:
"""将北京时间字符串转换为 UTC 时间字符串"""
if not beijing_str:
return None
try:
beijing_dt = datetime.strptime(beijing_str, "%Y-%m-%dT%H:%M")
beijing_dt = beijing_dt.replace(tzinfo=timezone(timedelta(hours=8)))
utc_dt = beijing_dt.astimezone(timezone.utc)
return utc_dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return beijing_str
# 在 query_audit_events 中
if start_time:
conditions.append("created_at >= ?")
params.append(_beijing_to_utc(start_time))
if end_time:
conditions.append("created_at <= ?")
params.append(_beijing_to_utc(end_time))
```
---
## 五、测试验证
### 5.1 测试用例
| 编号 | 测试步骤 | 预期结果 |
|------|---------|---------|
| T-001 | 查看审计页面时间 | 显示北京时间(比 UTC 多 8 小时) |
| T-002 | 使用时间筛选 | 筛选结果正确 |
| T-003 | 查看新产生的审计日志 | 时间正确 |
### 5.2 验证方法
1. 部署后访问 `/admin/audit`
2. 对比显示时间与实际时间
3. 测试时间筛选功能
---
## 六、优先级与排期
| 优先级 | 任务 | 预估时间 |
|--------|------|---------|
| P0 | 添加时区转换函数 | 10 分钟 |
| P0 | 修改查询函数 | 5 分钟 |
| P1 | 测试验证 | 5 分钟 |
**总计**20 分钟
---
## 附录
### A. 相关文件
- `shared/audit_events.py`:审计事件查询函数
- `auth/templates/admin/audit.html`:审计页面模板
- `auth/src/routes/admin.py`:审计页面路由
### B. 参考资料
- [SQLite Date And Time Functions](https://www.sqlite.org/lang_datefunc.html)
- [Python datetime timezone](https://docs.python.org/3/library/datetime.html#timezone-objects)