Springboot上传文件与物理删除

前端页面--新增:

复制代码
<!-- 文件上传区域 -->
<div class="form-group">
    <label class="col-sm-3 control-label is-required">照片文件:</label>
    <div class="col-sm-8">
        <input type="hidden" name="photoPath" id="photoPathHidden">

        <!-- 使用 label 原生触发,覆盖整个区域 -->
        <div id="drop-area" style="border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 10px; cursor: pointer;">
            <label for="fileElem" style="width:100%; height:100%; display:block;">
                <p>拖拽图片到这里,或点击选择文件</p>
            </label>
            <input type="file" id="fileElem" accept="image/*" style="display:none;" />
        </div>

        <div id="preview" style="margin-top: 10px;"></div>

        <button type="button" class="btn btn-primary btn-sm" id="uploadBtn" disabled>上传图片</button>
        <span id="uploadStatus" style="margin-left: 10px; color: green;"></span>
        <button type="button" class="btn btn-danger btn-sm" id="deleteBtn" style="display:none; margin-left:10px;">删除图片</button>
    </div>
</div>

<!-- 提交按钮 -->
<div class="form-group">
    <div class="col-sm-offset-3 col-sm-8">
        <button type="button" class="btn btn-success" onclick="submitHandler()">确定</button>
    </div>
</div>

<script th:inline="javascript">
    var prefix = ctx + "system/record";

    // 初始化日期控件
    $("input[name='photoDate']").datetimepicker({
        format: "yyyy-mm-dd",
        minView: "month",
        autoclose: true
    });

    // 表单验证 & 提交
    $("#form-record-add").validate({
        focusCleanup: true
    });

    function submitHandler() {
        if (!$('#photoPathHidden').val()) {
            alert('请先上传照片!');
            return;
        }
        if ($.validate.form()) {
            console.log('准备提交到:', prefix + "/add");
            $.operate.save(prefix + "/add", $('#form-record-add').serialize());
        }
    }

    // ========== 文件上传逻辑 ==========
    const dropArea = document.getElementById('drop-area');
    const fileElem = document.getElementById('fileElem');
    const preview = document.getElementById('preview');
    const uploadBtn = document.getElementById('uploadBtn');
    const uploadStatus = document.getElementById('uploadStatus');
    const deleteBtn = document.getElementById('deleteBtn');
    let selectedFile = null;

    // 文件选择变化
    fileElem.addEventListener('change', handleFiles);

    // 拖拽事件
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
    });

    function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }

    ['dragenter', 'dragover'].forEach(eventName => {
        dropArea.addEventListener(eventName, highlight, false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, unhighlight, false);
    });

    function highlight() {
        dropArea.style.borderColor = '#007bff';
    }

    function unhighlight() {
        dropArea.style.borderColor = '#ccc';
    }

    dropArea.addEventListener('drop', handleDrop, false);

    function handleDrop(e) {
        const dt = e.dataTransfer;
        const files = dt.files;
        // 手动构造 event 对象传给 handleFiles
        handleFiles({ target: { files: files } });
    }

    function handleFiles(e) {
        const files = e.target.files;
        if (files.length === 0) return;
        selectedFile = files[0];
        // 验证类型
        if (!selectedFile.type.match('image.*')) {
            alert('请选择图片文件(jpg/png/jpeg)');
            selectedFile = null;
            uploadBtn.disabled = true;
            // 清空 input,以便下次可重新选择(包括同名文件)
            fileElem.value = '';
            return;
        }

        // 预览缩略图
        preview.innerHTML = '';
        const img = document.createElement('img');
        img.src = URL.createObjectURL(selectedFile);
        img.style.maxWidth = '200px';
        img.style.maxHeight = '200px';
        preview.appendChild(img);
        uploadBtn.disabled = false;
        uploadStatus.textContent = '';       
        // 因为可能用户想重新上传同一文件(但通常不会),所以暂不在此清空
    }

    // 上传按钮点击
    uploadBtn.addEventListener('click', () => {
        if (!selectedFile) return;

    const formData = new FormData();
    formData.append("file", selectedFile);

    // ✅ 关键修复:使用 ctx 上下文路径,而非硬编码 /
    fetch(ctx + 'common/upload', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json())
    .then(data => {
        if (data.code === 0) {
        $('#photoPathHidden').val(data.fileName);
        uploadStatus.textContent = '✅ 上传成功';
        uploadBtn.disabled = true;
        deleteBtn.style.display = 'inline-block';

        // 上传成功后清空 file input,避免后续干扰
        fileElem.value = '';
    } else {
        alert('上传失败:' + (data.msg || '未知错误'));
        uploadStatus.textContent = '❌ 上传失败';
        // 失败时也清空,允许重试
        fileElem.value = '';
    }
    })
    .catch(err => {
        console.error('上传出错:', err);
    alert('网络错误,请重试');
    uploadStatus.textContent = '❌ 网络错误';
    fileElem.value = ''; // 允许重试
    });
    });

    // 删除按钮点击事件
    deleteBtn.addEventListener('click', function() {
        if (!confirm('确定要删除这张照片吗?')) return;

        const photoPath = $('#photoPathHidden').val();

        if (photoPath) {
            fetch(ctx + 'common/deleteFile?fileName=' + encodeURIComponent(photoPath), {
                method: 'POST'
            })
                .then(response => response.json())
        .then(data => {
                if (data.code !== 0) {
                alert('服务器删除失败:' + (data.msg || ''));
            }
            clearPhotoPreview();
        })
        .catch(err => {
                console.error('删除请求失败:', err);
            alert('网络错误,但本地预览已清除');
            clearPhotoPreview();
        });
        } else {
            clearPhotoPreview();
        }
    });

    // 清空预览和状态的函数
    function clearPhotoPreview() {
        selectedFile = null;
        $('#photoPathHidden').val('');
        preview.innerHTML = '';
        uploadStatus.textContent = '';
        uploadBtn.disabled = true;
        deleteBtn.style.display = 'none';
        // 同时清空文件输入框
        fileElem.value = '';
    }
</script>

修改com.ruoyi.project.common.CommonController的/deleteFile方法:

复制代码
/**
 * 删除文件
 * @param fileName
 * @return
 */
@PostMapping("/deleteFile")
@ResponseBody
public AjaxResult deleteFile(String fileName) {
    try {
        if (StringUtils.isBlank(fileName)) {
            return error("文件名为空");
        }
        if (fileName.contains("..") || !fileName.startsWith("/profile/")) {
            return error("非法文件路径");
        }
        String profilePath = RuoYiConfig.getProfile();
        String relativePath = fileName.substring("/profile/".length());
        // ✅ 使用 Paths.get 自动处理路径分隔符
        String realPath = Paths.get(profilePath, relativePath).toString();
        File file = new File(realPath);
        if (file.exists()) {
            if (file.delete()) {
                return success();
            } else {
                return error("文件删除失败,可能被占用或无权限");
            }
        }
        return success(); // 文件不存在也视为成功(幂等)
    } catch (Exception e) {
        log.error("删除文件异常", e);
        return error("系统异常:" + e.getMessage());
    }
}

前端页面---编辑:

复制代码
<!-- 文件上传区域(复用新增页逻辑) -->
<div class="form-group">
    <label class="col-sm-3 control-label is-required">照片文件:</label>
    <div class="col-sm-8">
        <!-- 隐藏域:存储服务器路径 -->
        <input type="hidden" name="photoPath" id="photoPathHidden" th:value="*{photoPath}">

        <!-- 拖拽区域 -->
        <div id="drop-area" style="border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 10px; cursor: pointer;">
            <label for="fileElem" style="width: 100%; height: 100%; display: block;">
                <p>拖拽图片到这里,或点击选择文件</p>
            </label>
            <input type="file" id="fileElem" accept="image/*" style="display:none;" />
        </div>

        <!-- 缩略图预览 -->
        <div id="preview" style="margin-top: 10px;"></div>

        <!-- 操作按钮 -->
        <button type="button" class="btn btn-primary btn-sm" id="uploadBtn" disabled>上传新图片</button>
        <span id="uploadStatus" style="margin-left: 10px; color: green;"></span>
        <button type="button" class="btn btn-danger btn-sm" id="deleteBtn" style="display:none; margin-left:10px;">删除当前图片</button>
    </div>
</div>

<script th:inline="javascript">
    var prefix = ctx + "system/record";

    // 初始化日期控件
    $("input[name='photoDate']").datetimepicker({
        format: "yyyy-mm-dd",
        minView: "month",
        autoclose: true
    });

    // 表单验证
    $("#form-record-edit").validate({ focusCleanup: true });

    function submitHandler() {
        if (!$('#photoPathHidden').val()) {
            alert('请保留或上传一张照片后再提交!');
            return;
        }
        if ($.validate.form()) {
            $.operate.save(prefix + "/edit", $('#form-record-edit').serialize());
        }
    }

    // ========== 文件上传逻辑 ==========
    const dropArea = document.getElementById('drop-area');
    const fileElem = document.getElementById('fileElem');
    const preview = document.getElementById('preview');
    const uploadBtn = document.getElementById('uploadBtn');
    const uploadStatus = document.getElementById('uploadStatus');
    const deleteBtn = document.getElementById('deleteBtn');
    let selectedFile = null;

    // 获取初始 photoPath(Thymeleaf 渲染)
    const initialPhotoPath = /*[[${photoRecord.photoPath}]]*/'';

    // 初始化预览(如果有已有图片)
    function initPreview() {
        if (initialPhotoPath) {
            preview.innerHTML = '';
            const img = document.createElement('img');
            // ⭐ 关键:如果 photoPath 是相对路径(如 upload/xxx.jpg),需拼接 ctx 或 /
            // 假设你的文件是通过 /common/upload 上传,且访问路径为 /upload/...
            // 如果后端返回的是完整 URL,则无需处理;否则建议统一前缀
            img.src = initialPhotoPath.startsWith('/') ? initialPhotoPath : ctx + initialPhotoPath;
            img.style.maxWidth = '200px';
            img.style.maxHeight = '200px';
            preview.appendChild(img);
            deleteBtn.style.display = 'inline-block';
        }
    }
    initPreview();

    // 文件选择变化
    fileElem.addEventListener('change', handleFiles);

    // 拖拽事件
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
    });

    function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }

    ['dragenter', 'dragover'].forEach(eventName => {
        dropArea.addEventListener(eventName, highlight, false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, unhighlight, false);
    });

    function highlight() {
        dropArea.style.borderColor = '#007bff';
    }

    function unhighlight() {
        dropArea.style.borderColor = '#ccc';
    }

    dropArea.addEventListener('drop', (e) => {
        preventDefaults(e);
    const files = e.dataTransfer.files;
    handleFiles({ target: { files } });
    });

    function handleFiles(e) {
        const files = e.target.files;
        if (files.length === 0) return;
        selectedFile = files[0];

        if (!selectedFile.type.match('image.*')) {
            alert('请选择图片文件(jpg/png/jpeg)');
            selectedFile = null;
            uploadBtn.disabled = true;
            fileElem.value = ''; // 清空,允许重新选择
            return;
        }

        // 更新预览
        preview.innerHTML = '';
        const img = document.createElement('img');
        img.src = URL.createObjectURL(selectedFile);
        img.style.maxWidth = '200px';
        img.style.maxHeight = '200px';
        preview.appendChild(img);

        uploadBtn.disabled = false;
        uploadStatus.textContent = '';
    }

    // 上传新图片
    uploadBtn.addEventListener('click', () => {
        if (!selectedFile) return;

    const formData = new FormData();
    formData.append("file", selectedFile);

    // ✅ 使用 ctx 上下文路径
    fetch(ctx + 'common/upload', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json())
    .then(data => {
        if (data.code === 0) {
        $('#photoPathHidden').val(data.fileName);
        uploadStatus.textContent = '✅ 上传成功';
        uploadBtn.disabled = true;
        deleteBtn.style.display = 'inline-block';

        // 上传成功后清空 input
        fileElem.value = '';
    } else {
        alert('上传失败:' + (data.msg || '未知错误'));
        uploadStatus.textContent = '❌ 上传失败';
        fileElem.value = ''; // 允许重试
    }
    })
    .catch(err => {
        console.error('上传出错:', err);
    alert('网络错误,请重试');
    uploadStatus.textContent = '❌ 网络错误';
    fileElem.value = '';
    });
    });

    // 删除当前图片
    deleteBtn.addEventListener('click', function() {
        if (!confirm('确定要删除这张照片吗?删除后无法恢复!')) return;

        const photoPath = $('#photoPathHidden').val();
        if (photoPath) {
            // ✅ 使用 ctx
            fetch(ctx + 'common/deleteFile?fileName=' + encodeURIComponent(photoPath), {
                method: 'POST'
            })
                .then(response => response.json())
        .then(data => {
                if (data.code !== 0) {
                alert('服务器删除失败:' + (data.msg || ''));
            }
            clearPhotoPreview();
        })
        .catch(err => {
                console.error('删除出错:', err);
            alert('网络错误,请重试');
            clearPhotoPreview(); // 仍清除前端
        });
        } else {
            alert('当前没有照片可删除');
            clearPhotoPreview();
        }
    });

    function clearPhotoPreview() {
        selectedFile = null;
        $('#photoPathHidden').val('');
        preview.innerHTML = '';
        uploadStatus.textContent = '';
        uploadBtn.disabled = true;
        deleteBtn.style.display = 'none';
        fileElem.value = ''; // 关键:重置文件输入框
    }

</script>

以上是添加记录过程中对于图片的上传与删除。

下面是删除数据库记录时,同步删除文件的主要过程。

/remove控制器

复制代码
@PostMapping( "/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
    return toAjax(photoRecordService.deletePhotoRecordByIds(ids));
}

/remove方法所对应的services

复制代码
@Override
public int deletePhotoRecordByIds(String ids) {
    Long[] idArray = Convert.toLongArray(ids);

    // 查询这些记录
    List<PhotoRecord> records = photoRecordMapper.selectPhotoRecordByIds(idArray);

    // 删除文件
    for (PhotoRecord r : records) {
        if (StringUtils.isNotBlank(r.getPhotoPath())) {
            try {
                String realPath = RuoYiConfig.getProfile() +
                        r.getPhotoPath().replaceFirst("^/profile", "");
                Path p = Paths.get(realPath);
                if (Files.exists(p)) Files.delete(p);
            } catch (Exception e) {
                log.warn("批量删除文件失败: {}", r.getPhotoPath(), e);
            }
        }
    }

    // 批量删除数据库
    return photoRecordMapper.deletePhotoRecordByIds(idArray);
}

mapper

复制代码
List<PhotoRecord> selectPhotoRecordByIds(@Param("ids") Long[] ids);

int deletePhotoRecordByIds(Long[] ids);

mapper.xml

复制代码
<select id="selectPhotoRecordByIds" resultType="PhotoRecord" resultMap="PhotoRecordResult">
    SELECT * FROM sys_photo_record
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<delete id="deletePhotoRecordByIds" parameterType="String">
    delete from sys_photo_record where id in 
    <foreach item="id" collection="array" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>
相关推荐
jay神2 小时前
基于SpringBoot的校园社团活动智能匹配与推荐系统
java·前端·spring boot·后端·毕业设计
可以吧可以吧2 小时前
idea全家桶【常见报错处理】当出现 “We could not validate your license ... “ 提示时
java·ide·intellij-idea
装不满的克莱因瓶2 小时前
IDEA rebuild project 到底有什么作用?
java·ide·intellij-idea
Java程序员威哥2 小时前
Arthas+IDEA实战:Java线上问题排查完整流程(Spring Boot项目落地)
java·开发语言·spring boot·python·c#·intellij-idea
Eugene__Chen2 小时前
Java的SPI机制(曼波版)
java·开发语言·python
程序猿20232 小时前
JVM与JAVA
java·jvm·python
Mr__Miss2 小时前
JMM中的工作内存实际存在吗?
java·前端·spring
Gary董2 小时前
内存泄漏和溢出
java·jvm
Elieal2 小时前
SpringBoot 中处理接口传参时常用的注解
java·spring boot·后端