From 62091bb640608f24812e069947928c49b4ebdedf Mon Sep 17 00:00:00 2001
From: linwei <872216696@qq.com>
Date: Sat, 11 Jul 2026 10:39:44 +0800
Subject: [PATCH] feat(resource): 集成OnlyOffice文档编辑功能

---
 drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java |  816 ++++++++++++++++++++++++++++++++++++++++++---------------
 1 files changed, 594 insertions(+), 222 deletions(-)

diff --git a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
index f12ed7f..80ee579 100644
--- a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -9,48 +9,48 @@
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
-import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springblade.core.boot.ctrl.BladeController;
-import org.springblade.core.cache.utils.CacheUtil;
-import org.springblade.core.log.annotation.ApiLog;
 import org.springblade.core.mp.support.Condition;
 import org.springblade.core.mp.support.Query;
-import org.springblade.core.secure.annotation.PreAuth;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.utils.AuthUtil;
 import org.springblade.core.tenant.annotation.NonDS;
 import org.springblade.core.tool.api.R;
-import org.springblade.core.tool.constant.RoleConstant;
+import org.springblade.core.tool.utils.BeanUtil;
 import org.springblade.core.tool.utils.DateTimeUtil;
 import org.springblade.core.tool.utils.Func;
-import org.springframework.util.CollectionUtils;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
-import org.sxkj.common.constant.CommonConstant;
 import org.sxkj.common.model.ResponseResult;
-import org.sxkj.common.model.TimeRange;
-import org.sxkj.common.utils.HeaderUtils;
-import org.sxkj.common.utils.TimeRangeUtils;
-import org.sxkj.resource.dto.WaylineJobInfoQueryDto;
-import org.sxkj.resource.dto.WaylineJobInfoQueryParam;
+import org.sxkj.resource.dto.AttachDto;
 import org.sxkj.resource.entity.Attach;
-import org.sxkj.resource.feign.IAttachClient;
+import org.sxkj.resource.param.AttachPageParam;
+import org.sxkj.resource.param.AttachParam;
 import org.sxkj.resource.service.IAttachService;
-import org.sxkj.resource.vo.*;
-import org.sxkj.system.vo.TreeVo;
+import org.sxkj.resource.util.OnlyofficeJwt;
+import org.sxkj.resource.vo.AttachVO;
 
 import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Objects;
-
-import static org.springblade.core.cache.constant.CacheConstant.RESOURCE_CACHE;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * 附件表 控制器
@@ -60,14 +60,30 @@
 @Slf4j
 @NonDS
 @RestController
-@AllArgsConstructor
+@RequiredArgsConstructor
 @RequestMapping("/attach")
 @Api(value = "附件表接口", tags = "附件")
 public class AttachController extends BladeController {
 
 	private final IAttachService attachService;
+	private final OnlyofficeJwt onlyofficeJwt;
 
-	private final IAttachClient attachClient;
+	/** OnlyOffice应用服务基础URL,供Document Server访问代理下载接口 */
+	@Value("${onlyoffice.base-url:}")
+	private String onlyofficeBaseUrl;
+
+	/** OnlyOffice回调地址 */
+	@Value("${onlyoffice.callback-url:}")
+	private String onlyofficeCallbackUrl;
+
+	/** 远程文件URL缓存,key为attachId,value为远程文件URL */
+	private static final ConcurrentHashMap<Long, String> REMOTE_URL_CACHE = new ConcurrentHashMap<>();
+
+	/** 代理下载连接超时时间(毫秒) */
+	private static final int CONNECT_TIMEOUT = 10000;
+
+	/** 代理下载读取超时时间(毫秒) */
+	private static final int READ_TIMEOUT = 30000;
 
 	/**
 	 * 详情
@@ -75,35 +91,9 @@
 	@GetMapping("/detail")
 	@ApiOperationSupport(order = 1)
 	@ApiOperation(value = "附件详情", notes = "传入attach")
-	public R<Attach> detail(Attach attach) {
-		Attach detail = attachService.getOne(Condition.getQueryWrapper(attach));
+	public R<Attach> detail(AttachParam attach) {
+		Attach detail = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attach.getId()));
 		return R.data(detail);
-	}
-
-	@GetMapping("/test")
-	public R test(@RequestParam(required = true) Date startTime, @RequestParam(required = true) Date endTime) {
-		CacheUtil.clear(RESOURCE_CACHE);
-		attachClient.processingYesterdayVideo(startTime, endTime);
-		return R.success("成功");
-	}
-
-	@GetMapping("/getlotInfoList")
-	public ResponseResult getlotInfoList(@ApiParam Long lotInfoId) {
-		return ResponseResult.success(attachService.getlotInfoList(lotInfoId));
-	}
-
-	/**
-	 * 分页 附件表
-	 */
-	@GetMapping("/list")
-	@ApiOperationSupport(order = 2)
-	@ApiOperation(value = "附件分页", notes = "传入attach")
-	public R<IPage<Attach>> list(Attach attach, Query query) {
-		List<String> wayLineJobIds = attach.getWayLineJobIds();
-		QueryWrapper<Attach> queryWrapper1 = Condition.getQueryWrapper(attach);
-		QueryWrapper<Attach> queryWrapper = queryWrapper1.in(!CollectionUtils.isEmpty(wayLineJobIds), "wayline_job_id", wayLineJobIds);
-		IPage<Attach> pages = attachService.page(Condition.getPage(query), queryWrapper);
-		return R.data(pages);
 	}
 
 	/**
@@ -112,89 +102,10 @@
 	@GetMapping("/page")
 	@ApiOperationSupport(order = 3)
 	@ApiOperation(value = "附件分页", notes = "传入attach")
-	public R<IPage<AttachVO>> page(AttachVO attach, Query query) {
-		String areaCode = HeaderUtils.getAreaCode();
-		attach.setAreaCode(areaCode);
+	public R<IPage<AttachVO>> page(AttachPageParam attach, Query query) {
+		BladeUser user = AuthUtil.getUser();
 		IPage<AttachVO> pages = attachService.selectAttachPage(Condition.getPage(query), attach);
-		AttachVO.settingNickName(pages.getRecords());
 		return R.data(pages);
-	}
-
-	/**
-	 * 自定义分页 附件表
-	 */
-	@PostMapping("/aiImagesPage")
-	@ApiOperationSupport(order = 3)
-	@ApiOperation(value = "自定义分页附件表分页", notes = "ai分页数据")
-	public R<IPage<AttachVO>> aiPage(@RequestBody AttachVO attach, Query query) {
-		String areaCode = HeaderUtils.getAreaCode();
-		attach.setAreaCode(areaCode);
-		List<Integer> resultTypes = attach.getResultTypes();
-		String wordOrderType = attach.getWordOrderType();
-		settingResultType(attach, wordOrderType, resultTypes);
-		if (Objects.nonNull(attach.getAiStatus()) && Objects.nonNull(attach.getResultTypes())
-			&& attach.getResultTypes().contains(Attach.RESULT_TYPE_AI)) {
-			attach.setResultTypes(Arrays.asList(Attach.RESULT_TYPE_AI));
-		}
-		List<String> aiEventMd5s = attachService.findAiEventMd5s(attach.getEventRecordIds());
-		attach.setMd5s(aiEventMd5s);
-		if (!CollectionUtils.isEmpty(attach.getEventRecordIds())) {
-			attach.setResultTypes(Arrays.asList(Attach.RESULT_TYPE_AI, Attach.RESULT_TYPE_IMG));
-		}
-		IPage page = Condition.getPage(query);
-		List<AttachVO> aiAttachImages = attachService.findAiAttachImages(page, attach);
-		AttachVO.settingNickName(aiAttachImages);
-		page.setRecords(aiAttachImages);
-		return R.data(page);
-	}
-
-	/**
-	 * 后台数据中心列表接口
-	 */
-	@PostMapping("/attachmentsPage")
-	@ApiOperationSupport(order = 4)
-	@ApiOperation(value = "后台数据中心列表接口")
-	public R<IPage<AttachVO>> attachmentsPage(@RequestBody AttachVO attach, Query query) {
-		IPage page = Condition.getPage(query);
-		IPage attachPage = attachService.findAttachImages(page, attach);
-		return R.data(attachPage);
-	}
-
-
-	private static void settingResultType(AttachVO attach, String wordOrderType, List<Integer> resultTypes) {
-		if (!StringUtils.isEmpty(wordOrderType) && !CollectionUtils.isEmpty(resultTypes)) {
-			if ("-1".equals(wordOrderType)) {
-				attach.setWordOrderType(null);
-				if (resultTypes.contains(Attach.RESULT_TYPE_IMG)) {
-					resultTypes = Arrays.asList(Attach.RESULT_TYPE_IMG);
-				}
-			} else {
-				if (resultTypes.contains(Attach.RESULT_TYPE_AI)) {
-					resultTypes = Arrays.asList(Attach.RESULT_TYPE_AI);
-				}
-			}
-			attach.setResultTypes(resultTypes);
-		}
-	}
-
-	/**
-	 * 新增 附件表
-	 */
-	@PostMapping("/save")
-	@ApiOperationSupport(order = 4)
-	@ApiOperation(value = "新增", notes = "传入attach")
-	public R save(@Valid @RequestBody Attach attach) {
-		return R.status(attachService.save(attach));
-	}
-
-	/**
-	 * 修改 附件表
-	 */
-	@PostMapping("/update")
-	@ApiOperationSupport(order = 5)
-	@ApiOperation(value = "修改", notes = "传入attach")
-	public R update(@Valid @RequestBody Attach attach) {
-		return R.status(attachService.updateById(attach));
 	}
 
 	/**
@@ -203,7 +114,8 @@
 	@PostMapping("/submit")
 	@ApiOperationSupport(order = 6)
 	@ApiOperation(value = "新增或修改", notes = "传入attach")
-	public R submit(@Valid @RequestBody Attach attach) {
+	public R submit(@Valid @RequestBody AttachDto attachDto) {
+		Attach attach = Objects.requireNonNull(BeanUtil.copy(attachDto, Attach.class));
 		return R.status(attachService.saveOrUpdate(attach));
 	}
 
@@ -220,70 +132,205 @@
 			.in(Attach::getId, Func.toLongList(ids))));
 	}
 
+	// @GetMapping("/test")
+	// public R test(@RequestParam(required = true) Date startTime, @RequestParam(required = true) Date endTime) {
+	// 	CacheUtil.clear(RESOURCE_CACHE);
+	// 	attachClient.processingYesterdayVideo(startTime, endTime);
+	// 	return R.success("成功");
+	// }
+
+	// @GetMapping("/getlotInfoList")
+	// public ResponseResult getlotInfoList(@ApiParam Long lotInfoId) {
+	// 	return ResponseResult.success(attachService.getlotInfoList(lotInfoId));
+	// }
+
+	/**
+	 * 分页 附件表
+	 */
+	 @GetMapping("/list")
+	 @ApiOperationSupport(order = 2)
+	 @ApiOperation(value = "附件分页", notes = "传入attach")
+	 public R<IPage<Attach>> list(Attach attach, Query query) {
+		 String patrolTaskId = attach.getPatrolTaskId();
+		 QueryWrapper<Attach> queryWrapper1 = Condition.getQueryWrapper(attach);
+	 	QueryWrapper<Attach> queryWrapper = queryWrapper1.eq(!StringUtils.isEmpty(patrolTaskId), "patrol_task_id", patrolTaskId);
+	 	queryWrapper.eq("is_deleted", 0);
+	 	IPage<Attach> pages = attachService.page(Condition.getPage(query), queryWrapper);
+	 	return R.data(pages);
+	 }
+
+	/**
+	 * 自定义分页 附件表
+	 */
+	// @PostMapping("/aiImagesPage")
+	// @ApiOperationSupport(order = 3)
+	// @ApiOperation(value = "自定义分页附件表分页", notes = "ai分页数据")
+	// public R<IPage<AttachVO>> aiPage(@RequestBody AttachVO attach, Query query) {
+	// 	String areaCode = HeaderUtils.getAreaCode();
+	// 	attach.setAreaCode(areaCode);
+	// 	List<Integer> resultTypes = attach.getResultTypes();
+	// 	String wordOrderType = attach.getWordOrderType();
+	// 	settingResultType(attach, wordOrderType, resultTypes);
+	// 	if (Objects.nonNull(attach.getAiStatus()) && Objects.nonNull(attach.getResultTypes())
+	// 		&& attach.getResultTypes().contains(Attach.RESULT_TYPE_AI)) {
+	// 		attach.setResultTypes(Arrays.asList(Attach.RESULT_TYPE_AI));
+	// 	}
+	// 	List<String> aiEventMd5s = attachService.findAiEventMd5s(attach.getEventRecordIds());
+	// 	attach.setMd5s(aiEventMd5s);
+	// 	if (!CollectionUtils.isEmpty(attach.getEventRecordIds())) {
+	// 		attach.setResultTypes(Arrays.asList(Attach.RESULT_TYPE_AI, Attach.RESULT_TYPE_IMG));
+	// 	}
+	// 	IPage page = Condition.getPage(query);
+	// 	List<AttachVO> aiAttachImages = attachService.findAiAttachImages(page, attach);
+	// 	AttachVO.settingNickName(aiAttachImages);
+	// 	page.setRecords(aiAttachImages);
+	// 	return R.data(page);
+	// }
+
+	/**
+	 * 后台数据中心列表接口
+	 */
+	// @PostMapping("/attachmentsPage")
+	// @ApiOperationSupport(order = 4)
+	// @ApiOperation(value = "后台数据中心列表接口")
+	// public R<IPage<AttachVO>> attachmentsPage(@RequestBody AttachVO attach, Query query) {
+	// 	IPage page = Condition.getPage(query);
+	// 	IPage attachPage = attachService.findAttachImages(page, attach);
+	// 	return R.data(attachPage);
+	// }
+
+
+
+	/**
+	 * 新增 附件表
+	 */
+	 @PostMapping("/save")
+	 @ApiOperationSupport(order = 4)
+	 @ApiOperation(value = "新增", notes = "传入attach")
+	 public R save(@Valid @RequestBody Attach attach) {
+	 	return R.status(attachService.save(attach));
+	 }
+
+	/**
+	 * 修改 附件表
+	 */
+	 @PostMapping("/update")
+	 @ApiOperationSupport(order = 5)
+	 @ApiOperation(value = "修改", notes = "传入attach")
+	 public R update(@Valid @RequestBody Attach attach) {
+	 	return R.status(attachService.updateById(attach));
+	 }
+
+	/**
+	 * 流式附件下载接口,支持下载zip压缩包,不使用本地存储
+	 * @param response
+	 * @throws IOException
+	 */
+	@ApiOperation(value = "流式附件下载接口", notes = "使用流方式返回数据,不使用本地存储")
+	@GetMapping("/downloadByByte")
+	public void downloadByByte(
+		@ApiParam(value = "附件下载参数") @RequestParam("attachIds") String attachIds, HttpServletResponse response) throws IOException {
+
+		// 设置文件名
+		String timestamp = DateTimeUtil.format(LocalDateTime.now(), "yyyyMMdd_HHmmss");
+		String fileName = "attachments_" + timestamp + ".zip";
+
+		// 设置响应头
+		response.setContentType("application/zip");
+		response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));
+
+		// 获取响应输出流
+		ServletOutputStream outputStream = null;
+		try {
+			outputStream = response.getOutputStream();
+
+			Boolean result = attachService.downloadByByte(attachIds, outputStream);
+			if (!result) {
+				return;
+			}
+		} catch (Exception e) {
+			if (!response.isCommitted()) {
+				response.reset();
+				response.setContentType("application/json;charset=UTF-8");
+				ResponseResult errorResult = ResponseResult.error("下载文件失败: " + e.getMessage());
+
+				// 使用输出流写入错误信息,避免getWriter()冲突
+				try {
+					byte[] errorBytes = JSON.toJSONString(errorResult).getBytes(StandardCharsets.UTF_8);
+					outputStream.write(errorBytes);
+				} catch (IOException ioException) {
+					log.error("Error writing error response: {}", ioException.getMessage());
+				}
+			} else {
+				log.error("Cannot send error response: response already committed. Exception: {}", e.getMessage());
+			}
+		}
+	}
+
 	/**
 	 * 删除 附件表及对应的附件信息
 	 *
 	 * @param ids
 	 * @return
 	 */
-	@PostMapping("/removeAttachAndData")
-	@ApiOperationSupport(order = 9)
-	@ApiOperation(value = "删除", notes = "传入ids")
-	@PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
-	public R removeAttachAndData(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) {
-		return R.status(attachService.removeBatchAndDataByIds(Func.toLongList(ids)));
-	}
+	// @PostMapping("/removeAttachAndData")
+	// @ApiOperationSupport(order = 9)
+	// @ApiOperation(value = "删除", notes = "传入ids")
+	// @PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
+	// public R removeAttachAndData(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) {
+	// 	return R.status(attachService.removeBatchAndDataByIds(Func.toLongList(ids)));
+	// }
 
-	@GetMapping("/deptSize")
-	@ApiOperationSupport(order = 8)
-	@ApiOperation(value = "获取部门文件大小")
-	public R<List<DeptAndSizeVo>> getDeptSize(@RequestParam String startTime, @RequestParam String endTime) {
-		return R.data(attachService.getDeptAllSize(startTime, endTime));
-	}
+	// @GetMapping("/deptSize")
+	// @ApiOperationSupport(order = 8)
+	// @ApiOperation(value = "获取部门文件大小")
+	// public R<List<DeptAndSizeVo>> getDeptSize(@RequestParam String startTime, @RequestParam String endTime) {
+	// 	return R.data(attachService.getDeptAllSize(startTime, endTime));
+	// }
 
 	/**
 	 * 历史正射图片处理
 	 *
 	 * @return
 	 */
-	@GetMapping("/hisOrtPicHandler")
-	@ApiOperationSupport(order = 9)
-	@ApiOperation(value = "历史正射图片处理")
-	public R hisOrtPicHandler() {
-		return R.data(attachService.hisOrtPicHandler());
-	}
+	// @GetMapping("/hisOrtPicHandler")
+	// @ApiOperationSupport(order = 9)
+	// @ApiOperation(value = "历史正射图片处理")
+	// public R hisOrtPicHandler() {
+	// 	return R.data(attachService.hisOrtPicHandler());
+	// }
 
 	/**
 	 * 自动更新附件表md5值
 	 *
 	 * @return
 	 */
-	@GetMapping("saveMd5")
-	public R saveMd5() {
-		attachService.saveMd5();
-		return R.success("");
-	}
+	// @GetMapping("saveMd5")
+	// public R saveMd5() {
+	// 	attachService.saveMd5();
+	// 	return R.success("");
+	// }
 
 
-	@ApiOperation(value = "地图位置上图片数据", notes = "")
-	@PostMapping("/mapAttachs")
-	public ResponseResult<TreeVo> mapEvents(@RequestBody AttachQueryParam dto) {
-		if (Objects.nonNull(dto.getDateEnum())) {
-			TimeRange timeRange = TimeRangeUtils.getTimeRange(dto.getDateEnum());
-			dto.setStartDate(timeRange.getStartTime());
-			dto.setEndDate(timeRange.getEndTime());
-		}
-		if (Objects.nonNull(dto.getStartDate())) {
-			dto.setStartTime(TimeRangeUtils.getEpochMilli(dto.getStartDate()));
-		}
-		if (Objects.nonNull(dto.getEndDate())) {
-			dto.setEndTime(TimeRangeUtils.getEpochMilli(dto.getEndDate()));
-		}
-		String areaCode = HeaderUtils.getHeader(CommonConstant.AREA_CODE).orElse(null);
-		dto.setAreaCode(!StringUtils.isEmpty(dto.getAreaCode()) ? dto.getAreaCode() : areaCode);
-		TreeVo treeVo = attachService.mapAttachEvents(dto);
-		return ResponseResult.success(treeVo);
-	}
+	// @ApiOperation(value = "地图位置上图片数据", notes = "")
+	// @PostMapping("/mapAttachs")
+	// public ResponseResult<TreeVo> mapEvents(@RequestBody AttachQueryParam dto) {
+	// 	if (Objects.nonNull(dto.getDateEnum())) {
+	// 		TimeRange timeRange = TimeRangeUtils.getTimeRange(dto.getDateEnum());
+	// 		dto.setStartDate(timeRange.getStartTime());
+	// 		dto.setEndDate(timeRange.getEndTime());
+	// 	}
+	// 	if (Objects.nonNull(dto.getStartDate())) {
+	// 		dto.setStartTime(TimeRangeUtils.getEpochMilli(dto.getStartDate()));
+	// 	}
+	// 	if (Objects.nonNull(dto.getEndDate())) {
+	// 		dto.setEndTime(TimeRangeUtils.getEpochMilli(dto.getEndDate()));
+	// 	}
+	// 	String areaCode = HeaderUtils.getHeader(CommonConstant.AREA_CODE).orElse(null);
+	// 	dto.setAreaCode(!StringUtils.isEmpty(dto.getAreaCode()) ? dto.getAreaCode() : areaCode);
+	// 	TreeVo treeVo = attachService.mapAttachEvents(dto);
+	// 	return ResponseResult.success(treeVo);
+	// }
 
 	/**
 	 * 图片分析对比 前后图片对比
@@ -291,33 +338,33 @@
 	 * @param id 图片id
 	 * @return
 	 */
-	@GetMapping("contrastiveAnalysis")
-	public ResponseResult contrastiveAnalysis(@RequestParam() Long id) {
-		List<AnalysisAttachVo> list = attachService.contrastiveAnalysis(id);
-		return ResponseResult.success(list);
-	}
+	// @GetMapping("contrastiveAnalysis")
+	// public ResponseResult contrastiveAnalysis(@RequestParam() Long id) {
+	// 	List<AnalysisAttachVo> list = attachService.contrastiveAnalysis(id);
+	// 	return ResponseResult.success(list);
+	// }
 
-	@ApiOperation(value = "附件详情", notes = "")
-	@GetMapping("/getAttachInfo")
-	public ResponseResult<AttachInfoVO> getAttachInfo(@ApiParam(value = "附件iD") @RequestParam("id") Long id) throws IOException {
-		return ResponseResult.success(attachService.getAttachInfo(id));
-	}
+	// @ApiOperation(value = "附件详情", notes = "")
+	// @GetMapping("/getAttachInfo")
+	// public ResponseResult<AttachInfoVO> getAttachInfo(@ApiParam(value = "附件iD") @RequestParam("id") Long id) throws IOException {
+	// 	return ResponseResult.success(attachService.getAttachInfo(id));
+	// }
 
-	@ApiOperation(value = "通过任务id查询附件详情集合", notes = "")
-	@GetMapping("/getAttachInfoByJobId")
-	public ResponseResult<List<AttachInfoVO>> getAttachInfoByJobId(@ApiParam(value = "任务id") @RequestParam("jobId") String jobId) throws IOException {
-		return ResponseResult.success(attachService.getAttachInfoByJobId(jobId));
-	}
+	// @ApiOperation(value = "通过任务id查询附件详情集合", notes = "")
+	// @GetMapping("/getAttachInfoByJobId")
+	// public ResponseResult<List<AttachInfoVO>> getAttachInfoByJobId(@ApiParam(value = "任务id") @RequestParam("jobId") String jobId) throws IOException {
+	// 	return ResponseResult.success(attachService.getAttachInfoByJobId(jobId));
+	// }
 
 
-	@ApiLog("修改文件名称")
-	@PostMapping("/updateFileName")
-	public ResponseResult updateFileName(@RequestParam Long id, @RequestParam String nickName) {
-		if (attachService.updateFileName(id, nickName) > 0) {
-			return ResponseResult.success();
-		}
-		return ResponseResult.error("修改失败");
-	}
+	// @ApiLog("修改文件名称")
+	// @PostMapping("/updateFileName")
+	// public ResponseResult updateFileName(@RequestParam Long id, @RequestParam String nickName) {
+	// 	if (attachService.updateFileName(id, nickName) > 0) {
+	// 		return ResponseResult.success();
+	// 	}
+	// 	return ResponseResult.error("修改失败");
+	// }
 
 
 	/**
@@ -325,39 +372,364 @@
 	 *
 	 * @return
 	 */
-	@GetMapping("deletedNotExistsUrl")
-	public ResponseResult deletedNotExistsUrl() {
-		attachService.deletedNotExistsUrl();
-		return ResponseResult.success();
-	}
+	// @GetMapping("deletedNotExistsUrl")
+	// public ResponseResult deletedNotExistsUrl() {
+	// 	attachService.deletedNotExistsUrl();
+	// 	return ResponseResult.success();
+	// }
 
-	@ApiLog("删除图片视频")
-	@DeleteMapping("/deleteMediaFile")
-	public ResponseResult deleteMediaFile(@RequestBody List<Long> ids) {
-		attachService.deleteMediaFile(ids);
-		return ResponseResult.success();
-	}
+	// @ApiLog("删除图片视频")
+	// @DeleteMapping("/deleteMediaFile")
+	// public ResponseResult deleteMediaFile(@RequestBody List<Long> ids) {
+	// 	attachService.deleteMediaFile(ids);
+	// 	return ResponseResult.success();
+	// }
 
 	/**
 	 * 附件统计
 	 */
-	@ApiOperation(value = "附件统计", notes = "")
-	@PostMapping("/attachTypeStatistics")
-	public ResponseResult<List<AttachTypeStatisticsVO>> attachTypeStatistics(@RequestBody AttachStatisticsVo attachStatisticsVo) {
-		List<AttachTypeStatisticsVO> result = attachService.attachTypeStatistics(attachStatisticsVo);
-		return ResponseResult.success(result);
-	}
+	// @ApiOperation(value = "附件统计", notes = "")
+	// @PostMapping("/attachTypeStatistics")
+	// public ResponseResult<List<AttachTypeStatisticsVO>> attachTypeStatistics(@RequestBody AttachStatisticsVo attachStatisticsVo) {
+	// 	List<AttachTypeStatisticsVO> result = attachService.attachTypeStatistics(attachStatisticsVo);
+	// 	return ResponseResult.success(result);
+	// }
 
 	/**
 	 * 个人中心附件统计
 	 */
-	@ApiOperation(value = "个人中心附件统计", notes = "")
-	@PostMapping("/getManageAttachTypeStatistics")
-	public ResponseResult<List<LineColumnDateVo>> getManageAttachTypeStatistics(@RequestBody WaylineJobInfoQueryDto dto) {
-		WaylineJobInfoQueryParam param = new WaylineJobInfoQueryParam(dto);
-		param.setFormatTime(ChartDataVo.getTimeFormatByEnum(param.getDateEnum()));
-		List<LineColumnDateVo> result = attachService.getManageAttachTypeStatistics(param);
-		return ResponseResult.success(result);
+	// @ApiOperation(value = "个人中心附件统计", notes = "")
+	// @PostMapping("/getManageAttachTypeStatistics")
+	// public ResponseResult<List<LineColumnDateVo>> getManageAttachTypeStatistics(@RequestBody WaylineJobInfoQueryDto dto) {
+	// 	WaylineJobInfoQueryParam param = new WaylineJobInfoQueryParam(dto);
+	// 	param.setFormatTime(ChartDataVo.getTimeFormatByEnum(param.getDateEnum()));
+	// 	List<LineColumnDateVo> result = attachService.getManageAttachTypeStatistics(param);
+	// 	return ResponseResult.success(result);
+	// }
+
+	/**
+	 * 获取OnlyOffice文档编辑器配置信息
+	 * <p>
+	 * 通过代理下载方式解决OnlyOffice Document Server无法直接访问远程文件URL的问题。
+	 * 将远程文件URL缓存到本地,并生成代理下载URL供Document Server使用。
+	 * </p>
+	 *
+	 * @param request  HTTP请求对象,用于构建代理下载URL
+	 * @param attachId 附件ID,用于查询附件信息
+	 * @return 返回包含用户信息和附件信息的OnlyOffice配置数据
+	 */
+	@GetMapping("/getOnlyOfficeConfig")
+	@ApiOperationSupport(order = 10)
+	@ApiOperation(value = "获取OnlyOffice配置", notes = "根据附件ID查询附件信息,返回包含当前用户信息和附件信息的OnlyOffice文档编辑器配置")
+	public R<Map<String, Object>> getOnlyOfficeConfig(
+		HttpServletRequest request,
+		@ApiParam(value = "附件ID", required = true) @RequestParam Long attachId,
+		@RequestParam(value = "mode", required = false) String mode) {
+		// 1. 获取当前登录用户信息
+		BladeUser user = AuthUtil.getUser();
+
+		// 2. 根据附件ID查询附件信息
+		Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
+		if (attach == null) {
+			return R.fail("附件不存在");
+		}
+
+		// 3. 构建用户信息对象
+		Map<String, Object> userConfig = new HashMap<>();
+		userConfig.put("name", user != null ? user.getUserName() : "未知用户");
+		userConfig.put("id", user != null ? user.getUserId() : "0");
+
+		// 4. 构建编辑器自定义配置
+		Map<String, Object> customization = new HashMap<>();
+		customization.put("autosave", true);
+		customization.put("forcesave", true);
+
+		// 5. 构建编辑器配置
+		Map<String, Object> editorConfig = new HashMap<>();
+		editorConfig.put("customization", customization);
+		editorConfig.put("mode", StringUtil.isBlank(mode) ? "view" : mode);
+		editorConfig.put("callbackUrl", onlyofficeCallbackUrl);
+		editorConfig.put("lang", "zh-CN");
+		editorConfig.put("user", userConfig);
+
+		// 6. 从附件对象获取文件信息
+		String fileName = attach.getName(); // 附件名称
+		String fileUrl = attach.getLink(); // 附件URL地址(远程URL)
+		String fileType = ""; // 文件类型(扩展名)
+
+		// 7. 从文件名中提取文件类型
+		if (fileName != null && fileName.contains(".")) {
+			fileType = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+		} else if (fileUrl != null && fileUrl.contains(".")) {
+			fileType = fileUrl.substring(fileUrl.lastIndexOf(".") + 1).toLowerCase();
+		}
+
+		// 8. 根据文件类型确定文档类型
+		String documentType = "word"; // 默认为word类型
+		if (fileType.equals("xlsx") || fileType.equals("xls")) {
+			documentType = "cell";
+		} else if (fileType.equals("pptx") || fileType.equals("ppt")) {
+			documentType = "slide";
+		}
+
+		// 9. 将远程文件URL缓存到本地,供代理下载接口使用
+		if (fileUrl != null && !fileUrl.isEmpty()) {
+			REMOTE_URL_CACHE.put(attachId, fileUrl);
+		}
+
+		// 10. 构建代理下载URL,替代直接使用远程URL
+		// Document Server通过代理下载接口获取文件,避免无法直接访问远程URL的问题
+		String documentUrl;
+		// String documentUrl = buildProxyDownloadUrl(request, attachId);
+		if (onlyofficeBaseUrl != null && !onlyofficeBaseUrl.isEmpty()) {
+			// 使用代理下载URL,Document Server通过本服务的代理接口获取文件
+			documentUrl = onlyofficeBaseUrl + "/attach/onlyoffice-download?attachId=" + attachId;
+		} else {
+			// 未配置baseUrl时回退到直接使用远程URL
+			documentUrl = fileUrl != null ? fileUrl : "";
+		}
+
+		// 11. 生成文档唯一标识
+		String key = generateRevisionId(fileName);
+
+		// 12. 构建文档配置,使用代理下载URL替代远程URL
+		Map<String, Object> document = new HashMap<>();
+		document.put("title", fileName != null ? fileName : "未命名文档");
+		document.put("url", documentUrl);
+		document.put("fileType", fileType);
+		document.put("key", attach.getMd5());
+
+		// 13. 构建最终返回结果
+		Map<String, Object> result = new HashMap<>();
+		result.put("documentType", documentType);
+		result.put("document", document);
+		result.put("editorConfig", editorConfig);
+
+		// 14. 生成 JWT token
+		Map<String, Object> result1 = onlyofficeJwt.getToken(result);
+		log.info("ONLYOFFICE配置生成完成,attachId: {}, documentUrl: {}", attachId, documentUrl);
+		return R.data(result1);
+	}
+
+
+	public static final Integer MAX_KEY_LENGTH = 20;
+	public static final Integer ANONYMOUS_USER_ID = 4;
+	public static final Integer KILOBYTE_SIZE = 1024;
+	public String generateRevisionId(final String expectedKey) {
+        /* if the expected key length is greater than 20
+        then he expected key is hashed and a fixed length value is stored in the string format */
+		String formatKey = expectedKey.length() > MAX_KEY_LENGTH
+			? Integer.toString(expectedKey.hashCode()) : expectedKey;
+		String key = formatKey.replace("[^0-9-.a-zA-Z_=]", "_");
+
+		return key.substring(0, Math.min(key.length(), MAX_KEY_LENGTH));  // the resulting key length is 20 or less
+	}
+
+	/**
+	 * 构建当前服务器的代理下载URL
+	 *
+	 * @param request  HTTP请求对象,用于获取服务器地址
+	 * @param attachId 附件ID
+	 * @return 代理下载URL地址
+	 */
+	private String buildProxyDownloadUrl(HttpServletRequest request, Long attachId) {
+		// 1. 获取请求的协议、主机名和端口
+		String scheme = request.getScheme();
+		String serverName = request.getServerName();
+		int serverPort = request.getServerPort();
+
+		// 2. 构建服务器基础URL(省略默认端口)
+		StringBuilder baseUrl = new StringBuilder();
+		baseUrl.append(scheme).append("://").append(serverName);
+		if (("http".equals(scheme) && serverPort != 80) || ("https".equals(scheme) && serverPort != 443)) {
+			baseUrl.append(":").append(serverPort);
+		}
+
+		// baseUrl.append("http:192.168.1.33:80/blade-resource");
+
+		// 3. 拼接代理下载URL
+		return baseUrl.toString() + "/attach/onlyoffice-download?attachId=" + attachId;
+	}
+
+	@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 {
+
+		// 处理 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;
+		}
+
+		log.info("ONLYOFFICE 下载请求,attachId: {}", attachId);
+
+		// 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();
+			connection.setRequestMethod("GET");
+			connection.setConnectTimeout(CONNECT_TIMEOUT);
+			connection.setReadTimeout(READ_TIMEOUT);
+
+			// 复制请求头(如果需要)
+			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;
+			}
+
+			// 6. 设置响应头
+			String contentType = connection.getContentType();
+			if (contentType == null || contentType.isEmpty()) {
+				contentType = getContentTypeByFileName(fileName);
+			}
+			response.setContentType(contentType);
+
+			// 设置 Content-Length
+			int contentLength = connection.getContentLength();
+			if (contentLength > 0) {
+				response.setContentLength(contentLength);
+			}
+
+			// 7. 正确设置 Content-Disposition
+			String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
+				.replaceAll("\\+", "%20");
+
+			// 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);
+			if (!response.isCommitted()) {
+				response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					"代理下载远程文件失败: " + e.getMessage());
+			}
+		} finally {
+			if (inputStream != null) {
+				try { inputStream.close(); } catch (IOException ignored) {}
+			}
+			if (outputStream != null) {
+				try { outputStream.close(); } catch (IOException ignored) {}
+			}
+			if (connection != null) {
+				connection.disconnect();
+			}
+		}
+	}
+
+	/**
+	 * 根据文件名获取 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";
+		}
 	}
 
 }

--
Gitblit v1.9.3