第二十章:项目实战——构建一个完整应用

第二十章:项目实战——构建一个完整应用

本章导读:学习了所有基础知识后,是时候将它们整合成一个完整的项目了。本章我们将构建一个"任务管理 CLI 工具"——从需求分析到最终发布,体验完整的 Rust 项目开发流程。这不仅仅是一个练习,更是一次将知识转化为技能的旅程。


📋 20.1 项目规划

🎯 20.1.1 需求分析

我们要构建一个名为 taskr 的命令行任务管理工具:

核心功能

  • 添加任务
  • 列出任务
  • 完成任务
  • 删除任务
  • 持久化存储

进阶功能

  • 优先级
  • 截止日期
  • 标签
  • 搜索过滤

📁 20.1.2 项目结构

taskr/
├── Cargo.toml
├── README.md
├── src/
│   ├── main.rs
│   ├── cli.rs
│   ├── task.rs
│   ├── storage.rs
│   └── error.rs
└── tests/
    └── integration_test.rs

📦 20.2 项目初始化

cargo new taskr
cd taskr
# Cargo.toml
[package]
name = "taskr"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your@email.com>"]
description = "A simple task management CLI tool"
license = "MIT"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
colored = "2"
anyhow = "1"
thiserror = "1"

[dev-dependencies]
tempfile = "3"

🛠️ 20.3 核心模块实现

❌ 20.3.1 错误处理

// src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum TaskError {
    #[error("任务未找到: {0}")]
    NotFound(String),

    #[error("无效的优先级: {0}")]
    InvalidPriority(String),

    #[error("存储错误: {0}")]
    Storage(#[from] std::io::Error),

    #[error("解析错误: {0}")]
    Parse(#[from] serde_json::Error),
}

pub type Result<T> = std::result::Result<T, TaskError>;

📝 20.3.2 任务模型

// src/task.rs
use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize};

/// 任务优先级
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
    Low,
    Medium,
    High,
}

impl Default for Priority {
    fn default() -> Self {
        Self::Medium
    }
}

impl std::fmt::Display for Priority {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Priority::Low => write!(f, "低"),
            Priority::Medium => write!(f, "中"),
            Priority::High => write!(f, "高"),
        }
    }
}

/// 任务状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
    Todo,
    Done,
}

impl Default for Status {
    fn default() -> Self {
        Self::Todo
    }
}

/// 任务
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: String,
    pub title: String,
    pub description: Option<String>,
    pub priority: Priority,
    pub status: Status,
    pub tags: Vec<String>,
    pub due_date: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl Task {
    /// 创建新任务
    pub fn new(title: String) -> Self {
        let now = Utc::now();
        Self {
            id: generate_id(),
            title,
            description: None,
            priority: Priority::default(),
            status: Status::default(),
            tags: Vec::new(),
            due_date: None,
            created_at: now,
            updated_at: now,
        }
    }

    /// 设置描述
    pub fn with_description(mut self, description: String) -> Self {
        self.description = Some(description);
        self.touch();
        self
    }

    /// 设置优先级
    pub fn with_priority(mut self, priority: Priority) -> Self {
        self.priority = priority;
        self.touch();
        self
    }

    /// 设置截止日期
    pub fn with_due_date(mut self, due_date: DateTime<Utc>) -> Self {
        self.due_date = Some(due_date);
        self.touch();
        self
    }

    /// 添加标签
    pub fn add_tag(&mut self, tag: String) {
        if !self.tags.contains(&tag) {
            self.tags.push(tag);
            self.touch();
        }
    }

    /// 标记完成
    pub fn complete(&mut self) {
        self.status = Status::Done;
        self.touch();
    }

    /// 标记未完成
    pub fn uncomplete(&mut self) {
        self.status = Status::Todo;
        self.touch();
    }

    /// 更新时间戳
    fn touch(&mut self) {
        self.updated_at = Utc::now();
    }
}

/// 生成唯一 ID
fn generate_id() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let duration = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap();
    format!("{:x}", duration.as_nanos())
}

💾 20.3.3 存储模块

// src/storage.rs
use crate::error::{Result, TaskError};
use crate::task::Task;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// 任务存储
pub struct Storage {
    path: PathBuf,
    tasks: HashMap<String, Task>,
}

impl Storage {
    /// 创建存储实例
    pub fn new() -> Result<Self> {
        let path = Self::default_path();
        Self::with_path(path)
    }

    /// 使用指定路径
    pub fn with_path(path: PathBuf) -> Result<Self> {
        let tasks = if path.exists() {
            let content = fs::read_to_string(&path)?;
            serde_json::from_str(&content)?
        } else {
            HashMap::new()
        };

        Ok(Self { path, tasks })
    }

    /// 获取默认存储路径
    fn default_path() -> PathBuf {
        let home = std::env::var("HOME")
            .or_else(|_| std::env::var("USERPROFILE"))
            .unwrap_or_else(|_| ".".to_string());

        Path::new(&home).join(".taskr.json")
    }

    /// 添加任务
    pub fn add(&mut self, task: Task) -> Result<()> {
        self.tasks.insert(task.id.clone(), task);
        self.save()
    }

    /// 获取任务
    pub fn get(&self, id: &str) -> Option<&Task> {
        self.tasks.get(id)
    }

    /// 获取可变任务
    pub fn get_mut(&mut self, id: &str) -> Option<&mut Task> {
        self.tasks.get_mut(id)
    }

    /// 列出所有任务
    pub fn list(&self) -> Vec<&Task> {
        self.tasks.values().collect()
    }

    /// 删除任务
    pub fn remove(&mut self, id: &str) -> Result<bool> {
        let removed = self.tasks.remove(id).is_some();
        if removed {
            self.save()?;
        }
        Ok(removed)
    }

    /// 保存到文件
    pub fn save(&self) -> Result<()> {
        let content = serde_json::to_string_pretty(&self.tasks)?;
        fs::write(&self.path, content)?;
        Ok(())
    }

    /// 任务数量
    pub fn len(&self) -> usize {
        self.tasks.len()
    }

    /// 是否为空
    pub fn is_empty(&self) -> bool {
        self.tasks.is_empty()
    }
}

💻 20.3.4 CLI 接口

// src/cli.rs
use clap::{Parser, Subcommand};
use chrono::{DateTime, Utc};

#[derive(Parser)]
#[command(name = "taskr")]
#[command(about = "任务管理 CLI 工具", long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// 添加新任务
    Add {
        /// 任务标题
        title: String,

        /// 任务描述
        #[arg(short, long)]
        description: Option<String>,

        /// 优先级 (low/medium/high)
        #[arg(short = 'P', long, default_value = "medium")]
        priority: String,

        /// 截止日期 (YYYY-MM-DD)
        #[arg(short, long)]
        due: Option<String>,

        /// 标签
        #[arg(short, long)]
        tags: Vec<String>,
    },

    /// 列出任务
    List {
        /// 仅显示未完成
        #[arg(short, long)]
        todo: bool,

        /// 仅显示已完成
        #[arg(short, long)]
        done: bool,

        /// 按标签过滤
        #[arg(short, long)]
        tag: Option<String>,

        /// 搜索关键词
        #[arg(short, long)]
        search: Option<String>,
    },

    /// 完成任务
    Done {
        /// 任务 ID
        id: String,
    },

    /// 取消完成
    Undo {
        /// 任务 ID
        id: String,
    },

    /// 删除任务
    Remove {
        /// 任务 ID
        id: String,
    },

    /// 编辑任务
    Edit {
        /// 任务 ID
        id: String,

        /// 新标题
        #[arg(short, long)]
        title: Option<String>,

        /// 新描述
        #[arg(short, long)]
        description: Option<String>,

        /// 新优先级
        #[arg(short = 'P', long)]
        priority: Option<String>,
    },

    /// 清除已完成任务
    Clean,
}

/// 解析优先级
pub fn parse_priority(s: &str) -> Option<crate::task::Priority> {
    match s.to_lowercase().as_str() {
        "low" | "l" | "低" => Some(crate::task::Priority::Low),
        "medium" | "m" | "中" => Some(crate::task::Priority::Medium),
        "high" | "h" | "高" => Some(crate::task::Priority::High),
        _ => None,
    }
}

/// 解析日期
pub fn parse_date(s: &str) -> Option<DateTime<Utc>> {
    chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
        .ok()
        .map(|d| d.and_hms_opt(23, 59, 59).unwrap().and_utc())
}

🎨 20.3.5 主程序

// src/main.rs
mod cli;
mod error;
mod storage;
mod task;

use crate::cli::{Cli, Commands, parse_date, parse_priority};
use crate::error::Result;
use crate::storage::Storage;
use crate::task::{Priority, Status, Task};
use clap::Parser;
use colored::Colorize;

fn main() -> Result<()> {
    let cli = Cli::parse();
    let mut storage = Storage::new()?;

    match cli.command {
        Commands::Add {
            title,
            description,
            priority,
            due,
            tags,
        } => {
            let priority = parse_priority(&priority)
                .unwrap_or(Priority::Medium);

            let mut task = Task::new(title);

            if let Some(desc) = description {
                task = task.with_description(desc);
            }

            task = task.with_priority(priority);

            if let Some(due_str) = due {
                if let Some(due_date) = parse_date(&due_str) {
                    task = task.with_due_date(due_date);
                }
            }

            for tag in tags {
                task.add_tag(tag);
            }

            let id = task.id.clone();
            storage.add(task)?;

            println!("{} {}", "✓ 任务已创建:".green(), id.cyan());
        }

        Commands::List {
            todo,
            done,
            tag,
            search,
        } => {
            let tasks = storage.list();

            let filtered: Vec<_> = tasks
                .into_iter()
                .filter(|t| {
                    // 状态过滤
                    if todo && t.status != Status::Todo {
                        return false;
                    }
                    if done && t.status != Status::Done {
                        return false;
                    }

                    // 标签过滤
                    if let Some(ref tag_filter) = tag {
                        if !t.tags.contains(tag_filter) {
                            return false;
                        }
                    }

                    // 搜索过滤
                    if let Some(ref search_term) = search {
                        let search_lower = search_term.to_lowercase();
                        if !t.title.to_lowercase().contains(&search_lower) {
                            return false;
                        }
                    }

                    true
                })
                .collect();

            if filtered.is_empty() {
                println!("{}", "没有找到任务".yellow());
            } else {
                println!("\n{}", "任务列表".bold());
                println!("{}", "─".repeat(60).dimmed());

                for task in filtered {
                    print_task(task);
                }
            }
        }

        Commands::Done { id } => {
            if let Some(task) = storage.get_mut(&id) {
                task.complete();
                storage.save()?;
                println!("{} {}", "✓ 任务已完成:".green(), task.title.cyan());
            } else {
                println!("{} {}", "✗ 任务未找到:".red(), id);
            }
        }

        Commands::Undo { id } => {
            if let Some(task) = storage.get_mut(&id) {
                task.uncomplete();
                storage.save()?;
                println!("{} {}", "✓ 任务已恢复:".green(), task.title.cyan());
            } else {
                println!("{} {}", "✗ 任务未找到:".red(), id);
            }
        }

        Commands::Remove { id } => {
            if storage.remove(&id)? {
                println!("{} {}", "✓ 任务已删除:".green(), id.cyan());
            } else {
                println!("{} {}", "✗ 任务未找到:".red(), id);
            }
        }

        Commands::Edit {
            id,
            title,
            description,
            priority,
        } => {
            if let Some(task) = storage.get_mut(&id) {
                if let Some(new_title) = title {
                    task.title = new_title;
                }
                if let Some(new_desc) = description {
                    task.description = Some(new_desc);
                }
                if let Some(new_priority) = priority {
                    if let Some(p) = parse_priority(&new_priority) {
                        task.priority = p;
                    }
                }
                storage.save()?;
                println!("{} {}", "✓ 任务已更新:".green(), task.title.cyan());
            } else {
                println!("{} {}", "✗ 任务未找到:".red(), id);
            }
        }

        Commands::Clean => {
            let tasks = storage.list();
            let done_ids: Vec<_> = tasks
                .iter()
                .filter(|t| t.status == Status::Done)
                .map(|t| t.id.clone())
                .collect();

            let count = done_ids.len();
            for id in done_ids {
                storage.remove(&id)?;
            }

            println!("{} {} 个已完成任务", "✓ 已清理:".green(), count);
        }
    }

    Ok(())
}

/// 打印任务
fn print_task(task: &Task) {
    let status_icon = match task.status {
        Status::Todo => "○".yellow(),
        Status::Done => "●".green(),
    };

    let priority_icon = match task.priority {
        Priority::High => "!!!".red(),
        Priority::Medium => "!!".yellow(),
        Priority::Low => "!".dimmed(),
    };

    let title = if task.status == Status::Done {
        task.title.dimmed().strikethrough()
    } else {
        task.title.normal()
    };

    println!(
        "{} {} {} {}",
        status_icon,
        title,
        priority_icon,
        task.id.dimmed()
    );

    if let Some(ref desc) = task.description {
        println!("  {}", desc.dimmed());
    }

    if !task.tags.is_empty() {
        let tags: String = task.tags.iter().map(|t| format!("#{}", t)).collect::<Vec<_>>().join(" ");
        println!("  {}", tags.blue());
    }

    if let Some(ref due) = task.due_date {
        let local = due.with_timezone(&chrono::Local);
        let is_overdue = due < &Utc::now() && task.status == Status::Todo;
        let due_str = format!("截止: {}", local.format("%Y-%m-%d"));
        if is_overdue {
            println!("  {}", due_str.red());
        } else {
            println!("  {}", due_str.dimmed());
        }
    }
}

🧪 20.4 测试

// tests/integration_test.rs
use std::process::Command;

fn taskr() -> Command {
    Command::new("./target/debug/taskr")
}

#[test]
fn test_add_and_list() {
    // 添加任务
    let output = taskr()
        .args(["add", "测试任务", "-P", "high"])
        .output()
        .expect("执行失败");

    assert!(output.status.success());

    // 列出任务
    let output = taskr()
        .args(["list"])
        .output()
        .expect("执行失败");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("测试任务"));
}

📦 20.5 发布

🔨 20.5.1 构建发布版本

cargo build --release

📋 20.5.2 编写 README

# taskr

一个简洁高效的命令行任务管理工具。

## 安装

cargo install taskr


## 使用

### 添加任务

taskr add "完成报告" -P high -d 2024-12-31 --tags work urgent


### 列出任务

taskr list taskr list --todo taskr list --tag work


### 完成任务

taskr done


### 删除任务

taskr remove


📝 本章小结

恭喜你完成了《Rust语言从入门到精通》的学习!

本章我们构建了一个完整的 CLI 工具,涵盖了:

  • 需求分析:明确项目目标
  • 模块设计:职责分离
  • 错误处理:使用 thiserror
  • 数据持久化:JSON 存储
  • CLI 接口:使用 clap
  • 测试:单元测试和集成测试

🎓 结语:Rust 之路

二十章的学习,从 Hello World 到完整项目,你已经掌握了 Rust 的核心知识:

基础篇:所有权、借用、类型系统 进阶篇:错误处理、模块、迭代器、生命周期、智能指针 高级篇:并发、异步、宏、Unsafe、类型系统高级特性 实战篇:Web 开发、CLI 工具、测试、嵌入式

Rust 的学习曲线陡峭,但收获丰厚。继续保持好奇心,不断实践,你会成为一名优秀的 Rustacean!

费曼技巧:最好的学习是教别人。尝试向朋友解释所有权系统——如果你能说清楚,你就真正理解了。

Happy Coding! 🦀

← 返回目录