后端适配新数据集及设备类型

This commit is contained in:
wanxiaoli 2026-05-07 09:19:12 +08:00
parent e1f770c795
commit c83b627674
21 changed files with 1692 additions and 44 deletions

View File

@ -114,12 +114,11 @@ public class SimBuilder {
List<Material> mats = materialService.list(new QueryWrapper<Material>().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());

View File

@ -103,7 +103,14 @@ public class AlgorithmModelController {
QueryWrapper<AlgorithmModel> 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<String, Object> options(@RequestParam String algorithmType,
@RequestParam String deviceType,
@RequestParam(required = false) String materialType) {
QueryWrapper<AlgorithmModel> 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<AlgorithmModel> list = algorithmModelService.list(qw);
String currentModelId = null;
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<AlgorithmModel> 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<AlgorithmModel> 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<String, Object> trainExcel(@RequestBody Map<String, Object> 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<AlgorithmModel> 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<String, Object> trainSamples(@RequestBody Map<String, Object> 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<Map>由前端提供
String modelDir = str(body.getOrDefault("model_dir", ""));
boolean activate = bool(body.getOrDefault("activate", false));
@ -276,7 +338,11 @@ public class AlgorithmModelController {
QueryWrapper<AlgorithmModel> 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 {

View File

@ -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<String, Object> validateCriticalDataV2(@RequestParam("file") MultipartFile file,
@RequestParam String deviceType) {
return criticalDataService.validateCriticalDataV2(file, deviceType);
}
@GetMapping("/v2/export")
public ResponseEntity<byte[]> exportCriticalDataV2(@RequestParam String deviceType,
@RequestParam(required = false) List<String> 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<byte[]> 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);
}
/**

View File

@ -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 导入设备ExcelV2
* 输入参数表单文件字段 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 校验设备ExcelV2
* 输入参数表单文件字段 file可选 projectId/deviceType
* 设计要点仅做解析与校验不落库返回 errors 与可用于导入的 _rows前端可据此预览/提示
* 权限projectId != -1 时校验项目写权限与导入保持一致
* 输出参数校验结果 Mapok/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<String, Object> 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 导出设备ExcelV2
* 输入参数可选 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<byte[]> exportDevicesV2(@RequestParam(required = false) String projectId,
@RequestParam(required = false) String deviceType,
@RequestParam(required = false) List<String> 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 下载导入模板ExcelV2
* 输入参数deviceType
* 设计要点模板表头与 size-schema 对齐避免前端/后端/导入导出口径漂移
* 输出参数Excel 二进制xlsx
* @param deviceType 设备类型
* @return xlsx 模板字节流
*/
@GetMapping("/v2/template")
@Operation(summary = "下载导入模板V2", description = "模板表头与 size-schema 对齐,便于导入导出与推理口径统一")
public ResponseEntity<byte[]> 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();

View File

@ -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<String, DeviceSizeSchema> all() {
return registry.getAllSchemas();
}
}

View File

@ -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<byte[]> exportMaterialsV2(@RequestParam String projectId,
@RequestParam(required = false) List<String> 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<byte[]> 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页码默认1pageSize每页条数默认10

View File

@ -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格式 */

View File

@ -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;

View File

@ -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;
}

View File

@ -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<DeviceSizeField> fields;
}

View File

@ -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<String, DeviceSizeSchema> 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<String, DeviceSizeSchema> getAllSchemas() {
return schemas;
}
/**
* 获取指定设备类型的扁平化 size keys order 有序
* 注意这些 key 同时作为导入/导出的 Excel 列名与 device.size JSON 的键必须保持稳定
*/
public List<String> getSizeKeys(String deviceType) {
DeviceSizeSchema s = getSchema(deviceType);
if (s == null || s.getFields() == null) return List.of();
List<String> out = new ArrayList<>();
for (DeviceSizeField f : s.getFields()) out.add(f.getKey());
return out;
}
private static Map<String, DeviceSizeSchema> buildSchemas() {
Map<String, DeviceSizeSchema> 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<DeviceSizeField> fields) {
DeviceSizeSchema s = new DeviceSizeSchema();
s.setDeviceType(deviceType);
s.setSchemaVersion("v2");
s.setFields(fields);
return s;
}
private static List<DeviceSizeField> fields(DeviceSizeField... fs) {
List<DeviceSizeField> 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;
}
}

View File

@ -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<CriticalData> {
/**
@ -11,5 +12,12 @@ public interface CriticalDataService extends IService<CriticalData> {
*/
boolean importCriticalData(MultipartFile file, String deviceType);
boolean importCriticalDataV2(MultipartFile file, String deviceType);
Map<String, Object> validateCriticalDataV2(MultipartFile file, String deviceType);
byte[] exportCriticalDataV2(String deviceType, List<String> ids);
byte[] templateCriticalDataV2(String deviceType);
}

View File

@ -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<Device> {
/**
* 导入设备
*/
boolean importDevices(MultipartFile file, String deviceType);
boolean importDevicesV2(MultipartFile file, String projectId, String deviceType);
Map<String, Object> validateDevicesV2(MultipartFile file, String projectId, String deviceType);
byte[] exportDevicesV2(String projectId, String deviceType, List<String> ids);
byte[] templateDevicesV2(String deviceType);
boolean createDevice(Device device) ;
boolean saveOrUpdateByBusiness(Device device);

View File

@ -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<Material> {
/**
@ -17,4 +18,8 @@ public interface MaterialService extends IService<Material> {
boolean saveMaterial(Material material);
boolean saveOrUpdateByBusiness(Material material);
byte[] exportMaterialsV2(String projectId, List<String> ids, String nameLike);
byte[] templateMaterialsV2();
}

View File

@ -36,15 +36,18 @@ public class AlgorithmModelServiceImpl extends ServiceImpl<AlgorithmModelMapper,
@Override
public AlgorithmModel getCurrentModel(String algorithmType, String deviceType, String materialType) {
log.debug("Querying current model for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
String mt = materialType == null ? null : materialType.trim();
if (mt != null && mt.isEmpty()) mt = null;
String mt = normalizeMaterialType(materialType);
QueryWrapper<AlgorithmModel> 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<AlgorithmModelMapper,
return null;
}
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;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteBatchWithCheck(List<String> ids) {

View File

@ -10,4 +10,30 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
public class AlgorithmServiceImpl extends ServiceImpl<AlgorithmMapper, Algorithm> implements AlgorithmService {
}
@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;
}
}

View File

@ -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<String, Object> res = validateCriticalDataV2(file, deviceType);
@SuppressWarnings("unchecked")
List<CriticalData> list = (List<CriticalData>) res.get("_rows");
if (list == null || list.isEmpty()) {
return false;
}
return this.saveBatch(list, 500);
}
@Override
public Map<String, Object> validateCriticalDataV2(MultipartFile file, String deviceType) {
Map<String, Object> out = new HashMap<>();
List<Map<String, Object>> 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<String> ids) {
try {
if (deviceType == null || deviceType.isBlank()) return new byte[0];
var qw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<CriticalData>()
.eq("device_type", deviceType)
.orderByDesc("created_at");
if (ids != null && !ids.isEmpty()) {
qw.in("critical_id", ids);
}
List<CriticalData> 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<String, Object> 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<String, Integer> 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<String> 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<Map<String, Object>> errors = new ArrayList<>();
List<CriticalData> 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<String, Object> 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<String, Object> 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<String> 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<String, Object> 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<String, Object> err(int row, String msg) {
Map<String, Object> m = new HashMap<>();
m.put("row", row);
m.put("msg", msg);
return m;
}
private List<String> 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<String> sizeCols = getSizeColumnsByDeviceType(deviceType);
List<String> 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<CriticalData> list) throws Exception {
List<String> sizeCols = getSizeColumnsByDeviceType(deviceType);
List<String> 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<String, String> 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<String, String> flattenSize(String sizeJson, List<String> cols) {
Map<String, String> 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();

View File

@ -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<String, Object> res = validateDevicesV2(file, projectId, deviceType);
@SuppressWarnings("unchecked")
List<Device> list = (List<Device>) res.get("_rows");
if (list == null || list.isEmpty()) return false;
return this.saveBatch(list, 500);
}
@Override
public Map<String, Object> 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<String> ids) {
try {
var qw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Device>()
.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<Device> 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<String, Object> 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<String, Integer> 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<Map<String, Object>> errors = new ArrayList<>();
List<Device> 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());
}
// 4size 来源规则优先用 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<String> sizeCols = getSizeColumnsByDeviceType(rowType);
if (sizeCols.isEmpty()) {
errors.add(err(r, "未知设备类型: " + rowType));
continue;
}
Map<String, Object> 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<String, Object> 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<String, Object> err(int row, String msg) {
Map<String, Object> m = new HashMap<>();
m.put("row", row);
m.put("msg", msg);
return m;
}
private List<String> getSizeColumnsByDeviceType(String deviceType) {
if (deviceType == null || deviceType.isBlank()) return Collections.emptyList();
try {
List<String> keys = deviceSizeSchemaRegistry.getSizeKeys(deviceType);
return keys == null ? Collections.emptyList() : keys;
} catch (Exception e) {
return Collections.emptyList();
}
}
private List<String> getAllSizeColumns() {
try {
java.util.LinkedHashSet<String> set = new java.util.LinkedHashSet<>();
Map<String, com.yfd.business.css.meta.DeviceSizeSchema> 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<String> sizeCols = getSizeColumnsByDeviceType(deviceType);
if (sizeCols.isEmpty()) sizeCols = getAllSizeColumns();
List<String> 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<Device> list) throws Exception {
List<String> sizeCols = (deviceType == null || deviceType.isBlank()) ? getAllSizeColumns() : getSizeColumnsByDeviceType(deviceType);
if (sizeCols.isEmpty()) sizeCols = getAllSizeColumns();
List<String> 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<String, String> 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<String, String> flattenSize(String deviceType, String sizeJson, List<String> cols) {
Map<String, String> 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<String, String> 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<String, String> 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;

View File

@ -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<String> ids, String nameLike) {
try {
QueryWrapper<Material> 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<Material> 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<String> 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<Material> list) throws Exception {
List<String> 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<String> materialExportHeaders() {
List<String> 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;
}
}

View File

@ -683,14 +683,16 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
throw new BizException("算法类型或设备类型不能为空");
}
// 解析材料类型
String materialType = "unknown";
String outputPath = task.getModelOutputPath();
if (outputPath != null && !outputPath.isBlank()) {
String[] parts = outputPath.replace("\\", "/").split("/");
// 预期结构: runs/{algorithmType}/{deviceType}/{materialType}/{taskId}/...
if (parts.length > 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<ModelTrainTaskMapper, Mod
return algorithmModelService.save(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 firstNonBlank(String... values) {
if (values == null) return null;
for (String v : values) {

View File

@ -810,12 +810,33 @@ public class ProjectServiceImpl
double diameter = sizeNode.get("diameter").asDouble();
state.put("diameter", diameter);
}
if (sizeNode.has("tray_diameter")) {
double diameter = sizeNode.get("tray_diameter").asDouble();
state.put("diameter", diameter);
}
if (sizeNode.has("tray_outer_diameter")) {
double diameter = sizeNode.get("tray_outer_diameter").asDouble();
state.put("diameter", diameter);
}
// 读取 height
if (sizeNode.has("height")) {
double height = sizeNode.get("height").asDouble();
state.put("height", height);
}
if (sizeNode.has("tray_height")) {
double height = sizeNode.get("tray_height").asDouble();
state.put("height", height);
} else if (sizeNode.has("upper_height")) {
double height = sizeNode.get("upper_height").asDouble();
state.put("height", height);
} else if (sizeNode.has("lower_height")) {
double height = sizeNode.get("lower_height").asDouble();
state.put("height", height);
}
}
} catch (Exception e) {
// 出错可以记录日志