PHP与MySQL实现单表单多文件上传:封面与多图集管理教程
在内容发布类系统中,经常遇到一个表单同时需要上传单个封面图和多个详情图的需求,比如文章发布、商品添加等场景。本文将详细介绍如何通过PHP配合MySQL实现单表单同时上传封面图和多图集,并存储相关路径到数据库,同时做好基础的文件校验和异常处理。
一、数据库表结构设计
首先我们需要设计两张数据库表,一张用于存储内容主信息(包含封面图路径),另一张用于存储多图集信息,通过内容ID关联主表。这里以商品表为例,表结构如下:
-- 商品主表,存储商品基本信息及封面图路径 CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `cover_image` varchar(255) NOT NULL COMMENT '封面图路径', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品图集表,存储商品的多张详情图 CREATE TABLE `product_image` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '关联的商品ID', `image_path` varchar(255) NOT NULL COMMENT '图片路径', `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序值,数值越小越靠前', PRIMARY KEY (`id`), KEY `idx_product_id` (`product_id`), CONSTRAINT `fk_product_image_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
二、前端上传表单设计
前端表单需要设置enctype="multipart/form-data"属性以支持文件上传,同时封面图使用单文件上传字段,多图集使用多文件上传字段,注意多文件字段需要添加multiple属性,并且字段名以数组形式命名。示例代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>商品添加</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<p>
<label>商品名称:</label>
<input type="text" name="name" required>
</p>
<p>
<label>封面图:</label>
<input type="file" name="cover" accept="image/*" required>
</p>
<p>
<label>商品图集(可多选):</label>
<input type="file" name="images[]" accept="image/*" multiple>
</p>
<p>
<button type="submit">提交</button>
</p>
</form>
</body>
</html>上述代码中,accept="image/*"用于限制文件选择器只允许选择图片类型文件,name="images[]"的数组命名方式可以让PHP接收到多个上传的文件。
三、PHP后端处理逻辑
后端处理主要分为几个步骤:文件上传目录准备、文件类型与大小校验、文件重命名与移动、数据库存储。我们分步实现:
1. 基础配置与公共函数定义
首先定义文件上传的相关配置,以及通用的文件校验和移动函数,方便后续复用:
<?php
// 数据库配置
$db_host = '127.0.0.1';
$db_user = 'root';
$db_pass = '123456';
$db_name = 'test';
$db_port = 3306;
// 文件上传配置
$upload_dir = __DIR__ . '/uploads/'; // 上传根目录
$cover_dir = $upload_dir . 'cover/'; // 封面图目录
$image_dir = $upload_dir . 'images/'; // 图集目录
$allow_types = ['image/jpeg', 'image/png', 'image/gif']; // 允许的图片类型
$max_size = 2 * 1024 * 1024; // 允许的最大文件大小:2MB
// 创建上传目录,如果不存在则创建
if (!is_dir($cover_dir)) {
mkdir($cover_dir, 0755, true);
}
if (!is_dir($image_dir)) {
mkdir($image_dir, 0755, true);
}
/**
* 校验上传文件是否合法
* @param array $file 单个上传文件的信息数组
* @return string 校验通过返回空字符串,否则返回错误信息
*/
function checkUploadFile($file) {
global $allow_types, $max_size;
// 检查是否有上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return '文件上传失败,错误码:' . $file['error'];
}
// 检查文件类型
if (!in_array($file['type'], $allow_types)) {
return '不支持的文件类型,仅允许JPG、PNG、GIF格式';
}
// 检查文件大小
if ($file['size'] > $max_size) {
return '文件大小超过限制,最大允许2MB';
}
return '';
}
/**
* 生成唯一的文件名并移动文件到目标目录
* @param array $file 单个上传文件的信息数组
* @param string $target_dir 目标目录
* @return string 成功返回文件路径,失败返回空字符串
*/
function moveUploadFile($file, $target_dir) {
// 获取文件扩展名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
// 生成唯一文件名,避免重复
$new_name = md5(uniqid() . $file['name']) . '.' . $ext;
$target_path = $target_dir . $new_name;
// 移动临时文件到目标目录
if (move_uploaded_file($file['tmp_name'], $target_path)) {
// 返回相对路径,方便存储到数据库
return 'uploads/' . basename($target_dir) . '/' . $new_name;
}
return '';
}2. 主上传处理逻辑
接下来编写主处理逻辑,首先校验表单提交的内容,然后依次处理封面图和多图集的上传,最后将数据存入数据库:
// 仅处理POST请求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('仅支持POST请求提交');
}
// 校验商品名称
$product_name = trim($_POST['name'] ?? '');
if (empty($product_name)) {
die('商品名称不能为空');
}
// 处理封面图上传
$cover_file = $_FILES['cover'] ?? null;
if (empty($cover_file) || $cover_file['error'] === UPLOAD_ERR_NO_FILE) {
die('请上传封面图');
}
$cover_error = checkUploadFile($cover_file);
if (!empty($cover_error)) {
die('封面图校验失败:' . $cover_error);
}
$cover_path = moveUploadFile($cover_file, $cover_dir);
if (empty($cover_path)) {
die('封面图保存失败');
}
// 连接数据库
$conn = new mysqli($db_host, $db_user, $db_pass, $db_name, $db_port);
if ($conn->connect_error) {
die('数据库连接失败:' . $conn->connect_error);
}
$conn->set_charset('utf8mb4');
// 开启事务,保证数据一致性
$conn->begin_transaction();
try {
// 插入商品主表数据
$stmt = $conn->prepare("INSERT INTO product (name, cover_image) VALUES (?, ?)");
$stmt->bind_param('ss', $product_name, $cover_path);
if (!$stmt->execute()) {
throw new Exception('商品主信息插入失败:' . $stmt->error);
}
$product_id = $stmt->insert_id; // 获取新插入的商品ID
$stmt->close();
// 处理多图集上传
$image_files = $_FILES['images'] ?? null;
if (!empty($image_files) && $image_files['error'][0] !== UPLOAD_ERR_NO_FILE) {
$image_count = count($image_files['name']);
$sort = 0;
// 遍历所有上传的图集文件
for ($i = 0; $i < $image_count; $i++) {
$single_file = [
'name' => $image_files['name'][$i],
'type' => $image_files['type'][$i],
'tmp_name' => $image_files['tmp_name'][$i],
'error' => $image_files['error'][$i],
'size' => $image_files['size'][$i]
];
// 校验单个图集文件
$img_error = checkUploadFile($single_file);
if (!empty($img_error)) {
throw new Exception('第' . ($i+1) . '张图集文件校验失败:' . $img_error);
}
// 移动文件
$img_path = moveUploadFile($single_file, $image_dir);
if (empty($img_path)) {
throw new Exception('第' . ($i+1) . '张图集文件保存失败');
}
// 插入图集数据到数据库
$stmt = $conn->prepare("INSERT INTO product_image (product_id, image_path, sort) VALUES (?, ?, ?)");
$stmt->bind_param('isi', $product_id, $img_path, $sort);
if (!$stmt->execute()) {
throw new Exception('第' . ($i+1) . '张图集数据插入失败:' . $stmt->error);
}
$stmt->close();
$sort++;
}
}
// 所有操作成功,提交事务
$conn->commit();
echo '商品添加成功,商品ID:' . $product_id;
} catch (Exception $e) {
// 操作失败,回滚事务,同时删除已经上传的文件(可选)
$conn->rollback();
// 这里可以根据需要添加已上传文件的删除逻辑
die('操作失败:' . $e->getMessage());
} finally {
if (isset($conn)) {
$conn->close();
}
}四、注意事项与优化建议
- 文件上传目录需要设置正确的读写权限,避免因权限不足导致文件移动失败。
- 实际生产环境中,建议对上传的图片进行二次处理,比如压缩图片大小、生成缩略图,减少服务器存储压力并提升访问速度。
- 数据库操作部分可以根据项目使用的框架进行调整,比如使用PDO或者框架自带的数据库操作类,本文使用原生mysqli是为了更清晰地展示逻辑。
- 文件校验部分可以增加对文件真实类型的校验,避免用户修改文件后缀名绕过类型检查,比如使用
getimagesize()函数判断是否为真实图片。 - 多文件上传时如果部分文件失败,可以根据业务需求选择跳过失败文件继续处理其他文件,或者全部回滚,本文示例采用全部回滚的策略保证数据一致性。
五、数据查询示例
当我们需要查询某个商品的封面图和多图集时,可以通过以下SQL语句实现:
-- 查询商品ID为1的商品信息及所有图集
SELECT
p.id, p.name, p.cover_image,
pi.id AS img_id, pi.image_path, pi.sort
FROM product p
LEFT JOIN product_image pi ON p.id = pi.product_id
WHERE p.id = 1
ORDER BY pi.sort ASC;通过上述查询可以得到商品的基本信息和按排序排列的图集列表,方便前端展示。