
简介面向想快速搭建直播互动平台的站长、创业者和技术学习者这套直播打赏与视频付费观看网站源码提供了一套可直接上线的完整方案自带直播数据集用户打赏、礼物互动、付费解锁视频、主播管理等功能于一体。压缩包共563个文件大小约125.55MB主要包含80个php后端逻辑文件、40个js交互脚本、34个mp4演示视频、104个png图片素材以及css样式、sql数据库脚本、html页面模板等覆盖前台展示、后台管理、数据统计等模块目录结构清晰便于直接部署或二次开发。目前已有584人学习并下载。资源内附完整文字搭建教程和示例数据从环境配置到上线运行都有说明源码已经过测试可帮助新手快速完成平台搭建对于需要研究打赏算法、付费视频机制或直播数据统计的开发者也是不错的参考模板能够在短时间内理解整套直播系统的运作逻辑。1. 这套源码真正值钱的地方不在播放器而在它怎么把「礼物-余额-结算」串起来标题里三个关键词——付费视频、直播间打赏、自带直播数据——单独拆开都不是新鲜东西但合在同一个 PHP 源码里就意味着你要处理的不是某个播放器集成而是一整套「内容加密、虚拟资产、实时推送、模拟流量」的闭环。市场上这类网站源码大多是 ThinkPHP 5 或者 Laravel 改出来的前台用 H5 Layui 或者 Vue 单页后台用 AdminLTE 套壳真正决定你能不能上线运营的是支付回调怎么写、播放鉴权怎么防抓、直播数据是真实转推还是定时模拟。这篇文章针对的读者不是「搜源码下载」的终端用户而是拿到压缩包之后要二次开发、部署、接支付、改协议的那批人。我会把这套系统按「视频付费链路、直播数据注入、打赏结算闭环、部署验收」四个层面拆开讲每一层给到能直接抄的参数和代码逻辑。自带直播数据这个功能点容易被当成演示页面的花架子但恰恰是它决定了运营初期房间有没有「人气」下面会给出一个不依赖第三方接口的伪直播数据生成方案。2. 付费视频的核心是鉴权调度不是视频文件本身2.1 视频播放地址为什么要走临时鉴权拿到源码先别急着传 MP4。多数便宜的源码会直接在标签里写静态文件地址这种结构在百度统计里能看到完整引用页在迅雷里能被直接嗅探在 F12 里复制 src 就能下载等于付费墙是纸糊的。正规做法是播放地址走「动态令牌 过期时间 客户端 IP 绑定」同一时刻一个付费用户只能取到一条可播的 URL过期或换 IP 都得重新请求接口。常见做法是在 Nginx 层做 secure_link 校验PHP 负责签发Nginx 负责校验这样视频文件本体不经过 PHP-FPM静态文件由 Nginx 直接吐性能损耗最小。签发的核心代码大约是这样// PlaySign.php ?php class PlaySign { private $secret your_secret_key_here; // 务必改为随机长字符串 private $expire 3600; // 链接有效时长单位秒 public function sign($filePath, $ip) { // 取文件最后修改时间作为哈希因子防止改文件后旧链接仍有效 $mtime filemtime($filePath); $uri rtrim($filePath, /) . $mtime; $md5 base64_encode(md5($this-secret . $uri . $ip, true)); $md5 strtr($md5, /, -_); // 过期时间直接拼在地址里Nginx 端用 $arg_st 取 return sprintf(%s?st%se%d, $filePath, $md5, time() $this-expire); } }代码里的$ip参数决定这条链接是否只允许某个 IP 播放如果你的用户走的是手机 4G 网络IP 频繁横跳会导致播放失败这时可以把$ip换成user_agent 随机字符串的组合代价是防盗链强度下降。$expire不要设太长客户端播放器一般会在视频即将播完时预取下一段链接1 小时的过期时间足够一节课视频看完又能防止链接被转发到群里三天后仍能播放。2.2 前端播放器与鉴权接口的配合方式前端不能直接把原始 MP4 地址写在 video 标签里要在play()事件中先请求一个接口接口内部判断用户是否已购、是否登录、是否在允许的设备数内通过后返回带签名的 URL。完整的一趟请求时序如下// player.js function playVideo(videoId) { axios.get(/api/video/play-url, { params: { id: videoId } }).then(res { if (res.data.code ! 0) { alert(res.data.msg); // 未购买、账号被异地登录等业务错误 return; } // 关键拿到的是临时 URL播放器每次加载都重新请求本接口 const signedUrl res.data.url; document.getElementById(videoPlayer).src signedUrl; }).catch(() { alert(鉴权服务异常请稍后重试); }); }这里有一个容易踩的坑部分源码会把签名 URL 缓存在前端 localStorage 里播放时优先读缓存结果用户把二维码分享给别人之后同一链接在多个设备上都能播。正确做法是每次play都强制请求一次即使响应头里加了Cache-Control: max-age60也只缓存接口结果不缓存签名 URL。后端配套要有一个「用户活跃设备数」的计数表字段建议这样设计字段名类型说明user_idint用户 ID联合主键device_codevarchar(64)设备指纹由前端生成后上报tokenvarchar(32)本次登录的会话 tokenlast_activedatetime最后活跃时间超过 7 天自动清理bind_video_countint该设备已绑定的视频数防止单设备批量下载上表的核心逻辑是「同一账号最多 N 个设备同时播放」N 一般取 2 到 3。超过限制时踢出最早活跃的设备这种设计在源码后台里通常命名为「设备管理」如果你拿到的源码没这个表建议自己补上——否则一个付费会员可以把账号借给全班同学轮播你的收入模型直接崩掉。3. 自带直播数据不是假数据是「模拟在线 定时礼物流水」的双层结构3.1 直播间的在线人数从哪里来Redis 计数与心跳上报标题里「自带直播数据」这六个字在源码里通常对应两种实现一种是房间页有一张定时刷新的「在线人数」表另一种是直接对接了第三方的直播样流地址。前者是纯静态数据后者是真实推流但大多数演示站用的是前者——把一个随机数写死在 HTML 里刷新页面数字变化实际没有任何推流。要让运营第一天房间看起来有人气同时又不依赖外部 API我的方案是「Redis 计数器 每个访客每 5 秒一次心跳」。《直播间在线人数的前端需要看到「xx 人在看」后端计数流程是// LivePresence.php ?php class LivePresence { private $redis; public function __construct($redis) { $this-redis $redis; } // 每 5 秒由前端 ajax 调用 public function beat($roomId, $userId 0) { $key live:online: . $roomId; $ip getUserIp(); // 无论登录与否每个访客有独立权重值为 1 $weight $userId 0 ? 3 : 1; // 登录用户算 3 个人提高房间热度 $this-redis-hIncrBy($key, $this-getBucketKey($ip, $userId), $weight); $this-redis-expire($key, 3600); return $this-getOnline($roomId); } public function getOnline($roomId) { $key live:online: . $roomId; // 只统计最近 15 秒内有心跳的桶 $buckets $this-redis-hKeys($key); $online 0; foreach ($buckets as $bucket) { list($time, $ip) explode(|, $bucket); if ($time time() - 15) { $online intval($this-redis-hGet($key, $bucket)); } } return max($online, $this-baseNumber($roomId)); } // 房间保底人数新房间没有真实流量时也显示一个基础值 private function baseNumber($roomId) { return intval(fmod($roomId * 13, 37)) 20; // 20~56 之间波动 } }逻辑说明getBucketKey把「当前时间窗5 秒为一个窗 IP 用户 ID」拼成 Redis Hash 的 fieldvalue 是权重。读取时只统计 15 秒内存在的桶这样离线用户自然消失在统计里不需要额外写清理任务。baseNumber是保底人数让新房间不冷场但保底值不能超过真实值的展示逻辑——否则用户多看几分钟就会发现人数毫无波动略显假了。注意一个关键点这个数据只能用于「展示层」的在线人数不能作为真实运营指标。如果后面接真实的直播流比如摄像头推流到 CDN在线数应该以 CDN 的回源统计为准Redis 数据只用于房间排名和热度榜。3.2 定时任务生成礼物流水让打赏记录持续滚动直播间除了在线人数还要有「xxx 送出了 火箭」这类滚动消息。真实平台里这些消息来自 WebSocket 推送但演示源码通常用 ajax 轮询一个接口接口后端从一个 pre_flow_log 表里随机取最近 20 条。新房的困境是这张表是空的需要运营手动插入或者靠定时脚本填充。我一般用 crontab 每分钟跑一个脚本插入「粉丝数在 1000 到 2 万之间」的虚拟主播的送礼记录时间戳错开在最近 10 分钟内这样前端轮询永远能拉到「新鲜」的消息流# crontab -e 添加任务每分钟执行一次 * * * * * php /www/betatest/scripts/fake_gift_flow.php --room101 /dev/null 21// fake_gift_flow.php ?php // 指定房间与用户池避免每次都全表扫 $roomId (int)$argv[1] ?? 101; $giftPool [玫瑰 1, 跑车 30, 火箭 100, 告白气球 10]; $nickPool [甜心Momo, 夜风不燥, Dream大旭, 小鹿要努力, 山河故人]; $pdo new PDO(mysql:host127.0.0.1;dbnamelive, root, pass); $insert $pdo-prepare( INSERT INTO gift_flow (room_id, user_name, gift_name, gift_price, created_at) VALUES (?, ?, ?, ?, ?) ); $stmt $pdo-prepare( SELECT user_name FROM users WHERE room_id? AND is_virtual1 ORDER BY RAND() LIMIT 3 ); $stmt-execute([$roomId]); $realVirts $stmt-fetchAll(PDO::FETCH_COLUMN); // 生成 3~8 条记录时间戳在最近 10 分钟内随机分布 for ($i 0; $i rand(3, 8); $i) { $name ($realVirts mt_rand() % 3 ! 0) ? $realVirts[array_rand($realVirts)] : $nickPool[array_rand($nickPool)]; $gift array_rand($giftPool); $insert-execute([ $roomId, $name, $gift, $giftPool[$gift], date(Y-m-d H:i:s, time() - mt_rand(0, 600)) ]); }参数说明is_virtual字段是我在后端用户表里加的一个标记位把注册的「托」和普通真实用户分开。gift_price是礼物单价算入主播结算时要用真实价格模拟流水的价格也要按实际礼物金额填否则后台的财务统计会平白多出利润缺口。脚本要控制插入频率建议最低 5 分钟一次太频繁会让流水总额虚高运营在看财务时对不上账。要让自己写的伪数据「更像真的」就加一个约束礼物总金额 打赏人数 × 平均客单价。插入前先查一下当前房的总打赏金额超过 10000 元时就只插小额礼物把大额礼物留给人工/主播自己刷。4. 打赏闭环与支付回写的正确顺序先记账再推流最后回调4.1 前端打赏按钮到余额扣减的完整数据流大多数源码把送礼逻辑写成「前端点击 - ajax 调接口 - 接口直接 UPDATE users SET balance balance - 价格」这个写法在低并发下没问题但直播间打赏的特点是瞬间高并发——同一个热门主播的房间主播喊一嗓子一小时内几千条礼物请求直接用 UPDATE 会导致行锁整个余额表的操作全部卡住。正确做法是先把打赏请求写入 Redis 队列由后台消费进程逐个落账前端用 WebSocket 或轮询拿到结果。下单环节这样写// GiftController.php public function sendGift(Request $request) { $userId $request-userId; $roomId $request-roomId; $giftId $request-giftId; $quantity (int)$request-quantity ?? 1; // 查礼物价格来自缓存表gift_list避免查主表 $gift $this-cache-remember(gift:$giftId, 3600, function() use ($giftId) { return Db::table(gift_list)-find($giftId); }); // 1. 扣减余额这里用 Redis 的 DECRBY实现原子扣减 // 余额以 MySQL 为准Redis 只做前端展示的快照 $balance $this-redis-hIncrBy(user:balance:$userId, balance, -($gift[price] * $quantity)); if ($balance 0) { // 扣成负数说明余额不足回滚 Redis返回错误 $this-redis-hIncrBy(user:balance:$userId, balance, $gift[price] * $quantity); return json_error(余额不足); } // 2. 记账写 gift_order 表状态为 pending等待消费进程确认 $orderId Db::table(gift_order)-insertGetId([ user_id $userId, room_id $roomId, gift_id $giftId, quantity $quantity, amount $gift[price] * $quantity, status 0, // 0 pending, 1 done, 2 failed created_at date(Y-m-d H:i:s) ]); // 3. 推入消费队列 $this-redis-lPush(queue:gift_order, json_encode([ order_id $orderId, user_id $userId, room_id $roomId, amount $gift[price] * $quantity ])); return json_success([order_id $orderId, balance $balance]); }上述逻辑有几个细节值得注意第一hIncrBy返回的是扣减后的最新值判断小于 0 就回滚能防止余额为负。第二MySQL 的gift_order表只在插入时锁定一次后续余额的最终一致性靠队列消费进程来保障。第三订单状态为 pending 时如果消费进程崩溃重启后要从队列里重新拉取所以队列要用lPop手动确认的方式或者用延时队列重试不能简单rPush后就丢弃。4.2 支付回调的幂等设计同一笔订单不能加两次余额打赏若走充值套餐比如充值 100 元送 120 钻那么支付回调和充值加钻的处理顺序就很重要。源码里常见的错是「先加钻、后更状态」一旦回调因为网络超时被微信/支付宝重发用户余额翻倍。一个可复用的幂等模板// PayNotify.php public function handle($outTradeNo) { // 唯一索引order_trade_no 唯一插入冲突则说明已处理 $insert Db::table(pay_log)-insertIgnoreGetId([ order_trade_no $outTradeNo, created_at date(Y-m-d H:i:s) ]); // 如果插入失败说明这条通知是重复的直接返回成功给支付平台 if (!$insert) { return success; } // 查订单并确认金额 $order Db::table(recharge_order)-where(trade_no, $outTradeNo)-first(); if (!$order || $order-status 1) { // 金额对不上直接记录人工介入不能返回成功 Log::error(balance mismatch: . $outTradeNo); return fail; } // 给用户增加钻在 redis 中累加异步回写 MySQL $this-redis-hIncrBy(user:balance:{$order-user_id}, balance, $order-amount); Db::table(recharge_order)-where(id, $order-id)-update([status 1]); return success; }代码核心在于insertIgnoreGetId这行——加上 pay_log 表里order_trade_no的唯一索引后同一笔交易无论回调几次只有第一次能插入成功后面的都走冲突分支直接返回 success。这里还有个细节返回success和fail的语义不同。微信支付遇到fail会隔几秒重试一次重试次数多了会进入风控所以除非是金额不一致这类可能涉及资损的情况其他异常都要返回success并记录日志排查。主播结算也在这个链路内用户送礼后gift_order表的status1时会写一条settle_queue记录按日/周累计到主播收益表。这个表不要每笔实时写而是用一个「每日定时汇总」的脚本处理防止主库写入压力过大。汇总脚本参数示例如下# 每日凌晨 2 点执行统计前一日 00:00-23:59 的礼物、付费视频分成 0 2 * * * php /www/betatest/commands/settle_calculate.php --date-15. 多房间数据隔离与打赏推送的实时性WebSocket 才是最终解5.1 为什么轮询数据不适合高热房间前文提到在线人数用 ajax 轮询实现这在房间同时在线少于 100 人时够用一旦房间热度上来真实运营或开了多房间引流轮询会把简单接口打成热点。比如一个房间 500 人在线每 5 秒一次请求就是每秒 100 个 PHP 请求Nginx 和 PHP-FPM 都能扛住但 MySQL 会扛不住——每个请求都要读 Redis 再回源查房间信息。而且轮询做不了「推送礼物特效」这类需要低延迟的功能。你不想让观众的礼物特效延迟 5 秒才在所有端上出现。所以第二步要引入 WebSocket。在 PHP 生态里用 Workerman 是成本最低的方案它和现有的 ThinkPHP 源码能共存// wsClient.js // 在直播间页面里建立 WebSocket 连接 const ws new WebSocket(ws://live.example.com:8282); ws.onopen function () { // 进入房间带上房间号和用户身份 ws.send(JSON.stringify({ type: join, roomId: 101, token: getUserToken() })); }; ws.onmessage function (evt) { const msg JSON.parse(evt.data); if (msg.type gift) { // 触发礼物特效动画不依赖轮询接口 playGiftEffect(msg.giftId, msg.userName); } };接 WebSocket 的目的不是替代轮询而是把送礼、弹幕、进入房间这三类「必须实时」的消息从 ajax 请求里卸下来剩余的在线人数统计仍走 5 秒轮询。要注意 Workerman 的监听端口要用独立端口如 8282并且在 Nginx 配置里加一条 location 转发否则 80 端口的 HTTP 请求会被正常路由WebSocket 握手失败。5.2 多房间数据隔离的三个层次运行多个直播间时「数据隔离」是运营后台最容易乱的地方。三个层次按顺序处理第一层Redis key 设计。房间维度的 key 统一以live:room:{roomId}:做前缀例如live:room:101:online、live:room:101:gift_list。这样后续做数据迁移、按房间开过期时间都方便。第二层业务表隔离。gift_order、recharge_order、settle_queue都要有room_id字段并在查询列表页强制带这个条件防止后台运营商互相看到流水。第三层主播结算隔离。主播只能看自己房间的礼物汇总场控/超管可以看到全部。很多现成源码在表结构设计上并没有 room_id 字段大量查询都直接依赖user_idjoin 用户表。这样带来的问题是当你想给一个房间做「每日礼物榜」时你要先把房间的所有主播列出来再查他们的流水SQL 复杂度和索引命中率都差。建议往后新增表时统一带 room_id。5.3 直播数据核验从伪数据切真实数据前的验证脚本从演示模式切到真实推流前要有个快速验证脚本确认现有 Redis 计数、消息队列和 WebSocket 是通的。我一般写一个 20 行的 PHP 命令行脚本做冒烟测试// smoke_test.php ?php $redis new Redis(); $redis-connect(127.0.0.1, 6379); $redis-auth(password); // 模拟两个访客同时打赏检验队列消费 for ($i1; $i2; $i) { $redis-lPush(queue:gift_order, json_encode([ order_id rand(10000, 99999), user_id $i, room_id 101, amount 1 ])); } $consumer shell_exec(php /www/betatest/workers/gift_consume.php --once); echo $consumer; // 验证写库结果 $pdo new PDO(mysql:host127.0.0.1;dbnamelive, root, pass); $cnt $pdo-query(SELECT COUNT(*) FROM gift_order WHERE status1)-fetchColumn(); echo confirmed orders: $cnt\n;脚本的用途是「快速确认每一层是通的」不是压测。生产环境压测要用 ab 或 wrk 去打 WebSocket 接口但那属于另一个话题。这个脚本在每次部署后跑一遍能省去大量「明明配置没问题但数据不涨」的排查时间。另外要提一下「自带直播数据」在真实运营里的去留。如果后续接入了真主播和真实推流演示数据的定时脚本一定要停掉否则流水对不上账。怎么优雅停在后台加一个「演示模式」开关开关关闭后只走真实队列保底在线人数的 baseNumber 也置空。这样一个站点可以在开发期用假数据养环境上线前用开关切换即可两套逻辑并存但互不污染。本文还有配套的精品资源点击获取