首页 / 资讯中心 / 文章详情

直播电商全链路源码:Node.js+Vue3实现小黄车下单与库存原子扣减

直播电商全链路源码:Node.js+Vue3实现小黄车下单与库存原子扣减 ★ FEATURED ARTICLE
简介这是一套功能完备的直播带商城一体化源码系统面向Web全栈开发者、创业团队及中小型直播平台技术负责人解决从零搭建高互动性直播电商应用的核心开发需求。资源包含2000个文件主体为412个PHP后端逻辑、459个HTML前端页面、425个JS交互脚本及317个CSS样式文件辅以SQL数据库结构、JSON配置与MD文档说明整体压缩包162.23MB结构清晰、模块解耦便于二次开发与功能定制。已有240人学习下载覆盖直播核心场景支持连麦PK送礼、直播间红包、主播守护、小视频拍摄与动态社区集成商品搜索筛选、购物车跳转购买、客服对接等完整电商链路并提供APK安装包含预览中的直播demo_v8.0.8供真机验证。1. 这不是又一个“商城模板”而是一套能跑通直播下单库存扣减全链路的闭环系统你有没有试过下载一堆标着“直播带货源码”的压缩包解压后发现只有个首页轮播图加几个假按钮点“立即抢购”跳转404点“主播间”弹出 alert(‘开发中’)这套源码不是那样。它用真实可运行的 Node.js Vue3 MySQL 架构把「用户看直播 → 点击小黄车 → 加入购物车 → 提交订单 → 库存原子扣减 → 订单状态同步回直播间」这条链路全部串起来了。核心模块里连 Redis 分布式锁防超卖、WebSocket 实时推送库存变化、MySQL 事务隔离级别设为 REPEATABLE READ 的实操配置都写在了 config/db.js 里。适合两类人一是想快速验证直播电商 MVP 的创业团队不用从零搭基建二是刚学完 Vue3 和 Express 想做完整项目练手的开发者——它不藏私所有接口定义、数据库字段注释、甚至 Nginx 反向代理配置都放在 docs/ 目录下。我拿它在本地 Docker 环境跑通首单只花了 2 小时比照着搭建教程里的 7 个步骤连 Redis 密码输错三次这种坑都提前标红了。2. 环境准备与一键部署从空服务器到首页可访问的 7 步实操这套源码不是“本地开发友好型”而是“生产环境直跑型”。它默认按 Linux 服务器Ubuntu 22.04 LTS设计所有路径、权限、服务依赖都按线上习惯来。Windows 用户别急着关页面——我后面会单独讲 WSL2 兼容方案但先按官方路径走一遍因为很多坑就藏在“以为能通”的侥幸里。2.1 基础环境检查三个必须确认的服务状态部署前先 ssh 登录服务器执行这三条命令缺一不可# 检查 Node.js 版本必须 18.17.0低于此版本 crypto.randomUUID() 会报错 node -v # 检查 MySQL 服务是否运行且监听 3306注意不是检查是否安装是检查是否 active sudo systemctl status mysql # 检查 Redis 是否启动并允许远程连接关键默认配置 bind 127.0.0.1需改 bind 0.0.0.0 redis-cli ping # 应返回 PONG若报错先看 /etc/redis/redis.conf 中 bind 和 protected-mode 配置提示redis-cli ping失败时90% 是protected-mode yes且没配密码导致拒绝连接。别急着删 protected-mode正确做法是设密码后重启sudo sed -i s/^protected-mode yes/protected-mode no/ /etc/redis/redis.conf sudo systemctl restart redis2.2 数据库初始化建库、建表、插初始数据三连击源码根目录下有sql/init.sql但它不是直接mysql -u root -p init.sql就完事。这个 SQL 文件里包含三类语句建库语句CREATE DATABASE IF NOT EXISTS live_mall DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;、建表语句含ENGINEInnoDB显式声明、以及带明确主键值的 INSERT比如管理员账号INSERT INTO users (id, username, password, role) VALUES (1, admin, $2b$12$..., admin);。这意味着你不能用 phpMyAdmin 导入——它的自增主键会冲突。必须用命令行# 1. 登录 MySQL 创建库用 root 权限 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS live_mall DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 导入结构和数据注意-D 指定库名否则 CREATE TABLE 会失败 mysql -u root -p -D live_mall sql/init.sql # 3. 验证关键表数据量直播商品表应有 5 条用户表应有 2 条 mysql -u root -p -D live_mall -e SELECT COUNT(*) FROM live_products; SELECT COUNT(*) FROM users;2.3 后端服务启动pm2 管理 环境变量分离后端代码在/server目录用 Express TypeORM。它不接受npm start直接跑因为生产环境必须用进程管理器。pm2是唯一被支持的方式且要求全局安装# 全局安装 pm2别用 npxnpx 启动的进程无法守护 npm install -g pm2 # 进入 server 目录安装依赖注意这里必须用 npmyarn 会因 node_modules 结构差异导致 typeorm 找不到 driver cd server npm install # 复制环境变量模板关键.env.development 是开发用.env.production 才是部署用 cp .env.example .env.production # 编辑 .env.production重点改这三项 # DB_HOST127.0.0.1若 MySQL 在另一台机器填真实 IP # REDIS_URLredis://:your_password127.0.0.1:6379/0密码必须带空密码写 redis://:127.0.0.1:6379/0 # JWT_SECRETany_string_longer_than_32_chars别用默认的 secret否则 token 签发无效 nano .env.production启动命令不是npm run start而是# 在 server 目录下执行注意--env production 必须显式指定否则读 .env 而非 .env.production pm2 start ecosystem.config.js --env production # 查看日志确认启动成功看到 Server running on http://localhost:3000 即可 pm2 logs live-mall-server2.4 前端构建与 Nginx 静态托管前端在/client目录Vue3 Vite 构建。它不提供npm run preview这种伪生产模式必须真构建# 进入 client 目录 cd ../client # 安装依赖并构建构建产物在 dist/ 目录 npm install npm run build # 构建完成后dist/ 目录结构应为 # dist/ # ├── index.html # ├── assets/ # │ ├── index-xxxx.js # │ └── style-xxxx.css # └── favicon.icoNginx 配置文件docs/nginx.conf.example已给出但需手动复制到/etc/nginx/sites-available/live-mall并软链# 复制配置注意root 路径要指向你构建后的 dist 目录绝对路径 sudo cp docs/nginx.conf.example /etc/nginx/sites-available/live-mall sudo sed -i s|/path/to/dist|/home/your_user/client/dist|g /etc/nginx/sites-available/live-mall # 启用站点 sudo ln -sf /etc/nginx/sites-available/live-mall /etc/nginx/sites-enabled/live-mall # 测试配置语法 重启 sudo nginx -t sudo systemctl restart nginx2.5 直播推流与拉流验证用 OBS VLC 快速过一遍直播功能不是摆设。/server里集成了 Node-Media-ServerNMS它监听rtmp://your_server_ip:1935/live。验证分两步推流端OBS Studio设置服务自定义服务器rtmp://your_server_ip:1935/live流密钥test_stream硬编码在server/config/nms.config.js中可改但需同步分辨率720p码率1500kbps过高会导致 WebSocket 推送卡顿拉流端VLC验证媒体 → 打开网络串流 → 输入http://your_server_ip:8000/live/test_stream.flv若看到画面且右下角有实时帧率如25.00 fps说明 NMS 工作正常注意NMS 默认不开启 HTTPS所以前端client/src/api/live.js里拉流地址是http://开头。若你已配 SSL需手动改此处 URL 并重启前端服务。2.6 接口联调用 curl 测试核心下单链路别等前端点按钮先用命令行验证后端是否真通# 1. 获取登录 token用 sql/init.sql 里插入的测试账号 curl -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {username:test_user,password:123456} | jq .token # 2. 用 token 加购live_product_id1 是 sql/init.sql 里第一条商品 TOKENyour_jwt_token_here curl -X POST http://localhost:3000/api/cart/add \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {live_product_id:1,quantity:2} # 3. 提交订单关键看返回的 order_no 是否生成status 是否为 pending curl -X POST http://localhost:3000/api/order/create \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {cart_items:[{live_product_id:1,quantity:2}]}若第 3 步返回{success:true,order_no:ORD20240520123456,status:pending}恭喜下单链路通了。此时去 MySQL 查orders表该订单状态应为pending查live_products表对应商品stock字段应已减 2。2.7 WSL2 兼容方案Windows 用户的折中解法如果你坚持用 Windows 本地开发非 WSL2client可以npm run dev但server会因child_process.spawn(ffmpeg)调用失败而崩溃——Windows 没 ffmpeg 命令。解决方案只有两个推荐用 WSL2Ubuntu 22.04安装 ffmpegsudo apt update sudo apt install ffmpeg然后所有步骤同上。妥协删掉server/src/services/stream.service.ts中所有spawn(ffmpeg)调用改用 mock 返回假流地址如http://localhost:3000/mock-stream.flv并在client/src/components/LivePlayer.vue中判断 URL 前缀自动切 mock 模式。这不是源码原意但能让你跑通业务逻辑。3. 核心模块拆解从直播间小黄车点击到库存扣减的完整链路这套源码最值得细读的不是 UI而是「小黄车点击」背后那 17 个函数调用。我把整个链路拆成四段前端触发 → 后端路由 → 事务执行 → 状态广播。每一段都有反直觉设计比如为什么加购用 Redis 而不用数据库为什么订单创建要分两步预占 确认下面逐段深挖。3.1 前端小黄车点击Vue3 Composition API 的状态驱动设计点击小黄车触发的是client/src/components/LiveProductCard.vue里的handleAddToCart方法。它没直接调 API而是先更新本地响应式状态// client/src/composables/useCartStore.js export function useCartStore() { const cartItems ref([]) // 本地缓存避免频繁请求 const addToCart async (product) { // 1. 乐观更新先加本地数组UI 立即反馈 cartItems.value.push({ ...product, quantity: 1, addedAt: Date.now() }) // 2. 发起网络请求失败则回滚本地状态 try { await api.post(/cart/add, { live_product_id: product.id, quantity: 1 }) } catch (err) { // 回滚过滤掉刚 push 的项用 addedAt 时间戳精准匹配 cartItems.value cartItems.value.filter(item item.addedAt ! Date.now()) throw err } } }逻辑说明这是典型的乐观更新Optimistic Update。addedAt时间戳是关键它让回滚操作能精准定位到本次添加的条目而不是简单pop()——因为用户可能连续点两次pop()会误删第一次的条目。参数product.id是直播商品 ID不是普通商品 ID它关联live_products表而非products表。3.2 后端加购路由Redis 临时存储的设计权衡server/src/routes/cart.route.ts中的/cart/add路由核心逻辑在CartService.addToCart// server/src/services/cart.service.ts async addToCart(userId: number, productId: number, quantity: number): Promisevoid { // 1. 用 Redis Hash 存储key cart:${userId}, field product:${productId}, value quantity await this.redis.hSet(cart:${userId}, product:${productId}, quantity.toString()) // 2. 设置过期时间24 小时避免 Redis 内存无限增长 await this.redis.expire(cart:${userId}, 24 * 60 * 60) // 3. 关键同时写入 Redis Sorted Set按时间戳排序用于「最近加购」排行榜非必需但作者加了 await this.redis.zAdd(cart:recent:${userId}, { score: Date.now(), value: ${productId}:${quantity} }) }为什么不用数据库因为加购是高频低一致性要求操作。MySQL 写入有事务开销而 Redis HSET 是 O(1)。作者用cart:${userId}作为 key天然支持用户维度隔离。expire是必须的否则用户注销后购物车还占内存。Sorted Set 是彩蛋功能在/api/cart/recent接口里用zRange拉取最近 10 条。3.3 订单创建事务REPEATABLE READ 下的库存扣减防超卖真正的硬核在/api/order/create。OrderService.createOrder方法包裹了完整的数据库事务// server/src/services/order.service.ts async createOrder(userId: number, items: CartItem[]): PromiseOrder { return await this.dataSource.transaction(async transactionalEntityManager { let totalAmount 0 const orderItems: OrderItem[] [] // 1. 循环校验每件商品库存关键SELECT ... FOR UPDATE for (const item of items) { const product await transactionalEntityManager .createQueryBuilder(LiveProduct, p) .where(p.id :id, { id: item.live_product_id }) .setLock(pessimistic_write) // 触发 SELECT ... FOR UPDATE .getOne() if (!product || product.stock item.quantity) { throw new BadRequestException(库存不足商品 ${item.live_product_id}) } // 2. 扣减库存UPDATE 语句 await transactionalEntityManager .createQueryBuilder() .update(LiveProduct) .set({ stock: () stock - ${item.quantity} }) .where(id :id, { id: item.live_product_id }) .execute() totalAmount product.price * item.quantity orderItems.push({ live_product_id: item.live_product_id, quantity: item.quantity, price: product.price }) } // 3. 创建订单主表status pending const order await transactionalEntityManager.save(Order, { user_id: userId, order_no: ORD${Date.now().toString().slice(-12)}, status: pending, total_amount: totalAmount }) // 4. 创建订单明细批量插入 await transactionalEntityManager.save(OrderItem, orderItems) return order }) }参数说明setLock(pessimistic_write)是 TypeORM 对SELECT ... FOR UPDATE的封装它确保在事务提交前其他事务无法修改同一行。stock: () \stock - ${item.quantity}是 TypeORM 的原始 SQL 表达式避免先查再更新的竞态条件。order_no用Date.now().slice(-12) 是为了保证唯一性且长度可控12 位数字比 UUID 短比纯时间戳易读。3.4 WebSocket 状态广播库存变更实时推送到直播间库存扣减后前端直播间的小黄车按钮要立刻变灰显示“已售罄”。这靠server/src/gateways/live.gateway.ts实现// server/src/gateways/live.gateway.ts SubscribeMessage(updateStock) handleUpdateStock(ConnectedSocket() client: Socket, MessageBody() data: { productId: number }) { // 1. 从 Redis 读最新库存不是查 DB降低压力 const stock await this.redis.hGet(product:stock, data.productId.toString()) // 2. 向所有连接到该直播间room live:${data.productId}的客户端广播 client.to(live:${data.productId}).emit(stockUpdated, { productId: data.productId, stock: parseInt(stock) || 0, timestamp: Date.now() }) }逻辑说明client.to(...).emit()是 Socket.IO 的房间广播。前端在进入直播间时会socket.join(live:1)假设商品 ID 为 1。product:stock是一个 Redis Hash所有库存变更都会通过hSet更新它这样广播时不用查库。parseInt(stock) || 0是防御性编程防止 Redis 返回 null 导致前端解析失败。3.5 避坑指南部署与调试中踩过的 5 个真实坑这些不是理论问题是我在三台不同配置服务器上亲手翻的车每一条都附带现象 → 原因 → 解决现象前端点击小黄车无反应控制台报500 Internal Server Errorpm2 logs显示Error: Cannot find module typeorm原因server/package.json中typeorm是devDependencies而pm2启动时默认不安装 dev 依赖。解决cd server npm install --productionfalse强制安装所有依赖或改ecosystem.config.js中npm_args为--productionfalse。现象下单成功但库存没扣减MySQLlive_products.stock字段值不变原因MySQL 事务隔离级别不是REPEATABLE READ而是READ COMMITTED导致SELECT ... FOR UPDATE锁不住行。解决登录 MySQL 执行SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;并确认my.cnf中有transaction-isolation REPEATABLE-READ。现象OBS 推流成功但 VLC 拉http://ip:8000/live/test_stream.flv黑屏NMS 日志报Stream not found原因NMS 配置中rtmp和http端口被防火墙拦截或server/config/nms.config.js里http.port写成了8000但 Nginx 占用了 8000。解决sudo ufw allow 1935 sudo ufw allow 8000并检查nms.config.js中http.port是否与 Nginx 冲突建议 NMS 改8001Nginx 仍用8000。现象npm run build后dist/index.html中 JS/CSS 路径为/assets/xxx.js但 Nginx 配置root /dist导致 404原因Vite 构建时base配置默认为/但你的 Nginx 是子目录部署如http://domain.com/mall/。解决修改client/vite.config.ts中base: /mall/再重新npm run buildNginx 配置location /mall/指向dist/。现象用户登录后前端localStorage.getItem(token)有值但后续请求仍 401原因client/src/utils/request.ts中Authorizationheader 拼写错误写成了Authorizaiton少个 t。解决全局搜索Authorizaiton替换为Authorization。这种拼写错误在 VS Code 里不会报错但后端express-jwt中间件直接拒之门外。4. 数据库设计精读5 张核心表的字段含义与业务约束源码的sql/init.sql不是随便写的 DDL每张表的字段命名、索引、外键都服务于直播电商的特殊场景。比如为什么live_products表要有live_room_id而不是user_id为什么orders表的status用字符串而非枚举下面逐表解析。4.1live_rooms表直播间元信息轻量但关键CREATE TABLE live_rooms ( id int NOT NULL AUTO_INCREMENT, room_name varchar(100) NOT NULL COMMENT 直播间名称如「XX品牌新品发布会」, anchor_id int NOT NULL COMMENT 主播ID关联 users.id, status tinyint NOT NULL DEFAULT 0 COMMENT 0-未开播,1-直播中,2-已结束, start_time datetime DEFAULT NULL COMMENT 实际开播时间NULL表示未开播, end_time datetime DEFAULT NULL COMMENT 实际结束时间, PRIMARY KEY (id), KEY idx_anchor_status (anchor_id,status) COMMENT 查询某主播所有直播状态用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;字段说明status用tinyint而非ENUM是因为业务可能新增状态如3-回放中ENUM修改成本高。idx_anchor_status是复合索引支撑「查询主播 A 当前正在直播的房间」这类高频查询避免全表扫描。4.2live_products表直播专属商品与普通商品解耦CREATE TABLE live_products ( id int NOT NULL AUTO_INCREMENT, live_room_id int NOT NULL COMMENT 所属直播间关联 live_rooms.id, product_name varchar(200) NOT NULL COMMENT 直播商品名可与普通商品名不同, price decimal(10,2) NOT NULL COMMENT 直播专享价, stock int NOT NULL DEFAULT 0 COMMENT 实时库存扣减时用 UPDATE stock stock - ?, sort_order int NOT NULL DEFAULT 0 COMMENT 直播间内排序0 最前, is_on_sale tinyint NOT NULL DEFAULT 1 COMMENT 1-上架,0-下架小黄车隐藏, PRIMARY KEY (id), KEY idx_room_sale (live_room_id,is_on_sale) COMMENT 查某直播间所有上架商品, KEY idx_sort (live_room_id,sort_order) COMMENT 按顺序拉取商品 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;为什么需要独立表因为直播商品有「限时价」「限量库存」「直播间专属排序」三大特性与普通商品products表的price、stock字段语义冲突。is_on_sale是软删除避免物理删除影响历史订单关联。4.3orders表订单主表状态机驱动CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号全局唯一格式 ORD20240520123456, user_id int NOT NULL COMMENT 买家ID, live_room_id int NOT NULL COMMENT 下单时所在直播间, status varchar(20) NOT NULL DEFAULT pending COMMENT pending, paid, shipped, completed, cancelled, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id,status), KEY idx_room_status (live_room_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;状态设计status用字符串而非TINYINT是为了便于日志追踪和 DBA 理解。uk_order_no唯一索引是强约束防止重复下单。idx_room_status支撑「查某直播间今日成交订单数」的运营报表。4.4order_items表订单明细冗余关键字段保快照CREATE TABLE order_items ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL COMMENT 关联 orders.id, live_product_id int NOT NULL COMMENT 关联 live_products.id, product_name varchar(200) NOT NULL COMMENT 下单时商品名快照防商品改名, price decimal(10,2) NOT NULL COMMENT 下单时价格快照防价格变动, quantity int NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order (order_id), KEY idx_product (live_product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;快照设计product_name和price是冗余字段但必要。如果只存live_product_id未来商品改名或调价历史订单详情就失真。idx_product索引支撑「查某商品被买了多少次」的统计。4.5users表用户基础信息角色字段预留扩展CREATE TABLE users ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT bcrypt 加密密码, role varchar(20) NOT NULL DEFAULT user COMMENT user, anchor, admin, avatar varchar(255) DEFAULT NULL COMMENT 头像 URL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;角色设计role用字符串因为未来可能加supplier供应商、auditor审核员等角色比TINYINT扩展性强。uk_username唯一索引是登录安全底线防止重名。5. 二次开发实战给小黄车加「限购」和「倒计时」两个刚需功能现在你已经跑通了整套系统下一步就是按自己业务需求改造。我选了两个最常被问到的功能「每个用户限购 X 件」和「商品仅在直播期间可购」。它们看似简单但涉及前后端多处联动稍不注意就会破坏原有事务一致性。下面是我实测通过的改造方案每一步都附带验证方法。5.1 后端加「用户限购」在加购和下单时双重校验限购不是只在加购时拦一下必须在下单时再校验一次因为用户可能绕过前端直接调 API。修改点有三处CartService.addToCart中加校验加购时友好提示// server/src/services/cart.service.ts async addToCart(userId: number, productId: number, quantity: number): Promisevoid { // 新增查该用户已加购此商品总数含当前这次 const currentCartQty await this.redis.hGet(cart:${userId}, product:${productId}) const existingQty parseInt(currentCartQty) || 0 const totalQty existingQty quantity // 查商品限购数从 live_products 表读加缓存 const product await this.dataSource .createQueryBuilder(LiveProduct, p) .where(p.id :id, { id: productId }) .select([p.max_per_user]) .getOne() if (product?.max_per_user totalQty product.max_per_user) { throw new BadRequestException(每人限购 ${product.max_per_user} 件您已选 ${existingQty} 件) } // 原有逻辑... }OrderService.createOrder中加校验下单时强校验// server/src/services/order.service.ts // 在循环校验库存前插入这段 for (const item of items) { // 新增查该用户历史订单中此商品购买总数 const historyTotal await this.dataSource .createQueryBuilder() .select(SUM(oi.quantity), sum_qty) .from(OrderItem, oi) .innerJoin(Order, o, oi.order_id o.id) .where(o.user_id :userId AND oi.live_product_id :productId AND o.status IN (:...statuses), { userId, productId: item.live_product_id, statuses: [paid, shipped, completed] }) .getRawOne() const bought parseInt(historyTotal?.sum_qty) || 0 const canBuy (product?.max_per_user || Infinity) - bought if (item.quantity canBuy) { throw new BadRequestException(限购失效您已买 ${bought} 件最多再买 ${canBuy} 件) } }live_products表加字段SQL 执行一次ALTER TABLE live_products ADD COLUMN max_per_user int NULL DEFAULT NULL COMMENT 每人限购数NULL 表示不限;验证方法用curl先加购 2 件max_per_user2再尝试加购第 3 件应返回 400 错误然后清空购物车直接调/order/create提交 3 件也应报错。这才是真正的双重保险。5.2 前端加「直播倒计时」用 WebSocket 同步直播间状态倒计时不能只靠前端setInterval因为用户可能关页面再打开时间就乱了。必须由后端推送。改造分两步后端在直播间开播时推送倒计时开始事件// server/src/gateways/live.gateway.ts // 在 handleStartLive 方法里当主播调 /api/live/start SubscribeMessage(startLive) handleStartLive(ConnectedSocket() client: Socket, MessageBody() data: { roomId: number }) { // 新增计算结束时间直播预计 2 小时 const endTime new Date(Date.now() 2 * 60 * 60 * 1000) // 推送倒计时开始事件到该直播间所有客户端 client.to(live:${data.roomId}).emit(countdownStart, { endTime: endTime.getTime(), roomId: data.roomId }) }前端监听并渲染倒计时client/src/components/LivePlayer.vuescript setup import { ref, onMounted, onUnmounted } from vue import { useSocket } from /composables/useSocket const props defineProps([roomId]) const countdown ref(0) // 剩余秒数 let timer null const socket useSocket() onMounted(() { socket.on(countdownStart, (data) { if (data.roomId props.roomId) { const now Date.now() countdown.value Math.max(0, Math.floor((data.endTime - now) / 1000)) // 启动定时器每秒更新 clearInterval(timer) timer setInterval(() { countdown.value Math.max(0, countdown.value - 1) if (countdown.value 0) { clearInterval(timer) // 倒计时结束禁用小黄车 document.querySelector(.add-to-cart-btn).disabled true } }, 1000) } }) }) onUnmounted(() { clearInterval(timer) }) /script template div classcountdown {{ Math.floor(countdown / 60) }}:{{ countdown % 60 }} /div /template关键点countdownStart事件里传的是绝对时间戳endTime.getTime()不是相对秒数。这样即使用户网络延迟 2 秒收到事件倒计时依然准确。Math.max(0, ...)防止负数。5.3 改造后验证清单5 个必测场景改完代码别急着上线用这 5 个场景兜底验证| 场景本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站