使用 Node.js 开发在线调查和投票应用程序

使用 Node.js 开发在线调查和投票应用程序

前言

大家好,欢迎来到今天的讲座!今天我们要一起探讨如何使用 Node.js 开发一个在线调查和投票应用程序。如果你是第一次接触 Node.js,或者对开发这类应用感到好奇,那么你来对地方了!我们将从头到尾一步步地构建这个应用,确保每个环节都清晰易懂。

在开始之前,先让我介绍一下 Node.js。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它允许我们在服务器端运行 JavaScript 代码。Node.js 的非阻塞 I/O 模型使得它非常适合处理高并发的网络应用,比如我们今天要做的在线调查和投票系统。

好了,废话不多说,让我们直接进入正题吧!😊

1. 项目规划与需求分析

1.1 项目背景

假设你是一家公司或组织的 IT 部门负责人,领导希望你能开发一个在线调查和投票系统,用于收集员工的意见和建议。这个系统需要具备以下功能:

  • 用户可以创建调查问卷。
  • 用户可以选择不同的问题类型(如单选、多选、文本输入等)。
  • 用户可以匿名或实名参与投票。
  • 系统能够实时统计投票结果,并生成图表展示。
  • 管理员可以查看所有调查的详细数据,并导出为 CSV 文件。

听起来是不是有点复杂?别担心,我们会一步一步来实现这些功能。😎

1.2 技术栈选择

为了实现这个项目,我们需要选择合适的技术栈。以下是我们的技术选型:

  • 后端:Node.js + Express.js
  • 数据库:MongoDB(NoSQL 数据库,适合存储结构化和非结构化的数据)
  • 前端:HTML + CSS + JavaScript(React.js 或 Vue.js 可以作为可选框架)
  • 身份验证:JWT(JSON Web Token)
  • 图表展示:Chart.js(轻量级的图表库)

1.3 项目架构

我们的项目将分为前后端两部分:

  • 后端:负责处理业务逻辑、数据库操作、用户认证等功能。
  • 前端:负责展示页面、与用户交互、发送请求给后端。

后端和前端通过 RESTful API 进行通信。我们可以使用 Postman 或者浏览器的开发者工具来测试 API。

1.4 数据库设计

为了更好地理解如何设计数据库,我们先来看看系统的数据模型。我们将有以下几个主要的集合(Collection):

  • Users:存储用户信息(用户名、密码、角色等)。
  • Surveys:存储调查问卷的信息(标题、描述、创建者、创建时间等)。
  • Questions:存储每个调查中的问题(问题类型、选项、答案等)。
  • Votes:存储用户的投票记录(用户 ID、调查 ID、问题 ID、选择的答案等)。

1.4.1 Users 表结构

字段名 类型 描述
_id ObjectId 用户唯一标识
username String 用户名
password String 加密后的密码
role String 用户角色(管理员/普通用户)
createdAt Date 用户创建时间

1.4.2 Surveys 表结构

字段名 类型 描述
_id ObjectId 调查唯一标识
title String 调查标题
description String 调查描述
creator ObjectId 创建者 ID
createdAt Date 创建时间
questions Array 问题列表

1.4.3 Questions 表结构

字段名 类型 描述
_id ObjectId 问题唯一标识
surveyId ObjectId 所属调查 ID
type String 问题类型(单选/多选/文本)
questionText String 问题内容
options Array 选项列表

1.4.4 Votes 表结构

字段名 类型 描述
_id ObjectId 投票唯一标识
userId ObjectId 投票用户 ID
surveyId ObjectId 所属调查 ID
questionId ObjectId 所属问题 ID
answer Mixed 用户选择的答案

2. 环境搭建

2.1 安装 Node.js 和 npm

首先,我们需要安装 Node.js 和 npm(Node 包管理器)。你可以从 Node.js 官方网站下载最新版本的安装包。安装完成后,打开终端或命令行工具,输入以下命令来检查是否安装成功:

node -v
npm -v

如果看到版本号输出,说明安装成功!

2.2 初始化项目

接下来,我们创建一个新的项目文件夹,并初始化 npm 项目。在终端中执行以下命令:

mkdir survey-app
cd survey-app
npm init -y

npm init -y 会自动生成一个 package.json 文件,里面包含了项目的依赖信息。

2.3 安装依赖

我们需要安装一些常用的依赖包。执行以下命令来安装必要的依赖:

npm install express mongoose bcryptjs jsonwebtoken cors dotenv
  • express:Node.js 的 web 框架,用于快速搭建 API。
  • mongoose:用于连接 MongoDB 并进行数据操作的 ORM 库。
  • bcryptjs:用于加密用户密码。
  • jsonwebtoken:用于生成和验证 JWT。
  • cors:用于解决跨域问题。
  • dotenv:用于加载环境变量。

2.4 配置环境变量

为了保护敏感信息(如数据库连接字符串、JWT 密钥等),我们应该将这些信息放在 .env 文件中。在项目根目录下创建一个 .env 文件,并添加以下内容:

PORT=5000
MONGO_URI=mongodb://localhost:27017/survey-app
JWT_SECRET=your_jwt_secret_key

然后,在 index.js 中引入 dotenv 并加载环境变量:

require('dotenv').config();

2.5 连接 MongoDB

我们使用 Mongoose 来连接 MongoDB。在 index.js 中添加以下代码:

const mongoose = require('mongoose');

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

2.6 启动服务器

现在,我们可以编写一个简单的 Express 服务器来测试是否一切正常。在 index.js 中添加以下代码:

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

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

保存文件后,在终端中运行以下命令启动服务器:

node index.js

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

MongoDB connected
Server is running on port 5000

打开浏览器,访问 http://localhost:5000,你应该能看到 "Hello, World!" 的欢迎页面。🎉

3. 用户认证

3.1 注册和登录

为了让用户能够创建调查并参与投票,我们需要实现用户注册和登录功能。我们将使用 JWT 来进行身份验证。

3.1.1 注册用户

首先,我们创建一个 auth.js 文件,用于处理用户注册和登录的路由。在 routes 文件夹下创建 auth.js,并添加以下代码:

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// 注册用户
router.post('/register', async (req, res) => {
  const { username, password } = req.body;

  // 检查用户是否已存在
  const existingUser = await User.findOne({ username });
  if (existingUser) {
    return res.status(400).json({ message: 'User already exists' });
  }

  // 加密密码
  const hashedPassword = await bcrypt.hash(password, 10);

  // 创建新用户
  const newUser = new User({
    username,
    password: hashedPassword,
  });

  await newUser.save();

  res.status(201).json({ message: 'User registered successfully' });
});

module.exports = router;

3.1.2 登录用户

接下来,我们实现用户登录功能。在 auth.js 中添加以下代码:

// 登录用户
router.post('/login', async (req, res) => {
  const { username, password } = req.body;

  // 查找用户
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // 验证密码
  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // 生成 JWT
  const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
    expiresIn: '1h',
  });

  res.json({ token });
});

3.1.3 设置路由

index.js 中引入 auth.js 并设置路由:

const authRoutes = require('./routes/auth');

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

3.2 保护路由

为了确保只有经过身份验证的用户才能访问某些路由,我们需要在这些路由上添加 JWT 验证。我们可以在 middleware 文件夹下创建一个 authMiddleware.js 文件,并添加以下代码:

const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
  const token = req.header('Authorization')?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ message: 'Access denied' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(403).json({ message: 'Invalid token' });
  }
};

module.exports = authenticateToken;

然后,在需要保护的路由上使用这个中间件。例如,在 survey.js 中创建一个受保护的路由:

const express = require('express');
const router = express.Router();
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');

// 创建调查
router.post('/', authenticateToken, async (req, res) => {
  const { title, description, questions } = req.body;

  const newSurvey = new Survey({
    title,
    description,
    creator: req.user.id,
    questions,
  });

  await newSurvey.save();

  res.status(201).json(newSurvey);
});

module.exports = router;

4. 创建调查问卷

4.1 定义调查模型

models 文件夹下创建一个 Survey.js 文件,定义调查的 Mongoose 模型:

const mongoose = require('mongoose');

const questionSchema = new mongoose.Schema({
  type: { type: String, required: true },
  questionText: { type: String, required: true },
  options: [String],
});

const surveySchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String, required: true },
  creator: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  createdAt: { type: Date, default: Date.now },
  questions: [questionSchema],
});

module.exports = mongoose.model('Survey', surveySchema);

4.2 创建调查路由

routes 文件夹下创建一个 survey.js 文件,用于处理调查相关的路由。我们已经实现了创建调查的功能,接下来我们再添加获取所有调查和获取单个调查的路由:

const express = require('express');
const router = express.Router();
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');

// 获取所有调查
router.get('/', async (req, res) => {
  const surveys = await Survey.find().populate('creator', 'username');
  res.json(surveys);
});

// 获取单个调查
router.get('/:id', async (req, res) => {
  const survey = await Survey.findById(req.params.id).populate('creator', 'username');
  if (!survey) {
    return res.status(404).json({ message: 'Survey not found' });
  }
  res.json(survey);
});

// 创建调查
router.post('/', authenticateToken, async (req, res) => {
  const { title, description, questions } = req.body;

  const newSurvey = new Survey({
    title,
    description,
    creator: req.user.id,
    questions,
  });

  await newSurvey.save();

  res.status(201).json(newSurvey);
});

module.exports = router;

4.3 设置路由

index.js 中引入 survey.js 并设置路由:

const surveyRoutes = require('./routes/survey');

app.use('/api/surveys', surveyRoutes);

5. 投票功能

5.1 定义投票模型

models 文件夹下创建一个 Vote.js 文件,定义投票的 Mongoose 模型:

const mongoose = require('mongoose');

const voteSchema = new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  surveyId: { type: mongoose.Schema.Types.ObjectId, ref: 'Survey', required: true },
  questionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Question', required: true },
  answer: { type: mongoose.Schema.Types.Mixed, required: true },
});

module.exports = mongoose.model('Vote', voteSchema);

5.2 创建投票路由

routes 文件夹下创建一个 vote.js 文件,用于处理投票相关的路由。我们可以通过 POST 请求提交投票,并确保用户不能重复投票:

const express = require('express');
const router = express.Router();
const Vote = require('../models/Vote');
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');

// 提交投票
router.post('/:surveyId/:questionId', authenticateToken, async (req, res) => {
  const { surveyId, questionId } = req.params;
  const { answer } = req.body;

  // 检查用户是否已经投过票
  const existingVote = await Vote.findOne({
    userId: req.user.id,
    surveyId,
    questionId,
  });

  if (existingVote) {
    return res.status(400).json({ message: 'You have already voted' });
  }

  // 创建新投票
  const newVote = new Vote({
    userId: req.user.id,
    surveyId,
    questionId,
    answer,
  });

  await newVote.save();

  res.status(201).json(newVote);
});

module.exports = router;

5.3 设置路由

index.js 中引入 vote.js 并设置路由:

const voteRoutes = require('./routes/vote');

app.use('/api/surveys/:surveyId/questions/:questionId/vote', voteRoutes);

6. 实时统计与图表展示

6.1 实时统计

为了实现实时统计投票结果,我们可以在每次提交投票后更新调查的统计数据。我们可以在 vote.js 中添加一个钩子函数,在每次保存投票时更新调查的统计数据:

voteSchema.post('save', async function () {
  const survey = await Survey.findById(this.surveyId);
  const question = survey.questions.id(this.questionId);

  if (question.type === 'multiple-choice') {
    question.options.forEach((option, index) => {
      if (this.answer === index) {
        option.votes++;
      }
    });
  } else if (question.type === 'text') {
    question.answers.push(this.answer);
  }

  await survey.save();
});

6.2 图表展示

为了展示投票结果,我们可以使用 Chart.js 库。在前端页面中引入 Chart.js,并根据后端返回的数据生成图表。以下是一个简单的示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Survey Results</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
  <canvas id="myChart" width="400" height="200"></canvas>
  <script>
    fetch('/api/surveys/123')
      .then(response => response.json())
      .then(data => {
        const labels = data.questions[0].options;
        const votes = data.questions[0].votes;

        const ctx = document.getElementById('myChart').getContext('2d');
        new Chart(ctx, {
          type: 'bar',
          data: {
            labels: labels,
            datasets: [{
              label: 'Votes',
              data: votes,
              backgroundColor: 'rgba(75, 192, 192, 0.2)',
              borderColor: 'rgba(75, 192, 192, 1)',
              borderWidth: 1
            }]
          },
          options: {
            scales: {
              y: {
                beginAtZero: true
              }
            }
          }
        });
      });
  </script>
</body>
</html>

7. 导出数据

7.1 导出为 CSV

为了方便管理员导出调查数据,我们可以实现一个导出功能。在 survey.js 中添加一个导出路由:

const csvParser = require('json2csv').parse;

// 导出调查数据为 CSV
router.get('/:id/export', authenticateToken, async (req, res) => {
  const survey = await Survey.findById(req.params.id).populate('creator', 'username');

  const fields = ['Question', 'Options', 'Votes'];
  const data = survey.questions.map(question => {
    return {
      Question: question.questionText,
      Options: question.options.join(', '),
      Votes: question.votes.join(', '),
    };
  });

  const csv = csvParser({ data, fields });

  res.setHeader('Content-Disposition', 'attachment; filename=survey-results.csv');
  res.set('Content-Type', 'text/csv');
  res.status(200).send(csv);
});

8. 总结

恭喜你!我们已经成功完成了一个完整的在线调查和投票应用程序的开发。通过这次讲座,你学会了如何使用 Node.js、Express、MongoDB 和 JWT 来构建一个功能丰富的后端 API,并结合前端技术展示了调查结果。

当然,这只是一个基础版本,你还可以根据实际需求进一步扩展和完善这个应用。比如:

  • 添加更多的问题类型(如评分、矩阵等)。
  • 实现更复杂的权限管理(如不同角色的用户可以有不同的操作权限)。
  • 优化性能,使用缓存机制来提高响应速度。
  • 添加更多的图表类型,提供更丰富的数据分析功能。

希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎随时留言交流。😊

祝你编码愉快,再见!👋

发表回复

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