fix(sync): 修复 Oracle id 重排导致的 sync_videos/sync_people 1062
Oracle 库重建/重排后 videos/people 的 id 跳变(2303 段→3408/4600+ 段), 旧 upsert 按 Oracle id 作主键插入时与已存在同 filename/label 行在 UNIQUE 键 上二次冲突报 1062,整批中止、游标不推进。 - upsert_sync_videos: 改为以 filename 业务键去重,命中就地 UPDATE 保留 NAS 原 id(防 sync_events/model_calls/identity_map 的 video_id 外键失效),Oracle id 落 oracle_id 列溯源;未命中插入优先用 Oracle id 对齐子表引用,主键冲突回退自增 - upsert_sync_people: 同模式,以 label 业务键去重(people.id 无外键引用) - ddl.sql: sync_videos/sync_people 的 id 改为 NAS 本地自增主键 + 新增 oracle_id 列 - NAS 现网已迁移(加列/回填/改自增)并重启验证:单批补拉 videos+348 events+1017 people+9 model_calls+500 identity_map+238,游标 09-02 13:55:21 → 09-03 08:51:11
This commit is contained in:
@@ -56,7 +56,16 @@ def get_conn():
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def upsert_sync_videos(rows: List[Dict]) -> int:
|
def upsert_sync_videos(rows: List[Dict]) -> int:
|
||||||
"""批量 upsert Oracle 传来的 videos 增量。rows 为 Oracle 端 dict 列表。"""
|
"""批量 upsert Oracle 传来的 videos 增量。rows 为 Oracle 端 dict 列表。
|
||||||
|
|
||||||
|
v2 修复 (2026-09-03):不再以 Oracle 的 videos.id 作为 NAS 主键。Oracle 库重建/
|
||||||
|
重排后 id 会复用/跳变(实测 2303 段跳到 3408/4600+ 段),旧写法按 id 插入时与
|
||||||
|
已存在的同 filename 行在 UNIQUE filename 上二次冲突报 1062,整批中止、游标不推进。
|
||||||
|
现改为:以 filename 为业务唯一键去重,命中则就地 UPDATE(保留 NAS 原 id,防止
|
||||||
|
sync_events/model_calls/identity_map 的 video_id 外键失效),Oracle id 仅落
|
||||||
|
oracle_id 列溯源;未命中则插入(优先用 Oracle id 作主键以对齐子表引用,主键冲突时
|
||||||
|
回退本地自增,避免 1062)。
|
||||||
|
"""
|
||||||
if not rows:
|
if not rows:
|
||||||
return 0
|
return 0
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -64,35 +73,57 @@ def upsert_sync_videos(rows: List[Dict]) -> int:
|
|||||||
try:
|
try:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
for r in rows:
|
for r in rows:
|
||||||
cur.execute(
|
fn = r.get('filename')
|
||||||
"""INSERT INTO sync_videos
|
oracle_id = r.get('id')
|
||||||
(id, drive_file_id, filename, camera_name, duration_sec,
|
cur.execute("SELECT id FROM sync_videos WHERE filename=%s", (fn,))
|
||||||
event_start_time, status, summary_json, events_json,
|
row = cur.fetchone()
|
||||||
people_json, compute_provider, created_at, updated_at,
|
if row:
|
||||||
processed_at, synced_at)
|
# 命中业务键:就地更新,保留原 NAS id(外键不失效)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())
|
cur.execute(
|
||||||
ON DUPLICATE KEY UPDATE
|
"""UPDATE sync_videos SET
|
||||||
drive_file_id=VALUES(drive_file_id),
|
oracle_id=%s, drive_file_id=%s, camera_name=%s,
|
||||||
filename=VALUES(filename),
|
duration_sec=%s, event_start_time=%s, status=%s,
|
||||||
camera_name=VALUES(camera_name),
|
summary_json=%s, events_json=%s, people_json=%s,
|
||||||
duration_sec=VALUES(duration_sec),
|
compute_provider=%s, created_at=%s, updated_at=%s,
|
||||||
event_start_time=VALUES(event_start_time),
|
processed_at=%s, synced_at=NOW()
|
||||||
status=VALUES(status),
|
WHERE id=%s""",
|
||||||
summary_json=VALUES(summary_json),
|
(oracle_id, r.get('drive_file_id'), r.get('camera_name'),
|
||||||
events_json=VALUES(events_json),
|
r.get('duration_sec') or 0, r.get('event_start_time'),
|
||||||
people_json=VALUES(people_json),
|
r.get('status'), r.get('summary_json'), r.get('events_json'),
|
||||||
compute_provider=VALUES(compute_provider),
|
r.get('people_json'), r.get('compute_provider'),
|
||||||
created_at=VALUES(created_at),
|
r.get('created_at'), r.get('updated_at'),
|
||||||
updated_at=VALUES(updated_at),
|
r.get('processed_at'), row[0]))
|
||||||
processed_at=VALUES(processed_at),
|
else:
|
||||||
synced_at=NOW()""",
|
# 未命中:新文件。优先用 Oracle id 作主键(对齐子表 video_id 引用)
|
||||||
(r.get('id'), r.get('drive_file_id'), r.get('filename'),
|
try:
|
||||||
r.get('camera_name'), r.get('duration_sec') or 0,
|
cur.execute(
|
||||||
r.get('event_start_time'), r.get('status'),
|
"""INSERT INTO sync_videos
|
||||||
r.get('summary_json'), r.get('events_json'), r.get('people_json'),
|
(id, oracle_id, filename, camera_name, duration_sec,
|
||||||
r.get('compute_provider'), r.get('created_at'),
|
event_start_time, status, summary_json, events_json,
|
||||||
r.get('updated_at'), r.get('processed_at'))
|
people_json, compute_provider, created_at, updated_at,
|
||||||
)
|
processed_at, synced_at)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())""",
|
||||||
|
(oracle_id, oracle_id, fn, r.get('camera_name'),
|
||||||
|
r.get('duration_sec') or 0, r.get('event_start_time'),
|
||||||
|
r.get('status'), r.get('summary_json'), r.get('events_json'),
|
||||||
|
r.get('people_json'), r.get('compute_provider'),
|
||||||
|
r.get('created_at'), r.get('updated_at'),
|
||||||
|
r.get('processed_at')))
|
||||||
|
except Exception:
|
||||||
|
# 主键 id 被复用(极端情况):回退本地自增,避免 1062
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT INTO sync_videos
|
||||||
|
(oracle_id, filename, camera_name, duration_sec,
|
||||||
|
event_start_time, status, summary_json, events_json,
|
||||||
|
people_json, compute_provider, created_at, updated_at,
|
||||||
|
processed_at, synced_at)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())""",
|
||||||
|
(oracle_id, fn, r.get('camera_name'),
|
||||||
|
r.get('duration_sec') or 0, r.get('event_start_time'),
|
||||||
|
r.get('status'), r.get('summary_json'), r.get('events_json'),
|
||||||
|
r.get('people_json'), r.get('compute_provider'),
|
||||||
|
r.get('created_at'), r.get('updated_at'),
|
||||||
|
r.get('processed_at')))
|
||||||
n += 1
|
n += 1
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return n
|
return n
|
||||||
@@ -304,7 +335,12 @@ def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def upsert_sync_people(rows: List[Dict]) -> int:
|
def upsert_sync_people(rows: List[Dict]) -> int:
|
||||||
"""批量 upsert Oracle 传来的 people 增量。"""
|
"""批量 upsert Oracle 传来的 people 增量。
|
||||||
|
|
||||||
|
v2 修复 (2026-09-03):同 videos,以 label 为业务唯一键去重,命中就地 UPDATE
|
||||||
|
保留 NAS 原 id(people.id 无外键引用,仅作展示排序),Oracle id 落 oracle_id 列
|
||||||
|
溯源;未命中插入(优先 Oracle id,主键冲突回退自增,避免 1062)。
|
||||||
|
"""
|
||||||
if not rows:
|
if not rows:
|
||||||
return 0
|
return 0
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -312,25 +348,44 @@ def upsert_sync_people(rows: List[Dict]) -> int:
|
|||||||
try:
|
try:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
for r in rows:
|
for r in rows:
|
||||||
cur.execute(
|
label = r.get('label')
|
||||||
"""INSERT INTO sync_people
|
oracle_id = r.get('id')
|
||||||
(id, label, canonical_name, first_seen, appearances,
|
cur.execute("SELECT id FROM sync_people WHERE label=%s", (label,))
|
||||||
source, features_json, display_uid, updated_at, synced_at)
|
row = cur.fetchone()
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())
|
if row:
|
||||||
ON DUPLICATE KEY UPDATE
|
cur.execute(
|
||||||
label=VALUES(label),
|
"""UPDATE sync_people SET
|
||||||
canonical_name=VALUES(canonical_name),
|
oracle_id=%s, canonical_name=%s, first_seen=%s,
|
||||||
first_seen=VALUES(first_seen),
|
appearances=%s, source=%s, features_json=%s,
|
||||||
appearances=VALUES(appearances),
|
display_uid=%s, updated_at=%s, synced_at=NOW()
|
||||||
source=VALUES(source),
|
WHERE id=%s""",
|
||||||
features_json=VALUES(features_json),
|
(oracle_id, r.get('canonical_name'), r.get('first_seen'),
|
||||||
display_uid=VALUES(display_uid),
|
r.get('appearances') or 0, r.get('source'),
|
||||||
updated_at=VALUES(updated_at),
|
r.get('features_json'), r.get('display_uid'),
|
||||||
synced_at=NOW()""",
|
r.get('updated_at'), row[0]))
|
||||||
(r.get('id'), r.get('label'), r.get('canonical_name'),
|
else:
|
||||||
r.get('first_seen'), r.get('appearances') or 0,
|
try:
|
||||||
r.get('source'), r.get('features_json'),
|
cur.execute(
|
||||||
r.get('display_uid'), r.get('updated_at')))
|
"""INSERT INTO sync_people
|
||||||
|
(id, oracle_id, label, canonical_name, first_seen,
|
||||||
|
appearances, source, features_json, display_uid,
|
||||||
|
updated_at, synced_at)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())""",
|
||||||
|
(oracle_id, oracle_id, label, r.get('canonical_name'),
|
||||||
|
r.get('first_seen'), r.get('appearances') or 0,
|
||||||
|
r.get('source'), r.get('features_json'),
|
||||||
|
r.get('display_uid'), r.get('updated_at')))
|
||||||
|
except Exception:
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT INTO sync_people
|
||||||
|
(oracle_id, label, canonical_name, first_seen,
|
||||||
|
appearances, source, features_json, display_uid,
|
||||||
|
updated_at, synced_at)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW())""",
|
||||||
|
(oracle_id, label, r.get('canonical_name'),
|
||||||
|
r.get('first_seen'), r.get('appearances') or 0,
|
||||||
|
r.get('source'), r.get('features_json'),
|
||||||
|
r.get('display_uid'), r.get('updated_at')))
|
||||||
n += 1
|
n += 1
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return n
|
return n
|
||||||
|
|||||||
@@ -128,8 +128,13 @@ CREATE TABLE IF NOT EXISTS family_members (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
-- 7.1 视频会话表(Oracle videos 镜像)
|
-- 7.1 视频会话表(Oracle videos 镜像)
|
||||||
|
-- v2 (2026-09-03):id 改为 NAS 本地自增主键(不再直接等于 Oracle id,因 Oracle 库
|
||||||
|
-- 重建/重排会复用 id,导致 filename UNIQUE 二次冲突 1062);Oracle 的 videos.id 仅
|
||||||
|
-- 落 oracle_id 列溯源。业务唯一键仍为 filename。子表 video_id 引用仍用 Oracle id
|
||||||
|
-- (新视频插入时 id=oracle_id;旧视频原地更新保留原 id,外键不失效)。
|
||||||
CREATE TABLE IF NOT EXISTS sync_videos (
|
CREATE TABLE IF NOT EXISTS sync_videos (
|
||||||
id INT PRIMARY KEY COMMENT 'Oracle videos.id',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'NAS 本地自增主键',
|
||||||
|
oracle_id INT COMMENT 'Oracle videos.id(仅溯源参考)',
|
||||||
drive_file_id VARCHAR(255) COMMENT 'Google 硬盘文件 ID',
|
drive_file_id VARCHAR(255) COMMENT 'Google 硬盘文件 ID',
|
||||||
filename VARCHAR(500) NOT NULL UNIQUE COMMENT '视频文件名(唯一)',
|
filename VARCHAR(500) NOT NULL UNIQUE COMMENT '视频文件名(唯一)',
|
||||||
camera_name VARCHAR(50) COMMENT '摄像头名称/位置',
|
camera_name VARCHAR(50) COMMENT '摄像头名称/位置',
|
||||||
@@ -164,8 +169,11 @@ CREATE TABLE IF NOT EXISTS sync_events (
|
|||||||
) ENGINE=InnoDB COMMENT='甲骨文事件镜像表';
|
) ENGINE=InnoDB COMMENT='甲骨文事件镜像表';
|
||||||
|
|
||||||
-- 7.3 人物规范表(Oracle people 镜像)
|
-- 7.3 人物规范表(Oracle people 镜像)
|
||||||
|
-- v2 (2026-09-03):同 7.1,id 改为 NAS 本地自增主键,Oracle people.id 落 oracle_id
|
||||||
|
-- 列溯源;业务唯一键仍为 label。people.id 无外键引用,仅作展示排序。
|
||||||
CREATE TABLE IF NOT EXISTS sync_people (
|
CREATE TABLE IF NOT EXISTS sync_people (
|
||||||
id INT PRIMARY KEY COMMENT 'Oracle people.id',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'NAS 本地自增主键',
|
||||||
|
oracle_id INT COMMENT 'Oracle people.id(仅溯源参考)',
|
||||||
label VARCHAR(100) NOT NULL UNIQUE COMMENT '抽象标识,如"人物A"',
|
label VARCHAR(100) NOT NULL UNIQUE COMMENT '抽象标识,如"人物A"',
|
||||||
canonical_name VARCHAR(100) COMMENT '规范名(用户命名或 LLM 合并),NULL 表示未命名',
|
canonical_name VARCHAR(100) COMMENT '规范名(用户命名或 LLM 合并),NULL 表示未命名',
|
||||||
first_seen VARCHAR(32) COMMENT '首次出现时间',
|
first_seen VARCHAR(32) COMMENT '首次出现时间',
|
||||||
|
|||||||
Reference in New Issue
Block a user