Java Web 传统项目异步分块上传系统实现方案

java大文件分片异步上传完整实施方案

一、方案概述

1.1 方案背景

在Web应用中,大文件(如超过100MB的文档、压缩包、图片集)直接上传时,易出现请求超时、内存溢出、网络中断导致上传失败等问题;同时,传统同步上传会阻塞页面,影响用户体验,且IE等老旧浏览器对大文件上传支持较差。为解决上述问题,设计并实现一套兼容多浏览器、支持分片异步上传、进度实时展示、具备安全校验的大文件上传方案。

1.2 核心目标

  • 兼容适配:支持现代浏览器(Chrome、Firefox、Edge等)与IE浏览器(含低版本),解决浏览器兼容性问题。

  • 分片上传:将大文件切割为固定大小分片,并行上传,避免单次上传超时和内存溢出。

  • 异步交互:上传过程不阻塞页面,实时展示上传进度,支持上传取消、文件删除操作。

  • 安全可靠:实现登录校验、请求来源校验、文件类型/大小/数量限制,防止恶意上传和路径遍历攻击。

  • 可扩展性:支持业务灵活配置,适配不同业务场景下的文件上传需求,便于后续功能扩展(如断点续传、MD5校验)。

1.3 适用场景

本方案适用于各类Web应用的大文件上传场景,尤其适合:

  • 业务系统中需要上传大体积文档(如合同、报表、设计图)的场景。

  • 需要兼容IE浏览器(政府、企业内网等老旧系统环境)的上传需求。

  • 对上传体验要求较高,需要实时进度展示、断点续传的场景。

二、需求分析

2.1 功能需求

需求类别 具体需求 实现说明
文件选择 支持单文件选择,显示文件名称、大小 前端页面提供上传区域,兼容IE低版本文件选择逻辑
分片处理 大文件自动分片,支持并行上传,可配置分片大小和并行数量 前端切割文件,服务端接收并保存分片,支持最多3个分片并行上传
进度展示 实时显示上传进度,支持百分比展示 现代浏览器用原生进度监听,IE浏览器用轮询查询进度
上传控制 支持上传确认、取消上传、已上传文件删除 前端提供操作按钮,服务端对应处理取消、删除逻辑,清理临时文件
文件合并 所有分片上传完成后,自动合并为完整文件 服务端校验分片完整性,按顺序合并,校验文件大小一致性
业务对接 上传完成后回调业务逻辑,保存文件信息 提供前端回调函数,服务端将文件信息存入Session,支持业务扩展

2.2 非功能需求

  • 性能:分片上传响应时间≤500ms,合并文件时间≤1000ms(单文件≤200MB),支持同时3个分片并行上传。

  • 兼容性:支持Chrome 80+、Firefox 75+、Edge 80+、IE 10+,IE低版本(8-9)可基础使用(需开启ActiveX控件)。

  • 安全性:登录校验、请求来源校验、文件类型白名单、文件大小限制、防路径遍历攻击,避免恶意文件上传。

  • 可靠性:上传中断后可取消,临时文件自动清理,合并文件后校验完整性,防止文件损坏。

  • 可维护性:代码模块化设计,配置可灵活调整,便于后续扩展和问题排查。

2.3 约束条件

  • 文件类型:仅支持图片(jpeg、png、gif、bmp)、文档(doc、docx、xls、xlsx、pdf)、压缩包(zip)。

  • 文件大小:默认单文件≤10MB,可按业务编码配置最大≤200MB,单用户单次上传文件数量≤5个。

  • 分片配置:默认分片大小20MB,最大分片数量≤30个,最大并行上传数≤3个。

  • 环境要求:服务端需支持Java Servlet 3.0+,前端需引入jQuery(用于DOM操作)。

三、架构设计

3.1 整体架构

本方案采用"前端分片上传+服务端接收处理+Session存储状态"的架构模式,分为前端层、服务层、存储层三个层级,各层级职责清晰,协同工作完成大文件上传全流程。

3.1.1 架构分层

  • 前端层:负责文件选择、分片切割、并行上传、进度展示、操作控制(确认、取消、删除),兼容多浏览器。

  • 服务层:由两个Servlet组成,负责接收分片、合并文件、查询进度、处理取消/删除请求,实现安全校验和业务逻辑对接。

  • 存储层:分为临时存储(分片文件)和最终存储(完整文件),采用本地文件系统存储,支持目录自动创建和清理。

3.1.2 核心交互流程

  1. 前端初始化:传入业务编码,绑定上传区域点击事件、文件选择事件、操作按钮事件。

  2. 文件选择:用户选择文件,前端校验文件名合法性,显示文件信息,生成唯一uploadId(用于跟踪上传状态)。

  3. 分片上传:前端切割文件为固定大小分片,并行上传分片,实时更新进度(现代浏览器监听上传进度,IE轮询查询进度)。

  4. 服务端接收:AsyncUploadServlet接收分片,校验参数和文件合法性,保存分片到临时目录,更新上传进度和已上传分片记录。

  5. 文件合并:所有分片上传完成后,前端发送合并请求,服务端校验分片完整性,合并分片为完整文件,校验文件大小,保存到最终目录。

  6. 业务回调:合并完成后,前端触发回调函数,服务端将文件信息存入Session,供业务系统调用。

  7. 操作控制:用户可取消上传(服务端清理临时分片和进度记录)、删除已上传文件(服务端删除文件和Session记录)。

3.2 核心组件设计

3.2.1 前端组件(asyncUpload.jsp)

前端核心组件负责用户交互和分片处理,包含页面样式、DOM元素、JavaScript逻辑三部分,关键功能如下:

  • 页面样式:提供上传区域、文件信息展示、进度条、操作按钮,适配不同浏览器样式。

  • DOM元素:上传区域、文件输入框、文件信息显示区、进度条、确认/取消按钮,支持IE条件注释提示。

  • JavaScript逻辑:初始化参数、文件选择处理、分片切割、并行上传、进度更新、上传控制(确认、取消、删除)、浏览器兼容处理。

3.2.2 服务端组件

  1. AsyncUploadServlet:核心Servlet,负责处理分片上传、文件合并、取消上传、删除文件请求,实现安全校验和业务对接。

    • 初始化:配置上传目录、允许的文件类型、文件大小限制,创建目录(若不存在)。

    • 分块上传处理:接收分片参数和数据,校验参数合法性,保存分片到临时目录,更新上传进度和已上传分片记录。

    • 文件合并处理:校验所有分片完整性,按顺序合并分片,校验文件大小,保存到最终目录,更新Session中的文件信息。

    • 取消上传处理:删除临时分片目录,清理上传进度和已上传分片记录。

    • 删除文件处理:删除已上传的完整文件,清理Session中的文件信息。

  2. AsyncUploadProgressServlet:辅助Servlet,专为IE浏览器提供上传进度查询功能,接收uploadId,返回当前上传进度。

3.2.3 存储设计

  • 临时存储目录:tmp/upload/asyncUploadTemp,用于保存分片文件,每个上传任务对应一个子目录(以uploadId命名),分片文件以"分片索引.part"命名。

  • 最终存储目录:tmp/upload/asyncUpload,用于保存合并后的完整文件,文件以UUID重命名(避免文件名重复和路径遍历攻击),保留原文件扩展名。

  • 目录管理:服务端初始化时自动创建临时目录和最终目录,文件合并完成后删除对应临时目录,取消上传时删除临时目录,支持后续添加定时清理临时文件功能。

四、核心实现(完整可运行代码)

4.1 环境依赖

服务端需引入以下依赖包(Maven配置),确保Servlet 3.0+环境:

xml 复制代码
<!-- 文件上传依赖 -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<!-- JSON序列化依赖 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.7</version>
</dependency>
<!-- 日志依赖 -->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
    

4.2 web.xml配置(修复异步支持)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 异步上传文件Servlet -->
<servlet>
        <servlet-name>asyncUploadServlet</servlet-name>
        <servlet-class>com.web.AsyncUploadServlet</servlet-class>
        <!-- 开启异步支持,解决大文件上传阻塞问题 -->
<async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>asyncUploadServlet</servlet-name>
<url-pattern>/asyncUpload</url-pattern>
    </servlet-mapping>

    <!-- 异步上传进度查询Servlet(适配IE浏览器) -->
    <servlet>
        <servlet-name>asyncUploadProgressServlet</servlet-name>
       <servlet-class>com.web.AsyncUploadProgressServlet</servlet-class>
<async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>asyncUploadProgressServlet</servlet-name>
        <url-pattern>/asyncUploadProgress</url-pattern>
    </servlet-mapping>

</web-app>
    

4.3 服务端代码

4.3.1 AsyncUploadServlet.java(完整版)

java 复制代码
package com.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @description: 大文件分片异步上传核心Servlet
 * @author: liuyongheng
 * @date: 2025-09-18 18:13:08
 */
public class AsyncUploadServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    protected final static Log log = LogFactory.getLog(AsyncUploadServlet.class);

    // 上传临时目录(分片存储)
    private String uploadTempDir;
    // 上传最终目录(完整文件存储)
    private String uploadDir;
    // 允许上传的文件类型(白名单)
    private Set<String> allowedFileTypes;
    // 允许上传的文件大小(key:业务编码,value:大小限制,单位:字节)
    public static Map<String, Long> alloweFileSize;
    // 上传进度跟踪 (uploadId -> 进度百分比),静态并发安全
    public static Map<String, Float> uploadProgress = new ConcurrentHashMap<>();
    // 已上传的分块 (uploadId -> Set<chunkIndex>),静态并发安全,解决多用户混乱问题
    public static Map<String, Set<Integer>> uploadedChunks = new ConcurrentHashMap<>();
    // 最大上传文件数量(单用户单次)
    private static final int MAX_FILE_COUNT = 5;
    // 最大上传文件分片数量
    private static final int MAX_CHUNK_COUNT = 30;

    @Override
    public void init() throws ServletException {
        // 初始化上传目录(可后续改为配置文件读取)
        uploadDir = "tmp/upload/asyncUpload";
        uploadTempDir = "tmp/upload/asyncUploadTemp";
        // 创建目录(如果不存在)
        File dir = new File(uploadDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        dir = new File(uploadTempDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 初始化允许的文件类型(白名单)
        allowedFileTypes = new HashSet<>();
        allowedFileTypes.add("image/jpeg");
        allowedFileTypes.add("image/png");
        allowedFileTypes.add("image/gif");
        allowedFileTypes.add("image/bmp");
        allowedFileTypes.add("application/zip");
        allowedFileTypes.add("application/pdf");
        allowedFileTypes.add("application/msword");
        allowedFileTypes.add("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        allowedFileTypes.add("application/vnd.ms-excel");
        allowedFileTypes.add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

        // 初始化允许的文件大小(默认10MB,业务编码可自定义,如200MB)
        alloweFileSize = new HashMap<>();
        alloweFileSize.put("default", 10485760L); // 10MB = 10*1024*1024
        // 此处根据业务需求灵活修改,key为业务唯一标识,value为对应大小限制(单位:字节)
        alloweFileSize.put("trans001", 209715200L); // 200MB = 200*1024*1024
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        log.info("AsyncUploadServlet Start");
        long startTime = System.currentTimeMillis();
        // 设置响应内容类型,避免中文乱码
        response.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = response.getWriter();
        HttpSession session = request.getSession();

        // 1. 登录校验:必须登录才能上传(根据业务调整session属性)
        if (session == null || session.getAttribute("session_login_model") == null) {
            sendError(out, "未登录,请先登录后再上传");
            return;
        }

        // 2. 安全校验:验证请求来源,防止跨域恶意请求
        String referer = request.getHeader("Referer");
        String serverName = request.getServerName();
        if (referer == null || !referer.contains(serverName)) {
            sendError(out, "不允许的请求来源,禁止跨域上传");
            return;
        }

        // 3. 处理不同操作类型(分片上传、合并、取消、删除)
        String action = request.getParameter("action");
        if ("complete".equals(action)) {
            // 处理文件合并(所有分片上传完成后调用)
            handleComplete(request, out);
        } else if ("cancel".equals(action)) {
            // 处理取消上传
            handleCancel(request, out);
        } else if ("delete".equals(action)) {
            // 处理删除已上传文件
            handleDelete(request, out);
        } else {
            // 默认处理分块上传
            handleChunkUpload(request, out);
        }

        log.info("AsyncUploadServlet End, 耗时: " + (System.currentTimeMillis() - startTime) + "ms");
    }

    /**
     * 处理分块上传
     * @param request 请求对象
     * @param out 响应输出流
     */
    private void handleChunkUpload(HttpServletRequest request, PrintWriter out) {
        // 初始化参数
        String uploadId = null; // 上传唯一标识,用于跟踪分片和进度
        String fileName = null; // 原始文件名
        String fileSizeStr = null; // 文件总大小(字符串)
        String chunkIndexStr = null; // 当前分片索引(从0开始)
        String totalChunksStr = null; // 总分片数量
        String transCode = null; // 业务编码(用于区分不同业务的大小限制)
        byte[] chunkData = null; // 当前分片数据

        // 校验请求是否为multipart/form-data类型(文件上传请求)
        if (ServletFileUpload.isMultipartContent(request)) {
            try {
                // 解析请求,获取表单字段和文件分片数据
                List<FileItem> items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
                for (FileItem item : items) {
                    if (!item.isFormField()) {
                        // 非表单字段:获取分片数据
                        chunkData = item.get();
                    } else {
                        // 表单字段:获取上传相关参数
                        String fieldName = item.getFieldName();
                        String fieldValue = item.getString();
                        if ("uploadId".equals(fieldName)) {
                            uploadId = URLDecoder.decode(fieldValue, "UTF-8");
                        } else if ("fileName".equals(fieldName)) {
                            fileName = URLDecoder.decode(fieldValue, "UTF-8");
                        } else if ("fileSize".equals(fieldName)) {
                            fileSizeStr = fieldValue;
                        } else if ("chunkIndex".equals(fieldName)) {
                            chunkIndexStr = fieldValue;
                        } else if ("totalChunks".equals(fieldName)) {
                            totalChunksStr = fieldValue;
                        } else if ("transCode".equals(fieldName)) {
                            transCode = fieldValue;
                        }
                    }
                }
            } catch (Exception e) {
                log.error("获取上传参数失败:", e);
                sendError(out, "获取上传参数失败,请重试");
                return;
            }
        }

        log.info("handleChunkUpload: chunkIndex=" + chunkIndexStr + ", totalChunks=" + totalChunksStr +
                ", uploadId=" + uploadId + ", fileName=" + fileName + ", fileSize=" + fileSizeStr + ", transCode=" + transCode);

        // 4. 参数合法性校验
        if (uploadId == null || fileName == null || chunkIndexStr == null || totalChunksStr == null) {
            sendError(out, "缺少必要的上传参数(uploadId、fileName、chunkIndex、totalChunks)");
            uploadProgress.remove(uploadId);
            return;
        }

        // 解析参数(字符串转对应类型)
        int chunkIndex;
        int totalChunks;
        long fileSize;
        try {
            chunkIndex = Integer.parseInt(chunkIndexStr);
            totalChunks = Integer.parseInt(totalChunksStr);
            fileSize = Long.parseLong(fileSizeStr);
        } catch (NumberFormatException e) {
            sendError(out, "参数格式错误(chunkIndex、totalChunks、fileSize需为数字)");
            uploadProgress.remove(uploadId);
            return;
        }

        // 5. 文件类型校验(白名单校验)
        String fileType = getFileType(fileName);
        if (!allowedFileTypes.contains(fileType)) {
            sendError(out, "不允许上传的文件类型: " + fileType + ",仅支持图片、文档、压缩包");
            uploadProgress.remove(uploadId);
            return;
        }

        // 6. 文件大小校验(根据业务编码获取对应限制,无业务编码则用默认值)
        long maxFileSize = alloweFileSize.getOrDefault(transCode, alloweFileSize.get("default"));
        if (fileSize > maxFileSize) {
            sendError(out, "文件大小超出限制,最大允许上传" + (maxFileSize / 1024 / 1024) + "MB");
            uploadProgress.remove(uploadId);
            return;
        }

        // 7. 分片数量校验(防止分片过多导致异常)
        if (chunkIndex > MAX_CHUNK_COUNT) {
            sendError(out, "文件分片过多,最大允许" + MAX_CHUNK_COUNT + "个分片");
            uploadProgress.remove(uploadId);
            return;
        }

        // 8. 创建临时目录(按uploadId区分,存储当前上传任务的所有分片)
        String tempDirPath = uploadTempDir + File.separator + uploadId;
        File tempDir = new File(tempDirPath);
        if (!tempDir.exists()) {
            tempDir.mkdirs();
        }

        // 9. 保存当前分片到临时目录
        String chunkFileName = chunkIndex + ".part"; // 分片文件名(索引.part)
        String chunkFilePath = tempDirPath + File.separator + chunkFileName;
        File chunkFile = new File(chunkFilePath);

        try (InputStream in = new ByteArrayInputStream(chunkData);
             OutputStream outStream = new FileOutputStream(chunkFile)) {
            byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区,提升写入效率
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                outStream.write(buffer, 0, bytesRead);
            }
        } catch (IOException e) {
            sendError(out, "保存分片失败: " + e.getMessage());
            uploadProgress.remove(uploadId);
            return;
        }

        // 10. 记录已上传的分片(并发安全)
        uploadedChunks.computeIfAbsent(uploadId, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
        uploadedChunks.get(uploadId).add(chunkIndex);

        // 11. 更新上传进度(百分比)
        int uploadedCount = uploadedChunks.get(uploadId).size();
        float progress = (float) uploadedCount / totalChunks * 100;
        uploadProgress.put(uploadId, progress);

        // 12. 返回分片上传成功响应
        sendSuccess(out, "分块 " + (chunkIndex + 1) + "/" + totalChunks + " 上传成功", "");
    }

    /**
     * 处理文件合并(所有分片上传完成后调用)
     * @param request 请求对象
     * @param out 响应输出流
     */
    private void handleComplete(HttpServletRequest request, PrintWriter out) {
        String uploadId = null;
        String fileName = null;
        try {
            // 解码参数,避免中文乱码
            uploadId = URLDecoder.decode(String.valueOf(request.getParameter("uploadId")), "UTF-8");
            fileName = URLDecoder.decode(String.valueOf(request.getParameter("fileName")), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            sendError(out, "缺少必要的参数(uploadId、fileName)");
            uploadProgress.remove(uploadId);
            return;
        }

        String fileSizeStr = request.getParameter("fileSize");
        String transCode = request.getParameter("transCode");
        String asyncUploadState = request.getParameter("asyncUploadState"); // 上传状态(是否为续传)
        log.info("handleComplete: uploadId=" + uploadId + ", fileName=" + fileName + ", fileSize=" + fileSizeStr + ", transCode=" + transCode);

        // 参数校验
        if (uploadId == null || fileName == null) {
            sendError(out, "缺少必要的参数(uploadId、fileName)");
            uploadProgress.remove(uploadId);
            return;
        }

        // 解析文件大小
        long fileSize;
        try {
            fileSize = Long.parseLong(fileSizeStr);
        } catch (NumberFormatException e) {
            sendError(out, "文件大小参数错误,请重试");
            uploadProgress.remove(uploadId);
            return;
        }

        // 校验已上传分片是否存在
        Set<Integer> chunks = uploadedChunks.get(uploadId);
        if (chunks == null || chunks.isEmpty()) {
            sendError(out, "未找到上传的分块数据,请重新上传");
            uploadProgress.remove(uploadId);
            return;
        }

        // 13. 创建安全的最终文件名(UUID重命名,防止路径遍历攻击和文件名重复)
        String safeFileName = createSafeFileName(fileName);
        String filePath = uploadDir + File.separator + safeFileName;

        // 14. 合并分片(按索引顺序合并)
        String tempDirPath = uploadTempDir + File.separator + uploadId;
        File tempDir = new File(tempDirPath);

        if (!tempDir.exists() || !tempDir.isDirectory()) {
            sendError(out, "临时分块目录不存在,请重新上传");
            uploadProgress.remove(uploadId);
            return;
        }

        try {
            OutputStream outStream = new FileOutputStream(filePath);
            // 按分片索引顺序合并(从0开始)
            for (int i = 0; i < chunks.size(); i++) {
                if (!chunks.contains(i)) {
                    // 缺少分片,删除已创建的文件和临时目录,返回错误
                    Files.deleteIfExists(Paths.get(filePath));
                    deleteDirectory(tempDir);
                    sendError(out, "缺少分块 " + (i + 1) + ",上传失败,请重新上传");
                    return;
                }

                // 读取当前分片并写入最终文件
                String chunkFilePath = tempDirPath + File.separator + i + ".part";
                File chunkFile = new File(chunkFilePath);
                try (InputStream in = new FileInputStream(chunkFile)) {
                    byte[] buffer = new byte[1024 * 1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) != -1) {
                        outStream.write(buffer, 0, bytesRead);
                    }
                }
                // 合并完成后删除当前分片,释放磁盘空间
                chunkFile.delete();
            }

            // 15. 校验合并后的文件大小是否与原始大小一致(防止文件损坏)
            File finalFile = new File(filePath);
            if (finalFile.length() != fileSize) {
                finalFile.delete();
                sendError(out, "文件上传不完整,大小不匹配,请重新上传");
                uploadProgress.remove(uploadId);
                return;
            }

            // 16. 再次校验文件大小(防止合并过程中异常)
            long maxFileSize = alloweFileSize.getOrDefault(transCode, alloweFileSize.get("default"));
            if (finalFile.length() > maxFileSize) {
                finalFile.delete();
                sendError(out, "文件大小超出限制,最大允许上传" + (maxFileSize / 1024 / 1024) + "MB");
                return;
            }

            // 17. 删除临时目录(合并完成,临时分片已删除)
            tempDir.delete();

            // 18. 清理进度记录和分片记录
            uploadProgress.remove(uploadId);
            uploadedChunks.remove(uploadId);

            // 19. 保存文件信息到Session(供业务系统调用)
            HttpSession session = request.getSession();
            String operSign = (String) ((Map) session.getAttribute("session_model")).get("opersign");
            // 构建Session key(区分业务编码和操作用户)
            StringBuilder sessionKey = new StringBuilder();
            sessionKey.append("asyncUploadList").append(transCode).append(operSign);
            // 非续传场景,清除历史上传记录
            if (!"01".equals(asyncUploadState)) {
                session.removeAttribute(sessionKey.toString());
            }

            // 校验上传文件数量是否超出限制
            List<?> fileList = (List<?>) session.getAttribute(sessionKey.toString());
            if (fileList == null) {
                fileList = new ArrayList<>();
            }
            if (fileList.size() >= MAX_FILE_COUNT) {
                sendError(out, "文件上传个数超出限制,最多允许上传" + MAX_FILE_COUNT + "个文件");
                return;
            }

            // 校验总文件大小是否超出限制
            long totalFileSize = fileSize;
            if (fileList.size() > 0) {
                for (Object obj : fileList) {
                    Map<?, ?> fileMap = (Map<?, ?>) obj;
                    totalFileSize += Long.parseLong(fileMap.get("fileSize").toString());
                    if (totalFileSize > maxFileSize) {
                        sendError(out, "文件总大小超出限制,最大允许上传" + (maxFileSize / 1024 / 1024) + "MB");
                        return;
                    }
                }
            }

            // 保存当前文件信息到Session
            HashMap<String, String> fileMap = new HashMap<>();
            fileMap.put("filePath", filePath);
            fileMap.put("fileSize", String.valueOf(finalFile.length()));
            fileMap.put("originalFileName", fileName);
            fileMap.put("safeFileName", safeFileName);
            ((List) fileList).add(fileMap);
            session.setAttribute(sessionKey.toString(), fileList);

            // 20. 返回合并成功响应(携带安全文件名,供前端回调使用)
            sendSuccess(out, "文件异步上传完成", safeFileName);
            log.info("文件异步上传完成,文件名:" + safeFileName + ",路径:" + filePath);
        } catch (IOException e) {
            uploadProgress.remove(uploadId);
            log.error("合并文件失败:", e);
            sendError(out, "合并文件失败,请重新上传");
        }
    }

    /**
     * 处理取消上传
     * @param request 请求对象
     * @param out 响应输出流
     */
    private void handleCancel(HttpServletRequest request, PrintWriter out) {
        String uploadId = null;
        try {
            uploadId = URLDecoder.decode(String.valueOf(request.getParameter("uploadId")), "UTF-8");
            log.info("handleCancel: uploadId=" + uploadId);
        } catch (UnsupportedEncodingException e) {
            sendError(out, "缺少uploadId参数,无法取消上传");
            return;
        }

        if (uploadId == null) {
            sendError(out, "缺少uploadId参数,无法取消上传");
            return;
        }

        // 删除临时分片目录
        String tempDirPath = uploadTempDir + File.separator + uploadId;
        File tempDir = new File(tempDirPath);
        deleteDirectory(tempDir);

        // 清理进度记录和分片记录
        uploadProgress.remove(uploadId);
        uploadedChunks.remove(uploadId);

        sendSuccess(out, "上传已取消", "");
    }

    /**
     * 处理删除已上传文件
     * @param request 请求对象
     * @param out 响应输出流
     */
    private void handleDelete(HttpServletRequest request, PrintWriter out) {
        HttpSession session = request.getSession();
        String transCode = request.getParameter("transCode");
        String refileName = request.getParameter("refileName"); // 安全文件名(删除依据)
        log.info("handleDelete: transCode=" + transCode + ", refileName=" + refileName);

        // 校验参数
        if (transCode == null || refileName == null) {
            sendError(out, "缺少必要的参数(transCode、refileName)");
            return;
        }

        // 获取操作用户标识,构建Session key
        String operSign = (String) ((Map) session.getAttribute("session_model")).get("opersign");
        StringBuilder sessionKey = new StringBuilder();
        sessionKey.append("asyncUploadList").append(transCode).append(operSign);

        // 从Session中获取已上传文件列表,删除对应文件
        List<?> fileList = (List<?>) session.getAttribute(sessionKey.toString());
        if (fileList != null && fileList.size() > 0) {
            for (int i = 0; i < fileList.size(); i++) {
                Map<?, ?> fileMap = (Map<?, ?>) fileList.get(i);
                String filePath = (String) fileMap.get("filePath");
                // 根据安全文件名匹配,删除对应文件
                if (filePath.endsWith(refileName)) {
                    // 删除磁盘文件
                    File file = new File(filePath);
                    if (file.exists()) {
                        file.delete();
                    }
                    // 从Session列表中删除
                    ((List) fileList).remove(i);
                    break;
                }
            }
            // 更新Session中的文件列表
            session.setAttribute(sessionKey.toString(), fileList);
        }

        sendSuccess(out, "文件删除成功", refileName);
    }

    /**
     * 创建安全的文件名(UUID重命名,防止路径遍历攻击和文件名重复)
     * @param originalFileName 原始文件名
     * @return 安全文件名(UUID+扩展名)
     */
    private String createSafeFileName(String originalFileName) {
        // 获取文件扩展名(如.jpg、.pdf)
        String extension = "";
        int dotIndex = originalFileName.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < originalFileName.length() - 1) {
            extension = originalFileName.substring(dotIndex);
        }
        // 生成UUID,拼接扩展名,确保文件名唯一
        return UUID.randomUUID().toString() + extension;
    }

    /**
     * 根据文件名获取文件MIME类型(简单判断,可扩展为更精确的方式)
     * @param fileName 文件名
     * @return 文件MIME类型
     */
    private String getFileType(String fileName) {
        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
            return "image/jpeg";
        } else if (fileName.endsWith(".png")) {
            return "image/png";
        } else if (fileName.endsWith(".gif")) {
            return "image/gif";
        } else if (fileName.endsWith(".bmp")) {
            return "image/bmp";
        } else if (fileName.endsWith(".pdf")) {
            return "application/pdf";
        } else if (fileName.endsWith(".doc")) {
            return "application/msword";
        } else if (fileName.endsWith(".docx")) {
            return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        } else if (fileName.endsWith(".xls")) {
            return "application/vnd.ms-excel";
        } else if (fileName.endsWith(".xlsx")) {
            return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        } else if (fileName.endsWith(".zip")) {
            return "application/zip";
        }
        // 未知类型,返回原始文件名(后续可扩展校验)
        return fileName;
    }

    /**
     * 递归删除目录及其内容(包括子目录和文件)
     * @param dir 要删除的目录
     * @return 删除结果(true:成功,false:失败)
     */
    private boolean deleteDirectory(File dir) {
        if (dir.isDirectory()) {
            File[] children = dir.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteDirectory(child);
                }
            }
        }
        // 删除目录(空目录或文件)
        return dir.delete();
    }

    /**
     * 发送成功响应(JSON格式)
     * @param out 响应输出流
     * @param message 成功消息
     * @param reFileName 安全文件名(可选)
     */
    private void sendSuccess(PrintWriter out, String message, String reFileName) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", message);
        result.put("reFileName", reFileName);
        out.write(toJson(result));
        log.info("上传成功:" + message);
    }

    /**
     * 发送错误响应(JSON格式)
     * @param out 响应输出流
     * @param message 错误消息
     */
    private void sendError(PrintWriter out, String message) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", message);
        out.write(toJson(result));
        log.error("上传错误:" + message);
    }

    /**
     * 将Map转换为JSON字符串(静态方法,供进度查询Servlet调用)
     * @param result 要转换的Map
     * @return JSON字符串
     */
    public static String toJson(Map<String, Object> result) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(result);
        } catch (Exception e) {
            log.error("JSON序列化失败:", e);
        }
        // 序列化失败时返回默认错误响应
        return "{\"success\":false,\"message\":\"JSON序列化失败\"}";
    }
}
    

4.3.2 AsyncUploadProgressServlet.java(完整版)

java 复制代码
package com.web;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @description: 为IE浏览器提供上传进度查询功能(IE不支持原生上传进度监听)
 * @author: liuyongheng
 * @date: 2025-09-19 08:44:30
 */
public class AsyncUploadProgressServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 设置响应格式,避免中文乱码
        response.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = response.getWriter();

        // 获取上传唯一标识(uploadId)
        String uploadId = request.getParameter("uploadId");
        if (uploadId == null) {
            sendError(out, "缺少uploadId参数,无法查询进度");
            return;
        }

        // 从AsyncUploadServlet中获取上传进度
        Float progress = AsyncUploadServlet.uploadProgress.get(uploadId);

        if (progress == null) {
            sendError(out, "未找到上传进度信息,请确认uploadId是否正确");
        } else {
            // 返回进度信息(百分比)
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("progress", progress);
            out.write(AsyncUploadServlet.toJson(result));
        }
    }

    /**
     * 发送错误响应(适配IE浏览器,返回进度为0)
     * @param out 响应输出流
     * @param message 错误消息
     */
    private void sendError(PrintWriter out, String message) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", message);
        result.put("progress", 0); // IE浏览器需要进度字段,避免解析异常
        out.write(AsyncUploadServlet.toJson(result));
    }
}
    

4.4 前端代码(asyncUpload.jsp,修复IE兼容BUG)

jsp 复制代码
<%@ page language="java" pageEncoding="UTF-8" contentType="text/html;charset=utf-8" %>
<style>
	.asyncUploadContainer { max-width: 800px; margin: 0 auto; }
	.upload-area { border: 2px dashed #ccc; padding: 40px; text-align: center; margin-bottom: 20px; cursor: pointer; }
	.upload-area:hover { border-color: #666; }
	.progress-container { width: 100%; height: 20px; border: 1px solid #ccc; border-radius: 10px; overflow: hidden; margin: 10px 0; }
	.asyncUploadProgress-bar { height: 100%; background-color: #ff5263; width: 0%; }
	.file-info { margin: 10px 0; padding: 10px; border: 1px solid #eee;}
	.status { color: #666; margin: 10px 0; word-break:break-all;}
	.asyncBtn { padding: 10px 20px; background-color: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer; }
	.asyncbtn:hover { background-color: #45a049; }
	.hidden { display: none; }
	.upload-btn { width: 100%;text-align: center; }
	.ie-notice { color: #f00; padding: 10px; background-color: #fff3cd; border: 1px solid #ffeeba; margin-bottom: 20px; }
</style>
<div class="asyncUploadContainer" style="display: none;" id="asyncUploadContainer">
	<!-- IE浏览器提示 -->
	<!--[if IE]>
	<div class="ie-notice">
		您正在使用IE浏览器,部分功能可能受限。建议使用现代浏览器以获得最佳体验。
	</div>
	<![endif]-->

	<div class="upload-area" id="uploadArea">
		<p>点击这里上传</p>
		<input type="file" id="fileInput" class="hidden" />
	</div>

	<div id="fileInfo" class="file-info" style="display: none;">
		<p>文件名: <span id="fileName"></span></p>
		<p>大小: <span id="fileSize"></span></p>
		<div class="progress-container">
			<div class="asyncUploadProgress-bar" id="progressBar"></div>
		</div>
		<p class="status" id="uploadStatus">等待上传...</p>
		<div class="upload-btn">
			<button class="asyncBtn" id="confirmUpload">确认</button>
			<button class="asyncBtn" id="cancelUpload" style="background-color: #f44336; margin-left: 10px;" disabled>取消</button>
		</div>
	</div>
</div>

<script>

	// 全局变量
	var file = null;
	var uploadId = null;
	var isUploading = false;
	var progressInterval = null;
	var chunkSize = 20 * 1024 * 1024; // 20MB每块,根据测试情况灵活修改
	var totalChunks = 0;
	var uploadedChunks = 0;
	var pendingChunks = [];
	var activeUploads = 0;
	var MAX_PARALLEL_UPLOADS = 3;     // 最大并行上传数
	// DOM元素
	var uploadArea = null;
	var fileInput = null;
	var fileInfo = null;
	var fileName = null;
	var fileSize = null;
	var progressBar = null;
	var uploadStatus = null;
	var confirmUpload =null;
	var cancelUploadBtn =null;
	// 检测浏览器是否为IE
	var isIE = null;
	var transCode = null;
	var uploadAborted = false;

	//初始化参数
	function asyncUploadInit (transcode){

		uploadArea = $(".fy-alert-content #uploadArea").get(0);
		fileInput = $(".fy-alert-content #fileInput").get(0);
		fileInfo = $(".fy-alert-content #fileInfo").get(0);
		fileName = $(".fy-alert-content #fileName").get(0);
		fileSize = $(".fy-alert-content #fileSize").get(0);
		progressBar = $(".fy-alert-content #progressBar").get(0);
		uploadStatus = $(".fy-alert-content #uploadStatus").get(0);
		confirmUpload = $(".fy-alert-content #confirmUpload").get(0);
		cancelUploadBtn = $(".fy-alert-content #cancelUpload").get(0);
		transCode = transcode;
		isIE = /*@cc_on!@*/false || !!document.documentMode;

		// 事件监听
		 if (uploadArea.addEventListener) {
			 uploadArea.addEventListener('click', function() {
				 console.log('Click event triggered');
				 fileInput.click();
			 });
		 } else if (uploadArea.attachEvent) {
			 uploadArea.attachEvent('onclick', function() {
				 console.log('Click event triggered');
				 fileInput.click();
			 });
		 }

		if (fileInput.addEventListener) {
			fileInput.addEventListener('change', handleFileSelect);
		} else if (fileInput.attachEvent) {
			fileInput.attachEvent('onchange', handleFileSelect);
		}

		if (confirmUpload.addEventListener) {
			confirmUpload.addEventListener('click', closeWindow);
		} else if (confirmUpload.attachEvent) {
			confirmUpload.attachEvent('onclick', closeWindow);
		}

		if (cancelUploadBtn.addEventListener) {
			cancelUploadBtn.addEventListener('click', cancelUpload);
		} else if (cancelUploadBtn.attachEvent) {
			cancelUploadBtn.attachEvent('onclick', cancelUpload);
		}
	}
	// 处理文件选择
	function handleFileSelect(e) {
		if (e.target && e.target.files && e.target.files.length > 0) {
			handleFile(e.target.files[0]);
		} else if (e.srcElement && e.srcElement.value) {
			// 处理IE低版本浏览器的文件选择
			handleFileIE(e.srcElement.value);
		}
		cancelUploadBtn.disabled = false;
		uploadAborted = false;
		if(!validateFileName(e.target.files[0].name)){
			if(typeof mesgAlert === 'function'){
				mesgAlert("文件名称不合法");
			} else {
				alert("文件名称不合法");
			}
			return;
		}
		startUpload();
	}

	//处理IE低版本浏览器的文件选择
	function handleFileIE(filePath) {
		try {
			// 这里可以添加处理 IE文件选择的逻辑,例如,使用 ActiveXObject 读取文件内容
			var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
			var file = fileSystem.GetFile(filePath);
			var fileContent = file.OpenAsTextStream(1, 0).ReadAll();
			// 调用 handleFile 函数处理文件内容
			handleFile(fileContent);
		} catch (e) {
			alert("无法读取文件,请检查浏览器的安全设置。",e);
		}
	}

	// 处理文件信息
	function handleFile(selectedFile) {
		file = selectedFile;
		// 显示文件信息
		fileName.textContent = file.name;
		fileSize.textContent = formatFileSize(file.size);
		fileInfo.style.display="block";
		// 生成上传ID
		uploadId = generateUploadId(file);
	}

	// 生成上传ID (基于文件名、大小和当前时间)
	function generateUploadId(file) {
		var time = new Date().getTime();
		// IE不支持File API的某些属性,做兼容处理
		var name = file.name || 'unknown';
		var size = file.size || 0;
		return 'upload_' + name + '_' + size + '_' + time +'_' + transCode;
	}

	// 格式化文件大小
	function formatFileSize(bytes) {
		if (bytes === 0) return '0 Bytes';
		var k = 1024;
		var sizes = ['Bytes', 'KB', 'MB', 'GB'];
		var i = Math.floor(Math.log(bytes) / Math.log(k));
		return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
	}

	// 开始上传
	function startUpload() {
		if (!file || isUploading) return;
		isUploading = true;
		cancelUploadBtn.disabled = false;
		uploadStatus.textContent = '准备上传...';
		//debugger;
		// 计算分块数量
		totalChunks = Math.ceil(file.size / chunkSize);
		uploadedChunks = 0;
		pendingChunks = [];

		// 对于IE浏览器,使用轮询方式获取进度
		if (isIE) {
			progressInterval = setInterval(checkUploadProgress, 1000);
		}
		 for (var i = 0; i < totalChunks; i++) {
			pendingChunks.push(i);
		}
		// 开始上传分块
		startChunkUploads();
	}

	function startChunkUploads(){
		if(!uploadAborted) {
			while (activeUploads < MAX_PARALLEL_UPLOADS && pendingChunks.length > 0) {
				var chunkIndex = pendingChunks.shift();
				uploadNextChunk(chunkIndex);
				activeUploads++;
			}
		}
	}

	// 上传下一个分块
	function uploadNextChunk(chunkIndex) {
		var start = chunkIndex * chunkSize;
		var end = Math.min(start + chunkSize, file.size);
		var chunk = getFileChunk(file, start, end);
		uploadStatus.textContent = '上传中: ' + (chunkIndex + 1) + '/' + totalChunks;
		// 创建表单数据
		var formData = createFormData(uploadId, file, chunk, chunkIndex, totalChunks, start, end);
		// 上传分块
		uploadChunk(formData);
	}

	// 获取文件分块 - 兼容IE
	function getFileChunk(file, start, end) {
		// 对于IE,使用FileReader或ActiveXObject
		if (false) {//isIE && window.ActiveXObject
			// IE的特殊处理
			try {
				var fileStream = new ActiveXObject("ADODB.Stream");
			//	var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
				if (!fileSystem.FileExists(file.path)) {
					throw new Error("文件不存在");
				}
				fileStream.Type = 1; // Binary
				fileStream.Open();
				fileStream.LoadFromFile(file.path);
				fileStream.Position = start;
				var chunkData = fileStream.Read(end - start);
				fileStream.Close();
				return chunkData;
			} catch (e) {
				alert("IE浏览器上传需要启用ActiveX控件: " + e.message);
				cancelUpload();
				return null;
			}
		} else if (file.slice) {//IE11走这个
			return file.slice(start, end);
		} else if (file.mozSlice) {
			return file.mozSlice(start, end);
		} else if (file.webkitSlice) {
			return file.webkitSlice(start, end);
		}
		return null;
	}

	// 创建表单数据
	function createFormData(uploadId, file, chunk, chunkIndex, totalChunks, start, end) {
		var formData = new FormData();
		formData.append('uploadId', encodeURIComponent(uploadId));
		formData.append('fileName', encodeURIComponent(file.name));
		formData.append('fileSize', file.size);
		formData.append('chunkIndex', chunkIndex);
		formData.append('totalChunks', totalChunks);
		formData.append('start', start);
		formData.append('end', end);
		formData.append('chunkData', chunk);
		formData.append('transCode', transCode);
		return formData;
	}

	// 上传分块
	function uploadChunk(formData) {
		if (uploadAborted) return;
		// 创建XMLHttpRequest,兼容IE
		var xhr = createXMLHttpRequest();
		// 非IE浏览器可以监听progress事件
		if (!isIE) {
			xhr.upload.addEventListener('progress', function(e) {
				if (e.lengthComputable) {
					var chunkProgress = e.loaded / e.total;
					var overallProgress = (uploadedChunks + chunkProgress) / totalChunks * 100;
					updateProgress(overallProgress);
				}
			});
		}

		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4) {
				activeUploads--;
				if (uploadAborted) return;
				if (xhr.status === 200) {
					try {
						// 处理IE的JSON解析问题
						var response = isIE ? eval('(' + xhr.responseText + ')') : JSON.parse(xhr.responseText);

						if (response.success) {
							uploadedChunks++;
							// 更新进度(IE通过轮询更新,这里只更新非IE)
							if (!isIE) {
								var progress = (uploadedChunks / totalChunks) * 100;
								updateProgress(progress);
							}
							//uploadNextChunk();
							if(uploadedChunks === totalChunks ){
								completeUpload();
							} else {
								startChunkUploads();
							}
						} else {
							cancelUpload();
							uploadStatus.textContent = '上传失败: ' + (response.message || '未知错误');
						}
					} catch (e) {
						cancelUpload();
						uploadStatus.textContent = '解析响应失败: ' + e.message;
					}
				} else {
					cancelUpload();
					uploadStatus.textContent = '上传失败,HTTP状态: ' + xhr.status;
				}
			}
		};
		xhr.open('POST', '/asyncUpload', true);
		xhr.send(formData);
	}

	// 完成上传,通知服务器合并文件
	function completeUpload() {
		var xhr = createXMLHttpRequest();
		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4) {
				clearInterval(progressInterval);

				if (xhr.status === 200) {
					try {
						var response = isIE ? eval('(' + xhr.responseText + ')') : JSON.parse(xhr.responseText);

						if (response.success) {
							response.fileName = file.name;
							updateProgress(100);
							uploadStatus.textContent = '上传完成!' ;
							asyncUploadCallback(response);
						} else {
							uploadStatus.textContent = '合并文件失败: ' + (response.message || '未知错误');
						}
					} catch (e) {
						uploadStatus.textContent = '解析响应失败: ' + e.message;
					}
				} else {
					uploadStatus.textContent = '合并文件失败,HTTP状态: ' + xhr.status;
				}

				isUploading = false;
				cancelUploadBtn.disabled = true;
			}
		};

		xhr.open('POST', '/asyncUpload?action=complete', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
		xhr.send('uploadId=' + encodeURIComponent(uploadId) +
				'&fileName=' + encodeURIComponent(file.name) +
				'&fileSize=' + file.size +
				'&transCode=' + transCode +
				'&asyncUploadState=' + getAsyncUploadState());
	}

	// 取消上传
	function cancelUpload() {
		isUploading = false;
		uploadAborted = true;
		clearInterval(progressInterval);
		// 通知服务器取消上传
		var xhr = createXMLHttpRequest();
		xhr.open('POST', '/asyncUpload?action=cancel', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
		xhr.send('uploadId=' + encodeURIComponent(uploadId));
		uploadStatus.textContent = '上传已取消';
		cancelUploadBtn.disabled = true;
	}

	// 删除上传
	function deleteUpload(refileName) {
		var xhr = createXMLHttpRequest();
		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4) {
				if (xhr.status === 200) {
					try {
						var response = isIE ? eval('(' + xhr.responseText + ')') : JSON.parse(xhr.responseText);
						if (response.success) {
							deleteUploadCallBack(response)
						} else {
							uploadStatus.textContent = '删除文件失败: ' + (response.message || '未知错误');
						}
					} catch (e) {
						uploadStatus.textContent = '解析响应失败: ' + e.message;
					}
				} else {
					uploadStatus.textContent = '删除文件失败,HTTP状态: ' + xhr.status;
				}
			}
		};
		xhr.open('POST', '/asyncUpload?action=delete', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
		xhr.send('transCode=' + transCode +'&refileName=' +refileName);
	}
	// 检查上传进度(IE专用)
	function checkUploadProgress() {
		if (!isUploading) return;

		var xhr = createXMLHttpRequest();
		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4 && xhr.status === 200) {
				try {
					var response = isIE ? eval('(' + xhr.responseText + ')') : JSON.parse(xhr.responseText);
					if (response.progress !== undefined) {
						updateProgress(response.progress);
					}
				} catch (e) {
					console.log('获取进度失败: ' + e.message);
				}
			}
		};

		xhr.open('GET', '/asyncUploadProgress?uploadId=' + encodeURIComponent(uploadId)+'&tmp='+Math.random(), true);
		xhr.send();
	}

	// 更新进度条
	function updateProgress(percent) {
		percent = Math.min(100, Math.max(0, percent));
		progressBar.style.width = percent + '%';
		uploadStatus.textContent = '上传中: ' + Math.round(percent) + '%';
	}

	// 创建XMLHttpRequest对象,兼容IE
	function createXMLHttpRequest() {
		if (window.XMLHttpRequest) {
			return new XMLHttpRequest();
		} else {
			return new ActiveXObject('Microsoft.XMLHTTP');;
		}
	}

	function validateFileName(fileName) {
		// 定义允许的文件名字符集(字母、数字、下划线、连字符、点、汉字)
		var allowedPattern = /^[\u4e00-\u9fa5a-zA-Z0-9_.\-]+$/;
		// 检查文件名是否符合允许的字符集
		if (!allowedPattern.test(fileName)) {
			return false; // 文件名含有特殊字符
		}
		return true; // 文件名合法
	}

	// 点击确定按钮关闭弹窗
	function closeWindow(){
		$(".fy-alert-shadow").hide();
		$(".fy-alert-box").hide();
		$(".fy-alert-box").remove();
	}
</script>
相关推荐
百撕可乐2 小时前
NextJS官网实战01:Vue与React的区别
前端·react.js·前端框架
Можно2 小时前
Vue 组件样式隔离完全指南:从原理到实战
前端·javascript·vue.js
bu_shuo2 小时前
c++中对数组求和
开发语言·c++
赫瑞2 小时前
Java中的大数处理 —— BigInteger
java·开发语言
r_oo_ki_e_2 小时前
java25--Collection集合
java·开发语言
bearpping2 小时前
WebSpoon9.0(KETTLE的WEB版本)编译 + tomcatdocker部署 + 远程调试教程
前端
色空大师2 小时前
网站搭建实操(五)后台管理-短信模块
java·阿里云短信·网站·短信
极创信息2 小时前
信创软件安全加固指南,信创软件的纵深防御体系
java·大数据·数据库·金融·php·mvc·软件工程
elseif1232 小时前
【Markdown】指南(上)
linux·开发语言·前端·javascript·c++·笔记