diff --git a/business-css/SimController 及相关接口实现建议v4-2.md b/business-css/SimController 及相关接口实现建议v4-2.md new file mode 100644 index 0000000..96595bf --- /dev/null +++ b/business-css/SimController 及相关接口实现建议v4-2.md @@ -0,0 +1,151 @@ +# SimController 及相关接口实现建议 v4-2(结合当前工程现状) + +本文档基于 [情景模拟分析结果v3.md](file:///e:/projectJava/JavaProjectRepo/business-css/%E6%83%85%E6%99%AF%E6%A8%A1%E6%8B%9F%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9Cv3.md) 第 3 条,并结合当前可运行代码的真实调用链,给出可落地的实现建议。 + +--- + +## 0. 现状盘点(必须先对齐) + +### 0.1 当前“线上可用”的仿真接口在哪里 +目前仿真入口实际在 `ProjectController`,而不是 `SimController`: +- 初始化:`POST /projects/simulation/init` → [ProjectController.java:L275-L300](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java#L275-L300) → [ProjectServiceImpl.initSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L614-L774) +- 运行:`POST /projects/simulation/run` → [ProjectController.java:L288-L300](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java#L288-L300) → [ProjectServiceImpl.runSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1296-L1351) + +### 0.2 SimController 为什么不可用 +[SimController.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/controller/SimController.java#L1-L43) 目前是“原型草稿”,在本仓库中无法落地,原因是: +- 引用的 `ProjectRepository/EventRepository/InfluenceRepository/InferenceConverter` 仅在该文件出现,仓库中不存在真实实现。 +- `SimBuilder.buildUnits/buildEvents/buildInfluenceNodes` 也不存在([SimBuilder.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java#L1-L44) 只有注释草稿)。 +- `SimService` 当前逻辑是 KV 级简化引擎,并且事件优先级与现有 `initSimulation` 的行为不一致([SimService.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/SimService.java#L7-L51))。 + +### 0.3 当前系统的“真实数据格式” +`runSimulation` 解析的是一种“frames → devices”结构,并按 `deviceType` 分组: +- 调用入口:[ProjectServiceImpl.runSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1296-L1351) +- 解析工具:[DeviceDataParser.parseAndGroupDeviceData](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/utils/DeviceDataParser.java#L1-L88) +- 单步模型:[DeviceStepInfo.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/model/DeviceStepInfo.java) + +因此,“让 SimController 可用”的关键,不是从 0 造一套新格式,而是:**复用现有 frames 格式(或在 SimController 内把 SimContext 转为该格式)**,以便直接复用 `DeviceDataParser` + `DeviceInferService` 的整条推理/落库链路。 + +--- + +## 1. 建议的目标形态:SimController 成为“仿真编排入口” + +建议把 SimController 从“原型”升级为生产入口,但要遵循当前工程已存在的服务边界: + +### 1.1 目标职责划分(贴合当前代码) +- **SimController**:只处理 HTTP + 参数校验 + 返回值结构统一(保持与 ProjectController 相同的 `{code,msg,data}` 或复用统一响应体)。 +- **SimulationFacade(新增)**:负责把“现有 services + topology JSON + event 表”组装成可计算输入;并在需要时调用推理、落库、更新情景状态。 +- **SimulationEngine(新增或重构现有 SimService)**:只做“时序帧生成/影响计算/事件注入”,纯内存计算,不碰 DB。 +- **Converter(新增)**:把 Engine 输出转成 `DeviceDataParser` 能吃的 frames JSON(或直接输出 Map 结构)。 + +这样做的好处:既保留 `SimController` 分层方向,又不引入仓库里不存在的 Repository/Converter 类型。 + +--- + +## 2. 具体落地建议(按当前文件/类名对齐) + +### 2.1 先做最小可用:让 /sim 接口复用现有 ProjectServiceImpl 的能力 +为了快速验证链路,第一阶段建议不要立刻重写引擎,而是“搬运+封装”: + +1) **新增 `SimulationFacade`(建议放在 `com.yfd.business.css.service.sim` 包)** +内部依赖现有 service:`ProjectService/ScenarioService/EventService/DeviceInferService`(以及 `MaterialService` 若需要 DB 补全静态物料属性)。 +它暴露两类能力: +- `init(projectId, scenarioId, params)`:复用 [ProjectServiceImpl.initSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L614-L774) 或将其中解析/计算段迁移出来。 +- `run(projectId, scenarioId, params)`:复用 [ProjectServiceImpl.runSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1296-L1351)。 + +2) **SimController 直接调用 facade,并提供与 ProjectController 一致的返回结构** +这样能做到:新增 `/sim/*` 不影响现有 `/projects/simulation/*`,并且复用现有稳定链路。 + +这一阶段的目标是:**先让 SimController 可用、可回归测试、可逐步迁移**。 + +### 2.2 第二阶段:把 initSimulation 拆成“解析/计算/输出”三个可替换模块 +当前 `initSimulation` 里混杂了:拓扑解析、设备顺序、静态注入、影响计算、事件解析、帧输出。建议拆成 3 个模块,便于未来替换而不改 API: + +#### A. Topology & Static 解析模块(建议:TopologyParser) +直接复用现有实现(迁移或抽取): +- 设备顺序:`parseDeviceOrder(projectId)`:[ProjectServiceImpl.java:L370-L397](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L370-L397) +- 设备-物料绑定:`buildDeviceMaterialMap`:[ProjectServiceImpl.java:L1063-L1090](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1063-L1090) +- 静态/影响解析: + - 设备:`parseDeviceStaticsAndInfluences`:[ProjectServiceImpl.java:L806-L860](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L806-L860) + - 物料:`parseMaterialStaticsAndInfluences`:[ProjectServiceImpl.java:L861-L928](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L861-L928) +- DB 物料静态补全:`buildMaterialStaticFromDb`:[ProjectServiceImpl.java:L1116-L1141](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1116-L1141) +- 设备 size 注入:`injectDeviceSize`:[ProjectServiceImpl.java:L776-L804](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L776-L804) + +建议输出一个结构化的 DTO,例如: +- `List orderedDevices` +- `Map` +- `Map` +- `devStatic/devInfluence/matStatic/matInfluence/matStaticDb` + +#### B. Event 解析模块(建议:EventScheduleBuilder) +直接复用现有 `attr_changes` 解析逻辑: +- `buildValueProviders`:[ProjectServiceImpl.java:L929-L996](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L929-L996) +- `collectTimePoints`:[ProjectServiceImpl.java:L1037-L1061](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1037-L1061) +- `readValue`:[ProjectServiceImpl.java:L998-L1035](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L998-L1035) + +补强建议(贴合现状的缺口): +- 当 `timePoints` 为空时不要直接失败:允许用 params 提供 `start/end/interval` 生成时间网格(否则“无事件仿真”无法跑)。 +- ramp 事件目前仅把起止时刻作为输出点;若前端想看平滑曲线,需要补齐采样点(例如每 1s)。 + +#### C. Frame 生成模块(建议:FrameGenerator) +复用 `initSimulation` 的生成循环,但把“覆盖策略”参数化: +- 现在的覆盖策略是:**Static → Influence → overrideWithEvents(最终覆盖)** + 代码:[overrideWithEvents](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1106-L1114) +- 建议支持两类事件:Input 与 Override(可先用 params 开关模拟) + - Input:计算前注入 + - Override:计算后强制覆盖 + +这样第三阶段才需要动到“事件类型”定义;第二阶段只要把钩子留好即可。 + +--- + +## 3. SimService/SimModel 如何与现有链路对接(不要重新造轮子) + +### 3.1 现有 SimModel 的适配建议(只做必要改造) +当前 SimModel 过于抽象(纯 KV),无法表达“设备/物料静态属性注入、deviceType、material 绑定”等现有业务关键点: +- `SimUnit` 只有 `unitId/deviceId/materialId/deviceType`:[SimUnit.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/model/SimUnit.java) +- `SimContext` 只有 `Map` 的 currentValues:[SimContext.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/model/SimContext.java) + +建议的最小增强(为了能生成当前 runSimulation 可消费的 frames): +- `SimUnit` 增加 `Map staticProps`(至少承载 diameter/height 与物料关键属性)。 +- `SimEvent` 增加 `boolean override` 或扩展 EventType(区分 Input/Override)。 +- `SimInfluenceSource.delay` 目前未在 `SimService` 使用;若要支持 delay,必须用 `ctx.timeline` 回看历史值(当前 `SimContext` 已能保留 step 的快照)。 + +### 3.2 Converter:把 SimContext 输出转换成现有 frames 格式 +建议增加一个 Converter(替代原型里的 `InferenceConverter`),输出结构与 `DeviceDataParser` 一致: + +```json +{ + \"data\": { + \"frames\": [ + { + \"step\": 0, + \"time\": 0, + \"devices\": { + \"dev-001\": {\"deviceType\": \"CylindricalTank\", \"diameter\": 20, \"height\": 20, \"u_concentration\": 20} + } + } + ] + } +} +``` + +理由:这样 `SimController` 可以直接调用 `DeviceDataParser.parseAndGroupDeviceData` + `DeviceInferService.processDeviceInference`,与现有落库路径完全一致。 + +--- + +## 4. SimController 的接口形态建议(与现有系统兼容) + +建议同时支持“两段式”和“一段式”,避免推倒重来: + +### 4.1 两段式(兼容现有前端/流程) +- `POST /sim/init`:返回 frames(与 `/projects/simulation/init` 对齐) +- `POST /sim/run`:接收 frames,调用推理并落库(与 `/projects/simulation/run` 对齐) + +### 4.2 一段式(面向后端批处理/自动化) +- `POST /sim/run-all`:内部调用 init 生成 frames,再立即 run 推理落库,返回摘要(如 snapshots、结果条数、耗时) + +--- + +## 5. 迁移与风险控制(建议强制执行) + +1) **先引入新接口,不删旧接口**:让 `/sim/*` 与 `/projects/simulation/*` 并行一段时间。\n\n2) **帧格式不变**:任何新实现必须输出 `DeviceDataParser` 可解析的结构,否则推理链路与前端都要一起改,风险最大。\n\n3) **行为一致性测试**:对比新旧 init 输出(同 projectId/scenarioId)是否一致;对比 run 后写入 `scenario_result` 条数与 key 字段是否一致。\n\n4) **清理 System.out.println**:`ProjectServiceImpl` 的 init/run 里有大量 `System.out.println`(例如 [ProjectServiceImpl.java:L644-L661](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L644-L661) 与 [runSimulation debug](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1319-L1338)),建议迁移到统一日志体系后再移除,以免污染生产日志与性能。\n\n---\n\n## 6. 最小实现清单(按优先级排序)\n\n- 建议 1:让 `SimController` 先“可用”——删除/替换不存在的 Repository/Converter 依赖,改用现有 Service。\n- 建议 2:新增 `SimulationFacade`,把现有 `ProjectServiceImpl.initSimulation/runSimulation` 先封装起来。\n- 建议 3:逐步抽取 `TopologyParser/EventScheduleBuilder/FrameGenerator`,把 `ProjectServiceImpl` 中的计算逻辑迁移出来。\n- 建议 4:最后再考虑把增强后的 `SimService/SimModel` 正式替换成唯一引擎实现。\n+ diff --git a/business-css/SimController 及相关接口实现建议v4.md b/business-css/SimController 及相关接口实现建议v4.md new file mode 100644 index 0000000..ab8865f --- /dev/null +++ b/business-css/SimController 及相关接口实现建议v4.md @@ -0,0 +1,152 @@ +# SimController 及相关接口实现建议 v4 + +基于 [情景模拟分析结果v3.md](file:///e:/projectJava/JavaProjectRepo/business-css/%E6%83%85%E6%99%AF%E6%A8%A1%E6%8B%9F%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9Cv3.md) 中的分析,`SimController` 及其配套组件 (`SimService`, `SimBuilder`, `SimModel`) 代表了系统向**模块化、可测试化**方向演进的正确路径。 + +当前 `SimController` 处于不可用状态(依赖缺失、逻辑简化、代码注释)。为了将其转化为生产可用的仿真服务,以下是具体的实现建议与重构方案。 + +--- + +## 1. 总体架构设计 + +目标是将仿真逻辑从 `ProjectServiceImpl` 中剥离,构建独立的仿真层。 + +* **Controller 层 (`SimController`)**: 仅负责接收 HTTP 请求,参数校验,调用 Service,返回结果。 +* **Facade 层 (`SimDataFacade`)**: **新增组件**。负责与现有的 `ProjectService`, `EventService`, `InfluenceService` 交互,获取原始数据(Project 实体, Event 列表等),并屏蔽数据库细节。*注:原设计中的 `ProjectRepository` 等接口在本项目中没有实现,直接复用现有的 Service 层更符合现状。* +* **Builder 层 (`SimBuilder`)**: 负责将原始数据(Entity/JSON)转换为仿真专用的领域模型 (`SimUnit`, `SimEvent`, `SimInfluenceNode`)。 +* **Engine 层 (`SimService`)**: **核心计算引擎**。纯内存计算,不依赖数据库。执行 `Static -> Event -> Influence -> Override` 的标准管线。 + +--- + +## 2. 详细实现建议 + +### 2.1 补齐数据获取层 (SimDataFacade) + +原 `SimController` 依赖了不存在的 `ProjectRepository` 等接口。建议创建一个 `SimDataFacade` 来封装数据获取逻辑。 + +```java +@Component +public class SimDataFacade { + private final ProjectService projectService; + private final EventService eventService; + // ... 其他 Service + + // 封装获取逻辑:获取项目拓扑、事件列表、影响关系等 + public SimDataPackage loadSimulationData(String projectId, String scenarioId) { + Project project = projectService.getById(projectId); + List events = eventService.list(new QueryWrapper().eq("scenario_id", scenarioId)); + // ... 获取其他必要数据 + return new SimDataPackage(project, events); + } +} +``` + +### 2.2 激活并增强 Builder (SimBuilder) + +`SimBuilder` 目前被注释掉了,需要激活并实现核心转换逻辑。重点是将 `ProjectServiceImpl.initSimulation` 中的解析逻辑迁移过来。 + +* **`buildUnits`**: 解析 `Project.topology` JSON,提取 Device 和 Material,构建 `SimUnit` 列表。 + * *关键点*:需要包含静态属性(Static Values)的解析,作为 `SimUnit` 的初始状态。 +* **`buildEvents`**: 解析 `Event.attr_changes` JSON,构建 `SimEvent` 列表。 + * *关键点*:区分 **普通事件 (Input)** 和 **强制覆盖事件 (Override)**。建议在 `SimEvent` 中增加 `isOverride` 标志。 +* **`buildInfluenceNodes`**: 解析 `Project.topology` 中的 `properties` -> `influence` 节点,构建 `SimInfluenceNode` 列表。 + +### 2.3 重构计算引擎 (SimService) + +这是最核心的部分,必须修正当前的“事件 -> 计算”逻辑,改为 **标准管线**。 + +**建议代码结构:** + +```java +public class SimService { + + public SimContext runSimulation(List units, + List events, + List nodes, + int steps) { + SimContext ctx = new SimContext(); + + // 1. 初始化静态基线 (Static) + // 将 SimUnit 中携带的静态属性写入 ctx (t=0) + for (SimUnit unit : units) { + unit.getStaticProperties().forEach((k, v) -> + ctx.setValue(SimPropertyKey.of(unit.id(), k), v) + ); + } + + // 2. 时间步推进 + for (int step = 0; step <= steps; step++) { + // 2.1 应用输入事件 (Event Input) + // 筛选当前 step 的普通事件,写入 ctx + applyEvents(ctx, events, step, false); + + // 2.2 执行影响计算 (Influence) + // 基于当前 ctx 状态,计算所有 InfluenceNode + // 注意:为了避免计算顺序依赖,建议使用双缓冲 (Snapshot) 或 拓扑排序 + // 简单实现可先计算 diff,再统一应用 + applyInfluences(ctx, nodes); + + // 2.3 应用强制覆盖事件 (Event Override) + // 筛选当前 step 的强制事件,再次写入 ctx,覆盖计算结果 + applyEvents(ctx, events, step, true); + + // 2.4 保存快照 + ctx.snapshot(step); + } + return ctx; + } +} +``` + +### 2.4 统一 API 接口 (SimController) + +建议将 `SimController` 作为仿真功能的唯一入口。 + +```java +@RestController +@RequestMapping("/sim") +public class SimController { + + @PostMapping("/run") + public Result run(@RequestBody SimulationRequest req) { + // 1. 加载数据 + SimDataPackage data = simDataFacade.loadSimulationData(req.getProjectId(), req.getScenarioId()); + + // 2. 构建模型 + List units = SimBuilder.buildUnits(data.getProject()); + List events = SimBuilder.buildEvents(data.getEvents()); + List nodes = SimBuilder.buildInfluenceNodes(data.getProject()); + + // 3. 执行仿真 + SimContext ctx = simService.runSimulation(units, events, nodes, req.getSteps()); + + // 4. 结果转换 (适配前端图表或 AI 推理) + return Result.success(SimResultConverter.convert(ctx)); + } +} +``` + +--- + +## 3. 实施路线图 + +1. **基础类准备**: + * 完善 `SimUnit`: 增加 `Map staticProperties` 字段。 + * 完善 `SimEvent`: 增加 `boolean isOverride` 字段。 + * 创建 `SimDataFacade` 类。 + +2. **迁移解析逻辑**: + * 将 `ProjectServiceImpl` 中解析 JSON (Topology, Event) 的代码块复制到 `SimBuilder` 中并适配。 + * 确保单元测试覆盖 `SimBuilder`,保证解析正确性。 + +3. **实现计算逻辑**: + * 编写 `SimService.runSimulation`,严格按照推荐的 4 步管线实现。 + * 编写单元测试,验证“事件覆盖计算”和“计算基于静态值”的场景。 + +4. **接口接入**: + * 在 `SimController` 中装配上述组件。 + * 前端对接新的 `/sim/run` 接口。 + +5. **清理**: + * 标记 `ProjectServiceImpl` 中的旧模拟代码为 `@Deprecated`,并在验证新接口无误后删除。 + +通过以上步骤,可以将复杂的仿真逻辑从业务 Service 中解耦,构建一个清晰、可维护、易扩展的仿真引擎。 diff --git a/business-css/SimController及相关接口详细实现文档.md b/business-css/SimController及相关接口详细实现文档.md new file mode 100644 index 0000000..c241d59 --- /dev/null +++ b/business-css/SimController及相关接口详细实现文档.md @@ -0,0 +1,194 @@ +# SimController 及相关接口详细实现文档 v4-2 + +本文档基于 [SimController 及相关接口实现建议v4.md](file:///e:/projectJava/JavaProjectRepo/business-css/SimController%20%E5%8F%8A%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3%E5%AE%9E%E7%8E%B0%E5%BB%BA%E8%AE%AEv4.md) 进一步细化,提供了更贴合当前代码库(ProjectServiceImpl)的详细实现方案。 + +## 1. 核心目标 + +* **激活 `SimController`**:使其成为仿真服务的唯一入口,替代分散在 `ProjectController` 中的逻辑。 +* **标准化计算管线**:实现 `Static -> Event(Input) -> Influence -> Event(Override)` 的标准计算流程。 +* **复用现有逻辑**:最大程度复用 `ProjectServiceImpl` 中已有的 JSON 解析和数据组装代码,避免重复造轮子。 + +--- + +## 2. 模块详细设计与实现 + +### 2.1 数据获取层 (SimDataFacade) + +负责从 DB 获取原始数据,并进行初步的组装。 + +**File:** `src/main/java/com/yfd/business/css/facade/SimDataFacade.java` (新建) + +```java +@Component +public class SimDataFacade { + @Autowired private ProjectService projectService; + @Autowired private EventService eventService; + @Autowired private MaterialService materialService; // 用于补全物料静态属性 + + public SimDataPackage loadSimulationData(String projectId, String scenarioId) { + // 1. 获取项目与拓扑 + Project project = projectService.getById(projectId); + if (project == null) throw new IllegalArgumentException("Project not found: " + projectId); + + // 2. 获取事件 + List events = eventService.list(new QueryWrapper().eq("scenario_id", scenarioId)); + + // 3. 预加载物料库 (用于后续补全静态属性) + // 逻辑复用 ProjectServiceImpl.buildMaterialStaticFromDb + // 这里先返回原始数据,由 Builder 处理具体的补全逻辑 + return new SimDataPackage(project, events); + } +} +``` + +### 2.2 模型构建层 (SimBuilder) + +负责将 `Project` (JSON) 和 `Event` (JSON) 转换为 `SimUnit`, `SimEvent`, `SimInfluenceNode`。 + +**关键策略**:直接复用 `ProjectServiceImpl` 中的解析方法(`parseDeviceStaticsAndInfluences`, `buildValueProviders` 等),将其重构为静态工具方法或独立组件。 + +**File:** `src/main/java/com/yfd/business/css/build/SimBuilder.java` (激活并重构) + +```java +public class SimBuilder { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // 1. 构建单元 (SimUnit) - 包含静态属性 + public List buildUnits(Project project, MaterialService materialService) { + List units = new ArrayList<>(); + JsonNode root = objectMapper.readTree(project.getTopology()); + + // 解析设备静态属性 (复用 ProjectServiceImpl.parseDeviceStaticsAndInfluences 的部分逻辑) + // 解析物料静态属性 (复用 ProjectServiceImpl.buildMaterialStaticFromDb) + + // 伪代码示例 + for (JsonNode deviceNode : root.path("devices")) { + String deviceId = deviceNode.path("deviceId").asText(); + Map staticProps = new HashMap<>(); + + // 1.1 解析 topology 中的 static 节点 + // 1.2 解析 Device.size 并注入 + // 1.3 如果有绑定的 material,查询 DB 并注入物料属性 + + units.add(new SimUnit(deviceId, ..., staticProps)); + } + return units; + } + + // 2. 构建事件 (SimEvent) + public List buildEvents(List events) { + // 复用 ProjectServiceImpl.buildValueProviders 解析 attr_changes + // 将解析出的 Schedule 转换为 List + // 注意区分 isOverride (强制覆盖) + } + + // 3. 构建影响关系 (SimInfluenceNode) + public List buildInfluenceNodes(Project project) { + // 复用 ProjectServiceImpl.parseDeviceStaticsAndInfluences 中的 influence 解析逻辑 + } +} +``` + +### 2.3 核心计算引擎 (SimService) + +实现标准计算管线。 + +**File:** `src/main/java/com/yfd/business/css/service/SimService.java` (重构) + +```java +public class SimService { + + public SimContext runSimulation(List units, + List events, + List nodes, + int steps) { + SimContext ctx = new SimContext(); + + // Step 1: 初始化静态基线 (t=0) + for (SimUnit unit : units) { + unit.getStaticProperties().forEach((k, v) -> + ctx.setValue(SimPropertyKey.of(unit.unitId(), k), v) + ); + } + + // Step 2: 循环推进 (t=1 to steps) + for (int step = 1; step <= steps; step++) { + // 2.1 Event (Input): 应用普通事件 + applyEvents(ctx, events, step, false); + + // 2.2 Influence: 计算影响关系 + // 基于当前 ctx (包含 static + input event) 计算 + applyInfluences(ctx, nodes); + + // 2.3 Event (Override): 应用强制覆盖事件 + // 再次覆盖,确保强制逻辑生效 + applyEvents(ctx, events, step, true); + + // 2.4 Snapshot + ctx.snapshot(step); + } + return ctx; + } + + // 辅助方法:applyEvents, applyInfluences +} +``` + +### 2.4 统一控制器 (SimController) + +**File:** `src/main/java/com/yfd/business/css/controller/SimController.java` (激活) + +```java +@RestController +@RequestMapping("/sim") +public class SimController { + + @Autowired private SimDataFacade simDataFacade; + @Autowired private SimBuilder simBuilder; // 如果是 Bean + @Autowired private SimService simService; + + // 标准运行接口 + @PostMapping("/run") + public Result> run(@RequestBody SimulationRequest req) { + // 1. Load Data + SimDataPackage data = simDataFacade.loadSimulationData(req.getProjectId(), req.getScenarioId()); + + // 2. Build Model + List units = simBuilder.buildUnits(data.getProject(), ...); + List events = simBuilder.buildEvents(data.getEvents()); + List nodes = simBuilder.buildInfluenceNodes(data.getProject()); + + // 3. Run Engine + SimContext ctx = simService.runSimulation(units, events, nodes, req.getSteps()); + + // 4. Convert Result (复用 DeviceDataParser 或 InferenceConverter) + // 保持与前端/推理接口的数据格式兼容 + return Result.success(SimResultConverter.toFrames(ctx)); + } +} +``` + +--- + +## 3. 具体复用点清单 (ProjectServiceImpl -> SimBuilder) + +为了加快开发,以下方法建议直接从 `ProjectServiceImpl` 提取到 `SimBuilder` 或工具类 `SimParserUtils` 中: + +1. **`injectDeviceSize`**: 解析设备尺寸。 +2. **`buildMaterialStaticFromDb`**: 补全物料静态属性。 +3. **`parseDeviceStaticsAndInfluences`**: 解析设备静态值和影响关系。 +4. **`parseMaterialStaticsAndInfluences`**: 解析物料静态值和影响关系。 +5. **`buildValueProviders`**: 解析事件 JSON (attr_changes)。 + +**注意**:在提取时,需要将原本直接操作 `Map state` 的逻辑,改为构建 `SimUnit` 或 `SimInfluenceNode` 对象。 + +--- + +## 4. 兼容性与迁移 + +* **API 兼容**:新接口 `/sim/run` 的返回结构应尽量与原 `/projects/simulation/run` 中的 `frames` 结构保持一致,以便前端无缝切换。 +* **分步上线**: + 1. 先上线 `/sim/run` 供测试使用。 + 2. 验证无误后,将前端调用切到新接口。 + 3. 废弃 `ProjectController` 中的相关接口。 diff --git a/business-css/fix_material_h2c2o4.sql b/business-css/fix_material_h2c2o4.sql new file mode 100644 index 0000000..b584ef8 --- /dev/null +++ b/business-css/fix_material_h2c2o4.sql @@ -0,0 +1 @@ +ALTER TABLE material MODIFY COLUMN h2c2o4_concentration DECIMAL(20, 8); \ No newline at end of file diff --git a/business-css/frontend/node/node.exe b/business-css/frontend/node/node.exe new file mode 100644 index 0000000..b54f2ca Binary files /dev/null and b/business-css/frontend/node/node.exe differ diff --git a/business-css/frontend/node/npm b/business-css/frontend/node/npm new file mode 100644 index 0000000..a131a53 --- /dev/null +++ b/business-css/frontend/node/npm @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix + +basedir=`dirname "$0"` + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +NODE_EXE="$basedir/node.exe" +if ! [ -x "$NODE_EXE" ]; then + NODE_EXE="$basedir/node" +fi +if ! [ -x "$NODE_EXE" ]; then + NODE_EXE=node +fi + +# this path is passed to node.exe, so it needs to match whatever +# kind of paths Node.js thinks it's using, typically win32 paths. +CLI_BASEDIR="$("$NODE_EXE" -p 'require("path").dirname(process.execPath)')" +NPM_CLI_JS="$CLI_BASEDIR/node_modules/npm/bin/npm-cli.js" + +NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g` +if [ $? -ne 0 ]; then + # if this didn't work, then everything else below will fail + echo "Could not determine Node.js install directory" >&2 + exit 1 +fi +NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js" + +# a path that will fail -f test on any posix bash +NPM_WSL_PATH="/.." + +# WSL can run Windows binaries, so we have to give it the win32 path +# however, WSL bash tests against posix paths, so we need to construct that +# to know if npm is installed globally. +if [ `uname` = 'Linux' ] && type wslpath &>/dev/null ; then + NPM_WSL_PATH=`wslpath "$NPM_PREFIX_NPM_CLI_JS"` +fi +if [ -f "$NPM_PREFIX_NPM_CLI_JS" ] || [ -f "$NPM_WSL_PATH" ]; then + NPM_CLI_JS="$NPM_PREFIX_NPM_CLI_JS" +fi + +"$NODE_EXE" "$NPM_CLI_JS" "$@" diff --git a/business-css/frontend/vite.config.ts b/business-css/frontend/vite.config.ts index aa0c563..5df1cfe 100644 --- a/business-css/frontend/vite.config.ts +++ b/business-css/frontend/vite.config.ts @@ -27,7 +27,7 @@ export default ({ mode }: ConfigEnv): UserConfig => { // 线上API地址 // target: 'http://192.168.1.166:8090/', // 本地API地址 - target: 'http://192.168.1.38:8090', + target: 'http://localhost:8090', changeOrigin: true, rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') diff --git a/business-css/init_algorithm_models.sql b/business-css/init_algorithm_models.sql new file mode 100644 index 0000000..ec88356 --- /dev/null +++ b/business-css/init_algorithm_models.sql @@ -0,0 +1,12 @@ + +-- 插入 GPR 算法的默认模型记录 +-- 注意:模型路径仅为示例,请根据实际部署路径进行调整 +INSERT INTO algorithm_model (algorithm_model_id, algorithm_type, device_type, version_tag, model_path, is_current, created_at, updated_at) +VALUES +(UUID(), 'GPR', 'CylindricalTank', 'v1.0', 'models/gpr/cylindrical_tank_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'AnnularTank', 'v1.0', 'models/gpr/annular_tank_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'FlatTank', 'v1.0', 'models/gpr/flat_tank_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'TubeBundleTank', 'v1.0', 'models/gpr/tube_bundle_tank_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'ExtractionColumn', 'v1.0', 'models/gpr/extraction_column_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'FluidizedBed', 'v1.0', 'models/gpr/fluidized_bed_model.pkl', 1, NOW(), NOW()), +(UUID(), 'GPR', 'ACFTank', 'v1.0', 'models/gpr/acf_tank_model.pkl', 1, NOW(), NOW()); diff --git a/business-css/init_model_train_task.sql b/business-css/init_model_train_task.sql new file mode 100644 index 0000000..97c07e0 --- /dev/null +++ b/business-css/init_model_train_task.sql @@ -0,0 +1,18 @@ + +CREATE TABLE IF NOT EXISTS `model_train_task` ( + `task_id` char(36) NOT NULL COMMENT '任务ID', + `task_name` varchar(100) DEFAULT NULL COMMENT '任务名称', + `algorithm_type` varchar(50) DEFAULT NULL COMMENT '算法类型', + `device_type` varchar(50) DEFAULT NULL COMMENT '设备类型', + `dataset_path` varchar(255) DEFAULT NULL COMMENT '数据集路径', + `train_params` json DEFAULT NULL COMMENT '训练参数', + `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态', + `metrics` json DEFAULT NULL COMMENT '训练指标', + `model_output_path` varchar(255) DEFAULT NULL COMMENT '临时模型路径', + `feature_map_snapshot` json DEFAULT NULL COMMENT '特征映射', + `metrics_image_path` varchar(255) DEFAULT NULL COMMENT '指标图路径', + `error_log` text DEFAULT NULL COMMENT '错误日志', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`task_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/business-css/logs/business-css.log b/business-css/logs/business-css.log new file mode 100644 index 0000000..d22eb4f --- /dev/null +++ b/business-css/logs/business-css.log @@ -0,0 +1,52 @@ +2026-03-13T09:16:03.734+08:00 INFO 3672 --- [business-css] [main] c.y.b.css.CriticalScenarioApplication : Starting CriticalScenarioApplication using Java 17.0.17 with PID 3672 (E:\projectJava\JavaProjectRepo\business-css\target\classes started by Admin in E:\projectJava\JavaProjectRepo\business-css) +2026-03-13T09:16:03.736+08:00 INFO 3672 --- [business-css] [main] c.y.b.css.CriticalScenarioApplication : The following 2 profiles are active: "framework", "business" +2026-03-13T09:16:05.487+08:00 INFO 3672 --- [business-css] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8090 (http) +2026-03-13T09:16:05.497+08:00 INFO 3672 --- [business-css] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-03-13T09:16:05.498+08:00 INFO 3672 --- [business-css] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.24] +2026-03-13T09:16:05.602+08:00 INFO 3672 --- [business-css] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-03-13T09:16:05.603+08:00 INFO 3672 --- [business-css] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1830 ms +2026-03-13T09:16:05.788+08:00 WARN 3672 --- [business-css] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'algorithmController' defined in file [E:\projectJava\JavaProjectRepo\business-css\target\classes\com\yfd\business\css\controller\AlgorithmController.class]: Post-processing of merged bean definition failed +2026-03-13T09:16:05.790+08:00 INFO 3672 --- [business-css] [main] o.apache.catalina.core.StandardService : Stopping service [Tomcat] +2026-03-13T09:16:05.911+08:00 INFO 3672 --- [business-css] [main] .s.b.a.l.ConditionEvaluationReportLogger : + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2026-03-13T09:16:05.931+08:00 ERROR 3672 --- [business-css] [main] o.s.boot.SpringApplication : Application run failed + +org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'algorithmController' defined in file [E:\projectJava\JavaProjectRepo\business-css\target\classes\com\yfd\business\css\controller\AlgorithmController.class]: Post-processing of merged bean definition failed + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:577) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962) ~[spring-context-6.1.8.jar:6.1.8] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.8.jar:6.1.8] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.0.jar:3.3.0] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.0.jar:3.3.0] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.0.jar:3.3.0] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.0.jar:3.3.0] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.0.jar:3.3.0] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.0.jar:3.3.0] + at com.yfd.business.css.CriticalScenarioApplication.main(CriticalScenarioApplication.java:31) ~[classes/:na] +Caused by: java.lang.IllegalStateException: Failed to introspect Class [com.yfd.business.css.controller.AlgorithmController] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@6d06d69c] + at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:757) ~[spring-core-6.1.8.jar:6.1.8] + at org.springframework.util.ReflectionUtils.doWithLocalFields(ReflectionUtils.java:689) ~[spring-core-6.1.8.jar:6.1.8] + at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.buildResourceMetadata(CommonAnnotationBeanPostProcessor.java:431) ~[spring-context-6.1.8.jar:6.1.8] + at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.findResourceMetadata(CommonAnnotationBeanPostProcessor.java:412) ~[spring-context-6.1.8.jar:6.1.8] + at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessMergedBeanDefinition(CommonAnnotationBeanPostProcessor.java:312) ~[spring-context-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors(AbstractAutowireCapableBeanFactory.java:1085) ~[spring-beans-6.1.8.jar:6.1.8] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:574) ~[spring-beans-6.1.8.jar:6.1.8] + ... 15 common frames omitted +Caused by: java.lang.NoClassDefFoundError: com/yfd/platform/system/service/IUserService + at java.base/java.lang.Class.getDeclaredFields0(Native Method) ~[na:na] + at java.base/java.lang.Class.privateGetDeclaredFields(Class.java:3299) ~[na:na] + at java.base/java.lang.Class.getDeclaredFields(Class.java:2373) ~[na:na] + at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:752) ~[spring-core-6.1.8.jar:6.1.8] + ... 21 common frames omitted +Caused by: java.lang.ClassNotFoundException: com.yfd.platform.system.service.IUserService + at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na] + at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na] + at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525) ~[na:na] + ... 25 common frames omitted + diff --git a/business-css/pom.xml b/business-css/pom.xml index fdadb3a..6c8f5ed 100644 --- a/business-css/pom.xml +++ b/business-css/pom.xml @@ -63,7 +63,6 @@ com.yfd platform 1.0 - plain @@ -92,6 +91,7 @@ + org.springframework.boot spring-boot-maven-plugin 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 7f4188f..bd4197c 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 @@ -24,6 +24,10 @@ 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; +import org.springframework.web.multipart.MultipartFile; +import org.apache.poi.ss.usermodel.*; +import java.io.InputStream; +import java.util.Iterator; @RestController @RequestMapping("/events") @@ -306,6 +310,74 @@ public class EventController { } } + /** + * 上传 Excel 文件并解析出 time 和 value 列表 + * @param file Excel 文件 + * @return 包含 time 和 value 的列表 + */ + @PostMapping("/upload-excel") + public ResponseEntity> uploadExcel(@RequestParam("file") MultipartFile file) { + if (file.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "文件不能为空")); + } + + List> dataList = new ArrayList<>(); + try (InputStream is = file.getInputStream(); + Workbook workbook = WorkbookFactory.create(is)) { + + Sheet sheet = workbook.getSheetAt(0); // 读取第一个工作表 + Iterator rowIterator = sheet.iterator(); + + // 假设第一行是表头 + if (rowIterator.hasNext()) { + rowIterator.next(); // 跳过表头 + } + + while (rowIterator.hasNext()) { + Row row = rowIterator.next(); + + // 读取第一列为 time,第二列为 value (根据实际情况调整) + Cell timeCell = row.getCell(0); + Cell valueCell = row.getCell(1); + + if (timeCell != null && valueCell != null) { + double time = getCellValueAsDouble(timeCell); + double value = getCellValueAsDouble(valueCell); + + Map point = new HashMap<>(); + point.put("t", formatNumber(time)); + point.put("value", formatNumber(value)); + dataList.add(point); + } + } + + return ResponseEntity.ok(Map.of("code", 0, "msg", "解析成功", "data", dataList)); + + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "Excel解析失败: " + e.getMessage())); + } + } + + private Object formatNumber(double num) { + if (num == (long) num) { + return (long) num; // 如果是整数,返回 long 类型 + } + return num; // 否则保留小数 + } + + private double getCellValueAsDouble(Cell cell) { + if (cell.getCellType() == CellType.NUMERIC) { + return cell.getNumericCellValue(); + } else if (cell.getCellType() == CellType.STRING) { + try { + return Double.parseDouble(cell.getStringCellValue()); + } catch (NumberFormatException e) { + return 0.0; + } + } + return 0.0; + } + private String currentUsername() { try { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 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 25f3b14..98b27a9 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 @@ -105,7 +105,7 @@ public class ProjectController { * 4.1 导出所有项目(Excel) * 输入参数:无 * 导出描述:返回所有项目的 Excel 附件 `projects.xlsx` - * 导出列:project_id, code, name, description, created_at, updated_at,modifier + * 导出列:project_id, code, name, description, topology,created_at, updated_at,modifier * @return 附件响应,文件名为 projects.xlsx */ @GetMapping("/exportAllExports") @@ -122,7 +122,7 @@ public class ProjectController { * 5.1 导出项目工程(Excel 多 Sheet) * 输入参数:路径参数项目ID * 导出描述:返回 Excel 附件 `project_{id}.xlsx`,包含以下 Sheet: - * - projects(项目)- project_id, code, name, description, created_at, updated_at + * - projects(项目)- project_id, code, name, description, topology,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 diff --git a/business-css/src/main/java/com/yfd/business/css/model/SimContext.java b/business-css/src/main/java/com/yfd/business/css/model/SimContext.java index b11c884..ad1c848 100644 --- a/business-css/src/main/java/com/yfd/business/css/model/SimContext.java +++ b/business-css/src/main/java/com/yfd/business/css/model/SimContext.java @@ -8,6 +8,25 @@ public class SimContext { public void setValue(SimPropertyKey key, double value) { currentValues.put(key, value); } public double getValue(SimPropertyKey key) { return currentValues.getOrDefault(key, 0.0); } + + // 获取指定步的属性值(支持历史回溯) + public double getValueAtStep(SimPropertyKey key, int step) { + if (step < 0) step = 0; // 边界保护 + // 如果请求的是当前正在计算的步(尚未snapshot),则返回当前值? + // 通常在计算 step 时,snapshot 中存储的是 step-1 及之前的。 + // 如果 step 参数等于当前 SimService 中的 currentStep,实际上 timeline 中可能还没存。 + // 但这里的语义是获取“历史第 step 步”的值。 + + Map snap = timeline.get(step); + if (snap != null) { + return snap.getOrDefault(key, 0.0); + } + // 如果请求的步数不存在(比如未来步,或者当前步还未存入),降级为返回当前值,或者初始值 + // 考虑到 step=0 是初始状态,如果 step > 0 但没找到,可能是 bug 或逻辑问题。 + // 这里简单处理:如果找不到历史,就返回当前值(假设变化不剧烈或作为 fallback) + return getValue(key); + } + public void ensureProperty(SimPropertyKey key) { currentValues.putIfAbsent(key, 0.0); } public void snapshot(int step) { timeline.put(step, new HashMap<>(currentValues)); } diff --git a/business-css/src/main/java/com/yfd/business/css/service/SimService.java b/business-css/src/main/java/com/yfd/business/css/service/SimService.java index 0a25c6c..f93ddee 100644 --- a/business-css/src/main/java/com/yfd/business/css/service/SimService.java +++ b/business-css/src/main/java/com/yfd/business/css/service/SimService.java @@ -97,22 +97,23 @@ public class SimService { double sum = node.getBias(); for (SimInfluenceSource s : node.getSources()) { // 处理延迟: t - delay - // SimContext 目前只存储当前值。如果需要历史值,SimContext 需要支持根据 step 获取 snapshot。 - // 假设 SimContext.getValue(key) 返回当前值。 - // 如果有 delay,我们需要访问历史 snapshot。 + int delayStep = s.getDelay(); + int targetStep = currentStep - delayStep; - // 暂时假设无 delay 或 delay=0,读取当前值 - // 如果 SimInfluenceSource 有 delay 属性,需要 SimContext 支持历史回溯 - // 修改 SimContext 增加 getValue(key, step) ? - // 目前 SimContext 实现未知,假设只能读当前。 - // 如果需要支持 delay,SimContext 需要保留 history。 + // 如果 targetStep < 0,取初始值 (step=0) + if (targetStep < 0) targetStep = 0; - // 简单起见,这里先读当前值。完善版本需要 SimContext 提供 getHistoryValue(key, step - delay) + // 如果没有延迟 (delay=0),则读取当前正在计算的最新值 (currentValues) + // 这样可以支持同一帧内的级联更新(前提是计算顺序正确,或允许多次迭代) + // 但如果 delay > 0,则必须从 timeline 中读取历史快照 - // 修正:SimContext 应该能获取历史。我们先看 SimContext 定义。 - // 假设 SimContext 内部有 snapshots。 + double sourceVal; + if (delayStep == 0) { + sourceVal = ctx.getValue(s.getSource()); + } else { + sourceVal = ctx.getValueAtStep(s.getSource(), targetStep); + } - double sourceVal = ctx.getValue(s.getSource()); // 暂读当前 sum += sourceVal * s.getCoeff(); } updates.add(Map.entry(node.getTarget(), sum)); 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 14d88e6..c1c9b53 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 @@ -371,9 +371,7 @@ public class ModelTrainServiceImpl extends ServiceImpl staticProps) { + try { + String sizeJson = device.getSize(); + if (sizeJson == null || sizeJson.isBlank()) return; + + JsonNode sizeNode = objectMapper.readTree(sizeJson); + String type = device.getType(); // 假设 Device 有 getType() 方法,或从外部传入 + + if (type == null) { + // Fallback: 尝试通用解析 (现有逻辑) + parseCommonSize(sizeNode, staticProps); + return; + } + + switch (type) { + case "FlatTank": + if (sizeNode.has("width")) staticProps.put("width", sizeNode.get("width").asDouble()); + if (sizeNode.has("length")) staticProps.put("length", sizeNode.get("length").asDouble()); + if (sizeNode.has("height")) staticProps.put("height", sizeNode.get("height").asDouble()); + break; + + case "CylindricalTank": + case "AnnularTank": + case "TubeBundleTank": + parseCommonSize(sizeNode, staticProps); + break; + + case "ExtractionColumn": + // 优先提取 tray_section (塔身) + if (sizeNode.has("tray_section")) { + parseCommonSize(sizeNode.get("tray_section"), staticProps); + } + // 可选:将其他段的尺寸作为特殊属性注入,例如 lower_expanded_height + break; + + case "FluidizedBed": + // 优先提取 reaction_section (反应段) + if (sizeNode.has("reaction_section")) { + parseCommonSize(sizeNode.get("reaction_section"), staticProps); + } + break; + + case "ACFTank": + // 优先提取 annular_cylinder (圆柱段) + if (sizeNode.has("annular_cylinder")) { + parseCommonSize(sizeNode.get("annular_cylinder"), staticProps); + } + break; + + default: + parseCommonSize(sizeNode, staticProps); + } + } catch (Exception e) { + System.err.println("解析Device.size失败:" + e.getMessage()); + } + } + + private void parseCommonSize(JsonNode node, Map staticProps) { + if (node.has("outer_diameter")) { + staticProps.put("diameter", node.get("outer_diameter").asDouble()); + } else if (node.has("diameter")) { + staticProps.put("diameter", node.get("diameter").asDouble()); + } + + if (node.has("height")) { + staticProps.put("height", node.get("height").asDouble()); + } + } +``` + +## 3. 注意事项 + +1. **复合结构处理**:对于 `ExtractionColumn` 等分段设备,目前的建议是提取“主体段”的尺寸作为该设备的代表尺寸(diameter/height)。如果仿真计算需要每一段的精确尺寸,建议将 `staticProps` 的结构进行展平(例如 `tray_section_diameter`, `lower_expanded_height`)或嵌套存储(需修改 SimUnit 和 SimResultConverter 以支持嵌套 Map)。 +2. **单位统一**:确保 JSON 中的数值单位与仿真模型要求的单位一致(通常为 cm 或 mm)。 +3. **鲁棒性**:解析时需判空,避免因 JSON 结构缺失导致异常。 diff --git a/business-css/几个疑问.md b/business-css/几个疑问.md new file mode 100644 index 0000000..6f91d9f --- /dev/null +++ b/business-css/几个疑问.md @@ -0,0 +1,3 @@ +1.模拟数据初始化,有设备表、物料表的静态值,有始发事件表的突发值,还有影响关系设定的计算公式。该如何顺序及优先级呢? +2.请给出合理分析。 +3.再分析simController及相关接口,写入 情景模拟分析结果v2.md 文件 \ No newline at end of file diff --git a/business-css/情景模拟分析.md b/business-css/情景模拟分析.md new file mode 100644 index 0000000..7ee14d9 --- /dev/null +++ b/business-css/情景模拟分析.md @@ -0,0 +1,153 @@ +1.当前项目的项目的重点在initSimulation初始化模拟数据,以及runSimulation对模拟数据调用推理接口进行推理并把结束入库。这是一个初始版本,但是可能考虑不全面。尤其初始化模拟数据。 +2.初始化数据,需要考虑一下几个方面: + 1.1 项目设备属性、物料属性的静态值 + 1.2 模拟场景中始发事件设置的初始值(对应场景,event表attr_changeszi段定义了始发事件描述: + {"unit": "cm", "label": "条件3", "device": "圆柱槽02", "target": {"entityId": "94f20d37-89df-4fc1-b70c-06021fb10bc0", "property": "u_concentration", "entityType": "material"}, "material": "u_concentration", "segments": [{"end": "10", "start": "1", "timeline": [{"t": "1", "value": "20"}, {"t": "2", "value": "40"}, {"t": "3", "value": "60"}, {"t": "4", "value": "80"}, {"t": "5", "value": "100"}, {"t": "6", "value": "120"}, {"t": "7", "value": "150"}, {"t": "8", "value": "200"}, {"t": "9", "value": "250"}, {"t": "10", "value": "300"}], "segmentId": "分段-1"}]}。) + 1.3 项目建模时,设备重要信息(包括deviceId,type,有影响关系的设备属性"properties"),跟设备1对1的物料重要信息(包括id,有影响关系的物料属性)需要重点关注。每个属性的影响关系,可能有多个source属性。 + { + "projectId": "proj-0001-uuid", + "name": "测试项目", + "devices": [ + { + "deviceId": "dev-002-uuid", + "name": "设备01", + "type": "CylindricalTank", + "properties": { + "height": { + "type": "influence", + "unit": "cm", + "base": 0, + "bias": 0, + "sources": [ + { + "entityType": "device", + "entityId": "dev-002-uuid", + "property": "diameter", + "coefficient": 1, + "delay": { + "enabled": true, + "time": 0, + "unit": "s" + } + } + ], + "expression": "dev-002-uuid.diameter*1" + } + }, + "material": { + "materialId": "material-1", + "name": "1A", + "properties": { + + } + }, + "ui": { + "position": { + "x": 200, + "y": 500 + }, + "size": { + "width": 140, + "height": 90 + } + } + }, + { + "deviceId": "dev-003-uuid", + "name": "设备02", + "type": "AnnularTank", + "properties": { + "height": { + "type": "influence", + "unit": "cm", + "base": 0, + "bias": 0, + "sources": [ + { + "entityType": "device", + "entityId": "dev-003-uuid", + "property": "diameter", + "coefficient": 1, + "delay": { + "enabled": true, + "time": 0, + "unit": "s" + } + } + ], + "expression": "dev-003-uuid.diameter*1+bias" + } + }, + "material": { + "materialId": "material-2", + "name": "2B", + "properties": { + + } + }, + "ui": { + "position": { + "x": 500, + "y": 500 + }, + "size": { + "width": 140, + "height": 90 + } + } + } + ], + "pipelines": [ + { + "pipelineId": "E_D01_D02", + "from": "dev-002-uuid", + "to": "dev-003-uuid", + "line_type": "single_arrow", + "path": [ + { + "x": 340, + "y": 545 + }, + { + "x": 500, + "y": 545 + } + ] + } + ], + "systemboundaries": [ + { + "boundary_id": "boundary_001", + "name": "系统边界1", + "type": "rectangle", + "geometry": { + "x": 100, + "y": 400, + "width": 900, + "height": 300 + }, + "line_style": { + "type": "solid", + "width": 3, + "color": "#2563EB" + }, + "description": "系统核心功能区域" + } + ], + "globalDisplay": { + "device": { + "showProperties": [ + "height", + "diameter" + ] + }, + "material": { + "showProperties": [ + "u_concentration" + ] + } + } + } +3.重点分析一下当前版本的initSimulation,有哪些问题。再结合第2点提供的内容,给出建议,写在情景模拟分析结果.md文件中。 +4.有个半成品的迭代版本,SimController,还不够完全,也可以参考一下,一起分析一下。 +5.再分析一下runSimulation接口,有哪些问题。有新的需求变更,要求场景表,针对项目设备设置算法类型。整体分析一下。写在情景模拟分析结果.md文件 diff --git a/business-css/情景模拟分析结果.md b/business-css/情景模拟分析结果.md new file mode 100644 index 0000000..07f7a22 --- /dev/null +++ b/business-css/情景模拟分析结果.md @@ -0,0 +1,76 @@ +# 情景模拟分析结果 + +## 1. `initSimulation` 现状分析与建议 + +### 1.1 现状分析 +`initSimulation` 方法是模拟系统的核心入口,负责将静态的项目拓扑结构和动态的情景事件转化为可用于推理的时序数据帧(Frames)。目前的实现主要包含以下几个关键部分: + +1. **静态属性加载**: + * **拓扑解析**:方法首先加载 `Project` 实体,并解析其 `topology` JSON 字段。这包含了设备(Devices)和物料(Material)的基础定义。 + * **数据库补全**:通过 `buildMaterialStaticFromDb` 方法(L1116),系统会根据拓扑中的 `materialId` 查询 `Material` 数据库表,补全如 `u_concentration`, `pu_isotope`, `pu_concentration` 等关键理化属性。 + * **尺寸注入**:通过 `injectDeviceSize` 方法(L776),解析设备 `ui.size` 或 `properties` 中的尺寸信息(如 `diameter`, `height`)并注入到模拟状态中。 + * **符合度**:这部分逻辑基本覆盖了需求 1.1 中关于“项目设备属性、物料属性的静态值”的处理。 + +2. **始发事件处理**: + * **事件解析**:方法查询指定 `scenarioId` 下的所有 `Event` 记录。 + * **插值逻辑**:通过 `buildValueProviders` 方法(L929),解析 `attr_changes` JSON 字段。代码明确处理了 `step-set`(阶跃变化)和 `ramp`(线性斜坡)两种插值模式。 + * **符合度**:这部分逻辑能够处理需求 1.2 中描述的复杂事件定义(如“条件3”中的分段变化)。 + +3. **影响关系计算**: + * **传播模型**:在 `ProjectServiceImpl` 的主循环(L695-717)中,实现了基于拓扑定义的影响传播。 + * **计算公式**:采用了线性加权模型:`TargetValue = Bias + Σ(Coefficient * SourceValue(t - Delay))`。 + * **符合度**:这部分逻辑覆盖了需求 1.3 中关于设备间、设备与物料间属性影响关系的定义。 + +### 1.2 存在的问题与建议 + +1. **数据校验缺失**: + * **问题**:当前代码在解析 `topology` 后,缺乏对关键物理属性(如 `diameter`, `height`)的强校验。如果 JSON 中缺失这些字段,后续计算可能会抛出空指针异常或产生错误的 `0` 值。 + * **建议**:在 `initSimulation` 的第一步增加 `validateTopology` 逻辑,确保所有参与物理计算的属性都已正确定义且非空。 + +2. **初始状态一致性**: + * **问题**:当 `t=0` 时刻,静态拓扑中定义的默认值与 `Event` 中定义的初始值可能存在冲突。目前的逻辑是先加载静态值,再应用事件变化。 + * **建议**:明确并文档化优先级规则:**事件设定值(t=0) > 静态默认值**。在代码中确保事件应用逻辑在静态初始化之后执行。 + +3. **逻辑耦合度高**: + * **问题**:`ProjectServiceImpl` 承担了过多的职责(CRUD、拓扑解析、数学计算、推理调度)。`initSimulation` 方法过于庞大(数百行),维护困难。 + * **建议**:将模拟初始化的核心逻辑(特别是时间步推进和状态计算)抽取为独立的 `SimulationEngine` 或 `SimDataBuilder` 类。 + +## 2. `SimController` 分析 + +### 2.1 现状 +* `SimController` 目前处于**全注释状态**,完全未启用。 +* 当前的模拟相关接口(`/simulation/init`, `/simulation/run`)直接寄宿在 `ProjectController` 中,导致 `ProjectController` 臃肿。 + +### 2.2 建议 +1. **启用 SimController**:遵循单一职责原则(SRP),应激活 `SimController` 作为模拟相关业务的统一入口。 +2. **服务拆分**: + * 创建 `SimService` 接口及其实现类 `SimServiceImpl`。 + * 将 `ProjectServiceImpl` 中与模拟强相关的逻辑(`initSimulation`, `runSimulation`, `loadSimInfluenceNodes` 等)迁移至 `SimService`。 + * `ProjectService` 应只保留项目元数据的 CRUD 操作。 +3. **重构半成品**:参考 `SimController` 的原始设计,完善其输入参数校验和异常处理,使其符合当前的 RESTful API 规范。 + +## 3. `runSimulation` 分析与新需求建议 + +### 3.1 现状分析 +* **算法绑定限制**:目前代码(L1305)从 `Scenario` 对象中获取 `algorithmType`。这意味着**同一个情景下的所有设备必须使用同一种算法**。这无法满足“针对不同设备使用不同算法”的精细化需求。 +* **推理调用简单**:在调用 `deviceInferService.processDeviceInference` 时,仅传递了按 `DeviceType` 分组的数据,**未透传算法类型**。这意味着推理服务可能在使用默认算法,或者需要自行再次查询配置。 + +### 3.2 新需求适配建议:设备级算法配置 + +针对“要求场景表,针对项目设备设置算法类型”的需求变更,建议方案如下: + +1. **数据模型变更**: + * **方案 A(推荐 - 拓扑集成)**:在 `Project` 的 `topology` JSON 结构中,为每个 `device` 节点增加 `algorithmType` 字段。这样在建模阶段就确定了设备的算法特性。 + * **方案 B(场景覆盖)**:在 `Scenario` 表中增加一个 JSON 字段(如 `device_configs`),存储 `Map` 的映射关系,允许在不同情景下为同一设备指定不同算法。 + +2. **业务逻辑调整 (`runSimulation`)**: + * **分组策略升级**:目前的 `groupedDevices` 是 `Map>`。需要调整为更细粒度的分组,键应包含算法信息。 + * 新结构示例:`Map>`。 + * **参数透传**:在构建 `DeviceStepInfo` 时,必须将设备的 `algorithmType` 写入该对象。 + +3. **接口升级**: + * 修改 `deviceInferService.processDeviceInference` 接口签名,使其能够识别不同分组对应的算法类型。 + * **路由逻辑**:在 `DeviceInferService` 内部实现路由策略,根据传入的 `algorithmType`(如 `MCNP`, `PointNet`, `Analytical`)调用不同的底层推理模型或微服务。 + +### 总结 +当前系统已具备基本的模拟框架,但为了应对更复杂的业务需求(如设备级算法配置)和提升系统可维护性,必须进行**服务拆分(SimService)**和**数据模型升级(AlgorithmType下沉)**。 diff --git a/business-css/情景模拟分析结果v2.md b/business-css/情景模拟分析结果v2.md new file mode 100644 index 0000000..6d50ea1 --- /dev/null +++ b/business-css/情景模拟分析结果v2.md @@ -0,0 +1,65 @@ +# 情景模拟分析结果 v2 + +## 1. 模拟数据初始化的优先级分析 + +针对“设备表/物料表静态值”、“始发事件表突发值”以及“影响关系计算公式”这三者在模拟初始化中的执行顺序和优先级,我们对比了当前系统中的两套实现逻辑: + +### 1.1 核心逻辑对比 + +| 特性 | ProjectServiceImpl (当前运行逻辑) | SimService (SimController 逻辑) | +| :--- | :--- | :--- | +| **逻辑流** | **静态加载 → 影响计算 → 事件覆盖** | **事件预设 → 影响计算** | +| **优先级** | **事件最高** (Final Override) | **影响公式最高** (Calculation Override) | +| **输入读取** | 计算公式优先读取事件值,无事件时读静态值 | 计算公式读取上下文当前值(即事件预设值) | +| **典型场景** | 强制干预(如:强制设定液位为 100,忽略进出水计算) | 初始条件设定(如:设定初始液位,随后随公式变化) | + +### 1.2 详细执行顺序建议 + +基于业务场景的合理性(通常事件代表外部强制变更或设定),建议采用以下**混合优化的优先级策略**,并在重构时予以实施: + +1. **Step 1: 加载基础静态值 (Static)** + * 作为所有属性的默认“底色”。 + * *来源*:设备表、物料表。 +2. **Step 2: 应用 T=0 时刻的事件值 (Event Initial)** + * 将所有在 T=0 时刻生效的事件值应用到状态中。 + * *目的*:设定系统的初始状态(Initial Conditions),供 T=1 的计算使用。 +3. **Step 3: 执行影响关系计算 (Influence)** + * 基于当前状态(可能是静态值,也可能是被 Step 2 修改过的值)执行公式计算。 + * *注意*:如果是 T=0 时刻,通常不执行动态计算,或仅执行静态平衡计算。 +4. **Step 4: 强制事件覆盖 (Event Override - Optional)** + * 对于那些标记为“强制锁定”类型的事件,在计算结束后再次覆盖结果。 + * *目的*:模拟传感器故障、手动超控等场景。 + +--- + +## 2. SimController 及相关接口深入分析 + +`SimController` 及其配套的 `SimService` 代表了系统的演进方向,尽管目前未启用,但其设计模式优于当前的 `ProjectServiceImpl`。 + +### 2.1 架构设计意图 +* **外观模式 (Facade)**:`SimController` 充当了数据组装者的角色,负责从 `Project`、`Event` 等多个异构数据源获取数据,并将它们转换为统一的仿真领域模型(`SimContext`, `SimUnit`)。 +* **解耦计算与存储**:`SimService` 被设计为一个纯粹的计算引擎,它不关心数据来自数据库还是 JSON,只关心 `SimContext` 中的 KV 对。这与 `ProjectServiceImpl` 中深度耦合数据库查询的逻辑形成鲜明对比。 + +### 2.2 接口与模型问题 +虽然架构较好,但 `SimController` 的实现存在以下具体问题,导致无法直接使用: + +1. **优先级逻辑倒置**: + * 如前所述,`SimService` 先应用事件,后执行计算。这会导致:如果一个属性既有事件设定(如 `u_concentration=20`),又有上游影响(如 `u_concentration = source * 0.5`),最终结果会被计算值覆盖,导致事件设定失效。 + * **修正建议**:需要在计算循环结束后,再次检查是否有“持续生效”的事件,并重新应用覆盖。 + +2. **数据模型缺失**: + * `SimService` 目前主要面向通用的 KV 计算,缺乏对“设备-物料”层级关系的显式支持。 + * **修正建议**:增强 `SimContext`,使其能感知属性所属的实体类型(Device vs Material),以便正确处理如 `injectDeviceSize` 这类特定逻辑。 + +3. **推理接口对接**: + * 代码中包含 `InferenceConverter`,意图对接 AI 推理。但目前的逻辑是“先计算物理公式,再打包给 AI”。 + * **优化建议**:明确物理计算与 AI 推理的边界。是物理计算的结果作为 AI 的输入?还是 AI 的推理结果修正物理计算?当前逻辑倾向于前者。 + +### 2.3 结论与行动路线 + +**结论**:`SimController` 是正确的重构方向,但其核心计算逻辑(优先级处理)需要修正以符合业务直觉。 + +**建议行动**: +1. **废弃 `ProjectServiceImpl` 中的模拟逻辑**,将其迁移至 `SimService`。 +2. **修正 `SimService` 的计算流**:采用 `Static -> Event(Init) -> Influence -> Event(Override)` 的标准管线。 +3. **标准化接口**:将 `SimController` 的 `/simulation/run` 确立为唯一入口,统一管理同步/异步调用。 diff --git a/business-css/情景模拟分析结果v3.md b/business-css/情景模拟分析结果v3.md new file mode 100644 index 0000000..9e812ef --- /dev/null +++ b/business-css/情景模拟分析结果v3.md @@ -0,0 +1,77 @@ +# 情景模拟分析结果 v3(针对“几个疑问”) + +对应问题来源:[几个疑问.md](file:///e:/projectJava/JavaProjectRepo/business-css/%E5%87%A0%E4%B8%AA%E7%96%91%E9%97%AE.md) + +## 1. 初始化顺序与优先级:静态值、事件值、影响计算如何排布? + +### 1.1 三类数据的“角色定义” +- **静态值(设备表/物料表/拓扑 static)**:系统的“默认底色/初值基线”。没有事件、没有影响计算时,也必须能给出一个自洽的初始状态。 +- **事件值(始发事件/attr_changes)**:系统的“外部输入”。既可能是一次性的设定(某一步将某属性改成某值),也可能代表持续的干预(在一段时间内强制维持某值)。 +- **影响计算(influence 公式)**:系统的“内部传播/派生规则”。它根据来源属性计算得到目标属性,是一个“生成值”的过程。 + +### 1.2 当前代码实现的顺序(实际行为) +在 `ProjectServiceImpl.initSimulation` 里,单帧生成顺序是: +1) 注入尺寸(Device.size) +2) 写入设备静态值 +3) 执行设备 influence 计算并写入 state +4) 写入物料静态值(DB + topology) +5) 执行物料 influence 计算并写入 state +6) **最后对 device/material 应用事件覆盖(overrideWithEvents)** + +这相当于:**Static → Influence → Event(最终覆盖)**。代码位置见:[ProjectServiceImpl.initSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L614-L767) 与 [overrideWithEvents/readValue](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L998-L1114)。 + +另外,`readValue()` 在计算 influence 时会“优先读事件、再读静态”,但最终仍会被 `overrideWithEvents()` 再覆盖一次,这意味着事件在该实现里拥有“最终裁决权”。见:[readValue](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L998-L1035)。 + +### 1.3 推荐的合理顺序(建议落地的“标准管线”) +建议将初始化/每步演进统一成一个明确的“管线”,并对事件类型做区分: + +**推荐管线:** +1) **Static 基线**:加载设备/物料静态值(含 DB 补全)作为默认状态。 +2) **Event(输入/初始条件)**:应用在当前时刻生效的事件(尤其是 t=0 或 step=1 的“初始条件事件”)。 +3) **Influence(派生计算)**:基于当前状态执行影响关系计算,得到派生值。 +4) **Event(强制覆盖/锁定)**:仅对“强制锁定类事件”再次覆盖(可选,但强烈建议引入),确保“外部强制干预”不会被公式计算反覆盖。 + +这样做的好处: +- 既能让事件作为“输入”影响传播(步骤 2 发生在计算前),又能让“强制事件”在输出阶段生效(步骤 4)。 +- 能解释并覆盖两类业务直觉: + - “我设了初始浓度,后续按公式变化”(步骤 2 + 3) + - “我强制把某槽位液位锁死,不管公式怎么算”(步骤 4) + +### 1.4 关键补充:时间轴/采样点也属于优先级的一部分 +当前 `initSimulation` 的帧生成时间点来自事件时间集合:`collectTimePoints(valueProviders)`,若没有事件时间点会直接返回“无法生成帧”。见:[collectTimePoints](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L1037-L1061) 与 [initSimulation timePoints 为空处理](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/impl/ProjectServiceImpl.java#L667-L675)。 + +建议: +- 当没有事件时,允许通过参数指定 `start/end/stepInterval`,生成基础时间网格(否则“纯静态模拟”永远跑不起来)。 +- 对于 ramp/linear 事件,当前只把 start/end 加入时间点(中间不采样),但 `readValue` 支持任意 t 的线性插值。若希望输出更“连续”的曲线,必须补齐采样点(例如每 1s/每步都采样)。 + +## 2. 合理性分析:为什么推荐上述顺序? + +### 2.1 从“因果关系”角度 +- 静态值是**先验事实**(设备尺寸、物料基础参数),应先进入状态。 +- 事件是**外部驱动/输入**,会改变系统边界条件,应在影响传播前进入状态,否则传播用的仍是旧值。 +- influence 是**系统内部响应**,应在输入就绪后计算。 +- 强制覆盖事件是**高优先级干预**,必须在计算后仍能维持,否则“强制”二字失去意义。 + +### 2.2 从“可维护性/可解释性”角度 +把事件分成两类最关键: +- **Event-Input(输入型)**:参与计算的输入(初值设定、控制变量变化)。 +- **Event-Override(强制型)**:输出阶段的锁定/强制(故障注入、人工接管)。 + +如果不区分,只能在“事件最高”与“公式最高”之间二选一,最终会在不同业务场景下反复打补丁。 + +## 3. SimController 及相关接口分析(并给出建议) + +### 3.1 现状:SimController 是“半成品/原型”,当前不可用 +[SimController.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/controller/SimController.java#L1-L43) 体现了一套更清晰的分层意图:`Controller -> Repo 装配 -> Builder -> SimService -> 推理输入`,但存在几处硬性问题: +- 依赖的 `ProjectRepository / EventRepository / InfluenceRepository / InferenceConverter` 在项目代码中找不到定义,无法编译/运行(当前仓库中仅此文件引用这些类型)。 +- `SimBuilder` 中对应的 `buildUnits/buildEvents/buildInfluenceNodes` 方法是注释掉的,占位未实现。见:[SimBuilder.java](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/build/SimBuilder.java#L1-L44)。 +- `SimService` 当前实现仅是 KV 级别的简化引擎,且采用 **事件先写、后计算覆盖** 的顺序(会导致“事件设定失效”)。见:[SimService.runSimulation](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/service/SimService.java#L9-L51)。 + +### 3.2 与当前线上接口的关系 +当前“初始化/运行模拟”入口实际在 `ProjectController`(/simulation/init、/simulation/run)并调用 `ProjectServiceImpl`。例如 `runSimulation`:见 [ProjectController.java:L289-L300](file:///e:/projectJava/JavaProjectRepo/business-css/src/main/java/com/yfd/business/css/controller/ProjectController.java#L289-L300)。 + +### 3.3 建议:保留 SimController 的分层方向,但必须补齐三件事 +1) **补齐依赖与 Builder**:明确 Repo 层要读什么(拓扑、事件、影响关系)并落地成可编译的类/接口;实现 `SimBuilder` 的构建方法或直接复用 `ProjectServiceImpl` 的解析器。 +2) **统一优先级管线**:让 `SimService` 采用“Static → Event(Input) → Influence → Event(Override)”(见第 1 章建议),避免出现“事件被公式反覆盖”。 +3) **统一对外 API**:建议最终以 `/sim/*` 作为唯一入口。 + diff --git a/business-css/新接口初始化模拟数据.txt b/business-css/新接口初始化模拟数据.txt new file mode 100644 index 0000000..447f46a --- /dev/null +++ b/business-css/新接口初始化模拟数据.txt @@ -0,0 +1,233 @@ +{ + "msg": "操作成功", + "code": "0", + "data": { + "frames": [ + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 20.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 10.0 + } + }, + "step": 0, + "time": 0 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 10.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 18.0 + } + }, + "step": 1, + "time": 1 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 20.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 9.0 + } + }, + "step": 2, + "time": 2 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 30.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 18.0 + } + }, + "step": 3, + "time": 3 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 40.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 27.0 + } + }, + "step": 4, + "time": 4 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 36.0 + } + }, + "step": 5, + "time": 5 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 6, + "time": 6 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 7, + "time": 7 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 8, + "time": 8 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 9, + "time": 9 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 10, + "time": 10 + } + ], + "generated": { + "snapshots": 11 + }, + "projectId": "dfd827e5a538e6ca79c1fddd9fb71638", + "scenarioId": "e733a8d841ab41b5a2a79be92875f3f2" + } +} \ No newline at end of file diff --git a/business-css/新旧接口模拟数据结果分析及优化建议.md b/business-css/新旧接口模拟数据结果分析及优化建议.md new file mode 100644 index 0000000..50ec40b --- /dev/null +++ b/business-css/新旧接口模拟数据结果分析及优化建议.md @@ -0,0 +1,77 @@ +# 新旧接口模拟数据结果分析及优化建议 + +通过对比旧接口 (`/projects/simulation/init`) 和新接口 (`/sim/run`) 的返回数据,发现以下主要差异: + +## 1. 数据结构与层级差异 + +| 差异点 | 旧接口 (`/projects/simulation/init`) | 新接口 (`/sim/run`) | 影响分析 | +| :--- | :--- | :--- | :--- | +| **顶层包装** | 包含 `msg`, `code`, `data` (标准 Result 结构) | 仅包含 `data` (需确认 Controller 是否使用了 `Result` 包装) | 前端解析逻辑需调整,或新接口需补齐统一响应封装。 | +| **元数据 (Metadata)** | `data` 层包含 `projectId`, `scenarioId`, `generated`, `issues` | **缺失**这些元数据 | 缺少调试信息和上下文关联,不利于前端展示“生成了多少帧/事件”。 | +| **帧列表 (Frames)** | `data.frames` | `data.frames` | 结构一致,核心数据都在。 | + +## 2. 帧内数据 (Frame Content) 差异 + +| 差异点 | 旧接口 | 新接口 | 影响分析 | +| :--- | :--- | :--- | :--- | +| **设备属性完整性** | 包含 `diameter`, `height`, `u_enrichment`, `u_concentration` 等**所有**属性 | **仅包含** `u_concentration` 和 `deviceType` | **严重**。新接口未输出静态属性(如尺寸)或未变化的属性,前端渲染(如 3D 模型)可能因缺少 `diameter/height` 而无法绘制设备。 | +| **时间步 (Time)** | 从 `time: 1` 开始 | 从 `time: 0` 开始 | 新接口更符合“初始状态 t=0”的逻辑,旧接口可能跳过了 t=0 或索引偏移。 | +| **数值精度** | `20.0` (保留一位小数) | `20.0` | 一致。 | + +## 3. 根本原因分析 + +1. **静态属性缺失**: + * 新接口的 `SimService` 或 `SimResultConverter` 在生成快照或转换结果时,可能仅输出了“变化量”或“当前计算量”,而漏掉了 `SimUnit` 中的 `staticProperties`。 + * 虽然 `SimService` 初始化时将静态值写入了 `SimContext`,但在 `SimResultConverter.toFrames` 中,可能未正确将所有属性(静态+动态)合并输出。 + +2. **元数据缺失**: + * 新接口的返回值直接是 `SimResultConverter.toFrames` 的结果(仅含 frames),未包装 `projectId` 等上下文信息。 + +## 4. 优化建议 + +### 4.1 补齐静态属性 (High Priority) +修改 `SimResultConverter` 或 `SimService`,确保输出的 `devices` 状态中包含: +* **静态属性**:`diameter`, `height`, `volume` 等(从 Topology 或 DB 加载的)。 +* **初始属性**:`u_enrichment` 等在 t=0 时设定的值。 + +**建议方案**:在 `SimResultConverter` 中,遍历 `SimContext` 的快照时,确保 key 覆盖了所有属性,或者在输出前将 `SimUnit.staticProperties` merge 到每一帧的 device state 中。 + +### 4.2 统一响应结构 (Medium Priority) +建议 `SimController` 返回 `Result>` 而非直接返回 `Map`,保持与旧接口一致的 `code/msg/data` 结构。 + +### 4.3 补充元数据 (Low Priority) +在返回的 `data` 对象中,补充 `projectId`, `scenarioId` 以及 `generated` 统计信息,方便前端展示。 + +### 4.4 时间轴对齐 +确认业务需要 t=0。通常 t=0 作为初始状态是必要的。 + +--- + +## 5. 预期修正后的 JSON 结构 + +```json +{ + "code": 0, + "msg": "success", + "data": { + "projectId": "...", + "scenarioId": "...", + "frames": [ + { + "step": 0, + "time": 0, + "devices": { + "device_id_1": { + "deviceType": "CylindricalTank", + "diameter": 20.0, <-- 补齐静态值 + "height": 20.0, <-- 补齐静态值 + "u_enrichment": 0.1, <-- 补齐初始值 + "u_concentration": 10.0 + } + } + } + // ... + ] + } +} +``` diff --git a/business-css/新版本仿真计算方法.md b/business-css/新版本仿真计算方法.md new file mode 100644 index 0000000..1ba4685 --- /dev/null +++ b/business-css/新版本仿真计算方法.md @@ -0,0 +1,84 @@ +# 新版本仿真计算方法 + +新版本仿真计算采用 **SimController** 统一入口,通过 **SimDataFacade** 加载数据,**SimBuilder** 构建模型,**SimService** 执行核心计算,最终由 **SimResultConverter** 转换结果。相较于旧版本(`ProjectController` + `ProjectServiceImpl` + `DeviceInferService`),新架构职责更清晰,扩展性更强。 + +## 1. 核心流程 + +1. **入口**: `SimController.run(req)` + * 接收 `projectId`, `scenarioId`, `steps` 参数。 +2. **数据加载**: `SimDataFacade.loadSimulationData` + * 加载 `Project` (拓扑), `Device` (设备列表), `Events` (场景事件)。 +3. **模型构建**: `SimBuilder.buildUnits` & `buildEvents` & `buildInfluenceNodes` + * **Units**: 将设备和物料转换为 `SimUnit`,注入静态属性(包括从 `Device.size` 解析的几何尺寸,从 `Material` 表加载的物理化学属性)。 + * **Events**: 将 `Event` 数据转换为 `SimEvent` 时间序列。 + * **Influence**: 解析拓扑中的 `influence` 节点,构建 `SimInfluenceNode` 依赖关系图。 +4. **核心计算**: `SimService.runSimulation` + * **初始化**: `t=0`,写入所有 `SimUnit` 的静态属性。 + * **时间步推进**: `t=1` to `steps` + 1. **Apply Events (Input)**: 应用普通设定值事件。 + 2. **Apply Influence**: 基于拓扑关系计算派生属性(如 `bias + coeff * source`)。 + 3. **Apply Events (Override)**: 应用强制覆盖事件(如故障注入)。 + 4. **Snapshot**: 保存当前步所有属性状态。 +5. **结果转换**: `SimResultConverter.toFrames` + * 将 `SimContext` 中的时间轴数据转换为前端所需的 Frame 结构。 + * **补全静态属性**: 确保每一帧都包含设备的不变属性(如 `diameter`, `height`)。 + * **封装元数据**: 添加 `projectId`, `scenarioId` 等信息,遵循统一响应格式。 + +## 2. 与旧版本对比 + +| 特性 | 旧版本 (`ProjectController` / `ProjectServiceImpl`) | 新版本 (`SimController` / `SimService`) | +| :--- | :--- | :--- | +| **架构模式** | 事务脚本模式,逻辑集中在 ServiceImpl | 领域模型模式,组件职责分离 (Builder, Engine, Facade) | +| **计算逻辑** | 混合了 JSON 解析、数据查询和硬编码公式 | 基于 `SimUnit`, `SimEvent`, `SimInfluenceNode` 的抽象模型计算 | +| **扩展性** | 难以扩展新设备类型或计算规则 | 易于扩展(只需修改 Builder 或 Engine 策略) | +| **数据完整性** | 容易遗漏属性(如 `size` 解析分散) | `SimBuilder` 集中处理属性注入,`ResultConverter` 保证输出完整 | +| **推理集成** | 强依赖 `DeviceInferService`,解析前端传入的 JSON | **按需集成**,直接基于仿真计算生成的 `SimContext` 数据进行推理 | + +## 3. 关键组件详解 + +### 3.1 SimBuilder (模型构建) +负责将异构的原始数据(JSON 拓扑、数据库实体)标准化为仿真计算单元。 +* **多态尺寸解析**: `injectDeviceSize` 支持 `FlatTank`, `ExtractionColumn` 等多种设备几何解析。 +* **全量物料属性**: `buildMaterialStaticFromDb` 加载所有物料理化性质。 + +### 3.2 SimService (计算引擎) +纯内存计算引擎,支持: +* **静态基线**: `t=0` 状态。 +* **动态演变**: `Event` 驱动的状态变化。 +* **级联影响**: 拓扑网络中的属性传播(支持延迟 `delay` 和系数 `coeff`)。 + +### 3.3 SimResultConverter (结果适配) +* **Frame 生成**: 将离散的 `SimContext` 转换为连续的动画帧。 +* **数据补全**: 自动将 `staticProperties` 注入每一帧,解决前端渲染缺失静态参数的问题。 + +## 4. 推理调用与结果持久化优化 (New) + +新版本针对推理接口调用和结果入表逻辑进行了深度优化,消除了对前端 JSON 格式的强依赖。 + +### 4.1 旧版本逻辑回顾 +* **流程**: `ProjectController` -> `ProjectServiceImpl` -> `DeviceDataParser` -> `DeviceInferService` -> `Python API` -> `ScenarioResult` 表。 +* **痛点**: 依赖前端传入的特定格式 JSON (`params`);`DeviceDataParser` 解析逻辑复杂且脆弱;推理逻辑与 Service 强耦合。 + +### 4.2 新版本优化方案 +1. **直接基于 SimContext 推理**: + * 仿真计算完成后,直接从 `SimContext` 提取每一帧的属性状态,不再需要 `DeviceDataParser` 解析 JSON 字符串。 + * `SimService` 提供 `getDeviceStateList(deviceId)` 方法,快速导出时序数据。 +2. **组件化推理服务**: + * 引入 `SimInferService`,负责封装 `SimContext` 为 `InferRequest` 格式。 + * 异步调用 Python 接口,提高系统响应速度。 +3. **批量持久化**: + * 推理结果 (`Keff`, `features`) 统一封装为 `ScenarioResult` 列表,使用 `MyBatis-Plus` 的 `saveBatch` 异步写入,避免频繁数据库操作。 + +### 4.3 核心伪代码示例 +```java +// 在 SimController 中调用 +SimContext ctx = simService.runSimulation(units, events, nodes, steps); +// 异步执行推理并入表 +simInferService.asyncInferAndSave(projectId, scenarioId, ctx, units); +``` + +## 5. 后续演进建议 + +1. **集成 Python 推理**: 目前 `SimService` 主要执行线性影响计算。如需复杂机理模型(如 Keff 计算),可在 `SimService` 中通过 `DeviceInferService` 调用 Python API。 +2. **性能优化**: 对于长时序、大规模拓扑,`SimContext` 可优化为基于数组的存储,而非 Map。 +3. **实时流式输出**: 支持 WebSocket 或 SSE,实现边计算边推送。 diff --git a/business-css/旧接口模拟数据.txt b/business-css/旧接口模拟数据.txt new file mode 100644 index 0000000..30e7776 --- /dev/null +++ b/business-css/旧接口模拟数据.txt @@ -0,0 +1,115 @@ +{ + "msg": "初始化完成", + "data": { + "frames": [ + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 10.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 9.0 + } + }, + "step": 0, + "time": 1 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 20.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 18.0 + } + }, + "step": 1, + "time": 2 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 30.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 27.0 + } + }, + "step": 2, + "time": 3 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 40.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 36.0 + } + }, + "step": 3, + "time": 4 + }, + { + "devices": { + "65a988a6-69a0-4ed2-ade6-926af0caf6ff": { + "deviceType": "CylindricalTank", + "diameter": 20.0, + "u_enrichment": 0.1, + "height": 20.0, + "u_concentration": 50.0 + }, + "9569038d-1da6-40de-88a2-7d970d29b011": { + "deviceType": "CylindricalTank", + "diameter": 300.0, + "u_enrichment": 0.2, + "height": 300.0, + "u_concentration": 45.0 + } + }, + "step": 4, + "time": 5 + } + ], + "generated": { + "snapshots": 10, + "events": 1 + }, + "projectId": "dfd827e5a538e6ca79c1fddd9fb71638", + "scenarioId": "e733a8d841ab41b5a2a79be92875f3f2", + "issues": [] + }, + "code": 0 +} \ No newline at end of file diff --git a/framework/src/main/java/com/yfd/platform/config/GlobalExceptionHandler.java b/framework/src/main/java/com/yfd/platform/config/GlobalExceptionHandler.java index d9c7d7a..95a8859 100644 --- a/framework/src/main/java/com/yfd/platform/config/GlobalExceptionHandler.java +++ b/framework/src/main/java/com/yfd/platform/config/GlobalExceptionHandler.java @@ -1,10 +1,15 @@ package com.yfd.platform.config; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * @author TangWei * @Date: 2023/3/27 18:07 @@ -21,4 +26,28 @@ public class GlobalExceptionHandler { return ResponseResult.error(e.getMessage()); } + private static final Pattern DUPLICATE_ENTRY_PATTERN = Pattern.compile("Duplicate entry '(.*?)' for key"); + + /** + * 处理唯一键冲突异常 + */ + @ResponseBody + @ExceptionHandler(value = {DuplicateKeyException.class, SQLIntegrityConstraintViolationException.class}) + public ResponseResult handleDuplicateKeyException(Exception e) { + log.error("DuplicateKeyException: {}", e.getMessage()); + String msg = e.getMessage(); + String resultMsg = "数据已存在,请检查重复录入"; + + // 尝试提取具体重复的字段信息 + if (msg != null) { + Matcher matcher = DUPLICATE_ENTRY_PATTERN.matcher(msg); + if (matcher.find()) { + String value = matcher.group(1); + resultMsg = "数据 '" + value + "' 已存在,不允许重复"; + } + } + + return ResponseResult.error(resultMsg); + } + }