使用 Node.js 开发在线广告平台的后端

使用 Node.js 开发在线广告平台的后端

引言 🎯

大家好,欢迎来到今天的讲座!今天我们要聊的是如何使用 Node.js 开发一个在线广告平台的后端。如果你是第一次接触 Node.js,或者对广告平台的技术实现感兴趣,那么这篇讲座绝对适合你!我们不仅会探讨如何搭建一个功能齐全的广告平台,还会深入讲解一些常见的技术挑战和解决方案。准备好了吗?让我们开始吧!

为什么选择 Node.js?

在选择后端技术栈时,Node.js 是一个非常流行的选择,尤其是在处理高并发、实时通信和事件驱动的应用场景中。Node.js 的非阻塞 I/O 模型使得它非常适合处理大量的网络请求,这对于广告平台来说尤为重要。广告平台通常需要处理来自多个来源的请求,比如用户点击、广告展示、数据统计等,而 Node.js 的异步特性正好可以应对这些需求。

此外,Node.js 还有一个庞大的生态系统,提供了丰富的第三方库和工具,可以帮助我们快速构建应用。比如,Express.js 是一个非常流行的 Web 框架,可以帮助我们快速搭建 API;MongoDB 是一个 NoSQL 数据库,适合存储灵活的广告数据;Redis 可以用于缓存热点数据,提升性能;等等。

我们要做什么?

在这次讲座中,我们将逐步构建一个简单的在线广告平台的后端。这个平台将包括以下几个核心功能:

  1. 广告管理:管理员可以创建、编辑和删除广告。
  2. 广告投放:根据用户的兴趣、地理位置等条件,动态展示合适的广告。
  3. 广告统计:记录广告的展示次数、点击次数等数据,并生成报表。
  4. 用户管理:支持用户注册、登录和权限管理。
  5. 支付系统:允许广告主为广告投放付费。

我们会使用 Node.js 作为主要的编程语言,结合 Express.js、MongoDB 和 Redis 等工具来实现这些功能。每个部分都会包含详细的代码示例和解释,帮助你更好地理解整个开发过程。

环境搭建 🛠️

在我们开始编写代码之前,首先需要准备好开发环境。以下是你需要安装的工具和依赖项:

1. Node.js 和 npm

Node.js 是 JavaScript 运行时环境,npm 是 Node.js 的包管理工具。你可以通过以下命令检查是否已经安装了 Node.js 和 npm:

node -v
npm -v

如果没有安装,可以通过 Node.js 官方网站 下载并安装最新版本。

2. MongoDB

MongoDB 是一个 NoSQL 数据库,适合存储灵活的文档数据。你可以通过以下命令启动 MongoDB 服务:

mongod

如果你使用的是 macOS 或 Linux,建议通过 Homebrew 或 apt-get 安装 MongoDB。Windows 用户可以直接下载安装包。

3. Redis

Redis 是一个内存中的键值存储系统,常用于缓存和消息队列。你可以通过以下命令启动 Redis 服务:

redis-server

同样,macOS 和 Linux 用户可以通过 Homebrew 或 apt-get 安装 Redis,Windows 用户可以下载官方的 Windows 版本。

4. Postman

Postman 是一个 API 测试工具,可以帮助我们方便地测试和调试 API。你可以从 Postman 官方网站 下载并安装。

5. IDE

你可以使用任何你喜欢的代码编辑器,比如 VSCode、WebStorm 或 Sublime Text。我个人推荐使用 VSCode,因为它有丰富的插件和扩展,能够大大提高开发效率。

项目初始化 📦

现在我们已经准备好开发环境,接下来让我们开始创建项目。首先,我们需要初始化一个新的 Node.js 项目,并安装一些必要的依赖项。

1. 初始化项目

在终端中创建一个新的文件夹,并进入该文件夹:

mkdir ad-platform
cd ad-platform

然后,使用 npm init 命令初始化项目:

npm init -y

这将会在当前目录下生成一个 package.json 文件,记录项目的依赖项和其他配置信息。

2. 安装依赖项

接下来,我们需要安装一些常用的依赖项。我们将使用以下库:

  • express:一个轻量级的 Web 框架,用于处理 HTTP 请求。
  • mongoose:一个 MongoDB ORM 库,用于与数据库交互。
  • bcrypt:用于加密用户密码。
  • jsonwebtoken:用于生成和验证 JWT(JSON Web Token),实现用户认证。
  • redis:用于连接 Redis 数据库,实现缓存功能。
  • dotenv:用于加载环境变量。

你可以通过以下命令安装这些依赖项:

npm install express mongoose bcrypt jsonwebtoken redis dotenv

3. 创建项目结构

为了保持代码的整洁和可维护性,我们可以按照以下结构组织项目文件:

ad-platform/
├── config/
│   └── db.js
├── controllers/
│   ├── adController.js
│   ├── authController.js
│   └── userController.js
├── models/
│   ├── Ad.js
│   ├── User.js
│   └── Click.js
├── routes/
│   ├── adRoutes.js
│   ├── authRoutes.js
│   └── userRoutes.js
├── utils/
│   └── token.js
├── .env
├── app.js
└── package.json
  • config/:存放数据库连接和其他配置文件。
  • controllers/:存放业务逻辑代码。
  • models/:存放数据库模型定义。
  • routes/:存放路由定义。
  • utils/:存放一些工具函数。
  • .env:存放环境变量。
  • app.js:应用程序的入口文件。

4. 配置环境变量

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

PORT=3000
MONGO_URI=mongodb://localhost:27017/ad_platform
REDIS_HOST=localhost
REDIS_PORT=6379
JWT_SECRET=mysecretkey

然后,在 config/db.js 中加载这些环境变量并连接到 MongoDB 和 Redis:

// config/db.js
require('dotenv').config();
const mongoose = require('mongoose');
const redis = require('redis');

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

// 连接到 Redis
const client = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
});

client.on('connect', () => {
  console.log('Redis connected');
});

client.on('error', (err) => {
  console.error('Redis error:', err);
});

module.exports = { mongoose, client };

5. 创建应用入口

app.js 中,我们将设置 Express 服务器,并引入路由和中间件:

// app.js
const express = require('express');
const mongoose = require('./config/db').mongoose;
const client = require('./config/db').client;
const cors = require('cors');
const adRoutes = require('./routes/adRoutes');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');

const app = express();

// 解析 JSON 请求体
app.use(express.json());

// 允许跨域请求
app.use(cors());

// 定义路由
app.use('/api/ads', adRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

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

用户管理与认证 🔒

在广告平台中,用户管理是一个非常重要的功能。我们需要支持用户注册、登录和权限管理。为了实现这些功能,我们将使用 JWT(JSON Web Token)进行用户认证。

1. 用户模型

首先,我们在 models/User.js 中定义用户模型:

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now },
});

// 在保存用户之前加密密码
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// 检查密码是否匹配
userSchema.methods.checkPassword = async function (password) {
  return await bcrypt.compare(password, this.password);
};

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

2. 注册接口

接下来,我们在 controllers/authController.js 中实现用户注册的逻辑:

// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const { generateToken } = require('../utils/token');

exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;

    // 检查用户是否已存在
    const existingUser = await User.findOne({ $or: [{ username }, { email }] });
    if (existingUser) {
      return res.status(400).json({ message: '用户名或邮箱已存在' });
    }

    // 创建新用户
    const user = new User({ username, email, password });
    await user.save();

    // 生成 JWT
    const token = generateToken(user._id, user.role);

    res.status(201).json({ token, user });
  } catch (err) {
    res.status(500).json({ message: '注册失败', error: err.message });
  }
};

3. 登录接口

同样地,我们实现用户登录的逻辑:

// controllers/authController.js
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // 查找用户
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ message: '用户不存在' });
    }

    // 检查密码是否匹配
    const isMatch = await user.checkPassword(password);
    if (!isMatch) {
      return res.status(400).json({ message: '密码错误' });
    }

    // 生成 JWT
    const token = generateToken(user._id, user.role);

    res.status(200).json({ token, user });
  } catch (err) {
    res.status(500).json({ message: '登录失败', error: err.message });
  }
};

4. JWT 工具函数

为了生成和验证 JWT,我们在 utils/token.js 中定义两个工具函数:

// utils/token.js
const jwt = require('jsonwebtoken');
const { JWT_SECRET } = process.env;

// 生成 JWT
exports.generateToken = (userId, role) => {
  return jwt.sign({ userId, role }, JWT_SECRET, { expiresIn: '1h' });
};

// 验证 JWT
exports.verifyToken = (token) => {
  try {
    return jwt.verify(token, JWT_SECRET);
  } catch (err) {
    return null;
  }
};

5. 路由定义

最后,我们在 routes/authRoutes.js 中定义注册和登录的路由:

// routes/authRoutes.js
const express = require('express');
const { register, login } = require('../controllers/authController');

const router = express.Router();

router.post('/register', register);
router.post('/login', login);

module.exports = router;

6. 测试 API

现在,我们可以在 Postman 中测试这些 API。首先,发送一个 POST 请求到 /api/auth/register,传入以下参数:

{
  "username": "testuser",
  "email": "test@example.com",
  "password": "password123"
}

如果注册成功,你会收到一个包含 JWT 的响应。接下来,使用相同的邮箱和密码发送一个 POST 请求到 /api/auth/login,你应该会收到相同的 JWT。

广告管理 📢

广告管理是广告平台的核心功能之一。我们需要支持广告的创建、编辑、删除和展示。为了实现这些功能,我们将使用 Mongoose 定义广告模型,并在控制器中实现相应的业务逻辑。

1. 广告模型

models/Ad.js 中,我们定义广告模型:

// models/Ad.js
const mongoose = require('mongoose');

const adSchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String, required: true },
  imageUrl: { type: String, required: true },
  link: { type: String, required: true },
  status: { type: String, enum: ['active', 'inactive'], default: 'inactive' },
  createdAt: { type: Date, default: Date.now },
  createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
});

module.exports = mongoose.model('Ad', adSchema);

2. 创建广告

controllers/adController.js 中,我们实现创建广告的逻辑:

// controllers/adController.js
const Ad = require('../models/Ad');
const { verifyToken } = require('../utils/token');

exports.createAd = async (req, res) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    const decoded = verifyToken(token);

    if (!decoded || decoded.role !== 'admin') {
      return res.status(403).json({ message: '权限不足' });
    }

    const { title, description, imageUrl, link } = req.body;

    const ad = new Ad({
      title,
      description,
      imageUrl,
      link,
      createdBy: decoded.userId,
    });

    await ad.save();

    res.status(201).json(ad);
  } catch (err) {
    res.status(500).json({ message: '创建广告失败', error: err.message });
  }
};

3. 获取广告列表

我们还需要实现获取广告列表的功能:

// controllers/adController.js
exports.getAds = async (req, res) => {
  try {
    const ads = await Ad.find().populate('createdBy', 'username');
    res.status(200).json(ads);
  } catch (err) {
    res.status(500).json({ message: '获取广告失败', error: err.message });
  }
};

4. 更新广告状态

为了控制广告的展示状态,我们可以实现更新广告状态的功能:

// controllers/adController.js
exports.updateAdStatus = async (req, res) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    const decoded = verifyToken(token);

    if (!decoded || decoded.role !== 'admin') {
      return res.status(403).json({ message: '权限不足' });
    }

    const { id, status } = req.params;

    const ad = await Ad.findById(id);
    if (!ad) {
      return res.status(404).json({ message: '广告不存在' });
    }

    ad.status = status;
    await ad.save();

    res.status(200).json(ad);
  } catch (err) {
    res.status(500).json({ message: '更新广告状态失败', error: err.message });
  }
};

5. 删除广告

最后,我们实现删除广告的功能:

// controllers/adController.js
exports.deleteAd = async (req, res) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    const decoded = verifyToken(token);

    if (!decoded || decoded.role !== 'admin') {
      return res.status(403).json({ message: '权限不足' });
    }

    const { id } = req.params;

    const ad = await Ad.findByIdAndDelete(id);
    if (!ad) {
      return res.status(404).json({ message: '广告不存在' });
    }

    res.status(200).json({ message: '广告删除成功' });
  } catch (err) {
    res.status(500).json({ message: '删除广告失败', error: err.message });
  }
};

6. 路由定义

routes/adRoutes.js 中,我们定义广告相关的路由:

// routes/adRoutes.js
const express = require('express');
const { createAd, getAds, updateAdStatus, deleteAd } = require('../controllers/adController');

const router = express.Router();

router.post('/', createAd);
router.get('/', getAds);
router.put('/:id/:status', updateAdStatus);
router.delete('/:id', deleteAd);

module.exports = router;

7. 测试 API

现在,我们可以在 Postman 中测试这些 API。首先,确保你已经登录并获得了 JWT。然后,发送一个 POST 请求到 /api/ads,传入以下参数:

{
  "title": "Test Ad",
  "description": "This is a test ad",
  "imageUrl": "https://example.com/image.jpg",
  "link": "https://example.com"
}

如果创建成功,你会收到一个包含广告信息的响应。接下来,你可以发送 GET 请求到 /api/ads 来获取所有广告的列表。

广告投放与统计 📊

广告投放和统计是广告平台的关键功能。我们需要根据用户的兴趣、地理位置等条件,动态展示合适的广告,并记录广告的展示次数和点击次数。

1. 广告展示

为了实现广告展示,我们可以在 controllers/adController.js 中添加一个 getActiveAds 方法,返回所有状态为 "active" 的广告:

// controllers/adController.js
exports.getActiveAds = async (req, res) => {
  try {
    const ads = await Ad.find({ status: 'active' }).populate('createdBy', 'username');
    res.status(200).json(ads);
  } catch (err) {
    res.status(500).json({ message: '获取活动广告失败', error: err.message });
  }
};

然后,在 routes/adRoutes.js 中定义相应的路由:

// routes/adRoutes.js
router.get('/active', getActiveAds);

2. 广告点击统计

为了记录广告的点击次数,我们可以在 models/Click.js 中定义点击模型:

// models/Click.js
const mongoose = require('mongoose');

const clickSchema = new mongoose.Schema({
  ad: { type: mongoose.Schema.Types.ObjectId, ref: 'Ad', required: true },
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  clickedAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Click', clickSchema);

接下来,在 controllers/adController.js 中实现记录点击的功能:

// controllers/adController.js
exports.recordClick = async (req, res) => {
  try {
    const { adId, userId } = req.params;

    const ad = await Ad.findById(adId);
    if (!ad) {
      return res.status(404).json({ message: '广告不存在' });
    }

    const user = await User.findById(userId);
    if (!user) {
      return res.status(404).json({ message: '用户不存在' });
    }

    const click = new Click({ ad: adId, user: userId });
    await click.save();

    res.status(200).json({ message: '点击记录成功' });
  } catch (err) {
    res.status(500).json({ message: '记录点击失败', error: err.message });
  }
};

最后,在 routes/adRoutes.js 中定义相应的路由:

// routes/adRoutes.js
router.post('/click/:adId/:userId', recordClick);

3. 广告统计报表

为了生成广告的统计报表,我们可以在 controllers/adController.js 中实现一个 getAdStats 方法,返回每个广告的展示次数和点击次数:

// controllers/adController.js
exports.getAdStats = async (req, res) => {
  try {
    const ads = await Ad.find().populate('createdBy', 'username');

    const stats = await Promise.all(
      ads.map(async (ad) => {
        const clicks = await Click.countDocuments({ ad: ad._id });
        return {
          ...ad.toObject(),
          clicks,
        };
      })
    );

    res.status(200).json(stats);
  } catch (err) {
    res.status(500).json({ message: '获取广告统计失败', error: err.message });
  }
};

然后,在 routes/adRoutes.js 中定义相应的路由:

// routes/adRoutes.js
router.get('/stats', getAdStats);

4. 测试 API

现在,我们可以在 Postman 中测试这些 API。首先,发送一个 GET 请求到 /api/ads/active,获取所有活动广告的列表。然后,发送一个 POST 请求到 /api/ads/click/:adId/:userId,记录广告的点击。最后,发送一个 GET 请求到 /api/ads/stats,获取广告的统计报表。

支付系统 💳

为了让广告主为广告投放付费,我们需要实现一个简单的支付系统。我们可以使用第三方支付网关(如 Stripe 或 PayPal)来处理支付,但在本次讲座中,我们将实现一个模拟的支付系统,以便更专注于广告平台的核心功能。

1. 支付模型

models/Payment.js 中,我们定义支付模型:

// models/Payment.js
const mongoose = require('mongoose');

const paymentSchema = new mongoose.Schema({
  ad: { type: mongoose.Schema.Types.ObjectId, ref: 'Ad', required: true },
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  amount: { type: Number, required: true },
  paidAt: { type: Date, default: Date.now },
  status: { type: String, enum: ['pending', 'completed'], default: 'pending' },
});

module.exports = mongoose.model('Payment', paymentSchema);

2. 创建支付

controllers/paymentController.js 中,我们实现创建支付的逻辑:

// controllers/paymentController.js
const Payment = require('../models/Payment');

exports.createPayment = async (req, res) => {
  try {
    const { adId, amount } = req.body;
    const userId = req.user._id; // 从 JWT 中获取用户 ID

    const payment = new Payment({ ad: adId, user: userId, amount });
    await payment.save();

    res.status(201).json(payment);
  } catch (err) {
    res.status(500).json({ message: '创建支付失败', error: err.message });
  }
};

3. 完成支付

为了模拟支付完成的过程,我们可以在 controllers/paymentController.js 中添加一个 completePayment 方法:

// controllers/paymentController.js
exports.completePayment = async (req, res) => {
  try {
    const { id } = req.params;

    const payment = await Payment.findById(id);
    if (!payment) {
      return res.status(404).json({ message: '支付记录不存在' });
    }

    payment.status = 'completed';
    await payment.save();

    res.status(200).json({ message: '支付完成' });
  } catch (err) {
    res.status(500).json({ message: '完成支付失败', error: err.message });
  }
};

4. 获取支付记录

我们还可以实现一个 getPayments 方法,返回用户的支付记录:

// controllers/paymentController.js
exports.getPayments = async (req, res) => {
  try {
    const userId = req.user._id; // 从 JWT 中获取用户 ID

    const payments = await Payment.find({ user: userId }).populate('ad', 'title');
    res.status(200).json(payments);
  } catch (err) {
    res.status(500).json({ message: '获取支付记录失败', error: err.message });
  }
};

5. 路由定义

routes/paymentRoutes.js 中,我们定义支付相关的路由:

// routes/paymentRoutes.js
const express = require('express');
const { createPayment, completePayment, getPayments } = require('../controllers/paymentController');

const router = express.Router();

router.post('/', createPayment);
router.put('/:id', completePayment);
router.get('/', getPayments);

module.exports = router;

6. 测试 API

现在,我们可以在 Postman 中测试这些 API。首先,发送一个 POST 请求到 /api/payments,传入以下参数:

{
  "adId": "60c8e4f5a9b3c4001b6d5f5f",
  "amount": 100
}

如果创建成功,你会收到一个包含支付信息的响应。接下来,发送一个 PUT 请求到 /api/payments/:id,完成支付。最后,发送一个 GET 请求到 /api/payments,获取用户的支付记录。

总结 🏁

恭喜你!我们已经完成了在线广告平台的后端开发。通过这次讲座,我们学习了如何使用 Node.js、Express.js、MongoDB 和 Redis 构建一个功能齐全的广告平台。我们实现了用户管理、广告管理、广告投放、广告统计和支付系统等功能,并且使用 JWT 实现了用户认证。

当然,这只是一个简单的示例项目,实际的广告平台可能会更加复杂,涉及到更多的功能和技术。例如,你可以考虑集成第三方支付网关、实现广告竞价系统、优化广告投放算法等。希望这次讲座能够为你提供一些启发,帮助你在未来的项目中更好地使用 Node.js 构建高效、可扩展的后端系统。

如果你有任何问题或建议,欢迎在评论区留言!感谢大家的参与,下次再见!👋

发表回复

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