feat(timeline): 事件时间轴支持删除视频会话
三端联动实现(fam-ui -> fam-core -> fam-edge),沿用项目里"NAS 转发写请求 到 Oracle"的既有模式(照抄 identity_correct 那套骨架): - fam-edge:oracle_db.py 新增 delete_video()(删 events+videos 行 + 磁盘上 的运动片段文件;不清理 ss_motion_events 源事件,因为素材一旦处理完就不会 被生产者重新捡起,删片段不会触发重新分割);api_gateway.py 新增 POST /api/oracle/video/delete - fam-core:新增 db_layer.delete_sync_video()(第一个"NAS 直接写自己镜像表" 的函数——增量同步只做 upsert 感知不到删除,不能像纠错那样靠 trigger_now() 拉增量顺带清理)+ oracle_sync.push_video_delete() + ui_api.py 新增 DELETE /api/ui/videos/<id>(先回推 Oracle,成功后才清本地镜像,避免数据 不一致) - fam-ui:Timeline.vue 详情卡片加删除按钮(原生 confirm() 二次确认,项目里 之前没有确认弹窗组件先例);api.js 新增 deleteVideo() 新增 6 个 delete_video 单元测试;已在 Oracle/NAS 用自造测试数据完整跑通端 到端链路(转发成功、两端记录清理、磁盘文件删除、幂等 404),未触碰任何真 实监控数据。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -363,3 +363,64 @@ def test_correct_video_identity_without_prior_mapping_uses_current_name_as_raw_u
|
||||
rows = db._conn.execute(
|
||||
"SELECT person_list_json FROM events WHERE video_id=?", (vid,)).fetchall()
|
||||
assert json.loads(rows[0]["person_list_json"]) == ["爸爸"]
|
||||
|
||||
|
||||
def test_delete_video_removes_video_and_events_rows(tmp_path):
|
||||
db = _db(tmp_path)
|
||||
vid = _seed_video_with_events(db)
|
||||
|
||||
local_path = db.delete_video(vid)
|
||||
|
||||
assert local_path == f"/tmp/motion_1_1000.mp4"
|
||||
assert db._conn.execute("SELECT * FROM videos WHERE id=?", (vid,)).fetchone() is None
|
||||
assert db._conn.execute("SELECT * FROM events WHERE video_id=?", (vid,)).fetchall() == []
|
||||
|
||||
|
||||
def test_delete_video_removes_disk_file(tmp_path):
|
||||
db = _db(tmp_path)
|
||||
clip_path = tmp_path / "motion_9_2000.mp4"
|
||||
clip_path.write_bytes(b"fake mp4 bytes")
|
||||
vid = db.ensure_video("motion_9_2000.mp4", str(clip_path), event_start_time="2026-08-22 10:00:00")
|
||||
db.mark_video_processed(vid, "摘要", [], [], "gemini")
|
||||
|
||||
db.delete_video(vid)
|
||||
|
||||
assert not clip_path.exists()
|
||||
|
||||
|
||||
def test_delete_video_missing_file_on_disk_does_not_raise(tmp_path):
|
||||
"""核心诉求: local_path 指向的文件已经不存在(比如手动清理过)时,删除记录
|
||||
本身不能因为 os.remove 报错而失败——文件缺失不是数据库操作的错误。"""
|
||||
db = _db(tmp_path)
|
||||
vid = db.ensure_video("motion_9_2000.mp4", str(tmp_path / "already_gone.mp4"),
|
||||
event_start_time="2026-08-22 10:00:00")
|
||||
db.mark_video_processed(vid, "摘要", [], [], "gemini")
|
||||
|
||||
local_path = db.delete_video(vid)
|
||||
|
||||
assert local_path == str(tmp_path / "already_gone.mp4")
|
||||
assert db._conn.execute("SELECT * FROM videos WHERE id=?", (vid,)).fetchone() is None
|
||||
|
||||
|
||||
def test_delete_video_nonexistent_returns_none(tmp_path):
|
||||
db = _db(tmp_path)
|
||||
assert db.delete_video(99999) is None
|
||||
|
||||
|
||||
def test_delete_video_does_not_touch_ss_motion_events(tmp_path):
|
||||
"""核心诉求: ss_motion_events 是运动侦测源事件,跟切出来的视频片段生命周期
|
||||
独立,删视频不该连带删掉源事件(否则分割逻辑的幂等判断会被破坏)。"""
|
||||
db = _db(tmp_path)
|
||||
db.record_motion_events([
|
||||
{"event_id": 555, "camera_id": 2, "event_type": 10,
|
||||
"start_time": 1700000000, "duration": 10, "thumbnail_url": ""},
|
||||
])
|
||||
vid = db.ensure_video("motion_555_1700000000.mp4", "/tmp/motion_555_1700000000.mp4",
|
||||
event_start_time="2026-08-22 10:00:00", motion_event_id=555)
|
||||
db.mark_video_processed(vid, "摘要", [], [], "gemini")
|
||||
|
||||
db.delete_video(vid)
|
||||
|
||||
assert db.get_video_by_motion_event_id(555) is None
|
||||
row = db._conn.execute("SELECT * FROM ss_motion_events WHERE event_id=?", (555,)).fetchone()
|
||||
assert row is not None
|
||||
|
||||
Reference in New Issue
Block a user