Skip to content

Repository files navigation

🏆 实时竞拍大师 · 抖音电商直播竞拍全栈系统

项目简介

「实时竞拍大师」是一套面向抖音电商直播场景设计的高并发竞拍全栈系统, 采用 Redis Lua 原子脚本 + WebSocket 房间广播架构, 实现毫秒级出价响应与多直播间并发隔离。

Node.js TypeScript Redis MySQL React License


✨ 核心功能

🔐 商家管理后台

  • 密钥鉴权:通过 x-admin-secret 请求头 + 常量时间比较(timingSafeEqual)防护时序攻击。
  • 竞拍发布:支持自定义商品名称、图片 URL、起拍价、加价幅度、竞拍时长。
  • 竞拍概览:实时展示当前所有竞拍场次(状态前端本地修正,防止 Redis 状态延迟导致的展示偏差)。
  • 危险操作:支持主播取消竞拍(广播 auction_cancelled)或提前结束(广播 auction_ended)。
  • 一键复制:发布成功后一键复制直播间链接,支持新窗口打开

🏛️ 拍卖大厅

  • 竞拍列表:MySQL(静态信息)+ Redis(实时价格/倒计时)并行合并,最多展示 20 条。
  • 自动轮询:每 10 秒自动刷新,支持手动刷新。
  • 状态筛选:显示「进行中」/「即将开始」标签,倒计时归零后自动过滤。

⚡ 高并发交易引擎

核心出价逻辑封装在单个不可中断的 Redis Lua 脚本(src/lua/place_bid.lua)中,8 步原子执行

步骤 名称 说明
Step 1 限流检查 INCR 固定窗口计数器,单用户每秒最多 BID_RATE_LIMIT_PER_SECOND 次,TTL 2s
Step 2 幂等检查 SET NX PX 原子操作,同一用户/金额 3s 内重复出价被拦截
Step 3 读取状态 HGETALL 一次性获取竞拍全部 10+ 字段
Step 4 状态校验 status == "active" 接受出价
Step 5 时间校验 服务端传入 server_timestamp_ms(绝对权威),≥ end_time_ms 则拒绝
Step 6 金额校验 首单允许以起拍价出价,非首单需 ≥ current_price + min_increment
Step 7 状态更新 HSET 更新最高价/得标者;最后 N 秒内出价则自动延长 extend_seconds
Step 8 排行榜 ZADD 写入排行榜(同 userId 自动去重),ZREMRANGEBYRANK 裁剪保留 TOP 50

关键设计特点

  • 金额统一以**分(整数)**存储和计算,规避浮点精度问题。
  • 所有校验-更新操作在单个 Lua 脚本内完成,利用 Redis 单线程模型有效规避并发写冲突。

📡 实时状态同步

  • 房间隔离:基于 Socket.io Room 机制,每个 auctionId 一个房间,多直播间逻辑隔离。
  • 事件契约:前后端共用 SOCKET_EVENTS 常量,保障接口定义一致性。
  • 断线重连:指数退避重试,重连后自动发送 request_state 拉取完整状态,前端无感知恢复。
  • 广播隔离auction_state 仅发给请求者(socket.emit),不广播给房间内其他用户,保持安静重连体验。
  • 心跳配置:pingInterval 25s + pingTimeout 60s,适应 NAT 网络环境

🎨 视觉交互与动效

  • 移动端适配:高保真复刻直播间 UI(480×844 比例),含圆角边框和阴影。
  • 折叠/展开:BIDDING 状态支持面板收起,切换动画采用 Framer Motion。
  • 排行榜面板:左侧实时展示 TOP 5 出价榜,当前用户高亮。
  • 出价步进器:快速加减按钮,支持自定义金额输入及超大金额自适应缩放。
  • 落锤庆典 Modal:竞拍结束时自动弹出,互斥锁保证不重复触发。

🔧 技术栈

层级 技术选型 版本 选型理由
后端运行时 Node.js + TypeScript ≥20 LTS / 5.4 高并发异步 I/O,类型安全
Web 框架 Express ^4.19 轻量成熟,生态丰富
ORM Prisma ^5.14 类型安全查询,自动迁移
Redis 客户端 ioredis ^5.3 支持 Lua 脚本、Pipeline、集群
缓存/原子锁 Redis 7.0-alpine 单线程模型天然原子性,AOF 持久化
数据库 MySQL 8.0 成熟关系型,支持高并发写入
实时通信 Socket.io ^4.8 (server + client) WebSocket 优先,自动降级 polling
请求校验 Zod ^3.23 类型推导 + 运行时校验
前端框架 React 18 (Vite) 高效渲染,Hooks 模式
样式 TailwindCSS 内联样式 抖音风格定制,无框架依赖
动效 Framer Motion latest 高性能声明式动画
容器化 Docker Compose 3.9 一键部署 MySQL + Redis

📁 项目目录结构

auction-master/
├── src/                              # 后端源码
│   ├── config/
│   │   └── env.ts                    # 环境变量统一读取与类型化
│   ├── controllers/
│   │   ├── auction.controller.ts     # C 端接口:出价、状态、列表、详情
│   │   └── admin.controller.ts       # B 端接口:发布、取消、结束竞拍
│   ├── services/
│   │   ├── auction.service.ts        # 核心业务:Lua 调用、广播、排行榜解析
│   │   └── admin.service.ts          # 管理业务:发布竞拍(含回滚)、取消、结束
│   ├── routes/
│   │   ├── auction.routes.ts         # /api/auctions/* 路由注册
│   │   └── admin.routes.ts           # /api/admin/* 路由注册(含 adminAuth)
│   ├── socket/
│   │   ├── index.ts                  # Socket.io 单例初始化与导出
│   │   └── handlers/
│   │       └── auction.handler.ts    # 事件处理:join/leave/request_state/disconnect
│   ├── middleware/
│   │   └── adminAuth.ts             # 管理后台鉴权(timingSafeEqual)
│   ├── db/
│   │   ├── redis.ts                 # Redis 连接 + Lua 脚本加载 + EVALSHA 调用
│   │   └── prisma.ts                # Prisma 客户端单例
│   ├── lua/
│   │   └── place_bid.lua            # 核心出价 8 步原子脚本
│   ├── types/
│   │   └── index.ts                 # 全局类型:API 响应、Socket Payload、Lua 返回码
│   └── app.ts                       # Express 应用入口 + bootstrap 启动流程
├── frontend/                         # 前端源码(React + Vite + TailwindCSS)
│   ├── src/
│   │   ├── pages/
│   │   │   ├── Lobby.tsx            # 拍卖大厅首页(自动轮询)
│   │   │   ├── AuctionRoom.tsx      # 竞拍房间(手机模拟器 + 视图状态机)
│   │   │   └── Admin.tsx            # 商家后台(密钥登录 + 发布表单 + 危险操作)
│   │   ├── components/
│   │   │   ├── auction/
│   │   │   │   └── AuctionBottomSheet.tsx  # 底部竞拍面板(出价/倒计时/结果)
│   │   │   └── live/
│   │   │       ├── LiveBackground.tsx      # 视频背景层
│   │   │       ├── LiveHeader.tsx          # 顶部主播信息
│   │   │       ├── DanmuFeed.tsx           # 弹幕区
│   │   │       ├── InteractionBar.tsx      # 右侧互动栏
│   │   │       └── LeaderboardPanel.tsx    # 左侧排行榜 TOP 5
│   │   ├── hooks/
│   │   │   └── useAuction.ts        # 竞拍状态机(useReducer + Socket + 时钟校准)
│   │   ├── utils/
│   │   │   ├── request.ts           # Axios 实例封装(含 401 拦截)
│   │   │   ├── priceFormat.ts       # 价格格式化工具
│   │   │   └── haptics.ts           # 触觉反馈封装
│   │   ├── constants/
│   │   │   └── socket-events.ts     # Socket 事件名常量(与后端同步)
│   │   ├── types/
│   │   │   └── index.ts             # 前端类型定义(含 UseAuctionReturn 等)
│   │   └── App.tsx                  # 路由容器(/ → Lobby, /auction/:id → Room, /admin → Admin)
│   ├── vite.config.ts               # Vite 配置(代理 /api 和 /socket.io 到后端)
│   └── .env.example                 # 前端环境变量示例
├── prisma/
│   └── schema.prisma                # 数据库 Schema(5 张核心表 + 2 个 Enum)
├── init-sql/                        # MySQL 初始化 SQL(Docker 首次启动自动执行)
├── docs/
│   ├── api_protocol.md              # API 协议文档
│   └── benchmark.md                 # 性能压测报告
├── docker-compose.yml               # 容器编排(MySQL 8.0 + Redis 7.0)
├── .env.example                     # 根目录环境变量示例
└── package.json                     # 后端项目元信息

🚀 快速开始

环境要求

依赖 最低版本 说明
Node.js ≥ 20.0.0 LTS 后端运行时
Docker ≥ 24.0 运行 MySQL + Redis 容器
Docker Compose ≥ 3.9 一键编排数据库服务
npm ≥ 9.0 包管理(随 Node.js 附带)

一键启动(推荐)

# 1. 克隆仓库
git clone <仓库地址>
cd auction-master

# 2. 配置环境变量
cp .env.example .env
# 编辑 .env,将 <CHANGE_ME_*> 占位符替换为真实密码
# 开发环境可用默认值快速启动(请参考 .env.example 配置本地环境变量)

# 3. 启动数据库服务(MySQL + Redis)
docker compose up -d

# 4. 等待 MySQL 初始化完成(约 30 秒)
docker compose logs -f mysql
# 看到 "ready for connections" 后 Ctrl+C 退出

# 5. 安装后端依赖 + 初始化数据库
npm install
npx prisma generate
npx prisma migrate dev

# 6. 启动后端服务(端口 13000)
npm run dev

# 7. 新开终端,安装并启动前端
cd frontend
npm install
npm run dev -- --port 8888

# 8. 访问
#   前端(拍卖大厅):http://localhost:5173
#   竞拍房间示例:  http://localhost:5173/auction/1
#   管理后台:       http://localhost:5173/admin
#   后端健康检查:   http://localhost:13000/health

关键环境变量说明

变量名 必填 默认值 说明
MYSQL_ROOT_PASSWORD MySQL root 密码
MYSQL_DATABASE auction_db 应用数据库名
MYSQL_USER auction_user 应用专用账户
MYSQL_PASSWORD 应用账户密码
MYSQL_PORT 3306 MySQL 宿主机映射端口
REDIS_PASSWORD Redis 认证密码
REDIS_MAX_MEMORY 256mb Redis 最大内存限制
REDIS_PORT 6379 Redis 宿主机映射端口
APP_PORT 3000 后端 HTTP 服务端口
NODE_ENV development 运行环境
DATABASE_URL MySQL 连接字符串(Prisma)
REDIS_URL Redis 连接字符串(ioredis)
ADMIN_SECRET 管理后台密钥(生产必改)
JWT_SECRET dev-secret-... JWT 签名密钥
BID_RATE_LIMIT_PER_SECOND 3 单用户每秒最大出价次数
BID_IDEMPOTENCY_TTL_MS 3000 幂等窗口(毫秒)
AUCTION_EXTEND_THRESHOLD_SECONDS 10 延时触发阈值(秒)

⚠️ 注:为规避部分主机默认端口占用问题,本项目默认将 MySQL 映射至 13306,Redis 映射至 16379


📡 API 概览

详细接口规范见 docs/api_protocol.md

C 端接口(竞拍用户)

方法 路径 说明
GET /api/auctions 大厅竞拍列表(active/pending,MySQL+Redis 合并)
GET /api/auctions/:id/info 竞拍静态信息(商品名、图片、起拍价)
GET /api/auctions/:id/state 实时状态(Redis HGETALL,< 1ms)
POST /api/auctions/:id/bid 提交出价(核心高频接口,走 Lua 原子脚本)
POST /api/auctions/test-init 测试初始化(仅开发环境)

B 端接口(商家后台)— 需 x-admin-secret Header

方法 路径 说明
GET /api/admin/auctions/latest 最新竞拍实时概览
GET /api/admin/auctions/:id 按 ID 查竞拍概览
POST /api/admin/auctions 发布真实竞拍
POST /api/admin/auctions/init 一键初始化测试竞拍(兼容旧接口)
POST /api/admin/auctions/:id/cancel 取消竞拍(广播 auction_cancelled
POST /api/admin/auctions/:id/end 提前结束竞拍(广播 auction_ended

WebSocket 事件(核心实时通道)

方向 事件名 Payload
C→S join_auction { auctionId: string }
C→S leave_auction { auctionId: string }
C→S request_state { auctionId: string }
S→C joined_success { auctionId, serverTime }
S→C bid_update { auctionId, currentPrice, winnerId, endTimeMs, bidCount, isExtended, leaderboard }
S→C auction_state { auctionId, currentPrice, ..., serverTime, productName, leaderboard }
S→C auction_ended { auctionId, winnerId, finalPrice }
S→C auction_cancelled { auctionId, reason }

🧩 关键工程难点与解决方案

难点 1:高并发出价的原子性保证

问题:多用户同时出价时,传统「查询→判断→更新」模式存在竞态条件(Race Condition),可能导致超卖或数据不一致。

解决:将限流、幂等、状态校验、价格比较、状态更新等 8 个步骤封装为单个 Redis Lua 脚本原子执行。所有金额以**分(整数)**存储和比较,规避浮点精度问题。时间权威采用服务端传入的时间戳,不依赖 Redis 内部时钟。

难点 2:服务端时钟权威与前端倒计时校准

问题:客户端时钟不可信任(用户可能修改系统时间),且网络延迟导致服务端与客户端存在不确定的时差。

解决:设计三层时钟校准协议。joined_success 事件下发服务端时间戳,前端计算 serverTimeDelta 记录偏差。所有需精确时间的组件使用 Date.now() - serverTimeDelta 替代原生时间,将倒计时误差收敛至网络 RTT 级别。

难点 3:断线重连的无感知状态恢复

问题:用户网络波动导致 WebSocket 断开后重连,期间可能错过多条 bid_update 广播(价格变化、延时触发等),直接显示旧数据会导致 UI 与实际状态不一致。

解决:采用双路径夹投机制。路径一:收到 request_state 事件后,服务端从 Redis 全量读取状态并仅向请求者发送 auction_state(安静重连)。路径二:前端本地 setInterval 轮询检测超时。配合 winnerModalFiredRef 互斥锁,可靠应对广播延迟或丢包场景。

难点 4:Redis 写入失败时的 MySQL 一致性保护

问题:发布竞拍需要同时写入 MySQL(商品+场次)和 Redis(实时状态)。若 MySQL 写入成功但 Redis HSET 失败(如网络瞬断),数据库中留下"幽灵竞拍"——存在记录但实际不可用。

解决:在 Service 层引入补偿事务回滚机制。采用 MySQL → Redis 的写入顺序,若 Redis hset 抛出异常,主动捕获并执行 Prisma delete 删除已创建的 MySQL 记录,保障最终一致性。


🤖 AI 辅助开发说明

使用模型

  • DeepSeek V4 — 架构设计讨论、技术选型评审
  • Claude Sonnet 4.6 — 核心代码生成(后端 API、Lua 脚本、React 组件)
  • Gemini 3.5 Pro Preview — 代码审查、文档生成

Prompt 工程策略:Master PRD 驱动

本项目采用「主文档驱动 + 分步精准投喂」的 Prompt 工程策略:

  1. 阶段 0-1(基建 + 数据库):先由 AI 生成 MySQL DDL 和 Redis 数据结构规范文档,作为后续所有阶段的 Context 锚点,确保类型系统一致

  2. 阶段 2(核心 API):将 DDL 和 Redis 规范作为上下文,要求 AI 生成 Node.js 接口和 Redis Lua 脚本。关键约束:金额统一以分(整数)存储,全程整数比较

  3. 阶段 3(WebSocket):在阶段 2 代码基础上增量注入,锁定 Socket 事件契约(SOCKET_EVENTS 常量),前后端共用类型定义消除接口漂移

  4. 阶段 4(前端):以 API 规范文档(api_protocol.md)为红线,约束前端 AI 不得假设后端未实现的字段,实现视觉组件与数据层的严格分离

防幻觉机制

  • 每阶段生成前,强制 AI 读取前序阶段的输出文件作为 Context
  • 类型定义统一在 src/types/index.ts 中维护,前后端共用
  • 关键业务规则(金额单位、时间戳格式)在 Prompt 中以「红线」形式约束

📊 性能指标

以下数据来自本地压测环境(Windows 10,Docker 容器),测试竞拍 ID=29

指标 数值 测试条件
出价接口 P50 延迟 8.6 ms 单用户串行 10 次出价
出价接口 P99 延迟 12.4 ms 单用户串行
出价接口 P50 延迟 61 ms 50 并发(含限流中间件处理)
出价接口 P99 延迟 108 ms 50 并发
出价接口峰值 QPS 768 req/s autocannon 50 并发 10s(含限流拦截)
WebSocket 20 连接建立 67 ms 全部连接完成
WebSocket 20 加入房间 74 ms 全部 joined_success 完成
Redis PING 延迟 < 1 ms Docker 内部回环
Redis 内存占用 1.25 MB / 256 MB 空闲状态

⚠️ 并发压测中,单用户 3 次/秒限流拦截了大部分请求,实际 QPS 反映的是限流中间件的处理能力而非完整出价流程。完整压测报告见 docs/benchmark.md


📄 License

MIT License — 详见 LICENSE 文件。


Made with ❤️ by AI + Human collaboration

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages