使用 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 可以用于缓存热点数据,提升性能;等等。
我们要做什么?
在这次讲座中,我们将逐步构建一个简单的在线广告平台的后端。这个平台将包括以下几个核心功能:
- 广告管理:管理员可以创建、编辑和删除广告。
- 广告投放:根据用户的兴趣、地理位置等条件,动态展示合适的广告。
- 广告统计:记录广告的展示次数、点击次数等数据,并生成报表。
- 用户管理:支持用户注册、登录和权限管理。
- 支付系统:允许广告主为广告投放付费。
我们会使用 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 构建高效、可扩展的后端系统。
如果你有任何问题或建议,欢迎在评论区留言!感谢大家的参与,下次再见!👋