# 审计页面时间显示问题 > **版本**: 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 {{ event.created_at or "-" }} ``` **查询函数**:`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 {{ event.created_at | format_datetime }} ``` --- ## 四、实现细节 ### 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)