5.6 KiB
5.6 KiB
审计页面时间显示问题
版本: v1.0
日期: 2026-05-06
状态: 📝 待评审
一、问题描述
1.1 现象
审计页面 (/admin/audit) 显示的时间比实际时间少 8 小时。
1.2 复现步骤
- 登录 auth.ephron.ren 管理后台
- 访问
/admin/audit - 观察「时间」列
预期行为:显示北京时间(UTC+8)
实际行为:显示 UTC 时间(比北京时间少 8 小时)
1.3 影响范围
- 影响所有审计日志的时间显示
- 影响时间筛选功能(筛选条件也是 UTC 时间)
二、根因分析
2.1 数据库表结构
CREATE TABLE IF NOT EXISTS audit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
2.2 问题根因
- SQLite 的
CURRENT_TIMESTAMP返回 UTC 时间,格式为YYYY-MM-DD HH:MM:SS - 模板直接显示
{{ event.created_at }},没有转换 - 服务器时区可能是 UTC,或者 Python 没有正确处理时区转换
2.3 代码位置
模板:auth/templates/admin/audit.html 第 272 行
<td>{{ event.created_at or "-" }}</td>
查询函数:shared/audit_events.py 第 146 行
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 转换为北京时间。
实现:
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 过滤器:
# 在路由中注册过滤器
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
模板中使用:
<td>{{ event.created_at | format_datetime }}</td>
四、实现细节
4.1 修改文件
shared/audit_events.py:添加时区转换函数,修改query_audit_events()
4.2 注意事项
- 时间筛选:筛选条件(
start_time、end_time)也需要转换为 UTC 再查询 - 兼容性:确保旧数据(可能已经是北京时间)不会被重复转换
- 性能:时区转换是轻量操作,不会影响查询性能
4.3 筛选条件处理
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 验证方法
- 部署后访问
/admin/audit - 对比显示时间与实际时间
- 测试时间筛选功能
六、优先级与排期
| 优先级 | 任务 | 预估时间 |
|---|---|---|
| P0 | 添加时区转换函数 | 10 分钟 |
| P0 | 修改查询函数 | 5 分钟 |
| P1 | 测试验证 | 5 分钟 |
总计:20 分钟
附录
A. 相关文件
shared/audit_events.py:审计事件查询函数auth/templates/admin/audit.html:审计页面模板auth/src/routes/admin.py:审计页面路由