/*
* Copyright (c) 京北方信息技术股份有限公司 Corporation 2022 . All rights reserved.
*
*/
package net.northking.cctp.executePlan.api.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.thread.ExecutorBuilder;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Sets;
import com.hzbank.testteam.autotest.dependencies.authDependency.utils.RequestUtil;
import com.hzbank.testteam.autotest.dependencies.baseDefineDependency.constants.BaseDefineDependencyConstants;
import com.hzbank.testteam.autotest.dependencies.baseDefineDependency.dto.TagDto;
import com.hzbank.testteam.autotest.dependencies.baseDefineDependency.entity.Environment;
import com.hzbank.testteam.autotest.dependencies.baseDefineDependency.exception.InvalidException;
import com.hzbank.testteam.autotest.dependencies.baseDefineDependency.vo.ResponseVO;
import com.hzbank.testteam.autotest.dependencies.monsterDependency.dto.TestGroupDTO;
import com.hzbank.testteam.autotest.dependencies.monsterDependency.dto.TestGroupUserDTO;
import com.hzbank.testteam.autotest.dependencies.monsterDependency.dto.UserDTO;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import net.northking.cctp.common.cache.EntityNameCache;
import net.northking.cctp.common.cache.EntityNameCatalog;
import net.northking.cctp.common.db.BasicService;
import net.northking.cctp.common.db.DefaultPagination;
import net.northking.cctp.common.db.Pagination;
import net.northking.cctp.common.db.SqlOrderBy;
import net.northking.cctp.common.db.annotation.DatabaseReadOnly;
import net.northking.cctp.common.db.annotation.DatabaseWriteOnly;
import net.northking.cctp.common.db.excel.AbstractExcelService;
import net.northking.cctp.common.db.excel.ExcelHeader;
import net.northking.cctp.common.dto.AdvancedQueryOptions;
import net.northking.cctp.common.dto.AssociatedFilesDto;
import net.northking.cctp.common.dto.CpLogAddDto;
import net.northking.cctp.common.dto.UserOptions;
import net.northking.cctp.common.exception.PlatformRuntimeException;
import net.northking.cctp.common.feign.ApiAtsEnviromentFeign;
import net.northking.cctp.common.feign.MonsterCommonFeignClient;
import net.northking.cctp.common.feign.MonsterLogServer;
import net.northking.cctp.common.http.QueryByPage;
import net.northking.cctp.common.http.ResultWrapper;
import net.northking.cctp.common.s3.FileDownloadException;
import net.northking.cctp.common.s3.SimpleStorageService;
import net.northking.cctp.common.security.authentication.NKSecurityContext;
import net.northking.cctp.common.util.UUIDUtil;
import net.northking.cctp.executePlan.api.third.feilang.dto.FeiLangUserDto;
import net.northking.cctp.executePlan.bus.log.LogPublisher;
import net.northking.cctp.executePlan.config.WebhookConfig;
import net.northking.cctp.executePlan.constants.*;
import net.northking.cctp.executePlan.db.entity.*;
import net.northking.cctp.executePlan.db.service.*;
import net.northking.cctp.executePlan.dto.planBatch.*;
import net.northking.cctp.executePlan.dto.planEntrust.AtuEntrustMsgDto;
import net.northking.cctp.executePlan.dto.planEntrust.DeviceListDto;
import net.northking.cctp.executePlan.dto.planEntrust.EngineInfoListDto;
import net.northking.cctp.executePlan.dto.planEntrust.EntrustDeviceDto;
import net.northking.cctp.executePlan.dto.planInfo.*;
import net.northking.cctp.executePlan.dto.planScript.*;
import net.northking.cctp.executePlan.dto.planTask.AtuPlanInputDto;
import net.northking.cctp.executePlan.dto.planTask.AtuPlanInputQueryDto;
import net.northking.cctp.executePlan.dto.planTask.AtuScriptDetailDto;
import net.northking.cctp.executePlan.dto.planTask.ScriptInputFileDto;
import net.northking.cctp.executePlan.enums.MobilePlatformEnum;
import net.northking.cctp.executePlan.exception.ExecPlanError;
import net.northking.cctp.executePlan.feign.*;
import net.northking.cctp.executePlan.feign.dto.*;
import net.northking.cctp.executePlan.pub.dto.ScriptFirstExecutionDTO;
import net.northking.cctp.executePlan.pub.dto.TestPlanScriptExecutionDetailDTO;
import net.northking.cctp.executePlan.quartz.ScheduleUtils;
import net.northking.cctp.executePlan.utils.EnvNameUtils;
import net.northking.cctp.executePlan.utils.MinioPathUtils;
import net.northking.cctp.executePlan.utils.PercentageCalculatorUtil;
import org.apache.commons.lang.StringUtils;
import org.quartz.CronExpression;
import org.quartz.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.LocalDate;
import java.util.List;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 执行计划表服务实现类
 *
 * @author maven-cctp-plugin
 * @since 1.0
 */

@Service
public class AtuPlanInfoApiServiceImpl extends AbstractExcelService<AtuPlanInfo> implements AtuPlanInfoApiService
{
  /**
   * 日志
   */
  private final static Logger logger = LoggerFactory.getLogger(AtuPlanInfoApiServiceImpl.class);

  @Autowired
  private AtuPlanInfoService atuPlanInfoService;

  @Autowired
  private PlatformFeignClient platformFeignClient;

  @Autowired
  private ApiMonsterCommonService monsterCommonService;

  @Autowired
  private AtuPlanBatchService atuPlanBatchService;

  @Autowired
  private AtuPlanScriptLinkService planScriptLinkService;

  @Autowired
  private AtuPlanDeviceLinkService atuPlanDeviceLinkService;

  @Autowired
  private RedisTemplate<String, Object> redisTemplate;

  @Autowired
  private AtuPlanAppLinkService atuPlanAppLinkService;

  @Autowired
  private AtuPlanTaskApiService atuPlanTaskApiService;

  @Autowired
  private AtuPlanSceneCaseTaskService atuPlanSceneCaseTaskService;

  @Autowired
  private AtuPlanBatchDeviceLinkService atuPlanBatchDeviceLinkService;

  @Autowired
  private AmqpAdmin amqpAdmin;

  @Autowired
  private ScriptCaseFeignClient scriptCaseFeignClient;

  @Autowired
  private AmqpTemplate amqpTemplate;

  @Autowired
  private PublicFeignClient publicFeignClient;

  @Autowired
  private DeviceFeignClient deviceFeignClient;

  @Autowired
  private WebhookConfig webhookConfig;

  @Autowired
  private SchedulerFactoryBean factoryBean;

  @Autowired
  private EntityNameCache entityNameCache;

  @Autowired
  private LogPublisher logPublisher;

  @Autowired
  private AtuPlanEnvLinkService planEnvLinkService;

  @Autowired
  private SimpleStorageService simpleStorageService;

  @Autowired
  private AtuPlanScriptLinkMdService planScriptLinkMdService;


  @Autowired
  private EnvNameUtils envNameUtils;

  @Autowired
  private ApiMonsterDictService apiMonsterDictService;

  @Autowired
  private ApiTagService apiTagService;

  @Autowired
  private ApiAtsEnviromentFeign apiAtsEnviromentFeign;

  @Autowired
  MonsterCommonFeignClient monsterCommonFeignClient;
  @Autowired
  private AttachmentFeignClient attachmentFeignClient;
  /**
   * 单次删除最大数量
   */
  public static final int DELETE_MAX_NUM = 500;

  public static final int EXECUTOR_CORE_POOL_SIZE = 2 * 2 + 2;
  public static final int EXECUTOR_MAX_POOL_SIZE = 20;
  public static final int EXECUTOR_LINKED_BLOCKING_SIZE = 50;

  private static final ExecutorService executor = ExecutorBuilder.create()
          .setCorePoolSize(EXECUTOR_CORE_POOL_SIZE)
          .setMaxPoolSize(EXECUTOR_MAX_POOL_SIZE)
          .setWorkQueue(new LinkedBlockingDeque<>(EXECUTOR_LINKED_BLOCKING_SIZE))
          .build();

  public AtuPlanInfoApiServiceImpl() {
  }

  @Override
  public BasicService<AtuPlanInfo> getService()
  {
    return this.atuPlanInfoService;
  }

  @Override
  public ExcelHeader[] excelHeaders()
  {
    ExcelHeader id = new ExcelHeader("主键", "id", String.class);
    ExcelHeader tenantId = new ExcelHeader("租户主键", "tenantId", String.class);
    ExcelHeader projectId = new ExcelHeader("系统主键", "projectId", String.class);
    ExcelHeader planName = new ExcelHeader("计划名称", "planName", String.class);
    ExcelHeader planDesc = new ExcelHeader("计划描述", "planDesc", String.class);
    ExcelHeader enabled = new ExcelHeader("是否启用;0-否，1-是", "enabled", Boolean.class);
    ExcelHeader status = new ExcelHeader("状态;0-新建，1-等待中，2-执行中，3-已完成，4-已取消", "status", String.class);
    ExcelHeader appSet = new ExcelHeader("应用设置;1-清除数据，2-卸载安装", "appSet", String.class);
    ExcelHeader screenRecordSet = new ExcelHeader("录屏设置;0-不保存，1-全部保存，2-出错保存", "screenRecordSet", String.class);
    ExcelHeader screenshotSet = new ExcelHeader("截图设置;0-不保存，1-全部保存，2-出错保存", "screenshotSet", String.class);
    ExcelHeader priority = new ExcelHeader("优先级;0-低，1-高", "priority", String.class);
    ExcelHeader cronExpr = new ExcelHeader("Cron表达式", "cronExpr", String.class);
    ExcelHeader upFrontPlanId = new ExcelHeader("前置计划ID", "upFrontPlanId", String.class);
    ExcelHeader webhookUrl = new ExcelHeader("触发链接", "webhookUrl", String.class);
    ExcelHeader webhookToken = new ExcelHeader("密保令牌", "webhookToken", String.class);
    ExcelHeader sendMail = new ExcelHeader("是否发送邮件;0-否，1-是", "sendMail", Boolean.class);
    ExcelHeader mailAddress = new ExcelHeader("邮件地址;当需要发送邮件时不能为空", "mailAddress", String.class);
    ExcelHeader failRetry = new ExcelHeader("是否失败重试;0-否，1-是", "failRetry", Boolean.class);
    ExcelHeader failRetryCount = new ExcelHeader("失败重试次数", "failRetryCount", Integer.class);
    ExcelHeader failCreatePlan = new ExcelHeader("是否失败生成计划;0-否，1-是", "failCreatePlan", Boolean.class);
    ExcelHeader lastExecTime = new ExcelHeader("最近执行时间", "lastExecTime", Date.class);
    ExcelHeader nextExecTime = new ExcelHeader("下次执行时间", "nextExecTime", Date.class);
    ExcelHeader lastBatchId = new ExcelHeader("最后批次号", "lastBatchId", String.class);
    ExcelHeader waitingNum = new ExcelHeader("最后批次等待中任务数", "waitingNum", Integer.class);
    ExcelHeader runningNum = new ExcelHeader("最后批次执行中任务数", "runningNum", Integer.class);
    ExcelHeader successNum = new ExcelHeader("最后批次成功任务数", "successNum", Integer.class);
    ExcelHeader execFailNum = new ExcelHeader("最后批次执行失败任务数", "execFailNum", Integer.class);
    ExcelHeader assertFailNum = new ExcelHeader("最后批次断言失败任务数", "assertFailNum", Integer.class);
    ExcelHeader timeoutNum = new ExcelHeader("最后批次超时任务数", "timeoutNum", Integer.class);
    ExcelHeader cancelNum = new ExcelHeader("最后批次取消任务数", "cancelNum", Integer.class);
    ExcelHeader createdBy = new ExcelHeader("创建人", "createdBy", String.class);
    ExcelHeader createdTime = new ExcelHeader("创建时间", "createdTime", Date.class);
    ExcelHeader updatedBy = new ExcelHeader("更新人", "updatedBy", String.class);
    ExcelHeader updatedTime = new ExcelHeader("更新时间", "updatedTime", Date.class);
    ExcelHeader deleted = new ExcelHeader("是否删除;0-否，1-是", "deleted", Boolean.class);

    return new ExcelHeader[]{};
  }

  /**
   * Excel导入，入库之前的处理
   * @param records 从Excel读取到的记录
   * @return 待入库的记录
   */
  @Override
  protected List<AtuPlanInfo> beforeInsertBatch(List<AtuPlanInfo> records)
  {
    for (AtuPlanInfo record : records)
    {
      record.setId(UUIDUtil.create32UUID());
    }
    return records;
  }

  /**
   * 写入Excel之前的处理<br>
   * 数据的特殊处理在此处加工处理
   *
   * @param record 来自数据库记录
   * @return 待写入Excel的记录
   */
  @Override
  protected AtuPlanInfo beforeWriteExcel(AtuPlanInfo record)
  {
    return record;
  }

  @Override
  public Pagination<AtuPlanInfoDetailDto> pagingQuery(QueryByPage<AtuPlanInfoQueryDto> queryByPage)
  {
    queryByPage.getQuery().setTenantId(NKSecurityContext.getTenantId());
    buildQueryPage(queryByPage);
    Pagination<AtuPlanInfoDetailDto> query = this.atuPlanInfoService.queryPlanInfoPage(queryByPage);
    for (AtuPlanInfoDetailDto record : query.getRecords()) {
      String projectName = entityNameCache.translateById(EntityNameCatalog.MONSTER_SYSTEM, record.getProjectId());
      String principalName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getPrincipalId());
      record.setProjectName(projectName);
      record.setLastBatchScriptSumMap(getBatchScriptSumData(record));
      record.setPrincipalId(principalName);
      String createdBy = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getCreatedBy());
      String updatedBy = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getUpdatedBy());
      record.setCreatedBy(createdBy);
      record.setUpdatedBy(updatedBy);
      //判断是否有下一次执行时间
      if (record.getHasCorn()) {
        Date date = ScheduleUtils.getNextOneTriggerTime(record.getCronExpr());
        if (date == null) {
          AtuPlanInfo atuPlanInfo = new AtuPlanInfo();
          atuPlanInfo.setId(record.getId());
          atuPlanInfo.setNextExecTime(null);
          this.atuPlanInfoService.updatePlanNextExecTime(atuPlanInfo);
          record.setNextExecTime(null);
        }
      }
    }
    return query;
  }

  private void buildQueryPage(QueryByPage<AtuPlanInfoQueryDto> queryByPage) {
    AtuPlanInfoQueryDto query = queryByPage.getQuery();
    if (!StringUtils.isEmpty(query.getPrincipal())) {
      List<String> principalIds = getUserIds(query.getPrincipal());
      if (!CollectionUtil.isEmpty(principalIds)) {
        query.setPrincipalIds(principalIds);
      } else {
        principalIds.add("-1");
        query.setPrincipalIds(principalIds);
      }
    }

    if (!StringUtils.isEmpty(query.getCreator())) {
      List<String> createBys = getUserIds(query.getCreator());
      if (!CollectionUtils.isEmpty(createBys)) {
        query.setCreateBys(createBys);
      } else {
        createBys.add("-1");
        query.setCreateBys(createBys);
      }
    }
    //创建时间--时间区间  修改时间--时间区间
    String createdTimeStart = query.getCreatedTimeStart();
    if (!StringUtils.isEmpty(createdTimeStart)) {
      Date startCreatedTime = null;
      try {
        startCreatedTime = net.northking.cctp.common.util.DateUtil.parse(createdTimeStart+" 00:00:00","yyyy-MM-dd HH:ss:mm");

      } catch (ParseException e) {
        logger.error("时间转换异常，createdTimeStart，{}，e,{}",createdTimeStart,e.getMessage());
        throw new PlatformRuntimeException(ExecPlanError.PARSE_DATE_ERROR);
      }
      query.setStartCreatedTime(startCreatedTime);
    }
    String createdTimeEnd = query.getCreatedTimeEnd();
    if (!StringUtils.isEmpty(createdTimeEnd)) {
      Date endCreatedTime = null;
      try {
        endCreatedTime = net.northking.cctp.common.util.DateUtil.parse(createdTimeEnd +" 23:59:59","yyyy-MM-dd HH:ss:mm");

      } catch (ParseException e){
        logger.error("时间转换异常，createdTimeEnd，{}，e,{}",createdTimeEnd,e.getMessage());
        throw new PlatformRuntimeException(ExecPlanError.PARSE_DATE_ERROR);
      }
      query.setEndCreatedTime(endCreatedTime);
    }
    // 更新时间
    String updatedTimeStart = query.getUpdatedTimeStart();
    if (!StringUtils.isEmpty(updatedTimeStart)) {
      Date startUpdatedTime = null;
      try{
        startUpdatedTime = net.northking.cctp.common.util.DateUtil.parse(updatedTimeStart+" 00:00:00","yyyy-MM-dd HH:ss:mm");
      }catch (ParseException e){
        logger.error("时间转换异常，updatedTimeStart，{}，e,{}",updatedTimeStart,e.getMessage());
        throw new PlatformRuntimeException(ExecPlanError.PARSE_DATE_ERROR);
      }
      query.setStartUpdatedTime(startUpdatedTime);
    }
    String updatedTimeEnd = query.getUpdatedTimeEnd();
    if (!StringUtils.isEmpty(updatedTimeEnd)) {
      Date endUpdatedTime = null;
      try {
        endUpdatedTime = net.northking.cctp.common.util.DateUtil.parse(updatedTimeEnd+" 23:59:59","yyyy-MM-dd HH:ss:mm");
      } catch (ParseException e) {
        logger.error("时间转换异常，updatedTimeEnd，{}，e,{}",updatedTimeEnd,e.getMessage());
        throw new PlatformRuntimeException(ExecPlanError.PARSE_DATE_ERROR);
      }
      query.setEndUpdatedTime(endUpdatedTime);
    }

  }

  /**
   * 查询人名可带正则表达式
   * @param userName
   * @return
   */
  private List<String> getUserIds(String userName) {
    UserQueryDto example = new UserQueryDto();
    if(userName.contains(",")||userName.contains("，")){
      userName = userName.replace(",","|")
              .replace("，","|")
              .replace("(","\\(")
              .replace(")","\\)");
      example.setUserChnNames(userName);
    }else{
      example.setUserChnName(userName);
    }
    example.setTenantId(NKSecurityContext.getTenantId());
    List<String> userIds = platformFeignClient.selectUserId(example);
    if (userIds == null) {
      userIds = new ArrayList<>();
    }
    return userIds;
  }



  @Override
  @DatabaseReadOnly
  public AtuPlanInfoDetailDto findByPK(String uuid)
  {
    AtuPlanInfo example = new AtuPlanInfo();
    example.setId(uuid);
    //查询基本信息
    List<AtuPlanInfo> entities = this.atuPlanInfoService.query(example);
    if (entities.isEmpty())
    {
      return null;
    }
    AtuPlanInfoDetailDto dto = new AtuPlanInfoDetailDto();
    //查询主表关联的脚本
    AtuPlanScriptLink scriptLink = new AtuPlanScriptLink();
    scriptLink.setPlanId(uuid);
    List<AtuPlanScriptLink> scriptList = this.planScriptLinkService.query(scriptLink);
    List<AtuPlanScriptLinkMd> insertList = new ArrayList<>();
    List<AtuPlanScriptLinkDto> resultDto = new ArrayList<>();
    List<String> idList = new ArrayList<>();
    AtomicBoolean isMobile = new AtomicBoolean(false);
    if (scriptList.size()>0) {
      scriptList.forEach(one -> {
        //新增到副表的数据
        AtuPlanScriptLinkMd linkMd = new AtuPlanScriptLinkMd();
        BeanUtils.copyProperties(one, linkMd);
        linkMd.setUserId(NKSecurityContext.getUserId());
        //返现数据 脚本内不执行的测试意图
        AtuPlanScriptLinkDto linkDto = new AtuPlanScriptLinkDto();
        BeanUtils.copyProperties(linkMd, linkDto);
        resultDto.add(linkDto);
        insertList.add(linkMd);
        idList.add(one.getScriptId());
        if (one.getScriptType().equals(PlanConstant.SCRIPT_TYPE_ANDROID) ||
                one.getScriptType().equals(PlanConstant.SCRIPT_TYPE_IOS) ||
                one.getScriptType().equals(PlanConstant.SCRIPT_TYPE_SCENE)){
          isMobile.set(true);
        }
      });
      //删除原来的md表的数据
      AtuPlanScriptLinkMd linkMd = new AtuPlanScriptLinkMd();
      linkMd.setUserId(NKSecurityContext.getUserId());
      linkMd.setPlanId(uuid);
      this.planScriptLinkMdService.deleteByExample(linkMd);
      //把数据加到md表
      this.planScriptLinkMdService.insertMdBatch(insertList);
    }
    //复制数据
    BeanUtils.copyProperties(entities.get(0), dto);
    dto.setPlanScriptResultDto(queryScriptData(idList, resultDto));
    dto.setLastBatchScriptSumMap(getBatchScriptSumData(dto));
    //查询关联环境
    AtuPlanEnvLink envLink = new AtuPlanEnvLink();
    envLink.setPlanId(dto.getId());
    List<AtuPlanEnvLink> resultLinks = this.planEnvLinkService.query(envLink);
    if (resultLinks.size()>0){
      List<String> envList = new ArrayList<>();
      for (AtuPlanEnvLink link : resultLinks) {
        envList.add(link.getEnvId());
      }
      dto.setEnvIdList(envList);
    }
    //查询飞狼用户中文名
    if(dto.getSendMail()&&
            Objects.nonNull(dto.getMailAddress())&&
            dto.getMailAddress().length()>0){
      List<String> paramLoginNoList = new ArrayList<>();
      List<FeiLangUserDto> resultList = new ArrayList<>();
      if(dto.getMailAddress().contains(",")){
        paramLoginNoList = Arrays.asList(dto.getMailAddress().split(","));
      }else{
        paramLoginNoList.add(dto.getMailAddress());
      }
      ResponseVO<Map<String, String>> responseVO = monsterCommonService.queryUserInfoByLoginNo(paramLoginNoList);
      if (!BaseDefineDependencyConstants.SYSTEM_RESPONSE_CODE_SUCCESS.equals(responseVO.getCode())) {
        logger.info("查询发送飞狼用户信息失败" + responseVO.getMessage());
        throw new InvalidException("查询发送飞狼用户信息失败");
      }
      Map<String, String> userInfoList = responseVO.getData();
      if (!CollectionUtils.isEmpty(userInfoList)) {
        for (String key : userInfoList.keySet()) {
          FeiLangUserDto feiLangUserDto = new FeiLangUserDto();
          feiLangUserDto.setLoginNo(key);
          feiLangUserDto.setUserChnName(userInfoList.get(key));
          resultList.add(feiLangUserDto);
        }
      }
      dto.setFeiLangUserDtoList(resultList);
    }
    // 查询应用编号
    if (isMobile.get()){
      AtuPlanAppLink appLink = new AtuPlanAppLink();
      appLink.setPlanId(dto.getId());
      List<AtuPlanAppLink> appLinks = atuPlanAppLinkService.query(appLink);
      if (CollUtil.isNotEmpty(appLinks)){
        List<String> appIdList = appLinks.stream().map(AtuPlanAppLink::getAppId).collect(Collectors.toList());
        dto.setAppIdList(appIdList);
      }
    }
    return dto;
  }

  @Override
  @DatabaseWriteOnly
  public Integer deleteByPK(String uuid) {
    AtuPlanInfo entity = this.atuPlanInfoService.findByPrimaryKey(uuid);
    if (entity.getStatus().equals(PlanConstant.PLAN_WAITING_STATUS)
            || entity.getStatus().equals(PlanConstant.PLAN_EXECUTING_STATUS)) {
      throw new PlatformRuntimeException(ExecPlanError.PLAN_STATUS_CANNOT_DELETE);
    }
    entity.setDeleted(true);
    int row = this.atuPlanInfoService.updateByPrimaryKey(entity);
    //删除定时任务
    if (row > 0) {
      Scheduler scheduler = factoryBean.getScheduler();
      if (ScheduleUtils.checkExists(scheduler, uuid)) {
        ScheduleUtils.deleteJob(scheduler, uuid);
      }
      //发送队列：平台删除日志
      CpLogAddDto cpLogAddDto = new CpLogAddDto();
      String proName = entityNameCache.translateById(EntityNameCatalog.MONSTER_SYSTEM, entity.getProjectId());
      cpLogAddDto.setContent("【删除执行计划】系统：" + proName + "，计划名称：" + entity.getPlanName());
      cpLogAddDto.setModule(MsgConstant.EXE_PLAN);
      cpLogAddDto.setType(MsgConstant.LOG_DELETE);
      logPublisher.deployLogContent(cpLogAddDto);
      MonsterLogServer.sendLogToMonster(cpLogAddDto);
     logger.debug("删除批次和任务信息");
      executor.execute(() -> {
        logger.debug("异步删除批次和任务信息");
        int taskTotal = atuPlanTaskApiService.countByPlanId(uuid);
        logger.debug("任务总数：{}", taskTotal);
        int sceneTaskTotal = atuPlanSceneCaseTaskService.countByPlanId(uuid);
        logger.debug("场景节点任务总数：{}", sceneTaskTotal);

        // 计算需要分几次删除
        logger.debug("单次最大删除数据量：{}", DELETE_MAX_NUM);
        int num;
        if (taskTotal > 0) {
          logger.debug("删除任务信息");
          List<AtuPlanTask> tasks = atuPlanTaskApiService.getTasksByPlanId(uuid);
          num = NumberUtil.ceilDiv(taskTotal, DELETE_MAX_NUM);
          List<String> deletedFilePath = new ArrayList<>();
          List<String> taskIds = new ArrayList<>();
          tasks.forEach(task -> {
            taskIds.add(task.getId());
            String execResultFile = task.getExecResultFile();
            if (StringUtils.isNotBlank(execResultFile)){
              deletedFilePath.add(execResultFile);
              atuPlanTaskApiService.getActualImagePath(deletedFilePath, execResultFile);
            }
            String perDataPath = task.getPerDataPath();
            if (StringUtils.isNotBlank(perDataPath)) {
              deletedFilePath.add("/" + MinioPathUtils.idToPath(perDataPath)[1]);
            }
            String videoUrl = task.getVideoUrl();
            if (StringUtils.isNotBlank(videoUrl)) {
              deletedFilePath.add("/" + MinioPathUtils.idToPath(videoUrl)[1]);
            }
            // 场景用例脚本json是单独的需要删除
            if (task.getCaseType().equals("5")) {
              deletedFilePath.add("/" + MinioPathUtils.idToPath(task.getScriptJson())[1]);
            }
          });
          if (!CollectionUtils.isEmpty(taskIds)) {
            List<AtuPlanSceneCaseTask> atuPlanSceneCaseTaskList = atuPlanSceneCaseTaskService.querySceneCaseTasksByTaskIds(taskIds);
            atuPlanSceneCaseTaskList.forEach(atuPlanSceneCaseTask -> {
              String videoUrl = atuPlanSceneCaseTask.getVideoUrl();
              if (StringUtils.isNotBlank(videoUrl)) {
                deletedFilePath.add("/" + MinioPathUtils.idToPath(videoUrl)[1]);
              }
              String perDataPath = atuPlanSceneCaseTask.getPerDataPath();
              if (StringUtils.isNotBlank(perDataPath)) {
                deletedFilePath.add("/" + MinioPathUtils.idToPath(perDataPath)[1]);
              }
              String execResultFile = atuPlanSceneCaseTask.getExecResultFile();
              if (StringUtils.isNotBlank(execResultFile)) {
                deletedFilePath.add(execResultFile);
                atuPlanTaskApiService.getActualImagePath(deletedFilePath, execResultFile);
              }
            });
          }
          if (!CollectionUtils.isEmpty(deletedFilePath)) {
            AssociatedFilesDto associatedFilesDto = new AssociatedFilesDto();
            associatedFilesDto.setObjId("-1");
            associatedFilesDto.setFileIds(deletedFilePath);
            attachmentFeignClient.associatedFiles(associatedFilesDto);
          }
          for (; num > 0; num--) {
            atuPlanTaskApiService.deleteByPlanIdLimit(uuid, DELETE_MAX_NUM);
          }
        }
        if (sceneTaskTotal > 0) {
          logger.debug("删除场景节点任务信息");
          num = NumberUtil.ceilDiv(sceneTaskTotal, DELETE_MAX_NUM);
          for (; num > 0; num--) {
            atuPlanSceneCaseTaskService.deleteByPlanIdLimit(uuid, DELETE_MAX_NUM);
          }
        }
        logger.debug("删除批次信息");
        AtuPlanBatch planBatch = new AtuPlanBatch();
        planBatch.setPlanId(uuid);
        List<AtuPlanBatch> query = atuPlanBatchService.query(planBatch);
        List<String> deletedFilePath = new ArrayList<>();
        query.forEach(atuPlanBatch -> {
          String perDataPath = atuPlanBatch.getPerDataPath();
          if (StringUtils.isNotBlank(perDataPath)) {
            deletedFilePath.add("/" + MinioPathUtils.idToPath(perDataPath)[1]);
          }
        });
        if (!CollectionUtils.isEmpty(deletedFilePath)) {
          AssociatedFilesDto associatedFilesDto = new AssociatedFilesDto();
          associatedFilesDto.setObjId("-1");
          associatedFilesDto.setFileIds(deletedFilePath);
          attachmentFeignClient.associatedFiles(associatedFilesDto);
        }
        atuPlanBatchService.deleteByExample(planBatch);
      });
    }
    return row;
  }

  /**
   * 校验计划设备是否离线
   *
   * @param planId 计划Id
   * @return 批次ID
   */
  @Override
  public Map<String, Object> checkDeviceOffline(String planId, String batchId) {
    // allOffline 0-在线，1-部分离线，2-全部离线
    Map<String, Object> result = new HashMap<>();
    if (batchId == null) {
      batchId = IdUtil.simpleUUID();
    }
    result.put(PlanConstant.DEVICE_OFFLINE_KEY, PlanConstant.DEVICE_OFFLINE_PORTION);
    result.put(PlanConstant.PLAN_BATCH_ID, batchId);

    AtuPlanDeviceLink deviceLinkParams = new AtuPlanDeviceLink();
    deviceLinkParams.setPlanId(planId);
    List<AtuPlanDeviceLink> deviceLinkList = atuPlanDeviceLinkService.query(deviceLinkParams);
    if (CollUtil.isEmpty(deviceLinkList)){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_DEVICE_IS_NULL);
    }
    // 设备根据类型分类
    List<String> deviceIdList = new ArrayList<>();
    Map<String, List<String>> deviceTypeMap = new HashMap<>();
    for (AtuPlanDeviceLink deviceLink : deviceLinkList) {
      deviceIdList.add(deviceLink.getDeviceId());
      String type = deviceLink.getDeviceType();
      if (StrUtil.isNotBlank(deviceLink.getPlatformType())){
        type = deviceLink.getPlatformType();
      }
      List<String> typeDeviceIdList = new ArrayList<>();
      if (deviceTypeMap.containsKey(type)){
        typeDeviceIdList = deviceTypeMap.get(type);
      }
      typeDeviceIdList.add(deviceLink.getDeviceId());
      deviceTypeMap.put(type, typeDeviceIdList);
    }

    // 校验设备是否有离线
    List<Map<String, Object>> offlineDeviceList = publicFeignClient.queryDeviceStatus(deviceIdList);
    if (CollUtil.isEmpty(offlineDeviceList)){
      result.put(PlanConstant.DEVICE_OFFLINE_KEY, PlanConstant.DEVICE_OFFLINE_NO);
      result.put(PlanConstant.PLAN_BATCH_ID, "");
      return result;
    }
    List<String> offlineDeviceIdList = new ArrayList<>();
    AtomicBoolean offlineAll = new AtomicBoolean(false);
    for (Map<String, Object> offlineDeviceMap : offlineDeviceList) {
      String deviceId = offlineDeviceMap.get("deviceId").toString();
      deviceTypeMap.forEach((type, deviceList) -> {
        deviceList.removeIf(deviceId::equals);
        offlineDeviceIdList.add(deviceId);
        if (CollUtil.isEmpty(deviceList)){
          offlineAll.set(true);
        }
      });
    }
    if (offlineAll.get()){
      result.put(PlanConstant.DEVICE_OFFLINE_KEY, PlanConstant.DEVICE_OFFLINE_ALL);
      result.put(PlanConstant.PLAN_BATCH_ID, "");
      return result;
    }
    // 将离线设备ID缓存-后续执行移除离线设备-过期时间1h
    redisTemplate.opsForValue().set(RedisConstant.PLAN_BATCH_OFFLINE_DEVICE + batchId,
            JSONUtil.toJsonStr(offlineDeviceIdList),
            RedisConstant.PLAN_BATCH_OFFLINE_DEVICE_EXP_TIME, RedisConstant.PLAN_BATCH_OFFLINE_DEVICE_TIME_UNIT);
    return result;
  }

  @Override
  public Boolean updatePlanNextRunTime(AtuPlanInfo planInfo) {
    List<Date> nextTriggerTimes = ScheduleUtils.getNextTriggerTimes(planInfo.getCronExpr(), 2);
    if (CollUtil.isEmpty(nextTriggerTimes)){
      return true;
    }
    Date nextOneTriggerTime;
    if (ObjectUtil.isNull(planInfo.getLastExecTime())){
      nextOneTriggerTime = nextTriggerTimes.get(0);
    }else {
      long between = DateUtil.between(nextTriggerTimes.get(0), planInfo.getLastExecTime(), DateUnit.SECOND);
      if (between > 0) {
        nextOneTriggerTime = nextTriggerTimes.get(0);
      } else {
        if (nextTriggerTimes.size() >= 2) {
          nextOneTriggerTime = nextTriggerTimes.get(1);
        } else {
          nextOneTriggerTime = nextTriggerTimes.get(0);
        }
      }
    }
    AtuPlanInfo update = new AtuPlanInfo();
    update.setId(planInfo.getId());
    update.setNextExecTime(nextOneTriggerTime);
    int row = atuPlanInfoService.updateByPrimaryKey(update);
    return row > 0;
  }

  @Override
  @Transactional(rollbackFor = Exception.class)
  public String clonePlanInfo(String planId) {
    String uuid = UUIDUtil.create32UUID();
    //计划基本信息
    AtuPlanInfo planInfo = this.atuPlanInfoService.findByPrimaryKey(planId);
    //计划名称
    String planName = getComNewName(planInfo);
    if (planName.length() > 30){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_NAME_OVERLONG, planInfo.getPlanName());
    }
    //webhookUrl
    if (!StringUtils.isEmpty(planInfo.getWebhookUrl())){
      String webhookUrl = planInfo.getWebhookUrl().replaceAll(planInfo.getId(), uuid);
      planInfo.setWebhookUrl(webhookUrl);
    }
    planInfo.setId(uuid);
    planInfo.setPlanName(planName);
    planInfo.setWebhookToken(null);
    planInfo.setLastBatchId(null);
    planInfo.setLastExecTime(null);
    planInfo.setNextExecTime(null);
    planInfo.setWaitingNum(0);
    planInfo.setRunningNum(0);
    planInfo.setSuccessNum(0);
    planInfo.setExecFailNum(0);
    planInfo.setAssertFailNum(0);
    planInfo.setTimeoutNum(0);
    planInfo.setCancelNum(0);
    planInfo.setStatus(PlanConstant.PLAN_NEW_STATUS);
    Date date = new Date();
    planInfo.setCreatedBy(NKSecurityContext.getUserId());
    planInfo.setCreatedTime(date);
    planInfo.setUpdatedBy(NKSecurityContext.getUserId());
    planInfo.setUpdatedTime(date);
    this.atuPlanInfoService.insert(planInfo);

    // 计划关联环境
    AtuPlanEnvLink envLink = new AtuPlanEnvLink();
    envLink.setPlanId(planId);
    List<AtuPlanEnvLink> envLinkList = this.planEnvLinkService.query(envLink);
    if (envLinkList != null && envLinkList.size() > 0){
      for (AtuPlanEnvLink atuPlanEnvLink : envLinkList) {
        atuPlanEnvLink.setPlanId(uuid);
      }
      this.planEnvLinkService.insertByBatch(envLinkList);
    }
    // 计划关联脚本
    AtuPlanScriptLink scriptLink = new AtuPlanScriptLink();
    scriptLink.setPlanId(planId);
    List<AtuPlanScriptLink> scriptLinkList = this.planScriptLinkService.query(scriptLink);
    if (scriptLinkList != null && scriptLinkList.size() > 0){
      for (AtuPlanScriptLink atuPlanScriptLink : scriptLinkList) {
        atuPlanScriptLink.setPlanId(uuid);
      }
      this.planScriptLinkService.insertByBatch(scriptLinkList);
    }
    //计划关联的软件
    AtuPlanAppLink appLink = new AtuPlanAppLink();
    appLink.setPlanId(planId);
    List<AtuPlanAppLink> appLinkList = this.atuPlanAppLinkService.query(appLink);
    if (appLinkList.size() > 0) {
      List<AtuPlanAppLink> appAddLink = new ArrayList<>();
      for (AtuPlanAppLink atuPlanAppLink : appLinkList) {
        atuPlanAppLink.setId(UUIDUtil.create32UUID());
        atuPlanAppLink.setPlanId(uuid);
        appAddLink.add(atuPlanAppLink);
      }
      this.atuPlanAppLinkService.insertByBatch(appAddLink);
    }

    //计划关联的设备
    AtuPlanDeviceLink deviceLink = new AtuPlanDeviceLink();
    deviceLink.setPlanId(planId);
    List<AtuPlanDeviceLink> deviceLinkList = this.atuPlanDeviceLinkService.query(deviceLink);
    if (deviceLinkList.size() > 0) {
      List<AtuPlanDeviceLink> deviceAddLink = new ArrayList<>();
      for (AtuPlanDeviceLink atuPlanDeviceLink : deviceLinkList) {
        atuPlanDeviceLink.setId(UUIDUtil.create32UUID());
        atuPlanDeviceLink.setPlanId(uuid);
        deviceAddLink.add(atuPlanDeviceLink);
      }
      this.atuPlanDeviceLinkService.insertByBatch(deviceAddLink);
    }

    return uuid;
  }

  @Override
  public AtuPlanInfoAppAndDeviceDto queryPlanAppAndDevice(String planId) {

    AtuPlanInfoAppAndDeviceDto dto = new AtuPlanInfoAppAndDeviceDto();

    AtuPlanAppLink appLink = new AtuPlanAppLink();
    appLink.setPlanId(planId);
    List<AtuPlanAppLink> query = this.atuPlanAppLinkService.query(appLink);
    dto.setAppList(query);

    AtuPlanDeviceLink deviceLink = new AtuPlanDeviceLink();
    deviceLink.setPlanId(planId);
    deviceLink.setDeviceType(PlanConstant.DEVICE_TYPE_MOBILE);
    List<AtuPlanDeviceLink> mobDeviceList = this.atuPlanDeviceLinkService.query(deviceLink);
    dto.setMobDeviceList(mobDeviceList);

    deviceLink.setDeviceType(PlanConstant.DEVICE_TYPE_PC);
    List<AtuPlanDeviceLink> pcDeviceList = this.atuPlanDeviceLinkService.query(deviceLink);
    dto.setPcDeviceList(pcDeviceList);

    return dto;
  }

  @Override
  public Integer updatePlanEnable(AtuPlanInfo atuPlanInfo) {
    int row = this.atuPlanInfoService.updateByPrimaryKey(atuPlanInfo);
    AtuPlanInfo planInfo = this.atuPlanInfoService.findByPrimaryKey(atuPlanInfo.getId());
    if (row > 0){
      Scheduler scheduler = factoryBean.getScheduler();
      if (ScheduleUtils.checkExists(scheduler, planInfo.getId())){
        ScheduleUtils.deleteJob(scheduler, planInfo.getId());
      }
      if (planInfo.getHasCorn() && planInfo.getEnabled()){
        ScheduleUtils.createScheduleJob(scheduler, planInfo);
        // 更新下一次执行时间
        updatePlanNextRunTime(planInfo);
      }
    }
    return row;
  }

  @Override
  public List<Map<String, Object>> queryTaskLive(String taskId) {
    List<Map<String,Object>> mapList = new ArrayList<>();
    AtuPlanTask planTask = this.atuPlanTaskApiService.findByPrimaryKey(taskId);
    //当前用例是否场景用例
    if (planTask.getCaseType().equals(PlanConstant.SCRIPT_TYPE_SCENE)) {
      //查询场景用例的执行状态
      if (planTask.getStatus().equals(PlanConstant.TASK_START_EXECUTE_STATUS)){
        //执行中
        AtuPlanSceneCaseTask atuPlanSceneCaseTask = new AtuPlanSceneCaseTask();
        atuPlanSceneCaseTask.setTaskId(taskId);
        atuPlanSceneCaseTask.setStatus("1");
        List<AtuPlanSceneCaseTask> query = this.atuPlanSceneCaseTaskService.query(atuPlanSceneCaseTask);
        if (query.size()>0){
          if (PlanConstant.SCRIPT_TYPE_API.equals(query.get(0).getNodeType())){
            throw new PlatformRuntimeException(ExecPlanError.SCENE_NODE_INFO_ERROR);
          }
          Map<String,Object> map = new HashMap<>();
          map.put(MsgConstant.DEVICE_ID,query.get(0).getDeviceId());
          map.put(MsgConstant.TYPE,query.get(0).getNodeType());
          map.put(MsgConstant.STATUS,query.get(0).getStatus());
          map.put(MsgConstant.SCRIPT_NAME,query.get(0).getScriptName());
          String token = deviceFeignClient.queryDeviceToken(query.get(0).getDeviceId());
          map.put(MsgConstant.TOKEN,token);
          mapList.add(map);
        }
      } else {
        //执行完成
        AtuPlanSceneCaseTask atuPlanSceneCaseTask = new AtuPlanSceneCaseTask();
        atuPlanSceneCaseTask.setTaskId(taskId);
        List<AtuPlanSceneCaseTask> query = this.atuPlanSceneCaseTaskService.query(atuPlanSceneCaseTask);
        for (AtuPlanSceneCaseTask dto : query) {
          if(!PlanConstant.SCRIPT_TYPE_API.equals(dto.getNodeType())){
            Map<String,Object> map = new HashMap<>();
            map.put(MsgConstant.TYPE,dto.getNodeType());
            map.put(MsgConstant.STATUS,dto.getStatus());
            map.put(MsgConstant.URL,dto.getVideoUrl());
            mapList.add(map);
          }
        }
      }
    }else {
      Map<String,Object> map = new HashMap<>();
      if (planTask.getStatus().equals(PlanConstant.TASK_START_EXECUTE_STATUS)){
        //执行中
        map.put(MsgConstant.DEVICE_ID,planTask.getDeviceId());
        map.put(MsgConstant.TYPE,planTask.getCaseType());
        map.put(MsgConstant.STATUS,planTask.getStatus());
        String token = deviceFeignClient.queryDeviceToken(planTask.getDeviceId());
        map.put(MsgConstant.TOKEN,token);
      } else {
        //已完成 执行失败 断言失败
        map.put(MsgConstant.TYPE,planTask.getCaseType());
        map.put(MsgConstant.STATUS,planTask.getStatus());
        map.put(MsgConstant.URL,planTask.getVideoUrl());
      }
      mapList.add(map);
    }
    return mapList;
  }

  @Override
  public Boolean isUse(List<String> params) {
    // TODO: 2023/8/31 绑定修改

    return false;
  }

  @Override
  public String selectScriptByPlan(AtuPlanScriptDto dto) {
    //feign调用 1、查询父节点下的脚本  2、场景脚本内关联的脚本
    Map<String, Object> queryMap = new HashMap<>();
    queryMap.put(MsgConstant.SCRIPT_LIST, dto.getScriptList());
    queryMap.put(MsgConstant.PARENT_LIST, dto.getParentList());
    ResultWrapper<Map<String, Object>> wrapper = scriptCaseFeignClient.querySceneScript(queryMap);
    //feign返回数据
    Map<String, Object> resultData = wrapper.getData();
    String planId = null;
    if (StringUtils.isBlank(dto.getPlanId())) {
      //新增
      planId = UUIDUtil.create32UUID();
    } else {
      //编辑
      planId = dto.getPlanId();
    }
    List<String> scriptList = new ArrayList<>();
    List<AtuPlanScriptLinkMd> list = new ArrayList<>();
    HashMap<String, String> scriptTypeMap = new HashMap<>();
    //场景脚本id
    if (resultData.get(MsgConstant.SCENE_LIST) != null && resultData.get(MsgConstant.SCENE_LIST) != "") {
      List<String> sceneList = (List<String>) resultData.get(MsgConstant.SCENE_LIST);
      if (sceneList != null && sceneList.size() > 0) {
        for (String sceneId : sceneList) {
          AtuPlanScriptLinkMd insertData = new AtuPlanScriptLinkMd();
          insertData.setScriptId(sceneId);
          insertData.setPlanId(planId);
          insertData.setUserId(NKSecurityContext.getUserId());
          insertData.setScriptType("5");//场景脚本
          scriptList.add(sceneId);
          scriptTypeMap.put(sceneId, "5");
          list.add(insertData);
        }
      }
    }
    //一般脚本
    if (resultData.get(MsgConstant.INSERT_LIST) != null && resultData.get(MsgConstant.INSERT_LIST) != "") {
      List<AtuScriptInfo> insertList = JSONObject.parseArray(JSONObject.toJSONString(resultData.get(MsgConstant.INSERT_LIST))).toJavaList(AtuScriptInfo.class);
      if (insertList != null && insertList.size() > 0) {
        for (AtuScriptInfo atuScriptInfo : insertList) {
          AtuPlanScriptLinkMd insertData = new AtuPlanScriptLinkMd();
          insertData.setScriptId(atuScriptInfo.getId());
          insertData.setPlanId(planId);
          insertData.setUserId(NKSecurityContext.getUserId());
          insertData.setScriptType(atuScriptInfo.getScriptType());
          scriptList.add(atuScriptInfo.getId());
          scriptTypeMap.put(atuScriptInfo.getId(), atuScriptInfo.getScriptType());
          list.add(insertData);
        }
      }
    }
    //先删后增
    AtuPlanScriptLinkMd del = new AtuPlanScriptLinkMd();
    del.setPlanId(planId);
    del.setUserId(NKSecurityContext.getUserId());
    this.planScriptLinkMdService.deleteByExample(del);
    //scriptLinkMd
    if (list.size() > 0) {
      this.planScriptLinkMdService.insertMdBatch(list);
    }
    return planId;
  }

  @Override
  public String savePlanScript(AtuPlanScriptSaveDto dto) {
    String planId = "";
    if (StringUtils.isBlank(dto.getPlanId())) {
      planId = UUIDUtil.create32UUID(); //新增
    } else {
      planId = dto.getPlanId();  //编辑
    }
    List<AtuPlanScriptReturnDto> resultList = scriptCaseFeignClient.feignQueryScriptId(dto);
    if (resultList.size() > 0) {
      List<AtuPlanScriptLinkMd> list = new ArrayList<>();
      for (AtuPlanScriptReturnDto returnDto : resultList) {
        AtuPlanScriptLinkMd linkMd = new AtuPlanScriptLinkMd();
        linkMd.setScriptId(returnDto.getScriptId());
        linkMd.setScriptType(returnDto.getScriptType());
        linkMd.setUserId(NKSecurityContext.getUserId());
        linkMd.setPlanId(planId);
        list.add(linkMd);
      }
      this.planScriptLinkMdService.insertMdBatch(list);
    } else {
      throw new PlatformRuntimeException(ExecPlanError.PLAN_SCRIPT_IS_NULL);
    }
    return planId;
  }

  @Override
  public String savePlanData(AtuPlanInfoAddDto dto) {
    logger.info("-------------------->开始保存计划信息");
    //校验关联设备、应用是否为空
    checkDeviceAndApp(dto);
    logger.info("-------------------->校验关联设备、应用是否为空");
    if (dto.getPlanName().length()>30){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_NAME_TOO_LONG);
    }
    //校验正则表达式是否合法
    if (dto.getHasCorn()) {
      boolean expression = CronExpression.isValidExpression(dto.getCronExpr());
      if (!expression) {
        throw new PlatformRuntimeException(ExecPlanError.PLAN_CORN_ERROR);
      }
      //校验最近执行时间是否1s内
      List<Date> result = ScheduleUtils.getNextTriggerTimes(dto.getCronExpr(), 3);
      boolean time1 = false;
      boolean time2 = false;
      if(!CollUtil.isEmpty(result)){
        if(result.size()>1){
          time1 = (result.get(1).getTime()-result.get(0).getTime()) < 2000L;
        }
        if(result.size()>2){
          time2 = (result.get(2).getTime()-result.get(1).getTime()) < 2000L;
        }
        if(time1 || time2){
          throw new PlatformRuntimeException(ExecPlanError.PLAN_CORN_TIME_TOO_LESS);
        }
      }
    }
    logger.info("-------------------->基础数据与执行策略校验");
    AtuPlanScriptLinkMd linkMd = new AtuPlanScriptLinkMd();
    linkMd.setPlanId(dto.getId());
    linkMd.setUserId(NKSecurityContext.getUserId());

    List<AtuPlanScriptLinkMd> mdList = this.planScriptLinkMdService.query(linkMd);
    List<String> scriptList = new ArrayList<>();
    if (mdList != null && mdList.size() > 0) {
      mdList.forEach(one -> {
        scriptList.add(one.getScriptId());
      });
    }else {
      throw new PlatformRuntimeException(ExecPlanError.PLAN_SCRIPT_LINK_IS_NULL);
    }
    AtuScriptInfoFeignDto feignDto = new AtuScriptInfoFeignDto();
    feignDto.setScriptId(scriptList);
    feignDto.setAppLists(dto.getAppList());
    feignDto.setDeviceLists(dto.getDeviceList());
    //校验数据
    logger.info("-------------------->处理副表数据并组装数据调用feign");
    checkPlanData(feignDto);
    logger.info("-------------------->调用feign校验数据结束");
    return savePlanInfo(dto, mdList);
  }


  @Transactional
  private String savePlanInfo(AtuPlanInfoAddDto dto, List<AtuPlanScriptLinkMd> mdList){
    String uuid = dto.getId();
    AtuPlanInfo entity = new AtuPlanInfo();
    BeanUtils.copyProperties(dto, entity);
    //基础数据与执行策略校验
    AtuPlanInfo atuPlanInfo = new AtuPlanInfo();
    atuPlanInfo.setId(dto.getId());
    atuPlanInfo.setPlanName(dto.getPlanName());
    atuPlanInfo.setProjectId(dto.getProjectId());
    atuPlanInfo.setDeleted(false);

    Integer uniqueNum = this.atuPlanInfoService.checkNameUnique(atuPlanInfo);
    //校验是否重名
    if (uniqueNum != null) {
      throw new PlatformRuntimeException(ExecPlanError.PLAN_NAME_ERROR);
    }
    //判断是否有前置计划
    if (dto.getHasUpFront()) {
      if (StringUtils.isNotBlank(dto.getUpFrontPlanId())) {
        AtuPlanInfo countParams = new AtuPlanInfo();
        countParams.setId(dto.getUpFrontPlanId());
        countParams.setIsDeleted(1);
        long count = this.atuPlanInfoService.countdeleted(countParams);
        if (count > 0){
          throw new PlatformRuntimeException(ExecPlanError.PLAN_UPFRONT_IS_DELETE, dto.getPlanName());
        }
      }else {
        throw new PlatformRuntimeException(ExecPlanError.PLAN_UPFRONT_NULL);
      }
    }
    if (!entity.getHasCorn() && StrUtil.isNotBlank(entity.getCronExpr())){
      entity.setCronExpr("");
    }
    logger.info("-------------------->判断是否有前置计划");
    //判断是否存在
    AtuPlanInfo planInfo = this.atuPlanInfoService.findByPrimaryKey(uuid);
    if (planInfo != null) {
      entity.setPrincipalId(planInfo.getPrincipalId());
      if (!entity.getHasCorn()) {
        Scheduler scheduler = factoryBean.getScheduler();
        if (ScheduleUtils.checkExists(scheduler, entity.getId())) {
          ScheduleUtils.deleteJob(scheduler, entity.getId());
        }
        entity.setNextExecTime(null);
        // 增加更新下一次执行时间方法，updateByPrimaryKey会将null值过滤不更新
        this.atuPlanInfoService.updatePlanNextExecTime(entity);
      }
      entity.setUpdatedBy(NKSecurityContext.getUserId());
      entity.setUpdatedTime(new Date());
      this.atuPlanInfoService.updatePlanRetryCount(entity);
      this.atuPlanInfoService.updateByPrimaryKey(entity);
      entity.setPrincipalId(planInfo.getPrincipalId());
    } else {
      entity.setId(uuid);
      entity.setTenantId(NKSecurityContext.getTenantId());
      entity.setId(dto.getId());
      entity.setCaseChangeFlag(false);
      entity.setPrincipalId(NKSecurityContext.getUserId());
      entity.setCreatedBy(NKSecurityContext.getUserId());
      entity.setCreatedTime(new Date());
      entity.setUpdatedBy(NKSecurityContext.getUserId());
      entity.setUpdatedTime(new Date());
      this.atuPlanInfoService.insert(entity);
    }
    logger.info("-------------------->保存计划基础信息");
    //新增计划与软件之间的关联
    this.atuPlanAppLinkService.deleteAppByPlanId(dto.getId());
    if (dto.getAppList() != null && dto.getAppList().size() > 0) {
      List<AtuPlanAppLink> appLinkList = new ArrayList<>();
      StringBuffer sb = new StringBuffer();
      for (AppList list : dto.getAppList()) {
        if (StringUtils.isEmpty(list.getAppId())) {
          sb.append("【");
          sb.append(list.getAppPackage());
          sb.append("】");
        }
        AtuPlanAppLink appLink = new AtuPlanAppLink();
        BeanUtils.copyProperties(list, appLink);
        appLink.setId(UUIDUtil.create32UUID());
        appLink.setPlanId(uuid);
        appLinkList.add(appLink);
      }
      if (sb.length() > 0) {
        throw new PlatformRuntimeException(ExecPlanError.PLAN_APP_IS_NULL_ERROR, sb);
      }
      this.atuPlanAppLinkService.insertByBatch(appLinkList);
    }
    logger.info("-------------------->新增计划与软件之间的关联");
    //新增计划与设备之间的关联
    this.atuPlanDeviceLinkService.deleteDeviceIdByPlanId(dto.getId());
    if (dto.getDeviceList() != null && dto.getDeviceList().size() > 0) {
      List<AtuPlanDeviceLink> deviceLinkList = new ArrayList<>();
      for (DeviceList deviceList : dto.getDeviceList()) {
        AtuPlanDeviceLink deviceLink = new AtuPlanDeviceLink();
        BeanUtils.copyProperties(deviceList, deviceLink);
        deviceLink.setId(UUIDUtil.create32UUID());
        deviceLink.setPlanId(uuid);
        //设备类型;1-移动端 2-PC
        if (PlanConstant.DEVICE_TYPE_PC.equals(deviceLink.getDeviceType())) {
          deviceLink.setPlatformType(null);
        }
        deviceLinkList.add(deviceLink);
      }
      this.atuPlanDeviceLinkService.insertByBatch(deviceLinkList);
    }
    logger.info("-------------------->新增计划与设备之间的关联");
    // 判断是否定时执行，是则将计划信息放入调度器
    if (entity.getHasCorn() && entity.getEnabled()) {
      Scheduler scheduler = factoryBean.getScheduler();
      ScheduleUtils.createScheduleJob(scheduler, entity);
      // 更新下一次执行时间
      updatePlanNextRunTime(entity);
    }
    logger.info("-------------------->判断是否定时执行，是则将计划信息放入调度器");
    // 保存用例集
    savePlanScriptData(uuid, mdList);
    logger.info("-------------------->保存用例集");
    // 保存环境
    saveEnv(uuid, dto.getEnvList());
    logger.info("-------------------->保存环境");
    return uuid;
  }



  private void savePlanScriptData(String planId,List<AtuPlanScriptLinkMd> mdList){
    List<AtuPlanScriptLink> scriptLinks = new ArrayList<>();
    for (AtuPlanScriptLinkMd linkMd : mdList) {
      AtuPlanScriptLink scriptLink = new AtuPlanScriptLink();
      BeanUtils.copyProperties(linkMd,scriptLink);
      scriptLinks.add(scriptLink);
    }
    //先删除原来的
    AtuPlanScriptLink link = new AtuPlanScriptLink();
    link.setPlanId(planId);
    this.planScriptLinkService.deleteByExample(link);
    //新增关联
    this.planScriptLinkService.insertLinkList(scriptLinks);
  }


  private void saveEnv(String planId, List<String> envList) {
    if (envList == null || envList.isEmpty()) {
      return;
    }
    Long projectId = RequestUtil.getProjectId();
    ResponseVO<List<Environment>> responseVO = apiAtsEnviromentFeign.listByProject(projectId);
    List<Environment> data = responseVO.getData();
    List<String> envIds = data.stream().map(e -> e.getId().toString()).collect(Collectors.toList());
    for (String id : envList) {
      boolean contains = envIds.contains(id);
      if (!contains) {
        throw new InvalidException("环境信息不正确，请修改环境");
      }
    }
    //删除原来关联的环境
    AtuPlanEnvLink envLink = new AtuPlanEnvLink();
    envLink.setPlanId(planId);
    planEnvLinkService.deleteByExample(envLink);
    List<AtuPlanEnvLink> inserList = new ArrayList<>();
    int sort = 0;
    for (String envId : envList) {
      AtuPlanEnvLink link = new AtuPlanEnvLink();
      link.setPlanId(planId);
      link.setEnvId(envId);
      sort = sort + 1;
      link.setSort(sort);
      inserList.add(link);
    }
    if (envList.size() > 0) {
      this.planEnvLinkService.insertLinkList(inserList);
    }
  }


  @Override
  public AtuPlanScriptResultDto queryLinkMdPage(String planId) {
    if (StringUtils.isBlank(planId)){
      return null;
    }
    List<AtuPlanScriptLinkDto> result = this.planScriptLinkMdService.queryLinkMdPage(planId,new ArrayList<>());
    //feign调用查询脚本类型
    List<String> scriptList = new ArrayList<>();
    result.forEach(one -> {
      scriptList.add(one.getScriptId());
    });
    return queryScriptData(scriptList,result);
  }

  private AtuPlanScriptResultDto queryScriptData(List<String> scriptList,List<AtuPlanScriptLinkDto> result) {
    AtuPlanScriptResultDto resultDto = new AtuPlanScriptResultDto();
    ResultWrapper<AtuScriptInfoResultDto> wrapper = scriptCaseFeignClient.checkScriptData(scriptList);
    //脚本名称
    Map<String, String> scriptMap = wrapper.getData().getScriptMap();
    //校验结果
    BeanUtils.copyProperties(wrapper.getData(),resultDto);
    //查询不执行的测试意图
    List<AtuPlanScriptLinkDto> simpleList = new ArrayList<>();
    List<AtuPlanScriptLinkDto> sceneList = new ArrayList<>();
    for (AtuPlanScriptLinkDto record : result) {
      String scriptName = scriptMap.get(record.getScriptId());
      record.setScriptName(scriptName);
      if (record.getScriptType().equals(MsgConstant.ATU_PLAN_SCRIPT_LINK_DTO_SCRIPT_TYPE)){
        sceneList.add(record);
      }else {
        simpleList.add(record);
      }
    }
    resultDto.setSceneList(sceneList);
    resultDto.setSimpleList(simpleList);
    return resultDto;
  }

  @Override
  public Integer deletePlanScriptLinkMd(AtuPlanScriptDeleteDto dto) {
    //先删除md表中数据集的数据
    dto.setUserId(NKSecurityContext.getUserId());
    return planScriptLinkMdService.deleteLinkMd(dto);
  }

  @Override
  public List<String> queryPlanScriptList(String planId) {
    AtuPlanScriptLinkMd linkMd = new AtuPlanScriptLinkMd();
    linkMd.setPlanId(planId);
    linkMd.setUserId(NKSecurityContext.getUserId());
    List<AtuPlanScriptLinkMd> query = planScriptLinkMdService.query(linkMd);
    List<String> scriptList = new ArrayList<>();
    query.forEach( one -> {
      scriptList.add(one.getScriptId());
    });
    return scriptList;
  }

  @Override
  public Integer updatePlanStatus(String planId) {
    AtuPlanInfo info = new AtuPlanInfo();
    info.setId(planId);
    info.setCaseChangeFlag(false);
    return atuPlanInfoService.updateByPrimaryKey(info);
  }

  @Override
  public Boolean checkScript(List<String> ids) {
    //主表
    boolean result = false;
    long count = planScriptLinkService.countByScriptIdList(ids);
    if (count > 0) {
      result = true;
    }
    return result;
  }

  @Override
  public AtuPlanScriptPageDto queryScriptPage(QueryByPage<AtuPlanScriptQueryDto> queryByPage) {
    AtuPlanScriptPageDto pageDto = new AtuPlanScriptPageDto();
    if (StringUtils.isBlank(queryByPage.getQuery().getPlanId())) {
      return pageDto;
    }
    List<AtuPlanScriptLinkDto> dtoList = this.planScriptLinkMdService.queryLinkMdPage(queryByPage.getQuery().getPlanId(),new ArrayList<>());
    if (dtoList == null ||  dtoList.size()==0){
      return pageDto;
    }
    List<String> scriptList = new ArrayList<>();
    dtoList.forEach( one -> {scriptList.add(one.getScriptId());});
    PlanPageQueryDto queryDto = new PlanPageQueryDto();
    queryDto.setAllList(scriptList);
    if (!StringUtils.isBlank(queryByPage.getQuery().getSetName())) {
      queryDto.setSetName(queryByPage.getQuery().getSetName());
    }
    //查询所有脚本类型
    PlanScriptPageDto planScriptPageDto = scriptCaseFeignClient.queryScriptByPlan(queryDto);
    Pagination<AtuCaseScriptResultDto> resultDto = new DefaultPagination<>();
    pageDto.setInfoDto(planScriptPageDto.getInfoDto());
    List<String> resultList = new ArrayList<>();
    if (planScriptPageDto.getResultDtoList() != null && planScriptPageDto.getResultDtoList().size()>0) {
      planScriptPageDto.getResultDtoList().forEach( one ->{resultList.add(one.getScriptId());});
    }else {
      pageDto.setPagination(resultDto);
      return pageDto;
    }
    QueryByPage<PlanScriptQueryPageDto> planPage = new QueryByPage<>();
    BeanUtils.copyProperties(queryByPage,planPage);
    PlanScriptQueryPageDto dto = new PlanScriptQueryPageDto();
    dto.setPlanId(queryByPage.getQuery().getPlanId());
    dto.setScriptList(resultList);
    planPage.setQuery(dto);
    Pagination<String> pagination = this.planScriptLinkMdService.queryScriptLink(planPage);
    List<AtuCaseScriptResultDto> resultDtoList = new ArrayList<>();
    if (pagination.getRecords().size()>0) {
      pagination.getRecords().forEach( one ->{
        for (AtuCaseScriptResultDto scriptResultDto : planScriptPageDto.getResultDtoList()) {
          if (scriptResultDto.getId().equals(one)) {
            String principalName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, scriptResultDto.getPrincipalId());
            String updatedName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, scriptResultDto.getUpdatedBy());
            scriptResultDto.setPrincipalName(principalName);
            scriptResultDto.setUpdatedName(updatedName);
            resultDtoList.add(scriptResultDto);
          }
        }
      });
    }
    resultDto.setPageNo(pagination.getPageNo());
    resultDto.setRecordCount(pagination.getRecordCount());
    resultDto.setPageSize(pagination.getPageSize());
    resultDto.setRecords(resultDtoList);
    pageDto.setPagination(resultDto);
    return pageDto;
  }

  @Override
  public BatchScriptReportDto queryPlanBatchReport(String batchId) {
    BatchScriptReportDto reportDto = new BatchScriptReportDto();
    // 批次基本信息
    BatchInfoReportDto batchInfo = new BatchInfoReportDto();
    AtuPlanBatchDetailDto batchDetailDto = this.atuPlanBatchService.queryBatchDetailById(batchId);
    if (batchDetailDto == null){
      throw new PlatformRuntimeException(ExecPlanError.BATCH_INFO_IS_NULL);
    }

    batchInfo.setPlanName(batchDetailDto.getPlanName());
    batchInfo.setCreatedTime(batchDetailDto.getCreatedTime());
    batchInfo.setEndTime(batchDetailDto.getEndTime());
    if (batchDetailDto.getStartTime() != null && batchDetailDto.getEndTime() != null) {
      long between = batchDetailDto.getEndTime().getTime() - batchDetailDto.getStartTime().getTime();
      batchInfo.setElapsedTime(DateUtil.formatBetween(between, BetweenFormatter.Level.SECOND));
    }
    batchInfo.setScriptTotal(batchDetailDto.getScriptNum());
    batchInfo.setCaseTotal(batchDetailDto.getTaskTotal());
    AtuPlanBatchDeviceLink deviceLink = new AtuPlanBatchDeviceLink();
    deviceLink.setBatchId(batchId);
    long deviceCount = this.atuPlanBatchDeviceLinkService.count(deviceLink);
    batchInfo.setTestDeviceNum(deviceCount);
    batchInfo.setRetryDeviceNum(deviceCount);
    batchInfo.setTestBy(entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, batchDetailDto.getExecutedBy()));
    batchInfo.setTriggerType(batchDetailDto.getTriggerType());
    String envId = atuPlanTaskApiService.queryEnvIdByBatchId(batchDetailDto.getId());
    String envNameObj = envNameUtils.getEnvName(envId);
    String envName = "";
    if (envNameObj != null) {
      envName = envNameObj.toString();
    }
    batchInfo.setEnvName(envName);
    reportDto.setBatchInfo(batchInfo);

    // APP基本信息
    Map<String, String> appNameMap = new HashMap<>();
    List<AppBaseInfoDto> appBaseInfoList = getAppBaseInfo(batchId, appNameMap);
    reportDto.setAppBaseInfoList(appBaseInfoList);

    // app和设备性能相关信息
    List<BatchAppInfoDto> appPerList = getBatchAppInfo(batchId, batchDetailDto.getTenantId(), appNameMap);
    reportDto.setAppPerList(appPerList);

    String clusterKeyPrefix = RedisConstant.CLUSTER_KEY_PREFIX + batchId.substring(0,4) +
              RedisConstant.CLUSTER_KEY_SUFFIX;
    // 脚本执行情况
    Map<String, Integer> scriptExecMap = new HashMap<>();
    // 执行成功
    getCacheData(batchId, PlanConstant.TASK_EXECUTE_SUCCESS_STATUS, MsgConstant.STATUS_CACHEKEY_SUCCESSNUM, scriptExecMap,
            batchDetailDto.getSuccessNum(), clusterKeyPrefix);
    // 执行失败
    getCacheData(batchId, PlanConstant.TASK_EXECUTE_FAIL_STATUS, MsgConstant.STATUS_CACHEKEY_FAILNUM, scriptExecMap,
            batchDetailDto.getExecFailNum(), clusterKeyPrefix);
    // 取消
    getCacheData(batchId, PlanConstant.TASK_CANCEL_STATUS, MsgConstant.STATUS_CACHEKEY_CANCELNUM, scriptExecMap,
            batchDetailDto.getCancelNum(), clusterKeyPrefix);
    // 超时
    getCacheData(batchId, PlanConstant.TASK_TIMEOUT_STATUS, MsgConstant.STATUS_CACHEKEY_TIMEOUTNUM, scriptExecMap,
            batchDetailDto.getTimeoutNum(), clusterKeyPrefix);
    reportDto.setScriptExecMap(scriptExecMap);

    // 查询脚本执行详情
    reportDto.setScriptExecList(getScriptExecDetailList(batchId));
    return reportDto;
  }

  private List<AppBaseInfoDto> getAppBaseInfo(String batchId, Map<String, String> appNameMap){
    List<AppBaseInfoDto> appBaseInfoList = new ArrayList<>();
    // 查询批次任务中的app id集合
    List<String> appIdList = this.atuPlanTaskApiService.queryAppIdListByBatchId(batchId);
    // 查询场景节点任务app id集合
    List<String> sceneAppIdList = this.atuPlanSceneCaseTaskService.queryAppIdListByBatchId(batchId);
    if (CollUtil.isEmpty(appIdList)){
      appIdList = sceneAppIdList;
    }else if(CollUtil.isNotEmpty(sceneAppIdList)){
      appIdList.addAll(sceneAppIdList);
    }
    if (CollUtil.isNotEmpty(appIdList)) {
      try {
        ResultWrapper<List<AppDetailDto>> resultWrapper = publicFeignClient.queryAppByIds(appIdList);
        if (resultWrapper != null && resultWrapper.getData() != null) {
          List<AppDetailDto> appList = resultWrapper.getData();
          if (CollUtil.isNotEmpty(appList)) {
            for (AppDetailDto detailDto : appList) {
              appNameMap.put(detailDto.getId(), detailDto.getAppName());
              AppBaseInfoDto baseInfoDto = new AppBaseInfoDto();
              baseInfoDto.setAppName(detailDto.getAppName());
              baseInfoDto.setAppVersion(detailDto.getBuildVersion());
              baseInfoDto.setAppPackage(detailDto.getPackageName());
              baseInfoDto.setVersionDesc(detailDto.getRemarks());
              baseInfoDto.setChannelName(detailDto.getPlatform());
              appBaseInfoList.add(baseInfoDto);
            }
          }
        }
      } catch (Exception e) {
        logger.error("查询应用信息异常", e);
      }
    }
    return appBaseInfoList;
  }

  /**
   * 查询脚本执行详情
   * @param batchId 批次id
   * @return 结果集
   */
  private List<BatchScriptExecDetailDto> getScriptExecDetailList(String batchId){
    List<BatchScriptExecDetailDto> scriptExecList = new ArrayList<>();
    AtuPlanTask taskParams = new AtuPlanTask();
    taskParams.setBatchId(batchId);
    SqlOrderBy orderBy = new SqlOrderBy();
    orderBy.desc("created_time");
    List<AtuPlanTask> taskList = this.atuPlanTaskApiService.query(taskParams, orderBy);
    if (CollUtil.isEmpty(taskList)){
      return scriptExecList;
    }
    Map<String, String> deviceNameMap = getDeviceInfo(taskList);
    logger.debug("deviceNameMap => {}", JSONUtil.toJsonStr(deviceNameMap));
    for (AtuPlanTask atuPlanTask : taskList) {
      BatchScriptExecDetailDto detailDto = new BatchScriptExecDetailDto();
      BeanUtils.copyProperties(atuPlanTask, detailDto);

      Object resultCode = redisTemplate.opsForValue().get("UITEST-ScriptCode:" + atuPlanTask.getScriptId());
      if (resultCode!= null && !resultCode.equals("")){
        detailDto.setScriptCode(resultCode.toString());
      }else {
        Map<String,String> scriptMap = new HashMap<>();
        scriptMap.put(MsgConstant.ID,atuPlanTask.getScriptId());
        scriptMap.put(MsgConstant.TYPE,atuPlanTask.getCaseType());
        try {
          String scriptCode = scriptCaseFeignClient.queryScriptCode(scriptMap);
          if (!StringUtils.isBlank(scriptCode)) {
            detailDto.setScriptCode(scriptCode);
          }
        }catch (Exception e){
          logger.error("查询脚本编号异常", e);
        }
      }
      detailDto.setElapsedTime("-");
      if (atuPlanTask.getStartTime() != null && atuPlanTask.getEndTime() != null) {
        long between = atuPlanTask.getEndTime() - atuPlanTask.getStartTime();
        detailDto.setElapsedTime(between + "");
      }

      // 查询设备id
      String deviceName = "";
      if (PlanConstant.SCRIPT_TYPE_SCENE.equals(detailDto.getCaseType())){
        Set<String> sceneDeviceIdSet = atuPlanSceneCaseTaskService.queryDeviceIdListByTaskId(atuPlanTask.getId());
        logger.debug("场景节点设备数量：{}", sceneDeviceIdSet.size());
        if (CollUtil.isNotEmpty(sceneDeviceIdSet)){
          logger.debug("场景节点设备id：{}", JSONUtil.toJsonStr(sceneDeviceIdSet));
          StringBuilder deviceNames = new StringBuilder();
          for (String deviceId : sceneDeviceIdSet) {
            if (StrUtil.isBlank(deviceId)){
              continue;
            }
            if (deviceNameMap.containsKey(deviceId)) {
              deviceNames.append(deviceNameMap.get(deviceId)).append(";");
            }
          }
          if (deviceNames.length() > 0) {
            deviceName = deviceNames.substring(0, deviceNames.lastIndexOf(";"));
          }
        }
      }else {
        if (deviceNameMap.containsKey(atuPlanTask.getDeviceId())) {
          deviceName = deviceNameMap.get(atuPlanTask.getDeviceId());
        }
      }
      detailDto.setDeviceName(deviceName);
      scriptExecList.add(detailDto);
    }
    return scriptExecList;
  }

  private Map<String, String> getDeviceInfo(List<AtuPlanTask> taskList){
    // 查询设备信息
    Set<String> deviceIdSet = new HashSet<>();
    for (AtuPlanTask planTask : taskList) {
      // 判断是否场景用例，场景用例则查询多个设备信息
      if (ObjectUtil.equal(PlanConstant.SCRIPT_TYPE_SCENE, planTask.getCaseType())) {
        // 查询场景任务设备ID列表
        Set<String> sceneDeviceIdSet = atuPlanSceneCaseTaskService.queryDeviceIdListByTaskId(planTask.getId());
        deviceIdSet.addAll(sceneDeviceIdSet);
      } else {
        deviceIdSet.add(planTask.getDeviceId());
      }
    }
    return getDeviceNameMap(deviceIdSet);
  }

  /**
   * 根据设备id查询设备名称集合
   * @param deviceIdSet 设备id集合
   * @return 结果
   */
  private Map<String, String> getDeviceNameMap(Set<String> deviceIdSet){
    Map<String, String> deviceNameMap = new HashMap<>();
    try {
      List<Map<String, Object>> deviceInfoList = deviceFeignClient.queryDeviceInfo(deviceIdSet);
      if (CollUtil.isNotEmpty(deviceInfoList)) {
        for (Map<String, Object> deviceInfoMap : deviceInfoList) {
          deviceNameMap.put(deviceInfoMap.get(MsgConstant.ID).toString(), deviceInfoMap.get(MsgConstant.DEVICE_NAME).toString());
        }
      }
    } catch (Exception e) {
      logger.error("查询设备信息异常", e);
    }
    return deviceNameMap;
  }

  private void getCacheData(String batchId, String statusCacheKey, String statusMapKey,
                            Map<String, Integer> scriptExecMap, int statusNum, String clusterKeyPrefix){

    Object obj = redisTemplate.opsForHash().get(clusterKeyPrefix + RedisConstant.BATCH_SCRIPT_SUM_KEY + batchId,
            clusterKeyPrefix + statusCacheKey);
    if (obj != null){
      int num = Integer.parseInt(obj.toString());
      scriptExecMap.put(statusMapKey, num);
    }else{
      scriptExecMap.put(statusMapKey, statusNum);
    }
  }

  @Override
  public String updatePrincipal(AtuPlanPrincipalDto dto) {
    Long requestUserId = RequestUtil.getRequestUserId();
    dto.setUpdatedBy(String.valueOf(requestUserId));
    dto.setUpdatedTime(new Date());
    atuPlanInfoService.updatePrincipal(dto);
    return "success";
  }

  @Override
  public void exportBatchExecResult(HttpServletResponse response, String batchId) {
      AtuPlanBatchDetailDto atuPlanBatchDetailDto = this.atuPlanBatchService.queryBatchDetailById(batchId);
      if (atuPlanBatchDetailDto == null){
          throw new PlatformRuntimeException(ExecPlanError.BATCH_INFO_IS_NULL);
      }
      String title = atuPlanBatchDetailDto.getPlanName() + "第" + atuPlanBatchDetailDto.getBatch() + "批次汇总报告";
      List<BatchScriptExecDetailDto> scriptExecDetailList = getScriptExecDetailList(batchId);
      if (CollUtil.isNotEmpty(scriptExecDetailList)){
        Map<String, String> scriptTypeDict = getDictItem(DictConstant.SCRIPT_CASE_TYPE);
        Map<String, String> taskStatusDict = getDictItem(DictConstant.PLAN_TASK_STATUS);
        for (BatchScriptExecDetailDto detailDto : scriptExecDetailList) {
          if (scriptTypeDict.containsKey(detailDto.getCaseType())){
            detailDto.setCaseType(scriptTypeDict.get(detailDto.getCaseType()));
          }
          if (taskStatusDict.containsKey(detailDto.getStatus())){
            detailDto.setStatus(taskStatusDict.get(detailDto.getStatus()));
          }
        }
      }

      ExcelWriter writer = ExcelUtil.getWriter();
      writer.addHeaderAlias("scriptCode", "脚本编号");
      writer.addHeaderAlias("scriptName", "脚本名称");
      writer.addHeaderAlias("caseName", "用例名称");
      writer.addHeaderAlias("caseType", "用例类型");
      writer.addHeaderAlias("elapsedTime", "耗时(毫秒)");
      writer.addHeaderAlias("deviceName", "执行设备");
      writer.addHeaderAlias("status", "执行状态");
      writer.addHeaderAlias("errorMsg", "失败原因");
      writer.setOnlyAlias(true);
      writer.merge(8, title);
      writer.write(scriptExecDetailList, true);

      response.setContentType(MsgConstant.CONTENT_VNDMS_EXCEL_UTF8);

      ServletOutputStream output = null;
      try {
        String fileName = URLEncoder.encode(title + ".xls", "utf-8");
        response.setHeader(MsgConstant.HTTP_SER_RESPONSE_KEY_CONTENT_DISPOSITION, MsgConstant.HTTP_SER_RESPONSE_VALUE_ATTACHMENT + fileName);
        output = response.getOutputStream();
      }catch (Exception e){
          logger.error("导出数据异常", e);
      }
      writer.flush(output, true);
      writer.close();
      IoUtil.close(output);
  }

  private Map<String, String> getDictItem(String catalogId){
    Map<String, String> data = new HashMap<>();
    DictCatalogSearchDto searchDto = new DictCatalogSearchDto();
    searchDto.setTenantId(NKSecurityContext.getTenantId());
    List<String> dictTypeList = new ArrayList<>();
    dictTypeList.add(catalogId);
    searchDto.setResultList(dictTypeList);
    try {
      ResultWrapper<Map<String, Object>> queryResult = publicFeignClient.query(searchDto);
      if (queryResult.isSuccess() && CollUtil.isNotEmpty(queryResult.getData())){
        Map<String, Object> resultData = queryResult.getData();
        List<LinkedHashMap<String, Object>> dictItemList = (List<LinkedHashMap<String, Object>>) resultData.get(catalogId);
        if (CollUtil.isEmpty(dictItemList)){
          return data;
        }
        for (LinkedHashMap<String, Object> linkedHashMap : dictItemList) {
          String itemCode = linkedHashMap.get(MsgConstant.ITEM_CODE).toString();
          String itemName = linkedHashMap.get(MsgConstant.ITEM_NAME).toString();
          data.put(itemCode, itemName);
        }
        return data;
      }
    }catch (Exception e){
      logger.error("获取字典[" + catalogId + "]数据异常", e);
    }
    return data;
  }

  @Override
  public void exportBatchReport(HttpServletResponse response, AtuPlanExportDto exportDto) {
    // 设置response参数，可以打开下载页面
    response.reset();
    response.setCharacterEncoding(MsgConstant.CHARATER_ENCODING_UTF8);
    // 定义输出类型
    response.setContentType(MsgConstant.CONTENT_PDF_UTF8);
    // 设置名称
    AtuPlanBatchDetailDto atuPlanBatchDetailDto = this.atuPlanBatchService.queryBatchDetailById(exportDto.getBatchId());
    if (atuPlanBatchDetailDto == null){
      throw new PlatformRuntimeException(ExecPlanError.BATCH_INFO_IS_NULL);
    }
    String title = atuPlanBatchDetailDto.getPlanName() + "第" + atuPlanBatchDetailDto.getBatch() + "批次汇总报告";

    // 查询填充的基础数据
    Map<String, Object> dataMap = getTextFillData(atuPlanBatchDetailDto);

    try {
      String fileName = URLEncoder.encode(title + MsgConstant.ENC_TITLE_PDF, MsgConstant.ENC_UTF8);
      response.setHeader(MsgConstant.HTTP_SER_RESPONSE_KEY_CONTENT_DISPOSITION, MsgConstant.HTTP_SER_RESPONSE_VALUE_ATTACHMENT + fileName);
      ServletOutputStream out = response.getOutputStream();
      // 模板路径记
      this.fillTemp(dataMap, exportDto, atuPlanBatchDetailDto.getTenantId(), out);
    } catch (IOException e) {
      logger.error("报告导出异常，e,{}",e);
    }
  }

    @Override
    public int updateStatusByLastBatchId(String lastBatchId, String planStatus) {
        return atuPlanInfoService.updatePlanByLastBatchId(lastBatchId, planStatus);
    }

    /**
   * 获取文本填充数据
   * @param atuPlanBatchDetailDto 计划批次信息
   * @return 结果集
   */
  private Map<String, Object> getTextFillData(AtuPlanBatchDetailDto atuPlanBatchDetailDto){
    Map<String, Object> dataMap = new HashMap<>(16);

    dataMap.put(MsgConstant.DATA_MAP_START_TIME, DateUtil.format(atuPlanBatchDetailDto.getCreatedTime(), PlanConstant.DATETIME_FORMAT));
    Map<String, String> dictItemMap = getDictItem(DictConstant.PLAN_TRIGGER_TYPE);
    dataMap.put(MsgConstant.DATA_MAP_TRIGGER_TYPE, atuPlanBatchDetailDto.getTriggerType());
    if (dictItemMap.containsKey(atuPlanBatchDetailDto.getTriggerType())) {
      dataMap.put(MsgConstant.DATA_MAP_TRIGGER_TYPE, dictItemMap.get(atuPlanBatchDetailDto.getTriggerType()));
    }
    dataMap.put(MsgConstant.DATA_MAP_ELAPSED_TIMED, DateUtil.formatBetween(atuPlanBatchDetailDto.getEndTime().getTime() -
            atuPlanBatchDetailDto.getStartTime().getTime(), BetweenFormatter.Level.SECOND));
    // 暂定一个环境
    String envId = atuPlanTaskApiService.queryEnvIdByBatchId(atuPlanBatchDetailDto.getId());
    String envNameObj = envNameUtils.getEnvName(envId);
    String envName = "";
    if (envNameObj != null) {
      envName = envNameObj.toString();
    }
    dataMap.put(MsgConstant.DATA_MAP_ENV_NAME, envName);
    dataMap.put(MsgConstant.DATA_MAP_END_TIME, DateUtil.format(atuPlanBatchDetailDto.getEndTime(), PlanConstant.DATETIME_FORMAT));
    dataMap.put(MsgConstant.DATA_MAP_CASE_TOTAL, atuPlanBatchDetailDto.getTaskTotal());
    dataMap.put(MsgConstant.DATA_MAP_SCRIPT_TOTAL, atuPlanBatchDetailDto.getScriptNum());

    AtuPlanBatchDeviceLink deviceLink = new AtuPlanBatchDeviceLink();
    deviceLink.setBatchId(atuPlanBatchDetailDto.getId());
    long deviceCount = this.atuPlanBatchDeviceLinkService.count(deviceLink);
    dataMap.put(MsgConstant.DATA_MAP_TEST_DEVICE_NUM, deviceCount);
    // 暂时定重试设备数量即该批次执行的设备数量
    dataMap.put(MsgConstant.DATA_MAP_RETRY_DEVICE_NUM, deviceCount);
    // 因为模板定义的包名字段为package，而package为java关键字，故此处需转换
    dataMap.put(MsgConstant.DATA_MAP_PACKAGE, dataMap.get(MsgConstant.DATA_MAP_APP_PACKAGE));
    return dataMap;
  }

  private List<BatchAppInfoDto> getBatchAppInfo(String batchId, String tenantId, Map<String, String> appNameMap){
    List<BatchAppInfoDto> resultList = new ArrayList<>();
    AtuPlanBatch planBatch = atuPlanBatchService.findByPrimaryKey(batchId);
    String filePath = planBatch.getPerDataPath();
    if (StrUtil.isBlank(filePath)){
      return resultList;
    }
    List<BatchAppInfoDto> dtoList = downloadAppProInfoList(tenantId, filePath);
    if (CollUtil.isEmpty(dtoList)){
      return dtoList;
    }
    Set<String> deviceIdSet = dtoList.stream().map(BatchAppInfoDto::getDeviceId).collect(Collectors.toSet());
    // 查询设备信息
    Map<String, String> deviceNameMap = getDeviceNameMap(deviceIdSet);

    for (BatchAppInfoDto appInfoDto : dtoList) {
      if (CollUtil.isNotEmpty(deviceNameMap)){
        appInfoDto.setDeviceName(appInfoDto.getDeviceId());
        if (deviceNameMap.containsKey(appInfoDto.getDeviceId())) {
          appInfoDto.setDeviceName(deviceNameMap.get(appInfoDto.getDeviceId()));
        }
      }
      if (appNameMap.containsKey(appInfoDto.getAppId())) {
        appInfoDto.setAppName(appNameMap.get(appInfoDto.getAppId()));
      }

    }
    return dtoList;
  }

  public List<BatchAppInfoDto> downloadAppProInfoList(String tenantId, String filePath){
    List<BatchAppInfoDto> resultList = new ArrayList<>();
    JSONArray jsonArray = null;
    File file = null;
    try {
      String path = filePath.replace(tenantId, "");
      path = path.replace("_","/");
      file = simpleStorageService.downloadAsFile(tenantId, path);
      byte[] bytes = FileUtil.readBytes(file);
      if (bytes != null){
        String jsonStr = new String(bytes, StandardCharsets.UTF_8);
        jsonArray = JSONUtil.parseArray(jsonStr);
      }
    } catch (Exception e) {
      logger.error("读取结果文件失败", e);
    } finally {
      // 删除下载的临时文件
      if (file != null) {
        if (!file.delete()) {
          logger.error("临时文件删除失败， file => " + file.getAbsolutePath());
        }
      }
    }
    if (jsonArray == null){
      return resultList;
    }
    resultList = JSON.parseArray(JSON.toJSONString(jsonArray), BatchAppInfoDto.class);
    return resultList;
  }

    @Override
    public Boolean checkDataSetUsedByScriptIdList(List<String> scriptIdList) {
      //主表
      boolean result = false;
      long count = planScriptLinkService.countByScriptIdList(scriptIdList);
      if (count > 0) {
        result = true;
      }
      return result;
    }

  @Override
  public Pagination<AtuPlanInfoDetailDto> pageUpFrontPlan(QueryByPage<AtuPlanInfoPageDto> queryByPage) {
    queryByPage.getQuery().setTenantId(NKSecurityContext.getTenantId());
    Pagination<AtuPlanInfoDetailDto> query = atuPlanInfoService.queryUpFrontPlan(queryByPage);
    for (AtuPlanInfoDetailDto record : query.getRecords()) {
      String projectName = entityNameCache.translateById(EntityNameCatalog.MONSTER_SYSTEM, record.getProjectId());
      String principalName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getPrincipalId());
      record.setProjectName(projectName);
      record.setLastBatchScriptSumMap(getBatchScriptSumData(record));
      record.setPrincipalId(principalName);
      String createdBy = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getCreatedBy());
      String updatedBy = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, record.getUpdatedBy());
      record.setCreatedBy(createdBy);
      record.setUpdatedBy(updatedBy);
      //判断是否有下一次执行时间
      if (record.getHasCorn()) {
        Date date = ScheduleUtils.getNextOneTriggerTime(record.getCronExpr());
        if (date == null) {
          AtuPlanInfo atuPlanInfo = new AtuPlanInfo();
          atuPlanInfo.setId(record.getId());
          atuPlanInfo.setNextExecTime(null);
          this.atuPlanInfoService.updatePlanNextExecTime(atuPlanInfo);
          record.setNextExecTime(null);
        }
      }
    }
    return query;
  }

  /**
   * ATS获取执行计划中脚本执行情况
   * @return
   */
    @Override
    public List<TestPlanScriptExecutionDetailDTO> scriptExecutionDetail() {
      String code = "compliantTestPlanTag";
      ResponseVO<Map<String, String>> responseVO = apiMonsterDictService.findItemsKVByCatalogCode(code, false);
      if (!BaseDefineDependencyConstants.SYSTEM_RESPONSE_CODE_SUCCESS.equals(responseVO.getCode())) {
        logger.error("获取字典配置异常,响应信息：" + JSON.toJSONString(responseVO));
        throw new InvalidException("获取字典配置异常");
      }
      Map<String, String> data = responseVO.getData();
      if (CollectionUtils.isEmpty(data)) {
        return new ArrayList<>();
      }
      List<String> tagNames = new ArrayList<>(data.values());
      List<TagDto> tagDtos = apiTagService.getTagDtosByNameAndType(tagNames, 4);
      if (tagDtos == null) {
        logger.error("根据标签和类型获取关联计划Id异常,响应信息：" + JSON.toJSONString(responseVO));
        throw new InvalidException("根据标签和类型获取关联计划Id异常");
      }
      if (CollectionUtils.isEmpty(tagDtos)) {
        return new ArrayList<>();
      }
      Map<String, List<TagDto>> tagMapByObjId = tagDtos.stream().collect(Collectors.groupingBy(TagDto::getObjectId));
      Set<String> objIdsSet = tagDtos.stream().map(TagDto::getObjectId).collect(Collectors.toSet());
      List<AtuPlanInfo> atuPlanInfos = atuPlanInfoService.queryPlanInfoByIds(new ArrayList<>(objIdsSet));
      if (CollectionUtils.isEmpty(atuPlanInfos)) {
        return new ArrayList<>();
      }
      Set<Long> principalIds = new HashSet<>();
      List<TestPlanScriptExecutionDetailDTO> relust = new ArrayList<>();
      Map<String, Set<String>> scriptRefNumMap = new HashMap<>();
      for (AtuPlanInfo atuPlanInfo : atuPlanInfos) {
        AtuPlanScriptLink atuPlanScriptLink = new AtuPlanScriptLink();
        atuPlanScriptLink.setPlanId(atuPlanInfo.getId());
        List<AtuPlanScriptLink> atuPlanScriptLinkList = planScriptLinkService.query(atuPlanScriptLink);
        PlanPageQueryDto queryDto = new PlanPageQueryDto();
        Set<String> collect = atuPlanScriptLinkList.stream().map(AtuPlanScriptLink::getScriptId).collect(Collectors.toSet());
        queryDto.setAllList(new ArrayList<>(collect));
        
        //查询所有脚本类型
        PlanScriptPageDto planScriptPageDto = scriptCaseFeignClient.queryScriptByPlan(queryDto);
        List<AtuCaseScriptResultDto> resultDtoList = planScriptPageDto.getResultDtoList();
        for (AtuCaseScriptResultDto atuCaseScriptResultDto : resultDtoList) {
          String principalName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, atuCaseScriptResultDto.getPrincipalId());
          String updatedName = entityNameCache.translateById(EntityNameCatalog.MONSTER_USER, atuCaseScriptResultDto.getUpdatedBy());
          atuCaseScriptResultDto.setPrincipalName(principalName);
          atuCaseScriptResultDto.setUpdatedName(updatedName);
        }

        Long testPlanManager = null;
        try {
            testPlanManager = Long.valueOf(atuPlanInfo.getPrincipalId());
            principalIds.add(testPlanManager);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        for (AtuCaseScriptResultDto record : resultDtoList) {
          TestPlanScriptExecutionDetailDTO detailDto = new TestPlanScriptExecutionDetailDTO();
          detailDto.setTestPlanId(atuPlanInfo.getId()); //测试计划id
          detailDto.setTestPlanName(atuPlanInfo.getPlanName()); //测试计划名称
          Long systemId = null;
          try {
            systemId = Long.valueOf(atuPlanInfo.getProjectId());
            detailDto.setSystemId(systemId); //系统id
          } catch (NumberFormatException e) {
            e.printStackTrace();
          }
          StringBuilder tag = new StringBuilder();
          List<TagDto> dtos = tagMapByObjId.get(atuPlanInfo.getId());
          for (String tagName : tagNames) {
            for (TagDto dto : dtos) {
              if (tagName.equals(dto.getName())) {
                tag.append(dto.getName()).append(",");
              }
            }
          }
          tag.deleteCharAt(tag.length() - 1);
          detailDto.setTag(tag.toString()); // 标签
          detailDto.setTestPlanManager(testPlanManager);// 计划负责人id
          detailDto.setScriptId(record.getScriptId()); // 脚本id
          detailDto.setScriptCode(record.getScriptCode()); // 脚本编号
          detailDto.setScriptName(record.getSetName()); // 脚本名称
          Long scriptManager = null;
          try {
            scriptManager = Long.valueOf(record.getPrincipalId());
            detailDto.setScriptManager(scriptManager); // 脚本负责人id
            principalIds.add(scriptManager);
          } catch (NumberFormatException e) {
              e.printStackTrace();
          }

          detailDto.setScriptCreateTime(record.getCreatedTime());// 脚本创建日期
          ScriptFirstExecutionDTO scriptFirstExecutionDTOS = atuPlanTaskApiService.getFirstExecuteInfo(record.getScriptId());
          if (scriptFirstExecutionDTOS != null) {
            detailDto.setFirstExecutionPlanId(scriptFirstExecutionDTOS.getFirstExecutionPlanId()); // 首次执行计划id
            detailDto.setFirstExecutionPlanName(scriptFirstExecutionDTOS.getFirstExecutionPlanName());  // 首次执行计划名称，
            if (scriptFirstExecutionDTOS.getFirstExecutionTime() != null) {
              detailDto.setFirstExecutionTime(new Date(scriptFirstExecutionDTOS.getFirstExecutionTime())); // 首次执行日期，
            }
          }
          LocalDate now = LocalDate.now().minusDays(1);
          Integer num = atuPlanTaskApiService.getScriptExecuteSuccessNum(record.getScriptId(), now);
          detailDto.setExecutionSuccessNum(num); // 执行成功次数
          Set<String> palnIdSet = scriptRefNumMap.computeIfAbsent(record.getScriptId(), key -> new HashSet<>());
          palnIdSet.add(atuPlanInfo.getId());

          relust.add(detailDto);
        }
      }

      List<TestGroupDTO> testGroup = monsterCommonService.getTestGroup(new ArrayList<>(principalIds));
      Map<Long, Long> testGroupUserMap = new HashMap<>();
      for (TestGroupDTO testGroupDTO : testGroup) {
        if (CollectionUtils.isEmpty(testGroupDTO.getUserList())) {
          continue;
        }
        for (TestGroupUserDTO testGroupUserDTO : testGroupDTO.getUserList()) {
          testGroupUserMap.put(testGroupUserDTO.getUserId(), testGroupUserDTO.getManagerId());
        }
      }

      for (TestPlanScriptExecutionDetailDTO detailDTO : relust) {
        Long testPlanManager = detailDTO.getTestPlanManager();
        Long testGroupManager = testGroupUserMap.get(detailDTO.getScriptManager());
        // 是否组员
        if (testGroupManager != null && testGroupManager.equals(testPlanManager)) {
          detailDTO.setIsTeamMember(1); // 是组员
        } else {
          detailDTO.setIsTeamMember(0); // 不是组员
        }

      }

      for (TestPlanScriptExecutionDetailDTO detailDTO : relust) {
        Set<String> palnIdSet = scriptRefNumMap.get(detailDTO.getScriptId());
        detailDTO.setRefScriptPlanNum(palnIdSet.size()); // 引用脚本计划数 具有标签的计划
      }
      return relust;
    }

    @Override
    public AdvancedQueryOptions advancedQueryOptions(String projectId) {
      AdvancedQueryOptions options = new AdvancedQueryOptions();
      options.setManagerList(new ArrayList<>());
      options.setCreatorList(new ArrayList<>());
      options.setUpdaterList(new ArrayList<>());
      List<String> mangerList = atuPlanInfoService.selectManagerByProject(projectId);
      Set<Long> userIds = new HashSet<>();
      if (!CollectionUtils.isEmpty(mangerList)) {
        mangerList.forEach(id -> {
          if (id.length() <= 20) {
            userIds.add(Long.valueOf(id));
          }
        });
      }
      List<String> creatorList = atuPlanInfoService.selectCreatorByProject(projectId);
      if (!CollectionUtils.isEmpty(creatorList)) {
        creatorList.forEach(id -> {
          if (id.length() <= 20) {
            userIds.add(Long.valueOf(id));
          }
        });
      }
      List<String> updaterList = atuPlanInfoService.selectUpdaterByProject(projectId);
      if (!CollectionUtils.isEmpty(updaterList)) {
        updaterList.forEach(id -> {
          if (id.length() <= 20) {
            userIds.add(Long.valueOf(id));
          }
        });
      }
      if (CollectionUtils.isEmpty(userIds)){
        return options;
      }
      List<UserDTO> userDTOS = monsterCommonFeignClient.getUserByIds(new ArrayList<>(userIds));
      userDTOS.forEach(user -> {
        if (mangerList.contains(user.getId().toString())) {
          options.getManagerList().add(UserOptions.userDTOtoUserOptions(user));
        }
        if (creatorList.contains(user.getId().toString())) {
            options.getCreatorList().add(UserOptions.userDTOtoUserOptions(user));
        }
        if (updaterList.contains(user.getId().toString())) {
            options.getUpdaterList().add(UserOptions.userDTOtoUserOptions(user));
        }
      });
      return options;
    }

    /**
   * pdf模板填充
   * @param data 填充的文本域数据
   * @param exportDto 导出参数
   * @param tenantId 租户id
   * @param out 输出流
   */
  private void fillTemp(Map<String, Object> data, AtuPlanExportDto exportDto, String tenantId,
                        ServletOutputStream out){
    //模板路径，放到系统里用这个ClassPathResource
    ClassPathResource classPathResource = new ClassPathResource(MsgConstant.CLASS_PATH_RESOURCE_PDF_TEMP);
    InputStream inputStream;

    PdfReader reader;
    ByteArrayOutputStream bos;
    PdfStamper stamper;
    try {
      inputStream = classPathResource.getInputStream();
      reader = new PdfReader(inputStream);
      bos = new ByteArrayOutputStream();
      stamper = new PdfStamper(reader, bos);

      AcroFields acroFields = stamper.getAcroFields();
      // 模板数据填充
      for (String name : acroFields.getFields().keySet()) {
        String value = data.get(name) == null ? "" : data.get(name).toString();
        acroFields.setField(name, value);
      }

      // 图片赋值
      PdfContentByte cb = stamper.getOverContent(1);
      // 填充脚本执行统计图

      String barBase64Str = exportDto.getExecResultChartBase64Str();
      if (StrUtil.isNotBlank(barBase64Str)) {
        try {
          Rectangle barSignRect = acroFields.getFieldPositions(MsgConstant.FIELD_POSITION_SCRIPT_EXECSUM).get(0).position;
          //替换转移字符
          String newImageInfo = barBase64Str.replaceAll(" ", "+");
          //取出图片部分
          String[] arr = newImageInfo.split(MsgConstant.REGEX_BASE64);
          //添加echarts图片替换数据
          barBase64Str = arr[1];
          byte[] imageBytes = Base64.getDecoder().decode(barBase64Str);
          Image barImage = Image.getInstance(imageBytes);
          // 设根据域大小设置缩放图片
          barImage.scaleToFit(barSignRect.getWidth(), barSignRect.getHeight());
          // 设置居中
          barImage.setAlignment(Image.MIDDLE);
          // 绝对定位
          barImage.setAbsolutePosition(barSignRect.getLeft(), barSignRect.getBottom());
          cb.addImage(barImage);
        }catch (Exception e){
          logger.error("填充脚本统计概括异常", e);
        }
      }
      cb.stroke();

      // 如果为false那么生成的PDF文件还能编辑，一定要设为true
      stamper.setFormFlattening(true);
      stamper.close();

      //*******************填充编辑好后的pdf**************
      reader = new PdfReader(bos.toByteArray());
      Rectangle pageSize = reader.getPageSize(1);
      Document document = new Document(pageSize);
      PdfWriter writer = PdfWriter.getInstance(document, out);
      writer.setPageEvent(new PageEvent());

      // 打开文档
      document.open();
      PdfContentByte cbUnder = writer.getDirectContentUnder();
      PdfImportedPage pageTemplate = writer.getImportedPage(reader, 1);
      cbUnder.addTemplate(pageTemplate, 0, 0);

      //document.newPage();
      //添加间隙,这里为进行了一个封装，因为这个模板第一页只有一些title啥的，
      //重开一页太浪费，只需要确定表格要在什么位置生成，添加一个间隙就可以了
      createBlankTable(writer, document, 360);
      // 填充app基础数据
      Map<String, String> appNameMap = new HashMap<>();
      createAppTable(writer, document, exportDto.getBatchId(), appNameMap);
      createBlankTable(writer, document, 50);
      // 填充app性能数据
      createAppPerTable(writer, document, exportDto.getBatchId(), tenantId, appNameMap);
      createBlankTable(writer, document, 50);
      // 填充脚本执行详情
      createTaskTable(writer, document, exportDto.getBatchId());

      document.close();
      out.close();

    }catch (Exception e){
      logger.error("模板填充异常", e);
    }
  }

  private void createTaskTable(PdfWriter writer, Document document, String batchId)
          throws IOException, DocumentException {
    List<String> tableHeardList = new ArrayList<>();
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_SCRIPT_NUM);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_SCRIPT_NAME);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_TEST_CASE_NAME);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_TEST_CASE_TYPE);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_TIME_CONSUMING);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_EXEC_DEVICE);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_EXEC_STATUS);
    tableHeardList.add(MsgConstant.TABLE_HEARD_LIST_FAILURE_REASON);
    List<BatchScriptExecDetailDto> scriptExecDetailList = getScriptExecDetailList(batchId);
    if (CollUtil.isEmpty(scriptExecDetailList)){
      return;
    }
    Map<String, String> caseTypeMap = getDictItem(DictConstant.SCRIPT_CASE_TYPE);
    Map<String, String> taskStatusMap = getDictItem(DictConstant.PLAN_TASK_STATUS);
    for (BatchScriptExecDetailDto detailDto : scriptExecDetailList) {
      if (caseTypeMap.containsKey(detailDto.getCaseType())){
        detailDto.setCaseType(caseTypeMap.get(detailDto.getCaseType()));
      }
      if (taskStatusMap.containsKey(detailDto.getStatus())){
        detailDto.setStatus(taskStatusMap.get(detailDto.getStatus()));
      }
    }
    createTable(writer, document, MsgConstant.TABLE_TITLE_SCRIPT_EXECUTION_DETAILS, tableHeardList, scriptExecDetailList);
  }


  private void createAppPerTable(PdfWriter writer, Document document, String batchId,
                                 String tenantId, Map<String, String> appNameMap)
          throws IOException, DocumentException {
    List<BatchAppInfoDto> appPerList = getBatchAppInfo(batchId, tenantId, appNameMap);
    if (CollUtil.isEmpty(appPerList)){
      return;
    }

    int totalColumn =11;
    PdfPTable table = new PdfPTable(totalColumn);
    table.setTotalWidth(520);
    table.setPaddingTop(500);
    table.setLockedWidth(true);
    //居中
    table.setHorizontalAlignment(Element.ALIGN_CENTER);
    table.writeSelectedRows(0, -1,500,800,writer.getDirectContentUnder());

    //定义数据的字体
    BaseFont baseFont = BaseFont.createFont(MsgConstant.FONT_NAME_STSONG_LIGHT,MsgConstant.FONT_ENCODING_UNIGB_UCS2_H,BaseFont.NOT_EMBEDDED);
    List<String> appIconPathList = new ArrayList<>();
    appIconPathList.add(MsgConstant.ICON_PATH_APP_APPINSTALLTIME_PNG);
    appIconPathList.add(MsgConstant.ICON_PATH_APP_APPSTARTTIME_PNG);
    appIconPathList.add(MsgConstant.ICON_PATH_APP_CPU_PNG);
    appIconPathList.add(MsgConstant.ICON_PATH_APP_FLOW_PNG);
    appIconPathList.add(MsgConstant.ICON_PATH_APP_MEMORY_PNG);
    List<byte[]> appIconList = getAllImages(appIconPathList);
    List<String> deviceIconPathList = new ArrayList<>();
    deviceIconPathList.add(MsgConstant.ICON_PATH_DEVICE_CPU_PNG);
    deviceIconPathList.add(MsgConstant.ICON_PATH_DEVICE_FLOW_PNG);
    deviceIconPathList.add(MsgConstant.ICON_PATH_DEVICE_MEMORY_PNG);
    deviceIconPathList.add(MsgConstant.ICON_PATH_DEVICE_TEMPERATURE_PNG);
    List<byte[]> deviceIconList = getAllImages(deviceIconPathList);

    for (BatchAppInfoDto appInfoDto : appPerList) {
      // 添加app性能表格标题
      addAppPerTitle(table, baseFont, "应用[" + appInfoDto.getAppName() + "]性能", totalColumn);

      addBlankCell(table, 1);
      // 添加app性能图标
      addIconCell(table, appIconList);
      // 添加app性能平均数据
      addBlankCell(table, 1);
      List<String> appAvgList = new ArrayList<>();
      appAvgList.add(appInfoDto.getInstallTime());
      appAvgList.add(appInfoDto.getStartElapsedTime());
      appAvgList.add(appInfoDto.getAppCpu());
      appAvgList.add(appInfoDto.getAppFlow());
      appAvgList.add(appInfoDto.getAppMemory());
      addPerCell(appAvgList, table, baseFont, 16, BaseColor.BLUE);

      String maxStr = MsgConstant.MAX_STR+"：";

      // 添加app性能最大值数据
      addBlankCell(table, 1);
      List<String> appMaxList = new ArrayList<>();
      String maxInstallTime = appInfoDto.getMaxInstallTime() == null ? "" : maxStr + appInfoDto.getMaxInstallTime();
      appMaxList.add(maxInstallTime);
      String maxStartTime = appInfoDto.getMaxStartElapsedTime() == null ? "" : maxStr + appInfoDto.getMaxStartElapsedTime();
      appMaxList.add(maxStartTime);
      String maxAppCpu = appInfoDto.getMaxAppCpu() == null ? "" : maxStr + appInfoDto.getMaxAppCpu();
      appMaxList.add(maxAppCpu);
      String maxAppFlow = appInfoDto.getMaxAppFlow() == null ? "" : maxStr + appInfoDto.getMaxAppFlow();
      appMaxList.add(maxAppFlow);
      String maxAppMemory = appInfoDto.getMaxAppMemory() == null ? "" : maxStr + appInfoDto.getMaxAppMemory();
      appMaxList.add(maxAppMemory);
      addPerCell(appMaxList, table, baseFont, 8, BaseColor.RED);

      // 添加设备性能标题
      addAppPerTitle(table, baseFont, "设备[" + appInfoDto.getDeviceName() + "]性能", totalColumn);
      addBlankCell(table, 1);
      // 添加app性能图标
      addIconCell(table, deviceIconList);
      // 设备数据少2列，填充两个空单元格
      addBlankCell(table, 2);

      // 添加设备性能平均数据
      addBlankCell(table, 1);
      List<String> deviceAvgList = new ArrayList<>();
      deviceAvgList.add(appInfoDto.getDeviceCpu());
      deviceAvgList.add(appInfoDto.getDeviceFlow());
      deviceAvgList.add(appInfoDto.getDeviceMemory());
      deviceAvgList.add(appInfoDto.getCellTemperature());
      addPerCell(deviceAvgList, table, baseFont, 16, BaseColor.BLUE);
      addBlankCell(table, 2);

      // 添加设备性能最大值数据
      addBlankCell(table, 1);
      List<String> deviceMaxList = new ArrayList<>();
      String maxDeviceCpu = appInfoDto.getMaxDeviceCpu() == null ? "" : maxStr + appInfoDto.getMaxDeviceCpu();
      deviceMaxList.add(maxDeviceCpu);
      String maxDeviceFlow = appInfoDto.getMaxDeviceFlow() == null ? "" : maxStr + appInfoDto.getMaxDeviceFlow();
      deviceMaxList.add(maxDeviceFlow);
      String maxDeviceMemory = appInfoDto.getMaxDeviceMemory() == null ? "" : maxStr + appInfoDto.getMaxDeviceMemory();
      deviceMaxList.add(maxDeviceMemory);
      String maxCellTemperature = appInfoDto.getMaxCellTemperature() == null ? "" : maxStr + appInfoDto.getMaxCellTemperature();
      deviceMaxList.add(maxCellTemperature);
      addPerCell(deviceMaxList, table, baseFont, 8, BaseColor.RED);
      addBlankCell(table, 2);
    }
    document.add(table);
  }

  private void addPerCell(List<String> dataList, PdfPTable table, BaseFont baseFont, int fontSize, BaseColor color){
    Font font = new Font(baseFont, fontSize, Font.NORMAL);
    font.setColor(color);
    for (String str : dataList) {
      PdfPCell cell = new PdfPCell();
      //垂直居中
      cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
      // 水平居左
      cell.setHorizontalAlignment(Element.ALIGN_LEFT);
      cell.setPhrase(new Phrase(str, font));
      cell.setPaddingLeft(10);
      cell.setBorder(Rectangle.NO_BORDER);
      cell.setColspan(2);
      table.addCell(cell);
    }
  }

  private void addIconCell(PdfPTable table, List<byte[]> images) throws IOException, BadElementException {
    if (CollUtil.isEmpty(images)){
      return;
    }
    for (byte[] bytes : images) {
      PdfPCell cell = new PdfPCell();
      cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
      cell.setHorizontalAlignment(Element.ALIGN_LEFT);
      Image image = Image.getInstance(bytes);
      image.scaleAbsolute(30, 30);
      cell.addElement(image);
      cell.setPaddingLeft(5);
      cell.setBorder(Rectangle.NO_BORDER);
      cell.setColspan(2);
      table.addCell(cell);
    }
  }

  private List<byte[]> getAllImages(List<String> dirList){
    List<byte[]> images = new ArrayList<>();
    if (CollUtil.isEmpty(dirList)){
      return images;
    }
    dirList.forEach(dir -> {
      ClassPathResource resource = new ClassPathResource(dir);
      try {
        byte[] imageBytes = IoUtil.readBytes(resource.getInputStream());
        images.add(imageBytes);
      } catch (IOException e) {
        logger.error("获取文件失败,e,{}", e);
        throw new PlatformRuntimeException(ExecPlanError.GET_FILE_FAIL);
      }
    });
    return images;
  }

  private void addBlankCell(PdfPTable table, int colspan){
    PdfPCell blankCell = new PdfPCell();
    blankCell.setBorder(Rectangle.NO_BORDER);
    blankCell.setColspan(colspan);
    table.addCell(blankCell);
  }

  private void addAppPerTitle(PdfPTable table, BaseFont baseFont, String title, int colSpan){
    Font appTitleFont = new Font(baseFont, 16, Font.NORMAL);
    PdfPCell appTitleCell = new PdfPCell();
    appTitleCell.setPhrase(new Phrase(title, appTitleFont));
    appTitleCell.setColspan(colSpan);
    appTitleCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
    appTitleCell.setHorizontalAlignment(Element.ALIGN_LEFT);
    appTitleCell.setBorder(Rectangle.NO_BORDER);
    table.addCell(appTitleCell);
  }

  private void createAppTable(PdfWriter writer, Document document, String batchId, Map<String, String> appNameMap)
          throws IOException, DocumentException {
    List<String> headList = new ArrayList<>();
    headList.add(MsgConstant.HEAD_LIST_APP_NAME);
    headList.add(MsgConstant.HEAD_LIST_APP_VERSION);
    headList.add(MsgConstant.HEAD_LIST_PACKAGE_NAME);
    headList.add(MsgConstant.HEAD_LIST_CHANNEL_NAME);
    headList.add(MsgConstant.HEAD_LIST_VERSION_NOTES);
    List<AppBaseInfoDto> appBaseInfoList = getAppBaseInfo(batchId, appNameMap);
    if (CollUtil.isEmpty(appBaseInfoList)){
      return;
    }
    Map<String, String> platformMap = getDictItem(DictConstant.MOBILE_PLATFORM_TYPE);
    for (AppBaseInfoDto baseInfoDto : appBaseInfoList) {
      if (platformMap.containsKey(baseInfoDto.getChannelName())){
        baseInfoDto.setChannelName(platformMap.get(baseInfoDto.getChannelName()));
      }
    }
    createTable(writer, document, MsgConstant.TABLE_TITLE_APP_LIST, headList, appBaseInfoList);
  }


  private void createBlankTable(PdfWriter writer, Document document, int height)
          throws DocumentException, IOException {
    final BaseFont font = BaseFont.createFont(MsgConstant.FONT_NAME_STSONG_LIGHT, MsgConstant.FONT_ENCODING_UNIGB_UCS2_H, BaseFont.NOT_EMBEDDED);
    PdfPTable table = new PdfPTable(new float[]{30});
    table.setTotalWidth(520);
    table.setPaddingTop(500);
    table.setLockedWidth(true);
    //居中
    table.setHorizontalAlignment(Element.ALIGN_CENTER);
    table.writeSelectedRows(0, -1, 500, 800, writer.getDirectContentUnder());
    Font textFont = new Font(font, 10, Font.NORMAL);
    PdfPCell cell = new PdfPCell(new Paragraph(" ", textFont));

    cell.setHorizontalAlignment(Element.ALIGN_LEFT);

    cell.setVerticalAlignment(Element.ALIGN_BOTTOM);

    cell.setBorder(Rectangle.NO_BORDER);
    cell.setFixedHeight(height);

    cell.setColspan(1);

    table.addCell(cell);
    document.add(table);
  }

  /**
   * 添加表格
   * @param writer 写入
   * @param document 文档
   * @param title 表格标题
   * @param tableHeadList 表格表头
   * @param tableDataList 表格数据
   * @throws DocumentException 文档异常
   * @throws IOException io异常
   */
  private void createTable(PdfWriter writer, Document document, String title,
                           List<String> tableHeadList, List<?> tableDataList)
          throws DocumentException, IOException {

    PdfPTable table = new PdfPTable(tableHeadList.size());
    table.setTotalWidth(520);
    table.setPaddingTop(500);
    table.setLockedWidth(true);
    //居中
    table.setHorizontalAlignment(Element.ALIGN_CENTER);
    table.writeSelectedRows(0, -1,500,800,writer.getDirectContentUnder());
    //每页都显示表头,输入几就是第几行的表头固定
    table.setHeaderRows(2);

    //定义数据的字体
    BaseFont baseFont = BaseFont.createFont(MsgConstant.FONT_NAME_STSONG_LIGHT,MsgConstant.FONT_ENCODING_UNIGB_UCS2_H,BaseFont.NOT_EMBEDDED);
    Font textFont = new Font(baseFont, 6, Font.NORMAL);
    Font titleFont = new Font(baseFont, 12, Font.NORMAL);

    //填充标题
    PdfPCell headCell = new PdfPCell();
    headCell.setRowspan(1);
    headCell.setColspan(tableHeadList.size());
    headCell.setFixedHeight(30);
    headCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
    headCell.setHorizontalAlignment(Element.ALIGN_CENTER);
    headCell.setPhrase(new Phrase(title, titleFont));
    table.addCell(headCell);

    // 填充表头
    for (String tableHead : tableHeadList) {
      table.addCell(createSetCell(tableHead, textFont));
    }

    //填充列表数据
    for (Object obj : tableDataList) {
      Class<?> clazz = obj.getClass();
      Field[] fields = clazz.getDeclaredFields();
      for (Field field : fields) {
        // 设置为可访问
        field.setAccessible(true);
        try {
          String value = field.get(obj) == null ? "" : field.get(obj).toString();
          table.addCell(createSetCell(value, textFont));
        }catch (Exception e) {
          logger.error("填充数据异常", e);
        }
      }
    }
    document.add(table);
  }


  /**
   * 为一个表格添加内容
   * @param value 值
   * @param font 字体
   * @return 单元格
   */
  public PdfPCell createSetCell(String value,Font font){
    PdfPCell cell = new PdfPCell();
    cell.setPhrase(new Phrase(value,font));
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);
    cell.setMinimumHeight(20);
    return cell;
  }

  @Override
  @Transactional(rollbackFor = Exception.class)
  public Integer clonePlan(List<String> batch)
  {
    List<String> planList = new ArrayList<>();
    for (String targetId : batch) {
      String planId = this.clonePlanInfo(targetId);
      planList.add(planId);
    }
    return planList.size();
  }

  /**
   * 计划执行
   *
   * @param atuPlanRunDto 执行参数
   */
  @Override
  @Transactional(rollbackFor = Exception.class)
  public void run(AtuPlanRunDto atuPlanRunDto) {
    // 不是手动执行时，校验设备是否离线
    if (!PlanConstant.TRIGGER_TYPE_MANUAL.equals(atuPlanRunDto.getTriggerType())){
      Map<String,Object> offlineMap = checkDeviceOffline(atuPlanRunDto.getPlanId(),atuPlanRunDto.getBatchId());
      Object allOfflineObj = offlineMap.get(PlanConstant.DEVICE_OFFLINE_KEY);
      if (allOfflineObj != null && PlanConstant.DEVICE_OFFLINE_ALL == Integer.parseInt(allOfflineObj.toString())) {
        logger.error("该计划绑定的设备已全部离线");
        return;
      }
    }
    // 1.查询计划信息
    AtuPlanInfo planInfo = this.atuPlanInfoService.findByPrimaryKey(atuPlanRunDto.getPlanId());
    if (ObjectUtil.isNull(planInfo)){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_IS_NULL);
    }
    if (!planInfo.getEnabled()) {//false为禁用
      throw new PlatformRuntimeException(ExecPlanError.PLAN_IS_UN_ENABLE);
    }
    String tenantId = planInfo.getTenantId();
    NKSecurityContext.setTenantId(tenantId);
    logger.debug("生成计划批次信息");
    AtuPlanBatch planBatch = new AtuPlanBatch();
    if (StrUtil.isBlank(atuPlanRunDto.getBatchId())){
      planBatch.setId(IdUtil.simpleUUID());
    }else{
      planBatch.setId(atuPlanRunDto.getBatchId());
    }
    planBatch.setPlanId(atuPlanRunDto.getPlanId());
    int batchIdx = Objects.requireNonNull(redisTemplate.opsForValue()
            .increment(RedisConstant.PLAN_BATCH_NUM_KEY + atuPlanRunDto.getPlanId())).intValue();
    planBatch.setBatch(batchIdx);
    planBatch.setCreatedTime(new Date());
    planBatch.setExecutedBy(atuPlanRunDto.getExecutedBy());
    planBatch.setTriggerType(atuPlanRunDto.getTriggerType());

    logger.info("查询计划绑定的环境id");
    List<String> envIdList = planEnvLinkService.queryEnvIdByPlanIdOderBySort(planInfo.getId());
    logger.info("绑定的环境数量：" + envIdList.size());
    if (CollUtil.isEmpty(envIdList)){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_ENV_IS_NULL);
    }
    Map<String, Object> caseMap;
    try {
      // 3.获取计划绑定的用例集
      caseMap = processBatchCaseSetData(planInfo, envIdList);
      logger.debug("脚本用例类型信息 => " + JSONUtil.toJsonStr(caseMap));

      int taskTotal = Integer.parseInt(caseMap.get(PlanConstant.TASK_TOTAL).toString());
      planBatch.setTaskTotal(taskTotal);

      long waitTotal = Long.parseLong(caseMap.get(PlanConstant.WAIT_TOTAL).toString());

      int scriptTotal = Integer.parseInt(caseMap.get(PlanConstant.SCRIPT_TOTAL).toString());
      planBatch.setScriptNum(scriptTotal);
      logger.debug("脚本总数为：" + scriptTotal);

      planBatch.setSuccessRate(MsgConstant.PLAN_BATCH_SUCCESS_RATE);

      // 判断用例是否全部删除
      if (taskTotal > 0) {
        // 4.保存计划批次信息
        atuPlanBatchService.insert(planBatch);

        // 4.1 缓存批次的统计信息
        logger.info("缓存批次的统计信息 batchId=>" + planBatch.getId() + ", batch=>" + planBatch.getBatch() +
                ", taskTotal=>" + taskTotal);

        // 缓存脚本统计信息
        String clusterPrefix = RedisConstant.CLUSTER_KEY_PREFIX + planBatch.getId().substring(0,4) +
                  RedisConstant.CLUSTER_KEY_SUFFIX;
        String scriptSumKey = clusterPrefix + RedisConstant.BATCH_SCRIPT_SUM_KEY + planBatch.getId();
        redisTemplate.opsForHash().put(scriptSumKey, clusterPrefix + PlanConstant.PLAN_BATCH_NUM, planBatch.getBatch());
        redisTemplate.opsForHash().put(scriptSumKey, clusterPrefix + PlanConstant.SCRIPT_TOTAL, scriptTotal);
        redisTemplate.opsForHash().put(scriptSumKey, clusterPrefix + PlanConstant.TASK_WAIT_EXECUTE_STATUS, waitTotal);

        logger.debug("更新计划信息最后批次信息");
        planInfo.setStatus(PlanConstant.PLAN_WAITING_STATUS);
        planInfo.setLastBatchId(planBatch.getId());
        planInfo.setWaitingNum((int) waitTotal);
        planInfo.setRunningNum(0);
        planInfo.setSuccessNum(0);
        planInfo.setExecFailNum(0);
        planInfo.setAssertFailNum(0);
        planInfo.setTimeoutNum(0);
        planInfo.setCancelNum(0);
        planInfo.setLastExecTime(new Date());
        atuPlanInfoService.updateByPrimaryKey(planInfo);
        // 计划的更新方法会获取当前的租户，计划定时执行时会导致租户为空
        planInfo.setTenantId(tenantId);

        List<AtuPlanInputDto> inputDataList = (List<AtuPlanInputDto>) caseMap.get(PlanConstant.SCRIPT_INPUT_LIST);
        if (CollUtil.isEmpty(inputDataList)) {
          throw new PlatformRuntimeException(ExecPlanError.SCRIPT_INPUT_DATA_IS_NULL);
        }
        // 异步生成任务
        executor.execute(() ->{
          boolean hasOfflineDevice = StrUtil.isNotBlank(atuPlanRunDto.getBatchId());
          List<EntrustDeviceDto> pcDeviceList = new ArrayList<>();
          List<EntrustDeviceDto> mobDeviceList = new ArrayList<>();
          saveBatchDeviceLink(planInfo.getId(), planBatch.getId(), hasOfflineDevice, false, pcDeviceList, mobDeviceList);

          atuPlanTaskApiService.createdScriptTaskInfo(planInfo, planBatch.getId() , envIdList, inputDataList);
          // 6.生成委托信息
          try {
            // 创建委托信息
            createEntrustMsg(planBatch.getId(), planInfo.getId(),  planInfo.getPriority(), pcDeviceList, mobDeviceList, caseMap);
          }catch (Exception e){
            logger.error("计划批次委托失败", e);
            throw new PlatformRuntimeException(ExecPlanError.PLAN_ENTRUST_FAIL);

          }
        });
      }else {
        logger.debug("计划用例全部删除批次完成");
        saveFinishBatchAndPlan(planInfo, planBatch);
      }
    }catch (Exception e){
      logger.error("计划批次创建失败", e);
      // 回退批次号
      redisTemplate.opsForValue().increment(RedisConstant.PLAN_BATCH_NUM_KEY + atuPlanRunDto.getPlanId(), -1);
      throw new PlatformRuntimeException(ExecPlanError.PLAN_EXECUTE_FAIL, e.getMessage());
    }
  }

  /**
   * 批次结束保存状态与计划信息
   * @param planInfo 计划信息
   * @param planBatch 批次信息
   */
  private void saveFinishBatchAndPlan(AtuPlanInfo planInfo, AtuPlanBatch planBatch){
    // 4.保存计划批次信息
    planBatch.setStatus(PlanConstant.BATCH_FINISH_STATUS);
    Date date = new Date();
    planBatch.setStartTime(date);
    planBatch.setEndTime(date);
    planBatch.setSuccessNum(0);
    planBatch.setExecFailNum(0);
    planBatch.setAssertFailNum(0);
    planBatch.setTimeoutNum(0);
    planBatch.setCancelNum(0);
    //增加devops调用时 塞值批次生成渠道channel和devops请求id todo
    atuPlanBatchService.insert(planBatch);

    // 5.更新计划信息最后批次信息
    planInfo.setStatus(PlanConstant.PLAN_FINISH_STATUS);
    planInfo.setLastBatchId(planBatch.getId());
    planInfo.setWaitingNum(planBatch.getTaskTotal());
    planInfo.setRunningNum(0);
    planInfo.setSuccessNum(0);
    planInfo.setExecFailNum(0);
    planInfo.setAssertFailNum(0);
    planInfo.setTimeoutNum(0);
    planInfo.setCancelNum(0);
    planInfo.setLastExecTime(new Date());
    atuPlanInfoService.updateByPrimaryKey(planInfo);
  }

  /**
   * 处理批次用例集数据
   * @param planInfo 计划信息
   * @return 结果
   */
  private Map<String, Object> processBatchCaseSetData(AtuPlanInfo planInfo, List<String> envIdList){
    logger.info("查询计划绑定的脚本数据");
    AtuPlanScriptLink params = new AtuPlanScriptLink();
    params.setPlanId(planInfo.getId());
    List<AtuPlanScriptLink> planScriptLinkList = planScriptLinkService.query(params);
    logger.info("绑定脚本数据大小 => " + planScriptLinkList.size());

    logger.info("判断该批次存在哪种类型用例");
    Map<String, Object> caseMap = new HashMap<>();
    caseMap.put(MsgConstant.CASEMAP_HAS_PC, false);
    caseMap.put(MsgConstant.CASEMAP_HAS_MOB, false);
    caseMap.put(MsgConstant.CASEMAP_HAS_INTERFACE, false);
    Map<String, String> scriptTypeMap = new HashMap<>();
    Map<String, List<String>> unselectedInputMap = new HashMap<>();
    for (AtuPlanScriptLink scriptLink : planScriptLinkList) {
      if (!unselectedInputMap.containsKey(scriptLink.getScriptId())){
        unselectedInputMap.put(scriptLink.getScriptId(), CollUtil.newArrayList());
      }
      scriptTypeMap.put(scriptLink.getScriptId(), scriptLink.getScriptType());
      handleAutoType(scriptLink.getScriptType(), caseMap);
    }
    logger.info("用例类型：" + JSONUtil.toJsonStr(caseMap));
    // 根据用例集ID查询未删除用例总数
    getTaskTotal(unselectedInputMap, scriptTypeMap, envIdList, caseMap);
    return caseMap;
  }

  @Override
  public void handleAutoType(String scriptType, Map<String, Object> caseMap){
    switch (scriptType){
      case PlanConstant.SCRIPT_TYPE_API:
        caseMap.put(MsgConstant.CASEMAP_HAS_INTERFACE, true);
        break;
      case PlanConstant.SCRIPT_TYPE_BS:
      case PlanConstant.SCRIPT_TYPE_CS:
        caseMap.put(MsgConstant.CASEMAP_HAS_PC, true);
        break;
      case PlanConstant.SCRIPT_TYPE_ANDROID:
      case PlanConstant.SCRIPT_TYPE_IOS:
        caseMap.put(MsgConstant.CASEMAP_HAS_MOB, true);
        break;
      case PlanConstant.SCRIPT_TYPE_SCENE:
        caseMap.put(MsgConstant.CASEMAP_HAS_INTERFACE, true);
        caseMap.put(MsgConstant.CASEMAP_HAS_PC, true);
        caseMap.put(MsgConstant.CASEMAP_HAS_MOB, true);
        break;
      default:
        logger.error("脚本类型异常 type => " + scriptType);
        break;
    }
  }

  /**
   * 获取任务总数
   * @return 总数
   */
  private void getTaskTotal(Map<String, List<String>> unselectedInputMap,
                           Map<String, String> scriptTypeMap, List<String> envIdList,
                           Map<String, Object> caseMap){
    int total = 0;
    caseMap.put(PlanConstant.WAIT_TOTAL, 0);
    caseMap.put(PlanConstant.TASK_TOTAL, total);
    List<AtuPlanInputDto> inputDataList = new ArrayList<>();
    caseMap.put(PlanConstant.SCRIPT_INPUT_LIST, inputDataList);
    List<String> scriptIdAllList = new ArrayList<>(unselectedInputMap.keySet());
    caseMap.put(PlanConstant.SCRIPT_TOTAL, scriptIdAllList.size());

    logger.info("调用脚本用例服务查询脚本输入项存放地址信息");
    AtuPlanInputQueryDto queryDto = new AtuPlanInputQueryDto();
    queryDto.setEnvIdList(envIdList);
    queryDto.setScriptIdList(scriptIdAllList);
    // 查询脚本输入项文件地址
    try {
      ResultWrapper<List<AtuPlanInputDto>> queryResult = publicFeignClient.queryInputByEnvIdAndScriptId(queryDto);
      if (queryResult != null && queryResult.isSuccess()){
        if (CollUtil.isEmpty(queryResult.getData())){
          logger.debug("无输入项数据");
          return;
        }
        inputDataList = queryResult.getData();
      }
    }catch (Exception e){
      logger.error("查询脚本输入项数据异常", e);
      throw new PlatformRuntimeException(ExecPlanError.QUERY_SCRIPT_INPUT_IS_NULL);
    }
    logger.info("脚本输入项：" + inputDataList);
    if (CollUtil.isEmpty(inputDataList)) {
      return;
    }
    logger.info("下载数据文件，并解析");
    for (AtuPlanInputDto atuPlanInputDto : inputDataList) {
      String[] resultPath = MinioPathUtils.idToPath(atuPlanInputDto.getDataUrl());
      InputStream inputStream = null;
      List<Map<String, Object>> dataList;
      try {
        inputStream = simpleStorageService.downloadAsStream(resultPath[0], "/" + resultPath[1]);
        ScriptInputFileDto fileData = MinioPathUtils.getFileContent(inputStream, ScriptInputFileDto.class);
        //获取数据
        dataList = fileData.getTableContent();

      } catch (FileDownloadException e) {
        logger.error("读取环境[{}]脚本[{}]输入项文件失败，文件地址：{}",
                atuPlanInputDto.getEnvId(), atuPlanInputDto.getScriptId(), atuPlanInputDto.getDataUrl());
        logger.error("失败原因：", e);
        ResultWrapper<Map<String, AtuScriptDetailDto>> wrapper = publicFeignClient.queryScriptDetailMap(Sets.newHashSet(atuPlanInputDto.getScriptId()));
        String scriptName = "";
        if (wrapper.getData() != null) {
          scriptName = wrapper.getData().get(atuPlanInputDto.getScriptId()).getScriptName();
        }
        throw new PlatformRuntimeException(ExecPlanError.QUERY_SCRIPT_INPUT_ERROR, envNameUtils.getEnvName(atuPlanInputDto.getEnvId()),
                scriptName, e.getMessage());
      } finally {
        if (null != inputStream) {
          try {
            inputStream.close();
          } catch (IOException e) {
            logger.error("",e);
          }
        }
      }
      logger.debug("例集用例数据为" + dataList);
      if (CollUtil.isEmpty(dataList)){
        logger.debug("该用例集无用例数据");
        continue;
      }
      // 排除未被选择的意图
      if (unselectedInputMap.containsKey(atuPlanInputDto.getScriptId())){
        List<String> unselectedList = unselectedInputMap.get(atuPlanInputDto.getScriptId());
        if (CollUtil.isNotEmpty(unselectedList)) {
          Iterator<Map<String, Object>> iterator = dataList.iterator();
          while (iterator.hasNext()){
            Map<String, Object> dataMap = iterator.next();
            for (String caseId : unselectedList) {
              if (dataMap.get(PlanConstant.SCRIPT_TEST_PURPOSE_ID).equals(caseId)){
                iterator.remove();
              }
            }
          }
        }
      }
      atuPlanInputDto.setDataList(dataList);
      atuPlanInputDto.setScriptType(scriptTypeMap.get(atuPlanInputDto.getScriptId()));
      total = total + dataList.size();
    }
    logger.info("脚本输入项数据：" + inputDataList);
    caseMap.put(PlanConstant.TASK_TOTAL, total);
    caseMap.put(PlanConstant.SCRIPT_INPUT_LIST, inputDataList);
    long count = inputDataList.stream()
            .filter(inputData -> CollUtil.isNotEmpty(inputData.getDataList()))
            .map(AtuPlanInputDto::getScriptId)
            .distinct()
            .count();
    caseMap.put(PlanConstant.WAIT_TOTAL, count);
  }


  public void saveBatchDeviceLink(String planId, String batchId, boolean hasOfflineDevice, boolean isRetry,
                                   List<EntrustDeviceDto> pcDeviceList, List<EntrustDeviceDto> mobDeviceList){
    logger.debug("保存在线计划设备至批次设备绑定");
    // 1.查询计划设备信息
    AtuPlanDeviceLink planDeviceLink = new AtuPlanDeviceLink();
    planDeviceLink.setPlanId(planId);
    List<AtuPlanDeviceLink> planDeviceLinks = atuPlanDeviceLinkService.query(planDeviceLink);

    // 获取缓存执行时离线的设备ID
    List<String> offlineDeviceIdList = new ArrayList<>();
    if (hasOfflineDevice){
      Object offlineDeviceId = redisTemplate.opsForValue().get(RedisConstant.PLAN_BATCH_OFFLINE_DEVICE + batchId);
      if (offlineDeviceId != null){
        offlineDeviceIdList = JSONUtil.toList(offlineDeviceId.toString(), String.class);
        // 删除缓存中的离线设备信息
        redisTemplate.delete(RedisConstant.PLAN_BATCH_OFFLINE_DEVICE + batchId);
      }
    }

    List<AtuPlanBatchDeviceLink> batchDeviceList = new ArrayList<>();
    // 1.1.获取PC与移动设备集合
    for (AtuPlanDeviceLink deviceLink : planDeviceLinks) {
      // 过滤离线设备ID
      if (CollUtil.isNotEmpty(offlineDeviceIdList) && offlineDeviceIdList.contains(deviceLink.getDeviceId())){
        continue;
      }
      EntrustDeviceDto deviceDto = new EntrustDeviceDto();
      deviceDto.setDeviceId(deviceLink.getDeviceId());
      deviceDto.setBrowserType(deviceLink.getBrowserType());

      if (ObjectUtil.equal(PlanConstant.DEVICE_TYPE_PC, deviceLink.getDeviceType())){
        pcDeviceList.add(deviceDto);
      }else if (ObjectUtil.equal(PlanConstant.DEVICE_TYPE_MOBILE, deviceLink.getDeviceType())){
        mobDeviceList.add(deviceDto);
      }
      AtuPlanBatchDeviceLink batchDevice = new AtuPlanBatchDeviceLink();
      batchDevice.setId(IdUtil.simpleUUID());
      batchDevice.setBatchId(batchId);
      batchDevice.setDeviceId(deviceLink.getDeviceId());
      batchDevice.setDeviceType(deviceLink.getDeviceType());
      batchDevice.setOffline(false);
      batchDeviceList.add(batchDevice);
    }

    // 插入批次设备绑定信息表
    if (CollUtil.isEmpty(batchDeviceList)){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_DEVICE_IS_NULL);
    }
    List<AtuPlanBatchDeviceLink> deviceLinks = updateDeviceOfflineInfo(batchDeviceList);
    if (isRetry){
      for (AtuPlanBatchDeviceLink deviceLink : deviceLinks) {
        if (deviceLink.getOffline()){
          atuPlanBatchDeviceLinkService.updateByPrimaryKey(deviceLink);
        }
      }
    }else {
      atuPlanBatchDeviceLinkService.insertByBatch(deviceLinks);
    }
  }

  /**
   * 创建委托信息
   * @param planId 计划ID
   * @param priority 优先级
   * @param batchId 批次ID
   * @param hasOfflineDevice 是否含有离线设备
   */
  @Override
  public void handlePlanDevice(String planId, String priority, String batchId,
                               boolean hasOfflineDevice, Map<String, Object> caseMap, boolean isRetry){
    List<EntrustDeviceDto> pcDeviceList = new ArrayList<>();
    List<EntrustDeviceDto> mobDeviceList = new ArrayList<>();
    saveBatchDeviceLink(planId, batchId, hasOfflineDevice, isRetry, pcDeviceList, mobDeviceList);
    // 创建委托信息
    createEntrustMsg(batchId, planId, priority, pcDeviceList, mobDeviceList, caseMap);
  }

  @Override
  public AtuPlanInfo findByBatchId(String batchId) {
    return this.atuPlanInfoService.findByBatchId(batchId);
  }

  /**
   * 更新设备离线信息
   * @param batchDeviceList 批次设备列表
   * @return 更新后列表信息
   */
  private List<AtuPlanBatchDeviceLink> updateDeviceOfflineInfo(List<AtuPlanBatchDeviceLink> batchDeviceList){
    // 查询设备是否离线及离线时间
    List<String> deviceIds = batchDeviceList.stream().map(AtuPlanBatchDeviceLink::getDeviceId).collect(Collectors.toList());
    logger.debug("批次设备列表 => " + JSONUtil.toJsonStr(deviceIds));
    // 设备离线状态查询
    List<Map<String, Object>> offlineDeviceList = new ArrayList<>();
    try {
      offlineDeviceList = publicFeignClient.queryDeviceStatus(deviceIds);
    }catch (Exception e){
      logger.error("查询设备离线信息异常 => ", e);
      //throw new PlatformRuntimeException(ExecPlanError.QUERY_DEVICE_OFFLINE_INFO);
    }
    if (CollUtil.isEmpty(offlineDeviceList)){
      return batchDeviceList;
    }
    Map<String, Object> offlineMap = new HashMap<>();
    for (Map<String, Object> map : offlineDeviceList) {
      offlineMap.put(map.get(MsgConstant.OFFLINE_MAP_DEVICE_ID).toString(), map.get(MsgConstant.LAST_HEART_BEAT_TIME));
    }

    for (AtuPlanBatchDeviceLink batchDeviceLink : batchDeviceList) {
      if (offlineMap.containsKey(batchDeviceLink.getDeviceId())){
        batchDeviceLink.setOffline(true);
        long offlineTime = Long.parseLong(offlineMap.get(batchDeviceLink.getDeviceId()).toString());
        String format = DateUtil.format(new Date(offlineTime), PlanConstant.DATETIME_FORMAT);
        batchDeviceLink.setOfflineTime(DateUtil.parse(format));
      }
    }

    return batchDeviceList;
  }

  /**
   * 创建委托信息
   * @param batchId 批次号
   * @param planId 计划ID
   * @param priority 优先级
   * @param pcDeviceList PC设备集合
   * @param mobDeviceList 移动设备集合
   * @param caseMap 用例分类集合
   */
  private void createEntrustMsg(String batchId, String planId, String priority, List<EntrustDeviceDto> pcDeviceList,
                                List<EntrustDeviceDto> mobDeviceList, Map<String, Object> caseMap){
    logger.debug("创建委托信息");
    List<DeviceListDto> deviceTypeList = new ArrayList<>();
    if (CollUtil.isNotEmpty(pcDeviceList)){
      List<String> pcDeviceIdList = pcDeviceList.stream().map(EntrustDeviceDto::getDeviceId).collect(Collectors.toList());
      DeviceListDto deviceListDto = new DeviceListDto();
      deviceListDto.setType(PlanConstant.DEVICE_TYPE_PC);
      deviceListDto.setDeviceList(pcDeviceIdList);
      deviceTypeList.add(deviceListDto);
    }
    if (CollUtil.isNotEmpty(mobDeviceList)){
      List<String> mobDeviceIdList = mobDeviceList.stream().map(EntrustDeviceDto::getDeviceId).collect(Collectors.toList());
      DeviceListDto deviceListDto = new DeviceListDto();
      deviceListDto.setType(PlanConstant.DEVICE_TYPE_MOBILE);
      deviceListDto.setDeviceList(mobDeviceIdList);
      deviceTypeList.add(deviceListDto);
    }

    logger.debug("查询引擎信息");
    List<EngineInfoListDto> engineInfoList;
    try {
      logger.debug("查询引擎信息参数 => " + JSONUtil.toJsonStr(deviceTypeList));
      engineInfoList = publicFeignClient.queryEngineByType(deviceTypeList);
      logger.debug("查询引擎信息结果 => " + JSONUtil.toJsonStr(engineInfoList));
    }catch (Exception e){
      logger.error("获取引擎信息异常", e);
      throw new PlatformRuntimeException(ExecPlanError.QUERY_ENGINE_INFO_ERROR);
    }

    if (CollUtil.isEmpty(engineInfoList)){
      logger.error("引擎信息不存在");
    }

    logger.debug("用例分类信息 => " +JSONUtil.toJsonStr(caseMap));
    boolean hasPc = (boolean) caseMap.get(MsgConstant.CASEMAP_HAS_PC);
    boolean hasMob = (boolean) caseMap.get(MsgConstant.CASEMAP_HAS_MOB);
    boolean hasApi = (boolean) caseMap.get(MsgConstant.CASEMAP_HAS_INTERFACE);

    logger.debug("发送委托信息至消息队列");
    for (EngineInfoListDto engineInfoListDto : engineInfoList) {
      logger.debug("引擎类型：{}", engineInfoListDto.getType());
      if (CollUtil.isEmpty(engineInfoListDto.getEngineInfoList())){
        continue;
      }
      AtuEntrustMsgDto entrustMsg = new AtuEntrustMsgDto();
      entrustMsg.setPlanId(planId);
      entrustMsg.setBatchId(batchId);
      entrustMsg.setPriority(priority);
      // 2.1.PC类引擎委托
      if (hasPc) {
        // 创建任务队列
        logger.debug("发送PC引擎委托");
        String queueName = RabbitConstant.TASK_EXEC_QUEUE_PC_KEY + batchId;
        logger.debug("PC设备集合 => " + JSONUtil.toJsonStr(pcDeviceList));
        if (CollUtil.isNotEmpty(pcDeviceList)) {
          amqpAdmin.declareQueue(new Queue(queueName));
          sendEntrustMsgToQueue(entrustMsg, engineInfoListDto.getEngineInfoList(),
                  PlanConstant.ENGINE_TYPE_PC, queueName, pcDeviceList);
        }
      }
      if (hasMob){
        // 2.2.移动类引擎委托
        logger.debug("发送移动引擎委托");
        logger.debug("移动设备集合 => " + JSONUtil.toJsonStr(mobDeviceList));
        if (CollUtil.isNotEmpty(mobDeviceList)){
          // 区分ios和安卓
          Map<String, List<String>> deviceTypeMap;
          List<String> deviceIdList = mobDeviceList.stream().map(EntrustDeviceDto::getDeviceId).collect(Collectors.toList());
          try {
            ResultWrapper<Map<String, List<String>>> resultWrapper = publicFeignClient.queryDeviceType(deviceIdList);
            if (resultWrapper == null || !resultWrapper.isSuccess() || resultWrapper.getData() == null){
              throw new PlatformRuntimeException(ExecPlanError.QUERY_MOBILE_DEVICE_TYPE_IS_NULL);
            }
            deviceTypeMap = resultWrapper.getData();
          }catch (Exception e){
            logger.error("查询移动设备分类异常", e);
            throw new PlatformRuntimeException(ExecPlanError.QUERY_MOBILE_DEVICE_TYPE_IS_NULL);
          }
          deviceTypeMap.forEach((type, deviceList) -> {
            String name = MobilePlatformEnum.getNameByType(type);
            logger.debug("移动端执行平台为：{}", name);
            String queueName = RabbitConstant.TASK_EXEC_QUEUE_MOB_KEY + name + "." + batchId;
            amqpAdmin.declareQueue(new Queue(queueName));
            List<EntrustDeviceDto> mobileTypeList = mobDeviceList.stream()
                    .filter(entrustDeviceDto -> deviceList.contains(entrustDeviceDto.getDeviceId()))
                    .collect(Collectors.toList());
            logger.debug("设备数量为：{}", mobileTypeList.size());
            sendEntrustMsgToQueue(entrustMsg, engineInfoListDto.getEngineInfoList(),
                    PlanConstant.ENGINE_TYPE_MOBILE, queueName, mobileTypeList);
          });
        }
      }
      if (hasApi) {
        // 2.3.接口类引擎委托
        logger.debug("发送接口引擎委托");
        String queueName = RabbitConstant.TASK_EXEC_QUEUE_API_KEY + batchId;
        logger.debug("接口PC设备集合 => " + JSONUtil.toJsonStr(pcDeviceList));
        if (CollUtil.isNotEmpty(pcDeviceList)) {
          amqpAdmin.declareQueue(new Queue(queueName));
          sendEntrustMsgToQueue(entrustMsg, engineInfoListDto.getEngineInfoList(),
                  PlanConstant.ENGINE_TYPE_API, queueName, pcDeviceList);
        }
      }
    }
  }

  /**
   * 发送委托信息至队列
   * @param entrustMsg 委托信息
   * @param engineInfoList 引擎集合
   * @param type 引擎类型
   * @param queueName 队列名称
   * @param deviceList 设备集合
   */
  private void sendEntrustMsgToQueue(AtuEntrustMsgDto entrustMsg,
                                     List<Map<String,String>> engineInfoList, String type, String queueName,
                                     List<EntrustDeviceDto> deviceList){
    entrustMsg.setType(type);
    entrustMsg.setQueueName(queueName);
    entrustMsg.setDeviceList(deviceList);
    logger.debug("委托引擎信息 => " + JSONUtil.toJsonStr(engineInfoList));
    for (Map<String, String> engineInfo : engineInfoList) {
      entrustMsg.setIp(engineInfo.get("ip"));
      entrustMsg.setPort(Integer.parseInt(engineInfo.get("port")));
      logger.debug("委托信息 => " + JSONUtil.toJsonStr(entrustMsg));
      amqpTemplate.convertAndSend(RabbitConstant.PLAN_BATCH_ENTRUST, JSONUtil.toJsonStr(entrustMsg));
    }
  }


  //克隆计划名称
  public String getComNewName(AtuPlanInfo target) {
    AtuPlanInfo atuPlanInfo = new AtuPlanInfo();
    String name = target.getPlanName();
    name = name.replace("(","\\(")
            .replace(")","\\)")
            .replace("^","\\^")
            .replace("*","\\*")
            .replace("+","\\+")+"-副本(\\([0-9]{1,2}\\)$)";
    atuPlanInfo.setPlanName(name);
    atuPlanInfo.setTenantId(NKSecurityContext.getTenantId());
    atuPlanInfo.setProjectId(target.getProjectId());

    String mdName  = atuPlanInfoService.clonePlanName(atuPlanInfo);
    int md = getMaxNum(mdName);

    return target.getPlanName()+"-副本("+ md +")";
  }



  private int getMaxNum(String getName){
    int last = 0;
    if (StringUtils.isEmpty(getName)) {
      last = 1;
    }else{
      String reg = "(\\([0-9]{1,2}\\)$)";
      Pattern pattern = Pattern.compile(reg);
      Matcher matcher = pattern.matcher(getName);
      while (matcher.find()) {
        String value = matcher.group();
        int integer = Integer.parseInt(value.substring(1, value.length() - 1));
        last = integer+1;
      }
    }
    return last;
  }


  //feign调用：校验数据
  public Map<String, Object> checkPlanData(AtuScriptInfoFeignDto feignDto) {
    ResultWrapper<Map<String, Object>> wrapper = scriptCaseFeignClient.checkPlanByCaseSet(feignDto);
    Map<String, Object> map = wrapper.getData();
    if (map!=null){
      logger.info(map.toString());
    }
    //校验用例数量
    int caseNum = Integer.parseInt(map.get("caseNum").toString());
    if (caseNum == 0) {
      throw new PlatformRuntimeException(ExecPlanError.PLAN_CASE_NUMBER_ZERO);
    }
    //计划关联应用 未被删除的
    Map<String, AppList> appMap = new HashMap<>();
    List<String> appIdList = new ArrayList<>();
    if (map.get(MsgConstant.MAP_JSON_APPALIVE)!=null) {
      String jsonString = JSON.toJSONString(map.get(MsgConstant.MAP_JSON_APPALIVE));
      logger.info("appAlive------------------" + jsonString);
      List<AppList> appAlive = JSON.parseArray(jsonString, AppList.class);
      appAlive.forEach(one -> {
        if (PlanConstant.PLATFORM_TYPE_ANDROID.equals(one.getPlatformType())) {
          logger.info("appMap:"+"【(Android)" + one.getAppPackage() + "】");
          appMap.put("【(Android)" + one.getAppPackage() + "】", one);
        }else if (PlanConstant.PLATFORM_TYPE_IOS.equals(one.getPlatformType())) {
          logger.info("appMap:"+"【(IOS)" + one.getAppPackage() + "】");
          appMap.put("【(IOS)" + one.getAppPackage() + "】", one);
        }else if (PlanConstant.PLATFORM_TYPE_HARMONY.equals(one.getPlatformType())) {
          logger.info("appMap:"+"【(Harmony)" + one.getAppPackage() + "】");
          appMap.put("【(Harmony)" + one.getAppPackage() + "】", one);
        }
        appIdList.add(one.getAppId());
      });
    }

    boolean androidDevice = false;
    boolean iosDevice = false;
    boolean pcDevice = false;
    for (DeviceList device : feignDto.getDeviceLists()) {
      if (PlanConstant.DEVICE_TYPE_PC.equals(device.getDeviceType())){
        pcDevice = true;
      }else{
        //系统类型 0-Android 1-IOS 2-HarmonyOS
        switch (device.getPlatformType()) {
          case PlanConstant.PLATFORM_TYPE_ANDROID:
            androidDevice = true;
            break;
          case PlanConstant.PLATFORM_TYPE_IOS:
            iosDevice = true;
            break;
          default:
            break;
        }
      }
    }
    //用例类型
    boolean hasAndroid = (boolean) map.get(MsgConstant.QUERY_MOBDEVICE_PLATFORM_TYPE_ANDROID);
    boolean hasIOS = (boolean) map.get(MsgConstant.QUERY_MOBDEVICE_PLATFORM_TYPE_IOS);
    boolean hasPC = (boolean) map.get(MsgConstant.PLATFORM_TYPE_HAS_PC);
    boolean hasMob = (boolean) map.get(MsgConstant.PLATFORM_TYPE_HAS_MOB);
    StringBuilder deviceType = new StringBuilder();
    //移动端的校验
    if (hasMob) {
      if (hasAndroid && !androidDevice) {
        deviceType.append(MsgConstant.ERROR_PLATFORM_TYPE_ANDROID);
      }
      if (hasIOS && !iosDevice) {
        deviceType.append(MsgConstant.ERROR_PLATFORM_TYPE_IOS);
      }
    }
    //PC端的校验
    if (hasPC && !pcDevice) {
      deviceType.append(MsgConstant.ERROR_PLATFORM_TYPE_PC);
    }
    if (deviceType.length()>0){
      throw new PlatformRuntimeException(ExecPlanError.PLAN_UPDATE_DEVICE_ERROR, deviceType.toString());
    }
    return map;
  }

  /**
   * 获取批次统计数据
   * @param record 计划信息
   * @return 统计数据
   */
  private Map<String, Integer> getBatchScriptSumData(AtuPlanInfoDetailDto record){
    Map<String, Integer> statusMap = new HashMap<>();
    String clusterKeyPrefix = "";
    String lastBatchId = record.getLastBatchId();
    int successNum = record.getSuccessNum();
    int failNum = record.getExecFailNum();
    int cancelNum = record.getCancelNum();
    int timeoutNum = record.getTimeoutNum();
    int waitingNum = record.getWaitingNum();
    int runningNum = record.getRunningNum();
    Object totalObj = null;
    if (StrUtil.isNotBlank(lastBatchId)) {
      clusterKeyPrefix = RedisConstant.CLUSTER_KEY_PREFIX + lastBatchId.substring(0,4) +
              RedisConstant.CLUSTER_KEY_SUFFIX;
      String scriptSumKey = clusterKeyPrefix + RedisConstant.BATCH_SCRIPT_SUM_KEY + record.getLastBatchId();
      Map<Object, Object> sumDataCacheMap = redisTemplate.opsForHash().entries(scriptSumKey);
      // 执行成功数 已缓存数据优先
      Object successObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_EXECUTE_SUCCESS_STATUS);
      if (successObj != null){
        successNum = Integer.parseInt(successObj.toString());
      }
      // 执行失败数
      Object failObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_EXECUTE_FAIL_STATUS);
      if (failObj != null){
        failNum = Integer.parseInt(failObj.toString());
      }
      // 取消任务数
      Object cancelObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_CANCEL_STATUS);
      if (cancelObj != null){
        cancelNum = Integer.parseInt(cancelObj.toString());
      }
      //超时任务数
      Object timeoutObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_TIMEOUT_STATUS);
      if (timeoutObj != null){
        timeoutNum = Integer.parseInt(timeoutObj.toString());
      }
      //等待中任务数
      Object waitingObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_WAIT_EXECUTE_STATUS);
      if (waitingObj != null){
        waitingNum = Integer.parseInt(waitingObj.toString());
      }
      //执行中任务数
      Object runningObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.TASK_START_EXECUTE_STATUS);
      if (runningObj != null){
        runningNum = Integer.parseInt(runningObj.toString());
      }
      totalObj = sumDataCacheMap.get(clusterKeyPrefix + PlanConstant.SCRIPT_TOTAL);
    }

    if (totalObj != null){
      int totalNum = Integer.parseInt(totalObj.toString());
      if ((waitingNum != 0 || runningNum != 0) && totalNum > waitingNum){
        record.setStatus(PlanConstant.PLAN_EXECUTING_STATUS);
      }
      if (waitingNum == 0 && runningNum == 0){
        record.setStatus(PlanConstant.PLAN_FINISH_STATUS);
      }
      if (cancelNum > 0){
        record.setStatus(PlanConstant.PLAN_CANCEL_STATUS);
      }
    }

    if (PlanConstant.PLAN_FINISH_STATUS.equals(record.getStatus())
            || PlanConstant.PLAN_CANCEL_STATUS.equals(record.getStatus())){
      int finishNum = successNum + failNum + cancelNum + timeoutNum;
      if (finishNum > 0){
        statusMap.put(MsgConstant.STATUS_CACHEKEY_SUCCESSNUM, successNum);
        statusMap.put(MsgConstant.STATUS_CACHEKEY_FAILNUM, failNum);
        statusMap.put(MsgConstant.STATUS_CACHEKEY_CANCELNUM, cancelNum);
        statusMap.put(MsgConstant.STATUS_CACHEKEY_TIMEOUTNUM, timeoutNum);
        try {
          statusMap = PercentageCalculatorUtil.computePercentage(statusMap);
        }catch (Exception e){
          logger.error("计算脚本状态占比异常", e);
        }
      }
    }
    return statusMap;
  }

  //校验计划是否关联应用与设备
  private void checkDeviceAndApp(AtuPlanInfoAddDto dto){
    if (dto.isHasMob()){
      if (dto.getAppList() == null || dto.getAppList().size() == 0) {
        throw new PlatformRuntimeException(ExecPlanError.PLAN_APP_LIST_IS_NULL);
      }
    }
    if (dto.isHasPC() || dto.isHasMob() || dto.isHasAPI()) {
      if (dto.getDeviceList() == null || dto.getDeviceList().size() == 0) {
        throw new PlatformRuntimeException(ExecPlanError.PLAN_DEVICE_LIST_IS_NULL);
      }
    }
  }
}

