1212import shutil
1313import zipfile
1414from dataclasses import dataclass , field
15- from datetime import datetime
15+ from datetime import datetime , timezone
1616from pathlib import Path
1717from typing import TYPE_CHECKING , Any
1818
@@ -452,7 +452,7 @@ async def _clear_main_db(self) -> None:
452452 await session .execute (delete (model_class ))
453453 logger .debug (f"已清空表 { table_name } " )
454454 except Exception as e :
455- logger . warning (f"清空表 { table_name } 失败: { e } " )
455+ raise RuntimeError (f"清空表 { table_name } 失败: { e } " ) from e
456456
457457 async def _clear_kb_data (self ) -> None :
458458 """清空知识库数据"""
@@ -494,9 +494,18 @@ async def _import_main_database(
494494 if not model_class :
495495 logger .warning (f"未知的表: { table_name } " )
496496 continue
497+ normalized_rows = rows
498+ if table_name == "platform_stats" :
499+ normalized_rows , duplicate_count = (
500+ self ._merge_platform_stats_rows (rows )
501+ )
502+ if duplicate_count > 0 :
503+ logger .warning (
504+ f"检测到 platform_stats 重复键 { duplicate_count } 条,已在导入前聚合"
505+ )
497506
498507 count = 0
499- for row in rows :
508+ for row in normalized_rows :
500509 try :
501510 # 转换 datetime 字符串为 datetime 对象
502511 row = self ._convert_datetime_fields (row , model_class )
@@ -511,6 +520,86 @@ async def _import_main_database(
511520
512521 return imported
513522
523+ def _merge_platform_stats_rows (
524+ self , rows : list [dict [str , Any ]]
525+ ) -> tuple [list [dict [str , Any ]], int ]:
526+ merged : dict [tuple [str , str , str ], dict [str , Any ]] = {}
527+ timestamp_cache : dict [str , str ] = {}
528+ invalid_count_warned = 0
529+ invalid_count_warn_limit = 5
530+ duplicate_count = 0
531+ for row in rows :
532+ raw_timestamp = row .get ("timestamp" )
533+ if isinstance (raw_timestamp , str ):
534+ normalized_timestamp = timestamp_cache .get (raw_timestamp )
535+ if normalized_timestamp is None :
536+ normalized_timestamp = self ._normalize_platform_stats_timestamp (
537+ raw_timestamp
538+ )
539+ timestamp_cache [raw_timestamp ] = normalized_timestamp
540+ else :
541+ normalized_timestamp = self ._normalize_platform_stats_timestamp (
542+ raw_timestamp
543+ )
544+ key = (
545+ normalized_timestamp ,
546+ str (row .get ("platform_id" )),
547+ str (row .get ("platform_type" )),
548+ )
549+ existing = merged .get (key )
550+ if existing is None :
551+ merged [key ] = dict (row )
552+ continue
553+ duplicate_count += 1
554+ existing_raw_count = existing .get ("count" , 0 )
555+ try :
556+ existing_count = int (existing_raw_count )
557+ except (TypeError , ValueError ):
558+ existing_count = 0
559+ if invalid_count_warned < invalid_count_warn_limit :
560+ logger .warning (
561+ "platform_stats count 非法,已按 0 处理: "
562+ f"value={ existing_raw_count !r} , key={ key } "
563+ )
564+ invalid_count_warned += 1
565+
566+ incoming_raw_count = row .get ("count" , 0 )
567+ try :
568+ incoming_count = int (incoming_raw_count )
569+ except (TypeError , ValueError ):
570+ incoming_count = 0
571+ if invalid_count_warned < invalid_count_warn_limit :
572+ logger .warning (
573+ "platform_stats count 非法,已按 0 处理: "
574+ f"value={ incoming_raw_count !r} , key={ key } "
575+ )
576+ invalid_count_warned += 1
577+ existing ["count" ] = existing_count + incoming_count
578+ return list (merged .values ()), duplicate_count
579+
580+ def _normalize_platform_stats_timestamp (self , value : Any ) -> str :
581+ if isinstance (value , datetime ):
582+ dt = value
583+ if dt .tzinfo is not None :
584+ dt = dt .astimezone (timezone .utc )
585+ return dt .isoformat ()
586+ if isinstance (value , str ):
587+ timestamp = value .strip ()
588+ if not timestamp :
589+ return ""
590+ if timestamp .endswith ("Z" ):
591+ timestamp = f"{ timestamp [:- 1 ]} +00:00"
592+ try :
593+ dt = datetime .fromisoformat (timestamp )
594+ if dt .tzinfo is not None :
595+ dt = dt .astimezone (timezone .utc )
596+ return dt .isoformat ()
597+ except ValueError :
598+ return value .strip ()
599+ if value is None :
600+ return ""
601+ return str (value )
602+
514603 async def _import_knowledge_bases (
515604 self ,
516605 zf : zipfile .ZipFile ,
0 commit comments