docs: 添加项目开发规范文档
- 添加项目概览文档 - 添加前端开发规范 - 添加后端开发规范 - 添加数据库设计规范 - 添加UI/UX设计规范 - 添加开发指南 - 为项目提供完整的开发标准和最佳实践
This commit is contained in:
950
.promptx/backend-coding-standards.md
Normal file
950
.promptx/backend-coding-standards.md
Normal file
@@ -0,0 +1,950 @@
|
||||
# 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最佳实践
|
||||
- [ ] 输入验证是否完整
|
||||
- [ ] 是否存在安全漏洞
|
||||
Reference in New Issue
Block a user