跳到主要内容

Sequelize 第一个项目实战 - 用户管理系统

· 阅读需 8 分钟
一介布衣
全栈开发者

发布时间:2024-01-22
作者:一介布衣
标签:Sequelize, 实战项目, 用户管理, CRUD操作

前言

前面三篇文章我们学习了 Sequelize 的基础概念、环境搭建和数据库连接。今天咱们来动手实践,从零开始构建一个简单但完整的用户管理系统。

这个项目虽然简单,但是包含了 Sequelize 的核心功能:模型定义、数据验证、CRUD 操作等。我会尽量贴近实际开发场景,让大家能够举一反三。

说实话,我第一次用 Sequelize 做项目的时候,光是模型定义就搞了半天,各种数据类型、验证规则搞得我头都大了。今天我就把这些经验整理出来,让大家少走弯路。

项目需求分析

我们要做一个简单的用户管理系统,功能包括:

  • 用户注册(邮箱、用户名、密码)
  • 用户登录验证
  • 用户信息查询
  • 用户信息更新
  • 用户删除(软删除)
  • 用户列表分页查询

数据库字段设计:

  • id:主键,自增
  • username:用户名,唯一
  • email:邮箱,唯一
  • password:密码(加密存储)
  • avatar:头像 URL
  • status:状态(active/inactive)
  • lastLoginAt:最后登录时间
  • createdAt:创建时间
  • updatedAt:更新时间
  • deletedAt:删除时间(软删除)

项目初始化

首先创建项目目录结构:

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

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

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

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

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

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 脚本:

{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
}

启动项目:

npm run dev

测试接口(使用 curl 或 Postman):

# 注册用户
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 版本:

npm install bcryptjs@^2.4.3

2. 数据库同步问题

如果模型修改后数据库没有更新:

// 开发环境可以使用 force: true(会删除现有数据)
await sequelize.sync({ force: true });

// 生产环境建议使用迁移
await sequelize.sync({ alter: true });

3. 唯一约束冲突

处理唯一约束错误:

if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({
success: false,
message: '数据已存在',
field: error.errors[0].path
});
}

总结

今天我们完成了第一个 Sequelize 实战项目,主要学习了:

  • ✅ 完整的项目结构设计
  • ✅ 用户模型的定义和验证
  • ✅ 密码加密和验证
  • ✅ CRUD 操作的实现
  • ✅ 分页查询和条件筛选
  • ✅ 错误处理和数据验证
  • ✅ 软删除的使用

这个项目虽然简单,但是涵盖了 Sequelize 的核心功能。掌握了这些基础,你就可以开发更复杂的应用了。

下一篇文章,我们将深入学习 Sequelize 的模型定义,包括更多的数据类型、验证规则和模型选项。


相关文章推荐:

项目源码已上传到 GitHub,欢迎 Star 和 Fork!