Files
xiaoxia-saas/packages/domain/duplication.py
T
灵应 ee8c60a143 perf(duplication): 查重模块代码优化 — 修复6个代码质量问题
1. 修复 hamming_distance 不等长哈希处理(hex() 前导零丢失)
2. 集成颜色直方图到 check_duplicate(此前计算但未使用,浪费 CPU)
3. check_duplicate 改为返回最佳匹配而非首个匹配
4. 修复仓库删除顺序(先删片段再删记录,防止孤儿数据)
5. 域模型添加 can_retry()/reset_for_retry(),仅 failed 状态允许重试
6. 列表接口暴露 offset/limit 分页参数
2026-07-01 14:48:15 +08:00

140 lines
4.4 KiB
Python

"""查重记录领域实体。"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
@dataclass(slots=True)
class DuplicateSegment:
"""重复片段 — 描述上传视频中的一段与已有视频的匹配关系。"""
id: str
source_start: float
source_end: float
matched_video_id: str
matched_video_name: str
matched_start: float
matched_end: float
similarity: float # 0-100
@classmethod
def create(
cls,
source_start: float,
source_end: float,
matched_video_id: str,
matched_video_name: str,
matched_start: float,
matched_end: float,
similarity: float,
) -> "DuplicateSegment":
if source_start < 0 or source_end <= source_start:
raise ValueError("invalid source segment range")
if matched_start < 0 or matched_end <= matched_start:
raise ValueError("invalid matched segment range")
if not 0 <= similarity <= 100:
raise ValueError("similarity must be between 0 and 100")
return cls(
id=uuid4().hex,
source_start=source_start,
source_end=source_end,
matched_video_id=matched_video_id,
matched_video_name=matched_video_name,
matched_start=matched_start,
matched_end=matched_end,
similarity=similarity,
)
@dataclass(slots=True)
class DuplicationRecord:
"""查重记录 — 一次视频查重请求的完整生命周期。"""
id: str
user_id: str
filename: str
file_size: int
storage_key: str # OSS 对象键
duration_seconds: float = 0.0
status: str = "pending" # pending / processing / completed / failed
duplicate_rate: float | None = None # 0-100
duplicate_count: int = 0
video_fingerprint: dict[str, Any] | None = None
error_message: str = ""
segments: list[DuplicateSegment] = field(default_factory=list)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
user_id: str,
filename: str,
file_size: int,
storage_key: str,
*,
duration_seconds: float = 0.0,
) -> "DuplicationRecord":
if not user_id.strip():
raise ValueError("user_id cannot be empty")
if not filename.strip():
raise ValueError("filename cannot be empty")
if file_size <= 0:
raise ValueError("file_size must be positive")
return cls(
id=uuid4().hex,
user_id=user_id.strip(),
filename=filename.strip(),
file_size=file_size,
storage_key=storage_key,
duration_seconds=duration_seconds,
)
def mark_processing(self) -> None:
self.status = "processing"
self.updated_at = datetime.now(timezone.utc)
def mark_completed(self, duplicate_rate: float, duplicate_count: int, segments: list[DuplicateSegment]) -> None:
if not 0 <= duplicate_rate <= 100:
raise ValueError("duplicate_rate must be between 0 and 100")
self.status = "completed"
self.duplicate_rate = duplicate_rate
self.duplicate_count = duplicate_count
self.segments = segments
self.updated_at = datetime.now(timezone.utc)
def mark_failed(self, error_message: str) -> None:
self.status = "failed"
self.error_message = error_message
self.updated_at = datetime.now(timezone.utc)
def can_retry(self) -> bool:
"""
判断是否可重试。
仅 failed 状态的记录允许重试,防止误操作已完成的记录。
Returns:
True 表示可以重试
"""
return self.status == "failed"
def reset_for_retry(self) -> None:
"""
重置记录以重新查重。
清空查重结果和错误信息,状态回到 pending。
调用前应先通过 can_retry() 确认状态合法。
"""
self.status = "pending"
self.duplicate_rate = None
self.duplicate_count = 0
self.error_message = ""
self.segments = []
self.video_fingerprint = None
self.updated_at = datetime.now(timezone.utc)