Node.js: 任务 API 项目(中)
最后更新:2026-08-26
1. 业务核心:Alice 的第二天
第二天,Alice 负责实现任务的增删改查,Bob 则专注筛选、分页和权限控制。"CRUD 是骨架,筛选和分页是体验,权限是安全,"Alice 说,"三者缺一不可。"
- CRUD API 是任务管理的核心业务逻辑
- Query filter 让用户按状态/优先级/日期筛选任务
- 分页与排序避免大数据量时性能下降
- 角色权限确保普通用户只能操作自己的任务
- express-validator 统一验证请求参数格式
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) 请求处理流程
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,一次性执行多个写操作,比循环单条操作性能好很多。
- Q: 分页怎么做? A: 用 Mongoose 的
.skip((page-1)*limit).limit(limit)实现偏移分页,同时用countDocuments返回总数计算总页数。 - Q: 如何限制用户只操作自己的任务? A: 在查询 filter 中加入
assignedTo: req.user._id,更新/删除前检查task.assignedTo是否等于当前用户 ID。 - Q: 批量删除怎么做? A: 使用
Task.deleteMany({ _id: { $in: ids } }),但建议限制为 admin 角色才可执行批量操作。 - Q: 排序多个字段怎么处理? A: 用逗号分隔如
?sort=-priority,createdAt,解析为{ priority: -1, createdAt: 1 }传给 Mongoose.sort()。 - Q: 验证错误怎么统一格式? A: 用 express-validator 的
validationResult,统一返回{ errors: [{ msg, param, value }] }结构。 - Q: 分页性能有优化空间吗? A: 大数据量时 skip 较慢,可改用游标分页(基于
_id或createdAt的$gt过滤),避免跳过大量文档。
📖 小节
- 业务核心:Alice 的第二天的核心概念与使用方法
- 任务 CRUD API的核心概念与使用方法
- 列表筛选与分页排序的核心概念与使用方法
- 数据验证与权限中间件的核心概念与使用方法
- 综合示例:完整的 tasks 路由的核心概念与使用方法
📝 作业
- 实现完整的 Task CRUD 路由,用 Postman 逐一测试创建、查询、更新、删除。
- 添加筛选和分页功能,测试
?status=completed&page=2&limit=5&sort=-priority等组合查询。 - 编写
requireAdmin中间件,测试普通用户访问 admin 接口时返回 403。 - 为注册和登录添加 express-validator 验证规则,测试空字段和非法格式的错误响应。