25 KiB
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>,
}
模型设计原则
- 不可变性: 优先使用不可变字段
- 验证: 内置数据验证方法
- 序列化: 支持JSON序列化
- 文档: 详细的文档注释
- 测试: 为模型编写单元测试
仓库模式规范
仓库接口设计
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)
}
}
仓库设计原则
- 接口分离: 定义清晰的仓库接口
- 异步操作: 所有数据库操作都是异步的
- 错误处理: 统一的错误处理机制
- 连接管理: 合理使用数据库连接
- 查询优化: 优化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(())
}
}
服务设计原则
- 业务封装: 封装复杂的业务逻辑
- 验证: 完整的业务规则验证
- 事务: 合理使用数据库事务
- 错误处理: 详细的错误信息
- 测试: 完整的单元测试覆盖
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())
}
命令设计原则
- 参数验证: 严格验证输入参数
- 错误转换: 将内部错误转换为用户友好的消息
- 状态管理: 正确使用Tauri状态管理
- 异步处理: 所有命令都是异步的
- 文档: 详细的命令文档
错误处理规范
错误类型定义
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最佳实践
- 输入验证是否完整
- 是否存在安全漏洞