Files
mixvideo-v2/apps/desktop/src-tauri/src/data/models/video_classification.rs
imeepos 82b62a4b0f fix: 修复AI分类统计数值溢出问题
问题:成功分类数显示18446744073709552(接近u64最大值)
原因:错误地用分类记录总数减去失败任务数,导致负数转u64溢出

修复内容:
- 扩展ClassificationStats结构体,添加分类记录状态统计
- 改进数据库查询,正确统计各状态的分类记录数
- 修正ai_analysis_log_service中的统计计算逻辑
- 添加数值安全检查,防止负数转无符号整数
- 更新前端类型定义保持一致性

现在统计数据来源一致且逻辑正确:
- 任务统计来自video_classification_tasks表
- 分类记录统计来自video_classification_records表
2025-07-18 14:27:25 +08:00

342 lines
9.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// AI视频分类记录数据模型
/// 遵循 Tauri 开发规范的数据模型设计原则
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoClassificationRecord {
/// 分类记录唯一标识符
pub id: String,
/// 素材片段ID
pub segment_id: String,
/// 素材ID
pub material_id: String,
/// 项目ID
pub project_id: String,
/// 分类结果
pub category: String,
/// 置信度 (0.0 - 1.0)
pub confidence: f64,
/// 分类理由
pub reasoning: String,
/// 关键特征
pub features: Vec<String>,
/// 商品匹配度
pub product_match: bool,
/// 质量评分 (0.0 - 1.0)
pub quality_score: f64,
/// Gemini文件URI
pub gemini_file_uri: Option<String>,
/// 原始AI响应
pub raw_response: Option<String>,
/// 分类状态
pub status: ClassificationStatus,
/// 错误信息
pub error_message: Option<String>,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 更新时间
pub updated_at: DateTime<Utc>,
}
/// AI视频分类任务数据模型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoClassificationTask {
/// 任务唯一标识符
pub id: String,
/// 素材片段ID
pub segment_id: String,
/// 素材ID
pub material_id: String,
/// 项目ID
pub project_id: String,
/// 视频文件路径
pub video_file_path: String,
/// 任务状态
pub status: TaskStatus,
/// 任务优先级
pub priority: i32,
/// 重试次数
pub retry_count: i32,
/// 最大重试次数
pub max_retries: i32,
/// Gemini文件URI上传后获得
pub gemini_file_uri: Option<String>,
/// 使用的提示词
pub prompt_text: Option<String>,
/// 错误信息
pub error_message: Option<String>,
/// 开始处理时间
pub started_at: Option<DateTime<Utc>>,
/// 完成时间
pub completed_at: Option<DateTime<Utc>>,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 更新时间
pub updated_at: DateTime<Utc>,
}
/// 分类状态枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ClassificationStatus {
/// 已分类
Classified,
/// 分类失败
Failed,
/// 需要人工审核
NeedsReview,
}
/// 任务状态枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TaskStatus {
/// 等待处理
Pending,
/// 上传中
Uploading,
/// 分析中
Analyzing,
/// 已完成
Completed,
/// 失败
Failed,
/// 已取消
Cancelled,
}
/// 批量分类请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchClassificationRequest {
/// 素材ID
pub material_id: String,
/// 项目ID
pub project_id: String,
/// 是否覆盖已有分类
pub overwrite_existing: bool,
/// 任务优先级
pub priority: Option<i32>,
}
/// 项目一键分类请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectBatchClassificationRequest {
/// 项目ID
pub project_id: String,
/// 是否覆盖已有分类
pub overwrite_existing: Option<bool>,
/// 要处理的素材类型(可选,默认只处理视频)
pub material_types: Option<Vec<crate::data::models::material::MaterialType>>,
/// 任务优先级
pub priority: Option<i32>,
}
/// 项目一键分类响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectBatchClassificationResponse {
/// 项目中总素材数
pub total_materials: u32,
/// 符合条件的素材数
pub eligible_materials: u32,
/// 创建的任务数
pub created_tasks: u32,
/// 创建的任务ID列表
pub task_ids: Vec<String>,
/// 跳过的素材ID列表已有分类
pub skipped_materials: Vec<String>,
}
/// 分类任务查询参数
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClassificationTaskQuery {
/// 项目ID过滤
pub project_id: Option<String>,
/// 素材ID过滤
pub material_id: Option<String>,
/// 状态过滤
pub status: Option<TaskStatus>,
/// 分页大小
pub limit: Option<i32>,
/// 分页偏移
pub offset: Option<i32>,
}
/// 分类统计信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassificationStats {
/// 总任务数
pub total_tasks: i32,
/// 等待中任务数
pub pending_tasks: i32,
/// 处理中任务数
pub processing_tasks: i32,
/// 已完成任务数
pub completed_tasks: i32,
/// 失败任务数
pub failed_tasks: i32,
/// 总分类记录数
pub total_classifications: i32,
/// 成功分类记录数
pub successful_classifications: i32,
/// 失败分类记录数
pub failed_classifications: i32,
/// 需要审核记录数
pub needs_review_classifications: i32,
/// 平均置信度
pub average_confidence: f64,
/// 平均质量评分
pub average_quality_score: f64,
}
/// Gemini API响应结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeminiClassificationResponse {
/// 分类结果
pub category: String,
/// 置信度
pub confidence: f64,
/// 分类理由
pub reasoning: String,
/// 关键特征
pub features: Vec<String>,
/// 商品匹配度
pub product_match: bool,
/// 质量评分
pub quality_score: f64,
}
impl VideoClassificationRecord {
/// 创建新的分类记录
pub fn new(
segment_id: String,
material_id: String,
project_id: String,
gemini_response: GeminiClassificationResponse,
gemini_file_uri: Option<String>,
raw_response: Option<String>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
segment_id,
material_id,
project_id,
category: gemini_response.category,
confidence: gemini_response.confidence,
reasoning: gemini_response.reasoning,
features: gemini_response.features,
product_match: gemini_response.product_match,
quality_score: gemini_response.quality_score,
gemini_file_uri,
raw_response,
status: ClassificationStatus::Classified,
error_message: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
/// 标记为失败
pub fn mark_as_failed(&mut self, error_message: String) {
self.status = ClassificationStatus::Failed;
self.error_message = Some(error_message);
self.updated_at = Utc::now();
}
/// 标记为需要审核
pub fn mark_as_needs_review(&mut self, reason: String) {
self.status = ClassificationStatus::NeedsReview;
self.error_message = Some(reason);
self.updated_at = Utc::now();
}
/// 是否需要人工审核(低置信度或低质量)
pub fn needs_review(&self) -> bool {
self.confidence < 0.7 || self.quality_score < 0.6
}
}
impl VideoClassificationTask {
/// 创建新的分类任务
pub fn new(
segment_id: String,
material_id: String,
project_id: String,
video_file_path: String,
priority: Option<i32>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
segment_id,
material_id,
project_id,
video_file_path,
status: TaskStatus::Pending,
priority: priority.unwrap_or(0),
retry_count: 0,
max_retries: 3,
gemini_file_uri: None,
prompt_text: None,
error_message: None,
started_at: None,
completed_at: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
/// 开始处理任务
pub fn start_processing(&mut self) {
self.status = TaskStatus::Uploading;
self.started_at = Some(Utc::now());
self.updated_at = Utc::now();
}
/// 设置为分析状态
pub fn set_analyzing(&mut self, gemini_file_uri: String, prompt_text: String) {
self.status = TaskStatus::Analyzing;
self.gemini_file_uri = Some(gemini_file_uri);
self.prompt_text = Some(prompt_text);
self.updated_at = Utc::now();
}
/// 完成任务
pub fn complete(&mut self) {
self.status = TaskStatus::Completed;
self.completed_at = Some(Utc::now());
self.updated_at = Utc::now();
}
/// 任务失败
pub fn fail(&mut self, error_message: String) {
self.status = TaskStatus::Failed;
self.error_message = Some(error_message);
self.retry_count += 1;
self.updated_at = Utc::now();
}
/// 是否可以重试
pub fn can_retry(&self) -> bool {
self.retry_count < self.max_retries && self.status == TaskStatus::Failed
}
/// 重置为等待状态(用于重试)
pub fn reset_for_retry(&mut self) {
self.status = TaskStatus::Pending;
self.error_message = None;
self.started_at = None;
self.completed_at = None;
self.updated_at = Utc::now();
}
}
impl Default for ClassificationStatus {
fn default() -> Self {
ClassificationStatus::Classified
}
}
impl Default for TaskStatus {
fn default() -> Self {
TaskStatus::Pending
}
}