跳转至

后端-数据库-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 自动时间戳,改用 Python datetime.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 — 修复时区双重偏移