Node.js: 任务 API 项目(中)

最后更新:2026-08-26

1. 业务核心:Alice 的第二天

第二天,Alice 负责实现任务的增删改查,Bob 则专注筛选、分页和权限控制。"CRUD 是骨架,筛选和分页是体验,权限是安全,"Alice 说,"三者缺一不可。"



2. 任务 CRUD API

(1) CRUD 端点设计

方法 路径 说明 权限
POST /api/tasks 创建任务 登录用户
GET /api/tasks 获取任务列表 登录用户
GET /api/tasks/:id 获取单个任务 本人或 admin
PUT /api/tasks/:id 更新任务 本人或 admin
DELETE /api/tasks/:id 删除任务 本人或 admin

▶ 示例:创建任务

JAVASCRIPT
router.post('/', auth, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) {
    next(err);
  }
});
▶ 试一试

▶ 示例:获取单个任务

JAVASCRIPT
router.get('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id).populate('assignedTo', 'username email');
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo._id.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    res.json(task);
  } catch (err) {
    next(err);
  }
});
▶ 试一试

▶ 示例:更新任务

JAVASCRIPT
router.put('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    Object.assign(task, req.body);
    await task.save();
    res.json(task);
  } catch (err) {
    next(err);
  }
});
▶ 试一试

▶ 示例:删除任务

JAVASCRIPT
router.delete('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    await task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) {
    next(err);
  }
});
▶ 试一试

3. 列表筛选与分页排序

(1) 筛选参数说明

参数 类型 说明 示例
status String 按状态筛选 ?status=completed
priority String 按优先级筛选 ?priority=high
assignedTo ObjectId 按指派人筛选(admin) ?assignedTo=userId
dueBefore ISO Date 截止日期早于 ?dueBefore=2025-12-31
dueAfter ISO Date 截止日期晚于 ?dueAfter=2025-01-01
search String 标题模糊搜索 ?search=deploy

(2) 分页参数

参数 默认值 说明
page 1 当前页码
limit 10 每页条数(最大 100)
sort -createdAt 排序字段,-前缀表示降序

(3) 权限规则

角色 可见范围 可操作范围
user 仅自己的任务 仅自己的任务
admin 所有任务 所有任务
user + query assignedTo 忽略该参数

▶ 示例:带筛选的分页列表

JAVASCRIPT
router.get('/', auth, async (req, res, next) => {
  try {
    const { status, priority, dueBefore, dueAfter, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    else if (req.query.assignedTo) filter.assignedTo = req.query.assignedTo;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (dueBefore || dueAfter) filter.dueDate = {};
    if (dueBefore) filter.dueDate.$lte = new Date(dueBefore);
    if (dueAfter) filter.dueDate.$gte = new Date(dueAfter);
    if (search) filter.title = { $regex: search, $options: 'i' };

    const total = await Task.countDocuments(filter);
    const tasks = await Task.find(filter)
      .populate('assignedTo', 'username email')
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(Number(limit));

    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) {
    next(err);
  }
});
▶ 试一试

▶ 示例:多字段排序处理

JAVASCRIPT
// ?sort=-priority,createdAt  →  { priority: -1, createdAt: 1 }
const parseSort = (sortStr) => {
  const sortObj = {};
  sortStr.split(',').forEach(field => {
    if (field.startsWith('-')) sortObj[field.slice(1)] = -1;
    else sortObj[field] = 1;
  });
  return sortObj;
};
▶ 试一试

4. 数据验证与权限中间件

▶ 示例:(1) 请求处理流程

100%
graph LR
    A[客户端请求] --> B[express-validator]
    B --> C[auth 中间件]
    C --> D[权限检查]
    D --> E[业务逻辑]
    E --> F[统一响应]
    B -->|验证失败| G[400 错误]
    C -->|未认证| H[401 错误]
    D -->|无权限| I[403 错误]

▶ 示例:express-validator 验证规则

JAVASCRIPT
const { body, query, validationResult } = require('express-validator');

const validateTask = [
  body('title').notEmpty().withMessage('Title is required').isLength({ max: 100 }).withMessage('Title too long'),
  body('status').optional().isIn(['pending', 'in-progress', 'completed']),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601().withMessage('Invalid date format'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
    next();
  }
];
▶ 试一试

▶ 示例:权限检查中间件

JAVASCRIPT
const requireAdmin = (req, res, next) => {
  if (req.user.role !== 'admin') return res.status(403).json({ message: 'Admin access required' });
  next();
};

const requireOwnerOrAdmin = (model) => async (req, res, next) => {
  const doc = await model.findById(req.params.id);
  if (!doc) return res.status(404).json({ message: 'Not found' });
  if (doc.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.doc = doc;
  next();
};
▶ 试一试

▶ 示例:批量删除任务

JAVASCRIPT
router.delete('/batch', auth, requireAdmin, async (req, res, next) => {
  try {
    const { ids } = req.body;
    const result = await Task.deleteMany({ _id: { $in: ids } });
    res.json({ deleted: result.deletedCount });
  } catch (err) {
    next(err);
  }
});
▶ 试一试

5. 综合示例:完整的 tasks 路由

将 CRUD、筛选、分页、验证、权限整合为一个完整路由模块:

JAVASCRIPT
const router = require('express').Router();
const Task = require('../models/Task');
const auth = require('../middleware/auth');
const { body, query, validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
  next();
};

const checkOwner = async (req, res, next) => {
  const task = await Task.findById(req.params.id);
  if (!task) return res.status(404).json({ message: 'Task not found' });
  if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.task = task;
  next();
};

router.post('/', auth, [
  body('title').notEmpty().isLength({ max: 100 }),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601()
], validate, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) { next(err); }
});

router.get('/', auth, async (req, res, next) => {
  try {
    const { status, priority, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (search) filter.title = { $regex: search, $options: 'i' };
    const total = await Task.countDocuments(filter);
    const tasks = await Task.find(filter).populate('assignedTo', 'username').sort(sort).skip((page - 1) * limit).limit(Number(limit));
    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) { next(err); }
});

router.get('/:id', auth, checkOwner, (req, res) => res.json(req.task));

router.put('/:id', auth, checkOwner, [
  body('title').optional().notEmpty().isLength({ max: 100 }),
  body('status').optional().isIn(['pending', 'in-progress', 'completed'])
], validate, async (req, res, next) => {
  try {
    Object.assign(req.task, req.body);
    await req.task.save();
    res.json(req.task);
  } catch (err) { next(err); }
});

router.delete('/:id', auth, checkOwner, async (req, res, next) => {
  try {
    await req.task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) { next(err); }
});

module.exports = router;

❓ 常见问题

Q express-validator 和 Joi 怎么选?
A express-validator 基于 validator.js,与 Express 中间件无缝集成;Joi 更强大但需单独调用。Express 项目推荐 express-validator。
Q 如何实现软删除?
A 在 Schema 中添加 deletedAt 字段,查询时加 { deletedAt: null } 过滤;或用 mongoose-delete 插件自动处理。
Q 任务权限怎么控制?
A 在路由中间件中比较 req.userId 和 task.author,只有作者才能修改/删除自己的任务,其他人返回 403。
Q 如何处理批量操作?
A 用 Mongoose 的 bulkWrite 或 updateMany,一次性执行多个写操作,比循环单条操作性能好很多。

📖 小节

📝 作业

  1. 实现完整的 Task CRUD 路由,用 Postman 逐一测试创建、查询、更新、删除。
  2. 添加筛选和分页功能,测试 ?status=completed&page=2&limit=5&sort=-priority 等组合查询。
  3. 编写 requireAdmin 中间件,测试普通用户访问 admin 接口时返回 403。
  4. 为注册和登录添加 express-validator 验证规则,测试空字段和非法格式的错误响应。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏