951 lines
25 KiB
Markdown
951 lines
25 KiB
Markdown
# MixVideo 后端开发规范 (Rust/Tauri)
|
|
|
|
## 技术栈规范
|
|
|
|
### 核心技术
|
|
- **Tauri 2.0**: 跨平台桌面应用框架
|
|
- **Rust 1.70+**: 系统编程语言
|
|
- **SQLite**: 嵌入式数据库 (WAL模式)
|
|
- **Tokio**: 异步运行时
|
|
- **Serde**: 序列化/反序列化
|
|
- **anyhow/thiserror**: 错误处理
|
|
|
|
### 依赖管理
|
|
```toml
|
|
[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)
|
|
```rust
|
|
// 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)
|
|
```rust
|
|
// src/data/mod.rs
|
|
pub mod models;
|
|
pub mod repositories;
|
|
|
|
// 数据访问层职责:
|
|
// - 数据模型定义
|
|
// - 数据库操作封装
|
|
// - 查询优化
|
|
// - 事务管理
|
|
```
|
|
|
|
### 3. 业务逻辑层 (Business)
|
|
```rust
|
|
// src/business/mod.rs
|
|
pub mod services;
|
|
pub mod errors;
|
|
|
|
// 业务逻辑层职责:
|
|
// - 核心业务逻辑
|
|
// - 业务规则验证
|
|
// - 工作流编排
|
|
// - 领域模型
|
|
```
|
|
|
|
### 4. 表现层 (Presentation)
|
|
```rust
|
|
// src/presentation/mod.rs
|
|
pub mod commands;
|
|
|
|
// 表现层职责:
|
|
// - Tauri命令定义
|
|
// - 参数验证
|
|
// - 响应格式化
|
|
// - 错误转换
|
|
```
|
|
|
|
## 代码组织规范
|
|
|
|
### 模块结构
|
|
```rust
|
|
// 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`)
|
|
|
|
## 数据模型规范
|
|
|
|
### 实体模型设计
|
|
```rust
|
|
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. **测试**: 为模型编写单元测试
|
|
|
|
## 仓库模式规范
|
|
|
|
### 仓库接口设计
|
|
```rust
|
|
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查询性能
|
|
|
|
## 业务服务规范
|
|
|
|
### 服务层设计
|
|
```rust
|
|
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命令规范
|
|
|
|
### 命令定义
|
|
```rust
|
|
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. **文档**: 详细的命令文档
|
|
|
|
## 错误处理规范
|
|
|
|
### 错误类型定义
|
|
```rust
|
|
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 },
|
|
}
|
|
```
|
|
|
|
### 错误处理模式
|
|
```rust
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 测试规范
|
|
|
|
### 单元测试模板
|
|
```rust
|
|
#[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("名称不能为空"));
|
|
}
|
|
}
|
|
```
|
|
|
|
### 集成测试
|
|
```rust
|
|
#[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();
|
|
}
|
|
}
|
|
```
|
|
|
|
## 性能优化规范
|
|
|
|
### 数据库优化
|
|
```rust
|
|
// 使用连接池
|
|
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(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### 内存优化
|
|
```rust
|
|
// 使用流式处理大量数据
|
|
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(())
|
|
}
|
|
```
|
|
|
|
## 日志和监控规范
|
|
|
|
### 结构化日志
|
|
```rust
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 性能监控
|
|
```rust
|
|
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
|
|
}
|
|
}
|
|
```
|
|
|
|
## 安全规范
|
|
|
|
### 输入验证
|
|
```rust
|
|
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注入防护
|
|
```rust
|
|
// ✅ 使用参数化查询
|
|
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配置
|
|
```toml
|
|
# Cargo.toml
|
|
[lints.clippy]
|
|
all = "warn"
|
|
pedantic = "warn"
|
|
nursery = "warn"
|
|
cargo = "warn"
|
|
|
|
# 允许的lint
|
|
too_many_arguments = "allow"
|
|
module_name_repetitions = "allow"
|
|
```
|
|
|
|
### 代码审查清单
|
|
- [ ] 是否遵循四层架构设计
|
|
- [ ] 错误处理是否完整
|
|
- [ ] 是否有适当的日志记录
|
|
- [ ] 数据库操作是否优化
|
|
- [ ] 是否有相应的测试覆盖
|
|
- [ ] 文档注释是否完整
|
|
- [ ] 是否符合Rust最佳实践
|
|
- [ ] 输入验证是否完整
|
|
- [ ] 是否存在安全漏洞
|