diff --git a/business-css/pom.xml b/business-css/pom.xml index 8763eea..925ee02 100644 --- a/business-css/pom.xml +++ b/business-css/pom.xml @@ -23,7 +23,7 @@ UTF-8 - + org.springframework.boot @@ -66,18 +66,6 @@ plain - - - org.apache.poi - poi - 4.1.2 - - - org.apache.poi - poi-ooxml - 4.1.2 - - @@ -101,6 +89,29 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-java + + enforce + + + + + [17,) + + + [3.6.3,) + + + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/business-css/src/main/java/com/yfd/business/css/config/BusinessCssAutoConfiguration.java b/business-css/src/main/java/com/yfd/business/css/config/BusinessCssAutoConfiguration.java new file mode 100644 index 0000000..2f66575 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/config/BusinessCssAutoConfiguration.java @@ -0,0 +1,13 @@ +package com.yfd.business.css.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration(proxyBeanMethods = false) +@ComponentScan(basePackages = { + "com.yfd.business.css.controller" +}) +@Import(OpenApiConfig.class) +public class BusinessCssAutoConfiguration { +} diff --git a/business-css/src/main/java/com/yfd/business/css/config/OpenApiConfig.java b/business-css/src/main/java/com/yfd/business/css/config/OpenApiConfig.java new file mode 100644 index 0000000..d49934f --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/config/OpenApiConfig.java @@ -0,0 +1,64 @@ +package com.yfd.business.css.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import java.time.format.DateTimeFormatter; +import java.io.IOException; + +@Configuration +public class OpenApiConfig { + + @Bean + public GroupedOpenApi businessCssGroup() { + return GroupedOpenApi.builder() + .group("css-business") + .packagesToScan("com.yfd.business.css.controller") + .pathsToMatch("/events/**", "/scenario-results/**", "/projects/**") + .build(); + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { + return builder -> { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + builder.serializers(new LocalDateTimeSerializer(fmt)); + builder.deserializers(new LocalDateTimeDeserializer(fmt)); + builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); + builder.serializationInclusion(JsonInclude.Include.ALWAYS); + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new BeanSerializerModifier() { + @Override + public java.util.List changeProperties( + com.fasterxml.jackson.databind.SerializationConfig config, + com.fasterxml.jackson.databind.BeanDescription beanDesc, + java.util.List beanProperties) { + for (BeanPropertyWriter writer : beanProperties) { + if (writer.getType() != null && writer.getType().getRawClass() == String.class) { + writer.assignNullSerializer(new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(""); + } + }); + } + } + return beanProperties; + } + }); + builder.modules(module); + }; + } +} 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 83f48c1..0c05ddc 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 @@ -16,17 +16,7 @@ public class AlgorithmController { @Autowired private AlgorithmService algorithmService; - @GetMapping - public List getAllAlgorithms() { - return algorithmService.list(); - } - - @GetMapping("/page") - public Page getAlgorithmsPage(@RequestParam(defaultValue = "1") int current, - @RequestParam(defaultValue = "10") int size) { - return algorithmService.page(new Page<>(current, size)); - } - + @GetMapping("/{id}") public Algorithm getAlgorithmById(@PathVariable String id) { return algorithmService.getById(id); @@ -48,12 +38,29 @@ public class AlgorithmController { return algorithmService.removeById(id); } - @GetMapping("/search") - public List searchAlgorithms(@RequestParam String keyword) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.like("name", keyword) - .or() - .like("description", keyword); - return algorithmService.list(queryWrapper); + @DeleteMapping + public boolean deleteAlgorithms(@RequestBody List ids) { + return algorithmService.removeByIds(ids); } -} \ No newline at end of file + + /** + * 根据算法名称搜索并分页返回 + * 输入参数:查询参数 name(算法名称关键词,可为空),pageNum(页码,默认1),pageSize(每页条数,默认10) + * 输出参数:算法分页列表 + * @param name 算法名称关键词(可为空) + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 算法分页列表 + */ + @GetMapping("/search") + public Page searchAlgorithms(@RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "10") long pageSize) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (name != null && !name.isEmpty()) { + queryWrapper.like("name", name); + } + Page page = new Page<>(pageNum, pageSize); + return algorithmService.page(page, queryWrapper); + } +} diff --git a/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java b/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java index d3c39c4..14b3695 100644 --- a/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java +++ b/business-css/src/main/java/com/yfd/business/css/controller/CriticalDataController.java @@ -1,6 +1,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.CriticalData; import com.yfd.business.css.service.CriticalDataService; import org.springframework.web.bind.annotation.*; @@ -81,15 +82,21 @@ public class CriticalDataController { /** - * 6. 根据设备类型获取临界数据列表 - * 输入参数:查询参数 deviceType(设备类型) - * 输出参数:临界数据列表(按创建时间倒序) + * 6. 根据设备类型获取临界数据分页列表 + * 输入参数:查询参数 deviceType(设备类型),pageNum(页码,默认1),pageSize(每页条数,默认10) + * 输出参数:临界数据分页列表(按创建时间倒序) * @param deviceType 设备类型 - * @return 临界数据列表 + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 临界数据分页列表 */ @GetMapping("/by-device-type") - public List listByDeviceType(@RequestParam String deviceType) { - return criticalDataService.list( + public Page listByDeviceType(@RequestParam String deviceType, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "10") long pageSize) { + Page page = new Page<>(pageNum, pageSize); + return criticalDataService.page( + page, new QueryWrapper() .eq("device_type", deviceType) .orderByDesc("created_at") 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 5c28783..0381e34 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 @@ -1,6 +1,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.service.DeviceService; import org.springframework.web.bind.annotation.*; @@ -96,34 +97,24 @@ public class DeviceController { } - /** - * 6. 根据设备类型获取设备列表 - * 输入参数:查询参数 type(设备类型) - * 输出参数:设备列表(按创建时间倒序) - * @param type 设备类型 - * @return 设备列表 - */ - @GetMapping("/by-type") - public List listByType(@RequestParam String type) { - return deviceService.list( - new QueryWrapper() - .eq("type", type) - .orderByDesc("created_at") - ); - } + /** - * 7. 设备查询(类型 + 名称) - * 输入参数:查询参数 type(可选),name(可选) - * 输出参数:设备列表(按创建时间倒序) + * 6. 设备分页查询(类型 可选 + 名称 可选) + * 输入参数:查询参数 type(可选),name(可选),pageNum(页码,默认1),pageSize(每页条数,默认10) + * 输出参数:设备分页列表(按创建时间倒序) * @param type 设备类型(可选) * @param name 设备名称关键词(可选) - * @return 设备列表 + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 设备分页列表 */ @GetMapping("/search") - public List search(@RequestParam(required = false) String type, - @RequestParam(required = false) String name) { - + public Page search(@RequestParam(required = false) String type, + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "20") long pageSize) { + Page page = new Page<>(pageNum, pageSize); QueryWrapper wrapper = new QueryWrapper<>(); if (type != null && !type.isEmpty()) { @@ -133,6 +124,6 @@ public class DeviceController { wrapper.like("name", name); } - return deviceService.list(wrapper.orderByDesc("created_at")); + return deviceService.page(page, wrapper.orderByDesc("created_at")); } } 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 8b6e990..7aaa32f 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 @@ -8,15 +8,25 @@ import org.springframework.web.bind.annotation.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import java.util.Map; import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.yfd.business.css.dto.EventAttrParseResult; +import com.yfd.business.css.dto.EventAttrSegment; +import com.yfd.business.css.dto.EventAttrPoint; @RestController @RequestMapping("/events") public class EventController { private final EventService eventService; + private final ObjectMapper objectMapper; - public EventController(EventService eventService) { + public EventController(EventService eventService, ObjectMapper objectMapper) { this.eventService = eventService; + this.objectMapper = objectMapper; } /** @@ -33,6 +43,32 @@ public class EventController { )); } + /** + * 修改始发事件 + * 输入参数:路径参数 eventId(事件ID),请求体中的事件对象 + * 输出参数:标准响应结构,包含修改后的事件对象 + * @param eventId 事件ID + * @param event 事件对象 + * @return 修改结果 + */ + @PutMapping("/{eventId}") + public ResponseEntity> updateEvent(@PathVariable String eventId, + @RequestBody Event event) { + event.setEventId(eventId); + boolean ok = eventService.updateById(event); + if (!ok) { + return ResponseEntity.badRequest().body(Map.of( + "code", 1, + "msg", "修改失败" + )); + } + return ResponseEntity.ok(Map.of( + "code", 0, + "msg", "修改成功", + "data", event + )); + } + /** * 修改 Event 的 attr_changes */ @@ -48,6 +84,14 @@ public class EventController { "msg", "attr_changes不能为空" )); } + try { + objectMapper.readTree(String.valueOf(attrChanges)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of( + "code", 1, + "msg", "attr_changes JSON解析失败: " + e.getMessage() + )); + } Event updatedEvent = new Event(); updatedEvent.setEventId(eventId); @@ -98,4 +142,129 @@ public class EventController { )); } } + + /** + * 解析事件 attr_changes(按事件ID) + * 输入参数:路径参数 eventId(事件ID) + * 输出参数:标准响应结构,data 为 EventAttrParseResult(目标、单位、分段时间序列、派生 schedule 与问题列表) + * @param eventId 事件ID + * @return 解析结果 + */ + @GetMapping("/{eventId}/attr-parse") + public ResponseEntity> parseAttrChanges(@PathVariable String eventId) { + Event ev = eventService.getById(eventId); + List issues = new ArrayList<>(); + EventAttrParseResult result = new EventAttrParseResult(); + result.setEventId(eventId); + result.setIssues(issues); + result.setSegments(new ArrayList<>()); + result.setSchedule(new ArrayList<>()); + if (ev == null) { + issues.add("事件不存在: " + eventId); + return ResponseEntity.ok(Map.of("code", 0, "msg", "解析完成", "data", result)); + } + String json = ev.getAttrChanges(); + if (json == null || json.isBlank()) { + issues.add("attr_changes为空"); + return ResponseEntity.ok(Map.of("code", 0, "msg", "解析完成", "data", result)); + } + try { + JsonNode root = objectMapper.readTree(json); + JsonNode target = root.path("target"); + String entityType = optText(target, "entityType"); + String entityId = optText(target, "entityId"); + String property = optText(target, "property"); + if (entityType == null || entityId == null || property == null) { + issues.add("target字段缺失"); + } else { + Map tgt = new HashMap<>(); + tgt.put("entityType", entityType); + tgt.put("entityId", entityId); + tgt.put("property", property); + result.setTarget(tgt); + } + String unit = optText(root, "unit"); + result.setUnit(unit); + JsonNode segments = root.path("segments"); + if (segments.isArray()) { + for (JsonNode seg : segments) { + EventAttrSegment s = new EventAttrSegment(); + s.setSegmentId(optText(seg, "segmentId")); + s.setStart(seg.path("start").isNumber() ? seg.path("start").asDouble() : 0.0); + s.setEnd(seg.path("end").isNumber() ? seg.path("end").asDouble() : s.getStart()); + s.setInterp(optText(seg, "interp")); + List pts = new ArrayList<>(); + JsonNode timeline = seg.path("timeline"); + if (timeline.isArray()) { + for (JsonNode p : timeline) { + EventAttrPoint ep = new EventAttrPoint(); + ep.setT(p.path("t").isNumber() ? p.path("t").asDouble() : 0.0); + ep.setValue(p.path("value").isNumber() ? p.path("value").asDouble() + : parseDouble(optText(p, "value"), issues, "value不是数值")); + pts.add(ep); + } + } else { + issues.add("segments.timeline不是数组"); + } + s.setTimeline(pts); + result.getSegments().add(s); + // derive schedule (linear ramp between consecutive points) + if ("linear".equalsIgnoreCase(s.getInterp()) && pts.size() >= 2) { + for (int i = 0; i < pts.size() - 1; i++) { + EventAttrPoint a = pts.get(i); + EventAttrPoint b = pts.get(i + 1); + double dt = b.getT() - a.getT(); + if (dt <= 0) { + issues.add("timeline时间非升序: " + a.getT() + " -> " + b.getT()); + continue; + } + double rate = (b.getValue() - a.getValue()) / dt; + Map ramp = new HashMap<>(); + ramp.put("type", "ramp"); + ramp.put("startTime", a.getT()); + ramp.put("endTime", b.getT()); + ramp.put("rate", rate); + ramp.put("unit", unit); + result.getSchedule().add(ramp); + } + } else { + // default step-set at each point + for (EventAttrPoint ep : pts) { + Map step = new HashMap<>(); + step.put("type", "step-set"); + step.put("time", ep.getT()); + step.put("value", ep.getValue()); + step.put("unit", unit); + result.getSchedule().add(step); + } + } + } + } else { + issues.add("segments不是数组"); + } + return ResponseEntity.ok(Map.of("code", 0, "msg", "解析成功", "data", result)); + } catch (Exception e) { + issues.add("解析失败: " + e.getMessage()); + return ResponseEntity.ok(Map.of("code", 0, "msg", "解析完成", "data", result)); + } + } + + private static String optText(JsonNode n, String field) { + JsonNode v = n.path(field); + if (v.isMissingNode() || v.isNull()) return null; + return v.asText(null); + } + + private static double parseDouble(String s, List issues, String warn) { + if (s == null) { + issues.add(warn); + return 0.0; + } + try { + return Double.parseDouble(s); + } catch (Exception e) { + issues.add(warn + ": " + s); + return 0.0; + } + } } 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 f8fc9ff..f3a0c54 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 @@ -1,6 +1,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.Material; import com.yfd.business.css.service.MaterialService; import org.springframework.web.bind.annotation.*; @@ -79,18 +80,23 @@ public class MaterialController { } /** - * 5. 根据物料名称搜索 - * 输入参数:查询参数 name(物料名称关键词) - * 输出参数:物料列表(按创建时间倒序) - * @param name 物料名称关键词 - * @return 物料列表 + * 5. 根据物料名称搜索(可为空)并分页返回 + * 输入参数:查询参数 name(物料名称关键词,可为空),pageNum(页码,默认1),pageSize(每页条数,默认10) + * 输出参数:物料分页列表(按创建时间倒序) + * @param name 物料名称关键词(可为空) + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 物料分页列表 */ @GetMapping("/search") - public List search(@RequestParam String name) { - return materialService.list( - new QueryWrapper() - .like("name", name) - .orderByDesc("created_at") - ); + public Page search(@RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "10") long pageSize) { + QueryWrapper wrapper = new QueryWrapper<>(); + if (name != null && !name.isEmpty()) { + wrapper.like("name", name); + } + Page page = new Page<>(pageNum, pageSize); + return materialService.page(page, wrapper.orderByDesc("created_at")); } } 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 324b5d1..8dff9b0 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 @@ -1,6 +1,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.service.ProjectService; import org.springframework.http.HttpHeaders; @@ -11,6 +12,12 @@ import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; +import java.io.ByteArrayOutputStream; +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.xssf.usermodel.XSSFWorkbook; +import java.time.format.DateTimeFormatter; @RestController @RequestMapping("/projects") @@ -18,6 +25,8 @@ public class ProjectController { @Resource private ProjectService projectService; + @Resource + private com.fasterxml.jackson.databind.ObjectMapper objectMapper; /** * 1. 新增项目 @@ -67,63 +76,65 @@ public class ProjectController { return projectService.removeByIds(ids); } + /** - * 4. 导出所有项目 + * 4.1 导出所有项目(Excel) * 输入参数:无 - * 导出描述:返回所有项目的 JSON 数组,作为附件 `projects.json` - * 输出参数:附件字节流(application/octet-stream) - * @return 附件响应,文件名为 projects.json + * 导出描述:返回所有项目的 Excel 附件 `projects.xlsx` + * 导出列:project_id, code, name, description, created_at, updated_at,modifier + * @return 附件响应,文件名为 projects.xlsx */ - @GetMapping("/export/all") - public ResponseEntity exportAll() { - byte[] data = projectService.exportAllProjects(); - + @GetMapping("/exportAllExports") + public ResponseEntity exportAllExports() { + byte[] bytes = projectService.exportAllProjectsExcel(); return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=projects.json") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=projects.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(data); + .body(bytes); } /** - * 5. 导出项目工程 + * 5.1 导出项目工程(Excel 多 Sheet) * 输入参数:路径参数项目ID - * 导出描述:返回 ZIP 附件 `project_{id}.zip`,包含以下 JSON 文件: - * - project.json:项目对象 - * - devices.json:设备列表 - * - materials.json:物料列表 - * - scenarios.json:情景列表 - * - events.json:事件列表 - * - scenario-results.json:情景结果列表 + * 导出描述:返回 Excel 附件 `project_{id}.xlsx`,包含以下 Sheet: + * - projects(项目)- project_id, code, name, description, created_at, updated_at + * - devices(设备)- device_id, project_id, code, type, name, size, volume, flow_rate, pulse_velocity, created_at, updated_at + * - materials(物料)- material_id, project_id, name, u_concentration, uo2_density, u_enrichment, pu_concentration, puo2_density, pu_isotope, hno3_acidity, h2c2o4_concentration, organic_ratio, moisture_content, custom_attrs, created_at, updated_at + * - scenarios(情景)- scenario_id, project_id, name, description, created_at, updated_at + * - events(事件)- event_id, scenario_id, device_id, material_id, trigger_time, attr_changes, created_at + * - scenario_results(情景结果)- scenario_id, device_id, step, keff_value, attr_state * 输出参数:附件字节流(application/octet-stream) * @param id 项目ID - * @return 附件响应,文件名为 project_{id}.zip + * @return 附件响应,文件名为 project_{id}.xlsx */ - @GetMapping("/{id}/export") - public ResponseEntity exportProject(@PathVariable String id) { - byte[] data = projectService.exportProjectEngineering(id); - + @GetMapping("/{id}/exportProject") + public ResponseEntity exportProjectExcel(@PathVariable String id) { + byte[] bytes = projectService.exportProjectEngineeringExcel(id); return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=project_" + id + ".zip") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=project_" + id + ".xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(data); + .body(bytes); } /** - * 6. 根据项目名称搜索 - * 输入参数:查询参数 name(项目名称关键词) - * 输出参数:项目列表 - * @param name 项目名称关键词 - * @return 匹配的项目列表 + * 6. 根据项目名称搜索(可为空)并分页返回 + * 输入参数:查询参数 name(项目名称关键词,可为空),pageNum(页码,默认1),pageSize(每页条数,默认10) + * 输出参数:项目分页列表(按创建时间倒序) + * @param name 项目名称关键词(可为空) + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 项目分页列表 */ @GetMapping("/search") - public List searchByName(@RequestParam String name) { - return projectService.list( - new QueryWrapper() - .like("name", name) - .orderByDesc("created_at") - ); + public Page search(@RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "10") long pageSize) { + QueryWrapper wrapper = new QueryWrapper<>(); + if (name != null && !name.isEmpty()) { + wrapper.like("name", name); + } + Page page = new Page<>(pageNum, pageSize); + return projectService.page(page, wrapper.orderByDesc("created_at")); } /** @@ -156,9 +167,24 @@ public class ProjectController { "msg", "topology不能为空" )); } + String json; + try { + if (topology instanceof String) { + var node = objectMapper.readTree((String) topology); + json = objectMapper.writeValueAsString(node); + } else { + json = objectMapper.writeValueAsString(topology); + objectMapper.readTree(json); + } + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of( + "code", 1, + "msg", "topology JSON解析失败: " + e.getMessage() + )); + } Project updated = new Project(); updated.setProjectId(id); - updated.setTopology(String.valueOf(topology)); + updated.setTopology(json); projectService.updateById(updated); return ResponseEntity.ok(Map.of( "code", 0, @@ -167,5 +193,63 @@ public class ProjectController { )); } + @GetMapping("/{id}/topology/parse") + /** + * 解析指定项目的拓扑结构,返回属性节点、影响关系边及计算计划 + * @param id 项目ID + * @return 标准响应结构,data 为 TopologyParseResult + */ + public ResponseEntity> parseTopology(@PathVariable String id) { + var result = projectService.parseTopology(id); + return ResponseEntity.ok(Map.of( + "code", 0, + "msg", "解析成功", + "data", result + )); + } + + @GetMapping("/{id}/topology/devices") + /** + * 根据项目拓扑中的 devices 节点提取 deviceId 并按出现顺序返回设备列表 + * @param id 项目ID + * @return 标准响应结构,data 为 List + */ + public ResponseEntity> parseDeviceOrder(@PathVariable String id) { + var list = projectService.parseDeviceOrder(id); + return ResponseEntity.ok(Map.of( + "code", 0, + "msg", "解析成功", + "data", list + )); + } + + @GetMapping("/{id}/topology/canvas") + /** + * 解析画布视图数据(设备、管线、边界、显示配置),供前端 UI 渲染 + * @param id 项目ID + * @return 标准响应结构,data 为视图对象(devices/pipelines/boundaries/display) + */ + public ResponseEntity> parseCanvas(@PathVariable String id) { + var view = projectService.parseCanvasView(id); + return ResponseEntity.ok(Map.of( + "code", 0, + "msg", "解析成功", + "data", view + )); + } + + @PostMapping("/{id}/scenarios/{scenarioId}/simulation/init") + public ResponseEntity> initSimulation(@PathVariable String id, + @PathVariable String scenarioId, + @RequestBody(required = false) Map params) { + var res = projectService.initSimulation(id, scenarioId, params == null ? Map.of() : params); + return ResponseEntity.ok(Map.of( + "code", 0, + "msg", "初始化完成", + "data", res + )); + } + + } 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 a1b08e1..b3d7589 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 @@ -1,6 +1,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.service.ScenarioService; import org.springframework.web.bind.annotation.*; @@ -63,21 +64,7 @@ public class ScenarioController { return scenarioService.removeByIds(ids); } - /** - * 4. 根据情景名称搜索 - * 输入参数:查询参数 name(情景名称关键词) - * 输出参数:情景列表(按创建时间倒序) - * @param name 情景名称关键词 - * @return 情景列表 - */ - @GetMapping("/search") - public List searchByName(@RequestParam String name) { - return scenarioService.list( - new QueryWrapper() - .like("name", name) - .orderByDesc("created_at") - ); - } + /** * 5. 根据情景ID获取情景记录 @@ -92,18 +79,25 @@ public class ScenarioController { } /** - * 6. 根据项目ID查询情景列表 - * 输入参数:查询参数 projectId(项目ID) - * 输出参数:情景列表(按创建时间倒序) + * 6. 根据项目ID与情景名称查询情景列表(分页) + * 输入参数:projectId(项目ID),name(情景名称关键词,可为空),pageNum(默认1),pageSize(默认20) + * 输出参数:情景分页列表(按创建时间倒序) * @param projectId 项目ID - * @return 情景列表 + * @param name 情景名称关键词(可为空) + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 情景分页列表 */ @GetMapping("/by-project") - public List listByProject(@RequestParam String projectId) { - return scenarioService.list( - new QueryWrapper() - .eq("project_id", projectId) - .orderByDesc("created_at") - ); + public Page listByProject(@RequestParam String projectId, + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "20") long pageSize) { + Page page = new Page<>(pageNum, pageSize); + QueryWrapper wrapper = new QueryWrapper().eq("project_id", projectId); + if (name != null && !name.isEmpty()) { + wrapper.like("name", name); + } + return scenarioService.page(page, wrapper.orderByDesc("created_at")); } } 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 0548466..4dc5466 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 @@ -1,54 +1,57 @@ 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.ScenarioResult; import com.yfd.business.css.service.ScenarioResultService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import java.util.List; -import java.util.Map; +import jakarta.annotation.Resource; @RestController @RequestMapping("/scenario-results") public class ScenarioResultController { - private final ScenarioResultService scenarioResultService; - - public ScenarioResultController(ScenarioResultService scenarioResultService) { - this.scenarioResultService = scenarioResultService; - } + @Resource + private ScenarioResultService scenarioResultService; /** - * 新增情景结果 - */ - @PostMapping - public ResponseEntity> addScenarioResult(@RequestBody ScenarioResult result) { - scenarioResultService.save(result); - ScenarioResult savedResult = result; - return ResponseEntity.ok(Map.of( - "code", 0, - "msg", "新增成功", - "data", savedResult - )); - } - - /** - * 根据情景ID与设备ID获取情景结果列表 - * 输入参数:查询参数 scenarioId(情景ID)、deviceId(设备ID) - * 输出参数:情景结果列表(按时间点 step 排序) - * @param scenarioId 情景ID - * @param deviceId 设备ID - * @return 情景结果列表 + * 根据场景ID分页查询模拟结果 + * @param scenarioId 场景ID + * @param deviceId 设备ID(可选) + * @param stepFrom 起始步(可选) + * @param stepTo 结束步(可选) + * @param pageNum 页码(默认1) + * @param pageSize 每页条数(默认10) + * @return 标准响应结构,data 为 Page */ @GetMapping("/by-scenario") - public List listByScenario(@RequestParam String scenarioId, - @RequestParam String deviceId) { - return scenarioResultService.list( - new QueryWrapper() - .eq("scenario_id", scenarioId) - .eq("device_id", deviceId) - .orderByAsc("step") - ); + public ResponseEntity listByScenario( + @RequestParam String scenarioId, + @RequestParam(required = false) String deviceId, + @RequestParam(required = false) Integer stepFrom, + @RequestParam(required = false) Integer stepTo, + @RequestParam(defaultValue = "1") long pageNum, + @RequestParam(defaultValue = "10") long pageSize + ) { + QueryWrapper qw = new QueryWrapper() + .eq("scenario_id", scenarioId); + if (deviceId != null && !deviceId.isEmpty()) { + qw.eq("device_id", deviceId); + } + if (stepFrom != null) { + qw.ge("step", stepFrom); + } + if (stepTo != null) { + qw.le("step", stepTo); + } + Page page = new Page<>(pageNum, pageSize); + Page data = scenarioResultService.page(page, qw.orderByAsc("step").orderByAsc("device_id")); + return ResponseEntity.ok(java.util.Map.of( + "code", 0, + "msg", "查询成功", + "data", data + )); } } diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Algorithm.java b/business-css/src/main/java/com/yfd/business/css/domain/Algorithm.java index 7cb178a..488e2be 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Algorithm.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Algorithm.java @@ -38,4 +38,7 @@ public class Algorithm implements Serializable { @TableField("output_params") private String outputParams; -} \ No newline at end of file + + @TableField("modifier") + private String modifier; +} diff --git a/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java b/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java index f5bf6cc..c01d596 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/CriticalData.java @@ -45,4 +45,7 @@ public class CriticalData implements Serializable { @TableField("updated_at") private LocalDateTime updatedAt; + + @TableField("modifier") + private String modifier; } diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Device.java b/business-css/src/main/java/com/yfd/business/css/domain/Device.java index ca7d0d8..cd3ed21 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Device.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Device.java @@ -47,4 +47,7 @@ public class Device implements Serializable { @TableField("updated_at") private LocalDateTime updatedAt; + + @TableField("modifier") + private String modifier; } diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Event.java b/business-css/src/main/java/com/yfd/business/css/domain/Event.java index 6ceff81..4e794f5 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Event.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Event.java @@ -35,4 +35,7 @@ public class Event implements Serializable { @TableField("created_at") private LocalDateTime createdAt; + + @TableField("modifer") + private String modifier; } diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Material.java b/business-css/src/main/java/com/yfd/business/css/domain/Material.java index e1f8169..47f7a20 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Material.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Material.java @@ -63,4 +63,7 @@ public class Material implements Serializable { @TableField("updated_at") private LocalDateTime updatedAt; + + @TableField("modifier") + private String modifier; } 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 e5ac6d0..9a1c8ba 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 @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.io.Serializable; @@ -31,8 +32,13 @@ public class Project implements Serializable { private String topology; @TableField("created_at") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt; @TableField("updated_at") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updatedAt; + + @TableField("modifier") + private String modifier; } diff --git a/business-css/src/main/java/com/yfd/business/css/domain/Scenario.java b/business-css/src/main/java/com/yfd/business/css/domain/Scenario.java index 3ab42a2..f692bf3 100644 --- a/business-css/src/main/java/com/yfd/business/css/domain/Scenario.java +++ b/business-css/src/main/java/com/yfd/business/css/domain/Scenario.java @@ -32,4 +32,7 @@ public class Scenario implements Serializable { @TableField("updated_at") private LocalDateTime updatedAt; + + @TableField("modifier") + private String modifier; } diff --git a/business-css/src/main/java/com/yfd/business/css/dto/EventAttrParseResult.java b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrParseResult.java new file mode 100644 index 0000000..cac2cd4 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrParseResult.java @@ -0,0 +1,16 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class EventAttrParseResult { + private String eventId; + private Map target; + private String unit; + private List segments; + private List> schedule; // optional derived ramp/step + private List issues; +} diff --git a/business-css/src/main/java/com/yfd/business/css/dto/EventAttrPoint.java b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrPoint.java new file mode 100644 index 0000000..bcfc920 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrPoint.java @@ -0,0 +1,9 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +@Data +public class EventAttrPoint { + private double t; + private double value; +} diff --git a/business-css/src/main/java/com/yfd/business/css/dto/EventAttrSegment.java b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrSegment.java new file mode 100644 index 0000000..ffbb9da --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/EventAttrSegment.java @@ -0,0 +1,14 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class EventAttrSegment { + private String segmentId; + private double start; + private double end; + private String interp; + private List timeline; +} diff --git a/business-css/src/main/java/com/yfd/business/css/dto/TopoEdge.java b/business-css/src/main/java/com/yfd/business/css/dto/TopoEdge.java new file mode 100644 index 0000000..0758780 --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/TopoEdge.java @@ -0,0 +1,15 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +@Data +public class TopoEdge { + private String fromEntityType; + private String fromEntityId; + private String fromProperty; + private String toEntityType; + private String toEntityId; + private String toProperty; + private Double coefficient; + private Long delayMs; +} diff --git a/business-css/src/main/java/com/yfd/business/css/dto/TopoNode.java b/business-css/src/main/java/com/yfd/business/css/dto/TopoNode.java new file mode 100644 index 0000000..f6ba98c --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/TopoNode.java @@ -0,0 +1,11 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +@Data +public class TopoNode { + private String entityType; + private String entityId; + private String property; + private String unit; +} diff --git a/business-css/src/main/java/com/yfd/business/css/dto/TopologyParseResult.java b/business-css/src/main/java/com/yfd/business/css/dto/TopologyParseResult.java new file mode 100644 index 0000000..69d882c --- /dev/null +++ b/business-css/src/main/java/com/yfd/business/css/dto/TopologyParseResult.java @@ -0,0 +1,18 @@ +package com.yfd.business.css.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class TopologyParseResult { + private String projectId; + private Integer deviceCount; + private Integer nodeCount; + private Integer edgeCount; + private List nodes; + private List edges; + private List> plans; + private List issues; +} 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 1d39a51..d2189dd 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 @@ -4,14 +4,38 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.business.css.domain.Project; public interface ProjectService extends IService { + /** - * 导出所有项目 + * 导出所有项目为 Excel */ - byte[] exportAllProjects(); + byte[] exportAllProjectsExcel(); /** - * 导出项目工程(设备 + 物料 + 场景 + 事件 + 结果) + * 导出所有项目为 Excel(多 Sheet) */ - byte[] exportProjectEngineering(String projectId); + byte[] exportProjectEngineeringExcel(String projectId); + + /** + * 解析指定项目的拓扑结构 + * @param projectId 项目ID + * @return TopologyParseResult(节点、边、计算计划与问题列表) + */ + com.yfd.business.css.dto.TopologyParseResult parseTopology(String projectId); + + /** + * 解析指定项目的设备有序列表 + * @param projectId 项目ID + * @return List(按拓扑 devices 出现顺序返回) + */ + java.util.List parseDeviceOrder(String projectId); + + /** + * 解析画布视图数据(设备、管线、边界、显示配置) + * @param projectId 项目ID + * @return Map 视图对象:devices/pipelines/boundaries/display + */ + java.util.Map parseCanvasView(String projectId); + + java.util.Map initSimulation(String projectId, String scenarioId, java.util.Map params); } 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 f83f9a9..adaf3c4 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 @@ -6,6 +6,7 @@ import com.yfd.business.css.mapper.ProjectMapper; import com.yfd.business.css.service.ProjectService; import org.springframework.stereotype.Service; 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 com.yfd.business.css.domain.Device; @@ -18,10 +19,25 @@ import com.yfd.business.css.service.MaterialService; import com.yfd.business.css.service.ScenarioService; import com.yfd.business.css.service.EventService; import com.yfd.business.css.service.ScenarioResultService; +import com.yfd.business.css.dto.TopologyParseResult; +import com.yfd.business.css.dto.TopoNode; +import com.yfd.business.css.dto.TopoEdge; import java.util.List; -import java.util.zip.ZipOutputStream; -import java.util.zip.ZipEntry; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.Objects; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.Queue; +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.xssf.usermodel.XSSFWorkbook; +import java.time.format.DateTimeFormatter; @Service public class ProjectServiceImpl @@ -39,75 +55,928 @@ public class ProjectServiceImpl private EventService eventService; @Resource private ScenarioResultService scenarioResultService; + @Override - public byte[] exportAllProjects() { - try { - List projects = this.list(); - return objectMapper.writeValueAsBytes(projects); + public byte[] exportAllProjectsExcel() { + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + List list = this.list(new QueryWrapper().orderByDesc("created_at")); + Sheet sheet = wb.createSheet("projects"); + int r = 0; + Row header = sheet.createRow(r++); + String[] cols = {"project_id","code","name","description","created_at","updated_at","modifier"}; + for (int i = 0; i < cols.length; i++) header.createCell(i).setCellValue(cols[i]); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + for (Project p : list) { + Row row = sheet.createRow(r++); + row.createCell(0).setCellValue(p.getProjectId()); + row.createCell(1).setCellValue(p.getCode() == null ? "" : p.getCode()); + row.createCell(2).setCellValue(p.getName() == null ? "" : p.getName()); + row.createCell(3).setCellValue(p.getDescription() == null ? "" : p.getDescription()); + row.createCell(4).setCellValue(p.getCreatedAt() == null ? "" : fmt.format(p.getCreatedAt())); + row.createCell(5).setCellValue(p.getUpdatedAt() == null ? "" : fmt.format(p.getUpdatedAt())); + row.createCell(6).setCellValue(p.getModifier() == null ? "" : p.getModifier()); + } + for (int i = 0; i < cols.length; i++) sheet.autoSizeColumn(i); + wb.write(out); + return out.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } } @Override - public byte[] exportProjectEngineering(String projectId) { + /** + * 解析指定项目的拓扑结构,生成节点、边与线性计算计划 + * @param projectId 项目ID + * @return TopologyParseResult + */ + public TopologyParseResult parseTopology(String projectId) { try { - Project project = this.getById(projectId); - if (project == null) { - throw new IllegalArgumentException("项目不存在: " + projectId); + Project p = this.getById(projectId); + TopologyParseResult r = new TopologyParseResult(); + r.setProjectId(projectId); + List nodes = new ArrayList<>(); + List edges = new ArrayList<>(); + List> plans = new ArrayList<>(); + List issues = new ArrayList<>(); + if (p == null) { + issues.add("项目不存在"); + r.setNodes(nodes); + r.setEdges(edges); + r.setPlans(plans); + r.setIssues(issues); + r.setDeviceCount(0); + r.setNodeCount(0); + r.setEdgeCount(0); + return r; } + if (p.getTopology() == null || p.getTopology().isBlank()) { + issues.add("topology为空"); + r.setNodes(nodes); + r.setEdges(edges); + r.setPlans(plans); + r.setIssues(issues); + r.setDeviceCount(0); + r.setNodeCount(0); + r.setEdgeCount(0); + return r; + } + JsonNode root = objectMapper.readTree(p.getTopology()); + JsonNode devicesNode = root.path("devices"); + Set deviceIds = new HashSet<>(); + if (devicesNode.isArray()) { + for (JsonNode dn : devicesNode) { + String deviceId = optText(dn, "deviceId"); + if (deviceId == null || deviceId.isEmpty()) { + issues.add("设备缺少deviceId"); + continue; + } + deviceIds.add(deviceId); + JsonNode staticNode = dn.path("static"); + if (staticNode.isObject()) { + staticNode.fieldNames().forEachRemaining(k -> { + TopoNode n = new TopoNode(); + n.setEntityType("device"); + n.setEntityId(deviceId); + n.setProperty(k); + n.setUnit(optText(staticNode, "unit")); + nodes.add(n); + }); + } + JsonNode props = dn.path("properties"); + if (props.isObject()) { + props.fieldNames().forEachRemaining(propName -> { + JsonNode prop = props.path(propName); + TopoNode n = new TopoNode(); + n.setEntityType("device"); + n.setEntityId(deviceId); + n.setProperty(propName); + n.setUnit(optText(prop, "unit")); + nodes.add(n); + if ("influence".equalsIgnoreCase(optText(prop, "type"))) { + List> srcList = new ArrayList<>(); + JsonNode sources = prop.path("sources"); + if (sources.isArray()) { + for (JsonNode s : sources) { + String seType = optText(s, "entityType"); + String seId = optText(s, "entityId"); + String seProp = optText(s, "property"); + Double coef = s.path("coefficient").isNumber() ? s.path("coefficient").asDouble() : 1.0; + Long delayMs = 0L; + JsonNode delay = s.path("delay"); + if (delay.path("enabled").asBoolean(false)) { + long t = delay.path("time").isNumber() ? delay.path("time").asLong() : 0L; + String u = optText(delay, "unit"); + delayMs = toMillis(t, u); + } + if (seType == null || seId == null || seProp == null) { + issues.add("sources缺少引用字段:" + deviceId + "." + propName); + } else { + TopoEdge e = new TopoEdge(); + e.setFromEntityType(seType); + e.setFromEntityId(seId); + e.setFromProperty(seProp); + e.setToEntityType("device"); + e.setToEntityId(deviceId); + e.setToProperty(propName); + e.setCoefficient(coef); + e.setDelayMs(delayMs); + edges.add(e); + Map src = new HashMap<>(); + src.put("entityType", seType); + src.put("entityId", seId); + src.put("property", seProp); + src.put("coefficient", coef); + src.put("delayMs", delayMs); + srcList.add(src); + } + } + } + Map plan = new HashMap<>(); + plan.put("target", Map.of("entityType","device","entityId",deviceId,"property",propName)); + plan.put("bias", prop.path("bias").isNumber() ? prop.path("bias").asDouble() : 0.0); + plan.put("sources", srcList); + plans.add(plan); + } + }); + } + // materials can be array or single object; also some payloads use "material" key + JsonNode mats = dn.path("materials"); + if (mats.isMissingNode() || mats.isNull()) { + mats = dn.path("material"); + } + if (mats.isArray()) { + for (JsonNode mn : mats) { + parseMaterialNode(mn, deviceId, nodes, edges, plans, issues); + } + } else if (mats.isObject()) { + parseMaterialNode(mats, deviceId, nodes, edges, plans, issues); + } + } + } else { + issues.add("devices缺失或不是数组"); + } + r.setDeviceCount(deviceIds.size()); + r.setNodes(nodes); + r.setEdges(edges); + r.setPlans(plans); + r.setIssues(issues); + r.setNodeCount(nodes.size()); + r.setEdgeCount(edges.size()); + return r; + } catch (Exception e) { + throw new RuntimeException(e); + } + } - List devices = deviceService.list( - new QueryWrapper().eq("project_id", projectId) - ); - List materials = materialService.list( - new QueryWrapper().eq("project_id", projectId) - ); - List scenarios = scenarioService.list( - new QueryWrapper().eq("project_id", projectId) + private void parseMaterialNode(JsonNode mn, + String deviceId, + List nodes, + List edges, + List> plans, + List issues) { + String mid = optText(mn, "materialId"); + if (mid == null || mid.isEmpty()) { + issues.add("材料缺少materialId: device=" + deviceId); + return; + } + JsonNode mstatic = mn.path("static"); + if (mstatic.isObject()) { + mstatic.fieldNames().forEachRemaining(k -> { + TopoNode n = new TopoNode(); + n.setEntityType("material"); + n.setEntityId(mid); + n.setProperty(k); + n.setUnit(optText(mstatic, "unit")); + nodes.add(n); + }); + } + JsonNode mprops = mn.path("properties"); + if (mprops.isObject()) { + mprops.fieldNames().forEachRemaining(propName -> { + JsonNode prop = mprops.path(propName); + TopoNode n = new TopoNode(); + n.setEntityType("material"); + n.setEntityId(mid); + n.setProperty(propName); + n.setUnit(optText(prop, "unit")); + nodes.add(n); + if ("influence".equalsIgnoreCase(optText(prop, "type"))) { + List> srcList = new ArrayList<>(); + JsonNode sources = prop.path("sources"); + if (sources.isArray()) { + for (JsonNode s : sources) { + String seType = optText(s, "entityType"); + String seId = optText(s, "entityId"); + String seProp = optText(s, "property"); + Double coef = s.path("coefficient").isNumber() ? s.path("coefficient").asDouble() : 1.0; + Long delayMs = 0L; + JsonNode delay = s.path("delay"); + if (delay.path("enabled").asBoolean(false)) { + long t = delay.path("time").isNumber() ? delay.path("time").asLong() : 0L; + String u = optText(delay, "unit"); + delayMs = toMillis(t, u); + } + if (seType == null || seId == null || seProp == null) { + issues.add("materials.sources缺少引用字段:" + mid + "." + propName); + } else { + TopoEdge e = new TopoEdge(); + e.setFromEntityType(seType); + e.setFromEntityId(seId); + e.setFromProperty(seProp); + e.setToEntityType("material"); + e.setToEntityId(mid); + e.setToProperty(propName); + e.setCoefficient(coef); + e.setDelayMs(delayMs); + edges.add(e); + Map src = new HashMap<>(); + src.put("entityType", seType); + src.put("entityId", seId); + src.put("property", seProp); + src.put("coefficient", coef); + src.put("delayMs", delayMs); + srcList.add(src); + } + } + } + Map plan = new HashMap<>(); + plan.put("target", Map.of("entityType","material","entityId",mid,"property",propName)); + plan.put("bias", prop.path("bias").isNumber() ? prop.path("bias").asDouble() : 0.0); + plan.put("sources", srcList); + plans.add(plan); + } + }); + } + } + + private static String optText(JsonNode n, String field) { + JsonNode v = n.path(field); + if (v.isMissingNode() || v.isNull()) return null; + if (v.isTextual()) return v.asText(); + if (v.isNumber() || v.isBoolean()) return String.valueOf(v.asText()); + return v.asText(null); + } + + private static long toMillis(long t, String unit) { + if (unit == null) return t; + String u = unit.toLowerCase(); + if (Objects.equals(u, "ms")) return t; + if (Objects.equals(u, "s")) return t * 1000L; + if (Objects.equals(u, "min")) return t * 60_000L; + return t; + } + + @Override + /** + * 基于项目拓扑 devices,收集 deviceId 并返回对应设备列表(按出现顺序) + * @param projectId 项目ID + * @return List + */ + public List parseDeviceOrder(String projectId) { + try { + Project p = this.getById(projectId); + List devices = new ArrayList<>(); + if (p == null || p.getTopology() == null || p.getTopology().isBlank()) { + return devices; + } + JsonNode root = objectMapper.readTree(p.getTopology()); + JsonNode devicesNode = root.path("devices"); + List ids = new ArrayList<>(); + if (devicesNode.isArray()) { + for (JsonNode dn : devicesNode) { + String deviceId = optText(dn, "deviceId"); + if (deviceId != null && !deviceId.isEmpty()) ids.add(deviceId); + } + } + if (ids.isEmpty()) return devices; + List fetched = deviceService.list(new QueryWrapper().in("device_id", ids)); + Map devMap = new HashMap<>(); + for (Device d : fetched) devMap.put(d.getDeviceId(), d); + for (String id : ids) { + Device d = devMap.get(id); + if (d != null) devices.add(d); + } + return devices; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + /** + * 提取画布视图所需数据(设备、管线、边界、显示配置) + * @param projectId 项目ID + * @return Map 视图对象 + */ + public Map parseCanvasView(String projectId) { + try { + Project p = this.getById(projectId); + Map res = new HashMap<>(); + List> devicesView = new ArrayList<>(); + List> pipelinesView = new ArrayList<>(); + List> boundariesView = new ArrayList<>(); + Map display = new HashMap<>(); + res.put("devices", devicesView); + res.put("pipelines", pipelinesView); + res.put("boundaries", boundariesView); + res.put("display", display); + if (p == null || p.getTopology() == null || p.getTopology().isBlank()) return res; + JsonNode root = objectMapper.readTree(p.getTopology()); + JsonNode devicesNode = root.path("devices"); + if (devicesNode.isArray()) { + for (JsonNode dn : devicesNode) { + String deviceId = optText(dn, "deviceId"); + Map d = new HashMap<>(); + d.put("deviceId", deviceId); + d.put("name", optText(dn, "name")); + d.put("type", optText(dn, "type")); + JsonNode ui = dn.path("ui"); + JsonNode pos = ui.path("position"); + JsonNode size = ui.path("size"); + d.put("positionX", pos.path("x").isNumber() ? pos.path("x").asInt() : 0); + d.put("positionY", pos.path("y").isNumber() ? pos.path("y").asInt() : 0); + d.put("width", size.path("width").isNumber() ? size.path("width").asInt() : 0); + d.put("height", size.path("height").isNumber() ? size.path("height").asInt() : 0); + JsonNode mats = dn.path("materials"); + if (mats.isMissingNode() || mats.isNull()) mats = dn.path("material"); + List> matList = new ArrayList<>(); + if (mats.isArray()) { + for (JsonNode mn : mats) { + String mid = optText(mn, "materialId"); + if (mid != null && !mid.isEmpty()) { + Map m = new HashMap<>(); + m.put("materialId", mid); + m.put("name", optText(mn, "name")); + matList.add(m); + } + } + } else if (mats.isObject()) { + String mid = optText(mats, "materialId"); + if (mid != null && !mid.isEmpty()) { + Map m = new HashMap<>(); + m.put("materialId", mid); + m.put("name", optText(mats, "name")); + matList.add(m); + } + } + d.put("materials", matList); + devicesView.add(d); + } + } + JsonNode pipelines = root.path("pipelines"); + if (pipelines.isArray()) { + for (JsonNode pn : pipelines) { + Map pl = new HashMap<>(); + pl.put("pipelineId", optText(pn, "pipelineId")); + pl.put("from", optText(pn, "from")); + pl.put("to", optText(pn, "to")); + pl.put("line_type", optText(pn, "line_type")); + List> path = new ArrayList<>(); + JsonNode pathNode = pn.path("path"); + if (pathNode.isArray()) { + for (JsonNode pt : pathNode) { + Map xy = new HashMap<>(); + xy.put("x", pt.path("x").isNumber() ? pt.path("x").asInt() : 0); + xy.put("y", pt.path("y").isNumber() ? pt.path("y").asInt() : 0); + path.add(xy); + } + } + pl.put("path", path); + pipelinesView.add(pl); + } + } + JsonNode boundaries = root.path("systemboundaries"); + if (boundaries.isArray()) { + for (JsonNode bn : boundaries) { + Map b = new HashMap<>(); + b.put("boundary_id", optText(bn, "boundary_id")); + b.put("name", optText(bn, "name")); + b.put("type", optText(bn, "type")); + Map geometry = new HashMap<>(); + JsonNode geom = bn.path("geometry"); + geometry.put("x", geom.path("x").isNumber() ? geom.path("x").asInt() : 0); + geometry.put("y", geom.path("y").isNumber() ? geom.path("y").asInt() : 0); + geometry.put("width", geom.path("width").isNumber() ? geom.path("width").asInt() : 0); + geometry.put("height", geom.path("height").isNumber() ? geom.path("height").asInt() : 0); + b.put("geometry", geometry); + Map line = new HashMap<>(); + JsonNode ls = bn.path("line_style"); + line.put("type", optText(ls, "type")); + line.put("width", ls.path("width").isNumber() ? ls.path("width").asInt() : 0); + line.put("color", optText(ls, "color")); + b.put("line_style", line); + boundariesView.add(b); + } + } + JsonNode gd = root.path("globalDisplay"); + if (gd.isObject()) { + display.put("device", objectMapper.convertValue(gd.path("device"), Map.class)); + display.put("material", objectMapper.convertValue(gd.path("material"), Map.class)); + } + return res; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Map initSimulation(String projectId, String scenarioId, Map params) { + Map out = new HashMap<>(); + out.put("projectId", projectId); + out.put("scenarioId", scenarioId); + List issues = new ArrayList<>(); + out.put("issues", issues); + try { + Project p = this.getById(projectId); + if (p == null || p.getTopology() == null || p.getTopology().isBlank()) { + issues.add("topology为空"); + out.put("generated", Map.of("events", 0, "snapshots", 0)); + return out; + } + JsonNode root = objectMapper.readTree(p.getTopology()); + long start = getLongParam(params, "startTime", 0L); + long end = getLongParam(params, "endTime", 60L); + long stepSec = getLongParam(params, "step", 1L); + if (end < start) end = start; + List devs = parseDeviceOrder(projectId); + if (devs.isEmpty()) { + issues.add("devices为空"); + out.put("generated", Map.of("events", 0, "snapshots", 0)); + return out; + } + Map> devStatic = new HashMap<>(); + Map> devInfluence = new HashMap<>(); + parseDeviceStaticsAndInfluences(root, devStatic, devInfluence, issues); + Map> matStatic = new HashMap<>(); + Map> matInfluence = new HashMap<>(); + parseMaterialStaticsAndInfluences(root, matStatic, matInfluence, issues); + List events = eventService.list( + new QueryWrapper() + .select("event_id","scenario_id","device_id","material_id","attr_changes","trigger_time","created_at","modifier") + .eq("scenario_id", scenarioId) ); + Map valueProviders = buildValueProviders(events, issues); + List all = new ArrayList<>(); + int snapshots = 0; + for (long t = start; t <= end; t += stepSec) { + int stepIndex = (int) ((t - start) / stepSec); + Map> deviceStates = new HashMap<>(); + for (Device d : devs) { + String did = d.getDeviceId(); + Map state = new HashMap<>(); + Map s = devStatic.getOrDefault(did, Map.of()); + for (Map.Entry e : s.entrySet()) state.put(e.getKey(), e.getValue()); + Map infl = devInfluence.getOrDefault(did, Map.of()); + for (Map.Entry e : infl.entrySet()) { + String prop = e.getKey(); + @SuppressWarnings("unchecked") + Map plan = (Map) e.getValue(); + double bias = toDouble(plan.get("bias")); + double sum = 0.0; + @SuppressWarnings("unchecked") + List> sources = (List>) plan.get("sources"); + if (sources != null) { + for (Map src : sources) { + String seType = String.valueOf(src.get("entityType")); + String seId = String.valueOf(src.get("entityId")); + String seProp = String.valueOf(src.get("property")); + double coef = toDouble(src.get("coefficient")); + long delayMs = ((Number) src.getOrDefault("delayMs", 0L)).longValue(); + long dt = t - delayMs / 1000L; + double val = readValue(seType, seId, seProp, dt, devStatic, matStatic, valueProviders); + sum += coef * val; + } + } + state.put(prop, sum + bias); + } + Map materialsState = new HashMap<>(); + for (Map.Entry> me : matStatic.entrySet()) { + String mid = me.getKey(); + Map mstate = new HashMap<>(); + for (Map.Entry e : me.getValue().entrySet()) mstate.put(e.getKey(), e.getValue()); + Map minfl = matInfluence.getOrDefault(mid, Map.of()); + for (Map.Entry e : minfl.entrySet()) { + String prop = e.getKey(); + @SuppressWarnings("unchecked") + Map plan = (Map) e.getValue(); + double bias = toDouble(plan.get("bias")); + double sum = 0.0; + @SuppressWarnings("unchecked") + List> sources = (List>) plan.get("sources"); + if (sources != null) { + for (Map src : sources) { + String seType = String.valueOf(src.get("entityType")); + String seId = String.valueOf(src.get("entityId")); + String seProp = String.valueOf(src.get("property")); + double coef = toDouble(src.get("coefficient")); + long delayMs = ((Number) src.getOrDefault("delayMs", 0L)).longValue(); + long dt = t - delayMs / 1000L; + double val = readValue(seType, seId, seProp, dt, devStatic, matStatic, valueProviders); + sum += coef * val; + } + } + mstate.put(prop, sum + bias); + } + materialsState.put(mid, mstate); + } + state.put("materials", materialsState); + deviceStates.put(did, state); + ScenarioResult sr = new ScenarioResult(); + sr.setScenarioId(scenarioId); + sr.setDeviceId(did); + sr.setStep(stepIndex); + sr.setAttrState(objectMapper.writeValueAsString(state)); + sr.setKeffValue(null); + all.add(sr); + } + snapshots += devs.size(); + } + if (!all.isEmpty()) scenarioResultService.saveBatch(all); + out.put("generated", Map.of("events", events.size(), "snapshots", snapshots)); + return out; + } catch (Exception ex) { + issues.add("初始化失败:" + ex.getMessage()); + out.put("generated", Map.of("events", 0, "snapshots", 0)); + return out; + } + } + + private void parseDeviceStaticsAndInfluences(JsonNode root, + Map> devStatic, + Map> devInfluence, + List issues) { + JsonNode devicesNode = root.path("devices"); + if (!devicesNode.isArray()) return; + for (JsonNode dn : devicesNode) { + String deviceId = optText(dn, "deviceId"); + if (deviceId == null || deviceId.isEmpty()) continue; + Map s = devStatic.computeIfAbsent(deviceId, k -> new HashMap<>()); + JsonNode st = dn.path("static"); + if (st.isObject()) { + st.fieldNames().forEachRemaining(k -> { + if (!"unit".equals(k)) { + JsonNode v = st.path(k); + if (v.isNumber() || v.isTextual()) s.put(k, v.isNumber() ? v.asDouble() : parseDouble(v.asText(), issues)); + } + }); + } + Map infl = devInfluence.computeIfAbsent(deviceId, k -> new HashMap<>()); + JsonNode props = dn.path("properties"); + if (props.isObject()) { + props.fieldNames().forEachRemaining(propName -> { + JsonNode prop = props.path(propName); + if ("influence".equalsIgnoreCase(optText(prop, "type"))) { + Map plan = new HashMap<>(); + plan.put("bias", prop.path("bias").isNumber() ? prop.path("bias").asDouble() : 0.0); + List> sources = new ArrayList<>(); + JsonNode srcs = prop.path("sources"); + if (srcs.isArray()) { + for (JsonNode sNode : srcs) { + Map src = new HashMap<>(); + src.put("entityType", optText(sNode, "entityType")); + src.put("entityId", optText(sNode, "entityId")); + src.put("property", optText(sNode, "property")); + src.put("coefficient", sNode.path("coefficient").isNumber() ? sNode.path("coefficient").asDouble() : 1.0); + long delayMs = 0L; + JsonNode delay = sNode.path("delay"); + if (delay.path("enabled").asBoolean(false)) { + long t = delay.path("time").isNumber() ? delay.path("time").asLong() : 0L; + String u = optText(delay, "unit"); + delayMs = toMillis(t, u); + } + src.put("delayMs", delayMs); + sources.add(src); + } + } + plan.put("sources", sources); + infl.put(propName, plan); + } + }); + } + } + } + + private void parseMaterialStaticsAndInfluences(JsonNode root, + Map> matStatic, + Map> matInfluence, + List issues) { + JsonNode devicesNode = root.path("devices"); + if (!devicesNode.isArray()) return; + for (JsonNode dn : devicesNode) { + JsonNode mats = dn.path("materials"); + if (mats.isMissingNode() || mats.isNull()) mats = dn.path("material"); + if (mats.isArray()) { + for (JsonNode mn : mats) parseOneMaterial(mn, matStatic, matInfluence, issues); + } else if (mats.isObject()) { + parseOneMaterial(mats, matStatic, matInfluence, issues); + } + } + } + + private void parseOneMaterial(JsonNode mn, + Map> matStatic, + Map> matInfluence, + List issues) { + String mid = optText(mn, "materialId"); + if (mid == null || mid.isEmpty()) return; + Map s = matStatic.computeIfAbsent(mid, k -> new HashMap<>()); + JsonNode st = mn.path("static"); + if (st.isObject()) { + st.fieldNames().forEachRemaining(k -> { + if (!"unit".equals(k)) { + JsonNode v = st.path(k); + if (v.isNumber() || v.isTextual()) s.put(k, v.isNumber() ? v.asDouble() : parseDouble(v.asText(), issues)); + } + }); + } + Map infl = matInfluence.computeIfAbsent(mid, k -> new HashMap<>()); + JsonNode props = mn.path("properties"); + if (props.isObject()) { + props.fieldNames().forEachRemaining(propName -> { + JsonNode prop = props.path(propName); + if ("influence".equalsIgnoreCase(optText(prop, "type"))) { + Map plan = new HashMap<>(); + plan.put("bias", prop.path("bias").isNumber() ? prop.path("bias").asDouble() : 0.0); + List> sources = new ArrayList<>(); + JsonNode srcs = prop.path("sources"); + if (srcs.isArray()) { + for (JsonNode sNode : srcs) { + Map src = new HashMap<>(); + src.put("entityType", optText(sNode, "entityType")); + src.put("entityId", optText(sNode, "entityId")); + src.put("property", optText(sNode, "property")); + src.put("coefficient", sNode.path("coefficient").isNumber() ? sNode.path("coefficient").asDouble() : 1.0); + long delayMs = 0L; + JsonNode delay = sNode.path("delay"); + if (delay.path("enabled").asBoolean(false)) { + long t = delay.path("time").isNumber() ? delay.path("time").asLong() : 0L; + String u = optText(delay, "unit"); + delayMs = toMillis(t, u); + } + src.put("delayMs", delayMs); + sources.add(src); + } + } + plan.put("sources", sources); + infl.put(propName, plan); + } + }); + } + } + + private Map buildValueProviders(List events, List issues) { + Map providers = new HashMap<>(); + for (Event ev : events) { + String json = ev.getAttrChanges(); + if (json == null || json.isBlank()) continue; + try { + JsonNode root = objectMapper.readTree(json); + String unit = optText(root, "unit"); + JsonNode target = root.path("target"); + String entityType = optText(target, "entityType"); + String entityId = optText(target, "entityId"); + String property = optText(target, "property"); + if (entityType == null || entityId == null || property == null) continue; + String key = entityType + ":" + entityId + ":" + property; + List> schedule = new ArrayList<>(); + JsonNode segments = root.path("segments"); + if (segments.isArray()) { + for (JsonNode seg : segments) { + String interp = optText(seg, "interp"); + JsonNode timeline = seg.path("timeline"); + if (timeline.isArray()) { + List> pts = new ArrayList<>(); + for (JsonNode p : timeline) { + Map pt = new HashMap<>(); + double tt = p.path("t").isNumber() ? p.path("t").asDouble() : parseDouble(optText(p, "t"), issues); + double val = p.path("value").isNumber() ? p.path("value").asDouble() : parseDouble(optText(p, "value"), issues); + pt.put("t", tt); + pt.put("value", val); + pts.add(pt); + } + if ("linear".equalsIgnoreCase(interp) && pts.size() >= 2) { + for (int i = 0; i < pts.size() - 1; i++) { + double t1 = toDouble(pts.get(i).get("t")); + double v1 = toDouble(pts.get(i).get("value")); + double t2 = toDouble(pts.get(i + 1).get("t")); + double v2 = toDouble(pts.get(i + 1).get("value")); + double dt = t2 - t1; + if (dt <= 0) continue; + double rate = (v2 - v1) / dt; + Map ramp = new HashMap<>(); + ramp.put("type", "ramp"); + ramp.put("startTime", t1); + ramp.put("endTime", t2); + ramp.put("rate", rate); + ramp.put("unit", unit); + ramp.put("startValue", v1); + schedule.add(ramp); + } + } else { + for (Map pt : pts) { + Map step = new HashMap<>(); + step.put("type", "step-set"); + step.put("time", pt.get("t")); + step.put("value", pt.get("value")); + step.put("unit", unit); + schedule.add(step); + } + } + } + } + } + providers.put(key, Map.of("unit", unit, "schedule", schedule)); + } catch (Exception e) { + issues.add("事件解析失败:" + e.getMessage()); + } + } + return providers; + } + + private double readValue(String entityType, + String entityId, + String property, + long t, + Map> devStatic, + Map> matStatic, + Map providers) { + String key = entityType + ":" + entityId + ":" + property; + @SuppressWarnings("unchecked") + Map pv = (Map) providers.get(key); + if (pv != null) { + @SuppressWarnings("unchecked") + List> schedule = (List>) pv.get("schedule"); + double last = Double.NaN; + for (Map s : schedule) { + String type = String.valueOf(s.get("type")); + if ("step-set".equals(type)) { + double time = toDouble(s.get("time")); + if (t >= (long) time) last = toDouble(s.get("value")); + } else if ("ramp".equals(type)) { + double st = toDouble(s.get("startTime")); + double et = toDouble(s.get("endTime")); + if (t >= st && t <= et) { + double rate = toDouble(s.get("rate")); + double sv = toDouble(s.get("startValue")); + last = sv + rate * (t - st); + } else if (t > et) { + double rate = toDouble(s.get("rate")); + double sv = toDouble(s.get("startValue")); + last = sv + rate * (et - st); + } + } + } + if (!Double.isNaN(last)) return last; + } + Map s = "device".equals(entityType) ? devStatic.getOrDefault(entityId, Map.of()) : matStatic.getOrDefault(entityId, Map.of()); + return s.getOrDefault(property, 0.0); + } + + private long getLongParam(Map params, String key, long def) { + if (params == null) return def; + Object v = params.get(key); + if (v == null) return def; + if (v instanceof Number) return ((Number) v).longValue(); + try { return Long.parseLong(String.valueOf(v)); } catch (Exception e) { return def; } + } + + private double parseDouble(String s, List issues) { + if (s == null) return 0.0; + try { return Double.parseDouble(s); } catch (Exception e) { issues.add("数值解析失败:" + s); return 0.0; } + } + + private double toDouble(Object o) { + if (o == null) return 0.0; + if (o instanceof Number) return ((Number) o).doubleValue(); + try { return Double.parseDouble(String.valueOf(o)); } catch (Exception e) { return 0.0; } + } + + @Override + public byte[] exportProjectEngineeringExcel(String projectId) { + try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + List projects = this.list(new QueryWrapper().eq("project_id", projectId)); + List devices = deviceService.list(new QueryWrapper().eq("project_id", projectId)); + List materials = materialService.list(new QueryWrapper().eq("project_id", projectId)); + List scenarios = scenarioService.list(new QueryWrapper().eq("project_id", projectId)); List scenarioIds = scenarios.stream().map(Scenario::getScenarioId).toList(); - List events = scenarioIds.isEmpty() ? List.of() - : eventService.list(new QueryWrapper().in("scenario_id", scenarioIds)); + : eventService.list(new QueryWrapper() + .select("event_id","scenario_id","device_id","material_id","attr_changes","trigger_time","created_at","modifier") + .in("scenario_id", scenarioIds)); List results = scenarioIds.isEmpty() ? List.of() : scenarioResultService.list(new QueryWrapper().in("scenario_id", scenarioIds)); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(baos)) { - // 项目信息 - zos.putNextEntry(new ZipEntry("project.json")); - zos.write(objectMapper.writeValueAsBytes(project)); - zos.closeEntry(); - - // 设备 - zos.putNextEntry(new ZipEntry("devices.json")); - zos.write(objectMapper.writeValueAsBytes(devices)); - zos.closeEntry(); - - // 物料 - zos.putNextEntry(new ZipEntry("materials.json")); - zos.write(objectMapper.writeValueAsBytes(materials)); - zos.closeEntry(); - - // 情景 - zos.putNextEntry(new ZipEntry("scenarios.json")); - zos.write(objectMapper.writeValueAsBytes(scenarios)); - zos.closeEntry(); - - // 事件 - zos.putNextEntry(new ZipEntry("events.json")); - zos.write(objectMapper.writeValueAsBytes(events)); - zos.closeEntry(); - - // 结果 - zos.putNextEntry(new ZipEntry("scenario-results.json")); - zos.write(objectMapper.writeValueAsBytes(results)); - zos.closeEntry(); + Sheet s1 = wb.createSheet("projects"); + String[] h1 = {"project_id","code","name","description","created_at","updated_at","modifier"}; + int r = 0; Row rh1 = s1.createRow(r++); for (int i=0;i= 0); + Assertions.assertTrue(generatedSnapshots >= 0); + + mockMvc.perform(MockMvcRequestBuilders.get("/scenario-results/by-scenario") + .param("scenarioId", scenarioId) + .param("pageNum", "1") + .param("pageSize", "50")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(0)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.records").isArray()); + } + + @Test + public void initSimulation_direct() throws Exception { + String projectId = "proj-0001-uuid"; + String scenarioId = "scen-001-uuid"; + String initBody = "{\"startTime\":0,\"endTime\":10,\"step\":2}"; + String initResp = mockMvc.perform(MockMvcRequestBuilders.post("/projects/" + projectId + "/scenarios/" + scenarioId + "/simulation/init") + .contentType(MediaType.APPLICATION_JSON) + .content(initBody)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(0)) + .andReturn().getResponse().getContentAsString(); + JsonNode initNode = objectMapper.readTree(initResp).path("data"); + int generatedEvents = initNode.path("generated").path("events").asInt(); + int generatedSnapshots = initNode.path("generated").path("snapshots").asInt(); + Assertions.assertTrue(generatedEvents >= 0); + Assertions.assertTrue(generatedSnapshots >= 0); + mockMvc.perform(MockMvcRequestBuilders.get("/scenario-results/by-scenario") + .param("scenarioId", scenarioId) + .param("pageNum", "1") + .param("pageSize", "50")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(0)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.records").isArray()); + } + + @Test + public void initSimulation_returnData() throws Exception { + String projectId = "proj-0001-uuid"; + String scenarioId = "scen-001-uuid"; + String initBody = "{\"startTime\":0,\"endTime\":12,\"step\":3}"; + String initResp = mockMvc.perform(MockMvcRequestBuilders.post("/projects/" + projectId + "/scenarios/" + scenarioId + "/simulation/init") + .contentType(MediaType.APPLICATION_JSON) + .content(initBody)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(0)) + .andReturn().getResponse().getContentAsString(); + System.out.println("initResp=" + initResp); + JsonNode initNode = objectMapper.readTree(initResp).path("data"); + int eventsCount = initNode.path("generated").path("events").asInt(); + int snapshotsCount = initNode.path("generated").path("snapshots").asInt(); + System.out.println("eventsCount=" + eventsCount + ", snapshotsCount=" + snapshotsCount); + String resultResp = mockMvc.perform(MockMvcRequestBuilders.get("/scenario-results/by-scenario") + .param("scenarioId", scenarioId) + .param("pageNum", "1") + .param("pageSize", "20")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(0)) + .andReturn().getResponse().getContentAsString(); + System.out.println("resultResp=" + resultResp); + JsonNode resultNode = objectMapper.readTree(resultResp).path("data").path("records"); + Assertions.assertTrue(resultNode.isArray()); + System.out.println("recordsSize=" + resultNode.size()); + } +} diff --git a/framework/pom.xml b/framework/pom.xml index bf9f6cf..17b245b 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -356,33 +356,6 @@ - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.4.1 - - - enforce-rules - - enforce - - - - - - [3.6.3,) - - - [17,) - - - - - - - - org.apache.maven.plugins diff --git a/mvn-settings.xml b/mvn-settings.xml new file mode 100644 index 0000000..d2918e8 --- /dev/null +++ b/mvn-settings.xml @@ -0,0 +1,45 @@ + + + + + aliyunmaven + Aliyun Maven + central + https://maven.aliyun.com/repository/public + + + + + aliyun + + + central + https://repo.maven.apache.org/maven2 + + true + + + true + + + + + + central + https://repo.maven.apache.org/maven2 + + true + + + true + + + + + + + aliyun + + diff --git a/scripts/mvn17.cmd b/scripts/mvn17.cmd new file mode 100644 index 0000000..73701a2 --- /dev/null +++ b/scripts/mvn17.cmd @@ -0,0 +1,4 @@ +@echo off +set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.17.10-hotspot" +set "PATH=%JAVA_HOME%\bin;%PATH%" +mvn %* diff --git a/系统代码结构框架规划.md b/系统代码结构框架规划.md index 337139e..b3addde 100644 --- a/系统代码结构框架规划.md +++ b/系统代码结构框架规划.md @@ -162,6 +162,8 @@ src/main/resources # 1. 安装 framework 到本地仓库 mvn -DskipTests clean install -pl framework + + # 2. 启动业务模块(自动依赖 framework) mvn -DskipTests spring-boot:run -pl business-css ``` @@ -218,4 +220,27 @@ business-css/target/business-css-1.0-SNAPSHOT.jar # 业务服务(含内嵌 To --- -> **一句话总结**:framework 做“平台”,business-css 做“产品”;平台沉淀,产品迭代,互不污染,横向复制。 \ No newline at end of file +> **一句话总结**:framework 做“平台”,business-css 做“产品”;平台沉淀,产品迭代,互不污染,横向复制。 + +## 快速稳定方案(作用:脚本先设置 JAVA_HOME 与 PATH 指向 17,再调用 Maven,确保所有构建/运行都用 JDK 17,不受工具自带 JDK 8 影响。) +使用项目内脚本强制 JDK 17(已为你添加): +scripts\mvn17.cmd -s mvn-settings.xml -DskipTests -pl framework -am install + +scripts\mvn17.cmd -s mvn-settings.xml -DskipTests -f business-css\pom.xml spring-boot:run -Dspring-boot.run.profiles=business + +## 在工具内终端验证并调整到 17: +- 验证: mvn -version 、 where java 、 where mvn +- 临时修正当前终端(一次性手动): + - set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.17.10-hotspot" + - set "PATH=%JAVA_HOME%\bin;%PATH%" + - 再 mvn -version 应为 17 + +## 开发模式 + +- 进入前端目录: business-css/frontend +- 安装依赖: npm install +- 启动开发服务器: npm run dev +- 访问: http://localhost:3000/ + +set "PATH=d:/Program Files/nodejs;%PATH%" +