From 2b7e91e9181957e6fccb20bbf0d6c83570d8d015 Mon Sep 17 00:00:00 2001 From: wenshijun Date: Wed, 31 Dec 2025 14:51:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E5=BA=A6=E6=A8=A1=E5=9D=97=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BB=85=E7=AB=8B=E5=BA=93=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9Emqtt=E5=8A=9F=E8=83=BD=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=96=BD=E5=B7=A5=E9=98=9F=E4=BF=A1=E6=81=AF=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 343 +++++++++--------- .../ConstructionTeamController.java | 25 +- .../information/controller/MtdController.java | 1 - .../information/domain/ConstructionTeam.java | 10 +- .../service/IConstructionTeamService.java | 8 + .../impl/ConstructionTeamServiceImpl.java | 73 ++++ .../wisdom/config/MqttClientConfig.java | 182 ++++++++++ .../project/wisdom/config/MqttProperties.java | 46 +++ .../wisdom/config/MqttPublishClient.java | 74 ++++ .../controller/AgvTaskResultController.java | 2 +- .../wisdom/controller/DdTaskController.java | 8 + .../com/zg/project/wisdom/domain/DdTask.java | 210 +++++++---- .../wisdom/mapper/AgvTaskResultMapper.java | 5 + .../project/wisdom/mapper/DdTaskMapper.java | 5 + .../dispatcher/MqttMessageDispatcher.java | 55 +++ .../mqtt/handler/HumiSensorHandler.java | 43 +++ .../mqtt/handler/SmokeSensorHandler.java | 47 +++ .../mqtt/handler/TempSensorHandler.java | 46 +++ .../wisdom/service/IDdTaskService.java | 6 + .../wisdom/service/QwenOcrRemoteService.java | 17 +- .../impl/AgvTaskResultServiceImpl.java | 72 ++-- .../service/impl/DdTaskServiceImpl.java | 194 +++++++--- .../impl/WcsTaskResultServiceImpl.java | 77 +++- src/main/resources/application-druid.yml | 4 +- src/main/resources/application.yml | 50 ++- .../mybatis/wisdom/AgvTaskResultMapper.xml | 3 + .../resources/mybatis/wisdom/DdTaskMapper.xml | 199 ++++++++-- 27 files changed, 1414 insertions(+), 391 deletions(-) create mode 100644 src/main/java/com/zg/project/wisdom/config/MqttClientConfig.java create mode 100644 src/main/java/com/zg/project/wisdom/config/MqttProperties.java create mode 100644 src/main/java/com/zg/project/wisdom/config/MqttPublishClient.java create mode 100644 src/main/java/com/zg/project/wisdom/mqtt/dispatcher/MqttMessageDispatcher.java create mode 100644 src/main/java/com/zg/project/wisdom/mqtt/handler/HumiSensorHandler.java create mode 100644 src/main/java/com/zg/project/wisdom/mqtt/handler/SmokeSensorHandler.java create mode 100644 src/main/java/com/zg/project/wisdom/mqtt/handler/TempSensorHandler.java diff --git a/pom.xml b/pom.xml index 4e254c1..6d2e104 100644 --- a/pom.xml +++ b/pom.xml @@ -1,197 +1,206 @@ - + + + + + 4.0.0 com.smart smart 3.8.9 jar - zg http://www.zg.vip 智慧实物管理系统 + + + org.springframework.boot spring-boot-starter-parent 2.5.15 - + + + + + UTF-8 UTF-8 1.8 1.8 1.8 - 3.1.1 - 1.4.7 - 2.0.53 + + 9.0.102 + 5.3.39 + 5.7.12 + 1.2.13 + + 1.2.23 + 1.4.7 + 3.5.5 + + + 2.0.53 2.13.0 1.21 0.9.1 - 2.3.3 + + 3.0.0 4.1.2 - + + 5.8.5 2.3 - - 9.0.102 - 1.2.13 - 5.7.12 - 5.3.39 + 2.3.3 + + + 2.5.15 + 4.12.0 + 1.2.5 + + + 7.1.4 + + 1.0.0 4.4.7 - 3.5.5 - 7.1.4 - 4.12.0 + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + mysql + mysql-connector-java + runtime + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.spring.boot.starter.version} + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.commons + commons-pool2 + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + ${paho.mqtt.version} + + com.squareup.okhttp3 okhttp ${okhttp.version} - - - org.springframework.boot - spring-boot-starter - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - org.springframework.boot - spring-boot-starter-aop - - - - - org.springframework.boot - spring-boot-starter-web - - - - - - - - org.springframework.boot - spring-boot-starter-security - - - - - org.springframework.boot - spring-boot-starter-websocket - - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - - org.apache.commons - commons-pool2 - - - - - mysql - mysql-connector-java - runtime - - - - - com.github.pagehelper - pagehelper-spring-boot-starter - ${pagehelper.spring.boot.starter.version} - - - - - com.alibaba - druid-spring-boot-starter - ${druid.version} - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.apache.commons - commons-lang3 - - - - - commons-io - commons-io - ${commons.io.version} - - - - - eu.bitwalker - UserAgentUtils - ${bitwalker.version} - - - + com.alibaba.fastjson2 fastjson2 ${fastjson.version} - - org.springframework - spring-context-support + org.apache.commons + commons-lang3 - + + commons-io + commons-io + ${commons.io.version} + + + + eu.bitwalker + UserAgentUtils + ${bitwalker.version} + + + io.jsonwebtoken jjwt ${jwt.version} - javax.xml.bind jaxb-api - + io.springfox springfox-boot-starter @@ -204,48 +213,31 @@ - io.swagger swagger-models 1.6.2 - - - com.github.oshi - oshi-core - ${oshi.version} - - - org.apache.poi poi-ooxml ${poi.version} - + - io.minio - minio - ${minio.version} + com.github.oshi + oshi-core + ${oshi.version} - - com.baomidou - mybatis-plus-boot-starter - ${mybatis-plus.version} - - - org.apache.velocity velocity-engine-core ${velocity.version} - org.quartz-scheduler quartz @@ -257,40 +249,59 @@ - pro.fessional kaptcha ${kaptcha.version} - servlet-api javax.servlet + servlet-api + + + io.minio + minio + ${minio.version} + + + com.zkweix RFIDReaderAPI - 1.0.0 + ${rfidreader.version} com.zmtech ZMPrinter - 4.4.7 + ${zmprinter.version} + org.projectlombok lombok + + + org.springframework.boot + spring-boot-starter-test + test + + + + + WisdomManagement + org.springframework.boot @@ -300,7 +311,6 @@ - org.apache.maven.plugins maven-compiler-plugin @@ -308,35 +318,20 @@ ${maven.compiler.source} ${maven.compiler.target} - + + + - public + aliyun aliyun nexus https://maven.aliyun.com/repository/public - - true - - - - public - aliyun nexus - https://maven.aliyun.com/repository/public - - true - - - false - - - - diff --git a/src/main/java/com/zg/project/information/controller/ConstructionTeamController.java b/src/main/java/com/zg/project/information/controller/ConstructionTeamController.java index 9c550e9..a9f2db8 100644 --- a/src/main/java/com/zg/project/information/controller/ConstructionTeamController.java +++ b/src/main/java/com/zg/project/information/controller/ConstructionTeamController.java @@ -4,14 +4,7 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import com.zg.framework.aspectj.lang.annotation.Log; import com.zg.framework.aspectj.lang.enums.BusinessType; import com.zg.project.information.domain.ConstructionTeam; @@ -20,6 +13,7 @@ import com.zg.framework.web.controller.BaseController; import com.zg.framework.web.domain.AjaxResult; import com.zg.common.utils.poi.ExcelUtil; import com.zg.framework.web.page.TableDataInfo; +import org.springframework.web.multipart.MultipartFile; /** * 施工队信息Controller @@ -59,6 +53,21 @@ public class ConstructionTeamController extends BaseController util.exportExcel(response, list, "施工队信息数据"); } + /** + * 导入施工队数据(全部新增,不校验重复) + */ + @PreAuthorize("@ss.hasPermi('information:construction:import')") + @Log(title = "施工队信息", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(@RequestParam("file") MultipartFile file) throws Exception + { + ExcelUtil util = new ExcelUtil<>(ConstructionTeam.class); + List teamList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = constructionTeamService.importConstructionTeamList(teamList, operName); + return AjaxResult.success(message); + } + /** * 获取施工队信息详细信息 */ diff --git a/src/main/java/com/zg/project/information/controller/MtdController.java b/src/main/java/com/zg/project/information/controller/MtdController.java index 3c0bdeb..f930d2f 100644 --- a/src/main/java/com/zg/project/information/controller/MtdController.java +++ b/src/main/java/com/zg/project/information/controller/MtdController.java @@ -65,7 +65,6 @@ public class MtdController extends BaseController ExcelUtil util = new ExcelUtil<>(Mtd.class); List mtdList = util.importExcel(file.getInputStream()); String operName = getUsername(); -// String operName = "大爷的!!!"; String message = mtdService.importMtd(mtdList, updateSupport, operName); return success(message); } diff --git a/src/main/java/com/zg/project/information/domain/ConstructionTeam.java b/src/main/java/com/zg/project/information/domain/ConstructionTeam.java index 8a22353..111ad6e 100644 --- a/src/main/java/com/zg/project/information/domain/ConstructionTeam.java +++ b/src/main/java/com/zg/project/information/domain/ConstructionTeam.java @@ -29,25 +29,25 @@ public class ConstructionTeam extends BaseEntity private String teamCode; /** 创建人 */ - @Excel(name = "创建人") +// @Excel(name = "创建人") private String createdBy; /** 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd") - @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd") +// @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd") private Date createdAt; /** 修改人 */ - @Excel(name = "修改人") +// @Excel(name = "修改人") private String updatedBy; /** 修改时间 */ @JsonFormat(pattern = "yyyy-MM-dd") - @Excel(name = "修改时间", width = 30, dateFormat = "yyyy-MM-dd") +// @Excel(name = "修改时间", width = 30, dateFormat = "yyyy-MM-dd") private Date updatedAt; /** 是否删除(0正常 1删除) */ - @Excel(name = "是否删除", readConverterExp = "0=正常,1=删除") +// @Excel(name = "是否删除", readConverterExp = "0=正常,1=删除") private String isDelete; public void setId(Long id) diff --git a/src/main/java/com/zg/project/information/service/IConstructionTeamService.java b/src/main/java/com/zg/project/information/service/IConstructionTeamService.java index bf85aa0..9f81559 100644 --- a/src/main/java/com/zg/project/information/service/IConstructionTeamService.java +++ b/src/main/java/com/zg/project/information/service/IConstructionTeamService.java @@ -58,4 +58,12 @@ public interface IConstructionTeamService * @return 结果 */ public int deleteConstructionTeamById(Long id); + + /** + * 导入施工队信息数据 + * + * @param teamList 工单数据列表 + * @param operName 操作人员 + */ + String importConstructionTeamList(List teamList, String operName); } diff --git a/src/main/java/com/zg/project/information/service/impl/ConstructionTeamServiceImpl.java b/src/main/java/com/zg/project/information/service/impl/ConstructionTeamServiceImpl.java index a4e0ae7..8f2ae17 100644 --- a/src/main/java/com/zg/project/information/service/impl/ConstructionTeamServiceImpl.java +++ b/src/main/java/com/zg/project/information/service/impl/ConstructionTeamServiceImpl.java @@ -1,6 +1,10 @@ package com.zg.project.information.service.impl; +import java.util.Date; import java.util.List; + +import com.zg.common.exception.ServiceException; +import com.zg.common.utils.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.zg.project.information.mapper.ConstructionTeamMapper; @@ -90,4 +94,73 @@ public class ConstructionTeamServiceImpl implements IConstructionTeamService { return constructionTeamMapper.deleteConstructionTeamById(id); } + + /** + * 导入施工队信息列表 + * + * @param teamList 导入的施工队信息列表 + * @param operName 操作人员 + * @return 结果 + */ + @Override + public String importConstructionTeamList(List teamList, String operName) + { + if (teamList == null || teamList.isEmpty()) + { + throw new ServiceException("导入数据不能为空!"); + } + + int successNum = 0; + int failureNum = 0; + StringBuilder failureMsg = new StringBuilder(); + + Date now = new Date(); + + for (ConstructionTeam team : teamList) + { + try + { + // 可选:最低限度必填校验(建议保留,不然空行也会入库) + if (StringUtils.isBlank(team.getTeamName()) || StringUtils.isBlank(team.getTeamCode())) + { + failureNum++; + failureMsg.append("
施工队名称/编号不能为空,已跳过。"); + continue; + } + + // 按你供应计划逻辑:不校验重复,全部新增 + team.setCreatedBy(operName); + team.setCreatedAt(now); + team.setUpdatedBy(operName); + team.setUpdatedAt(now); + + // isDelete 默认 0 + if (StringUtils.isBlank(team.getIsDelete())) + { + team.setIsDelete("0"); + } + + // 走你现有 insert(Mapper/XML 已存在) + insertConstructionTeam(team); + + successNum++; + } + catch (Exception e) + { + failureNum++; + failureMsg.append("
施工队编号:").append(team.getTeamCode()) + .append(",施工队名称:").append(team.getTeamName()) + .append(" 导入失败:").append(e.getMessage()); + } + } + + if (failureNum > 0) + { + failureMsg.insert(0, "导入完成,但有 " + failureNum + " 条记录失败:"); + throw new ServiceException(failureMsg.toString()); + } + + return "导入成功,共 " + successNum + " 条数据"; + } + } diff --git a/src/main/java/com/zg/project/wisdom/config/MqttClientConfig.java b/src/main/java/com/zg/project/wisdom/config/MqttClientConfig.java new file mode 100644 index 0000000..a227864 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/config/MqttClientConfig.java @@ -0,0 +1,182 @@ +package com.zg.project.wisdom.config; + +import com.zg.project.wisdom.mqtt.dispatcher.MqttMessageDispatcher; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; + +/** + * MQTT 客户端初始化配置类 + * + * 作用说明: + * 1. Spring Boot 启动时自动创建 MQTT 客户端 + * 2. 连接 EMQX Broker + * 3. 订阅 application.yml 中配置的 Topic(支持通配符) + * 4. 接收传感器上报的数据 + * + * 说明: + * - 同一个 MQTT Client 同时支持「订阅(接收)」和「发布(下发指令)」 + * - 后端一般只需要一个 Client + */ +@Slf4j +@Configuration +public class MqttClientConfig { + + /** + * MQTT 客户端实例 + * 由本配置类统一维护 + */ + private MqttClient mqttClient; + + @Resource + private MqttMessageDispatcher mqttMessageDispatcher; + + /** + * 创建 MQTT Client Bean + * + * @param props MQTT 配置(来自 application.yml) + */ + @Bean + public MqttClient mqttClient(MqttProperties props) throws MqttException { + + // 如果未开启 MQTT 功能,直接不初始化 + // 注意:此时 mqttClient Bean 为 null + if (!props.isEnabled()) { + log.warn("[MQTT] mqtt.enabled=false,MQTT 功能未启用"); + return null; + } + + // 1. 创建 MQTT 客户端 + mqttClient = new MqttClient( + props.getBroker(), + props.getClientId(), + new MemoryPersistence() + ); + + // 2. MQTT 连接参数 + MqttConnectOptions options = new MqttConnectOptions(); + + // 是否清除会话: + // true - 断开后清除订阅(推荐后端使用) + // false - 保留会话(多用于设备端) + options.setCleanSession(props.isCleanSession()); + + // 心跳时间(秒) + options.setKeepAliveInterval(props.getKeepAlive()); + + // 连接超时时间(秒) + options.setConnectionTimeout(props.getTimeout()); + + // 自动重连(非常重要,防止网络抖动) + options.setAutomaticReconnect(true); + + // 设置用户名密码(如果配置了) + if (props.getUsername() != null && !props.getUsername().trim().isEmpty()) { + options.setUserName(props.getUsername().trim()); + } + if (props.getPassword() != null && !props.getPassword().trim().isEmpty()) { + options.setPassword(props.getPassword().toCharArray()); + } + + // 3. 设置 MQTT 回调 + mqttClient.setCallback(new MqttCallbackExtended() { + + /** + * 连接成功(或重连成功)回调 + */ + @Override + public void connectComplete(boolean reconnect, String serverURI) { + log.info("[MQTT] 连接成功 reconnect={}, serverURI={}", reconnect, serverURI); + // 连接成功后订阅 Topic + subscribe(props); + } + + /** + * 连接丢失回调 + */ + @Override + public void connectionLost(Throwable cause) { + log.warn("[MQTT] 连接断开", cause); + } + + /** + * 接收到消息回调 + * + * @param topic 消息主题(用于区分设备、类型) + * @param message 消息内容 + */ + + @Override + public void messageArrived(String topic, MqttMessage message) { + String payload = new String(message.getPayload(), StandardCharsets.UTF_8); + mqttMessageDispatcher.dispatch(topic, payload); + } + + /** + * 消息发送完成回调(发布消息后触发) + */ + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + // 后端下发指令时可用于确认发送完成 + } + }); + + // 4. 连接 Broker + log.info("[MQTT] 正在连接 Broker:{}", props.getBroker()); + mqttClient.connect(options); + log.info("[MQTT] MQTT 已连接"); + + // 5. 启动时先订阅一次(防止 connectComplete 未触发) + subscribe(props); + + return mqttClient; + } + + /** + * 订阅 Topic(支持通配符) + */ + private void subscribe(MqttProperties props) { + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[MQTT] 订阅失败:客户端未连接"); + return; + } + if (props.getTopics() == null || props.getTopics().isEmpty()) { + log.warn("[MQTT] 未配置订阅 Topic"); + return; + } + + try { + for (String topic : props.getTopics()) { + if (topic == null || topic.trim().isEmpty()) { + continue; + } + mqttClient.subscribe(topic.trim(), props.getQos()); + log.info("[MQTT] 已订阅 Topic:{},QoS={}", topic.trim(), props.getQos()); + } + } catch (Exception e) { + log.error("[MQTT] 订阅 Topic 失败", e); + } + } + + /** + * Spring 容器关闭时,优雅断开 MQTT 连接 + */ + @PreDestroy + public void destroy() { + try { + if (mqttClient != null) { + if (mqttClient.isConnected()) { + mqttClient.disconnect(); + } + mqttClient.close(); + } + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/com/zg/project/wisdom/config/MqttProperties.java b/src/main/java/com/zg/project/wisdom/config/MqttProperties.java new file mode 100644 index 0000000..89d3a25 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/config/MqttProperties.java @@ -0,0 +1,46 @@ +package com.zg.project.wisdom.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * MQTT 配置属性(对应 application.yml 中 mqtt.*) + */ +@Data +@Component +@ConfigurationProperties(prefix = "mqtt") +public class MqttProperties { + + /** 是否启用 MQTT */ + private boolean enabled; + + /** Broker 地址:tcp://192.168.1.29:1883 */ + private String broker; + + /** 客户端ID(在 EMQX 中唯一) */ + private String clientId; + + /** 账号 */ + private String username; + + /** 密码 */ + private String password; + + /** 是否清除会话 */ + private boolean cleanSession = true; + + /** 心跳时间(秒) */ + private int keepAlive = 30; + + /** 连接超时时间(秒) */ + private int timeout = 10; + + /** 默认 QoS */ + private int qos = 1; + + /** 订阅 Topic 列表(支持通配符) */ + private List topics; +} diff --git a/src/main/java/com/zg/project/wisdom/config/MqttPublishClient.java b/src/main/java/com/zg/project/wisdom/config/MqttPublishClient.java new file mode 100644 index 0000000..04c605e --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/config/MqttPublishClient.java @@ -0,0 +1,74 @@ +package com.zg.project.wisdom.config; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * MQTT 消息发布客户端 + * + * 作用说明: + * - 专门用于向指定设备下发控制指令 + * - 例如:开启 / 关闭 / 重启 传感器 + * + * 说明: + * - 依赖同一个 MqttClient + * - 发布消息时使用“精确 Topic”(不能使用通配符) + */ +@Slf4j +@Component +public class MqttPublishClient { + + /** + * MQTT 客户端(由 Spring 注入) + */ + private final MqttClient mqttClient; + + public MqttPublishClient(MqttClient mqttClient) { + this.mqttClient = mqttClient; + } + + /** + * 发布 MQTT 消息(默认 QoS=1,不保留消息) + * + * @param topic 目标 Topic(必须是具体设备的 Topic) + * @param payload 消息内容(JSON 字符串) + */ + public void publish(String topic, String payload) { + publish(topic, payload, 1, false); + } + + /** + * 发布 MQTT 消息(自定义参数) + */ + public void publish(String topic, String payload, int qos, boolean retained) { + + // 如果 MQTT 未启用或未初始化 + if (mqttClient == null) { + throw new IllegalStateException("MQTT 客户端未初始化(mqtt.enabled=false)"); + } + + // 如果 MQTT 尚未连接 + if (!mqttClient.isConnected()) { + throw new IllegalStateException("MQTT 客户端未连接到 Broker"); + } + + try { + MqttMessage msg = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); + msg.setQos(qos); + msg.setRetained(retained); + + mqttClient.publish(topic, msg); + + log.info("[MQTT] 指令已发送 topic={}, qos={}, retained={}, payload={}", + topic, qos, retained, payload); + + } catch (Exception e) { + log.error("[MQTT] 指令发送失败 topic=" + topic, e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/zg/project/wisdom/controller/AgvTaskResultController.java b/src/main/java/com/zg/project/wisdom/controller/AgvTaskResultController.java index e706ab3..dbea512 100644 --- a/src/main/java/com/zg/project/wisdom/controller/AgvTaskResultController.java +++ b/src/main/java/com/zg/project/wisdom/controller/AgvTaskResultController.java @@ -12,7 +12,6 @@ import com.zg.project.wisdom.domain.dto.OutGoodsDTO; import com.zg.project.wisdom.service.IAgvTaskResultService; import com.zg.project.wisdom.service.IWcsTaskResultService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Date; @@ -48,6 +47,7 @@ public class AgvTaskResultController extends BaseController { return AjaxResult.success("WCS 回调接收成功"); } + /** * 查询 AGV 状态 */ diff --git a/src/main/java/com/zg/project/wisdom/controller/DdTaskController.java b/src/main/java/com/zg/project/wisdom/controller/DdTaskController.java index 10af7bc..20087ad 100644 --- a/src/main/java/com/zg/project/wisdom/controller/DdTaskController.java +++ b/src/main/java/com/zg/project/wisdom/controller/DdTaskController.java @@ -36,6 +36,14 @@ public class DdTaskController extends BaseController { return toAjax(ddTaskService.insertDdTask(ddTask)); } + /** + * 批量新增调度任务 + */ + @PostMapping("/batchAdd") + public AjaxResult batchAdd(@RequestBody List list) { + return toAjax(ddTaskService.insertDdTaskBatch(list)); + } + /** * 执行任务 * @param diff --git a/src/main/java/com/zg/project/wisdom/domain/DdTask.java b/src/main/java/com/zg/project/wisdom/domain/DdTask.java index 2e68ee9..b116067 100644 --- a/src/main/java/com/zg/project/wisdom/domain/DdTask.java +++ b/src/main/java/com/zg/project/wisdom/domain/DdTask.java @@ -3,6 +3,7 @@ package com.zg.project.wisdom.domain; import com.fasterxml.jackson.annotation.JsonFormat; import com.zg.framework.web.domain.BaseEntity; import com.zg.framework.aspectj.lang.annotation.Excel; + import java.util.Date; /** @@ -29,7 +30,7 @@ public class DdTask extends BaseEntity { @Excel(name = "任务类型") private String taskType; - /** 任务状态(0待建,1完成,-1取消) */ + /** 任务状态(0待执行,1执行中,2完成,-1取消) */ @Excel(name = "任务状态") private Integer taskStatus; @@ -65,13 +66,13 @@ public class DdTask extends BaseEntity { @Excel(name = "审核员") private String approver; - /** 单据创建时间(原始单据时间) */ + /** 任务创建时间(原始单据时间) */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Excel(name = "单据创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "任务创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date rcptim; /** 绑定单据ID */ - @Excel(name = "rcp id") + @Excel(name = "绑定单据ID") private String rid; /** 执行次数 */ @@ -82,85 +83,168 @@ public class DdTask extends BaseEntity { @Excel(name = "封签码/订单号") private String prf; + /** + * 调度模式 + * 1 = 仅立库(WCS) + * 2 = 立库 + AGV + */ + @Excel(name = "调度模式", readConverterExp = "1=仅立库,2=立库+AGV") + private Integer dispatchMode; + /** 是否删除(0正常 1删除) */ private String isDelete; - // getter/setter - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } + // ===================== getter / setter ===================== - public String getTaskNo() { return taskNo; } - public void setTaskNo(String taskNo) { this.taskNo = taskNo; } + public Long getId() { + return id; + } - public String getTaskDtl() { return taskDtl; } - public void setTaskDtl(String taskDtl) { this.taskDtl = taskDtl; } + public void setId(Long id) { + this.id = id; + } - public String getTaskType() { return taskType; } - public void setTaskType(String taskType) { this.taskType = taskType; } + public String getTaskNo() { + return taskNo; + } - public Integer getTaskStatus() { return taskStatus; } - public void setTaskStatus(Integer taskStatus) { this.taskStatus = taskStatus; } + public void setTaskNo(String taskNo) { + this.taskNo = taskNo; + } - public Integer getMidStatus() { return midStatus; } - public void setMidStatus(Integer midStatus) { this.midStatus = midStatus; } + public String getTaskDtl() { + return taskDtl; + } - public String getMid() { return mid; } - public void setMid(String mid) { this.mid = mid; } + public void setTaskDtl(String taskDtl) { + this.taskDtl = taskDtl; + } - public Integer getNum() { return num; } - public void setNum(Integer num) { this.num = num; } + public String getTaskType() { + return taskType; + } - public String getMidType() { return midType; } - public void setMidType(String midType) { this.midType = midType; } + public void setTaskType(String taskType) { + this.taskType = taskType; + } - public String getSourceName() { return sourceName; } - public void setSourceName(String sourceName) { this.sourceName = sourceName; } + public Integer getTaskStatus() { + return taskStatus; + } - public String getTargetName() { return targetName; } - public void setTargetName(String targetName) { this.targetName = targetName; } + public void setTaskStatus(Integer taskStatus) { + this.taskStatus = taskStatus; + } - public String getOperator() { return operator; } - public void setOperator(String operator) { this.operator = operator; } + public Integer getMidStatus() { + return midStatus; + } - public String getApprover() { return approver; } - public void setApprover(String approver) { this.approver = approver; } + public void setMidStatus(Integer midStatus) { + this.midStatus = midStatus; + } - public Date getRcptim() { return rcptim; } - public void setRcptim(Date rcptim) { this.rcptim = rcptim; } + public String getMid() { + return mid; + } - public String getRid() { return rid; } - public void setRid(String rid) { this.rid = rid; } + public void setMid(String mid) { + this.mid = mid; + } - public Integer getDoCount() { return doCount; } - public void setDoCount(Integer doCount) { this.doCount = doCount; } + public Integer getNum() { + return num; + } - public String getPrf() { return prf; } - public void setPrf(String prf) { this.prf = prf; } + public void setNum(Integer num) { + this.num = num; + } - public String getIsDelete() { return isDelete; } - public void setIsDelete(String isDelete) { this.isDelete = isDelete; } + public String getMidType() { + return midType; + } - @Override - public String toString() { - return "DdTask{" + - "id=" + id + - ", taskNo='" + taskNo + '\'' + - ", taskDtl='" + taskDtl + '\'' + - ", taskType='" + taskType + '\'' + - ", taskStatus=" + taskStatus + - ", midStatus=" + midStatus + - ", mid='" + mid + '\'' + - ", num=" + num + - ", midType='" + midType + '\'' + - ", sourceName='" + sourceName + '\'' + - ", targetName='" + targetName + '\'' + - ", operator='" + operator + '\'' + - ", approver='" + approver + '\'' + - ", rcptim=" + rcptim + - ", rid='" + rid + '\'' + - ", doCount=" + doCount + - ", prf='" + prf + '\'' + - ", isDelete='" + isDelete + '\'' + - '}'; + public void setMidType(String midType) { + this.midType = midType; + } + + public String getSourceName() { + return sourceName; + } + + public void setSourceName(String sourceName) { + this.sourceName = sourceName; + } + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } + + public String getApprover() { + return approver; + } + + public void setApprover(String approver) { + this.approver = approver; + } + + public Date getRcptim() { + return rcptim; + } + + public void setRcptim(Date rcptim) { + this.rcptim = rcptim; + } + + public String getRid() { + return rid; + } + + public void setRid(String rid) { + this.rid = rid; + } + + public Integer getDoCount() { + return doCount; + } + + public void setDoCount(Integer doCount) { + this.doCount = doCount; + } + + public String getPrf() { + return prf; + } + + public void setPrf(String prf) { + this.prf = prf; + } + + public Integer getDispatchMode() { + return dispatchMode; + } + + public void setDispatchMode(Integer dispatchMode) { + this.dispatchMode = dispatchMode; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; } } diff --git a/src/main/java/com/zg/project/wisdom/mapper/AgvTaskResultMapper.java b/src/main/java/com/zg/project/wisdom/mapper/AgvTaskResultMapper.java index cb1e841..16c57c9 100644 --- a/src/main/java/com/zg/project/wisdom/mapper/AgvTaskResultMapper.java +++ b/src/main/java/com/zg/project/wisdom/mapper/AgvTaskResultMapper.java @@ -34,4 +34,9 @@ public interface AgvTaskResultMapper { @Param("status") String status); AgvTaskResult selectByTaskNo(String taskNo); + + /** + * 统计指定 taskNo 的记录数量 + */ + int countByTaskNo(String taskNo); } diff --git a/src/main/java/com/zg/project/wisdom/mapper/DdTaskMapper.java b/src/main/java/com/zg/project/wisdom/mapper/DdTaskMapper.java index 0c6e715..c417a50 100644 --- a/src/main/java/com/zg/project/wisdom/mapper/DdTaskMapper.java +++ b/src/main/java/com/zg/project/wisdom/mapper/DdTaskMapper.java @@ -53,4 +53,9 @@ public interface DdTaskMapper { * @return DdTask 对象 */ DdTask selectByTaskNo(@Param("taskNo") String taskNo); + + /** + * 批量新增调度任务 + */ + int batchInsertDdTask(@Param("list") List list); } diff --git a/src/main/java/com/zg/project/wisdom/mqtt/dispatcher/MqttMessageDispatcher.java b/src/main/java/com/zg/project/wisdom/mqtt/dispatcher/MqttMessageDispatcher.java new file mode 100644 index 0000000..26fbdfe --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/mqtt/dispatcher/MqttMessageDispatcher.java @@ -0,0 +1,55 @@ +package com.zg.project.wisdom.mqtt.dispatcher; + +import com.zg.project.wisdom.mqtt.handler.HumiSensorHandler; +import com.zg.project.wisdom.mqtt.handler.SmokeSensorHandler; +import com.zg.project.wisdom.mqtt.handler.TempSensorHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * MQTT消息分发器 + * 负责根据MQTT主题(topic)将消息分发给相应的传感器处理器 + */ +@Slf4j +@Component +public class MqttMessageDispatcher { + + @Autowired + private TempSensorHandler tempSensorHandler; + @Autowired + private HumiSensorHandler humiSensorHandler; + @Autowired + private SmokeSensorHandler smokeSensorHandler; + + /** + * 分发MQTT消息到对应的处理器 + * 根据topic的前缀判断消息类型,并调用相应的处理器进行处理 + * + * @param topic MQTT主题,用于判断消息类型 + * @param payload 消息负载,包含具体的传感器数据 + */ + public void dispatch(String topic, String payload) { + + // 处理温度传感器消息 + if (topic.startsWith("zg/wms/test/temp/")) { + tempSensorHandler.handle(topic, payload); + return; + } + + // 处理湿度传感器消息 + if (topic.startsWith("zg/wms/test/humi/")) { + humiSensorHandler.handle(topic, payload); + return; + } + + // 处理烟雾传感器消息 + if (topic.startsWith("zg/wms/test/smoke/")) { + smokeSensorHandler.handle(topic, payload); + return; + } + + log.warn("未知 MQTT Topic: {}", topic); + } +} + diff --git a/src/main/java/com/zg/project/wisdom/mqtt/handler/HumiSensorHandler.java b/src/main/java/com/zg/project/wisdom/mqtt/handler/HumiSensorHandler.java new file mode 100644 index 0000000..ebfe2c1 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/mqtt/handler/HumiSensorHandler.java @@ -0,0 +1,43 @@ +package com.zg.project.wisdom.mqtt.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 湿度传感器 Handler + * Topic 示例:zg/wms/test/humi/{deviceId} + */ +@Slf4j +@Component +public class HumiSensorHandler { + + public static final String TOPIC_PREFIX = "zg/wms/test/humi/"; + + /** + * 处理湿度传感器消息 + * @param topic MQTT主题 + * @param payload 消息负载 + */ + public void handle(String topic, String payload) { + String deviceId = resolveDeviceId(topic, TOPIC_PREFIX); + + log.info("[HUMI] deviceId={}, topic={}, payload={}", deviceId, topic, payload); + + // TODO: 这里后续写湿度业务逻辑 + } + + /** + * 从MQTT主题中解析设备ID + * @param topic MQTT主题 + * @param prefix 主题前缀 + * @return 设备ID + */ + private String resolveDeviceId(String topic, String prefix) { + if (topic == null || !topic.startsWith(prefix)) { + return ""; + } + // 提取设备ID部分 + return topic.substring(prefix.length()); + } +} + diff --git a/src/main/java/com/zg/project/wisdom/mqtt/handler/SmokeSensorHandler.java b/src/main/java/com/zg/project/wisdom/mqtt/handler/SmokeSensorHandler.java new file mode 100644 index 0000000..c82fb8f --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/mqtt/handler/SmokeSensorHandler.java @@ -0,0 +1,47 @@ +package com.zg.project.wisdom.mqtt.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 烟雾传感器 Handler + * Topic 示例:zg/wms/test/smoke/{deviceId} + */ +@Slf4j +@Component +public class SmokeSensorHandler { + + /** + * MQTT主题前缀,用于烟雾传感器消息 + */ + public static final String TOPIC_PREFIX = "zg/wms/test/smoke/"; + + /** + * 处理烟雾传感器消息 + * + * @param topic MQTT主题 + * @param payload 消息负载内容 + */ + public void handle(String topic, String payload) { + String deviceId = resolveDeviceId(topic, TOPIC_PREFIX); + + log.info("[SMOKE] deviceId={}, topic={}, payload={}", deviceId, topic, payload); + + // TODO: 这里后续写烟雾业务逻辑 + } + + /** + * 从MQTT主题中解析设备ID + * + * @param topic 完整的MQTT主题 + * @param prefix 主题前缀 + * @return 设备ID,如果主题格式不正确则返回空字符串 + */ + private String resolveDeviceId(String topic, String prefix) { + if (topic == null || !topic.startsWith(prefix)) { + return ""; + } + // 提取前缀后的设备ID部分 + return topic.substring(prefix.length()); + } +} diff --git a/src/main/java/com/zg/project/wisdom/mqtt/handler/TempSensorHandler.java b/src/main/java/com/zg/project/wisdom/mqtt/handler/TempSensorHandler.java new file mode 100644 index 0000000..44b14f6 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/mqtt/handler/TempSensorHandler.java @@ -0,0 +1,46 @@ +package com.zg.project.wisdom.mqtt.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 温度传感器 Handler + * Topic 示例:zg/wms/test/temp/{deviceId} + */ +@Slf4j +@Component +public class TempSensorHandler { + + /** + * MQTT主题前缀,用于温度传感器数据 + */ + public static final String TOPIC_PREFIX = "zg/wms/test/temp/"; + + /** + * 处理温度传感器数据 + * + * @param topic MQTT主题,格式为 zg/wms/test/temp/{deviceId} + * @param payload 传感器数据载荷 + */ + public void handle(String topic, String payload) { + String deviceId = resolveDeviceId(topic, TOPIC_PREFIX); + + log.info("[TEMP] deviceId={}, topic={}, payload={}", deviceId, topic, payload); + + // TODO: 这里后续写温度业务逻辑(解析JSON、落库、告警等) + } + + /** + * 从MQTT主题中解析设备ID + * + * @param topic MQTT主题 + * @param prefix 主题前缀 + * @return 设备ID,如果主题格式不正确则返回空字符串 + */ + private String resolveDeviceId(String topic, String prefix) { + if (topic == null || !topic.startsWith(prefix)) { + return ""; + } + return topic.substring(prefix.length()); + } +} diff --git a/src/main/java/com/zg/project/wisdom/service/IDdTaskService.java b/src/main/java/com/zg/project/wisdom/service/IDdTaskService.java index a076bd1..82bb683 100644 --- a/src/main/java/com/zg/project/wisdom/service/IDdTaskService.java +++ b/src/main/java/com/zg/project/wisdom/service/IDdTaskService.java @@ -48,4 +48,10 @@ public interface IDdTaskService { * @return */ TaskExecuteResultVO executeTask(TaskExecuteDTO dto); + + /** + * 批量新增调度任务 + */ + int insertDdTaskBatch(List list); + } diff --git a/src/main/java/com/zg/project/wisdom/service/QwenOcrRemoteService.java b/src/main/java/com/zg/project/wisdom/service/QwenOcrRemoteService.java index aaf40fa..518da2c 100644 --- a/src/main/java/com/zg/project/wisdom/service/QwenOcrRemoteService.java +++ b/src/main/java/com/zg/project/wisdom/service/QwenOcrRemoteService.java @@ -29,15 +29,19 @@ public class QwenOcrRemoteService { } /** - * 接收上传的图片文件,转成 Base64 后, + * 从上传的图片文件中提取ERP订单号 * 调用 250 上的 /ocr/extractErpByBase64 接口,返回采购订单号(ERP) + * + * @param file 上传的图片文件,支持JPEG和PNG格式 + * @return 提取到的ERP订单号,如果未找到则返回空字符串 + * @throws RuntimeException 当调用OCR服务失败或发生IO异常时抛出 */ public String extractErpOrderNo(MultipartFile file) { try { - // 1. 读取文件字节 + // 读取文件字节内容 byte[] bytes = file.getBytes(); - // 2. 推断 contentType + // 推断文件的content type,如果无法获取则根据文件扩展名判断 String contentType = file.getContentType(); if (contentType == null || contentType.isEmpty()) { String name = file.getOriginalFilename(); @@ -48,11 +52,11 @@ public class QwenOcrRemoteService { } } - // 3. 转成 Base64,并加上 data: 前缀 + // 将图片字节转换为Base64编码并添加data URI前缀 String base64 = Base64.getEncoder().encodeToString(bytes); String imageBase64 = "data:" + contentType + ";base64," + base64; - // 4. 组装 JSON 请求体:{ "imageBase64": "data:image/jpeg;base64,xxxx" } + // 构建包含Base64图片数据的JSON请求体 String json = objectMapper.createObjectNode() .put("imageBase64", imageBase64) .toString(); @@ -69,7 +73,7 @@ public class QwenOcrRemoteService { .post(body) .build(); - // 5. 发送 HTTP 请求 + // 发送HTTP POST请求到OCR服务 Response response = httpClient.newCall(request).execute(); try { if (!response.isSuccessful()) { @@ -95,4 +99,5 @@ public class QwenOcrRemoteService { throw new RuntimeException("调用远程 OCR 服务异常:" + e.getMessage(), e); } } + } diff --git a/src/main/java/com/zg/project/wisdom/service/impl/AgvTaskResultServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/AgvTaskResultServiceImpl.java index 74a4428..cda0c0a 100644 --- a/src/main/java/com/zg/project/wisdom/service/impl/AgvTaskResultServiceImpl.java +++ b/src/main/java/com/zg/project/wisdom/service/impl/AgvTaskResultServiceImpl.java @@ -214,29 +214,34 @@ public class AgvTaskResultServiceImpl implements IAgvTaskResultService { * @param */ @Override + @Transactional(rollbackFor = Exception.class) public void handleOutGoods(OutGoodsDTO dto) { + String taskNo = dto.getTaskNo(); - Integer materialStatus = dto.getMaterialStatus(); // ✅ 关键判断字段 + Integer materialStatus = dto.getMaterialStatus(); String userId = SecurityUtils.getUserId().toString(); Date now = DateUtils.getNowDate(); - // 1. 查询 AGV 原任务记录 - AgvTaskResult task = agvTaskResultMapper.selectByTaskNo(taskNo); - if (task == null) { - throw new ServiceException("未找到任务:" + taskNo); + // 1️⃣ 先查调度主任务(关键) + DdTask ddTask = ddTaskMapper.selectByTaskNo(taskNo); + if (ddTask == null) { + throw new ServiceException("未找到调度任务:" + taskNo); } - // 2. 调用 WCS 下架接口 + Integer dispatchMode = ddTask.getDispatchMode(); // 1=仅WCS,2=WCS+AGV + + // 2️⃣ 调用 WCS 下架(所有模式都要) String wcsTaskId = taskNo + "22"; + JSONObject wcsParam = new JSONObject(); wcsParam.put("TaskID", wcsTaskId); wcsParam.put("TrayNo", ""); wcsParam.put("Materialstatus", materialStatus); if (Objects.equals(materialStatus, 1)) { - wcsParam.put("MaterialCode", task.getMid()); - wcsParam.put("Quantity", task.getNum()); - String spec = mtdMapper.selectWlmsByWlbh(task.getMid()); + wcsParam.put("MaterialCode", ddTask.getMid()); + wcsParam.put("Quantity", ddTask.getNum()); + String spec = mtdMapper.selectWlmsByWlbh(ddTask.getMid()); wcsParam.put("Specification", spec != null ? spec : ""); } else { wcsParam.put("MaterialCode", ""); @@ -244,11 +249,10 @@ public class AgvTaskResultServiceImpl implements IAgvTaskResultService { wcsParam.put("Specification", ""); } - wcsParam.put("Mlocation", task.getSourceName()); // 来源库位 - wcsParam.put("Tlocation", task.getTargetName()); // 目标库位 - wcsParam.put("outBack", 1); // 固定值 + wcsParam.put("Mlocation", ddTask.getSourceName()); + wcsParam.put("Tlocation", ddTask.getTargetName()); + wcsParam.put("outBack", 1); - log.info("[下架] 调用 WCS 接口: {}", wcsParam.toJSONString()); String wcsResp = HttpUtils.sendPost(wcsJobCreateUrl, wcsParam.toJSONString()); if (StringUtils.isBlank(wcsResp)) { throw new ServiceException("WCS接口无响应"); @@ -259,34 +263,51 @@ public class AgvTaskResultServiceImpl implements IAgvTaskResultService { throw new ServiceException("WCS下架失败:" + wcsJson.getString("msg")); } - // 3. 记录 WCS 任务 + // 3️⃣ 记录 WCS 任务 WcsTaskResult wcs = new WcsTaskResult(); wcs.setTaskId(wcsTaskId); - wcs.setTaskStatus("3"); // 等待下架 + wcs.setTaskStatus("3"); // WCS已接收 wcs.setMsg(wcsJson.getString("msg")); wcs.setOwner("wms"); wcs.setType("2"); wcs.setPriority("1"); - wcs.setSourceName(task.getSourceName()); - wcs.setTargetName(task.getTargetName()); + wcs.setSourceName(ddTask.getSourceName()); + wcs.setTargetName(ddTask.getTargetName()); wcs.setIsDelete("0"); wcs.setCreateBy(userId); wcs.setCreateTime(now); wcs.setUpdateTime(now); wcsTaskResultMapper.insertWcsTaskResult(wcs); - // 4. 调用 AGV 出库搬运(下架点 → 出口) + // ========================= + // 4️⃣ 仅 WCS 模式:直接结束 + // ========================= + if (dispatchMode == 1) { + log.info("[下架] 仅WCS调度,任务结束 taskNo={}", taskNo); + return; + } + + // ========================= + // 5️⃣ WCS + AGV 模式 + // ========================= + + // 查 AGV 原任务(只在 mode=2 时) + AgvTaskResult agvOrigin = agvTaskResultMapper.selectByTaskNo(taskNo); + if (agvOrigin == null) { + throw new ServiceException("未找到 AGV 原任务:" + taskNo); + } + String agvRequestId = taskNo + "12"; + JSONObject agvParam = new JSONObject(); agvParam.put("requestId", agvRequestId); agvParam.put("taskNo", taskNo); agvParam.put("owner", "wms"); - agvParam.put("type", 2); // 下架搬运 + agvParam.put("type", 2); agvParam.put("priority", "1"); - agvParam.put("sourceName", task.getTargetName()); + agvParam.put("sourceName", ddTask.getTargetName()); agvParam.put("targetName", "出口口A"); - log.info("[下架] 调用 AGV 接口: {}", agvParam.toJSONString()); String agvResp = HttpUtils.sendPost(agvJobCreateUrl, agvParam.toJSONString()); if (StringUtils.isBlank(agvResp)) { throw new ServiceException("AGV接口无响应"); @@ -297,16 +318,15 @@ public class AgvTaskResultServiceImpl implements IAgvTaskResultService { throw new ServiceException("AGV搬运失败:" + agvJson.getString("msg")); } - // 5. 保存 AGV 出库搬运记录 AgvTaskResult agv = new AgvTaskResult(); agv.setRequestId(agvRequestId); + agv.setTaskNo(taskNo); agv.setStatus("CREATED"); agv.setMsg(agvJson.getString("msg")); agv.setOwner("wms"); agv.setType("2"); agv.setPriority("1"); - agv.setTaskNo(taskNo); - agv.setSourceName(task.getTargetName()); + agv.setSourceName(ddTask.getTargetName()); agv.setTargetName("出口口A"); agv.setIsDelete("0"); agv.setCreateBy(userId); @@ -314,16 +334,14 @@ public class AgvTaskResultServiceImpl implements IAgvTaskResultService { agv.setUpdateTime(now); agvTaskResultMapper.insertAgvTaskResult(agv); - // 6. 保存 AGV/WCS 对应关系 AgyWcs mapping = new AgyWcs(); mapping.setRequestId(agvRequestId); mapping.setTaskId(wcsTaskId); agyWcsMapper.insertAgvWcs(mapping); - - log.info("[下架] AGV 搬运任务提交成功"); } + @Override public boolean existsByRequestIdAndStatus(String requestId, String status) { return agvTaskResultMapper.countByRequestIdAndStatus(requestId, status) > 0; diff --git a/src/main/java/com/zg/project/wisdom/service/impl/DdTaskServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/DdTaskServiceImpl.java index 2f0d6d7..799ad47 100644 --- a/src/main/java/com/zg/project/wisdom/service/impl/DdTaskServiceImpl.java +++ b/src/main/java/com/zg/project/wisdom/service/impl/DdTaskServiceImpl.java @@ -7,6 +7,7 @@ import com.zg.common.utils.DateUtils; import com.zg.common.utils.SecurityUtils; import com.zg.common.utils.StringUtils; import com.zg.common.utils.http.OkHttpUtils; +import com.zg.common.utils.uuid.IdUtils; import com.zg.project.information.mapper.MtdMapper; import com.zg.project.wisdom.domain.AgvTaskResult; import com.zg.project.wisdom.domain.AgyWcs; @@ -54,9 +55,6 @@ public class DdTaskServiceImpl implements IDdTaskService { @Value("${agv.job.create-url}") private String agvJobCreateUrl; - @Value("${wcs.job.create-url}") - private String wcsJobCreateUrl; - @Value("${wcs.job.chuku-url}") private String wcsJobChukuUrl; @@ -78,31 +76,38 @@ public class DdTaskServiceImpl implements IDdTaskService { @Override public int insertDdTask(DdTask ddTask) { - // 1. 生成任务编号(如 DD20250701143059001) + + // 1. 生成任务编号 String taskNo = "DD" + DateUtils.dateTimeNow("yyyyMMddHHmmssSSS"); ddTask.setTaskNo(taskNo); - // 2. 判断 mid 是否为空,设置 midStatus(0空托盘,1有货) - if (ddTask.getMid() == null || ddTask.getMid().trim().isEmpty()) { + // 2. midStatus 判断 + if (StringUtils.isBlank(ddTask.getMid())) { ddTask.setMidStatus(0); } else { ddTask.setMidStatus(1); } - // 3. 设置任务初始状态为待建(0) + // 3. 初始任务状态 ddTask.setTaskStatus(0); - // 4. 设置操作员和审核员(从当前登录用户上下文中获取) + // 4. 调度模式校验(1=仅WCS,2=WCS+AGV) +// if (ddTask.getDispatchMode() == null +// || (ddTask.getDispatchMode() != 1 && ddTask.getDispatchMode() != 2)) { +// throw new ServiceException("dispatchMode非法,只能是 1(仅立库) 或 2(立库+AGV)"); +// } + ddTask.setDispatchMode(2); + + // 5. 操作人 / 审核人 Long userId = SecurityUtils.getUserId(); ddTask.setOperator(userId.toString()); ddTask.setApprover(userId.toString()); - // 5. 设置接收时间 rcptim 和初始化字段 + // 6. 其他初始化 ddTask.setRcptim(DateUtils.getNowDate()); ddTask.setRid(""); ddTask.setDoCount(0); - // 6. 设置通用字段 ddTask.setCreateBy(userId.toString()); ddTask.setCreateTime(DateUtils.getNowDate()); ddTask.setUpdateBy(userId.toString()); @@ -112,6 +117,65 @@ public class DdTaskServiceImpl implements IDdTaskService { return ddTaskMapper.insertDdTask(ddTask); } + @Override + @Transactional(rollbackFor = Exception.class) + public int insertDdTaskBatch(List list) { + + if (list == null || list.isEmpty()) { + throw new ServiceException("调度任务数据不能为空"); + } + + Long userId = SecurityUtils.getUserId(); + Date now = DateUtils.getNowDate(); + + for (DdTask ddTask : list) { + + // 1️⃣ 生成唯一任务号 + String taskNo = "DD" + IdUtils.fastSimpleUUID(); + ddTask.setTaskNo(taskNo); + + // 2️⃣ 调度模式校验(1 / 2) + if (ddTask.getDispatchMode() == null + || (ddTask.getDispatchMode() != 1 && ddTask.getDispatchMode() != 2)) { + throw new ServiceException("dispatchMode非法,只能是 1(仅立库) 或 2(立库+AGV)"); + } + + // 3️⃣ midStatus + if (StringUtils.isBlank(ddTask.getMid())) { + ddTask.setMidStatus(0); + } else { + ddTask.setMidStatus(1); + } + + // 4️⃣ 初始状态 + ddTask.setTaskStatus(0); + + // 5️⃣ 操作员 / 审核员 + ddTask.setOperator(userId.toString()); + ddTask.setApprover(userId.toString()); + + // 6️⃣ 业务字段兜底 + if (ddTask.getRcptim() == null) { + ddTask.setRcptim(now); + } + if (ddTask.getRid() == null) { + ddTask.setRid(""); + } + if (ddTask.getDoCount() == null) { + ddTask.setDoCount(0); + } + + // 7️⃣ 通用字段 + ddTask.setCreateBy(userId.toString()); + ddTask.setCreateTime(now); + ddTask.setUpdateBy(userId.toString()); + ddTask.setUpdateTime(now); + ddTask.setIsDelete("0"); + } + + return ddTaskMapper.batchInsertDdTask(list); + } + @Override public int updateDdTask(DdTask ddTask) { return ddTaskMapper.updateDdTask(ddTask); @@ -219,53 +283,74 @@ public class DdTaskServiceImpl implements IDdTaskService { wcs.setUpdateTime(now); wcsTaskResultMapper.insertWcsTaskResult(wcs); - // 调用 AGV(使用 OkHttpUtils) - requestId = taskNo + "12"; - JSONObject agvParam = new JSONObject(); - agvParam.put("owner", "wms"); - agvParam.put("type", "1"); - agvParam.put("priority", 1); - agvParam.put("sourceName", "v01-010101"); - agvParam.put("targetName", targetName); - agvParam.put("taskNo", taskNo); - agvParam.put("requestId", requestId); + // ✅ 新增:出库时根据 dispatch_mode 决定是否走 AGV + // dispatch_mode:1=仅立库(不走AGV),2=立库+AGV(走AGV) + Integer dispatchMode = task.getDispatchMode(); + if (Integer.valueOf(1).equals(dispatchMode) || dispatchMode == null) { + log.info("[任务执行] 出库任务,dispatch_mode={}(1=仅立库/空=默认仅立库),跳过 AGV 调度", dispatchMode); + task.setTaskStatus(2); // 已完成 + task.setDoCount(Optional.ofNullable(task.getDoCount()).orElse(0) + 1); + task.setUpdateBy(userId); + task.setUpdateTime(now); + ddTaskMapper.updateDdTask(task); - log.info("[任务执行] 出库 → 调用AGV,requestId={}, param={}", requestId, agvParam); - response = OkHttpUtils.postJson(agvJobCreateUrl, agvParam.toJSONString()); + log.info("[任务执行] 仅立库模式,立库调用成功即完成 taskNo={}", taskNo); - try { - respJson = JSON.parseObject(response); - code = respJson.getInteger("code"); - msg = respJson.getString("msg"); - } catch (Exception e) { - throw new ServiceException("AGV响应解析失败: " + response); + // ❗非常关键:直接 return,后面不要再把状态改成 1 + TaskExecuteResultVO vo = new TaskExecuteResultVO(); + vo.setRequestId(null); + vo.setCode(200); + vo.setMsg("仅立库模式,任务已完成"); + return vo; + } else { + // 调用 AGV(使用 OkHttpUtils) + requestId = taskNo + "12"; + JSONObject agvParam = new JSONObject(); + agvParam.put("owner", "wms"); + agvParam.put("type", "1"); + agvParam.put("priority", 1); + agvParam.put("sourceName", "v01-010101"); + agvParam.put("targetName", targetName); + agvParam.put("taskNo", taskNo); + agvParam.put("requestId", requestId); + + log.info("[任务执行] 出库 → 调用AGV,requestId={}, param={}", requestId, agvParam); + response = OkHttpUtils.postJson(agvJobCreateUrl, agvParam.toJSONString()); + + try { + respJson = JSON.parseObject(response); + code = respJson.getInteger("code"); + msg = respJson.getString("msg"); + } catch (Exception e) { + throw new ServiceException("AGV响应解析失败: " + response); + } + + if (code != 200) { + throw new ServiceException("AGV任务执行失败: " + msg); + } + + AgvTaskResult agv = new AgvTaskResult(); + agv.setRequestId(requestId); + agv.setTaskNo(taskNo); + agv.setStatus("CREATED"); + agv.setMsg(msg); + agv.setOwner("wms"); + agv.setType(taskType); + agv.setPriority("1"); + agv.setSourceName(task.getSourceName()); + agv.setTargetName(targetName); + agv.setIsDelete("0"); + agv.setCreateBy(userId); + agv.setCreateTime(now); + agv.setUpdateTime(now); + agvTaskResultMapper.insertAgvTaskResult(agv); + + AgyWcs mapping = new AgyWcs(); + mapping.setRequestId(requestId); + mapping.setTaskId(taskIdParam); + agyWcsMapper.insertAgvWcs(mapping); } - if (code != 200) { - throw new ServiceException("AGV任务执行失败: " + msg); - } - - AgvTaskResult agv = new AgvTaskResult(); - agv.setRequestId(requestId); - agv.setTaskNo(taskNo); - agv.setStatus("CREATED"); - agv.setMsg(msg); - agv.setOwner("wms"); - agv.setType(taskType); - agv.setPriority("1"); - agv.setSourceName(task.getSourceName()); - agv.setTargetName(targetName); - agv.setIsDelete("0"); - agv.setCreateBy(userId); - agv.setCreateTime(now); - agv.setUpdateTime(now); - agvTaskResultMapper.insertAgvTaskResult(agv); - - AgyWcs mapping = new AgyWcs(); - mapping.setRequestId(requestId); - mapping.setTaskId(taskIdParam); - agyWcsMapper.insertAgvWcs(mapping); - } else { // 入库/移库任务 → 调用 AGV(使用 OkHttpUtils) requestId = taskNo + ("0".equals(taskType) ? "11" : "12"); @@ -324,4 +409,5 @@ public class DdTaskServiceImpl implements IDdTaskService { return vo; } - } + +} diff --git a/src/main/java/com/zg/project/wisdom/service/impl/WcsTaskResultServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/WcsTaskResultServiceImpl.java index ea83b4d..8f22ba4 100644 --- a/src/main/java/com/zg/project/wisdom/service/impl/WcsTaskResultServiceImpl.java +++ b/src/main/java/com/zg/project/wisdom/service/impl/WcsTaskResultServiceImpl.java @@ -1,35 +1,94 @@ package com.zg.project.wisdom.service.impl; import com.zg.common.utils.DateUtils; +import com.zg.project.wisdom.domain.DdTask; import com.zg.project.wisdom.domain.WcsTaskResult; +import com.zg.project.wisdom.mapper.AgvTaskResultMapper; +import com.zg.project.wisdom.mapper.DdTaskMapper; import com.zg.project.wisdom.mapper.WcsTaskResultMapper; import com.zg.project.wisdom.service.IWcsTaskResultService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Date; - +@Slf4j @Service public class WcsTaskResultServiceImpl implements IWcsTaskResultService { @Autowired private WcsTaskResultMapper wcsTaskResultMapper; + @Autowired + private DdTaskMapper ddTaskMapper; + @Override + @Transactional(rollbackFor = Exception.class) public void handleWcsCallback(WcsTaskResult dto) { + String taskId = dto.getTaskId(); + Date now = DateUtils.getNowDate(); - WcsTaskResult result = new WcsTaskResult(); - result.setTaskId(taskId); - result.setTaskStatus(dto.getTaskStatus()); - result.setMsg(dto.getMsg()); - result.setCreateBy("wcs"); - result.setCreateTime(DateUtils.getNowDate()); - result.setIsDelete("0"); + // 1️⃣ 先记录 WCS 回调日志 + WcsTaskResult record = new WcsTaskResult(); + record.setTaskId(taskId); + record.setTaskStatus(dto.getTaskStatus()); + record.setMsg(dto.getMsg()); + record.setOwner("wcs"); + record.setIsDelete("0"); + record.setCreateBy("wcs"); + record.setCreateTime(now); + record.setUpdateTime(now); + wcsTaskResultMapper.insertWcsTaskResult(record); - wcsTaskResultMapper.insertWcsTaskResult(result); } +// @Override +// @Transactional(rollbackFor = Exception.class) +// public void handleWcsCallback(WcsTaskResult dto) { +// +// String taskId = dto.getTaskId(); +// Date now = DateUtils.getNowDate(); +// +// // ✅ 仅立库:TaskStatus=1 视为成功,并同步完成主任务 +// if (taskId != null && taskId.length() >= 3) { +// +// String taskNo = taskId.substring(0, taskId.length() - 2); +// DdTask ddTask = ddTaskMapper.selectByTaskNo(taskNo); +// +// if (ddTask != null +// && Integer.valueOf(1).equals(ddTask.getDispatchMode()) +// && "1".equals(dto.getTaskStatus())) { +// +// // 1) 回调日志状态:1 -> 3(成功) +// dto.setTaskStatus("3"); +// +// // 2) 主任务完成:dd_task.task_status -> 2 +// DdTask update = new DdTask(); +// update.setId(ddTask.getId()); +// update.setTaskStatus(2); // 已完成 +// update.setUpdateBy("wcs"); +// update.setUpdateTime(now); +// ddTaskMapper.updateDdTask(update); +// } +// } +// +// // 1️⃣ 先记录 WCS 回调日志(原逻辑不变,只是 dto 可能已被修正为 3) +// WcsTaskResult record = new WcsTaskResult(); +// record.setTaskId(taskId); +// record.setTaskStatus(dto.getTaskStatus()); +// record.setMsg(dto.getMsg()); +// record.setOwner("wcs"); +// record.setIsDelete("0"); +// record.setCreateBy("wcs"); +// record.setCreateTime(now); +// record.setUpdateTime(now); +// wcsTaskResultMapper.insertWcsTaskResult(record); +// } + + + @Override public boolean checkTaskStatusExists(String taskId) { return wcsTaskResultMapper.countByTaskId(taskId) > 0; diff --git a/src/main/resources/application-druid.yml b/src/main/resources/application-druid.yml index 9111b2c..3cde3ee 100644 --- a/src/main/resources/application-druid.yml +++ b/src/main/resources/application-druid.yml @@ -7,9 +7,9 @@ spring: # 主库数据源 master: # url: jdbc:mysql://101.132.133.142:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 - url: jdbc:mysql://192.168.1.28:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# url: jdbc:mysql://192.168.1.28:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 # url: jdbc:mysql://192.168.1.192:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 -# url: jdbc:mysql://192.168.1.251:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + url: jdbc:mysql://192.168.1.251:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 # url: jdbc:mysql://localhost:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: shzg diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d63026f..fb74471 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -169,4 +169,52 @@ mock: #配送系统中调用大模型进行图片识别 qwen-ocr: - base-url: http://192.168.1.253:8087/ocr/extractErpByBase64 \ No newline at end of file + base-url: http://192.168.1.253:8087/ocr/extractErpByBase64 + +# ========================= +# MQTT 配置(EMQX 接入) +# ========================= +mqtt: + # 是否启用 MQTT 功能 + # true:系统启动时自动连接 EMQX 并订阅 Topic + # false:不初始化 MQTT 客户端 + enabled: true + + # MQTT Broker 地址 + # tcp://IP:1883 —— MQTT TCP + # ws://IP:8083 —— MQTT WebSocket + # ssl://IP:8883 —— MQTT SSL + broker: tcp://192.168.1.29:1883 + + # MQTT 客户端 ID(在 EMQX 中唯一) + # 建议:系统名 + 模块名,避免重复 + clientId: zg-wms-backend + + # MQTT 账号(EMQX Dashboard 中配置) + username: demo02 + + # MQTT 密码 + password: admin123 + + # 是否清除会话 + # true :断开后清除订阅和未接收消息(推荐后端使用) + # false :保留会话(适合设备端) + cleanSession: true + + # 心跳时间(秒) + # 客户端与 Broker 保持连接的心跳间隔 + keepAlive: 30 + + # 连接超时时间(秒) + timeout: 10 + + # 默认消息 QoS + # 0:最多一次(不保证到达,性能最好) + # 1:至少一次(推荐,业务数据常用) + # 2:仅一次(最严格,性能最差) + qos: 1 + + # 订阅的 Topic 列表(支持通配符) + # 建议先使用测试 Topic,确认链路正常后再接入真实业务 Topic + topics: + - zg/wms/test/# \ No newline at end of file diff --git a/src/main/resources/mybatis/wisdom/AgvTaskResultMapper.xml b/src/main/resources/mybatis/wisdom/AgvTaskResultMapper.xml index e7bb956..2ec78e8 100644 --- a/src/main/resources/mybatis/wisdom/AgvTaskResultMapper.xml +++ b/src/main/resources/mybatis/wisdom/AgvTaskResultMapper.xml @@ -71,6 +71,9 @@ + INSERT INTO agv_task_result diff --git a/src/main/resources/mybatis/wisdom/DdTaskMapper.xml b/src/main/resources/mybatis/wisdom/DdTaskMapper.xml index 09476c9..d3f0b04 100644 --- a/src/main/resources/mybatis/wisdom/DdTaskMapper.xml +++ b/src/main/resources/mybatis/wisdom/DdTaskMapper.xml @@ -1,34 +1,41 @@ - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + INSERT INTO dd_task ( - task_no, task_dtl, task_type, task_status, mid_status, mid, num, mid_type, source_name, - target_name, operator, approver, rcptim, rid, do_count, prf, - create_by, create_time, update_by, update_time, is_delete + task_no, + task_dtl, + task_type, + task_status, + mid_status, + mid, + num, + mid_type, + source_name, + target_name, + operator, + approver, + rcptim, + rid, + do_count, + prf, + dispatch_mode, + create_by, + create_time, + update_by, + update_time, + is_delete ) VALUES ( - #{taskNo}, #{taskDtl}, #{taskType}, #{taskStatus}, #{midStatus}, #{mid}, #{num}, #{midType}, #{sourceName}, - #{targetName}, #{operator}, #{approver}, #{rcptim}, #{rid}, #{doCount}, #{prf}, - #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{isDelete} + #{taskNo}, + #{taskDtl}, + #{taskType}, + #{taskStatus}, + #{midStatus}, + #{mid}, + #{num}, + #{midType}, + #{sourceName}, + #{targetName}, + #{operator}, + #{approver}, + #{rcptim}, + #{rid}, + #{doCount}, + #{prf}, + #{dispatchMode}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{isDelete} ) - + + + INSERT INTO dd_task ( + task_no, + task_dtl, + task_type, + task_status, + mid_status, + mid, + num, + mid_type, + source_name, + target_name, + operator, + approver, + rcptim, + rid, + do_count, + prf, + dispatch_mode, + create_by, + create_time, + update_by, + update_time, + is_delete + ) + VALUES + + ( + #{item.taskNo}, + #{item.taskDtl}, + #{item.taskType}, + #{item.taskStatus}, + #{item.midStatus}, + #{item.mid}, + #{item.num}, + #{item.midType}, + #{item.sourceName}, + #{item.targetName}, + #{item.operator}, + #{item.approver}, + #{item.rcptim}, + #{item.rid}, + #{item.doCount}, + #{item.prf}, + #{item.dispatchMode}, + #{item.createBy}, + #{item.createTime}, + #{item.updateBy}, + #{item.updateTime}, + #{item.isDelete} + ) + + + + + UPDATE dd_task - SET task_no = #{taskNo}, + SET task_dtl = #{taskDtl}, task_type = #{taskType}, task_status = #{taskStatus}, @@ -85,12 +198,14 @@ rid = #{rid}, do_count = #{doCount}, prf = #{prf}, + dispatch_mode = #{dispatchMode}, update_by = #{updateBy}, - update_time = #{updateTime}, - is_delete = #{isDelete} + update_time = #{updateTime} WHERE id = #{id} + AND is_delete = '0' + UPDATE dd_task SET task_status = #{status}, @@ -99,15 +214,19 @@ AND is_delete = '0' + - DELETE FROM dd_task WHERE id IN + DELETE FROM dd_task + WHERE id IN #{id} + - DELETE FROM dd_task WHERE id = #{id} + DELETE FROM dd_task + WHERE id = #{id}