引言:积分制商城系统的核心价值与挑战

在当今电商竞争激烈的环境中,用户留存和复购是决定平台生死的关键指标。传统的促销方式如折扣、满减虽然有效,但成本高昂且难以形成用户粘性。积分制商城系统应运而生,它通过虚拟货币(积分)激励用户持续活跃、完成特定行为(如注册、签到、购物、分享),从而实现低成本的用户留存和复购提升。

积分制商城的核心逻辑是:用户行为 → 积分奖励 → 积分消费 → 行为循环。这个闭环如果设计得当,能将一次性用户转化为忠实粉丝。然而,搭建一个高效的积分系统并非易事,它涉及复杂的业务逻辑、数据模型、并发处理和风控机制。本文将从零开始,详细指导你搭建一个完整的积分制商城系统源码,重点解决用户留存与复购难题。我们将使用 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,提高性能(积分扣减需原子操作)。
  • 异步任务处理积分流水,防止阻塞主流程。
  • 模块化便于扩展,例如添加微信小程序接口。

环境准备

  1. 安装 Node.js (v18+) 和 MySQL (v8+)。
  2. 初始化项目:npm init -y,安装依赖:npm i express mysql2 redis jsonwebtoken bcryptjs cors dotenv
  3. 创建 .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_balancetotal_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_tasksorders,计算 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 本地测试

  1. 启动 MySQL 和 Redis:mysql.server startredis-server
  2. 运行:node app.js
  3. 测试端点:
    • 注册: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 生产部署

  1. 环境:使用 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。
  2. 云服务:部署到阿里云/腾讯云,使用 Nginx 反向代理,HTTPS 配置。
  3. 扩展:高并发时,使用 PM2 集群:pm2 start app.js -i max
  4. 备份:定期备份 MySQL,使用 Redis 持久化(AOF)。

成本估算:小型系统(1000用户),云服务器 ~200元/月。

结语:持续迭代与价值

通过以上步骤,你已从零搭建了一个完整的积分制商城系统源码。核心在于:积分驱动行为,数据驱动优化。留存率可通过签到提升20-30%,复购率通过推荐提升15%以上。实际应用中,监控用户反馈,迭代规则(如动态积分价值)。

如果需要前端集成、微信支付或更高级功能(如 AI 推荐),可基于此扩展。遇到问题,优先检查事务和缓存一致性。祝你的商城大获成功!