SELECTED PROJECTS

账号管理与兑换平台:三端架构复盘

记录账号、兑换记录、后台与客户端分离的架构,以及事务和通知一致性问题。

阅读设置

项目文稿说明:本文按现有笔记整理,代码是节选或设计示意;仓库实现、线上地址和测试结果尚未逐项核验。后续计划不代表已经完成。

从零搭建 Cursor 账号管理与分发平台:架构、实战与踩坑记录

博客:菜牙爱吃菜 技术栈:Node.js + Express + MySQL + React + Ant Design + Vite 线上地址:app.example.com | 管理后台


一、写在前面:为什么会有这个项目

Cursor 的账号需求量一直挺大的——学生、独立开发者、小团队,都想低成本用上 Cursor Pro。但手动管理一堆账号、卡密、兑换记录,很快就会变成噩梦。

于是就有了这个项目:Cursor 账号管理与分发平台。简单说就是一个系统,让管理员能批量管理 Cursor 账号和对应的卡密(Card Key),客户端用户可以在前台查看和兑换自己的卡密,所有记录落库、通知邮件自动发送。

项目从 2025 年 12 月开始迭代,到现在已经比较稳定了。这篇文章做一次完整的复盘,把架构设计、数据库建模、API 设计、部署经验都记录下来,权当个人技术笔记,也希望能帮到有需要的人。


二、整体架构:三服务分离

整个平台由三个服务组成,各跑各的端口,互不干扰:

┌─────────────────────────────────────────────┐
│              生产服务器 198.51.100.10      │
│                                             │
│  ┌───────────┐  ┌───────────┐  ┌──────────┐ │
│  │  Client   │  │   Admin   │  │  Backend │ │
│  │  Frontend │  │ Frontend  │  │  Server  │ │
│  │  :5174    │  │  :5173    │  │  :3000   │ │
│  └─────┬─────┘  └─────┬─────┘  └────┬─────┘ │
│        │              │              │       │
│        └──────────────┴──────────────┘       │
│                       │                      │
│                 ┌─────▼─────┐                │
│                 │   MySQL   │                │
│                 └───────────┘                │
└─────────────────────────────────────────────┘
服务 端口 域名 职责
Client Frontend 5174 app.example.com 客户端:查看卡密、兑换记录
Admin Frontend 5173 admin.example.com 管理后台:账号管理、卡密管理、Agent 管理
Backend API 3000 — 所有业务逻辑、数据库操作、JWT 认证

三个服务跑在同一台阿里云 ECS 上,用 Nginx 做反向代理,把域名指到对应端口。


三、技术栈一览

层级 选型 说明
后端框架 Node.js + Express 轻量、生态好,CRUD 场景够用
数据库 MySQL 8 关系型,数据一致性有保障
ORM 原生 mysql2 + 手写 SQL 项目不复杂,ORM 反而添乱
认证 JWT (jsonwebtoken) 无状态、易扩展,管理后台和客户端共用
管理后台 React + Ant Design + Vite Ant Design 的 Table/Form 开箱即用
客户端 React + Vite 轻量 SPA
邮件通知 nodemailer 兑换成功后自动发邮件
部署 Nginx + PM2 经典组合

四、数据库设计

数据库是整个系统的核心。原稿计划了 6 张表,以下展示其中 5 张:

4.1 用户表(users)

CREATE TABLE users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  email VARCHAR(100),
  role ENUM('admin', 'agent', 'client') DEFAULT 'client',
  status ENUM('active', 'disabled') DEFAULT 'active',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 账号表(cursor_accounts)

CREATE TABLE cursor_accounts (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(100) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL,
  plan_type ENUM('free', 'pro', 'business') DEFAULT 'pro',
  status ENUM('available', 'used', 'expired') DEFAULT 'available',
  agent_id INT UNSIGNED,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (agent_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.3 卡密表(card_keys)

CREATE TABLE card_keys (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  key_code VARCHAR(64) NOT NULL UNIQUE,
  account_id INT UNSIGNED NOT NULL,
  status ENUM('unused', 'redeemed', 'expired') DEFAULT 'unused',
  redeemed_by INT UNSIGNED,
  redeemed_at TIMESTAMP NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (account_id) REFERENCES cursor_accounts(id),
  FOREIGN KEY (redeemed_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.4 兑换记录表(redemption_records)

CREATE TABLE redemption_records (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id INT UNSIGNED NOT NULL,
  card_key_id INT UNSIGNED NOT NULL,
  account_id INT UNSIGNED NOT NULL,
  redeemed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (card_key_id) REFERENCES card_keys(id),
  FOREIGN KEY (account_id) REFERENCES cursor_accounts(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.5 会话表(sessions)

CREATE TABLE sessions (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id INT UNSIGNED NOT NULL,
  token VARCHAR(512) NOT NULL,
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计要点

  • JWT + 双 Token 机制:accessToken 短期有效(2 小时),refreshToken 长期有效(7 天),refreshToken 存 sessions 表,可以主动踢人。
  • 状态机:卡密的状态流转 unused → redeemed → expired,账号状态 available → used / expired,用枚举约束。
  • 外键约束:核心关系用外键保证一致性,删除策略需逐表确认,并非所有关系都设置了级联删除。
  • 时间戳:表中按用途记录 created_at 或 redeemed_at 等时间,方便后续做数据统计和审计。

五、API 设计

后端遵循 RESTful 风格,统一返回格式:

{
  "code": 0,
  "message": "success",
  "data": { }
}

code 为 0 表示成功,非 0 表示错误,message 为错误描述。

5.1 认证相关

方法 路径 说明
POST /api/auth/register 用户注册
POST /api/auth/login 登录,返回双 Token
POST /api/auth/refresh 刷新 Token
POST /api/auth/logout 登出,删除会话

5.2 账号管理(管理员)

方法 路径 说明
GET /api/accounts 账号列表(分页、筛选)
POST /api/accounts 新增账号
PUT /api/accounts/:id 更新账号
DELETE /api/accounts/:id 删除账号
POST /api/accounts/batch 批量导入

5.3 卡密管理

方法 路径 说明
GET /api/keys 卡密列表
POST /api/keys 生成卡密
PUT /api/keys/:id/status 更新卡密状态
DELETE /api/keys/:id 删除卡密

5.4 兑换(客户端)

方法 路径 说明
GET /api/redeem/available 查询可用卡密
POST /api/redeem 执行兑换
GET /api/redeem/my-records 我的兑换记录

5.5 典型 API 代码示例

以兑换卡密接口为例:

// routes/redeem.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const db = require('../db');

// POST /api/redeem
router.post('/', authenticateToken, async (req, res) => {
  const userId = req.user.id;
  const { keyCode } = req.body;

  if (!keyCode) {
    return res.json({ code: 1, message: '卡密不能为空' });
  }

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

    // 1. 查询卡密是否存在且未使用
    const [keys] = await connection.execute(
      'SELECT * FROM card_keys WHERE key_code = ? AND status = "unused" FOR UPDATE',
      [keyCode]
    );

    if (keys.length === 0) {
      await connection.rollback();
      return res.json({ code: 1, message: '卡密无效或已被使用' });
    }

    const cardKey = keys[0];

    // 2. 更新卡密状态
    await connection.execute(
      'UPDATE card_keys SET status = "redeemed", redeemed_by = ?, redeemed_at = NOW() WHERE id = ?',
      [userId, cardKey.id]
    );

    // 3. 更新账号状态为已使用
    await connection.execute(
      'UPDATE cursor_accounts SET status = "used" WHERE id = ?',
      [cardKey.account_id]
    );

    // 4. 写入兑换记录
    await connection.execute(
      'INSERT INTO redemption_records (user_id, card_key_id, account_id) VALUES (?, ?, ?)',
      [userId, cardKey.id, cardKey.account_id]
    );

    await connection.commit();
    // 简化示意:先提交,再发通知;生产应考虑可靠 outbox/队列
    sendRedeemEmail(userId, cardKey.account_id).catch(console.error);
    res.json({
      code: 0,
      message: '兑换成功',
      data: { accountId: cardKey.account_id }
    });
  } catch (err) {
    await connection.rollback();
    console.error('Redeem error:', err);
    res.json({ code: 1, message: '服务器错误' });
  } finally {
    connection.release();
  }
});

module.exports = router;

几个关键设计点:

  • 事务:兑换是一个多步操作,必须用事务保证一致性,任何一步失败全部回滚。
  • FOR UPDATE:查卡密时加行锁,防止并发情况下同一个卡密被两个人同时兑换。
  • 异步发邮件:邮件发送不放在主流程里,用 .catch 异步处理,避免第三方服务超时拖慢接口。

六、前端设计

6.1 管理后台(Admin Frontend)

管理后台用 React + Ant Design,主要页面包括:

  • 仪表盘:账号总数、已分发数、待兑换数等统计卡片
  • 账号管理:表格展示 + 批量导入 + 状态筛选
  • 卡密管理:生成、导出、状态管理
  • Agent 管理:下级代理的账号分配
  • 兑换记录:全量记录查询和导出
// 账号列表示例(简化版)
import { Table, Button, Tag, Space, message } from 'antd';
import { useEffect, useState } from 'react';
import axios from 'axios';

const AccountList = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });

  const fetchData = async (page = 1, pageSize = 20) => {
    setLoading(true);
    try {
      const res = await axios.get('/api/accounts', {
        params: { page, pageSize },
        headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
      });
      setData(res.data.data.list);
      setPagination({ ...pagination, current: page, pageSize, total: res.data.data.total });
    } catch (err) {
      message.error('加载失败');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { fetchData(); }, []);

  const columns = [
    { title: '邮箱', dataIndex: 'email', key: 'email' },
    { title: '套餐', dataIndex: 'plan_type', key: 'plan' },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status) => (
        <Tag color={status === 'available' ? 'green' : status === 'used' ? 'orange' : 'red'}>
          {status === 'available' ? '可用' : status === 'used' ? '已使用' : '已过期'}
        </Tag>
      )
    },
    { title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
    {
      title: '操作',
      key: 'action',
      render: (_, record) => (
        <Space>
          <Button size="small" onClick={() => handleEdit(record)}>编辑</Button>
          <Button size="small" danger onClick={() => handleDelete(record)}>删除</Button>
        </Space>
      )
    },
  ];

  return (
    <Table
      columns={columns}
      dataSource={data}
      loading={loading}
      rowKey="id"
      pagination={pagination}
      onChange={(p) => fetchData(p.current, p.pageSize)}
    />
  );
};

6.2 客户端(Client Frontend)

客户端保持简洁,就三个页面:

  1. 卡密列表:展示当前可用的卡密(脱敏显示)
  2. 兑换页:输入卡密,一键兑换
  3. 我的记录:历史兑换记录

移动端适配用 Ant Design 的栅格系统,响应式搞定。


七、部署经验

7.1 服务器环境

阿里云 ECS,2 核 4G,Ubuntu 22.04。安装 Node.js 20、MySQL 8、Nginx。

## 安装 Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install -y nodejs

## 安装 PM2
npm install -g pm2

7.2 Nginx 配置

## /etc/nginx/sites-available/getcode
server {
    listen 80;
    server_name app.example.com;

    root /var/www/client-frontend/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

## /etc/nginx/sites-available/getcodeadmin
server {
    listen 80;
    server_name admin.example.com;

    root /var/www/admin-frontend/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

## /etc/nginx/sites-available/api
server {
    listen 80;
    server_name app.example.com;

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

7.3 PM2 守护进程

## 启动后端
cd /var/www/backend
pm2 start app.js --name cursor-api

## 设置开机自启
pm2 save
pm2 startup

7.4 HTTPS 配置

用 Certbot 一键配 Let’s Encrypt:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -d admin.example.com

7.5 CI/CD 流程

目前是比较简单的手动部署——本地 build 后 scp 上传 dist 文件,然后 pm2 restart。后续打算接 GitHub Actions 做自动部署。


八、踩坑记录与经验教训

8.1 坑一:JWT 泄露问题

问题:一开始把 JWT 存 localStorage,前端 XSS 漏洞导致 Token 被盗。

解决:

  • 引入双 Token 机制,accessToken 存内存,refreshToken 存 httpOnly Cookie
  • 后端维护 sessions 表,支持强制下线

教训:前端存储方案没有绝对安全,纵深防御才是正道。

8.2 坑二:并发兑换导致一码多用

问题:两个用户同时兑换同一个卡密,因为没加锁,两边都成功了。

解决:SELECT ... FOR UPDATE 加行锁 + 事务保证原子性。

教训:涉及"抢占"场景,一定要考虑并发。

8.3 坑三:MySQL 连接池耗尽

问题:高并发时连接池被打满,后续请求全部超时。

解决:

const pool = mysql.createPool({
  host: 'localhost',
  user: 'cursor_app',
  password: process.env.DB_PASSWORD,
  database: 'cursor_management',
  waitForConnections: true,
  connectionLimit: 20,
  queueLimit: 0
});

教训:连接池不是默认就好的,要根据实际并发量调参。监控连接池使用率很重要。

8.4 坑四:邮件发送阻塞主流程

问题:兑换成功后同步发邮件,SMTP 超时直接卡住接口。

解决:改成异步发送,用独立的队列(后续可以升级成 Bull + Redis)。

8.5 坑五:Nginx 部署 React 路由 404

问题:直接访问 /accounts 路由时 Nginx 返回 404。

解决:try_files $uri $uri/ /index.html; 让所有路由都回退到 index.html,由 React Router 处理。


九、后续规划

  • [ ] 引入 Redis 缓存,减轻数据库压力
  • [ ] 接入消息队列,邮件和通知异步化
  • [ ] 增加操作日志审计功能
  • [ ] 管理后台增加数据可视化图表
  • [ ] 完善 CI/CD,实现自动部署
  • [ ] 增加限流和防刷机制

十、总结

这个项目不算大,但从前端到后端到部署,全链路走了一遍,学到了不少东西:

  1. 架构设计要克制:三服务分离已经够用,不要过度设计。
  2. 数据库是根基:表结构设计好了,后面写代码就是顺水推舟。
  3. 并发和数据一致性不能靠猜:事务、锁、约束,该上就上。
  4. 部署不是最后一步:Nginx、PM2、HTTPS,每一步都有坑。
  5. 记录踩坑才有成长:这篇文章本身就是最好的证明。

如果你也在做类似的项目,或者对某个模块感兴趣,欢迎评论区交流。


我是菜牙,一个热爱写代码的开发者。这个平台还在持续迭代中,后续有新功能会再写文章分享。

生产实现仍需补齐

前端线上应托管构建后的静态文件,不直接暴露 Vite 开发服务器。同一域名同一端口的多个 server 块不会合并路由,静态资源与 /api/ 应放入同一个匹配的 server 中。示例账号 password 字段未说明加密方案;会话 Token 存储、Cookie/CSRF 与账号分发权限均需独立评审。行锁只锁住当前查询记录,还需要账号唯一分配约束、权限检查及事务后可靠通知。

Node.jsExpressMySQLReact