package com.njcn.gather.report.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.constant.PatternRegex;
import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.utils.images.ImageConverter;
import com.njcn.gather.detection.pojo.constant.DetectionCommunicateConstant;
import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.DetectionData;
import com.njcn.gather.detection.pojo.vo.SocketMsg;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.device.mapper.PqDevMapper;
import com.njcn.gather.device.pojo.enums.CheckStateEnum;
import com.njcn.gather.device.pojo.enums.DevReportStateEnum;
import com.njcn.gather.device.pojo.param.PqDevParam;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.pojo.vo.PqDevVO;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.err.pojo.po.PqErrSys;
import com.njcn.gather.err.service.IPqErrSysService;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.service.IPqMonitorService;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import com.njcn.gather.plan.pojo.enums.PlanReportStateEnum;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import com.njcn.gather.plan.service.IAdPlanService;
import com.njcn.gather.plan.service.IAdPlanSourceService;
import com.njcn.gather.plan.service.IAdPlanTestConfigService;
import com.njcn.gather.pojo.enums.DetectionResponseEnum;
import com.njcn.gather.report.mapper.PqReportMapper;
import com.njcn.gather.report.pojo.DevReportParam;
import com.njcn.gather.report.pojo.constant.ItemAnchorConstant;
import com.njcn.gather.report.pojo.constant.PowerConstant;
import com.njcn.gather.report.pojo.enums.*;
import com.njcn.gather.report.pojo.param.ReportParam;
import com.njcn.gather.report.pojo.po.PqReport;
import com.njcn.gather.report.pojo.result.ContrastTestResult;
import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.report.pojo.vo.PqReportVO;
import com.njcn.gather.report.service.IPqReportService;
import com.njcn.gather.report.util.ItemAnchorAssembler;
import com.njcn.gather.report.util.ItemAnchorSectionUtil;
import com.njcn.gather.result.service.IResultService;
import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO;
import com.njcn.gather.script.service.IPqScriptDtlsService;
import com.njcn.gather.source.pojo.po.PqSource;
import com.njcn.gather.storage.pojo.param.SingleNonHarmParam;
import com.njcn.gather.storage.pojo.po.SimAndDigHarmonicResult;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import com.njcn.gather.storage.service.SimAndDigHarmonicService;
import com.njcn.gather.storage.service.SimAndDigNonHarmonicService;
import com.njcn.gather.system.cfg.pojo.enums.SceneEnum;
import com.njcn.gather.system.cfg.service.ISysTestConfigService;
import com.njcn.gather.system.config.PathConfig;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.pojo.enums.SystemResponseEnum;
import com.njcn.gather.tools.report.model.constant.ReportConstant;
import com.njcn.gather.tools.report.service.IWordReportService;
import com.njcn.gather.tools.report.util.*;
import com.njcn.gather.type.pojo.po.DevType;
import com.njcn.gather.type.service.IDevTypeService;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.http.util.RestTemplateUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.RequestUtil;
import io.netty.channel.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.*;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBElement;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author caozehui
 * @date 2025-03-19
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> implements IPqReportService {

    /**
     * resultMap 内部协议：当 dealDataLine 跑完一轮但未采集到任何有效合格性数据时，
     * 在 resultMap 中塞入这个 key 作为"已尝试且无数据"的哨兵，避免后续 TEST_RESULT_*
     * 分支的 fallback 重复调用 dealDataLine 浪费资源；同时 dealTestResultLine 检测到
     * 此 key 存在时跳过结论表生成。
     * <p>
     * 故意使用明显非业务字符串，避免与任何 PqScriptDtls.scriptCode / PowerIndexEnum
     * 的真实业务取值撞车。
     */
    private static final String RESULT_MAP_NO_DATA_FLAG = "__internal_no_data__";
    private static final String[] CHINESE_DIGITS = {"O", "一", "二", "三", "四", "五", "六", "七", "八", "九"};

    //    @Value("${report.template:D:\\template}")
//    private String templatePath;
//
//    @Value("${report.reportDir:D:\\report}")
//    private String reportPath;
    private final PathConfig pathConfig;
    
    @Value("${qr.cloud}")
    private String cloudUrl;

    @Value("${qr.dev.name}")
    private String devName;

    @Value("${qr.dev.password}")
    private String devPsd;

    @Value("${qr.dev.port}")
    private Integer devPort;

    @Value("${qr.dev.path}")
    private String devPath;

    @Value("${qr.gcDev.name}")
    private String gcDevName;

    @Value("${qr.gcDev.password}")
    private String gcDevPsd;

    @Value("${qr.gcDev.port}")
    private Integer gcDevPort;

    @Value("${qr.gcDev.path}")
    private String gcDevPath;

    @Value("${report.dateFormat}")
    private String dateFormat;

    private final IPqDevService iPqDevService;
    private final PqDevMapper pqDevMapper;
    private final IPqDevSubService iPqDevSubService;
    private final IDictDataService dictDataService;
    private final IAdPlanService adPlanService;
    private final IAdPlanSourceService adPlanSourceService;
    private final IPqScriptDtlsService pqScriptDtlsService;
    private final SimAndDigNonHarmonicService adNonHarmonicService;
    private final SimAndDigHarmonicService adHarmonicService;
    private final IDevTypeService devTypeService;
    private final IResultService resultService;
    private final ISysTestConfigService sysTestConfigService;
    private final SocketManager socketManager;
    private final IWordReportService wordReportService;
    private final ISysUserService sysUserService;
    private final IPqErrSysService pqErrSysService;
    private final IAdPlanTestConfigService adPlanTestConfigService;
    private final IPqMonitorService pqMonitorService;

    @Resource
    private RestTemplateUtil restTemplateUtil;


    @Override
    public Page<PqReportVO> list(ReportParam.QueryParam queryParam) {
        QueryWrapper<PqReport> wrapper = new QueryWrapper<>();
        wrapper.like(StrUtil.isNotBlank(queryParam.getName()), "name", queryParam.getName())
                .eq(StrUtil.isNotBlank(queryParam.getVersion()), "version", queryParam.getVersion())
                .eq("pattern", queryParam.getPattern())
                .eq("state", DataStateEnum.ENABLE.getCode());
        wrapper.orderByDesc("Update_Time");
        Page<PqReport> page1 = this.page(new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), wrapper);

        List<PqReportVO> pqReportVOList = page1.getRecords().stream().map(pqReport -> {
            PqReportVO pqReportVO = new PqReportVO();
            BeanUtils.copyProperties(pqReport, pqReportVO);

            PqReportVO.FileVO baseFileVo = new PqReportVO.FileVO();
            baseFileVo.setName(pqReport.getBasePath().substring(pqReport.getBasePath().lastIndexOf(File.separator) + 1));
            baseFileVo.setUrl(pqReport.getBasePath());
            pqReportVO.setBaseFileVO(baseFileVo);

            PqReportVO.FileVO detailFileVo = new PqReportVO.FileVO();
            detailFileVo.setName(pqReport.getDetailPath().substring(pqReport.getDetailPath().lastIndexOf(File.separator) + 1));
            detailFileVo.setUrl(pqReport.getDetailPath());
            pqReportVO.setDetailFileVO(detailFileVo);

            return pqReportVO;
        }).collect(Collectors.toList());

        Page<PqReportVO> page2 = new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam));
        page2.setTotal(page1.getTotal());
        page2.setOrders(page1.orders());
        page2.setPages(page1.getPages());
        page2.setRecords(pqReportVOList);
        return page2;
    }

    @Override
    public PqReportVO getByReportId(String id) {
        PqReport pqReport = this.baseMapper.selectById(id);
        PqReportVO pqReportVO = new PqReportVO();
        BeanUtils.copyProperties(pqReport, pqReportVO);

        PqReportVO.FileVO baseFileVo = new PqReportVO.FileVO();
        baseFileVo.setName(pqReport.getBasePath().substring(pqReport.getBasePath().lastIndexOf(File.separator) + 1));
        baseFileVo.setUrl(pqReport.getBasePath());
        pqReportVO.setBaseFileVO(baseFileVo);

        PqReportVO.FileVO detailFileVo = new PqReportVO.FileVO();
        detailFileVo.setName(pqReport.getDetailPath().substring(pqReport.getDetailPath().lastIndexOf(File.separator) + 1));
        detailFileVo.setUrl(pqReport.getDetailPath());
        pqReportVO.setDetailFileVO(detailFileVo);

        return pqReportVO;
    }

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public boolean add(ReportParam reportParam) {
        this.checkPattern(reportParam);
        this.checkRepeat(reportParam, false);
        PqReport pqReport = new PqReport();
        BeanUtils.copyProperties(reportParam, pqReport);
        pqReport.setState(DataStateEnum.ENABLE.getCode());

        // 上传文件
        this.uploadFile(reportParam, pqReport, true);
        return this.save(pqReport);
    }

    private void checkPattern(ReportParam reportParam) {
        String name = reportParam.getName();
        String version = reportParam.getVersion();
        if (!Pattern.matches(PatternRegex.REPORT_NAME_REGEX, name)) {
            throw new BusinessException(ReportResponseEnum.REPORT_NAME_PATTERN_ERROR);
        }
        if (!Pattern.matches(PatternRegex.REPORT_VERSION_REGEX, version)) {
            throw new BusinessException(ReportResponseEnum.REPORT_VERSION_PATTERN_ERROR);
        }
    }

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public boolean update(ReportParam.UpdateParam reportParam) {
        this.checkPattern(reportParam);
        this.checkRepeat(reportParam, true);
        PqReport pqReport = new PqReport();
        BeanUtils.copyProperties(reportParam, pqReport);

        // 上传文件
        this.uploadFile(reportParam, pqReport, false);
        return this.updateById(pqReport);
    }

    @Override
    public boolean delete(List<String> ids) {
        List<String> boundReportIds = this.baseMapper.getBoundReportIds();
        if (CollectionUtil.containsAny(boundReportIds, ids)) {
            throw new BusinessException(ReportResponseEnum.DELETE_BOUND_REPORT_ERROR);
        }

        // 删除对应的文件
        this.deleteFile(ids);
        return this.lambdaUpdate().in(CollectionUtil.isNotEmpty(ids), PqReport::getId, ids)
                .set(PqReport::getState, DataStateEnum.DELETED.getCode())
                .update();
    }

    @Override
    public List<PqReportVO.OptionVO> listOptions(String pattern) {
        return this.baseMapper.listOptionsByPattern(pattern);
    }

    /**
     * 上传文件，并设置pqReport的basePath和detailPath属性
     *
     * @param reportParam 报告参数
     * @param pqReport    报告信息
     * @param isAdd       是否添加
     */
    private void uploadFile(ReportParam reportParam, PqReport pqReport, boolean isAdd) {
        MultipartFile baseFile = reportParam.getBaseFile();
        MultipartFile detailFile = reportParam.getDetailFile();
        String relativePath = this.buildReportTemplateRelativeDir(reportParam.getPattern(), reportParam.getName(), reportParam.getVersion());
        String newDir = pathConfig.getReportTemplatePath() + File.separator + relativePath;

        long FILE_SIZE_LIMIT = 5 * 1024 * 1024;
        if (isAdd) {
            if (ObjectUtil.isNotNull(baseFile) && !baseFile.isEmpty() && ObjectUtil.isNotNull(detailFile) && !detailFile.isEmpty()) {

                String baseOriginalFilename = baseFile.getOriginalFilename();
                String detailOriginalFilename = detailFile.getOriginalFilename();

                if (!baseOriginalFilename.endsWith(ReportConstant.DOCX) || !detailOriginalFilename.endsWith(ReportConstant.DOCX)) {
                    throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR);
                }
                if (baseOriginalFilename.equals(detailOriginalFilename)) {
                    throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR);
                }
                if (baseFile.getSize() > FILE_SIZE_LIMIT || detailFile.getSize() > FILE_SIZE_LIMIT) {
                    throw new BusinessException(ReportResponseEnum.FILE_SIZE_ERROR);
                }

                pqReport.setBasePath(relativePath + baseOriginalFilename);
                pqReport.setDetailPath(relativePath + detailOriginalFilename);

                this.createDirectory(newDir);
                this.clearDirectory(newDir);
                this.uploadFile(baseFile, newDir + baseOriginalFilename);
                this.uploadFile(detailFile, newDir + detailOriginalFilename);
            } else {
                throw new BusinessException(ReportResponseEnum.FILE_NOT_NULL);
            }
        } else {
            PqReport oldPqReport = this.getById(pqReport.getId());
            String oldDir = oldPqReport.getBasePath().substring(0, oldPqReport.getBasePath().lastIndexOf(File.separator) + 1);

            String baseFileOriginalFilename = "";
            if (ObjectUtil.isNotNull(baseFile) && !baseFile.isEmpty()) {
                baseFileOriginalFilename = baseFile.getOriginalFilename();
                if (!baseFileOriginalFilename.endsWith(ReportConstant.DOCX)) {
                    throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR);
                }
                if (baseFile.getSize() > FILE_SIZE_LIMIT) {
                    throw new BusinessException(ReportResponseEnum.FILE_SIZE_ERROR);
                }
            }

            String detailFileOriginalFilename = "";
            if (ObjectUtil.isNotNull(detailFile) && !detailFile.isEmpty()) {
                detailFileOriginalFilename = detailFile.getOriginalFilename();
                if (!detailFileOriginalFilename.endsWith(ReportConstant.DOCX)) {
                    throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR);
                }
                if (detailFile.getSize() > FILE_SIZE_LIMIT) {
                    throw new BusinessException(ReportResponseEnum.FILE_SIZE_ERROR);
                }
            }

            if (!baseFileOriginalFilename.isEmpty() && !detailFileOriginalFilename.isEmpty()) {
                if (baseFileOriginalFilename.equals(detailFileOriginalFilename)) {
                    throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR);
                }
            }
            if (!baseFileOriginalFilename.isEmpty()) {
                if (baseFileOriginalFilename.equals(oldPqReport.getDetailPath().substring(oldPqReport.getDetailPath().lastIndexOf(File.separator) + 1))) {
                    throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR);
                }
            }
            if (!detailFileOriginalFilename.isEmpty()) {
                if (detailFileOriginalFilename.equals(oldPqReport.getBasePath().substring(oldPqReport.getBasePath().lastIndexOf(File.separator) + 1))) {
                    throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR);
                }
            }

            //若修改了文件名称、版本号，则需要重命名文件
            this.createDirectory(newDir);
            if (!oldDir.equals(relativePath)) {
                // 文件夹重命名
                String oldBasePathStr = oldPqReport.getBasePath();
                String baseName = oldBasePathStr.substring(oldBasePathStr.lastIndexOf(File.separator) + 1);
                Path oldBasePath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + oldBasePathStr);
                Path newBasePath = Paths.get(newDir + baseName);
                pqReport.setBasePath(relativePath + baseName);

                String oldDetailPathStr = oldPqReport.getDetailPath();
                String detailName = oldDetailPathStr.substring(oldDetailPathStr.lastIndexOf(File.separator) + 1);
                Path oldDetailPath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + oldDetailPathStr);
                Path newDetailPath = Paths.get(newDir + detailName);
                pqReport.setDetailPath(relativePath + detailName);

                try {
                    // windows下文件夹名称不区分大小写
                    if (!oldDir.equalsIgnoreCase(relativePath)) {
                        this.clearDirectory(newDir);
                        Files.move(oldBasePath, newBasePath);
                        Files.move(oldDetailPath, newDetailPath);
                        if (!oldPqReport.getName().equals(reportParam.getName()) && !this.existSameName(pqReport.getId(), oldPqReport.getName())) {
                            this.recursionDeleteDirectory(pathConfig.getReportTemplatePath() + File.separator + oldPqReport.getName());
                        } else {
                            Paths.get(pathConfig.getReportTemplatePath() + oldDir).toFile().delete();
                        }
                    } else {
                        // 文件夹重命名
                        Paths.get(pathConfig.getReportTemplatePath() + oldDir).toFile().renameTo(Paths.get(newDir).toFile());
                    }
                } catch (IOException e) {
                    throw new BusinessException(ReportResponseEnum.FILE_RENAME_FAILED);
                }
            }

            if (!baseFileOriginalFilename.isEmpty()) {
                pqReport.setBasePath(relativePath + baseFileOriginalFilename);
                Paths.get(pathConfig.getReportTemplatePath() + File.separator + oldPqReport.getBasePath()).toFile().delete();
                Paths.get(newDir + oldPqReport.getBasePath().substring(oldPqReport.getBasePath().lastIndexOf(File.separator) + 1)).toFile().delete();
                this.uploadFile(baseFile, newDir + baseFileOriginalFilename);
            }
            if (!detailFileOriginalFilename.isEmpty()) {
                pqReport.setDetailPath(relativePath + detailFileOriginalFilename);
                Paths.get(pathConfig.getReportTemplatePath() + File.separator + oldPqReport.getDetailPath()).toFile().delete();
                Paths.get(newDir + oldPqReport.getDetailPath().substring(oldPqReport.getDetailPath().lastIndexOf(File.separator) + 1)).toFile().delete();
                this.uploadFile(detailFile, newDir + detailFileOriginalFilename);
            }
        }
    }

    private void uploadFile(MultipartFile file, String filePath) {
        if (!ObjectUtil.isNull(file) && !file.isEmpty()) {
            try {
                // 如果文件存在，则先删除
                Path path = Paths.get(filePath);
                File file1 = path.toFile();
                if (file1.exists()) {
                    file1.delete();
                }

                // 保存文件
                byte[] bytes = file.getBytes();
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filePath));
                bufferedOutputStream.write(bytes);
                bufferedOutputStream.flush();
                bufferedOutputStream.close();

                bufferedOutputStream.close();
            } catch (IOException e) {
                throw new BusinessException(ReportResponseEnum.FILE_UPLOAD_FAILED);
            }
        }
    }

    /**
     * 返回是否存在同名（但是不同版本号）的报告
     *
     * @param id
     * @param name
     * @return
     */
    public boolean existSameName(String id, String name) {
        return this.lambdaQuery().eq(PqReport::getName, name)
                .ne(PqReport::getId, id)
                .eq(PqReport::getState, DataStateEnum.ENABLE.getCode())
                .count() > 0;
    }

    /**
     * 创建目录并清空目录下的文件
     *
     * @param dirPath
     * @throws IOException
     */
    private void createDirectory(String dirPath) {
        // 创建上传目录（如果不存在）
        Path uploadPath = Paths.get(dirPath);
        if (!Files.exists(uploadPath)) {
            try {
                Files.createDirectories(uploadPath);
            } catch (IOException e) {
                throw new BusinessException(DetectionResponseEnum.CREATE_DIRECTORY_FAILED);
            }
        }
    }

    /**
     * 清空目录下的文件
     *
     * @param dirPath
     */
    private void clearDirectory(String dirPath) {
        File[] files = Paths.get(dirPath).toFile().listFiles();
        if (ArrayUtil.isNotEmpty(files)) {
            for (int i = 0; i < files.length; i++) {
                files[i].delete();
            }
        }
    }

    /**
     * 根据id删除已有的文件
     *
     * @param ids
     */
    private void deleteFile(List<String> ids) {
        List<PqReport> pqReports = this.listByIds(ids);
        for (PqReport pqReport : pqReports) {
            String uploadDir = pathConfig.getReportTemplatePath() + File.separator + pqReport.getName() + File.separator + pqReport.getVersion() + File.separator;
            Path uploadPath = Paths.get(uploadDir);
            if (Files.exists(uploadPath)) {
                //清空目录下的文件
                File file1 = uploadPath.toFile();
                File[] fileArr1 = file1.listFiles();
                if (ArrayUtil.isNotEmpty(fileArr1)) {
                    for (int i = 0; i < fileArr1.length; i++) {
                        fileArr1[i].delete();
                    }
                }
                file1.delete();
                String dir = pathConfig.getReportTemplatePath() + File.separator + pqReport.getName() + File.separator;
                Path dirPath = Paths.get(dir);
                File dirFile = dirPath.toFile();
                File[] fileArr2 = dirFile.listFiles();
                if (ArrayUtil.isEmpty(fileArr2)) {
                    dirFile.delete();
                }
            }
        }
    }

    /**
     * 递归删除目录
     *
     * @param dirPath
     */
    private void recursionDeleteDirectory(String dirPath) {
        Path path = Paths.get(dirPath);
        if (Files.exists(path)) {
            try {
                Files.walk(path)
                        .sorted(Comparator.reverseOrder())
                        .map(Path::toFile)
                        .forEach(File::delete);
            } catch (IOException e) {
                throw new BusinessException(DetectionResponseEnum.DELETE_DIRECTORY_FAILED);
            }
        }
    }

    @Override
    public void generateReport(DevReportParam devReportParam) {
        AdPlan plan = adPlanService.getById(devReportParam.getPlanId());
        //  0 - 模板占位符更新, 1 - 根据配置模版动态组合生产的报告
        if (plan.getAssociateReport() == 1) {
            DictData dictDataById = dictDataService.getDictDataById(plan.getPattern());
            if (Objects.isNull(dictDataById)) {
                throw new BusinessException("计划没有配置合理的模式");
            }
            // 比对模式
            if (dictDataById.getCode().equals("Contrast")) {
                this.generateReportByPlanContrast(plan, devReportParam);
            } else {
                // 数模式
                this.generateReportByPlan(plan, devReportParam);
            }
        } else if (plan.getAssociateReport() == 0) {
            this.generateReportByDevType(devReportParam);
        }
    }


    /**
     * 根据设备类型生成报告
     * 注：该方法目前仅支持楼下出厂检测场景，属于模板占位符替换方式，后期可能会有调整
     * <p>
     * 批量语义：采用"任一设备失败则整批中断"——forEach 循环内任一设备抛 BusinessException
     * 会终止整个循环，已生成的报告文件保留，中断之后的设备不再处理；失败原因通过异常抛给
     * 调用方，调用方应提示用户修复问题后重新发起批量。此为有意保留的批量原子语义，
     * 而非待修复缺陷。
     *
     * @param devReportParam 被检设备信息
     */
    private void generateReportByDevType(DevReportParam devReportParam) {
        devReportParam.getDevIdList().forEach(devId -> {
            // 根据设备类型找到报告模板
            PqDevVO pqDevVO = iPqDevService.getPqDevById(devId);
            devReportParam.setDevId(devId);
            if (Objects.isNull(pqDevVO)) {
                throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST);
            }
            // 获取设备型号
            DevType devType = devTypeService.getById(pqDevVO.getDevType());
            if (Objects.isNull(devType)) {
                throw new BusinessException(ReportResponseEnum.DEVICE_TYPE_NOT_EXIST);
            }
            DictData reportName = devTypeService.getReportName(pqDevVO.getDevType());
            if (Objects.isNull(reportName)) {
                throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST);
            }
            // 读取模板文件
            ClassPathResource resource = new ClassPathResource("/model/" + reportName.getCode() + ReportConstant.DOCX);
            try (InputStream inputStream = resource.getInputStream()) {
                // 处理基础模版中的信息
                Map<String, String> baseModelDataMap = dealBaseModelData(pqDevVO, devType);
                InputStream wordFinishInputStream = wordReportService.replacePlaceholders(inputStream, baseModelDataMap);
                List<InputStream> wordFileInputStreams = new ArrayList<>();
                wordFileInputStreams.add(wordFinishInputStream);
                // 处理数据页中的信息
                dealDataModel(wordFileInputStreams, devReportParam, pqDevVO);
                // 合并文档
                InputStream finalWordStream = DocxMergeUtil.mergeDocuments(wordFileInputStreams);
                // 处理需要输出的目录地址 基础路径+设备类型+装置编号.docx
                // 最终文件输出的路径
//                String dirPath = pathConfig.getReportPath().concat(File.separator).concat(devType.getName());
                String dirPath = pathConfig.getReportPath();
                // 确保目录存在
                ensureDirectoryExists(dirPath);
                String reportFullPath = dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX);
                // 4. 保存新的Word文档
                WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(finalWordStream);
                wordPackage.save(new File(reportFullPath));
                this.updateDevAndPlanState(devId, devReportParam.getPlanId());
                // 异步将有效的二维码下装到被检设备
                CompletableFuture.runAsync(() -> {
                    try {
                        sendQrToDevice(pqDevVO.getIp(), pqDevVO.getCreateId() + ReportConstant.DOCX);
                        log.info("二维码下装成功，设备IP: {}", pqDevVO.getIp());
                    } catch (Exception e) {
                        log.error("二维码下装失败，设备IP: {}", pqDevVO.getIp(), e);
                    }
                });
                // 异步将检测报告上传到云端，但是不一定成功，需要无线网支撑
                CompletableFuture.runAsync(() -> {
                    File file = new File(reportFullPath);
                    if (file.exists()) {
                        try {
                            ResponseEntity<String> responseEntity = restTemplateUtil.uploadFile(cloudUrl + "/upload", file);
                            if (responseEntity.getStatusCode().is2xxSuccessful()) {
                                // 将被检设备的报告状态改为已生成且已上传
                                iPqDevService.updatePqDevReportState(devReportParam.getDevId(), DevReportStateEnum.GENERATED_UPLOADED.getValue());
                            }
                        } catch (Exception e) {
                            log.error("文件上传到云端失败", e);
                        }
                    }
                });
            } catch (NoSuchFileException e) {
                String filePath = e.getFile() != null ? e.getFile().replaceAll("\\\\", "/") : "未知文件";
                log.error("报告模板文件不存在 - 文件路径: {}", filePath, e);
                throw new BusinessException(SystemResponseEnum.FILE_NOT_FOUND);
            } catch (IOException e) {
                log.error("报告文件读写异常", e);
                throw new BusinessException(SystemResponseEnum.FILE_IO_ERROR);
            } catch (Exception e) {
                log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e);
                throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR);
            }
        });
    }


    /**
     * 发送二维码至装置
     *
     * @param devIp      装置IP
     * @param reportName 报告名称
     */
    private void sendQrToDevice(String devIp, String reportName) {
        try {
            // 上传没问题后，拼接url生成二维码，并将二维码转为bin格式文件传递给设备通讯模块
            String qrUrl = cloudUrl + "/download/" + reportName;
            BufferedImage bufferedImage = QrCodeUtil.generate(qrUrl, 148, 148);
            // 将 BufferedImage 转换为单色图像
            BufferedImage monoImage = new BufferedImage(148, 148, BufferedImage.TYPE_BYTE_BINARY);
            Graphics2D g2d = monoImage.createGraphics();
            g2d.drawImage(bufferedImage, 0, 0, null);
            g2d.dispose();
            // 3. 【关键修正】创建一个新的1-bit单色图像，并将原始图像绘制上去，以强制转换为单色位图
            BufferedImage monochromeImage = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
            monochromeImage.getGraphics().drawImage(bufferedImage, 0, 0, null);
            // 4. 将单色图像在内存中编码为完整的BMP文件流
            byte[] finalBinData = ImageConverter.convertToBinFormat(bufferedImage);
            String base64String = Base64.getEncoder().encodeToString(finalBinData);

            // 测试ftp是否连接成功


            // 组装设备通讯模块的参数
            SocketMsg<String> sendFileMsg = new SocketMsg<>();
            sendFileMsg.setOperateCode(SourceOperateCodeEnum.FTP_SEND_01.getValue());
            sendFileMsg.setRequestId(SourceOperateCodeEnum.FTP_SEND_01.getValue());

            // 组装业务数据
            JSONObject data = new JSONObject();
            if (testFTPConnection(devIp, devPort, devName, devPsd)) {
                data.set("name", devName);
                data.set("password", devPsd);
                data.set("port", devPort);
                data.set("path", "ftp://" + devIp + devPath);
            } else if (testFTPConnection(devIp, gcDevPort, gcDevName, gcDevPsd)) {
                data.set("name", gcDevName);
                data.set("password", gcDevPsd);
                data.set("port", gcDevPort);
                data.set("path", "ftp://" + devIp + gcDevPath);
            } else {
                throw new BusinessException(ReportResponseEnum.FTP_CONNECTION_FAILED);
            }
            data.set("file", base64String);
            sendFileMsg.setData(data.toString());
            String msg = JSON.toJSONString(sendFileMsg);
            Channel channel = SocketManager.getChannelByUserId(RequestUtil.getLoginName() + DetectionCommunicateConstant.DEV);
            if (Objects.isNull(channel) || !channel.isActive()) {
                // 进行源通信连接
                PreDetectionParam preDetectionParam = new PreDetectionParam();
                preDetectionParam.setUserPageId(RequestUtil.getLoginName());
                preDetectionParam.setSendWebMsg(false);
                // 使用智能发送工具类，自动管理设备连接
                socketManager.smartSendToDevice(preDetectionParam, msg);
            } else {
                channel.writeAndFlush(msg + "\n");
            }
        } catch (Exception e) {
            // 进行日志记录入口
            System.out.println("异常为：" + e.getMessage());
        }
    }


    /**
     * 测试FTP连接
     *
     * @param host     FTP服务器地址
     * @param port     FTP端口
     * @param username 用户名
     * @param password 密码
     * @return 连接是否成功
     */
    public static boolean testFTPConnection(String host, int port, String username, String password) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接FTP服务器
            ftpClient.connect(host, port);
            int replyCode = ftpClient.getReplyCode();

            if (!FTPReply.isPositiveCompletion(replyCode)) {
                System.err.println("FTP服务器拒绝连接，回复代码: " + replyCode);
                return false;
            }

            // 登录FTP服务器
            boolean loginSuccess = ftpClient.login(username, password);
            if (!loginSuccess) {
                System.err.println("FTP登录失败，请检查用户名和密码");
                return false;
            }
            // 设置传输模式为被动模式
            //ftpClient.enterLocalPassiveMode();
            // 设置文件传输类型为二进制
            //ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            System.out.println("FTP连接测试成功!");
            return true;
        } catch (IOException e) {
            System.err.println("FTP连接测试失败: " + e.getMessage());
            return false;
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                System.err.println("关闭FTP连接时出错: " + e.getMessage());
            }
        }
    }

    /**
     * 需要支持批量生成,如果用户选择批量生成,则默认都采用测试数据的第一个合格,如果
     * 比对模式下生成检测报告,实际后期需要根据用户选择的检测数据或者某次波形数据生成报告
     * <p>
     * 批量语义：采用"任一设备失败则整批中断"——forEach 循环内任一设备抛 BusinessException
     * 会终止整个循环，已生成的报告文件保留，中断之后的设备不再处理；失败原因通过异常抛给
     * 调用方，调用方应提示用户修复问题后重新发起批量。此为有意保留的批量原子语义，
     * 而非待修复缺陷。
     *
     * @param plan           计划信息
     * @param devReportParam 设备信息
     */
    private void generateReportByPlanContrast(AdPlan plan, DevReportParam devReportParam) {
        // 支持批量生成报告
        devReportParam.getDevIdList().forEach(devId -> {
            // 准备被检设备的基础信息
            PqDevVO pqDevVO = iPqDevService.getPqDevById(devId);
            if (Objects.isNull(pqDevVO)) {
                throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST);
            }
            if (CheckStateEnum.CHECKED.getValue().compareTo(pqDevVO.getCheckState()) <= 0) {
                devReportParam.setDevId(devId);
                // 获取设备型号
                DevType devType = devTypeService.getById(pqDevVO.getDevType());
                if (Objects.isNull(devType)) {
                    throw new BusinessException(ReportResponseEnum.DEVICE_TYPE_NOT_EXIST);
                }
                PqReport report = this.lambdaQuery().eq(PqReport::getId, plan.getReportTemplateId()).eq(PqReport::getState, DataStateEnum.ENABLE.getCode()).one();
                if (Objects.isNull(report)) {
                    throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST);
                }
                Path basePath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + report.getBasePath());
                Path detailPath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + report.getDetailPath());
                try (InputStream baseInputStream = Files.newInputStream(basePath);
                     InputStream detailInputStream = Files.newInputStream(detailPath)) {
                    WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(detailInputStream);
                    // 获取文档基础部分，并替换占位符
                    Map<String, String> baseModelDataMap = dealBaseModelContrastData(plan, pqDevVO, devType);
                    InputStream wordFinishInputStream = wordReportService.replacePlaceholders(baseInputStream, baseModelDataMap);
                    WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(wordFinishInputStream);
                    MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart();

                    // 获取数据模版页内容，根据脚本动态组装数据页内容
                    MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart();
                    dealDataModelScatteredByBookmarkByPlanContrast(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO);
                    // 保存新的文档
                    String dirPath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(plan.getName()));
                    // 确保目录存在
                    ensureDirectoryExists(dirPath);
                    // 构建文件名：cityName_gdName_subName_name.docx
                    String fileName = String.format("%s_%s_%s_%s.docx",
                            pqDevVO.getCityName() != null ? pqDevVO.getCityName() : "未知地市",
                            pqDevVO.getGdName() != null ? pqDevVO.getGdName() : "未知供电公司",
                            pqDevVO.getSubName() != null ? pqDevVO.getSubName() : "未知电站",
                            pqDevVO.getName() != null ? pqDevVO.getName() : "未知设备");
                    // 判断是否需要在报告上输出水印
                    String leader = baseModelDataMap.get(BaseReportKeyEnum.AUDIT_BY.getKey());
                    String loginName = RequestUtil.getLoginNameByToken();
                    SysUser user = sysUserService.getUserByLoginName(loginName);
                    if (!leader.equals(user.getName())) {
                        log.info("当前用户不是审核人，添加非正式水印");
                        Docx4jUtil.addWatermarkToDocument(baseModelDocument, "非正式");
                    }
                    Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(baseModelDocument);
                    Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument);
                    baseModelDocument.save(new File(dirPath.concat(File.separator).concat(fileName)));
                    this.updateDevAndPlanState(devId, devReportParam.getPlanId());
                } catch (NoSuchFileException e) {
                    String filePath = e.getFile() != null ? e.getFile().replaceAll("\\\\", "/") : "未知文件";
                    log.error("报告模板文件不存在 - 文件路径: {}", filePath, e);
                    throw new BusinessException(SystemResponseEnum.FILE_NOT_FOUND);
                } catch (IOException e) {
                    log.error("报告文件读写异常", e);
                    throw new BusinessException(SystemResponseEnum.FILE_IO_ERROR);
                } catch (Exception e) {
                    log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e);
                    throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR);
                }
            }
        });

    }


    /**
     * 根据计划绑定的报告模板生成报告
     * 注：该方法目前属于同用信息占位符替换，数据页为面向对象动态填充拼凑方式
     * <p>
     * 批量语义：采用"任一设备失败则整批中断"——forEach 循环内任一设备抛 BusinessException
     * 会终止整个循环，已生成的报告文件保留，中断之后的设备不再处理；失败原因通过异常抛给
     * 调用方，调用方应提示用户修复问题后重新发起批量。此为有意保留的批量原子语义，
     * 而非待修复缺陷。
     *
     * @param plan           计划信息
     * @param devReportParam 设备信息
     */
    private void generateReportByPlan(AdPlan plan, DevReportParam devReportParam) {
        // 支持批量生成报告
        devReportParam.getDevIdList().forEach(devId -> {
            // 准备被检设备的基础信息
            PqDevVO pqDevVO = iPqDevService.getPqDevById(devId);
            if (Objects.isNull(pqDevVO)) {
                throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST);
            }
            devReportParam.setDevId(devId);
            // 获取设备型号
            DevType devType = devTypeService.getById(pqDevVO.getDevType());
            if (Objects.isNull(devType)) {
                throw new BusinessException(ReportResponseEnum.DEVICE_TYPE_NOT_EXIST);
            }
            PqReport report = this.lambdaQuery().eq(PqReport::getId, plan.getReportTemplateId()).eq(PqReport::getState, DataStateEnum.ENABLE.getCode()).one();
            if (Objects.isNull(report)) {
                throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST);
            }
            Path basePath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + report.getBasePath());
            Path detailPath = Paths.get(pathConfig.getReportTemplatePath() + File.separator + report.getDetailPath());
            try (InputStream baseInputStream = Files.newInputStream(basePath);
                 InputStream detailInputStream = Files.newInputStream(detailPath)) {
                WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(detailInputStream);
                // 获取文档基础部分，并替换占位符
                Map<String, String> baseModelDataMap = dealBaseModelData(pqDevVO, devType);
                // 补充检测源名称：计划↔源走 ad_plan_source 中间表（业务上单源），datasourceId 是数据源类型枚举、不指向 pq_source
                List<PqSource> planSources = adPlanSourceService.listPqSourceByPlanId(plan.getId());
                baseModelDataMap.put(BaseReportKeyEnum.SOURCE_NAME.getKey(),
                        CollUtil.isNotEmpty(planSources) ? planSources.get(0).getName() : StrUtil.EMPTY);
                InputStream wordFinishInputStream = wordReportService.replacePlaceholders(baseInputStream, baseModelDataMap);
                WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(wordFinishInputStream);
                MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart();

                // 获取数据模版页内容，根据脚本动态组装数据页内容
                MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart();
                // 新旧模板分流：base 含白名单内 item_ 前缀书签 → 指标锚点新型模板（云南检定报告类），走独立旁路；
                // 否则走原路径，行为与改动前完全一致。设计文档 specs/2026-07-02-item-anchor-report-template-design.md §4.1
                // 书签全文档扫描只做一次，分流判断与新路径共用；老路径方法签名不动，内部自扫
                List<BookmarkUtil.BookmarkInfo> baseBookmarks = BookmarkUtil.findAllBookmarks(baseDocumentPart);
                if (hasItemAnchor(baseBookmarks)) {
                    dealDataModelByItemAnchor(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, baseBookmarks);
                } else {
                    dealDataModelScatteredByBookmark(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO);
                }

                // 保存新的文档
                String dirPath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName()));
                // 确保目录存在
                ensureDirectoryExists(dirPath);
                Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument);
                baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX)));
                this.updateDevAndPlanState(devId, devReportParam.getPlanId());
            } catch (NoSuchFileException e) {
                String filePath = e.getFile() != null ? e.getFile().replaceAll("\\\\", "/") : "未知文件";
                log.error("报告模板文件不存在 - 文件路径: {}", filePath, e);
                throw new BusinessException(SystemResponseEnum.FILE_NOT_FOUND);
            } catch (IOException e) {
                log.error("报告文件读写异常", e);
                throw new BusinessException(SystemResponseEnum.FILE_IO_ERROR);
            } catch (Exception e) {
                log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e);
                throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR);
            }
        });
    }

    /**
     * 比对式报告
     * 通过提前在模板文档里埋下书签
     * 1、目录信息
     * 2、准确度测试详情
     * 3、测试结果页
     * 上述3个锚点位置不固定，可能结果页在通用信息中间，也有可能在文档最末端。
     * 注：当存在目录信息时，目录最后生成。
     *
     * @param baseDocumentPart   通用信息文档
     * @param detailDocumentPart 数据项模板文档
     * @param devReportParam     测试报告参数
     * @param pqDevVO            被检设备
     */
    private void dealDataModelScatteredByBookmarkByPlanContrast(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO) {
        // 查找 base 文档中所有书签
        List<BookmarkUtil.BookmarkInfo> bookmarks = BookmarkUtil.findAllBookmarks(baseDocumentPart);
        if (CollUtil.isNotEmpty(bookmarks)) {
            // 转换为枚举，便于排序，防止结论性的书签在文档的前面
            List<BookmarkEnum> bookmarkEnums = new ArrayList<>();
            for (BookmarkUtil.BookmarkInfo info : bookmarks) {
                String name = info.bookmark.getName();
                BookmarkEnum bookmarkEnum = BookmarkEnum.getByKey(name);
                if (Objects.nonNull(bookmarkEnum)) {
                    bookmarkEnums.add(bookmarkEnum);
                }
            }
            /*
             * 从结构上分析，处理的顺序：
             * 1、数据项
             * 2、结果信息
             * 3、目录信息
             * 按 BookmarkEnum.sort 字段排序，避免依赖枚举常量声明顺序
             * */
            bookmarkEnums.sort(Comparator.comparingInt(BookmarkEnum::getSort).thenComparingInt(Enum::ordinal));
            List<Object> todoInsertList;
            BookmarkUtil.BookmarkInfo bookmarkInfo;
            Map<Integer/*回路号*/, List<ContrastTestResult>> resultMap = null;
            // 书签在文档的位置
            for (int i = 0; i < bookmarkEnums.size(); i++) {
                BookmarkEnum bookmarkEnum = bookmarkEnums.get(i);
                switch (bookmarkEnum) {
                    case DATA_LINE:
                        // 获取标签信息
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.DATA_LINE.getKey(), bookmarks);
                        // 第一次调用时获取数据并保存到resultMap
                        if (resultMap == null) {
                            resultMap = resultService.getContrastResultForReport(devReportParam, pqDevVO);
                        }
                        todoInsertList = dealDataLineContrast(detailDocumentPart, devReportParam, pqDevVO, resultMap);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    case TEST_RESULT_DETAIL:
                        // 判断是否已经处理过数据了，有了结论性的描述
                        if (CollUtil.isEmpty(resultMap)) {
                            // 如果resultMap还是空的，先获取数据
                            resultMap = resultService.getContrastResultForReport(devReportParam, pqDevVO);
                            dealDataLineContrast(baseDocumentPart, devReportParam, pqDevVO, resultMap);
                        }
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.TEST_RESULT_DETAIL.getKey(), bookmarks);
                        todoInsertList = dealTestResultLineContrast(devReportParam, resultMap, DocAnchorEnum.TEST_RESULT_DETAIL);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    default:
                        break;
                }
            }
        }

    }

    /**
     * 比对式的结论数据创建
     *
     * @param devReportParam 被检设备的参数
     * @param resultMap      测试结果
     * @param docAnchorEnum  文档电接点
     */
    private List<Object> dealTestResultLineContrast(DevReportParam devReportParam, Map<Integer, List<ContrastTestResult>> resultMap, DocAnchorEnum docAnchorEnum) {
        List<Object> todoInsertList = new ArrayList<>();
        ObjectFactory factory = new ObjectFactory();

        // 1. 准备数据：提取所有检测项目和回路信息
        Set<String> allTestItemsSet = new LinkedHashSet<>();
        List<String> circuitNames = new ArrayList<>();
        Map<String, Map<Integer, String>> testItemResults = new LinkedHashMap<>();

        // 遍历所有回路，收集检测项目和结果
        for (Map.Entry<Integer, List<ContrastTestResult>> entry : resultMap.entrySet()) {
            Integer monitorNum = entry.getKey();
            circuitNames.add("测量回路 " + monitorNum);
            for (ContrastTestResult testResult : entry.getValue()) {
                String itemKey = testResult.getScriptName();
                allTestItemsSet.add(itemKey);
                // 存储每个检测项目在各回路的结果
                testItemResults.putIfAbsent(itemKey, new LinkedHashMap<>());
                testItemResults.get(itemKey).put(monitorNum, testResult.getResult());
            }
        }

        // 2. 转换数据格式以适配表格生成方法
        List<String> testItems = new ArrayList<>(allTestItemsSet);
        String[][] testResultsArray = new String[testItems.size()][circuitNames.size()];
        for (int i = 0; i < testItems.size(); i++) {
            String testItem = testItems.get(i);
            Map<Integer, String> itemResults = testItemResults.get(testItem);
            int j = 0;
            for (Map.Entry<Integer, List<ContrastTestResult>> entry : resultMap.entrySet()) {
                Integer monitorNum = entry.getKey();
                String result = itemResults.getOrDefault(monitorNum, "未检测");
                testResultsArray[i][j] = result;
                j++;
            }
        }

        String planId = devReportParam.getPlanId();
        AdPlan adPlan = adPlanService.getById(planId);
        // 4. 根据结果确定样品判定和数据处理参数
        String sampleResult = calculateOverallResult(testResultsArray);
        String dataType = getDataTypeFromParam(adPlan);
        String numOfSamples = getSampleCount(adPlan);
        String dataRule = getDataRuleFromParam(adPlan);
        // 5. 生成动态表格
        JAXBElement<Tbl> table = Docx4jUtil.createDynamicTestResultTable(
                factory,
                testItems,
                circuitNames,
                testResultsArray,
                sampleResult,
                dataType,
                numOfSamples,
                dataRule
        );
        todoInsertList.add(table);

        return todoInsertList;
    }

    /**
     * 计算整体判定结果
     * 逻辑：
     * 1. 存在一个检测项为"不符合"则样品结论就为"不符合"
     * 2. 所有的均为"无法比较"，样品结论才是"无法比较"
     * 3. 其余均为"符合"
     */
    private String calculateOverallResult(String[][] results) {
        int totalCount = 0;
        int uncomparableCount = 0;

        for (String[] row : results) {
            for (String result : row) {
                if (result != null && !result.trim().isEmpty()) {
                    totalCount++;

                    // 只要有一个"不符合"，立即返回"不符合"
                    if ("不符合".equals(result) || "不合格".equals(result)) {
                        return "不符合";
                    }

                    // 统计"无法比较"的数量
                    if ("无法比较".equals(result)) {
                        uncomparableCount++;
                    }
                }
            }
        }

        // 如果所有检测项都是"无法比较"，返回"无法比较"
        if (totalCount > 0 && uncomparableCount == totalCount) {
            return "无法比较";
        }

        // 其余情况均为"符合"
        return "符合";
    }

    /**
     * 获取数据类型配置
     * 实时数据：3秒数据（150周波数据）
     * 录波：录波数据
     * 分钟统计数据：分钟统计数据-最大值、分钟统计数据-最小值、分钟统计数据-平均值、分钟统计数据-CP95值
     */
    private String getDataTypeFromParam(AdPlan adPlan) {
        String dataSource = adPlan.getDatasourceId();
        String[] dataSourceArray = dataSource.split(",");
        StringBuilder dataType = new StringBuilder();
        for (String item : dataSourceArray) {
            dataType.append(Objects.requireNonNull(DataSourceEnum.ofByValue(item)).getMsg()).append("、");
        }
        dataType.deleteCharAt(dataType.length() - 1);
        return dataType.toString();
    }

    /**
     * 获取样本数量
     * 不同的dataSource对应不同的样本数量
     */
    private String getSampleCount(AdPlan adPlan) {
        String dataSource = adPlan.getDatasourceId();
        String[] dataSourceArray = dataSource.split(",");
        AdPlanTestConfig adPlanTestConfig = adPlanTestConfigService.getByPlanId(adPlan.getId());
        StringBuilder sampleCount = new StringBuilder();
        for (String item : dataSourceArray) {
            if (item.equalsIgnoreCase(DataSourceEnum.REAL_DATA.getValue())) {
                sampleCount.append("实时数据采集").append(adPlanTestConfig.getRealTime()).append("组，");
            } else if (item.equalsIgnoreCase(DataSourceEnum.WAVE_DATA.getValue())) {
                sampleCount.append("录波数据采集").append(adPlanTestConfig.getWaveRecord()).append("组，");
            } else {
                sampleCount.append("统计数据采集").append(adPlanTestConfig.getStatistics()).append("组，");
            }
        }
        sampleCount.deleteCharAt(sampleCount.length() - 1);
        return sampleCount.toString();
    }

    /**
     * 获取数据处理规则
     */
    private String getDataRuleFromParam(AdPlan adPlan) {
        String dataRule = adPlan.getDataRule();
        // 去字典获取
        DictData dictData = dictDataService.getDictDataById(dataRule);
        return dictData.getName();
    }


    /**
     * 比对模式下获取报告需要的数据项
     *
     * @param detailDocumentPart 数据项模板文档
     * @param devReportParam     测试报告参数
     * @param pqDevVO            被检设备
     * @param dataMap            各指标对应的各回路结论
     */
    private List<Object> dealDataLineContrast(MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO, Map<Integer/*回路号*/, List<ContrastTestResult>> dataMap) {
        List<Object> todoInsertList = new ArrayList<>();
        ObjectFactory factory = new ObjectFactory();
        // 处理表格数据
        List<Object> allContent = detailDocumentPart.getContent();
        List<Docx4jUtil.HeadingContent> headingContents = Docx4jUtil.extractHeading5Contents(allContent, detailDocumentPart);
        Map<String, List<Docx4jUtil.HeadingContent>> contentMap = headingContents.stream().collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList()));
        Iterator<Map.Entry<Integer, List<ContrastTestResult>>> iterator = dataMap.entrySet().iterator();
        int stepIndex = 1;
        while (iterator.hasNext()) {
            Map.Entry<Integer, List<ContrastTestResult>> next = iterator.next();
            // 线路号
            Integer monitorNum = next.getKey();
            // 线路下的指标数据
            List<ContrastTestResult> contrastTestResults = next.getValue();
            PqMonitor pqMonitor = pqMonitorService.getByDevIdAndNum(pqDevVO.getId(), monitorNum);
            // 插入回路号前，先换个页
            todoInsertList.add(Docx4jUtil.createPageBreakParagraph());
            // 回路标题
            todoInsertList.add(getContrastLineTitle(contentMap, monitorNum, stepIndex, factory, pqDevVO));
            int scriptIndex = 1;
            for (ContrastTestResult contrastTestResult : contrastTestResults) {
                // 比如电压 V
                String scriptCode = contrastTestResult.getScriptCode();
                String scriptName = contrastTestResult.getScriptName();
                // 创建测试项标题
                todoInsertList.add(getContrastScriptTitle(stepIndex + "." + scriptIndex + " " + scriptName + "检测分析", factory));
                // 根据code获取对应需要填充的内容
                List<Docx4jUtil.HeadingContent> tempContent = contentMap.get(scriptCode);
                // 需要区分下谐波类和非谐波类
                List<Object> tempList = fillContentInTemplateContrast(tempContent, factory, contrastTestResult, pqMonitor);
                todoInsertList.addAll(tempList);
                scriptIndex++;
            }
            stepIndex++;
        }

        return todoInsertList;
    }


    /**
     * 判断 base 文档是否为"指标锚点"新型模板（含任一白名单内 item_ 前缀书签）。
     * 白名单校验在 resolveItemScriptCode 内：老模板中手工遗留的同前缀书签（如 item_1）
     * 不会触发新路径，避免整份报告错路由导致 DATA_LINE / TEST_RESULT_* 全不填。
     *
     * @param bookmarks base 文档书签集合（调用方已扫，避免全文档重复扫描）
     */
    private boolean hasItemAnchor(List<BookmarkUtil.BookmarkInfo> bookmarks) {
        for (BookmarkUtil.BookmarkInfo info : bookmarks) {
            if (ItemAnchorConstant.resolveItemScriptCode(info.bookmark.getName()) != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * "指标锚点"新型模板装配（云南检定报告类）。
     * 流程：逐 item_* 锚点 → detail 池取 H5 模板表 → 按回路构造成品表插入锚点；
     * 无数据/无模板的指标记入隐藏集合 → 区段删除 → 第 7 节编号重排 → 残留书签清理。
     * 设计文档：docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §4.2
     */
    private void dealDataModelByItemAnchor(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart,
                                           DevReportParam devReportParam, PqDevVO pqDevVO,
                                           List<BookmarkUtil.BookmarkInfo> bookmarks) {
        ObjectFactory factory = new ObjectFactory();
        // 检测脚本明细按 scriptCode 分组
        List<PqScriptDtlDataVO> pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId());
        Map<String, List<PqScriptDtlDataVO>> scriptMap = pqScriptDtlsList.stream()
                .collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getScriptCode()), LinkedHashMap::new, Collectors.toList()));
        // detail 模板池按 H5 分组
        List<Docx4jUtil.HeadingContent> headingContents = Docx4jUtil.extractHeading5Contents(detailDocumentPart.getContent(), detailDocumentPart);
        Map<String, List<Docx4jUtil.HeadingContent>> contentMap = headingContents.stream()
                .collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getHeadingText()), LinkedHashMap::new, Collectors.toList()));
        int devChns = pqDevVO.getDevChns() == null ? 1 : pqDevVO.getDevChns();
        Set<String> hiddenCodes = new LinkedHashSet<>();
        for (BookmarkUtil.BookmarkInfo info : bookmarks) {
            String scriptCode = ItemAnchorConstant.resolveItemScriptCode(info.bookmark.getName());
            if (scriptCode == null) {
                // 新路径按设计忽略老锚点（互斥分流，spec §4.1）；发现 BookmarkEnum 残留提示模板可能混用
                if (Objects.nonNull(BookmarkEnum.getByKey(info.bookmark.getName()))) {
                    log.warn("指标锚点模板中发现老模板锚点书签 {}，新路径不处理该书签", info.bookmark.getName());
                }
                continue;
            }
            List<PqScriptDtlDataVO> itemDtls = scriptMap.get(scriptCode);
            List<Docx4jUtil.HeadingContent> tpl = contentMap.get(scriptCode);
            if (CollUtil.isEmpty(itemDtls) || CollUtil.isEmpty(tpl)) {
                log.warn("指标锚点 {} 无脚本数据或无模板分组，整节隐藏", info.bookmark.getName());
                hiddenCodes.add(scriptCode);
                continue;
            }
            List<String> tableKeys = ItemAnchorAssembler.extractTableKeys(tpl);
            // 有脚本明细且模板分组在，keys 仍取不到属模板错误 → 显性中断，不得静默走"未测→整节隐藏"
            ItemAnchorAssembler.requireTableKeys(scriptCode, tableKeys);
            List<Object> todoInsertList = new ArrayList<>();
            for (int line = 0; line < devChns; line++) {
                List<SingleTestResult> results = new ArrayList<>();
                if (PowerConstant.TIME.contains(scriptCode)) {
                    // 谐波类：一个 scriptIndex 一张表（与老路径语义一致）
                    Map<Integer, List<PqScriptDtlDataVO>> scriptIndexMap = itemDtls.stream()
                            .collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptIndex, TreeMap::new, Collectors.toList()));
                    for (List<PqScriptDtlDataVO> indexItem : scriptIndexMap.values()) {
                        SingleTestResult single = resultService.getFinalContent(indexItem, devReportParam.getPlanCode(), pqDevVO.getId(), line + 1, tableKeys);
                        if (Objects.nonNull(single)) {
                            results.add(single);
                        }
                    }
                } else {
                    SingleTestResult single = resultService.getFinalContent(itemDtls, devReportParam.getPlanCode(), pqDevVO.getId(), line + 1, tableKeys);
                    if (Objects.nonNull(single)) {
                        results.add(single);
                    }
                }
                List<Object> lineTables = ItemAnchorAssembler.buildTables(scriptCode, results, tpl.get(0), tableKeys, factory);
                if (lineTables.isEmpty()) {
                    // 该回路无数据，跳过；是否整节隐藏由所有回路汇总后判断
                    continue;
                }
                // 回路标题：单回路省略；多回路"测量回路{n}"从 1 起（spec §4.2）
                if (devChns > 1) {
                    todoInsertList.add(buildItemLineTitle(contentMap, line, factory));
                }
                todoInsertList.addAll(lineTables);
            }
            if (todoInsertList.isEmpty()) {
                log.warn("指标 {} 所有回路均无检测数据，整节隐藏", scriptCode);
                hiddenCodes.add(scriptCode);
                continue;
            }
            BookmarkUtil.insertElement(info, todoInsertList);
            // 已填充节不再需要 item/sect 标记：空锚点段直接删除，避免表格前残留空行；
            // 若未来模板把锚点放进非空段，则只清书签保留段落正文。
            if (!ItemAnchorSectionUtil.removeEmptyItemAnchorParagraph(info)) {
                BookmarkUtil.removeBookmark(info);
            }
        }
        ItemAnchorSectionUtil.removeSectRanges(baseDocumentPart, hiddenCodes);
        ItemAnchorSectionUtil.renumberChapter7(baseDocumentPart);
        ItemAnchorSectionUtil.stripItemAnchorBookmarks(baseDocumentPart);
    }

    private static String itemAnchorGroupKey(String value) {
        return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
    }

    /**
     * 新型模板的回路标题（仅多回路时被调用）："测量回路{n}"，n 从 1 起。
     * 与老路径 getLineTitle 的差异（有意）：不带 "n." 前缀、单回路由调用方直接省略。
     */
    private P buildItemLineTitle(Map<String, List<Docx4jUtil.HeadingContent>> contentMap, int lineIndex, ObjectFactory factory) {
        int lineNo = lineIndex + 1;
        P titleParagraph = factory.createP();
        PPr pPr = factory.createPPr();
        PPrBase.Spacing spacing = factory.createPPrBaseSpacing();
        spacing.setBefore(BigInteger.valueOf(60));
        spacing.setAfter(BigInteger.valueOf(20));
        spacing.setLine(BigInteger.valueOf(240));
        spacing.setLineRule(STLineSpacingRule.AUTO);
        pPr.setSpacing(spacing);
        titleParagraph.setPPr(pPr);
        Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 24, true);
        return titleParagraph;
    }

    /**
     * 通过提前在模板文档里埋下书签
     * 1、目录信息
     * 2、准确度测试详情
     * 3、测试结果页
     * 上述3个锚点位置不固定，可能结果页在通用信息中间，也有可能在文档最末端。
     * 注：当存在目录信息时，目录最后生成。
     *
     * @param baseDocumentPart   通用信息文档
     * @param detailDocumentPart 数据项模板文档
     * @param devReportParam     测试报告参数
     * @param pqDevVO            被检设备
     */
    private void dealDataModelScatteredByBookmark(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO) {
        // 查找 base 文档中所有书签
        List<BookmarkUtil.BookmarkInfo> bookmarks = BookmarkUtil.findAllBookmarks(baseDocumentPart);
        if (CollUtil.isNotEmpty(bookmarks)) {
            // 转换为枚举，便于排序，防止结论性的书签在文档的前面
            List<BookmarkEnum> bookmarkEnums = new ArrayList<>();
            for (BookmarkUtil.BookmarkInfo info : bookmarks) {
                String name = info.bookmark.getName();
                BookmarkEnum bookmarkEnum = BookmarkEnum.getByKey(name);
                if (Objects.nonNull(bookmarkEnum)) {
                    bookmarkEnums.add(bookmarkEnum);
                }
            }
            /*
             * 从结构上分析，处理的顺序：
             * 1、数据项
             * 2、结果信息
             * 3、目录信息
             * 按 BookmarkEnum.sort 字段排序，避免依赖枚举常量声明顺序
             * */
            bookmarkEnums.sort(Comparator.comparingInt(BookmarkEnum::getSort).thenComparingInt(Enum::ordinal));
            // 定义个结果，以便存在结果信息的书签
            Map<String/*指标名称*/, List<Boolean/*以回路的顺序填充结果*/>> resultMap = new HashMap<>();
            List<Object> todoInsertList;
            BookmarkUtil.BookmarkInfo bookmarkInfo;
            // 书签在文档的位置
            for (int i = 0; i < bookmarkEnums.size(); i++) {
                BookmarkEnum bookmarkEnum = bookmarkEnums.get(i);
                switch (bookmarkEnum) {
                    case DATA_LINE:
                        // 获取标签信息
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.DATA_LINE.getKey(), bookmarks);
                        todoInsertList = dealDataLine(detailDocumentPart, devReportParam, pqDevVO, resultMap);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            // 锚点位于 base 模板中间（其后仍有有效内容）时，在数据块末尾补一个分页符，
                            // 让 base 剩余部分另起新页；锚点在文档末尾时不加，存量模板行为不变
                            if (hasContentAfterBookmark(bookmarkInfo)) {
                                todoInsertList.add(Docx4jUtil.getPageBreak());
                            }
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    case DATA_SCRIPT:
                        break;
                    case TEST_RESULT_DEV:
                        // 判断是否已经处理过数据了，有了结论性的描述
                        if (CollUtil.isEmpty(resultMap)) {
                            dealDataLine(baseDocumentPart, devReportParam, pqDevVO, resultMap);
                        }
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.TEST_RESULT_DEV.getKey(), bookmarks);
                        todoInsertList = dealTestResultLine(devReportParam, resultMap, DocAnchorEnum.TEST_RESULT_DEV);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    case TEST_RESULT_LINE:
                        // 判断是否已经处理过数据了，有了结论性的描述
                        if (CollUtil.isEmpty(resultMap)) {
                            dealDataLine(baseDocumentPart, devReportParam, pqDevVO, resultMap);
                        }
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.TEST_RESULT_LINE.getKey(), bookmarks);
                        todoInsertList = dealTestResultLine(devReportParam, resultMap, DocAnchorEnum.TEST_RESULT_LINE);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    case TEST_RESULT_DETAIL:
                        // 判断是否已经处理过数据了，有了结论性的描述
                        if (CollUtil.isEmpty(resultMap)) {
                            dealDataLine(baseDocumentPart, devReportParam, pqDevVO, resultMap);
                        }
                        bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.TEST_RESULT_DETAIL.getKey(), bookmarks);
                        todoInsertList = dealTestResultLine(devReportParam, resultMap, DocAnchorEnum.TEST_RESULT_DETAIL);
                        if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) {
                            BookmarkUtil.insertElement(bookmarkInfo, todoInsertList);
                            BookmarkUtil.removeBookmark(bookmarkInfo);
                        }
                        break;
                    case CATALOG:
                        break;
                    default:
                        break;
                }
            }
        }

    }

    /**
     * 判断书签段落之后是否还有有意义的内容（非空文本段落，或表格等非段落元素）。
     * <p>
     * 用于数模式 DATA_LINE 锚点位于 base 模板中间时，决定是否在插入的数据块末尾追加分页符，
     * 使 base 剩余部分另起新页；书签在文档末尾（其后仅剩空段落）时返回 false，不追加分页符，
     * 避免末尾凭空多出一页空白，存量模板（锚点在末尾）行为保持不变。
     * <p>
     * 注：仅以文本判断段落是否有意义，纯图片段落不会被识别为有效内容。
     *
     * @param bookmarkInfo 书签信息
     * @return 书签之后存在有意义内容时返回 true
     */
    private boolean hasContentAfterBookmark(BookmarkUtil.BookmarkInfo bookmarkInfo) {
        List<Object> parentContent = bookmarkInfo.parentContainer.getContent();
        int idx = parentContent.indexOf(bookmarkInfo.parentParagraph);
        if (idx < 0) {
            return false;
        }
        for (int i = idx + 1; i < parentContent.size(); i++) {
            Object obj = parentContent.get(i);
            Object realObj = (obj instanceof JAXBElement) ? ((JAXBElement<?>) obj).getValue() : obj;
            if (realObj instanceof P) {
                // 非空文本段落才算有意义，纯空段落不触发分页
                if (StrUtil.isNotBlank(Docx4jUtil.getTextFromP((P) realObj))) {
                    return true;
                }
            } else {
                // 表格等非段落元素一律视为有意义内容
                return true;
            }
        }
        return false;
    }


    /**
     * 如何处理结果性数据进文档，各省级平台的结果表格不一致，如何做到一致
     *
     * @param resultMap 各测试项的结果
     */
    private List<Object> dealTestResultLine(DevReportParam devReportParam, Map<String, List<Boolean>> resultMap, DocAnchorEnum docAnchorEnum) {
        List<Object> todoInsertList = new ArrayList<>();
        // 先判断数据有没有，如果没有，则不处理（哨兵协议详见 RESULT_MAP_NO_DATA_FLAG 注释）
        if (!resultMap.containsKey(RESULT_MAP_NO_DATA_FLAG)) {
            ObjectFactory factory = Context.getWmlObjectFactory();
            // 源文档的内容
            // 创建表格（示例为3列，列数可任意调整）
            Tbl table = factory.createTbl();
            List<List<Boolean>> tempResultList = new ArrayList<>(resultMap.values());
            int lineNum = tempResultList.get(0).size();
            // 处理表头
            List<String> title = new ArrayList<>();
            title.add("检测项目");
            switch (docAnchorEnum) {
                case TEST_RESULT_DEV:
                    title.add("结论");
                    break;
                case TEST_RESULT_LINE:
                    for (int i = 1; i < lineNum + 1; i++) {
                        title.add("测量回路" + i);
                    }
                    break;
                case TEST_RESULT_DETAIL:
                    for (int i = 1; i < lineNum + 1; i++) {
                        title.add("测量回路" + i);
                    }
                    title.add("结论");
                    break;
                default:
                    break;
            }
            List<Integer> centerFlag = new ArrayList<>();
            centerFlag.add(0);
            Tr titleRow = Docx4jUtil.createCustomRow(factory, title, "Arial", "宋体", 21, true, centerFlag);
            table.getContent().add(titleRow);

            // 处理业务数据
            List<PqScriptDtlDataVO> pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId());
            pqScriptDtlsList.sort(Comparator.comparing(PqScriptDtlDataVO::getScriptIndex));
            Set<String> scriptCodeSet = new HashSet<>();
            for (PqScriptDtlDataVO pqScriptDtlDataVO : pqScriptDtlsList) {
                String scriptCode = pqScriptDtlDataVO.getScriptCode();
                if (scriptCodeSet.contains(scriptCode)) {
                    continue;
                } else {
                    scriptCodeSet.add(scriptCode);
                }
                List<Boolean> value = resultMap.get(scriptCode);
                List<String> cellValues = new ArrayList<>();
                PowerIndexEnum indexEnum = PowerIndexEnum.getByKey(scriptCode);
                if (indexEnum != null) {
                    if (scriptCode.equals(PowerIndexEnum.VOLTAGE.getKey())) {
                        cellValues.add(indexEnum.getDesc());
                    } else {
                        cellValues.add(indexEnum.getDesc().concat("测量准确度"));
                    }
                } else {
                    cellValues.add(PowerIndexEnum.UNKNOWN.getDesc().concat("测量准确度"));
                }
                // 判断是否有不合格的
                List<Boolean> totalValue = value.stream().filter(item -> !item).collect(Collectors.toList());
                String total = !totalValue.isEmpty() ? "不合格" : "合格";
                switch (docAnchorEnum) {
                    case TEST_RESULT_DEV:
                        cellValues.add(total);
                        break;
                    case TEST_RESULT_LINE:
                        for (Boolean aBoolean : value) {
                            cellValues.add(aBoolean ? "合格" : "不合格");
                        }
                        break;
                    case TEST_RESULT_DETAIL:
                        for (Boolean aBoolean : value) {
                            cellValues.add(aBoolean ? "合格" : "不合格");
                        }
                        cellValues.add(total);
                        break;
                    default:
                        break;
                }
                Tr tempRow = Docx4jUtil.createCustomRow(factory, cellValues, "Arial", "宋体", 21, false, centerFlag);
                table.getContent().add(tempRow);
            }
            TblPr tblPr = Docx4jUtil.getTblPr(factory);
            table.setTblPr(tblPr);
            todoInsertList.add(table);
        }
        return todoInsertList;
    }


    /**
     * 以回路维度处理数据项，并填充 resultMap（指标合格性结论）。
     * <p>
     * 本方法承担双重职责：
     * <ol>
     *   <li><b>返回值</b>：根据 modelPart 中的 H5 分组表格模板，灌入实测数据后生成可插入文档的元素列表；</li>
     *   <li><b>副作用</b>：把每个指标在各回路上的合格性写入 resultMap，供后续 dealTestResultLine 构造结论表使用。</li>
     * </ol>
     * 调用约定（务必区分以下两条路径，避免误解形参用途）：
     * <ul>
     *   <li><b>常规路径</b>（DATA_LINE 分支）：modelPart 传 detail 文档（数据页模板池），返回值会被插入 DATA_LINE 书签锚点。</li>
     *   <li><b>fallback 路径</b>（TEST_RESULT_* 分支且 resultMap 为空时）：modelPart 允许传 base 文档（封面页）；此时模板池为空，返回值无业务意义，调用方应丢弃，仅借此触发 resultMap 计算。</li>
     * </ul>
     * 因此 modelPart 命名保持中性，不要改回 detailXxx 等带语义偏向的名字。
     *
     * @param modelPart      文档模板部分；常规传 detail 文档，fallback 仅需算 resultMap 时可传 base 文档
     * @param devReportParam 测试报告参数
     * @param pqDevVO        被检设备
     * @param resultMap      结果性数据集合（被本方法写入；fallback 路径正是借此计算）
     * @return 待插入文档的元素列表；fallback 路径下应被调用方丢弃
     */
    private List<Object> dealDataLine(MainDocumentPart modelPart, DevReportParam devReportParam, PqDevVO pqDevVO, Map<String, List<Boolean>> resultMap) {
        List<Object> todoInsertList = new ArrayList<>();
        // 以回路维度处理数据项
        Integer devChns = pqDevVO.getDevChns();
        ObjectFactory factory = new ObjectFactory();
        // 读取该计划的检测大项组装数据内容
        List<PqScriptDtlDataVO> pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId());
        Map<String, List<PqScriptDtlDataVO>> scriptMap = pqScriptDtlsList.stream().collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptCode, LinkedHashMap::new, Collectors.toList()));
        List<Object> allContent = modelPart.getContent();
        List<Docx4jUtil.HeadingContent> headingContents = Docx4jUtil.extractHeading5Contents(allContent, modelPart);
        Map<String, List<Docx4jUtil.HeadingContent>> contentMap = headingContents.stream().collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList()));
        for (int i = 0; i < devChns; i++) {
            // 回路标题
            todoInsertList.add(getLineTitle(contentMap, i, devChns, factory));
            // 依次处理大项文档内容
            Iterator<Map.Entry<String, List<PqScriptDtlDataVO>>> iterator = scriptMap.entrySet().iterator();
            String prefixCode = "";
            boolean first;
            while (iterator.hasNext()) {
                Map.Entry<String, List<PqScriptDtlDataVO>> next = iterator.next();
                // 比如电压 V
                String scriptCode = next.getKey();
                List<PqScriptDtlDataVO> dtlScriptItemList = next.getValue();
                List<Docx4jUtil.HeadingContent> tempContent = contentMap.get(scriptCode);
                // 获取表格需要填充的key，此处主要是用于可能暂态时间的单位需要特殊判断用途，没有其他特殊用途
                List<String> tableKeys = Docx4jUtil.getTableFillKeys(tempContent);
                // 段落keys值赋值
                /* tableKeys值赋值，注：由于谐波类检测数据与非谐波检测类数据的区别，此处要做区分
                 * 1、谐波类每个scriptIndex对应一个Excel表格
                 * 2、非谐波类则是一个误差范围对应一个Excel表格
                 */
                SingleTestResult singleTestResult = null;
                // 根据code找到名称
                List<Boolean> scriptResult = resultMap.get(scriptCode);
                boolean needFill = false;
                if (CollUtil.isEmpty(scriptResult)) {
                    scriptResult = new ArrayList<>();
                    needFill = true;
                } else if (scriptResult.size() < (i + 1)) {
                    needFill = true;
                }
                // 控制指标名是否第一次，如果第一次就显示，不是则隐藏
                first = !prefixCode.equalsIgnoreCase(scriptCode);
                prefixCode = scriptCode;
                if (PowerConstant.TIME.contains(scriptCode)) {
                    // 谐波类，以scriptIndex区分
                    Map<Integer, List<PqScriptDtlDataVO>> scriptIndexMap = dtlScriptItemList.stream()
                            .collect(Collectors.groupingBy(
                                    PqScriptDtlDataVO::getScriptIndex,
                                    TreeMap::new,
                                    Collectors.toList()
                            ));
                    // 谐波类针对是否第一次还要额外做个处理，因为每个测点需要单独表示
                    for (List<PqScriptDtlDataVO> scriptDtlDataItem : scriptIndexMap.values()) {
                        singleTestResult = resultService.getFinalContent(scriptDtlDataItem, devReportParam.getPlanCode(), pqDevVO.getId(), (i + 1), tableKeys);
                        List<Object> tempList;
                        if (first) {
                            tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, true, scriptCode);
                            first = false;
                        } else {
                            tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, false, scriptCode);
                        }
                        todoInsertList.addAll(tempList);
                    }
                } else {
                    // 非谐波类
                    singleTestResult = resultService.getFinalContent(dtlScriptItemList, devReportParam.getPlanCode(), pqDevVO.getId(), (i + 1), tableKeys);
                    List<Object> tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, first, scriptCode);
                    todoInsertList.addAll(tempList);
                }
                if (Objects.nonNull(singleTestResult) && needFill) {
                    singleTestResult.setScriptCode(scriptCode);
                    scriptResult.add(singleTestResult.isQualified());
                    resultMap.put(scriptCode, scriptResult);
                }
            }
        }
        // 经过一轮处理仍未采集到任何合格性数据时，塞入哨兵：
        // 一是让后续 TEST_RESULT_* 分支的 fallback 看到 resultMap 非空而跳过重复调用本方法，
        // 二是供 dealTestResultLine 据此跳过结论表生成（哨兵协议详见 RESULT_MAP_NO_DATA_FLAG 注释）。
        if (CollUtil.isEmpty(resultMap)) {
            resultMap.put(RESULT_MAP_NO_DATA_FLAG, Collections.singletonList(false));
        }
        return todoInsertList;
    }


    /**
     * 将查询的所有有效数据填充到集合中，待后续填充文档
     */
    private List<Object> fillContentInTemplate(Map<String, List<Map<String, List<Map<String, String>>>>> finalContent, List<Docx4jUtil.HeadingContent> tempContent,
                                               ObjectFactory factory, List<String> tableKeys, boolean first, String scriptCode) {
        String influenceContent = "";
        List<Object> todoInsertList = new ArrayList<>();
        if (CollUtil.isNotEmpty(finalContent)) {
            Iterator<Map.Entry<String, List<Map<String, List<Map<String, String>>>>>> allContentIterator = finalContent.entrySet().iterator();
            boolean nameFlag = false;
            while (allContentIterator.hasNext()) {
                boolean influenceFlag = false;
                // 以影响量分类的数据内容
                Map.Entry<String, List<Map<String, List<Map<String, String>>>>> contentMapEntry = allContentIterator.next();
                // 此处的key是影响量的文字描述
                String influence = contentMapEntry.getKey();
                List<Map<String, List<Map<String, String>>>> contentByInfluence = contentMapEntry.getValue();
                for (Map<String, List<Map<String, String>>> stringListMap : contentByInfluence) {
                    Iterator<Map.Entry<String, List<Map<String, String>>>> errorContentIterator = stringListMap.entrySet().iterator();
                    while (errorContentIterator.hasNext()) {
                        // 同一个影响量下的误差范围分类的数据内容
                        Map.Entry<String, List<Map<String, String>>> errorContent = errorContentIterator.next();
                        // 此处的key是误差范围
                        String errorContentKey = errorContent.getKey();
                        // 待填充数据内容
                        List<Map<String, String>> dataList = errorContent.getValue();
                        // 填充模板内容
                        if (CollUtil.isNotEmpty(tempContent)) {
                            // 读取该表下模板里面的内容，并动态赋值渲染文档
                            Docx4jUtil.HeadingContent headingContent = tempContent.get(0);
                            for (Object object : headingContent.getSubContent()) {
                                // 段落
                                if (object instanceof P) {
                                    /* 如果是段落，目前的配置项无非就是：name、nameDetail、influence、errorScope、scriptDetail
                                        需要注意的是：
                                        1、如果name、nameDetail、influence独立存在时，仅赋值一次，避免重复出现；
                                        2、如果name、nameDetail、influence与其他配置项同时存在，通过 - 分隔，则每次都要输出
                                     */
                                    P innerP = factory.createP();
                                    // 如果是段落，渲染段落内容
                                    P paragraph = (P) object;
                                    // 获取该段落的样式
                                    RPr rPr = Docx4jUtil.getTcPrFromParagraph(paragraph);
                                    PPr pPr = paragraph.getPPr();
                                    String textFromP = Docx4jUtil.getTextFromP(paragraph);
                                    String text = "";
                                    if (StrUtil.isNotBlank(textFromP)) {
                                        // 需要判断是否含有'-'
                                        if (textFromP.contains(ItemReportKeyEnum.NAME.getKey())
                                                || textFromP.contains(ItemReportKeyEnum.NAME_DETAIL.getKey())
                                                || textFromP.contains(ItemReportKeyEnum.INFLUENCE.getKey())) {
                                            if (textFromP.contains(StrPool.DASHED)) {
                                                String[] pArray = textFromP.split(StrPool.DASHED);
                                                for (String item : pArray) {
                                                    text = text.concat(getValueFromDataMap(item, errorContentKey, influence, scriptCode));
                                                }
                                            } else {
                                                // 调整标志位
                                                if (textFromP.contains(ItemReportKeyEnum.INFLUENCE.getKey()) && !influenceFlag) {
                                                    if (!influenceContent.equalsIgnoreCase(influence)) {
                                                        text = getValueFromDataMap(textFromP, errorContentKey, influence, scriptCode);
                                                        influenceFlag = true;
                                                        influenceContent = influence;
                                                    }
                                                } else {
                                                    if (!nameFlag && first) {
                                                        text = getValueFromDataMap(textFromP, errorContentKey, influence, scriptCode);
                                                        nameFlag = true;
                                                    }
                                                }
                                            }
                                        }
                                        if (textFromP.contains(ItemReportKeyEnum.SCRIPT_DETAIL.getKey())) {
                                            if (textFromP.contains(StrPool.DASHED)) {
                                                String[] pArray = textFromP.split(StrPool.DASHED);
                                                for (String item : pArray) {
                                                    if (item.equalsIgnoreCase(ItemReportKeyEnum.SCRIPT_DETAIL.getKey())) {
                                                        text = dataList.get(0).get(ItemReportKeyEnum.SCRIPT_DETAIL.getKey());
                                                    } else {
                                                        if (StrUtil.isBlank(text)) {
                                                            text = "";
                                                        }
                                                        text = text.concat(getValueFromDataMap(item, errorContentKey, influence, scriptCode));
                                                    }
                                                }
                                            } else {
                                                text = dataList.get(0).get(ItemReportKeyEnum.SCRIPT_DETAIL.getKey());
                                            }
                                        }
                                    }
                                    // text为空，则不创建段落
                                    if (StrUtil.isNotBlank(text)) {
                                        Docx4jUtil.addPContent(factory, innerP, text, rPr, pPr);
                                        //插入段落
                                        todoInsertList.add(innerP);
                                    }
                                } else if (object instanceof JAXBElement) {
                                    // 表格需要注意深拷贝，避免修改了原对象
                                    JAXBElement<Tbl> temp = (JAXBElement<Tbl>) object;
                                    JAXBElement<Tbl> copiedTableElement;
                                    try {
                                        copiedTableElement = Docx4jUtil.deepCopyTbl(temp);
                                    } catch (Exception e) {
                                        throw new RuntimeException(e);
                                    }
                                    // 解析表格并插入对应数据，最关键的是得知道表格是横向还是纵向以及表头占了几行
                                    Tbl tbl = copiedTableElement.getValue();
                                    // 获取表格的行
                                    List<Object> rows = tbl.getContent();
                                    boolean isRow = Docx4jUtil.judgeTableCross(rows.get(0));
                                    if (isRow) {
                                        // 获取现有表头的样式
                                        Tr headerRow = (Tr) tbl.getContent().get(0);
                                        // 设置表头行属性
                                        TrPr headerTrPr = factory.createTrPr();
                                        headerRow.setTrPr(headerTrPr);

                                        // 获取现有行的样式
                                        Tr existingRow = (Tr) tbl.getContent().get(rows.size() - 1);

                                        // 获取现有样式
                                        TrPr trPr = existingRow.getTrPr();
                                        // 获取每个单元格的TcPr（保留各单元格独立的边框设置）
                                        List<TcPr> tcPrList = new ArrayList<>();
                                        RPr templateRPr = null;
                                        for (Object cellObj : existingRow.getContent()) {
                                            if (cellObj instanceof JAXBElement) {
                                                JAXBElement<Tc> cellElement = (JAXBElement<Tc>) cellObj;
                                                Tc templateCell = cellElement.getValue();
                                                TcPr tcPr = templateCell.getTcPr();
                                                // 设置单元格宽度
                                                if (tcPr == null) {
                                                    tcPr = factory.createTcPr();
                                                }
                                                TblWidth cellWidth = factory.createTblWidth();
                                                cellWidth.setType("dxa");
                                                cellWidth.setW(BigInteger.valueOf(5000 / tableKeys.size()));
                                                tcPr.setTcW(cellWidth);
                                                tcPrList.add(tcPr);
                                                // 从第一个单元格获取字体样式
                                                if (templateRPr == null && !templateCell.getContent().isEmpty() && templateCell.getContent().get(0) instanceof P) {
                                                    P templateP = (P) templateCell.getContent().get(0);
                                                    templateRPr = Docx4jUtil.getTcPrFromParagraph(templateP);
                                                }
                                            }
                                        }
                                        tbl.getContent().remove(existingRow);
                                        // 迭代增加行,需要填充的表格keys在tableKeys集合中
                                        for (Map<String, String> stringStringMap : dataList) {
                                            Tr newRow = Docx4jUtil.createCustomRow(factory, stringStringMap, tableKeys, trPr, tcPrList, templateRPr, true);
                                            tbl.getContent().add(newRow);
                                        }
                                    } else {
                                        // 纵向表格暂不考虑
                                    }
                                    // 插入段落,处理下样式
                                    todoInsertList.add(copiedTableElement);
                                }
                            }
                            // 全部渲染完毕后，添加几个换行
//                            P p = factory.createP();
//                            Docx4jUtil.addBr(factory, p, 1);
//                            todoInsertList.add(p);
                        }
                    }
                }
            }
        }
        return todoInsertList;
    }

    /**
     * 比对模式下对所有数据的填充
     *
     * @return
     */
    private List<Object> fillContentInTemplateContrast(List<Docx4jUtil.HeadingContent> tempContent, ObjectFactory factory, ContrastTestResult contrastTestResult, PqMonitor pqMonitor) {
        List<Object> todoInsertList = new ArrayList<>();
        Docx4jUtil.HeadingContent headingContent = tempContent.get(0);
        List<String> tableKeys = Docx4jUtil.getTableFillKeys(tempContent);
        for (Object object : headingContent.getSubContent()) {
            // 段落
            if (object instanceof P) {
                P innerP = factory.createP();
                // 如果是段落，渲染段落内容
                P paragraph = (P) object;
                // 获取该段落的样式
                RPr rPr = Docx4jUtil.getTcPrFromParagraph(paragraph);
                PPr pPr = paragraph.getPPr();
                String textFromP = Docx4jUtil.getTextFromP(paragraph);
                String text = "";
                if (StrUtil.isNotBlank(textFromP)) {
                    // todo ... 根据配置文本填充
                }
                // text为空，则不创建段落
                if (StrUtil.isNotBlank(text)) {
                    Docx4jUtil.addPContent(factory, innerP, text, rPr, pPr);
                    //插入段落
                    todoInsertList.add(innerP);
                }
            } else if (object instanceof JAXBElement) {
                // 表格需要注意深拷贝，避免修改了原对象
                JAXBElement<Tbl> temp = (JAXBElement<Tbl>) object;
                JAXBElement<Tbl> copiedTableElement;
                try {
                    copiedTableElement = Docx4jUtil.deepCopyTbl(temp);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                // 解析表格并插入对应数据，最关键的是得知道表格是横向还是纵向以及表头占了几行
                Tbl tbl = copiedTableElement.getValue();
                // 获取表格的行
                List<Object> rows = tbl.getContent();
                boolean isRow = Docx4jUtil.judgeTableCross(rows.get(0));
                if (isRow) {
                    // 获取现有表头的样式
                    Tr headerRow = (Tr) tbl.getContent().get(0);
                    // 设置表头行属性
                    TrPr headerTrPr = factory.createTrPr();
                    headerRow.setTrPr(headerTrPr);

                    // 获取现有行的样式
                    Tr existingRow = (Tr) tbl.getContent().get(rows.size() - 1);
                    // 获取现有样式
                    TrPr trPr = existingRow.getTrPr();
                    JAXBElement<Tc> element = (JAXBElement<Tc>) existingRow.getContent().get(0);
                    TcPr tcPr = element.getValue().getTcPr();
                    TblWidth cellWidth = factory.createTblWidth();
                    cellWidth.setType("dxa");
                    cellWidth.setW(BigInteger.valueOf(5000 / tableKeys.size()));
                    tcPr.setTcW(cellWidth);
                    tbl.getContent().remove(existingRow);
                    // 需要区分谐波和非谐波
                    boolean hasValidData = false;
                    if (contrastTestResult.isHarmonic()) {
                        List<Map<String, Map<String, Map<String, String>>>> checkResultHarmonic = contrastTestResult.getCheckResultHarmonic();
                        // 检查谐波数据是否为空或无有效数据
                        if (checkResultHarmonic != null && !checkResultHarmonic.isEmpty()) {
                            // 三层循环获取最内层的Map<String, String>
                            for (Map<String, Map<String, Map<String, String>>> level1Map : checkResultHarmonic) {
                                if (level1Map != null && !level1Map.isEmpty()) {
                                    // 对于每个谐波次数
                                    for (Map.Entry<String, Map<String, Map<String, String>>> harmonicEntry : level1Map.entrySet()) {
                                        // 谐波次数
                                        String harmonicOrder = harmonicEntry.getKey();
                                        // 相别数据
                                        Map<String, Map<String, String>> phaseDataMap = harmonicEntry.getValue();
                                        if (phaseDataMap != null && !phaseDataMap.isEmpty()) {
                                            hasValidData = true;
                                            // 用于跟踪是否是该次谐波的第一个相
                                            int phaseIndex = 0;
                                            // 该次谐波的相数（通常是3）
                                            int phaseCount = phaseDataMap.size();
                                            for (Map.Entry<String, Map<String, String>> phaseEntry : phaseDataMap.entrySet()) {
                                                // 相别（A相、B相、C相）
                                                String phase = phaseEntry.getKey();
                                                // 具体数据
                                                Map<String, String> dataMap = phaseEntry.getValue();
                                                // 创建新行，传入额外的参数用于处理合并单元格
                                                Tr newRow = Docx4jUtil.createHarmonicTableRow(factory, dataMap, tableKeys, trPr, tcPr,
                                                        phaseIndex == 0, phaseIndex == phaseCount - 1, 21);
                                                tbl.getContent().add(newRow);
                                                phaseIndex++;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } else {
                        // 迭代增加行,需要填充的表格keys在tableKeys集合中
                        Map<String, Map<String, String>> dataList = contrastTestResult.getCheckResultNonHarmonic();
                        if (dataList != null && !dataList.isEmpty()) {
                            hasValidData = true;
                            for (Map<String, String> stringStringMap : dataList.values()) {
                                Tr newRow = Docx4jUtil.createCustomRow(factory, stringStringMap, tableKeys, trPr, tcPr, true, 21);
                                tbl.getContent().add(newRow);
                            }
                        }
                    }

                    // 只有当有有效数据时才插入表格
                    if (hasValidData) {
                        // 插入段落,处理下样式
                        todoInsertList.add(copiedTableElement);
                    }
                } else {
                    // 纵向表格暂不考虑
                }

                StringBuilder description = new StringBuilder();
                if (contrastTestResult.getScriptCode().equalsIgnoreCase("I")) {
                    // 获取该通道的额定电流
                    String ct = pqMonitor.getCt();
                    // 理论上ct是 xxx:xxx的格式。比如10:1，100:5
                    if (ct.indexOf(":") > 0) {
                        String[] ctArray = ct.split(":");
                        description.append("注：当前回路额定电流为：").append(ctArray[1]).append("A。");
                    }
                } else if (contrastTestResult.getScriptCode().equalsIgnoreCase("V")) {
                    // 获取该通道的额定电压
                    String pt = pqMonitor.getPt();
                    // 理论上pt是 xxx:xxx的格式。比如380:380，10000:100
                    if (pt.indexOf(":") > 0) {
                        String[] ptArray = pt.split(":");
                        // 电压需要特殊处理下，处理为相电压值
                        String voltage = ptArray[1];
                        if (voltage.equalsIgnoreCase("100")) {
                            voltage = "57.74";
                        } else if (voltage.equalsIgnoreCase("380")) {
                            voltage = "220";
                        } else {
                            // 其他场景下就除以根号3
                            double result = Double.parseDouble(voltage) / Math.sqrt(3);
                            voltage = doubleRound(2, result);
                        }
                        description.append("注：当前回路额定电流为：").append(voltage).append("V。");
                    }
                }

                // 如果存在特殊说明，在表格后添加一个段落 如果有电压和电流需要把额定电压和电流标注进来 pqMonitor
                if (StrUtil.isNotBlank(contrastTestResult.getSpecialCase()) || StrUtil.isNotBlank(description.toString())) {
                    P specialCaseP;
                    if (StrUtil.isNotBlank(description.toString())) {
                        if (StrUtil.isNotBlank(contrastTestResult.getSpecialCase())) {
                            description.append(contrastTestResult.getSpecialCase().replace("注：", ""));
                        }
                        specialCaseP = Docx4jUtil.createSpecialCaseParagraph(factory, description.toString());
                    } else {
                        specialCaseP = Docx4jUtil.createSpecialCaseParagraph(factory, contrastTestResult.getSpecialCase());
                    }
                    todoInsertList.add(specialCaseP);
                }
            }
        }
        return todoInsertList;
    }


    /**
     * 从数据集合中获取对应key的值，误差范围需要做个特殊处理
     *
     * @param item key
     */
    private String getValueFromDataMap(String item, String errorScope, String influence, String scriptCode) {
        String value = "";
        if (item.equalsIgnoreCase(ItemReportKeyEnum.ERROR_SCOPE.getKey())) {
            value = "(最大允许误差：" + errorScope + ")";
        } else if (item.equalsIgnoreCase(ItemReportKeyEnum.INFLUENCE.getKey())) {
            value = influence;
        } else if (item.equalsIgnoreCase(ItemReportKeyEnum.NAME.getKey())) {
            PowerIndexEnum powerIndexEnum = PowerIndexEnum.getByKey(scriptCode);
            if (powerIndexEnum != null) {
                value = powerIndexEnum.getDesc();
            }
        } else if (item.equalsIgnoreCase(ItemReportKeyEnum.NAME_DETAIL.getKey())) {
            PowerIndexEnum powerIndexEnum = PowerIndexEnum.getByKey(scriptCode);
            if (powerIndexEnum != null) {
                value = powerIndexEnum.getDesc() + "测量准确度";
            }
        }
        return value;
    }


    private void updateDevAndPlanState(String devId, String planId) {
        // 将改设备的报告生成状态调整为已生成
        iPqDevService.updatePqDevReportState(devId, DevReportStateEnum.GENERATED.getValue());
        // 判断该计划下是否所有设备报告已生成，如果已生成则将计划的报告状态给为已生成
        long count = iPqDevService.countUnReportDev(planId);
        LambdaUpdateWrapper<AdPlan> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(AdPlan::getId, planId);
        if (count == 0) {
            updateWrapper.set(AdPlan::getReportState, PlanReportStateEnum.REPORT_STATE_ALL_GENERATED.getValue());
        } else {
            updateWrapper.set(AdPlan::getReportState, PlanReportStateEnum.REPORT_STATE_PARTIALLY_GENERATED.getValue());
        }
        adPlanService.update(updateWrapper);
    }

    @Override
    public void downloadReport(DevReportParam devReportParam, HttpServletResponse response) {
        PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId());
        if (Objects.isNull(pqDevVO)) {
            throw new BusinessException("请检查装置是否存在！");
        }

        // 获取计划信息以确定模式
        AdPlan plan = null;
        if (devReportParam.getPlanId() != null) {
            plan = adPlanService.getById(devReportParam.getPlanId());
        }

        // 获取设备型号
        DevType devType = devTypeService.getById(pqDevVO.getDevType());
        if (Objects.isNull(devType)) {
            throw new BusinessException("设备类型丢失，请联系管理员！");
        }

        // 根据不同模式构建文件路径和文件名
        String filePath;
        String downloadFileName;

        String currrentScene = sysTestConfigService.getCurrrentScene();

        if (SceneEnum.LEAVE_FACTORY_TEST.getValue().equals(currrentScene)) {
            // 出厂测试场景
            filePath = pathConfig.getReportPath().concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX);
            downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX;
        } else if (plan != null) {
            // 根据计划模式确定路径结构
            DictData dictDataById = dictDataService.getById(plan.getPattern());
            if (dictDataById != null && "Contrast".equals(dictDataById.getCode())) {
                // 比对模式：使用新的路径结构
                String fileName = String.format("%s_%s_%s_%s.docx",
                        pqDevVO.getCityName() != null ? pqDevVO.getCityName() : "未知地市",
                        pqDevVO.getGdName() != null ? pqDevVO.getGdName() : "未知供电公司",
                        pqDevVO.getSubName() != null ? pqDevVO.getSubName() : "未知电站",
                        pqDevVO.getName() != null ? pqDevVO.getName() : "未知设备");
                filePath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(plan.getName())).concat(File.separator).concat(fileName);
                downloadFileName = fileName;
            } else {
                // 数字/模拟模式：使用原来的路径结构
                filePath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName())).concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX);
                downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX;
            }
        } else {
            // 兜底：使用旧的路径结构
            filePath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName())).concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX);
            downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX;
        }

        File reportFile = new File(filePath);
        if (!reportFile.exists()) {
            // 如果文件不存在，则将改设备的报告生成状态调整为未生成
            iPqDevService.updatePqDevReportState(devReportParam.getDevId(), 0);
            throw new BusinessException("报告文件不存在，请重新生成报告！");
        } else {
            try (FileInputStream fis = new FileInputStream(reportFile);
                 OutputStream os = response.getOutputStream()) {

                // 设置响应头
                response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
//                response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFileName + "\"");
                try {
                    // 对中文文件名进行URL编码// 将+号替换为%20（空格的正确编码）
                    String encodedFileName = URLEncoder.encode(downloadFileName, "UTF-8")
                            .replaceAll("\\+", "%20");

                    // 使用RFC 5987标准格式
                    response.setHeader("Content-Disposition",
                            "attachment; filename*=UTF-8''" + encodedFileName);

                    // 或者同时提供两种格式以获得最好的兼容性
                    // response.setHeader("Content-Disposition",
                    //     "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
                } catch (UnsupportedEncodingException e) {
                    // 处理编码异常
                    response.setHeader("Content-Disposition",
                            "attachment; filename=\"download.docx\"");
                }

                // 将文件内容写入响应输出流
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }
                // 刷新输出流
                os.flush();
            } catch (IOException e) {
                e.printStackTrace();
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean documented(List<String> ids) {
        if (CollUtil.isNotEmpty(ids)) {
            List<PqDevVO> pqDevVOList = pqDevMapper.listByDevIds(ids);
            List<String> devIds = new ArrayList<>();
            PqDevVO tempPqDevVO = null;
            PqDevVO pqDevVO = null;

            for (int i = 0; i < pqDevVOList.size(); i++) {
                pqDevVO = pqDevVOList.get(i);
                tempPqDevVO = pqDevVO;
                if (!pqDevVO.getCheckState().equals(CheckStateEnum.CHECKED.getValue())) {
                    throw new BusinessException(DetectionResponseEnum.DEV_UN_CHECKED);
                }
                if (!pqDevVO.getReportState().equals(DevReportStateEnum.GENERATED.getValue())) {
                    devIds.add(pqDevVO.getId());
                }
            }

            iPqDevSubService.lambdaUpdate()
                    .set(PqDevSub::getCheckState, CheckStateEnum.DOCUMENTED.getValue())
                    .in(PqDevSub::getDevId, ids)
                    .update();
            // 判断计划下所有设备是否都已归档，如果是则将计划改为已完成
            // 查询该计划下所有设备的检测状态，是否有不为归档的
            PqDevParam.QueryParam queryParam = new PqDevParam.QueryParam();
            queryParam.setPlanIdList(Arrays.asList(tempPqDevVO.getPlanId()));
            List<PqDevVO> pqDevVOList1 = pqDevMapper.selectByQueryParam(queryParam);

            long count = pqDevVOList1.stream().filter(obj -> obj.getCheckState() == null || obj.getCheckState() < CheckStateEnum.DOCUMENTED.getValue()).count();
            if (count == 0) {
                // 如果非归档状态的设备数量为0，则更新计划已完成
                pqDevMapper.finishPlan(pqDevVO.getPlanId());
            }

            if (CollUtil.isNotEmpty(devIds)) {
                DevReportParam devReportParam = new DevReportParam();
                devReportParam.setDevIdList(devIds);
                devReportParam.setPlanId(tempPqDevVO.getPlanId());
                AdPlan plan = adPlanService.getById(tempPqDevVO.getPlanId());
                devReportParam.setScriptId(plan.getScriptId());
                devReportParam.setPlanCode(String.valueOf(plan.getCode()));
                this.generateReport(devReportParam);
            }
        }
        return true;
    }

    /**
     * 处理基础模版中的信息，非数据页报告
     * 因为Docx4j工具包替换时会默认增加${}
     */
    private Map<String, String> dealBaseModelData(PqDevVO pqDevVO, DevType devType) {
        // 首先获取非数据页中需要的信息
        Map<String, String> baseModelMap = new HashMap<>(32);
        // 获取设备型号
        baseModelMap.put(BaseReportKeyEnum.DEV_TYPE.getKey(), devType.getName());
        // 检测员
        baseModelMap.put(BaseReportKeyEnum.INSPECTOR.getKey(), pqDevVO.getCheckBy() + "");
        baseModelMap.put(BaseReportKeyEnum.INSPECTOR.getKey(), pqDevVO.getCheckBy() + "");
        String datePattern = DatePattern.NORM_DATE_PATTERN;
        if (DatePattern.CHINESE_DATE_PATTERN.equals(dateFormat)) {
            datePattern = DatePattern.CHINESE_DATE_PATTERN;
        }

        // 出厂日期
        baseModelMap.put(BaseReportKeyEnum.CREATEDATE.getKey(), pqDevVO.getCreateDate().format(DateTimeFormatter.ofPattern(datePattern)));
        // 硬件版本
        baseModelMap.put(BaseReportKeyEnum.HW_VERSION.getKey(), StrUtil.isNotBlank(pqDevVO.getHardwareVersion()) ? pqDevVO.getHardwareVersion() : StrUtil.EMPTY);
        // 软件版本
        baseModelMap.put(BaseReportKeyEnum.SW_VERSION.getKey(), StrUtil.isNotBlank(pqDevVO.getSoftwareVersion()) ? pqDevVO.getSoftwareVersion() : StrUtil.EMPTY);
        // 调试日期
        if (pqDevVO.getCheckEndTime() != null) {
            baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(pqDevVO.getCheckEndTime(), DatePattern.CHINESE_DATE_PATTERN));
        } else {
            baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN));
        }
        // 装置编码
        baseModelMap.put(BaseReportKeyEnum.DEV_CODE.getKey(), pqDevVO.getCreateId());
        baseModelMap.put(BaseReportKeyEnum.DEV_NAME.getKey(), StrUtil.isBlank(pqDevVO.getName()) ? StrPool.TAB : pqDevVO.getName());
        // 工作电源
        baseModelMap.put(BaseReportKeyEnum.POWER.getKey(), devType.getPower());
        // 额定电流
        baseModelMap.put(BaseReportKeyEnum.DEV_CURR.getKey(), pqDevVO.getDevCurr().toString().concat(PowerConstant.CURRENT_UNIT));
        // 额定电压
        baseModelMap.put(BaseReportKeyEnum.DEV_VOLT.getKey(), pqDevVO.getDevVolt().toString().concat(PowerConstant.VOLTAGE_UNIT));
        // 通道数
        baseModelMap.put(BaseReportKeyEnum.COUNT.getKey(), pqDevVO.getDevChns().toString());
        // 制造厂家
        DictData dictData = dictDataService.getDictDataById(pqDevVO.getManufacturer());
        if (ObjectUtil.isNotNull(dictData)) {
            baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), dictData.getName());
        } else {
            baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), StrPool.TAB);
        }
        // 委托方
        String delegate = pqDevVO.getDelegate();
        if (StrUtil.isNotBlank(delegate)) {
            DictData delegateDictData = dictDataService.getDictDataById(pqDevVO.getManufacturer());
            if (ObjectUtil.isNotNull(delegateDictData)) {
                baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), delegateDictData.getName());
            } else {
                baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB);
            }
        } else {
            baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB);
        }

        // 实验室温度
        baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString());
        // 实验室湿度
        baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString());

        // 样品编号
        baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId());
        // 收样日期
        baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate()));
        baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE_C.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : formatNumberChineseDate(pqDevVO.getArrivedDate()));
        // 检测日期
        baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), Objects.isNull(pqDevVO.getCheckEndTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckEndTime()).substring(0, 10));
        baseModelMap.put(BaseReportKeyEnum.TEST_DATE_C.getKey(), Objects.isNull(pqDevVO.getCheckEndTime()) ? StrPool.TAB : formatNumberChineseDate(pqDevVO.getCheckEndTime().toLocalDate()));
        baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString());
        baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString());
        baseModelMap.put(BaseReportKeyEnum.YEAR.getKey(), DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN));
        baseModelMap.put(BaseReportKeyEnum.MONTH.getKey(), DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4));
        baseModelMap.put(BaseReportKeyEnum.DAY.getKey(), DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6));
        baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY.getKey(), DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN));
        baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY_UP.getKey(), formatChineseReportDate(LocalDate.now()));
        baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN));
        return baseModelMap;
    }

    /**
     * 比对模式下需要获取的数据
     * 处理基础模版中的信息，非数据页报告
     * 因为Docx4j工具包替换时会默认增加${}
     */
    private Map<String, String> dealBaseModelContrastData(AdPlan plan, PqDevVO pqDevVO, DevType devType) {
        // 首先获取非数据页中需要的信息
        Map<String, String> baseModelMap = new HashMap<>(32);
        // 委托方
        String delegate = pqDevVO.getDelegate();
        if (StrUtil.isNotBlank(delegate)) {
            DictData delegateDictData = dictDataService.getDictDataById(pqDevVO.getDelegate());
            if (ObjectUtil.isNotNull(delegateDictData)) {
                baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), delegateDictData.getName());
            } else {
                baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB);
            }
        } else {
            baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB);
        }
        // 样品编号
        baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getName()) ? StrPool.TAB : pqDevVO.getName());
        // 报告日期
        baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN));
        // 供电部门
        baseModelMap.put(BaseReportKeyEnum.GD_NAME.getKey(), pqDevVO.getGdName());
        // 变电站名称
        baseModelMap.put(BaseReportKeyEnum.SUB_NAME.getKey(), pqDevVO.getSubName());
        // 检测人
        String[] members = plan.getMembers().split(",");
        String member = members[0];
        SysUser memberUser = sysUserService.getById(member);
        if (Objects.isNull(memberUser)) {
            baseModelMap.put(BaseReportKeyEnum.CHECK_BY.getKey(), member);
        } else {
            baseModelMap.put(BaseReportKeyEnum.CHECK_BY.getKey(), memberUser.getName());
        }
        // 负责人
        SysUser leader = sysUserService.getById(plan.getLeader());
        if (Objects.isNull(leader)) {
            baseModelMap.put(BaseReportKeyEnum.AUDIT_BY.getKey(), plan.getLeader());
        } else {
            baseModelMap.put(BaseReportKeyEnum.AUDIT_BY.getKey(), leader.getName());
        }
        // 误差体系的名称
        String errorId = plan.getErrorSysId();
        PqErrSys errSys = pqErrSysService.getPqErrSysById(errorId);
        baseModelMap.put(BaseReportKeyEnum.ERROR_SYS_NAME.getKey(), errSys.getStandardName());
        // 获取设备型号
        baseModelMap.put(BaseReportKeyEnum.DEV_TYPE.getKey(), devType.getName());
        // 制造厂家
        DictData dictData = dictDataService.getDictDataById(pqDevVO.getManufacturer());
        if (ObjectUtil.isNotNull(dictData)) {
            baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), dictData.getName());
        } else {
            baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), StrPool.TAB);
        }
        // 生产日期
        baseModelMap.put(BaseReportKeyEnum.CREATE_DATE.getKey(), LocalDateTimeUtil.format(pqDevVO.getCreateDate(), DatePattern.NORM_DATE_PATTERN));
        // 检测日期
        PqDevSub devSub = iPqDevSubService.lambdaQuery().eq(PqDevSub::getDevId, pqDevVO.getId()).one();
        if (Objects.nonNull(devSub)) {
            LocalDateTime checkEndTime = devSub.getCheckEndTime();
            if (Objects.nonNull(checkEndTime)) {
                baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(checkEndTime, DatePattern.NORM_DATE_PATTERN));
            } else {
                baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), StrPool.TAB);
            }
        } else {
            baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), StrPool.TAB);
        }
        return baseModelMap;
    }


    /**
     * 获取数据页的信息
     *
     * @param devReportParam 查询参数
     */
    private void dealDataModel(List<InputStream> wordFileInputStreams, DevReportParam devReportParam, PqDevVO pqDevVO) throws Exception {
        Integer devChns = pqDevVO.getDevChns();
        for (int i = 1; i <= devChns; i++) {
            String path = "/model/report_table.docx";
            DevType devType = devTypeService.getById(pqDevVO.getDevType());
            if (devType.getName().equals("PQ-COM")) {
                path = "/model/report_table - PQ-COM.docx";
            }
            ClassPathResource resource = new ClassPathResource(path);
            XWPFDocument dataModelDocumentTemp = new XWPFDocument(resource.getInputStream());

            SingleNonHarmParam singleNonHarmParam = new SingleNonHarmParam();
            singleNonHarmParam.setPlanCode(devReportParam.getPlanCode());
            singleNonHarmParam.setDevId(pqDevVO.getId());
            singleNonHarmParam.setChannelNo(i);

            // 获取数据
            Map<String, String> dataModelMap = new HashMap<>(16);
            // 读取模板文件中的占位符
            Set<String> allMarkList = WordDocumentUtil.extractPlaceholders(resource.getInputStream(), false, Arrays.asList("CreateId", "total", "count"));
            Map<String, Set<String>> indexKeysMap = allMarkList.stream()
                    .collect(Collectors.groupingBy(
                            obj -> obj.split("#")[0],
                            Collectors.mapping(obj -> {
                                int index1 = obj.indexOf("#") + 1;
                                return obj.substring(index1, obj.indexOf("#", index1));
                                //key为index，value为该index下所有测试项对应的code
                            }, Collectors.toSet())));

            List<SimAndDigNonHarmonicResult> simAndDigNonHarmonicResultList = adNonHarmonicService.listSimAndDigBaseResult(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i);
            List<SimAndDigHarmonicResult> adHarmonicResultList = adHarmonicService.listAllResultData(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i);

            // 填充数据
            int finalI = i;
            indexKeysMap.forEach((index, keys) -> {
                String s1 = keys.stream().findFirst().get();
                // 谐波类
                if (DetectionCodeEnum.V2_50.getCode().equals(s1) || DetectionCodeEnum.I2_50.getCode().equals(s1) || DetectionCodeEnum.SV_1_49.getCode().equals(s1) || DetectionCodeEnum.SI_1_49.getCode().equals(s1)) {
                    // 查找一下U1
                    double u1 = 57.74;
                    List<SimAndDigHarmonicResult> rawData = adHarmonicService.listAllRawData(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + finalI);
                    SimAndDigHarmonicResult adHarmonicResult = rawData.stream().filter(obj -> obj.getAdType().equals(DetectionCodeEnum.V2_50.getCode())).sorted((obj1, obj2) -> obj2.getTimeId().compareTo(obj1.getTimeId())).findFirst().orElse(null);
                    if (ObjectUtil.isNotNull(adHarmonicResult)) {
                        String aValue1 = adHarmonicResult.getAValue1();
                        u1 = Double.parseDouble(aValue1);
                    }
                    fillMapValueHarm(adHarmonicResultList, dataModelMap, keys, index, u1);
                } else {
                    // 非谐波类
                    if (DetectionCodeEnum.V_UNBAN.getCode().equals(s1) || DetectionCodeEnum.FREQ.getCode().equals(s1)) {
                        fillMapValueT(simAndDigNonHarmonicResultList, dataModelMap, keys, index);
                    } else {
                        fillMapValueABC(simAndDigNonHarmonicResultList, dataModelMap, keys, index);
                    }
                }
            });
            dataModelMap.put("CreateId", pqDevVO.getCreateId());
            dataModelMap.put("total", pqDevVO.getDevChns().toString());
            dataModelMap.put("count", i + "");
            // 替换文档内容
            wordFileInputStreams.add(wordReportService.replacePlaceholders(resource.getInputStream(), dataModelMap));
        }
    }

    /**
     * 填充数据(ABC相)
     *
     * @param allNonHarmonicResultList 结果数据
     * @param dataModelMap             替换数据
     * @param keys                     keys
     * @param index                    index
     */
    private void fillMapValueABC(List<SimAndDigNonHarmonicResult> allNonHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index) {
        keys.forEach(key -> {
            List<SimAndDigNonHarmonicResult> resultList = allNonHarmonicResultList.stream().filter(obj -> obj.getAdType().equals(key) && obj.getSort().toString().equals(index)).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(resultList)) {
                SimAndDigNonHarmonicResult adNonHarmonicResult = resultList.get(0);
                if (ObjectUtil.isNotNull(adNonHarmonicResult)) {
                    dataModelMap.put(index + "#" + key + "#A", devValue(adNonHarmonicResult.getAValue(), 1, 1));
                    dataModelMap.put(index + "#" + key + "#B", devValue(adNonHarmonicResult.getBValue(), 1, 1));
                    dataModelMap.put(index + "#" + key + "#C", devValue(adNonHarmonicResult.getCValue(), 1, 1));
                }
            }
        });
    }

    /**
     * 填充数据(T相)
     *
     * @param allNonHarmonicResultList 结果数据
     * @param dataModelMap             替换数据
     * @param keys                     key
     * @param index                    index
     */
    private void fillMapValueT(List<SimAndDigNonHarmonicResult> allNonHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index) {
        keys.forEach(key -> {
            List<SimAndDigNonHarmonicResult> resultList = allNonHarmonicResultList.stream().filter(obj -> obj.getAdType().equals(key) && obj.getSort().toString().equals(index)).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(resultList)) {
                SimAndDigNonHarmonicResult adNonHarmonicResult = resultList.get(0);
                if (ObjectUtil.isNotNull(adNonHarmonicResult)) {
                    dataModelMap.put(index + "#" + key + "#T", devValue(adNonHarmonicResult.getTValue(), 1, 1));
                }
            }
        });
    }

    /**
     * 填充数据(谐波类)
     *
     * @param allHarmonicResultList 结果数据
     * @param dataModelMap          替换数据
     * @param keys                  key
     * @param index                 index
     */
    private void fillMapValueHarm(List<SimAndDigHarmonicResult> allHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index, double baseValue) {
        keys.forEach(key -> {
            List<SimAndDigHarmonicResult> resultList = allHarmonicResultList.stream().filter(obj -> obj.getAdType().equals(key) && obj.getSort().toString().equals(index)).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(resultList)) {
                SimAndDigHarmonicResult adHarmonicResult = resultList.get(0);
                if (Objects.nonNull(adHarmonicResult)) {
                    if (DetectionCodeEnum.V2_50.getCode().equals(key) || DetectionCodeEnum.SV_1_49.getCode().equals(key)) {
                        fillHarm(dataModelMap, adHarmonicResult, index, key, baseValue, 100);
                    }
                    if (DetectionCodeEnum.I2_50.getCode().equals(key) || DetectionCodeEnum.SI_1_49.getCode().equals(key)) {
                        fillHarm(dataModelMap, adHarmonicResult, index, key, 1, 1);
                    }
                }
            }
        });
    }

    /**
     * 对谐波类数据进行处理，并填充到map中
     *
     * @param dataModelMap
     * @param adHarmonicResult
     * @param index
     * @param key
     * @param baseValue
     * @param percent
     */
    private void fillHarm(Map<String, String> dataModelMap, SimAndDigHarmonicResult adHarmonicResult, String index, String key, double baseValue, int percent) {
        // 要处理 2 5 7 11 23 35 43 50

        String aBase = devValue(adHarmonicResult.getAValue1(), 1, 1);
        String bBase = devValue(adHarmonicResult.getBValue1(), 1, 1);
        String cBase = devValue(adHarmonicResult.getCValue1(), 1, 1);
        dataModelMap.put(index + "#" + key + "#A#1", aBase);
        dataModelMap.put(index + "#" + key + "#B#1", bBase);
        dataModelMap.put(index + "#" + key + "#C#1", cBase);

        // 基波
        double aBaseValue = baseValue;
        double bBaseValue = baseValue;
        double cBaseValue = baseValue;
        if (!DetectionCodeEnum.I2_50.getCode().equals(adHarmonicResult.getAdType())) {
            if (!aBase.equals(StrPool.SLASH)) {
                aBaseValue = Double.parseDouble(aBase);
            }
            if (!bBase.equals(StrPool.SLASH)) {
                bBaseValue = Double.parseDouble(bBase);
            }
            if (!cBase.equals(StrPool.SLASH)) {
                cBaseValue = Double.parseDouble(cBase);
            }
        }

        dataModelMap.put(index + "#" + key + "#A#2", devValue(adHarmonicResult.getAValue2(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#2", devValue(adHarmonicResult.getBValue2(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#2", devValue(adHarmonicResult.getCValue2(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#5", devValue(adHarmonicResult.getAValue5(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#5", devValue(adHarmonicResult.getBValue5(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#5", devValue(adHarmonicResult.getCValue5(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#7", devValue(adHarmonicResult.getAValue7(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#7", devValue(adHarmonicResult.getBValue7(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#7", devValue(adHarmonicResult.getCValue7(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#11", devValue(adHarmonicResult.getAValue11(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#11", devValue(adHarmonicResult.getBValue11(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#11", devValue(adHarmonicResult.getCValue11(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#23", devValue(adHarmonicResult.getAValue23(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#23", devValue(adHarmonicResult.getBValue23(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#23", devValue(adHarmonicResult.getCValue23(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#35", devValue(adHarmonicResult.getAValue35(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#35", devValue(adHarmonicResult.getBValue35(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#35", devValue(adHarmonicResult.getCValue35(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#43", devValue(adHarmonicResult.getAValue43(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#43", devValue(adHarmonicResult.getBValue43(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#43", devValue(adHarmonicResult.getCValue43(), cBaseValue, percent));

        dataModelMap.put(index + "#" + key + "#A#50", devValue(adHarmonicResult.getAValue50(), aBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#B#50", devValue(adHarmonicResult.getBValue50(), bBaseValue, percent));
        dataModelMap.put(index + "#" + key + "#C#50", devValue(adHarmonicResult.getCValue50(), cBaseValue, percent));
    }

    private String devValue(String dataJson, double baseValue, Integer percent) {
        DetectionData tempA = JSONUtil.toBean(dataJson, DetectionData.class);
        if (Objects.nonNull(tempA) && Objects.nonNull(tempA.getData())) {
            return doubleRound(4, (tempA.getData() / percent) * baseValue);
        }
        return StrPool.SLASH;
    }


    /**
     * 根据参数返回double的四舍五入值
     *
     * @param i     保留的位数
     * @param value double原值
     */
    public static String doubleRound(int i, double value) {
        BigDecimal bp = new BigDecimal(value);
        return String.format("%.4f", bp.setScale(i, RoundingMode.HALF_UP).doubleValue());
    }

    static String formatChineseReportDate(LocalDate date) {
        Objects.requireNonNull(date, "date");
        return formatChineseYear(date.getYear()) + "年"
                + formatChineseMonthDayNumber(date.getMonthValue()) + "月"
                + formatChineseMonthDayNumber(date.getDayOfMonth()) + "日";
    }

    static String formatNumberChineseDate(LocalDate date) {
        Objects.requireNonNull(date, "date");
        return date.getYear() + "年" + date.getMonthValue() + "月" + date.getDayOfMonth() + "日";
    }

    private static String formatChineseYear(int year) {
        String yearValue = String.valueOf(year);
        StringBuilder builder = new StringBuilder(yearValue.length());
        for (int i = 0; i < yearValue.length(); i++) {
            builder.append(CHINESE_DIGITS[yearValue.charAt(i) - '0']);
        }
        return builder.toString();
    }

    private static String formatChineseMonthDayNumber(int value) {
        if (value < 10) {
            return CHINESE_DIGITS[value];
        }
        if (value == 10) {
            return "十";
        }
        if (value < 20) {
            return "十" + CHINESE_DIGITS[value % 10];
        }
        return CHINESE_DIGITS[value / 10] + "十" + (value % 10 == 0 ? "" : CHINESE_DIGITS[value % 10]);
    }


    private void ensureDirectoryExists(String directoryPath) {
        File directory = new File(directoryPath);
        if (!directory.exists()) {
            boolean created = directory.mkdirs();
            if (!created) {
                throw new BusinessException("目录创建失败: " + directoryPath);
            }
        } else {
            System.out.println("目录已存在: " + directoryPath);
        }
    }

    /**
     * 检查模板文件是否重复
     *
     * @param reportParam   报告模板参数
     * @param isExcludeSelf 是否排除自己
     */
    private void checkRepeat(ReportParam reportParam, boolean isExcludeSelf) {
        QueryWrapper<PqReport> wrapper = new QueryWrapper();
        wrapper.eq("pattern", reportParam.getPattern())
                .eq("name", reportParam.getName())
                .eq("version", reportParam.getVersion())
                .eq("state", DataStateEnum.ENABLE.getCode());
        if (isExcludeSelf) {
            if (reportParam instanceof ReportParam.UpdateParam) {
                wrapper.ne("id", ((ReportParam.UpdateParam) reportParam).getId());
            }
        }
        int count = this.count(wrapper);
        if (count > 0) {
            throw new BusinessException(ReportResponseEnum.REPORT_REPEATED);
        }
    }

    private String sanitizePathSegment(String value) {
        return StrUtil.blankToDefault(value, StrUtil.EMPTY).replaceAll("[\\\\/:*?\"<>|]", "_").trim();
    }

    private String buildReportTemplateRelativeDir(String patternId, String name, String version) {
        DictData dictData = dictDataService.getDictDataById(patternId);
        if (ObjectUtil.isNull(dictData) || StrUtil.isBlank(dictData.getName())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "模式不存在或字典名称缺失");
        }
        String patternName = this.sanitizePathSegment(dictData.getName());
        String reportName = this.sanitizePathSegment(name);
        String reportVersion = this.sanitizePathSegment(version);
        return patternName + File.separator + reportName + File.separator + reportVersion + File.separator;
    }

    @Override
    public void uploadReportToCloud(List<String> deviceIds) {
        log.info("开始批量上传检测报告到云端，设备ID列表：{}", deviceIds);
        List<PqDevSub> deviceSubs = iPqDevSubService.lambdaQuery()
                .eq(PqDevSub::getReportState, DevReportStateEnum.GENERATED.getValue())
                .in(CollUtil.isNotEmpty(deviceIds), PqDevSub::getDevId, deviceIds).list();
        List<String> devIds = deviceSubs.stream().map(PqDevSub::getDevId).collect(Collectors.toList());
        List<PqDev> devices = iPqDevService.lambdaQuery()
                .in(CollUtil.isNotEmpty(devIds), PqDev::getId, devIds).list();
        if (CollUtil.isEmpty(devices)) {
            log.warn("未找到符合条件的设备，无需上传");
            return;
        }
        log.info("找到{}台设备需要上传报告", devices.size());
        String dirPath = pathConfig.getReportPath();
        // 确保目录存在
        ensureDirectoryExists(dirPath);
        // 异步批量上传每台设备的报告
        CompletableFuture.runAsync(() -> {
            for (PqDev device : devices) {
                try {
                    // 构建报告文件路径
                    String fileName = device.getCreateId() + ReportConstant.DOCX;
                    String reportFullPath = dirPath.concat(File.separator).concat(device.getCreateId()).concat(ReportConstant.DOCX);
                    File reportFile = new File(reportFullPath);

                    if (!reportFile.exists()) {
                        log.warn("设备{}的报告文件不存在：{}", device.getId(), fileName);
                        continue;
                    }

                    // 上传文件到云端
                    ResponseEntity<String> responseEntity = restTemplateUtil.uploadFile(cloudUrl + "/upload", reportFile);

                    if (responseEntity.getStatusCode().is2xxSuccessful()) {
                        // 更新设备报告状态为已生成且已上传
                        iPqDevService.updatePqDevReportState(device.getId(), DevReportStateEnum.GENERATED_UPLOADED.getValue());
                        log.info("设备{}报告上传成功", device.getId());
                    } else {
                        log.error("设备{}报告上传失败，HTTP状态码：{}", device.getId(), responseEntity.getStatusCode());
                    }

                } catch (Exception e) {
                    log.error("设备{}报告上传异常", device.getId(), e);
                }
            }
            log.info("批量上传任务完成");
        });
    }


    /**
     * 创建回路标题到报告中
     */
    private P getLineTitle(Map<String, List<Docx4jUtil.HeadingContent>> contentMap, int currentLine, int totalLine, ObjectFactory factory) {
        List<Docx4jUtil.HeadingContent> headingContents = contentMap.get(PowerIndexEnum.LINE_TITLE.getKey());
        int lineNo = currentLine + 1;
        // 如果contentMap中有指定内容，创建大纲级别为2的标题
        // 回路标题恒带回路号（单回路也显示"测量回路1"），与结论表头"测量回路N"保持一致
        if (CollUtil.isNotEmpty(headingContents)) {
            return Docx4jUtil.createTitle(factory, 2, lineNo + ".测量回路" + lineNo,
                    "SimSun", 30, true);
        }
        // 没有模板配置时，创建默认样式段落
        P titleParagraph = factory.createP();
        Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo,
                28, true);
        return titleParagraph;
    }


    /**
     * 创建回路标题到报告中
     */
    private P getContrastLineTitle(Map<String, List<Docx4jUtil.HeadingContent>> contentMap, int monitorNum, int index, ObjectFactory factory, PqDevVO pqDevVO) {
        PqMonitor pqMonitor = pqMonitorService.getByDevIdAndNum(pqDevVO.getId(), monitorNum);
        String monitorInfoName = "";
        if (StrUtil.isNotBlank(pqDevVO.getSubName()) && Objects.nonNull(pqMonitor)) {
            monitorInfoName = "(" + pqDevVO.getSubName() + "-" + pqMonitor.getName() + ")";
        }
        List<Docx4jUtil.HeadingContent> headingContents = contentMap.get(PowerIndexEnum.LINE_TITLE.getKey());
        // 如果contentMap中有指定内容，创建大纲级别为2的标题
        if (CollUtil.isNotEmpty(headingContents)) {
            return Docx4jUtil.createTitle(factory, 2, index + ".测量回路" + monitorNum + monitorInfoName,
                    "SimSun", 30, true);
        }
        // 没有模板配置时，创建默认样式段落
        P titleParagraph = factory.createP();
        Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + monitorNum + monitorInfoName,
                28, true);
        return titleParagraph;
    }

    /**
     * 创建检测项标题
     *
     * @param title 标题名称
     * @return 标题段落
     */
    private P getContrastScriptTitle(String title, ObjectFactory factory) {
        return Docx4jUtil.createTitle(factory, 3, title,
                "SimSun", 28, true);
    }

    /**
     * 设置表格单元格纵向合并开始
     *
     * @param row       表格行
     * @param cellIndex 单元格索引
     * @param rowSpan   合并行数
     * @param factory   对象工厂
     */
    private void setVerticalMergeStart(Tr row, int cellIndex, int rowSpan, ObjectFactory factory) {
        if (row.getContent() != null && cellIndex < row.getContent().size()) {
            Object cellObj = row.getContent().get(cellIndex);
            if (cellObj instanceof JAXBElement) {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) cellObj;
                if (jaxbElement.getValue() instanceof Tc) {
                    Tc tc = (Tc) jaxbElement.getValue();
                    TcPr tcPr = tc.getTcPr();
                    if (tcPr == null) {
                        tcPr = factory.createTcPr();
                        tc.setTcPr(tcPr);
                    }
                    // 设置纵向合并开始
                    TcPrInner.VMerge vMerge = new TcPrInner.VMerge();
                    vMerge.setVal("restart");
                    tcPr.setVMerge(vMerge);

                    // 设置跨行数（如果需要的话）
                    if (rowSpan > 1) {
                        TcPrInner.GridSpan gridSpan = new TcPrInner.GridSpan();
                        gridSpan.setVal(BigInteger.valueOf(1)); // 列跨度为1
                        tcPr.setGridSpan(gridSpan);
                    }
                }
            }
        }
    }

    /**
     * 设置表格单元格纵向合并延续
     *
     * @param row       表格行
     * @param cellIndex 单元格索引
     * @param factory   对象工厂
     */
    private void setVerticalMergeContinue(Tr row, int cellIndex, ObjectFactory factory) {
        if (row.getContent() != null && cellIndex < row.getContent().size()) {
            Object cellObj = row.getContent().get(cellIndex);
            if (cellObj instanceof JAXBElement) {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) cellObj;
                if (jaxbElement.getValue() instanceof Tc) {
                    Tc tc = (Tc) jaxbElement.getValue();

                    // 清空单元格内容，因为它将被合并
                    tc.getContent().clear();
                    // 添加一个空段落（Word表格单元格需要至少有一个段落）
                    P emptyP = factory.createP();
                    tc.getContent().add(emptyP);

                    // 设置单元格属性
                    TcPr tcPr = tc.getTcPr();
                    if (tcPr == null) {
                        tcPr = factory.createTcPr();
                        tc.setTcPr(tcPr);
                    }
                    // 设置纵向合并延续
                    TcPrInner.VMerge vMerge = new TcPrInner.VMerge();
                    // vMerge.setVal("continue"); // 不设置val或设置为空表示延续
                    tcPr.setVMerge(vMerge);
                }
            }
        }
    }

    /**
     * 设置谐波表格次数列的纵向合并
     * 参考Docx4jUtil的实现方式
     *
     * @param row       表格行
     * @param cellIndex 单元格索引（通常是0，次数列）
     * @param mergeType 合并类型："restart"开始，"continue"延续
     * @param factory   对象工厂
     */
    private void setHarmonicCellVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) {
        if (row.getContent() != null && cellIndex < row.getContent().size()) {
            Object cellObj = row.getContent().get(cellIndex);
            Tc tc = null;

            // 获取Tc对象
            if (cellObj instanceof JAXBElement) {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) cellObj;
                if (jaxbElement.getValue() instanceof Tc) {
                    tc = (Tc) jaxbElement.getValue();
                }
            } else if (cellObj instanceof Tc) {
                tc = (Tc) cellObj;
            }

            if (tc != null) {
                // 设置单元格属性
                TcPr tcPr = tc.getTcPr();
                if (tcPr == null) {
                    tcPr = factory.createTcPr();
                    tc.setTcPr(tcPr);
                }

                // 设置VMerge
                TcPrInner.VMerge vMerge = new TcPrInner.VMerge();
                vMerge.setVal(mergeType);
                tcPr.setVMerge(vMerge);
            }
        }
    }

    /**
     * 只设置次数列的纵向合并，参考Docx4jUtil样品结果列的实现
     *
     * @param row       表格行
     * @param cellIndex 次数列索引（通常是0）
     * @param mergeType 合并类型："restart"开始，"continue"延续
     * @param factory   对象工厂
     */
    private void setOnlyHarmonicColumnVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) {
        if (row.getContent() != null && cellIndex < row.getContent().size()) {
            Object cellObj = row.getContent().get(cellIndex);
            Tc tc = null;

            // 获取Tc对象
            if (cellObj instanceof JAXBElement) {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) cellObj;
                if (jaxbElement.getValue() instanceof Tc) {
                    tc = (Tc) jaxbElement.getValue();
                }
            } else if (cellObj instanceof Tc) {
                tc = (Tc) cellObj;
            }

            if (tc != null) {
                // 如果是continue，按照Docx4jUtil的方式清空内容
                if ("continue".equals(mergeType)) {
                    // 清空单元格内容
                    tc.getContent().clear();
                    // 添加空段落
                    P emptyP = factory.createP();
                    tc.getContent().add(emptyP);
                }

                // 设置单元格属性
                TcPr tcPr = tc.getTcPr();
                if (tcPr == null) {
                    tcPr = factory.createTcPr();
                    tc.setTcPr(tcPr);
                }

                // 设置VMerge
                TcPrInner.VMerge vMerge = new TcPrInner.VMerge();
                vMerge.setVal(mergeType);
                tcPr.setVMerge(vMerge);
            }
        }
    }

    /**
     * 简单的VMerge设置，只处理次数列
     *
     * @param row       表格行
     * @param cellIndex 单元格索引（0表示次数列）
     * @param mergeType 合并类型："restart"或"continue"
     * @param factory   对象工厂
     */
    private void setSimpleVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) {
        if (row.getContent() != null && cellIndex < row.getContent().size()) {
            Object cellObj = row.getContent().get(cellIndex);
            Tc tc = null;

            // 获取Tc对象
            if (cellObj instanceof JAXBElement) {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) cellObj;
                if (jaxbElement.getValue() instanceof Tc) {
                    tc = (Tc) jaxbElement.getValue();
                }
            } else if (cellObj instanceof Tc) {
                tc = (Tc) cellObj;
            }

            if (tc != null) {
                // 如果是continue，清空次数列内容
                if ("continue".equals(mergeType)) {
                    tc.getContent().clear();
                    P emptyP = factory.createP();
                    tc.getContent().add(emptyP);
                }

                // 设置VMerge
                TcPr tcPr = tc.getTcPr();
                if (tcPr == null) {
                    tcPr = factory.createTcPr();
                    tc.setTcPr(tcPr);
                }
                TcPrInner.VMerge vMerge = new TcPrInner.VMerge();
                vMerge.setVal(mergeType);
                tcPr.setVMerge(vMerge);
            }
        }
    }


}
