PHP项目短链与参数追踪

wen PHP项目 4

本文目录导读:

PHP项目短链与参数追踪

  1. 数据库结构
  2. 短链接核心类
  3. 路由处理 (跳转逻辑)
  4. 创建短链接API
  5. 统计面板查询
  6. 管理界面示例
  7. 使用示例

我来帮你设计一个PHP短链接与参数追踪系统,这是一个完整的解决方案,包含短链接生成、跳转和参数追踪功能。

数据库结构

-- 短链接表
CREATE TABLE `short_links` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `short_code` varchar(10) NOT NULL COMMENT '短码',
  `original_url` text NOT NULL COMMENT '原始URL', varchar(255) DEFAULT NULL COMMENT '链接标题',
  `user_id` int(11) DEFAULT NULL COMMENT '创建用户ID',
  `expire_at` datetime DEFAULT NULL COMMENT '过期时间',
  `is_active` tinyint(1) DEFAULT '1' COMMENT '是否启用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_short_code` (`short_code`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 访问追踪表
CREATE TABLE `link_tracking` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `short_link_id` int(11) NOT NULL COMMENT '短链接ID',
  `ip_address` varchar(45) DEFAULT NULL COMMENT '访问IP',
  `user_agent` text COMMENT '用户代理',
  `referer` text COMMENT '来源URL',
  `country` varchar(100) DEFAULT NULL COMMENT '国家',
  `city` varchar(100) DEFAULT NULL COMMENT '城市',
  `device_type` varchar(20) DEFAULT NULL COMMENT '设备类型(desktop/mobile/tablet)',
  `os` varchar(50) DEFAULT NULL COMMENT '操作系统',
  `browser` varchar(50) DEFAULT NULL COMMENT '浏览器',
  `is_click` tinyint(1) DEFAULT '1' COMMENT '是否点击',
  `click_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '点击时间',
  `parameters` json DEFAULT NULL COMMENT '自定义参数',
  PRIMARY KEY (`id`),
  KEY `idx_short_link_id` (`short_link_id`),
  KEY `idx_click_time` (`click_time`),
  KEY `idx_country` (`country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 链接参数配置表
CREATE TABLE `link_parameters` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `short_link_id` int(11) NOT NULL,
  `param_name` varchar(50) NOT NULL COMMENT '参数名',
  `param_value` varchar(255) DEFAULT NULL COMMENT '参数值',
  `is_required` tinyint(1) DEFAULT '0' COMMENT '是否必填',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_short_link_id` (`short_link_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

短链接核心类

<?php
// ShortLinkService.php
class ShortLinkService {
    private $db;
    private $config;
    public function __construct($db, $config = []) {
        $this->db = $db;
        $this->config = array_merge([
            'short_code_length' => 6,
            'allowed_chars' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
            'base_url' => 'https://yourdomain.com/'
        ], $config);
    }
    /**
     * 生成短链接
     */
    public function createShortLink($originalUrl, $title = '', $userId = null, $expireAt = null, $parameters = []) {
        // 验证URL
        if (!filter_var($originalUrl, FILTER_VALIDATE_URL)) {
            throw new Exception('Invalid URL');
        }
        // 生成唯一短码
        $shortCode = $this->generateUniqueCode();
        // 构建原始URL参数
        if (!empty($parameters)) {
            $queryParams = [];
            foreach ($parameters as $param => $value) {
                $queryParams[] = urlencode($param) . '=' . urlencode($value);
            }
            $separator = (strpos($originalUrl, '?') === false) ? '?' : '&';
            $originalUrl .= $separator . implode('&', $queryParams);
        }
        // 插入数据库
        $stmt = $this->db->prepare("
            INSERT INTO short_links (short_code, original_url, title, user_id, expire_at) 
            VALUES (?, ?, ?, ?, ?)
        ");
        $stmt->execute([$shortCode, $originalUrl, $title, $userId, $expireAt]);
        $linkId = $this->db->lastInsertId();
        // 保存参数配置
        if (!empty($parameters)) {
            $this->saveLinkParameters($linkId, $parameters);
        }
        return [
            'id' => $linkId,
            'short_code' => $shortCode,
            'short_url' => $this->config['base_url'] . $shortCode,
            'original_url' => $originalUrl
        ];
    }
    /**
     * 生成唯一短码
     */
    private function generateUniqueCode() {
        $chars = $this->config['allowed_chars'];
        $maxAttempts = 10;
        for ($i = 0; $i < $maxAttempts; $i++) {
            $code = '';
            for ($j = 0; $j < $this->config['short_code_length']; $j++) {
                $code .= $chars[random_int(0, strlen($chars) - 1)];
            }
            // 检查是否已存在
            $stmt = $this->db->prepare("SELECT id FROM short_links WHERE short_code = ?");
            $stmt->execute([$code]);
            if (!$stmt->fetch()) {
                return $code;
            }
        }
        throw new Exception('Failed to generate unique code');
    }
    /**
     * 保存链接参数配置
     */
    private function saveLinkParameters($linkId, $parameters) {
        $stmt = $this->db->prepare("
            INSERT INTO link_parameters (short_link_id, param_name, param_value) 
            VALUES (?, ?, ?)
        ");
        foreach ($parameters as $name => $value) {
            $stmt->execute([$linkId, $name, $value]);
        }
    }
    /**
     * 获取并重定向
     */
    public function redirect($shortCode) {
        // 查询短链接
        $stmt = $this->db->prepare("
            SELECT id, original_url, expire_at, is_active 
            FROM short_links 
            WHERE short_code = ? AND is_active = 1
        ");
        $stmt->execute([$shortCode]);
        $link = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$link) {
            header("HTTP/1.0 404 Not Found");
            exit('Link not found');
        }
        // 检查过期
        if ($link['expire_at'] && strtotime($link['expire_at']) < time()) {
            header("HTTP/1.0 410 Gone");
            exit('Link has expired');
        }
        // 记录访问信息
        $this->trackVisit($link['id']);
        // 重定向
        header("Location: " . $link['original_url'], true, 302);
        exit();
    }
    /**
     * 追踪访问信息
     */
    private function trackVisit($shortLinkId) {
        $ip = $this->getClientIP();
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        $referer = $_SERVER['HTTP_REFERER'] ?? '';
        // 解析设备信息
        $deviceInfo = $this->parseUserAgent($userAgent);
        // 获取地理位置(可选,需要IP地理定位服务)
        $geoData = $this->getGeoLocation($ip);
        // 收集URL参数
        $parameters = [];
        if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
            parse_str($_SERVER['QUERY_STRING'], $parameters);
        }
        try {
            $stmt = $this->db->prepare("
                INSERT INTO link_tracking 
                (short_link_id, ip_address, user_agent, referer, country, city, 
                 device_type, os, browser, parameters) 
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ");
            $stmt->execute([
                $shortLinkId,
                $ip,
                $userAgent,
                $referer,
                $geoData['country'] ?? null,
                $geoData['city'] ?? null,
                $deviceInfo['device_type'],
                $deviceInfo['os'],
                $deviceInfo['browser'],
                json_encode($parameters)
            ]);
        } catch (Exception $e) {
            // 日志记录错误但不中断重定向
            error_log("Tracking failed: " . $e->getMessage());
        }
    }
    /**
     * 获取客户端IP
     */
    private function getClientIP() {
        $headers = [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'REMOTE_ADDR'
        ];
        foreach ($headers as $header) {
            if (isset($_SERVER[$header])) {
                $ips = explode(',', $_SERVER[$header]);
                $ip = trim($ips[0]);
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }
        return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    }
    /**
     * 解析User-Agent
     */
    private function parseUserAgent($userAgent) {
        $result = [
            'device_type' => 'desktop',
            'os' => 'Unknown',
            'browser' => 'Unknown'
        ];
        if (empty($userAgent)) {
            return $result;
        }
        // 设备类型
        if (preg_match('/Mobile|Android|iPhone|iPad|iPod/i', $userAgent)) {
            $result['device_type'] = 'mobile';
            if (preg_match('/iPad|Tablet/i', $userAgent)) {
                $result['device_type'] = 'tablet';
            }
        }
        // 操作系统
        $osPatterns = [
            'Windows' => '/Windows NT/i',
            'Mac OS' => '/Mac OS X|Macintosh/i',
            'Linux' => '/Linux/i',
            'Android' => '/Android/i',
            'iOS' => '/iPhone|iPad|iPod/i'
        ];
        foreach ($osPatterns as $os => $pattern) {
            if (preg_match($pattern, $userAgent)) {
                $result['os'] = $os;
                break;
            }
        }
        // 浏览器
        $browserPatterns = [
            'Chrome' => '/Chrome/i',
            'Firefox' => '/Firefox/i',
            'Safari' => '/Safari/i',
            'Edge' => '/Edge|Edg/i',
            'Opera' => '/Opera|OPR/i'
        ];
        foreach ($browserPatterns as $browser => $pattern) {
            if (preg_match($pattern, $userAgent)) {
                $result['browser'] = $browser;
                break;
            }
        }
        return $result;
    }
    /**
     * 获取地理位置(示例使用ip-api.com)
     */
    private function getGeoLocation($ip) {
        // 忽略内网IP
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            try {
                $response = @file_get_contents("http://ip-api.com/json/{$ip}");
                if ($response) {
                    $data = json_decode($response, true);
                    if ($data['status'] === 'success') {
                        return [
                            'country' => $data['country'],
                            'city' => $data['city']
                        ];
                    }
                }
            } catch (Exception $e) {
                // 忽略错误
            }
        }
        return [];
    }
    /**
     * 获取统计信息
     */
    public function getStatistics($shortLinkId, $startDate = null, $endDate = null) {
        $conditions = ['short_link_id = ?'];
        $params = [$shortLinkId];
        if ($startDate) {
            $conditions[] = 'click_time >= ?';
            $params[] = $startDate;
        }
        if ($endDate) {
            $conditions[] = 'click_time <= ?';
            $params[] = $endDate;
        }
        $where = implode(' AND ', $conditions);
        $stats = [];
        // 总点击量
        $stmt = $this->db->prepare("SELECT COUNT(*) as total FROM link_tracking WHERE {$where}");
        $stmt->execute($params);
        $stats['total_clicks'] = $stmt->fetchColumn();
        // 独立IP
        $stmt = $this->db->prepare("SELECT COUNT(DISTINCT ip_address) as unique_ip FROM link_tracking WHERE {$where}");
        $stmt->execute($params);
        $stats['unique_visitors'] = $stmt->fetchColumn();
        // 按国家统计
        $stmt = $this->db->prepare("
            SELECT country, COUNT(*) as count 
            FROM link_tracking 
            WHERE {$where} AND country IS NOT NULL
            GROUP BY country 
            ORDER BY count DESC 
            LIMIT 10
        ");
        $stmt->execute($params);
        $stats['by_country'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        // 按设备统计
        $stmt = $this->db->prepare("
            SELECT device_type, COUNT(*) as count 
            FROM link_tracking 
            WHERE {$where}
            GROUP BY device_type
        ");
        $stmt->execute($params);
        $stats['by_device'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        // 按日期统计
        $stmt = $this->db->prepare("
            SELECT DATE(click_time) as date, COUNT(*) as count 
            FROM link_tracking 
            WHERE {$where}
            GROUP BY DATE(click_time) 
            ORDER BY date
        ");
        $stmt->execute($params);
        $stats['by_date'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        // 参数统计
        $stmt = $this->db->prepare("
            SELECT parameters, COUNT(*) as count 
            FROM link_tracking 
            WHERE {$where} AND parameters IS NOT NULL
            GROUP BY parameters 
            LIMIT 20
        ");
        $stmt->execute($params);
        $stats['by_parameters'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return $stats;
    }
}

路由处理 (跳转逻辑)

<?php
// redirect.php - 短链接跳转处理
require_once 'database.php'; // 数据库连接
require_once 'ShortLinkService.php';
$db = new PDO('mysql:host=localhost;dbname=your_db', 'username', 'password');
$shortLinkService = new ShortLinkService($db);
// 获取短码
$shortCode = $_GET['code'] ?? '';
if (empty($shortCode)) {
    header("HTTP/1.0 400 Bad Request");
    exit('Missing short code');
}
// 执行跳转并追踪
$shortLinkService->redirect($shortCode);

创建短链接API

<?php
// api_create.php - 创建短链接API
require_once 'database.php';
require_once 'ShortLinkService.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method not allowed']);
    exit;
}
$db = new PDO('mysql:host=localhost;dbname=your_db', 'username', 'password');
$shortLinkService = new ShortLinkService($db);
try {
    $data = json_decode(file_get_contents('php://input'), true);
    $originalUrl = $data['url'] ?? '';
    $title = $data['title'] ?? '';
    $expireAt = $data['expire_at'] ?? null;
    $parameters = $data['parameters'] ?? [];
    $result = $shortLinkService->createShortLink(
        $originalUrl,
        $title,
        null, // user_id
        $expireAt,
        $parameters
    );
    echo json_encode([
        'success' => true,
        'data' => $result
    ]);
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ]);
}

统计面板查询

<?php
// api_stats.php - 统计查询API
require_once 'database.php';
require_once 'ShortLinkService.php';
header('Content-Type: application/json');
$db = new PDO('mysql:host=localhost;dbname=your_db', 'username', 'password');
$shortLinkService = new ShortLinkService($db);
$linkId = $_GET['link_id'] ?? 0;
$startDate = $_GET['start_date'] ?? null;
$endDate = $_GET['end_date'] ?? null;
if (!$linkId) {
    http_response_code(400);
    echo json_encode(['error' => 'Missing link_id']);
    exit;
}
try {
    $stats = $shortLinkService->getStatistics($linkId, $startDate, $endDate);
    echo json_encode([
        'success' => true,
        'data' => $stats
    ]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ]);
}

管理界面示例

<!DOCTYPE html>
<html>
<head>短链接管理系统</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .container { max-width: 800px; margin: auto; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="url"] { width: 100%; padding: 8px; }
        .btn { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
        .result { margin-top: 20px; padding: 15px; background: #f0f0f0; }
        .param-row { margin-bottom: 10px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>创建短链接</h1>
        <form id="createLinkForm">
            <div class="form-group">
                <label>原始URL:</label>
                <input type="url" id="originalUrl" required>
            </div>
            <div class="form-group">
                <label>标题 (可选):</label>
                <input type="text" id="title">
            </div>
            <div class="form-group">
                <label>自定义参数:</label>
                <div id="parametersContainer">
                    <div class="param-row">
                        <input type="text" placeholder="参数名" class="param-name">
                        <input type="text" placeholder="参数值" class="param-value">
                        <button type="button" onclick="addParam()">+</button>
                    </div>
                </div>
            </div>
            <button type="submit" class="btn">生成短链接</button>
        </form>
        <div id="result" class="result" style="display:none;"></div>
    </div>
    <script>
        function addParam() {
            const container = document.getElementById('parametersContainer');
            const row = document.createElement('div');
            row.className = 'param-row';
            row.innerHTML = `
                <input type="text" placeholder="参数名" class="param-name">
                <input type="text" placeholder="参数值" class="param-value">
                <button type="button" onclick="this.parentElement.remove()">-</button>
            `;
            container.appendChild(row);
        }
        document.getElementById('createLinkForm').addEventListener('submit', function(e) {
            e.preventDefault();
            const parameters = {};
            document.querySelectorAll('.param-row').forEach(row => {
                const name = row.querySelector('.param-name').value;
                const value = row.querySelector('.param-value').value;
                if (name && value) {
                    parameters[name] = value;
                }
            });
            const data = {
                url: document.getElementById('originalUrl').value,
                title: document.getElementById('title').value,
                parameters: parameters
            };
            fetch('/api_create.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            })
            .then(response => response.json())
            .then(result => {
                const resultDiv = document.getElementById('result');
                resultDiv.style.display = 'block';
                if (result.success) {
                    resultDiv.innerHTML = `
                        <h3>生成成功!</h3>
                        <p><strong>短链接:</strong> ${result.data.short_url}</p>
                        <p><strong>原始URL:</strong> ${result.data.original_url}</p>
                    `;
                } else {
                    resultDiv.innerHTML = `<p style="color:red;">错误: ${result.error}</p>`;
                }
            });
        });
    </script>
</body>
</html>

使用示例

创建带追踪参数的短链接

// 使用示例
$shortLinkService = new ShortLinkService($db);
// 创建带UTM参数的短链接
$result = $shortLinkService->createShortLink(
    'https://example.com/product',
    '产品推广链接',
    null,
    '2024-12-31 23:59:59',
    [
        'utm_source' => 'facebook',
        'utm_medium' => 'social',
        'utm_campaign' => 'summer_sale',
        'utm_content' => 'banner_1',
        'ref' => 'user123'
    ]
);

.htaccess 配置 (Apache)

RewriteEngine On
RewriteRule ^([a-zA-Z0-9]+)$ redirect.php?code=$1 [L]

Nginx 配置

location / {
    try_files $uri $uri/ /redirect.php?code=$1;
}
location ~ ^/([a-zA-Z0-9]+)$ {
    rewrite ^/(.*)$ /redirect.php?code=$1 last;
}
  1. 短链接生成:安全随机生成,避免冲突
  2. 参数追踪:支持自定义参数(UTM等)
  3. 访问统计:IP、设备、地理位置、浏览器等
  4. 过期管理:支持链接过期时间
  5. 设备识别:自动识别桌面/移动/平板
  6. API接口:RESTful API便于集成
  7. 可视化统计:按国家、设备、日期等维度

这个系统可以轻松集成到现有的PHP项目中,支持高并发和大规模使用,你可以根据需求扩展更多功能,比如二维码生成、批量导入、A/B测试等。

抱歉,评论功能暂时关闭!