Skip to content

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!