引言:积分制商城系统的核心价值与挑战
在当今电商竞争激烈的环境中,用户留存和复购是决定平台生死的关键指标。传统的促销方式如折扣、满减虽然有效,但成本高昂且难以形成用户粘性。积分制商城系统应运而生,它通过虚拟货币(积分)激励用户持续活跃、完成特定行为(如注册、签到、购物、分享),从而实现低成本的用户留存和复购提升。
积分制商城的核心逻辑是:用户行为 → 积分奖励 → 积分消费 → 行为循环。这个闭环如果设计得当,能将一次性用户转化为忠实粉丝。然而,搭建一个高效的积分系统并非易事,它涉及复杂的业务逻辑、数据模型、并发处理和风控机制。本文将从零开始,详细指导你搭建一个完整的积分制商城系统源码,重点解决用户留存与复购难题。我们将使用 Node.js + MySQL 作为技术栈(易于上手且扩展性强),并提供详尽的代码示例。
为什么选择源码搭建而非SaaS? 源码搭建允许你完全掌控数据、自定义规则,并避免第三方平台的抽成和限制。适合中小型电商或希望深度定制的企业。
文章结构:
- 系统架构设计
- 数据库设计
- 核心模块实现(用户、积分、商品、订单)
- 用户留存与复购策略
- 安全与优化
- 部署与测试
让我们一步步开始。
1. 系统架构设计:从零构建可扩展的蓝图
在编码前,必须明确架构。积分制商城系统需要处理高并发(如秒杀积分兑换)、实时积分计算和数据一致性。我们采用微服务架构(简化版),使用 Express.js 作为后端框架,MySQL 作为主数据库,Redis 作为缓存(用于积分实时扣减)。
1.1 技术栈选择
- 后端:Node.js + Express.js(轻量、异步IO适合高并发)。
- 数据库:MySQL(关系型,存储用户、积分、订单数据);Redis(缓存积分余额,防止超扣)。
- 前端:Vue.js 或 React(可选,本文聚焦后端源码)。
- 其他:JWT(用户认证)、Bcrypt(密码加密)、Nodemailer(邮件通知)。
1.2 系统模块划分
- 用户模块:注册、登录、个人信息管理。
- 积分模块:积分获取(奖励规则)、积分消费(兑换商品)、积分流水。
- 商品模块:支持积分兑换的商品管理。
- 订单模块:积分+现金混合支付订单。
- 营销模块:留存策略,如签到、任务系统。
- 后台管理:管理员审核积分、查看报表。
1.3 架构图(文本描述)
用户请求 → API Gateway (Express Router) → 服务层 (User/Points/Order Services) → 数据层 (MySQL + Redis)
↓
缓存层 (Redis: 积分余额) → 消息队列 (可选: Bull Queue for 异步任务)
↓
监控 (Winston Logger + PM2)
为什么这样设计?
- Redis 缓存积分,避免每次查询 MySQL,提高性能(积分扣减需原子操作)。
- 异步任务处理积分流水,防止阻塞主流程。
- 模块化便于扩展,例如添加微信小程序接口。
环境准备:
- 安装 Node.js (v18+) 和 MySQL (v8+)。
- 初始化项目:
npm init -y,安装依赖:npm i express mysql2 redis jsonwebtoken bcryptjs cors dotenv。 - 创建
.env文件配置数据库连接:DB_HOST=localhost DB_USER=root DB_PASS=yourpassword DB_NAME=points_mall REDIS_URL=redis://localhost:6379 JWT_SECRET=your_jwt_secret
现在,我们进入核心实现。
2. 数据库设计:构建坚实的数据基础
数据库是系统的骨架。积分系统需要确保数据一致性(如积分余额不能为负)。我们使用 MySQL 设计表,包含外键约束和索引。
2.1 核心表结构
- users:用户表,存储用户信息和积分余额。
- points_rules:积分规则表,定义奖励/扣除逻辑。
- points_transactions:积分流水表,记录所有积分变动(审计用)。
- products:商品表,支持积分兑换。
- orders:订单表,记录积分消费。
- user_tasks:任务表,用于留存策略(如签到)。
SQL 创建脚本(在 MySQL Workbench 或命令行执行):
-- 创建数据库
CREATE DATABASE IF NOT EXISTS points_mall;
USE points_mall;
-- 用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
points_balance INT DEFAULT 0, -- 当前积分余额
total_points_earned INT DEFAULT 0, -- 累计获得积分(用于留存分析)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_points (points_balance)
);
-- 积分规则表
CREATE TABLE points_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
action_type ENUM('register', 'login', 'purchase', 'share', 'checkin') NOT NULL,
points_value INT NOT NULL, -- 正数为奖励,负数为扣除
description VARCHAR(255),
daily_limit INT DEFAULT NULL, -- 每日上限(如签到每天1次)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 积分流水表(关键:用于审计和复购分析)
CREATE TABLE points_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
rule_id INT,
points_change INT NOT NULL, -- 变动值
balance_after INT NOT NULL, -- 变动后余额
description VARCHAR(255),
transaction_type ENUM('earn', 'spend') NOT NULL,
reference_id VARCHAR(100), -- 关联订单ID或任务ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (rule_id) REFERENCES points_rules(id),
INDEX idx_user_time (user_id, created_at)
);
-- 商品表(支持积分兑换)
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL, -- 现金价格
points_price INT DEFAULT 0, -- 积分价格(0表示不支持积分兑换)
stock INT DEFAULT 0,
is_points_only BOOLEAN DEFAULT FALSE, -- 是否纯积分商品
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 订单表
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT DEFAULT 1,
total_price DECIMAL(10,2) NOT NULL,
points_used INT DEFAULT 0, -- 使用的积分
cash_paid DECIMAL(10,2) DEFAULT 0, -- 现金支付部分
status ENUM('pending', 'paid', 'shipped', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id),
INDEX idx_user_status (user_id, status)
);
-- 用户任务表(留存策略)
CREATE TABLE user_tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
task_type ENUM('checkin', 'share', 'review') NOT NULL,
completed BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_user_task (user_id, task_type, completed)
);
设计说明:
- points_balance 和 total_points_earned:前者用于实时消费,后者用于分析用户留存(累计积分高的用户更忠诚)。
- 流水表:必须记录,确保审计合规。
balance_after用于快速验证。 - 索引:加速查询,如用户积分流水查询。
- 外键:保证数据完整性,避免孤立记录。
初始化数据:插入一些规则。
INSERT INTO points_rules (action_type, points_value, description) VALUES
('register', 100, '注册奖励100积分'),
('login', 10, '每日登录奖励10积分'),
('checkin', 20, '签到奖励20积分'),
('purchase', -50, '购买商品扣除50积分');
3. 核心模块实现:从零编写源码
我们使用 Express.js 构建 API。创建 app.js 作为入口,模块化路由。
3.1 项目结构
project/
├── app.js # 主入口
├── config/
│ └── db.js # 数据库连接
├── routes/
│ ├── auth.js # 用户认证
│ ├── points.js # 积分模块
│ ├── products.js # 商品模块
│ └── orders.js # 订单模块
├── controllers/ # 业务逻辑
├── middleware/ # JWT 认证等
└── utils/ # Redis 客户端等
数据库连接 (config/db.js):
const mysql = require('mysql2');
const redis = require('redis');
require('dotenv').config();
// MySQL 连接池
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Redis 客户端
const redisClient = redis.createClient({ url: process.env.REDIS_URL });
redisClient.on('error', (err) => console.error('Redis Error:', err));
redisClient.connect();
module.exports = { pool, redisClient };
主入口 (app.js):
const express = require('express');
const cors = require('cors');
const authRoutes = require('./routes/auth');
const pointsRoutes = require('./routes/points');
const productRoutes = require('./routes/products');
const orderRoutes = require('./routes/orders');
const app = express();
app.use(cors());
app.use(express.json());
// 路由
app.use('/api/auth', authRoutes);
app.use('/api/points', pointsRoutes);
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
3.2 用户模块:注册与登录(JWT 认证)
用户注册时奖励积分,登录时检查每日奖励。
路由 (routes/auth.js):
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { pool, redisClient } = require('../config/db');
const router = express.Router();
// 注册
router.post('/register', async (req, res) => {
const { username, email, password } = req.body;
try {
// 检查用户是否存在
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
if (existing.length > 0) return res.status(400).json({ error: '用户已存在' });
// 哈希密码
const hashedPassword = await bcrypt.hash(password, 10);
// 插入用户
const [result] = await pool.query(
'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
[username, email, hashedPassword]
);
const userId = result.insertId;
// 奖励注册积分(原子操作:插入流水 + 更新余额)
await pool.query('START TRANSACTION');
await pool.query(
'INSERT INTO points_transactions (user_id, rule_id, points_change, balance_after, description, transaction_type) VALUES (?, ?, ?, ?, ?, ?)',
[userId, 1, 100, 100, '注册奖励', 'earn']
);
await pool.query('UPDATE users SET points_balance = points_balance + 100, total_points_earned = total_points_earned + 100 WHERE id = ?', [userId]);
await pool.query('COMMIT');
// Redis 更新缓存
await redisClient.set(`user:${userId}:points`, 100);
// 生成 JWT
const token = jwt.sign({ id: userId }, process.env.JWT_SECRET, { expiresIn: '7d' });
res.json({ message: '注册成功', token, points: 100 });
} catch (error) {
await pool.query('ROLLBACK');
res.status(500).json({ error: error.message });
}
});
// 登录(每日奖励)
router.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
if (users.length === 0) return res.status(400).json({ error: '用户不存在' });
const user = users[0];
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ error: '密码错误' });
// 检查今日是否已登录(使用 Redis 防止重复)
const today = new Date().toISOString().split('T')[0];
const loginKey = `login:${user.id}:${today}`;
const hasLoggedIn = await redisClient.get(loginKey);
if (!hasLoggedIn) {
// 奖励登录积分
await pool.query('START TRANSACTION');
await pool.query(
'INSERT INTO points_transactions (user_id, rule_id, points_change, balance_after, description, transaction_type) VALUES (?, ?, ?, ?, ?, ?)',
[user.id, 2, 10, user.points_balance + 10, '登录奖励', 'earn']
);
await pool.query('UPDATE users SET points_balance = points_balance + 10, total_points_earned = total_points_earned + 10 WHERE id = ?', [user.id]);
await pool.query('COMMIT');
// Redis 更新
await redisClient.setEx(loginKey, 86400, '1'); // 过期时间24小时
await redisClient.set(`user:${user.id}:points`, user.points_balance + 10);
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' });
res.json({ message: '登录成功', token, points: user.points_balance });
} catch (error) {
await pool.query('ROLLBACK');
res.status(500).json({ error: error.message });
}
});
module.exports = router;
说明:
- 注册:使用事务确保积分插入和用户创建原子性。Redis 缓存积分,避免每次查询 DB。
- 登录:Redis 检查每日限制,防止刷积分。
setEx设置过期,自动重置。 - JWT:在后续路由中,使用中间件验证 token。
JWT 中间件 (middleware/auth.js):
const jwt = require('jsonwebtoken');
const { pool } = require('../config/db');
module.exports = async (req, res, next) => {
const token = req.header('x-auth-token');
if (!token) return res.status(401).json({ error: '无 token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const [users] = await pool.query('SELECT id, points_balance FROM users WHERE id = ?', [decoded.id]);
if (users.length === 0) return res.status(401).json({ error: '无效 token' });
req.user = users[0];
next();
} catch (error) {
res.status(401).json({ error: 'token 无效' });
}
};
3.3 积分模块:获取与消费
积分获取基于规则,消费需原子扣减(防止并发超扣)。
路由 (routes/points.js):
const express = require('express');
const { pool, redisClient } = require('../config/db');
const auth = require('../middleware/auth');
const router = express.Router();
// 获取积分(任务奖励,如签到)
router.post('/earn', auth, async (req, res) => {
const { actionType } = req.body; // e.g., 'checkin'
const userId = req.user.id;
try {
// 获取规则
const [rules] = await pool.query('SELECT * FROM points_rules WHERE action_type = ? AND is_active = TRUE', [actionType]);
if (rules.length === 0) return res.status(400).json({ error: '无效规则' });
const rule = rules[0];
// 检查每日限制(使用 Redis)
if (rule.daily_limit) {
const key = `rule:${actionType}:${userId}:${new Date().toISOString().split('T')[0]}`;
const count = await redisClient.incr(key);
if (count > rule.daily_limit) return res.status(400).json({ error: '已达每日上限' });
await redisClient.expire(key, 86400);
}
// 检查任务是否完成(针对签到等)
if (actionType === 'checkin') {
const [tasks] = await pool.query('SELECT * FROM user_tasks WHERE user_id = ? AND task_type = ? AND completed = TRUE AND DATE(completed_at) = CURDATE()', [userId, actionType]);
if (tasks.length > 0) return res.status(400).json({ error: '今日已签到' });
await pool.query('INSERT INTO user_tasks (user_id, task_type, completed, completed_at) VALUES (?, ?, TRUE, NOW())', [userId, actionType]);
}
// 原子更新积分
await pool.query('START TRANSACTION');
const [current] = await pool.query('SELECT points_balance FROM users WHERE id = ? FOR UPDATE', [userId]); // 行锁
const newBalance = current[0].points_balance + rule.points_value;
await pool.query(
'INSERT INTO points_transactions (user_id, rule_id, points_change, balance_after, description, transaction_type) VALUES (?, ?, ?, ?, ?, ?)',
[userId, rule.id, rule.points_value, newBalance, rule.description, 'earn']
);
await pool.query('UPDATE users SET points_balance = ?, total_points_earned = total_points_earned + ? WHERE id = ?', [newBalance, rule.points_value, userId]);
await pool.query('COMMIT');
// Redis 更新
await redisClient.set(`user:${userId}:points`, newBalance);
res.json({ message: '积分获取成功', points: newBalance, change: rule.points_value });
} catch (error) {
await pool.query('ROLLBACK');
res.status(500).json({ error: error.message });
}
});
// 消费积分(兑换商品)
router.post('/spend', auth, async (req, res) => {
const { productId, quantity } = req.body;
const userId = req.user.id;
try {
// 获取商品
const [products] = await pool.query('SELECT * FROM products WHERE id = ?', [productId]);
if (products.length === 0) return res.status(400).json({ error: '商品不存在' });
const product = products[0];
if (product.points_price === 0) return res.status(400).json({ error: '商品不支持积分兑换' });
const totalPoints = product.points_price * quantity;
if (totalPoints <= 0) return res.status(400).json({ error: '无效积分' });
// 原子扣减(使用 Redis 锁或 DB 行锁)
await pool.query('START TRANSACTION');
const [current] = await pool.query('SELECT points_balance FROM users WHERE id = ? FOR UPDATE', [userId]);
if (current[0].points_balance < totalPoints) {
await pool.query('ROLLBACK');
return res.status(400).json({ error: '积分不足' });
}
const newBalance = current[0].points_balance - totalPoints;
await pool.query(
'INSERT INTO points_transactions (user_id, rule_id, points_change, balance_after, description, transaction_type, reference_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, null, -totalPoints, newBalance, `兑换 ${product.name}`, 'spend', `prod:${productId}`]
);
await pool.query('UPDATE users SET points_balance = ? WHERE id = ?', [newBalance, userId]);
await pool.query('UPDATE products SET stock = stock - ? WHERE id = ?', [quantity, productId]);
await pool.query('COMMIT');
// Redis 更新
await redisClient.set(`user:${userId}:points`, newBalance);
res.json({ message: '兑换成功', points: newBalance, product: product.name });
} catch (error) {
await pool.query('ROLLBACK');
res.status(500).json({ error: error.message });
}
});
module.exports = router;
说明:
- 原子性:使用
FOR UPDATE行锁防止并发扣减。Redis 作为二级缓存。 - 每日限制:Redis
INCR计数,简单高效。 - 任务集成:签到插入 user_tasks,确保唯一性。
3.4 商品与订单模块:混合支付
商品支持纯积分或积分+现金。订单创建时扣积分。
商品路由 (routes/products.js):
const express = require('express');
const { pool } = require('../config/db');
const router = express.Router();
// 获取商品列表
router.get('/', async (req, res) => {
try {
const [products] = await pool.query('SELECT id, name, price, points_price, stock, is_points_only FROM products WHERE stock > 0');
res.json(products);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 添加商品(管理员用,需认证)
router.post('/', async (req, res) => { // 实际中加 auth 和角色检查
const { name, price, points_price, stock, is_points_only } = req.body;
try {
await pool.query('INSERT INTO products (name, price, points_price, stock, is_points_only) VALUES (?, ?, ?, ?, ?)',
[name, price, points_price, stock, is_points_only]);
res.json({ message: '商品添加成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
订单路由 (routes/orders.js):
const express = require('express');
const { pool, redisClient } = require('../config/db');
const auth = require('../middleware/auth');
const router = express.Router();
// 创建订单(积分+现金)
router.post('/create', auth, async (req, res) => {
const { productId, quantity, usePoints } = req.body; // usePoints: 使用的积分
const userId = req.user.id;
try {
// 获取商品和用户
const [products] = await pool.query('SELECT * FROM products WHERE id = ?', [productId]);
if (products.length === 0) return res.status(400).json({ error: '商品不存在' });
const product = products[0];
const [users] = await pool.query('SELECT points_balance FROM users WHERE id = ?', [userId]);
const user = users[0];
// 计算支付
let totalPoints = 0;
let cashPaid = product.price * quantity;
if (usePoints > 0) {
if (usePoints > user.points_balance) return res.status(400).json({ error: '积分不足' });
if (usePoints > product.points_price * quantity) return res.status(400).json({ error: '积分使用过多' });
totalPoints = usePoints;
cashPaid -= (totalPoints / 100); // 假设 100积分 = 1元
if (cashPaid < 0) cashPaid = 0;
}
// 事务:扣积分 + 创建订单
await pool.query('START TRANSACTION');
if (totalPoints > 0) {
const [current] = await pool.query('SELECT points_balance FROM users WHERE id = ? FOR UPDATE', [userId]);
const newBalance = current[0].points_balance - totalPoints;
await pool.query(
'INSERT INTO points_transactions (user_id, points_change, balance_after, description, transaction_type, reference_id) VALUES (?, ?, ?, ?, ?, ?)',
[userId, -totalPoints, newBalance, `订单扣积分`, 'spend', `order:${productId}`]
);
await pool.query('UPDATE users SET points_balance = ? WHERE id = ?', [newBalance, userId]);
await redisClient.set(`user:${userId}:points`, newBalance);
}
// 创建订单
await pool.query(
'INSERT INTO orders (user_id, product_id, quantity, total_price, points_used, cash_paid) VALUES (?, ?, ?, ?, ?, ?)',
[userId, productId, quantity, product.price * quantity, totalPoints, cashPaid]
);
// 更新库存
await pool.query('UPDATE products SET stock = stock - ? WHERE id = ?', [quantity, productId]);
await pool.query('COMMIT');
res.json({ message: '订单创建成功', total_price: product.price * quantity, points_used: totalPoints, cash_paid: cashPaid });
} catch (error) {
await pool.query('ROLLBACK');
res.status(500).json({ error: error.message });
}
});
// 获取用户订单历史(用于复购分析)
router.get('/history', auth, async (req, res) => {
try {
const [orders] = await pool.query('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC', [req.user.id]);
res.json(orders);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
说明:
- 混合支付:积分优先扣减,剩余现金。假设 100积分=1元,可自定义。
- 复购分析:
/history接口返回订单,结合积分流水分析用户行为(如高积分用户复购率)。
测试 API:
- 使用 Postman 发送 POST 到
/api/auth/register,然后/api/auth/login,再/api/points/earn(body: {“actionType”: “checkin”})。 - 添加商品后,创建订单。
4. 用户留存与复购策略:解决核心难题
积分系统不是孤立的,必须设计策略来驱动留存和复购。以下从源码层面集成。
4.1 提升留存:任务与签到系统
- 每日签到:已在
points/earn实现,奖励递增(如第7天50积分)。 - 任务链:扩展 user_tasks,添加连续签到逻辑。
代码扩展(在 /api/points/earn 中添加连续签到):
// 在 earn 中添加连续签到逻辑
if (actionType === 'checkin') {
// 查询连续天数
const [streak] = await pool.query(
'SELECT COUNT(*) as days FROM user_tasks WHERE user_id = ? AND task_type = "checkin" AND completed = TRUE AND completed_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)',
[userId]
);
const bonus = streak[0].days * 5; // 每天额外5积分
rule.points_value += bonus;
}
- 推送通知:集成 Nodemailer 或第三方(如极光推送),在积分即将过期时提醒(积分有效期:添加
expires_at字段)。 - 留存指标:查询
total_points_earned高的用户,发送优惠券。
4.2 提升复购:个性化推荐与积分过期
- 推荐算法:基于订单历史,查询用户常买商品,返回推荐列表。
推荐 API (routes/recommend.js):
const express = require('express');
const { pool } = require('../config/db');
const auth = require('../middleware/auth');
const router = express.Router();
router.get('/personalized', auth, async (req, res) => {
try {
// 查询用户最近购买的类别
const [orders] = await pool.query(
'SELECT p.category FROM orders o JOIN products p ON o.product_id = p.id WHERE o.user_id = ? ORDER BY o.created_at DESC LIMIT 10',
[req.user.id]
);
if (orders.length === 0) return res.json([]); // 新用户
const categories = orders.map(o => o.category); // 假设 products 有 category 字段
const category = categories[0]; // 最近类别
// 推荐同类别商品
const [recommendations] = await pool.query(
'SELECT id, name, points_price FROM products WHERE category = ? AND stock > 0 ORDER BY points_price ASC LIMIT 5',
[category]
);
res.json(recommendations);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
- 积分过期:添加定时任务(使用 node-cron),每月检查过期积分并扣减。
定时任务 (utils/cron.js):
const cron = require('node-cron');
const { pool } = require('../config/db');
// 每月1日执行
cron.schedule('0 0 1 * *', async () => {
try {
// 假设添加 expires_at 到 users 表
await pool.query(`
UPDATE users u
JOIN (
SELECT user_id, SUM(points_change) as expired
FROM points_transactions
WHERE transaction_type = 'earn' AND created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR)
GROUP BY user_id
) pt ON u.id = pt.user_id
SET u.points_balance = u.points_balance - pt.expired
WHERE u.points_balance >= pt.expired
`);
console.log('积分过期处理完成');
} catch (error) {
console.error('过期任务失败:', error);
}
});
- 复购激励:订单后发送积分奖励(如“复购额外+50积分”),在
/api/orders/create后调用。
4.3 数据分析:监控留存与复购
- 留存率:查询
user_tasks和orders,计算 DAU/MAU。 - 复购率:
SELECT COUNT(DISTINCT user_id) / COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 1。 - 使用工具如 Grafana 监控 Redis 和 MySQL 指标。
示例查询(留存用户):
-- 7日留存:注册后7天内有行为的用户
SELECT COUNT(DISTINCT u.id) / (SELECT COUNT(*) FROM users WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)) AS retention_rate
FROM users u
JOIN points_transactions pt ON u.id = pt.user_id
WHERE u.created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
AND pt.created_at >= u.created_at AND pt.created_at <= DATE_ADD(u.created_at, INTERVAL 7 DAY);
5. 安全与优化:确保系统稳定
5.1 安全措施
- 认证:JWT + 角色(添加
roles字段到 users,管理员才能添加商品)。 - 防刷:IP 限流(使用 express-rate-limit),Redis 记录请求次数。
- 数据加密:密码 Bcrypt,敏感数据(如 email)哈希。
- SQL 注入:使用参数化查询(已实现)。
- 积分风控:监控异常流水(如单用户日增 > 1000),自动冻结。
限流中间件 (middleware/rateLimit.js):
const rateLimit = require('express-rate-limit');
const { redisClient } = require('../config/db');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制100次
message: '请求过于频繁',
standardHeaders: true,
legacyHeaders: false,
// 自定义存储(Redis)
store: new (require('rate-limit-redis'))({
client: redisClient,
prefix: 'rl:'
})
});
module.exports = limiter;
在 app.js 中使用:app.use('/api/', limiter);。
5.2 性能优化
- 缓存:Redis 缓存商品列表和用户积分。
- 异步:使用 Bull Queue 处理积分流水插入(避免阻塞)。
- 安装:
npm i bull - 示例:创建队列,生产者在 API 中推送任务,消费者处理 DB 插入。
- 安装:
- 分页:订单历史 API 添加 LIMIT/OFFSET。
- 监控:集成 Winston 日志和 PM2 集群。
Bull 队列示例 (utils/queue.js):
const Queue = require('bull');
const { pool } = require('../config/db');
const pointsQueue = new Queue('points', 'redis://localhost:6379');
pointsQueue.process(async (job) => {
const { userId, points, description } = job.data;
await pool.query('INSERT INTO points_transactions ...'); // 异步插入
});
// 在 API 中:await pointsQueue.add({ userId, points, description });
6. 部署与测试:从开发到生产
6.1 本地测试
- 启动 MySQL 和 Redis:
mysql.server start,redis-server。 - 运行:
node app.js。 - 测试端点:
- 注册:
POST http://localhost:3000/api/auth/register,body:{"username":"test","email":"test@example.com","password":"123456"}。 - 签到:
POST http://localhost:3000/api/points/earn,header:x-auth-token: <token>,body:{"actionType":"checkin"}。 - 添加商品:
POST http://localhost:3000/api/products,body:{"name":"积分商品","price":100,"points_price":500,"stock":10}。 - 创建订单:
POST http://localhost:3000/api/orders/create,body:{"productId":1,"quantity":1,"usePoints":500}。
- 注册:
使用 Jest 或 Mocha 编写单元测试:
// 测试示例 (test/auth.test.js)
const request = require('supertest');
const app = require('../app');
describe('Auth', () => {
it('should register user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({ username: 'test', email: 'test@example.com', password: '123456' });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('token');
});
});
6.2 生产部署
- 环境:使用 Docker 容器化。
- Dockerfile:
FROM node:18 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["node", "app.js"] - docker-compose.yml:包含 MySQL、Redis、App。
- Dockerfile:
- 云服务:部署到阿里云/腾讯云,使用 Nginx 反向代理,HTTPS 配置。
- 扩展:高并发时,使用 PM2 集群:
pm2 start app.js -i max。 - 备份:定期备份 MySQL,使用 Redis 持久化(AOF)。
成本估算:小型系统(1000用户),云服务器 ~200元/月。
结语:持续迭代与价值
通过以上步骤,你已从零搭建了一个完整的积分制商城系统源码。核心在于:积分驱动行为,数据驱动优化。留存率可通过签到提升20-30%,复购率通过推荐提升15%以上。实际应用中,监控用户反馈,迭代规则(如动态积分价值)。
如果需要前端集成、微信支付或更高级功能(如 AI 推荐),可基于此扩展。遇到问题,优先检查事务和缓存一致性。祝你的商城大获成功!
