吉安感知网项目-后端
linwei
2026-07-10 41bc55bc6ab5e70a12736ae174a248b5d37ec5bd
feat(resource): 集成OnlyOffice文档编辑功能
1 files modified
165 ■■■■ changed files
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java 165 ●●●● patch | view | raw | blame | history
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -547,40 +547,63 @@
        return baseUrl.toString() + "/attach/onlyoffice-download?attachId=" + attachId;
    }
    /**
     * OnlyOffice代理下载接口,用于代理下载远程文件
     * <p>
     * 当OnlyOffice Document Server无法直接访问远程文件URL时,
     * 通过本接口代理下载文件并流式返回给Document Server。
     * </p>
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param attachId 附件ID,用于从缓存中查找对应的远程文件URL
     * @throws IOException 当下载或写入文件流时发生I/O异常
     */
    @GetMapping("/onlyoffice-download")
    @ApiOperation(value = "OnlyOffice代理下载", notes = "代理下载远程文件供OnlyOffice Document Server访问")
    public void onlyofficeDownload(
            HttpServletRequest request,
            HttpServletResponse response,
            @ApiParam(value = "附件ID", required = true) @RequestParam Long attachId) throws IOException {
        HttpServletRequest request,
        HttpServletResponse response,
        @ApiParam(value = "附件ID", required = true) @RequestParam Long attachId) throws IOException {
        // 1. 从缓存中获取远程文件URL
        String remoteUrl = REMOTE_URL_CACHE.get(attachId);
        if (remoteUrl == null || remoteUrl.isEmpty()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "未找到对应的远程文件URL");
        // 处理 OPTIONS 预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
            response.setHeader("Access-Control-Allow-Headers", "*");
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        // 2. 根据附件ID查询附件信息,获取文件名
        Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
        String fileName = (attach != null && attach.getName() != null) ? attach.getName() : "document.docx";
        log.info("ONLYOFFICE 下载请求,attachId: {}", attachId);
        // 3. 从远程URL下载文件
        // 1. 根据附件ID查询附件信息
        Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
        if (attach == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "附件不存在,attachId: " + attachId);
            return;
        }
        // 2. 优先从缓存中获取远程文件URL
        String remoteUrl = REMOTE_URL_CACHE.get(attachId);
        if (remoteUrl == null || remoteUrl.isEmpty()) {
            remoteUrl = attach.getLink();
            log.info("从数据库获取远程URL,attachId: {}, link: {}", attachId, remoteUrl);
        }
        // 3. 验证远程URL
        if (remoteUrl == null || remoteUrl.isEmpty()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "附件缺少远程文件URL,attachId: " + attachId);
            return;
        }
        // 4. 获取纯文件名(不含路径)
        String fileName = attach.getName();
        if (fileName == null || fileName.isEmpty()) {
            fileName = "document.docx";
        }
        // 移除路径部分
        if (fileName.contains("/")) {
            fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
        }
        if (fileName.contains("\\")) {
            fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
        }
        log.info("下载文件名: {}", fileName);
        // 5. 下载远程文件
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            URL url = new URL(remoteUrl);
            connection = (HttpURLConnection) url.openConnection();
@@ -588,42 +611,78 @@
            connection.setConnectTimeout(CONNECT_TIMEOUT);
            connection.setReadTimeout(READ_TIMEOUT);
            // 4. 检查响应状态码
            // 复制请求头(如果需要)
            String userAgent = request.getHeader("User-Agent");
            if (userAgent != null) {
                connection.setRequestProperty("User-Agent", userAgent);
            }
            int responseCode = connection.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                log.error("远程文件下载失败,状态码: {}, url: {}", responseCode, remoteUrl);
                response.sendError(HttpServletResponse.SC_BAD_GATEWAY, "远程文件下载失败,状态码: " + responseCode);
                return;
            }
            // 5. 获取远程文件的Content-Type
            // 6. 设置响应头
            String contentType = connection.getContentType();
            if (contentType == null || contentType.isEmpty()) {
                contentType = "application/octet-stream";
                contentType = getContentTypeByFileName(fileName);
            }
            response.setContentType(contentType);
            // 设置 Content-Length
            int contentLength = connection.getContentLength();
            if (contentLength > 0) {
                response.setContentLength(contentLength);
            }
            // 6. 设置响应头
            response.setContentType(contentType);
            response.setHeader("Content-Disposition", "attachment; filename=\""
                    + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + "\"");
            // 7. 正确设置 Content-Disposition
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
                .replaceAll("\\+", "%20");
            // 7. 流式传输文件内容
            // ONLYOFFICE 需要 inline 方式访问
            response.setHeader("Content-Disposition",
                "inline; filename=\"" + encodedFileName + "\"; " +
                    "filename*=UTF-8''" + encodedFileName);
            // CORS 头
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            // 缓存控制
            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Expires", "0");
            // 8. 流式传输
            inputStream = connection.getInputStream();
            outputStream = response.getOutputStream();
            byte[] buffer = new byte[8192];
            int bytesRead;
            long totalBytes = 0;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
            }
            outputStream.flush();
            log.info("下载完成,文件大小: {} bytes", totalBytes);
        } catch (Exception e) {
            log.error("代理下载远程文件失败,attachId: {}, remoteUrl: {}, 错误: {}", attachId, remoteUrl, e.getMessage(), e);
            log.error("代理下载远程文件失败,attachId: {}, remoteUrl: {}, 错误: {}",
                attachId, remoteUrl, e.getMessage(), e);
            if (!response.isCommitted()) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "代理下载远程文件失败: " + e.getMessage());
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "代理下载远程文件失败: " + e.getMessage());
            }
        } finally {
            // 8. 关闭资源
            if (inputStream != null) {
                try { inputStream.close(); } catch (IOException ignored) {}
            }
            if (outputStream != null) {
                try { outputStream.close(); } catch (IOException ignored) {}
            }
            if (connection != null) {
                connection.disconnect();
@@ -631,4 +690,42 @@
        }
    }
    /**
     * 根据文件名获取 Content-Type
     */
    private String getContentTypeByFileName(String fileName) {
        if (fileName == null) return "application/octet-stream";
        String extension = "";
        int lastDot = fileName.lastIndexOf(".");
        if (lastDot > 0) {
            extension = fileName.substring(lastDot + 1).toLowerCase();
        }
        switch (extension) {
            case "docx":
                return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            case "doc":
                return "application/msword";
            case "xlsx":
                return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            case "xls":
                return "application/vnd.ms-excel";
            case "pptx":
                return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
            case "ppt":
                return "application/vnd.ms-powerpoint";
            case "pdf":
                return "application/pdf";
            case "txt":
                return "text/plain";
            case "csv":
                return "text/csv";
            case "rtf":
                return "application/rtf";
            default:
                return "application/octet-stream";
        }
    }
}