简介:这是一套面向Web开发初学者与PHP爱好者的短网址生成网站源码,核心解决长链接缩短与链接防红两大需求,适用于社交媒体分享、营销推广及链接管理场景。压缩包共73个文件,约1.1MB,以21个php文件构成核心业务逻辑,20个css与12个js负责前后端交互与页面样式,另含字体、图片、svg等静态资源及2个sql数据库脚本,整体结构完整、便于部署。源码内置后台管理系统,涵盖用户权限管理、长短网址增删改查、访问量来源与时间统计、短码前缀及防红策略配置,并集成加密解密、代理转发、混淆算法与动态生成等防红实现,同时包含防SQL注入与XSS的安全防护。目前已有1250人学习下载,读者可借此理解哈希与自定义短码映射原理,掌握后台管理系统搭建与Web安全防护思路,并在此基础上二次开发API接口、优化防红策略或提升性能,是兼具学习与实践价值的PHP项目素材。
1. 短网址生成网站源码:防红这件事,到底在防什么
做短网址站的人,十个里有八个最后都会撞上同一个问题:链接发出去,没几分钟就被拦了。你以为是短链服务挂了,其实短链本身活得好好的,是目标域名被标记了。这就是圈子里说的「防红」——让分享出去的链接在社交平台、浏览器、IM 里尽量不被识别成风险链接。短网址生成网站源码加上防红源码,本质上是一套「链接中转 + 域名轮换 + 落地页伪装」的组合拳,不是单一功能。
这套东西适合谁?做私域引流、活动页分发、渠道推广的团队,以及想自己搭一套可控短链系统的后端开发者。它解决的不是「把长链接变短」这个表面需求,而是「短链在传播链路里活得更久」。下面我按一套能跑起来的方案,把选型、建表、生成逻辑、防红策略和踩坑点讲清楚,你照着能搭出一个最小可用版本。
2. 短网址生成网站源码的选型与最小可跑架构
2.1 为什么我建议 PHP + MySQL 起步,而不是一上来就上微服务
短网址系统的核心读写模型极其简单:写一次,读无数次,读的 QPS 远大于写。这个特征决定了它不需要复杂架构。常见做法是用 PHP 做接口层、MySQL 存映射关系、Redis 做热点缓存。选 PHP 不是因为它多先进,而是短网址生成网站源码这个方向里,PHP 生态的现成轮子最多,改起来快,部署成本低,一台 2 核 4G 的机器就能扛住中小规模的量。
如果你团队是 Java 或 Python 背景,换成 Spring Boot 或 FastAPI 也完全没问题,核心表结构和跳转逻辑是一样的。我一般会先确认三件事:短码生成算法用哪种、跳转走 301 还是 302、防红策略放在哪一层。这三个决定后面所有代码的形态。
短码生成有两种主流做法。一种是自增 ID 转 62 进制,优点是短、无冲突、可预测长度;缺点是短码连续,容易被遍历爬取。另一种是随机字符串加唯一索引,优点是离散、不易被枚举;缺点是要处理碰撞重试。做防红的场景我倾向后者,因为连续短码被批量扫描后,整个域名更容易被判定为垃圾链接源。
2.2 建表:三张表撑起短链与防红的基础
先把数据结构定下来,后面所有逻辑都围绕它转。最小集合是三张表:短链映射表、域名池表、访问日志表。
-- 短链映射表:核心表,存短码到长链接的映射 CREATE TABLE `short_url` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `short_code` VARCHAR(10) NOT NULL COMMENT '短码,唯一', `long_url` VARCHAR(2048) NOT NULL COMMENT '目标长链接', `domain_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用的域名池ID', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', `expire_at` DATETIME DEFAULT NULL COMMENT '过期时间,NULL为永久', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_short_code` (`short_code`), KEY `idx_domain` (`domain_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 域名池表:防红的核心,多个备用域名轮换 CREATE TABLE `domain_pool` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `domain` VARCHAR(128) NOT NULL COMMENT '短链域名', `weight` INT NOT NULL DEFAULT 100 COMMENT '轮换权重', `health` TINYINT NOT NULL DEFAULT 1 COMMENT '1健康 0异常', `blocked_count` INT NOT NULL DEFAULT 0 COMMENT '被拦截次数', PRIMARY KEY (`id`), UNIQUE KEY `uk_domain` (`domain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 访问日志表:用于统计和被拦后回溯 CREATE TABLE `visit_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `short_code` VARCHAR(10) NOT NULL, `ip` VARCHAR(45) DEFAULT NULL, `ua` VARCHAR(512) DEFAULT NULL, `referer` VARCHAR(512) DEFAULT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_code_time` (`short_code`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;short_code上加唯一索引是必须的,它同时承担去重和查询加速两个职责。domain_id字段把短链和域名池绑定,这是防红轮换的落点——同一条长链接可以生成多条短链,分别挂在不同域名上。visit_log的idx_code_time联合索引是为了按短码查最近访问,排查「某条链接突然被拦」时用得上。
提示:
long_url给到 2048 是因为很多带参数的推广链接很长,别图省事用 255,后期改字段很麻烦。
2.3 短码生成与跳转接口的最小实现
下面这段 PHP 是短码生成加跳转的核心逻辑,能直接跑。重点看随机短码的碰撞处理和域名选择策略。
<?php // 生成短码:随机 + 唯一索引兜底,避免连续可枚举 function genShortCode($pdo, $len = 6) { $chars = 'abcdefghijkmnpqrstuvwxyz23456789'; // 去掉易混淆字符 $max = strlen($chars) - 1; for ($try = 0; $try < 5; $try++) { $code = ''; for ($i = 0; $i < $len; $i++) { $code .= $chars[random_int(0, $max)]; } // 依赖唯一索引,插入冲突就重试 $stmt = $pdo->prepare( "INSERT INTO short_url (short_code, long_url, domain_id) VALUES (?, ?, ?)" ); try { $stmt->execute([$code, $longUrl, $domainId]); return $code; } catch (PDOException $e) { if ($e->getCode() != 23000) throw $e; // 非唯一键冲突直接抛 } } throw new Exception('短码生成失败,重试超限'); } // 从域名池按权重选一个健康域名 function pickDomain($pdo) { $rows = $pdo->query( "SELECT id, domain FROM domain_pool WHERE health = 1" )->fetchAll(PDO::FETCH_ASSOC); if (!$rows) throw new Exception('域名池为空'); $total = array_sum(array_column($rows, 'weight')); $rand = random_int(1, $total); foreach ($rows as $r) { $rand -= $r['weight']; if ($rand <= 0) return $r; } return $rows[0]; }genShortCode里用random_int而不是rand,是因为前者是密码学安全的随机源,短码不可预测性更强,降低被批量枚举的风险。碰撞处理靠数据库唯一索引抛出的 23000 错误码来兜底,重试 5 次基本够用——6 位短码在 32 字符集下有十亿级组合,实际碰撞概率极低。pickDomain用加权随机,权重高的域名分到更多流量,某个域名被拦时把它的health置 0 就能自动摘除。
跳转接口本身很短,但 301 和 302 的选择有讲究。301 是永久重定向,浏览器会缓存,后续请求不再经过你的服务器,省资源但失去了统计和动态换域名的能力。做防红必须用 302,因为你需要每次跳转都经过服务端,才能根据域名健康状态实时切换落地。
<?php // 跳转入口:必须用 302,保留每次请求的控制权 $code = $_GET['c'] ?? ''; $stmt = $pdo->prepare( "SELECT long_url, status, expire_at FROM short_url WHERE short_code = ?" ); $stmt->execute([$code]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row || $row['status'] != 1) { header('HTTP/1.1 404 Not Found'); exit('链接不存在或已失效'); } if ($row['expire_at'] && strtotime($row['expire_at']) < time()) { header('HTTP/1.1 410 Gone'); exit('链接已过期'); } // 记录访问,异步或落库都行 header('Location: ' . $row['long_url'], true, 302); exit;3. 防红源码的核心策略:域名轮换、落地页与拦截兜底
3.1 域名轮换不是多买几个域名就完事
很多人对防红的理解停留在「多准备几个域名换着用」,结果发现换得再勤还是被拦。问题出在轮换粒度。如果你的所有短链都从同一个入口域名跳出去,那这个入口一旦被标记,全站遭殃。正确做法是让域名池里的每个域名都能独立承担跳转,短链生成时就把域名分配好,而不是跳转时临时选。
具体到实现,生成短链时调用pickDomain拿到域名,拼成https://{domain}/{code}返回给用户。这样每条短链天生绑定一个域名,域名之间互不影响。当某个域名被拦,你只需要把domain_pool.health置 0,新生成的短链自动避开它,老短链如果还想救,可以批量更新short_url.domain_id指向新域名。
轮换的触发条件也要设计。我一般会接一个简单的健康检查:定时用几个常见 UA 去请求自己的短链,看返回状态码和响应内容。如果返回 404、403 或者被替换成拦截提示页,就把该域名health置 0 并blocked_count加一。这个检查不用太复杂,能覆盖主要拦截形态就行。
3.2 落地页伪装:中间页比直接跳转更抗拦
直接 302 跳到目标域名,拦截系统看到的是「短链域名 → 目标域名」的跳转链,目标域名一旦在黑名单里,短链跟着遭殃。加一层中间页能打断这个直接关联。中间页是你自己域名下的一个 HTML,里面用 JS 做二次跳转,或者展示一个「正在跳转」的过渡。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <title>页面跳转中</title> </head> <body> <script> // 中间页二次跳转,打断直接跳转链 var target = decodeURIComponent("<?php echo urlencode($longUrl); ?>"); // 延迟一点,模拟正常页面加载 setTimeout(function () { location.replace(target); }, 300); </script> <p>正在跳转,请稍候…</p> </body> </html>meta referrer设成no-referrer是为了不把来源信息带给目标站,减少关联痕迹。location.replace而不是location.href,是为了不往浏览器历史里塞记录,用户体验上也更干净。延迟 300ms 是给拦截系统的爬虫一个「这是正常页面」的信号,太快反而可疑。
注意:中间页方案会增加一次请求,对跳转速度有影响。如果你的场景对速度极敏感,可以只在被拦风险高的渠道启用中间页,普通渠道直接 302。
3.3 拦截兜底:被拦之后用户看到什么
再好的防红也有被拦的时候,关键是拦了之后怎么办。如果用户点开短链看到的是浏览器或平台的拦截页,这条流量就彻底丢了。兜底方案是准备一个备用落地:当检测到当前环境可能被拦(比如 UA 异常、Referer 来自已知拦截页),跳到一个提示页,引导用户手动复制链接到浏览器打开,或者换一个备用短链。
实现上可以在跳转接口里加一层判断:
<?php // 简易拦截兜底:可疑环境走提示页而非直接跳转 $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $suspect = false; // 常见拦截爬虫或异常 UA 特征,按需补充 if (stripos($ua, 'spider') !== false || strlen($ua) < 20) { $suspect = true; } if ($suspect) { header('Location: /notice.html'); // 提示页 exit; } header('Location: ' . $row['long_url'], true, 302);这段逻辑很粗,但思路是对的:把可疑流量引到自己的提示页,而不是让它撞上平台的拦截页。提示页上放备用链接和操作指引,至少能捞回一部分流量。具体哪些 UA 算可疑,得根据你实际被拦的日志去调,没有通用清单。
4. 短网址生成与防红落地时的避坑排查
4.1 短码重复导致插入失败,日志里全是 23000
现象:生成短链时偶发失败,错误日志里大量SQLSTATE[23000]唯一键冲突。原因通常是短码长度太短,或者字符集太小,碰撞概率上来了。6 位 32 字符集理论碰撞率很低,但如果你的量级到了千万级,或者短码长度设成了 4 位,碰撞就会明显。解决办法是把短码长度提到 6 到 7 位,字符集去掉 0/O、1/l 这类易混淆字符后仍有 32 个左右,够用。同时确认重试逻辑真的在重试,而不是捕获异常后直接返回失败。
4.2 301 缓存导致换域名后老链接还在跳旧地址
现象:某个域名被拦后,你更新了短链的domain_id,但用户访问老短链还是跳到被拦的地址。原因是之前用了 301,浏览器和中间层把重定向缓存了,根本不回你的服务器。这是血泪经验——做防红从第一天起就必须用 302,别为了省那点服务器资源用 301。已经用了 301 的,只能换短码重新生成,老链接基本救不回来。
4.3 域名池健康检查误判,把好域名全摘了
现象:健康检查跑完,domain_pool里所有域名health都变成 0,新短链生成直接报「域名池为空」。原因是检查逻辑太激进,比如用固定 UA 请求被目标站正常限流,或者检查脚本本身网络抖动,把正常域名判成异常。解决办法是加连续失败阈值,比如连续 3 次检查失败才置 0,并且检查请求要带正常浏览器 UA,别用脚本默认 UA。另外保留至少一个域名不参与自动摘除,作为兜底。
4.4 访问日志表膨胀拖慢数据库
现象:站点跑几个月后,跳转变慢,visit_log表几千万行,查询和插入都吃力。原因是每次跳转都同步写日志,且没有清理机制。解决办法有两个方向:一是把日志写入改成异步,用消息队列或先写 Redis 再批量落库;二是给日志表按时间分区,定期归档或删除超过 90 天的数据。如果只是做基础统计,甚至可以只记录按天聚合的计数,不存明细。
4.5 中间页被识别成跳转页,反而更容易被拦
现象:加了中间页之后,拦截率没降反升。原因是中间页的 JS 跳转特征太明显,比如location.href直接赋值、页面没有任何实质内容、加载即跳转。拦截系统对这类「空跳转页」有专门识别。解决办法是给中间页加点正常内容,延迟跳转时间拉长到 500ms 以上,用location.replace替代href,并且中间页的 URL 不要带明显的redirect、jump这类参数名。
5. 把防红做成可运营能力:监控指标与灰度换域名
前面讲的都是「能跑起来」,这一章讲「跑得久」。防红不是一次性配置,是持续对抗,你得有一套监控和切换机制,否则就是被动挨打。
先说要监控什么。我一般盯四个指标:单域名拦截率、短链平均存活时长、跳转成功率、域名池健康数。拦截率靠健康检查的blocked_count除以该域名的总请求数估算;存活时长从短链创建到首次被拦的时间;跳转成功率是 302 返回数除以总请求数;健康数直接查domain_pool。这四个指标里,存活时长最能反映防红策略是否有效——如果新域名平均活不过一天,说明你的落地页或跳转方式有硬伤,换再多域名也没用。
再说灰度换域名。不要等域名被拦了才切,那样中间会有一段流量损失。做法是给域名池设一个「预备域名」状态,新域名先小流量跑,观察它的拦截率。如果跑一周拦截率低于阈值,再逐步提高权重。切换时用权重渐变,而不是一刀切,避免流量突刺触发风控。
-- 按域名统计近7天拦截情况,辅助决策是否降权 SELECT d.domain, d.blocked_count, COUNT(v.id) AS total_visit, ROUND(d.blocked_count / GREATEST(COUNT(v.id), 1) * 100, 2) AS block_rate FROM domain_pool d LEFT JOIN short_url s ON s.domain_id = d.id LEFT JOIN visit_log v ON v.short_code = s.short_code AND v.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY d.id ORDER BY block_rate DESC;这条 SQL 把域名按拦截率排序,拦截率高的优先降权或摘除。GREATEST(COUNT(v.id), 1)是防止除零。实际用的时候可以再加时间窗口对比,看拦截率是突增还是缓慢上升,突增往往是域名被批量标记,缓慢上升可能是正常损耗。
最后一个技巧:短链的「生命周期」要主动管理。不是所有短链都需要永久有效。活动类短链设个 7 天或 30 天过期,过期后自动 410,既减少被扫的面,也降低数据库压力。永久短链只留给真正长期需要的场景。我吃过亏——早期所有短链都设永久,结果半年后库里躺着几百万条早已失效的链接,清理时才发现很多连目标域名都打不开了。现在我默认给短链加过期时间,需要永久的单独标记,这个习惯帮我省了很多事后清理的麻烦。希望帮到你。
本文还有配套的精品资源,点击获取