简介:黑色简洁风格的PHP短网址短链接生成源码,面向需要自建短链服务的站长、开发者或小型团队,可快速部署一套带后台广告管理的轻量级短链系统。前端实现自定义短链、密码保护、访问统计、暗色主题与小书签快捷创建,后端支持网址删除、站点设置、广告位编辑和自定义CSS,前后端联动覆盖短链生成、跳转与数据查看的完整闭环,满足从创建链接到查看访问数据的日常使用场景。压缩包共103个文件,约681KB,包含34个PHP核心功能文件、11个JS交互脚本、10个CSS样式表,以及SCSS/LESS预处理源码、字体图标、SQL数据库脚本和图片素材,目录划分清晰,便于部署维护与二次开发。现有212人学习/下载,适合PHP初中级开发者学习短链路由跳转、API交互及后台管理设计,也可直接部署到自有服务器作为带广告短链工具,方便快速上线或作为学习参考模板。
1. 黑色简洁的PHP短网址源码,值不值得下?先看它解决了什么
上周帮一个做电商运营的朋友查问题:他发出去的推广短信,链接太长,被运营商自动截断,H5活动页后面十几个跟踪参数全丢,转化率直接腰斩。这种场景不需要讲大道理,把链接变短、把参数收敛,是最快的止血方案。这份黑色简洁的PHP短网址短链接生成源码,就是一个开箱即用的自建短链服务:PHP接收长链接,生成短码并入库,访问短链时按存储的映射做302跳转,顺带记录一次点击。适合三类人:运营或产品想自己掌控跳转统计,不想被第三方短链平台限流;PHP新人想找一个结构清楚、能二次开发的小项目练手;以及需要在内网环境里做短链服务的从业者。下面把短码生成、部署配置、伪静态和点击统计这几块的踩坑点全部摊开讲。
2. 核心链路:短码生成、映射存储与重定向跳转的PHP实现
短网址系统表面看就是个“查表跳转”,但短码怎么生成、存储结构怎么设计、跳转用哪个状态码,直接决定这套源码能在多大访问量下站稳。
2.1 短码生成方案:为什么不用MD5截断,而选ID转62进制
短码生成常见有三种做法,我先把对比列出来。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MD5截断取6位 | 实现快、无需查库 | 碰撞概率高,同链接多次生成短码固定,无法区分投放批次 | 只做一次性演示 |
| 数据库自增ID转62进制 | 无碰撞、短码长度可控 | 必须先插入记录再生成短码,需要两步操作 | 多数生产环境 |
| 随机字符串+查重 | 可离线生成、短码无序不可枚举 | 重复率随数据量上升,查重SQL会变慢 | 外网公开服务 |
MD5截断这个方案的坑在于短码空间实际很小,取6位十六进制字符只有约1600万组合,数据量到几千条后,生日悖论就会让碰撞开始出现;更麻烦的是MD5对相同输入产生相同输出,同一个长链接反复生成会得到同一个短码,运营场景经常要为不同渠道生成指向同一落地页但带不同参数的链接,这时相同短码根本无法区分来源。
所以我一般会选择“先写入长链接拿到自增ID,再把ID转成62进制短码”的方案。62进制用数字加大小写字母,6位短码的理论空间是62的6次方,约568亿,而且只要自增ID不同,短码必然不同,完全不需要碰撞检测。代价是入库流程从一条SQL变成三步,但对轻量短链服务来说,这个代价可以忽略。
2.2 62进制转换函数与完整短码入库流程
PHP有个内置函数base_convert,但它最多支持36进制,62进制必须手写。核心函数不复杂:
<?php function toShortCode(int $id): string { // 字母表顺序直接决定短码排序规律,数字+小写+大写是常见约定 $alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $base = strlen($alphabet); // 62 $code = ''; while ($id > 0) { // 取余数定位字符,注意拼接方向要放在前面 $code = $alphabet[$id % $base] . $code; // 用 intdiv 做整除,避免老写法 / 可能产生浮点误差 $id = intdiv($id, $base); } // ID 为 0 时返回 '0',保证每个 ID 都有短码 return $code === '' ? '0' : $code; }逻辑说明:循环里每次取当前ID对62取余,得到字符表下标,再把ID整除62继续下一轮,直到ID归零。字符串拼接方向决定了短码的排列顺序,如果把$code放到后面,生成的短码会完全颠倒。
拿到转换函数后,入库流程建议走三步两阶段:
<?php // 用事务包住整个生成过程,避免插入成功但短码回填失败 $pdo->beginTransaction(); $stmt = $pdo->prepare('INSERT INTO urls (long_url, create_time) VALUES (?, NOW())'); $stmt->execute([$longUrl]); $id = (int)$pdo->lastInsertId(); $code = toShortCode($id); $stmt = $pdo->prepare('UPDATE urls SET short_code = ? WHERE id = ?'); $stmt->execute([$code, $id]); $pdo->commit();参数说明:$longUrl建议在入口处先做trim和协议校验,只允许http/https开头;lastInsertId拿到的是本次连接的自增ID,不会受其他并发连接影响;UPDATE回填短码这步加事务是为了防止回填失败时留下短码为空的脏记录。如果不想用事务,也可以把生成的短码直接当成业务主键,但那样就得在插入前处理重复回滚,反而复杂。
2.3 跳转细节:301/302的取舍与循环跳转保护
短链跳转用301还是302,是个经典选择题。
| 状态码 | 浏览器行为 | 对统计的影响 | 适用场景 |
|---|---|---|---|
| 301 | 永久跳转,浏览器会缓存 | 第二次访问可能不再请求PHP,点击统计丢失 | 目标地址永久固定不变 |
| 302 | 临时跳转,每次都会重新请求 | 每次都命中PHP,统计完整 | 活动推广、目标可调、A/B切换 |
短链服务里常见做法是统一用302。理由很直接:运营活动链接随时可能换落地页,301一旦被浏览器缓存,改后台目标也救不回来,用户那边永远指向旧地址。代码就两行:
<?php header('Location: ' . $targetUrl, true, 302); exit;这里有个容易忽略的循环跳转保护。如果短码A的目标地址是本站另一个短码B,B又指回A,浏览器会一直重定向直到报错。我一般会在跳转前做一次域名比对:
<?php $targetHost = strtolower(parse_url($targetUrl, PHP_URL_HOST) ?? ''); $selfHost = strtolower($_SERVER['HTTP_HOST'] ?? ''); // 目标地址host等于当前站内host时,直接拒绝,防止互相引用形成死循环 if ($targetHost === $selfHost) { http_response_code(400); echo '目标地址不能是本站短链,已阻止循环跳转'; exit; }注意这里只比对了host,如果站点跑在非默认端口,需要把端口拼上再比较,比如yourdomain.com:8080。简版只比host对多数默认80/443场景够用,但你在二次开发时得知道这个边界。
2.4 点击统计:把每次访问自增数落到实处
统计是运营要数据时的命根子,不能做成黑匣子。我建议每次跳转前先插入一条访问明细,再执行跳转:
<?php $stmt = $pdo->prepare( 'INSERT INTO clicks (url_id, ip, referer, user_agent, click_time) VALUES (?, ?, ?, ?, NOW())' ); $stmt->execute([ $urlId, $_SERVER['REMOTE_ADDR'] ?? '', mb_substr($_SERVER['HTTP_REFERER'] ?? '', 0, 255), mb_substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255) ]);逻辑说明:先落明细再跳转,保证PHP执行完统计逻辑后才走header。这里直接取REMOTE_ADDR而不是X-Forwarded-For,因为后者在没经过可信代理时会伪造来源,本机或内网部署场景下不需要给自己添麻烦。referer和user_agent使用mb_substr截断到255字符,防止异常请求塞入超长垃圾数据拖慢写入。统计明细到手后,列表页按短码聚合就一句话:SELECT COUNT(*) FROM clicks WHERE url_id = ? AND click_time >= CURDATE(),当日、累计都能实时查。
3. 落地部署:LNMP环境下源码包配置顺序与伪静态规则
源代码拿到手,别急着改代码,先把环境和配置理顺。短链接服务一半的问题出在部署环节,尤其是伪静态。
3.1 典型文件结构与部署前环境检查
这类精简PHP短链项目,文件结构高度相似,我按角色的典型分布列一下:
| 文件 | 职责 | 是否必改 |
|---|---|---|
| index.php | 前端表单与生成处理 | 否 |
| r.php 或 go.php | 短码跳转入口 | 否 |
| config.php | 数据库连接与域名配置 | 是 |
| install.sql | 初始化表结构 | 导入一次 |
| README.txt | 部署说明 | 否 |
部署前先检查环境,三条命令就能定位九成问题:
php -v php -m | grep pdo_mysql nginx -t逻辑说明:第一行确认PHP版本,这类源码兼容PHP 7.4/8.0/8.2,只要别用PHP 5时代的mysql_*函数就没大问题;第二行确认PDO的MySQL驱动已安装,没有它config里的DSN直接报错;第三行验证Nginx配置语法,伪静态改完必须用nginx -t过一遍再reload。
3.2 数据库表设计:urls表与clicks表
源码包里通常带install.sql,我强烈建议你不要直接双击导入完事,先看清楚表结构能不能扛住你的场景。两张表是标准配置:
CREATE TABLE `urls` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `short_code` VARCHAR(16) NOT NULL DEFAULT '', `long_url` TEXT NOT NULL, `create_time` DATETIME NOT NULL, `expire_time` DATETIME DEFAULT NULL, `visit_count` INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_short_code` (`short_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `clicks` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `url_id` INT UNSIGNED NOT NULL, `ip` VARCHAR(45) NOT NULL DEFAULT '', `referer` VARCHAR(255) NOT NULL DEFAULT '', `user_agent` VARCHAR(255) NOT NULL DEFAULT '', `click_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_url_id_time` (`url_id`, `click_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:short_code的唯一索引是短码查询的性能保障,查询走的INDEX UNIQUE SCAN而不是全表扫;long_url用TEXT而不是VARCHAR(255),因为带中文和长参数的链接很容易超过255;字符集必须utf8mb4,否则中文参数和emoji入库会变乱码。visit_count是冗余计数,明细表里有原始数据,这里保留计数器是为了列表页快速展示,运营查总数时不必每次count全表。
3.3 配置文件里最容易忽略的五个参数
config.php是部署时唯一要手改的文件,我见过的翻车点高度集中在这几项:
| 配置项 | 示例 | 作用 | 常见坑 |
|---|---|---|---|
| DB_DSN | mysql:host=127.0.0.1;dbname=short_url;charset=utf8mb4 | PDO连接串 | 写成localhost可能走socket连不上 |
| DB_USER | shorturl | 数据库账号 | 权限不足时SQL执行静默失败 |
| BASE_URL | http://yourdomain.com | 拼接短链前缀 | 末尾多写一个/会拼出双斜杠 |
| URL_EXPIRE_DAYS | 30 | 过期天数,null为永久 | 活动链接设了过期导致中途失效 |
| SALT | 随机长字符串 | API token校验 | 不参与短码生成,别误用 |
其中BASE_URL是整个配置里最容易被忽视的。生成短链的完整地址是BASE_URL . '/' . $shortCode,如果BASE_URL写成http://yourdomain.com/,拼接后就是http://yourdomain.com//abc123,虽然多数服务器能容错,但Nginx的rewrite规则匹配[0-9A-Za-z]+时会被第二个斜杠干扰,表现就是短码访问404。
提示:BASE_URL统一写协议+域名,不带末尾斜杠,这是部署前先给自己定下的规则。
3.4 Nginx与Apache两套伪静态写法
伪静态的作用是把/abc123这类路径解析成index.php?code=abc123。Nginx的写法:
server { listen 80; server_name yourdomain.com; root /www/wwwroot/shorturl; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }逻辑说明:try_files先检查物理文件是否存在,$uri和$uri/都找不到时,把请求交给index.php处理,query_string原样保留。location ~ \.php$正则块保证PHP文件仍走fastcgi。如果你用宝塔面板,伪静态下拉框里选通用规则即可,最稳妥还是直接粘贴这段。
Apache环境下用.htaccess:
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^([0-9A-Za-z]+)$ index.php?code=$1 [L,QSA]逻辑说明:RewriteCond两行是“文件不存在且目录不存在”才走重写,避免把css、js、图片请求也吞进PHP入口;RewriteRule正则是[0-9A-Za-z]+,严格匹配短码字符集,QSA标志把原查询参数追加到重写后的URL后面。这个正则里的字符集是刻意限制的,如果放宽成.*,访问/favicon.ico这种路径也会被当成短码去查库,纯属浪费。
3.5 功能验收清单:生成、跳转、统计、过期
部署完别急着交付,按这个顺序过一遍:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 生成 | 输入长链接,点击生成 | 页面返回短码,前缀正确,无双斜杠 |
| 跳转 | 浏览器访问短码 | 302跳转到原地址,无404 |
| 统计 | 再次访问短码后查后台列表 | 当日点击加1,明细表出现记录 |
| 过期 | 把urls表expire_time改为过去时间 | 访问提示链接已失效,不跳转 |
| 防循环 | 生成指向本站其他短码的短码 | 返回400并提示已阻止 |
注意:验收跳转用
curl -I模拟,不要用浏览器反复刷新,否则统计表里混入你自己的测试流量,后面核对数据时说不清。curl只看响应头不会执行JS,也不会缓存302,每敲一次就是一次干净的请求。
4. 避坑排查:短链服务最容易翻车的五个真实场景
下面五条全部来自实际部署中见过的报错,按“现象→原因→解决”写,每一条都不是猜的。
4.1 短码跳转全部404,页面显示File not found
现象:手输/abc123访问短码,Nginx返回404页面,但首页和后台都正常。
原因:伪静态没生效,请求根本没进PHP入口。常见的有三种:Nginx的conf文件改了没reload;Apache的mod_rewrite没启用;或者.htaccess里的RewriteRule正则是直接复制来的,把路径匹配写成了^([a-zA-Z0-9]+)/$,末尾多了个斜杠,短码根本配不上。
解决:先php -l index.php确认入口语法没问题,再nginx -t验证配置,最后reload。Apache环境终端执行a2enmod rewrite后重启apache。如果这两个都对,再查短码本身是不是带了特殊字符——这一步和第4.3节直接相关。伪静态不生效是短链部署里最常见的“玄学”,九成是rewrite规则没加载,不要先去怀疑PHP版本。
4.2 中文长链接入库后跳转乱码或参数丢失
现象:生成时粘贴一条带中文query参数的链接,短码能生成,但跳转后落地页表现为乱码或参数被截断。
原因:中文和空格没有做URL编码就被直接写入数据库,跳转时header的Location头又带着未经处理的原始字符,浏览器解析时把中文部分按当前页面编码处理,跟在原始页面里看到的完全不是一回事。另外,如果表结构不是utf8mb4,入库那一刻中文就已经变成问号,后面怎么修补都白搭。
解决:入口处做过滤和校验,常见做法是先trim再校验协议,非http/https直接拒绝:
<?php $longUrl = trim(filter_input(INPUT_POST, 'url', FILTER_SANITIZE_URL) ?? ''); if (!preg_match('#^https?://#i', $longUrl)) { die('仅支持 http/https 链接'); }逻辑说明:FILTER_SANITIZE_URL会剥掉URL里明显非法的控制字符,正则保证协议头存在;对于中文路径和中文参数,建议在入库前用rawurlencode对query部分单独编码,落库的始终是ASCII串,跳转就不会乱码。数据库建表时字符集统一utf8mb4,这一步在3.2节的SQL里已经埋好,不要手动改成utf8。
4.3 短码里出现+号,导致匹配不到记录
现象:库里short_code字段存的短码包含+,前端点这链接必404,其他短码正常。
原因:实现转码的人误用了base_convert($id, 10, 36),或者用了带特殊字符的字母表。base_convert最大只支持36进制,硬传62会报错;还有人把字符表写成0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/,+和/在URL解析里都有特殊含义,查询字符串中的+会被解码成空格,自然匹配不到原样短码。
解决:短码字符集必须是严格的[0-9A-Za-z],转换函数用2.2节贴的那版。生成后加一道保险,正则校验不通过就重新生成:
<?php if (!preg_match('/^[0-9A-Za-z]+$/', $code)) { $code = toShortCode($id + 1000); // 跳过可能出问题的区间 }逻辑说明:加1000是临时补救做法,正常按2.2节的函数生成不会出现特殊字符,这行纯粹是给旧数据兜底。如果你从别处迁移来的短码表里已经混入+,建议直接重算所有短码并更新urls表,短码这东西不值得留历史包袱。
4.4 点击统计全是0,但后台明明有访问
现象:跳转正常,网页也打开了,后台列表点击量始终是0,clicks表空的。
原因:统计写入代码放在了header('Location: ...')之后。PHP执行到header之后如果不写exit,理论上还会继续执行,但要重定向时很多框架会强制exit收尾;更典型的是跳转入口文件直接写了header('Location: ...'); exit;,统计代码在exit后面根本没机会跑。另一类原因是跳转逻辑走了浏览器缓存或Node缓存,没经过PHP,但这个在小站点很少见,优先查代码顺序。
解决:把统计写入放在header之前,顺序固定为“查库拿目标URL → 写clicks明细 → header跳转 → exit”。检查入口文件里有没有在统计前提前输出HTML或echo,任何输出都会让header抛错,跳转就变成了白屏加警告。
提示:还有人把统计放在跳转后的页面回调里,比如目标站回传参数,这种方案在本地测试时不回传就永远是0,上线前一定要知道统计依赖外部请求,否则你无法区分是代码问题还是缺少回传。
4.5 测试阶段点击量虚高,运营数据没法看
现象:刚上线一天,短码点击量远超推广流量,后台列表一看几百次,实际投放才几十个UV。
原因:爬虫扫描器在扫短码路径,它们枚举常见短码组合,每访问一次就是一条统计;再加上自己验收时用浏览器反复刷新,浏览器对302不缓存但你手工刷新了十几次,每次都入库。
解决:做最朴素的去重,60秒内同一个IP对同一个短码只记一次:
<?php $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM clicks WHERE url_id = ? AND ip = ? AND click_time > (NOW() - INTERVAL 60 SECOND)' ); $stmt->execute([$urlId, $ip]); if ((int)$stmt->fetchColumn() === 0) { // 写入明细的代码放这里 }逻辑说明:这个SQL实现的是分钟级窗口去重,60秒内同一IP的第二次访问不再写入明细,能挡掉绝大多数扫描器和手滑刷新。代价是真实用户在60秒内重复点击也不会记两次,对运营场景来说这个误差可以接受。如果站点上了CDN,IP字段取HTTP_X_FORWARDED_FOR的第一个合法IP,前提是你控制CDN配置,信任来自回源层的这个头。
5. 进阶:给源码补上API接口、频率限制和过期清理
基础功能跑通只能算及格,往生产环境放还需要三样东西:开放给外部系统调用的API、防刷的频率限制、以及过期链接的日常清理。
5.1 生成入口API化
运营后台或投放系统要接入短链生成功能,不能让人手工去网页上点。常见做法是提供一个get接口,token做最简鉴权:
<?php if (($_GET['token'] ?? '') !== API_TOKEN) { http_response_code(401); exit(json_encode(['code' => 401, 'msg' => 'unauthorized'])); } $longUrl = filter_var($_GET['url'] ?? '', FILTER_VALIDATE_URL); if ($longUrl === false) { http_response_code(400); exit(json_encode(['code' => 400, 'msg' => 'invalid url'])); } $shortCode = createShortUrl($pdo, $longUrl); exit(json_encode(['code' => 0, 'short_url' => BASE_URL . '/' . $shortCode]));参数说明:API_TOKEN在config.php里定义,固定字符串,接入方放在请求参数里传;FILTER_VALIDATE_URL校验通过才继续,失败直接400。返回JSON里带short_url完整地址,调用方拿到就能直接用。
5.2 最简单的IP频率限制
被刷是短链服务的高频事故,接口不能裸奔。在跳转入口加一道基于clicks表的滑动窗口检查:
<?php $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM clicks WHERE ip = ? AND click_time > (NOW() - INTERVAL 60 SECOND)' ); $stmt->execute([$ip]); if ((int)$stmt->fetchColumn() > 30) { http_response_code(429); exit('访问过于频繁,请稍后再试'); }逻辑说明:60秒内同一IP超过30次访问直接返回429,这个阈值对正常用户够用,对扫描器基本一击必中。它复用了clicks表,不需要额外引入Redis,内网和小流量场景完全扛得住。
5.3 过期清理脚本与上线验证
最后补一个过期清理脚本,放进crontab每天跑一次:
<?php $pdo->prepare( 'DELETE FROM urls WHERE expire_time IS NOT NULL AND expire_time < NOW()' )->execute();联动删除clicks里的明细,避免孤儿数据越积越多:先删明细再删主表,或者用外键ON DELETE CASCADE,二选一。从那以后,我每次上线短链服务都强制走一遍这套验证:先curl -I过所有新生成的短码样本,确认响应是302、Location指向原目标、统计ID在clicks表里能查到,然后才把接口交付给运营。一个简单的短链服务不难,但每一环的“我以为没问题”都是后面要还的债。希望帮到你。
本文还有配套的精品资源,点击获取