basic-server/src/main/java/com/ruoyi/basic/utils/StandardTemplateInspectionItemParser.java
@@ -13,9 +13,10 @@ import java.util.List; import java.util.Set; /** 解析 Luckysheet 原始记录模板中的“检验项”标记。 */ /** 解析 Luckysheet 原始记录模板中的“检验项”;未标记时回退到“导出值”行。 */ public final class StandardTemplateInspectionItemParser { private static final String INSPECTION_ITEM_MARK = "检验项"; private static final String EXPORT_VALUE_MARK = "导出值"; private StandardTemplateInspectionItemParser() { } @@ -34,11 +35,22 @@ } catch (Exception e) { throw new BaseException("原始记录模板格式错误"); } List<TemplateInspectionItemDto> result = new ArrayList<>(); Set<Integer> rows = new HashSet<>(); if (sheets == null) { throw new BaseException("原始记录模板格式错误"); } List<TemplateInspectionItemDto> result = parseMarkedCells(sheets, templateId, templateSort, INSPECTION_ITEM_MARK); if (result.isEmpty()) { result = parseMarkedCells(sheets, templateId, templateSort, EXPORT_VALUE_MARK); } result.sort(Comparator.comparing(TemplateInspectionItemDto::getTemplateRowIndex)); return result; } private static List<TemplateInspectionItemDto> parseMarkedCells(JSONArray sheets, Integer templateId, Integer templateSort, String mark) { List<TemplateInspectionItemDto> result = new ArrayList<>(); Set<Integer> rows = new HashSet<>(); for (Object sheetObject : sheets) { JSONObject sheet = (JSONObject) sheetObject; JSONArray cells = sheet.getJSONArray("celldata"); @@ -48,16 +60,19 @@ for (Object cellObject : cells) { JSONObject cell = (JSONObject) cellObject; JSONObject value = cell.getJSONObject("v"); if (value == null || !isInspectionItem(value)) { if (value == null || !hasMark(value, mark)) { continue; } Integer row = cell.getInteger("r"); String item = getCellText(value); if (row == null || !StringUtils.hasText(item)) { throw new BaseException("“检验项”标记所在单元格必须填写检验项名称"); throw new BaseException("“" + mark + "”标记所在单元格必须填写检验项名称"); } if (!rows.add(row)) { throw new BaseException("同一模板行只能配置一个“检验项”标记"); if (EXPORT_VALUE_MARK.equals(mark)) { continue; } throw new BaseException("同一模板行只能配置一个“" + mark + "”标记"); } TemplateInspectionItemDto dto = new TemplateInspectionItemDto(); dto.setTemplateId(templateId); @@ -67,7 +82,6 @@ result.add(dto); } } result.sort(Comparator.comparing(TemplateInspectionItemDto::getTemplateRowIndex)); return result; } @@ -101,8 +115,8 @@ return richText.toString(); } private static boolean isInspectionItem(JSONObject value) { private static boolean hasMark(JSONObject value, String mark) { JSONObject ps = value.getJSONObject("ps"); return ps != null && INSPECTION_ITEM_MARK.equals(ps.getString("value")); return ps != null && mark.equals(ps.getString("value")); } } inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrder.java
@@ -42,6 +42,9 @@ @Schema(description = "质量监控计划明细ID") private Integer qualityMonitorDetailsId; @Schema(description = "新聚直绑模板下单时选择的标准库二级节点ID") private Integer standardTreeId; @Schema(description = "下单客户") private String custom; inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrderStandardTemplate.java
@@ -64,6 +64,9 @@ @Schema(description = "原始记录已填写单元格,按坐标保存") private String recordValues; @Schema(description = "默认设备是否已在首次进入检验任务时初始化:0否,1是") private Integer deviceDefaultsInitialized; @Schema(description = "该模板检验时的温度") private String temperature; inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -1659,6 +1659,8 @@ refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark()); refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing()); refreshed.setRecordValues(templateRecordValuesMap.get(templateId)); refreshed.setDeviceDefaultsInitialized(oldSnapshot == null ? 1 : oldSnapshot.getDeviceDefaultsInitialized()); refreshed.setTemperature(templateTemperatureMap.get(templateId)); refreshed.setHumidity(templateHumidityMap.get(templateId)); // 刷新模板结构时必须保留检验阶段填写的业务数据,避免进入复核后回显为空。 inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java
@@ -581,6 +581,9 @@ .thenComparing(item -> item.getTemplateRowIndex() == null ? Integer.MAX_VALUE : item.getTemplateRowIndex())) .collect(Collectors.toList()); if (templateItems.isEmpty()) { throw new ErrorException("所选原始记录模板未配置检验项或导出值,不能下单"); } StandardMethod standardMethod = standardMethodService.getById(standard.getStandardMethodId()); if (standardMethod == null || StringUtils.isBlank(standardMethod.getCode())) { @@ -596,6 +599,7 @@ order.setIsExemption(0); order.setFactory(null); order.setLaboratory(standard.getLaboratory()); order.setStandardTreeId(dto.getStandardTreeId()); order.setSampleType(null); order.setSample(standardCode); order.setPrepareUserEn(null); @@ -740,6 +744,9 @@ .sorted(Comparator.comparing((TemplateInspectionItemDto item) -> templateSortMap.get(item.getTemplateId())) .thenComparing(item -> item.getTemplateRowIndex() == null ? Integer.MAX_VALUE : item.getTemplateRowIndex())) .collect(Collectors.toList()); if (templateItems.isEmpty()) { throw new ErrorException("所选原始记录模板未配置检验项或导出值,不能下单"); } StandardMethod standardMethod = standardMethodService.getById(standard.getStandardMethodId()); if (standardMethod == null || StringUtils.isBlank(standardMethod.getCode())) { throw new ErrorException("标准编号不存在"); @@ -754,6 +761,7 @@ order.setIsExemption(currentOrder.getIsExemption()); order.setFactory(null); order.setLaboratory(standard.getLaboratory()); order.setStandardTreeId(dto.getStandardTreeId()); order.setSampleType(null); order.setSample(standardCode); order.setPrepareUserEn(null); @@ -839,6 +847,7 @@ snapshot.setRemark(template.getRemark()); snapshot.setThing(template.getThing()); snapshot.setSort(selection.getSort()); snapshot.setDeviceDefaultsInitialized(0); insOrderStandardTemplateMapper.insert(snapshot); StandardTreeTemplateBindingVo binding = bindingMap.get(selection.getTemplateId()); @@ -929,7 +938,9 @@ .eq(StandardMethod::getCode, insOrder.getSample()) .select(StandardMethod::getId)).stream() .map(StandardMethod::getId).collect(Collectors.toList()); if (!methodIds.isEmpty() && !orderTemplateIds.isEmpty()) { if (insOrder.getStandardTreeId() != null) { map.put("standardTreeId", insOrder.getStandardTreeId()); } else if (!methodIds.isEmpty() && !orderTemplateIds.isEmpty()) { List<StandardTree> candidates = standardTreeService.list(Wrappers.<StandardTree>lambdaQuery() .in(StandardTree::getStandardMethodId, methodIds) .isNull(StandardTree::getSampleType) @@ -1053,8 +1064,10 @@ } @Override @Transactional(rollbackFor = Exception.class) public Map<String, Object> getInsOrderAndSample(Integer id, String laboratory) { Map<String, Object> map = new HashMap<>(); initializeTemplateDevicesOnFirstOpen(id); // InsOrder insOrder = insOrderMapper.selectById(id); IfsOrderVO ifsOrderVO = insOrderMapper.selectOrderInfoById(id); Map<Integer, List<Integer>> templateDeviceIds = insOrderTemplateDeviceMapper.selectList( @@ -1097,6 +1110,127 @@ return map; } /** 首次打开检验任务时,将仍为空的模板设备从对应标准补入订单。 */ private void initializeTemplateDevicesOnFirstOpen(Integer orderId) { InsOrder order = insOrderMapper.selectById(orderId); if (order == null || !DIRECT_TEMPLATE_LABORATORY.equals(order.getLaboratory()) || !Objects.equals(order.getTypeSource(), -1) || !Objects.equals(order.getState(), 1) || "QUALITY_MONITOR".equals(order.getTaskSource())) { return; } List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList( Wrappers.<InsOrderStandardTemplate>lambdaQuery() .eq(InsOrderStandardTemplate::getInsOrderId, orderId) .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId) .last("FOR UPDATE")); if (snapshots.isEmpty() || snapshots.stream().allMatch( snapshot -> Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1))) { return; } Set<Integer> existingTemplateIds = insOrderTemplateDeviceMapper.selectList( Wrappers.<InsOrderTemplateDevice>lambdaQuery() .eq(InsOrderTemplateDevice::getInsOrderId, orderId)) .stream().map(InsOrderTemplateDevice::getTemplateId) .filter(Objects::nonNull).collect(Collectors.toSet()); boolean inspectionStarted = order.getFirstInspectDate() != null || (order.getInsState() != null && order.getInsState() > 0) || snapshots.stream().anyMatch(snapshot -> StringUtils.isNotBlank(snapshot.getRecordValues()) || snapshot.getDetectionTime() != null || snapshot.getTemperature() != null || snapshot.getDetectionPlace() != null); Map<Integer, List<Integer>> defaults = inspectionStarted ? Collections.emptyMap() : findStandardDeviceDefaults(order, snapshots); for (InsOrderStandardTemplate snapshot : snapshots) { if (Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1)) { continue; } Integer templateId = snapshot.getTemplateId(); if (!inspectionStarted && !existingTemplateIds.contains(templateId) && defaults.containsKey(templateId)) { List<Integer> deviceIds = defaults.get(templateId); for (int index = 0; index < deviceIds.size(); index++) { InsOrderTemplateDevice relation = new InsOrderTemplateDevice(); relation.setInsOrderId(orderId); relation.setTemplateId(templateId); relation.setDeviceId(deviceIds.get(index)); relation.setSort(index); insOrderTemplateDeviceMapper.insert(relation); } } // 无法唯一确定来源时也只尝试这一次,避免以后标准库调整时意外改动订单。 insOrderStandardTemplateMapper.update(null, Wrappers.<InsOrderStandardTemplate>lambdaUpdate() .eq(InsOrderStandardTemplate::getId, snapshot.getId()) .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1)); } } private Map<Integer, List<Integer>> findStandardDeviceDefaults(InsOrder order, List<InsOrderStandardTemplate> snapshots) { Set<Integer> templateIds = snapshots.stream().map(InsOrderStandardTemplate::getTemplateId) .filter(Objects::nonNull).collect(Collectors.toSet()); Integer standardTreeId = order.getStandardTreeId(); if (standardTreeId == null && StringUtils.isNotBlank(order.getSample()) && !templateIds.isEmpty()) { List<Integer> methodIds = standardMethodService.list(Wrappers.<StandardMethod>lambdaQuery() .eq(StandardMethod::getCode, order.getSample()) .select(StandardMethod::getId)).stream() .map(StandardMethod::getId).collect(Collectors.toList()); if (!methodIds.isEmpty()) { List<StandardTree> candidates = standardTreeService.list(Wrappers.<StandardTree>lambdaQuery() .in(StandardTree::getStandardMethodId, methodIds) .eq(StandardTree::getLaboratory, order.getLaboratory()) .isNotNull(StandardTree::getSample) .isNull(StandardTree::getSampleType) .isNull(StandardTree::getModel)); List<StandardTree> matches = candidates.stream() .filter(tree -> StringUtils.isNotBlank(tree.getSample())) .filter(tree -> { Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream() .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet()); return boundIds.containsAll(templateIds); }).collect(Collectors.toList()); if (matches.size() == 1) { standardTreeId = matches.get(0).getId(); } else { List<StandardTree> exact = matches.stream().filter(tree -> standardTreeService.selectTemplateBindings(tree.getId()).stream() .map(StandardTreeTemplateBindingVo::getTemplateId) .collect(Collectors.toSet()).equals(templateIds)) .collect(Collectors.toList()); if (exact.size() == 1) { standardTreeId = exact.get(0).getId(); } } } } if (standardTreeId == null) { return Collections.emptyMap(); } StandardTree standard = standardTreeService.getById(standardTreeId); if (standard == null || StringUtils.isBlank(standard.getSample()) || !Objects.equals(standard.getLaboratory(), order.getLaboratory()) || standard.getSampleType() != null || standard.getModel() != null) { return Collections.emptyMap(); } if (order.getStandardTreeId() == null) { insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate() .eq(InsOrder::getId, order.getId()) .isNull(InsOrder::getStandardTreeId) .set(InsOrder::getStandardTreeId, standardTreeId)); } List<StandardTreeTemplateBindingVo> bindings = standardTreeService.selectTemplateBindings(standardTreeId); Map<Integer, List<Integer>> defaults = new HashMap<>(); for (StandardTreeTemplateBindingVo binding : bindings) { if (templateIds.contains(binding.getTemplateId())) { defaults.put(binding.getTemplateId(), binding.getDeviceIds() == null ? Collections.emptyList() : binding.getDeviceIds()); } } return defaults; } @Override public IPage<SampleProductDto2> selectSampleAndProductByOrderId(IPage<SampleProductDto2> page, SampleProductDto2 sampleProductDto) { IPage<SampleProductDto2> productDto2IPage = insOrderMapper.selectSampleAndProductByOrderId(page, inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java
@@ -94,6 +94,11 @@ InsOrderStandardTemplate::getInsResult, dto.getInsResult())); } if (dto.isDeviceIdsPresent()) { // 手动选择或清空都视为已初始化,后续进入任务不得再次套用标准库默认值。 insOrderStandardTemplateMapper.update(null, Wrappers.<InsOrderStandardTemplate>lambdaUpdate() .eq(InsOrderStandardTemplate::getId, snapshot.getId()) .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1)); insOrderTemplateDeviceMapper.delete( Wrappers.<InsOrderTemplateDevice>lambdaQuery() .eq(InsOrderTemplateDevice::getInsOrderId, dto.getId()) inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsSampleReceiptServiceImpl.java
@@ -4,46 +4,44 @@ import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.basic.dto.TemplateInspectionItemDto; import com.ruoyi.basic.utils.StandardTemplateInspectionItemParser; import com.ruoyi.common.utils.GZipUtil; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.exception.ErrorException; import com.ruoyi.inspect.dto.SampleReceiptActionDto; import com.ruoyi.inspect.dto.SampleReceiptPageDto; import com.ruoyi.inspect.dto.SampleReceiptQueryDto; import com.ruoyi.inspect.mapper.InsOrderMapper; import com.ruoyi.inspect.mapper.InsOrderStandardTemplateMapper; import com.ruoyi.inspect.mapper.InsProductMapper; import com.ruoyi.inspect.mapper.InsSampleMapper; import com.ruoyi.inspect.mapper.InsSampleReceiptMapper; import com.ruoyi.inspect.mapper.InsSampleUserMapper; import com.ruoyi.inspect.pojo.InsOrder; import com.ruoyi.inspect.pojo.InsOrderStandardTemplate; import com.ruoyi.inspect.pojo.InsProduct; import com.ruoyi.inspect.pojo.InsSample; import com.ruoyi.inspect.pojo.InsSampleReceipt; import com.ruoyi.inspect.pojo.InsSampleUser; import com.ruoyi.inspect.mapper.*; import com.ruoyi.inspect.pojo.*; import com.ruoyi.inspect.service.InsOrderService; import com.ruoyi.inspect.service.InsOrderStateService; import com.ruoyi.inspect.service.InsSampleReceiptService; import com.ruoyi.system.mapper.UserMapper; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @Service @AllArgsConstructor public class InsSampleReceiptServiceImpl extends ServiceImpl<InsSampleReceiptMapper, InsSampleReceipt> implements InsSampleReceiptService { private static final String DIRECT_TEMPLATE_LABORATORY = "新聚"; private final InsSampleReceiptMapper receiptMapper; private final InsOrderMapper insOrderMapper; private final InsOrderStandardTemplateMapper insOrderStandardTemplateMapper; @@ -55,13 +53,17 @@ private final UserMapper userMapper; @Override public com.baomidou.mybatisplus.core.metadata.IPage<SampleReceiptPageDto> page(Page<?> page, SampleReceiptQueryDto query) { return receiptMapper.selectPage(page, query); public IPage<SampleReceiptPageDto> page(Page<?> page, SampleReceiptQueryDto query) { IPage<SampleReceiptPageDto> result = receiptMapper.selectPage(page, query); result.getRecords().forEach(this::fillInspectionItemsFromSnapshots); return result; } @Override public SampleReceiptPageDto detail(Integer id) { return receiptMapper.selectDetail(id); SampleReceiptPageDto detail = receiptMapper.selectDetail(id); fillInspectionItemsFromSnapshots(detail); return detail; } @Override @@ -91,7 +93,26 @@ Long productCount = insProductMapper.selectCount(Wrappers.<InsProduct>lambdaQuery() .eq(InsProduct::getInsSampleId, sample.getId()).eq(InsProduct::getState, 1)); if (productCount == 0) { throw new ErrorException("该委托未绑定检验项目,不能提交检验"); List<TemplateInspectionItemDto> items = getSnapshotInspectionItems(order.getId()); if (items.isEmpty()) { throw new ErrorException("订单原始记录模板未配置检验项或导出值,不能提交检验"); } int sort = 0; for (TemplateInspectionItemDto item : items) { InsProduct product = new InsProduct(); product.setInsSampleId(sample.getId()); product.setInspectionItem(item.getInspectionItem()); product.setInspectionItemClass(""); product.setInspectionItemSubclass(""); product.setLaboratory(order.getLaboratory()); product.setSample(order.getSample()); product.setTemplateId(item.getTemplateId()); product.setTemplateRowIndex(item.getTemplateRowIndex()); product.setSonLaboratory(DIRECT_TEMPLATE_LABORATORY); product.setState(1); product.setSort(sort++); insProductMapper.insert(product); } } if (userMapper.selectById(dto.getMainInspectorId()) == null) { throw new ErrorException("主检验人不存在或已停用"); @@ -160,9 +181,43 @@ } } private List<TemplateInspectionItemDto> getSnapshotInspectionItems(Integer orderId) { List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList( Wrappers.<InsOrderStandardTemplate>lambdaQuery() .eq(InsOrderStandardTemplate::getInsOrderId, orderId) .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId)); List<TemplateInspectionItemDto> items = new ArrayList<>(); for (InsOrderStandardTemplate snapshot : snapshots) { String thing = snapshot.getThing(); try { thing = GZipUtil.uncompress(thing); } catch (RuntimeException ignored) { // 兼容未压缩的历史模板快照。 } items.addAll(StandardTemplateInspectionItemParser.parse( snapshot.getTemplateId(), snapshot.getSort(), thing)); } return items; } private void fillInspectionItemsFromSnapshots(SampleReceiptPageDto row) { if (row == null || row.getOrderId() == null || StringUtils.isNotBlank(row.getInspectionItem())) { return; } try { String names = getSnapshotInspectionItems(row.getOrderId()).stream() .map(TemplateInspectionItemDto::getInspectionItem) .distinct().collect(Collectors.joining("\n")); row.setInspectionItem(names); } catch (RuntimeException ignored) { // 列表仍可查看;提交检验时会返回具体的模板解析错误。 } } @Override public void export(SampleReceiptQueryDto query, HttpServletResponse response) { List<SampleReceiptPageDto> list = receiptMapper.selectExportList(query); list.forEach(this::fillInspectionItemsFromSnapshots); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("UTF-8"); try {