inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java
@@ -2,6 +2,11 @@
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.aspose.words.FontSettings;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -13,6 +18,9 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.DocumentException;
@@ -20,36 +28,50 @@
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.ruoyi.basic.mapper.IfsInventoryQuantityMapper;
import com.ruoyi.basic.mapper.SealConfigMapper;
import com.ruoyi.basic.pojo.IfsInventoryQuantity;
import com.ruoyi.basic.pojo.SealConfig;
import com.ruoyi.basic.vo.IfsInventoryQuantityVO;
import com.ruoyi.common.config.WechatProperty;
import com.ruoyi.common.constant.InsOrderTypeConstants;
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.properties.WechatProperty;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.DateImageUtil;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.common.utils.api.IfsApiUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.dto.InsReportExport;
import com.ruoyi.inspect.dto.ReportPageDto;
import com.ruoyi.inspect.mapper.*;
import com.ruoyi.inspect.pojo.*;
import com.ruoyi.inspect.service.InsOrderService;
import com.ruoyi.inspect.service.InsReportService;
import com.ruoyi.inspect.mapper.InsUnqualifiedHandlerMapper;
import com.ruoyi.inspect.service.RawMaterialOrderService;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.util.ReportSealLayoutUtil;
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -71,6 +93,15 @@
@Slf4j
public class InsReportServiceImpl extends ServiceImpl<InsReportMapper, InsReport>
        implements InsReportService {
    static {
        // Linux 服务器缺少中文字体,报告转 PDF 时中文会变成方块;将模板字体映射到 Noto CJK 兜底。
        FontSettings.addFontSubstitutes("宋体", "Noto Serif CJK SC");
        FontSettings.addFontSubstitutes("黑体", "Noto Sans CJK SC");
    }
    private static final int DETECTION_REPORT = 0;
    private static final int INSPECTION_REPORT = 1;
    @Resource
    private UserMapper userMapper;
    @Resource
@@ -91,6 +122,8 @@
    @Resource
    private IfsInventoryQuantityMapper ifsInventoryQuantityMapper;
    @Resource
    private SealConfigMapper sealConfigMapper;
    @Resource
    private InsUnqualifiedHandlerMapper insUnqualifiedHandlerMapper;
    @Resource
    private InsSampleMapper insSampleMapper;
@@ -106,6 +139,8 @@
    private IfsApiUtils ifsApiUtils;
    @Resource
    private InsSampleUserMapper insSampleUserMapper;
    @Autowired
    private RawMaterialOrderService rawMaterialOrderService;
    @Override
@@ -114,9 +149,11 @@
        // todo: 仅看自己
        //获取当前人所属实验室id
        String laboratory = null;
        Integer createOrderUser = null;
        Integer createOrderUser = reportPageDto.getCreateOrderUser();
        String queryStatus = reportPageDto.getQueryStatus();
        reportPageDto.setQueryStatus(null);
        reportPageDto.setCreateOrderUser(null);
        map.put("body", insReportMapper.pageInsReport(page,
                QueryWrappers.queryWrappers(reportPageDto),
@@ -190,30 +227,30 @@
        info.setViewStatus(false);
        info.setJumpPath(MenuJumpPathConstants.REPORT_PREPARATION);
        informationNotificationService.addInformationNotification(info);
        //系统生成报告地址
        String url = insReport.getUrl();
        //手动上传报告地址
        String urlS = insReport.getUrlS();
        String primaryReportUrl = getPrimaryReportUrl(insReport);
        // 判断是否是原材料  需要替换****成供应商
        IfsInventoryQuantity one = ifsInventoryQuantityMapper.selectOne(new LambdaQueryWrapper<IfsInventoryQuantity>()
                .eq(IfsInventoryQuantity::getId, order.getIfsInventoryId()));
        if (one != null) {
            if (isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY)) {
                changeText(new HashMap<String, String>() {{
                    put("**********", one.getSupplierName());
                }}, (StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl));
                for (String reportUrl : getApprovalReportUrls(insReport)) {
                    changeText(new HashMap<String, String>() {{
                        put("**********", one.getSupplierName());
                    }}, reportUrl.replace("/word", wordUrl));
                }
            }
        }
        wordInsertUrl(new HashMap<String, Object>() {{
            put("writeUrl", Pictures.ofLocal(imgUrl + "/" + signatureUrl).create());
        Map<String, Object> writeMarks = new HashMap<String, Object>() {{
            put("writeUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
            put("writeDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
            put("insUrl", Pictures.ofLocal(imgUrl + "/" + signatureUrl).create());
        }}, (StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl));
            put("insUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
        }};
        writeMarksToReportFiles(insReport, writeMarks);
        // 修改临时pdf
        String tempUrlPdf = wordToPdfTemp((StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl));
        String tempUrlPdf = wordToPdfTemp(primaryReportUrl.replace("/word", wordUrl));
        insReport.setTempUrlPdf("/word/" + tempUrlPdf);
        insReportMapper.updateById(insReport);
@@ -284,6 +321,9 @@
            if (userId == null) {
                throw new ErrorException("缺少批准人");
            }
            if (Objects.equals(insReport.getExamineUserId(), userId)) {
                throw new ErrorException("审核人和批准人不能为同一人,请重新选择批准人");
            }
        }
        insReport.setRatifyUserId(userId);//批准人
        //获取审核人的签名地址
@@ -311,17 +351,15 @@
        info.setViewStatus(false);
        info.setJumpPath(MenuJumpPathConstants.REPORT_PREPARATION);
        informationNotificationService.addInformationNotification(info);
        //系统生成报告地址
        String url = insReport.getUrl();
        //手动上传报告地址
        String urlS = insReport.getUrlS();
        wordInsertUrl(new HashMap<String, Object>() {{
            put("examineUrl", Pictures.ofLocal(imgUrl + "/" + signatureUrl).create());
        String primaryReportUrl = getPrimaryReportUrl(insReport);
        Map<String, Object> examineMarks = new HashMap<String, Object>() {{
            put("examineUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
            put("examineDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
        }}, (StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl));
        }};
        writeMarksToReportFiles(insReport, examineMarks);
        // 修改临时pdf
        String tempUrlPdf = wordToPdfTemp((StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl));
        String tempUrlPdf = wordToPdfTemp(primaryReportUrl.replace("/word", wordUrl));
        insReport.setTempUrlPdf("/word/" + tempUrlPdf);
        // 发送企业微信通知(通知批准人审批)
@@ -366,8 +404,12 @@
    //批准
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int ratifyReport(Integer id, Integer isRatify, String ratifyTell) {
    public int ratifyReport(Integer id, Integer isRatify, String ratifyTell, String sealIds,
                            Integer maSealId, Integer reportSealId, Integer seamSealId) {
        InsReport insReport = insReportMapper.selectById(id);
        if (Objects.equals(insReport.getExamineUserId(), SecurityUtils.getUserId().intValue())) {
            throw new ErrorException("审核人和批准人不能为同一人,请重新选择批准人");
        }
        insReport.setIsRatify(isRatify);
        if (ObjectUtils.isNotEmpty(ratifyTell)) {
            insReport.setRatifyTell(ratifyTell);
@@ -437,45 +479,79 @@
        InsOrder order = insOrderMapper.selectById(insOrderId);
        boolean isRawMater = order.getTypeSource() != null && order.getTypeSource().equals(1);
        //获取场所的报告专用章
        String sealUrl;
        String laboratory = insOrderMapper.selectById(insReport.getInsOrderId()).getLaboratory();
        try {
            String type = "";
            if (isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY)) {
                type = "进厂报告";
            } else {
                type = "委托报告";
            }
            sealUrl = insReportMapper.getLaboratoryByName(laboratory, type);
        } catch (Exception e) {
            throw new ErrorException(laboratory + "找不到报告专用章");
        // 新版按用途选择:MA章、报告章(模板 seal1/seal2)、PDF骑缝章。
        // 兼容旧前端:只传 sealIds 时,第一枚章继续作为报告章和骑缝章,MA章留空。
        SealConfig maSeal;
        SealConfig reportSeal;
        SealConfig seamSeal;
        boolean positionMode = maSealId != null || reportSealId != null || seamSealId != null;
        if (positionMode) {
            maSeal = resolveSeal(maSealId);
            reportSeal = resolveSeal(reportSealId);
            seamSeal = resolveSeal(seamSealId);
        } else {
            SealConfig legacySeal = resolveSeals(sealIds).get(0);
            maSeal = null;
            reportSeal = legacySeal;
            seamSeal = legacySeal;
        }
        if (StringUtils.isBlank(sealUrl)) {
            throw new ErrorException(laboratory + "找不到报告专用章");
        }
        //系统生成报告地址
        String url = insReport.getUrl();
        //手动上传报告地址
        String urlS = insReport.getUrlS();
        String finalUrl = (StrUtil.isBlank(urlS) ? url : urlS).replace("/word", wordUrl);
        wordInsertUrl(new HashMap<String, Object>() {{
            put("ratifyUrl", Pictures.ofLocal(imgUrl + "/" + signatureUrl).create());
            put("ratifyDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
            put("seal1", Pictures.ofLocal(imgUrl + "/" + sealUrl).create());
            put("seal2", Pictures.ofLocal(imgUrl + "/" + sealUrl).create());
        }}, finalUrl);
        LinkedHashMap<Integer, SealConfig> chosenById = new LinkedHashMap<>();
        Arrays.asList(maSeal, reportSeal, seamSeal).stream()
                .filter(Objects::nonNull)
                .forEach(seal -> chosenById.put(seal.getId(), seal));
        List<SealConfig> chosenSeals = new ArrayList<>(chosenById.values());
        if (chosenSeals.isEmpty()) {
            throw new ErrorException("请至少选择一种报告印章");
        }
        insReport.setSealIds(chosenSeals.stream().map(s -> String.valueOf(s.getId())).collect(Collectors.joining(",")));
        insReport.setSealNames(chosenSeals.stream().map(SealConfig::getName).collect(Collectors.joining(",")));
        insReport.setMaSealId(maSeal == null ? null : maSeal.getId());
        insReport.setMaSealName(maSeal == null ? null : maSeal.getName());
        insReport.setReportSealId(reportSeal == null ? null : reportSeal.getId());
        insReport.setReportSealName(reportSeal == null ? null : reportSeal.getName());
        insReport.setSeamSealId(seamSeal == null ? null : seamSeal.getId());
        insReport.setSeamSealName(seamSeal == null ? null : seamSeal.getName());
        String primaryReportUrl = getPrimaryReportUrl(insReport);
        String finalUrl = primaryReportUrl.replace("/word", wordUrl);
        Map<String, Object> ratifyMarks = new HashMap<>();
        ratifyMarks.put("ratifyUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
        ratifyMarks.put("ratifyDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
        if (reportSeal != null) {
            ratifyMarks.put("seal1", createSealPicture(reportSeal.getAddress()));
            ratifyMarks.put("seal2", createSealPicture(reportSeal.getAddress()));
        }
        writeApprovalMarksToReportFiles(insReport, ratifyMarks);
        // 印章必须使用浮动定位,否则行内图片会撑高段落/表格并改变原报告分页。
        for (String reportUrl : getApprovalReportUrls(insReport)) {
            try {
                ReportSealLayoutUtil.apply(
                        reportUrl.replace("/word", wordUrl),
                        reportSeal == null ? null : Paths.get(imgUrl, reportSeal.getAddress()).toString(),
                        maSeal == null ? null : Paths.get(imgUrl, maSeal.getAddress()).toString());
            } catch (Exception e) {
                log.error("报告印章浮动定位失败: {}", reportUrl, e);
                throw new ErrorException("报告盖章失败,请检查印章图片和报告文件");
            }
        }
        // 修改临时pdf
        insReport.setTempUrlPdf((StrUtil.isBlank(urlS) ? url : urlS).replace(".docx", ".pdf"));
        insReport.setTempUrlPdf(primaryReportUrl.replace(".docx", ".pdf"));
        InsOrder insOrder = new InsOrder();
        insOrder.setId(insOrderId);
        insOrder.setState(4);
        insOrderMapper.updateById(insOrder);
        wordToPdf(finalUrl, sealUrl, isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY));
        for (String reportUrl : getApprovalReportUrls(insReport)) {
            wordToPdf(reportUrl.replace("/word", wordUrl),
                    null,
                    seamSeal == null ? null : seamSeal.getAddress(),
                    isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY));
        }
        // 判断是否为原材料
        if (isRawMater) {
@@ -504,15 +580,132 @@
        return insReportMapper.updateById(insReport);
    }
    /**
     * 解析批准时选择的印章(逗号分隔的印章id)。
     * 未传或全部无效时回退到第一个启用印章(默认“1号章”)。
     */
    private List<SealConfig> resolveSeals(String sealIds) {
        List<SealConfig> chosen = new ArrayList<>();
        if (StringUtils.isNotBlank(sealIds)) {
            List<Integer> ids = new ArrayList<>();
            for (String s : sealIds.split(",")) {
                if (StringUtils.isNotBlank(s)) {
                    ids.add(Integer.valueOf(s.trim()));
                }
            }
            if (!ids.isEmpty()) {
                Map<Integer, SealConfig> byId = sealConfigMapper.selectBatchIds(ids).stream()
                        .collect(Collectors.toMap(SealConfig::getId, x -> x));
                for (Integer sid : ids) {
                    SealConfig seal = byId.get(sid);
                    if (seal != null && Objects.equals(seal.getStatus(), 1)
                            && StringUtils.isNotBlank(seal.getAddress())) {
                        chosen.add(seal);
                    }
                }
            }
        }
        if (chosen.isEmpty()) {
            List<SealConfig> enabled = sealConfigMapper.selectList(Wrappers.<SealConfig>lambdaQuery()
                    .eq(SealConfig::getStatus, 1).orderByAsc(SealConfig::getCode));
            if (!enabled.isEmpty()) {
                chosen.add(enabled.get(0));
            }
        }
        if (chosen.isEmpty()) {
            throw new ErrorException("未找到可用印章,请先在印章管理中维护并启用印章");
        }
        return chosen;
    }
    private SealConfig resolveSeal(Integer sealId) {
        if (sealId == null) {
            return null;
        }
        SealConfig seal = sealConfigMapper.selectById(sealId);
        if (seal == null) {
            throw new ErrorException("所选印章不存在,请重新选择");
        }
        if (!Objects.equals(seal.getStatus(), 1)) {
            throw new ErrorException("印章“" + seal.getName() + "”已停用,请重新选择");
        }
        if (StringUtils.isBlank(seal.getAddress())) {
            throw new ErrorException("印章“" + seal.getName() + "”未上传图片,请重新选择");
        }
        return seal;
    }
    /**
     * 按图片内容识别印章格式,兼容历史数据中扩展名被保存为“_png”等非标准文件名的情况。
     */
    private PictureRenderData createSealPicture(String sealUrl) {
        Path sealPath = Paths.get(imgUrl, sealUrl);
        if (!Files.isRegularFile(sealPath)) {
            throw new ErrorException("印章图片文件不存在,请重新上传印章图片");
        }
        try {
            byte[] imageBytes = Files.readAllBytes(sealPath);
            PictureType pictureType = PictureType.suggestFileType(imageBytes);
            return Pictures.ofBytes(imageBytes, pictureType).create();
        } catch (Exception e) {
            log.error("读取印章图片失败: {}", sealPath, e);
            throw new ErrorException("印章图片无法识别,请重新上传 jpg 或 png 图片");
        }
    }
    @Override
    public int wordInsertUrl(Map<String, Object> map, String url) {
        XWPFTemplate template = XWPFTemplate.compile(url).render(map);
        Configure configure = Configure.builder()
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(url, configure).render(map);
        try {
            template.writeAndClose(Files.newOutputStream(Paths.get(url)));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 1;
    }
    private int wordInsertApprovalMarks(Map<String, Object> map, String url) {
        Configure configure = Configure.builder()
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler(
                        new HashSet<>(Arrays.asList("seal1", "seal2"))))
                .build();
        XWPFTemplate template = XWPFTemplate.compile(url, configure).render(map);
        try {
            template.writeAndClose(Files.newOutputStream(Paths.get(url)));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 1;
    }
    private String getPrimaryReportUrl(InsReport report) {
        String reportUrl = StringUtils.isNotBlank(report.getUrlS()) ? report.getUrlS() : report.getUrl();
        if (StringUtils.isBlank(reportUrl)) {
            throw new ErrorException("检测报告文件不存在");
        }
        return reportUrl;
    }
    private List<String> getApprovalReportUrls(InsReport report) {
        return Arrays.asList(getPrimaryReportUrl(report), report.getInspectionUrl()).stream()
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.toList());
    }
    private void writeMarksToReportFiles(InsReport report, Map<String, Object> marks) {
        for (String reportUrl : getApprovalReportUrls(report)) {
            wordInsertUrl(marks, reportUrl.replace("/word", wordUrl));
        }
    }
    private void writeApprovalMarksToReportFiles(InsReport report, Map<String, Object> marks) {
        for (String reportUrl : getApprovalReportUrls(report)) {
            wordInsertApprovalMarks(marks, reportUrl.replace("/word", wordUrl));
        }
    }
@@ -552,6 +745,53 @@
            e.printStackTrace();
        }
        return "/word/zip/output.zip";
    }
    @Override
    public void download(Integer id, Integer reportType, Integer createOrderUser, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("报告不能为空");
        }
        reportType = reportType == null ? DETECTION_REPORT : reportType;
        if (!Objects.equals(reportType, DETECTION_REPORT) && !Objects.equals(reportType, INSPECTION_REPORT)) {
            throw new ErrorException("报告类型不正确");
        }
        InsReport report = insReportMapper.selectDownloadableReport(id, createOrderUser);
        if (report == null) {
            throw new ErrorException("报告不存在、尚未生成或无权下载");
        }
        String reportUrl = Objects.equals(reportType, INSPECTION_REPORT)
                ? report.getInspectionUrl() : getPrimaryReportUrl(report);
        if (StringUtils.isBlank(reportUrl) || !reportUrl.replace('\\', '/').startsWith("/word/")) {
            throw new ErrorException((Objects.equals(reportType, INSPECTION_REPORT) ? "检验报告" : "检测报告")
                    + "文件不存在,请联系管理员");
        }
        Path root = Paths.get(wordUrl).toAbsolutePath().normalize();
        Path file = root.resolve(reportUrl.replace('\\', '/').substring("/word/".length())).normalize();
        if (!file.startsWith(root) || !Files.isRegularFile(file)) {
            throw new ErrorException("报告文件不存在,请联系管理员");
        }
        try {
            String filename = file.getFileName().toString();
            String encodedName = URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
            response.setHeader("download-filename", encodedName);
            response.setContentLengthLong(Files.size(file));
            try (InputStream inputStream = Files.newInputStream(file);
                 OutputStream outputStream = response.getOutputStream()) {
                byte[] buffer = new byte[8192];
                int length;
                while ((length = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, length);
                }
                outputStream.flush();
            }
        } catch (IOException e) {
            log.error("下载报告失败,reportId={}", id, e);
            throw new ErrorException("报告下载失败");
        }
    }
    //批量上传
@@ -669,16 +909,17 @@
    }
    @Override
    public void wordToPdf(String path, String sealUrl, boolean isRawMater) {
    public void wordToPdf(String path, String maSealUrl, String seamSealUrl, boolean isRawMater) {
        try {
            wordToPdf(path, path.replace(".docx", ".pdf"), sealUrl, isRawMater);
            wordToPdf(path, path.replace(".docx", ".pdf"), maSealUrl, seamSealUrl, isRawMater);
        } catch (Exception e) {
            throw new ErrorException("转换失败");
        }
    }
    public String wordToPdf(String wordPath, String pdfPath, String sealUrl, boolean isRawMater) {
    public String wordToPdf(String wordPath, String pdfPath, String maSealUrl,
                            String seamSealUrl, boolean isRawMater) {
        FileOutputStream os = null;
        try {
            //凭证 不然切换后有水印
@@ -689,27 +930,31 @@
                log.info("License验证不通过...");
                return null;
            }
            //生成一个空的PDF文件
            File file;
            //判断是否是进厂报告
            if (!isRawMater) {
                file = new File(pdfPath.replace(".pdf", "-1.pdf"));
            } else {
                file = new File(pdfPath.replace(".pdf", ".pdf"));
            }
            String effectiveSeamSealUrl = isRawMater ? null : seamSealUrl;
            boolean addPdfMarks = StringUtils.isNotBlank(maSealUrl)
                    || StringUtils.isNotBlank(effectiveSeamSealUrl);
            String sourcePdfPath = addPdfMarks ? pdfPath.replace(".pdf", "-stamp-source.pdf") : pdfPath;
            File file = new File(sourcePdfPath);
            os = new FileOutputStream(file);
            //要转换的word文件
            com.aspose.words.Document doc = new com.aspose.words.Document(wordPath);
            doc.save(os, SaveFormat.PDF);
            os.close();
            os = null;
            //添加骑缝章
            if (!isRawMater) {
                stamperCheckMarkPDF(pdfPath.replace(".pdf", "-1.pdf"), pdfPath, imgUrl + "/" + sealUrl);
            // MA章固定在第1页左上角;骑缝章按页切片固定在右侧中部。
            if (addPdfMarks) {
                stamperReportMarksPDF(
                        sourcePdfPath,
                        pdfPath,
                        StringUtils.isBlank(maSealUrl) ? null : Paths.get(imgUrl, maSealUrl).toString(),
                        StringUtils.isBlank(effectiveSeamSealUrl)
                                ? null : Paths.get(imgUrl, effectiveSeamSealUrl).toString());
            }
            return new File(pdfPath).getName();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("生成盖章PDF失败: {}", wordPath, e);
            throw new ErrorException("报告转PDF或盖章失败");
        } finally {
            if (os != null) {
                try {
@@ -719,7 +964,6 @@
                }
            }
        }
        return null;
    }
    /**
@@ -744,7 +988,8 @@
                subImg = img.getSubimage(i * sw, 0, sw, h);
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ImageIO.write(subImg, Path.substring(Path.lastIndexOf('.') + 1), out);
            // 统一编码为 PNG,兼容历史“_png”等没有标准扩展名的图片文件。
            ImageIO.write(subImg, "png", out);
            nImage[i] = com.itextpdf.text.Image.getInstance(out.toByteArray());
        }
@@ -752,37 +997,51 @@
    }
    /**
     * 盖骑缝章
     * 按参考报告盖 PDF 标志章:MA章位于第1页左上角,骑缝章位于每页右侧中部。
     *
     * @param infilePath  原PDF路径
     * @param outFilePath 输出PDF路径
     */
    public static void stamperCheckMarkPDF(String infilePath, String outFilePath, String picPath) throws IOException, DocumentException {
    public static void stamperReportMarksPDF(String infilePath, String outFilePath,
                                             String maPicPath, String seamPicPath)
            throws IOException, DocumentException {
        PdfReader reader = new PdfReader(infilePath);//选择需要印章的pdf
        PdfStamper stamp = new PdfStamper(reader, new FileOutputStream(outFilePath));//加完印章后的pdf
        com.itextpdf.text.Rectangle pageSize = reader.getPageSize(1);//获得第一页
        float height = pageSize.getHeight();
        float width = pageSize.getWidth();
        int nums = reader.getNumberOfPages();
        com.itextpdf.text.Image[] nImage = slicingImages(picPath, nums);//生成骑缝章切割图片
        boolean hasMaSeal = maPicPath != null && !maPicPath.trim().isEmpty();
        boolean hasSeamSeal = seamPicPath != null && !seamPicPath.trim().isEmpty();
        com.itextpdf.text.Image[] seamImages = hasSeamSeal ? slicingImages(seamPicPath, nums) : null;
        for (int n = 1; n <= nums; n++) {
            com.itextpdf.text.Rectangle pageSize = reader.getPageSize(n);
            float height = pageSize.getHeight();
            float width = pageSize.getWidth();
            PdfContentByte over = stamp.getOverContent(n);//设置在第几页打印印章
            com.itextpdf.text.Image img = nImage[n - 1];//选择图片
            float newHeight = 100f;
            float newWidth = img.getWidth() / (img.getHeight() / 100);
            img.scaleAbsolute(newWidth, newHeight);//控制图片大小
            img.setAbsolutePosition(width - newWidth, height / 2 - newHeight / 2);//控制图片位置
            over.addImage(img);
            if (n == 1 && hasMaSeal) {
                com.itextpdf.text.Image maImage = com.itextpdf.text.Image.getInstance(maPicPath);
                maImage.scaleToFit(100f, 90f);
                maImage.setAbsolutePosition(30f, height - maImage.getScaledHeight() - 30f);
                over.addImage(maImage);
            }
            if (hasSeamSeal) {
                com.itextpdf.text.Image seamImage = seamImages[n - 1];
                float newHeight = 100f;
                float newWidth = seamImage.getWidth() / (seamImage.getHeight() / newHeight);
                seamImage.scaleAbsolute(newWidth, newHeight);
                seamImage.setAbsolutePosition(width - newWidth, height / 2 - newHeight / 2);
                over.addImage(seamImage);
            }
        }
        stamp.close();
        reader.close();
    }
    @Transactional(rollbackFor = Exception.class)
    public void isRawMaterial(InsOrder insOrder) {
    @Override
    @Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED)
    public void isRawMaterial(InsOrder insOrder,Boolean registerInsResults,Boolean hasExemption) {
        IfsInventoryQuantity one = ifsInventoryQuantityMapper.selectOne(new LambdaQueryWrapper<IfsInventoryQuantity>()
                .eq(IfsInventoryQuantity::getId, insOrder.getIfsInventoryId()));
        if (Objects.isNull(one)) {
@@ -796,9 +1055,26 @@
        // 判断是否有不合格
        Long unqualifiedCount = getUnqualifiedCount(insOrder);
        if (count.equals(0L) && unqualifiedCount.equals(0L) && one.getIsFinish().equals(0) && one.getIsSource().equals(1)) {
        if (count.equals(0L) && unqualifiedCount.equals(0L) && one.getIsFinish().equals(0)
                && one.getIsSource().equals(1) && Boolean.TRUE.equals(registerInsResults)) {
            // 原材料移库
            toLocation = this.moveRawMaterial(one);
            //如果是拆分的订单,则把拆分的所有批次都移库
            if(one.getIsSplitOrder()==1 && !hasExemption){
                //查询拆分订单详情
                List<IfsInventoryQuantityVO> ifsInventoryQuantityVOS = ifsInventoryQuantityMapper.selectSplitOrderList(one.getPartNo(), one.getLineNo(), one.getReleaseNo(), one.getReceiptNo(), one.getOrderNo());
                for (IfsInventoryQuantityVO vo : ifsInventoryQuantityVOS) {
                    //如果有委托单信息且合格,走登记后移库;如果没有,则走免检;不合格不做处理
                    if(Objects.isNull(vo.getInsOrderId())){
                        rawMaterialOrderService.rawOrderRelease(vo.getId(), vo.getPartDesc());
                    }else if(Objects.equals(vo.getInsOrderId(),insOrder.getId())){
                        toLocation = insOrderService.moveRawMaterial(vo);
                    }else if(Objects.nonNull(vo.getInsResult()) && 1 == vo.getInsResult()){
                        toLocation = insOrderService.moveRawMaterial(vo);
                    }
                }
            }else{
                toLocation = insOrderService.moveRawMaterial(one);
            }
        }
        // 判断结束状态修改合格状态
@@ -886,75 +1162,6 @@
    }
    /**
     * ifs移库操作
     * @param one
     * @return
     */
    @Override
    public String moveRawMaterial(IfsInventoryQuantity one) {
        String toLocation;
        // 登记采购检验结果STD
        if (one.getIsRegister().equals(0)) {
            Map<String, Object> resultMap = new HashMap<>();
            List<Map<String, Object>> resultList = new ArrayList<>();
            Map<String, Object> map = new HashMap<>();
            map.put("ORDER_NO", one.getOrderNo()); // 采购订单号
            map.put("LINE_NO", one.getLineNo()); // 行号
            map.put("RELEASE_NO", one.getReleaseNo()); // 下达号
            map.put("RECEIPT_NO", one.getReceiptNo()); // 接收号
            map.put("PURCH_QTY", one.getQtyToInspect()); // 要检验的采购数量
            resultList.add(map);
            resultMap.put("RECORD_ID", UUID.randomUUID().toString());
            resultMap.put("SYSCODE", "LIMS");
            resultMap.put("SYSMODEL", "登记采购检验结果");
            resultMap.put("BATCH_INFO", resultList);
            Result result = ifsApiUtils.getProcurementResults(JSONUtil.toJsonStr(resultMap));
            if (result.getCode() != 200) {
                throw new ErrorException("IFS登记采购检验结果失败: " + result.getMessage());
            }
        }
        insOrderService.updateIfsInventoryQuantity(one.getId());
        /**
         * TODO 后续需要调用IFS的接口 移入的库位号 toLocation
         */
        // 检验后移库
        toLocation = "1301";
        Map<String, Object> moveResultMap = new HashMap<>();
        List<Map<String, Object>> moveResultList = new ArrayList<>();
        Map<String, Object> moveMap = new HashMap<>();
        moveMap.put("ORDER_NO", one.getOrderNo()); // 采购订单号
        moveMap.put("LINE_NO", one.getLineNo());
        moveMap.put("RELEASE_NO", one.getReleaseNo());
        moveMap.put("RECEIPT_NO", one.getReceiptNo());
        moveMap.put("PART_NO", one.getPartNo());
        moveMap.put("QTY", one.getQtyArrived());
        moveMap.put("LOCATION_NO", one.getLocationNo());
        moveMap.put("TO_LOCATION_NO", toLocation);
        moveMap.put("LOT_BATCH_NO", one.getLotBatchNo());
        moveMap.put("SERIAL_NO", one.getSerialNo());
        moveMap.put("WAIV_DEV_REJ_NO", one.getWaivDevRejNo());
        moveMap.put("ENG_CHG_LEVEL", one.getEngChgLevel());
        moveMap.put("ACTIVITY_SEQ", one.getActivitySeq());
        moveResultList.add(moveMap);
        moveResultMap.put("RECORD_ID", UUID.randomUUID().toString());
        moveResultMap.put("SYSCODE", "LIMS");
        moveResultMap.put("SYSMODEL", "检验后移库");
        moveResultMap.put("BATCH_INFO", moveResultList);
        Result result1 = ifsApiUtils.moveReceipt(JSONUtil.toJsonStr(moveResultMap));
        // 如果有必须为零件指定批号报错需要重新提交移库信息去指定批号
        if (result1.getCode() != 200) {
            String message = result1.getMessage();
            if (message.contains("必须为零件") && message.contains("指定批号")) {
                updaeBatch(one, toLocation);
            } else {
                throw new ErrorException("IFS检验后移库失败: " + result1.getMessage());
            }
        }
        return toLocation;
    }
    /**
     * 退回到检验任务
     * @param id
     * @return
@@ -963,20 +1170,22 @@
    @Transactional(rollbackFor = Exception.class)
    public boolean sendBackTask(Integer id) {
        InsReport report = insReportMapper.selectById(id);
        // 根据订单查询试验室
        String laboratory = insOrderMapper.selectLaboratoryByOrderId(report.getInsOrderId());
        if (report == null || report.getInsOrderId() == null) {
            throw new ErrorException("报告不存在或未关联检验单");
        }
        // 修改订单状态
        // 报告由订单下各试验室共同形成;退回时全部恢复为待复核,避免只随机退回一个试验室。
        insOrderStateMapper.update(null, Wrappers.<InsOrderState>lambdaUpdate()
                .eq(InsOrderState::getInsOrderId, report.getInsOrderId())
                .eq(InsOrderState::getLaboratory, laboratory)
                .set(InsOrderState::getInsState, 4));
        Integer insSampleId = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
        InsSampleUser sampleUser = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
                .eq(InsSampleUser::getInsSampleId, report.getInsOrderId())
                .orderByDesc(InsSampleUser::getId)
                .last("limit 1")).getId();
        insSampleUserMapper.deleteById(insSampleId);
                .last("limit 1"));
        if (sampleUser != null) {
            insSampleUserMapper.deleteById(sampleUser.getId());
        }
        // 修改报告行为未显示
        insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
@@ -986,6 +1195,46 @@
        return false;
    }
    /**
     * 报告报表导出
     * @param dto
     * @param response
     */
    @Override
    public void reportAllExport(ReportPageDto dto, HttpServletResponse response) throws UnsupportedEncodingException {
        Integer createOrderUser = dto.getCreateOrderUser();
        String queryStatus = dto.getQueryStatus();
        dto.setQueryStatus(null);
        dto.setCreateOrderUser(null);
        List<InsReportExport> insReportExports = insReportMapper.reportAllExport(QueryWrappers.queryWrappers(dto),
                SecurityUtils.getUserId().intValue(),
                queryStatus,
                createOrderUser);
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("UTF-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("报告报表导出", "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        try {
            //新建ExcelWriter
            ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
            //获取sheet0对象
            WriteSheet mainSheet = EasyExcel.writerSheet(0, "报告报表导出").head(InsReportExport.class).build();
            //向sheet0写入数据 传入空list这样只导出表头
            excelWriter.write(insReportExports, mainSheet);
            //关闭流
            excelWriter.finish();
        } catch (IOException e) {
            throw new RuntimeException("导出失败");
        }
    }
    /**
     * 先修改采购订单批次号, 后进行移库操作