From f59dd61da4c49e8108470449aac85fa88fac208e Mon Sep 17 00:00:00 2001 From: wanxiaoli Date: Thu, 2 Apr 2026 15:31:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../css/controller/AlgorithmController.java | 9 +- .../controller/AlgorithmModelController.java | 7 +- .../css/controller/DeviceController.java | 30 ++ .../css/controller/EventController.java | 52 ++++ .../css/controller/MaterialController.java | 30 ++ .../css/controller/ModelTrainController.java | 10 +- .../css/controller/ProjectController.java | 72 ++++- .../css/controller/ScenarioController.java | 36 ++- .../controller/ScenarioResultController.java | 15 + .../business/css/domain/ModelTrainTask.java | 4 + .../com/yfd/business/css/domain/Project.java | 6 + .../css/security/ProjectAccessHelper.java | 94 ++++++ .../css/service/ModelTrainService.java | 4 + .../business/css/service/ProjectService.java | 2 + .../service/impl/ModelTrainServiceImpl.java | 116 +++++++ .../css/service/impl/ProjectServiceImpl.java | 282 ++++++++++++++++++ 16 files changed, 763 insertions(+), 6 deletions(-) create mode 100644 business-css/src/main/java/com/yfd/business/css/security/ProjectAccessHelper.java diff --git a/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmController.java b/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmController.java index 1af3fb7..d3e2438 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/AlgorithmController.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.Authentication; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.AnonymousAuthenticationToken; import com.yfd.platform.annotation.Log; @@ -45,6 +46,7 @@ public class AlgorithmController { } @Log(value = "新增算法", module = "算法管理") + // @PreAuthorize("hasAuthority('algorithm:add')") @PostMapping @Operation(summary = "新增算法", description = "请求体传入算法对象,返回是否新增成功") public boolean createAlgorithm(@RequestBody Algorithm algorithm) { @@ -55,6 +57,7 @@ public class AlgorithmController { } @Log(value = "修改算法", module = "算法管理") + // @PreAuthorize("hasAuthority('algorithm:update')") @PutMapping @Operation(summary = "修改算法", description = "请求体传入算法对象(需包含主键),返回是否修改成功") public boolean updateAlgorithm(@RequestBody Algorithm algorithm) { @@ -64,6 +67,7 @@ public class AlgorithmController { } @Log(value = "删除算法", module = "算法管理") + // @PreAuthorize("hasAuthority('algorithm:delete')") @DeleteMapping("/{id}") @Operation(summary = "删除算法(单条)", description = "根据算法ID删除算法") public boolean deleteAlgorithm(@PathVariable String id) { @@ -71,6 +75,7 @@ public class AlgorithmController { } @Log(value = "批量删除算法", module = "算法管理") + // @PreAuthorize("hasAuthority('algorithm:deleteBatch')") @DeleteMapping @Operation(summary = "删除算法(批量)", description = "请求体传入算法ID列表,批量删除算法") public boolean deleteAlgorithms(@RequestBody List ids) { @@ -78,7 +83,7 @@ public class AlgorithmController { } @Log(value = "激活算法", module = "算法管理") - //算法类型激活 + // @PreAuthorize("hasAuthority('algorithm:activate')") @PostMapping("/activate") @Operation(summary = "激活算法", description = "激活当前算法类型") public boolean activate(@RequestParam String algorithmId) { @@ -92,7 +97,7 @@ public class AlgorithmController { } @Log(value = "关闭算法", module = "算法管理") - //算法类型关闭 + // @PreAuthorize("hasAuthority('algorithm:unactivate')") @PostMapping("/unactivate") @Operation(summary = "关闭算法", description = "关闭当前算法类型") public boolean unactivate(@RequestParam String algorithmId) { 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 fd38b78..a8e1c9b 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 @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.Authentication; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.AnonymousAuthenticationToken; import com.yfd.platform.annotation.Log; @@ -53,6 +54,7 @@ public class AlgorithmModelController { } @Log(value = "新增模型版本", module = "算法模型管理") + // @PreAuthorize("hasAuthority('algorithmModel:add')") @PostMapping @Operation(summary = "新增模型版本", description = "请求体传入模型版本对象,返回是否新增成功") public boolean create(@RequestBody AlgorithmModel model) { @@ -63,6 +65,7 @@ public class AlgorithmModelController { } @Log(value = "修改模型版本", module = "算法模型管理") + // @PreAuthorize("hasAuthority('algorithmModel:update')") @PutMapping @Operation(summary = "修改模型版本", description = "请求体传入模型版本对象(需包含主键),返回是否修改成功") public boolean update(@RequestBody AlgorithmModel model) { @@ -72,6 +75,7 @@ public class AlgorithmModelController { } @Log(value = "删除模型版本", module = "算法模型管理") + // @PreAuthorize("hasAuthority('algorithmModel:delete')") @DeleteMapping("/{id}") @Operation(summary = "删除模型版本(单条)", description = "根据模型ID删除模型版本") public boolean delete(@PathVariable String id) { @@ -79,6 +83,7 @@ public class AlgorithmModelController { } @Log(value = "批量删除模型版本", module = "算法模型管理") + // @PreAuthorize("hasAuthority('algorithmModel:deleteBatch')") @DeleteMapping @Operation(summary = "删除模型版本(批量)", description = "请求体传入模型ID列表,批量删除模型版本") public boolean deleteBatch(@RequestBody List ids) { @@ -124,7 +129,7 @@ public class AlgorithmModelController { } @Log(value = "激活模型版本", module = "算法模型管理") - //版本激活 + @PreAuthorize("hasAuthority('algorithmModel:activate')") @PostMapping("/activate") @Operation(summary = "激活模型版本", description = "将目标模型版本设为当前,并将同组(算法+设备+材料)其他版本设为非当前") public boolean activate(@RequestParam String algorithmModelId) { 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 1af1b1c..ba210fc 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 @@ -3,6 +3,7 @@ package com.yfd.business.css.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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 org.springframework.web.bind.annotation.*; @@ -24,6 +25,8 @@ public class DeviceController { private DeviceService deviceService; @Resource private IUserService userService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** * 1. 新增设备 @@ -35,6 +38,9 @@ public class DeviceController { @Log(value = "新增设备", module = "设备管理") @PostMapping public boolean create(@RequestBody Device device) { + if (device.getProjectId() != null && !device.getProjectId().isBlank() && !"-1".equals(device.getProjectId())) { + projectAccessHelper.assertCanWriteProject(device.getProjectId()); + } device.setModifier(currentUsername()); return deviceService.createDevice(device); } @@ -42,6 +48,9 @@ public class DeviceController { @Log(value = "保存或更新设备", module = "设备管理") @PostMapping("/saveOrUpdate") public boolean saveOrUpdate(@RequestBody Device device) { + if (device.getProjectId() != null && !device.getProjectId().isBlank() && !"-1".equals(device.getProjectId())) { + projectAccessHelper.assertCanWriteProject(device.getProjectId()); + } device.setModifier(currentUsername()); return deviceService.saveOrUpdateByBusiness(device); } @@ -56,6 +65,12 @@ public class DeviceController { @Log(value = "编辑设备", module = "设备管理") @PutMapping public boolean update(@RequestBody Device device) { + if (device.getDeviceId() != null && !device.getDeviceId().isBlank()) { + Device db = deviceService.getById(device.getDeviceId()); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank() && !"-1".equals(db.getProjectId())) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } + } device.setModifier(currentUsername()); device.setUpdatedAt(LocalDateTime.now()); return deviceService.updateById(device); @@ -71,6 +86,10 @@ public class DeviceController { @Log(value = "删除设备", module = "设备管理") @DeleteMapping("/{id}") public boolean delete(@PathVariable String id) { + Device db = deviceService.getById(id); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank() && !"-1".equals(db.getProjectId())) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } return deviceService.removeById(id); } @@ -84,6 +103,14 @@ public class DeviceController { @Log(value = "批量删除设备", module = "设备管理") @DeleteMapping public boolean deleteBatch(@RequestBody List ids) { + if (ids != null && !ids.isEmpty()) { + List list = deviceService.list(new QueryWrapper().in("device_id", ids)); + for (Device d : list) { + if (d.getProjectId() != null && !d.getProjectId().isBlank() && !"-1".equals(d.getProjectId())) { + projectAccessHelper.assertCanWriteProject(d.getProjectId()); + } + } + } return deviceService.removeByIds(ids); } @@ -113,6 +140,9 @@ public class DeviceController { */ @GetMapping("/by-project") public List listByProject(@RequestParam String projectId) { + if (projectId != null && !projectId.isBlank() && !"-1".equals(projectId)) { + projectAccessHelper.assertCanReadProject(projectId); + } return deviceService.list( new QueryWrapper() .eq("project_id", projectId) diff --git a/business-css/src/main/java/com/yfd/business/css/controller/EventController.java b/business-css/src/main/java/com/yfd/business/css/controller/EventController.java index 550ad41..9e55cf9 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/EventController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/EventController.java @@ -2,7 +2,10 @@ package com.yfd.business.css.controller; import com.yfd.business.css.domain.Device; import com.yfd.business.css.domain.Event; +import com.yfd.business.css.domain.Scenario; +import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.EventService; +import com.yfd.business.css.service.ScenarioService; import com.yfd.platform.system.service.IUserService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -40,6 +43,10 @@ public class EventController { private ObjectMapper objectMapper; @Resource private IUserService userService; + @Resource + private ScenarioService scenarioService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** @@ -48,6 +55,7 @@ public class EventController { @Log(value = "新增始发事件", module = "事件管理") @PostMapping public ResponseEntity> addEvent(@RequestBody Event event) { + assertCanWriteByScenarioId(event.getScenarioId()); event.setModifier(currentUsername()); eventService.save(event); Event savedEvent = event; @@ -69,6 +77,14 @@ public class EventController { @Log(value = "修改始发事件", module = "事件管理") @PutMapping public boolean update(@RequestBody Event event) { + if (event.getScenarioId() != null && !event.getScenarioId().isBlank()) { + assertCanWriteByScenarioId(event.getScenarioId()); + } else if (event.getEventId() != null && !event.getEventId().isBlank()) { + Event db = eventService.getById(event.getEventId()); + if (db != null) { + assertCanWriteByScenarioId(db.getScenarioId()); + } + } event.setModifier(currentUsername()); event.setUpdatedAt(LocalDateTime.now()); return eventService.updateById(event); @@ -89,6 +105,14 @@ public class EventController { List savedEvents = new ArrayList<>(); for (Event event : events) { + if (event.getScenarioId() != null && !event.getScenarioId().isBlank()) { + assertCanWriteByScenarioId(event.getScenarioId()); + } else if (event.getEventId() != null && !event.getEventId().isBlank()) { + Event db = eventService.getById(event.getEventId()); + if (db != null) { + assertCanWriteByScenarioId(db.getScenarioId()); + } + } if (event.getEventId() != null && eventService.getById(event.getEventId()) != null) { // 更新逻辑 event.setModifier(currentUser); @@ -122,6 +146,10 @@ public class EventController { @PathVariable String eventId, @RequestBody Map requestBody ) { + Event db = eventService.getById(eventId); + if (db != null) { + assertCanWriteByScenarioId(db.getScenarioId()); + } Object attrChanges = requestBody.get("attr_changes"); if (attrChanges == null) { return ResponseEntity.badRequest().body(Map.of( @@ -159,6 +187,7 @@ public class EventController { */ @GetMapping("/by-scenario") public List listByScenario(@RequestParam String scenarioId) { + assertCanReadByScenarioId(scenarioId); return eventService.list( new QueryWrapper() .select("event_id","scenario_id","device_id","material_id","attr_changes","trigger_time","created_at","modifier") @@ -177,6 +206,10 @@ public class EventController { @Log(value = "删除始发事件", module = "事件管理") @DeleteMapping("/{eventId}") public ResponseEntity> deleteEvent(@PathVariable String eventId) { + Event db = eventService.getById(eventId); + if (db != null) { + assertCanWriteByScenarioId(db.getScenarioId()); + } boolean ok = eventService.removeById(eventId); if (ok) { return ResponseEntity.ok(Map.of( @@ -201,6 +234,9 @@ public class EventController { @GetMapping("/{eventId}/attr-parse") public ResponseEntity> parseAttrChanges(@PathVariable String eventId) { Event ev = eventService.getById(eventId); + if (ev != null) { + assertCanReadByScenarioId(ev.getScenarioId()); + } List issues = new ArrayList<>(); EventAttrParseResult result = new EventAttrParseResult(); result.setEventId(eventId); @@ -384,6 +420,22 @@ public class EventController { return 0.0; } + private void assertCanReadByScenarioId(String scenarioId) { + if (scenarioId == null || scenarioId.isBlank()) return; + Scenario sc = scenarioService.getById(scenarioId); + if (sc == null) return; + if (sc.getProjectId() == null || sc.getProjectId().isBlank()) return; + projectAccessHelper.assertCanReadProject(sc.getProjectId()); + } + + private void assertCanWriteByScenarioId(String scenarioId) { + if (scenarioId == null || scenarioId.isBlank()) return; + Scenario sc = scenarioService.getById(scenarioId); + if (sc == null) return; + if (sc.getProjectId() == null || sc.getProjectId().isBlank()) return; + projectAccessHelper.assertCanWriteProject(sc.getProjectId()); + } + private String currentUsername() { try { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 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 906d2ad..4390011 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 @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yfd.business.css.domain.Device; 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.web.bind.annotation.*; @@ -25,6 +26,8 @@ public class MaterialController { private MaterialService materialService; @Resource private IUserService userService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** * 1. 新增物料 @@ -36,6 +39,9 @@ public class MaterialController { @Log(value = "新增物料", module = "物料管理") @PostMapping public boolean create(@RequestBody Material material) { + if (material.getProjectId() != null && !material.getProjectId().isBlank() && !"-1".equals(material.getProjectId())) { + projectAccessHelper.assertCanWriteProject(material.getProjectId()); + } material.setModifier(currentUsername()); return materialService.saveMaterial(material); } @@ -43,6 +49,9 @@ public class MaterialController { @Log(value = "保存或更新物料", module = "物料管理") @PostMapping("/saveOrUpdate") public boolean saveOrUpdate(@RequestBody Material material) { + if (material.getProjectId() != null && !material.getProjectId().isBlank() && !"-1".equals(material.getProjectId())) { + projectAccessHelper.assertCanWriteProject(material.getProjectId()); + } material.setModifier(currentUsername()); return materialService.saveOrUpdateByBusiness(material); } @@ -57,6 +66,12 @@ public class MaterialController { @Log(value = "编辑物料", module = "物料管理") @PutMapping public boolean update(@RequestBody Material material) { + if (material.getMaterialId() != null && !material.getMaterialId().isBlank()) { + Material db = materialService.getById(material.getMaterialId()); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank() && !"-1".equals(db.getProjectId())) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } + } material.setModifier(currentUsername()); material.setUpdatedAt(LocalDateTime.now()); return materialService.updateById(material); @@ -72,6 +87,10 @@ public class MaterialController { @Log(value = "删除物料", module = "物料管理") @DeleteMapping("/{id}") public boolean delete(@PathVariable String id) { + Material db = materialService.getById(id); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank() && !"-1".equals(db.getProjectId())) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } return materialService.removeById(id); } @@ -85,6 +104,14 @@ public class MaterialController { @Log(value = "批量删除物料", module = "物料管理") @DeleteMapping public boolean deleteBatch(@RequestBody List ids) { + if (ids != null && !ids.isEmpty()) { + List list = materialService.list(new QueryWrapper().in("material_id", ids)); + for (Material m : list) { + if (m.getProjectId() != null && !m.getProjectId().isBlank() && !"-1".equals(m.getProjectId())) { + projectAccessHelper.assertCanWriteProject(m.getProjectId()); + } + } + } return materialService.removeByIds(ids); } @@ -130,6 +157,9 @@ public class MaterialController { public Page pageByProject(@RequestParam String projectId, @RequestParam(defaultValue = "1") long pageNum, @RequestParam(defaultValue = "20") long pageSize) { + if (projectId != null && !projectId.isBlank() && !"-1".equals(projectId)) { + projectAccessHelper.assertCanReadProject(projectId); + } QueryWrapper qw = new QueryWrapper() .eq("project_id", projectId) .orderByDesc("created_at"); diff --git a/business-css/src/main/java/com/yfd/business/css/controller/ModelTrainController.java b/business-css/src/main/java/com/yfd/business/css/controller/ModelTrainController.java index 0b9645e..582d61c 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/ModelTrainController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/ModelTrainController.java @@ -9,6 +9,7 @@ import com.yfd.business.css.service.ModelTrainService; import com.yfd.business.css.service.TrainWebSocketService; import com.yfd.platform.config.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import com.yfd.platform.annotation.Log; import org.springframework.web.multipart.MultipartFile; @@ -57,13 +58,17 @@ public class ModelTrainController { @PostMapping("/upload") public ResponseResult upload(@RequestParam("file") MultipartFile file) { String path = modelTrainService.uploadDataset(file); - return ResponseResult.successData(path); + return ResponseResult.successData(Map.of( + "path", path, + "columns", modelTrainService.parseDatasetColumns(path) + )); } /** * 提交训练任务 (支持文件上传和 JSON 参数) */ @Log(value = "提交训练任务", module = "模型训练") + // @PreAuthorize("hasAuthority('modelTrain:submit')") @PostMapping("/submit") public ResponseResult submit(@RequestPart("task") String taskJson, @RequestPart(value = "file", required = false) MultipartFile file) { @@ -137,6 +142,7 @@ public class ModelTrainController { * 发布模型 */ @Log(value = "发布训练模型", module = "模型训练") + // @PreAuthorize("hasAuthority('modelTrain:publish')") @PostMapping("/publish") public ResponseResult publish(@RequestBody Map body) { String taskId = body.get("taskId"); @@ -149,6 +155,8 @@ public class ModelTrainController { * 删除训练任务 */ @Log(value = "删除训练任务", module = "模型训练") + @PreAuthorize("hasAuthority('modelTrain:delete')") + //删除训练任务 @DeleteMapping("/{taskId}") public ResponseResult delete(@PathVariable String taskId) { boolean success = modelTrainService.removeById(taskId); diff --git a/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java b/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java index 64df1e1..811713d 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java @@ -3,6 +3,7 @@ package com.yfd.business.css.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yfd.business.css.domain.Project; +import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.ProjectService; import com.yfd.platform.system.service.IUserService; import io.swagger.v3.oas.annotations.Operation; @@ -16,6 +17,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.Authentication; import com.yfd.platform.annotation.Log; import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; import java.util.List; @@ -33,6 +35,8 @@ public class ProjectController { private com.fasterxml.jackson.databind.ObjectMapper objectMapper; @Resource private IUserService userService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** * 1. 新增项目 @@ -45,6 +49,15 @@ public class ProjectController { @PostMapping @Operation(summary = "新增项目", description = "请求体传入项目对象,返回是否新增成功") public boolean create(@RequestBody Project project) { + if (project.getVisibility() == null || project.getVisibility().isBlank()) { + project.setVisibility("PRIVATE"); + } else { + project.setVisibility(projectAccessHelper.normalizeVisibility(project.getVisibility())); + } + if (project.getCreator() == null || project.getCreator().isBlank()) { + var u = userService.getUserInfo(); + if (u != null) project.setCreator(u.getId()); + } project.setModifier(currentUsername()); return projectService.save(project); } @@ -60,11 +73,32 @@ public class ProjectController { @PutMapping @Operation(summary = "修改项目", description = "请求体传入项目对象(需包含主键),返回是否修改成功") public boolean update(@RequestBody Project project) { + if (project.getProjectId() != null) { + projectAccessHelper.assertCanWriteProject(project.getProjectId()); + } project.setModifier(currentUsername()); project.setUpdatedAt(LocalDateTime.now()); return projectService.updateById(project); } + @Log(value = "修改项目可见性", module = "项目管理") + @PutMapping("/{id}/visibility") + @Operation(summary = "修改项目可见性", description = "仅项目所有者或管理员可修改(PRIVATE/READONLY/PUBLIC)") + public boolean updateVisibility(@PathVariable @Parameter(description = "项目ID", required = true) String id, + @RequestBody Map body) { + projectAccessHelper.assertCanManageVisibility(id); + String visibility = body == null ? null : body.get("visibility"); + if (visibility == null || visibility.isBlank()) { + return false; + } + Project p = new Project(); + p.setProjectId(id); + p.setVisibility(projectAccessHelper.normalizeVisibility(visibility)); + p.setModifier(currentUsername()); + p.setUpdatedAt(LocalDateTime.now()); + return projectService.updateById(p); + } + private String currentUsername() { try { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); @@ -88,6 +122,7 @@ public class ProjectController { @DeleteMapping("/{id}") @Operation(summary = "删除项目(单条)", description = "根据项目ID删除项目") public boolean delete(@PathVariable @Parameter(description = "项目ID", required = true) String id) { + projectAccessHelper.assertCanWriteProject(id); return projectService.removeById(id); } @@ -102,6 +137,11 @@ public class ProjectController { @DeleteMapping @Operation(summary = "删除项目(批量)", description = "请求体传入项目ID列表,批量删除项目") public boolean deleteBatch(@RequestBody List ids) { + if (ids != null) { + for (String id : ids) { + projectAccessHelper.assertCanWriteProject(id); + } + } return projectService.removeByIds(ids); } @@ -142,6 +182,7 @@ public class ProjectController { @GetMapping("/{id}/exportProject") @Operation(summary = "导出项目工程(多Sheet)", description = "根据项目ID导出工程数据,返回 Excel 附件 project_{id}.xlsx") public ResponseEntity exportProjectExcel(@PathVariable @Parameter(description = "项目ID", required = true) String id) { + projectAccessHelper.assertCanReadProject(id); byte[] bytes = projectService.exportProjectEngineeringExcel(id); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=project_" + id + ".xlsx") @@ -149,6 +190,15 @@ public class ProjectController { .body(bytes); } + @Log(value = "导入项目工程", module = "项目管理") + @PostMapping("/importProject") + @Operation(summary = "导入项目工程(多Sheet)", description = "上传 Excel 工程文件;若项目ID存在且 overwrite=false 则返回提示;overwrite=true 将先删除再导入") + public ResponseEntity> importProject(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "false") boolean overwrite) { + var res = projectService.importProjectEngineeringExcel(file, overwrite); + return ResponseEntity.ok(res); + } + /** * 6. 根据项目名称搜索(可为空)并分页返回 * 输入参数:查询参数 name(项目名称关键词,可为空),pageNum(页码,默认1),pageSize(每页条数,默认10) @@ -167,6 +217,14 @@ public class ProjectController { if (name != null && !name.isEmpty()) { qw.like("name", name); } + if (!projectAccessHelper.isAdmin()) { + String uid = projectAccessHelper.currentUserId(); + qw.and(w -> w.isNull("visibility") + .or().eq("visibility", "") + .or().eq("visibility", "PUBLIC") + .or().eq("visibility", "READONLY") + .or().eq("creator", uid)); + } Page page = new Page<>(pageNum, pageSize, true); return projectService.page(page, qw); } @@ -181,7 +239,13 @@ public class ProjectController { @GetMapping("/{id}") @Operation(summary = "根据项目ID获取项目信息", description = "项目不存在时返回空对象") public Project getById(@PathVariable @Parameter(description = "项目ID", required = true) String id) { - return projectService.getById(id); + Project p = projectService.getById(id); + if (p != null) { + if (!projectAccessHelper.canRead(p)) { + return null; + } + } + return p; } /** @@ -197,6 +261,7 @@ public class ProjectController { @Operation(summary = "更新项目拓扑结构", description = "请求体需包含合法的 topology JSON(对象或字符串)") public ResponseEntity> updateTopology(@PathVariable @Parameter(description = "项目ID", required = true) String id, @RequestBody Map requestBody) { + projectAccessHelper.assertCanWriteProject(id); Object topology = requestBody.get("topology"); if (topology == null) { return ResponseEntity.badRequest().body(Map.of( @@ -240,6 +305,7 @@ public class ProjectController { */ @Operation(summary = "解析项目拓扑结构", description = "返回属性节点、影响边与线性计算计划") public ResponseEntity> parseTopology(@PathVariable @Parameter(description = "项目ID", required = true) String id) { + projectAccessHelper.assertCanReadProject(id); var result = projectService.parseTopology(id); return ResponseEntity.ok(Map.of( "code", 0, @@ -256,6 +322,7 @@ public class ProjectController { */ @Operation(summary = "解析拓扑设备顺序", description = "根据 devices 出现顺序返回设备与物料属性视图") public ResponseEntity> parseDeviceOrder(@PathVariable @Parameter(description = "项目ID", required = true) String id) { + projectAccessHelper.assertCanReadProject(id); Object list = projectService.parseDeviceOrderWithMaterials(id); return ResponseEntity.ok(Map.of( "code", 0, @@ -272,6 +339,7 @@ public class ProjectController { */ @Operation(summary = "解析画布视图数据", description = "返回设备/管线/边界与显示配置") public ResponseEntity> parseCanvas(@PathVariable @Parameter(description = "项目ID", required = true) String id) { + projectAccessHelper.assertCanReadProject(id); var view = projectService.parseCanvasView(id); return ResponseEntity.ok(Map.of( "code", 0, @@ -285,6 +353,7 @@ public class ProjectController { public ResponseEntity> initSimulation(@RequestParam @Parameter(description = "项目ID", required = true) String projectId, @RequestParam @Parameter(description = "情景ID", required = true) String scenarioId, @RequestBody(required = false) Map params) { + projectAccessHelper.assertCanWriteProject(projectId); var res = projectService.initSimulation(projectId, scenarioId, params == null ? Map.of() : params); return ResponseEntity.ok(Map.of( "code", 0, @@ -299,6 +368,7 @@ public class ProjectController { public ResponseEntity> runSimulation(@RequestParam @Parameter(description = "项目ID", required = true) String projectId, @RequestParam @Parameter(description = "情景ID", required = true) String scenarioId, @RequestBody(required = false) Map params) { + projectAccessHelper.assertCanWriteProject(projectId); var res = projectService.runSimulation(projectId, scenarioId, params == null ? Map.of() : params); return ResponseEntity.ok(Map.of( "code", 0, diff --git a/business-css/src/main/java/com/yfd/business/css/controller/ScenarioController.java b/business-css/src/main/java/com/yfd/business/css/controller/ScenarioController.java index 3c8f9e5..83bafc0 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/ScenarioController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/ScenarioController.java @@ -3,6 +3,7 @@ package com.yfd.business.css.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yfd.business.css.domain.Scenario; +import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.ScenarioService; import com.yfd.platform.system.service.IUserService; import org.springframework.web.bind.annotation.*; @@ -23,6 +24,8 @@ public class ScenarioController { private ScenarioService scenarioService; @Resource private IUserService userService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** * 1. 新增情景 @@ -34,6 +37,9 @@ public class ScenarioController { @Log(value = "新增情景", module = "情景管理") @PostMapping public boolean create(@RequestBody Scenario scenario) { + if (scenario.getProjectId() != null && !scenario.getProjectId().isBlank()) { + projectAccessHelper.assertCanWriteProject(scenario.getProjectId()); + } scenario.setModifier(currentUsername()); scenario.setCreatedAt(LocalDateTime.now()); scenario.setUpdatedAt(LocalDateTime.now()); @@ -51,6 +57,9 @@ public class ScenarioController { @Log(value = "新增情景并返回", module = "情景管理") @PostMapping("/createAndReturn") public java.util.Map createAndReturn(@RequestBody Scenario scenario) { + if (scenario.getProjectId() != null && !scenario.getProjectId().isBlank()) { + projectAccessHelper.assertCanWriteProject(scenario.getProjectId()); + } scenario.setModifier(currentUsername()); scenario.setCreatedAt(LocalDateTime.now()); scenario.setUpdatedAt(LocalDateTime.now()); @@ -72,6 +81,12 @@ public class ScenarioController { @Log(value = "修改情景", module = "情景管理") @PutMapping public boolean update(@RequestBody Scenario scenario) { + if (scenario.getScenarioId() != null && !scenario.getScenarioId().isBlank()) { + Scenario db = scenarioService.getById(scenario.getScenarioId()); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank()) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } + } scenario.setModifier(currentUsername()); scenario.setUpdatedAt(LocalDateTime.now()); return scenarioService.updateById(scenario); @@ -87,6 +102,10 @@ public class ScenarioController { @Log(value = "删除情景", module = "情景管理") @DeleteMapping("/{id}") public boolean delete(@PathVariable String id) { + Scenario db = scenarioService.getById(id); + if (db != null && db.getProjectId() != null && !db.getProjectId().isBlank()) { + projectAccessHelper.assertCanWriteProject(db.getProjectId()); + } return scenarioService.removeById(id); } @@ -100,6 +119,14 @@ public class ScenarioController { @Log(value = "批量删除情景", module = "情景管理") @DeleteMapping public boolean deleteBatch(@RequestBody List ids) { + if (ids != null && !ids.isEmpty()) { + List list = scenarioService.list(new QueryWrapper().in("scenario_id", ids)); + for (Scenario sc : list) { + if (sc.getProjectId() != null && !sc.getProjectId().isBlank()) { + projectAccessHelper.assertCanWriteProject(sc.getProjectId()); + } + } + } return scenarioService.removeByIds(ids); } @@ -114,7 +141,11 @@ public class ScenarioController { */ @GetMapping("/{id}") public Scenario getById(@PathVariable String id) { - return scenarioService.getById(id); + Scenario sc = scenarioService.getById(id); + if (sc != null && sc.getProjectId() != null && !sc.getProjectId().isBlank()) { + projectAccessHelper.assertCanReadProject(sc.getProjectId()); + } + return sc; } /** @@ -132,6 +163,9 @@ public class ScenarioController { @RequestParam(required = false) String name, @RequestParam(defaultValue = "1") long pageNum, @RequestParam(defaultValue = "20") long pageSize) { + if (projectId != null && !projectId.isBlank()) { + projectAccessHelper.assertCanReadProject(projectId); + } QueryWrapper qw = new QueryWrapper().eq("project_id", projectId).orderByDesc("created_at"); if (name != null && !name.isEmpty()) { qw.like("name", name); diff --git a/business-css/src/main/java/com/yfd/business/css/controller/ScenarioResultController.java b/business-css/src/main/java/com/yfd/business/css/controller/ScenarioResultController.java index d58f388..5a57e67 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/ScenarioResultController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/ScenarioResultController.java @@ -2,8 +2,11 @@ package com.yfd.business.css.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.business.css.domain.Scenario; import com.yfd.business.css.domain.ScenarioResult; +import com.yfd.business.css.security.ProjectAccessHelper; import com.yfd.business.css.service.ScenarioResultService; +import com.yfd.business.css.service.ScenarioService; import java.io.IOException; @@ -23,6 +26,10 @@ public class ScenarioResultController { @Resource private ScenarioResultService scenarioResultService; + @Resource + private ScenarioService scenarioService; + @Resource + private ProjectAccessHelper projectAccessHelper; /** * 根据场景ID分页查询模拟结果 @@ -43,6 +50,10 @@ public class ScenarioResultController { @RequestParam(defaultValue = "1") long pageNum, @RequestParam(defaultValue = "10") long pageSize ) { + Scenario sc = scenarioService.getById(scenarioId); + if (sc != null && sc.getProjectId() != null && !sc.getProjectId().isBlank()) { + projectAccessHelper.assertCanReadProject(sc.getProjectId()); + } QueryWrapper qw = new QueryWrapper() .eq("scenario_id", scenarioId); if (deviceId != null && !deviceId.isEmpty()) { @@ -79,6 +90,10 @@ public class ScenarioResultController { @RequestParam(required = false) String deviceId, @RequestParam(required = false) Integer stepFrom, @RequestParam(required = false) Integer stepTo) throws IOException { + Scenario sc = scenarioService.getById(scenarioId); + if (sc != null && sc.getProjectId() != null && !sc.getProjectId().isBlank()) { + projectAccessHelper.assertCanReadProject(sc.getProjectId()); + } // 设置响应头信息以便浏览器识别并触发下载行为 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); diff --git a/business-css/src/main/java/com/yfd/business/css/domain/ModelTrainTask.java b/business-css/src/main/java/com/yfd/business/css/domain/ModelTrainTask.java index 6bf72e1..234e56c 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/ModelTrainTask.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/ModelTrainTask.java @@ -5,6 +5,7 @@ import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Map; @Data @TableName(value = "model_train_task", autoResultMap = true) @@ -47,6 +48,9 @@ public class ModelTrainTask implements Serializable { @TableField("error_log") private String errorLog; + + @TableField(exist = false) + private Map featureMapConfig; @TableField(value = "created_at") private LocalDateTime createdAt; diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Project.java b/business-css/src/main/java/com/yfd/business/css/domain/Project.java index 82751ae..32fc72b 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Project.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Project.java @@ -30,6 +30,12 @@ public class Project implements Serializable { @TableField("topology") private String topology; + @TableField("visibility") + private String visibility; + + @TableField("creator") + private String creator; + @TableField("created_at") private LocalDateTime createdAt; diff --git a/business-css/src/main/java/com/yfd/business/css/security/ProjectAccessHelper.java b/business-css/src/main/java/com/yfd/business/css/security/ProjectAccessHelper.java new file mode 100644 index 0000000..db907c2 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/security/ProjectAccessHelper.java @@ -0,0 +1,94 @@ +package com.yfd.business.css.security; + +import com.yfd.business.css.common.exception.BizException; +import com.yfd.business.css.domain.Project; +import com.yfd.business.css.service.ProjectService; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.IUserService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +@Component +public class ProjectAccessHelper { + + @Resource + private ProjectService projectService; + + @Resource + private IUserService userService; + + public boolean isAdmin() { + SysUser u = userService.getUserInfo(); + return u != null && u.getUsertype() != null && u.getUsertype() == 0; + } + + public String currentUserId() { + SysUser u = userService.getUserInfo(); + return u == null ? null : u.getId(); + } + + public void assertCanReadProject(String projectId) { + Project p = projectService.getById(projectId); + if (p == null) { + throw new BizException("项目不存在"); + } + if (!canRead(p)) { + throw new BizException("无权限访问该项目"); + } + } + + public void assertCanWriteProject(String projectId) { + Project p = projectService.getById(projectId); + if (p == null) { + throw new BizException("项目不存在"); + } + if (!canWrite(p)) { + throw new BizException("无权限修改该项目"); + } + } + + public void assertCanManageVisibility(String projectId) { + Project p = projectService.getById(projectId); + if (p == null) { + throw new BizException("项目不存在"); + } + if (!canManageVisibility(p)) { + throw new BizException("无权限修改项目可见性"); + } + } + + public boolean canRead(Project p) { + if (p == null) return false; + if (isAdmin()) return true; + String visibility = normalizeVisibility(p.getVisibility()); + if ("PUBLIC".equals(visibility) || "READONLY".equals(visibility)) return true; + if ("PRIVATE".equals(visibility)) { + String uid = currentUserId(); + return uid != null && uid.equals(p.getCreator()); + } + return true; + } + + public boolean canWrite(Project p) { + if (p == null) return false; + if (isAdmin()) return true; + String uid = currentUserId(); + if (uid != null && uid.equals(p.getCreator())) return true; + String visibility = normalizeVisibility(p.getVisibility()); + return "PUBLIC".equals(visibility); + } + + public boolean canManageVisibility(Project p) { + if (p == null) return false; + if (isAdmin()) return true; + String uid = currentUserId(); + return uid != null && uid.equals(p.getCreator()); + } + + public String normalizeVisibility(String visibility) { + if (visibility == null || visibility.isBlank()) return "PUBLIC"; + String v = visibility.trim().toUpperCase(); + if ("PRIVATE".equals(v) || "READONLY".equals(v) || "PUBLIC".equals(v)) return v; + return "PUBLIC"; + } +} diff --git a/business-css/src/main/java/com/yfd/business/css/service/ModelTrainService.java b/business-css/src/main/java/com/yfd/business/css/service/ModelTrainService.java index 4f16b03..f742b5c 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/ModelTrainService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/ModelTrainService.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.business.css.domain.ModelTrainTask; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + public interface ModelTrainService extends IService { /** * 上传数据集 @@ -12,6 +14,8 @@ public interface ModelTrainService extends IService { */ String uploadDataset(MultipartFile file); + List parseDatasetColumns(String datasetPath); + /** * 提交训练任务 * @param task 任务信息 diff --git a/business-css/src/main/java/com/yfd/business/css/service/ProjectService.java b/business-css/src/main/java/com/yfd/business/css/service/ProjectService.java index ee090db..1294ed4 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/ProjectService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/ProjectService.java @@ -18,6 +18,8 @@ public interface ProjectService extends IService { */ byte[] exportProjectEngineeringExcel(String projectId); + java.util.Map importProjectEngineeringExcel(org.springframework.web.multipart.MultipartFile file, boolean overwrite); + /** * 解析指定项目的拓扑结构 * @param projectId 项目ID 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 33f2b56..3cda870 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 @@ -23,9 +23,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; import java.io.File; import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -33,6 +41,9 @@ import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.Map; import java.util.UUID; import java.util.regex.Pattern; @@ -108,6 +119,25 @@ public class ModelTrainServiceImpl extends ServiceImpl parseDatasetColumns(String datasetPath) { + if (datasetPath == null || datasetPath.isBlank()) { + throw new BizException("数据集路径不能为空"); + } + Path p = Paths.get(datasetPath); + if (!Files.exists(p)) { + throw new BizException("数据集文件不存在: " + datasetPath); + } + String lower = p.getFileName().toString().toLowerCase(); + if (lower.endsWith(".xlsx") || lower.endsWith(".xls")) { + return parseExcelHeader(p.toFile()); + } + if (lower.endsWith(".csv")) { + return parseCsvHeader(p); + } + throw new BizException("不支持的数据集格式: " + p.getFileName()); + } + @Override @Transactional public String submitTask(ModelTrainTask task) { @@ -116,6 +146,20 @@ public class ModelTrainServiceImpl extends ServiceImpl params = new HashMap<>(); + if (task.getTrainParams() != null && !task.getTrainParams().isBlank()) { + try { + params.putAll(objectMapper.readValue(task.getTrainParams(), new TypeReference>() {})); + } catch (Exception ignored) { + } + } + params.put("feature_map", task.getFeatureMapConfig()); + try { + task.setTrainParams(objectMapper.writeValueAsString(params)); + } catch (JsonProcessingException ignored) { + } + } this.save(task); // 2. 异步调用 Python 训练 @@ -145,6 +189,12 @@ public class ModelTrainServiceImpl extends ServiceImpl params = objectMapper.readValue(task.getTrainParams(), new TypeReference>() {}); request.put("hyperparameters", params); + if (task.getFeatureMapConfig() == null) { + Object fm = params.get("feature_map"); + if (fm instanceof Map) { + request.put("feature_map", fm); + } + } } catch (Exception e) { log.error("解析训练参数失败,将作为原始字符串发送: {}", e.getMessage()); request.put("hyperparameters", task.getTrainParams()); @@ -152,6 +202,9 @@ public class ModelTrainServiceImpl extends ServiceImpl()); } + if (task.getFeatureMapConfig() != null && !task.getFeatureMapConfig().isEmpty()) { + request.put("feature_map", task.getFeatureMapConfig()); + } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -200,6 +253,69 @@ public class ModelTrainServiceImpl extends ServiceImpl parseExcelHeader(File file) { + DataFormatter df = new DataFormatter(); + try (Workbook wb = WorkbookFactory.create(file)) { + Sheet sheet = wb.getNumberOfSheets() > 0 ? wb.getSheetAt(0) : null; + if (sheet == null) return List.of(); + Row row = sheet.getRow(sheet.getFirstRowNum()); + if (row == null) return List.of(); + short last = row.getLastCellNum(); + List cols = new ArrayList<>(); + for (int i = 0; i < last; i++) { + String v = df.formatCellValue(row.getCell(i)); + if (v != null) { + String t = v.trim(); + if (!t.isBlank()) cols.add(t); + } + } + return cols; + } catch (Exception e) { + throw new BizException("解析Excel表头失败: " + e.getMessage()); + } + } + + private List parseCsvHeader(Path p) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(p), StandardCharsets.UTF_8))) { + String line = br.readLine(); + if (line == null) return List.of(); + List cols = parseCsvLine(line).stream() + .map(s -> s == null ? "" : s.trim()) + .filter(s -> !s.isBlank()) + .toList(); + return cols; + } catch (Exception e) { + throw new BizException("解析CSV表头失败: " + e.getMessage()); + } + } + + private List parseCsvLine(String line) { + List out = new ArrayList<>(); + if (line == null) return out; + StringBuilder sb = new StringBuilder(); + boolean inQuotes = false; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '"') { + if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') { + sb.append('"'); + i++; + } else { + inQuotes = !inQuotes; + } + continue; + } + if (c == ',' && !inQuotes) { + out.add(sb.toString()); + sb.setLength(0); + continue; + } + sb.append(c); + } + out.add(sb.toString()); + return out; + } + @Override public ModelTrainTask syncTaskStatus(String taskId) { ModelTrainTask task = this.getById(taskId); diff --git a/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java b/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java index b618945..13635d8 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java +++ b/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java @@ -43,8 +43,13 @@ import java.util.Comparator; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.web.multipart.MultipartFile; import java.time.format.DateTimeFormatter; +import java.time.LocalDateTime; +import java.math.BigDecimal; @Slf4j @Service @@ -1308,6 +1313,283 @@ public class ProjectServiceImpl } } + @Override + public Map importProjectEngineeringExcel(MultipartFile file, boolean overwrite) { + if (file == null || file.isEmpty()) { + return Map.of("code", 1, "msg", "文件不能为空"); + } + + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + DataFormatter df = new DataFormatter(); + + try (Workbook wb = new XSSFWorkbook(file.getInputStream())) { + Sheet projectsSheet = wb.getSheet("projects"); + if (projectsSheet == null) { + return Map.of("code", 1, "msg", "缺少 projects Sheet"); + } + Row header = projectsSheet.getRow(0); + Row dataRow = projectsSheet.getRow(1); + if (header == null || dataRow == null) { + return Map.of("code", 1, "msg", "projects Sheet 内容为空"); + } + Map h = headerIndex(header, df); + String projectId = cell(df, dataRow, h.get("project_id")); + if (projectId == null || projectId.isBlank()) { + return Map.of("code", 1, "msg", "project_id 不能为空"); + } + + Project exists = this.getById(projectId); + if (exists != null && !overwrite) { + return Map.of("code", 2, "msg", "项目ID已存在", "data", Map.of("projectId", projectId)); + } + if (exists != null) { + deleteProjectCascade(projectId); + } + + Project p = new Project(); + p.setProjectId(projectId); + p.setCode(cell(df, dataRow, h.get("code"))); + p.setName(cell(df, dataRow, h.get("name"))); + p.setDescription(cell(df, dataRow, h.get("description"))); + p.setTopology(cell(df, dataRow, h.get("topology"))); + p.setModifier(cell(df, dataRow, h.get("modifier"))); + + String visibility = cell(df, dataRow, h.get("visibility")); + if (visibility != null && !visibility.isBlank()) { + p.setVisibility(visibility); + } + String creator = cell(df, dataRow, h.get("creator")); + if (creator != null && !creator.isBlank()) { + p.setCreator(creator); + } + + String createdAt = cell(df, dataRow, h.get("created_at")); + if (createdAt != null && !createdAt.isBlank()) { + try { p.setCreatedAt(LocalDateTime.parse(createdAt, fmt)); } catch (Exception ignored) { } + } + String updatedAt = cell(df, dataRow, h.get("updated_at")); + if (updatedAt != null && !updatedAt.isBlank()) { + try { p.setUpdatedAt(LocalDateTime.parse(updatedAt, fmt)); } catch (Exception ignored) { } + } + + this.save(p); + + Sheet devicesSheet = wb.getSheet("devices"); + if (devicesSheet != null) { + List devices = new ArrayList<>(); + Map dh = headerIndex(devicesSheet.getRow(0), df); + int last = devicesSheet.getLastRowNum(); + for (int i = 1; i <= last; i++) { + Row r = devicesSheet.getRow(i); + if (r == null) continue; + String deviceId = cell(df, r, dh.get("device_id")); + if (deviceId == null || deviceId.isBlank()) continue; + Device d = new Device(); + d.setDeviceId(deviceId); + d.setProjectId(projectId); + d.setCode(cell(df, r, dh.get("code"))); + d.setType(cell(df, r, dh.get("type"))); + d.setName(cell(df, r, dh.get("name"))); + d.setSize(cell(df, r, dh.get("size"))); + d.setVolume(parseDoubleObj(cell(df, r, dh.get("volume")))); + d.setFlowRate(parseDoubleObj(cell(df, r, dh.get("flow_rate")))); + d.setPulseVelocity(parseDoubleObj(cell(df, r, dh.get("pulse_velocity")))); + d.setModifier(cell(df, r, dh.get("modifier"))); + String dCreated = cell(df, r, dh.get("created_at")); + if (dCreated != null && !dCreated.isBlank()) { + try { d.setCreatedAt(LocalDateTime.parse(dCreated, fmt)); } catch (Exception ignored) { } + } + String dUpdated = cell(df, r, dh.get("updated_at")); + if (dUpdated != null && !dUpdated.isBlank()) { + try { d.setUpdatedAt(LocalDateTime.parse(dUpdated, fmt)); } catch (Exception ignored) { } + } + devices.add(d); + } + if (!devices.isEmpty()) { + deviceService.saveBatch(devices); + } + } + + Sheet materialsSheet = wb.getSheet("materials"); + if (materialsSheet != null) { + List materials = new ArrayList<>(); + Map mh = headerIndex(materialsSheet.getRow(0), df); + int last = materialsSheet.getLastRowNum(); + for (int i = 1; i <= last; i++) { + Row r = materialsSheet.getRow(i); + if (r == null) continue; + String materialId = cell(df, r, mh.get("material_id")); + if (materialId == null || materialId.isBlank()) continue; + Material m = new Material(); + m.setMaterialId(materialId); + m.setProjectId(projectId); + m.setName(cell(df, r, mh.get("name"))); + m.setUConcentration(parseBigDecimal(cell(df, r, mh.get("u_concentration")))); + m.setUo2Density(parseBigDecimal(cell(df, r, mh.get("uo2_density")))); + m.setUEnrichment(parseBigDecimal(cell(df, r, mh.get("u_enrichment")))); + m.setPuConcentration(parseBigDecimal(cell(df, r, mh.get("pu_concentration")))); + m.setPuo2Density(parseBigDecimal(cell(df, r, mh.get("puo2_density")))); + m.setPuIsotope(parseBigDecimal(cell(df, r, mh.get("pu_isotope")))); + m.setHno3Acidity(parseBigDecimal(cell(df, r, mh.get("hno3_acidity")))); + m.setH2c2o4Concentration(parseBigDecimal(cell(df, r, mh.get("h2c2o4_concentration")))); + m.setOrganicRatio(parseBigDecimal(cell(df, r, mh.get("organic_ratio")))); + m.setMoistureContent(parseBigDecimal(cell(df, r, mh.get("moisture_content")))); + m.setCustomAttrs(cell(df, r, mh.get("custom_attrs"))); + m.setModifier(cell(df, r, mh.get("modifier"))); + String mCreated = cell(df, r, mh.get("created_at")); + if (mCreated != null && !mCreated.isBlank()) { + try { m.setCreatedAt(LocalDateTime.parse(mCreated, fmt)); } catch (Exception ignored) { } + } + String mUpdated = cell(df, r, mh.get("updated_at")); + if (mUpdated != null && !mUpdated.isBlank()) { + try { m.setUpdatedAt(LocalDateTime.parse(mUpdated, fmt)); } catch (Exception ignored) { } + } + materials.add(m); + } + if (!materials.isEmpty()) { + materialService.saveBatch(materials); + } + } + + Sheet scenariosSheet = wb.getSheet("scenarios"); + List scenarioIds = new ArrayList<>(); + if (scenariosSheet != null) { + List scenarios = new ArrayList<>(); + Map sh = headerIndex(scenariosSheet.getRow(0), df); + int last = scenariosSheet.getLastRowNum(); + for (int i = 1; i <= last; i++) { + Row r = scenariosSheet.getRow(i); + if (r == null) continue; + String scenarioId = cell(df, r, sh.get("scenario_id")); + if (scenarioId == null || scenarioId.isBlank()) continue; + Scenario sc = new Scenario(); + sc.setScenarioId(scenarioId); + sc.setProjectId(projectId); + sc.setName(cell(df, r, sh.get("name"))); + sc.setDescription(cell(df, r, sh.get("description"))); + sc.setModifier(cell(df, r, sh.get("modifier"))); + String sCreated = cell(df, r, sh.get("created_at")); + if (sCreated != null && !sCreated.isBlank()) { + try { sc.setCreatedAt(LocalDateTime.parse(sCreated, fmt)); } catch (Exception ignored) { } + } + String sUpdated = cell(df, r, sh.get("updated_at")); + if (sUpdated != null && !sUpdated.isBlank()) { + try { sc.setUpdatedAt(LocalDateTime.parse(sUpdated, fmt)); } catch (Exception ignored) { } + } + scenarios.add(sc); + scenarioIds.add(scenarioId); + } + if (!scenarios.isEmpty()) { + scenarioService.saveBatch(scenarios); + } + } + + Sheet eventsSheet = wb.getSheet("events"); + if (eventsSheet != null) { + List events = new ArrayList<>(); + Map eh = headerIndex(eventsSheet.getRow(0), df); + int last = eventsSheet.getLastRowNum(); + for (int i = 1; i <= last; i++) { + Row r = eventsSheet.getRow(i); + if (r == null) continue; + String eventId = cell(df, r, eh.get("event_id")); + if (eventId == null || eventId.isBlank()) continue; + Event ev = new Event(); + ev.setEventId(eventId); + ev.setScenarioId(cell(df, r, eh.get("scenario_id"))); + ev.setDeviceId(cell(df, r, eh.get("device_id"))); + ev.setMaterialId(cell(df, r, eh.get("material_id"))); + ev.setTriggerTime(parseDoubleObj(cell(df, r, eh.get("trigger_time")))); + ev.setAttrChanges(cell(df, r, eh.get("attr_changes"))); + ev.setModifier(cell(df, r, eh.get("modifier"))); + String eCreated = cell(df, r, eh.get("created_at")); + if (eCreated != null && !eCreated.isBlank()) { + try { ev.setCreatedAt(LocalDateTime.parse(eCreated, fmt)); } catch (Exception ignored) { } + } + events.add(ev); + } + if (!events.isEmpty()) { + eventService.saveBatch(events); + } + } + + Sheet resultsSheet = wb.getSheet("scenario_results"); + if (resultsSheet != null) { + List results = new ArrayList<>(); + Map rh = headerIndex(resultsSheet.getRow(0), df); + int last = resultsSheet.getLastRowNum(); + for (int i = 1; i <= last; i++) { + Row r = resultsSheet.getRow(i); + if (r == null) continue; + String sid = cell(df, r, rh.get("scenario_id")); + if (sid == null || sid.isBlank()) continue; + ScenarioResult sr = new ScenarioResult(); + sr.setScenarioId(sid); + sr.setDeviceId(cell(df, r, rh.get("device_id"))); + String step = cell(df, r, rh.get("step")); + if (step != null && !step.isBlank()) { + try { sr.setStep((int) Double.parseDouble(step)); } catch (Exception ignored) { } + } + sr.setKeffValue(parseBigDecimal(cell(df, r, rh.get("keff_value")))); + sr.setAttrState(cell(df, r, rh.get("attr_state"))); + results.add(sr); + } + if (!results.isEmpty()) { + scenarioResultService.saveBatch(results); + } + } + + return Map.of("code", 0, "msg", "导入成功", "data", Map.of("projectId", projectId)); + } catch (Exception e) { + return Map.of("code", 1, "msg", "导入失败: " + e.getMessage()); + } + } + + private void deleteProjectCascade(String projectId) { + List scenarios = scenarioService.list(new QueryWrapper().select("scenario_id").eq("project_id", projectId)); + List scenarioIds = scenarios.stream().map(Scenario::getScenarioId).filter(Objects::nonNull).toList(); + if (!scenarioIds.isEmpty()) { + scenarioResultService.remove(new QueryWrapper().in("scenario_id", scenarioIds)); + eventService.remove(new QueryWrapper().in("scenario_id", scenarioIds)); + } + scenarioService.remove(new QueryWrapper().eq("project_id", projectId)); + materialService.remove(new QueryWrapper().eq("project_id", projectId)); + deviceService.remove(new QueryWrapper().eq("project_id", projectId)); + this.removeById(projectId); + } + + private Map headerIndex(Row header, DataFormatter df) { + Map map = new HashMap<>(); + if (header == null) return map; + short last = header.getLastCellNum(); + for (int i = 0; i < last; i++) { + Cell c = header.getCell(i); + String v = c == null ? "" : df.formatCellValue(c); + if (v != null && !v.isBlank()) { + map.put(v.trim(), i); + } + } + return map; + } + + private String cell(DataFormatter df, Row row, Integer idx) { + if (row == null || idx == null) return null; + Cell c = row.getCell(idx); + if (c == null) return null; + String v = df.formatCellValue(c); + return v == null ? null : v.trim(); + } + + private Double parseDoubleObj(String s) { + if (s == null || s.isBlank()) return null; + try { return Double.parseDouble(s); } catch (Exception e) { return null; } + } + + private BigDecimal parseBigDecimal(String s) { + if (s == null || s.isBlank()) return null; + try { return new BigDecimal(s.trim()); } catch (Exception e) { return null; } + } + //运行项目模拟 @Override public java.util.Map runSimulation(String projectId, String scenarioId, java.util.Map params) {