折腾侠
项目实战

从零打造个人待办事项管理系统:Node.js + Express + MongoDB 全栈实践

折腾侠
2026/03/28 发布
10约 11 分钟1487 字 / 1575 词00

从零打造个人待办事项管理系统:Node.js + Express + MongoDB 全栈实践

项目概述

在本文中,我将带你从零开始构建一个功能完整的个人待办事项管理系统(Todo List Application)。这个项目不仅适合初学者学习全栈开发,也能帮助有经验的开发者巩固 Node.js 生态系统的核心技能。通过完成这个项目,你将掌握后端 API 设计、数据库建模、RESTful 接口实现以及项目结构化组织等关键技能。

项目功能说明

核心功能

  1. 任务管理

    • 创建新任务(支持标题、描述、优先级、截止日期)
    • 查看任务列表(支持分页、筛选、排序)
    • 更新任务状态(待办/进行中/已完成)
    • 删除任务(支持软删除)
    • 批量操作(批量完成、批量删除)
  2. 任务分类

    • 自定义分类标签(工作、学习、生活、购物等)
    • 按分类筛选任务
    • 分类统计(每类任务数量)
  3. 优先级系统

    • 四级优先级(紧急重要、重要不紧急、紧急不重要、普通)
    • 优先级颜色标识
    • 按优先级排序
  4. 数据统计

    • 今日任务概览
    • 本周完成情况统计
    • 任务完成趋势图表数据
  5. 用户认证

    • JWT Token 认证
    • 用户注册与登录
    • 密码加密存储

扩展功能(可选)

  • 任务提醒(邮件/短信通知)
  • 任务分享与协作
  • 子任务支持
  • 附件上传
  • 移动端适配

技术栈选择

后端技术栈

技术版本用途
Node.js18.x LTS运行时环境
Express4.xWeb 框架
MongoDB6.x数据库
Mongoose7.xODM 库
JWT9.x身份认证
Bcrypt5.x密码加密
dotenv16.x环境变量管理

开发工具

工具用途
nodemon开发环境热重载
Jest单元测试
ESLint代码规范检查
PostmanAPI 测试

技术选型理由

  1. Node.js + Express:轻量高效,适合快速构建 RESTful API,生态系统成熟,社区活跃。

  2. MongoDB + Mongoose:NoSQL 数据库适合存储非结构化数据,Mongoose 提供强大的 Schema 验证和中间件支持。

  3. JWT 认证:无状态认证方案,适合前后端分离架构,支持跨域请求。

项目结构设计

todo-app/
├── src/
│   ├── config/
│   │   ├── database.js      # 数据库连接配置
│   │   └── index.js         # 配置中心
│   ├── controllers/
│   │   ├── authController.js    # 认证控制器
│   │   ├── todoController.js    # 任务控制器
│   │   └── statsController.js   # 统计控制器
│   ├── middleware/
│   │   ├── auth.js          # JWT 认证中间件
│   │   ├── validation.js    # 请求验证中间件
│   │   └── errorHandler.js  # 全局错误处理
│   ├── models/
│   │   ├── User.js          # 用户模型
│   │   ├── Todo.js          # 任务模型
│   │   └── Category.js      # 分类模型
│   ├── routes/
│   │   ├── index.js         # 路由汇总
│   │   ├── auth.js          # 认证路由
│   │   ├── todos.js         # 任务路由
│   │   └── stats.js         # 统计路由
│   ├── utils/
│   │   ├── logger.js        # 日志工具
│   │   └── helpers.js       # 辅助函数
│   ├── validators/
│   │   ├── todoValidator.js # 任务验证规则
│   │   └── userValidator.js # 用户验证规则
│   └── app.js               # 应用入口
├── tests/
│   ├── controllers/
│   ├── middleware/
│   └── models/
├── .env                     # 环境变量
├── .env.example             # 环境变量示例
├── .gitignore
├── package.json
├── README.md
└── server.js                # 服务器启动入口

核心代码实现

1. 数据库连接配置

JavaScript
// src/config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log(`MongoDB 连接成功:${conn.connection.host}`);
  } catch (error) {
    console.error(`数据库连接失败:${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

2. 用户模型设计

JavaScript
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, '用户名不能为空'],
    unique: true,
    trim: true,
    minlength: 3,
    maxlength: 30
  },
  email: {
    type: String,
    required: [true, '邮箱不能为空'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
  },
  password: {
    type: String,
    required: [true, '密码不能为空'],
    minlength: 6,
    select: false
  },
  avatar: {
    type: String,
    default: ''
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 密码加密中间件
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) {
    return next();
  }
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// 密码比对方法
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

3. 任务模型设计

JavaScript
// src/models/Todo.js
const mongoose = require('mongoose');

const todoSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, '任务标题不能为空'],
    trim: true,
    maxlength: 100
  },
  description: {
    type: String,
    trim: true,
    maxlength: 500
  },
  status: {
    type: String,
    enum: ['pending', 'in-progress', 'completed'],
    default: 'pending'
  },
  priority: {
    type: String,
    enum: ['urgent-important', 'important', 'urgent', 'normal'],
    default: 'normal'
  },
  category: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Category'
  },
  dueDate: {
    type: Date
  },
  completedAt: {
    type: Date
  },
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  isDeleted: {
    type: Boolean,
    default: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// 更新时自动更新时间戳
todoSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

// 完成时记录完成时间
todoSchema.pre('save', function(next) {
  if (this.isModified('status') && this.status === 'completed') {
    this.completedAt = Date.now();
  }
  next();
});

// 索引优化查询性能
todoSchema.index({ user: 1, status: 1 });
todoSchema.index({ user: 1, dueDate: 1 });
todoSchema.index({ user: 1, isDeleted: 1 });

module.exports = mongoose.model('Todo', todoSchema);

4. JWT 认证中间件

JavaScript
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
  let token;

  // 从 Authorization header 获取 token
  if (req.headers.authorization && 
      req.headers.authorization.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1];
  }

  if (!token) {
    return res.status(401).json({
      success: false,
      message: '未授权访问,请先登录'
    });
  }

  try {
    // 验证 token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // 获取用户信息(排除密码字段)
    req.user = await User.findById(decoded.id).select('-password');
    
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: '用户不存在'
      });
    }
    
    next();
  } catch (error) {
    return res.status(401).json({
      success: false,
      message: 'Token 无效或已过期'
    });
  }
};

module.exports = { protect };

5. 任务控制器实现

JavaScript
// src/controllers/todoController.js
const Todo = require('../models/Todo');

// @desc    获取任务列表
// @route   GET /api/todos
// @access  Private
const getTodos = async (req, res, next) => {
  try {
    const {
      status,
      priority,
      category,
      page = 1,
      limit = 10,
      sortBy = 'createdAt',
      order = 'desc'
    } = req.query;

    // 构建查询条件
    const query = {
      user: req.user._id,
      isDeleted: false
    };

    if (status) query.status = status;
    if (priority) query.priority = priority;
    if (category) query.category = category;

    // 执行分页查询
    const todos = await Todo.find(query)
      .populate('category', 'name color')
      .sort({ [sortBy]: order })
      .limit(limit * 1)
      .skip((page - 1) * limit);

    const count = await Todo.countDocuments(query);

    res.status(200).json({
      success: true,
      data: todos,
      pagination: {
        currentPage: page,
        totalPages: Math.ceil(count / limit),
        totalItems: count,
        itemsPerPage: limit
      }
    });
  } catch (error) {
    next(error);
  }
};

// @desc    创建新任务
// @route   POST /api/todos
// @access  Private
const createTodo = async (req, res, next) => {
  try {
    const { title, description, priority, category, dueDate } = req.body;

    const todo = await Todo.create({
      title,
      description,
      priority,
      category,
      dueDate,
      user: req.user._id
    });

    res.status(201).json({
      success: true,
      data: todo
    });
  } catch (error) {
    next(error);
  }
};

// @desc    更新任务
// @route   PUT /api/todos/:id
// @access  Private
const updateTodo = async (req, res, next) => {
  try {
    const todo = await Todo.findOneAndUpdate(
      { _id: req.params.id, user: req.user._id, isDeleted: false },
      req.body,
      { new: true, runValidators: true }
    );

    if (!todo) {
      return res.status(404).json({
        success: false,
        message: '任务不存在'
      });
    }

    res.status(200).json({
      success: true,
      data: todo
    });
  } catch (error) {
    next(error);
  }
};

// @desc    删除任务(软删除)
// @route   DELETE /api/todos/:id
// @access  Private
const deleteTodo = async (req, res, next) => {
  try {
    const todo = await Todo.findOneAndUpdate(
      { _id: req.params.id, user: req.user._id },
      { isDeleted: true },
      { new: true }
    );

    if (!todo) {
      return res.status(404).json({
        success: false,
        message: '任务不存在'
      });
    }

    res.status(200).json({
      success: true,
      message: '任务已删除'
    });
  } catch (error) {
    next(error);
  }
};

module.exports = {
  getTodos,
  createTodo,
  updateTodo,
  deleteTodo
};

6. 路由配置

JavaScript
// src/routes/todos.js
const express = require('express');
const router = express.Router();
const { protect } = require('../middleware/auth');
const {
  getTodos,
  createTodo,
  updateTodo,
  deleteTodo
} = require('../controllers/todoController');
const { validateTodo } = require('../validators/todoValidator');

// 所有路由都需要认证
router.use(protect);

router.route('/')
  .get(getTodos)
  .post(validateTodo, createTodo);

router.route('/:id')
  .put(validateTodo, updateTodo)
  .delete(deleteTodo);

module.exports = router;

7. 应用入口文件

JavaScript
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// 安全中间件
app.use(helmet());

// CORS 配置
app.use(cors({
  origin: process.env.FRONTEND_URL || '*',
  credentials: true
}));

// 请求限流
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100 // 每个 IP 最多 100 个请求
});
app.use('/api', limiter);

// 解析 JSON 请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// API 路由
app.use('/api', routes);

// 健康检查
app.get('/health', (req, res) => {
  res.status(200).json({
    success: true,
    message: '服务运行正常',
    timestamp: new Date().toISOString()
  });
});

// 全局错误处理
app.use(errorHandler);

// 404 处理
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: '接口不存在'
  });
});

module.exports = app;

8. 服务器启动入口

JavaScript
// server.js
require('dotenv').config();
const app = require('./src/app');
const connectDB = require('./src/config/database');

const PORT = process.env.PORT || 3000;

// 连接数据库
connectDB();

// 启动服务器
const server = app.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT}`);
  console.log(`环境:${process.env.NODE_ENV}`);
});

// 处理未捕获的异常
process.on('unhandledRejection', (err) => {
  console.error(`错误:${err.message}`);
  server.close(() => process.exit(1));
});

module.exports = server;

运行步骤

第一步:环境准备

  1. 安装 Node.js(推荐 18.x LTS 版本)

    Bash
    # 使用 nvm 安装
    nvm install 18
    nvm use 18
    
  2. 安装 MongoDB

    • macOS: INLINE_CODE_0
    • Ubuntu: INLINE_CODE_1
    • Windows: 下载安装包安装
    • 或使用 Docker: INLINE_CODE_2

第二步:项目初始化

Bash
# 创建项目目录
mkdir todo-app
cd todo-app

# 初始化 npm 项目
npm init -y

# 安装生产依赖
npm install express mongoose dotenv bcryptjs jsonwebtoken cors helmet express-rate-limit

# 安装开发依赖
npm install -D nodemon jest eslint

第三步:配置文件

创建 INLINE_CODE_3 文件:

.env
# 服务器配置
PORT=3000
NODE_ENV=development

# 数据库配置
MONGODB_URI=mongodb://localhost:27017/todo-app

# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRE=7d

# 前端地址(CORS)
FRONTEND_URL=http://localhost:3001

第四步:创建项目结构

按照上方项目结构设计创建所有目录和文件,将核心代码复制到对应文件中。

第五步:启动开发服务器

Bash
# 开发模式(支持热重载)
npm run dev

# 生产模式
npm start

第六步:API 测试

使用 Postman 或 curl 测试 API:

Bash
# 用户注册
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","password":"123456"}'

# 用户登录
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"123456"}'

# 创建任务(使用登录返回的 token)
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"title":"学习 Node.js","description":"完成教程","priority":"important"}'

# 获取任务列表
curl -X GET http://localhost:3000/api/todos \
  -H "Authorization: Bearer YOUR_TOKEN"

项目优化建议

性能优化

  1. 数据库索引:为常用查询字段创建索引
  2. 请求缓存:使用 Redis 缓存频繁查询
  3. 分页优化:大数据量时使用游标分页
  4. 连接池:配置 MongoDB 连接池参数

安全加固

  1. 输入验证:使用 Joi 或 express-validator 进行严格验证
  2. XSS 防护:对用户输入进行转义
  3. CSRF 防护:添加 CSRF Token
  4. 敏感信息:使用环境变量管理密钥

代码质量

  1. 单元测试:使用 Jest 编写测试用例,覆盖率达到 80% 以上
  2. 代码规范:配置 ESLint + Prettier
  3. Git 规范:使用 Conventional Commits
  4. CI/CD:配置 GitHub Actions 自动化测试和部署

总结

通过这个项目,你掌握了:

  • ✅ Express 框架搭建 RESTful API
  • ✅ MongoDB 数据库设计与 Mongoose ODM 使用
  • ✅ JWT 身份认证实现
  • ✅ 中间件开发与使用
  • ✅ 项目结构化组织
  • ✅ 错误处理与日志记录
  • ✅ 环境变量配置
  • ✅ API 测试方法

这个项目可以作为你简历上的亮点,也可以作为学习更复杂系统的起点。建议你在此基础上继续扩展功能,如添加前端界面、实现实时通知、支持文件上传等,逐步打造一个完整的全栈应用。

下一步学习建议

  1. 使用 React/Vue 构建前端界面
  2. 添加 WebSocket 实现实时更新
  3. 部署到云服务器(阿里云/腾讯云)
  4. 配置 Docker 容器化部署
  5. 添加 CI/CD 自动化流程

祝你在 Node.js 全栈开发的道路上越走越远!

分享到:

如果这篇文章对你有帮助,欢迎请作者喝杯咖啡 ☕

加载评论中...