liding
9 小时以前 ed56ae711e18c6f7624dac633c4af3ecfcf09520
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -45,6 +45,7 @@
import com.ruoyi.inspect.pojo.*;
import com.ruoyi.inspect.service.*;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.vo.InsOrderPlanTaskSwitchVo;
import com.ruoyi.inspect.vo.InsOrderPlanVO;
import com.ruoyi.inspect.vo.InsSampleUserVO;
@@ -81,6 +82,7 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
 * 检验任务-业务实现层
@@ -90,6 +92,7 @@
    private static final int DETECTION_REPORT = 0;
    private static final int INSPECTION_REPORT = 1;
    private static final String DIRECT_TEMPLATE_LABORATORY = "新聚";
    @Resource
    private InsSampleMapper insSampleMapper;
@@ -153,6 +156,8 @@
    private InsOrderStandardTemplateService insOrderStandardTemplateService;
    @Resource
    private InsOrderDeviceRecordMapper insOrderDeviceRecordMapper;
    @Resource
    private InsOrderTemplateDeviceMapper insOrderTemplateDeviceMapper;
    @Resource
    private InsOrderDeviceRecordService insOrderDeviceRecordService;
    @Resource
@@ -221,14 +226,8 @@
    @Override
    public Map<String, Object> doInsOrder(Integer id, String laboratory) {
        InsOrder insOrder = new InsOrder();
        insOrder.setId(id);
        ensureDirectTemplateOrderState(id);
        InsOrder order = insOrderMapper.selectById(id);
        if (BeanUtil.isEmpty(order.getInsTime())) {
            insOrder.setInsTime(LocalDateTime.now());
            insOrderMapper.updateById(insOrder);
            insOrderStateMapper.update(null, Wrappers.<InsOrderState>lambdaUpdate().eq(InsOrderState::getInsOrderId, id).eq(InsOrderState::getLaboratory, laboratory).set(InsOrderState::getInsTime, LocalDateTime.now()).set(InsOrderState::getInsState, 1));
        }
        Map<String, Object> map = insOrderService.getInsOrderAndSample(id, laboratory);
        List<SampleProductDto> list = JSON.parseArray(JSON.toJSONString(map.get("sampleProduct")), SampleProductDto.class);
        map.put("sampleProduct", list);
@@ -267,60 +266,109 @@
                insProducts = insSampleMapper.getInsProduct6(dto.getId(), dto.getLaboratory(), dto.getRawMaterialTag());
                break;
        }
        // 查询订单Id
        InsOrder order = insOrderMapper.selectFirstSubmit(dto.getId());
        if (order == null) {
            return BeanUtil.isEmpty(insProducts) ? null : insProducts;
        }
        insProducts = mergeDirectTemplateSnapshots(order, dto.getId(), dto.getLaboratory(), insProducts);
        if (BeanUtil.isEmpty(insProducts)) {
            return null;
        }
        // 查询订单Id
        InsOrder order = insOrderMapper.selectFirstSubmit(dto.getId());
        getTemplateThing(order, insProducts);
        getTemplateThing(order, insProducts, dto.getLaboratory());
        return insProducts;
    }
    /**
     * 直绑模板订单的页签来源是订单模板快照,而不是已生成的检验项。
     * <p>
     * 仍保留每个模板下已有的检验项记录;如果某个模板没有“检验项”标记,补一条仅用于
     * 承载模板内容的返回对象,使前端也能展示、填写该模板。
     */
    private List<InsProduct> mergeDirectTemplateSnapshots(InsOrder order, Integer sampleId,
                                                          String laboratory, List<InsProduct> products) {
        List<InsProduct> source = products == null ? new ArrayList<>() : products;
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, order.getId())
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        if (snapshots.isEmpty()) {
            return source;
        }
        InsSample sample = insSampleMapper.selectById(sampleId);
        boolean directTemplateOrder = source.stream()
                .anyMatch(product -> product.getTemplateRowIndex() != null)
                || isDirectTemplateSample(order, sample);
        if (!directTemplateOrder) {
            return source;
        }
        Map<Integer, List<InsProduct>> productsByTemplate = source.stream()
                .filter(product -> product.getTemplateId() != null)
                .collect(Collectors.groupingBy(InsProduct::getTemplateId, LinkedHashMap::new, Collectors.toList()));
        List<InsProduct> result = new ArrayList<>();
        for (InsOrderStandardTemplate snapshot : snapshots) {
            List<InsProduct> templateProducts = productsByTemplate.remove(snapshot.getTemplateId());
            if (CollectionUtils.isNotEmpty(templateProducts)) {
                templateProducts.forEach(product -> product.setTemplateSort(snapshot.getSort()));
                result.addAll(templateProducts);
                continue;
            }
            InsProduct templateCarrier = new InsProduct();
            templateCarrier.setInsSampleId(sampleId);
            templateCarrier.setTemplateId(snapshot.getTemplateId());
            templateCarrier.setSonLaboratory(laboratory);
            templateCarrier.setState(1);
            templateCarrier.setSort(snapshot.getSort());
            templateCarrier.setTemplateSort(snapshot.getSort());
            result.add(templateCarrier);
        }
        // 兼容历史异常数据:订单快照以外的检验项仍然返回,但放在快照页签之后。
        for (List<InsProduct> remaining : productsByTemplate.values()) {
            result.addAll(remaining);
        }
        source.stream().filter(product -> product.getTemplateId() == null).forEach(result::add);
        return result;
    }
    private boolean isDirectTemplateSample(InsOrder order, InsSample sample) {
        return sample != null
                && Objects.equals(sample.getInsOrderId(), order.getId())
                && sample.getStandardMethodListId() == null
                && StringUtils.isBlank(sample.getFactory())
                && StringUtils.isBlank(sample.getSampleType())
                && StringUtils.isBlank(sample.getModel())
                && StringUtils.isBlank(order.getFactory())
                && StringUtils.isBlank(order.getSampleType());
    }
    @Override
    public Map<String,Object> checkSubmitPlan(Integer orderId, String laboratory, Integer reportType) {
    public Map<String, Object> checkSubmitPlan(Integer orderId, String laboratory) {
        Map<String, Object> map = new HashMap<>();
        List<String> collect = new ArrayList<>();
        if (Objects.equals(DETECTION_REPORT, reportType)) {
        if (isDirectTemplateOrder(orderId)) {
            List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                    Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                            .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                            .orderByAsc(InsOrderStandardTemplate::getSort,
                                    InsOrderStandardTemplate::getId));
            for (InsOrderStandardTemplate snapshot : snapshots) {
                String templateName = StringUtils.defaultIfBlank(snapshot.getName(),
                        "模板ID " + snapshot.getTemplateId());
                if (snapshot.getDetectionTime() == null) {
                    collect.add("请选择“" + templateName + "”的检测时间");
                }
                if (StringUtils.isBlank(snapshot.getDetectionPlace())) {
                    collect.add("请填写“" + templateName + "”的检测地点");
                }
            }
            map.put("errorMsg", collect);
            map.put("missingExportItems", getMissingTemplateExportValues(orderId));
            map.put("unInsOrderCount", 0L);
            return map;
        }
        List<InsSample> insSamples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery().eq(InsSample::getInsOrderId, orderId).select(InsSample::getId));
        List<Integer> ids = insSamples.stream().map(a -> a.getId()).collect(Collectors.toList());
        List<InsProduct> insProducts = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                .in(InsProduct::getInsSampleId, ids)
                .eq(InsProduct::getSonLaboratory, laboratory)
                .eq(InsProduct::getState, 1)
                .eq(InsProduct::getInsResult, 0));
        // 过滤判断是有复测的值是合格的
        List<InsProduct> productList = insProducts.stream().filter(insProduct -> {
            // 查询不合格复测
            Long count = insUnqualifiedRetestProductMapper.selectCount(Wrappers.<InsUnqualifiedRetestProduct>lambdaQuery()
                    .eq(InsUnqualifiedRetestProduct::getInsProductId, insProduct.getId())
                    .ne(InsUnqualifiedRetestProduct::getInsResult, 0));
            if (count != 2) {
                return true;
            }
            return false;
        }).collect(Collectors.toList());
        if (productList.size() > 0) {
            collect = productList.stream().map(insProduct -> {
                return insProduct.getInspectionItem() + "-" + insProduct.getInspectionItemSubclass();
            }).collect(Collectors.toList());
        }
        //查询ifs拆分订单是否有已下单但是未检完的单子
        long count = 0L;
        InsOrder insOrder = insOrderMapper.selectById(orderId);
        if(Objects.nonNull(insOrder.getIfsInventoryId())){
            IfsInventoryQuantity one = ifsInventoryQuantityMapper.selectById(insOrder.getIfsInventoryId());
            //过滤出不合格或未提交的单子
            count = ifsInventoryQuantityMapper.selectSplitOrderList(one.getPartNo(),one.getLineNo(),one.getReleaseNo(),one.getReceiptNo(),one.getOrderNo())
                    .stream()
                    .filter(f->(Objects.nonNull(f.getInsOrderId()) && !Objects.equals(f.getInsOrderId(),orderId)) && (Objects.isNull(f.getInsResult()) || 0==f.getInsResult())).count();
        }
        map.put("errorMsg",collect);
        map.put("unInsOrderCount",count);
        map.put("errorMsg", collect);
        map.put("unInsOrderCount", 0L);
        return map;
    }
@@ -556,6 +604,275 @@
        });
    }
    /**
     * 新聚订单的外层 key 是 templateId,不是 ins_product.id;因此单独按模板快照保存。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveTemplateContext(SaveTemplateContextDto dto) {
        if (dto.getOrderId() == null || dto.getSampleId() == null || dto.getTemplateId() == null) {
            throw new ErrorException("订单、样品和模板不能为空");
        }
        InsSample sample = insSampleMapper.selectById(dto.getSampleId());
        if (sample == null || !Objects.equals(sample.getInsOrderId(), dto.getOrderId())) {
            throw new ErrorException("样品不属于当前检验单");
        }
        InsOrderStandardTemplate snapshot = insOrderStandardTemplateService.getOne(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, dto.getOrderId())
                        .eq(InsOrderStandardTemplate::getTemplateId, dto.getTemplateId())
                        .last("limit 1"));
        if (snapshot == null) {
            throw new ErrorException("未找到当前订单的原始记录模板快照");
        }
        ensureDirectTemplateOrderState(dto.getOrderId());
        List<InsProduct> products = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                .eq(InsProduct::getInsSampleId, dto.getSampleId())
                .eq(InsProduct::getTemplateId, dto.getTemplateId())
                .isNotNull(InsProduct::getTemplateRowIndex)
                .eq(InsProduct::getState, 1));
        JSONObject values = JSON.parseObject(JSON.toJSONString(dto.getValues() == null
                ? Collections.emptyMap() : dto.getValues()));
        // 检验期间页面展示的是标准维护中的最新模板,保存校验必须使用同一份模板,
        // 避免模板新增“导出值”批注后仍按下单时的旧快照误报未配置。
        String currentTemplateThing = getCurrentTemplateThing(snapshot);
        List<String> exportValueCoordinates = getExportValueCoordinates(currentTemplateThing).values().stream()
                .flatMap(Collection::stream)
                .distinct()
                .collect(Collectors.toList());
        if (exportValueCoordinates.isEmpty()) {
            throw new ErrorException("模板必须至少配置一个导出值批注");
        }
        // 导出值按模板配置,不再要求每个检验项区块分别配置。一个模板取第一个导出值作为结果。
        String exportValueCoordinate = exportValueCoordinates.get(0);
        JSONObject exportValueCell = values.getJSONObject(exportValueCoordinate);
        String exportValue = exportValueCell == null || exportValueCell.get("v") == null
                ? "" : String.valueOf(exportValueCell.get("v"));
        Integer userId = SecurityUtils.getUserId().intValue();
        for (InsProduct product : products) {
            InsProduct update = new InsProduct();
            update.setId(product.getId());
            update.setLastValue(exportValue);
            update.setUpdateUser(userId);
            insProductMapper.updateById(update);
            insProductUserMapper.insert(new InsProductUser(null, userId, LocalDateTime.now(), product.getId()));
        }
        snapshot.setRecordValues(values.toJSONString());
        insOrderStandardTemplateService.updateById(snapshot);
        if (hasSavedTemplateValue(values)) {
            markInspectionStarted(dto.getOrderId(), products);
        }
        Long unfinished = insProductMapper.selectCount(Wrappers.<InsProduct>lambdaQuery()
                .eq(InsProduct::getInsSampleId, dto.getSampleId())
                .isNotNull(InsProduct::getTemplateRowIndex)
                .eq(InsProduct::getState, 1)
                .and(w -> w.isNull(InsProduct::getLastValue).or().eq(InsProduct::getLastValue, "")));
        sample.setInsState(unfinished == 0 ? 2 : 1);
        insSampleMapper.updateById(sample);
    }
    /**
     * 第一次成功保存任意非空模板值时,记录真实检验开始时间并将任务从待检改为在检。
     */
    private void markInspectionStarted(Integer orderId, List<InsProduct> products) {
        LocalDateTime now = LocalDateTime.now();
        insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                .eq(InsOrder::getId, orderId)
                .isNull(InsOrder::getFirstInspectDate)
                .set(InsOrder::getFirstInspectDate, now)
                // 保留原字段供检验任务列表中的“检验开始时间”继续展示。
                .set(InsOrder::getInsTime, now));
        Set<String> laboratories = products.stream()
                .map(InsProduct::getSonLaboratory)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toSet());
        if (laboratories.isEmpty() && isDirectTemplateOrder(orderId)) {
            laboratories.add(DIRECT_TEMPLATE_LABORATORY);
        }
        if (!laboratories.isEmpty()) {
            insOrderStateMapper.update(null, Wrappers.<InsOrderState>lambdaUpdate()
                    .eq(InsOrderState::getInsOrderId, orderId)
                    .in(InsOrderState::getLaboratory, laboratories)
                    .eq(InsOrderState::getInsState, 0)
                    .set(InsOrderState::getInsTime, now)
                    .set(InsOrderState::getInsState, 1));
        }
    }
    private boolean hasSavedTemplateValue(JSONObject values) {
        if (values == null || values.isEmpty()) {
            return false;
        }
        for (Object rawCell : values.values()) {
            JSONObject cell = rawCell instanceof JSONObject
                    ? (JSONObject) rawCell
                    : JSON.parseObject(JSON.toJSONString(rawCell));
            if (cell != null && hasMeaningfulValue(cell.get("v"))) {
                return true;
            }
        }
        return false;
    }
    private boolean hasMeaningfulValue(Object value) {
        if (value == null) {
            return false;
        }
        if (value instanceof CharSequence) {
            return StringUtils.isNotBlank(value.toString());
        }
        if (value instanceof Collection) {
            return ((Collection<?>) value).stream().anyMatch(this::hasMeaningfulValue);
        }
        if (value instanceof Map) {
            return ((Map<?, ?>) value).values().stream().anyMatch(this::hasMeaningfulValue);
        }
        return true;
    }
    private NavigableMap<Integer, List<String>> getExportValueCoordinates(String compressedThing) {
        if (StrUtil.isBlank(compressedThing)) {
            throw new ErrorException("原始记录模板为空");
        }
        String thing;
        try {
            thing = GZipUtil.uncompress(compressedThing);
        } catch (Exception ignored) {
            thing = compressedThing;
        }
        JSONArray cellData = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0).getJSONArray("celldata");
        NavigableMap<Integer, List<String>> result = new TreeMap<>();
        Integer inspectionResultColumn = null;
        for (Object item : cellData) {
            JSONObject cell = JSON.parseObject(JSON.toJSONString(item));
            Integer column = cell.getInteger("c");
            JSONObject value = cell.getJSONObject("v");
            String displayValue = value == null ? null : value.getString("v");
            if (inspectionResultColumn == null && "检验结果".equals(StringUtils.trim(displayValue))) {
                inspectionResultColumn = column;
            }
        }
        for (Object item : cellData) {
            JSONObject cell = JSON.parseObject(JSON.toJSONString(item));
            JSONObject value = cell.getJSONObject("v");
            JSONObject ps = value == null ? null : value.getJSONObject("ps");
            if (ps == null || !"导出值".equals(ps.getString("value"))) {
                continue;
            }
            Integer row = cell.getInteger("r");
            Integer column = cell.getInteger("c");
            if (row != null && column != null) {
                // “导出值”标在检测项目标题单元格时,真正导出的值是同一行“检验结果”列的单元格。
                String coordinate = row + "-" + column;
                String resultCoordinate = row + "-" + inspectionResultColumn;
                if (inspectionResultColumn != null && !Objects.equals(column, inspectionResultColumn)) {
                    coordinate = resultCoordinate;
                }
                result.computeIfAbsent(row, ignored -> new ArrayList<>()).add(coordinate);
            }
        }
        return result;
    }
    /**
     * 检验保存、提交校验读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。
     * 检验完成后的展示和报告仍由订单快照负责,不会随标准模板后续修改而变化。
     */
    private String getCurrentTemplateThing(InsOrderStandardTemplate snapshot) {
        if (snapshot != null && snapshot.getTemplateId() != null) {
            StandardTemplate currentTemplate = standardTemplateService.getById(snapshot.getTemplateId());
            if (currentTemplate != null && StrUtil.isNotBlank(currentTemplate.getThing())) {
                return currentTemplate.getThing();
            }
        }
        return snapshot == null ? null : snapshot.getThing();
    }
    /**
     * 直绑模板订单只校验模板中标记为“导出值”的单元格,不使用检验项状态或 last_value 判定。
     */
    private List<String> getMissingTemplateExportValues(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        List<String> missing = new ArrayList<>();
        if (snapshots.isEmpty()) {
            missing.add("当前订单未找到原始记录模板快照");
            return missing;
        }
        for (InsOrderStandardTemplate snapshot : snapshots) {
            Integer templateId = snapshot.getTemplateId();
            List<String> coordinates = getExportValueCoordinates(getCurrentTemplateThing(snapshot)).values().stream()
                    .flatMap(Collection::stream)
                    .distinct()
                    .collect(Collectors.toList());
            String templateName = StringUtils.defaultIfBlank(snapshot.getName(), "模板ID " + templateId);
            if (coordinates.isEmpty()) {
                missing.add(templateName + "(未配置导出值)");
                continue;
            }
            JSONObject recordValues = StrUtil.isBlank(snapshot.getRecordValues())
                    ? new JSONObject() : JSON.parseObject(snapshot.getRecordValues());
            for (String coordinate : coordinates) {
                JSONObject cell = recordValues.getJSONObject(coordinate);
                Object value = cell == null ? null : cell.get("v");
                if (value == null || StringUtils.isBlank(String.valueOf(value))) {
                    missing.add(templateName + "(" + coordinate + ")");
                }
            }
        }
        return missing;
    }
    @Override
    public List<Integer> getOrderDeviceIds(Integer orderId) {
        return insOrderDeviceRecordMapper.selectList(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                        .eq(InsOrderDeviceRecord::getInsOrderId, orderId))
                .stream().map(InsOrderDeviceRecord::getDeviceId).filter(Objects::nonNull).distinct()
                .collect(Collectors.toList());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrderDevices(SaveOrderDevicesDto dto) {
        if (dto.getOrderId() == null || insOrderMapper.selectById(dto.getOrderId()) == null) {
            throw new ErrorException("检验单不存在");
        }
        Set<Integer> deviceIds = dto.getDeviceIds() == null ? Collections.emptySet() : dto.getDeviceIds().stream()
                                                                                       .filter(Objects::nonNull).collect(Collectors.toSet());
        List<InsOrderDeviceRecord> oldRecords = insOrderDeviceRecordMapper.selectList(
                Wrappers.<InsOrderDeviceRecord>lambdaQuery().eq(InsOrderDeviceRecord::getInsOrderId, dto.getOrderId()));
        Set<Integer> oldIds = oldRecords.stream().map(InsOrderDeviceRecord::getDeviceId).collect(Collectors.toSet());
        if (!oldIds.isEmpty()) {
            insOrderDeviceRecordMapper.delete(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                    .eq(InsOrderDeviceRecord::getInsOrderId, dto.getOrderId())
                    .notIn(!deviceIds.isEmpty(), InsOrderDeviceRecord::getDeviceId, deviceIds));
        }
        InsOrder order = insOrderMapper.selectById(dto.getOrderId());
        User user = userMapper.selectById(SecurityUtils.getUserId().intValue());
        List<InsOrderDeviceRecord> additions = deviceIds.stream().filter(id -> !oldIds.contains(id)).map(id -> {
            InsOrderDeviceRecord record = new InsOrderDeviceRecord();
            record.setInsOrderId(dto.getOrderId());
            record.setDeviceId(id);
            record.setSampleCode(order.getEntrustCode());
            record.setUseBefore(1);
            record.setUseAfter(1);
            record.setUsePersonId(user.getId());
            record.setUsePerson(user.getName());
            return record;
        }).collect(Collectors.toList());
        if (!additions.isEmpty()) {
            insOrderDeviceRecordService.saveBatch(additions);
        }
    }
    private synchronized void addDeviceRecord(InsSample insSample, Integer userId) {
        InsOrder order = insOrderMapper.selectById(insSample.getInsOrderId());
        User user = userMapper.selectById(userId);
@@ -626,42 +943,59 @@
    /**
     * 查询模板内容
     *
     * @param order
     * @param insProducts
     */
    private void getTemplateThing(InsOrder order, List<InsProduct> insProducts) {
    private void getTemplateThing(InsOrder order, List<InsProduct> insProducts, String laboratory) {
        Set<Integer> set = new HashSet<>();
        // 查询订单状态判断是否是查历史模板
        if (order.getIsFirstSubmit() != null && order.getIsFirstSubmit().equals(1)) {
            InsOrderState insOrderState = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery()
                    .eq(InsOrderState::getInsOrderId, order.getId())
                    .last("limit 1"));
            if (insOrderState != null && (!insOrderState.getInsState().equals(3) || !insOrderState.getInsState().equals(5))) {
                for (InsProduct product : insProducts) {
                    if (product.getTemplateId() == null) {
                        product.setTemplate(new ArrayList<>());
        Map<Integer, String> recordValuesByTemplate = new HashMap<>();
        for (InsOrderStandardTemplate snapshot : insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, order.getId()))) {
            // record_values 在首次填写前允许为空;Collectors.toMap 不接受 null value。
            if (snapshot.getTemplateId() != null && snapshot.getRecordValues() != null) {
                recordValuesByTemplate.putIfAbsent(snapshot.getTemplateId(), snapshot.getRecordValues());
            }
        }
        boolean directTemplateOrder = insProducts.stream()
                .anyMatch(product -> product.getTemplateRowIndex() != null);
        Long snapshotCount = insOrderStandardTemplateService.count(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                .eq(InsOrderStandardTemplate::getInsOrderId, order.getId()));
        InsOrderState insOrderState = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, order.getId())
                .eq(StringUtils.isNotBlank(laboratory), InsOrderState::getLaboratory, laboratory)
                .last("limit 1"));
        Integer insState = insOrderState == null ? null : insOrderState.getInsState();
        boolean hasSnapshot = snapshotCount > 0 && (directTemplateOrder
                || Objects.equals(order.getIsFirstSubmit(), 1));
        // 待检验、检验中使用基础配置的最新模板;已检验及后续状态使用订单快照。
        boolean useSnapshot = hasSnapshot && insState != null && insState >= 2;
        if (useSnapshot) {
            for (InsProduct product : insProducts) {
                if (product.getTemplateId() == null) {
                    product.setTemplate(new ArrayList<>());
                    continue;
                }
                if (set.add(product.getTemplateId())) {
                    InsOrderStandardTemplate one = insOrderStandardTemplateService.getOne(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                            .eq(InsOrderStandardTemplate::getTemplateId, product.getTemplateId())
                            .eq(InsOrderStandardTemplate::getInsOrderId, order.getId()));
                    if (one == null || StrUtil.isEmpty(one.getThing())) {
                        // 快照不完整时允许后面的最新模板查询兜底。
                        set.remove(product.getTemplateId());
                        continue;
                    }
                    String thing = null;
                    if (product.getTemplateId() != null && set.add(product.getTemplateId())) {
                        // 查询历史模板
                        InsOrderStandardTemplate one = insOrderStandardTemplateService.getOne(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                                .eq(InsOrderStandardTemplate::getTemplateId, product.getTemplateId())
                                .eq(InsOrderStandardTemplate::getInsOrderId, order.getId()));
                        thing = one.getThing();
                        if (StrUtil.isNotEmpty(thing)) {
                            thing = GZipUtil.uncompress(thing);
                            JSONObject sheet = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0);
                            JSONObject config = sheet.getJSONObject("config");
                            List<JSONObject> cellData = JSON.parseArray(JSON.toJSONString(sheet.get("celldata")), JSONObject.class);
                            Map<String, Object> style = new HashMap<>();
                            style.put("rowlen", config.get("rowlen"));
                            style.put("columnlen", config.get("columnlen"));
                            product.setTemplate(cellData);
                            product.setStyle(style);
                            product.setTemplateName(one.getName());
                        }
                    }
                    String thing = GZipUtil.uncompress(one.getThing());
                    JSONObject sheet = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0);
                    JSONObject config = sheet.getJSONObject("config");
                    List<JSONObject> cellData = JSON.parseArray(JSON.toJSONString(sheet.get("celldata")), JSONObject.class);
                    Map<String, Object> style = new HashMap<>();
                    style.put("rowlen", config.get("rowlen"));
                    style.put("columnlen", config.get("columnlen"));
                    product.setTemplate(cellData);
                    product.setStyle(style);
                    product.setTemplateName(one.getName());
                }
            }
        }
@@ -685,11 +1019,13 @@
                product.setStyle(style);
                product.setTemplateName(standardTemplateService.getStandTempNameById(product.getTemplateId()));
            }
            product.setRecordValues(recordValuesByTemplate.get(product.getTemplateId()));
        }
    }
    /**
     * todo: 原始记录模板清除没有使用的检验项(暂时有bug无法使用)
     *
     * @param sheet
     * @param itemNameList
     */
@@ -762,6 +1098,7 @@
    /**
     * 坐标拼接
     *
     * @param r 横坐标
     * @param c 纵坐标
     * @return
@@ -774,19 +1111,12 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int submitPlan(Integer orderId, String laboratory, Integer verifyUser, String entrustCode, Boolean registerInsResults, Integer reportType) {
        if (!Objects.equals(DETECTION_REPORT, reportType) && !Objects.equals(INSPECTION_REPORT, reportType)) {
            throw new ErrorException("请选择报告类型");
        }
    public int submitPlan(Integer orderId, String laboratory, Integer verifyUser, String entrustCode, Boolean registerInsResults) {
        ensureDirectTemplateOrderState(orderId);
        int reportType = DETECTION_REPORT;
        InsOrder order = insOrderMapper.selectOne(Wrappers.<InsOrder>lambdaQuery()
                .eq(InsOrder::getId, orderId)
                .last("FOR UPDATE"));
        InsReport existingReport = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId));
        if (existingReport != null && existingReport.getReportType() != null
                && !Objects.equals(existingReport.getReportType(), reportType)) {
            throw new ErrorException("同一检验订单只能使用一种报告类型");
        }
        boolean isInspectionReport = Objects.equals(INSPECTION_REPORT, reportType);
        registerInsResults = isInspectionReport && Boolean.TRUE.equals(registerInsResults);
        // 1. 判断是否有重复编号, 有重复编号做提醒
@@ -799,38 +1129,50 @@
        }
        // 2. 判断该订单是否是第一次生产(后续报告生成只取第一次提交时间)
        if (!(order.getIsFirstSubmit() != null && order.getIsFirstSubmit().equals(1))) {
        boolean firstSubmit = !Objects.equals(order.getIsFirstSubmit(), 1);
        if (firstSubmit) {
            insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                    .eq(InsOrder::getId, orderId)
                    .set(InsOrder::getIsFirstSubmit, 1)
                    .set(InsOrder::getFirstSubmitDate, LocalDateTime.now()));
        }
        // 3. 判断是否有未检项
        // 3. 直绑模板仅校验“导出值”;普通订单沿用原检验项校验。
        List<InsSample> insSamples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery()
                .eq(InsSample::getInsOrderId, orderId).select(InsSample::getId));
        List<Integer> InsSampleIds = insSamples.stream().map(InsSample::getId).collect(Collectors.toList());
        List<InsProduct> insProducts = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                .in(InsProduct::getInsSampleId, InsSampleIds)
                .eq(InsProduct::getSonLaboratory, laboratory)
                .eq(InsProduct::getState, 1)
                .and(wrapper -> {
                    if (Objects.equals(reportType, DETECTION_REPORT)) {
                        wrapper.isNull(InsProduct::getLastValue).or().eq(InsProduct::getLastValue, "");
                    } else {
                        wrapper.isNull(InsProduct::getInsResult).or().eq(InsProduct::getInsResult, 2);
                    }
                })
                .ne(InsProduct::getIsBinding, 1));
        if (Objects.equals(reportType, INSPECTION_REPORT)) {
            insProducts.addAll(insProductMapper.selectFiberInsProduct(InsSampleIds, laboratory));
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        List<InsProduct> insProducts;
        if (directTemplateOrder) {
            List<String> missingExportValues = getMissingTemplateExportValues(orderId);
            if (!missingExportValues.isEmpty()) {
                throw new ErrorException("模板中的导出值不能为空:\n" + String.join("\n", missingExportValues));
            }
            insProducts = Collections.emptyList();
        } else {
            insProducts = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                    .in(InsProduct::getInsSampleId, InsSampleIds)
                    .eq(InsProduct::getSonLaboratory, laboratory)
                    .eq(InsProduct::getState, 1)
                    .and(wrapper -> {
                        if (Objects.equals(reportType, DETECTION_REPORT)) {
                            wrapper.isNull(InsProduct::getLastValue).or().eq(InsProduct::getLastValue, "");
                        } else {
                            wrapper.isNull(InsProduct::getInsResult).or().eq(InsProduct::getInsResult, 2);
                        }
                    })
                    .ne(InsProduct::getIsBinding, 1));
        }
        if (!directTemplateOrder && Objects.equals(reportType, INSPECTION_REPORT)) {
            List<InsProduct> insProducts1 = insProductMapper.selectFiberInsProduct(InsSampleIds, laboratory);
            insProducts.addAll(insProducts1);
        }
        if (insProducts.size() > 0) {
            String str = "";
            int count = 0;
            for (InsProduct product : insProducts) {
                count++;
                str +=  (count != 0 ? "\n" : "") + count + ":" +
                str += (count != 0 ? "\n" : "") + count + ":" +
                        product.getInspectionItemClass() + " " +
                        product.getInspectionItem() + " " +
                        product.getInspectionItemSubclass();
@@ -887,20 +1229,16 @@
            }
        });
        // 8.提交生成报告
        this.generateReport(orderId, reportType);
        // 8.首次提交时将本次检验使用的最新模板冻结为订单快照。
        Long snapshotCount = insOrderStandardTemplateService.count(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                .eq(InsOrderStandardTemplate::getInsOrderId, orderId));
        if (firstSubmit || snapshotCount == 0) {
            this.templateCopy(orderId, InsSampleIds);
        }
        // 9.添加临时pdf生成地址
        InsReport report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId));
        String tempUrlPdf = this.wordToPdfTemp(report.getUrl().replace("/word", wordUrl));
        report.setTempUrlPdf("/word/" + tempUrlPdf);
        insReportMapper.updateById(report);
        // 9. 检测报告仅在全部复核通过时生成,提交复核阶段不生成报告。
        // 10.原始记录模板复制(添加备份, 避免修改原始模板影响到已经完成的单子)
        this.templateCopy(orderId, InsSampleIds);
        // 11.成品抽样添加合格状态
        // 10.成品抽样添加合格状态
        // 判断是否有抽样信息
        if (isInspectionReport && order.getQuarterItemId() != null) {
            // 判断是否有不合格
@@ -908,7 +1246,7 @@
        }
        // 12.添加订单费用统计信息
        // 11.添加订单费用统计信息
        List<InsProduct> productList = insProductMapper.selectProductByOrderId(orderId);
        // 删除原本费用信息
        insOrderRatesService.remove(Wrappers.<InsOrderRates>lambdaQuery()
@@ -931,7 +1269,7 @@
        insOrderRatesService.saveBatch(orderRatesList);
        // 13.发送企业微信通知
        // 12.发送企业微信通知
        // 查询原材料
        IfsInventoryQuantity ifsInventoryQuantity = ifsInventoryQuantityMapper.selectById(order.getIfsInventoryId());
        // 查询样品信息
@@ -950,20 +1288,20 @@
            }
            //发送企业微信消息通知  提交复核
            try {
                WxCpUtils.inform(sendUserAccount, message, null);
//                WxCpUtils.inform(sendUserAccount, message, null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        // 14.ifs移库(原材料需要进行移库操作) --> 最后执行,因为失败无法回滚
        // 13.ifs移库(原材料需要进行移库操作) --> 最后执行,因为失败无法回滚
        if (ifsInventoryQuantity != null && isInspectionReport) {
            // 登记检验结果
            // 判断是否有不合格, 有不合格不能移库
            // todo: ifs移库
            insReportService.isRawMaterial(order,registerInsResults,false);
            insReportService.isRawMaterial(order, registerInsResults, false);
            // 15 判断当前样品是否为原材料, 原材料需要进行数据分析, 判断之前10条数据同一个供应商, 同一个检验项的偏差是否超过10%
            // 14 判断当前样品是否为原材料, 原材料需要进行数据分析, 判断之前10条数据同一个供应商, 同一个检验项的偏差是否超过10%
            // 查询ifs信息获取获取前10个供应商一样的, 检验项一样信息
            threadPoolTaskExecutor.execute(() -> {
                // 添加分析数据
@@ -971,7 +1309,7 @@
            });
        } else if (isInspectionReport) {
        } else if (isInspectionReport && !directTemplateOrder) {
            // 修改成品状态
            // 判断是否有不合格
            Long unqualifiedCount = insReportService.getUnqualifiedCount(order);
@@ -991,6 +1329,7 @@
    /**
     * *****添加分析数据******
     *
     * @param productList
     * @param ifsInventoryQuantity
     * @param order
@@ -1081,6 +1420,7 @@
    /**
     * *****计算偏差****
     *
     * @param data
     * @param targetStr
     * @return
@@ -1106,12 +1446,13 @@
    /**
     * *****计算偏差(要求值)****
     *
     * @param asked
     * @param targetStr
     * @return
     */
    public static double isDeviationOverTenPercentByAsked(String asked, String targetStr) {
        if(!isNumeric(asked)) return 0;
        if (!isNumeric(asked)) return 0;
        double average = Double.parseDouble(asked);
        double target = Double.parseDouble(targetStr);
        double deviationPercent = Math.abs(target - average) / average * 100;
@@ -1124,39 +1465,90 @@
    /**
     * ******原始记录模板复制*****
     *
     * @param orderId
     * @param ids
     */
    private void templateCopy(Integer orderId, List<Integer> ids) {
        // 删除原本模板
        insOrderStandardTemplateService.remove(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                .eq(InsOrderStandardTemplate::getInsOrderId, orderId));
        // 复制模板
        Set<Integer> set = new HashSet<>();
        // 保留模板页签顺序,并在首次提交复核时用检验期间采用的最新模板刷新快照。
        // 直绑模板订单必须以原订单快照为模板清单,不能只按 ins_product 重建:
        // 没有“检验项”标记的模板不会生成 ins_product,否则提交后会被错误删除。
        List<InsOrderStandardTemplate> oldSnapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        Map<Integer, Integer> templateSortMap = new HashMap<>();
        Map<Integer, String> templateRecordValuesMap = new HashMap<>();
        Map<Integer, String> templateTemperatureMap = new HashMap<>();
        Map<Integer, String> templateHumidityMap = new HashMap<>();
        Map<Integer, InsOrderStandardTemplate> oldSnapshotMap = new LinkedHashMap<>();
        int nextTemplateSort = 0;
        for (InsOrderStandardTemplate snapshot : oldSnapshots) {
            if (snapshot.getTemplateId() != null) {
                oldSnapshotMap.putIfAbsent(snapshot.getTemplateId(), snapshot);
                if (snapshot.getSort() != null) {
                    templateSortMap.putIfAbsent(snapshot.getTemplateId(), snapshot.getSort());
                    nextTemplateSort = Math.max(nextTemplateSort, snapshot.getSort() + 1);
                }
                if (snapshot.getRecordValues() != null) {
                    templateRecordValuesMap.putIfAbsent(snapshot.getTemplateId(), snapshot.getRecordValues());
                }
                if (snapshot.getTemperature() != null) {
                    templateTemperatureMap.putIfAbsent(snapshot.getTemplateId(), snapshot.getTemperature());
                }
                if (snapshot.getHumidity() != null) {
                    templateHumidityMap.putIfAbsent(snapshot.getTemplateId(), snapshot.getHumidity());
                }
            }
        }
        LinkedHashSet<Integer> templateIds = new LinkedHashSet<>();
        if (isDirectTemplateOrder(orderId)) {
            // 先加入订单原来绑定的全部模板,包括不生成检验项记录的模板。
            templateIds.addAll(oldSnapshotMap.keySet());
        }
        List<InsProduct> insProductList = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                .in(InsProduct::getInsSampleId, ids)
                .select(InsProduct::getTemplateId));
        insProductList.stream()
                .map(InsProduct::getTemplateId)
                .filter(Objects::nonNull)
                .forEach(templateIds::add);
        for (InsProduct product : insProductList) {
            // 查询模板id
            if (product.getTemplateId() != null && set.add(product.getTemplateId())) {
                StandardTemplate standardTemplate = standardTemplateService.getById(product.getTemplateId());
                if (standardTemplate != null) {
                    InsOrderStandardTemplate insOrderStandardTemplate = new InsOrderStandardTemplate();
                    insOrderStandardTemplate.setInsOrderId(orderId);
                    insOrderStandardTemplate.setTemplateId(standardTemplate.getId());
                    insOrderStandardTemplate.setNumber(standardTemplate.getNumber());
                    insOrderStandardTemplate.setName(standardTemplate.getName());
                    insOrderStandardTemplate.setThing(standardTemplate.getThing());
                    insOrderStandardTemplateService.save(insOrderStandardTemplate);
                }
        List<InsOrderStandardTemplate> refreshedSnapshots = new ArrayList<>();
        for (Integer templateId : templateIds) {
            StandardTemplate standardTemplate = standardTemplateService.getById(templateId);
            InsOrderStandardTemplate oldSnapshot = oldSnapshotMap.get(templateId);
            if (standardTemplate == null && oldSnapshot == null) {
                continue;
            }
            InsOrderStandardTemplate refreshed = new InsOrderStandardTemplate();
            refreshed.setInsOrderId(orderId);
            refreshed.setTemplateId(templateId);
            refreshed.setNumber(standardTemplate == null ? oldSnapshot.getNumber() : standardTemplate.getNumber());
            refreshed.setName(standardTemplate == null ? oldSnapshot.getName() : standardTemplate.getName());
            refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark());
            refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing());
            refreshed.setRecordValues(templateRecordValuesMap.get(templateId));
            refreshed.setTemperature(templateTemperatureMap.get(templateId));
            refreshed.setHumidity(templateHumidityMap.get(templateId));
            Integer templateSort = templateSortMap.get(templateId);
            refreshed.setSort(templateSort == null ? nextTemplateSort++ : templateSort);
            refreshedSnapshots.add(refreshed);
        }
        // 新快照已完整组装后再替换;事务失败时会整体回滚。
        insOrderStandardTemplateService.remove(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                .eq(InsOrderStandardTemplate::getInsOrderId, orderId));
        if (!refreshedSnapshots.isEmpty()) {
            insOrderStandardTemplateService.saveBatch(refreshedSnapshots);
        }
    }
    /**
     * 检验任务复核
     *
     * @param orderId
     * @param laboratory
     * @param type
@@ -1166,6 +1558,44 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int rawMaterialVerifyPlan(Integer orderId, String laboratory, Integer type, String tell, Integer userId) {
        VerifyPlanDto dto = new VerifyPlanDto();
        dto.setOrderId(orderId);
        dto.setLaboratory(laboratory);
        dto.setType(type);
        dto.setTell(tell);
        dto.setUserId(userId);
        return rawMaterialVerifyPlan(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int rawMaterialVerifyPlan(VerifyPlanDto dto) {
        Integer orderId = dto.getOrderId();
        String laboratory = dto.getLaboratory();
        Integer type = dto.getType();
        String tell = dto.getTell();
        Integer userId = dto.getUserId();
        if (orderId == null || laboratory == null || type == null) {
            throw new ErrorException("复核参数不完整");
        }
        ensureDirectTemplateOrderState(orderId);
        InsOrderState currentState = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, orderId)
                .eq(InsOrderState::getLaboratory, laboratory)
                .last("limit 1"));
        InsReport existingReport = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId)
                .last("limit 1"));
        if (currentState != null && Objects.equals(currentState.getInsState(), 5)
                && existingReport != null && StringUtils.isNotBlank(existingReport.getUrl())) {
            // 防止复核页面重复点击或网络重试重复生成报告、重复发起报告审批。
            return 1;
        }
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        if (directTemplateOrder && Objects.equals(type, 1)
                && !Objects.equals(dto.getInsResult(), 0) && !Objects.equals(dto.getInsResult(), 1)) {
            throw new ErrorException("请选择检验单结论");
        }
        Integer num = (type == 1 ? 5 : 4);
        LocalDateTime now = LocalDateTime.now();
        insOrderStateMapper.update(null, Wrappers.<InsOrderState>lambdaUpdate()
@@ -1179,31 +1609,65 @@
                .eq(InsOrderState::getInsOrderId, orderId)
                .ne(InsOrderState::getInsState, 5));
        InsReport report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId));
                .eq(InsReport::getInsOrderId, orderId)
                .last("limit 1"));
        Integer writeUserId = report != null && report.getWriteUserId() != null
                ? report.getWriteUserId() : resolveInspectionUserId(orderId, laboratory);
        // 查询订单
        InsOrder order = insOrderMapper.selectById(orderId);
        if (directTemplateOrder && Objects.equals(type, 1)) {
            List<Integer> templateResults = insOrderStandardTemplateService.list(
                            Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                                    .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                                    .select(InsOrderStandardTemplate::getInsResult))
                    .stream().map(InsOrderStandardTemplate::getInsResult)
                    .collect(Collectors.toList());
            if (templateResults.isEmpty()
                    || templateResults.stream().anyMatch(result -> !Objects.equals(result, 0)
                    && !Objects.equals(result, 1))) {
                throw new ErrorException("请先填写全部模板的检验结论");
            }
            Integer aggregateResult = templateResults.stream().allMatch(result -> Objects.equals(result, 1))
                    ? 1 : 0;
            insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                    .eq(InsOrder::getId, orderId)
                    .set(InsOrder::getInsResult, aggregateResult));
            order.setInsResult(aggregateResult);
        }
        if (count == 0 && num == 5) {
            // 修改报告状态
            insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
                    .eq(InsReport::getInsOrderId, orderId)
                    .set(InsReport::getIsPass, 1));
            // 修改订单状态
            insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                    .eq(InsOrder::getId, orderId)
                    .set(InsOrder::getInsState, 5));
            // 检验人员提交复核时不再生成报告;仅在全部试验室复核通过后生成检测报告。
            if (report == null || StringUtils.isBlank(report.getUrl())) {
                this.generateReport(orderId, DETECTION_REPORT, writeUserId);
                report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                        .eq(InsReport::getInsOrderId, orderId)
                        .last("limit 1"));
            }
            if (report == null || StringUtils.isBlank(report.getUrl())) {
                throw new ErrorException("检测报告生成失败");
            }
            if (report.getTempUrlPdf() == null) {
                String tempUrlPdf = this.wordToPdfTemp(report.getUrl().replace("/word", wordUrl));
                report.setTempUrlPdf("/word/" + tempUrlPdf);
            }
            report.setIsPass(1);
            insReportMapper.updateById(report);
            //生成原材料进货验证原始记录到附件里
            if (order.getTypeSource() != null && order.getTypeSource().equals(1)) {
                this.reportFactoryVerify(orderId, userId, report.getWriteUserId());
                this.reportFactoryVerify(orderId, userId, writeUserId);
            }
            // 查询检验任务的检验任务
            // 报告盖上批准人
            insReportService.writeReport(report.getId(), userId, report.getWriteUserId());
            insReportService.writeReport(report.getId(), userId, writeUserId);
            // 检验人
            String userName = insProductMapper.selectUserById(report.getWriteUserId()).get("name");
            String userName = insProductMapper.selectUserById(writeUserId).get("name");
            // 复核人
            Integer checkUserId = SecurityUtils.getUserId().intValue();
@@ -1252,7 +1716,7 @@
            // 发送消息的人
            // 查询发送人信息
            String sendUserAccount = insProductMapper.selectUserById(report.getWriteUserId()).get("account");
            String sendUserAccount = insProductMapper.selectUserById(writeUserId).get("account");
            // 发送企业微信通知(检验任务退回)
            threadPoolTaskExecutor.execute(() -> {
@@ -1284,28 +1748,81 @@
        return 1;
    }
    private boolean isDirectTemplateOrder(Integer orderId) {
        List<InsSample> samples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery()
                .eq(InsSample::getInsOrderId, orderId));
        if (samples.isEmpty()) {
            return false;
        }
        Long snapshotCount = insOrderStandardTemplateService.count(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId));
        InsOrder order = snapshotCount == 0 ? null : insOrderMapper.selectById(orderId);
        if (order != null && samples.stream().anyMatch(sample -> isDirectTemplateSample(order, sample))) {
            return true;
        }
        // 兼容已经生成了模板行号、但订单基础字段不完整的历史直绑订单。
        return insProductMapper.selectCount(Wrappers.<InsProduct>lambdaQuery()
                .in(InsProduct::getInsSampleId, samples.stream().map(InsSample::getId).collect(Collectors.toList()))
                .isNotNull(InsProduct::getTemplateRowIndex)) > 0;
    }
    /**
     * 兼容修复上线前已经下发、但因没有 ins_product 而缺少任务状态的直绑模板订单。
     */
    private void ensureDirectTemplateOrderState(Integer orderId) {
        if (!isDirectTemplateOrder(orderId)) {
            return;
        }
        Long stateCount = insOrderStateMapper.selectCount(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, orderId)
                .eq(InsOrderState::getLaboratory, DIRECT_TEMPLATE_LABORATORY));
        if (stateCount > 0) {
            return;
        }
        InsOrderState state = new InsOrderState();
        state.setInsOrderId(orderId);
        state.setLaboratory(DIRECT_TEMPLATE_LABORATORY);
        state.setInsState(0);
        insOrderStateMapper.insert(state);
    }
    private Integer resolveInspectionUserId(Integer orderId, String laboratory) {
        InsSampleUser inspector = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
                .eq(InsSampleUser::getInsSampleId, orderId)
                .eq(InsSampleUser::getState, 0)
                .eq(StringUtils.isNotBlank(laboratory), InsSampleUser::getSonLaboratory, laboratory)
                .orderByDesc(InsSampleUser::getId)
                .last("limit 1"));
        if (inspector == null || inspector.getUserId() == null) {
            throw new ErrorException("找不到当前检验任务的检验人,无法生成检测报告");
        }
        return inspector.getUserId();
    }
    /**
     * 生成报告
     *
     * @param orderId
     */
    private void generateReport(Integer orderId, Integer reportType) {
    private void generateReport(Integer orderId, Integer reportType, Integer writeUserId) {
        /*样品下的项目只要有一个项目不合格则检验结果为0,否则为1*/
        //这里的insSamples是订单下的所有样品包括("/")
        List<InsSample> insSamples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery().eq(InsSample::getInsOrderId, orderId));
        if (Objects.equals(reportType, INSPECTION_REPORT)) {
        for (InsSample insSample : insSamples) {
            List<InsProduct> insProducts = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                    .eq(InsProduct::getInsSampleId, insSample.getId())
                    .eq(InsProduct::getState, 1)
                    .ne(InsProduct::getIsBinding, 1));
            List<Integer> results = insProducts.stream().map(InsProduct::getInsResult).filter(Objects::nonNull).collect(Collectors.toList());
            if (results.contains(0)) {
                insSample.setInsResult(0);
            } else {
                insSample.setInsResult(1);
            for (InsSample insSample : insSamples) {
                List<InsProduct> insProducts = insProductMapper.selectList(Wrappers.<InsProduct>lambdaQuery()
                        .eq(InsProduct::getInsSampleId, insSample.getId())
                        .eq(InsProduct::getState, 1)
                        .ne(InsProduct::getIsBinding, 1));
                List<Integer> results = insProducts.stream().map(InsProduct::getInsResult).filter(Objects::nonNull).collect(Collectors.toList());
                if (results.contains(0)) {
                    insSample.setInsResult(0);
                } else {
                    insSample.setInsResult(1);
                }
                insSampleMapper.updateById(insSample);
            }
            insSampleMapper.updateById(insSample);
        }
        }
        InsOrder insOrder = insOrderMapper.selectById(orderId);
        // 抽检变成委托检验
@@ -1316,10 +1833,10 @@
        // 判断是大报告还是小报告  进厂小报告, 其他都是大报告
        if (insOrder.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY)) {
            // 生成小报告
            addSmallReport(orderId, insOrder, insSamples, reportType);
            addSmallReport(orderId, insOrder, insSamples, reportType, writeUserId);
        } else {
            //生成大报告
            addBitReport(orderId, insOrder, reportType);
            addBitReport(orderId, insOrder, reportType, writeUserId);
        }
    }
@@ -1327,6 +1844,7 @@
    /**
     * 电缆配置, 查看配置标识
     *
     * @param id
     * @param laboratory
     * @return
@@ -1338,6 +1856,7 @@
    /**
     * 原材料查看标识
     *
     * @param id
     * @param laboratory
     * @return
@@ -1349,6 +1868,7 @@
    /**
     * 查看重复标识
     *
     * @param id
     * @param laboratory
     * @return
@@ -1360,6 +1880,7 @@
    /**
     * 新增不合格复测内容
     *
     * @return
     */
    @Override
@@ -1418,7 +1939,7 @@
            return null;
        }
        InsOrder order = insOrderMapper.selectFirstSubmit(dto.getId());
        getTemplateThing(order, Collections.unmodifiableList(insProducts));
        getTemplateThing(order, Collections.unmodifiableList(insProducts), dto.getLaboratory());
        return insProducts;
    }
@@ -1556,6 +2077,7 @@
    /**
     * 查询进货原始记录
     *
     * @param insOrderId
     * @return
     */
@@ -1611,6 +2133,7 @@
    /**
     * 保存原材料进货验证
     *
     * @param factoryVerify
     * @return
     */
@@ -1662,22 +2185,34 @@
    /**
     * 设置表格样式
     *
     * @param max 标识最大个数
     * @return
     */
    private TableStyle setTableStyle(int max, boolean showResult){
    private TableStyle setTableStyle(int max, boolean showResult) {
        //设置样式
        TableStyle tableStyle = new TableStyle();
        if (!showResult) {
            switch (max) {
                case 1: tableStyle.setColWidths(new int[]{650, 3000, 850, 2400, 3100}); break;
                case 2: tableStyle.setColWidths(new int[]{650, 2800, 850, 2200, 1750, 1750}); break;
                case 3: tableStyle.setColWidths(new int[]{650, 2600, 850, 1800, 1400, 1400, 1400}); break;
                case 4: tableStyle.setColWidths(new int[]{650, 2400, 850, 1600, 1125, 1125, 1125, 1125}); break;
                case 5: tableStyle.setColWidths(new int[]{650, 2200, 850, 1500, 960, 960, 960, 960, 960}); break;
                default: break;
                case 1:
                    tableStyle.setColWidths(new int[]{650, 3000, 850, 2400, 3100});
                    break;
                case 2:
                    tableStyle.setColWidths(new int[]{650, 2800, 850, 2200, 1750, 1750});
                    break;
                case 3:
                    tableStyle.setColWidths(new int[]{650, 2600, 850, 1800, 1400, 1400, 1400});
                    break;
                case 4:
                    tableStyle.setColWidths(new int[]{650, 2400, 850, 1600, 1125, 1125, 1125, 1125});
                    break;
                case 5:
                    tableStyle.setColWidths(new int[]{650, 2200, 850, 1500, 960, 960, 960, 960, 960});
                    break;
                default:
                    break;
            }
        } else if(max<=5){
        } else if (max <= 5) {
            for (int i = 1; i <= max; i++) {
                // 根据检验结果个数修改长度
                switch (i) {
@@ -1716,30 +2251,31 @@
    /**
     * 处理有电缆颜色标识的检测项
     * @param cableTags 电缆颜色标识
     * @param collect   检测项列表
     *
     * @param cableTags  电缆颜色标识
     * @param collect    检测项列表
     * @param startIndex 起始下标
     * @param endIndex   结束下标
     * @return
     */
    private List<SampleProductExportDto> transformSampleProduct(List<String> cableTags,Map<String, List<SampleProductExportDto>> collect,int startIndex,int endIndex){
    private List<SampleProductExportDto> transformSampleProduct(List<String> cableTags, Map<String, List<SampleProductExportDto>> collect, int startIndex, int endIndex) {
        List<SampleProductExportDto> sampleProductExportDtos = new ArrayList<>();
        for (String s : collect.keySet()) {
            List<String> lastValueList = new ArrayList<>();
            SampleProductExportDto dto = new SampleProductExportDto();
            BeanUtil.copyProperties(collect.get(s).get(0),dto);
            BeanUtil.copyProperties(collect.get(s).get(0), dto);
            Set<String> tellSet = new HashSet<>();
            for (int i = startIndex; i < endIndex; i++) {
                String cableTag = cableTags.get(i);
                for (SampleProductExportDto sDto : collect.get(s)) {
                    tellSet.add(sDto.getTell());
                    if(sDto.getCableTag().equals(cableTag)){
                    if (sDto.getCableTag().equals(cableTag)) {
                        lastValueList.add(sDto.getLastValue());
                    }
                }
            }
            //切割电缆配置项
            dto.setTell(String.join("\n",tellSet));
            dto.setTell(String.join("\n", tellSet));
            dto.setLastValueList(lastValueList);
            sampleProductExportDtos.add(dto);
        }
@@ -1748,10 +2284,11 @@
    /**
     * 检测项排序
     *
     * @param sourceMap
     * @param targetMap
     */
    private void sortSampleProduct(Map<String, List<SampleProductExportDto>> sourceMap,Map<String, List<SampleProductExportDto>> targetMap){
    private void sortSampleProduct(Map<String, List<SampleProductExportDto>> sourceMap, Map<String, List<SampleProductExportDto>> targetMap) {
        List<Map.Entry<String, List<SampleProductExportDto>>> entries = new ArrayList<>(sourceMap.entrySet());
        entries.sort(Comparator.comparingInt(o -> (o.getValue().get(0).getSort() == null ? 0 : o.getValue().get(0).getSort())));
        for (Map.Entry<String, List<SampleProductExportDto>> entry : entries) {
@@ -1761,11 +2298,12 @@
    /**
     * 小报告生成
     *
     * @param orderId
     * @param insOrder
     * @param insSamples
     */
    private void addSmallReport(Integer orderId, InsOrder insOrder, List<InsSample> insSamples, Integer reportType) {
    private void addSmallReport(Integer orderId, InsOrder insOrder, List<InsSample> insSamples, Integer reportType, Integer writeUserId) {
        InsReport insReport = new InsReport();
        AtomicReference<String> resultCh = new AtomicReference<>("");
        boolean showResult = Objects.equals(reportType, INSPECTION_REPORT);
@@ -1782,7 +2320,7 @@
        //查询零件属性
        IfsPartPropsRecord ifsPartPropsRecord = ifsPartPropsRecordMapper.selectOne(Wrappers.<IfsPartPropsRecord>lambdaQuery()
                .eq(IfsPartPropsRecord::getIfsInventoryId, ifsInventoryQuantity.getId()));
        if(Objects.nonNull(ifsPartPropsRecord)){
        if (Objects.nonNull(ifsPartPropsRecord)) {
            enterFactoryReport.setOuterColor(ifsPartPropsRecord.getOuterColor());
        }
        enterFactoryReport.setQtyArrived(ifsInventoryQuantity.getQtyArrived() == null ? "" :
@@ -1807,7 +2345,7 @@
        List<RowRenderData> rows = new ArrayList<>();
        List<TextRenderData> text = new ArrayList<>();
        RowRenderData rowRenderData = null;
        List<Map<String,Object>> cableTagEnclosureTables = new ArrayList<>();
        List<Map<String, Object>> cableTagEnclosureTables = new ArrayList<>();
        // 查询检验内容
        List<SampleProductExportDto> sampleProductDto2s = insOrderMapper.selectSampleBySampleId(insSamples.stream()
@@ -1820,7 +2358,7 @@
        Integer max = insSamples.stream().mapToInt(InsSample::getQuantity).sum();
        TableRenderData tableRenderData = new TableRenderData();
        String templateName;
        if(StringUtils.equals(OrderType.WG.getValue(),ifsInventoryQuantity.getOrderType())){
        if (StringUtils.equals(OrderType.WG.getValue(), ifsInventoryQuantity.getOrderType())) {
            //过滤不判定的检测项
            List<SampleProductExportDto> filterItems = sampleProductDto2s.stream()
                    .filter(f -> !showResult || f.getInsResult() != 3)
@@ -1829,7 +2367,7 @@
            templateName = showResult ? "/static/small-wg-report-template.docx" : "/static/small-wg-detection-template.docx";
            //查询检验单消息
            InsSampleUserVO insSampleUser = insSampleUserMapper.selectUserNameByOrderId(orderId);
            if(Objects.nonNull(insSampleUser)){
            if (Objects.nonNull(insSampleUser)) {
                enterFactoryReport.setPartDesc(insSampleUser.getModel());
                enterFactoryReport.setInspector(insSampleUser.getInspector());
                enterFactoryReport.setInspectDate(insSampleUser.getInspectDate());
@@ -1841,7 +2379,7 @@
            AtomicInteger finalIndex = new AtomicInteger(1);
            List<String> cableTags = insOrderMapper.selectSampleCableTag(insSample.getId());
            max = Math.max(cableTags.size(),1);
            max = Math.max(cableTags.size(), 1);
            //处理电缆配置检测项
            Map<String, List<SampleProductExportDto>> tempMap = new HashMap<>();
            Map<String, List<SampleProductExportDto>> tempMap2 = new HashMap<>();
@@ -1853,41 +2391,41 @@
                List<SampleProductExportDto> sampleProductExportDtos2;
                //处理电缆配置项
                Map<String, List<SampleProductExportDto>> collect = listMap2.get(key).stream().collect(Collectors.groupingBy(SampleProductExportDto::getInspectionItemSubclass));
                if(cableTags.size()>maxCableTag){
                    sampleProductExportDtos = transformSampleProduct(cableTags,collect,0,maxCableTag);
                    sampleProductExportDtos2 = transformSampleProduct(cableTags,collect,maxCableTag,cableTags.size());
                    tempMap2.put(key,sampleProductExportDtos2);
                }else{
                    sampleProductExportDtos = transformSampleProduct(cableTags,collect,0,cableTags.size());
                if (cableTags.size() > maxCableTag) {
                    sampleProductExportDtos = transformSampleProduct(cableTags, collect, 0, maxCableTag);
                    sampleProductExportDtos2 = transformSampleProduct(cableTags, collect, maxCableTag, cableTags.size());
                    tempMap2.put(key, sampleProductExportDtos2);
                } else {
                    sampleProductExportDtos = transformSampleProduct(cableTags, collect, 0, cableTags.size());
                }
                cableTagItem.put(key,sampleProductExportDtos);
                cableTagItem.put(key, sampleProductExportDtos);
            }
            //处理非电缆配置检测项
            Map<String, List<SampleProductExportDto>> listMap = filterItems.stream()
                    .filter(f -> StringUtils.isBlank(f.getCableTag()) && StringUtils.isNotBlank(f.getInspectionItem()))
                    .collect(Collectors.groupingBy(s->s.getInspectionItem()+"&"));
                    .collect(Collectors.groupingBy(s -> s.getInspectionItem() + "&"));
            //合并检测项列表
            tempMap.putAll(cableTagItem);
            tempMap.putAll(listMap);
            sortSampleProduct(tempMap,totalItem);
            int tagNum = Math.min(max,maxCableTag);
            List<String> tagList = cableTags.isEmpty()?new ArrayList<>():cableTags.subList(0,tagNum);
            sortSampleProduct(tempMap, totalItem);
            int tagNum = Math.min(max, maxCableTag);
            List<String> tagList = cableTags.isEmpty() ? new ArrayList<>() : cableTags.subList(0, tagNum);
            handlerSampleItems(OrderType.WG.getValue(), totalItem, finalIndex, sampleList, tagNum, text, rows, rowRenderData, resultCh, tagList, true, showResult);
            if(CollectionUtil.isNotEmpty(tempMap2)){
                sortSampleProduct(tempMap2,cableTagEnclosureItem);
            if (CollectionUtil.isNotEmpty(tempMap2)) {
                sortSampleProduct(tempMap2, cableTagEnclosureItem);
                //生成附件电缆表格
                TableRenderData tableRenderData2 = new TableRenderData();
                List<String> newCableTags = cableTags.subList(maxCableTag,cableTags.size());
                List<String> newCableTags = cableTags.subList(maxCableTag, cableTags.size());
                AtomicInteger finalIndex2 = new AtomicInteger(1);
                List<TextRenderData> newText = new ArrayList<TextRenderData>();
                List<RowRenderData> newRows = new ArrayList<>();
                RowRenderData newRowRenderData = null;
                handlerSampleItems(OrderType.WG.getValue(), cableTagEnclosureItem, finalIndex2, new ArrayList<>(), newCableTags.size(), newText, newRows, newRowRenderData, resultCh, newCableTags, true, showResult);
                tableRenderData2.setRows(newRows);
            tableRenderData2.setTableStyle(setTableStyle(newCableTags.size(), showResult));
                tableRenderData2.setTableStyle(setTableStyle(newCableTags.size(), showResult));
                HashMap<String, Object> tableMap = new HashMap<>();
                tableMap.put("enclosureTable",tableRenderData2);
                tableMap.put("enclosureTable", tableRenderData2);
                tableMap.put("resultCh", resultCh);
                tableMap.put("writeUrl", null);
                tableMap.put("examineUrl", null);
@@ -1907,7 +2445,7 @@
                resultCh.set("本产品符合相关标准要求,经检验合格准予出厂(盖章有效)");
            }
        }else{
        } else {
            templateName = showResult ? "/static/small-report-template.docx" : "/static/small-detection-template.docx";
            // 转成Mpa进行排序
            Map<String, List<SampleProductExportDto>> sortedMap = sampleProductDto2s.stream()
@@ -1915,7 +2453,7 @@
                    .collect(Collectors.groupingBy(SampleProductExportDto::getInspectionItem));
//            // 创建一个 LinkedHashMap 来保持插入顺序
            Map<String, List<SampleProductExportDto>> item = new LinkedHashMap<>();
            sortSampleProduct(sortedMap,item);
            sortSampleProduct(sortedMap, item);
            AtomicInteger finalIndex = new AtomicInteger(1);
@@ -1940,7 +2478,7 @@
                    put("standardMethod", standardMethod2.toString().equals("null") ? "" : standardMethod2);
                    put("orderType", orderType);
                    put("table", tableRenderData);
                    put("enclosureTables", cableTagEnclosureTables.isEmpty()?null:cableTagEnclosureTables);
                    put("enclosureTables", cableTagEnclosureTables.isEmpty() ? null : cableTagEnclosureTables);
                    put("resultCh", resultCh);
                    put("writeUrl", null);
                    put("examineUrl", null);
@@ -1954,15 +2492,13 @@
            // 修改换行和合并问题
            updaeMerge(template.getXWPFDocument(), true);
            String name = insReport.getCode().replace("/", "") + (showResult ? "-J.docx" : "-C.docx");
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl + "/" + name)));
            Files.createDirectories(Paths.get(wordUrl));
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl, name)));
            insReport.setUrl("/word/" + name);
            insReport.setIsPass(0);
            insReport.setWriteUserId(SecurityUtils.getUserId().intValue());//提交人
            insReport.setWriteUserId(writeUserId);//编制人(检验人)
            insReport.setWriteTime(LocalDateTime.now());//提交时间
            // 查询报告, 判断之前是否添加过, 添加过删除
            insReportMapper.delete(Wrappers.<InsReport>lambdaQuery()
                    .eq(InsReport::getInsOrderId, insOrder.getId()));
            insReportMapper.insert(insReport);
            saveGeneratedReport(insReport);
            inputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
@@ -1971,6 +2507,7 @@
    /**
     * 处理常规检测项
     *
     * @param item
     * @param finalIndex
     * @param sampleList
@@ -1996,7 +2533,7 @@
                    String productName = productDto2.getInspectionItemClass() + productDto2.getInspectionItem() + productDto2.getCableTag();
                    if (map.containsKey(productName)) {
                        // 如果名称已经存在,添加 lastValue 值到 lastValueList 列表
                        if(CollectionUtil.isEmpty(map.get(productName).getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(),orderType)){
                        if (CollectionUtil.isEmpty(map.get(productName).getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(), orderType)) {
                            map.get(productName)
                                    .getLastValueList()
                                    .add(productDto2.getLastValue());
@@ -2006,7 +2543,7 @@
                                .add(productDto2.getInsResult());
                    } else {
                        // 如果名称不存在,直接放入 map
                        if(CollectionUtil.isEmpty(productDto2.getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(),orderType)){
                        if (CollectionUtil.isEmpty(productDto2.getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(), orderType)) {
                            productDto2.setLastValueList(new ArrayList<>()); // 检验内容
                            productDto2.getLastValueList().add(productDto2.getLastValue());
                        }
@@ -2041,7 +2578,7 @@
                    String productName = productDto2.getInspectionItemClass() + productDto2.getInspectionItem() + productDto2.getInspectionItemSubclass() + productDto2.getCableTag();
                    if (map.containsKey(productName)) {
                        // 如果名称已经存在,添加 lastValue 值到 lastValueList 列表
                        if(CollectionUtil.isEmpty(map.get(productName).getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(),orderType)){
                        if (CollectionUtil.isEmpty(map.get(productName).getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(), orderType)) {
                            map.get(productName)
                                    .getLastValueList()
                                    .add(productDto2.getLastValue());
@@ -2051,7 +2588,7 @@
                                .add(productDto2.getInsResult());
                    } else {
                        // 如果名称不存在,直接放入 map
                        if(CollectionUtil.isEmpty(productDto2.getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(),orderType)){
                        if (CollectionUtil.isEmpty(productDto2.getLastValueList()) || StringUtils.equals(OrderType.RAW.getValue(), orderType)) {
                            productDto2.setLastValueList(new ArrayList<>()); // 检验内容
                            productDto2.getLastValueList().add(productDto2.getLastValue());
                        }
@@ -2072,7 +2609,7 @@
        });
        // 添加小报告表头
        if(hasAddHead){
        if (hasAddHead) {
            text = addSmallHead(text, max, rows, cableTagList, showResult);
        }
@@ -2092,8 +2629,8 @@
            // 检验项目
            TextRenderData middleRenderData2 = new TextRenderData();
            String[] split = sample.getInspectionName().split("&");
            String itemName = split.length>0?split[0]:sample.getInspectionName();
            middleRenderData2.setText(itemName+"∑"+itemName+i+"_"+finalIndex);
            String itemName = split.length > 0 ? split[0] : sample.getInspectionName();
            middleRenderData2.setText(itemName + "∑" + itemName + i + "_" + finalIndex);
            Style middleStyle2 = new Style();
            middleStyle2.setFontFamily("宋体");
            middleStyle2.setColor("000000");
@@ -2180,7 +2717,7 @@
                    TextRenderData middleRenderData6 = new TextRenderData();
                    middleRenderData6.setText((StringUtils.isNotEmpty(sample.getLastValue()) ?
                            sample.getLastValue() : "")
                            + "∑" + (finalIndex.get() +"_"+ i));
                            + "∑" + (finalIndex.get() + "_" + i));
                    Style middleStyle6 = new Style();
                    middleStyle6.setFontFamily("宋体");
                    middleStyle6.setColor("000000");
@@ -2241,13 +2778,13 @@
            }
            if (showResult) {
            TextRenderData middleRenderData7 = new TextRenderData();
            middleRenderData7.setText(result);
            Style middleStyle7 = new Style();
            middleStyle7.setFontFamily("宋体");
            middleStyle7.setColor("000000");
            middleRenderData7.setStyle(middleStyle7);
            text.add(middleRenderData7);
                TextRenderData middleRenderData7 = new TextRenderData();
                middleRenderData7.setText(result);
                Style middleStyle7 = new Style();
                middleStyle7.setFontFamily("宋体");
                middleStyle7.setColor("000000");
                middleRenderData7.setStyle(middleStyle7);
                text.add(middleRenderData7);
            }
            TextRenderData[] text2 = text.toArray(new TextRenderData[0]);
@@ -2264,6 +2801,7 @@
    /**
     * 添加小报告表头
     *
     * @param text
     * @param max
     * @param rows
@@ -2314,13 +2852,13 @@
            text.add(headRenderData6);
        }
        if (showResult) {
        TextRenderData headRenderData7 = new TextRenderData();
        headRenderData7.setText("单项判断");
        Style headStyle7 = new Style();
        headStyle7.setFontFamily("宋体");
        headStyle7.setColor("000000");
        headRenderData7.setStyle(headStyle7);
        text.add(headRenderData7);
            TextRenderData headRenderData7 = new TextRenderData();
            headRenderData7.setText("单项判断");
            Style headStyle7 = new Style();
            headStyle7.setFontFamily("宋体");
            headStyle7.setColor("000000");
            headRenderData7.setStyle(headStyle7);
            text.add(headRenderData7);
        }
        TextRenderData[] text3 = text.toArray(new TextRenderData[0]);
@@ -2372,13 +2910,13 @@
                text.add(tagRenderData6);
            }
            if (showResult) {
            TextRenderData tagRenderData7 = new TextRenderData();
            tagRenderData7.setText("");
            Style tagStyle7 = new Style();
            tagStyle7.setFontFamily("宋体");
            tagStyle7.setColor("000000");
            tagRenderData7.setStyle(tagStyle7);
            text.add(tagRenderData7);
                TextRenderData tagRenderData7 = new TextRenderData();
                tagRenderData7.setText("");
                Style tagStyle7 = new Style();
                tagStyle7.setFontFamily("宋体");
                tagStyle7.setColor("000000");
                tagRenderData7.setStyle(tagStyle7);
                text.add(tagRenderData7);
            }
            TextRenderData[] text4 = text.toArray(new TextRenderData[0]);
@@ -2389,7 +2927,7 @@
        }
        //如果有电缆颜色,生成颜色标识行
        if(CollectionUtil.isNotEmpty(cableTagList)){
        if (CollectionUtil.isNotEmpty(cableTagList)) {
            TextRenderData tagRenderData1 = new TextRenderData();
            tagRenderData1.setText("");
            Style tagStyle1 = new Style();
@@ -2432,13 +2970,13 @@
                text.add(tagRenderData6);
            }
            if (showResult) {
            TextRenderData tagRenderData7 = new TextRenderData();
            tagRenderData7.setText("/");
            Style tagStyle7 = new Style();
            tagStyle7.setFontFamily("宋体");
            tagStyle7.setColor("000000");
            tagRenderData7.setStyle(tagStyle7);
            text.add(tagRenderData7);
                TextRenderData tagRenderData7 = new TextRenderData();
                tagRenderData7.setText("/");
                Style tagStyle7 = new Style();
                tagStyle7.setFontFamily("宋体");
                tagStyle7.setColor("000000");
                tagRenderData7.setStyle(tagStyle7);
                text.add(tagRenderData7);
            }
            TextRenderData[] text4 = text.toArray(new TextRenderData[0]);
@@ -2453,13 +2991,18 @@
    /**
     * 创建大报告
     *
     * @param orderId
     * @param insOrder
     * @param
     */
    private void addBitReport(Integer orderId, InsOrder insOrder, Integer reportType) {
    private void addBitReport(Integer orderId, InsOrder insOrder, Integer reportType, Integer writeUserId) {
        //samples是不包括带有"/"的样品
        List<SampleProductDto> samples = insSampleMapper.selectSampleProductListByOrderId(orderId);
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        if (!directTemplateOrder && CollectionUtils.isEmpty(samples)) {
            throw new ErrorException("当前订单未找到可生成报告的有效检验项目");
        }
        InsReport insReport = new InsReport();
        insReport.setCode(insOrder.getEntrustCode());
        insReport.setInsOrderId(orderId);
@@ -2494,6 +3037,11 @@
                        }
                    }
                }
            }
            // 新版检测报告直接使用固定四列结果行,不再构造旧版可变列 TableRenderData。
            // 检验报告仍沿用下方原有的大表格逻辑。
            if (!showResult) {
                return;
            }
            // 收样日期
            List<RowRenderData> rows = new ArrayList<>();
@@ -2541,12 +3089,16 @@
                        }
                    }
                }
                insOrder.setSampleView(insOrder.getSample());
                String sampleViewEn = insSampleMapper.getSampleEn(insOrder.getSample());
                if (StringUtils.isBlank(sampleViewEn)) {
                    sampleViewEn = insSampleMapper.getSampleEnByObject(insOrder.getSample());
                if (StringUtils.isBlank(insOrder.getSampleView())) {
                    insOrder.setSampleView(insOrder.getSample());
                }
                insOrder.setSampleViewEn(sampleViewEn);
                if (StringUtils.isBlank(insOrder.getSampleViewEn())) {
                    String sampleViewEn = insSampleMapper.getSampleEn(insOrder.getSample());
                    if (StringUtils.isBlank(sampleViewEn)) {
                        sampleViewEn = insSampleMapper.getSampleEnByObject(insOrder.getSample());
                    }
                    insOrder.setSampleViewEn(sampleViewEn);
                }
            } else {
                // 获得批量检验的总数
                max = insOrderMapper.selectSampleMax(a.getId());
@@ -2686,15 +3238,26 @@
        for (String s : standardMethod) {
            standardMethod2.append("、").append(s);
        }
        standardMethod2.replace(0, 1, "");
        if (standardMethod2.length() > 0) {
            standardMethod2.deleteCharAt(0);
        }
        tables.forEach(table -> {
            table.put("tableSize", tables.size() + 1);
        });
        // 设备信息
        List<Map<String, String>> deviceList = null;
        if (CollectionUtils.isNotEmpty(deviceSet)) {
            deviceList = insOrderMapper.selectDeviceList(deviceSet);
        // 新订单按模板选择设备;旧订单没有模板设备关系时回退到订单级设备记录。
        Set<String> reportDeviceNumbers = insOrderTemplateDeviceMapper
                .selectManagementNumbersByOrderId(orderId).stream()
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (reportDeviceNumbers.isEmpty()) {
            reportDeviceNumbers.addAll(insOrderDeviceRecordMapper.selectDeviceNumber(orderId).stream()
                    .map(InsOrderDeviceRecordDto::getManagementNumber)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.toList()));
        }
        reportDeviceNumbers.addAll(deviceSet);
        List<Map<String, String>> deviceList = reportDeviceNumbers.isEmpty()
                ? new ArrayList<>() : insOrderMapper.selectDeviceList(reportDeviceNumbers);
        if (CollectionUtils.isNotEmpty(deviceList)) {
            int count = 1;
            for (Map<String, String> stringMap : deviceList) {
@@ -2702,12 +3265,6 @@
                count++;
            }
        }
        Map<String, String> codeStr = new HashMap<>();
        codeStr.put("报告编号", insReport.getCode());
        codeStr.put("样品名称", insOrder.getSample());
        codeStr.put("规格型号", samples.get(0).getModel());
        codeStr.put("发放日期", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        String modelStr = CollUtil.join(models, "\n");
@@ -2722,9 +3279,11 @@
        // 来样方式
        String formType = iSysDictTypeService.selectLabelByDict(DictDataConstants.FORM_TYPE, insOrder.getFormType());
        // 样品状态
        String sampleStatus = iSysDictTypeService.selectLabelByDict(DictDataConstants.SAMPLE_STATUS_LIST, insOrder.getSampleStatus());
        ;
        // 样品外观优先展示字典名称;历史数据或直接保存文本时,回退展示订单原值。
        String sampleStatus = defaultReportText(
                iSysDictTypeService.selectLabelByDict(
                        DictDataConstants.SAMPLE_STATUS_LIST, insOrder.getSampleStatus()),
                insOrder.getSampleStatus());
        // 公司信息
        Custom custom = customMapper.selectById(insOrder.getCompanyId());
@@ -2790,13 +3349,16 @@
        // 抽样日期
        // 成品是抽样时间
        LocalDateTime sendTime = insOrder.getSendTime();
        if (insOrder.getTypeSource() != null && insOrder.getTypeSource().equals(1)) {
        IfsInventoryQuantity reportInventory = null;
        if (insOrder.getIfsInventoryId() != null) {
            reportInventory = ifsInventoryQuantityMapper.selectById(insOrder.getIfsInventoryId());
        }
        if (insOrder.getTypeSource() != null && insOrder.getTypeSource().equals(1) && reportInventory != null) {
            // 原材料收样日期是报检日期
            IfsInventoryQuantity ifsInventoryQuantity = ifsInventoryQuantityMapper.selectById(insOrder.getIfsInventoryId());
            sendTime = ifsInventoryQuantity.getDeclareDate();
            sendTime = reportInventory.getDeclareDate();
        }
        // 检验时间  抽样时间-提交时间
        // 检测日期:首次保存模板值时间-首次提交时间
        LocalDateTime now = LocalDateTime.now();
        // 提交时间
        LocalDateTime submitTime = LocalDateTime.now();
@@ -2805,15 +3367,20 @@
            submitTime = insOrder.getFirstSubmitDate();
        }
        String insTime = insOrder.getSendTime().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")) + "-"
        LocalDateTime firstInspectDate = insOrder.getFirstInspectDate();
        // 字段上线前的历史订单沿用原来的下发时间,避免改变既有报告口径。
        if (firstInspectDate == null) {
            firstInspectDate = insOrder.getSendTime();
        }
        String insTime = firstInspectDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")) + "-"
                + submitTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        String insTimeEn = monthNames[insOrder.getSendTime().getMonthValue() - 1] + " " + insOrder.getSendTime().format(DateTimeFormatter.ofPattern("dd, yyyy")) + "-"
        String insTimeEn = monthNames[firstInspectDate.getMonthValue() - 1] + " " + firstInspectDate.format(DateTimeFormatter.ofPattern("dd, yyyy")) + "-"
                + monthNames[submitTime.getMonthValue() - 1] + " " + submitTime.format(DateTimeFormatter.ofPattern("dd, yyyy"));
        //检验项目的环境
        String environment = "";
        environment = (ObjectUtils.isNotEmpty(insOrder.getTemperature()) ? insOrder.getTemperature() + "℃ " : "") + (ObjectUtils.isNotEmpty(insOrder.getHumidity()) ? insOrder.getHumidity() + "%" : "");
        // 检验环境:优先汇总各模板填写的温湿度,报告展示全部模板的最小值到最大值。
        String environment = buildReportEnvironment(orderId, insOrder);
        String finalEnvironment = environment;
        LocalDateTime finalSendTime = sendTime;
        String finalResultCh = resultCh;
@@ -2822,12 +3389,88 @@
        List<Map<String, String>> finalDeviceList = deviceList;
        String finalModelStr = modelStr;
        InputStream inputStream = this.getClass().getResourceAsStream(showResult ? "/static/report-template.docx" : "/static/detection-report-template.docx");
        // 直绑模板订单没有 ins_product,检测项目、单位和结果均从订单模板快照中获取。
        List<Map<String, String>> detectionResultRows = directTemplateOrder
                ? buildTemplateDetectionResultRows(orderId)
                : buildDetectionResultRows(samples);
        String productName = defaultReportText(insOrder.getSampleView(), insOrder.getSample());
        String customerName = defaultReportText(custom == null ? null : custom.getCompany(), insOrder.getCompany());
        String detectionCategory = defaultReportText(finalOrderType, "委托检测");
        String sampleCode = samples.stream()
                .map(SampleProductDto::getSampleCode)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.joining("、"));
        String productAndModel = productName;
        if (StringUtils.isNotBlank(finalModelStr) && !Objects.equals(productName, finalModelStr)) {
            productAndModel = productName + " " + finalModelStr;
        }
        String standardMethodText = "null".equals(standardMethod2.toString()) ? "" : standardMethod2.toString();
        String productionDate = "/";
        String batchNo = "/";
        if (reportInventory != null) {
            if (reportInventory.getProductDate() != null) {
                productionDate = reportInventory.getProductDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
            }
            batchNo = defaultReportText(reportInventory.getUpdateBatchNo(), reportInventory.getLotBatchNo());
        }
        // 新聚直绑模板订单:报告信息取委托信息和订单模板快照,不再取能力范围标准方法。
        List<InsOrderStandardTemplate> templateSnapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        boolean newJuTemplateOrder = !templateSnapshots.isEmpty() && directTemplateOrder;
        if (newJuTemplateOrder) {
            productName = defaultReportText(insOrder.getSampleView(), insOrder.getSample());
            finalModelStr = "/";
            productAndModel = productName;
            if (insOrder.getSampleDate() != null) {
                finalSendTime = insOrder.getSampleDate().atStartOfDay();
            }
            if (insOrder.getProductionDate() != null) {
                productionDate = insOrder.getProductionDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
            }
            batchNo = defaultReportText(insOrder.getProductionBatch(), "/");
            standardMethodText = templateSnapshots.stream()
                    .map(InsOrderStandardTemplate::getRemark)
                    .filter(StringUtils::isNotBlank)
                    .distinct()
                    .collect(Collectors.joining(";"));
        }
        boolean containsOrganicChlorine = detectionResultRows.stream()
                .map(row -> row.get("itemName"))
                .filter(Objects::nonNull)
                .anyMatch(name -> name.contains("有机氯"));
        String resultRemark = containsOrganicChlorine
                ? "微库仑法,有机氯含量小于0.005%时,以未检出报出。"
                : defaultReportText(insOrder.getRemark(), "/");
        // 检测报告和检验报告统一使用新版报告模板
        String templateName = "/static/detection-report-template-v2.docx";
        InputStream inputStream = this.getClass().getResourceAsStream(templateName);
        if (inputStream == null) {
            throw new RuntimeException("找不到报告模板: " + templateName);
        }
        Configure configure = Configure.builder()
                .bind("deviceList", new HackLoopTableRenderPolicy())
                .bind("r", new HackLoopTableRenderPolicy(true))
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .useSpringEL(true)
                .build();
        String finalProductAndModel = productAndModel;
        String finalProductName = productName;
        String finalCustomerName = customerName;
        String finalDetectionCategory = detectionCategory;
        String finalSampleCode = defaultReportText(sampleCode, "/");
        String finalStandardMethodText = defaultReportText(standardMethodText, "/");
        String finalProductionDate = productionDate;
        String finalBatchNo = batchNo;
        String finalResultRemark = resultRemark;
        String equipmentText = buildEquipmentText(finalDeviceList);
        String issueDate = now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        String finalReportModelStr = finalModelStr;
        LocalDateTime finalReportSendTime = finalSendTime;
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("order", insOrder);
@@ -2837,10 +3480,10 @@
                    put("sampleSize", insOrder.getTestQuantity());
                    put("tables", tables);
                    put("tableSize", tables.size() + 1);
                    put("standardMethod", (standardMethod2.toString().equals("null") ? "" : standardMethod2));
                    put("standardMethod", finalStandardMethodText);
                    put("deviceList", finalDeviceList);
                    put("twoCode", null);
                    put("models", finalModelStr);
                    put("models", finalReportModelStr);
                    put("productSize", productSize.get());
                    put("createTime", now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                    put("createTimeEn", monthNames[now.getMonthValue() - 1] + " " + now.format(DateTimeFormatter.ofPattern("dd, yyyy")));
@@ -2852,8 +3495,8 @@
                    put("examineUrl", null);
                    put("ratifyUrl", null);
                    put("orderType", finalOrderType);
                    put("getTime", finalSendTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                    put("getTimeEn", monthNames[finalSendTime.getMonthValue() - 1] + " " + finalSendTime.format(DateTimeFormatter.ofPattern("dd, yyyy")));
                    put("getTime", finalReportSendTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                    put("getTimeEn", monthNames[finalReportSendTime.getMonthValue() - 1] + " " + finalReportSendTime.format(DateTimeFormatter.ofPattern("dd, yyyy")));
                    put("seal1", null);
                    put("seal2", null);
                    put("formTypeCh", formType);
@@ -2861,39 +3504,347 @@
                    put("resultCh", finalResultCh);
                    put("resultEn", finalResultEn);
                    put("sampleStatus", sampleStatus);
                    // 新版检测报告模板(参照《有机氯检测报告》)使用的扁平字段。
                    put("no", insReport.getCode());
                    put("cover", finalProductAndModel);
                    put("product", finalProductName);
                    put("model", defaultReportText(finalReportModelStr, "/"));
                    put("customer", finalCustomerName);
                    put("category", finalDetectionCategory);
                    put("date", issueDate);
                    put("maker", defaultReportText(insOrder.getProduction(), "/"));
                    put("env", finalEnvironment);
                    put("qty", defaultReportText(insOrder.getTestQuantity(), "/"));
                    put("receive", finalReportSendTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                    put("sampleNo", finalSampleCode);
                    put("appearance", defaultReportText(sampleStatus, "/"));
                    put("sender", defaultReportText(insOrder.getPrepareUser(), "/"));
                    put("produced", finalProductionDate);
                    put("batch", finalBatchNo);
                    put("phone", defaultReportText(insOrder.getPhone(), "/"));
                    put("testDate", insTime);
                    put("entrustNo", defaultReportText(insOrder.getEntrustCode(), "/"));
                    put("sampling", "/");
                    put("standard", finalStandardMethodText);
                    put("equipment", equipmentText);
                    put("conclusion", "样品按委托方要求进行检测,数据见检测结果。");
                    put("remark", "本报告检测结果仅对当日所接收样品负责。");
                    put("r", detectionResultRows);
                    put("resultNote", finalResultRemark);
                }});
        try {
            // 修改换行和合并问题
            updaeMerge(template.getXWPFDocument(), false);
            String name = insReport.getCode().replace("/", "") + (showResult ? ".docx" : "-C.docx");
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl + "/" + name)));
            Files.createDirectories(Paths.get(wordUrl));
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl, name)));
            insReport.setUrl("/word/" + name);
            insReport.setWriteUserId(SecurityUtils.getUserId().intValue());//提交人
            insReport.setWriteUserId(writeUserId);//编制人(检验人)
            insReport.setWriteTime(LocalDateTime.now());//提交时间
            // 查询报告, 判断之前是否添加过, 添加过删除
            insReportMapper.delete(Wrappers.<InsReport>lambdaQuery()
                    .eq(InsReport::getInsOrderId, insOrder.getId()));
            insReportMapper.insert(insReport);
            saveGeneratedReport(insReport);
            inputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private void saveGeneratedReport(InsReport report) {
        InsReport existing = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, report.getInsOrderId())
                .last("limit 1"));
        if (existing == null) {
            insReportMapper.insert(report);
            return;
        }
        report.setId(existing.getId());
        insReportMapper.updateById(report);
    }
    /**
     * 新版检测报告的结果表固定为四列:序号、检测项目、单位、检测结果。
     */
    private List<Map<String, String>> buildDetectionResultRows(List<SampleProductDto> samples) {
        List<Map<String, String>> rows = new ArrayList<>();
        if (CollectionUtils.isEmpty(samples)) {
            return rows;
        }
        boolean multipleSamples = samples.size() > 1;
        int sequence = 1;
        for (SampleProductDto sample : samples) {
            if (CollectionUtils.isEmpty(sample.getInsProduct())) {
                continue;
            }
            for (InsProduct product : sample.getInsProduct()) {
                // 绑定项目仅参与计算,不应出现在报告中。
                if (product == null || Objects.equals(product.getIsBinding(), 1)) {
                    continue;
                }
                String itemName = defaultReportText(
                        product.getInspectionItemSubclass(),
                        defaultReportText(product.getInspectionItem(), product.getInspectionItemClass()));
                if (multipleSamples && StringUtils.isNotBlank(sample.getSampleCode())) {
                    itemName = sample.getSampleCode() + " - " + itemName;
                }
                Map<String, String> row = new LinkedHashMap<>();
                row.put("seq", String.valueOf(sequence++));
                row.put("itemName", itemName);
                row.put("unit", defaultReportText(product.getUnit(), "/"));
                row.put("result", defaultReportText(product.getLastValue(), "/"));
                rows.add(row);
            }
        }
        return rows;
    }
    /**
     * 新聚直绑模板订单:模板中每一个“导出值”批注都对应报告中的一条检测项目。
     */
    private List<Map<String, String>> buildTemplateDetectionResultRows(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        List<Map<String, String>> rows = new ArrayList<>();
        int sequence = 1;
        for (InsOrderStandardTemplate snapshot : snapshots) {
            List<TemplateExportCell> exportCells = getTemplateExportCells(snapshot.getThing());
            JSONObject recordValues = StrUtil.isBlank(snapshot.getRecordValues())
                    ? new JSONObject() : JSON.parseObject(snapshot.getRecordValues());
            for (TemplateExportCell exportCell : exportCells) {
                String result = readTemplateCellText(recordValues, exportCell.resultCoordinate, exportCell.templateValues);
                if (StringUtils.isBlank(result)) {
                    throw new ErrorException("模板“" + defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId())
                            + "”的导出值不能为空(" + exportCell.resultCoordinate + ")");
                }
                String unit = exportCell.unitCoordinate == null ? null
                        : readTemplateCellText(recordValues, exportCell.unitCoordinate, exportCell.templateValues);
                String itemName = exportCell.itemCoordinate == null ? null
                        : readTemplateCellText(recordValues, exportCell.itemCoordinate, exportCell.templateValues);
                Map<String, String> row = new LinkedHashMap<>();
                row.put("seq", String.valueOf(sequence++));
                String reportItemName = defaultReportText(itemName,
                        defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId()));
                row.put("itemName", formatExportItemName(reportItemName));
                row.put("unit", defaultReportText(unit, "/"));
                row.put("result", result);
                rows.add(row);
            }
        }
        if (rows.isEmpty()) {
            throw new ErrorException("当前订单未找到可生成报告的原始记录模板");
        }
        return rows;
    }
    private List<TemplateExportCell> getTemplateExportCells(String compressedThing) {
        if (StrUtil.isBlank(compressedThing)) {
            throw new ErrorException("原始记录模板为空");
        }
        String thing;
        try {
            thing = GZipUtil.uncompress(compressedThing);
        } catch (Exception ignored) {
            thing = compressedThing;
        }
        JSONArray cellData = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0).getJSONArray("celldata");
        Integer resultColumn = null;
        Integer itemColumn = null;
        Integer unitColumn = null;
        List<int[]> exportCells = new ArrayList<>();
        Map<String, String> templateValues = new HashMap<>();
        for (Object item : cellData) {
            JSONObject cell = JSON.parseObject(JSON.toJSONString(item));
            Integer row = cell.getInteger("r");
            Integer column = cell.getInteger("c");
            JSONObject value = cell.getJSONObject("v");
            if (row == null || column == null || value == null) {
                continue;
            }
            String displayValue = value.get("v") == null ? null : String.valueOf(value.get("v"));
            templateValues.put(row + "-" + column, displayValue);
            if (resultColumn == null && "检验结果".equals(StringUtils.trim(displayValue))) {
                resultColumn = column;
            }
            if (itemColumn == null && ("检测项目".equals(StringUtils.trim(displayValue))
                    || "检验项目".equals(StringUtils.trim(displayValue)))) {
                itemColumn = column;
            }
            if (unitColumn == null && "单位".equals(StringUtils.trim(displayValue))) {
                unitColumn = column;
            }
            JSONObject ps = value.getJSONObject("ps");
            if (ps != null && "导出值".equals(ps.getString("value"))) {
                exportCells.add(new int[]{row, column});
            }
        }
        if (exportCells.isEmpty()) {
            throw new ErrorException("模板必须至少配置一个导出值批注");
        }
        exportCells.sort(Comparator.comparingInt((int[] cell) -> cell[0]).thenComparingInt(cell -> cell[1]));
        List<TemplateExportCell> results = new ArrayList<>();
        for (int[] exportCell : exportCells) {
            int resultColumnForRow = resultColumn == null ? exportCell[1] : resultColumn;
            // 通常“导出值”批注配置在项目名称单元格;若配置在结果单元格,则回到“检测项目”列取名称。
            Integer itemColumnForRow = !Objects.equals(exportCell[1], resultColumnForRow)
                    ? exportCell[1] : itemColumn;
            TemplateExportCell result = new TemplateExportCell();
            result.resultCoordinate = exportCell[0] + "-" + resultColumnForRow;
            result.itemCoordinate = itemColumnForRow == null ? null : exportCell[0] + "-" + itemColumnForRow;
            result.unitCoordinate = unitColumn == null ? null : exportCell[0] + "-" + unitColumn;
            result.templateValues = templateValues;
            results.add(result);
        }
        return results;
    }
    private String readTemplateCellText(JSONObject recordValues, String coordinate, Map<String, String> templateValues) {
        JSONObject recordCell = recordValues == null ? null : recordValues.getJSONObject(coordinate);
        if (recordCell != null && recordCell.get("v") != null) {
            String value = String.valueOf(recordCell.get("v"));
            if (StringUtils.isNotBlank(value)) {
                return value;
            }
        }
        return templateValues.get(coordinate);
    }
    private static class TemplateExportCell {
        private String itemCoordinate;
        private String resultCoordinate;
        private String unitCoordinate;
        private Map<String, String> templateValues;
    }
    private static String buildEquipmentText(List<Map<String, String>> deviceList) {
        if (CollectionUtils.isEmpty(deviceList)) {
            return "/";
        }
        Set<String> lines = new LinkedHashSet<>();
        int sequence = 1;
        for (Map<String, String> device : deviceList) {
            String name = getMapText(device, "deviceName", "device_name");
            String managementNumber = getMapText(device, "managementNumber", "management_number");
            String line = sequence++ + ". " + defaultReportText(name, "/");
            if (StringUtils.isNotBlank(managementNumber)) {
                line += ":" + managementNumber;
            }
            lines.add(line);
        }
        return String.join("\n", lines);
    }
    private String buildReportEnvironment(Integer orderId, InsOrder order) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .select(InsOrderStandardTemplate::getTemperature,
                                InsOrderStandardTemplate::getHumidity));
        List<String> temperatures = snapshots.stream()
                .map(InsOrderStandardTemplate::getTemperature)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toList());
        List<String> humidities = snapshots.stream()
                .map(InsOrderStandardTemplate::getHumidity)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toList());
        String temperature = formatConditionRange(temperatures, order.getTemperature(), "℃");
        String humidity = formatConditionRange(humidities, order.getHumidity(), "%");
        return Stream.of(temperature, humidity)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining(" "));
    }
    private static String formatConditionRange(List<String> values, String fallback, String unit) {
        List<BigDecimal> numbers = new ArrayList<>();
        List<String> candidates = CollectionUtils.isEmpty(values)
                ? Collections.singletonList(fallback) : values;
        Pattern numberPattern = Pattern.compile("-?\\d+(?:\\.\\d+)?");
        for (String value : candidates) {
            if (StringUtils.isBlank(value)) {
                continue;
            }
            java.util.regex.Matcher matcher = numberPattern.matcher(value);
            while (matcher.find()) {
                numbers.add(new BigDecimal(matcher.group()));
            }
        }
        if (numbers.isEmpty()) {
            // “/”表示模板明确填写了不适用,不能再按空值处理或追加单位。
            return candidates.stream()
                    .filter(StringUtils::isNotBlank)
                    .map(String::trim)
                    .distinct()
                    .collect(Collectors.joining("~"));
        }
        BigDecimal minimum = Collections.min(numbers);
        BigDecimal maximum = Collections.max(numbers);
        String minimumText = minimum.stripTrailingZeros().toPlainString();
        if (minimum.compareTo(maximum) == 0) {
            return minimumText + unit;
        }
        return minimumText + "~" + maximum.stripTrailingZeros().toPlainString() + unit;
    }
    /**
     * 报告项目名称不展示模板计算过程中的“平均值”字样及其后的分隔逗号。
     */
    private static String formatExportItemName(String itemName) {
        if (StringUtils.isBlank(itemName)) {
            return itemName;
        }
        return itemName.replaceAll("平均值\\s*[,,]?\\s*", "");
    }
    private static String getMapText(Map<String, String> map, String... keys) {
        if (map == null) {
            return null;
        }
        for (String key : keys) {
            String value = map.get(key);
            if (StringUtils.isNotBlank(value)) {
                return value;
            }
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)
                        && StringUtils.isNotBlank(entry.getValue())) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }
    private static String defaultReportText(String value, String fallback) {
        if (StringUtils.isNotBlank(value)) {
            return value;
        }
        return StringUtils.isNotBlank(fallback) ? fallback : "/";
    }
    /**
     * 计算表格列宽度
     *
     * @param max 最大检验个数
     * @return
     */
    private static int[] calcTableColWidths(int max, boolean showResult){
    private static int[] calcTableColWidths(int max, boolean showResult) {
        if (!showResult) {
            switch (max) {
                case 1: return new int[]{650, 2200, 2200, 850, 2300, 1800};
                case 2: return new int[]{650, 1600, 1600, 850, 1900, 1700, 1700};
                case 3: return new int[]{650, 1400, 1400, 850, 1600, 1375, 1375, 1375};
                case 4: return new int[]{650, 1250, 1250, 850, 1450, 1140, 1140, 1140, 1140};
                case 5: return new int[]{650, 1150, 1150, 850, 1300, 980, 980, 980, 980, 980};
                default: return new int[0];
                case 1:
                    return new int[]{650, 2200, 2200, 850, 2300, 1800};
                case 2:
                    return new int[]{650, 1600, 1600, 850, 1900, 1700, 1700};
                case 3:
                    return new int[]{650, 1400, 1400, 850, 1600, 1375, 1375, 1375};
                case 4:
                    return new int[]{650, 1250, 1250, 850, 1450, 1140, 1140, 1140, 1140};
                case 5:
                    return new int[]{650, 1150, 1150, 850, 1300, 980, 980, 980, 980, 980};
                default:
                    return new int[0];
            }
        }
        int[] colWidths = null;
@@ -2914,16 +3865,16 @@
                colWidths = new int[]{650, 1100, 1100, 850, 1350, 750, 750, 750, 750, 750, 1200};
                break;
        }
        if(ObjectUtils.isNull(colWidths)){
        if (ObjectUtils.isNull(colWidths)) {
            List<Integer> defaultColWidths = new ArrayList<>(Arrays.asList(650, 1100, 1100, 850, 1350));
            int totalWidth = 3000;//总宽度
            int byOneWidth = totalWidth/max;//每一个的宽度
            int byOneWidth = totalWidth / max;//每一个的宽度
            int realWidth = 0;//实际宽度
            for (int i = 0; i < max; i++) {
                realWidth+=byOneWidth;
                realWidth += byOneWidth;
                defaultColWidths.add(byOneWidth);
            }
            defaultColWidths.add(1200+(totalWidth-realWidth));
            defaultColWidths.add(1200 + (totalWidth - realWidth));
            return defaultColWidths.stream().mapToInt(Integer::intValue).toArray();
        }
        return colWidths;
@@ -2931,6 +3882,7 @@
    /**
     * 调整高度
     *
     * @param row
     * @param rowHeight
     * @return
@@ -3162,12 +4114,13 @@
    /**
     * 添加报告表头
     *
     * @param sample
     * @param text
     * @param rowRenderData
     * @param rows
     * @param max
     * @param cableTags 线芯颜色
     * @param cableTags     线芯颜色
     */
    private static void addHead(SampleProductDto sample, List<TextRenderData> text, RowRenderData rowRenderData, List<RowRenderData> rows, int max, List<String> cableTags, boolean showResult) {
        // 第一行
@@ -3246,6 +4199,11 @@
            }
        }
        // 检测报告不展示“结论”列,首行也要同步减少一个单元格,
        // 否则首行为 max + 6 列、其余行为 max + 5 列,poi-tl 无法渲染表格。
        if (!showResult && !text.isEmpty()) {
            text.remove(text.size() - 1);
        }
        TextRenderData[] text2 = text.toArray(new TextRenderData[0]);
        rowRenderData = Rows.of(text2).center().rowAtleastHeight(1).create();
        rows.add(rowRenderData);
@@ -3302,13 +4260,13 @@
            text.add(headRenderData6);
        }
        if (showResult) {
        TextRenderData headRenderData7 = new TextRenderData();
        headRenderData7.setText("结论@Conclusion");
        Style headStyle7 = new Style();
        headStyle7.setFontFamily("宋体");
        headStyle7.setColor("000000");
        headRenderData7.setStyle(headStyle7);
        text.add(headRenderData7);
            TextRenderData headRenderData7 = new TextRenderData();
            headRenderData7.setText("结论@Conclusion");
            Style headStyle7 = new Style();
            headStyle7.setFontFamily("宋体");
            headStyle7.setColor("000000");
            headRenderData7.setStyle(headStyle7);
            text.add(headRenderData7);
        }
        TextRenderData[] text3 = text.toArray(new TextRenderData[0]);
@@ -3368,13 +4326,13 @@
                text.add(cableRenderData6);
            }
            if (showResult) {
            TextRenderData cableRenderData7 = new TextRenderData();
            cableRenderData7.setText("-");
            Style cableStyle7 = new Style();
            cableStyle7.setFontFamily("宋体");
            cableStyle7.setColor("000000");
            cableRenderData7.setStyle(cableStyle7);
            text.add(cableRenderData7);
                TextRenderData cableRenderData7 = new TextRenderData();
                cableRenderData7.setText("-");
                Style cableStyle7 = new Style();
                cableStyle7.setFontFamily("宋体");
                cableStyle7.setColor("000000");
                cableRenderData7.setStyle(cableStyle7);
                text.add(cableRenderData7);
            }
            TextRenderData[] text4 = text.toArray(new TextRenderData[0]);
@@ -3389,16 +4347,17 @@
    /**
     * 添加检测值
     * @param a   当前样品
     *
     * @param a             当前样品
     * @param text
     * @param rowRenderData
     * @param rows
     * @param max  检验数量
     * @param max           检验数量
     * @param resultChList  不符合信息中文
     * @param resultEhList  不符合信息英文
     * @param insSamples
     * @param cableTags 线芯颜色
     * @param isOneSample 判断是否是只有一个样品
     * @param cableTags     线芯颜色
     * @param isOneSample   判断是否是只有一个样品
     */
    private int addTestValue(SampleProductDto a,
                             List<TextRenderData> text,
@@ -3933,74 +4892,74 @@
            }
            // 判定结果
            if (showResult) {
            String result = "";
            if (sample.getInsResult() != null) {
                switch (sample.getInsResult()) {
                    case 1:
                        result = "√";
                        break;
                    case 2:
                        result = "×";
                        break;
                    case 3:
                        result = "-";
                        break;
                }
            }
            if (CollectionUtils.isNotEmpty(sample.getLastValueList())) {
                // 判断是否有一个错误
                if (sample.getInsResultList().stream().anyMatch(s -> Objects.equals(s, 3))) {
                    result = "-";
                } else {
                    boolean error = sample.getInsResultList().stream().anyMatch(s -> Objects.equals(s, 0));
                    if (error) {
                        List<String> collect = new ArrayList<>();
                        int index = 0;
                        for (Integer count : sample.getInsResultList()) {
                            String type;
                            if (Objects.equals(count, 0)) {
                                String itemCh = "";
                                String itemEn = "";
                                // 添加不合格描述
                                // 判断长度是否为1
                                if (sample.getLastValueList().size() == 1) {
                                    this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                } else if (CollectionUtils.isNotEmpty(cableTags)) {
                                    // 判断是否有电缆配置, 不是的话可能为原材料
                                    // 添加不合格描述
                                    itemCh = (max == 1 ? "" : cableTags.get(index));
                                    itemEn = (max == 1 ? "" : "The " + Integer.toString(index + 1) + " time ");
                                    this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                } else {
                                    // 添加不合格描述
                                    itemCh = (max == 1 ? "" : "第" + Integer.toString(index + 1) + "次");
                                    itemEn = (max == 1 ? "" : "The " + Integer.toString(index + 1) + " time ");
                                    this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                }
                                type = "×";
                            } else {
                                type = "√";
                            }
                            collect.add(type);
                            index++;
                        }
                        result = CollUtil.join(collect, " ");
                        ;
                    } else {
                        result = "√";
                String result = "";
                if (sample.getInsResult() != null) {
                    switch (sample.getInsResult()) {
                        case 1:
                            result = "√";
                            break;
                        case 2:
                            result = "×";
                            break;
                        case 3:
                            result = "-";
                            break;
                    }
                }
            }
                if (CollectionUtils.isNotEmpty(sample.getLastValueList())) {
                    // 判断是否有一个错误
                    if (sample.getInsResultList().stream().anyMatch(s -> Objects.equals(s, 3))) {
                        result = "-";
                    } else {
                        boolean error = sample.getInsResultList().stream().anyMatch(s -> Objects.equals(s, 0));
                        if (error) {
                            List<String> collect = new ArrayList<>();
                            int index = 0;
                            for (Integer count : sample.getInsResultList()) {
                                String type;
                                if (Objects.equals(count, 0)) {
                                    String itemCh = "";
                                    String itemEn = "";
                                    // 添加不合格描述
                                    // 判断长度是否为1
                                    if (sample.getLastValueList().size() == 1) {
                                        this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                    } else if (CollectionUtils.isNotEmpty(cableTags)) {
                                        // 判断是否有电缆配置, 不是的话可能为原材料
                                        // 添加不合格描述
                                        itemCh = (max == 1 ? "" : cableTags.get(index));
                                        itemEn = (max == 1 ? "" : "The " + Integer.toString(index + 1) + " time ");
                                        this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                    } else {
                                        // 添加不合格描述
                                        itemCh = (max == 1 ? "" : "第" + Integer.toString(index + 1) + "次");
                                        itemEn = (max == 1 ? "" : "The " + Integer.toString(index + 1) + " time ");
                                        this.fillReportErrorResult(errorClassItemMapCn, errorClassItemMapEn, sample, itemCh, itemEn);
                                    }
                                    type = "×";
                                } else {
                                    type = "√";
                                }
                                collect.add(type);
                                index++;
                            }
                            result = CollUtil.join(collect, " ");
                            ;
                        } else {
                            result = "√";
                        }
                    }
                }
            if (showResult) {
            TextRenderData headRenderData7 = new TextRenderData();
            headRenderData7.setText(result);
            Style headStyle7 = new Style();
            headStyle7.setFontFamily("宋体");
            headStyle7.setColor("000000");
            headRenderData7.setStyle(headStyle7);
            text.add(headRenderData7);
            }
                if (showResult) {
                    TextRenderData headRenderData7 = new TextRenderData();
                    headRenderData7.setText(result);
                    Style headStyle7 = new Style();
                    headStyle7.setFontFamily("宋体");
                    headStyle7.setColor("000000");
                    headRenderData7.setStyle(headStyle7);
                    text.add(headRenderData7);
                }
            }
            TextRenderData[] text2 = text.toArray(new TextRenderData[0]);
@@ -4082,11 +5041,12 @@
    /**
     * NBSP(非间断空格) 转成 普通空格
     *
     * @param oldStr
     * @return
     */
    private String normalizedSpaces(String oldStr){
        if(StringUtils.isBlank(oldStr)){
    private String normalizedSpaces(String oldStr) {
        if (StringUtils.isBlank(oldStr)) {
            return oldStr;
        }
        return oldStr.replaceAll("\u00A0", " ").trim();
@@ -4094,6 +5054,7 @@
    /**
     * 添加报告结论中英文
     *
     * @param sample
     * @param itemCh
     * @param itemEn
@@ -4153,6 +5114,7 @@
    /**
     * 格式化修改检验项
     *
     * @param sampleProductDto2s
     */
    private void formatProducts(List<SampleProductExportDto> sampleProductDto2s) {
@@ -4222,6 +5184,7 @@
    /**
     * 添加结尾
     *
     * @param text
     * @param rowRenderData
     * @param rows
@@ -4253,6 +5216,7 @@
    /**
     * 判断当前内容是否是科学计数法
     *
     * @param str
     * @return
     */
@@ -4269,6 +5233,7 @@
    /**
     * 修改要求描述的科学计数法
     *
     * @param input
     */
    public static String convertToScientificNotation(String input) {
@@ -4298,6 +5263,7 @@
    /**
     * 展示成科学计数法
     *
     * @param number
     * @return
     */
@@ -4394,6 +5360,7 @@
    /**
     * 根据要求描述保留结果小数点位数
     *
     * @param reference
     * @param value
     * @return
@@ -4462,6 +5429,7 @@
    /**
     * 保留位数, 如果等于0 返回找到的一个非0位数
     *
     * @param number 当前数字
     * @param scale  原本保留的位数
     * @return
@@ -4500,9 +5468,10 @@
    /**
     * *****保存元此阿里进货验证原始记录*****
     * @param insOrderId 订单Id
     * @param examineUserId  复核人Id
     * @param writeUserId  检验员Id
     *
     * @param insOrderId    订单Id
     * @param examineUserId 复核人Id
     * @param writeUserId   检验员Id
     */
    private void reportFactoryVerify(Integer insOrderId, Integer examineUserId, Integer writeUserId) {
        // 查询进货验证原始记录
@@ -4655,6 +5624,7 @@
    /**
     * ***格式化进厂验证内容****
     *
     * @param basicType
     * @return
     */
@@ -4684,6 +5654,7 @@
    /**
     * ***word转换pdf***
     *
     * @param path
     * @return
     */
@@ -4732,6 +5703,7 @@
    /**
     * *****修改成品抽样状态******
     *
     * @param insSamples
     * @param order
     */
@@ -4769,6 +5741,7 @@
    /**
     * 添加工时
     *
     * @param userId
     * @param insProduct
     * @param insOrder