mixvideo-v2/.promptx/backend-coding-standards.md

25 KiB

MixVideo 后端开发规范 (Rust/Tauri)

技术栈规范

核心技术

  • Tauri 2.0: 跨平台桌面应用框架
  • Rust 1.70+: 系统编程语言
  • SQLite: 嵌入式数据库 (WAL模式)
  • Tokio: 异步运行时
  • Serde: 序列化/反序列化
  • anyhow/thiserror: 错误处理

依赖管理

[workspace.dependencies]
# Tauri 相关
tauri = { version = "2.0", features = ["api-all"] }
tauri-build = { version = "2.0", features = [] }

# 异步和并发
tokio = { version = "1.0", features = ["full"] }
futures = "0.3"
async-trait = "0.1"

# 序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 错误处理
anyhow = "1.0"
thiserror = "1.0"

# 数据库
rusqlite = { version = "0.29", features = ["bundled", "chrono"] }
chrono = { version = "0.4", features = ["serde"] }

# 日志
tracing = "0.1"
tracing-subscriber = "0.3"

四层架构设计

1. 基础设施层 (Infrastructure)

// src/infrastructure/mod.rs
pub mod database;
pub mod logging;
pub mod file_system;
pub mod gemini_service;
pub mod ffmpeg;
pub mod event_bus;
pub mod performance;

// 基础设施层职责:
// - 数据库连接管理
// - 外部服务集成
// - 文件系统操作
// - 日志和监控
// - 事件总线

2. 数据访问层 (Data)

// src/data/mod.rs
pub mod models;
pub mod repositories;

// 数据访问层职责:
// - 数据模型定义
// - 数据库操作封装
// - 查询优化
// - 事务管理

3. 业务逻辑层 (Business)

// src/business/mod.rs
pub mod services;
pub mod errors;

// 业务逻辑层职责:
// - 核心业务逻辑
// - 业务规则验证
// - 工作流编排
// - 领域模型

4. 表现层 (Presentation)

// src/presentation/mod.rs
pub mod commands;

// 表现层职责:
// - Tauri命令定义
// - 参数验证
// - 响应格式化
// - 错误转换

代码组织规范

模块结构

// lib.rs - 库入口
extern crate lazy_static;

// 四层架构模块
pub mod infrastructure;
pub mod data;
pub mod business;
pub mod presentation;
pub mod services;

// 应用配置
pub mod app_state;
pub mod config;

use app_state::AppState;
use presentation::commands;
use tauri::Manager;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .manage(AppState::new())
        .invoke_handler(tauri::generate_handler![
            // 注册所有命令
            commands::project_commands::create_project,
            commands::material_commands::get_all_materials,
            // ... 其他命令
        ])
        .setup(|app| {
            // 应用初始化逻辑
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

文件命名规范

  • 模块文件: snake_case (如 material_service.rs)
  • 结构体: PascalCase (如 MaterialRepository)
  • 函数: snake_case (如 create_material)
  • 常量: SCREAMING_SNAKE_CASE (如 DEFAULT_PAGE_SIZE)

数据模型规范

实体模型设计

use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

/// 素材实体模型
/// 遵循 Tauri 开发规范的数据模型设计原则
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Material {
    pub id: String,
    pub name: String,
    pub path: String,
    pub material_type: MaterialType,
    pub size: u64,
    pub duration: Option<u64>,
    pub project_id: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub is_active: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MaterialType {
    Video,
    Audio,
    Image,
}

impl Material {
    /// 创建新的素材实例
    pub fn new(request: CreateMaterialRequest) -> Self {
        let now = Utc::now();
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name: request.name,
            path: request.path,
            material_type: request.material_type,
            size: request.size,
            duration: request.duration,
            project_id: request.project_id,
            created_at: now,
            updated_at: now,
            is_active: true,
        }
    }
    
    /// 验证素材数据
    pub fn validate(&self) -> Result<(), String> {
        if self.name.trim().is_empty() {
            return Err("素材名称不能为空".to_string());
        }
        
        if self.path.trim().is_empty() {
            return Err("素材路径不能为空".to_string());
        }
        
        Ok(())
    }
    
    /// 更新素材信息
    pub fn update(&mut self, request: UpdateMaterialRequest) {
        if let Some(name) = request.name {
            self.name = name;
        }
        
        if let Some(material_type) = request.material_type {
            self.material_type = material_type;
        }
        
        self.updated_at = Utc::now();
    }
}

/// 创建素材请求
#[derive(Debug, Deserialize)]
pub struct CreateMaterialRequest {
    pub name: String,
    pub path: String,
    pub material_type: MaterialType,
    pub size: u64,
    pub duration: Option<u64>,
    pub project_id: String,
}

/// 更新素材请求
#[derive(Debug, Deserialize)]
pub struct UpdateMaterialRequest {
    pub name: Option<String>,
    pub material_type: Option<MaterialType>,
}

模型设计原则

  1. 不可变性: 优先使用不可变字段
  2. 验证: 内置数据验证方法
  3. 序列化: 支持JSON序列化
  4. 文档: 详细的文档注释
  5. 测试: 为模型编写单元测试

仓库模式规范

仓库接口设计

use async_trait::async_trait;
use anyhow::Result;

/// 素材仓库接口
/// 定义素材数据访问的抽象接口
#[async_trait]
pub trait MaterialRepositoryTrait {
    async fn create(&self, material: &Material) -> Result<()>;
    async fn get_by_id(&self, id: &str) -> Result<Option<Material>>;
    async fn get_all(&self) -> Result<Vec<Material>>;
    async fn get_by_project_id(&self, project_id: &str) -> Result<Vec<Material>>;
    async fn update(&self, material: &Material) -> Result<()>;
    async fn delete(&self, id: &str) -> Result<()>;
    async fn search(&self, query: &MaterialQueryParams) -> Result<Vec<Material>>;
}

/// 素材仓库实现
/// 遵循 Tauri 开发规范的仓库模式
pub struct MaterialRepository {
    database: Arc<Database>,
}

impl MaterialRepository {
    pub fn new(database: Arc<Database>) -> Self {
        Self { database }
    }
}

#[async_trait]
impl MaterialRepositoryTrait for MaterialRepository {
    async fn create(&self, material: &Material) -> Result<()> {
        let conn = self.database.get_connection().await?;
        
        conn.execute(
            r#"
            INSERT INTO materials (
                id, name, path, material_type, size, duration,
                project_id, created_at, updated_at, is_active
            ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
            "#,
            params![
                material.id,
                material.name,
                material.path,
                serde_json::to_string(&material.material_type)?,
                material.size,
                material.duration,
                material.project_id,
                material.created_at.to_rfc3339(),
                material.updated_at.to_rfc3339(),
                material.is_active,
            ],
        )?;
        
        Ok(())
    }
    
    async fn get_by_id(&self, id: &str) -> Result<Option<Material>> {
        let conn = self.database.get_read_connection().await?;
        
        let mut stmt = conn.prepare(
            r#"
            SELECT id, name, path, material_type, size, duration,
                   project_id, created_at, updated_at, is_active
            FROM materials
            WHERE id = ?1 AND is_active = 1
            "#
        )?;
        
        let material = stmt.query_row(params![id], |row| {
            Ok(Material {
                id: row.get(0)?,
                name: row.get(1)?,
                path: row.get(2)?,
                material_type: serde_json::from_str(&row.get::<_, String>(3)?)
                    .map_err(|e| rusqlite::Error::InvalidColumnType(3, "material_type".to_string(), rusqlite::types::Type::Text))?,
                size: row.get(4)?,
                duration: row.get(5)?,
                project_id: row.get(6)?,
                created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(7)?)
                    .map_err(|e| rusqlite::Error::InvalidColumnType(7, "created_at".to_string(), rusqlite::types::Type::Text))?
                    .with_timezone(&Utc),
                updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(8)?)
                    .map_err(|e| rusqlite::Error::InvalidColumnType(8, "updated_at".to_string(), rusqlite::types::Type::Text))?
                    .with_timezone(&Utc),
                is_active: row.get(9)?,
            })
        }).optional()?;
        
        Ok(material)
    }
}

仓库设计原则

  1. 接口分离: 定义清晰的仓库接口
  2. 异步操作: 所有数据库操作都是异步的
  3. 错误处理: 统一的错误处理机制
  4. 连接管理: 合理使用数据库连接
  5. 查询优化: 优化SQL查询性能

业务服务规范

服务层设计

use anyhow::{anyhow, Result};
use crate::data::repositories::material_repository::MaterialRepository;
use crate::data::models::material::{Material, CreateMaterialRequest, UpdateMaterialRequest};

/// 素材管理服务
/// 遵循 Tauri 开发规范的业务服务设计
pub struct MaterialService;

impl MaterialService {
    /// 创建素材
    pub async fn create_material(
        repository: &MaterialRepository,
        request: CreateMaterialRequest,
    ) -> Result<Material> {
        // 业务验证
        if request.name.trim().is_empty() {
            return Err(anyhow!("素材名称不能为空"));
        }
        
        if !std::path::Path::new(&request.path).exists() {
            return Err(anyhow!("素材文件不存在"));
        }
        
        // 创建素材实例
        let material = Material::new(request);
        
        // 验证素材数据
        material.validate()
            .map_err(|e| anyhow!("素材验证失败: {}", e))?;
        
        // 保存到数据库
        repository.create(&material).await
            .map_err(|e| anyhow!("创建素材失败: {}", e))?;
        
        Ok(material)
    }
    
    /// 获取素材详情
    pub async fn get_material_by_id(
        repository: &MaterialRepository,
        id: &str,
    ) -> Result<Option<Material>> {
        repository.get_by_id(id).await
            .map_err(|e| anyhow!("获取素材详情失败: {}", e))
    }
    
    /// 更新素材
    pub async fn update_material(
        repository: &MaterialRepository,
        id: &str,
        request: UpdateMaterialRequest,
    ) -> Result<Material> {
        // 获取现有素材
        let mut material = repository.get_by_id(id).await?
            .ok_or_else(|| anyhow!("素材不存在"))?;
        
        // 更新素材信息
        material.update(request);
        
        // 验证更新后的数据
        material.validate()
            .map_err(|e| anyhow!("素材验证失败: {}", e))?;
        
        // 保存更新
        repository.update(&material).await
            .map_err(|e| anyhow!("更新素材失败: {}", e))?;
        
        Ok(material)
    }
    
    /// 删除素材
    pub async fn delete_material(
        repository: &MaterialRepository,
        id: &str,
    ) -> Result<()> {
        // 检查素材是否存在
        let material = repository.get_by_id(id).await?
            .ok_or_else(|| anyhow!("素材不存在"))?;
        
        // 执行删除
        repository.delete(id).await
            .map_err(|e| anyhow!("删除素材失败: {}", e))?;
        
        Ok(())
    }
}

服务设计原则

  1. 业务封装: 封装复杂的业务逻辑
  2. 验证: 完整的业务规则验证
  3. 事务: 合理使用数据库事务
  4. 错误处理: 详细的错误信息
  5. 测试: 完整的单元测试覆盖

Tauri命令规范

命令定义

use tauri::{command, State};
use crate::app_state::AppState;
use crate::business::services::material_service::MaterialService;
use crate::data::models::material::{Material, CreateMaterialRequest, UpdateMaterialRequest};

/// 创建素材命令
/// 遵循 Tauri 开发规范的命令设计模式
#[command]
pub async fn create_material(
    state: State<'_, AppState>,
    request: CreateMaterialRequest,
) -> Result<Material, String> {
    let repository_guard = state.get_material_repository()
        .map_err(|e| format!("获取素材仓库失败: {}", e))?;
    
    let repository = repository_guard.as_ref()
        .ok_or("素材仓库未初始化")?;

    MaterialService::create_material(repository, request)
        .await
        .map_err(|e| e.to_string())
}

/// 获取所有素材命令
#[command]
pub async fn get_all_materials(
    state: State<'_, AppState>,
) -> Result<Vec<Material>, String> {
    let repository_guard = state.get_material_repository()
        .map_err(|e| format!("获取素材仓库失败: {}", e))?;
    
    let repository = repository_guard.as_ref()
        .ok_or("素材仓库未初始化")?;

    repository.get_all()
        .await
        .map_err(|e| e.to_string())
}

/// 根据ID获取素材命令
#[command]
pub async fn get_material_by_id(
    state: State<'_, AppState>,
    id: String,
) -> Result<Option<Material>, String> {
    let repository_guard = state.get_material_repository()
        .map_err(|e| format!("获取素材仓库失败: {}", e))?;
    
    let repository = repository_guard.as_ref()
        .ok_or("素材仓库未初始化")?;

    MaterialService::get_material_by_id(repository, &id)
        .await
        .map_err(|e| e.to_string())
}

/// 更新素材命令
#[command]
pub async fn update_material(
    state: State<'_, AppState>,
    id: String,
    request: UpdateMaterialRequest,
) -> Result<Material, String> {
    let repository_guard = state.get_material_repository()
        .map_err(|e| format!("获取素材仓库失败: {}", e))?;
    
    let repository = repository_guard.as_ref()
        .ok_or("素材仓库未初始化")?;

    MaterialService::update_material(repository, &id, request)
        .await
        .map_err(|e| e.to_string())
}

/// 删除素材命令
#[command]
pub async fn delete_material(
    state: State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    let repository_guard = state.get_material_repository()
        .map_err(|e| format!("获取素材仓库失败: {}", e))?;
    
    let repository = repository_guard.as_ref()
        .ok_or("素材仓库未初始化")?;

    MaterialService::delete_material(repository, &id)
        .await
        .map_err(|e| e.to_string())
}

命令设计原则

  1. 参数验证: 严格验证输入参数
  2. 错误转换: 将内部错误转换为用户友好的消息
  3. 状态管理: 正确使用Tauri状态管理
  4. 异步处理: 所有命令都是异步的
  5. 文档: 详细的命令文档

错误处理规范

错误类型定义

use thiserror::Error;

/// 业务错误类型
#[derive(Error, Debug)]
pub enum BusinessError {
    #[error("验证错误: {message}")]
    ValidationError { message: String },
    
    #[error("资源不存在: {resource}")]
    NotFound { resource: String },
    
    #[error("权限不足: {operation}")]
    PermissionDenied { operation: String },
    
    #[error("业务规则冲突: {rule}")]
    BusinessRuleViolation { rule: String },
    
    #[error("外部服务错误: {service} - {message}")]
    ExternalServiceError { service: String, message: String },
}

/// 数据库错误类型
#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("连接错误: {0}")]
    ConnectionError(#[from] rusqlite::Error),
    
    #[error("事务错误: {message}")]
    TransactionError { message: String },
    
    #[error("查询错误: {query}")]
    QueryError { query: String },
    
    #[error("数据完整性错误: {constraint}")]
    IntegrityError { constraint: String },
}

错误处理模式

use anyhow::{Context, Result};

/// 带上下文的错误处理
pub async fn process_material(id: &str) -> Result<Material> {
    let material = repository.get_by_id(id)
        .await
        .with_context(|| format!("获取素材失败: {}", id))?
        .ok_or_else(|| anyhow!("素材不存在: {}", id))?;
    
    validate_material(&material)
        .with_context(|| "素材验证失败")?;
    
    Ok(material)
}

/// 错误转换
impl From<BusinessError> for String {
    fn from(error: BusinessError) -> Self {
        match error {
            BusinessError::ValidationError { message } => {
                format!("输入验证失败: {}", message)
            }
            BusinessError::NotFound { resource } => {
                format!("找不到资源: {}", resource)
            }
            _ => error.to_string(),
        }
    }
}

测试规范

单元测试模板

#[cfg(test)]
mod tests {
    use super::*;
    use crate::infrastructure::database::Database;
    use std::sync::Arc;
    
    async fn setup_test_database() -> Arc<Database> {
        let db = Database::new_in_memory().unwrap();
        db.migrate().await.unwrap();
        Arc::new(db)
    }
    
    #[tokio::test]
    async fn test_create_material_success() {
        // Arrange
        let db = setup_test_database().await;
        let repository = MaterialRepository::new(db);
        let request = CreateMaterialRequest {
            name: "Test Material".to_string(),
            path: "/test/path".to_string(),
            material_type: MaterialType::Video,
            size: 1024,
            duration: Some(60),
            project_id: "project-1".to_string(),
        };
        
        // Act
        let result = MaterialService::create_material(&repository, request).await;
        
        // Assert
        assert!(result.is_ok());
        let material = result.unwrap();
        assert_eq!(material.name, "Test Material");
        assert_eq!(material.size, 1024);
    }
    
    #[tokio::test]
    async fn test_create_material_validation_error() {
        // Arrange
        let db = setup_test_database().await;
        let repository = MaterialRepository::new(db);
        let request = CreateMaterialRequest {
            name: "".to_string(), // 空名称应该失败
            path: "/test/path".to_string(),
            material_type: MaterialType::Video,
            size: 1024,
            duration: Some(60),
            project_id: "project-1".to_string(),
        };
        
        // Act
        let result = MaterialService::create_material(&repository, request).await;
        
        // Assert
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("名称不能为空"));
    }
}

集成测试

#[cfg(test)]
mod integration_tests {
    use super::*;
    use crate::app_state::AppState;
    use tauri::test::{mock_app, MockRuntime};
    
    #[tokio::test]
    async fn test_material_crud_workflow() {
        // 设置测试应用
        let app = mock_app();
        let state = AppState::new();
        
        // 测试创建
        let create_request = CreateMaterialRequest {
            name: "Integration Test Material".to_string(),
            path: "/test/integration".to_string(),
            material_type: MaterialType::Video,
            size: 2048,
            duration: Some(120),
            project_id: "integration-project".to_string(),
        };
        
        let created = create_material(state.clone(), create_request).await.unwrap();
        
        // 测试读取
        let retrieved = get_material_by_id(state.clone(), created.id.clone()).await.unwrap();
        assert!(retrieved.is_some());
        
        // 测试更新
        let update_request = UpdateMaterialRequest {
            name: Some("Updated Material".to_string()),
            material_type: None,
        };
        
        let updated = update_material(state.clone(), created.id.clone(), update_request).await.unwrap();
        assert_eq!(updated.name, "Updated Material");
        
        // 测试删除
        delete_material(state.clone(), created.id).await.unwrap();
    }
}

性能优化规范

数据库优化

// 使用连接池
pub struct Database {
    pool: Arc<ConnectionPool>,
}

impl Database {
    pub async fn get_connection(&self) -> Result<PooledConnection> {
        self.pool.get().await
            .map_err(|e| anyhow!("获取数据库连接失败: {}", e))
    }
    
    // 读写分离
    pub async fn get_read_connection(&self) -> Result<PooledConnection> {
        self.pool.get_read_only().await
            .map_err(|e| anyhow!("获取只读连接失败: {}", e))
    }
}

// 批量操作优化
impl MaterialRepository {
    pub async fn batch_create(&self, materials: &[Material]) -> Result<()> {
        let conn = self.database.get_connection().await?;
        let tx = conn.transaction()?;
        
        for material in materials {
            tx.execute(
                "INSERT INTO materials (...) VALUES (...)",
                // 参数
            )?;
        }
        
        tx.commit()?;
        Ok(())
    }
}

内存优化

// 使用流式处理大量数据
pub async fn process_large_dataset<F>(
    repository: &MaterialRepository,
    processor: F,
) -> Result<()>
where
    F: Fn(&Material) -> Result<()>,
{
    let mut offset = 0;
    const BATCH_SIZE: usize = 100;
    
    loop {
        let materials = repository.get_batch(offset, BATCH_SIZE).await?;
        if materials.is_empty() {
            break;
        }
        
        for material in materials {
            processor(&material)?;
        }
        
        offset += BATCH_SIZE;
    }
    
    Ok(())
}

日志和监控规范

结构化日志

use tracing::{info, warn, error, debug, instrument};

#[instrument(skip(repository))]
pub async fn create_material(
    repository: &MaterialRepository,
    request: CreateMaterialRequest,
) -> Result<Material> {
    info!(
        material_name = %request.name,
        project_id = %request.project_id,
        "开始创建素材"
    );
    
    let material = Material::new(request);
    
    match repository.create(&material).await {
        Ok(_) => {
            info!(
                material_id = %material.id,
                material_name = %material.name,
                "素材创建成功"
            );
            Ok(material)
        }
        Err(e) => {
            error!(
                error = %e,
                material_name = %material.name,
                "素材创建失败"
            );
            Err(e)
        }
    }
}

性能监控

use std::time::Instant;

pub struct PerformanceMonitor {
    metrics: Arc<Mutex<HashMap<String, Vec<Duration>>>>,
}

impl PerformanceMonitor {
    pub fn record_operation<F, R>(&self, operation: &str, f: F) -> R
    where
        F: FnOnce() -> R,
    {
        let start = Instant::now();
        let result = f();
        let duration = start.elapsed();
        
        let mut metrics = self.metrics.lock().unwrap();
        metrics.entry(operation.to_string())
            .or_insert_with(Vec::new)
            .push(duration);
        
        result
    }
}

安全规范

输入验证

use validator::{Validate, ValidationError};

#[derive(Debug, Deserialize, Validate)]
pub struct CreateMaterialRequest {
    #[validate(length(min = 1, max = 255, message = "名称长度必须在1-255字符之间"))]
    pub name: String,

    #[validate(custom = "validate_file_path")]
    pub path: String,

    #[validate(range(min = 1, message = "文件大小必须大于0"))]
    pub size: u64,
}

fn validate_file_path(path: &str) -> Result<(), ValidationError> {
    if !std::path::Path::new(path).exists() {
        return Err(ValidationError::new("文件不存在"));
    }

    // 防止路径遍历攻击
    if path.contains("..") {
        return Err(ValidationError::new("非法路径"));
    }

    Ok(())
}

SQL注入防护

// ✅ 使用参数化查询
conn.execute(
    "SELECT * FROM materials WHERE name = ?1 AND project_id = ?2",
    params![name, project_id],
)?;

// ❌ 避免字符串拼接
// let sql = format!("SELECT * FROM materials WHERE name = '{}'", name);

代码质量规范

Clippy配置

# Cargo.toml
[lints.clippy]
all = "warn"
pedantic = "warn"
nursery = "warn"
cargo = "warn"

# 允许的lint
too_many_arguments = "allow"
module_name_repetitions = "allow"

代码审查清单

  • 是否遵循四层架构设计
  • 错误处理是否完整
  • 是否有适当的日志记录
  • 数据库操作是否优化
  • 是否有相应的测试覆盖
  • 文档注释是否完整
  • 是否符合Rust最佳实践
  • 输入验证是否完整
  • 是否存在安全漏洞