diff --git a/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java b/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java index 3c8477d..8c9ff1e 100644 --- a/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java +++ b/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java @@ -114,12 +114,11 @@ public class SimBuilder { List mats = materialService.list(new QueryWrapper().in("material_id", mids)); for (Material m : mats) { String type = "unknown"; - // 简单推导规则: 优先判定 Pu,其次 U - if (m.getPuConcentration() != null && m.getPuConcentration().doubleValue() > 0) { - type = "Pu"; - } else if (m.getUConcentration() != null && m.getUConcentration().doubleValue() > 0) { - type = "U"; - } + boolean hasU = m.getUConcentration() != null && m.getUConcentration().doubleValue() > 0; + boolean hasPu = m.getPuConcentration() != null && m.getPuConcentration().doubleValue() > 0; + if (hasU && hasPu) type = "Mixed"; + else if (hasPu) type = "Pu"; + else if (hasU) type = "U"; out.put(m.getMaterialId(), type); } return out; @@ -151,27 +150,71 @@ public class SimBuilder { case "TubeBundleTank"://管束槽 parseCommonSize(sizeNode, staticProps); break; + case "PulsedCylindricalColumn": + if (sizeNode.has("upper_inner_diameter")) staticProps.put("upper_inner_diameter", sizeNode.get("upper_inner_diameter").asDouble()); + if (sizeNode.has("upper_outer_diameter")) staticProps.put("upper_outer_diameter", sizeNode.get("upper_outer_diameter").asDouble()); + if (sizeNode.has("upper_height")) staticProps.put("upper_height", sizeNode.get("upper_height").asDouble()); + if (sizeNode.has("tray_diameter")) staticProps.put("tray_diameter", sizeNode.get("tray_diameter").asDouble()); + if (sizeNode.has("tray_height")) staticProps.put("tray_height", sizeNode.get("tray_height").asDouble()); + if (sizeNode.has("lower_inner_diameter")) staticProps.put("lower_inner_diameter", sizeNode.get("lower_inner_diameter").asDouble()); + if (sizeNode.has("lower_outer_diameter")) staticProps.put("lower_outer_diameter", sizeNode.get("lower_outer_diameter").asDouble()); + if (sizeNode.has("lower_height")) staticProps.put("lower_height", sizeNode.get("lower_height").asDouble()); + // if (staticProps.containsKey("tray_diameter")) staticProps.put("diameter", staticProps.get("tray_diameter")); + // if (staticProps.containsKey("tray_height")) staticProps.put("height", staticProps.get("tray_height")); + break; + case "PulsedAnnularColumn": + if (sizeNode.has("upper_inner_diameter")) staticProps.put("upper_inner_diameter", sizeNode.get("upper_inner_diameter").asDouble()); + if (sizeNode.has("upper_outer_diameter")) staticProps.put("upper_outer_diameter", sizeNode.get("upper_outer_diameter").asDouble()); + if (sizeNode.has("upper_height")) staticProps.put("upper_height", sizeNode.get("upper_height").asDouble()); + if (sizeNode.has("tray_inner_diameter")) staticProps.put("tray_inner_diameter", sizeNode.get("tray_inner_diameter").asDouble()); + if (sizeNode.has("tray_outer_diameter")) staticProps.put("tray_outer_diameter", sizeNode.get("tray_outer_diameter").asDouble()); + if (sizeNode.has("tray_height")) staticProps.put("tray_height", sizeNode.get("tray_height").asDouble()); + if (sizeNode.has("lower_inner_diameter")) staticProps.put("lower_inner_diameter", sizeNode.get("lower_inner_diameter").asDouble()); + if (sizeNode.has("lower_outer_diameter")) staticProps.put("lower_outer_diameter", sizeNode.get("lower_outer_diameter").asDouble()); + if (sizeNode.has("lower_height")) staticProps.put("lower_height", sizeNode.get("lower_height").asDouble()); + // if (staticProps.containsKey("tray_outer_diameter")) staticProps.put("diameter", staticProps.get("tray_outer_diameter")); + // if (staticProps.containsKey("tray_height")) staticProps.put("height", staticProps.get("tray_height")); + break; case "ExtractionColumn": - // 优先提取 tray_section (塔身) - if (sizeNode.has("tray_section")) { - parseCommonSize(sizeNode.get("tray_section"), staticProps); - } - // 可选:将其他段的尺寸作为特殊属性注入,例如 lower_expanded_height + if (sizeNode.has("tray_diameter")) staticProps.put("tray_diameter", sizeNode.get("tray_diameter").asDouble()); + if (sizeNode.has("tray_height")) staticProps.put("tray_height", sizeNode.get("tray_height").asDouble()); + if (sizeNode.has("upper_diameter")) staticProps.put("upper_diameter", sizeNode.get("upper_diameter").asDouble()); + if (sizeNode.has("upper_height")) staticProps.put("upper_height", sizeNode.get("upper_height").asDouble()); + if (sizeNode.has("lower_diameter")) staticProps.put("lower_diameter", sizeNode.get("lower_diameter").asDouble()); + if (sizeNode.has("lower_height")) staticProps.put("lower_height", sizeNode.get("lower_height").asDouble()); + // if (staticProps.containsKey("tray_diameter")) staticProps.put("diameter", staticProps.get("tray_diameter")); + // if (staticProps.containsKey("tray_height")) staticProps.put("height", staticProps.get("tray_height")); + // if (!staticProps.containsKey("diameter") && sizeNode.has("tray_section")) { + // parseCommonSize(sizeNode.get("tray_section"), staticProps); + // } break; case "FluidizedBed": - // 优先提取 reaction_section (反应段) - if (sizeNode.has("reaction_section")) { - parseCommonSize(sizeNode.get("reaction_section"), staticProps); - } + if (sizeNode.has("reaction_diameter")) staticProps.put("reaction_diameter", sizeNode.get("reaction_diameter").asDouble()); + if (sizeNode.has("reaction_height")) staticProps.put("reaction_height", sizeNode.get("reaction_height").asDouble()); + if (sizeNode.has("expanded_diameter")) staticProps.put("expanded_diameter", sizeNode.get("expanded_diameter").asDouble()); + if (sizeNode.has("expanded_height")) staticProps.put("expanded_height", sizeNode.get("expanded_height").asDouble()); + if (sizeNode.has("transition_height")) staticProps.put("transition_height", sizeNode.get("transition_height").asDouble()); + // if (staticProps.containsKey("reaction_diameter")) staticProps.put("diameter", staticProps.get("reaction_diameter")); + // if (staticProps.containsKey("reaction_height")) staticProps.put("height", staticProps.get("reaction_height")); + // if (!staticProps.containsKey("diameter") && sizeNode.has("reaction_section")) { + // parseCommonSize(sizeNode.get("reaction_section"), staticProps); + // } break; case "ACFTank": - // 优先提取 annular_cylinder (圆柱段) - if (sizeNode.has("annular_cylinder")) { - parseCommonSize(sizeNode.get("annular_cylinder"), staticProps); - } + if (sizeNode.has("cylinder_diameter")) staticProps.put("cylinder_diameter", sizeNode.get("cylinder_diameter").asDouble()); + if (sizeNode.has("cylinder_height")) staticProps.put("cylinder_height", sizeNode.get("cylinder_height").asDouble()); + if (sizeNode.has("bottom_diameter")) staticProps.put("bottom_diameter", sizeNode.get("bottom_diameter").asDouble()); + if (sizeNode.has("bottom_height")) staticProps.put("bottom_height", sizeNode.get("bottom_height").asDouble()); + if (sizeNode.has("scab_thickness")) staticProps.put("scab_thickness", sizeNode.get("scab_thickness").asDouble()); + if (sizeNode.has("scab_height")) staticProps.put("scab_height", sizeNode.get("scab_height").asDouble()); + // if (staticProps.containsKey("cylinder_diameter")) staticProps.put("diameter", staticProps.get("cylinder_diameter")); + // if (staticProps.containsKey("cylinder_height")) staticProps.put("height", staticProps.get("cylinder_height")); + // if (!staticProps.containsKey("diameter") && sizeNode.has("annular_cylinder")) { + // parseCommonSize(sizeNode.get("annular_cylinder"), staticProps); + // } break; default: @@ -364,6 +407,11 @@ public class SimBuilder { if (m.getPuConcentration() != null) s.put("pu_concentration", m.getPuConcentration().doubleValue()); if (m.getUEnrichment() != null) s.put("u_enrichment", m.getUEnrichment().doubleValue()); if (m.getPuIsotope() != null) s.put("pu_isotope", m.getPuIsotope().doubleValue()); + if (m.getEPu240() != null) s.put("e_pu240", m.getEPu240().doubleValue()); + if (m.getEPu242() != null) s.put("e_pu242", m.getEPu242().doubleValue()); + if (m.getEPu241() != null) s.put("e_pu241", m.getEPu241().doubleValue()); + if (m.getEPu239() != null) s.put("e_pu239", m.getEPu239().doubleValue()); + if (m.getEPu238() != null) s.put("e_pu238", m.getEPu238().doubleValue()); if (m.getUo2Density() != null) s.put("uo2_density", m.getUo2Density().doubleValue()); if (m.getPuo2Density() != null) s.put("puo2_density", m.getPuo2Density().doubleValue()); if (m.getHno3Acidity() != null) s.put("hno3_acidity", m.getHno3Acidity().doubleValue()); diff --git a/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmModelController.java b/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmModelController.java index c83ae00..5ec9ca4 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmModelController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmModelController.java @@ -103,7 +103,14 @@ public class AlgorithmModelController { QueryWrapper qw = new QueryWrapper<>(); if (algorithmType != null && !algorithmType.isEmpty()) qw.eq("algorithm_type", algorithmType); if (deviceType != null && !deviceType.isEmpty()) qw.eq("device_type", deviceType); - if (materialType != null && !materialType.isEmpty()) qw.eq("material_type", materialType); + String mt = normalizeMaterialType(materialType); + if (mt != null) { + if ("Mixed".equals(mt)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", mt); + } + } if (versionTag != null && !versionTag.isEmpty()) qw.eq("version_tag", versionTag); if (isCurrent != null && !isCurrent.isEmpty()) qw.eq("is_current", isCurrent); qw.orderByDesc("updated_at"); @@ -111,6 +118,46 @@ public class AlgorithmModelController { return algorithmModelService.page(page, qw); } + @GetMapping("/options") + @Operation(summary = "获取模型版本选项列表", description = "用于界面下拉选择:按算法类型与设备类型返回模型版本列表(value=模型ID,label=版本号),并返回当前激活模型ID") + public Map options(@RequestParam String algorithmType, + @RequestParam String deviceType, + @RequestParam(required = false) String materialType) { + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("algorithm_type", algorithmType); + qw.eq("device_type", deviceType); + String mt = normalizeMaterialType(materialType); + if (mt != null) { + if ("Mixed".equals(mt)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", mt); + } + } + qw.orderByDesc("updated_at"); + List list = algorithmModelService.list(qw); + + String currentModelId = null; + List> options = new java.util.ArrayList<>(); + for (AlgorithmModel m : list) { + if (m == null) continue; + if (currentModelId == null && m.getIsCurrent() != null && m.getIsCurrent() == 1) { + currentModelId = m.getAlgorithmModelId(); + } + Map opt = new HashMap<>(); + opt.put("value", m.getAlgorithmModelId()); + opt.put("label", m.getVersionTag()); + opt.put("isCurrent", m.getIsCurrent()); + opt.put("materialType", m.getMaterialType()); + opt.put("updatedAt", m.getUpdatedAt()); + options.add(opt); + } + Map out = new HashMap<>(); + out.put("currentModelId", currentModelId); + out.put("options", options); + return out; + } + //返回:该算法+设备类型+材料类型的当前激活版本 @GetMapping("/current") @Operation(summary = "获取当前激活版本", description = "根据算法类型、设备类型与材料类型,返回 is_current=1 的模型版本") @@ -120,8 +167,13 @@ public class AlgorithmModelController { QueryWrapper qw = new QueryWrapper<>(); qw.eq("algorithm_type", algorithmType); qw.eq("device_type", deviceType); - if (materialType != null && !materialType.isEmpty()) { - qw.eq("material_type", materialType); + String mt = normalizeMaterialType(materialType); + if (mt != null) { + if ("Mixed".equals(mt)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", mt); + } } qw.eq("is_current", 1); qw.orderByDesc("updated_at"); @@ -129,7 +181,7 @@ public class AlgorithmModelController { } @Log(value = "激活模型版本", module = "算法模型管理") - @PreAuthorize("hasAuthority('algorithmModel:activate')") + // @PreAuthorize("hasAuthority('algorithmModel:activate')") @PostMapping("/activate") @Operation(summary = "激活模型版本", description = "将目标模型版本设为当前,并将同组(算法+设备+材料)其他版本设为非当前") public boolean activate(@RequestParam String algorithmModelId) { @@ -139,8 +191,13 @@ public class AlgorithmModelController { QueryWrapper qw = new QueryWrapper<>(); qw.eq("algorithm_type", model.getAlgorithmType()); qw.eq("device_type", model.getDeviceType()); - if (model.getMaterialType() != null && !model.getMaterialType().isEmpty()) { - qw.eq("material_type", model.getMaterialType()); + String mt = normalizeMaterialType(model.getMaterialType()); + if (mt != null) { + if ("Mixed".equals(mt)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", mt); + } } else { qw.and(wrapper -> wrapper.isNull("material_type").or().eq("material_type", "")); } @@ -149,6 +206,7 @@ public class AlgorithmModelController { algorithmModelService.update(upd, qw); // 设置当前版本 model.setIsCurrent(1); + model.setMaterialType(mt); model.setModifier(currentUsername()); model.setUpdatedAt(LocalDateTime.now()); return algorithmModelService.updateById(model); @@ -161,7 +219,7 @@ public class AlgorithmModelController { public Map trainExcel(@RequestBody Map body) { String algorithmType = str(body.get("algorithm_type")); String deviceType = str(body.get("device_type")); - String materialType = str(body.getOrDefault("material_type", "")); + String materialType = normalizeMaterialType(str(body.getOrDefault("material_type", ""))); String datasetPath = str(body.get("dataset_path")); String modelDir = str(body.getOrDefault("model_dir", "")); boolean activate = bool(body.getOrDefault("activate", false)); @@ -216,7 +274,11 @@ public class AlgorithmModelController { QueryWrapper qw = new QueryWrapper<>(); qw.eq("algorithm_type", algorithmType).eq("device_type", deviceType); if (!isBlank(materialType)) { - qw.eq("material_type", materialType); + if ("Mixed".equals(materialType)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", materialType); + } } else { qw.and(wrapper -> wrapper.isNull("material_type").or().eq("material_type", "")); } @@ -235,7 +297,7 @@ public class AlgorithmModelController { public Map trainSamples(@RequestBody Map body) { String algorithmType = str(body.get("algorithm_type")); String deviceType = str(body.get("device_type")); - String materialType = str(body.getOrDefault("material_type", "")); + String materialType = normalizeMaterialType(str(body.getOrDefault("material_type", ""))); Object samples = body.get("samples"); // 期望为 List,由前端提供 String modelDir = str(body.getOrDefault("model_dir", "")); boolean activate = bool(body.getOrDefault("activate", false)); @@ -276,7 +338,11 @@ public class AlgorithmModelController { QueryWrapper qw = new QueryWrapper<>(); qw.eq("algorithm_type", algorithmType).eq("device_type", deviceType); if (!isBlank(materialType)) { - qw.eq("material_type", materialType); + if ("Mixed".equals(materialType)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", materialType); + } } else { qw.and(wrapper -> wrapper.isNull("material_type").or().eq("material_type", "")); } @@ -288,6 +354,16 @@ public class AlgorithmModelController { return Map.of("code", 0, "msg", "训练成功", "data", model); } + private String normalizeMaterialType(String raw) { + if (raw == null) return null; + String s = raw.trim(); + if (s.isEmpty()) return null; + if ("U".equalsIgnoreCase(s)) return "U"; + if ("Pu".equalsIgnoreCase(s)) return "Pu"; + if ("Mixed".equalsIgnoreCase(s) || "MIX".equalsIgnoreCase(s)) return "Mixed"; + return s; + } + private String currentUsername() { try { diff --git a/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java b/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java index bb2d0f0..2207cd0 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java @@ -8,6 +8,9 @@ import com.yfd.platform.system.service.IUserService; import com.yfd.platform.annotation.Log; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -18,6 +21,7 @@ import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/critical-data") @@ -121,6 +125,37 @@ public class CriticalDataController { return criticalDataService.importCriticalData(file, deviceType); } + @PostMapping("/v2/import") + public boolean importCriticalDataV2(@RequestParam("file") MultipartFile file, + @RequestParam String deviceType) { + return criticalDataService.importCriticalDataV2(file, deviceType); + } + + @PostMapping("/v2/validate") + public Map validateCriticalDataV2(@RequestParam("file") MultipartFile file, + @RequestParam String deviceType) { + return criticalDataService.validateCriticalDataV2(file, deviceType); + } + + @GetMapping("/v2/export") + public ResponseEntity exportCriticalDataV2(@RequestParam String deviceType, + @RequestParam(required = false) List ids) { + byte[] bytes = criticalDataService.exportCriticalDataV2(deviceType, ids); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=critical_data_" + deviceType + ".xlsx") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + + @GetMapping("/v2/template") + public ResponseEntity templateCriticalDataV2(@RequestParam String deviceType) { + byte[] bytes = criticalDataService.templateCriticalDataV2(deviceType); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=critical_data_template_" + deviceType + ".xlsx") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + /** diff --git a/business-css/src/main/java/com/yfd/business/css/controller/DeviceController.java b/business-css/src/main/java/com/yfd/business/css/controller/DeviceController.java index 73f0978..67d5750 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/DeviceController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/DeviceController.java @@ -6,6 +6,11 @@ import com.yfd.business.css.domain.Device; import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.DeviceService; import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.security.core.context.SecurityContextHolder; @@ -15,10 +20,16 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import jakarta.annotation.Resource; import java.util.List; +import java.util.Map; import java.time.LocalDateTime; +/** + * 设备管理接口(模板库与项目设备) + * 包含基础增删改查,以及设备 V2 Excel 导入/校验/导出/模板接口(尺寸字段按 size-schema 收拢/展开)。 + */ @RestController @RequestMapping("/devices") +@Tag(name = "设备管理接口", description = "设备模板库/项目设备的增删改查与 Excel 导入导出") public class DeviceController { @Resource @@ -130,6 +141,99 @@ public class DeviceController { return deviceService.importDevices(file, deviceType); } + /** + * 4.2 导入设备(Excel,V2) + * 输入参数:表单文件字段 file;可选 projectId/deviceType + * 设计要点:支持按 deviceType 的 size-schema 将尺寸列收拢为 size(JSON);projectId 为空时默认写入 -1(模板库) + * 权限:projectId != -1 时校验项目写权限 + * 输出参数:布尔值,表示是否导入成功 + * @param file Excel/CSV 文件 + * @param projectId 项目ID(可选,默认 -1) + * @param deviceType 设备类型(可选) + * @return 是否导入成功 + */ + @PostMapping("/v2/import") + @Operation(summary = "导入设备(V2)", description = "支持按 size-schema 收拢尺寸列为 size(JSON);projectId 为空默认 -1(模板库)") + public boolean importDevicesV2(@RequestParam("file") MultipartFile file, + @RequestParam(required = false) String projectId, + @RequestParam(required = false) String deviceType) { + String pid = (projectId == null || projectId.isBlank()) ? "-1" : projectId; + if (!"-1".equals(pid)) { + projectAccessHelper.assertCanWriteProject(pid); + } + return deviceService.importDevicesV2(file, pid, deviceType); + } + + /** + * 4.3 校验设备(Excel,V2) + * 输入参数:表单文件字段 file;可选 projectId/deviceType + * 设计要点:仅做解析与校验,不落库;返回 errors 与可用于导入的 _rows(前端可据此预览/提示) + * 权限:projectId != -1 时校验项目写权限(与导入保持一致) + * 输出参数:校验结果 Map(ok/errorRows/validRows/errors/_rows 等) + * @param file Excel/CSV 文件 + * @param projectId 项目ID(可选,默认 -1) + * @param deviceType 设备类型(可选) + * @return 校验结果 + */ + @PostMapping("/v2/validate") + @Operation(summary = "校验设备(V2)", description = "仅解析与校验不落库;返回 errors 与可用于导入的 _rows") + public Map validateDevicesV2(@RequestParam("file") MultipartFile file, + @RequestParam(required = false) String projectId, + @RequestParam(required = false) String deviceType) { + String pid = (projectId == null || projectId.isBlank()) ? "-1" : projectId; + if (!"-1".equals(pid)) { + projectAccessHelper.assertCanWriteProject(pid); + } + return deviceService.validateDevicesV2(file, pid, deviceType); + } + + /** + * 4.4 导出设备(Excel,V2) + * 输入参数:可选 projectId/deviceType/ids + * 设计要点:按 size-schema 将 size(JSON) 展开为多列输出,便于人工编辑与回填 + * 权限:projectId != -1 时校验项目读权限 + * 输出参数:Excel 二进制(xlsx) + * @param projectId 项目ID(可选,默认 -1) + * @param deviceType 设备类型(可选) + * @param ids 指定导出设备ID列表(可选;为空则按筛选条件导出) + * @return xlsx 文件字节流 + */ + @GetMapping("/v2/export") + @Operation(summary = "导出设备(V2)", description = "按 size-schema 将 size(JSON) 展开为多列输出;支持按 ids 定向导出") + public ResponseEntity exportDevicesV2(@RequestParam(required = false) String projectId, + @RequestParam(required = false) String deviceType, + @RequestParam(required = false) List ids) { + String pid = (projectId == null || projectId.isBlank()) ? "-1" : projectId; + if (!"-1".equals(pid)) { + projectAccessHelper.assertCanReadProject(pid); + } + byte[] bytes = deviceService.exportDevicesV2(pid, deviceType, ids); + String fn = "devices_" + (deviceType == null || deviceType.isBlank() ? "all" : deviceType) + ".xlsx"; + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fn) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + + /** + * 4.5 下载导入模板(Excel,V2) + * 输入参数:deviceType + * 设计要点:模板表头与 size-schema 对齐,避免前端/后端/导入导出口径漂移 + * 输出参数:Excel 二进制(xlsx) + * @param deviceType 设备类型 + * @return xlsx 模板字节流 + */ + @GetMapping("/v2/template") + @Operation(summary = "下载导入模板(V2)", description = "模板表头与 size-schema 对齐,便于导入导出与推理口径统一") + public ResponseEntity templateDevicesV2(@RequestParam String deviceType) { + byte[] bytes = deviceService.templateDevicesV2(deviceType); + String fn = "device_template_" + deviceType + ".xlsx"; + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fn) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + /** * 5. 根据项目ID获取设备列表 @@ -183,6 +287,11 @@ public class DeviceController { return deviceService.page(page, qw); } + /** + * 获取当前登录账号 username(用于 creator/modifier 字段) + * 未登录或取不到用户信息时返回 anonymous。 + * @return 当前登录账号 username + */ private String currentUsername() { try { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); diff --git a/business-css/src/main/java/com/yfd/business/css/controller/DeviceMetaController.java b/business-css/src/main/java/com/yfd/business/css/controller/DeviceMetaController.java new file mode 100644 index 0000000..0ddc686 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/controller/DeviceMetaController.java @@ -0,0 +1,33 @@ +package com.yfd.business.css.controller; + +import com.yfd.business.css.meta.DeviceSizeSchema; +import com.yfd.business.css.meta.DeviceSizeSchemaRegistry; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; +import java.util.Map; + +@RestController +@RequestMapping("/devices/v2") +@Tag(name = "设备元数据接口", description = "提供设备尺寸 size-schema 元数据,供前端动态渲染与校验使用") +public class DeviceMetaController { + @Resource + private DeviceSizeSchemaRegistry registry; + + @GetMapping("/size-schema") + @Operation(summary = "获取指定设备类型的尺寸 schema", description = "返回 deviceType 对应的字段元数据(key/label/unit/required/order/min/max),用于前端动态渲染与与导入导出/推理口径对齐") + public DeviceSizeSchema schema(@RequestParam String deviceType) { + return registry.getSchema(deviceType); + } + + @GetMapping("/size-schema/all") + @Operation(summary = "获取全部设备类型的尺寸 schema", description = "返回所有 deviceType 的 schema Map,适合前端一次性缓存") + public Map all() { + return registry.getAllSchemas(); + } +} diff --git a/business-css/src/main/java/com/yfd/business/css/controller/MaterialController.java b/business-css/src/main/java/com/yfd/business/css/controller/MaterialController.java index 5a58a18..29640bf 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/MaterialController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/MaterialController.java @@ -7,6 +7,9 @@ import com.yfd.business.css.domain.Material; import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.MaterialService; import com.yfd.platform.system.service.IUserService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.security.core.context.SecurityContextHolder; @@ -118,7 +121,7 @@ public class MaterialController { /** * 4. 导入物料(Excel/CSV) * 输入参数:表单文件字段 file - * 模板表头:name, u_concentration, uo2_density, u_enrichment, pu_concentration, puo2_density, pu_isotope, hno3_acidity, h2c2o4_concentration, organic_ratio, moisture_content, custom_attrs + * 模板表头:name, u_concentration, uo2_density, u_enrichment, pu_concentration, puo2_density, e_pu240, e_pu242, e_pu241, e_pu239, e_pu238, hno3_acidity, h2c2o4_concentration, organic_ratio, moisture_content, custom_attrs * 采集规则:仅采集上述字段,表头中多余字段不予采集;custom_attrs 校验为合法 JSON,非法或占位符(-、—、/、/)将置为 null;导入记录的 project_id 统一写入 -1 * 输出参数:布尔值,表示是否导入成功 * @param file Excel/CSV 文件 @@ -130,6 +133,30 @@ public class MaterialController { return materialService.importMaterials(file); } + @GetMapping("/export") + public ResponseEntity exportMaterialsV2(@RequestParam String projectId, + @RequestParam(required = false) List ids, + @RequestParam(required = false) String nameLike) { + if (projectId != null && !projectId.isBlank() && !"-1".equals(projectId)) { + projectAccessHelper.assertCanReadProject(projectId); + } + byte[] bytes = materialService.exportMaterialsV2(projectId, ids, nameLike); + String fn = "materials.xlsx"; + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fn) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + + @GetMapping("/v2/template") + public ResponseEntity templateMaterialsV2() { + byte[] bytes = materialService.templateMaterialsV2(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=materials_template.xlsx") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } + /** * 5. 根据物料名称搜索(可为空)并分页返回 * 输入参数:查询参数 name(物料名称关键词,可为空),pageNum(页码,默认1),pageSize(每页条数,默认10) diff --git a/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java b/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java index f699edc..23d63f3 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java @@ -29,20 +29,47 @@ public class CriticalData implements Serializable { @TableField("device_type") private String deviceType; + @TableField("size") + private String size; + + @TableField("u_concentration") + private BigDecimal uConcentration; + + @TableField("u_enrichment") + private BigDecimal uEnrichment; + + @TableField("pu_concentration") + private BigDecimal puConcentration; + + @TableField("e_pu240") + private BigDecimal ePu240; + + @TableField("e_pu241") + private BigDecimal ePu241; + + @TableField("e_pu242") + private BigDecimal ePu242; + + @TableField("e_pu239") + private BigDecimal ePu239; + + @TableField("e_pu238") + private BigDecimal ePu238; + /** 等效直径 */ - @TableField("diameter") + @TableField(exist = false) private BigDecimal diameter; /** 等效高度 */ - @TableField("height") + @TableField(exist = false) private BigDecimal height; /** 核材料浓度(U 或 Pu) */ - @TableField("fissile_concentration") + @TableField(exist = false) private BigDecimal fissileConcentration; /** 同位素丰度(铀富集度 或 Pu-240 占比) */ - @TableField("isotopic_abundance") + @TableField(exist = false) private BigDecimal isotopicAbundance; /** 扩展物理/算法特征 - JSON格式 */ diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Material.java b/business-css/src/main/java/com/yfd/business/css/domain/Material.java index 929a7d5..e7edc4e 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Material.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Material.java @@ -46,6 +46,21 @@ public class Material implements Serializable { @TableField("pu_isotope") private BigDecimal puIsotope; + @TableField("e_pu240") + private BigDecimal ePu240; + + @TableField("e_pu242") + private BigDecimal ePu242; + + @TableField("e_pu241") + private BigDecimal ePu241; + + @TableField("e_pu239") + private BigDecimal ePu239; + + @TableField("e_pu238") + private BigDecimal ePu238; + @TableField("hno3_acidity") private BigDecimal hno3Acidity; diff --git a/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeField.java b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeField.java new file mode 100644 index 0000000..bdb237b --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeField.java @@ -0,0 +1,39 @@ +package com.yfd.business.css.meta; + +import lombok.Data; + +@Data +public class DeviceSizeField { + /** + * 扁平化 size JSON 的 key,必须与: + * 1) device.size(JSON) 内的键 + * 2) 设备导入/导出展开列名 + * 3) 训练/推理侧特征名(feature_map_snapshot) + * 保持一致,避免口径漂移。 + */ + private String key; + /** + * 前端展示用中文名称。 + */ + private String label; + /** + * 单位(默认 cm)。 + */ + private String unit; + /** + * 是否必填(用于前端渲染与导入校验)。 + */ + private boolean required; + /** + * 展示/导出列顺序(从 1 开始)。 + */ + private int order; + /** + * 最小值(可选,用于前端与校验)。 + */ + private Double min; + /** + * 最大值(可选,用于前端与校验)。 + */ + private Double max; +} diff --git a/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchema.java b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchema.java new file mode 100644 index 0000000..ab59bad --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchema.java @@ -0,0 +1,24 @@ +package com.yfd.business.css.meta; + +import lombok.Data; + +import java.util.List; + +@Data +public class DeviceSizeSchema { + /** + * 设备类型(与 Device.deviceType 取值一致,例如 FlatTank/CylindricalTank 等)。 + */ + private String deviceType; + /** + * schema 版本号;用于后续扩展/兼容(例如字段新增、命名调整等)。 + */ + private String schemaVersion; + /** + * 尺寸字段元数据列表(有序),用于: + * - 前端动态渲染表单/表格列 + * - 设备导入/导出/模板表头 + * - 与推理侧扁平化特征键保持一致 + */ + private List fields; +} diff --git a/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchemaRegistry.java b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchemaRegistry.java new file mode 100644 index 0000000..e3a5e15 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/meta/DeviceSizeSchemaRegistry.java @@ -0,0 +1,136 @@ +package com.yfd.business.css.meta; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Component +public class DeviceSizeSchemaRegistry { + private final Map schemas; + + public DeviceSizeSchemaRegistry() { + this.schemas = buildSchemas(); + } + + /** + * 获取指定设备类型的尺寸 schema。 + * 注意:deviceType 取值必须与设备表/业务枚举一致;若传入未知类型将返回 null。 + */ + public DeviceSizeSchema getSchema(String deviceType) { + if (deviceType == null) return null; + return schemas.get(deviceType); + } + + /** + * 获取所有设备类型的尺寸 schema(用于前端缓存与动态渲染)。 + * 注意:返回的 Map 为注册表内部持有对象;调用方应只读使用,不要修改。 + */ + public Map getAllSchemas() { + return schemas; + } + + /** + * 获取指定设备类型的扁平化 size keys(按 order 有序)。 + * 注意:这些 key 同时作为导入/导出的 Excel 列名与 device.size JSON 的键,必须保持稳定。 + */ + public List getSizeKeys(String deviceType) { + DeviceSizeSchema s = getSchema(deviceType); + if (s == null || s.getFields() == null) return List.of(); + List out = new ArrayList<>(); + for (DeviceSizeField f : s.getFields()) out.add(f.getKey()); + return out; + } + + private static Map buildSchemas() { + Map map = new LinkedHashMap<>(); + map.put("FlatTank", schema("FlatTank", fields( + req(1, "length", "长度"), + req(2, "width", "宽度"), + req(3, "height", "高度") + ))); + map.put("CylindricalTank", schema("CylindricalTank", fields( + req(1, "diameter", "直径"), + req(2, "height", "高度") + ))); + map.put("AnnularTank", schema("AnnularTank", fields( + req(1, "diameter", "环形槽外径"), + req(2, "height", "高度") + ))); + map.put("TubeBundleTank", schema("TubeBundleTank", fields( + req(1, "diameter", "管束槽外径"), + req(2, "height", "高度") + ))); + map.put("ExtractionColumn", schema("ExtractionColumn", fields( + req(1, "upper_diameter", "上扩大段直径"), + req(2, "upper_height", "上扩大段高度"), + req(3, "tray_diameter", "板段直径"), + req(4, "tray_height", "板段高度"), + req(5, "lower_diameter", "下扩大段直径"), + req(6, "lower_height", "下扩大段高度") + ))); + map.put("PulsedCylindricalColumn", schema("PulsedCylindricalColumn", fields( + req(1, "upper_inner_diameter", "上扩大段内径"), + req(2, "upper_outer_diameter", "上扩大段外径"), + req(3, "upper_height", "上扩大段高度"), + req(4, "tray_diameter", "板段直径"), + req(5, "tray_height", "板段高度"), + req(6, "lower_inner_diameter", "下扩大段内径"), + req(7, "lower_outer_diameter", "下扩大段外径"), + req(8, "lower_height", "下扩大段高度") + ))); + map.put("PulsedAnnularColumn", schema("PulsedAnnularColumn", fields( + req(1, "upper_inner_diameter", "上扩大段内径"), + req(2, "upper_outer_diameter", "上扩大段外径"), + req(3, "upper_height", "上扩大段高度"), + req(4, "tray_inner_diameter", "板段内径"), + req(5, "tray_outer_diameter", "板段外径"), + req(6, "tray_height", "板段高度"), + req(7, "lower_inner_diameter", "下扩大段内径"), + req(8, "lower_outer_diameter", "下扩大段外径"), + req(9, "lower_height", "下扩大段高度") + ))); + map.put("FluidizedBed", schema("FluidizedBed", fields( + req(1, "expanded_diameter", "扩大段直径"), + req(2, "expanded_height", "扩大段高度"), + req(3, "transition_height", "过渡段高度"), + req(4, "reaction_diameter", "反应段直径"), + req(5, "reaction_height", "反应段高度") + ))); + map.put("ACFTank", schema("ACFTank", fields( + req(1, "cylinder_diameter", "环形圆柱外径"), + req(2, "cylinder_height", "环形圆柱高度"), + req(3, "bottom_diameter", "圆锥台底部直径"), + req(4, "bottom_height", "圆锥台底部高度"), + req(5, "scab_thickness", "结疤厚度"), + req(6, "scab_height", "结疤高度") + ))); + return map; + } + + private static DeviceSizeSchema schema(String deviceType, List fields) { + DeviceSizeSchema s = new DeviceSizeSchema(); + s.setDeviceType(deviceType); + s.setSchemaVersion("v2"); + s.setFields(fields); + return s; + } + + private static List fields(DeviceSizeField... fs) { + List out = new ArrayList<>(); + for (DeviceSizeField f : fs) out.add(f); + return out; + } + + private static DeviceSizeField req(int order, String key, String label) { + DeviceSizeField f = new DeviceSizeField(); + f.setOrder(order); + f.setKey(key); + f.setLabel(label); + f.setUnit("cm"); + f.setRequired(true); + return f; + } +} diff --git a/business-css/src/main/java/com/yfd/business/css/service/CriticalDataService.java b/business-css/src/main/java/com/yfd/business/css/service/CriticalDataService.java index 6bfcf34..e8a58b5 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/CriticalDataService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/CriticalDataService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.business.css.domain.CriticalData; import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.Map; public interface CriticalDataService extends IService { /** @@ -11,5 +12,12 @@ public interface CriticalDataService extends IService { */ boolean importCriticalData(MultipartFile file, String deviceType); + boolean importCriticalDataV2(MultipartFile file, String deviceType); + + Map validateCriticalDataV2(MultipartFile file, String deviceType); + + byte[] exportCriticalDataV2(String deviceType, List ids); + + byte[] templateCriticalDataV2(String deviceType); } diff --git a/business-css/src/main/java/com/yfd/business/css/service/DeviceService.java b/business-css/src/main/java/com/yfd/business/css/service/DeviceService.java index 5b755e5..2318495 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/DeviceService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/DeviceService.java @@ -3,12 +3,22 @@ package com.yfd.business.css.service; import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.business.css.domain.Device; import org.springframework.web.multipart.MultipartFile; +import java.util.List; +import java.util.Map; public interface DeviceService extends IService { /** * 导入设备 */ boolean importDevices(MultipartFile file, String deviceType); + boolean importDevicesV2(MultipartFile file, String projectId, String deviceType); + + Map validateDevicesV2(MultipartFile file, String projectId, String deviceType); + + byte[] exportDevicesV2(String projectId, String deviceType, List ids); + + byte[] templateDevicesV2(String deviceType); + boolean createDevice(Device device) ; boolean saveOrUpdateByBusiness(Device device); diff --git a/business-css/src/main/java/com/yfd/business/css/service/MaterialService.java b/business-css/src/main/java/com/yfd/business/css/service/MaterialService.java index e9b717d..b6e1430 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/MaterialService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/MaterialService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.business.css.domain.Device; import com.yfd.business.css.domain.Material; import org.springframework.web.multipart.MultipartFile; +import java.util.List; public interface MaterialService extends IService { /** @@ -17,4 +18,8 @@ public interface MaterialService extends IService { boolean saveMaterial(Material material); boolean saveOrUpdateByBusiness(Material material); + + byte[] exportMaterialsV2(String projectId, List ids, String nameLike); + + byte[] templateMaterialsV2(); } diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmModelServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmModelServiceImpl.java index 270b910..8c082aa 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmModelServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmModelServiceImpl.java @@ -36,15 +36,18 @@ public class AlgorithmModelServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); qw.eq("algorithm_type", algorithmType) .eq("device_type", deviceType) .eq("is_current", 1); if (mt != null) { - qw.eq("material_type", mt); + if ("Mixed".equals(mt)) { + qw.in("material_type", List.of("Mixed", "MIX")); + } else { + qw.eq("material_type", mt); + } } else { qw.and(w -> w.isNull("material_type").or().eq("material_type", "")); } @@ -63,6 +66,16 @@ public class AlgorithmModelServiceImpl extends ServiceImpl ids) { diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmServiceImpl.java index b58fd6b..8dc34a2 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/AlgorithmServiceImpl.java @@ -10,4 +10,30 @@ import org.springframework.stereotype.Service; @Slf4j @Service public class AlgorithmServiceImpl extends ServiceImpl implements AlgorithmService { -} \ No newline at end of file + @Override + public boolean save(Algorithm entity) { + normalizeJsonFields(entity); + return super.save(entity); + } + + @Override + public boolean updateById(Algorithm entity) { + normalizeJsonFields(entity); + return super.updateById(entity); + } + + private void normalizeJsonFields(Algorithm a) { + if (a == null) return; + a.setInputParams(blankToDefaultJson(a.getInputParams(), "{}")); + a.setOutputParams(blankToDefaultJson(a.getOutputParams(), "{}")); + a.setSupportedDeviceTypes(blankToDefaultJson(a.getSupportedDeviceTypes(), "[]")); + a.setDefaultHyperParams(blankToDefaultJson(a.getDefaultHyperParams(), "{}")); + } + + private String blankToDefaultJson(String s, String defaultJson) { + if (s == null) return defaultJson; + String t = s.trim(); + if (t.isEmpty()) return defaultJson; + return t; + } +} diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/CriticalDataServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/CriticalDataServiceImpl.java index ab8c3a1..a4b02d7 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/CriticalDataServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/CriticalDataServiceImpl.java @@ -24,10 +24,16 @@ import org.springframework.security.core.Authentication; import org.springframework.security.authentication.AnonymousAuthenticationToken; import java.time.LocalDateTime; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.Collections; +import java.io.ByteArrayOutputStream; import lombok.extern.slf4j.Slf4j; @Service @@ -62,6 +68,81 @@ public class CriticalDataServiceImpl } } + @Override + public boolean importCriticalDataV2(MultipartFile file, String deviceType) { + Map res = validateCriticalDataV2(file, deviceType); + @SuppressWarnings("unchecked") + List list = (List) res.get("_rows"); + if (list == null || list.isEmpty()) { + return false; + } + return this.saveBatch(list, 500); + } + + @Override + public Map validateCriticalDataV2(MultipartFile file, String deviceType) { + Map out = new HashMap<>(); + List> errors = new ArrayList<>(); + out.put("errors", errors); + try { + String name = file.getOriginalFilename(); + if (name == null) { + out.put("ok", false); + out.put("msg", "文件名为空"); + return out; + } + if (deviceType == null || deviceType.isBlank()) { + out.put("ok", false); + out.put("msg", "deviceType 必填"); + return out; + } + String lower = name.toLowerCase(); + if (lower.endsWith(".xlsx")) { + return validateExcelV2(new XSSFWorkbook(file.getInputStream()), deviceType); + } else if (lower.endsWith(".xls")) { + return validateExcelV2(new HSSFWorkbook(file.getInputStream()), deviceType); + } else { + out.put("ok", false); + out.put("msg", "不支持的文件类型"); + return out; + } + } catch (Exception e) { + out.put("ok", false); + out.put("msg", "解析失败: " + e.getMessage()); + return out; + } + } + + @Override + public byte[] exportCriticalDataV2(String deviceType, List ids) { + try { + if (deviceType == null || deviceType.isBlank()) return new byte[0]; + var qw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper() + .eq("device_type", deviceType) + .orderByDesc("created_at"); + if (ids != null && !ids.isEmpty()) { + qw.in("critical_id", ids); + } + List list = this.list(qw); + byte[] bytes = buildExportWorkbookBytes(deviceType, list); + return bytes == null ? new byte[0] : bytes; + } catch (Exception e) { + log.error("critical-data export v2 failed", e); + return new byte[0]; + } + } + + @Override + public byte[] templateCriticalDataV2(String deviceType) { + try { + if (deviceType == null || deviceType.isBlank()) return new byte[0]; + return buildTemplateWorkbookBytes(deviceType); + } catch (Exception e) { + log.error("critical-data template v2 failed", e); + return new byte[0]; + } + } + private boolean importExcel(Workbook workbook, String deviceType) { try (Workbook wb = workbook) { FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); @@ -141,6 +222,337 @@ public class CriticalDataServiceImpl } } + private Map validateExcelV2(Workbook workbook, String deviceType) { + try (Workbook wb = workbook) { + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + evaluator.evaluateAll(); + DataFormatter formatter = new DataFormatter(); + Sheet sheet = wb.getSheetAt(0); + if (sheet == null) { + return Map.of("ok", false, "msg", "sheet为空", "errors", List.of()); + } + Row headerRow = sheet.getRow(0); + if (headerRow == null) { + return Map.of("ok", false, "msg", "表头为空", "errors", List.of()); + } + Map idx = new HashMap<>(); + for (int i = 0; i < headerRow.getLastCellNum(); i++) { + Cell c = headerRow.getCell(i); + if (c == null) continue; + String key = c.getStringCellValue(); + if (key != null) idx.put(key.trim().toLowerCase(), i); + } + + List sizeCols = getSizeColumnsByDeviceType(deviceType); + boolean hasSizeJsonHeader = idx.containsKey("size"); + boolean hasAnySizePartHeader = false; + for (String k : sizeCols) { + if (idx.containsKey(k)) { + hasAnySizePartHeader = true; + break; + } + } + + if (!idx.containsKey("keff_value")) { + return Map.of("ok", false, "msg", "缺少表头: keff_value", "errors", List.of()); + } + boolean hasUConcHeader = idx.containsKey("u_concentration"); + boolean hasUEnrHeader = idx.containsKey("u_enrichment"); + boolean hasPuConcHeader = idx.containsKey("pu_concentration"); + if (!hasUConcHeader && !hasPuConcHeader && !hasUEnrHeader) { + return Map.of("ok", false, "msg", "缺少表头: u_concentration/pu_concentration/u_enrichment", "errors", List.of()); + } + if (!hasSizeJsonHeader && !hasAnySizePartHeader) { + return Map.of("ok", false, "msg", "缺少表头: size 或任一尺寸列", "errors", List.of()); + } + + List> errors = new ArrayList<>(); + List rows = new ArrayList<>(); + + for (int r = 1; r <= sheet.getLastRowNum(); r++) { + Row row = sheet.getRow(r); + if (row == null) continue; + + Integer sizeIndex = idx.get("size"); + String sizeRaw = sizeIndex == null ? null : cleanString(getString(row, sizeIndex)); + String sizeJson = null; + if (sizeRaw != null) { + sizeJson = validateJson(sizeRaw); + if (sizeJson == null) { + errors.add(err(r, "size 非法JSON")); + continue; + } + try { + JsonNode node = objectMapper.readTree(sizeJson); + if (node != null && node.isObject()) { + Map m = new LinkedHashMap<>(); + node.fields().forEachRemaining(e -> { + JsonNode v = e.getValue(); + if (v == null || v.isNull()) return; + if (v.isNumber()) { + BigDecimal d = v.decimalValue(); + m.put(e.getKey(), roundScale(d, 3)); + } else if (v.isTextual()) { + m.put(e.getKey(), cleanString(v.asText())); + } else { + m.put(e.getKey(), v.toString()); + } + }); + sizeJson = objectMapper.writeValueAsString(m); + } + } catch (Exception ignored) { + } + } else { + Map m = new LinkedHashMap<>(); + for (String k : sizeCols) { + Integer colIndex = idx.get(k); + if (colIndex == null) continue; + BigDecimal v = getDecimalFlexible(row, colIndex, evaluator, formatter); + if (v != null) { + m.put(k, roundScale(v, 3)); + } + } + if (m.isEmpty()) { + errors.add(err(r, "尺寸列为空")); + continue; + } + sizeJson = objectMapper.writeValueAsString(m); + } + + Integer uConcIndex = idx.get("u_concentration"); + Integer uEnrIndex = idx.get("u_enrichment"); + Integer puConcIndex = idx.get("pu_concentration"); + Integer ePu240Index = idx.get("e_pu240"); + Integer ePu241Index = idx.get("e_pu241"); + Integer ePu242Index = idx.get("e_pu242"); + Integer ePu239Index = idx.get("e_pu239"); + Integer ePu238Index = idx.get("e_pu238"); + Integer keffIndex = idx.get("keff_value"); + + BigDecimal uConc = uConcIndex == null ? null : getDecimalFlexible(row, uConcIndex, evaluator, formatter); + BigDecimal uEnr = uEnrIndex == null ? null : getDecimalFlexible(row, uEnrIndex, evaluator, formatter); + BigDecimal puConc = puConcIndex == null ? null : getDecimalFlexible(row, puConcIndex, evaluator, formatter); + BigDecimal ePu240 = ePu240Index == null ? null : getDecimalFlexible(row, ePu240Index, evaluator, formatter); + BigDecimal ePu241 = ePu241Index == null ? null : getDecimalFlexible(row, ePu241Index, evaluator, formatter); + BigDecimal ePu242 = ePu242Index == null ? null : getDecimalFlexible(row, ePu242Index, evaluator, formatter); + BigDecimal ePu239 = ePu239Index == null ? null : getDecimalFlexible(row, ePu239Index, evaluator, formatter); + BigDecimal ePu238 = ePu238Index == null ? null : getDecimalFlexible(row, ePu238Index, evaluator, formatter); + BigDecimal keff = keffIndex == null ? null : getDecimalFlexible(row, keffIndex, evaluator, formatter); + + uConc = roundScale(uConc, 3); + puConc = roundScale(puConc, 3); + uEnr = roundScale(uEnr, 6); + ePu240 = roundScale(ePu240, 6); + ePu241 = roundScale(ePu241, 6); + ePu242 = roundScale(ePu242, 6); + ePu239 = roundScale(ePu239, 6); + ePu238 = roundScale(ePu238, 6); + keff = roundScale(keff, 6); + + Integer extraIndex = idx.get("extra_features"); + String extraRaw = extraIndex == null ? null : cleanString(getString(row, extraIndex)); + String extra = validateJson(extraRaw); + if (extraRaw != null && extra == null) { + errors.add(err(r, "extra_features 非法JSON")); + continue; + } + + List rowMiss = new ArrayList<>(); + if (keff == null) rowMiss.add("keff_value"); + if (sizeJson == null || sizeJson.isBlank()) rowMiss.add("size"); + if (uConc == null && puConc == null && uEnr == null) rowMiss.add("u_concentration/pu_concentration/u_enrichment"); + if (!rowMiss.isEmpty()) { + errors.add(err(r, "缺少字段: " + String.join(",", rowMiss))); + continue; + } + if (uEnr != null && (uEnr.compareTo(BigDecimal.ZERO) < 0 || uEnr.compareTo(BigDecimal.ONE) > 0)) { + errors.add(err(r, "u_enrichment 超范围(0-1)")); + continue; + } + if (ePu240 != null && (ePu240.compareTo(BigDecimal.ZERO) < 0 || ePu240.compareTo(BigDecimal.ONE) > 0)) { + errors.add(err(r, "e_pu240 超范围(0-1)")); + continue; + } + + CriticalData cd = new CriticalData(); + cd.setDeviceType(deviceType); + cd.setSize(sizeJson); + cd.setUConcentration(uConc); + cd.setUEnrichment(uEnr); + cd.setPuConcentration(puConc); + cd.setEPu240(ePu240); + cd.setEPu241(ePu241); + cd.setEPu242(ePu242); + cd.setEPu239(ePu239); + cd.setEPu238(ePu238); + cd.setExtraFeatures(extra); + cd.setKeffValue(keff); + cd.setCreatedAt(LocalDateTime.now()); + cd.setUpdatedAt(LocalDateTime.now()); + cd.setModifier(currentUsername()); + rows.add(cd); + } + + Map out = new HashMap<>(); + out.put("ok", !rows.isEmpty()); + out.put("msg", errors.isEmpty() ? "OK" : ("存在错误: " + errors.size())); + out.put("totalRows", sheet.getLastRowNum()); + out.put("validRows", rows.size()); + out.put("errorRows", errors.size()); + out.put("errors", errors.size() > 50 ? errors.subList(0, 50) : errors); + out.put("_rows", rows); + return out; + } catch (Exception e) { + return Map.of("ok", false, "msg", "解析失败: " + e.getMessage(), "errors", List.of()); + } + } + + private Map err(int row, String msg) { + Map m = new HashMap<>(); + m.put("row", row); + m.put("msg", msg); + return m; + } + + private List getSizeColumnsByDeviceType(String deviceType) { + if (deviceType == null) return Collections.emptyList(); + return switch (deviceType) { + case "FlatTank" -> List.of("length", "width", "height"); + case "CylindricalTank" -> List.of("diameter", "height"); + case "AnnularTank" -> List.of("diameter", "height"); + case "TubeBundleTank" -> List.of("diameter", "height"); + case "ExtractionColumn" -> List.of( + "upper_diameter", "upper_height", + "tray_diameter", "tray_height", + "lower_diameter", "lower_height" + ); + case "PulsedCylindricalColumn" -> List.of( + "upper_inner_diameter", "upper_outer_diameter", "upper_height", + "tray_diameter", "tray_height", + "lower_inner_diameter", "lower_outer_diameter", "lower_height" + ); + case "PulsedAnnularColumn" -> List.of( + "upper_inner_diameter", "upper_outer_diameter", "upper_height", + "tray_inner_diameter", "tray_outer_diameter", "tray_height", + "lower_inner_diameter", "lower_outer_diameter", "lower_height" + ); + case "FluidizedBed" -> List.of( + "expanded_diameter", "expanded_height", + "transition_height", + "reaction_diameter", "reaction_height" + ); + case "ACFTank" -> List.of( + "cylinder_diameter", "cylinder_height", + "bottom_diameter", "bottom_height", + "scab_thickness", "scab_height" + ); + default -> List.of("diameter", "height"); + }; + } + + private byte[] buildTemplateWorkbookBytes(String deviceType) throws Exception { + List sizeCols = getSizeColumnsByDeviceType(deviceType); + List headers = new ArrayList<>(); + headers.add("device_type"); + headers.addAll(sizeCols); + headers.add("size"); + headers.add("u_concentration"); + headers.add("u_enrichment"); + headers.add("pu_concentration"); + headers.add("e_pu240"); + headers.add("e_pu241"); + headers.add("e_pu242"); + headers.add("e_pu239"); + headers.add("e_pu238"); + headers.add("extra_features"); + headers.add("keff_value"); + + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("critical_data"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + Cell c = hr.createCell(i); + c.setCellValue(headers.get(i)); + } + Row r1 = sheet.createRow(1); + r1.createCell(0).setCellValue(deviceType); + wb.write(bos); + return bos.toByteArray(); + } + } + + private byte[] buildExportWorkbookBytes(String deviceType, List list) throws Exception { + List sizeCols = getSizeColumnsByDeviceType(deviceType); + List headers = new ArrayList<>(); + headers.addAll(sizeCols); + headers.add("u_concentration"); + headers.add("u_enrichment"); + headers.add("pu_concentration"); + headers.add("e_pu240"); + headers.add("e_pu241"); + headers.add("e_pu242"); + headers.add("e_pu239"); + headers.add("e_pu238"); + headers.add("extra_features"); + headers.add("keff_value"); + + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("critical_data"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + hr.createCell(i).setCellValue(headers.get(i)); + } + int rowNum = 1; + for (CriticalData cd : list) { + Row r = sheet.createRow(rowNum++); + int col = 0; + Map flat = flattenSize(cd.getSize(), sizeCols); + for (String k : sizeCols) { + r.createCell(col++).setCellValue(nvl(flat.get(k))); + } + + r.createCell(col++).setCellValue(nvl(toStr(cd.getUConcentration()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getUEnrichment()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getPuConcentration()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getEPu240()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getEPu241()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getEPu242()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getEPu239()))); + r.createCell(col++).setCellValue(nvl(toStr(cd.getEPu238()))); + r.createCell(col++).setCellValue(nvl(cd.getExtraFeatures())); + r.createCell(col++).setCellValue(nvl(toStr(cd.getKeffValue()))); + } + wb.write(bos); + return bos.toByteArray(); + } + } + + private Map flattenSize(String sizeJson, List cols) { + Map out = new HashMap<>(); + if (cols == null || cols.isEmpty()) return out; + if (sizeJson == null || sizeJson.isBlank()) return out; + try { + JsonNode node = objectMapper.readTree(sizeJson); + for (String k : cols) { + JsonNode v = node.get(k); + if (v == null || v.isNull()) continue; + if (v.isNumber()) out.put(k, v.numberValue().toString()); + else if (v.isTextual()) out.put(k, v.asText()); + else out.put(k, v.toString()); + } + } catch (Exception ignored) { + } + return out; + } + + private String toStr(BigDecimal d) { + return d == null ? null : d.stripTrailingZeros().toPlainString(); + } + + private String nvl(String s) { + return s == null ? "" : s; + } + private String getString(Row row, int i) { Cell c = row.getCell(i); if (c == null) return null; @@ -155,8 +567,28 @@ public class CriticalDataServiceImpl private BigDecimal getDecimalFlexible(Row row, int i, FormulaEvaluator evaluator, DataFormatter formatter) { Cell c = row.getCell(i); if (c == null) return null; - String txt = formatter.formatCellValue(c, evaluator); - return parseDecimal(txt); + switch (c.getCellType()) { + case NUMERIC: + return BigDecimal.valueOf(c.getNumericCellValue()); + case STRING: + return parseDecimal(cleanString(c.getStringCellValue())); + case FORMULA: { + var cv = evaluator.evaluate(c); + if (cv == null) return null; + switch (cv.getCellType()) { + case NUMERIC: + return BigDecimal.valueOf(cv.getNumberValue()); + case STRING: + return parseDecimal(cleanString(cv.getStringValue())); + default: + return null; + } + } + default: { + String txt = formatter.formatCellValue(c, evaluator); + return parseDecimal(cleanString(txt)); + } + } } private BigDecimal parseDecimal(String s) { @@ -188,6 +620,15 @@ public class CriticalDataServiceImpl } } + private BigDecimal roundScale(BigDecimal v, int scale) { + if (v == null) return null; + try { + return v.setScale(scale, RoundingMode.HALF_UP); + } catch (Exception ignored) { + return v; + } + } + private boolean isPlaceholder(String s) { if (s == null) return false; String t = s.trim(); diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/DeviceServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/DeviceServiceImpl.java index b1f6bf3..3cb930d 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/DeviceServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/DeviceServiceImpl.java @@ -17,6 +17,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.FormulaEvaluator; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -24,11 +26,15 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JsonNode; +import com.yfd.business.css.meta.DeviceSizeSchemaRegistry; import jakarta.annotation.Resource; +import java.io.ByteArrayOutputStream; import java.time.LocalDateTime; import java.util.List; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; @Slf4j @@ -40,6 +46,8 @@ public class DeviceServiceImpl private ObjectMapper objectMapper; @Resource private IUserService userService; + @Resource + private DeviceSizeSchemaRegistry deviceSizeSchemaRegistry; @Override public boolean importDevices(MultipartFile file, String deviceType) { try { @@ -63,6 +71,64 @@ public class DeviceServiceImpl } } + @Override + public boolean importDevicesV2(MultipartFile file, String projectId, String deviceType) { + Map res = validateDevicesV2(file, projectId, deviceType); + @SuppressWarnings("unchecked") + List list = (List) res.get("_rows"); + if (list == null || list.isEmpty()) return false; + return this.saveBatch(list, 500); + } + + @Override + public Map validateDevicesV2(MultipartFile file, String projectId, String deviceType) { + try { + String name = file.getOriginalFilename(); + if (name == null) return Map.of("ok", false, "msg", "文件名为空", "errors", List.of()); + String lower = name.toLowerCase(); + if (lower.endsWith(".xlsx")) { + return validateExcelV2(new XSSFWorkbook(file.getInputStream()), projectId, deviceType); + } else if (lower.endsWith(".xls")) { + return validateExcelV2(new HSSFWorkbook(file.getInputStream()), projectId, deviceType); + } else { + return Map.of("ok", false, "msg", "不支持的文件类型", "errors", List.of()); + } + } catch (Exception e) { + return Map.of("ok", false, "msg", "解析失败: " + e.getMessage(), "errors", List.of()); + } + } + + @Override + public byte[] exportDevicesV2(String projectId, String deviceType, List ids) { + try { + var qw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper() + .eq("project_id", projectId == null || projectId.isBlank() ? "-1" : projectId) + .orderByDesc("created_at"); + if (deviceType != null && !deviceType.isBlank()) { + qw.eq("type", deviceType); + } + if (ids != null && !ids.isEmpty()) { + qw.in("device_id", ids); + } + List list = this.list(qw); + return buildExportWorkbookBytes(deviceType, list); + } catch (Exception e) { + log.error("device export v2 failed", e); + return new byte[0]; + } + } + + @Override + public byte[] templateDevicesV2(String deviceType) { + try { + if (deviceType == null || deviceType.isBlank()) return new byte[0]; + return buildTemplateWorkbookBytes(deviceType); + } catch (Exception e) { + log.error("device template v2 failed", e); + return new byte[0]; + } + } + @Override public boolean createDevice(Device device) { @@ -162,6 +228,358 @@ public class DeviceServiceImpl } } + private Map validateExcelV2(Workbook workbook, String projectId, String deviceType) { + try (Workbook wb = workbook) { + // 1)初始化与基础判空 + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + evaluator.evaluateAll(); + DataFormatter formatter = new DataFormatter(); + Sheet sheet = wb.getSheetAt(0); + if (sheet == null) return Map.of("ok", false, "msg", "sheet为空", "errors", List.of()); + Row headerRow = sheet.getRow(0); + if (headerRow == null) return Map.of("ok", false, "msg", "表头为空", "errors", List.of()); + + // 2)读取表头,建立列名到列下标的映射 + Map idx = new HashMap<>(); + for (int i = 0; i < headerRow.getLastCellNum(); i++) { + Cell c = headerRow.getCell(i); + if (c == null) continue; + String key = c.getStringCellValue(); + if (key != null) idx.put(key.trim().toLowerCase(), i); + } + // 3)设备类型 deviceType / type 列 的强制规则 + List> errors = new ArrayList<>(); + List rows = new ArrayList<>(); + + boolean hasTypeCol = idx.containsKey("type") || idx.containsKey("device_type"); + if ((deviceType == null || deviceType.isBlank()) && !hasTypeCol) { + return Map.of("ok", false, "msg", "deviceType 必填(或在表里提供 type/device_type 列)", "errors", List.of()); + } + + // 4)size 来源规则:优先用 size(JSON) 列;否则按 deviceType 推导尺寸列 + boolean hasSizeJson = idx.containsKey("size"); + if (!hasSizeJson && (deviceType == null || deviceType.isBlank())) { + return Map.of("ok", false, "msg", "缺少 size 列且无法按 deviceType 推导尺寸列", "errors", List.of()); + } + + for (int r = 1; r <= sheet.getLastRowNum(); r++) { + Row row = sheet.getRow(r); + if (row == null) continue; + + String rowType = deviceType; + if (rowType == null || rowType.isBlank()) { + Integer ti = idx.get("type"); + if (ti == null) ti = idx.get("device_type"); + rowType = ti == null ? null : cleanString(getString(row, ti)); + } + if (rowType == null || rowType.isBlank()) { + errors.add(err(r, "缺少 type/device_type")); + continue; + } + + String code = idx.containsKey("code") ? cleanString(getString(row, idx.get("code"))) : null; + String name = idx.containsKey("name") ? cleanString(getString(row, idx.get("name"))) : null; + if ((code == null || code.isBlank()) && (name == null || name.isBlank())) { + continue; + } + + String sizeJson = null; + if (hasSizeJson) { + String raw = cleanString(getString(row, idx.get("size"))); + if (raw != null) { + sizeJson = validateJson(raw); + if (sizeJson == null) { + errors.add(err(r, "size 非法JSON")); + continue; + } + } + } + if (sizeJson == null) { + List sizeCols = getSizeColumnsByDeviceType(rowType); + if (sizeCols.isEmpty()) { + errors.add(err(r, "未知设备类型: " + rowType)); + continue; + } + Map m = new LinkedHashMap<>(); + for (String k : sizeCols) { + Integer i = idx.get(k); + if (i == null) continue; + Double v = getDoubleFlexible(row, i, evaluator, formatter); + if (v != null) { + m.put(k, v); + } + } + if (m.isEmpty()) { + errors.add(err(r, "尺寸列为空")); + continue; + } + sizeJson = objectMapper.writeValueAsString(m); + } + + Device d = new Device(); + d.setType(rowType); + d.setProjectId(projectId == null || projectId.isBlank() ? "-1" : projectId); + d.setCode(code); + d.setName(name); + d.setSize(sizeJson); + if (idx.containsKey("volume")) d.setVolume(getDoubleFlexible(row, idx.get("volume"), evaluator, formatter)); + if (idx.containsKey("flow_rate")) d.setFlowRate(getDoubleFlexible(row, idx.get("flow_rate"), evaluator, formatter)); + if (idx.containsKey("pulse_velocity")) d.setPulseVelocity(getDoubleFlexible(row, idx.get("pulse_velocity"), evaluator, formatter)); + d.setCreatedAt(LocalDateTime.now()); + d.setUpdatedAt(LocalDateTime.now()); + d.setModifier(currentUsername()); + rows.add(d); + } + + Map out = new HashMap<>(); + out.put("ok", !rows.isEmpty()); + out.put("msg", errors.isEmpty() ? "OK" : ("存在错误: " + errors.size())); + out.put("totalRows", sheet.getLastRowNum()); + out.put("validRows", rows.size()); + out.put("errorRows", errors.size()); + out.put("errors", errors.size() > 50 ? errors.subList(0, 50) : errors); + out.put("_rows", rows); + return out; + } catch (Exception e) { + return Map.of("ok", false, "msg", "解析失败: " + e.getMessage(), "errors", List.of()); + } + } + + private Double getDoubleFlexible(Row row, int i, FormulaEvaluator evaluator, DataFormatter formatter) { + Cell c = row.getCell(i); + if (c == null) return null; + String s = formatter.formatCellValue(c, evaluator); + if (s == null) return null; + String t = s.trim(); + if (t.isEmpty()) return null; + try { + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } + + private String cleanString(String s) { + if (s == null) return null; + String t = s.trim(); + return t.isEmpty() ? null : t; + } + + private Map err(int row, String msg) { + Map m = new HashMap<>(); + m.put("row", row); + m.put("msg", msg); + return m; + } + + private List getSizeColumnsByDeviceType(String deviceType) { + if (deviceType == null || deviceType.isBlank()) return Collections.emptyList(); + try { + List keys = deviceSizeSchemaRegistry.getSizeKeys(deviceType); + return keys == null ? Collections.emptyList() : keys; + } catch (Exception e) { + return Collections.emptyList(); + } + } + + private List getAllSizeColumns() { + try { + java.util.LinkedHashSet set = new java.util.LinkedHashSet<>(); + Map all = deviceSizeSchemaRegistry.getAllSchemas(); + if (all != null) { + for (com.yfd.business.css.meta.DeviceSizeSchema s : all.values()) { + if (s == null || s.getFields() == null) continue; + for (com.yfd.business.css.meta.DeviceSizeField f : s.getFields()) { + if (f != null && f.getKey() != null && !f.getKey().isBlank()) { + set.add(f.getKey()); + } + } + } + } + return new ArrayList<>(set); + } catch (Exception e) { + return Collections.emptyList(); + } + } + + private byte[] buildTemplateWorkbookBytes(String deviceType) throws Exception { + List sizeCols = getSizeColumnsByDeviceType(deviceType); + if (sizeCols.isEmpty()) sizeCols = getAllSizeColumns(); + List headers = new ArrayList<>(); + headers.add("type"); + headers.add("project_id"); + headers.add("code"); + headers.add("name"); + headers.addAll(sizeCols); + headers.add("size"); + headers.add("volume"); + headers.add("flow_rate"); + headers.add("pulse_velocity"); + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("devices"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + hr.createCell(i).setCellValue(headers.get(i)); + } + Row r1 = sheet.createRow(1); + r1.createCell(0).setCellValue(deviceType); + r1.createCell(1).setCellValue("-1"); + wb.write(bos); + return bos.toByteArray(); + } + } + + private byte[] buildExportWorkbookBytes(String deviceType, List list) throws Exception { + List sizeCols = (deviceType == null || deviceType.isBlank()) ? getAllSizeColumns() : getSizeColumnsByDeviceType(deviceType); + if (sizeCols.isEmpty()) sizeCols = getAllSizeColumns(); + List headers = new ArrayList<>(); + // headers.add("device_id"); + // headers.add("project_id"); + headers.add("type"); + headers.add("code"); + headers.add("name"); + headers.addAll(sizeCols); + // headers.add("size"); + headers.add("volume"); + headers.add("flow_rate"); + headers.add("pulse_velocity"); + // headers.add("created_at"); + // headers.add("updated_at"); + // headers.add("modifier"); + + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("devices"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + hr.createCell(i).setCellValue(headers.get(i)); + } + int rowNum = 1; + for (Device d : list) { + Row r = sheet.createRow(rowNum++); + int col = 0; + // r.createCell(col++).setCellValue(nvl(d.getDeviceId())); + // r.createCell(col++).setCellValue(nvl(d.getProjectId())); + r.createCell(col++).setCellValue(nvl(d.getType())); + r.createCell(col++).setCellValue(nvl(d.getCode())); + r.createCell(col++).setCellValue(nvl(d.getName())); + + Map flat = flattenSize(d.getType(), d.getSize(), sizeCols); + for (String k : sizeCols) { + r.createCell(col++).setCellValue(nvl(flat.get(k))); + } + // r.createCell(col++).setCellValue(nvl(d.getSize())); + r.createCell(col++).setCellValue(d.getVolume() == null ? "" : d.getVolume().toString()); + r.createCell(col++).setCellValue(d.getFlowRate() == null ? "" : d.getFlowRate().toString()); + r.createCell(col++).setCellValue(d.getPulseVelocity() == null ? "" : d.getPulseVelocity().toString()); + // r.createCell(col++).setCellValue(d.getCreatedAt() == null ? "" : d.getCreatedAt().toString()); + // r.createCell(col++).setCellValue(d.getUpdatedAt() == null ? "" : d.getUpdatedAt().toString()); + // r.createCell(col++).setCellValue(nvl(d.getModifier())); + } + wb.write(bos); + return bos.toByteArray(); + } + } + + private Map flattenSize(String deviceType, String sizeJson, List cols) { + Map out = new HashMap<>(); + if (cols == null || cols.isEmpty()) return out; + if (sizeJson == null || sizeJson.isBlank()) return out; + try { + JsonNode root = objectMapper.readTree(sizeJson); + if ("ExtractionColumn".equals(deviceType)) { + pull(root, out, "upper_diameter", "upper_height", "tray_diameter", "tray_height", "lower_diameter", "lower_height"); + if (!out.isEmpty()) return out; + JsonNode up = root.get("upper_expanded"); + JsonNode tray = root.get("tray_section"); + JsonNode low = root.get("lower_expanded"); + if (up != null) { + putIfNum(out, "upper_diameter", up.get("diameter")); + putIfNum(out, "upper_height", up.get("height")); + } + if (tray != null) { + putIfNum(out, "tray_diameter", tray.get("diameter")); + putIfNum(out, "tray_height", tray.get("height")); + } + if (low != null) { + putIfNum(out, "lower_diameter", low.get("diameter")); + putIfNum(out, "lower_height", low.get("height")); + } + } else if ("FluidizedBed".equals(deviceType)) { + pull(root, out, "expanded_diameter", "expanded_height", "transition_height", "reaction_diameter", "reaction_height"); + if (!out.isEmpty()) return out; + JsonNode ex = root.get("expanded_section"); + JsonNode tr = root.get("transition_section"); + JsonNode re = root.get("reaction_section"); + if (ex != null) { + putIfNum(out, "expanded_diameter", ex.get("diameter")); + putIfNum(out, "expanded_height", ex.get("height")); + } + if (tr != null) { + putIfNum(out, "transition_height", tr.get("height")); + } + if (re != null) { + putIfNum(out, "reaction_diameter", re.get("diameter")); + putIfNum(out, "reaction_height", re.get("height")); + } + } else if ("ACFTank".equals(deviceType)) { + pull(root, out, "cylinder_diameter", "cylinder_height", "bottom_diameter", "bottom_height", "scab_thickness", "scab_height"); + if (!out.isEmpty()) return out; + JsonNode cyl = root.get("annular_cylinder"); + if (cyl != null) { + JsonNode od = cyl.get("outer_diameter"); + if (od == null) od = cyl.get("diameter"); + putIfNum(out, "cylinder_diameter", od); + putIfNum(out, "cylinder_height", cyl.get("height")); + } + JsonNode bottom = root.get("frustum_bottom"); + if (bottom != null) { + putIfNum(out, "bottom_diameter", bottom.get("bottom_diameter")); + putIfNum(out, "bottom_height", bottom.get("height")); + } + JsonNode scab = root.get("scab"); + if (scab != null) { + putIfNum(out, "scab_thickness", scab.get("thickness")); + putIfNum(out, "scab_height", scab.get("height")); + } + } else { + pull(root, out, "length", "width", "height", "diameter"); + if (!out.containsKey("diameter")) { + JsonNode od = root.get("outer_diameter"); + if (od != null) putIfNum(out, "diameter", od); + } + } + for (String k : cols) { + if (out.containsKey(k)) continue; + JsonNode v = root.get(k); + if (v != null) { + if (v.isNumber()) out.put(k, v.numberValue().toString()); + else if (v.isTextual()) out.put(k, v.asText()); + } + } + } catch (Exception ignored) { + } + return out; + } + + private void pull(JsonNode root, Map out, String... keys) { + for (String k : keys) { + JsonNode v = root.get(k); + if (v == null || v.isNull()) continue; + if (v.isNumber()) out.put(k, v.numberValue().toString()); + else if (v.isTextual()) out.put(k, v.asText()); + } + } + + private void putIfNum(Map out, String key, JsonNode v) { + if (v == null || v.isNull()) return; + if (v.isNumber()) out.put(key, v.numberValue().toString()); + else if (v.isTextual()) out.put(key, v.asText()); + } + + private String nvl(String s) { + return s == null ? "" : s; + } + private String getString(Row row, int i) { Cell c = row.getCell(i); if (c == null) return null; diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/MaterialServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/MaterialServiceImpl.java index 0fa774c..606ff8a 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/MaterialServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/MaterialServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JsonNode; import jakarta.annotation.Resource; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -26,6 +27,7 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.Authentication; import org.springframework.security.authentication.AnonymousAuthenticationToken; +import java.io.ByteArrayOutputStream; import java.time.LocalDateTime; import java.math.BigDecimal; import java.util.ArrayList; @@ -99,6 +101,36 @@ public class MaterialServiceImpl } } + @Override + public byte[] exportMaterialsV2(String projectId, List ids, String nameLike) { + try { + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("project_id", projectId); + if (ids != null && !ids.isEmpty()) { + qw.in("material_id", ids); + } + if (nameLike != null && !nameLike.isBlank()) { + qw.like("name", nameLike); + } + qw.orderByDesc("created_at"); + List list = this.list(qw); + return buildExportWorkbookBytes(list); + } catch (Exception e) { + log.error("materials export v2 failed", e); + return new byte[0]; + } + } + + @Override + public byte[] templateMaterialsV2() { + try { + return buildTemplateWorkbookBytes(); + } catch (Exception e) { + log.error("materials template v2 failed", e); + return new byte[0]; + } + } + private boolean importExcel(Workbook workbook) { try (Workbook wb = workbook) { Sheet sheet = wb.getSheetAt(0); @@ -121,7 +153,7 @@ public class MaterialServiceImpl log.info("material excel header keys={}", idx.keySet()); String[] keys = new String[]{ "name","u_concentration","uo2_density","u_enrichment", - "pu_concentration","puo2_density","pu_isotope", + "pu_concentration","puo2_density","e_pu240","e_pu242","e_pu241","e_pu239","e_pu238", "hno3_acidity","h2c2o4_concentration","organic_ratio", "moisture_content","custom_attrs" }; @@ -144,7 +176,12 @@ public class MaterialServiceImpl m.setUEnrichment(cleanDecimal(getDecimal(row, idx.get("u_enrichment")))); m.setPuConcentration(cleanDecimal(getDecimal(row, idx.get("pu_concentration")))); m.setPuo2Density(cleanDecimal(getDecimal(row, idx.get("puo2_density")))); - m.setPuIsotope(cleanDecimal(getDecimal(row, idx.get("pu_isotope")))); + // System.out.println("e_pu240="+getDecimal(row, idx.get("e_pu240"))); + m.setEPu240(cleanDecimal(getDecimal(row, idx.get("e_pu240")))); + m.setEPu242(cleanDecimal(getDecimal(row, idx.get("e_pu242")))); + m.setEPu241(cleanDecimal(getDecimal(row, idx.get("e_pu241")))); + m.setEPu239(cleanDecimal(getDecimal(row, idx.get("e_pu239")))); + m.setEPu238(cleanDecimal(getDecimal(row, idx.get("e_pu238")))); m.setHno3Acidity(cleanDecimal(getDecimal(row, idx.get("hno3_acidity")))); m.setH2c2o4Concentration(cleanDecimal(getDecimal(row, idx.get("h2c2o4_concentration")))); m.setOrganicRatio(cleanDecimal(getDecimal(row, idx.get("organic_ratio")))); @@ -244,4 +281,92 @@ public class MaterialServiceImpl return null; } } + + private byte[] buildTemplateWorkbookBytes() throws Exception { + List headers = materialExportHeaders(); + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("materials"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + hr.createCell(i).setCellValue(headers.get(i)); + } + wb.write(bos); + return bos.toByteArray(); + } + } + + private byte[] buildExportWorkbookBytes(List list) throws Exception { + List headers = materialExportHeaders(); + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + Sheet sheet = wb.createSheet("materials"); + Row hr = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + hr.createCell(i).setCellValue(headers.get(i)); + } + int rowNum = 1; + for (Material m : list) { + Row r = sheet.createRow(rowNum++); + int col = 0; + // r.createCell(col++).setCellValue(nvl(m.getMaterialId())); + // r.createCell(col++).setCellValue(nvl(m.getProjectId())); + r.createCell(col++).setCellValue(nvl(m.getName())); + r.createCell(col++).setCellValue(nvl(toStr(m.getUConcentration()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getUEnrichment()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getUo2Density()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getPuConcentration()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getPuo2Density()))); + // r.createCell(col++).setCellValue(nvl(toStr(m.getPuIsotope()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getEPu240()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getEPu241()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getEPu242()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getEPu239()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getEPu238()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getHno3Acidity()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getH2c2o4Concentration()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getOrganicRatio()))); + r.createCell(col++).setCellValue(nvl(toStr(m.getMoistureContent()))); + r.createCell(col++).setCellValue(nvl(m.getCustomAttrs())); + // r.createCell(col++).setCellValue(m.getCreatedAt() == null ? "" : m.getCreatedAt().toString()); + // r.createCell(col++).setCellValue(m.getUpdatedAt() == null ? "" : m.getUpdatedAt().toString()); + // r.createCell(col++).setCellValue(nvl(m.getModifier())); + } + wb.write(bos); + return bos.toByteArray(); + } + } + + private List materialExportHeaders() { + List headers = new ArrayList<>(); + // headers.add("material_id"); + // headers.add("project_id"); + headers.add("name"); + headers.add("u_concentration"); + headers.add("u_enrichment"); + headers.add("uo2_density"); + headers.add("pu_concentration"); + headers.add("puo2_density"); + // headers.add("pu_isotope"); + headers.add("e_pu240"); + headers.add("e_pu241"); + headers.add("e_pu242"); + headers.add("e_pu239"); + headers.add("e_pu238"); + headers.add("hno3_acidity"); + headers.add("h2c2o4_concentration"); + headers.add("organic_ratio"); + headers.add("moisture_content"); + headers.add("custom_attrs"); + // headers.add("created_at"); + // headers.add("updated_at"); + // headers.add("modifier"); + return headers; + } + + private String toStr(BigDecimal d) { + return d == null ? null : d.stripTrailingZeros().toPlainString(); + } + + private String nvl(String s) { + return s == null ? "" : s; + } } diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/ModelTrainServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/ModelTrainServiceImpl.java index a62594b..e738174 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/ModelTrainServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/ModelTrainServiceImpl.java @@ -683,14 +683,16 @@ public class ModelTrainServiceImpl extends ServiceImpl 4 && "runs".equals(parts[0])) { - materialType = parts[3]; + String mt = normalizeMaterialType(parts[3]); + if (mt != null) { + materialType = mt; + } } } @@ -768,6 +770,16 @@ public class ModelTrainServiceImpl extends ServiceImpl