使用 Node.js 开发预订和预约系统的后端

使用 Node.js 开发预订和预约系统的后端

引言

大家好,欢迎来到今天的讲座!今天我们要一起探讨如何使用 Node.js 来开发一个预订和预约系统的后端。这个系统可以用于各种场景,比如餐厅预订、医生预约、会议室预定等。我们将从零开始,一步一步地构建这个系统,并且尽量让整个过程轻松有趣。

如果你对 Node.js 还不太熟悉,别担心!我们会从基础开始讲解,确保每个人都能跟上节奏。如果你已经有一定的 Node.js 经验,那么你也可以学到一些新的技巧和最佳实践。废话不多说,让我们直接进入正题吧!

1. 为什么选择 Node.js?

在开始之前,我们先来聊聊为什么选择 Node.js 作为我们的后端开发语言。Node.js 是基于 Chrome V8 引擎的 JavaScript 运行时,它允许我们在服务器端编写 JavaScript 代码。这听起来可能没什么特别的,但其实 Node.js 有以下几个显著的优点:

  • 异步 I/O:Node.js 使用事件驱动的非阻塞 I/O 模型,这意味着它可以高效地处理大量并发请求。对于预订和预约系统来说,这一点尤为重要,因为用户可能会同时发起多个请求。

  • 全栈 JavaScript:如果你已经在前端使用了 JavaScript,那么使用 Node.js 作为后端可以让你在整个项目中使用同一种语言。这样不仅可以减少学习成本,还能提高开发效率。

  • 丰富的生态系统:Node.js 拥有一个庞大的 npm(Node Package Manager)库,里面包含了成千上万的第三方模块。无论你需要什么功能,几乎都可以找到现成的解决方案。

  • 社区支持:Node.js 的社区非常活跃,遇到问题时很容易找到帮助。无论是 Stack Overflow 还是 GitHub,都有大量的资源可以参考。

总之,Node.js 是一个非常适合构建高性能、可扩展的后端应用的选择。接下来,我们就来看看如何开始我们的项目。

2. 项目初始化

2.1 安装 Node.js 和 npm

首先,我们需要确保已经安装了 Node.js 和 npm。你可以通过以下命令检查是否已经安装:

node -v
npm -v

如果还没有安装,可以通过官方文档中的步骤进行安装。安装完成后,我们就可以开始创建项目了。

2.2 创建项目目录

打开终端,创建一个新的项目目录,并进入该目录:

mkdir booking-system
cd booking-system

2.3 初始化项目

在项目目录下,运行以下命令来初始化一个新的 Node.js 项目:

npm init -y

这将生成一个 package.json 文件,其中包含项目的元数据和依赖项。-y 参数会自动接受所有默认设置,省去手动输入的麻烦。

2.4 安装必要的依赖

接下来,我们需要安装一些常用的依赖项。我们可以使用 npm 来安装这些包:

npm install express mongoose cors dotenv bcryptjs jsonwebtoken

这里我们安装了以下依赖:

  • Express:一个轻量级的 Web 框架,用于处理 HTTP 请求和响应。
  • Mongoose:一个 MongoDB ORM(对象关系映射)工具,用于与 MongoDB 数据库交互。
  • CORS:用于处理跨域资源共享(Cross-Origin Resource Sharing),确保前后端可以正常通信。
  • dotenv:用于加载环境变量,方便管理敏感信息(如数据库连接字符串、密钥等)。
  • bcryptjs:用于加密和验证密码,确保用户信息的安全性。
  • jsonwebtoken:用于生成和验证 JWT(JSON Web Token),实现用户身份验证。

2.5 配置环境变量

为了保护敏感信息,我们通常不会将它们硬编码在代码中。相反,我们会使用环境变量来存储这些信息。创建一个名为 .env 的文件,并在其中添加以下内容:

PORT=5000
MONGO_URI=mongodb://localhost:27017/booking-system
JWT_SECRET=your_jwt_secret_key
BCRYPT_SALT_ROUNDS=10

这些环境变量分别表示:

  • PORT:服务器监听的端口号。
  • MONGO_URI:MongoDB 数据库的连接字符串。
  • JWT_SECRET:用于生成 JWT 的密钥。
  • BCRYPT_SALT_ROUNDS:bcrypt 加密时使用的盐轮数,数值越大越安全,但也会增加计算时间。

2.6 设置基本的 Express 服务器

现在我们已经有了所有的依赖项,接下来可以创建一个基本的 Express 服务器。在项目根目录下创建一个名为 server.js 的文件,并添加以下代码:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(cors());
app.use(express.json());

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));

// Routes
app.get('/', (req, res) => {
  res.send('Welcome to the Booking System API!');
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

这段代码做了几件事:

  1. 加载环境变量并配置 Express 服务器。
  2. 使用 cors 中间件允许跨域请求。
  3. 使用 express.json() 中间件解析 JSON 请求体。
  4. 连接到 MongoDB 数据库。
  5. 定义一个简单的路由 /,返回欢迎消息。
  6. 启动服务器并监听指定的端口。

现在,你可以运行以下命令启动服务器:

node server.js

如果一切顺利,你应该会在终端中看到类似以下的输出:

MongoDB connected
Server running on port 5000

恭喜你,你已经成功创建了一个基本的 Node.js 服务器!接下来,我们将继续完善这个系统。

3. 用户认证

在一个预订和预约系统中,用户认证是非常重要的。我们需要确保只有经过验证的用户才能进行预订或查看他们的预约记录。为此,我们将实现一个基于 JWT 的用户认证系统。

3.1 创建用户模型

首先,我们需要定义一个用户模型。在 models 目录下创建一个名为 User.js 的文件,并添加以下代码:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please provide a name'],
    maxlength: 50,
  },
  email: {
    type: String,
    required: [true, 'Please provide an email'],
    match: [
      /^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/,
      'Please provide a valid email',
    ],
    unique: true,
  },
  password: {
    type: String,
    required: [true, 'Please provide a password'],
    minlength: 6,
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user',
  },
});

// Hash password before saving
UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) {
    next();
  }
  const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_SALT_ROUNDS));
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Compare password
UserSchema.methods.comparePassword = async function (candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', UserSchema);

这个用户模型包含了以下字段:

  • name:用户的姓名。
  • email:用户的电子邮件地址,必须唯一且符合标准格式。
  • password:用户的密码,保存时会进行哈希处理。
  • role:用户的角色,默认为 user,管理员为 admin

我们还定义了两个方法:

  • pre('save'):在保存用户之前,使用 bcrypt 对密码进行哈希处理。
  • comparePassword:用于验证用户提供的密码是否与数据库中的哈希密码匹配。

3.2 实现注册和登录功能

接下来,我们实现用户注册和登录的功能。在 routes 目录下创建一个名为 auth.js 的文件,并添加以下代码:

const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { check, validationResult } = require('express-validator');
const User = require('../models/User');

const router = express.Router();

// @route   POST /api/auth/register
// @desc    Register a new user
// @access  Public
router.post(
  '/register',
  [
    check('name', 'Name is required').not().isEmpty(),
    check('email', 'Please include a valid email').isEmail(),
    check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { name, email, password } = req.body;

    try {
      // Check if user already exists
      let user = await User.findOne({ email });
      if (user) {
        return res.status(400).json({ msg: 'User already exists' });
      }

      // Create new user
      user = new User({
        name,
        email,
        password,
      });

      await user.save();

      // Generate JWT
      const payload = {
        user: {
          id: user.id,
        },
      };

      jwt.sign(
        payload,
        process.env.JWT_SECRET,
        { expiresIn: '1h' },
        (err, token) => {
          if (err) throw err;
          res.json({ token });
        }
      );
    } catch (err) {
      console.error(err.message);
      res.status(500).send('Server error');
    }
  }
);

// @route   POST /api/auth/login
// @desc    Authenticate user & get token
// @access  Public
router.post(
  '/login',
  [
    check('email', 'Please include a valid email').isEmail(),
    check('password', 'Password is required').exists(),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { email, password } = req.body;

    try {
      // Check if user exists
      let user = await User.findOne({ email });
      if (!user) {
        return res.status(400).json({ msg: 'Invalid credentials' });
      }

      // Validate password
      const isMatch = await user.comparePassword(password);
      if (!isMatch) {
        return res.status(400).json({ msg: 'Invalid credentials' });
      }

      // Generate JWT
      const payload = {
        user: {
          id: user.id,
        },
      };

      jwt.sign(
        payload,
        process.env.JWT_SECRET,
        { expiresIn: '1h' },
        (err, token) => {
          if (err) throw err;
          res.json({ token });
        }
      );
    } catch (err) {
      console.error(err.message);
      res.status(500).send('Server error');
    }
  }
);

module.exports = router;

这段代码实现了两个路由:

  • /register:用于注册新用户。它会验证用户输入的信息是否有效,然后将用户保存到数据库中,并生成一个 JWT 令牌返回给客户端。
  • /login:用于用户登录。它会验证用户的电子邮件和密码是否正确,如果验证通过,则生成一个 JWT 令牌返回给客户端。

3.3 保护路由

为了让某些路由只能被已认证的用户访问,我们需要实现一个中间件来验证 JWT。在 middleware 目录下创建一个名为 auth.js 的文件,并添加以下代码:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const auth = async (req, res, next) => {
  // Get token from header
  const token = req.header('x-auth-token');

  // Check if no token
  if (!token) {
    return res.status(401).json({ msg: 'No token, authorization denied' });
  }

  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Get user from token
    req.user = await User.findById(decoded.user.id).select('-password');
    next();
  } catch (err) {
    res.status(401).json({ msg: 'Token is not valid' });
  }
};

module.exports = auth;

这个中间件会从请求头中提取 JWT,并验证其有效性。如果验证通过,它会将用户信息附加到 req.user 上,并调用 next() 继续执行下一个中间件或路由处理程序。

3.4 测试用户认证

现在我们已经实现了用户注册、登录和路由保护功能。你可以使用 Postman 或其他 API 测试工具来测试这些功能。以下是测试步骤:

  1. 发送一个 POST 请求到 /api/auth/register,提供用户名、电子邮件和密码。你应该会收到一个 JWT 令牌。
  2. 使用刚刚收到的 JWT 令牌,发送一个 POST 请求到 /api/auth/login,提供电子邮件和密码。你应该会再次收到一个 JWT 令牌。
  3. 在后续的请求中,将 JWT 令牌添加到请求头中,键名为 x-auth-token。例如,你可以尝试访问一个受保护的路由,看看是否能够成功访问。

4. 预订和预约功能

现在我们已经有了用户认证系统,接下来可以实现预订和预约的核心功能。我们将创建一个 Booking 模型,并实现相关的 CRUD 操作。

4.1 创建预订模型

models 目录下创建一个名为 Booking.js 的文件,并添加以下代码:

const mongoose = require('mongoose');

const BookingSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true,
  },
  service: {
    type: String,
    required: [true, 'Please provide a service'],
    enum: ['restaurant', 'doctor', 'meeting'],
  },
  date: {
    type: Date,
    required: [true, 'Please provide a date'],
  },
  time: {
    type: String,
    required: [true, 'Please provide a time'],
  },
  status: {
    type: String,
    enum: ['pending', 'confirmed', 'cancelled'],
    default: 'pending',
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('Booking', BookingSchema);

这个预订模型包含了以下字段:

  • user:引用 User 模型,表示预订的用户。
  • service:预订的服务类型,可以是餐厅、医生或会议室。
  • date:预订的日期。
  • time:预订的时间。
  • status:预订的状态,默认为 pending,还可以是 confirmedcancelled
  • createdAt:预订创建的时间。

4.2 实现 CRUD 操作

接下来,我们实现与预订相关的 CRUD 操作。在 routes 目录下创建一个名为 bookings.js 的文件,并添加以下代码:

const express = require('express');
const { check, validationResult } = require('express-validator');
const Booking = require('../models/Booking');
const auth = require('../middleware/auth');

const router = express.Router();

// @route   GET /api/bookings
// @desc    Get all bookings for the logged-in user
// @access  Private
router.get('/', auth, async (req, res) => {
  try {
    const bookings = await Booking.find({ user: req.user.id }).sort({ date: -1 });
    res.json(bookings);
  } catch (err) {
    console.error(err.message);
    res.status(500).send('Server error');
  }
});

// @route   POST /api/bookings
// @desc    Create a new booking
// @access  Private
router.post(
  '/',
  [
    auth,
    [
      check('service', 'Service is required').not().isEmpty(),
      check('date', 'Date is required').not().isEmpty(),
      check('time', 'Time is required').not().isEmpty(),
    ],
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { service, date, time } = req.body;

    try {
      const newBooking = new Booking({
        user: req.user.id,
        service,
        date,
        time,
      });

      await newBooking.save();
      res.json(newBooking);
    } catch (err) {
      console.error(err.message);
      res.status(500).send('Server error');
    }
  }
);

// @route   PUT /api/bookings/:id
// @desc    Update a booking
// @access  Private
router.put('/:id', auth, async (req, res) => {
  const { service, date, time, status } = req.body;

  const bookingFields = {};
  if (service) bookingFields.service = service;
  if (date) bookingFields.date = date;
  if (time) bookingFields.time = time;
  if (status) bookingFields.status = status;

  try {
    let booking = await Booking.findById(req.params.id);

    if (!booking) {
      return res.status(404).json({ msg: 'Booking not found' });
    }

    // Ensure user owns the booking
    if (booking.user.toString() !== req.user.id) {
      return res.status(401).json({ msg: 'Not authorized' });
    }

    booking = await Booking.findByIdAndUpdate(
      req.params.id,
      { $set: bookingFields },
      { new: true }
    );

    res.json(booking);
  } catch (err) {
    console.error(err.message);
    if (err.kind === 'ObjectId') {
      return res.status(404).json({ msg: 'Booking not found' });
    }
    res.status(500).send('Server error');
  }
});

// @route   DELETE /api/bookings/:id
// @desc    Delete a booking
// @access  Private
router.delete('/:id', auth, async (req, res) => {
  try {
    let booking = await Booking.findById(req.params.id);

    if (!booking) {
      return res.status(404).json({ msg: 'Booking not found' });
    }

    // Ensure user owns the booking
    if (booking.user.toString() !== req.user.id) {
      return res.status(401).json({ msg: 'Not authorized' });
    }

    await Booking.findByIdAndRemove(req.params.id);

    res.json({ msg: 'Booking removed' });
  } catch (err) {
    console.error(err.message);
    if (err.kind === 'ObjectId') {
      return res.status(404).json({ msg: 'Booking not found' });
    }
    res.status(500).send('Server error');
  }
});

module.exports = router;

这段代码实现了四个路由:

  • GET /api/bookings:获取当前登录用户的全部预订记录。
  • POST /api/bookings:创建一个新的预订。
  • PUT /api/bookings/:id:更新指定 ID 的预订。
  • DELETE /api/bookings/:id:删除指定 ID 的预订。

每个路由都使用了 auth 中间件来确保只有已认证的用户才能访问。此外,我们还添加了一些验证逻辑,确保用户输入的数据是有效的。

4.3 测试预订功能

现在你可以使用 Postman 或其他 API 测试工具来测试预订功能。以下是测试步骤:

  1. 发送一个 POST 请求到 /api/bookings,提供服务类型、日期和时间。你应该会收到一个新的预订记录。
  2. 发送一个 GET 请求到 /api/bookings,查看当前用户的全部预订记录。
  3. 发送一个 PUT 请求到 /api/bookings/:id,更新指定 ID 的预订记录。
  4. 发送一个 DELETE 请求到 /api/bookings/:id,删除指定 ID 的预订记录。

5. 错误处理和日志记录

在实际的生产环境中,错误处理和日志记录是非常重要的。我们需要确保应用程序能够优雅地处理各种异常情况,并记录下有用的信息以便排查问题。

5.1 错误处理中间件

我们可以创建一个全局的错误处理中间件,捕获所有未处理的错误并返回适当的响应。在 middleware 目录下创建一个名为 errorHandler.js 的文件,并添加以下代码:

const errorHandler = (err, req, res, next) => {
  console.error(err.stack);

  if (res.headersSent) {
    return next(err);
  }

  res.status(500).json({ msg: 'Server error' });
};

module.exports = errorHandler;

这个中间件会捕获所有未处理的错误,并将它们记录到控制台中。然后,它会返回一个 500 状态码和一条简短的错误消息。

5.2 日志记录

为了更好地跟踪应用程序的行为,我们可以使用日志记录工具。winston 是一个流行的 Node.js 日志库,支持多种日志级别和输出方式。我们可以在项目中安装 winston 并配置日志记录。

首先,安装 winston

npm install winston

然后,在 utils 目录下创建一个名为 logger.js 的文件,并添加以下代码:

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;

const myFormat = printf(({ level, message, label, timestamp }) => {
  return `${timestamp} [${label}] ${level}: ${message}`;
});

const logger = createLogger({
  level: 'info',
  format: combine(
    label({ label: 'booking-system' }),
    timestamp(),
    myFormat
  ),
  transports: [
    new transports.Console(),
    new transports.File({ filename: 'logs/error.log', level: 'error' }),
    new transports.File({ filename: 'logs/all.log' }),
  ],
});

module.exports = logger;

这段代码创建了一个 winston 日志实例,并配置了三种输出方式:

  • 控制台输出(Console):所有级别的日志都会输出到控制台。
  • 错误日志文件(error.log):只记录错误级别的日志。
  • 全部日志文件(all.log):记录所有级别的日志。

你可以在代码中使用 logger 来记录日志。例如:

const logger = require('../utils/logger');

// 记录一条信息日志
logger.info('User registered successfully');

// 记录一条错误日志
logger.error('Failed to save booking');

5.3 使用错误处理中间件

最后,我们需要在 server.js 中引入错误处理中间件和日志记录功能。修改 server.js 文件如下:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const logger = require('./utils/logger');
const errorHandler = require('./middleware/errorHandler');
const authRoutes = require('./routes/auth');
const bookingRoutes = require('./routes/bookings');

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/bookings', bookingRoutes);

// Error handling middleware
app.use(errorHandler);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => logger.info('MongoDB connected'))
.catch(err => logger.error(err));

// Start server
app.listen(PORT, () => {
  logger.info(`Server running on port ${PORT}`);
});

现在,你的应用程序不仅能够优雅地处理错误,还能够记录详细的日志信息,帮助你在出现问题时快速定位和解决问题。

6. 总结

恭喜你,你已经成功完成了一个基于 Node.js 的预订和预约系统的后端开发!在这个过程中,我们学习了如何使用 Express 框架搭建服务器,如何使用 Mongoose 与 MongoDB 进行交互,如何实现用户认证和授权,以及如何处理错误和记录日志。

当然,这只是一个基础的系统,还有很多可以改进的地方。例如,你可以添加更多的功能,如邮件通知、支付集成、管理员界面等。你还可以优化性能,提升安全性,或者将其部署到云平台上。

希望这篇文章对你有所帮助,也期待你在未来的项目中不断探索和创新!如果有任何问题或建议,欢迎随时交流 😊


Q&A 环节

如果你有任何问题,或者想要了解更多关于某个特定部分的内容,请随时提问!我会尽力为你解答 🙌


感谢大家的参与!

今天的讲座到这里就结束了,希望大家都能有所收获。如果你觉得这篇文章对你有帮助,别忘了点赞和分享哦!再见啦,祝你编程愉快 💻✨

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注