From 4e53a996297ee838690d98288267aaaa13cea699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 10 Sep 2026 11:54:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(media):=20upsert=5Fmedia=20MySQL=20?= =?UTF-8?q?=E6=82=AC=E7=A9=BA=20new.fetched=5Fat=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BD=95=E9=9F=B3=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=81=9C=E6=9B=B4?= =?UTF-8?q?=E5=A4=9A=E6=97=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第二轮同步审计实测复现(最小用例 0.3s 即炸):upsert_media 的 update_cols 取了全部表列,而 values 只含 11 个业务列——server_default 列 (fetched_at/updated_at)不在 INSERT 列集,MySQL 编译 ON DUPLICATE 时生成 new.fetched_at 悬空引用 → 1054 Unknown column → 每夜 full 的 call_recordings 步骤(录音下载本地化)自生产部署起**每夜静默失败**, 新录音一直没本地化(异常被 SyncRun 吞成 failed 日志,序列继续)。 修:update_cols 只取 values 里实际有的列(对齐 _build_upsert_stmt 的 「只更新实际插入列」原则);updated_at 在冲突重下时刷 now()。 SQLite 分支(excluded 引用宽容)同逻辑修正;722 测试绿。 Co-Authored-By: Claude Code --- yunke-server/app/storage/repos.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/yunke-server/app/storage/repos.py b/yunke-server/app/storage/repos.py index c82c18d..61f4d06 100644 --- a/yunke-server/app/storage/repos.py +++ b/yunke-server/app/storage/repos.py @@ -803,8 +803,13 @@ async def upsert_media( dialect = await _dialect_name(session) pk_cols = _pk_col_names(Media) + # 只更新实际插入的列(2026-09-10 生产修复):此前取全部表列,server_default + # 列(fetched_at/updated_at)不在 values 里 → MySQL 编译出 + # ``new.fetched_at`` 悬空引用 → 每夜 call_recordings 步骤 1054 静默 + # 失败,录音本地化停更多日。 update_cols = [ - c.name for c in Media.__table__.columns if c.name not in pk_cols + c.name for c in Media.__table__.columns + if c.name not in pk_cols and c.name in values ] media_cols = Media.__table__.c @@ -812,7 +817,8 @@ async def upsert_media( """Map each updated column to its conflict-set expression. Volatile download artifacts use ``COALESCE(incoming, existing)``; every - other column is refreshed directly from the incoming row. + other column is refreshed directly from the incoming row. ``updated_at`` + 刷新为 now()(冲突重下时反映最新一次落盘)。 """ set_: dict[str, Any] = {} for name in update_cols: @@ -820,6 +826,7 @@ async def upsert_media( set_[name] = sa.func.coalesce(other_cols[name], media_cols[name]) else: set_[name] = other_cols[name] + set_["updated_at"] = sa.func.now() return set_ if dialect == "sqlite":