PHP项目上传文件如何存储分类

wen PHP项目 25

本文目录导读:

PHP项目上传文件如何存储分类

  1. 核心原则(安全底线)
  2. 存储目录分类方案
  3. 数据库表结构设计
  4. 完整上传示例(带分类 + 安全处理)
  5. 访问控制(防止目录遍历)
  6. 扩展建议

为PHP项目的文件上传设计存储分类方案,需要综合考虑安全性扩展性可维护性,以下是经过实践检验的几种最佳方案,从简单到复杂可供选择。

核心原则(安全底线)

在讨论结构之前,必须遵守以下安全规则:

  1. 禁止直接存储:永远不要将上传的文件放在Web根目录(如public/)下,除非你完全控制了文件执行权限。
  2. 重命名文件:使用随机字符串或uniqid() + md5() + 时间戳生成新文件名,不要保留用户原始文件名(防止路径注入和乱码)。
  3. 限制扩展名:白名单机制(如只允许 jpg, png, pdf),不要用黑名单(容易被绕过)。

存储目录分类方案

方案1:按文件类型 + 日期 + 随机子目录(推荐)

这是最常用的方案,平衡了查询性能和文件数量管理。

// 目录结构:upload/文件类型/年-月/随机二级目录/文件
// upload/images/2025-01/ab/cdef1234.jpg
// upload/documents/2025-01/gh/5678abcd.pdf
function getUploadPath($fileType, $originalName) {
    // 1. 确定基目录(建议放在public之外,或配置拒绝PHP执行)
    $baseDir = __DIR__ . '/../storage/uploads';  // 假设在App根目录
    // 2. 安全获取扩展名
    $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
    $allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx', 'xlsx'];
    if (!in_array($ext, $allowedExt)) {
        throw new Exception('文件类型不允许');
    }
    // 3. 分类规则(可扩展)
    $typeMap = [
        'image' => ['jpg', 'jpeg', 'png', 'gif'],
        'document' => ['pdf', 'docx', 'xlsx'],
        'video' => ['mp4', 'avi'],
        'other' => ['zip', 'rar']
    ];
    $folder = 'other';
    foreach ($typeMap as $key => $exts) {
        if (in_array($ext, $exts)) {
            $folder = $key;
            break;
        }
    }
    // 4. 创建目录结构:type/年-月/随机子目录/
    $dateDir = date('Y-m');
    $subDir = substr(md5(uniqid()), 0, 2); // 避免一个目录文件过多
    $saveDir = "{$baseDir}/{$folder}/{$dateDir}/{$subDir}";
    if (!is_dir($saveDir)) {
        mkdir($saveDir, 0755, true);
    }
    // 5. 生成唯一文件名(保留扩展名用于识别)
    $newName = uniqid() . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
    $fullPath = $saveDir . '/' . $newName;
    // 返回完整路径和相对路径(存数据库用)
    return [
        'full_path' => $fullPath,
        'relative_path' => "{$folder}/{$dateDir}/{$subDir}/{$newName}"
    ];
}

方案2:按业务模块 + 用户ID(适合社交/用户中心)

如果文件与用户强关联(头像、相册、作品),可以按用户归类。

// 目录:upload/模块名/用户ID_前两位/用户ID/时间戳.扩展名
// upload/avatar/10/10001/1704100000.jpg
// upload/photo/10/10001/1704100001.jpg
function getUserFilePath($userId, $module, $ext) {
    $baseDir = __DIR__ . '/../storage/' . $module;
    $userPrefix = str_pad(floor($userId / 100) * 100, 4, '0', STR_PAD_LEFT);
    $saveDir = "{$baseDir}/{$userPrefix}/{$userId}";
    if (!is_dir($saveDir)) {
        mkdir($saveDir, 0755, true);
    }
    $fileName = time() . '_' . md5($userId . uniqid()) . '.' . $ext;
    return $saveDir . '/' . $fileName;
}

方案3:使用对象存储(OSS/S3)的键值分类(上云)

如果使用阿里云OSS、AWS S3等云存储时,分类体现在Key(路径)上,不生成物理目录。

// 伪代码 - 云存储Key设计
function getCloudStorageKey($category, $fileExt, $userId = null) {
    // 分类: category/年-月-日/随机32位hash.ext
    // 或 category/userID/时间戳_随机串.ext
    $prefix = $category . '/' . date('Y/m/d');
    $hash = bin2hex(random_bytes(16));
    if ($userId) {
        // 按用户分组
        $key = "{$prefix}/user_{$userId}/{$hash}.{$fileExt}";
    } else {
        $key = "{$prefix}/{$hash}.{$fileExt}";
    }
    return $key; // 存数据库供访问
}

数据库表结构设计

任何分类最终都需要在数据库中记录,以便查询和关联业务。

CREATE TABLE `file_uploads` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `original_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
    `stored_name` VARCHAR(255) NOT NULL COMMENT '存储文件名',
    `relative_path` VARCHAR(500) NOT NULL COMMENT '相对路径(不含域名)',
    `file_type` VARCHAR(50) NOT NULL COMMENT '分类:image/document/video',
    `mime_type` VARCHAR(100) DEFAULT NULL,
    `file_size` INT UNSIGNED NOT NULL COMMENT '字节数',
    `md5_hash` VARCHAR(32) DEFAULT NULL COMMENT '文件MD5(用于去重)',
    `user_id` INT UNSIGNED DEFAULT NULL,
    `business_type` VARCHAR(50) DEFAULT NULL COMMENT '业务类型:avatar/post/invoice',
    `is_deleted` TINYINT(1) DEFAULT 0,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX `idx_file_type` (`file_type`),
    INDEX `idx_user_id` (`user_id`),
    INDEX `idx_business_type` (`business_type`),
    INDEX `idx_md5` (`md5_hash`)  -- 用于查找重复文件
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

完整上传示例(带分类 + 安全处理)

function uploadFile($fileField, $allowedTypes = ['image'], $maxSize = 5 * 1024 * 1024) {
    $file = $_FILES[$fileField] ?? null;
    if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
        return ['error' => '上传失败'];
    }
    // 检查大小
    if ($file['size'] > $maxSize) {
        return ['error' => '文件超过大小限制'];
    }
    // 获取扩展名并验证
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    $typeConfig = [
        'image' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
        'document' => ['pdf', 'docx', 'xlsx', 'csv'],
        'video' => ['mp4', 'avi', 'mov'],
    ];
    $actualType = null;
    foreach ($allowedTypes as $type) {
        if (in_array($ext, $typeConfig[$type] ?? [])) {
            $actualType = $type;
            break;
        }
    }
    if (!$actualType) {
        return ['error' => '文件类型不在允许范围内'];
    }
    // 生成存储路径(使用方案1)
    $pathInfo = getUploadPath($actualType, $file['name']);
    $dir = dirname($pathInfo['full_path']);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    // 移动文件
    if (!move_uploaded_file($file['tmp_name'], $pathInfo['full_path'])) {
        return ['error' => '文件保存失败'];
    }
    // 可选:计算MD5用于去重
    $md5 = md5_file($pathInfo['full_path']);
    // 存入数据库
    $db = getDB(); // 你的数据库连接
    $stmt = $db->prepare("INSERT INTO file_uploads 
        (original_name, stored_name, relative_path, file_type, mime_type, file_size, md5_hash)
        VALUES (?, ?, ?, ?, ?, ?, ?)");
    $stmt->execute([
        $file['name'],
        basename($pathInfo['full_path']),
        $pathInfo['relative_path'],
        $actualType,
        mime_content_type($pathInfo['full_path']),
        $file['size'],
        $md5
    ]);
    return [
        'success' => true,
        'file_id' => $db->lastInsertId(),
        'path' => $pathInfo['relative_path']
    ];
}

访问控制(防止目录遍历)

即使文件不在Web根目录,如果需要通过URL访问,必须使用PHP脚本作为代理。

// download.php?id=1001
function serveFile($fileId) {
    $db = getDB();
    $stmt = $db->prepare("SELECT * FROM file_uploads WHERE id = ? AND is_deleted = 0");
    $stmt->execute([$fileId]);
    $file = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$file) {
        http_response_code(404);
        exit;
    }
    // 权限检查(只有文件上传者或管理员可访问)
    // if ($file['user_id'] != $_SESSION['user_id'] && !isAdmin()) { ... }
    $fullPath = __DIR__ . '/../storage/uploads/' . $file['relative_path'];
    if (!file_exists($fullPath)) {
        http_response_code(404);
        exit;
    }
    // 发送文件
    header('Content-Type: ' . $file['mime_type']);
    header('Content-Length: ' . $file['file_size']);
    header('Content-Disposition: inline; filename="' . $file['original_name'] . '"');
    readfile($fullPath);
    exit;
}

扩展建议

  1. 日志系统:每次上传记录到日志,便于审计和追踪恶意文件。
  2. 病毒扫描:上传后立即调用 ClamAV 或第三方API扫描。
  3. 图片压缩:如果是图片,自动生成缩略图(thumbnails/)并单独分类存储。
  4. 定期清理:设置 cron job 清理 is_deleted=1 超过30天的文件。
  5. CDN分发:如果是云存储,将 relative_path 映射到CDN URL。

选择哪种分类方案取决于你的业务场景:

  • 一般Web应用:方案1(按类型+日期)通用且高效。
  • 用户中心/社交:方案2(按用户+模块)便于管理用户文件。
  • 高并发/上云:方案3(对象存储键值)配合数据库索引。

做好这些分类和规范,你的文件系统可以轻松支撑百万级文件存储。

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