吉安感知网项目-后端
linwei
2026-07-10 6a77c800d56c6b4c3260c1b74ea7798d84f98ad8
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -12,45 +12,42 @@
import lombok.AllArgsConstructor;
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.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;
/**
 * 附件表 控制器
@@ -67,7 +64,14 @@
   private final IAttachService attachService;
   private final IAttachClient attachClient;
   /** 远程文件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,36 +79,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);
      queryWrapper.eq("is_deleted", 0);
      IPage<Attach> pages = attachService.page(Condition.getPage(query), queryWrapper);
      return R.data(pages);
   }
   /**
@@ -113,89 +90,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));
   }
   /**
@@ -204,7 +102,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));
   }
@@ -221,70 +120,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);
   // }
   /**
    * 图片分析对比 前后图片对比
@@ -292,33 +326,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("修改失败");
   // }
   /**
@@ -326,39 +360,257 @@
    *
    * @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) {
      // 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", "edit");
      editorConfig.put("callbackUrl", "http://192.168.1.33:8310/office/callback");
      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 proxyDownloadUrl = buildProxyDownloadUrl(request, attachId);
      // 11. 生成文档唯一标识
      String key = generateRevisionId(fileName);
      // 12. 构建文档配置,使用代理下载URL替代远程URL
      Map<String, Object> document = new HashMap<>();
      document.put("title", fileName != null ? fileName : "未命名文档");
      document.put("url", proxyDownloadUrl); // 使用代理下载URL
      document.put("fileType", fileType);
      document.put("attachId", attach.getId());
      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: {}, 代理下载URL: {}", attachId, proxyDownloadUrl);
      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
   }
   /**
    * 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 {
      // 1. 从缓存中获取远程文件URL
      String remoteUrl = REMOTE_URL_CACHE.get(attachId);
      if (remoteUrl == null || remoteUrl.isEmpty()) {
         response.sendError(HttpServletResponse.SC_NOT_FOUND, "未找到对应的远程文件URL");
         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";
      // 3. 从远程URL下载文件
      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);
         // 4. 检查响应状态码
         int responseCode = connection.getResponseCode();
         if (responseCode != HttpURLConnection.HTTP_OK) {
            response.sendError(HttpServletResponse.SC_BAD_GATEWAY, "远程文件下载失败,状态码: " + responseCode);
            return;
         }
         // 5. 获取远程文件的Content-Type
         String contentType = connection.getContentType();
         if (contentType == null || contentType.isEmpty()) {
            contentType = "application/octet-stream";
         }
         // 6. 设置响应头
         response.setContentType(contentType);
         response.setHeader("Content-Disposition", "attachment; filename=\""
               + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + "\"");
         // 7. 流式传输文件内容
         inputStream = connection.getInputStream();
         outputStream = response.getOutputStream();
         byte[] buffer = new byte[8192];
         int bytesRead;
         while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
         }
         outputStream.flush();
      } 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 {
         // 8. 关闭资源
         if (inputStream != null) {
            try { inputStream.close(); } catch (IOException ignored) {}
         }
         if (connection != null) {
            connection.disconnect();
         }
      }
   }
   /**
    * 构建当前服务器的代理下载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;
   }
}