账号管理与兑换平台:三端架构复盘
记录账号、兑换记录、后台与客户端分离的架构,以及事务和通知一致性问题。
项目文稿说明:本文按现有笔记整理,代码是节选或设计示意;仓库实现、线上地址和测试结果尚未逐项核验。后续计划不代表已经完成。
从零搭建 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)
客户端保持简洁,就三个页面:
- 卡密列表:展示当前可用的卡密(脱敏显示)
- 兑换页:输入卡密,一键兑换
- 我的记录:历史兑换记录
移动端适配用 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,实现自动部署
- [ ] 增加限流和防刷机制
十、总结
这个项目不算大,但从前端到后端到部署,全链路走了一遍,学到了不少东西:
- 架构设计要克制:三服务分离已经够用,不要过度设计。
- 数据库是根基:表结构设计好了,后面写代码就是顺水推舟。
- 并发和数据一致性不能靠猜:事务、锁、约束,该上就上。
- 部署不是最后一步:Nginx、PM2、HTTPS,每一步都有坑。
- 记录踩坑才有成长:这篇文章本身就是最好的证明。
如果你也在做类似的项目,或者对某个模块感兴趣,欢迎评论区交流。
我是菜牙,一个热爱写代码的开发者。这个平台还在持续迭代中,后续有新功能会再写文章分享。
生产实现仍需补齐
前端线上应托管构建后的静态文件,不直接暴露 Vite 开发服务器。同一域名同一端口的多个 server 块不会合并路由,静态资源与 /api/ 应放入同一个匹配的 server 中。示例账号 password 字段未说明加密方案;会话 Token 存储、Cookie/CSRF 与账号分发权限均需独立评审。行锁只锁住当前查询记录,还需要账号唯一分配约束、权限检查及事务后可靠通知。