简介面向需要搭建私域流量监测或朋友圈互动分析场景的PHP开发者这份微信朋友圈访客记录修复版源码可快速用于二次开发或直接部署。基于PHP7.4与MySQL5.6环境运行核心功能包括VIP会员付费、发布内容并记录访客点击数据已集成微信登录与易支付两大接口形成从付费到访问统计的完整闭环。资源包共140个文件以78个PHP业务逻辑文件为主辅以SQL数据库脚本、CSS/JS前端资源与各类图片素材并含配置、缓存、日志等目录压缩包大小约459KB目录结构清晰便于定位修改与上线前排查。目前已有116人学习下载对于想要快速搭建访客统计或熟悉微信生态支付闭环的开发者而言这份修复版源码省去自行排查接口对接的麻烦附带的数据库文件和搭建教程也能有效降低部署门槛。1. 先泼一盆冷水朋友圈访客记录在微信生态里是个伪命题微信朋友圈没有开放任何“谁看过我”的接口客户端不回调访客数据服务端也不对第三方提供这类数据。你把微信团队自己开发的接口文档从头翻到尾能找到的只有发表、删除、评论、点赞这类基于用户主动操作的接口没有任何“读取访客列表”的能力。所以市面流传的“微信朋友圈访客记录系统”本质上只有两种东西要么是前端做出来的假界面用一个本地数组模拟访客列表刷新页面换个名字要么是诱导用户扫码授权后拿到头像昵称做成所谓“访客墙”真实记录从第一行代码起就是不存在的。但“PHP修复版”这四个字有价值。它意味着你拿到的是一套已经被人修过 bug、补过缺失文件的 PHP 老项目里面通常包含一个完整的访客管理后台管理员登录、访客列表、IP 归属地、访问轨迹、浏览器指纹、黑名单屏蔽。这套东西的骨架和逻辑是真实可用的把它改造成自家网站、后台系统、小程序管理端的“访客管理工具”是正经能落地的事。这篇文章就顺着这个标题往下拆这套代码里哪些文件在干实事修复老 PHP 项目要踩哪些坑以及怎么把它改成一套合规、能跑、不依赖微信任何接口的访客管理系统。适合手里已经躺着这份源码却跑不起来的开发者也适合想借“访客记录”需求练手 PHP 后台开发的新手。话说在前头想靠它查谁看了你朋友圈趁早死心想靠它学一套访客管理系统的落地方案往下看。2. PHP修复版的骨架一份“访客系统”源码里到底有哪些文件在干活解开压缩包后你通常会看到一个典型的、没有使用框架的 PHP 项目目录。老派源码的目录结构没有强约束但万变不离其宗核心文件就那么几类入口脚本、公共配置、数据库连接、后台管理页面、前端展示页、以及一个装着 SQL 初始化语句的数据库文件。先搞清楚每个文件是干什么的比急着改代码重要得多——这类修复版源码最常见的毛病就是目录里躺着十几个没用但报错的残留文件。2.1 先按文件类型把源码分拣成四类我拿到任何一份陌生 PHP 源码第一件事不是打开 README而是打开文件管理器按类型分拣。这份源码里的 .php 文件可以分成四组入口和控制脚本index.php、login.php、logout.php、admin.php 这类、功能脚本record.php、visit.php、list.php 这类负责写访客数据和查访客数据、公共模块config.php、db.php、function.php负责数据库连接和通用函数、模板页面通常是一堆 .html 文件或者混在 PHP 文件里的 HTML 代码块。.sql 文件是数据库初始化脚本.txt 和 .md 是说明文档剩下的 .jpg、.css、.js 不用管。分拣完了你就知道这套系统的运行链路是什么用户访问某个页面 → 前端 JS 发起请求到 record.php → record.php 读取用户 IP、User-Agent、Referrer → 存入数据库 → 管理员打开 admin.php → 从数据库读取访客记录并展示。这条链路里访问者和管理员各走一套页面互不干扰任何访客管理系统的核心都是这么两条线。如果你打开源码后发现没有 record.php 或 visit.php只有一个写着“访客记录”的 PHP 文件那你拿到的多半是假系统——它只是把写死的数组渲染成了页面根本没有数据库交互。2.2 从 config.php 和 db.php 看这套系统的“体质”?php // config.php - 公共配置 define(DB_HOST, 127.0.0.1); // 数据库地址本地开发用 127.0.0.1 避免走 TCP 解析 define(DB_USER, root); // 数据库用户 define(DB_PASS, your_password); // 数据库密码 define(DB_NAME, visitor_db); // 数据库名请和 .sql 文件里的库名保持一致 // 时区必须设否则 date() 记录的时间比北京时间少 8 小时 date_default_timezone_set(Asia/Shanghai); // 是否开启调试模式。老源码习惯用这个常量控制错误显示 define(DEBUG, true); if (DEBUG) { error_reporting(E_ALL); ini_set(display_errors, 1); } else { error_reporting(0); } ??php // db.php - 数据库连接PDO 方式 require_once config.php; // 老源码里大多是 mysqli 或 mysql_* 函数修复版一般会换成 PDO // 用 PDO 的好处是参数绑定能挡住 SQL 注入而且换数据库不用改业务代码 try { $pdo new PDO( mysql:host . DB_HOST . ;dbname . DB_NAME . ;charsetutf8mb4, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, // 这行很关键PDO 默认是模拟预处理关掉它走真正的预处理 PDO::ATTR_EMULATE_PREPARES false, ] ); } catch (PDOException $e) { // 生产环境不要把异常信息直接吐给用户写日志就好 error_log($e-getMessage()); die(数据库连接失败请检查配置文件); } ?这里有两个判断源码新旧的关键点。第一个是数据库扩展如果代码里出现mysql_connect注意不是mysqli_connect那它的 PHP 版本要求不会高于 5.x在 PHP 7 之后这种函数已经被移除跑起来就是致命错误。第二个是数据库连接字符集老项目最常见的翻车点是只设置了SET NAMES UTF8用的还是utf8不是utf8mb4遇到 emoji 表情直接存不进去访客昵称里带个表情就报错。上面这个 config.php 里用常量定义参数是 PHP 老项目里最通用的写法陌生代码你只要看到define()开头就能顺着常量名找到所有配置项。2.3 数据库初始化脚本访客表结构长什么样-- visitor_log.sql - 访客记录表修复版常见结构 CREATE TABLE IF NOT EXISTS visitor_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, visitor_name VARCHAR(50) DEFAULT 匿名, -- 访客标识 visitor_ip VARCHAR(45) DEFAULT , -- IPv4 用 15 位IPv6 最长 45 位 visit_time DATETIME DEFAULT NULL, -- 访问时间 visit_page VARCHAR(255) DEFAULT , -- 访问了哪个页面 referrer VARCHAR(255) DEFAULT , -- 从哪个页面跳转过来 user_agent VARCHAR(255) DEFAULT , -- 浏览器和操作系统信息 ip_location VARCHAR(100) DEFAULT , -- IP 归属地 is_blocked TINYINT(1) DEFAULT 0, -- 是否拉黑 PRIMARY KEY (id), KEY idx_visit_time (visit_time), -- 时间字段必须建索引访客列表按时间排序是高频查询 KEY idx_ip (visitor_ip) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;一张能用的访客记录表至少需要这九个字段。visitor_name在老源码里通常抓取的是微信昵称但本地网站场景下你根本拿不到所以默认值设为“匿名”才是诚实的做法visitor_ip用VARCHAR(45)而不是VARCHAR(15)是为了兼容 IPv6user_agent别嫌长现在浏览器 UA 动辄两三百字符255 是底线is_blocked这个字段最有意思它是黑名单功能的开关你把它和visitor_ip组合起来查就能实现“某个 IP 被拉黑后不再记录”。索引的选择遵循一个朴素原则你 WHERE 和 ORDER BY 什么字段就给什么字段建索引访客系统里时间字段永远是查询第一条件。ENGINEInnoDB是必须的老源码里如果写的是 MyISAM建议你手动改过来InnoDB 支持事务、行级锁MyISAM 在并发写入时会锁表访客稍微多一点就卡。2.4 入口文件梳理先跑通一条完整链路再改代码# 假设你用的是本地 PHP 内置服务器PHP 5.4 自带 # 在源码根目录执行不需要配置 Apache/nginx 就能启动 php -S 127.0.0.1:8080 # 然后在浏览器访问 # http://127.0.0.1:8080/index.php跑通链路的最小步骤就两步把 .sql 文件导入 MySQL然后启动内置服务器。内置服务器适合调试它只用一个进程处理所有请求跟生产环境的并发模型不一样但排查“这个页面为什么 500”的时候它最直接。如果你用集成环境常见的有 phpStudy、XAMPP 这类就把源码整个丢进www或htdocs目录访问路径变成http://127.0.0.1/源码目录名/index.php。访问 index.php 之后你打开visitor_log表如果能看到一条记录说明整条链路已经通了——前端页面被打开JS 调用 record.phpIP 和 UA 被写进数据库。从这里开始你才算真正拿到了这套源码的“控制权”后续所有修改都在这条链路的基础上进行。3. 核心逻辑改造把“朋友圈访客”改成真正能用的网站访客管理系统当你确认链路通了之后就到了整个改造过程最核心的环节。为什么这个环节放在开头讲因为很多人拿到源码第一件事是登录后台管理界面点开“访客列表”发现空白一片于是以为代码是坏的。实际上后台没数据是因为前台根本没有埋点。访客管理系统的价值全在数据采集端采集端没打通后台做得再花哨也是空壳。3.1 重写 record.php采集端是访客系统的命脉?php // record.php - 访客数据采集入口 require_once db.php; // 第一步获取真实 IP // 不要用 $_SERVER[REMOTE_ADDR] 一把梭因为用户前面可能挂了 CDN 或反向代理 // 但要警惕伪造 X-Forwarded-For 头所以只在可信代理后取这个值 function getClientIp() { $ip $_SERVER[REMOTE_ADDR] ?? ; // 这里是修复版源码最常见的改动点加代理判断 if (!empty($_SERVER[HTTP_X_FORWARDED_FOR])) { // X-Forwarded-For 可能是逗号分隔的多个 IP最左边是原始客户端 $parts explode(,, $_SERVER[HTTP_X_FORWARDED_FOR]); $first trim($parts[0]); // 简单过滤防止伪造只保留合法 IP 格式 if (filter_var($first, FILTER_VALIDATE_IP)) { $ip $first; } } return $ip; } $ip getClientIp(); // 第二步准备入库数据 $visit_page $_SERVER[REQUEST_URI] ?? /; // 访问的页面路径 $referrer $_SERVER[HTTP_REFERER] ?? ; // 来源页为空表示直接访问 $user_agent substr($_SERVER[HTTP_USER_AGENT] ?? , 0, 250); // 截断防止超字段长度 // 第三步记录之前先查黑名单拉黑的 IP 直接跳过不写库 $stmt $pdo-prepare(SELECT COUNT(*) FROM visitor_log WHERE visitor_ip ? AND is_blocked 1); $stmt-execute([$ip]); if ($stmt-fetchColumn() 0) { http_response_code(403); exit; } // 第四步插入访客记录 $sql INSERT INTO visitor_log (visitor_ip, visit_time, visit_page, referrer, user_agent) VALUES (?, NOW(), ?, ?, ?); $stmt $pdo-prepare($sql); $stmt-execute([$ip, $visit_page, $referrer, $user_agent]); // 第五步返回空响应即可前端不需要关心结果 http_response_code(200); ?这段代码有四个值得展开讲的参数决策。第一个是NOW()直接交给数据库生成时间而不是在 PHP 里date(Y-m-d H:i:s)好处是时间来源统一不会出现 PHP 和 MySQL 时区不一致导致的时间错乱。第二个是字段全部用?占位符走预处理这是防 SQL 注入的红线访客的 UA 和 Referrer 是攻击者最容易塞恶意内容的地方直接拼接 SQL 等于把数据库门锁拆了。第三个是黑名单检查放在写入之前业务逻辑上讲得通——被拉黑的人不应该留下新的访问痕迹。第四个是http_response_code(200)有些修复版源码这里会输出一段 JSON 或一行文字完全没必要前端是无感的多输出任何内容都只是浪费带宽而且可能引发跨域问题。前端埋点代码一般长这样放在你网站的公共底部模板里// footer.js - 访客埋点 // 页面加载后异步发送请求不阻塞渲染 // sendBeacon 是比 fetch 更合适的选择 // 1. 页面关闭时也能把数据发出去fetch 可能被浏览器中断 // 2. 不关心响应内容完美匹配后端空响应 if (navigator.sendBeacon) { navigator.sendBeacon(record.php); } else { // 老浏览器降级方案 fetch(record.php, {method: POST, keepalive: true}); }sendBeacon是这个采集方案里最优先的选择。它在浏览器卸载页面时也会尽量保证把请求发出去这对访客记录系统来说是决定性的——很多用户访问完页面就关掉标签页用fetch或XMLHttpRequest发的异步请求可能还没来得及到达服务器就被浏览器杀掉了。降级到fetch加keepalive: true是为了兼容 IE 这类老浏览器虽然现在这种兼容意义不大但老源码的用户画像里总有几个用古董浏览器的。3.2 改造后台列表分页、筛选、IP 归属地三个必做功能?php // admin_list.php - 后台访客列表核心查询 require_once db.php; session_start(); // 简单的后台登录校验生产环境要换成更严密的鉴权 if (empty($_SESSION[is_admin])) { header(Location: login.php); exit; } // 接收筛选参数用 ? 占位符绑定防止 SQL 注入 $where []; $params []; if (!empty($_GET[ip])) { $where[] visitor_ip LIKE ?; $params[] % . $_GET[ip] . %; } if (!empty($_GET[start_date])) { $where[] visit_time ?; $params[] $_GET[start_date] . 00:00:00; } if (!empty($_GET[end_date])) { $where[] visit_time ?; $params[] $_GET[end_date] . 23:59:59; } $whereSql $where ? (WHERE . implode( AND , $where)) : ; // 分页参数page 从 1 开始每页 20 条是后台列表的通用默认值 $page max(1, intval($_GET[page] ?? 1)); $pageSize 20; $offset ($page - 1) * $pageSize; // 先查总数再查当前页数据两步是分页的标准姿势 $countStmt $pdo-query(SELECT COUNT(*) FROM visitor_log $whereSql); $total $countStmt-fetchColumn(); $pageStmt $pdo-prepare(SELECT * FROM visitor_log $whereSql ORDER BY visit_time DESC LIMIT ? OFFSET ?); $pageStmt-execute([$pageSize, $offset]); $list $pageStmt-fetchAll(); ?分页这里有个新手容易忽略的点LIMIT ? OFFSET ?里的两个参数在 PDO 里默认会被当成字符串处理如果你在execute()里传的是20和40MySQL 有时候会报语法错。解决办法有两个一个是上面代码里的写法——在prepare()之后用$pdo-setAttribute(PDO::ATTR_EMULATE_PREPARES, false)也就是 db.php 里已经做过的那个设置真实预处理模式下整型参数会被正确处理另一个保守做法是把分页参数强制intval()之后直接拼进 SQL因为已经是整型了没有注入风险。我倾向于把两件事都做了db.php 里关掉模拟预处理业务代码里再强制转一次整型双保险不丢人。IP 归属地这个功能要单独说因为老源码里最常见的做法是调用一个第三方接口按 IP 查归属地。这做法有两个隐患第一是第三方接口的稳定性不在你手里服务商挂了你的访客列表就少一列数据第二是很多老接口已经失效返回空数据。我更推荐把归属地解析做成离线方案引入一个 IP 段数据库文件网上有开源协议宽松的离线 IP 库在显示列表时用二分查找匹配 IP 段。匹配逻辑大概是把 IP 转成整型在有序数组里查它落在哪个区间然后返回对应的省市区。这样查询耗时稳定在毫秒级不依赖外网也不存在接口限额问题。代价是 IP 段数据库文件有几十 MB对老项目来说体积增加不算大但收益是实打实的稳定。3.3 黑名单功能从“记录谁来过”到“拦住不想见的人”?php // block.php - 拉黑/解封操作 require_once db.php; session_start(); if (empty($_SESSION[is_admin])) { exit(未登录); } $ip $_POST[ip] ?? ; $action $_POST[action] ?? ; // block 或 unblock // PHP 的 FILTER_VALIDATE_IP 支持 IPv4 和 IPv6 if (!filter_var($ip, FILTER_VALIDATE_IP)) { exit(无效 IP); } if ($action block) { $stmt $pdo-prepare(UPDATE visitor_log SET is_blocked 1 WHERE visitor_ip ?); $stmt-execute([$ip]); } elseif ($action unblock) { $stmt $pdo-prepare(UPDATE visitor_log SET is_blocked 0 WHERE visitor_ip ?); $stmt-execute([$ip]); } header(Location: admin_list.php); ?黑名单的实现要理解一个关键点它是加在访客记录行上的标记不是一张独立黑名单表。这样做的设计取舍是你要拉黑一个 IP只需要对着访客列表点一下按钮不需要手动填表单操作链路最短同时记录里保留了被黑之前的访问历史查证的时候翻得出来。缺点是如果你拉黑了某个 IP它之前的所有记录都被标记为黑名单状态列表筛选“只看正常访客”时这些历史记录就消失了。如果你觉得这个行为不合理可以把is_blocked从“对 IP 生效”改成“对单条记录生效”即拉黑时只置灰当前这一条。两种语义没有对错取决于你要解决什么问题——产品想表达“这个 IP 被我不欢迎”还是“这条访问记录异常”。我建议保留老源码的“IP 级拉黑”语义因为访客系统的诉求是拦住人不是标记一条数据。4. PHP修复版排坑五条踩过的雷从报错到假数据一次说清修复版源码之所以叫“修复版”恰恰是因为原版有太多坑。我按踩坑频率从高到低排了五条每一条都是实际跑代码时会被真实卡住的问题。这些坑不解决后面的改造都是空中楼阁。4.1 白屏或 500 错误PHP 版本带不动老语法现象浏览器访问 index.php 直接白屏打开错误显示后看到类似syntax error, unexpected [或Fatal error: Call to undefined function mysql_connect()的报错。原因老源码是 PHP 5.x 时代的写法mysql_connect()系列函数在 PHP 7.0 已经被移除短数组语法[]需要 PHP 5.4 但老项目里大量混用array()还有一些写法用到了 PHP 7 才弃用的特性。解决先看报错定位是哪一类。如果是Call to undefined function说明代码里还在用mysql_*老 API需要全局把mysql_connect、mysql_query、mysql_fetch_array分别替换为mysqli_connect、mysqli_query、mysqli_fetch_assoc或者像第 2 章那样整体切换到 PDO。如果是语法错误优先看是不是 PHP 7 不支持的类名或构造方法写法比如function ClassName()这种构造函数写法在 PHP 7 里已经不会被识别。最常见的偷懒方案是直接装一个 PHP 5.6 的环境来跑但我不推荐——你这是给自己埋雷以后所有基于这份代码的二次开发都被锁死在老版本上。正确做法是一次性把代码迁移到 PHP 7.4PDO mysqli虽然前两小时难受但换来的是一劳永逸。4.2 数据库乱码UTF8 与 UTF8MB4 的字符集之争现象访客昵称里带 emoji存进去之后显示成???或者后台列表里中文正常但导出到 Excel 后中文乱码。原因老源码建表时用的是DEFAULT CHARSETutf8MySQL 的utf8是“假的 UTF-8”它最多支持 3 字节的字符emoji 是 4 字节直接存不进去。另外很多老项目的数据库连接串里没有指定charset导致应用和数据库之间走默认的 latin1 通信。解决两步走。第一步把库和表的字符集全部改成utf8mb4注意COLLATE也要跟着改用ALTER TABLE visitor_log CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci。第二步在 db.php 里 PDO 连接串上显式加charsetutf8mb4像第 2 章代码里那样。做完这两步插一条带 emoji 的测试数据验证。还有一个隐性坑如果你的数据库连接串是charsetutf8mb4但表结构还是 utf8MySQL 不会报错但会悄悄截断字符表现为数据丢失但不报错——这种问题最阴间排查时记得两边都查。4.3 访客时间少了 8 小时时区配置的双层一致性现象访客访问时间是下午 14:00后台显示 06:00只有时间没有日期错误或者日期直接差了一天。原因PHP 和 MySQL 两个时区设置不一致。PHP 的默认时区可能是 UTCMySQL 的NOW()用的系统时区又是东八区或者反过来PHP 用date()写入字符串时按北京时间MySQL 读出来按 UTC 存。这个坑的隐蔽之处在于它不影响代码执行页面不报错只是数据错得离谱。解决第 2 章的 config.php 里date_default_timezone_set(Asia/Shanghai)是第一步它管住所有 PHP 侧的时间生成。第二步要在 MySQL 里执行SET time_zone 08:00这个设置要写进数据库初始化脚本里否则每次重启 MySQL 都会丢。第三步是检查 .sql 文件建表语句里visit_time字段有没有DEFAULT CURRENT_TIMESTAMP如果有MySQL 的时区设置直接决定这个默认值生成的时刻。三层都对齐了时间字段才会老实。4.4 访客列表一片空白采集端 JS 跨域与路径双重问题现象后台能登录管理员手动往数据库插一条数据也能显示但真实用户访问网站后列表里就是没记录。原因两个原因各占一半。采集 JS 文件路径不对是最常见的源码里 footer.js 用相对路径引用了 record.php但你的网站 URL 用了伪静态重写导致页面实际在/article/123.html而相对路径解析成了/article/record.php自然 404。另一个原因是部署了 CDN 或静态资源独立域名页面在一个域名JS 在另一个域名record.php 接口没配跨域头。解决排查时先在浏览器按 F12 打开 Network 面板刷新页面看 record.php 请求是不是红色失败失败就根据状态码判断——404 是路径问题把 footer.js 里的地址改成/record.php这种带根路径的绝对地址跨域报错就在 record.php 顶部加响应头header(Access-Control-Allow-Origin: *)注意这个*在生产环境要收敛成你自己的域名。如果是请求发出去了但数据库没反应把 UA 和 IP 字段打印到页面调试重点看是不是黑名单逻辑误杀了你自己——有些老代码会把本地回环地址 127.0.0.1 当成攻击 IP 自动拉黑。4.5 登录后台进不去会话配置与验证码功能的双坑现象输入正确的管理员密码点击登录后页面刷新回到登录页没有任何报错或者登录页验证码图片裂开/不显示。原因登录持久化依赖 PHP Session而 session 失效的常见原因有三个——session 保存路径不可写、session_start()被输出之后才调用导致 header 无法重定向、服务器时间偏差导致 session cookie 过期判断错误。验证码图片裂开多半是因为gd扩展没装或者输出验证码之前有输出比如 PHP 文件开头多了个空行导致 header 污染。解决第一条确认session_save_path指向的目录有写入权限用php -i | grep session.save_path看当前值Windows 上常见问题是默认路径不存在但 PHP 没有自动创建。第二条检查 login.php 里session_start()是不是在所有echo、HTML 输出之前包括检查一下有没有 UTF-8 BOM 头BOM 也算输出会导致header(Location: ...)失败。第三条最隐蔽——很多老代码用session_start()后设置了比较短的过期时间例如 30 分钟而服务器时间和真实时间差超过 30 分钟你就会出现“登录后立刻掉线”的幻觉。用date命令看服务器时间对不上就校正系统时间或在 config.php 里手工配置session.gc_maxlifetime的值。验证码裂开的问题先确认 PHP 装了php-gd扩展Ubuntu 上是apt install php-gd装完重启服务装了就检查验证码生成函数里有没有先ob_clean()清掉之前的输出缓冲。5. 把老源码改造成合规可用的访客统计系统三种落地场景与实现细节到这里你已经把源码的骨头拆明白了坑也填了。现在的问题是这东西到底能拿来做什么顺着“访客管理系统”这个本质需求有三个落地场景都值得做而且都在这套代码的改造范围内。明确了场景再动手改代码就不会东改一锤子西改一锤子。5.1 场景一自有网站的独立访客统计工具这是最直接的改造。把 record.php 埋到自己网站的公共头部或底部数据库里的visit_page字段记录用户访问了哪个页面referrer字段记录来源你就有了一套不依赖第三方统计工具的访客台账。这套台账的价值在数据完全私有——第三方统计工具的数据在别人服务器上你想跑个“本周访问了哪个产品页面最多”的 SQL 是做不到的因为数据不在你手里。具体改造点有两个。第一在 visitor_log 表上加一个device_type字段取值 pc/mobile/tablet通过解析 User-Agent 判断实现“移动端和 PC 端访客分布”的统计页面第二加一个简单的报表页用GROUP BY DATE(visit_time)按天聚合出访问趋势图表用现成的 Canvas 库画个折线图就行不要引入前端框架。这套改造下来大约 200 行 PHP工作量集中在一个报表页和数据维度字段不需要动原有链路。5.2 场景二内部系统的操作留痕与审计访客管理系统的能力边界本来就不止“记录谁看了我的网站”它同样能记录“谁在我的后台系统里做了什么事”。改造思路简单直接把 record.php 改名为audit.php在内部系统的关键操作入口调用把visit_page字段的语义改成action_name记录的是“导出订单”“删除商品”“修改密码”这些操作把visitor_ip改成operator_id记录操作人编号。这个场景下黑名单功能基本没用但is_blocked字段还好端端躺在表结构里改个名变成is_anomaly语义变成“这条操作是否被标记为异常”审计管理员勾选后置灰。这套系统的核心价值在留痕的完整性不是实时拦截。所以采集端的记录逻辑不能失败——audit.php里的插入操作如果失败要写错误日志但不能中断业务请求因为用户操作已经完成了你只是事后取证不能让 PHP 报错影响操作结果。5.3 场景三访客数据分析的“最小可用”报表报表的价值永远在“我能从数据里看出什么”而不是“我画了多少张图”。基于已有的referrer和visit_page字段你可以只做三张报表访问来源 TOP10——统计用户从哪些网站跳转过来的页面热度 TOP10——统计哪些页面被访问最多时段分布——按整点统计一天 24 小时的访问分布。这三张表用三个 GROUP BY 语句就能查出来代码量不超过 80 行。?php // report.php - 三张核心报表的查询范例 require_once db.php; session_start(); if (empty($_SESSION[is_admin])) { exit(未登录); } // 报表一访客来源 TOP10 $referrerStmt $pdo-query( SELECT CASE WHEN referrer THEN 直接访问 WHEN referrer LIKE %google.com% OR referrer LIKE %baidu.com% THEN 搜索引擎 ELSE referrer END AS source, COUNT(*) AS cnt FROM visitor_log GROUP BY source ORDER BY cnt DESC LIMIT 10 ); $referrers $referrerStmt-fetchAll(); // 报表二时段分布按小时 $hourStmt $pdo-query( SELECT HOUR(visit_time) AS hour_num, COUNT(*) AS cnt FROM visitor_log GROUP BY hour_num ORDER BY hour_num ); $hours $hourStmt-fetchAll(); ?报表的查询逻辑值得较真。CASE WHEN对referrer分类的写法是把空值和搜索引擎都做了归一化——直接访问是一个独立类别搜索引擎只按来源域名判断不会出现一个百度来源链接因为带了一长串参数而统计不上。HOUR(visit_time)按小时聚合是最细的时间粒度做访客分析小于小时的维度没有业务意义大于小时比如按周聚合又会把一天内的峰谷抹掉。这两段 SQL 是访客统计系统的“刮胡刀”三张表一拼就是一个能看的基础报表。5.4 验证方案灌数据、跑查询、看结果是访客系统的标准三步验证改完代码之后验证系统是否真正可用不能只靠管理员自己访问一遍页面来测试。我建议你直接往数据库灌模拟数据做全链路验证因为访客数据是天然的稀疏数据——你一个人访问产生的记录看不出统计报表的效果。-- 造数脚本生成最近 30 天的模拟访客数据用于验证报表和列表 -- 注意只用于测试环境生产环境不要跑这条 SQL INSERT INTO visitor_log (visitor_ip, visit_time, visit_page, referrer, user_agent) SELECT CONCAT(192.168., FLOOR(RAND()*255), ., FLOOR(RAND()*255)) AS ip, DATE_SUB(NOW(), INTERVAL FLOOR(RAND()*30) DAY) - INTERVAL FLOOR(RAND()*86400) SECOND AS visit_time, ELT(1 FLOOR(RAND()*5), /, /product.php, /about.php, /contact.php, /blog.php) AS page, ELT(1 FLOOR(RAND()*4), , https://www.baidu.com, https://www.google.com, https://www.zhihu.com) AS referrer, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 AS ua FROM information_schema.tables LIMIT 2000;这条造数 SQL 里有三个写入细节你要看懂IP 用RAND()生成的是随机的私有网段地址只满足查询验证不需要真实访问时间用DATE_SUB加负INTERVAL生成的随机时间覆盖了最近 30 天任意时刻页面和来源都是用ELT按随机数映射到预设列表保证数据分布有规律报表里能看到排序效果。灌完数据打开后台列表检查分页是否正常、报表三个 Tab 是否有数据、按 IP 筛选中输入某个生成的 IP 是否能查到对应记录。验证环节能通过这套系统才算真正从“源码”变成了“工具”。5.5 最后的边界什么需求不该用这套系统访客记录系统不是万能的有三类需求它做不了提前认清边界能省下大把返工时间。第一它做不了实时在线人数统计——数据库写入和查询的间隔决定了这类系统是“事后聚合”的架构做完的瞬间用户可能已经离开第二它做不了跨站的行为追踪——访客在 A 网站的行为和 B 网站的行为是两套独立数据库没有公共标识第三它做不了设备指纹级别的唯一访客识别——没有登录态的时候同一个用户换两个浏览器就会被算成两条记录。在选型的源头上认清这三个边界你会发现前面做的所有改造都是在“能做的事情”里抠细节而不是跟系统做不到的物理极限较劲。作为一线开发者的习惯我改造完这套老源码之后有一个必做动作去数据库里删掉测试期间灌的假数据然后连续观察三天真实访客记录确认每个时段的采样结果都合理再去写那个统计报表——报表的价值建立在数据可信的基础上这个顺序一次都不能乱。希望这套 PHP修复版的拆解和改造思路能帮你把手里的源码变成真正能用的工具。本文还有配套的精品资源点击获取