首页  >  文章  >  web前端  >  底层设计:轮询系统 - 使用 Nodejs

底层设计:轮询系统 - 使用 Nodejs

WBOY
WBOY原创
2024-08-31 13:03:07343浏览

Low-Level Design: Polling System - Using Nodejs

目录

  1. 数据库设置
    • MySQL 数据库架构
    • 投票系统的 ERD
  2. 后端设置
    • 第 1 步:初始化项目
    • 第 2 步:项目结构
  3. API实施
    • 第 1 步:数据库连接 (db/db.js)
    • 第 2 步:环境变量 (.env)
    • 第 3 步:轮询控制器 (controllers/pollController.js)
    • 第 4 步:投票路由 (routes/pollRoutes.js)
    • 第 5 步:服务器入口点 (index.js)
  4. 错误处理
  5. 测试
  6. 结论

请参考文章轮询系统基础底层设计-I

让我们将整个过程分解为详细的步骤,包括数据库设置、使用 Node.js 和 Express 实现 API 以及与 MySQL 的交互。我们将涵盖:

数据库设置

首先,我们将定义 MySQL 数据库的架构并创建必要的表。

MySQL 数据库架构

CREATE DATABASE polling_system;

USE polling_system;

CREATE TABLE polls (
    poll_id INT AUTO_INCREMENT PRIMARY KEY,
    question VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE options (
    option_id INT AUTO_INCREMENT PRIMARY KEY,
    poll_id INT,
    option_text VARCHAR(255) NOT NULL,
    FOREIGN KEY (poll_id) REFERENCES polls(poll_id) ON DELETE CASCADE
);

CREATE TABLE votes (
    vote_id INT AUTO_INCREMENT PRIMARY KEY,
    poll_id INT,
    user_id VARCHAR(255) NOT NULL,
    option_id INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (poll_id) REFERENCES polls(poll_id) ON DELETE CASCADE,
    FOREIGN KEY (option_id) REFERENCES options(option_id) ON DELETE CASCADE
);
  • 投票表:存储带有唯一标识符、问题和创建时间戳的投票信息。

  • 选项表:存储与投票相关的选项,通过 poll_id 链接。

  • 投票表:记录每次投票,链接到民意调查、选项和用户。

    投票系统的 ERD

实体

  1. Polls:代表投票本身,具有 poll_id 和 Question 等属性。
  2. Options:代表每次投票可用的选项,具有 option_id 和 option_text 等属性。
  3. Votes:表示用户的投票数,具有vote_id、user_id、timestamps等属性。

关系

  1. 投票和选项之间的一对多:每个投票可以有多个选项。
  2. 投票和选项之间的多对一:每一票都与一个选项相关联。
  3. 投票和民意调查之间的多对一:每张投票都与特定的民意调查相关联。

这是 ERD 的描述:

  1. 投票表

    • poll_id(主键)
    • 问题
    • 创建于
  2. 选项表:

    • option_id(主键)
    • poll_id(外键引用 polls.poll_id)
    • 选项文本
  3. 投票表

    • vote_id(主键)
    • poll_id(外键引用 polls.poll_id)
    • option_id(外键引用 options.option_id)
    • user_id
    • 创建于

关系将用实体之间的线表示:

  • 民意调查选项:一项民意调查可以有多个选项。
  • 选项投票:一个选项可以有多个投票。
  • 民意调查投票:一项民意调查可以有很多票。

后端设置

让我们使用 Express 和 MySQL 设置一个 Node.js 项目。

第 1 步:初始化项目

mkdir polling-system
cd polling-system
npm init -y
npm install express mysql2 dotenv

  • express:Node.js 的 Web 框架。
  • mysql2:Node.js 的 MySQL 客户端。
  • dotenv:用于管理环境变量。

第 2 步:项目结构

为项目创建基本结构:

polling-system/
│
├── .env
├── index.js
├── db/
│   └── db.js
├── routes/
│   └── pollRoutes.js
└── controllers/
    └── pollController.js

API实施

第1步:数据库连接

文件 - db/db.js

const mysql = require('mysql2/promise');
require('dotenv').config();

const pool = mysql.createPool({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0
});

module.exports = pool;

第2步:环境变量

文件 - .env

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=polling_system
PORT=3000

第 3 步:投票控制器

文件-controllers/pollController.js

该文件将实现轮询系统所有必需的 CRUD 操作。

const pool = require('../db/db');

// Create Poll
exports.createPoll = async (req, res) => {
    const { question, options } = req.body;

    if (!question || !options || !Array.isArray(options) || options.length < 2) {
        return res.status(400).json({ message: "Invalid input data. Question and at least two options are required." });
    }

    try {
        const connection = await pool.getConnection();
        await connection.beginTransaction();

        const [result] = await connection.execute(
            'INSERT INTO polls (question) VALUES (?)',
            [question]
        );

        const pollId = result.insertId;

        const optionQueries = options.map(option => {
            return connection.execute(
                'INSERT INTO options (poll_id, option_text) VALUES (?, ?)',
                [pollId, option]
            );
        });

        await Promise.all(optionQueries);

        await connection.commit();
        connection.release();

        res.status(201).json({ pollId, message: "Poll created successfully." });

    } catch (error) {
        console.error("Error creating poll:", error.message);
        res.status(500).json({ message: "Error creating poll." });
    }
};

// Update Poll
exports.updatePoll = async (req, res) => {
    const { pollId } = req.params;
    const { question, options } = req.body;

    if (!pollId || !question || !options || !Array.isArray(options) || options.length < 2) {
        return res.status(400).json({ message: "Invalid input data. Question and at least two options are required." });
    }

    try {
        const connection = await pool.getConnection();
        await connection.beginTransaction();

        const [pollResult] = await connection.execute(
            'UPDATE polls SET question = ? WHERE poll_id = ?',
            [question, pollId]
        );

        if (pollResult.affectedRows === 0) {
            await connection.rollback();
            connection.release();
            return res.status(404).json({ message: "Poll not found." });
        }

        await connection.execute('DELETE FROM options WHERE poll_id = ?', [pollId]);

        const optionQueries = options.map(option => {
            return connection.execute(
                'INSERT INTO options (poll_id, option_text) VALUES (?, ?)',
                [pollId, option]
            );
        });

        await Promise.all(optionQueries);

        await connection.commit();
        connection.release();

        res.status(200).json({ message: "Poll updated successfully." });

    } catch (error) {
        console.error("Error updating poll:", error.message);
        res.status(500).json({ message: "Error updating poll." });
    }
};

// Delete Poll
exports.deletePoll = async (req, res) => {
    const { pollId } = req.params;

    try {
        const connection = await pool.getConnection();

        const [result] = await connection.execute(
            'DELETE FROM polls WHERE poll_id = ?',
            [pollId]
        );

        connection.release();

        if (result.affectedRows === 0) {
            return res.status(404).json({ message: "Poll not found." });
        }

        res.status(200).json({ message: "Poll deleted successfully." });

    } catch (error) {
        console.error("Error deleting poll:", error.message);
        res.status(500).json({ message: "Error deleting poll." });
    }
};

// Vote in Poll
exports.voteInPoll = async (req, res) => {
    const { pollId } = req.params;
    const { userId, option } = req.body;

    if (!userId || !option) {
        return res.status(400).json({ message: "User ID and option are required." });
    }

    try {
        const connection = await pool.getConnection();

        const [userVote] = await connection.execute(
            'SELECT * FROM votes WHERE poll_id = ? AND user_id = ?',
            [pollId, userId]
        );

        if (userVote.length > 0) {
            connection.release();
            return res.status(400).json({ message: "User has already voted." });
        }

        const [optionResult] = await connection.execute(
            'SELECT option_id FROM options WHERE poll_id = ? AND option_text = ?',
            [pollId, option]
        );

        if (optionResult.length === 0) {
            connection.release();
            return res.status(404).json({ message: "Option not found." });
        }

        const optionId = optionResult[0].option_id;

        await connection.execute(
            'INSERT INTO votes (poll_id, user_id, option_id) VALUES (?, ?, ?)',
            [pollId, userId, optionId]
        );

        connection.release();

        res.status(200).json({ message: "Vote cast successfully." });

    } catch (error) {
        console.error("Error casting vote:", error.message);
        res.status(500).json({ message: "Error casting vote." });
    }
};

// View Poll Results
exports.viewPollResults = async (req, res) => {
    const { pollId } = req.params;

    try {
        const connection = await pool.getConnection();

        const [poll] = await connection.execute(
            'SELECT * FROM polls WHERE poll_id = ?',
            [pollId]
        );

        if (poll.length === 0) {
            connection.release();
            return res.status(404).json({ message: "Poll not found." });
        }

        const [options] = await connection.execute(
            'SELECT option_text, COUNT(votes.option_id) as vote_count FROM options ' +
            'LEFT JOIN votes ON options.option_id = votes.option_id ' +
            'WHERE options.poll_id = ? GROUP BY options.option_id',
            [pollId]
        );

        connection.release();

        res.status(200).json({
            pollId: poll[0].poll_id,
            question: poll[0].question,
            results: options.reduce((acc, option) => {
                acc[option.option_text] = option.vote_count;
                return acc;
            }, {})
        });

    } catch (error) {
        console.error("Error viewing poll results:", error.message);
        res.status(500).json({ message: "Error viewing poll results." });
    }
};

第 4 步:投票路线

文件-routes/pollRoutes.js
定义每个 API 端点的路由:

const express = require('express');
const router = express.Router();
const pollController = require('../controllers/pollController');

//

 Routes
router.post('/polls', pollController.createPoll);
router.put('/polls/:pollId', pollController.updatePoll);
router.delete('/polls/:pollId', pollController.deletePoll);
router.post('/polls/:pollId/vote', pollController.voteInPoll);
router.get('/polls/:pollId/results', pollController.viewPollResults);

module.exports = router;

第 5 步:服务器入口点

文件 - index.js
最后,设置服务器:

const express = require('express');
const pollRoutes = require('./routes/pollRoutes');
require('dotenv').config();

const app = express();
app.use(express.json());

// Routes
app.use('/api', pollRoutes);

// Error Handling Middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ message: "Internal server error" });
});

// Start Server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

错误处理

每种方法都包含对常见问题的错误处理,例如无效输入、重复投票、缺少投票或选项以及服务器错误。

  • 输入验证:执行检查以确保输入有效,例如检查必填字段是否存在且格式正确。
  • 事务管理:对于涉及多个查询的操作(例如,创建或更新投票),事务用于确保一致性。

测试

使用Postman或curl等工具测试每个端点。

  • 创建投票:使用包含问题和选项数组的 JSON 正文来 POST /api/polls。
  • 更新投票:PUT /api/polls/:pollId 包含更新的问题和选项。
  • 删除投票: DELETE /api/polls/:pollId.
  • 投票投票:使用 userId 和选项 POST /api/polls/:pollId/vote。
  • 查看投票结果:GET /api/polls/:pollId/results。

结论

这是一个使用 Node.js、Express 和 MySQL 的在线投票系统的全面模块化实现。它处理基本的CRUD操作并确保数据与事务的一致性。它还包括基本的错误处理,以使 API 更加健壮和用户友好。

请参考文章轮询系统基础底层设计-I

更多详情:

获取所有与系统设计相关的文章
标签:SystemDesignWithZeeshanAli

系统设计与zeeshanali

Git:https://github.com/ZeeshanAli-0704/SystemDesignWithZeeshanAli

以上是底层设计:轮询系统 - 使用 Nodejs的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn