fix(ui): 日期维度改用视频录制时间 - 前端事件时间轴/统计/关注事件与后端 db_layer 统一按 event_start_time(文件名解析) 分组排序,空值回退 processed_at;修复全量重分析导致处理时间(8/21)把 8/15-8/20 视频全堆同一天、与文件名对不上的问题
This commit is contained in:
@@ -103,32 +103,35 @@ def upsert_sync_videos(rows: List[Dict]) -> int:
|
|||||||
def get_sync_videos(limit=15, offset=0, date_filter=None) -> List[Dict]:
|
def get_sync_videos(limit=15, offset=0, date_filter=None) -> List[Dict]:
|
||||||
"""获取视频会话列表(已完成优先),支持日期筛选与分页。
|
"""获取视频会话列表(已完成优先),支持日期筛选与分页。
|
||||||
|
|
||||||
排序按 COALESCE(processed_at, updated_at, created_at) 降序。
|
排序/日期维度按视频实际录制时间(event_start_time,文件名解析),
|
||||||
date_filter 形如 '2026-08-21',匹配 processed_at 前缀。
|
为空回退 processed_at/updated_at/created_at。
|
||||||
|
date_filter 形如 '2026-08-21'。
|
||||||
"""
|
"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
cur = conn.cursor(pymysql.cursors.DictCursor)
|
cur = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
# 录制时间优先,回退分析时间
|
||||||
|
date_expr = "COALESCE(NULLIF(event_start_time,''), processed_at, updated_at, created_at)"
|
||||||
if date_filter:
|
if date_filter:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT id, filename, camera_name, event_start_time, status,
|
f"""SELECT id, filename, camera_name, event_start_time, status,
|
||||||
summary_json, events_json, people_json, compute_provider,
|
summary_json, events_json, people_json, compute_provider,
|
||||||
processed_at, updated_at,
|
processed_at, updated_at,
|
||||||
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count
|
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count
|
||||||
FROM sync_videos
|
FROM sync_videos
|
||||||
WHERE status='done' AND processed_at LIKE %s
|
WHERE status='done' AND {date_expr} LIKE %s
|
||||||
ORDER BY COALESCE(processed_at, updated_at, created_at) DESC
|
ORDER BY {date_expr} DESC
|
||||||
LIMIT %s OFFSET %s""",
|
LIMIT %s OFFSET %s""",
|
||||||
(f'{date_filter}%', limit, offset))
|
(f'{date_filter}%', limit, offset))
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT id, filename, camera_name, event_start_time, status,
|
f"""SELECT id, filename, camera_name, event_start_time, status,
|
||||||
summary_json, events_json, people_json, compute_provider,
|
summary_json, events_json, people_json, compute_provider,
|
||||||
processed_at, updated_at,
|
processed_at, updated_at,
|
||||||
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count
|
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count
|
||||||
FROM sync_videos
|
FROM sync_videos
|
||||||
WHERE status='done'
|
WHERE status='done'
|
||||||
ORDER BY COALESCE(processed_at, updated_at, created_at) DESC
|
ORDER BY {date_expr} DESC
|
||||||
LIMIT %s OFFSET %s""",
|
LIMIT %s OFFSET %s""",
|
||||||
(limit, offset))
|
(limit, offset))
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
@@ -201,7 +204,8 @@ def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]:
|
|||||||
"""问答上下文:某人在某天的事件。
|
"""问答上下文:某人在某天的事件。
|
||||||
|
|
||||||
说明: Oracle 事件 ts 为视频内相对时间点(如 00:01:23),不是绝对日期,
|
说明: Oracle 事件 ts 为视频内相对时间点(如 00:01:23),不是绝对日期,
|
||||||
因此按所属视频的 processed_at 日期过滤,再按 person_list_json 命中人名。
|
因此按所属视频的录制日期(event_start_time,回退 processed_at)过滤,
|
||||||
|
再按 person_list_json 命中人名。
|
||||||
person 可为真名或抽象标签(Oracle 回灌上下文用真名,但历史标签也保留)。
|
person 可为真名或抽象标签(Oracle 回灌上下文用真名,但历史标签也保留)。
|
||||||
"""
|
"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -212,10 +216,10 @@ def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]:
|
|||||||
v.camera_name, v.filename, v.event_start_time, v.processed_at
|
v.camera_name, v.filename, v.event_start_time, v.processed_at
|
||||||
FROM sync_events e
|
FROM sync_events e
|
||||||
JOIN sync_videos v ON e.video_id = v.id
|
JOIN sync_videos v ON e.video_id = v.id
|
||||||
WHERE v.processed_at LIKE %s
|
WHERE COALESCE(NULLIF(v.event_start_time,''), v.processed_at) LIKE %s
|
||||||
AND e.person_list_json IS NOT NULL
|
AND e.person_list_json IS NOT NULL
|
||||||
AND JSON_CONTAINS(e.person_list_json, JSON_QUOTE(%s), '$')
|
AND JSON_CONTAINS(e.person_list_json, JSON_QUOTE(%s), '$')
|
||||||
ORDER BY v.processed_at ASC, e.ts ASC""",
|
ORDER BY COALESCE(NULLIF(v.event_start_time,''), v.processed_at) ASC, e.ts ASC""",
|
||||||
(f'{date_str}%', person))
|
(f'{date_str}%', person))
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
finally:
|
finally:
|
||||||
@@ -414,18 +418,19 @@ def get_sync_stats(date_str: str = None) -> Dict:
|
|||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
cur = conn.cursor(pymysql.cursors.DictCursor)
|
cur = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
# 视频/事件/关注数
|
# 视频/事件/关注数(日期维度=录制时间 event_start_time,回退 processed_at)
|
||||||
|
_D = "COALESCE(NULLIF(sv.event_start_time,''), sv.processed_at)"
|
||||||
if date_str:
|
if date_str:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
COUNT(*) AS videos,
|
COUNT(*) AS videos,
|
||||||
(SELECT COUNT(*) FROM sync_events se
|
(SELECT COUNT(*) FROM sync_events se
|
||||||
JOIN sync_videos sv ON se.video_id=sv.id
|
JOIN sync_videos sv ON se.video_id=sv.id
|
||||||
WHERE sv.processed_at LIKE %s) AS events,
|
WHERE {d} LIKE %s) AS events,
|
||||||
(SELECT COALESCE(SUM(se.is_attention_event),0) FROM sync_events se
|
(SELECT COALESCE(SUM(se.is_attention_event),0) FROM sync_events se
|
||||||
JOIN sync_videos sv ON se.video_id=sv.id
|
JOIN sync_videos sv ON se.video_id=sv.id
|
||||||
WHERE sv.processed_at LIKE %s) AS attention
|
WHERE {d} LIKE %s) AS attention
|
||||||
FROM sync_videos sv WHERE sv.processed_at LIKE %s""",
|
FROM sync_videos sv WHERE {d} LIKE %s""".format(d=_D),
|
||||||
(f'{date_str}%', f'{date_str}%', f'{date_str}%'))
|
(f'{date_str}%', f'{date_str}%', f'{date_str}%'))
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|||||||
@@ -379,19 +379,22 @@ if page == "🕒 事件时间轴":
|
|||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
# 日期维度统一用视频实际录制时间 event_start_time(文件名解析),
|
||||||
|
# 为空时回退 processed_at(分析时间),避免"处理时间=8/21"把旧视频全堆同一天
|
||||||
|
_DATE_EXPR = "COALESCE(NULLIF(event_start_time,''), processed_at)"
|
||||||
if date_str:
|
if date_str:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT COUNT(*) v FROM sync_videos WHERE status='done' AND processed_at LIKE %s",
|
f"SELECT COUNT(*) v FROM sync_videos WHERE status='done' AND {_DATE_EXPR} LIKE %s",
|
||||||
(f'{date_str}%',))
|
(f'{date_str}%',))
|
||||||
v = cursor.fetchone().get('v', 0)
|
v = cursor.fetchone().get('v', 0)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT COUNT(*) e FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id WHERE sv.processed_at LIKE %s",
|
f"SELECT COUNT(*) e FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id WHERE {_DATE_EXPR} LIKE %s",
|
||||||
(f'{date_str}%',))
|
(f'{date_str}%',))
|
||||||
e = cursor.fetchone().get('e', 0)
|
e = cursor.fetchone().get('e', 0)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT COALESCE(SUM(se.is_attention_event),0) AS att
|
f"""SELECT COALESCE(SUM(se.is_attention_event),0) AS att
|
||||||
FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id
|
FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id
|
||||||
WHERE sv.processed_at LIKE %s""", (f'{date_str}%',))
|
WHERE {_DATE_EXPR} LIKE %s""", (f'{date_str}%',))
|
||||||
att = cursor.fetchone().get('att', 0)
|
att = cursor.fetchone().get('att', 0)
|
||||||
else:
|
else:
|
||||||
cursor.execute("SELECT COUNT(*) v FROM sync_videos WHERE status='done'")
|
cursor.execute("SELECT COUNT(*) v FROM sync_videos WHERE status='done'")
|
||||||
@@ -435,22 +438,22 @@ if page == "🕒 事件时间轴":
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
if date_str:
|
if date_str:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT id, filename, camera_name, event_start_time, summary_json,
|
f"""SELECT id, filename, camera_name, event_start_time, summary_json,
|
||||||
compute_provider, processed_at,
|
compute_provider, processed_at,
|
||||||
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count
|
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count
|
||||||
FROM sync_videos
|
FROM sync_videos
|
||||||
WHERE status='done' AND processed_at LIKE %s
|
WHERE status='done' AND {_DATE_EXPR} LIKE %s
|
||||||
ORDER BY COALESCE(processed_at, updated_at, created_at) DESC
|
ORDER BY {_DATE_EXPR} DESC
|
||||||
LIMIT %s OFFSET %s""",
|
LIMIT %s OFFSET %s""",
|
||||||
(f'{date_str}%', page_size, offset))
|
(f'{date_str}%', page_size, offset))
|
||||||
else:
|
else:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT id, filename, camera_name, event_start_time, summary_json,
|
f"""SELECT id, filename, camera_name, event_start_time, summary_json,
|
||||||
compute_provider, processed_at,
|
compute_provider, processed_at,
|
||||||
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count
|
(SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count
|
||||||
FROM sync_videos
|
FROM sync_videos
|
||||||
WHERE status='done'
|
WHERE status='done'
|
||||||
ORDER BY COALESCE(processed_at, updated_at, created_at) DESC
|
ORDER BY {_DATE_EXPR} DESC
|
||||||
LIMIT %s OFFSET %s""",
|
LIMIT %s OFFSET %s""",
|
||||||
(page_size, offset))
|
(page_size, offset))
|
||||||
videos = cursor.fetchall()
|
videos = cursor.fetchall()
|
||||||
@@ -479,10 +482,10 @@ if page == "🕒 事件时间轴":
|
|||||||
f'margin-bottom:10px;">视频会话 · {len(videos)} 条</div>',
|
f'margin-bottom:10px;">视频会话 · {len(videos)} 条</div>',
|
||||||
unsafe_allow_html=True)
|
unsafe_allow_html=True)
|
||||||
for vid in videos:
|
for vid in videos:
|
||||||
proc = vid.get('processed_at')
|
# 日期标签/时间用视频实际录制时间(event_start_time),为空回退处理时间
|
||||||
ptime = parse_ts(proc)
|
ev_time = parse_ts(vid.get('event_start_time')) or parse_ts(vid.get('processed_at'))
|
||||||
time_label = ptime.strftime('%m-%d %H:%M') if ptime else '--:--'
|
time_label = ev_time.strftime('%m-%d %H:%M') if ev_time else '--:--'
|
||||||
date_label = ptime.strftime('%Y-%m-%d') if ptime else ''
|
date_label = ev_time.strftime('%Y-%m-%d') if ev_time else ''
|
||||||
summary = (vid.get('summary_json') or '').strip()
|
summary = (vid.get('summary_json') or '').strip()
|
||||||
summary_short = summary if len(summary) <= 28 else summary[:28] + '…'
|
summary_short = summary if len(summary) <= 28 else summary[:28] + '…'
|
||||||
if not summary_short:
|
if not summary_short:
|
||||||
@@ -515,13 +518,15 @@ if page == "🕒 事件时间轴":
|
|||||||
|
|
||||||
with col_detail:
|
with col_detail:
|
||||||
vid = next(v for v in videos if v['id'] == selected_id)
|
vid = next(v for v in videos if v['id'] == selected_id)
|
||||||
proc = parse_ts(vid.get('processed_at'))
|
|
||||||
start = parse_ts(vid.get('event_start_time'))
|
start = parse_ts(vid.get('event_start_time'))
|
||||||
|
proc = parse_ts(vid.get('processed_at'))
|
||||||
|
# 主时间 = 录制时间(event_start_time),回退分析时间(processed_at)
|
||||||
|
main_t = start or proc
|
||||||
range_str = ''
|
range_str = ''
|
||||||
if proc:
|
if main_t:
|
||||||
range_str = proc.strftime('%Y-%m-%d %H:%M:%S')
|
range_str = main_t.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
if start:
|
if start and proc and (proc - start).total_seconds() > 60:
|
||||||
range_str += f"(起始 {start.strftime('%H:%M:%S')})"
|
range_str += f"(分析于 {proc.strftime('%m-%d %H:%M')})"
|
||||||
|
|
||||||
provider = vid.get('compute_provider') or ''
|
provider = vid.get('compute_provider') or ''
|
||||||
model_badges = ''
|
model_badges = ''
|
||||||
@@ -899,17 +904,18 @@ elif page == "📈 统计图表":
|
|||||||
st.markdown('<div style="font-size:15px;font-weight:700;color:#f1f5f9;'
|
st.markdown('<div style="font-size:15px;font-weight:700;color:#f1f5f9;'
|
||||||
'margin:22px 0 14px 0;">关注事件统计</div>', unsafe_allow_html=True)
|
'margin:22px 0 14px 0;">关注事件统计</div>', unsafe_allow_html=True)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT v.processed_at, e.person_list_json
|
"""SELECT COALESCE(NULLIF(v.event_start_time,''), v.processed_at) AS ev_date,
|
||||||
|
e.person_list_json
|
||||||
FROM sync_events e JOIN sync_videos v ON e.video_id=v.id
|
FROM sync_events e JOIN sync_videos v ON e.video_id=v.id
|
||||||
WHERE e.is_attention_event = 1
|
WHERE e.is_attention_event = 1
|
||||||
ORDER BY v.processed_at DESC""")
|
ORDER BY ev_date DESC""")
|
||||||
attention = cursor.fetchall()
|
attention = cursor.fetchall()
|
||||||
if attention:
|
if attention:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
rows = []
|
rows = []
|
||||||
for a in attention:
|
for a in attention:
|
||||||
persons = parse_persons(a['person_list_json'])
|
persons = parse_persons(a['person_list_json'])
|
||||||
ptime = parse_ts(a['processed_at'])
|
ptime = parse_ts(a['ev_date'])
|
||||||
rows.append({
|
rows.append({
|
||||||
"日期": ptime.strftime('%Y-%m-%d') if ptime else '?',
|
"日期": ptime.strftime('%Y-%m-%d') if ptime else '?',
|
||||||
"人物": ','.join(sorted(persons)) or '无人',
|
"人物": ','.join(sorted(persons)) or '无人',
|
||||||
|
|||||||
Reference in New Issue
Block a user