PHP直播带货系统源码实战:直播间状态流转与高并发设计
简介一套基于PHP实现、面向微信小程序的直播带货系统完整源码适合电商开发者、小程序学习者及希望快速搭建购物直播平台的团队。资源覆盖小程序前端WXML/WXSS/JS、PHP后端逻辑、数据库表结构及RESTful API接口设计核心功能包括用户登录注册、商品分类展示、购物车、订单生成、微信支付集成、直播间管理与礼物互动等同时兼顾安全性防SQL注入、XSS过滤、权限角色划分与性能优化是一份可直接借鉴的电商实战项目。压缩包共2000个文件以php、js、png、xml、json等类型为主包含源码文件、配置文件及素材资源总大小59.03MB目录结构清晰便于二次开发。目前已有115人学习浏览既可作为毕业设计、课程项目的完整参考也可为有经验的开发者提供快速搭建直播电商平台的基础底座帮助深入理解从直播推流到支付回调的端到端业务流程。1. 直播带货系统 PHP 源码到底要解决什么问题很多人看到“仿淘宝B站购物直播微信小程序带货完整PHP源码”这种标题第一反应是包含一套商城、一个直播间、一个可以卖货的小程序。但真正上手后你会发现页面反而是最简单的部分。这套系统的核心难点集中在一个词上联动。直播间有生命周期商品挂靠在直播间下弹幕消息要实时展示订单要跟着直播间的状态走每一条都要后端给出明确的状态和流转规则。对 PHP 工程师来说这种任务通常是第一次接触长连接和消息队列也是最能拉开代码水平的地方。本文按一套可落地的方案拆解先理清直播购物的数据模型再讲 uniapp 微信小程序端的组件与体验然后落到 PHP 的接口、队列和视频压缩最后给出一组自测和调优技巧让拿到源码的人能改得动、跑得通。2. 直播间的状态流转PHP 后端的第一优先级2.1 直播间、商品、订单在表结构上如何关联直播带货和普通商城最大的区别是“同时在线”这个约束。标题里的“仿淘宝B站”指的不是页面像素级相似而是交互形态类似主播开着视频讲解若干 SKU观众看到商品后不用离开直播间就能下单。对应到数据关系上核心是一张直播间表关联多张商品表订单表同时挂直播间 ID 和商品 ID。我一般会在源码里先找这几张表room、goods、order、msg。其中goods表必须冗余一个room_id字段而不是通过中间表关联因为用户下单时只需要知道“这个商品属于哪个直播间”多一次 join 在直播场景下就是多余的延迟。下面的字段设计基本能覆盖一个最小可用的带货直播间。字段类型说明room_idint直播间主键anchor_idint主播用户 ID关联 user 表titlevarchar直播间标题covervarchar封面图列表页展示statustinyint0 未开播 / 1 直播中 / 2 已结束pull_urlvarchar播放拉流地址给小程序端push_urlvarchar推流地址主播端使用start_time / end_timedatetime上下播时间运营统计用商品表在原有商城字段之上需要增加room_id、sort讲解顺序、live_status是否在讲解中用于高亮当前讲解的商品。订单表则除了 user_id、goods_id 外必须记录room_id和当时的下单价格。这样售后和业绩归属才能追到直播间纬度而不是只追到商品。2.2 为什么要把直播间状态放进 Redis 而不是 MySQL直播间状态是一个高并发读取的热点数据。用户进入直播间第一件事就是拉房间信息如果每次请求都查 MySQL一个在线五千人的直播间在开播瞬间就能把数据库连接打满。常见做法是写一层 Redis 缓存直播间的状态、在线人数、当前讲解商品 ID全部放 Hash 结构里。$status $redis-hGet(live:room: . $roomId, status); if ($status false) { $status getRoomStatusFromDb($roomId); $redis-hSet(live:room: . $roomId, status, $status); } // status 为 0 或 1配合下单判断这里用hGet而不是get是因为直播间在 Redis 里不止存一个状态字段status、online_count、current_goods_id、anchor_id都会同时存在。如果拆成多个 string 键查询时要多次请求Hash 一次取多个字段更划算。Redis 里的值只在开播、下播、切换商品时更新写频率远低于读频率缓存一致性压力很小。要注意一个常见的坏味道直播间切换商品时PHP 代码里把 Redis 更新和 MySQL 更新写成同步事务。事务里包含网络 IO 会让接口变慢开播瞬间这种操作一多Redis 连接就排队了。正确做法是 Redis 更新成功后立即返回MySQL 的落库丢给队列异步处理。2.3 下单接口要校验的“三件事”下单接口是整套系统里最容易超卖和刷单的地方。一个严谨的 PHP 下单方法至少要做三步校验缺一不可public function createOrder(int $roomId, int $goodsId, int $userId) { // 第一步直播间必须处于直播中 if (!$this-roomIsLive($roomId)) { return $this-error(40001, 当前直播间已下播); } // 第二步商品必须真实挂在该直播间下 $goods Goods::where(goods_id, $goodsId) -where(room_id, $roomId) -first(); if (!$goods) { return $this-error(40002, 商品不属于该直播间); } // 第三步库存预扣用 Redis 原子自减防止超卖 $stock $this-redis-decrBy(stock: . $goodsId, 1); if ($stock 0) { $this-redis-incrBy(stock: . $goodsId, 1); return $this-error(40003, 已抢光); } // 下单写库走异步队列不在请求里同步执行 $this-redis-lpush(queue:order, json_encode([ room_id $roomId, goods_id $goodsId, user_id $userId, price $goods-price, created_at date(Y-m-d H:i:s), ])); return $this-success(下单成功请尽快支付); }第三步里的decrBy是原子操作能避免并发下“读出库存剩 1两个请求同时扣减成 -1”的经典问题。判断到负数后再incrBy加回去保证 Redis 里的数量不会越扣越小。这里没有直接用 MySQL 的库存字段因为直播场景的限时抢购并发量远大于普通商城的加购量承受不住每个请求都 update 一次行锁。订单不立即写 MySQL而是推入 Redis 的 list这是为了把接口响应时间压到 100ms 以内。后端再跑一个常驻消费脚本去批量落库在第 4 章里会详细展开消费模型的写法。3. uniapp 微信小程序端live-player、购物面板与弹幕的写法3.1 用 HBuilderX 打开项目后的基础结构这类源码的小程序端基本都是 uniapp 写的拿到压缩包解压后用 HBuilderX 直接导入项目就能跑。常见目录结构是pages/home/index直播间列表、pages/room/index直播间内部、pages/goods/detail商品详情、pages/order/confirm确认订单。pages.json里会声明 tabBar 和页面路由直播间页不能放进 tabBar因为它需要沉浸式全屏展示放进 tabBar 会有底部栏遮挡视频。看pages.json时重点检查两处。第一处是navigationStyle直播间页要设为custom才能把标题栏去掉让视频真正全屏。第二处是appidmanifest.json里要替换为自己注册的小程序 AppID否则微信开发者工具会一直提示“未配置 appid”或“无权限”。直播间列表页是用户进直播间的第一道门网络数据得提前到位。常见的加载写法是用onLoad里触发一个loadLiveList方法拉接口后写入this.list模板里用v-iflist.length控制骨架屏切换。注意微信小程序的setData数据量有限直播间列表一次请求只返回第一页 10 条就够了翻页用onReachBottom触发加载下一次避免一次性渲染 50 个封面图造成首屏白屏。3.2 live-player 与 video 的选择以及关键参数直播正在进行的房间用live-player标签直播已结束的回放用video标签。两者都是微信小程序的原生组件不是普通 H5 的video所以样式和事件都要按小程序的规则走。常用参数如下表参数可选值作用modelive / videolive 为直播流video 为点播autoplay布尔值是否自动播放直播场景必须为 truemin-cache / max-cache0.5 ~ 3播放器缓存时间直播建议 0.5 / 1object-fitfill / contain视频画面填充方式mute布尔值是否静音默认不静音播放器代码在pages/room/index.vue里一般长这样live-player :srcliveUrl modelive autoplay min-cache0.5 max-cache1 object-fitfill statechangeonPlayStateChange /min-cache和max-cache是直播体验的关键。直播流讲究实时性缓存时间拉太长画面和弹幕就对不上主播说“一号链接上车”观众 3 秒后才看到对应动作冲动消费直接没了。包装成 0.5 到 1 秒在弱网下会稍微卡顿但保证了画面与互动的同步。如果源码里这两个值写的是 5 或 10建议直接改小。3.3 盖在视频上的购物面板只能用 cover-view这是新手最容易被坑的一点。live-player是原生组件层级永远在最顶层普通view写再多z-index也盖不上去。要在画面上放商品列表、购物车按钮、弹幕区域必须用cover-view和cover-image。cover-view classgoods-bar cover-view classgoods-item v-foritem in goodsList :keyitem.goods_id cover-image :srcitem.cover classgoods-cover / cover-view classgoods-name{{ item.name }}/cover-view cover-view classgoods-price¥{{ item.price }}/cover-view /cover-view /cover-viewcover-view有它的限制不能用overflow: scroll做内部滚动不能嵌套太深部分 CSS 属性如border-radius在小程序基础库低版本上会失效。所以商品面板做成一个个垂直排列的小卡片每次展示 2 到 3 个商品配合向上滑动的切换事件。弹幕区同理弹幕容器和每条弹幕都得用cover-view普通view在真机上会被播放器遮住测试时只有开发者工具里看是正常的上真机就露馅。如果要在商品卡片和视频之间做联动点卡片切换当前讲解商品推荐后端在商品接口里返回一个current_goods_id字段。前端拿到后用它的索引控制cover-view的视觉状态高亮边框或放大而不是自己维护一个currentIndex因为主播端切商品是后端状态变化前端响应式再快也比不上直接信后端数据。3.4 刚进直播间的加载页和顶部导航栏避让一个直播间页打开后需要同时做三件事拉房间详情、初始化播放器、获取商品列表和弹幕历史。这三件事并行发请求会比串行快不少。加载页不要简单放几张静态图用一个全屏cover-view遮罩显示“加载中”等播放器statechange事件触发play/playing状态后再把遮罩撤掉用户不会看到黑屏或撕裂画面。顶部导航栏在自定义模式下需要手动避让否则返回按钮会顶到胶囊按钮下面。微信小程序的胶囊按钮位置不是固定的不同机型高度不同。常见做法是通过uni.getMenuButtonBoundingClientRect()拿到胶囊左上角和右下角坐标动态计算出顶部安全区域高度const menu uni.getMenuButtonBoundingClientRect(); this.navBarHeight menu.top menu.height - 0;把计算出的高度绑定到模板里cover-view的padding-top返回箭头和房间标题就能避开胶囊按钮。这个细节如果源码里没写真机上会出现返回按钮和右上角胶囊“重叠”的丑态也是直播项目在小程序审核时容易被以“UI 遮挡”为由打回的高频原因。4. PHP 后端接口设计消息队列、视频压缩与跨域处理4.1 直播带货系统的接口清单直播间页面要跑起来后端最少要提供这些接口。路径方法用途/api/live/detailGET直播间详情含主播信息、状态、在线人数/api/live/goodsGET当前直播间的商品列表/api/live/orderPOST下单参数 room_id goods_id/api/msg/sendPOST发弹幕含文本和类型/api/msg/listGET进入房间时拉取最近 50 条历史弹幕/api/live/statusGET轮询房间状态用于检测下播/api/live/detail在下单流程里不是必须的但直播间页进入时一定会加载。这里要注意返回结构里务必带上status字段前端根据它决定展示播放器还是展示“直播已结束”的占位图。如果源码里这个字段缺失会导致直播结束后播放器仍然尝试拉流白白浪费流量。4.2 弹幕进仓库先写 Redis再让队列消费弹幕是整个系统里写入频率最高的数据。一个活跃直播间一秒钟可能产生几十条消息如果每条都INSERT一次 MySQL磁盘 IO 立刻被打满直播主流程的接口也会跟着卡。常见做法是把 PHP 消息处理拆成三段/api/msg/send接口只做校验和轻处理然后把消息推入 Redislist后端常驻进程循环从 Redis 列表取出消息批量写入 MySQL消息通过 WebSocket 服务广播给所有连接到该直播间的小程序端。第 3 步在 PHP 体系里的常见方案是 GatewayWorker由 workerman 提供常驻的 WebSocket 服务。PHP 主框架把弹幕推给 GatewayWorker由它分发给对应直播间连接的用户从而实现“主播说一句观众秒看到”。// 入口接口校验后入列并发布 public function sendMsg(Request $request) { $data [ room_id $request-post(room_id), user_name $request-post(user_name), content $request-post(content), created_at date(Y-m-d H:i:s), ]; $serialized json_encode($data, JSON_UNESCAPED_UNICODE); // Redis 列表作为持久化缓冲消费进程从这里取数据 $this-redis-lpush(queue:msg, $serialized); // Redis 发布订阅通知 GatewayWorker 立即广播 $this-redis-publish(msg:channel, $serialized); }这里的lpush是消息缓冲publish是实时通知。两者缺一不可。只发布订阅不写列表进程一重启消息全丢只写列表不发布订阅消息要等消费进程轮询才能到客户端实时性差。消费脚本则是另一个 CLI 常驻进程阻塞式从列表右侧弹出// queue_msg_worker.php while (true) { $raw $redis-brpop(queue:msg, 5); if (!$raw) { continue; } $msg json_decode($raw[1], true); DB::table(msg)-insertOrIgnore($msg); }brpop是阻塞式弹出列表为空时挂起等待而不是空转消耗 CPU。消费端每次处理完一条就立刻循环下一次保证 MySQL 写入是接近实时的同时压力又比每条弹幕同步写库小得多。用insertOrIgnore是为了配合在表上建的唯一索引防止消费脚本异常重启后重复插入同一条弹幕。4.3 PHP 做视频压缩FFmpeg 转码成 HLS 切片直播源生成的视频文件往往是主播端推上来的原始码流动辄几十 GB。这类源码里通常有一段后台管理功能把原始视频转成 HLS 流供直播回放入口使用。PHP 本身没有视频编解码能力常见做法是封装 FFmpeg 命令。ffmpeg -i input.mp4 \ -vf scale1280:720:force_original_aspect_ratiodecrease \ -b:v 1800k -maxrate 2000k -bufsize 3000k \ -c:v libx264 -preset veryfast -profile:v main \ -c:a aac -b:a 128k \ -f hls -hls_time 4 -hls_list_size 0 output.m3u8各参数的含义-vf scale把分辨率压到 720p 以内同时保持原比例-b:v 1800k是视频平均码率720p 的直播带货回放压到 1800kbps 已经能看清商品文字-preset veryfast是压速度转码一小时的视频用 veryfast 能比 slow 快出一倍画质损失在手机端几乎看不出来-f hls指定输出为 HLS 协议-hls_time 4表示每 4 秒切一个 ts 分片-hls_list_size 0表示分片全部保留生成完整回放列表而不是只保留最近几个分片。PHP 端调用这段命令要注意安全转义否则文件名里带特殊字符有注入风险$input /data/video/ . escapeshellarg($videoName); $output /data/hls/ . escapeshellarg($m3u8Name); exec(ffmpeg -i $input ... 21, $out, $code);执行后异步处理不要在前端请求里直接等转码完成。用一个任务队列把转码任务丢进去由 CLI 脚本逐个消费前端页面通过查询转码状态来展示“转码中 / 完成”的进度。4.4 接口跨域与 JSONPweb-view 场景绕不过的坑微信小程序自身的wx.request不受浏览器同源策略影响但直播带货系统往往会有一个运营后台或 H5 活动页塞进 web-view。这时候 PHP 接口就被浏览器环境调用跨域和 JSONP 的问题就绕不开。最省事的方案是后端统一加 CORS 头。header(Access-Control-Allow-Origin: . ($_SERVER[HTTP_ORIGIN] ?? *)); header(Access-Control-Allow-Credentials: true); header(Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With); header(Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS);Allow-Credentials为 true 时不能把Allow-Origin写成*必须返回具体来源。小程序 web-view 里 H5 的Origin是小程序指定的业务域名PHP 可以从HTTP_ORIGIN动态返回三个页面共享一套接口时也要确保每个域名都能拿到允许访问的响应头。另外OPTIONS预检请求必须直接返回 200不要在预检里做登录逻辑否则前面跨域白配了。5. 直播系统自测、压测与发布前必查项5.1 用本地 HLS 文件顶替直播流模拟直播中状态联调阶段往往拿不到主播端的真实推流地址。一个快速方案用 FFmpeg 把一段录好的 demo 视频切成 HLS 切片把生成的 m3u8 地址填到live-player的src里实现停不了的“伪直播”。ffmpeg -re -i demo.mp4 -c copy -f hls -hls_time 2 output.m3u8加-re参数后FFmpeg 以视频原速率输出切出来的 HLS 分片播放效果类似直播源。把小程序的liveUrl指向这个 m3u8再加上数据库里把直播间状态置为 1就能完整走通下单链路。唯一要注意的是微信开发者工具默认不校验合法域名可以在本地测试真机预览则要求live-player的播放域名配置在小程序后台的socket合法域名里且必须 HTTPS。本地的 m3u8 只能用于工具调试真机调试时要换一台能公网访问的测试服务器来放切片。5.2 用 ab 压一下下单接口验证队列和防超卖是否生效直播带货系统最容易在开播瞬间被打崩所以发布前最后一步是用 ApacheBench 模拟高并发下单。先确认 Redis 里的商品库存例如设置成 100 件然后用 200 个并发请求去打createOrder接口ab -n 2000 -c 100 -T application/json -p order.json \ https://your.domain/api/live/order压完以后看两个数Redis 中stock:xxx的最终值以及queue:order列表的积压长度。库存不能小于 0积压列表应该稳定回落而不是持续增长。如果库存出现了负数说明有人改掉了decrBy的判断逻辑如果订单列表越积越长看看消费进程是否掉了用ps aux | grep queue_order_worker确认。5.3 关闭直播后回补 MySQL 状态的小技巧开播和关闭直播时直播间状态先写在 Redis但运营后台的列表页往往直接读 MySQL。常见做法是写一个每分钟执行一次的定时脚本把 Redis 中已结束的直播间状态回写到 MySQL避免运营后台显示“直播中”与实际不符。回写动作不在用户接口链路里做不影响在线业务的响应速度。上线前把这个 crontab 加上直播结束后一分钟后状态就能同步用户端看到的就不再是“主播在讲但点了无法下单”的坏体验。本文还有配套的精品资源点击获取