Parcourir la source

feat:经销商导入导出不依赖于oss

yingjian.wu il y a 2 mois
Parent
commit
9dd8361f4d

+ 77 - 20
src/main/java/com/qlm/controller/jinzai/JxsNewController.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import com.alibaba.fastjson.JSON;
 import com.jfinal.aop.Clear;
 import com.jfinal.kit.HttpKit;
+import com.jfinal.kit.PathKit;
 import com.jfinal.upload.UploadFile;
 import com.qlm.annotation.RequestUrl;
 import com.qlm.common.ApiResponse;
@@ -18,6 +19,8 @@ import com.qlm.view.core.AdminView;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.ServletOutputStream;
+import java.io.File;
 import java.io.InputStream;
 import java.net.URLEncoder;
 import java.nio.file.Files;
@@ -146,7 +149,7 @@ public class JxsNewController extends CommonController {
             List<JxsImportDto> importList = EasyExcelUtil.getImportData(inputStream, JxsImportDto.class);
             inputStream.close();
             // 调用Service层导入数据
-            ApiResponse apiResponse = jxsService.importJxs(importList, loginUser.getUsername());
+            ApiResponse apiResponse = jxsService.importJxs(importList, loginUser.getUsername(), getRequest().getContextPath());
             renderJson(apiResponse);
         } catch (Exception e) {
             logger.error("导入产线产品关联信息异常:", e);
@@ -155,32 +158,86 @@ public class JxsNewController extends CommonController {
     }
 
     /**
-     * 导出经销商列表
+     * 导出经销商列表 - 直接流式下载
      */
     public void exportJxsList() {
-        ApiResponse apiResponse = ApiResponse.success();
-        try {
-            // 获取查询条件
-            String jxsName = getPara("jxsName");
-            String code = getPara("code");
-            Integer status = getParaToInt("status");
-
-            // 调用Service层获取导出数据
-            List<JxsExportDto> dtos = jxsService.exportJxsList(jxsName, code,status);
-
-            if (CollUtil.isEmpty(dtos)) {
-                renderJson(ApiResponse.error("没有要导出的数据"));
+        // 使用 try-with-resources 确保 InputStream 和 OutputStream 都能被正确关闭
+        try (InputStream inputStream = generateExportInputStream()) {
+            if (inputStream == null) {
                 return;
             }
-            // 使用EasyExcelUtil生成Excel文件流
-            InputStream inputStream = EasyExcelUtil.export(dtos, "经销商列表", JxsExportDto.class);
+
+            // 1. 设置响应头,告诉浏览器这是一个需要下载的文件
             String fileName = URLEncoder.encode("经销商列表.xlsx", "UTF-8");
-            String ossUrl = OssUtil.upload(inputStream, fileName);
-            apiResponse = ApiResponse.success(ossUrl);
+            getResponse().setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+
+            // 2. 设置文件的MIME类型
+            getResponse().setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
+
+            // 3. 手动将文件流写入响应的输出流
+            try (ServletOutputStream sos = getResponse().getOutputStream()) {
+                byte[] buffer = new byte[1024];
+                int len;
+                // 循环读取输入流,并写入到输出流
+                while ((len = inputStream.read(buffer)) != -1) {
+                    sos.write(buffer, 0, len);
+                }
+                sos.flush(); // 确保所有数据都被发送
+            }
+            renderNull();
         } catch (Exception e) {
             logger.error("导出经销商列表异常:", e);
-            renderJson(ApiResponse.error("导出经销商列表失败:" + e.getMessage()));
+            // 确保在异常情况下也返回一个标准的JSON错误
+            if (!getResponse().isCommitted()) {
+                renderJson(ApiResponse.error("导出经销商列表失败:" + e.getMessage()));
+            }
         }
-        renderJson(apiResponse);
+    }
+
+    /**
+     * 下载导入时生成的错误文件
+     */
+    public void downloadErrorFile() {
+        String fileId = getPara("fileId");
+
+        // 安全性检查:防止路径遍历攻击
+        if (fileId == null || fileId.contains("..") || fileId.contains("/")) {
+            renderJson(ApiResponse.error("无效的文件ID"));
+            return;
+        }
+
+        String tempDir = PathKit.getWebRootPath() + "/import_error_files/";
+        File file = new File(tempDir + fileId);
+
+        if (file.exists() && file.isFile()) {
+            // 使用 JFinal 的 renderFile 方法直接将文件流式返回给客户端
+            // 这是 renderFile 最标准的用法
+            renderFile(file);
+        } else {
+            renderJson(ApiResponse.error("文件不存在或已被清理"));
+        }
+    }
+
+
+    /**
+     * 辅助方法:生成导出文件的输入流
+     * @return 如果有数据则返回InputStream,否则返回null并直接渲染错误信息
+     */
+    private InputStream generateExportInputStream() throws Exception {
+        // 获取查询条件
+        String jxsName = getPara("jxsName");
+        String code = getPara("code");
+        Integer status = getParaToInt("status");
+
+        // 调用Service层获取导出数据
+        List<JxsExportDto> dtos = jxsService.exportJxsList(jxsName, code, status);
+
+        if (CollUtil.isEmpty(dtos)) {
+            renderJson(ApiResponse.error("没有要导出的数据"));
+            return null;
+        }
+
+        // 使用EasyExcelUtil生成Excel文件流
+        return EasyExcelUtil.export(dtos, "经销商列表", JxsExportDto.class);
     }
 }

+ 1 - 1
src/main/java/com/qlm/service/IJxsService.java

@@ -41,7 +41,7 @@ public interface IJxsService {
      */
     JxsDto convertRecordToDto(com.jfinal.plugin.activerecord.Record record);
 
-    ApiResponse importJxs(List<JxsImportDto> importList, String username);
+    ApiResponse importJxs(List<JxsImportDto> importList, String username,String contextPath);
 
     List<JxsExportDto> exportJxsList(String jxsName, String code, Integer status);
 }

+ 32 - 9
src/main/java/com/qlm/service/impl/JxsServiceImpl.java

@@ -2,6 +2,7 @@ package com.qlm.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateUtil;
+import com.jfinal.kit.PathKit;
 import com.jfinal.kit.StrKit;
 import com.qlm.common.ApiResponse;
 import com.qlm.common.PageResult;
@@ -16,6 +17,8 @@ import com.qlm.view.core.AdminView;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -272,7 +275,7 @@ public class JxsServiceImpl implements IJxsService {
     }
 
     @Override
-    public ApiResponse importJxs(List<JxsImportDto> importList, String username) {
+    public ApiResponse importJxs(List<JxsImportDto> importList, String username,String contextPath) {
         List<JxsImportDto> errorList = new ArrayList<>();
         int successCount = 0;
         ApiResponse response = ApiResponse.success();
@@ -388,15 +391,35 @@ public class JxsServiceImpl implements IJxsService {
         if (!errorList.isEmpty()) {
             record.set("errorCount", errorList.size());
             try {
-                String errorFileName = "item_import_error_" + System.currentTimeMillis() + ".xlsx";
-                InputStream errorInputStream = EasyExcelUtil.export(errorList, "导入错误数据", JxsImportDto.class);
-                // 上传到OSS
-                String ossUrl = OssUtil.upload(errorInputStream, errorFileName);
-                // 添加到响应
-                record.set("errorFileUrl", ossUrl);
+                // 1. 生成一个唯一的文件名,避免冲突
+                String uniqueFileName = "import_jxs_error" + UUID.randomUUID() + ".xlsx";
+
+                // 2. 定义临时文件存储路径 (例如:/tomcat/webapps/your_project/temp_files/)
+                //    确保这个目录存在并且应用有写入权限
+                String tempDir = PathKit.getWebRootPath() + "/import_error_files/";
+                File dir = new File(tempDir);
+                if (!dir.exists()) {
+                    dir.mkdirs();
+                }
+                File errorFile = new File(tempDir + uniqueFileName);
+
+                // 3. 使用 EasyExcel 生成文件流并写入到本地文件
+                try (InputStream errorInputStream = EasyExcelUtil.export(errorList, "导入错误数据", JxsImportDto.class);
+                     FileOutputStream fos = new FileOutputStream(errorFile)) {
+
+                    byte[] buffer = new byte[1024];
+                    int len;
+                    while ((len = errorInputStream.read(buffer)) != -1) {
+                        fos.write(buffer, 0, len);
+                    }
+                }
+
+                String downloadUrl =contextPath+"/jxsNew/downloadErrorFile?fileId=" + uniqueFileName;
+                record.set("errorFileUrl", downloadUrl);
+
             } catch (Exception e) {
-                logger.error("导出错误数据并上传到OSS异常:", e);
-                record.set("errorMsg", "导出错误数据并上传到OSS异常:" + e.getMessage());
+                logger.error("生成导入错误文件异常:", e);
+                record.set("errorMsg", "生成导入错误文件异常:" + e.getMessage());
             }
         }
         response.setData(record);

+ 108 - 29
src/main/webapp/page/jinzai/jxsNew.jsp

@@ -704,6 +704,7 @@
 
     // 在initTable()函数之后添加以下代码
 
+    // 导出数据功能实现
     // 导出数据功能实现
     function exportData() {
         // 显示加载中提示
@@ -714,44 +715,122 @@
         // 获取查询条件
         const jxsCode = $.trim($("#jxsCode").val());
         const jxsName = $.trim($("#jxsName").val());
+        const status = $("#status").val(); // 假设你有一个ID为status的查询条件
 
         // 构建查询参数
         const params = {
-            jxsCode: jxsCode,
-            jxsName: jxsName
+            jxsName: jxsName,
+            code: jxsCode, // 注意:后端代码里用的是 code, 而不是 jxsCode
+            status: status
         };
 
-        // 发送导出请求
-        $.ajax({
-            url: '${ctx}/jxsNew/exportJxsList',
-            type: 'POST',
-            data: JSON.stringify(params),
-            contentType: 'application/json',
-            success: function (res) {
-                // 关闭加载提示
-                layer.close(loadingIndex);
+        // 使用 fetch API 发送请求,因为它可以处理二进制文件流
+        fetch('${ctx}/jxsNew/exportJxsList', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(params)
+        })
+            .then(response => {
+                // 首先检查响应是否成功
+                if (!response.ok) {
+                    // 如果服务器返回了错误(例如,没有数据),它会是JSON格式
+                    // 我们需要解析这个JSON来获取错误信息
+                    return response.json().then(err => {
+                        throw new Error(err.msg || '导出失败');
+                    });
+                }
 
-                if (res.code === 0) {
-                    // 获取导出URL并打开
-                    let exportUrl = res.data;
-                    if (exportUrl) {
-                        // 使用新窗口打开URL进行下载
-                        window.open(exportUrl, '_blank');
-                        layer.msg('导出成功,请在新窗口中查看下载文件', {icon: 1});
-                    } else {
-                        layer.msg('未获取到有效的导出URL', {icon: 5});
+                // 从响应头中尝试获取文件名
+                const disposition = response.headers.get('Content-Disposition');
+                let fileName = '经销商列表.xlsx'; // 默认文件名
+                if (disposition && disposition.indexOf('attachment') !== -1) {
+                    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+                    const matches = filenameRegex.exec(disposition);
+                    if (matches != null && matches[1]) {
+                        fileName = decodeURI(matches[1].replace(/['"]/g, ''));
                     }
-                } else {
-                    layer.msg(res.msg || '导出失败', {icon: 5});
                 }
-            },
-            error: function () {
-                // 关闭加载提示
+
+                // 将响应体转换为Blob对象(二进制文件)
+                return response.blob().then(blob => ({ blob, fileName }));
+            })
+            .then(({ blob, fileName }) => {
+                // 创建一个指向Blob的URL
+                const url = window.URL.createObjectURL(blob);
+
+                // 创建一个隐藏的<a>标签用于触发下载
+                const a = document.createElement('a');
+                a.style.display = 'none';
+                a.href = url;
+                a.download = fileName; // 设置下载的文件名
+
+                // 将<a>标签添加到页面中,模拟点击,然后移除
+                document.body.appendChild(a);
+                a.click();
+
+                // 清理
+                window.URL.revokeObjectURL(url);
+                document.body.removeChild(a);
+
+                // 关闭加载提示并显示成功信息
                 layer.close(loadingIndex);
-                layer.msg('导出请求失败', {icon: 5});
-            }
-        });
+                layer.msg('导出成功,请检查你的下载内容', { icon: 1 });
+            })
+            .catch(error => {
+                // 捕获任何在过程中发生的错误
+                layer.close(loadingIndex);
+                layer.msg(error.message || '导出请求失败', { icon: 5 });
+            });
     }
+    <%--function exportData() {--%>
+    <%--    // 显示加载中提示--%>
+    <%--    var loadingIndex = layer.load(1, {--%>
+    <%--        shade: [0.2, '#000']--%>
+    <%--    });--%>
+
+    <%--    // 获取查询条件--%>
+    <%--    const jxsCode = $.trim($("#jxsCode").val());--%>
+    <%--    const jxsName = $.trim($("#jxsName").val());--%>
+
+    <%--    // 构建查询参数--%>
+    <%--    const params = {--%>
+    <%--        jxsCode: jxsCode,--%>
+    <%--        jxsName: jxsName--%>
+    <%--    };--%>
+
+    <%--    // 发送导出请求--%>
+    <%--    $.ajax({--%>
+    <%--        url: '${ctx}/jxsNew/exportJxsList',--%>
+    <%--        type: 'POST',--%>
+    <%--        data: JSON.stringify(params),--%>
+    <%--        contentType: 'application/json',--%>
+    <%--        success: function (res) {--%>
+    <%--            // 关闭加载提示--%>
+    <%--            layer.close(loadingIndex);--%>
+
+    <%--            if (res.code === 0) {--%>
+    <%--                // 获取导出URL并打开--%>
+    <%--                let exportUrl = res.data;--%>
+    <%--                if (exportUrl) {--%>
+    <%--                    // 使用新窗口打开URL进行下载--%>
+    <%--                    window.open(exportUrl, '_blank');--%>
+    <%--                    layer.msg('导出成功,请在新窗口中查看下载文件', {icon: 1});--%>
+    <%--                } else {--%>
+    <%--                    layer.msg('未获取到有效的导出URL', {icon: 5});--%>
+    <%--                }--%>
+    <%--            } else {--%>
+    <%--                layer.msg(res.msg || '导出失败', {icon: 5});--%>
+    <%--            }--%>
+    <%--        },--%>
+    <%--        error: function () {--%>
+    <%--            // 关闭加载提示--%>
+    <%--            layer.close(loadingIndex);--%>
+    <%--            layer.msg('导出请求失败', {icon: 5});--%>
+    <%--        }--%>
+    <%--    });--%>
+    <%--}--%>
 
     // 导入功能实现
     function importData() {
@@ -762,7 +841,7 @@
                     <i class="fa fa-cloud-upload" style="font-size: 50px; color: #ccc;"></i>
                 </div>
                 <p style="margin-bottom: 10px;">将文件拖到此处,或 <span style="color: #0066cc; cursor: pointer;" id="clickUpload">点击上传</span></p>
-                <p style="font-size: 12px; color: #999;">仅允许导入xls、xlsx格式文件。<a href="https://hyscancode.oss-cn-hangzhou.aliyuncs.com/jinzaiimport/%E7%BB%8F%E9%94%80%E5%95%86%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx" style="color: #0066cc;">下载模板</a></p>
+                <p style="font-size: 12px; color: #999;">仅允许导入xls、xlsx格式文件。<a href="${ctx}/templates/经销商导入模板.xlsx" style="color: #0066cc;">下载模板</a></p>
                 <div id="selectedFileName" style="margin-top: 15px; font-size: 12px; color: #333; display: none; justify-content: center; align-items: center;"></div>
                 <input type="file" id="fileUpload" accept=".xlsx, .xls" style="display: none;">
             </div>

BIN
src/main/webapp/templates/经销商导入模板.xlsx