
简介这套2026年最新防红cos系统是一套带后台管理的PHP防红源码面向网站运营者、独立站长和PHP二次开发人员解决域名被微信等平台标记后无法正常访问的问题。系统支持只绑定域名即可生成全新防红链接对已红域名也可实现全站绕红并增加域名是否被拦截的自动检测安装基于install.php兼容PHP 7.2以上版本。压缩包约631KB共84个文件核心由7个PHP文件后台、安装、配置、下载等构成搭配8个CSS、64个PNG用于界面展示另有JS、HTML、ini配置等辅助文件。目录包含前台页面、后台管理、数据库配置和安装脚本可直接部署也可作为二次开发模板。目前已有127人学习下载适合需要快速搭建全站防红服务或研究其拦截检测逻辑的开发者参考。1. 防红 cos 系统它在跳转链路里做了什么值得部署吗防红 cos 系统就是一套带后台管理的 PHP 链接中转站把一条容易被误拦的推广链接丢进去它生成一个短码形式的防红链接访客点开后先看到含目标地址的提示页再进入真正的落地页。相比直接把链接发出去这套方案最大的价值不是玄学而是让链接可停用、可换向、可统计——目标站封了或者域名变了后台改一条记录就完事不需要重新印物料。它适合两类人一类是天天发活动页、二维码引流的运营一类是接链接管理系统外包的 PHP 开发者。这套系统把建表、生成链接、跳转执行、后台管理串成了完整闭环部署门槛不高。2. 部署到 PHP 环境从目录结构到 Nginx 伪静态的一次到位先把环境说清楚。这套系统是纯 PHP 项目后端逻辑不依赖框架数据库用 MySQL前台展示直接输出 HTML。我实际部署时用的是 Nginx 1.20 PHP 8.0 MySQL 5.7 的组合PHP 7.4 也能跑只要别用 8.0 才有的新语法写业务代码。整套东西没有 composer 依赖解压就能用这也意味着目录结构必须自己控制好不能把入口文件散得到处都是。2.1 环境选型为什么是 PHP 而不是 Node 或 Go链接跳转这类场景技术上用 Node 或 Go 写反而更轻但放在真实业务里PHP 是最不折腾的选择。原因有三个一是部署环境好找虚拟主机、云服务器、甚至内网一台旧机器都能跑不要求你维护常驻进程二是这套系统天生就是「请求来了处理一下就走」的模型PHP-FPM 的短生命周期正合适不用担心内存泄漏三是后期接别的 PHP 项目时这套代码可以拆出公共模块复用。版本选择上PHP 8.0 的 PDO 和字符串处理都比 7.4 稳但 7.4 的用户基数仍然很大。我一般会按 7.4 兼容标准写代码避免用str_starts_with这类 8.0 才有的函数部署时再视服务器实际版本决定。数据库连接统一走 PDO不要用mysql_*老接口PHP 7.0 起那套 API 就移除了网上很多老教程还在抄照着写直接白屏。2.2 目录结构把入口、业务和静态文件分开拿到源码包后先看目录我建议的划分是public只放入口文件core放数据库连接和公共函数api放生成链接的接口admin放后台管理页面jump放跳转处理逻辑。这样 Nginx 的root直接指向public业务代码不暴露在静态文件目录下即使某个目录配错了也不至于把配置文件直接吐出去。fhcos/ ├── public/ │ └── index.php # 前台入口展示生成链接的表单 ├── admin/ │ ├── login.php # 后台登录 │ ├── index.php # 后台首页链接总览 │ ├── links.php # 链接管理列表 │ └── stats.php # 访问统计 ├── api/ │ └── create.php # 生成链接接口POST 请求 ├── jump/ │ └── index.php # 短码跳转处理形如 /j/1a2b3c4d ├── core/ │ ├── config.php # 数据库、站点配置 │ ├── db.php # PDO 连接 │ └── helper.php # 公共函数 └── static/ ├── css/ └── js/目录分层是这套系统的命根子。我在第一次部署时图省事把所有文件平铺在一个目录里结果 Nginx 把config.php当静态文件直接暴露了数据库密码被人扫出来后台被删了所有链接。后来强制自己遵守一个原则任何包含敏感信息的文件一律放在root目录之外或者放在入口文件不可达的目录下。Nginx 伪静态配置是这套系统能不能跑起来的关键。跳转链接形如/j/1a2b3c4d如果没配try_filesNginx 会去找真实路径j/1a2b3c4d直接返回 404。配置要点是把所有不存在的路径都交给index.php处理再由 PHP 内部解析短码。server { listen 80; server_name fh.example.com; root /www/fhcos/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.0-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }配置里两个地方最容易踩坑。root必须指向public目录不是项目根目录否则index.php也能被直接下载。fastcgi_pass的 sock 路径要按服务器实际 PHP 版本改我见过 CentOS 上写php7.4-fpm.sock实际装的却是 8.0PHP 请求全部 502。改完配置执行nginx -t校验语法再systemctl reload nginx不要直接restart避免正在跑的业务断一下。2.3 初始化数据库与配置三张表撑起整套系统这套系统核心就三张表链接表、访问日志表、规则表。链接表存短码和目标地址访问日志表记录每次跳转的来源规则表做前置过滤。不要设计得太复杂字段能少则少跳转场景高频读写的就那几条过多的索引反而拖慢写入。CREATE TABLE fh_link ( id int(11) unsigned NOT NULL AUTO_INCREMENT, code varchar(16) NOT NULL DEFAULT COMMENT 短码跳转链接的唯一标识, url text NOT NULL COMMENT 目标地址, jump_type tinyint(1) NOT NULL DEFAULT 1 COMMENT 1中间提示页 2302直跳, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1启用 0停用, expire_at datetime DEFAULT NULL COMMENT 过期时间NULL 表示长期有效, remark varchar(255) NOT NULL DEFAULT COMMENT 备注方便后台识别用途, visit_count int(11) unsigned NOT NULL DEFAULT 0 COMMENT 累计访问次数, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE fh_log ( id int(11) unsigned NOT NULL AUTO_INCREMENT, link_id int(11) unsigned NOT NULL COMMENT 对应 fh_link.id, ip varchar(45) NOT NULL DEFAULT COMMENT 访客 IP兼容 IPv6, ua varchar(255) NOT NULL DEFAULT COMMENT User-Agent, referer varchar(255) NOT NULL DEFAULT COMMENT 来源页, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_link_created (link_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE fh_rule ( id int(11) unsigned NOT NULL AUTO_INCREMENT, rule_type varchar(16) NOT NULL DEFAULT COMMENT domain域名黑名单 keyword关键词黑名单, rule_value varchar(255) NOT NULL DEFAULT COMMENT 匹配内容, action varchar(16) NOT NULL DEFAULT deny COMMENT deny拒绝生成, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时我特意指定了utf8mb4没用默认的utf8。原因很简单目标链接里可能出现 emoji 或生僻字utf8一个字符最多 3 字节遇到 4 字节的 emoji 直接报错整条链接存不进去。code字段设了唯一索引是用来防重复的生成短码时如果撞了唯一键程序要捕获异常换一个再试。数据库连好后core/config.php是整台机器上最敏感的文件。我习惯把数据库密码和站点地址集中在这里后面所有模块统一引用不要每个页面里各自写一遍连接参数不然改密码时要翻十几个文件。?php define(DB_HOST, 127.0.0.1); define(DB_PORT, 3306); define(DB_NAME, fhcos); define(DB_USER, fhcos_user); define(DB_PASS, 换成你自己的密码); define(BASE_URL, https://fh.example.com); function db(): PDO { static $pdo null; if ($pdo null) { $dsn sprintf(mysql:host%s;port%d;dbname%s;charsetutf8mb4, DB_HOST, DB_PORT, DB_NAME); $pdo new PDO($dsn, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); } return $pdo; }PDO::ATTR_EMULATE_PREPARES设为false让 MySQL 走真正的预处理而不是 PDO 在客户端模拟。这能避免一类很隐蔽的注入问题某些版本下模拟预处理会先把变量拼进 SQL 再整体转义转义不彻底就出漏洞。BASE_URL在生成短链时用来拼完整地址后面如果要换域名只改这一处。部署顺序我固定走四步先把源码解压到/www/fhcos然后改core/config.php里的数据库和站点配置再导入上面那份建表 SQL最后配 Nginx 伪静态并重载。上线后第一件事是登录后台改掉默认管理员密码——这套系统很多源码包默认密码是admin/admin888不改成随机强密码等于把后台大门敞着。提示config.php的权限我一般设成644属主是运行 PHP-FPM 的用户不要用777。权限过松是这类 PHP 项目被挂马的重灾区。3. 生成防红链接跳转逻辑、参数设置与规则过滤部署只是热身这套系统的核心在生成链接和跳转执行这两个环节。链接是怎么生成的、参数怎么控制、跳转时走了哪些判断直接决定它好不好用、稳不稳。这一章把生成接口和跳转执行拆开讲代码可以照着抄。3.1 三种跳转方式的取舍防红链接的跳转方式不是只有一种常见的有三类meta refresh 刷新跳转、JavaScript 跳转、302 服务端跳转。三者的差别在于目标地址出现在浏览器地址栏的时机以及访客是否能看到中间提示页。跳转方式实现成本访客体验适用场景meta refresh最低页面加一行 meta能看到提示页等待数秒默认推荐兜底能力强JavaScript 跳转低依赖 JS 执行能看到提示页跳转更柔和需要延迟跳转或弹窗提示时302 直跳最低服务端一行 header看不到中间页瞬间到达短链信任度高、不需要提示的场景这套系统的jump_type1走的是 meta refresh 加 JavaScript 双保险jump_type2走 302 直跳。双保险的意义在于兼容两类访客没开 JS 的浏览器靠 meta 兜底JS 被安全插件拦掉的情况靠 meta 也能走。两行代码都写上不是冗余是这套系统在真实环境里被验证过的容错做法。3.2 生成接口 create.php从参数校验到短码入库生成链接的入口是api/create.php只接受 POST 请求。前端表单提交后这里做四件事校验参数、验签、生成短码、入库。短码不是用自增 ID 直接暴露因为 ID 连续容易被遍历抓走全部链接我一般用random_bytes生成一段随机 hex 字符串取前 8 位。?php require __DIR__ . /../core/db.php; if ($_SERVER[REQUEST_METHOD] ! POST) { http_response_code(405); exit(json_encode([code 405, msg 仅支持 POST])); } $url trim($_POST[url] ?? ); $expire trim($_POST[expire] ?? ); $type (int)($_POST[type] ?? 1); $sign trim($_POST[sign] ?? ); if (!preg_match(#^https?://#i, $url)) { exit(json_encode([code 400, msg 仅允许 http/https 链接])); } if (strlen($url) 2048) { exit(json_encode([code 400, msg URL 过长])); } $secret 换成你自己的密钥; if ($sign ! md5($url . $expire . $secret)) { exit(json_encode([code 401, msg 签名校验失败])); } $code substr(bin2hex(random_bytes(4)), 0, 8); $stmt db()-prepare(INSERT INTO fh_link (code, url, jump_type, expire_at, created_at) VALUES (?, ?, ?, ?, NOW())); $stmt-execute([ $code, $url, $type, $expire ! ? date(Y-m-d H:i:s, strtotime($expire)) : null ]); echo json_encode([code 0, short_url BASE_URL . /j/ . $code]);这里有两个细节要说明。第一个是签名校验sign md5(url expire secret)前端在拿到这个签名时需要用同一套密钥计算否则接口会拒绝请求。加这一层是为了防止接口被外部脚本拿去乱发链接把你这台服务器变成免费的短链服务。如果后台生成链接时前端和接口在同一个站点下可以简化成校验登录态但接口暴露在公网时签名不能省。第二个是expire参数的处理。传空字符串表示永久有效插入数据库时给null传具体日期就用strtotime转成标准格式。这里有个小坑expire传的是前端格式化后的2026-12-31 23:59:59没问题但传时间戳会直接存成 1970 年过期判断立刻失效。前端传什么格式后端就要严格校验我一般会加一层strtotime($expire) false的判断格式不对直接报错。random_bytes(4)生成 8 位短码随机空间是 2 的 32 次方约 40 亿种组合短码撞车的概率很低。但建表时code字段设了唯一索引一旦真的撞上PDO 会抛异常。我一般把这个插入逻辑包在 try-catch 里捕获到唯一键冲突就重新生成一次短码再试最多重试三次。3.3 跳转执行 jump.php查库、打点、输出过渡页跳转执行是这套系统压力最大的地方每个访客点开链接都会走到这里。jump/index.php接收短码后按固定顺序处理先校验格式再查库接着判断状态和过期时间然后更新访问计数、写日志最后根据jump_type决定是 302 还是输出过渡页。?php require __DIR__ . /../core/db.php; $code $_GET[code] ?? ; if (!preg_match(/^[a-f0-9]{8}$/, $code)) { http_response_code(404); exit(链接不存在); } $pdo db(); $stmt $pdo-prepare(SELECT id, url, jump_type, status, expire_at FROM fh_link WHERE code ?); $stmt-execute([$code]); $link $stmt-fetch(); if (!$link) { http_response_code(404); exit(链接不存在); } if ($link[status] ! 1) { exit(该链接已停用); } if ($link[expire_at] strtotime($link[expire_at]) time()) { exit(该链接已过期); } $pdo-prepare(UPDATE fh_link SET visit_count visit_count 1 WHERE id ?) -execute([$link[id]]); $stmt $pdo-prepare(INSERT INTO fh_log (link_id, ip, ua, referer) VALUES (?, ?, ?, ?)); $stmt-execute([ $link[id], $_SERVER[REMOTE_ADDR] ?? , mb_substr($_SERVER[HTTP_USER_AGENT] ?? , 0, 255), mb_substr($_SERVER[HTTP_REFERER] ?? , 0, 255) ]); if ($link[jump_type] 2) { header(Location: . $link[url]); exit; } $safeUrl htmlspecialchars($link[url], ENT_QUOTES, UTF-8); $host parse_url($link[url], PHP_URL_HOST); ? !DOCTYPE html html langzh-CN head meta charsetutf-8 meta http-equivrefresh content5;url? $safeUrl ? title正在跳转/title /head body p即将前往 strong? htmlspecialchars($host) ?/strong请确认后等待跳转。/p pa href? $safeUrl ?立即访问/a/p scriptsetTimeout(function(){ location.href ? json_encode($link[url]) ?; }, 5000);/script /body /html这段代码里htmlspecialchars和json_encode是两道保险。目标链接是用户提交的内容可能带单引号、双引号、等字符直接在 HTML 里拼出来会破版甚至被注入脚本。htmlspecialchars(ENT_QUOTES)会连单引号一起转义json_encode输出的是合法的 JS 字符串字面量浏览器不会把它当代码执行。跳转前的状态判断顺序也重要。我先查库再判断status和expire_at这样停用和过期的链接不会产生日志记录。expire_at为null时strtotime返回falsefalse time()恒成立会导致永久链接全部被判过期所以判断里要先加$link[expire_at]非空的条件。日志写入放在了跳转输出之前。这里是有讲究的如果先输出 HTML 再做数据库操作PHP 在输出内容后仍然能继续执行但一旦前面的代码里出现exit或die日志就会漏记。把日志插入放在所有可能的exit之前能保证每个有效访问都留下记录。mb_substr限长是为了防止有人塞一个超长 UA 把日志表撑爆字段设计成varchar(255)入库前必须截断。注意jump_type2的 302 直跳header(Location:)前面不能有任何输出包括空格和 BOM。源码文件用 UTF-8 无 BOM 保存不然location头会失效浏览器拿到的是一堆乱码。4. 后台管理模块链接列表、统计报表与权限边界后台是这套系统面向运营的部分它决定了一个链接能不能停用、能不能换目标地址、访问数据能不能看明白。这套系统的后台做得比较克制没有花哨的 UI核心就是把链接管理、统计、规则三块做好。我在接这类项目时有个习惯后台功能宁可少不能多多一个功能就多一个被钻空子的入口。4.1 登录认证用 session 还是 JWT后台登录认证这套系统用的是传统 session 方案没有引入 JWT。原因很简单后台是同一个域名下的一套页面session 的 cookie 机制天然适配不需要额外处理 token 的存储和续期。JWT 适合前后端分离或接口被第三方调用的场景这里用不上。管理员密码不能明文存数据库password_hash是 PHP 自带的方案生成的结果自带盐比老式的md5(密码)靠谱得多。登录校验的代码逻辑是取表单提交的密码和数据库里的哈希对比验证通过后重建 session ID防止 session 固定攻击。?php session_start(); require __DIR__ . /../core/db.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $stmt db()-prepare(SELECT id, username, password_hash FROM admin_user WHERE username ? LIMIT 1); $stmt-execute([$username]); $admin $stmt-fetch(); if (!$admin || !password_verify($password, $admin[password_hash])) { exit(用户名或密码错误); } session_regenerate_id(true); $_SESSION[admin_id] $admin[id]; $_SESSION[login_at] time(); header(Location: index.php);后台管理员表我手动加一张admin_user源码包里未必自带但这是上线前必须补上的。password_verify是password_hash的配套函数它会从哈希串里自动识别算法和盐不需要你自己拼。session_regenerate_id(true)在登录成功后调用把旧的 session ID 作废避免别人在登录前就拿到你的 session 标识。权限边界是后台设计里最容易被忽略的部分。这套系统的后台只应该做四类操作管理链接、查看统计、配置规则、修改密码。其他的能力比如文件上传、模板编辑、命令执行一律不该出现在后台里。很多 PHP 后台被打不是因为登录密码弱而是后台附带了一个文件管理器或者模板编辑器攻击者拿下一个低权限账号后顺着文件上传直接 getshell。4.2 链接管理停用、换向与批量导出链接管理页面是后台使用频率最高的地方。运营每天在这里看哪些链接在用、哪些链接的访问量异常、哪些需要停用。这里我给链接列表设计了三个核心操作停用或启用、修改目标地址、删除。停用和换向是防红系统相对直接发链接最明显的优势——链接发出去被投诉了后台一键停用不用等平台处理。-- 按创建时间倒序带关键词搜索 SELECT id, code, url, status, visit_count, created_at, expire_at FROM fh_link WHERE (code LIKE :kw OR remark LIKE :kw OR url LIKE :kw) ORDER BY id DESC LIMIT 20 OFFSET 0; -- 按天统计某个链接的访问量 SELECT DATE(created_at) AS d, COUNT(*) AS total FROM fh_log WHERE link_id ? AND created_at DATE_SUB(NOW(), INTERVAL 14 DAY) GROUP BY DATE(created_at) ORDER BY d ASC;第一条 SQL 是链接列表页的分页查询LIMIT 20 OFFSET 0配合页码参数切换。搜索条件用LIKE时要注意%和_这两个通配符如果出现在关键词里会被当成通配符处理我一般会先addcslashes转义一下。第二条 SQL 是统计页的核心按天分组算出近 14 天的访问趋势画成简单的柱状图。换向操作在后端就一条UPDATE fh_link SET url ? WHERE id ?但前端要把原链接的目标地址展示出来让运营确认改的是哪条。这里有个细节修改目标地址时code不要变这样已经印出去的二维码和链接还能继续用。如果删掉旧链接再新增一条code 变了所有物料全部作废这就失去了后台管理的意义。4.3 规则过滤让不合适的目标链接在生成前就被拦下规则过滤是后台的第三块功能它的作用是让某些目标链接根本没机会进库。规则的载体是fh_rule表规则类型分两种域名黑名单和关键词黑名单。域名黑名单匹配目标 URL 的 host 部分关键词黑名单匹配整个 URL 字符串只要命中任一条规则生成接口直接拒绝。INSERT INTO fh_rule (rule_type, rule_value, action) VALUES (domain, example-bad.com, deny), (keyword, spam-keyword, deny);规则判断的逻辑放在create.php里和参数校验同一层。入库前把目标 URL 的 host 解析出来依次和规则表里的域名比对同时把完整 URL 拿去做关键词匹配。$host parse_url($url, PHP_URL_HOST); $rules db()-query(SELECT rule_type, rule_value FROM fh_rule WHERE action deny)-fetchAll(); foreach ($rules as $rule) { if ($rule[rule_type] domain stripos($host, $rule[rule_value]) ! false) { exit(json_encode([code 403, msg 该域名不允许生成链接])); } if ($rule[rule_type] keyword stripos($url, $rule[rule_value]) ! false) { exit(json_encode([code 403, msg 该链接包含不允许的关键词])); } }这里用的是stripos不区分大小写域名和关键词匹配都忽略大小写。域名匹配没做精确匹配example-bad.com能同时拦下www.example-bad.com和example-bad.com.cn这是故意设计的宽松匹配宁可多拦不可漏拦。规则表的数据量不会很大几百条内直接全量查出来循环判断不需要做索引优化。规则过滤有个容易忽略的细节parse_url解析不出 host 时返回null这种情况我直接拒绝生成。目标地址没有合法 host要么是格式错误要么是想塞协议外的东西这类链接没有生成的必要。如果哪天需要放行某个域名后台配一条actionallow的规则判断逻辑里先查放行规则命中就直接跳过黑名单校验这样比删除黑名单条目更安全操作有留痕。5. 避坑排查部署与使用中最常见的五个翻车现场这套系统我前后部署过好几套自己也踩过不少坑。这一章把最高频的五个翻车现场按阶段整理出来每条都按现象、原因、解决三个环节说透照顺序排查能省下大量试错时间。5.1 部署期伪静态失效与登录态丢失现象访问/j/1a2b3c4d直接 404后台登录成功后一刷新就退回登录页。原因伪静态失效和登录态丢失常常同时出现。Nginx 没有配try_files或者root指错了目录短码路径到不了index.php登录态丢失则是 session cookie 的路径或域名问题session.cookie_path如果不匹配当前目录浏览器不会回传 cookie。解决先确认 Nginx 配置里的try_files和root都正确nginx -t验证后重载再看 PHP session 配置session.cookie_secure在 HTTPS 环境下必须开启session.cookie_httponly设为 1。我习惯在config.php顶部统一设置 session 参数而不是依赖 php.iniini_set(session.cookie_httponly, 1); ini_set(session.use_strict_mode, 1); session_name(FHCOS_SESSID); session_start();use_strict_mode开启后PHP 只接受它自己生成的 session ID不接受客户端自定义的能挡住一部分会话固定攻击。改完这些后台刷新不掉登录短码路径也正常了。5.2 跳转期URL 编码、跳转循环与混合内容现象生成的防红链接点开后目标地址少了参数或者跳到一个错误页面有时浏览器地址栏报「您的内容包含不安全内容」。原因两个问题最常见。一是目标链接里带参数生成时参数被拆分入库的 URL 被截断二是站点启用了 HTTPS但目标地址是http://浏览器拦截了混合内容跳转被安全策略卡住。解决前端提交时对 URL 做一次encodeURIComponent后端取到后先urldecode再入库保证不会被拆。至于混合内容这类系统里的目标地址经常是老站点的 http 链接不能强制要求对方升级 HTTPS。我的做法是中间页不要依赖浏览器自动跳转把meta refresh和 JS 跳转都显式带上协议同时提示访客手动点击链接进入把选择权交给用户。5.3 统计期报表空白与访问数对不上现象后台统计页面某个链接显示 0 次访问但链接明明被打开过很多次或者统计数字虚高明显超过实际点击量。原因报表空白最常见是日志写入失败但没报错。跳转代码里如果把日志插入放在header(Location:)之后PHP 在发送 302 头后继续执行时可能被 FPM 中断统计虚高则是自己人或监控脚本在频繁访问没有过滤掉重复 UA 和爬虫。解决把日志写入挪到跳转输出之前就是第 3 章jump/index.php里的顺序。统计虚高的问题我在统计页加了一层简单去重同一个link_id加同一个 IP在 60 秒内只记一次。这个过滤逻辑放在写入时判断不放在统计时这样日志表本身就干净。$dup $pdo-prepare(SELECT COUNT(*) FROM fh_log WHERE link_id ? AND ip ? AND created_at DATE_SUB(NOW(), INTERVAL 60 SECOND)); $dup-execute([$link[id], $_SERVER[REMOTE_ADDR] ?? ]); if ((int)$dup-fetchColumn() 0) { // 执行日志插入 }5.4 环境差异PHP 版本与 MySQL 排序规则现象本地开发一切正常部署到线上后短码链接全部 404或者带特殊字符的链接存不进去线上 PHP 版本从 7.4 升到 8.0 后部分页面直接白屏。原因两个老问题。MySQL 5.7 之前的版本默认字符集是utf8不是utf8mb4建表语句没显式指定就把特殊字符截断了PHP 8.0 移除了create_function、each等老函数老代码里只要用了就致命错误。解决建表语句统一用utf8mb4并显式指定排序规则utf8mb4_unicode_ci这个在第 2 章的建表 SQL 里已经固定。PHP 代码我从一开始就要求自己不用任何 8.0 移除的函数提交前用php -l做语法检查再在 PHP 8.0 环境跑一遍跳转全流程。如果你手上的源码里还有mysql_*或create_function别犹豫先全部替换成 PDO 和匿名函数不然换了环境必炸。5.5 安全期后台弱口令与接口被刷现象后台频繁出现异地登录记录fh_log表里涌入大量同一个 IP 的访问生成接口被脚本刷了上千条垃圾短链。原因源码包的默认后台口令没改脚本可以直接扫登录页爆破生成接口没做频率限制谁拿到接口地址都能调用。解决上线第一步强制改后台密码这是死规矩。生成接口加两层防护第一层是第 3 章提过的签名校验密钥只在后台生成链接时使用第二层是 IP 频率限制同一个 IP 每分钟最多生成 30 条链接超过就拒绝。频率限制可以用简单的时间窗口计数读 Redis 没有的话就用数据库临时表记 IP 和请求时间每分钟清理一次。注意后台登录页面如果暴露在公网建议加一个访问频率限制同一 IP 连续输错 5 次密码就锁定 15 分钟。这一条能挡住绝大多数脚本爆破。6. 进阶UA 白名单跳转与过期链接回收部署稳定后有两件事值得做一是按访客环境区分跳转策略二是给过期链接建立回收机制。前者能明显提升不同场景下的打开率后者能让后台数据保持干净。区分访客环境本质上是看User-Agent。社交软件的内置浏览器、普通手机浏览器、PC 浏览器对中间页的接受度完全不同。普通浏览器直接 302 直跳最省事内置浏览器走中间提示页更稳妥——访客能先看到目标域名再决定是否进入这比突然跳转一个陌生页面更不容易被误操作。$ua $_SERVER[HTTP_USER_AGENT] ?? ; if (strpos($ua, MicroMessenger) ! false || strpos($ua, QQ/) ! false) { $link[jump_type] 1; // 内置浏览器走中间过渡页 } else { $link[jump_type] 2; // 普通浏览器302 直跳 }这段逻辑加在jump/index.php查库之后、跳转输出之前。注意 UA 判断不能完全覆盖所有情况UA 是可伪造的它只是策略分流的依据不是安全边界。我用它来平衡体验不指望它做权限控制。过期链接回收是后台长期健康的关键。链接表里大量过期数据不清理查询会越来越慢统计报表也会被历史数据拖累。我一般写一个每天凌晨执行的清理脚本把过期超过 7 天的链接停用超过 30 天的软删除归档而不是直接物理删除万一运营要追溯历史数据还有个退路。-- 每天凌晨执行先停用过期链接 UPDATE fh_link SET status 0 WHERE expire_at IS NOT NULL AND expire_at NOW(); -- 停用 30 天后物理清理 DELETE FROM fh_link WHERE status 0 AND expire_at IS NOT NULL AND expire_at DATE_SUB(NOW(), INTERVAL 30 DAY);这两条 SQL 放进 cron每天凌晨 3 点跑一次不占用业务高峰。第一次做这个功能时我没加 30 天缓冲直接按过期时间删除结果运营一个月后要查一条三个月前的数据库里已经没了只能翻备份。从那以后我每次部署这类系统都强制把「先停用、后延迟删除」写进清理流程给自己留后悔药。这套系统的进阶玩法不止这些给链接加分组标签、按来源渠道统计、对接企业微信告警都是在这个骨架上的自然延伸。希望帮到你。本文还有配套的精品资源点击获取