diff --git a/src/main/java/com/iflytop/gd/app/controller/CraftsController.java b/src/main/java/com/iflytop/gd/app/controller/CraftsController.java index 10a38ab..3fd2976 100644 --- a/src/main/java/com/iflytop/gd/app/controller/CraftsController.java +++ b/src/main/java/com/iflytop/gd/app/controller/CraftsController.java @@ -1,13 +1,9 @@ package com.iflytop.gd.app.controller; -import com.iflytop.gd.app.model.dto.PauseCraftsDto; -import com.iflytop.gd.app.model.dto.ResumeCraftsDto; -import com.iflytop.gd.app.model.dto.StartCraftsDTO; -import com.iflytop.gd.app.model.dto.StopCraftsDto; +import com.iflytop.gd.app.model.dto.*; import com.iflytop.gd.app.model.entity.Crafts; import com.iflytop.gd.app.model.vo.CraftStatusVO; import com.iflytop.gd.app.service.CraftsService; -import com.iflytop.gd.app.model.bo.CraftsStep; import com.iflytop.gd.common.result.Result; import com.iflytop.gd.common.result.ResultCode; import io.swagger.v3.oas.annotations.Operation; @@ -22,7 +18,6 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Map; @Tag(name = "工艺管理") @RestController @@ -79,14 +74,19 @@ public class CraftsController { return Result.failed(); } + @Operation(summary = "配置加热区工艺") + @PostMapping("/set") + public Result setCrafts(@Valid @RequestBody SetCraftsDTO setCraftsDTO) { + craftsService.setCraft(setCraftsDTO.getCraftId(), setCraftsDTO.getHeatId()); + return Result.success(); + } + + @Operation(summary = "开始执行工艺") @PostMapping("/start") public Result startCrafts(@Valid @RequestBody StartCraftsDTO startCraftsDTO) { - boolean isSuccess = craftsService.startCrafts(startCraftsDTO.getCraftId(), startCraftsDTO.getHeatId()); - if (isSuccess) { - return Result.success(); - } - return Result.failed(); + craftsService.startCrafts(startCraftsDTO.getHeatId()); + return Result.success(); } @Operation(summary = "暂停执行工艺") @@ -106,11 +106,8 @@ public class CraftsController { @Operation(summary = "停止执行工艺") @PostMapping("/stop") public Result stopCrafts(@Valid @RequestBody StopCraftsDto stopCraftsDto) { - boolean isSuccess = craftsService.stopCrafts(stopCraftsDto.getHeatId()); - if (isSuccess) { - return Result.success(); - } - return Result.failed(); + craftsService.stopCrafts(stopCraftsDto.getHeatId()); + return Result.success(); } @Operation(summary = "获取某个加热区工艺状态") diff --git a/src/main/java/com/iflytop/gd/app/core/CraftsContext.java b/src/main/java/com/iflytop/gd/app/core/CraftsContext.java index 7d7421b..2698057 100644 --- a/src/main/java/com/iflytop/gd/app/core/CraftsContext.java +++ b/src/main/java/com/iflytop/gd/app/core/CraftsContext.java @@ -24,41 +24,55 @@ import java.util.Map; @Getter public class CraftsContext implements Runnable { private final String heatId; + private final Crafts craft; private final List craftsStepList; private final StateMachine sm; private final WebSocketService ws; private final CraftsStepService craftsStepService; private int currentIndex = 0; - public CraftsContext(String heatId, Crafts craft, StateMachineFactory factory, WebSocketService ws, CraftsStepService craftsStepService) { + /** + * 构造方法,初始化上下文并启动状态机至 READY + */ + public CraftsContext(String heatId, + Crafts craft, + StateMachineFactory factory, + WebSocketService ws, + CraftsStepService craftsStepService) { this.heatId = heatId; + this.craft = craft; this.craftsStepList = JSONUtil.parseArray(craft.getSteps()).toList(CraftsStep.class); this.ws = ws; this.craftsStepService = craftsStepService; this.sm = factory.getStateMachine(heatId); - Mono.from(sm.startReactively()).block(); - Message startMsg = MessageBuilder.withPayload(CraftEvents.START).build(); - Mono.from(sm.sendEvent(Mono.just(startMsg))).block(); sm.addStateListener(new StateMachineListenerAdapter<>() { @Override - public void stateChanged(State from, State to) { + public void stateEntered(State state) { Map dataMap = new HashMap<>(); dataMap.put("heatId", heatId); - dataMap.put("event", to.getId()); + dataMap.put("state", state.getId()); dataMap.put("index", currentIndex); ws.push(WebSocketMessageType.CRAFTS_STATE, dataMap); } }); + Mono.from(sm.startReactively()).block(); } + /** + * 执行工艺脚本,遍历所有步骤并根据状态机控制流程 + */ @Override public void run() { try { + Message startMsg = MessageBuilder.withPayload(CraftEvents.START).build(); + Mono.from(sm.sendEvent(Mono.just(startMsg))).block(); + for (; currentIndex < craftsStepList.size(); currentIndex++) { if (sm.getState().getId() == CraftStates.STOPPED) break; CraftsStep step = craftsStepList.get(currentIndex); boolean ok = executeStep(step); + if (!ok) { Message errMsg = MessageBuilder.withPayload(CraftEvents.ERROR_OCCUR).build(); Mono.from(sm.sendEvent(Mono.just(errMsg))).block(); @@ -67,7 +81,6 @@ public class CraftsContext implements Runnable { Message compMsg = MessageBuilder.withPayload(CraftEvents.STEP_COMPLETE).build(); Mono.from(sm.sendEvent(Mono.just(compMsg))).block(); - // 如果被 PAUSE,阻塞等待 synchronized (this) { while (sm.getState().getId() == CraftStates.PAUSED) { this.wait(); @@ -75,8 +88,7 @@ public class CraftsContext implements Runnable { } } if (sm.getState().getId() == CraftStates.RUNNING) { - Message finishMsg = MessageBuilder - .withPayload(CraftEvents.FINISH).build(); + Message finishMsg = MessageBuilder.withPayload(CraftEvents.FINISH).build(); Mono.from(sm.sendEvent(Mono.just(finishMsg))).block(); } } catch (InterruptedException e) { @@ -85,23 +97,32 @@ public class CraftsContext implements Runnable { } } + /** + * 执行单个步骤,并推送开始及结果 + * + * @param step 当前工艺步骤 + * @return 是否执行成功 + * @throws InterruptedException 执行中被中断 + */ private boolean executeStep(CraftsStep step) throws InterruptedException { - Map dataMap = new HashMap<>(); - dataMap.put("heatId", heatId); - dataMap.put("currentStep", step.getMethod()); - ws.push(WebSocketMessageType.CRAFTS_STEP, dataMap); - - Thread.sleep(3000); + Map startData = new HashMap<>(); + startData.put("heatId", heatId); + startData.put("currentStep", step.getMethod()); + ws.push(WebSocketMessageType.CRAFTS_STEP, startData); return craftsStepService.executeStep(heatId, step); } - // 暂停 + /** + * 暂停执行,触发 PAUSE 事件 + */ public void pause() { Message msg = MessageBuilder.withPayload(CraftEvents.PAUSE).build(); Mono.from(sm.sendEvent(Mono.just(msg))).block(); } - // 恢复 + /** + * 恢复执行,触发 RESUME 事件并唤醒线程 + */ public void resume() { Message msg = MessageBuilder.withPayload(CraftEvents.RESUME).build(); Mono.from(sm.sendEvent(Mono.just(msg))).block(); @@ -110,7 +131,9 @@ public class CraftsContext implements Runnable { } } - // 用户手动停止 + /** + * 停止执行,触发 STOP 事件并唤醒线程 + */ public void stop() { Message msg = MessageBuilder.withPayload(CraftEvents.STOP).build(); Mono.from(sm.sendEvent(Mono.just(msg))).block(); diff --git a/src/main/java/com/iflytop/gd/app/model/dto/SetCraftsDTO.java b/src/main/java/com/iflytop/gd/app/model/dto/SetCraftsDTO.java new file mode 100644 index 0000000..37ed33f --- /dev/null +++ b/src/main/java/com/iflytop/gd/app/model/dto/SetCraftsDTO.java @@ -0,0 +1,19 @@ +package com.iflytop.gd.app.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +@Schema(description = "配置工艺") +@Data +public class SetCraftsDTO { + @Positive(message = "工艺ID 必须是正数") + @NotNull + @Schema(description = "工艺id") + private Long craftId; + + @NotNull + @Schema(description = "加热区id") + private String heatId; +} diff --git a/src/main/java/com/iflytop/gd/app/model/dto/StartCraftsDTO.java b/src/main/java/com/iflytop/gd/app/model/dto/StartCraftsDTO.java index 7ce928c..cb9d280 100644 --- a/src/main/java/com/iflytop/gd/app/model/dto/StartCraftsDTO.java +++ b/src/main/java/com/iflytop/gd/app/model/dto/StartCraftsDTO.java @@ -1,19 +1,12 @@ package com.iflytop.gd.app.model.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; import lombok.Data; @Schema(description = "开始工艺") @Data public class StartCraftsDTO { - @Positive(message = "工艺ID 必须是正数") - @NotNull - @Schema(description = "工艺id") - private Long craftId; - @NotNull @Schema(description = "加热区id") private String heatId; diff --git a/src/main/java/com/iflytop/gd/app/model/vo/CraftStatusVO.java b/src/main/java/com/iflytop/gd/app/model/vo/CraftStatusVO.java index 605d8e0..2a0a6b2 100644 --- a/src/main/java/com/iflytop/gd/app/model/vo/CraftStatusVO.java +++ b/src/main/java/com/iflytop/gd/app/model/vo/CraftStatusVO.java @@ -16,6 +16,9 @@ public class CraftStatusVO { @Schema(description = "加热区 ID") private String heatId; + @Schema(description = "工艺 ID") + private Long craftsId; + @Schema(description = "当前状态") private CraftStates state; diff --git a/src/main/java/com/iflytop/gd/app/service/CraftsService.java b/src/main/java/com/iflytop/gd/app/service/CraftsService.java index eb0ec38..23571cb 100644 --- a/src/main/java/com/iflytop/gd/app/service/CraftsService.java +++ b/src/main/java/com/iflytop/gd/app/service/CraftsService.java @@ -8,6 +8,8 @@ import com.iflytop.gd.app.core.CraftsContext; import com.iflytop.gd.app.mapper.CraftsMapper; import com.iflytop.gd.app.model.entity.Crafts; import com.iflytop.gd.app.model.vo.CraftStatusVO; +import com.iflytop.gd.common.exception.AppException; +import com.iflytop.gd.common.result.ResultCode; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.statemachine.config.StateMachineFactory; @@ -15,11 +17,14 @@ import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; -import java.util.concurrent.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.stream.Collectors; /** - * 工艺执行管理服务,每个 heatId 对应一个独立线程执行任务 + * 工艺执行管理服务 */ @Service @RequiredArgsConstructor @@ -38,28 +43,42 @@ public class CraftsService extends ServiceImpl { } /** - * 开始执行工艺 + * 配置工艺 */ - public synchronized boolean startCrafts(Long craftId, String heatId) { - if (futureMap.containsKey(heatId)) { - return false; + public synchronized void setCraft(Long craftId, String heatId) { + // 校验已有上下文状态,仅允许在 READY、STOPPED 或 FINISHED 状态下重置 + CraftsContext existing = contextMap.get(heatId); + if (existing != null) { + CraftStates state = existing.getSm().getState().getId(); + if (state == CraftStates.RUNNING || state == CraftStates.PAUSED) { + throw new AppException(ResultCode.CRAFT_RUNNING); + } + clearCraftContext(heatId); } Crafts craft = this.getById(craftId); - if (craft == null) { - return false; - } - CraftsContext ctx = new CraftsContext(heatId, craft, stateMachineFactory, webSocketService, craftsStepService); - CompletableFuture cf = CompletableFuture.runAsync(() -> { - try { - ctx.run(); - } finally { - contextMap.remove(heatId); - futureMap.remove(heatId); - } - }, executor); + CraftsContext ctx = new CraftsContext( + heatId, + craft, + stateMachineFactory, + webSocketService, + craftsStepService + ); contextMap.put(heatId, ctx); - futureMap.put(heatId, cf); - return true; + } + + /** + * 启动执行工艺(需先调用 setCraft) + */ + public synchronized void startCrafts(String heatId) { + CraftsContext ctx = contextMap.get(heatId); + if (ctx == null) { + throw new AppException(ResultCode.CRAFT_CONTEXT_NULL); + } + if (futureMap.containsKey(heatId)) { + throw new AppException(ResultCode.CRAFT_RUNNING); + } + Future future = executor.submit(ctx); + futureMap.put(heatId, future); } /** @@ -67,9 +86,10 @@ public class CraftsService extends ServiceImpl { */ public synchronized void pauseCrafts(String heatId) { CraftsContext ctx = contextMap.get(heatId); - if (ctx != null) { - ctx.pause(); + if (ctx == null) { + throw new AppException(ResultCode.CRAFT_CONTEXT_NULL); } + ctx.pause(); } /** @@ -77,37 +97,45 @@ public class CraftsService extends ServiceImpl { */ public synchronized void resumeCrafts(String heatId) { CraftsContext ctx = contextMap.get(heatId); - if (ctx != null) { - ctx.resume(); + if (ctx == null) { + throw new AppException(ResultCode.CRAFT_CONTEXT_NULL); } + ctx.resume(); } /** - * 停止执行工艺 + * 停止执行工艺,不清除上下文 */ - public synchronized boolean stopCrafts(String heatId) { + public synchronized void stopCrafts(String heatId) { CraftsContext ctx = contextMap.get(heatId); - Future future = futureMap.get(heatId); - if (ctx != null && future != null) { - ctx.stop(); - future.cancel(true); - contextMap.remove(heatId); - futureMap.remove(heatId); - return true; + Future future = futureMap.remove(heatId); + if (ctx == null || future == null) { + throw new AppException(ResultCode.CRAFT_CONTEXT_NULL); } - return false; + ctx.stop(); + future.cancel(true); + } + + /** + * 清理指定 heatId 的执行上下文和 Future + */ + public synchronized void clearCraftContext(String heatId) { + contextMap.remove(heatId); + Future future = futureMap.remove(heatId); + if (future != null) future.cancel(true); } /** * 查询指定 heatId 的执行状态 */ - public synchronized CraftStatusVO getStatus(String heatId) { + public CraftStatusVO getStatus(String heatId) { CraftsContext ctx = contextMap.get(heatId); if (ctx == null) { return null; } CraftStatusVO vo = new CraftStatusVO(); vo.setHeatId(heatId); + vo.setCraftsId(ctx.getCraft().getId()); vo.setState(ctx.getSm().getState().getId()); vo.setCurrentIndex(ctx.getCurrentIndex()); vo.setSteps(ctx.getCraftsStepList()); @@ -117,12 +145,13 @@ public class CraftsService extends ServiceImpl { /** * 查询所有正在执行的工艺状态 */ - public synchronized List getAllStatuses() { + public List getAllStatuses() { return contextMap.entrySet().stream().map(entry -> { - String heatId = entry.getKey(); + String heatIdKey = entry.getKey(); CraftsContext ctx = entry.getValue(); CraftStatusVO vo = new CraftStatusVO(); - vo.setHeatId(heatId); + vo.setHeatId(heatIdKey); + vo.setCraftsId(ctx.getCraft().getId()); vo.setState(ctx.getSm().getState().getId()); vo.setCurrentIndex(ctx.getCurrentIndex()); vo.setSteps(ctx.getCraftsStepList()); @@ -147,9 +176,7 @@ public class CraftsService extends ServiceImpl { } public boolean deleteCrafts(String idsStr) { - List ids = Arrays.stream(idsStr.split(",")) - .map(Long::parseLong) - .collect(Collectors.toList()); + List ids = Arrays.stream(idsStr.split(",")).map(Long::parseLong).collect(Collectors.toList()); return this.removeByIds(ids); } -} \ No newline at end of file +} diff --git a/src/main/java/com/iflytop/gd/app/service/CraftsStepService.java b/src/main/java/com/iflytop/gd/app/service/CraftsStepService.java index 8272c5a..2f05262 100644 --- a/src/main/java/com/iflytop/gd/app/service/CraftsStepService.java +++ b/src/main/java/com/iflytop/gd/app/service/CraftsStepService.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; /** - * 工艺步骤 + * 工艺步骤执行服务 */ @Service @RequiredArgsConstructor @@ -14,45 +14,65 @@ public class CraftsStepService { /** * 执行单个工艺步骤 + * + * @param heatId 加热区 ID + * @param step 工艺步骤,包括 method 和 params + * @return true 表示执行成功,false 表示失败 + * @throws InterruptedException 如果延时被中断 */ - public boolean executeStep(String heatId, CraftsStep step) throws InterruptedException { + public boolean executeStep(String heatId, CraftsStep step){ String method = step.getMethod(); JSONObject params = step.getParams(); + delay(3); return switch (method) { - case "UP_TRAY" -> upTray(heatId); - case "DOWN_TRAY" -> downTray(heatId); - case "ADD_LIQUID" -> addLiquid(heatId, params); - case "DELAY" -> delay(params); - default -> false; + case "ADD_LIQUID" -> addLiquid(heatId, params); + case "SHAKING" -> shaking(heatId, params); + case "TAKE_PHOTO" -> takePhoto(heatId); + default -> true; }; } /** - * 将托盘升起 + * 添加溶液 */ - private boolean upTray(String heatId) { + private boolean addLiquid(String heatId, JSONObject params) { + // TODO: 从 params 中获取溶液类型、体积等参数并调用设备服务 return true; } /** - * 将托盘降下 + * 摇匀操作 */ - private boolean downTray(String heatId) { + private boolean shaking(String heatId, JSONObject params) { + // TODO: 从 params 中获取速度、时长等参数并调用设备服务 return true; } /** - * 加液操作 + * 加热 */ - private boolean addLiquid(String heatId, JSONObject params) { + private boolean heating(String heatId, JSONObject params) { + // TODO: 从 params 中获取温度、时长等参数并调用设备服务 + return true; + } + + /** + * 拍照操作· + */ + private boolean takePhoto(String heatId) { + // TODO: 调用相机或设备服务执行拍照 return true; } /** - * 等待指定秒数 + * 延时等待 */ - private boolean delay(JSONObject params) throws InterruptedException { - Thread.sleep(params.getInt("second") * 1000L); + private boolean delay(int seconds){ + try { + Thread.sleep(seconds * 1000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/com/iflytop/gd/common/result/ResultCode.java b/src/main/java/com/iflytop/gd/common/result/ResultCode.java index 5895172..bbe2e2f 100644 --- a/src/main/java/com/iflytop/gd/common/result/ResultCode.java +++ b/src/main/java/com/iflytop/gd/common/result/ResultCode.java @@ -36,6 +36,8 @@ public enum ResultCode implements IResultCode, Serializable { INVALID_CREDENTIALS("4002", "用户名或密码错误"), OPERATION_NOT_ALLOWED("4003", "业务操作不允许"), DATA_ALREADY_EXISTS("4004", "数据已存在"), + CRAFT_RUNNING("4004", "工艺正在执行"), + CRAFT_CONTEXT_NULL("4004", "请先配置该加热区工艺"), //============================ 5xxx:系统 & 第三方 ============================ SYSTEM_ERROR("5000", "系统内部错误"), SERVICE_UNAVAILABLE("5001", "服务暂不可用"),