后端-数据库-SQLite-UTC时间导致所有时间显示偏差8小时
问题现象
| 页面 | 列名 | 预期 | 实际 |
|---|---|---|---|
| 公众号新闻 | 获取时间 | 爬虫入库时间 CST | 少 8h(UTC) |
| 收藏列表 | 收藏时间 | 点击收藏时间 CST | 少 8h(UTC) |
| 卡片管理 | 创建时间 | 卡片创建时间 CST | 少 8h(UTC) |
- 爬虫凌晨 3:07 CST 完成,数据库存成前一天 19:07 UTC
- 同一时间点操作,各页面显示的时间均相差 8 小时
- 排序和分页也因此受影响
核心问题
根因:SQLite CURRENT_TIMESTAMP 返回 UTC
项目服务器使用 SQLite(DB_PATH=data/news.db),SQLite 的 CURRENT_TIMESTAMP 默认返回 UTC 时间。服务器时区为 CST (UTC+8),但 API 返回 created_at 时未做时区转换。
受影响表(共 9 个)
src/core/models.py 中所有使用 server_default=text("CURRENT_TIMESTAMP") 的表:
| 表名 | 模型 | 受影响的列 | 已修复 API |
|---|---|---|---|
web_sites |
WebSite |
created_at, updated_at |
- |
cards |
Card |
created_at |
card_service.py |
web_news |
WebNews |
created_at, updated_at |
- |
wechat_accounts |
WechatAccount |
created_at, updated_at |
- |
wechat_articles |
WechatArticle |
created_at |
routes.py |
auth_sessions |
AuthSession |
created_at |
- |
favorites |
Favorite |
created_at |
favorites.py |
categories |
Category |
created_at |
- |
fetch_records |
FetchRecord |
created_at |
- |
关联的次要问题(公众号文章页面)
- 显示字段错误:
News.vue"获取时间"列绑定的是publish_time(文章元数据),而非created_at(入库时间) - 排序字段错误:
routes.py排序用publish_time,非created_at
解决方法
步骤 1:修复公众号文章排序
# modules/wechat/routes.py:526
stmt = base_stmt.order_by(desc(WechatArticle.created_at)) # 原为 publish_time
步骤 2:修复前端显示字段
<!-- modules/wechat/frontend/News.vue:283 -->
{{ formatTime(row.created_at) }} <!-- 原为 row.publish_time -->
步骤 3:API 层 UTC → CST 转换
统一方案:在 API 返回时将 naive UTC datetime + 8 小时。
from datetime import timedelta
# modules/wechat/routes.py:549 — 公众号文章获取时间
"created_at": (item.created_at + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'),
# src/api/favorites.py:72 — 收藏时间
"created_at": (item.created_at + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'),
# src/service/card_service.py:61,75 — 卡片创建时间
"created_at": (card.created_at + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') if card.created_at else "",
步骤 4:部署
# 构建前端
cd web && pnpm build && git add -f web/dist/
# 上传后端文件(若无法 git pull)
scp modules/wechat/routes.py news:/www/news/modules/wechat/routes.py
scp src/api/favorites.py news:/www/news/src/api/favorites.py
scp src/service/card_service.py news:/www/news/src/service/card_service.py
# 重启
ssh news "sudo systemctl restart news-backend"
剩余待修复
以下表的 created_at 若在页面暴露给用户,也需同样处理:
- web_news.created_at / updated_at
- wechat_accounts.created_at / updated_at
- web_sites.created_at / updated_at
- auth_sessions.created_at
- categories.created_at
- fetch_records.created_at
下次防错措施
快速定位
# 对比数据库最新记录时间与当前 CST 时间,差 8h 即为时区问题
ssh news "cd /www/news && venv/bin/python3 -c '
from datetime import datetime, timedelta
from modules.wechat.wechat_crawler import SyncSessionLocal
from src.core.models import WechatArticle
db = SyncSessionLocal()
a = db.query(WechatArticle).order_by(WechatArticle.id.desc()).first()
delta = datetime.now() - (a.created_at + timedelta(hours=8))
print(f\"db={a.created_at} cst={a.created_at+timedelta(hours=8)} 距今={delta}\")
db.close()
'"
流程改进
- 新增模块前确认数据库驱动的时区行为(SQLite 默认 UTC,MySQL 看
@@time_zone配置) - 慎用
server_default自动时间戳,改用 Pythondatetime.now()显式设置可避免时区不一致 - Code Review 时核对页面列绑定字段与列标题的一致性
- 新增含时间字段的 API 时,检查是否已做 UTC→CST 转换
巡检脚本
#!/bin/bash
# 检查所有使用 CURRENT_TIMESTAMP 的表,最新记录时间是否在 8 小时偏移内
ssh news "
cd /www/news && venv/bin/python3 -c '
from datetime import datetime, timedelta
from modules.wechat.wechat_crawler import SyncSessionLocal
tables = [\"web_sites\", \"cards\", \"web_news\", \"wechat_accounts\", \"wechat_articles\", \"favorites\", \"categories\"]
db = SyncSessionLocal()
for t in tables:
r = db.execute(text(f\"SELECT id, created_at FROM {t} ORDER BY id DESC LIMIT 1\")).fetchone()
if r:
utc = r[1]
cst = utc + timedelta(hours=8)
delta = datetime.now() - cst
print(f\"{t:20} id={r[0]:5} UTC={utc} CST={cst} 距今={delta}\")
db.close()
'
复发记录 — 2026-07-01
复发原因:MySQL 迁移后 UTC+8 偏移变为双重偏移
项目从 SQLite 迁移到 MySQL(commit d99084c)。MySQL 的 CURRENT_TIMESTAMP(@@time_zone = SYSTEM = CST)已直接返回中国时间,但步骤 3 中添加的 timedelta(hours=8) 偏移未被移除,导致所有时间字段多加了 8 小时(例如实际入库时间凌晨 3:00 CST,显示为 11:00)。
新的解决方法
移除所有 timedelta(hours=8) 偏移(5 个文件):
| 文件 | 行 | 修改 |
|---|---|---|
modules/wechat/routes.py |
545 | (item.created_at + timedelta(...)) → item.created_at |
src/api/favorites.py |
71 | 同上 |
src/service/card_service.py |
61 | 同上 |
modules/web-crawler/crawlers/scanner.py |
203-204 | 同上(created_at + updated_at) |
设置 MySQL 显式时区 + 数据库连接 init_command:
-- 服务器 MySQL
SET GLOBAL time_zone = '+08:00';
# src/core/database.py — 确保每个连接使用 CST
_TZ_INIT = {"init_command": "SET time_zone = '+08:00'"}
engine = create_async_engine(settings.DB_URL, connect_args=_TZ_INIT, ...)
sync_engine = create_engine(settings.DB_URL_SYNC, connect_args=_TZ_INIT, ...)
新防错措施
# 验证 MySQL 时区是否为 +08:00
mysql -u root -p -e "SELECT @@global.time_zone, @@session.time_zone, NOW()"
# 验证 API 返回时间与数据库一致(不应再加偏移)
ssh news "cd /www/news && venv/bin/python3 -c '
from datetime import datetime
from src.core.database import sync_engine, SyncSessionLocal
from sqlalchemy import text
with sync_engine.connect() as conn:
r = conn.execute(text(\"SELECT NOW()\")).fetchone()
print(f\"MySQL NOW: {r[0]}\")
from src.core.models import WechatArticle
db = SyncSessionLocal()
a = db.query(WechatArticle).order_by(WechatArticle.id.desc()).first()
print(f\"Latest created_at (raw): {a.created_at}\")
print(f\"NOW - created_at = {datetime.now() - a.created_at}\")
db.close()
'"
Git 提交
d99084c— 迁移到 MySQL(引入问题)bb48719— 修复时区双重偏移