Sequelize 第一个项目实战 - 用户管理系统
发布时间:2024-01-22
作者:一介布衣
标签:Sequelize, 实战项目, 用户管理, CRUD操作
前言
前面三篇文章我们学习了 Sequelize 的基础概念、环境搭建和数据库连接。今天咱们来动手实践,从零开始构建一个简单但完整的用户管理系统。
这个项目虽然简单,但是包含了 Sequelize 的核心功能:模型定义、数据验证、CRUD 操作等。我会尽量贴近实际开发场景,让大家能够举一反三。
说实话,我第一次用 Sequelize 做项目的时候,光是模型定义就搞了半天,各种数据类型、验证规则搞得我头都大了。今天我就把这些经验整理出来,让大家少走弯路。
项目需求分析
我们要做一个简单的用户管理系统,功能包括:
- 用户注册(邮箱、用户名、密码)
- 用户登录验证
- 用户信息查询
- 用户信息更新
- 用户删除(软删除)
- 用户列表分页查询
数据库字段设计:
- id:主键,自增
- username:用户名,唯一
- email:邮箱,唯一
- password:密码(加密存储)
- avatar:头像 URL
- status:状态(active/inactive)
- lastLoginAt:最后登录时间
- createdAt:创建时间
- updatedAt:更新时间
- deletedAt:删除时间(软删除)
项目初始化
首先创建项目目录结构:
bash
mkdir user-management
cd user-management
npm init -y
# 安装依赖
npm install sequelize sqlite3 bcryptjs express dotenv
npm install --save-dev nodemon
# 创建目录结构
mkdir config models controllers routes utils
touch app.js .env
数据库配置
创建 config/database.js
:
javascript
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database.sqlite',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
// 定义全局模型选项
define: {
timestamps: true, // 自动添加 createdAt 和 updatedAt
paranoid: true, // 启用软删除
underscored: false, // 使用驼峰命名
freezeTableName: true // 禁用表名复数化
}
});
module.exports = sequelize;
用户模型定义
创建 models/User.js
:
javascript
const { DataTypes } = require('sequelize');
const bcrypt = require('bcryptjs');
const sequelize = require('../config/database');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: {
name: 'username_unique',
msg: '用户名已存在'
},
validate: {
len: {
args: [3, 50],
msg: '用户名长度必须在3-50个字符之间'
},
isAlphanumeric: {
msg: '用户名只能包含字母和数字'
}
}
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: {
name: 'email_unique',
msg: '邮箱已被注册'
},
validate: {
isEmail: {
msg: '请输入有效的邮箱地址'
}
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
len: {
args: [6, 255],
msg: '密码长度至少6个字符'
}
}
},
avatar: {
type: DataTypes.STRING(500),
allowNull: true,
validate: {
isUrl: {
msg: '头像必须是有效的URL'
}
}
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active',
allowNull: false
},
lastLoginAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'users',
// 模型钩子
hooks: {
beforeCreate: async (user) => {
if (user.password) {
user.password = await bcrypt.hash(user.password, 10);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
user.password = await bcrypt.hash(user.password, 10);
}
}
}
});
// 实例方法:验证密码
User.prototype.validatePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
// 实例方法:更新最后登录时间
User.prototype.updateLastLogin = async function() {
this.lastLoginAt = new Date();
await this.save();
};
// 实例方法:获取安全的用户信息(不包含密码)
User.prototype.toSafeObject = function() {
const { password, ...safeUser } = this.toJSON();
return safeUser;
};
// 类方法:根据邮箱或用户名查找用户
User.findByEmailOrUsername = async function(identifier) {
return await this.findOne({
where: {
[sequelize.Sequelize.Op.or]: [
{ email: identifier },
{ username: identifier }
]
}
});
};
module.exports = User;
控制器实现
创建 controllers/userController.js
:
javascript
const User = require('../models/User');
const { Op } = require('sequelize');
class UserController {
// 用户注册
static async register(req, res) {
try {
const { username, email, password, avatar } = req.body;
// 检查用户是否已存在
const existingUser = await User.findByEmailOrUsername(email);
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名或邮箱已存在'
});
}
// 创建用户
const user = await User.create({
username,
email,
password,
avatar
});
res.status(201).json({
success: true,
message: '注册成功',
data: user.toSafeObject()
});
} catch (error) {
console.error('注册失败:', error);
// 处理验证错误
if (error.name === 'SequelizeValidationError') {
return res.status(400).json({
success: false,
message: '数据验证失败',
errors: error.errors.map(err => err.message)
});
}
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
// 用户登录
static async login(req, res) {
try {
const { identifier, password } = req.body; // identifier 可以是邮箱或用户名
// 查找用户
const user = await User.findByEmailOrUsername(identifier);
if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在'
});
}
// 验证密码
const isValidPassword = await user.validatePassword(password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: '密码错误'
});
}
// 检查用户状态
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: '账户已被禁用'
});
}
// 更新最后登录时间
await user.updateLastLogin();
res.json({
success: true,
message: '登录成功',
data: user.toSafeObject()
});
} catch (error) {
console.error('登录失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
// 获取用户信息
static async getUser(req, res) {
try {
const { id } = req.params;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
res.json({
success: true,
data: user.toSafeObject()
});
} catch (error) {
console.error('获取用户失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
// 更新用户信息
static async updateUser(req, res) {
try {
const { id } = req.params;
const { username, email, avatar, status } = req.body;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
// 更新用户信息
await user.update({
username,
email,
avatar,
status
});
res.json({
success: true,
message: '更新成功',
data: user.toSafeObject()
});
} catch (error) {
console.error('更新用户失败:', error);
if (error.name === 'SequelizeValidationError') {
return res.status(400).json({
success: false,
message: '数据验证失败',
errors: error.errors.map(err => err.message)
});
}
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
// 删除用户(软删除)
static async deleteUser(req, res) {
try {
const { id } = req.params;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
await user.destroy(); // 软删除
res.json({
success: true,
message: '删除成功'
});
} catch (error) {
console.error('删除用户失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
// 获取用户列表(分页)
static async getUsers(req, res) {
try {
const { page = 1, limit = 10, status, search } = req.query;
const offset = (page - 1) * limit;
// 构建查询条件
const where = {};
if (status) {
where.status = status;
}
if (search) {
where[Op.or] = [
{ username: { [Op.like]: `%\${search}%` } },
{ email: { [Op.like]: `%\${search}%` } }
];
}
const { count, rows } = await User.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']],
attributes: { exclude: ['password'] } // 排除密码字段
});
res.json({
success: true,
data: {
users: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(count / limit)
}
}
});
} catch (error) {
console.error('获取用户列表失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
}
module.exports = UserController;
路由配置
创建 routes/userRoutes.js
:
javascript
const express = require('express');
const UserController = require('../controllers/userController');
const router = express.Router();
// 用户注册
router.post('/register', UserController.register);
// 用户登录
router.post('/login', UserController.login);
// 获取用户信息
router.get('/:id', UserController.getUser);
// 更新用户信息
router.put('/:id', UserController.updateUser);
// 删除用户
router.delete('/:id', UserController.deleteUser);
// 获取用户列表
router.get('/', UserController.getUsers);
module.exports = router;
应用入口
创建 app.js
:
javascript
const express = require('express');
const sequelize = require('./config/database');
const userRoutes = require('./routes/userRoutes');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 路由
app.use('/api/users', userRoutes);
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
});
// 404 处理
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: '接口不存在'
});
});
// 启动服务器
async function startServer() {
try {
// 测试数据库连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 同步数据库模型
await sequelize.sync({ force: false });
console.log('✅ 数据库模型同步完成');
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:\${PORT}`);
});
} catch (error) {
console.error('❌ 启动失败:', error);
process.exit(1);
}
}
startServer();
// 优雅关闭
process.on('SIGTERM', async () => {
console.log('正在关闭服务器...');
await sequelize.close();
process.exit(0);
});
测试接口
创建 package.json
脚本:
json
{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
}
启动项目:
bash
npm run dev
测试接口(使用 curl 或 Postman):
bash
# 注册用户
curl -X POST http://localhost:3000/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@example.com",
"password": "123456"
}'
# 用户登录
curl -X POST http://localhost:3000/api/users/login \
-H "Content-Type: application/json" \
-d '{
"identifier": "test@example.com",
"password": "123456"
}'
# 获取用户列表
curl http://localhost:3000/api/users?page=1&limit=10
常见问题解决
1. 密码加密问题
如果遇到密码加密失败,检查 bcryptjs 版本:
bash
npm install bcryptjs@^2.4.3
2. 数据库同步问题
如果模型修改后数据库没有更新:
javascript
// 开发环境可以使用 force: true(会删除现有数据)
await sequelize.sync({ force: true });
// 生产环境建议使用迁移
await sequelize.sync({ alter: true });
3. 唯一约束冲突
处理唯一约束错误:
javascript
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({
success: false,
message: '数据已存在',
field: error.errors[0].path
});
}
总结
今天我们完成了第一个 Sequelize 实战项目,主要学习了:
- ✅ 完整的项目结构设计
- ✅ 用户模型的定义和验证
- ✅ 密码加密和验证
- ✅ CRUD 操作的实现
- ✅ 分页查询和条件筛选
- ✅ 错误处理和数据验证
- ✅ 软删除的使用
这个项目虽然简单,但是涵盖了 Sequelize 的核心功能。掌握了这些基础,你就可以开发更复杂的应用了。
下一篇文章,我们将深入学习 Sequelize 的模型定义,包括更多的数据类型、验证规则和模型选项。
相关文章推荐:
项目源码已上传到 GitHub,欢迎 Star 和 Fork!