You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现表单提交前预上传大文件?求技术方案与代码指引

问题描述

我现有一套带文件名转换、文件大小校验、文件扩展名校验的文件上传表单,上传500MB这类大文件时,网络环境良好能正常完成,但移动WiFi这类慢网络下会因请求超时被取消。我想实现:

  • 文件附加时自动预上传
  • 显示上传进度条和取消按钮
  • 预上传完成前禁止提交按钮
  • 最终提交时仅把临时文件夹里的文件移到上传目录

以下是当前代码和配置:

当前上传表单代码

<button type="submit" class="btn btn-primary btn-xs" id="add_file" name="add_file" form="upload-form" style="display:none;">Subir archivos</button>
<form action="info_order.php?order=<?php echo $order; ?>" id="upload-form" method="post" enctype="multipart/form-data">
    <?php if((strpos($_SERVER['HTTP_USER_AGENT'],'Windows'))||(strpos($_SERVER['HTTP_USER_AGENT'],'Macintosh'))){ ?>
        <div id="dropzone" class="drop-zone" style="display:none;">
            <span class="drop-zone__prompt">Haga click o arrastre los archivos que desea subir.</span>
            <input type="file" name="file-upload[]" style="display:none;" id="file-upload" class="drop-zone__input" multiple>
        </div>
    <?php } elseif((strpos($_SERVER['HTTP_USER_AGENT'],'Android'))||(strpos($_SERVER['HTTP_USER_AGENT'],'iPhone'))){ ?>
            <input type="file" name="file-upload[]" id="file-upload2" style="display:none;" onchange="document.getElementById('add_file').click()" multiple>
            <button type="button" class="btn btn-primary" onclick="document.getElementById('file-upload2').click()">Subir archivos</button>
            </input>
        <?php } ?>
    </form>

POST提交代码

if(isset($_POST['add_file'])) { $error_message = ''; $ok = 0; $nook = 0; $new_value = '';
        $array_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr');
        if($_POST['admin-upload'] == true){ $access_level = 1; } else { $access_level = 2; }
        $total_count = count($_FILES['file-upload']['name']);
        for($i=0;$i<$total_count;$i++) {
            $filename = $_FILES['file-upload']['name'][$i];
            $tmpFilePath = $_FILES['file-upload']['tmp_name'][$i];
            $filename = str_replace("'","",strtolower($filename)); $filename = $order.'_'.$filename;
            if($filename != ''){
                $ext = strtolower(substr( $filename, strrpos( $filename, '.' ) + 1 ) );
                if(in_array($ext, $array_ext)) {
                    $upload_path = 'uploads/orders';
                    if(is_writable($tmpFilePath)){
                        $search_file = $upload_path.'/'.$filename;
                        if(file_exists($search_file)){
                            for($i=2;$i<999;$i++) {
                                $filename_noext = basename($filename,'.'.$ext);
                                $new_search_file = $upload_path.'/'.$filename_noext.'('.$i.').'.$ext;
                                if(!file_exists($new_search_file)){ $filename = $filename_noext.'('.$i.').'.$ext; break; }
                            }
                            $search_file = $new_search_file;
                        }
                        if(($info_order['Archivos adjuntos'] == "")&&($new_value == '')){ $new_value = $filename.'β'.$access_level; } elseif(($info_order['Archivos adjuntos'] != "")&&($new_value == '')){ $new_value = $info_order['Archivos adjuntos'].'α'.$filename.'β'.$access_level; } else { $new_value .= 'α'.$filename.'β'.$access_level; }
                        $update_date = strftime("%Y-%m-%d %H:%M:%S"); $tracking_date = strftime("%d/%m/%Y - %H:%M");
                        $newtrack = '#'."<b>".$tracking_date."</b><br/>El usuario ".$name_user." ha subido el fichero ".$filename."."; $tracking .= $newtrack;
                        $sql = "UPDATE `ordenes` SET `Archivos adjuntos` = '{$new_value}', `Fecha última modificación` = '{$update_date}', `Tracking` = '{$tracking}' WHERE `Nº orden` = '{$order}'";
                        if($db->query($sql)){
                            if(move_uploaded_file($tmpFilePath,$search_file)){
                                unset($tmpFilePath);
                                $error_message .= '<br/><li>El fichero '.$filename.' se ha subido correctamente.</li>'; $ok++;
                            } else {
                                $error_message .= '<br/><li>El fichero '.$filename.' no se pudo subir al directorio correctamente.</li>'; $nook++;
                            }
                        } else {
                            $error_message .= '<br/><li>El fichero '.$filename.' no se pudo agregar a la base de datos correctamente.</li>'; $nook++;
                        }
                    } else {
                        $error_message .= '<br/><li>El fichero no se puede subir porque no tiene permisos de escritura.</li>'; $nook++;
                    }
                } else {
                    $error_message .= '<br/><li>El fichero '.$filename.' no se puede subir porque la extensión no está permitida.</li>'; $nook++;
                }
            } else {
                $error_message .= '<br/><li>El fichero no se puede subir porque no tiene nombre.</li>'; $nook++;
            }
        }
        if($nook == 0){ $session->msg('s',$error_message); } elseif($ok == 0){ $session->msg('d',$error_message); } else { $session->msg('w',$error_message); }
        redirect('info_order.php?order='.$order);
    }

php.ini配置

php.ini配置


解决方案

技术栈选择

  • 前端:原生JavaScript处理文件上传、进度监听、取消请求;HTML/CSS实现进度条和按钮状态控制
  • 后端:PHP(沿用现有技术栈),新增预上传接口、文件移动接口,负责临时文件管理

前端改造要点

  1. 新增进度条与取消按钮
    在表单中添加进度展示区域:
<!-- 进度条容器 -->
<div id="upload-progress" style="display:none; margin:10px 0;">
    <div class="progress-bar" style="width:0%; height:20px; background:#007bff; transition:width 0.3s;"></div>
    <span id="progress-text">0%</span>
    <button type="button" id="cancel-upload" class="btn btn-danger btn-xs" style="margin-left:10px;">取消上传</button>
</div>
<!-- 提交按钮(初始禁用) -->
<button type="submit" class="btn btn-primary btn-xs" id="submit-btn" name="submit-btn" form="upload-form" disabled>确认提交</button>
  1. 文件选择后自动预上传
    监听文件选择事件,用XMLHttpRequest发送文件到预上传接口,实时监听上传进度:
let xhr;
// 绑定桌面端文件选择事件
document.getElementById('file-upload').addEventListener('change', handleFileSelect);
// 绑定移动端文件选择事件
document.getElementById('file-upload2').addEventListener('change', handleFileSelect);

function handleFileSelect(e) {
    const files = e.target.files;
    if (files.length === 0) return;

    // 显示进度条,禁用提交按钮
    document.getElementById('upload-progress').style.display = 'block';
    document.getElementById('submit-btn').disabled = true;

    // 构建FormData
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
        formData.append('pre-upload[]', files[i]);
        // 传递订单号用于临时文件分类
        formData.append('order', '<?php echo $order; ?>');
    }

    // 初始化XHR请求
    xhr = new XMLHttpRequest();
    xhr.open('POST', 'pre_upload.php', true);

    // 监听上传进度
    xhr.upload.addEventListener('progress', function(e) {
        if (e.lengthComputable) {
            const percent = Math.round((e.loaded / e.total) * 100);
            document.querySelector('.progress-bar').style.width = percent + '%';
            document.getElementById('progress-text').textContent = percent + '%';
        }
    });

    // 上传完成回调
    xhr.addEventListener('load', function() {
        if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            if (response.success) {
                // 保存临时文件标识到表单,提交时使用
                const hiddenInput = document.createElement('input');
                hiddenInput.type = 'hidden';
                hiddenInput.name = 'temp_files';
                hiddenInput.value = JSON.stringify(response.temp_files);
                document.getElementById('upload-form').appendChild(hiddenInput);
                // 启用提交按钮,隐藏进度条
                document.getElementById('submit-btn').disabled = false;
                document.getElementById('upload-progress').style.display = 'none';
            } else {
                alert('预上传失败:' + response.message);
            }
        } else {
            alert('预上传请求失败');
        }
    });

    // 绑定取消上传事件
    document.getElementById('cancel-upload').addEventListener('click', function() {
        if (xhr) xhr.abort();
        document.getElementById('upload-progress').style.display = 'none';
        document.getElementById('submit-btn').disabled = false;
        // 清空文件选择框
        e.target.value = '';
    });

    // 发送预上传请求
    xhr.send(formData);
}

后端改造要点

  1. 预上传接口(pre_upload.php)
    负责接收文件并保存到订单专属临时目录,返回临时文件信息:
<?php
session_start();
$order = $_POST['order'];
$temp_dir = 'uploads/temp/' . $order . '/';

// 创建订单专属临时目录
if (!is_dir($temp_dir)) {
    mkdir($temp_dir, 0755, true);
}

$allowed_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr');
$temp_files = [];

foreach ($_FILES['pre-upload']['name'] as $key => $filename) {
    $tmp_path = $_FILES['pre-upload']['tmp_name'][$key];
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    // 扩展名校验
    if (!in_array($ext, $allowed_ext)) {
        echo json_encode(['success' => false, 'message' => '文件扩展名不允许']);
        exit;
    }

    // 文件名处理(和原有逻辑一致)
    $clean_filename = str_replace("'","",strtolower($filename));
    $clean_filename = $order . '_' . $clean_filename;
    $temp_file_path = $temp_dir . $clean_filename;

    // 重名处理
    $counter = 2;
    while (file_exists($temp_file_path)) {
        $filename_noext = pathinfo($clean_filename, PATHINFO_FILENAME);
        $temp_file_path = $temp_dir . $filename_noext . '(' . $counter . ').' . $ext;
        $counter++;
    }

    // 保存到临时目录
    if (move_uploaded_file($tmp_path, $temp_file_path)) {
        $temp_files[] = [
            'temp_path' => $temp_file_path,
            'final_filename' => basename($temp_file_path)
        ];
    } else {
        echo json_encode(['success' => false, 'message' => '文件保存失败']);
        exit;
    }
}

echo json_encode(['success' => true, 'temp_files' => $temp_files]);
?>
  1. 修改原提交接口(info_order.php)
    不再处理文件上传,改为读取临时文件路径并移动到正式上传目录:
if(isset($_POST['submit-btn'])) { 
    $error_message = ''; 
    $ok = 0; 
    $nook = 0; 
    $new_value = '';
    $array_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr');
    $access_level = isset($_POST['admin-upload']) && $_POST['admin-upload'] == true ? 1 : 2;
    $temp_files = json_decode($_POST['temp_files'], true);
    $upload_path = 'uploads/orders';

    foreach ($temp_files as $file) {
        $temp_path = $file['temp_path'];
        $final_filename = $file['final_filename'];
        $ext = strtolower(pathinfo($final_filename, PATHINFO_EXTENSION));

        // 二次校验扩展名(安全加固)
        if(!in_array($ext, $array_ext)) {
            $error_message .= '<br/><li>文件'.$final_filename.'扩展名不允许</li>';
            $nook++;
            continue;
        }

        $final_path = $upload_path . '/' . $final_filename;
        // 移动文件到正式目录
        if(rename($temp_path, $final_path)) {
            // 数据库更新逻辑(沿用原有逻辑)
            if(($info_order['Archivos adjuntos'] == "")&&($new_value == '')){ 
                $new_value = $final_filename.'β'.$access_level; 
            } elseif(($info_order['Archivos adjuntos'] != "")&&($new_value == '')){ 
                $new_value = $info_order['Archivos adjuntos'].'α'.$final_filename.'β'.$access_level; 
            } else { 
                $new_value .= 'α'.$final_filename.'β'.$access_level; 
            }

            $update_date = strftime("%Y-%m-%d %H:%M:%S"); 
            $tracking_date = strftime("%d/%m/%Y - %H:%M");
            $newtrack = '#'."<b>".$tracking_date."</b><br/>El usuario ".$name_user." ha subido el fichero ".$final_filename."."; 
            $tracking .= $newtrack;

            $sql = "UPDATE `ordenes` SET `Archivos adjuntos` = '{$new_value}', `Fecha última modificación` = '{$update_date}', `Tracking` = '{$tracking}' WHERE `Nº orden` = '{$order}'";
            if($db->query($sql)){
                $error_message .= '<br/><li>文件'.$final_filename.'上传成功</li>';
                $ok++;
            } else {
                $error_message .= '<br/><li>文件'.$final_filename.'数据库更新失败</li>';
                $nook++;
            }

            // 删除空临时目录
            $temp_dir = dirname($temp_path);
            if(count(scandir($temp_dir)) == 2) {
                rmdir($temp_dir);
            }
        } else {
            $error_message .= '<br/><li>文件'.$final_filename.'移动失败</li>';
            $nook++;
        }
    }

    if($nook == 0){ $session->msg('s',$error_message); } 
    elseif($ok == 0){ $session->msg('d',$error_message); } 
    else { $session->msg('w',$error_message); }
    redirect('info_order.php?order='.$order);
}

关键注意事项

  • php.ini配置调整:将upload_max_filesize、post_max_size设为1G以上,max_execution_time和max_input_time调至300秒以上,避免预上传超时
  • 临时文件清理:添加定时任务(如Linux Cron)定期清理超过24小时的临时文件
  • 中断处理:后端可检测XHR中断事件,删除已上传的临时文件片段
  • 移动端适配:保留原有移动端文件选择逻辑,替换自动提交为预上传逻辑

内容的提问来源于stack exchange,提问作者leesin koreano

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 02:18:07