十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信上墙系统PHP实现:消息队列与长轮询实战

微信上墙系统PHP实现:消息队列与长轮询实战 简介这是一套基于PHP开发的微信上墙大屏幕现场互动系统源码专为年会、发布会、校园活动、商业路演等需要现场大屏互动的场景设计适合活动策划团队、Web开发者以及有二次开发需求的程序员。压缩包共2000个文件压缩后约53.68MB其中JavaScript脚本多达1842个主要用于前端动效、消息推送、大屏渲染等交互逻辑CSS样式表133个负责整体视觉风格与响应式布局HTML页面13个构成大屏展示与交互的主页面结构另配套SQL数据库脚本、Shell部署脚本和Markdown说明文档方便初始化数据、自动化部署与快速上手从而支撑从活动创建、现场消息上墙到互动展示的完整流程。项目代码采用模块化组织页面展示与数据处理逻辑分层清晰可直接部署到PHP环境中运行也可以按活动主题调整配色、修改互动规则、增加新的展示模块同时源码附带完整的目录结构和部署辅助文件无论是本地调试还是迁移到云服务器都较为省时。整体来看这套源码既是一套可直接运行的现场互动应用也是一份学习PHP后台与大屏前端配合开发的实践范本。目前已有556人学习下载适合具备PHP基础、想尽快搭建微信现场互动大屏或学习此类项目源码的开发者参考使用。1. 从一条微信消息到大屏幕上墙系统到底在解决什么问题年会抽奖、产品发布会、校园歌手大赛主持人一声“扫码上墙”几百人同时掏出手机发消息大屏上瀑布流一样滚过祝福和吐槽。这套被称为“微信上墙”的现场互动核心技术链条其实只有三截用户把消息发到微信公众号、服务器把消息存下来、大屏幕按序拉取并渲染。难点不在单个环节而在“同时几百人发消息时消息链路不能断、大屏刷新不能卡”这个并发场景上。这个标题里的 V7.24 指的是某个版本迭代号源码包通常包含完整 PHP 后端、前端大屏页面、数据库初始化脚本和管理后台。对 PHP 开发者来说值钱的部分不是页面动画而是消息接收的验签逻辑、队列缓冲的设计、长轮询参数的选择。本文按“微信侧接入 → PHP 消息处理 → 大屏端拉取 → 部署排错”的顺序把一套可复现的上墙方案拆开讲。正文会给出可以直接落地的代码和命令不依赖任何框架原生 PHP 就能跑。2. 微信上墙的消息入口事件推送与 Token 验证2.1 三种主流“上墙方案”的选型对比市面上常见的微信上墙实现按消息获取方式可以分为三类各有适用场景方案消息到达方式实时性适用场景缺点订阅号群发图文粉丝在文章评论留言人工截图分钟级会议暖场没法互动已被活动弃用服务号/订阅号“扫码回复关键词”用户发送文本消息公众号服务器 Post 到你的接口秒级本标题场景需要已认证服务号且需要处理事件推送微信扫码登录 自建聊天室用户扫码登录网站在网页里输入消息百毫秒级需要更强互动控场不是“微信消息上墙”是“网页消息上墙”V7.24 这类源码走的是第二方案。用户关注公众号或给公众号发消息微信服务器把消息内容 POST 到你配置的 URL 上你的 PHP 接口接收后存入数据库或队列大屏端再拉取。2.2 开发者模式的 URL 校验第一次握手在微信公众号后台开启“服务器配置”后需要提供一个 URL。微信服务器会 GET 这个地址带上signature、timestamp、nonce、echostr四个参数。校验逻辑是把token、timestamp、nonce按字典序排序拼成字符串做 SHA1与signature比对。相等就原样返回echostr微信确认你是服务器主人。?php // wechat_verify.php $token your_token_here; // 与公众号后台配置的 Token 一致 $signature $_GET[signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; $echostr $_GET[echostr] ?? ; $tmpArr array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $tmpStr sha1($tmpStr); if ($tmpStr $signature) { echo $echostr; exit; } echo forbidden;排序用了SORT_STRING而不是默认的SORT_REGULAR这是细节——微信官方文档的示例代码用的是字符串比较但很多老代码直接sort($tmpArr)在纯数字字符串时没问题一旦 token 里混入字母和数字组合普通排序和字符串比较可能产生差异导致校验通过率不稳定。接收 POST 消息的入口也能复用这套验签只是echostr为空时返回success。这里的exit很关键。校验成功后必须立即结束输出不能让 PHP 脚本继续往下加载框架、连接数据库否则响应超时微信后台会反复重试。微信服务器对 URL 校验的等待时间很短一般 5 秒内没有响应就算失败。2.3 接收消息的入口XML 解析与消息去重用户发一条消息微信服务器 POST 来的是 XML 格式形如xml ToUserName![CDATA[gh_xxx]]/ToUserName FromUserName![CDATA[oXXXX]]/FromUserName CreateTime1700000000/CreateTime MsgType![CDATA[text]]/MsgType Content![CDATA[祝活动圆满]]/Content MsgId2400000000000000000/MsgId /xmlFromUserName是用户的 OpenIDContent就是上墙内容MsgId是微信侧的唯一消息 ID。解析时用simplexml_load_string要注意 PHP 8 以后simplexml扩展默认启用但如果在编译时禁用了需要用php -m | grep simplexml确认。解析后最容易被忽略的一步是消息幂等微信对超时未响应的请求会重试三次如果不加去重同一条用户消息会被存三遍大屏上就会出现“刷屏”。?php $xml file_get_contents(php://input); $msg simplexml_load_string($xml, SimpleXMLElement, LIBXML_NOCDATA); $openid (string)$msg-FromUserName; $content trim((string)$msg-Content); $msgId (string)$msg-MsgId; $msgType (string)$msg-MsgType; if ($msgType ! text || $content ) { exit(success); // 非文本消息或空消息直接丢弃 } $pdo new PDO(mysql:host127.0.0.1;dbnamewall;charsetutf8mb4, user, pass); $stmt $pdo-prepare(INSERT IGNORE INTO wall_messages (msg_id, openid, content, created_at) VALUES (?, ?, ?, NOW())); $stmt-execute([$msgId, $openid, $content]); echo success;INSERT IGNORE依赖msg_id的唯一索引。建表时给msg_id加UNIQUE KEY重复推送自然被数据库拦掉比先用SELECT查再用INSERT少一次查询在并发场景下也更稳。exit(success)是微信的规范——接收到消息后必须返回success大小写不敏感否则微信会视作失败并重试。3. PHP 后端核心从入库到队列再到长轮询3.1 为什么不能直接查数据库并发瓶颈分析活动开始时往往是“主持人话音刚落、几百条同时到达”。如果大屏端每 2 秒查一次SELECT * FROM wall_messages ORDER BY id DESC LIMIT 50问题不大但如果做了关键词过滤、黑名单校验、用户头像联查一次查询要 join 三张表在 500 人同时在线时会发现 MySQL 的连接数被拉满max_connections默认 151加上 WordPress 或后台管理也在用同一个库很快就会出现Too many connections。常见做法是引入 Redis 做缓冲层。消息接收接口只做三件事解析 XML、写 Redis 列表、返回 success。大屏端从 Redis 里取MySQL 只做最终归档。3.2 用 Redis List 当消息队列LPUSH 与 BRPOP# 用户消息写入队列PHP 端 $redis-lpush(wall:queue: . $screenId, json_encode([ openid $openid, content $content, time time() ])); # 大屏端消费者拉取PHP CLI 常驻脚本 while (true) { $data $redis-brpop(wall:queue: . $screenId, 5); if ($data) { // 处理过滤、存库、推送给大屏 } }LPUSH从队列头部写入BRPOP从尾部阻塞读取5表示如果没有数据最多阻塞 5 秒。这样消息按 FIFO 顺序被消费且 Redis 的BRPOP是原子的多开几个消费者进程也不会重复取同一条——这一点比用 MySQLSELECT ... FOR UPDATE做队列要轻量得多。这里有个很多人踩过的坑Redis 的 list 是内存结构消息量大的时候要注意内存水位。一条 JSON 消息按 200 字节估算5000 条也才 1MB 左右普通活动完全够用。但如果消费者进程挂了队列里的消息会一直堆积Redis 内存被打满触发淘汰策略默认noeviction会直接报错。所以要有兜底消费者进程用supervisor守护同时消费者拿到消息后要同时写入 MySQL。3.3 大屏拉取接口按 ID 增量而非按时间大屏端有两种拿新消息的方式每次请求都返回“从第 N 条之后的新消息”或者“从某个时间点之后的新消息”。时间方案在活动跨天时会出问题更稳妥的是用自增 ID 做游标。?php // api.php?actionmessagescursor1024limit50 $cursor (int)($_GET[cursor] ?? 0); $limit min(100, (int)($_GET[limit] ?? 50)); $stmt $pdo-prepare( SELECT id, openid, content, created_at FROM wall_messages WHERE id ? AND screen_id ? ORDER BY id ASC LIMIT ? ); $stmt-bindValue(1, $cursor, PDO::PARAM_INT); $stmt-bindValue(2, $screenId, PDO::PARAM_INT); $stmt-bindValue(3, $limit, PDO::PARAM_INT); $stmt-execute(); $rows $stmt-fetchAll(PDO::FETCH_ASSOC); $lastId $rows ? end($rows)[id] : $cursor; header(Content-Type: application/json); echo json_encode([last_id $lastId, items $rows]);用id 游标而不是LIMIT offset是因为 offset 翻页在数据量大时有严重的性能衰减。大屏端每次请求结束把自己拿到的最大last_id存到内存或 LocalStorage下一次请求带着它来天然实现增量拉取。MIN(id)和MAX(id)之间有空隙也不怕id cursor保证不漏。面试时如果被问到“微信上墙的数据接口怎么设计”这个增量 ID 方案比时间戳方案更有说服力。3.4 长轮询还是 WebSocket一个务实的判断V7.24 这种老牌源码多数用的是 Ajax 轮询每 1~2 秒请求一次。更优的做法是长轮询大屏端发起请求后服务端 hold 住连接最多 25 秒有消息立即返回没消息到超时返回空。这样把请求频率从每秒 0.5 次降到每秒 0.04 次PHP-FPM 的并发连接数压力骤减。实现长轮询最简单的办法是在接口里用 Redis 的BRPOP阻塞等待?php // api.php?actionlongpollcursor1024timeout25 $timeout min(25, (int)($_GET[timeout] ?? 25)); $redis new Redis(); $redis-connect(127.0.0.1, 6379); $result $redis-brpop(wall:new: . $screenId, $timeout); if ($result) { // 有新消息返回给大屏 } else { // 超时无消息返回空数组 原游标 echo json_encode([last_id $cursor, items []]); }注意这里不能直接用BRPOP消费生产者那边的队列——如果长轮询把队列里的消息 pop 走了消费进程就读不到了。常见做法是消息接收进程往两个地方写一个写 MySQL做归档、一个往wall:new:$screenId这个 list 里 push 一个“有新消息”的信号不包含内容。大屏长轮询收到信号就改用游标去按 ID 拉取内容。失去的是极端实时的内容推送换来的是逻辑简化。长轮询对 PHP 的max_execution_time有要求。CLI 模式不受限制但 FPM 模式默认 30 秒需要调大max_execution_time 35同时 Nginx 侧要关闭代理超时location /api { proxy_read_timeout 30s; fastcgi_read_timeout 30s; }4. 大屏端渲染与消息审查拉下来的消息怎么处理4.1 大屏页面用原生 JavaScript 拉取并渲染大屏端通常是一个独立的前端页面部署时可以放在screen.html由一台连接投影的电脑或电视盒子访问。代码不需要框架原生 JavaScript 加 CSS 动画就够。核心逻辑是启动后立即拉一次历史消息然后进入长轮询循环// screen.js let cursor 0; let container document.getElementById(wall-container); async function init() { const res await fetch(/api.php?actioninitscreen_id1limit100); const json await res.json(); cursor json.last_id; render(json.items); loop(); } async function loop() { try { const res await fetch(/api.php?actionlongpollcursor${cursor}timeout25); const json await res.json(); if (json.items.length 0) { cursor json.last_id; render(json.items); } } catch (e) { // 网络错误延迟 3 秒后重试 setTimeout(loop, 3000); return; } loop(); } function render(items) { items.forEach(item { let div document.createElement(div); div.className wall-item; div.innerHTML span classavatar${maskOpenid(item.openid)}/span span classcontent${escapeHtml(item.content)}/span ; container.appendChild(div); }); // 只保留最近 200 条防止 DOM 无限增长 while (container.children.length 200) { container.removeChild(container.firstChild); } } function escapeHtml(str) { const div document.createElement(div); div.textContent str; return div.innerHTML; }escapeHtml必须做。用户发消息的输入框在微信侧你控制不了内容万一有人发img srcx onerror...如果不转义就是存储型 XSS。maskOpenid也很重要——OpenID 是用户身份标识直接展示在公屏上不礼貌可以取前 2 位加***后 2 位。loop()在错误时延迟 3 秒重试而不是 1 秒是防止接口临时故障造成前端疯狂请求。4.2 消息过滤的三层策略上墙内容不能原样放出去。最基础的三层过滤层级手段实现思路接收侧正则黑名单匹配命中直接不入库展示侧敏感词替换保留消息但把敏感词替换为*管理侧人工审核开关后台先审后发适合正式会议正则黑名单直接加在第三节的接收逻辑里$blacklist [广告词1, 广告词2, ...]; foreach ($blacklist as $word) { if (mb_strpos($content, $word) ! false) { exit(success); // 直接丢弃不返回错误信息 } }注意丢弃时也返回success不要返回任何提示文案给微信——返回非 success 会让微信重试造成无效请求堆积。展示侧替换则用下面这段和黑名单不同替换后消息还能展示$content str_replace([敏感词A, 敏感词B], ***, $content);4.3 限流与频率控制防止一个人刷屏活动现场最容易出现的情况是某个人连发 20 条把别人都挤下去了。限流逻辑放在接收入口$redisKey rate:{$openid}: . date(YmdHi); // 同一分钟 $count $redis-incr($redisKey); if ($count 1) { $redis-expire($redisKey, 60); } if ($count 5) { exit(success); // 该用户这一分钟内发了超过 5 条静默丢弃 }incr加expire是 Redis 做滑动窗口限流最省事的方案。这里取整分钟窗口允许每用户每分钟 5 条活动场景一般够了。exit(success)同样是静默丢弃微信那边看起来消息发出去了只是大屏不显示避免提示用户导致对方反复试。如果要求更精细的滑动窗口需要改用 ZSET 记录每次发送时间戳再做范围计数但代码量会多出十几行普通活动没必要。5. Nginx 部署、超时参数与消息链路验证5.1 Nginx 与 PHP-FPM 的配置要点源码包通常自带 PHP 代码和前端页面部署在 LNMP 或宝塔环境里。接入微信后台时URL 必须是公网可访问的 HTTPS 地址80 端口的明文地址在微信公众平台提交时也能过但 2017 年后微信强制要求使用 80 或 443建议直接配证书。Nginx 站点配置里除了常规的fastcgi_pass要点是 PHP 的max_execution_time和请求体大小限制server { listen 80; server_name wall.example.com; root /var/www/wall/public; location /api { proxy_read_timeout 30s; fastcgi_read_timeout 30s; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/api.php; fastcgi_pass 127.0.0.1:9000; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }proxy_read_timeout和fastcgi_read_timeout设置成 30 秒是为了让长轮询接口能在 25 秒后正常返回不至于被网关提前掐断。另一个隐蔽的坑是 Nginx 默认client_max_body_size 1m微信消息 POST 的 XML 很小不受影响但如果后续要支持图片上墙必须调大。图片上墙另说——V7.24 的核心是文本上墙图片多了内存带宽都是压力可以后续加。5.2 消息链路自检从公众号后台到数据库部署完最容易出现的现象是前台发了消息大屏没反应。排查链路按顺序走# 1. 看 PHP 日志有没有报错 tail -f /var/log/php-fpm/error.log # 2. 看 MySQL 里有没有收到消息 mysql -uwall_user -p wall_db -e SELECT id, content, created_at FROM wall_messages ORDER BY id DESC LIMIT 5; # 3. 看 Redis 队列堆积 redis-cli llen wall:queue:1如果 MySQL 没有数据、Redis 也没有数据问题出在微信到 PHP 这一段。用curl模拟微信推送最直接curl -X POST https://wall.example.com/wechat.php \ -H Content-Type: application/xml \ -d xml ToUserName![CDATA[gh_test]]/ToUserName FromUserName![CDATA[o_test_openid]]/FromUserName CreateTime1700000000/CreateTime MsgType![CDATA[text]]/MsgType Content![CDATA[测试上墙]]/Content MsgId123456789/MsgId /xmlcurl 通了但微信不通原因集中在 IP 白名单。微信公众平台后台有一个“IP 白名单”配置只允许指定 IP 调用接口获取 access_token但服务器配置的 URL 接收消息不受 IP 白名单限制——这是很多人混淆的点。真正影响消息接收的是 URL 校验是否通过以及服务器是否在微信侧配置了“服务器配置”而非“明文模式”。还要注意微信服务器向你的 URL 推送时用的是固定网段如果你的服务器有防火墙需要放行微信官方公布的 IP 段具体可以在微信公众平台文档查。分享一个节约时间的验证思路在微信开发者工具里打开“公众平台测试号”配置相同的 URL 和 Token用测试号发消息能在大屏上看到就说明核心链路没问题再切换正式号排查认证和权限。5.3 把消费者进程交给 supervisor 守护第 3.2 节里写的消费者脚本是 CLI 常驻进程直接php consumer.php跑SSH 断开就挂了。用 supervisor 守护[program:wall-consumer] commandphp /var/www/wall/consumer.php process_name%(program_name)s_%(process_num)02d numprocs2 autostarttrue autorestarttrue redirect_stderrtrue stdout_logfile/var/log/wall-consumer.log stderr_logfile/var/log/wall-consumer-error.log userwww-datanumprocs2开两个消费者进程BRPOP的原子性保证同一消息不会被两个进程同时取到相当于天然分流。autorestarttrue让进程崩溃后自动拉起活动期间没人盯着服务器也能自愈。配置完成后supervisorctl reread supervisorctl update supervisorctl status这三个命令分别是重读配置、应用新配置、查看运行状态。如果supervisorctl status显示RUNNING但脚本实际没干活先看wall-consumer-error.log最常见的原因是 PHP 没装 Redis 扩展——用php -m | grep redis确认。5.4 活动结束后的归档与性能复盘活动结束后不要急着删数据。把wall_messages表按场次导出备档用时间戳分区或简单加一个screen_id条件就行。复盘时关注两个数字消息总数和峰值 QPS。消息总数直接COUNT(*)峰值 QPS 看 Nginx access log 里/api/路径的请求分布grep /api/ /var/log/nginx/access.log \ | awk {print $4} | cut -d: -f2 \ | sort | uniq -c | sort -rn | head -5这条命令统计了每分钟内/api/请求次数按降序显示前 5 名。如果你看到最高峰远超预期说明长轮询没生效前端可能退化成频繁轮询了。V7.24 源码里的前端默认间隔如果是 500ms建议直接改成 2 秒轮询或接长轮询效果立竿见影。现场活动容不得“现场调试”这套链路测熟上墙才真正可控。本文还有配套的精品资源点击获取
返回列表