问题:成功分类数显示18446744073709552(接近u64最大值) 原因:错误地用分类记录总数减去失败任务数,导致负数转u64溢出 修复内容: - 扩展ClassificationStats结构体,添加分类记录状态统计 - 改进数据库查询,正确统计各状态的分类记录数 - 修正ai_analysis_log_service中的统计计算逻辑 - 添加数值安全检查,防止负数转无符号整数 - 更新前端类型定义保持一致性 现在统计数据来源一致且逻辑正确: - 任务统计来自video_classification_tasks表 - 分类记录统计来自video_classification_records表
342 lines
9.3 KiB
Rust
342 lines
9.3 KiB
Rust
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
|
||
}
|
||
}
|