diff --git a/pom.xml b/pom.xml
index 7eb009f..d3a4787 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,11 +39,10 @@
1.2.13
5.7.14
5.3.39
-
-
1.18.32
1.2.5
1.7.36
+ 7.2.5
@@ -237,9 +236,8 @@
-
-
+
org.slf4j
slf4j-api
@@ -281,6 +279,24 @@
3.5.3
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+ com.itextpdf
+ kernel
+ ${itext.version}
+
+
+
+ com.itextpdf
+ layout
+ ${itext.version}
+
+
diff --git a/src/main/java/com/shzg/common/utils/DeptScopeUtils.java b/src/main/java/com/shzg/common/utils/DeptScopeUtils.java
new file mode 100644
index 0000000..607357b
--- /dev/null
+++ b/src/main/java/com/shzg/common/utils/DeptScopeUtils.java
@@ -0,0 +1,23 @@
+package com.shzg.common.utils;
+
+import com.shzg.common.utils.spring.SpringUtils;
+import com.shzg.project.system.service.ISysDeptService;
+
+import java.util.List;
+
+/**
+ * 部门数据范围工具类
+ */
+public class DeptScopeUtils
+{
+ /**
+ * 获取当前用户部门 + 子部门ID集合
+ */
+ public static List getDeptScope()
+ {
+ Long deptId = SecurityUtils.getDeptId();
+
+ return SpringUtils.getBean(ISysDeptService.class)
+ .selectDeptAndChildIds(deptId);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/framework/config/SecurityConfig.java b/src/main/java/com/shzg/framework/config/SecurityConfig.java
index 4f85d56..61b2bc0 100644
--- a/src/main/java/com/shzg/framework/config/SecurityConfig.java
+++ b/src/main/java/com/shzg/framework/config/SecurityConfig.java
@@ -115,6 +115,9 @@ public class SecurityConfig
"/register",
"/unique/code/**",
"/device/cmd/**",
+ "/worn/socket/**",
+ "/worn/inboundBill/**",
+ "/ws/**",
"/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
diff --git a/src/main/java/com/shzg/framework/web/domain/BaseEntity.java b/src/main/java/com/shzg/framework/web/domain/BaseEntity.java
index 679de9d..77f546a 100644
--- a/src/main/java/com/shzg/framework/web/domain/BaseEntity.java
+++ b/src/main/java/com/shzg/framework/web/domain/BaseEntity.java
@@ -4,13 +4,15 @@ import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
+import java.util.List;
+
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* Entity基类
- *
+ *
* @author ruoyi
*/
public class BaseEntity implements Serializable
@@ -42,6 +44,12 @@ public class BaseEntity implements Serializable
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map params;
+ /**
+ * 部门数据范围(用于数据隔离)
+ */
+ @JsonIgnore
+ private List dataScopeDeptIds;
+
public String getSearchValue()
{
return searchValue;
@@ -115,4 +123,14 @@ public class BaseEntity implements Serializable
{
this.params = params;
}
-}
+
+ public List getDataScopeDeptIds()
+ {
+ return dataScopeDeptIds;
+ }
+
+ public void setDataScopeDeptIds(List dataScopeDeptIds)
+ {
+ this.dataScopeDeptIds = dataScopeDeptIds;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java b/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java
index 480d2ce..a45694f 100644
--- a/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java
+++ b/src/main/java/com/shzg/project/system/mapper/SysDeptMapper.java
@@ -124,4 +124,12 @@ public interface SysDeptMapper
*/
public List selectDeptAndChildren(@Param("deptId") Long deptId,
@Param("keyword") String keyword);
+
+ /**
+ * 根据部门ID查询当前部门及其所有子部门ID集合
+ *
+ * @param deptId 部门ID
+ * @return 部门ID集合
+ */
+ List selectDeptAndChildIds(Long deptId);
}
diff --git a/src/main/java/com/shzg/project/system/service/ISysDeptService.java b/src/main/java/com/shzg/project/system/service/ISysDeptService.java
index f4a1127..52a13cf 100644
--- a/src/main/java/com/shzg/project/system/service/ISysDeptService.java
+++ b/src/main/java/com/shzg/project/system/service/ISysDeptService.java
@@ -129,4 +129,12 @@ public interface ISysDeptService
* @return 部门信息
*/
public List selectDeptAndChildrenByUserId(Long userId, String keyword);
+
+ /**
+ * 根据部门ID查询部门以及子部门ID列表
+ *
+ * @param deptId 部门ID
+ * @return 部门ID列表
+ */
+ List selectDeptAndChildIds(Long deptId);
}
diff --git a/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java b/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java
index 0c16b28..4815443 100644
--- a/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java
+++ b/src/main/java/com/shzg/project/system/service/impl/SysDeptServiceImpl.java
@@ -354,4 +354,10 @@ public class SysDeptServiceImpl implements ISysDeptService
return deptMapper.selectDeptAndChildren(deptId, keyword);
}
+
+ @Override
+ public List selectDeptAndChildIds(Long deptId)
+ {
+ return deptMapper.selectDeptAndChildIds(deptId);
+ }
}
diff --git a/src/main/java/com/shzg/project/unique/service/impl/WornUniqueCodeServiceImpl.java b/src/main/java/com/shzg/project/unique/service/impl/WornUniqueCodeServiceImpl.java
index 450cba6..c26992a 100644
--- a/src/main/java/com/shzg/project/unique/service/impl/WornUniqueCodeServiceImpl.java
+++ b/src/main/java/com/shzg/project/unique/service/impl/WornUniqueCodeServiceImpl.java
@@ -4,6 +4,7 @@ import java.util.List;
import com.shzg.common.exception.ServiceException;
import com.shzg.common.utils.DateUtils;
+import com.shzg.common.utils.DeptScopeUtils;
import com.shzg.common.utils.SecurityUtils;
import com.shzg.common.utils.StringUtils;
import com.shzg.framework.aspectj.lang.annotation.DataScope;
@@ -89,13 +90,12 @@ public class WornUniqueCodeServiceImpl implements IWornUniqueCodeService
* @param wornUniqueCode 唯一码管理
* @return 唯一码管理
*/
- @DataScope(deptAlias = "d")
@Override
public List selectWornUniqueCodeList(WornUniqueCode wornUniqueCode)
{
if (!SecurityUtils.isAdmin())
{
- wornUniqueCode.setProjectId(SecurityUtils.getDeptId());
+ wornUniqueCode.setDataScopeDeptIds(DeptScopeUtils.getDeptScope());
}
return wornUniqueCodeMapper.selectWornUniqueCodeList(wornUniqueCode);
}
diff --git a/src/main/java/com/shzg/project/worn/controller/DeviceCommandController.java b/src/main/java/com/shzg/project/worn/controller/DeviceCommandController.java
deleted file mode 100644
index 6b283df..0000000
--- a/src/main/java/com/shzg/project/worn/controller/DeviceCommandController.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.shzg.project.worn.controller;
-
-import com.shzg.framework.web.controller.BaseController;
-import com.shzg.framework.web.domain.AjaxResult;
-import com.shzg.project.worn.service.IDeviceCommandService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/device/cmd")
-public class DeviceCommandController extends BaseController {
-
- @Autowired
- private IDeviceCommandService deviceCommandService;
-
- /**
- * 烟雾传感器 - 持续消警
- */
- @PostMapping("/smoke/stop")
- public AjaxResult smokeStop(@RequestParam Long deviceId) {
- deviceCommandService.smokeStop(deviceId);
- return AjaxResult.success("消警指令已发送");
- }
-
- /**
- * 烟雾传感器 - 单次消警(80秒)
- */
- @PostMapping("/smoke/stopOnce")
- public AjaxResult smokeStopOnce(@RequestParam Long deviceId) {
- deviceCommandService.smokeStopOnce(deviceId);
- return AjaxResult.success("单次消警指令已发送");
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/controller/MqttSensorDataController.java b/src/main/java/com/shzg/project/worn/controller/MqttSensorDataController.java
index 8a20fee..8526015 100644
--- a/src/main/java/com/shzg/project/worn/controller/MqttSensorDataController.java
+++ b/src/main/java/com/shzg/project/worn/controller/MqttSensorDataController.java
@@ -2,6 +2,8 @@ package com.shzg.project.worn.controller;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
+
+import com.shzg.common.utils.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
diff --git a/src/main/java/com/shzg/project/worn/controller/MqttTopicConfigController.java b/src/main/java/com/shzg/project/worn/controller/MqttTopicConfigController.java
new file mode 100644
index 0000000..a17812c
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/controller/MqttTopicConfigController.java
@@ -0,0 +1,104 @@
+package com.shzg.project.worn.controller;
+
+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 com.shzg.framework.aspectj.lang.annotation.Log;
+import com.shzg.framework.aspectj.lang.enums.BusinessType;
+import com.shzg.project.worn.domain.MqttTopicConfig;
+import com.shzg.project.worn.service.IMqttTopicConfigService;
+import com.shzg.framework.web.controller.BaseController;
+import com.shzg.framework.web.domain.AjaxResult;
+import com.shzg.common.utils.poi.ExcelUtil;
+import com.shzg.framework.web.page.TableDataInfo;
+
+/**
+ * MQTT主题配置Controller
+ *
+ * @author shzg
+ * @date 2026-04-08
+ */
+@RestController
+@RequestMapping("/worn/mqttConfig")
+public class MqttTopicConfigController extends BaseController
+{
+ @Autowired
+ private IMqttTopicConfigService mqttTopicConfigService;
+
+ /**
+ * 查询MQTT主题配置列表
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(MqttTopicConfig mqttTopicConfig)
+ {
+ startPage();
+ List list = mqttTopicConfigService.selectMqttTopicConfigList(mqttTopicConfig);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出MQTT主题配置列表
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:export')")
+ @Log(title = "MQTT主题配置", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, MqttTopicConfig mqttTopicConfig)
+ {
+ List list = mqttTopicConfigService.selectMqttTopicConfigList(mqttTopicConfig);
+ ExcelUtil util = new ExcelUtil(MqttTopicConfig.class);
+ util.exportExcel(response, list, "MQTT主题配置数据");
+ }
+
+ /**
+ * 获取MQTT主题配置详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:query')")
+ @GetMapping(value = "/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ return success(mqttTopicConfigService.selectMqttTopicConfigById(id));
+ }
+
+ /**
+ * 新增MQTT主题配置
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:add')")
+ @Log(title = "MQTT主题配置", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody MqttTopicConfig mqttTopicConfig)
+ {
+ return toAjax(mqttTopicConfigService.insertMqttTopicConfig(mqttTopicConfig));
+ }
+
+ /**
+ * 修改MQTT主题配置
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:edit')")
+ @Log(title = "MQTT主题配置", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody MqttTopicConfig mqttTopicConfig)
+ {
+ return toAjax(mqttTopicConfigService.updateMqttTopicConfig(mqttTopicConfig));
+ }
+
+ /**
+ * 删除MQTT主题配置
+ */
+ @PreAuthorize("@ss.hasPermi('worn:mqttConfig:remove')")
+ @Log(title = "MQTT主题配置", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(mqttTopicConfigService.deleteMqttTopicConfigByIds(ids));
+ }
+}
diff --git a/src/main/java/com/shzg/project/worn/controller/SocketController.java b/src/main/java/com/shzg/project/worn/controller/SocketController.java
new file mode 100644
index 0000000..04b621c
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/controller/SocketController.java
@@ -0,0 +1,31 @@
+package com.shzg.project.worn.controller;
+
+import com.shzg.framework.web.domain.AjaxResult;
+import com.shzg.project.worn.service.SocketControlService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/worn/socket")
+public class SocketController {
+
+ @Autowired
+ private SocketControlService socketControlService;
+
+ /**
+ * 控制插座开关
+ */
+ @PostMapping("/control")
+ public AjaxResult control(@RequestParam String devEui,
+ @RequestParam Integer status) {
+
+ boolean on = status == 1;
+
+ socketControlService.controlSocket(devEui, on);
+
+ return AjaxResult.success("指令已发送");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/controller/WebSocketTestController.java b/src/main/java/com/shzg/project/worn/controller/WebSocketTestController.java
new file mode 100644
index 0000000..141656e
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/controller/WebSocketTestController.java
@@ -0,0 +1,31 @@
+package com.shzg.project.worn.controller;
+
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/ws/test")
+public class WebSocketTestController {
+
+ @Autowired
+ private WebSocketSessionManager sessionManager;
+
+ /**
+ * 测试推送(广播)
+ */
+ @GetMapping("/broadcast")
+ public String broadcast() {
+ sessionManager.sendAll("🔥 测试广播消息");
+ return "OK";
+ }
+
+ /**
+ * 测试按dept推送
+ */
+ @GetMapping("/dept/{deptId}")
+ public String sendToDept(@PathVariable Long deptId) {
+ sessionManager.sendToDept(deptId, "📦 这是dept推送消息");
+ return "OK";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/controller/WornInboundBillController.java b/src/main/java/com/shzg/project/worn/controller/WornInboundBillController.java
index b640d50..4b93597 100644
--- a/src/main/java/com/shzg/project/worn/controller/WornInboundBillController.java
+++ b/src/main/java/com/shzg/project/worn/controller/WornInboundBillController.java
@@ -113,4 +113,19 @@ public class WornInboundBillController extends BaseController
{
return toAjax(wornInboundBillService.voidBill(wornInboundBill.getBillNo()));
}
+
+ /**
+ * 打印入库单
+ */
+ @GetMapping("/print/{id}")
+ public void print(@PathVariable Long id, HttpServletResponse response) throws Exception
+ {
+ byte[] pdf = wornInboundBillService.generatePdf(id);
+
+ response.setContentType("application/pdf");
+ response.setHeader("Content-Disposition", "inline; filename=inbound.pdf");
+
+ response.getOutputStream().write(pdf);
+ }
+
}
diff --git a/src/main/java/com/shzg/project/worn/domain/MqttSensorData.java b/src/main/java/com/shzg/project/worn/domain/MqttSensorData.java
index 94490bc..ea7bc35 100644
--- a/src/main/java/com/shzg/project/worn/domain/MqttSensorData.java
+++ b/src/main/java/com/shzg/project/worn/domain/MqttSensorData.java
@@ -1,6 +1,7 @@
package com.shzg.project.worn.domain;
import java.math.BigDecimal;
+import java.util.List;
import com.shzg.framework.web.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -9,9 +10,6 @@ import com.shzg.framework.aspectj.lang.annotation.Excel;
/**
* 设备数据对象 mqtt_sensor_data
- *
- * @author shzg
- * @date 2026-04-01
*/
public class MqttSensorData extends BaseEntity
{
@@ -24,16 +22,22 @@ public class MqttSensorData extends BaseEntity
@Excel(name = "设备ID")
private Long deviceId;
+ /** 部门ID */
+ @Excel(name = "部门ID")
+ private Long deptId;
+
+ private List deptIds;
+
/** MQTT Topic */
@Excel(name = "MQTT Topic")
private String topic;
- /** 项目(如 tangshan) */
- @Excel(name = "项目", readConverterExp = "如=,t=angshan")
+ /** 项目 */
+ @Excel(name = "项目")
private String project;
- /** 仓库(如 dianchi) */
- @Excel(name = "仓库", readConverterExp = "如=,d=ianchi")
+ /** 仓库 */
+ @Excel(name = "仓库")
private String warehouse;
/** 原始消息 */
@@ -68,187 +72,86 @@ public class MqttSensorData extends BaseEntity
@Excel(name = "烟雾浓度")
private Long concentration;
- /** ✅ 水浸状态(0=正常,1=报警) */
+ /** 水浸状态 */
@Excel(name = "水浸状态")
private Integer waterStatus;
+ /** 插座状态(0关 1开) */
+ @Excel(name = "插座状态")
+ private Integer switchStatus;
+
/** 删除标识 */
@Excel(name = "删除标识")
private String isDelete;
- public void setId(Long id)
- {
- this.id = id;
- }
+ // ================== getter / setter ==================
- public Long getId()
- {
- return id;
- }
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
- public void setDeviceId(Long deviceId)
- {
- this.deviceId = deviceId;
- }
+ public Long getDeviceId() { return deviceId; }
+ public void setDeviceId(Long deviceId) { this.deviceId = deviceId; }
- public Long getDeviceId()
- {
- return deviceId;
- }
+ public Long getDeptId() { return deptId; }
+ public void setDeptId(Long deptId) { this.deptId = deptId; }
- public void setTopic(String topic)
- {
- this.topic = topic;
- }
+ public List getDeptIds() { return deptIds; }
+ public void setDeptIds(List deptIds) { this.deptIds = deptIds; }
- public String getTopic()
- {
- return topic;
- }
+ public String getTopic() { return topic; }
+ public void setTopic(String topic) { this.topic = topic; }
- public void setProject(String project)
- {
- this.project = project;
- }
+ public String getProject() { return project; }
+ public void setProject(String project) { this.project = project; }
- public String getProject()
- {
- return project;
- }
+ public String getWarehouse() { return warehouse; }
+ public void setWarehouse(String warehouse) { this.warehouse = warehouse; }
- public void setWarehouse(String warehouse)
- {
- this.warehouse = warehouse;
- }
+ public String getPayload() { return payload; }
+ public void setPayload(String payload) { this.payload = payload; }
- public String getWarehouse()
- {
- return warehouse;
- }
+ public String getDataJson() { return dataJson; }
+ public void setDataJson(String dataJson) { this.dataJson = dataJson; }
- public void setPayload(String payload)
- {
- this.payload = payload;
- }
+ public Long getBattery() { return battery; }
+ public void setBattery(Long battery) { this.battery = battery; }
- public String getPayload()
- {
- return payload;
- }
+ public BigDecimal getTemperature() { return temperature; }
+ public void setTemperature(BigDecimal temperature) { this.temperature = temperature; }
- public void setDataJson(String dataJson)
- {
- this.dataJson = dataJson;
- }
+ public BigDecimal getHumidity() { return humidity; }
+ public void setHumidity(BigDecimal humidity) { this.humidity = humidity; }
- public String getDataJson()
- {
- return dataJson;
- }
+ public BigDecimal getNh3() { return nh3; }
+ public void setNh3(BigDecimal nh3) { this.nh3 = nh3; }
- public void setBattery(Long battery)
- {
- this.battery = battery;
- }
+ public BigDecimal getH2s() { return h2s; }
+ public void setH2s(BigDecimal h2s) { this.h2s = h2s; }
- public Long getBattery()
- {
- return battery;
- }
+ public Long getConcentration() { return concentration; }
+ public void setConcentration(Long concentration) { this.concentration = concentration; }
- public void setTemperature(BigDecimal temperature)
- {
- this.temperature = temperature;
- }
+ public Integer getWaterStatus() { return waterStatus; }
+ public void setWaterStatus(Integer waterStatus) { this.waterStatus = waterStatus; }
- public BigDecimal getTemperature()
- {
- return temperature;
- }
+ public Integer getSwitchStatus() { return switchStatus; }
+ public void setSwitchStatus(Integer switchStatus) { this.switchStatus = switchStatus; }
- public void setHumidity(BigDecimal humidity)
- {
- this.humidity = humidity;
- }
+ public String getIsDelete() { return isDelete; }
+ public void setIsDelete(String isDelete) { this.isDelete = isDelete; }
- public BigDecimal getHumidity()
- {
- return humidity;
- }
-
- public void setNh3(BigDecimal nh3)
- {
- this.nh3 = nh3;
- }
-
- public BigDecimal getNh3()
- {
- return nh3;
- }
-
- public void setH2s(BigDecimal h2s)
- {
- this.h2s = h2s;
- }
-
- public BigDecimal getH2s()
- {
- return h2s;
- }
-
- public void setConcentration(Long concentration)
- {
- this.concentration = concentration;
- }
-
- public Long getConcentration()
- {
- return concentration;
- }
-
- public void setWaterStatus(Integer waterStatus)
- {
- this.waterStatus = waterStatus;
- }
-
- public Integer getWaterStatus()
- {
- return waterStatus;
- }
-
- public void setIsDelete(String isDelete)
- {
- this.isDelete = isDelete;
- }
-
- public String getIsDelete()
- {
- return isDelete;
- }
+ // ================== toString ==================
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("deviceId", getDeviceId())
+ .append("deptId", getDeptId())
.append("topic", getTopic())
- .append("project", getProject())
- .append("warehouse", getWarehouse())
.append("payload", getPayload())
- .append("dataJson", getDataJson())
- .append("battery", getBattery())
- .append("temperature", getTemperature())
- .append("humidity", getHumidity())
- .append("nh3", getNh3())
- .append("h2s", getH2s())
- .append("concentration", getConcentration())
- .append("waterStatus", getWaterStatus())
- .append("remark", getRemark())
- .append("createBy", getCreateBy())
+ .append("switchStatus", getSwitchStatus())
.append("createTime", getCreateTime())
- .append("updateBy", getUpdateBy())
- .append("updateTime", getUpdateTime())
- .append("isDelete", getIsDelete())
.toString();
}
}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/domain/MqttSensorEvent.java b/src/main/java/com/shzg/project/worn/domain/MqttSensorEvent.java
index 3fa568e..acaa9e3 100644
--- a/src/main/java/com/shzg/project/worn/domain/MqttSensorEvent.java
+++ b/src/main/java/com/shzg/project/worn/domain/MqttSensorEvent.java
@@ -5,9 +5,11 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.shzg.framework.aspectj.lang.annotation.Excel;
+import java.util.List;
+
/**
* 设备事件对象 mqtt_sensor_event
- *
+ *
* @author shzg
* @date 2026-04-01
*/
@@ -22,8 +24,12 @@ public class MqttSensorEvent extends BaseEntity
@Excel(name = "设备ID")
private Long deviceId;
+ /** 部门ID(项目/仓库) */
+ @Excel(name = "部门ID")
+ private Long deptId;
+
/** 事件类型(alarm / threshold / normal) */
- @Excel(name = "事件类型", readConverterExp = "a=larm,/=,t=hreshold,/=,n=ormal")
+ @Excel(name = "事件类型", readConverterExp = "alarm=报警,threshold=阈值,normal=正常")
private String eventType;
/** 事件描述 */
@@ -31,7 +37,7 @@ public class MqttSensorEvent extends BaseEntity
private String eventDesc;
/** 等级(LOW/MEDIUM/HIGH) */
- @Excel(name = "等级", readConverterExp = "L=OW/MEDIUM/HIGH")
+ @Excel(name = "等级", readConverterExp = "LOW=低,MEDIUM=中,HIGH=高")
private String level;
/** 处理状态(0未处理 1已处理) */
@@ -42,91 +48,116 @@ public class MqttSensorEvent extends BaseEntity
@Excel(name = "删除标识")
private String isDelete;
- public void setId(Long id)
+ private List deptIds;
+
+ // ==================== getter / setter ====================
+
+ public void setId(Long id)
{
this.id = id;
}
- public Long getId()
+ public Long getId()
{
return id;
}
- public void setDeviceId(Long deviceId)
+ public void setDeviceId(Long deviceId)
{
this.deviceId = deviceId;
}
- public Long getDeviceId()
+ public Long getDeviceId()
{
return deviceId;
}
- public void setEventType(String eventType)
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setEventType(String eventType)
{
this.eventType = eventType;
}
- public String getEventType()
+ public String getEventType()
{
return eventType;
}
- public void setEventDesc(String eventDesc)
+ public void setEventDesc(String eventDesc)
{
this.eventDesc = eventDesc;
}
- public String getEventDesc()
+ public String getEventDesc()
{
return eventDesc;
}
- public void setLevel(String level)
+ public void setLevel(String level)
{
this.level = level;
}
- public String getLevel()
+ public String getLevel()
{
return level;
}
- public void setStatus(String status)
+ public void setStatus(String status)
{
this.status = status;
}
- public String getStatus()
+ public String getStatus()
{
return status;
}
- public void setIsDelete(String isDelete)
+ public void setIsDelete(String isDelete)
{
this.isDelete = isDelete;
}
- public String getIsDelete()
+ public String getIsDelete()
{
return isDelete;
}
+ public List getDeptIds() {
+ return deptIds;
+ }
+
+ public void setDeptIds(List deptIds) {
+ this.deptIds = deptIds;
+ }
+
+ // ==================== toString ====================
+
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
- .append("id", getId())
- .append("deviceId", getDeviceId())
- .append("eventType", getEventType())
- .append("eventDesc", getEventDesc())
- .append("level", getLevel())
- .append("status", getStatus())
- .append("remark", getRemark())
- .append("createBy", getCreateBy())
- .append("createTime", getCreateTime())
- .append("updateBy", getUpdateBy())
- .append("updateTime", getUpdateTime())
- .append("isDelete", getIsDelete())
- .toString();
+ .append("id", getId())
+ .append("deviceId", getDeviceId())
+ .append("deptId", getDeptId())
+ .append("eventType", getEventType())
+ .append("eventDesc", getEventDesc())
+ .append("level", getLevel())
+ .append("status", getStatus())
+ .append("remark", getRemark())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("isDelete", getIsDelete())
+ .toString();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/domain/MqttSensorThreshold.java b/src/main/java/com/shzg/project/worn/domain/MqttSensorThreshold.java
index d39f7c0..08fb469 100644
--- a/src/main/java/com/shzg/project/worn/domain/MqttSensorThreshold.java
+++ b/src/main/java/com/shzg/project/worn/domain/MqttSensorThreshold.java
@@ -1,6 +1,7 @@
package com.shzg.project.worn.domain;
import java.math.BigDecimal;
+import java.util.List;
import com.shzg.framework.web.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -9,7 +10,7 @@ import com.shzg.framework.aspectj.lang.annotation.Excel;
/**
* 传感器阈值配置对象 mqtt_sensor_threshold
- *
+ *
* @author shzg
* @date 2026-04-01
*/
@@ -21,11 +22,18 @@ public class MqttSensorThreshold extends BaseEntity
private Long id;
/** 设备ID(关联 mqtt_sensor_device.id) */
- @Excel(name = "设备ID", readConverterExp = "关=联,m=qtt_sensor_device.id")
+ @Excel(name = "设备ID")
private Long deviceId;
+ /** 部门ID(项目/仓库) */
+ @Excel(name = "部门ID")
+ private Long deptId;
+
+ /** 数据权限(部门ID集合,用于数据隔离) */
+ private List deptIds;
+
/** 指标类型(temperature/humidity/nh3/h2s/battery) */
- @Excel(name = "指标类型", readConverterExp = "t=emperature/humidity/nh3/h2s/battery")
+ @Excel(name = "指标类型")
private String metricType;
/** 预警最小值 */
@@ -45,135 +53,136 @@ public class MqttSensorThreshold extends BaseEntity
private BigDecimal alarmMax;
/** 单位(℃/%/ppm) */
- @Excel(name = "单位", readConverterExp = "℃=/%/ppm")
+ @Excel(name = "单位")
private String unit;
/** 状态(1启用 0停用) */
- @Excel(name = "状态", readConverterExp = "1=启用,0=停用")
+ @Excel(name = "状态")
private String status;
/** 是否删除(0正常 1删除) */
- @Excel(name = "是否删除", readConverterExp = "0=正常,1=删除")
+ @Excel(name = "是否删除")
private String isDelete;
- public void setId(Long id)
- {
- this.id = id;
- }
+ // ==================== getter/setter ====================
- public Long getId()
- {
+ public Long getId() {
return id;
}
- public void setDeviceId(Long deviceId)
- {
- this.deviceId = deviceId;
+ public void setId(Long id) {
+ this.id = id;
}
- public Long getDeviceId()
- {
+ public Long getDeviceId() {
return deviceId;
}
- public void setMetricType(String metricType)
- {
- this.metricType = metricType;
+ public void setDeviceId(Long deviceId) {
+ this.deviceId = deviceId;
}
- public String getMetricType()
- {
+ public Long getDeptId() {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId) {
+ this.deptId = deptId;
+ }
+
+ public List getDeptIds() {
+ return deptIds;
+ }
+
+ public void setDeptIds(List deptIds) {
+ this.deptIds = deptIds;
+ }
+
+ public String getMetricType() {
return metricType;
}
- public void setWarnMin(BigDecimal warnMin)
- {
- this.warnMin = warnMin;
+ public void setMetricType(String metricType) {
+ this.metricType = metricType;
}
- public BigDecimal getWarnMin()
- {
+ public BigDecimal getWarnMin() {
return warnMin;
}
- public void setWarnMax(BigDecimal warnMax)
- {
- this.warnMax = warnMax;
+ public void setWarnMin(BigDecimal warnMin) {
+ this.warnMin = warnMin;
}
- public BigDecimal getWarnMax()
- {
+ public BigDecimal getWarnMax() {
return warnMax;
}
- public void setAlarmMin(BigDecimal alarmMin)
- {
- this.alarmMin = alarmMin;
+ public void setWarnMax(BigDecimal warnMax) {
+ this.warnMax = warnMax;
}
- public BigDecimal getAlarmMin()
- {
+ public BigDecimal getAlarmMin() {
return alarmMin;
}
- public void setAlarmMax(BigDecimal alarmMax)
- {
- this.alarmMax = alarmMax;
+ public void setAlarmMin(BigDecimal alarmMin) {
+ this.alarmMin = alarmMin;
}
- public BigDecimal getAlarmMax()
- {
+ public BigDecimal getAlarmMax() {
return alarmMax;
}
- public void setUnit(String unit)
- {
- this.unit = unit;
+ public void setAlarmMax(BigDecimal alarmMax) {
+ this.alarmMax = alarmMax;
}
- public String getUnit()
- {
+ public String getUnit() {
return unit;
}
- public void setStatus(String status)
- {
- this.status = status;
+ public void setUnit(String unit) {
+ this.unit = unit;
}
- public String getStatus()
- {
+ public String getStatus() {
return status;
}
- public void setIsDelete(String isDelete)
- {
- this.isDelete = isDelete;
+ public void setStatus(String status) {
+ this.status = status;
}
- public String getIsDelete()
- {
+ public String getIsDelete() {
return isDelete;
}
+ public void setIsDelete(String isDelete) {
+ this.isDelete = isDelete;
+ }
+
+ // ==================== toString ====================
+
@Override
public String toString() {
- return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
- .append("id", getId())
- .append("deviceId", getDeviceId())
- .append("metricType", getMetricType())
- .append("warnMin", getWarnMin())
- .append("warnMax", getWarnMax())
- .append("alarmMin", getAlarmMin())
- .append("alarmMax", getAlarmMax())
- .append("unit", getUnit())
- .append("status", getStatus())
- .append("remark", getRemark())
- .append("createBy", getCreateBy())
- .append("createTime", getCreateTime())
- .append("updateBy", getUpdateBy())
- .append("updateTime", getUpdateTime())
- .append("isDelete", getIsDelete())
- .toString();
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("id", getId())
+ .append("deviceId", getDeviceId())
+ .append("deptId", getDeptId())
+ .append("metricType", getMetricType())
+ .append("warnMin", getWarnMin())
+ .append("warnMax", getWarnMax())
+ .append("alarmMin", getAlarmMin())
+ .append("alarmMax", getAlarmMax())
+ .append("unit", getUnit())
+ .append("status", getStatus())
+ .append("remark", getRemark())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("isDelete", getIsDelete())
+ .toString();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java b/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java
new file mode 100644
index 0000000..42bbe0d
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/domain/MqttTopicConfig.java
@@ -0,0 +1,164 @@
+package com.shzg.project.worn.domain;
+
+import com.shzg.framework.aspectj.lang.annotation.Excel;
+import com.shzg.framework.web.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.List;
+
+/**
+ * MQTT主题配置对象 mqtt_topic_config
+ *
+ * @author shzg
+ * @date 2026-04-08
+ */
+public class MqttTopicConfig extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 主键ID */
+ private Long id;
+
+ /** 项目编码(如 tangshan) */
+ @Excel(name = "项目编码")
+ private String project;
+
+ /** 仓库编码(如 dianchi) */
+ @Excel(name = "仓库编码")
+ private String warehouse;
+
+ /** 所属部门ID */
+ @Excel(name = "部门ID")
+ private Long deptId;
+
+ /** 🔥 数据权限(部门ID集合) */
+ private List deptIds;
+
+ /** 上行订阅Topic */
+ @Excel(name = "上行Topic")
+ private String topicUp;
+
+ /** 下行Topic前缀 */
+ @Excel(name = "下行Topic前缀")
+ private String topicDownPrefix;
+
+ /** 状态(0启用 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=启用,1=停用")
+ private String status;
+
+ /** 删除标识(0正常 1删除) */
+ @Excel(name = "删除标识", readConverterExp = "0=正常,1=删除")
+ private String isDelete;
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public Long getId()
+ {
+ return id;
+ }
+
+ public void setProject(String project)
+ {
+ this.project = project;
+ }
+
+ public String getProject()
+ {
+ return project;
+ }
+
+ public void setWarehouse(String warehouse)
+ {
+ this.warehouse = warehouse;
+ }
+
+ public String getWarehouse()
+ {
+ return warehouse;
+ }
+
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setDeptIds(List deptIds)
+ {
+ this.deptIds = deptIds;
+ }
+
+ public List getDeptIds()
+ {
+ return deptIds;
+ }
+
+ public void setTopicUp(String topicUp)
+ {
+ this.topicUp = topicUp;
+ }
+
+ public String getTopicUp()
+ {
+ return topicUp;
+ }
+
+ public void setTopicDownPrefix(String topicDownPrefix)
+ {
+ this.topicDownPrefix = topicDownPrefix;
+ }
+
+ public String getTopicDownPrefix()
+ {
+ return topicDownPrefix;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setIsDelete(String isDelete)
+ {
+ this.isDelete = isDelete;
+ }
+
+ public String getIsDelete()
+ {
+ return isDelete;
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("id", getId())
+ .append("project", getProject())
+ .append("warehouse", getWarehouse())
+ .append("deptId", getDeptId())
+ .append("deptIds", getDeptIds())
+ .append("topicUp", getTopicUp())
+ .append("topicDownPrefix", getTopicDownPrefix())
+ .append("status", getStatus())
+ .append("remark", getRemark())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("isDelete", getIsDelete())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/domain/WornInboundItem.java b/src/main/java/com/shzg/project/worn/domain/WornInboundItem.java
index 1eb2414..ff3761a 100644
--- a/src/main/java/com/shzg/project/worn/domain/WornInboundItem.java
+++ b/src/main/java/com/shzg/project/worn/domain/WornInboundItem.java
@@ -108,6 +108,7 @@ public class WornInboundItem extends BaseEntity
private Long unitId;
/** 单位名称 */
private String unitName;
+ private String operatorName;
/** 主单据状态 */
private String billStatus;
@@ -237,6 +238,15 @@ public class WornInboundItem extends BaseEntity
{
return unitName;
}
+ public String getOperatorName()
+ {
+ return operatorName;
+ }
+
+ public void setOperatorName(String operatorName)
+ {
+ this.operatorName = operatorName;
+ }
public void setStatus(String status)
{
this.status = status;
@@ -273,4 +283,4 @@ public class WornInboundItem extends BaseEntity
.append("createTime", getCreateTime())
.toString();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/shzg/project/worn/domain/vo/InboundItemVO.java b/src/main/java/com/shzg/project/worn/domain/vo/InboundItemVO.java
new file mode 100644
index 0000000..a265a50
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/domain/vo/InboundItemVO.java
@@ -0,0 +1,24 @@
+package com.shzg.project.worn.domain.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 入库单明细打印VO
+ */
+@Data
+public class InboundItemVO {
+
+ /** 物料名称 */
+ private String materialName;
+
+ /** 单位 */
+ private String unit;
+
+ /** 数量 */
+ private BigDecimal quantity;
+
+ /** 唯一码 */
+ private String uniqueCode;
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/domain/vo/InboundPrintVO.java b/src/main/java/com/shzg/project/worn/domain/vo/InboundPrintVO.java
new file mode 100644
index 0000000..e671d12
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/domain/vo/InboundPrintVO.java
@@ -0,0 +1,31 @@
+package com.shzg.project.worn.domain.vo;
+
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 入库单打印VO
+ */
+@Data
+public class InboundPrintVO {
+
+ /** 单据号 */
+ private String billNo;
+
+ /** 仓库名称 */
+ private String warehouseName;
+
+ /** 库区名称 */
+ private String areaName;
+
+ /** 入库时间 */
+ private Date inboundTime;
+
+ /** 备注 */
+ private String remark;
+
+ /** 明细列表 */
+ private List items;
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/mapper/MqttSensorThresholdMapper.java b/src/main/java/com/shzg/project/worn/mapper/MqttSensorThresholdMapper.java
index d9d6f04..98d3de8 100644
--- a/src/main/java/com/shzg/project/worn/mapper/MqttSensorThresholdMapper.java
+++ b/src/main/java/com/shzg/project/worn/mapper/MqttSensorThresholdMapper.java
@@ -66,4 +66,13 @@ public interface MqttSensorThresholdMapper
* @return
*/
MqttSensorThreshold selectByDeviceAndMetric(Long deviceId, String metricType);
+
+ /**
+ * 根据部门id和指标类型查询阈值配置
+ * @param deptId
+ * @param metricType
+ * @return
+ */
+ MqttSensorThreshold selectByDeptAndMetric(Long deptId, String metricType);
+
}
diff --git a/src/main/java/com/shzg/project/worn/mapper/MqttTopicConfigMapper.java b/src/main/java/com/shzg/project/worn/mapper/MqttTopicConfigMapper.java
new file mode 100644
index 0000000..fe7256a
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/mapper/MqttTopicConfigMapper.java
@@ -0,0 +1,68 @@
+package com.shzg.project.worn.mapper;
+
+import com.shzg.project.worn.domain.MqttTopicConfig;
+import java.util.List;
+
+/**
+ * MQTT主题配置Mapper接口
+ *
+ * @author shzg
+ * @date 2026-04-08
+ */
+public interface MqttTopicConfigMapper
+{
+ /**
+ * 根据ID查询MQTT主题配置
+ *
+ * @param id 主键ID
+ * @return MQTT主题配置
+ */
+ public MqttTopicConfig selectMqttTopicConfigById(Long id);
+
+ /**
+ * 查询MQTT主题配置列表
+ *
+ * @param mqttTopicConfig 查询条件
+ * @return MQTT主题配置集合
+ */
+ public List selectMqttTopicConfigList(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 查询启用状态的MQTT主题配置
+ *
+ * @return MQTT主题配置集合
+ */
+ public List selectEnabledMqttTopicConfigList();
+
+ /**
+ * 新增MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ public int insertMqttTopicConfig(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 修改MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ public int updateMqttTopicConfig(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 删除MQTT主题配置
+ *
+ * @param id 主键ID
+ * @return 结果
+ */
+ public int deleteMqttTopicConfigById(Long id);
+
+ /**
+ * 批量删除MQTT主题配置
+ *
+ * @param ids 主键ID数组
+ * @return 结果
+ */
+ public int deleteMqttTopicConfigByIds(Long[] ids);
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/sensor/config/MqttClientConfig.java b/src/main/java/com/shzg/project/worn/sensor/config/MqttClientConfig.java
index 6bed523..71283eb 100644
--- a/src/main/java/com/shzg/project/worn/sensor/config/MqttClientConfig.java
+++ b/src/main/java/com/shzg/project/worn/sensor/config/MqttClientConfig.java
@@ -1,6 +1,8 @@
package com.shzg.project.worn.sensor.config;
+import com.shzg.project.worn.domain.MqttTopicConfig;
import com.shzg.project.worn.sensor.mqtt.dispatcher.MqttMessageDispatcher;
+import com.shzg.project.worn.service.IMqttTopicConfigService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
@@ -10,44 +12,81 @@ import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
import java.util.UUID;
/**
- * MQTT 客户端初始化配置类
+ * MQTT客户端配置类
+ *
+ * 作用:
+ * 1. 初始化 MQTT 客户端连接
+ * 2. 设置连接参数(心跳、自动重连等)
+ * 3. 注册回调函数(消息接收、连接状态等)
+ * 4. 自动订阅数据库中配置的 topic
+ * 5. 应用关闭时释放资源
*/
@Slf4j
@Configuration
public class MqttClientConfig {
+ /**
+ * MQTT客户端实例(全局唯一)
+ */
private MqttClient mqttClient;
+ /**
+ * MQTT消息分发器(核心组件)
+ * 负责将不同topic的数据分发到对应Handler
+ */
@Resource
private MqttMessageDispatcher mqttMessageDispatcher;
+ /**
+ * topic配置服务(从数据库读取订阅主题)
+ */
+ @Resource
+ private IMqttTopicConfigService mqttTopicConfigService;
+
+ /**
+ * 初始化 MQTT 客户端 Bean
+ *
+ * @param props MQTT配置(来自配置文件)
+ */
@Bean
public MqttClient mqttClient(MqttProperties props) throws MqttException {
+ // ================== 1️⃣ 开关控制 ==================
+ // 如果MQTT未启用,直接跳过初始化
if (!props.isEnabled()) {
- log.warn("[MQTT] mqtt.enabled=false,MQTT 功能未启用");
+ log.warn("[MQTT] mqtt.enabled=false, skip initialization");
return null;
}
- // ✅ clientId 动态生成
+ // ================== 2️⃣ 客户端ID生成 ==================
+ // 使用随机UUID,避免多个服务clientId冲突
String clientId = "worn-backend-" + UUID.randomUUID();
- mqttClient = new MqttClient(
- props.getBroker(),
- clientId,
- new MemoryPersistence()
- );
+ // 创建MQTT客户端(内存存储方式)
+ mqttClient = new MqttClient(props.getBroker(), clientId, new MemoryPersistence());
- // 连接参数
+ // ================== 3️⃣ 连接参数配置 ==================
MqttConnectOptions options = new MqttConnectOptions();
+
+ // 是否清除会话(true:每次都是新会话)
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());
}
@@ -55,70 +94,125 @@ public class MqttClientConfig {
options.setPassword(props.getPassword().toCharArray());
}
- // 回调
+ // ================== 4️⃣ 回调函数 ==================
mqttClient.setCallback(new MqttCallbackExtended() {
+ /**
+ * 连接成功回调(首次连接 + 重连都会触发)
+ */
@Override
public void connectComplete(boolean reconnect, String serverURI) {
- log.info("[MQTT] 连接成功 reconnect={}, serverURI={}", reconnect, serverURI);
+ log.info("[MQTT] connected reconnect={}, serverURI={}", reconnect, serverURI);
+
+ // 每次连接成功后重新订阅(防止重连丢订阅)
subscribe(props);
}
+ /**
+ * 连接丢失回调
+ */
@Override
public void connectionLost(Throwable cause) {
- log.warn("[MQTT] 连接断开", cause);
+ log.warn("[MQTT] connection lost", cause);
}
+ /**
+ * 收到消息回调(核心入口)
+ */
@Override
public void messageArrived(String topic, MqttMessage message) {
+
+ // 将byte[]转为字符串
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
- log.info("🔥 收到MQTT消息 topic={} payload={}", topic, payload);
+ log.info("[MQTT] message arrived topic={} payload={}", topic, payload);
+ // 分发给业务处理器(你写的Dispatcher)
mqttMessageDispatcher.dispatch(topic, payload);
}
+ /**
+ * 消息发送完成(发布消息时用)
+ */
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
+ // 当前项目未使用,可扩展日志或确认机制
}
});
- // 连接
- log.info("[MQTT] 正在连接 Broker:{}", props.getBroker());
+ // ================== 5️⃣ 建立连接 ==================
+ log.info("[MQTT] connecting broker={}", props.getBroker());
mqttClient.connect(options);
- log.info("[MQTT] MQTT 已连接");
+ log.info("[MQTT] connected");
- // 启动订阅
+ // ================== 6️⃣ 订阅主题 ==================
subscribe(props);
return mqttClient;
}
-
+ /**
+ * 订阅主题(从数据库动态加载)
+ */
private void subscribe(MqttProperties props) {
+
+ // 如果客户端未连接,直接跳过
if (mqttClient == null || !mqttClient.isConnected()) {
- log.warn("[MQTT] 订阅失败:客户端未连接");
+ log.warn("[MQTT] subscribe skipped because client is not connected");
return;
}
try {
+ // ================== 1️⃣ 查询数据库配置 ==================
+ List topicConfigs =
+ mqttTopicConfigService.selectEnabledMqttTopicConfigList();
- mqttClient.subscribe("worn/tangshan/dianchi/up", props.getQos());
+ // 使用Set去重,保证不会重复订阅
+ Set topics = new LinkedHashSet<>();
- log.info("[MQTT] 已订阅 Topic:worn/tangshan/dianchi/up,QoS={}", props.getQos());
+ for (MqttTopicConfig topicConfig : topicConfigs) {
+
+ // 只订阅上行topic(设备上报数据)
+ if (topicConfig.getTopicUp() != null
+ && !topicConfig.getTopicUp().trim().isEmpty()) {
+
+ topics.add(topicConfig.getTopicUp().trim());
+ }
+ }
+
+ // ================== 2️⃣ 空校验 ==================
+ if (topics.isEmpty()) {
+ log.warn("[MQTT] no enabled topic_up config found");
+ return;
+ }
+
+ // ================== 3️⃣ 执行订阅 ==================
+ for (String topic : topics) {
+
+ mqttClient.subscribe(topic, props.getQos());
+
+ log.info("[MQTT] subscribed topic={} qos={}", topic, props.getQos());
+ }
} catch (Exception e) {
- log.error("[MQTT] 订阅 Topic 失败", e);
+ log.error("[MQTT] subscribe failed", e);
}
}
+ /**
+ * 应用关闭时释放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/shzg/project/worn/sensor/config/MqttPublishClient.java b/src/main/java/com/shzg/project/worn/sensor/config/MqttPublishClient.java
index 6db4e10..8c3864f 100644
--- a/src/main/java/com/shzg/project/worn/sensor/config/MqttPublishClient.java
+++ b/src/main/java/com/shzg/project/worn/sensor/config/MqttPublishClient.java
@@ -8,11 +8,10 @@ import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
- * MQTT 消息发布客户端
+ * MQTT 消息发布客户端(最终版 - LoRaWAN专用)
*
- * 支持:
- * 1. JSON消息发送
- * 2. HEX指令发送(设备控制)
+ * 所有下行统一使用:
+ * JSON + Base64
*/
@Slf4j
@Component
@@ -24,7 +23,7 @@ public class MqttPublishClient {
this.mqttClient = mqttClient;
}
- // ================== JSON发送 ==================
+ // ================== ⭐ 标准下行发送 ==================
public void publish(String topic, String payload) {
publish(topic, payload, 1, false);
}
@@ -39,38 +38,12 @@ public class MqttPublishClient {
mqttClient.publish(topic, msg);
- log.info("[MQTT] JSON发送成功 topic={}, payload={}, qos={}, retained={}",
+ log.info("[MQTT] 下行发送成功 topic={}, payload={}, qos={}, retained={}",
topic, payload, qos, retained);
} catch (Exception e) {
- log.error("[MQTT] JSON发送失败 topic=" + topic + ", payload=" + payload, e);
- throw new RuntimeException("MQTT JSON发送失败", e);
- }
- }
-
- // ================== ⭐ HEX发送 ==================
- public void publishHex(String topic, String hexPayload) {
- publishHex(topic, hexPayload, 1, false);
- }
-
- public void publishHex(String topic, String hexPayload, int qos, boolean retained) {
- checkClient();
-
- try {
- byte[] bytes = hexStringToByteArray(hexPayload);
-
- MqttMessage msg = new MqttMessage(bytes);
- msg.setQos(qos);
- msg.setRetained(retained);
-
- mqttClient.publish(topic, msg);
-
- log.info("[MQTT] HEX发送成功 topic={}, hex={}, length={}, qos={}, retained={}",
- topic, hexPayload, bytes.length, qos, retained);
-
- } catch (Exception e) {
- log.error("[MQTT] HEX发送失败 topic=" + topic + ", hex=" + hexPayload, e);
- throw new RuntimeException("MQTT HEX发送失败", e);
+ log.error("[MQTT] 下行发送失败 topic=" + topic + ", payload=" + payload, e);
+ throw new RuntimeException("MQTT发送失败", e);
}
}
@@ -83,41 +56,4 @@ public class MqttPublishClient {
throw new IllegalStateException("MQTT客户端未连接到Broker");
}
}
-
- // ================== HEX工具 ==================
- /**
- * HEX字符串 → byte[]
- * 支持:
- * - 大小写
- * - 自动去空格
- *
- * 示例:
- * 2218000000C6 → [0x22,0x18,0x00,0x00,0x00,0xC6]
- */
- private byte[] hexStringToByteArray(String hex) {
-
- if (hex == null || hex.trim().isEmpty()) {
- throw new IllegalArgumentException("HEX字符串不能为空");
- }
-
- // 去空格 + 转大写
- hex = hex.replace(" ", "").toUpperCase();
-
- if (hex.length() % 2 != 0) {
- throw new IllegalArgumentException("HEX字符串长度必须为偶数:" + hex);
- }
-
- int len = hex.length();
- byte[] data = new byte[len / 2];
-
- try {
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
- }
- } catch (Exception e) {
- throw new IllegalArgumentException("HEX字符串解析失败:" + hex, e);
- }
-
- return data;
- }
}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/dispatcher/MqttMessageDispatcher.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/dispatcher/MqttMessageDispatcher.java
index f4b02ec..dffbf60 100644
--- a/src/main/java/com/shzg/project/worn/sensor/mqtt/dispatcher/MqttMessageDispatcher.java
+++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/dispatcher/MqttMessageDispatcher.java
@@ -6,6 +6,7 @@ import com.shzg.project.worn.domain.MqttSensorDevice;
import com.shzg.project.worn.sensor.mqtt.handler.EnvSensorHandler;
import com.shzg.project.worn.sensor.mqtt.handler.SmokeSensorHandler;
import com.shzg.project.worn.sensor.mqtt.handler.WaterSensorHandler;
+import com.shzg.project.worn.sensor.mqtt.handler.SmartSocketHandler;
import com.shzg.project.worn.unit.MqttDeviceCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,7 +16,7 @@ import org.springframework.stereotype.Component;
import java.util.concurrent.Executor;
/**
- * MQTT消息分发器(支持烟雾 / 环境 / 水浸)
+ * MQTT消息分发器(支持烟雾 / 环境 / 水浸 / 插座)
*/
@Slf4j
@Component
@@ -30,10 +31,12 @@ public class MqttMessageDispatcher {
@Autowired
private EnvSensorHandler envSensorHandler;
- // ✅ 新增:水浸处理器
@Autowired
private WaterSensorHandler waterSensorHandler;
+ @Autowired
+ private SmartSocketHandler smartSocketHandler;
+
@Autowired
@Qualifier("mqttExecutor")
private Executor executor;
@@ -72,7 +75,6 @@ public class MqttMessageDispatcher {
return;
}
- // 👉 统一小写(关键)
devEui = devEui.toLowerCase();
// =========================
@@ -110,12 +112,18 @@ public class MqttMessageDispatcher {
return;
}
- // 💧 水浸(新增)
+ // 💧 水浸
if (deviceType.contains("water")) {
waterSensorHandler.handle(device, topic, payload);
return;
}
+ // 🔌 智能排风插座
+ if (deviceType.contains("socket")) {
+ smartSocketHandler.handle(device, topic, payload);
+ return;
+ }
+
// ❌ 未识别
log.warn("[MQTT] 未识别设备类型 deviceType={}", deviceType);
diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java
index 1d1ad48..9cbcc8a 100644
--- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java
+++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/EnvSensorHandler.java
@@ -8,6 +8,7 @@ import com.shzg.project.worn.domain.MqttSensorThreshold;
import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService;
import com.shzg.project.worn.service.IMqttSensorThresholdService;
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -18,7 +19,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
- * 综合环境传感器 Handler(最终生产版:防重复报警 + 状态机)
+ * 综合环境传感器 Handler
*/
@Slf4j
@Component
@@ -33,18 +34,27 @@ public class EnvSensorHandler {
@Autowired
private IMqttSensorThresholdService thresholdService;
+ @Autowired
+ private WebSocketSessionManager sessionManager;
+
/**
- * 状态缓存(核心:防重复)
- * key = deviceId_metric
- * value = normal / warning / alarm
+ * 状态缓存(防重复报警)
*/
private static final Map STATUS_CACHE = new ConcurrentHashMap<>();
- /**
- * 主入口
- */
+
public void handle(MqttSensorDevice device, String topic, String payload) {
+ if (device == null) {
+ log.error("[ENV] device为空,忽略消息");
+ return;
+ }
+
+ if (device.getDeptId() == null) {
+ log.error("[ENV] device未绑定deptId,数据隔离失效!deviceId={}", device.getId());
+ return;
+ }
+
log.info("[ENV] deviceId={}, topic={}, payload={}", device.getId(), topic, payload);
JSONObject json = parseJson(payload);
@@ -55,14 +65,48 @@ public class EnvSensorHandler {
TopicInfo topicInfo = parseTopic(topic);
- // 1️⃣ 数据入库
+ // 1️⃣ 入库
saveData(device, topic, payload, topicInfo, val);
- // 2️⃣ 状态检测(核心)
+ // 2️⃣ WebSocket推送
+ pushWebSocket(device, val);
+
+ // 3️⃣ 事件检测
handleEvent(device, val);
}
+
+ /**
+ * WebSocket推送
+ */
+ private void pushWebSocket(MqttSensorDevice device, SensorValue v) {
+
+ try {
+ JSONObject msg = new JSONObject();
+
+ msg.put("type", "env");
+ msg.put("deviceId", device.getId());
+ msg.put("deviceName", device.getDeviceName());
+ msg.put("deptId", device.getDeptId());
+
+ msg.put("temperature", v.temperature);
+ msg.put("humidity", v.humidity);
+ msg.put("nh3", v.nh3);
+ msg.put("h2s", v.h2s);
+ msg.put("battery", v.battery);
+
+ msg.put("time", System.currentTimeMillis());
+
+ sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
+
+ } catch (Exception e) {
+ log.error("[ENV] WebSocket推送失败 deviceId={}", device.getId(), e);
+ }
+ }
+
+
// ================== 事件处理 ==================
+
private void handleEvent(MqttSensorDevice device, SensorValue v) {
check(device, "temperature", v.temperature, "温度");
@@ -70,9 +114,13 @@ public class EnvSensorHandler {
check(device, "nh3", v.nh3, "氨气");
check(device, "h2s", v.h2s, "硫化氢");
- // 电量(单独处理)
+ // 电量
if (v.battery != null) {
- MqttSensorThreshold t = thresholdService.getThreshold(device.getId(), "battery");
+ MqttSensorThreshold t = thresholdService.getThreshold(
+ device.getId(),
+ device.getDeptId(),
+ "battery"
+ );
if (t != null) {
BigDecimal val = new BigDecimal(v.battery);
String status = calcStatus(val, t);
@@ -81,7 +129,7 @@ public class EnvSensorHandler {
}
}
- // ================== 通用检测 ==================
+
private void check(MqttSensorDevice device,
String metric,
BigDecimal value,
@@ -89,7 +137,12 @@ public class EnvSensorHandler {
if (value == null) return;
- MqttSensorThreshold t = thresholdService.getThreshold(device.getId(), metric);
+ MqttSensorThreshold t = thresholdService.getThreshold(
+ device.getId(),
+ device.getDeptId(),
+ metric
+ );
+
if (t == null) return;
String status = calcStatus(value, t);
@@ -97,26 +150,18 @@ public class EnvSensorHandler {
changeStatus(device, metric, status, name + ":" + value);
}
- // ================== 状态计算 ==================
+
private String calcStatus(BigDecimal value, MqttSensorThreshold t) {
- if (t.getAlarmMax() != null && value.compareTo(t.getAlarmMax()) > 0) {
- return "alarm";
- }
- if (t.getAlarmMin() != null && value.compareTo(t.getAlarmMin()) < 0) {
- return "alarm";
- }
- if (t.getWarnMax() != null && value.compareTo(t.getWarnMax()) > 0) {
- return "warning";
- }
- if (t.getWarnMin() != null && value.compareTo(t.getWarnMin()) < 0) {
- return "warning";
- }
+ if (t.getAlarmMax() != null && value.compareTo(t.getAlarmMax()) > 0) return "alarm";
+ if (t.getAlarmMin() != null && value.compareTo(t.getAlarmMin()) < 0) return "alarm";
+ if (t.getWarnMax() != null && value.compareTo(t.getWarnMax()) > 0) return "warning";
+ if (t.getWarnMin() != null && value.compareTo(t.getWarnMin()) < 0) return "warning";
return "normal";
}
- // ================== 状态变更 ==================
+
private void changeStatus(MqttSensorDevice device,
String metric,
String newStatus,
@@ -125,15 +170,10 @@ public class EnvSensorHandler {
String key = device.getId() + "_" + metric;
String oldStatus = STATUS_CACHE.get(key);
- // ❗ 防重复:状态没变,不处理
- if (newStatus.equals(oldStatus)) {
- return;
- }
+ if (newStatus.equals(oldStatus)) return;
- // 更新缓存
STATUS_CACHE.put(key, newStatus);
- // 状态变化 → 记录事件
if ("alarm".equals(newStatus)) {
triggerEvent(device, "alarm", "HIGH", desc);
} else if ("warning".equals(newStatus)) {
@@ -143,7 +183,7 @@ public class EnvSensorHandler {
}
}
- // ================== 事件记录 ==================
+
private void triggerEvent(MqttSensorDevice device,
String type,
String level,
@@ -153,7 +193,10 @@ public class EnvSensorHandler {
device.getId(), type, level, desc);
MqttSensorEvent event = new MqttSensorEvent();
+
event.setDeviceId(device.getId());
+ event.setDeptId(device.getDeptId());
+
event.setEventType(type);
event.setLevel(level);
event.setEventDesc(desc);
@@ -162,13 +205,11 @@ public class EnvSensorHandler {
event.setCreateTime(new Date());
eventService.insertMqttSensorEvent(event);
-
- // TODO: D2D联动(风机/排风)
- // TODO: 短信通知
- // TODO: APP推送
}
- // ================== 数据入库 ==================
+
+ // ================== 入库 ==================
+
private void saveData(MqttSensorDevice device,
String topic,
String payload,
@@ -178,6 +219,8 @@ public class EnvSensorHandler {
MqttSensorData data = new MqttSensorData();
data.setDeviceId(device.getId());
+ data.setDeptId(device.getDeptId());
+
data.setTopic(topic);
data.setProject(topicInfo.project);
data.setWarehouse(topicInfo.warehouse);
@@ -197,7 +240,9 @@ public class EnvSensorHandler {
sensorDataService.insertMqttSensorData(data);
}
- // ================== JSON解析 ==================
+
+ // ================== 工具 ==================
+
private JSONObject parseJson(String payload) {
try {
return JSONObject.parseObject(payload);
@@ -207,7 +252,6 @@ public class EnvSensorHandler {
}
}
- // ================== 构建数据 ==================
private SensorValue buildValue(JSONObject json) {
SensorValue v = new SensorValue();
v.temperature = json.getBigDecimal("temperature");
@@ -218,7 +262,6 @@ public class EnvSensorHandler {
return v;
}
- // ================== topic解析 ==================
private TopicInfo parseTopic(String topic) {
TopicInfo info = new TopicInfo();
try {
@@ -231,7 +274,7 @@ public class EnvSensorHandler {
return info;
}
- // ================== 内部结构 ==================
+
private static class SensorValue {
BigDecimal temperature;
BigDecimal humidity;
diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java
new file mode 100644
index 0000000..ff22c4d
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmartSocketHandler.java
@@ -0,0 +1,118 @@
+package com.shzg.project.worn.sensor.mqtt.handler;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.shzg.project.worn.domain.MqttSensorData;
+import com.shzg.project.worn.domain.MqttSensorDevice;
+import com.shzg.project.worn.domain.MqttSensorEvent;
+import com.shzg.project.worn.service.IMqttSensorDataService;
+import com.shzg.project.worn.service.IMqttSensorEventService;
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * 智能插座(排风)Handler
+ */
+@Slf4j
+@Component
+public class SmartSocketHandler {
+
+ @Autowired
+ private IMqttSensorDataService dataService;
+
+ @Autowired
+ private IMqttSensorEventService eventService;
+
+ @Autowired
+ private WebSocketSessionManager sessionManager;
+
+ public void handle(MqttSensorDevice device, String topic, String payload) {
+
+ if (device == null) {
+ log.error("[SOCKET] device为空");
+ return;
+ }
+
+ if (device.getDeptId() == null) {
+ log.error("[SOCKET] device未绑定deptId!");
+ return;
+ }
+
+ log.info("[SOCKET] deviceId={}, topic={}, payload={}",
+ device.getId(), topic, payload);
+
+ JSONObject json;
+ try {
+ json = JSONObject.parseObject(payload);
+ } catch (Exception e) {
+ log.error("[SOCKET] JSON解析失败 payload={}", payload, e);
+ return;
+ }
+
+ Integer status = json.getInteger("socket_status");
+ if (status == null) {
+ log.warn("[SOCKET] 未包含socket_status");
+ return;
+ }
+
+ // 兼容两种协议:
+ // 旧:16=关,17=开
+ // 新:0=关,1=开
+ Integer switchStatus;
+ if (status == 17 || status == 1) {
+ switchStatus = 1;
+ } else if (status == 16 || status == 0) {
+ switchStatus = 0;
+ } else {
+ log.warn("[SOCKET] 未识别的socket_status={}, payload={}", status, payload);
+ return;
+ }
+
+ MqttSensorData data = new MqttSensorData();
+ data.setDeviceId(device.getId());
+ data.setDeptId(device.getDeptId());
+ data.setTopic(topic);
+ data.setPayload(payload);
+ data.setDataJson(payload);
+ data.setSwitchStatus(switchStatus);
+ data.setCreateTime(new Date());
+ data.setIsDelete("0");
+
+ dataService.insertMqttSensorData(data);
+
+ try {
+ JSONObject msg = new JSONObject();
+ msg.put("type", "socket");
+ msg.put("deviceId", device.getId());
+ msg.put("deviceName", device.getDeviceName());
+ msg.put("deptId", device.getDeptId());
+ msg.put("status", switchStatus);
+ msg.put("statusDesc", switchStatus == 1 ? "通电" : "断电");
+ msg.put("time", System.currentTimeMillis());
+
+ sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
+ } catch (Exception e) {
+ log.error("[SOCKET] WebSocket推送失败", e);
+ }
+
+ MqttSensorEvent event = new MqttSensorEvent();
+ event.setDeviceId(device.getId());
+ event.setDeptId(device.getDeptId());
+ event.setEventType(switchStatus == 1 ? "socket_on" : "socket_off");
+ event.setEventDesc(switchStatus == 1 ? "插座开启(通电)" : "插座关闭(断电)");
+ event.setLevel("LOW");
+ event.setStatus("0");
+ event.setIsDelete("0");
+ event.setCreateTime(new Date());
+
+ eventService.insertMqttSensorEvent(event);
+
+ log.info("[SOCKET] 原始状态={}, 转换状态={}, 描述={}, 已入库+已推送",
+ status,
+ switchStatus,
+ switchStatus == 1 ? "通电" : "断电");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java
index 78b21ba..7cef0b9 100644
--- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java
+++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/SmokeSensorHandler.java
@@ -6,63 +6,88 @@ import com.shzg.project.worn.domain.MqttSensorDevice;
import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService;
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+/**
+ * 烟雾传感器 Handler
+ */
@Slf4j
@Component
public class SmokeSensorHandler {
+ /**
+ * 状态缓存(防重复事件)
+ */
+ private static final Map STATUS_CACHE = new ConcurrentHashMap<>();
+
@Autowired
private IMqttSensorDataService dataService;
@Autowired
private IMqttSensorEventService eventService;
- /**
- *
- */
+ @Autowired
+ private WebSocketSessionManager sessionManager;
+
+
public void handle(MqttSensorDevice device, String topic, String payload) {
+ // ================= 安全校验 =================
+ if (device == null) {
+ log.error("[SMOKE] device为空,忽略消息");
+ return;
+ }
+
+ if (device.getDeptId() == null) {
+ log.error("[SMOKE] device未绑定deptId,数据隔离失效!deviceId={}", device.getId());
+ return;
+ }
+
log.info("[SMOKE] deviceId={}, deviceName={}, topic={}, payload={}",
device.getId(), device.getDeviceName(), topic, payload);
+ // ================== 1. JSON解析 ==================
JSONObject json;
-
try {
json = JSONObject.parseObject(payload);
} catch (Exception e) {
- log.error("[SMOKE] JSON解析失败 payload={}", payload, e);
+ log.error("[SMOKE] parse payload failed payload={}", payload, e);
return;
}
- // ================== topic解析 ==================
+ // ================== 2. topic解析 ==================
String project = null;
String warehouse = null;
-
try {
String[] arr = topic.split("/");
if (arr.length >= 3) {
- project = arr[1]; // tangshan
- warehouse = arr[2]; // dianchi
+ project = arr[1];
+ warehouse = arr[2];
}
} catch (Exception e) {
- log.warn("[SMOKE] topic解析失败 topic={}", topic);
+ log.warn("[SMOKE] parse topic failed topic={}", topic);
}
- // ================== 字段 ==================
+ // ================== 3. 业务字段 ==================
String event = json.getString("event");
Integer battery = json.getInteger("battery");
Integer concentration = json.getInteger("concentration");
Integer temperature = json.getInteger("temperature");
- // ================== 入库 ==================
+ // ================== 4. 数据落库 ==================
MqttSensorData data = new MqttSensorData();
-
data.setDeviceId(device.getId());
+
+ data.setDeptId(device.getDeptId());
+
data.setTopic(topic);
data.setProject(project);
data.setWarehouse(warehouse);
@@ -74,50 +99,110 @@ public class SmokeSensorHandler {
data.setConcentration(concentration != null ? concentration.longValue() : null);
data.setTemperature(temperature != null ? new BigDecimal(temperature) : null);
+ data.setCreateTime(new Date());
data.setIsDelete("0");
dataService.insertMqttSensorData(data);
- // ================== 事件 ==================
+ // ================== 5. WebSocket推送 ==================
+ pushWebSocket(device, event, battery, concentration, temperature);
+ // ================== 6. 事件逻辑 ==================
if ("silent".equals(event)) {
- insertEvent(device, "silent", "人为静音报警", "LOW");
+ changeStatus(device, "silent", "silent", "烟雾已消音", "LOW");
return;
}
if ("alarm".equals(event) || (concentration != null && concentration > 0)) {
- insertEvent(device, "alarm", "烟雾报警", "HIGH");
+ changeStatus(device, "alarm", "alarm", "烟雾报警", "HIGH");
return;
}
if ("removed".equals(event)) {
- insertEvent(device, "removed", "设备被拆/离位", "MEDIUM");
+ changeStatus(device, "removed", "removed", "设备被拆除", "MEDIUM");
return;
}
if ("low_battery".equals(event)) {
- insertEvent(device, "low_battery", "低电量告警", "LOW");
+ changeStatus(device, "low_battery", "low_battery", "电量低", "LOW");
return;
}
- if (battery != null && battery < 20) {
- log.info("[SMOKE] 电量偏低 deviceId={}, battery={}",
- device.getId(), battery);
- }
+ changeStatus(device, "normal", "recovery", "烟雾恢复正常", "LOW");
- log.info("[SMOKE] 正常数据 deviceId={}, battery={}, temp={}",
+ log.info("[SMOKE] normal data deviceId={}, battery={}, temp={}",
device.getId(), battery, temperature);
}
- private void insertEvent(MqttSensorDevice device, String type, String desc, String level) {
+
+ /**
+ * WebSocket推送(按dept隔离)
+ */
+ private void pushWebSocket(MqttSensorDevice device,
+ String event,
+ Integer battery,
+ Integer concentration,
+ Integer temperature) {
+
+ try {
+ JSONObject msg = new JSONObject();
+ msg.put("type", "smoke");
+ msg.put("deviceId", device.getId());
+ msg.put("deviceName", device.getDeviceName());
+ msg.put("deptId", device.getDeptId());
+
+ msg.put("event", event);
+ msg.put("battery", battery);
+ msg.put("concentration", concentration);
+ msg.put("temperature", temperature);
+
+ msg.put("time", System.currentTimeMillis());
+
+ sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
+
+ } catch (Exception e) {
+ log.error("[WebSocket] 推送失败 deviceId={}", device.getId(), e);
+ }
+ }
+
+
+ private void changeStatus(MqttSensorDevice device,
+ String newStatus,
+ String eventType,
+ String desc,
+ String level) {
+
+ String key = device.getId() + "_smoke";
+ String oldStatus = STATUS_CACHE.get(key);
+
+ if (newStatus.equals(oldStatus)) {
+ return;
+ }
+
+ STATUS_CACHE.put(key, newStatus);
+ insertEvent(device, eventType, desc, level);
+ }
+
+
+ /**
+ * 事件入库(已补 deptId)
+ */
+ private void insertEvent(MqttSensorDevice device,
+ String type,
+ String desc,
+ String level) {
MqttSensorEvent event = new MqttSensorEvent();
-
event.setDeviceId(device.getId());
+
+ event.setDeptId(device.getDeptId());
+
event.setEventType(type);
event.setEventDesc(desc);
event.setLevel(level);
event.setStatus("0");
+ event.setIsDelete("0");
+ event.setCreateTime(new Date());
eventService.insertMqttSensorEvent(event);
}
diff --git a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java
index a4a2d6a..11e84df 100644
--- a/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java
+++ b/src/main/java/com/shzg/project/worn/sensor/mqtt/handler/WaterSensorHandler.java
@@ -6,17 +6,16 @@ import com.shzg.project.worn.domain.MqttSensorData;
import com.shzg.project.worn.domain.MqttSensorEvent;
import com.shzg.project.worn.service.IMqttSensorDataService;
import com.shzg.project.worn.service.IMqttSensorEventService;
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-/**
- * 水浸传感器 Handler(对齐 Env 结构:状态机 + 防重复)
- */
@Slf4j
@Component
public class WaterSensorHandler {
@@ -27,86 +26,119 @@ public class WaterSensorHandler {
@Autowired
private IMqttSensorEventService eventService;
- /**
- * 状态缓存(防重复报警)
- * key = deviceId_water
- */
+ @Autowired
+ private WebSocketSessionManager sessionManager;
+
private static final Map STATUS_CACHE = new ConcurrentHashMap<>();
- /**
- * 主入口
- */
+
public void handle(MqttSensorDevice device, String topic, String payload) {
+ // ================= 安全校验 =================
+ if (device == null) {
+ log.error("[WATER] device为空");
+ return;
+ }
+
+ if (device.getDeptId() == null) {
+ log.error("[WATER] device未绑定deptId!deviceId={}", device.getId());
+ return;
+ }
+
log.info("[WATER] deviceId={}, topic={}, payload={}", device.getId(), topic, payload);
JSONObject json = parseJson(payload);
if (json == null) return;
- // 👉 从 data 字段解析(Base64)
- Integer water = parseWater(json);
- if (water == null) {
- log.warn("[WATER] 无法解析水浸状态");
+ WaterInfo info = parsePayload(json);
+ if (info == null) {
+ log.warn("[WATER] payload解析失败");
return;
}
TopicInfo topicInfo = parseTopic(topic);
- // 1️⃣ 数据入库
- saveData(device, topic, payload, topicInfo, water);
+ // ================== 1. 入库 ==================
+ saveData(device, topic, payload, topicInfo, info);
- // 2️⃣ 状态处理
- handleEvent(device, water);
+ // ================== 2. 普通数据推送 ==================
+ pushWebSocket(device, info);
+
+ // ================== 3. 事件处理 ==================
+ handleEvent(device, info.getWater());
}
+
+ // ================== WebSocket推送 ==================
+ private void pushWebSocket(MqttSensorDevice device, WaterInfo info) {
+
+ try {
+ JSONObject msg = new JSONObject();
+ msg.put("type", "water");
+ msg.put("deviceId", device.getId());
+ msg.put("deviceName", device.getDeviceName());
+ msg.put("deptId", device.getDeptId());
+
+ msg.put("battery", info.getBattery());
+ msg.put("water", info.getWater());
+
+ msg.put("status", (info.getWater() != null && info.getWater() == 1) ? "alarm" : "normal");
+ msg.put("time", System.currentTimeMillis());
+
+ sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
+
+ } catch (Exception e) {
+ log.error("[WATER] WebSocket推送失败 deviceId={}", device.getId(), e);
+ }
+ }
+
+
// ================== 事件处理 ==================
private void handleEvent(MqttSensorDevice device, Integer water) {
- String status;
+ if (water == null) return;
- // 👉 水浸:1=有水(报警),0=正常
- if (water == 1) {
- status = "alarm";
- } else {
- status = "normal";
- }
-
- changeStatus(device, status, water);
+ String status = (water == 1) ? "alarm" : "normal";
+ changeStatus(device, status);
}
- // ================== 状态变更 ==================
- private void changeStatus(MqttSensorDevice device,
- String newStatus,
- Integer water) {
+
+ private void changeStatus(MqttSensorDevice device, String newStatus) {
String key = device.getId() + "_water";
String oldStatus = STATUS_CACHE.get(key);
- // ❗ 防重复
- if (newStatus.equals(oldStatus)) {
- return;
- }
+ if (newStatus.equals(oldStatus)) return;
STATUS_CACHE.put(key, newStatus);
if ("alarm".equals(newStatus)) {
triggerEvent(device, "alarm", "HIGH", "浸水预警!");
- } else if ("normal".equals(newStatus)) {
- triggerEvent(device, "recovery", "LOW", "水浸正常");
+ } else {
+ triggerEvent(device, "recovery", "LOW", "水浸恢复正常");
}
}
- // ================== 事件记录 ==================
+
+ /**
+ * 🔥 事件写入 + 报警推送
+ */
private void triggerEvent(MqttSensorDevice device,
String type,
String level,
String desc) {
- log.warn("[WATER事件] deviceId={}, type={}, level={}, desc={}",
- device.getId(), type, level, desc);
+ log.warn("[WATER事件] deviceId={}, type={}, desc={}",
+ device.getId(), type, desc);
+ // ================== 1. 写库 ==================
MqttSensorEvent event = new MqttSensorEvent();
+
event.setDeviceId(device.getId());
+
+ // ✅ 核心(必须)
+ event.setDeptId(device.getDeptId());
+
event.setEventType(type);
event.setLevel(level);
event.setEventDesc(desc);
@@ -116,27 +148,51 @@ public class WaterSensorHandler {
eventService.insertMqttSensorEvent(event);
- // TODO: 联动排水 / 告警推送
+ // ================== 2. 报警推送 ==================
+ try {
+ JSONObject msg = new JSONObject();
+
+ msg.put("type", "alarm"); // 🔥关键
+ msg.put("deviceId", device.getId());
+ msg.put("deviceName", device.getDeviceName());
+ msg.put("deptId", device.getDeptId());
+
+ msg.put("eventType", type);
+ msg.put("level", level);
+ msg.put("eventDesc", desc);
+
+ msg.put("time", System.currentTimeMillis());
+
+ sessionManager.sendToDept(device.getDeptId(), msg.toJSONString());
+
+ } catch (Exception e) {
+ log.error("[WATER] 报警推送失败 deviceId={}", device.getId(), e);
+ }
}
- // ================== 数据入库 ==================
+
+ // ================== 入库 ==================
private void saveData(MqttSensorDevice device,
String topic,
String payload,
TopicInfo topicInfo,
- Integer water) {
+ WaterInfo info) {
MqttSensorData data = new MqttSensorData();
data.setDeviceId(device.getId());
+
+ // ✅ 数据隔离
+ data.setDeptId(device.getDeptId());
+
data.setTopic(topic);
data.setProject(topicInfo.project);
data.setWarehouse(topicInfo.warehouse);
-
data.setPayload(payload);
data.setDataJson(payload);
- data.setWaterStatus(water);
+ data.setWaterStatus(info.getWater());
+ data.setBattery(info.getBattery() == null ? null : info.getBattery().longValue());
data.setCreateTime(new Date());
data.setIsDelete("0");
@@ -144,7 +200,8 @@ public class WaterSensorHandler {
sensorDataService.insertMqttSensorData(data);
}
- // ================== JSON解析 ==================
+
+ // ================== JSON ==================
private JSONObject parseJson(String payload) {
try {
return JSONObject.parseObject(payload);
@@ -154,46 +211,8 @@ public class WaterSensorHandler {
}
}
- // ================== 解析水浸 ==================
- private Integer parseWater(JSONObject json) {
- try {
- // 👉 LoRaWAN 原始数据(Base64)
- String data = json.getString("data");
- if (data == null) return null;
-
- byte[] bytes = java.util.Base64.getDecoder().decode(data);
-
- /**
- * Milesight水浸协议(关键)
- * 一般结构:
- * [channel_id][channel_type][value]
- *
- * 水浸:
- * channel_id = 0x05
- * channel_type = 0x00
- * value = 0/1
- */
-
- for (int i = 0; i < bytes.length - 2; i++) {
-
- int channelId = bytes[i] & 0xFF;
- int channelType = bytes[i + 1] & 0xFF;
-
- // 👉 水浸
- if (channelId == 0x05 && channelType == 0x00) {
- return bytes[i + 2] & 0xFF;
- }
- }
-
- } catch (Exception e) {
- log.error("[WATER] 解析payload失败", e);
- }
-
- return null;
- }
-
- // ================== topic解析 ==================
+ // ================== topic ==================
private TopicInfo parseTopic(String topic) {
TopicInfo info = new TopicInfo();
try {
@@ -206,9 +225,61 @@ public class WaterSensorHandler {
return info;
}
- // ================== 内部结构 ==================
+
+ // ================== payload解析 ==================
+ private WaterInfo parsePayload(JSONObject json) {
+
+ try {
+ String data = json.getString("data");
+ if (data == null) return null;
+
+ byte[] bytes = Base64.getDecoder().decode(data);
+
+ WaterInfo result = new WaterInfo();
+
+ for (int i = 0; i < bytes.length - 2; ) {
+
+ int channelId = bytes[i] & 0xFF;
+ int channelType = bytes[i + 1] & 0xFF;
+
+ if (channelId == 0x01 && channelType == 0x75) {
+ result.setBattery(bytes[i + 2] & 0xFF);
+ i += 3;
+ continue;
+ }
+
+ if (channelId == 0x05 && channelType == 0x00) {
+ result.setWater(bytes[i + 2] & 0xFF);
+ i += 3;
+ continue;
+ }
+
+ i++;
+ }
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("[WATER] 解析payload失败", e);
+ }
+
+ return null;
+ }
+
+
private static class TopicInfo {
String project;
String warehouse;
}
+
+ private static class WaterInfo {
+ private Integer battery;
+ private Integer water;
+
+ public Integer getBattery() { return battery; }
+ public void setBattery(Integer battery) { this.battery = battery; }
+
+ public Integer getWater() { return water; }
+ public void setWater(Integer water) { this.water = water; }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/service/IDeviceCommandService.java b/src/main/java/com/shzg/project/worn/service/IDeviceCommandService.java
deleted file mode 100644
index 879ea7b..0000000
--- a/src/main/java/com/shzg/project/worn/service/IDeviceCommandService.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.shzg.project.worn.service;
-
-public interface IDeviceCommandService {
-
- void smokeStop(Long deviceId);
-
- void smokeStopOnce(Long deviceId);
-}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/service/IMqttSensorThresholdService.java b/src/main/java/com/shzg/project/worn/service/IMqttSensorThresholdService.java
index ed4323f..b37657b 100644
--- a/src/main/java/com/shzg/project/worn/service/IMqttSensorThresholdService.java
+++ b/src/main/java/com/shzg/project/worn/service/IMqttSensorThresholdService.java
@@ -59,11 +59,14 @@ public interface IMqttSensorThresholdService
*/
public int deleteMqttSensorThresholdById(Long id);
+
/**
- * 根据设备ID和指标类型获取阈值
- * @param deviceId
- * @param metricType
+ * 根据设备ID和部门ID获取阈值
+ * @param id
+ * @param deptId
+ * @param battery
* @return
*/
- MqttSensorThreshold getThreshold(Long deviceId, String metricType);
+ MqttSensorThreshold getThreshold(Long id, Long deptId, String battery);
+
}
diff --git a/src/main/java/com/shzg/project/worn/service/IMqttTopicConfigService.java b/src/main/java/com/shzg/project/worn/service/IMqttTopicConfigService.java
new file mode 100644
index 0000000..511c8e9
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/service/IMqttTopicConfigService.java
@@ -0,0 +1,68 @@
+package com.shzg.project.worn.service;
+
+import java.util.List;
+import com.shzg.project.worn.domain.MqttTopicConfig;
+
+/**
+ * MQTT主题配置Service接口
+ *
+ * @author shzg
+ * @date 2026-04-08
+ */
+public interface IMqttTopicConfigService
+{
+ /**
+ * 查询MQTT主题配置
+ *
+ * @param id MQTT主题配置主键
+ * @return MQTT主题配置
+ */
+ public MqttTopicConfig selectMqttTopicConfigById(Long id);
+
+ /**
+ * 查询MQTT主题配置列表
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return MQTT主题配置集合
+ */
+ public List selectMqttTopicConfigList(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 查询所有启用的 MQTT 主题配置
+ *
+ * @return MQTT 主题配置集合
+ */
+ public List selectEnabledMqttTopicConfigList();
+
+ /**
+ * 新增MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ public int insertMqttTopicConfig(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 修改MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ public int updateMqttTopicConfig(MqttTopicConfig mqttTopicConfig);
+
+ /**
+ * 批量删除MQTT主题配置
+ *
+ * @param ids 需要删除的MQTT主题配置主键集合
+ * @return 结果
+ */
+ public int deleteMqttTopicConfigByIds(Long[] ids);
+
+ /**
+ * 删除MQTT主题配置信息
+ *
+ * @param id MQTT主题配置主键
+ * @return 结果
+ */
+ public int deleteMqttTopicConfigById(Long id);
+}
diff --git a/src/main/java/com/shzg/project/worn/service/IWornInboundBillService.java b/src/main/java/com/shzg/project/worn/service/IWornInboundBillService.java
index 1fe1e12..2a1228e 100644
--- a/src/main/java/com/shzg/project/worn/service/IWornInboundBillService.java
+++ b/src/main/java/com/shzg/project/worn/service/IWornInboundBillService.java
@@ -74,4 +74,9 @@ public interface IWornInboundBillService
* @return 结果
*/
int voidBill(String billNo);
+
+ /**
+ * 生成入库单PDF
+ */
+ byte[] generatePdf(Long id) throws Exception;
}
diff --git a/src/main/java/com/shzg/project/worn/service/SocketControlService.java b/src/main/java/com/shzg/project/worn/service/SocketControlService.java
new file mode 100644
index 0000000..e459ba6
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/service/SocketControlService.java
@@ -0,0 +1,27 @@
+package com.shzg.project.worn.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.shzg.project.worn.sensor.config.MqttPublishClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SocketControlService {
+
+ @Autowired
+ private MqttPublishClient mqttPublishClient;
+
+ public void controlSocket(String devEui, boolean on) {
+
+ String topic = "worn/tangshan/dianchi/downlink/" + devEui.toLowerCase();
+
+ String base64 = on ? "CAEA/w==" : "CAAA/w==";
+
+ JSONObject json = new JSONObject();
+ json.put("confirmed", true);
+ json.put("fport", 85);
+ json.put("data", base64);
+
+ mqttPublishClient.publish(topic, json.toJSONString());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/service/impl/DeviceCommandServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/DeviceCommandServiceImpl.java
deleted file mode 100644
index 5f872d8..0000000
--- a/src/main/java/com/shzg/project/worn/service/impl/DeviceCommandServiceImpl.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package com.shzg.project.worn.service.impl;
-
-import com.alibaba.fastjson2.JSONObject;
-import com.shzg.project.worn.domain.MqttSensorCommand;
-import com.shzg.project.worn.domain.MqttSensorDevice;
-import com.shzg.project.worn.mapper.MqttSensorDeviceMapper;
-import com.shzg.project.worn.sensor.config.MqttPublishClient;
-import com.shzg.project.worn.service.IDeviceCommandService;
-import com.shzg.project.worn.service.IMqttSensorCommandService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.Base64;
-import java.util.Date;
-
-@Service
-public class DeviceCommandServiceImpl implements IDeviceCommandService {
-
- @Autowired
- private MqttPublishClient mqttPublishClient;
-
- @Autowired
- private MqttSensorDeviceMapper deviceMapper;
-
- @Autowired
- private IMqttSensorCommandService commandService;
-
- // ================== Topic ==================
-
- private String buildDownTopic(String devEui) {
- return "worn/tangshan/dianchi/downlink/" + devEui;
- }
-
- // ================== 指令 ==================
-
- @Override
- public void smokeStop(Long deviceId) {
-
- MqttSensorDevice device = getDevice(deviceId);
-
- String topic = buildDownTopic(device.getDevEui());
- String base64 = hexToBase64("2219000000C5");
-
- String payload = buildPayload(base64);
-
- executeCommand(deviceId, topic, "smokeStop", payload);
- }
-
- @Override
- public void smokeStopOnce(Long deviceId) {
-
- MqttSensorDevice device = getDevice(deviceId);
-
- String topic = buildDownTopic(device.getDevEui());
- String base64 = hexToBase64("2218000000C6");
-
- String payload = buildPayload(base64);
-
- executeCommand(deviceId, topic, "smokeStopOnce", payload);
- }
-
- // ================== 指令执行 ==================
-
- private void executeCommand(Long deviceId, String topic, String command, String payload) {
-
- MqttSensorCommand cmd = new MqttSensorCommand();
- cmd.setDeviceId(deviceId);
- cmd.setTopic(topic);
- cmd.setCommand(command);
- cmd.setPayload(payload);
- cmd.setStatus("0");
- cmd.setSendTime(new Date());
-
- commandService.insertMqttSensorCommand(cmd);
-
- try {
- mqttPublishClient.publish(topic, payload);
-
- cmd.setStatus("1");
- commandService.updateMqttSensorCommand(cmd);
-
- } catch (Exception e) {
-
- cmd.setStatus("2");
- commandService.updateMqttSensorCommand(cmd);
-
- throw new RuntimeException("指令发送失败", e);
- }
- }
-
- // ================== 公共方法 ==================
-
- private MqttSensorDevice getDevice(Long deviceId) {
-
- if (deviceId == null) {
- throw new RuntimeException("设备ID不能为空");
- }
-
- MqttSensorDevice device = deviceMapper.selectMqttSensorDeviceById(deviceId);
-
- if (device == null) {
- throw new RuntimeException("设备不存在");
- }
-
- if (device.getDevEui() == null || device.getDevEui().isEmpty()) {
- throw new RuntimeException("设备未配置devEUI");
- }
-
- if (!"0".equals(device.getStatus())) {
- throw new RuntimeException("设备已禁用");
- }
-
- return device;
- }
-
- // ================== 工具方法 ==================
-
- private String hexToBase64(String hex) {
- byte[] bytes = hexStringToByteArray(hex);
- return Base64.getEncoder().encodeToString(bytes);
- }
-
-
- private String buildPayload(String base64) {
-
- JSONObject json = new JSONObject();
- json.put("confirmed", true);
- json.put("fport", 85);
- json.put("data", base64);
-
- return json.toJSONString();
- }
-
- private byte[] hexStringToByteArray(String hex) {
-
- hex = hex.replace(" ", "").toUpperCase();
-
- int len = hex.length();
- byte[] data = new byte[len / 2];
-
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
- }
-
- return data;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDataServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDataServiceImpl.java
index 1756bc6..647240d 100644
--- a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDataServiceImpl.java
+++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorDataServiceImpl.java
@@ -2,6 +2,8 @@ package com.shzg.project.worn.service.impl;
import java.util.List;
import com.shzg.common.utils.DateUtils;
+import com.shzg.common.utils.DeptScopeUtils;
+import com.shzg.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shzg.project.worn.mapper.MqttSensorDataMapper;
@@ -41,6 +43,10 @@ public class MqttSensorDataServiceImpl implements IMqttSensorDataService
@Override
public List selectMqttSensorDataList(MqttSensorData mqttSensorData)
{
+ if (!SecurityUtils.isAdmin())
+ {
+ mqttSensorData.setDeptIds(DeptScopeUtils.getDeptScope());
+ }
return mqttSensorDataMapper.selectMqttSensorDataList(mqttSensorData);
}
diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java
index fb6b84c..46483c0 100644
--- a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java
+++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorEventServiceImpl.java
@@ -2,6 +2,8 @@ package com.shzg.project.worn.service.impl;
import java.util.List;
import com.shzg.common.utils.DateUtils;
+import com.shzg.common.utils.DeptScopeUtils;
+import com.shzg.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shzg.project.worn.mapper.MqttSensorEventMapper;
@@ -41,6 +43,12 @@ public class MqttSensorEventServiceImpl implements IMqttSensorEventService
@Override
public List selectMqttSensorEventList(MqttSensorEvent mqttSensorEvent)
{
+ // ================= 数据权限控制 =================
+ if (!SecurityUtils.isAdmin())
+ {
+ mqttSensorEvent.setDeptIds(DeptScopeUtils.getDeptScope());
+ }
+
return mqttSensorEventMapper.selectMqttSensorEventList(mqttSensorEvent);
}
diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorThresholdServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorThresholdServiceImpl.java
index bc440c9..60c528c 100644
--- a/src/main/java/com/shzg/project/worn/service/impl/MqttSensorThresholdServiceImpl.java
+++ b/src/main/java/com/shzg/project/worn/service/impl/MqttSensorThresholdServiceImpl.java
@@ -1,7 +1,13 @@
package com.shzg.project.worn.service.impl;
import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
import com.shzg.common.utils.DateUtils;
+import com.shzg.common.utils.DeptScopeUtils;
+import com.shzg.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shzg.project.worn.mapper.MqttSensorThresholdMapper;
@@ -9,100 +15,128 @@ import com.shzg.project.worn.domain.MqttSensorThreshold;
import com.shzg.project.worn.service.IMqttSensorThresholdService;
/**
- * 传感器阈值配置Service业务层处理
- *
- * @author shzg
- * @date 2026-04-01
+ * 传感器阈值配置Service业务层处理(最终版)
+ *
+ * 规则:
+ * 1. 设备优先
+ * 2. 仓库兜底
+ * 3. 统一入口(无旧方法)
*/
@Service
-public class MqttSensorThresholdServiceImpl implements IMqttSensorThresholdService
+public class MqttSensorThresholdServiceImpl implements IMqttSensorThresholdService
{
+ /**
+ * 阈值缓存
+ * key: deviceId:deptId:metric
+ */
+ private final ConcurrentMap> thresholdCache = new ConcurrentHashMap<>();
+
@Autowired
private MqttSensorThresholdMapper mqttSensorThresholdMapper;
- /**
- * 查询传感器阈值配置
- *
- * @param id 传感器阈值配置主键
- * @return 传感器阈值配置
- */
+ // ==================== 基础CRUD ====================
+
@Override
public MqttSensorThreshold selectMqttSensorThresholdById(Long id)
{
return mqttSensorThresholdMapper.selectMqttSensorThresholdById(id);
}
- /**
- * 查询传感器阈值配置列表
- *
- * @param mqttSensorThreshold 传感器阈值配置
- * @return 传感器阈值配置
- */
@Override
public List selectMqttSensorThresholdList(MqttSensorThreshold mqttSensorThreshold)
{
+ if (!SecurityUtils.isAdmin())
+ {
+ mqttSensorThreshold.setDeptIds(DeptScopeUtils.getDeptScope());
+ }
return mqttSensorThresholdMapper.selectMqttSensorThresholdList(mqttSensorThreshold);
}
- /**
- * 新增传感器阈值配置
- *
- * @param mqttSensorThreshold 传感器阈值配置
- * @return 结果
- */
@Override
public int insertMqttSensorThreshold(MqttSensorThreshold mqttSensorThreshold)
{
mqttSensorThreshold.setCreateTime(DateUtils.getNowDate());
- return mqttSensorThresholdMapper.insertMqttSensorThreshold(mqttSensorThreshold);
+ int rows = mqttSensorThresholdMapper.insertMqttSensorThreshold(mqttSensorThreshold);
+ clearThresholdCache();
+ return rows;
}
- /**
- * 修改传感器阈值配置
- *
- * @param mqttSensorThreshold 传感器阈值配置
- * @return 结果
- */
@Override
public int updateMqttSensorThreshold(MqttSensorThreshold mqttSensorThreshold)
{
mqttSensorThreshold.setUpdateTime(DateUtils.getNowDate());
- return mqttSensorThresholdMapper.updateMqttSensorThreshold(mqttSensorThreshold);
+ int rows = mqttSensorThresholdMapper.updateMqttSensorThreshold(mqttSensorThreshold);
+ clearThresholdCache();
+ return rows;
}
- /**
- * 批量删除传感器阈值配置
- *
- * @param ids 需要删除的传感器阈值配置主键
- * @return 结果
- */
@Override
public int deleteMqttSensorThresholdByIds(Long[] ids)
{
- return mqttSensorThresholdMapper.deleteMqttSensorThresholdByIds(ids);
+ int rows = mqttSensorThresholdMapper.deleteMqttSensorThresholdByIds(ids);
+ clearThresholdCache();
+ return rows;
}
- /**
- * 删除传感器阈值配置信息
- *
- * @param id 传感器阈值配置主键
- * @return 结果
- */
@Override
public int deleteMqttSensorThresholdById(Long id)
{
- return mqttSensorThresholdMapper.deleteMqttSensorThresholdById(id);
+ int rows = mqttSensorThresholdMapper.deleteMqttSensorThresholdById(id);
+ clearThresholdCache();
+ return rows;
}
-
/**
- * 根据设备id和指标类型获取阈值配置
- * @param deviceId
- * @param metricType
- * @return
+ * 获取阈值(设备优先 → 仓库兜底)
*/
@Override
- public MqttSensorThreshold getThreshold(Long deviceId, String metricType)
+ public MqttSensorThreshold getThreshold(Long deviceId, Long deptId, String metricType)
{
- return mqttSensorThresholdMapper.selectByDeviceAndMetric(deviceId, metricType);
+ if (metricType == null || metricType.trim().isEmpty()) {
+ return null;
+ }
+
+ String cacheKey = buildCacheKey(deviceId, deptId, metricType);
+
+ Optional cached = thresholdCache.computeIfAbsent(cacheKey, key -> {
+
+ // 1️⃣ 设备优先
+ if (deviceId != null) {
+ MqttSensorThreshold t =
+ mqttSensorThresholdMapper.selectByDeviceAndMetric(deviceId, metricType);
+ if (t != null) {
+ return Optional.of(t);
+ }
+ }
+
+ // 2️⃣ 仓库兜底
+ if (deptId != null) {
+ MqttSensorThreshold t =
+ mqttSensorThresholdMapper.selectByDeptAndMetric(deptId, metricType);
+ if (t != null) {
+ return Optional.of(t);
+ }
+ }
+
+ // 3️⃣ 未配置
+ return Optional.empty();
+ });
+
+ return cached.orElse(null);
}
-}
+
+ // ==================== 工具方法 ====================
+
+ private String buildCacheKey(Long deviceId, Long deptId, String metricType)
+ {
+ return (deviceId == null ? "0" : deviceId)
+ + ":" +
+ (deptId == null ? "0" : deptId)
+ + ":" +
+ metricType.trim().toLowerCase();
+ }
+
+ private void clearThresholdCache()
+ {
+ thresholdCache.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/service/impl/MqttTopicConfigServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/MqttTopicConfigServiceImpl.java
new file mode 100644
index 0000000..5cc2175
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/service/impl/MqttTopicConfigServiceImpl.java
@@ -0,0 +1,107 @@
+package com.shzg.project.worn.service.impl;
+
+import java.util.List;
+import com.shzg.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.shzg.project.worn.mapper.MqttTopicConfigMapper;
+import com.shzg.project.worn.domain.MqttTopicConfig;
+import com.shzg.project.worn.service.IMqttTopicConfigService;
+
+/**
+ * MQTT主题配置Service业务层处理
+ *
+ * @author shzg
+ * @date 2026-04-08
+ */
+@Service
+public class MqttTopicConfigServiceImpl implements IMqttTopicConfigService
+{
+ @Autowired
+ private MqttTopicConfigMapper mqttTopicConfigMapper;
+
+ /**
+ * 查询MQTT主题配置
+ *
+ * @param id MQTT主题配置主键
+ * @return MQTT主题配置
+ */
+ @Override
+ public MqttTopicConfig selectMqttTopicConfigById(Long id)
+ {
+ return mqttTopicConfigMapper.selectMqttTopicConfigById(id);
+ }
+
+ /**
+ * 查询MQTT主题配置列表
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return MQTT主题配置
+ */
+ @Override
+ public List selectMqttTopicConfigList(MqttTopicConfig mqttTopicConfig)
+ {
+ return mqttTopicConfigMapper.selectMqttTopicConfigList(mqttTopicConfig);
+ }
+
+ /**
+ * 查询所有启用的 MQTT 主题配置
+ *
+ * @return MQTT 主题配置集合
+ */
+ @Override
+ public List selectEnabledMqttTopicConfigList()
+ {
+ return mqttTopicConfigMapper.selectEnabledMqttTopicConfigList();
+ }
+
+ /**
+ * 新增MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ @Override
+ public int insertMqttTopicConfig(MqttTopicConfig mqttTopicConfig)
+ {
+ mqttTopicConfig.setCreateTime(DateUtils.getNowDate());
+ return mqttTopicConfigMapper.insertMqttTopicConfig(mqttTopicConfig);
+ }
+
+ /**
+ * 修改MQTT主题配置
+ *
+ * @param mqttTopicConfig MQTT主题配置
+ * @return 结果
+ */
+ @Override
+ public int updateMqttTopicConfig(MqttTopicConfig mqttTopicConfig)
+ {
+ mqttTopicConfig.setUpdateTime(DateUtils.getNowDate());
+ return mqttTopicConfigMapper.updateMqttTopicConfig(mqttTopicConfig);
+ }
+
+ /**
+ * 批量删除MQTT主题配置
+ *
+ * @param ids 需要删除的MQTT主题配置主键
+ * @return 结果
+ */
+ @Override
+ public int deleteMqttTopicConfigByIds(Long[] ids)
+ {
+ return mqttTopicConfigMapper.deleteMqttTopicConfigByIds(ids);
+ }
+
+ /**
+ * 删除MQTT主题配置信息
+ *
+ * @param id MQTT主题配置主键
+ * @return 结果
+ */
+ @Override
+ public int deleteMqttTopicConfigById(Long id)
+ {
+ return mqttTopicConfigMapper.deleteMqttTopicConfigById(id);
+ }
+}
diff --git a/src/main/java/com/shzg/project/worn/service/impl/WornInboundBillServiceImpl.java b/src/main/java/com/shzg/project/worn/service/impl/WornInboundBillServiceImpl.java
index a2f37e6..b5ef6aa 100644
--- a/src/main/java/com/shzg/project/worn/service/impl/WornInboundBillServiceImpl.java
+++ b/src/main/java/com/shzg/project/worn/service/impl/WornInboundBillServiceImpl.java
@@ -1,7 +1,27 @@
package com.shzg.project.worn.service.impl;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
+
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.layout.Document;
+import com.itextpdf.layout.element.Cell;
+import com.itextpdf.layout.element.Paragraph;
+import com.itextpdf.layout.element.Table;
+import com.itextpdf.layout.properties.TextAlignment;
+import com.itextpdf.layout.properties.VerticalAlignment;
+import org.apache.commons.io.IOUtils;
import com.shzg.common.utils.DateUtils;
import com.shzg.common.utils.SecurityUtils;
import com.shzg.common.utils.StringUtils;
@@ -14,12 +34,16 @@ import com.shzg.project.worn.domain.dto.WornInboundPartialFinishDTO;
import com.shzg.project.worn.domain.dto.WornInboundPartialFinishItemDTO;
import com.shzg.project.worn.mapper.WornInboundItemMapper;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import com.shzg.project.worn.mapper.WornInboundBillMapper;
import com.shzg.project.worn.domain.WornInboundBill;
import com.shzg.project.worn.service.IWornInboundBillService;
import org.springframework.transaction.annotation.Transactional;
+
/**
* 入库库存Service业务层处理
*
@@ -27,7 +51,7 @@ import org.springframework.transaction.annotation.Transactional;
* @date 2026-03-26
*/
@Service
-public class WornInboundBillServiceImpl implements IWornInboundBillService
+public class WornInboundBillServiceImpl implements IWornInboundBillService
{
@Autowired
private WornInboundBillMapper wornInboundBillMapper;
@@ -41,6 +65,12 @@ public class WornInboundBillServiceImpl implements IWornInboundBillService
@Autowired
private WornUniqueCodeEventMapper eventMapper;
+ @Autowired
+ private ResourceLoader resourceLoader;
+
+ @Value("${worn.pdf-font-locations:classpath:fonts/simhei.ttf,/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc,/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc,/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,/usr/share/fonts/truetype/arphic/ukai.ttc,C:/Windows/Fonts/simhei.ttf,C:/Windows/Fonts/simsun.ttc}")
+ private String pdfFontLocations;
+
/**
* 查询入库库存
@@ -92,6 +122,7 @@ public class WornInboundBillServiceImpl implements IWornInboundBillService
/* ================== 3. 主表赋值 ================== */
wornInboundBill.setCreateTime(DateUtils.getNowDate());
wornInboundBill.setCreateBy(String.valueOf(userId));
+ wornInboundBill.setInboundTime(DateUtils.getNowDate());
wornInboundBill.setBillType("0");
wornInboundBill.setStatus("0");
wornInboundBill.setIsDelete("0");
@@ -475,7 +506,189 @@ public class WornInboundBillServiceImpl implements IWornInboundBillService
return 1;
}
+ @Override
+ public byte[] generatePdf(Long id) throws Exception
+ {
+ WornInboundBill bill = wornInboundBillMapper.selectWornInboundBillById(id);
+ if (bill == null)
+ {
+ throw new RuntimeException("入库单不存在");
+ }
+ List itemList = wornInboundItemMapper.selectWornInboundItemByBillId(id);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ PdfWriter writer = new PdfWriter(out);
+ PdfDocument pdf = new PdfDocument(writer);
+ Document document = new Document(pdf, PageSize.A4);
+
+ // ===== 中文字体 =====
+ PdfFont font = createPdfFont();
+ document.setFont(font);
+
+ // ===== 标题 =====
+ document.add(new Paragraph("智汇APP报表系统")
+ .setTextAlignment(TextAlignment.CENTER)
+ .setFontSize(16));
+
+ document.add(new Paragraph("入库单")
+ .setTextAlignment(TextAlignment.CENTER)
+ .setFontSize(14)
+ .setBold());
+
+ document.add(new Paragraph("\n"));
+
+ // ===== 表格 =====
+ // 新增“存储设施类型”后,列数改为11列
+ float[] widths = {2, 6, 6, 5, 4, 4, 4, 2, 3, 3, 4};
+ Table table = new Table(widths).useAllAvailableWidth();
+
+ // ===== 表头 =====
+ table.addCell(cell("序号"));
+ table.addCell(cell("入库批次编号"));
+ table.addCell(cell("入库时间"));
+ table.addCell(cell("存储设施类型"));
+ table.addCell(cell("物料名称"));
+ table.addCell(cell("规格型号"));
+ table.addCell(cell("入库数量"));
+ table.addCell(cell("单位"));
+ table.addCell(cell("操作人"));
+ table.addCell(cell("备注"));
+ table.addCell(cell("唯一码"));
+
+ // ===== 时间格式 =====
+ String datePart = "";
+ String timePart = "";
+ if (bill.getInboundTime() != null)
+ {
+ datePart = new SimpleDateFormat("yyyy-MM-dd").format(bill.getInboundTime());
+ timePart = new SimpleDateFormat("HH:mm:ss").format(bill.getInboundTime());
+ }
+
+ // ===== 数据 =====
+ int index = 1;
+ BigDecimal total = BigDecimal.ZERO;
+
+ for (WornInboundItem item : itemList)
+ {
+ table.addCell(cell(String.valueOf(index++)));
+ table.addCell(cell(bill.getBillNo()));
+
+ Cell timeCell = new Cell()
+ .add(new Paragraph(datePart + "\n" + timePart))
+ .setTextAlignment(TextAlignment.CENTER)
+ .setVerticalAlignment(VerticalAlignment.MIDDLE);
+ table.addCell(timeCell);
+
+ // ===== 新增:存储设施类型(warehouse_name)=====
+ table.addCell(cell(bill.getWarehouseName()));
+
+ table.addCell(cell(item.getMaterialName()));
+ table.addCell(cell(buildSpecificationText(item)));
+ table.addCell(cell(item.getQuantity() == null ? "" : item.getQuantity().toString()));
+ table.addCell(cell(item.getUnitName()));
+ table.addCell(cell(item.getOperatorName()));
+ table.addCell(cell(item.getRemark()));
+ table.addCell(cell(item.getUniqueCode() == null ? "" : item.getUniqueCode().toString()));
+
+ if (item.getQuantity() != null)
+ {
+ total = total.add(item.getQuantity());
+ }
+ }
+
+ // ===== 合计 =====
+ // 前6列合并:序号、入库批次编号、入库时间、存储设施类型、物料名称、规格型号
+ table.addCell(new Cell(1, 6)
+ .add(new Paragraph("合计"))
+ .setTextAlignment(TextAlignment.CENTER)
+ .setVerticalAlignment(VerticalAlignment.MIDDLE));
+
+ // 入库数量
+ table.addCell(cell(total.toString()));
+
+ // 剩余4列留空:单位、操作人、备注、唯一码
+ table.addCell(new Cell(1, 4)
+ .add(new Paragraph(""))
+ .setTextAlignment(TextAlignment.CENTER)
+ .setVerticalAlignment(VerticalAlignment.MIDDLE));
+
+ document.add(table);
+
+ document.close();
+
+ return out.toByteArray();
+ }
+
+ private Cell cell(String text)
+ {
+ return new Cell()
+ .add(new Paragraph(text == null ? "" : text))
+ .setTextAlignment(TextAlignment.CENTER)
+ .setVerticalAlignment(VerticalAlignment.MIDDLE);
+ }
+
+ private String buildSpecificationText(WornInboundItem item)
+ {
+ String specification = item.getSpecification();
+ String model = item.getModel();
+
+ if (StringUtils.isNotEmpty(specification) && StringUtils.isNotEmpty(model)) {
+ return specification + "/" + model;
+ }
+ if (StringUtils.isNotEmpty(specification)) {
+ return specification;
+ }
+ if (StringUtils.isNotEmpty(model)) {
+ return model;
+ }
+ return "";
+ }
+
+ private PdfFont createPdfFont() throws IOException
+ {
+ for (String location : pdfFontLocations.split(",")) {
+ String trimmed = location.trim();
+ if (trimmed.isEmpty()) {
+ continue;
+ }
+
+ byte[] fontBytes = loadFontBytes(trimmed);
+ if (fontBytes == null) {
+ continue;
+ }
+
+ return PdfFontFactory.createFont(
+ fontBytes,
+ "Identity-H",
+ PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED
+ );
+ }
+
+ throw new IllegalStateException("未找到可用的PDF中文字体,请检查 worn.pdf-font-locations 配置");
+ }
+
+ private byte[] loadFontBytes(String location) throws IOException
+ {
+ if (location.startsWith("classpath:")) {
+ Resource resource = resourceLoader.getResource(location);
+ if (!resource.exists()) {
+ return null;
+ }
+ return IOUtils.toByteArray(resource.getInputStream());
+ }
+
+ Path path = location.startsWith("file:")
+ ? Paths.get(location.substring("file:".length()))
+ : Paths.get(location);
+
+ if (!Files.exists(path) || Files.isDirectory(path)) {
+ return null;
+ }
+
+ return Files.readAllBytes(path);
+ }
}
diff --git a/src/main/java/com/shzg/project/worn/websocket/config/WebSocketConfig.java b/src/main/java/com/shzg/project/worn/websocket/config/WebSocketConfig.java
new file mode 100644
index 0000000..070feba
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/websocket/config/WebSocketConfig.java
@@ -0,0 +1,20 @@
+package com.shzg.project.worn.websocket.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * WebSocket配置类
+ */
+@Configuration
+public class WebSocketConfig {
+
+ /**
+ * 开启WebSocket支持
+ */
+ @Bean
+ public ServerEndpointExporter serverEndpointExporter() {
+ return new ServerEndpointExporter();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/websocket/domain/WsUserInfo.java b/src/main/java/com/shzg/project/worn/websocket/domain/WsUserInfo.java
new file mode 100644
index 0000000..a436f50
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/websocket/domain/WsUserInfo.java
@@ -0,0 +1,32 @@
+package com.shzg.project.worn.websocket.domain;
+
+import lombok.Data;
+
+import java.util.Set;
+
+/**
+ * WebSocket用户信息
+ */
+@Data
+public class WsUserInfo {
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 用户名
+ */
+ private String userName;
+
+ /**
+ * 是否管理员
+ */
+ private boolean isAdmin;
+
+ /**
+ * 可访问的部门ID集合
+ */
+ private Set deptIds;
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java b/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java
new file mode 100644
index 0000000..fcef0d8
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/websocket/endpoint/WornWebSocketServer.java
@@ -0,0 +1,112 @@
+package com.shzg.project.worn.websocket.endpoint;
+
+import com.shzg.project.system.domain.SysDept;
+import com.shzg.project.system.mapper.SysDeptMapper;
+import com.shzg.project.worn.websocket.domain.WsUserInfo;
+import com.shzg.project.worn.websocket.manager.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Slf4j
+@Component
+@ServerEndpoint("/ws")
+public class WornWebSocketServer {
+
+ private static WebSocketSessionManager sessionManager;
+ private static SysDeptMapper deptMapper;
+
+ @Autowired
+ public void setSessionManager(WebSocketSessionManager manager) {
+ WornWebSocketServer.sessionManager = manager;
+ }
+
+ @Autowired
+ public void setDeptMapper(SysDeptMapper mapper) {
+ WornWebSocketServer.deptMapper = mapper;
+ }
+
+ @OnOpen
+ public void onOpen(Session session) {
+
+ log.info("[WebSocket] 连接建立 sessionId={}", session.getId());
+
+ String query = session.getQueryString();
+
+ if (query == null || !query.contains("deptId")) {
+ closeSession(session, "missing deptId");
+ return;
+ }
+
+ Long deptId;
+
+ try {
+ deptId = parseDeptId(query);
+ } catch (Exception e) {
+ closeSession(session, "invalid deptId");
+ return;
+ }
+
+ List deptList = deptMapper.selectDeptAndChildren(deptId, null);
+
+ Set deptIds = new HashSet<>();
+ for (SysDept dept : deptList) {
+ deptIds.add(dept.getDeptId());
+ }
+
+ // 构造用户
+ WsUserInfo userInfo = new WsUserInfo();
+ userInfo.setUserId(1L);
+ userInfo.setUserName("test");
+ userInfo.setAdmin(false);
+ userInfo.setDeptIds(deptIds);
+
+ sessionManager.register(session, userInfo);
+
+ log.info("[WebSocket] 注册成功 sessionId={}, deptIds={}",
+ session.getId(), deptIds);
+ }
+
+ @OnClose
+ public void onClose(Session session) {
+ sessionManager.remove(session);
+ }
+
+ @OnError
+ public void onError(Session session, Throwable error) {
+ if (session != null) {
+ sessionManager.remove(session);
+ }
+ }
+
+ private Long parseDeptId(String query) {
+
+ String[] params = query.split("&");
+
+ for (String param : params) {
+ if (param.startsWith("deptId=")) {
+ return Long.parseLong(param.substring(7));
+ }
+ }
+
+ throw new IllegalArgumentException("deptId not found");
+ }
+
+ private void closeSession(Session session, String reason) {
+ try {
+ session.close(new CloseReason(
+ CloseReason.CloseCodes.CANNOT_ACCEPT,
+ reason
+ ));
+ } catch (IOException e) {
+ log.error("关闭失败", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/shzg/project/worn/websocket/manager/WebSocketSessionManager.java b/src/main/java/com/shzg/project/worn/websocket/manager/WebSocketSessionManager.java
new file mode 100644
index 0000000..b8dca27
--- /dev/null
+++ b/src/main/java/com/shzg/project/worn/websocket/manager/WebSocketSessionManager.java
@@ -0,0 +1,162 @@
+package com.shzg.project.worn.websocket.manager;
+
+import com.shzg.project.worn.websocket.domain.WsUserInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * WebSocket Session 管理器
+ */
+@Slf4j
+@Component
+public class WebSocketSessionManager {
+
+ /**
+ * sessionId -> 用户信息
+ */
+ private final ConcurrentHashMap sessionUserMap = new ConcurrentHashMap<>();
+
+ /**
+ * deptId -> session集合
+ */
+ private final ConcurrentHashMap> deptSessionMap = new ConcurrentHashMap<>();
+
+
+ /**
+ * 注册连接
+ */
+ public void register(Session session, WsUserInfo userInfo) {
+
+ String sessionId = session.getId();
+
+ // 保存用户信息
+ sessionUserMap.put(sessionId, userInfo);
+
+ // 加入部门分组
+ for (Long deptId : userInfo.getDeptIds()) {
+ deptSessionMap
+ .computeIfAbsent(deptId, k -> new CopyOnWriteArraySet<>())
+ .add(session);
+ }
+
+ log.info("[WebSocket] 连接注册 sessionId={}, userId={}, deptIds={}",
+ sessionId, userInfo.getUserId(), userInfo.getDeptIds());
+ }
+
+
+ /**
+ * 移除连接
+ */
+ public void remove(Session session) {
+
+ String sessionId = session.getId();
+ WsUserInfo userInfo = sessionUserMap.remove(sessionId);
+
+ if (userInfo != null) {
+ for (Long deptId : userInfo.getDeptIds()) {
+
+ Set sessions = deptSessionMap.get(deptId);
+ if (sessions != null) {
+ sessions.remove(session);
+
+ // 清理空集合(优化点)
+ if (sessions.isEmpty()) {
+ deptSessionMap.remove(deptId);
+ }
+ }
+ }
+ }
+
+ log.info("[WebSocket] 连接移除 sessionId={}", sessionId);
+ }
+
+
+ /**
+ * 广播(所有连接)
+ */
+ public void sendAll(String message) {
+
+ for (String sessionId : sessionUserMap.keySet()) {
+ Session session = getSession(sessionId);
+ send(session, message);
+ }
+ }
+
+
+ /**
+ * 按部门推送
+ */
+ public void sendToDept(Long deptId, String message) {
+
+ Set sessions = deptSessionMap.get(deptId);
+ if (sessions == null || sessions.isEmpty()) {
+ return;
+ }
+
+ for (Session session : sessions) {
+ send(session, message);
+ }
+ }
+
+
+ /**
+ * 发送消息(统一封装)
+ */
+ private void send(Session session, String message) {
+
+ if (session == null) {
+ return;
+ }
+
+ // 🔥 关键:判活
+ if (!session.isOpen()) {
+ remove(session);
+ return;
+ }
+
+ try {
+ session.getBasicRemote().sendText(message);
+ } catch (IOException e) {
+
+ log.error("[WebSocket] 推送失败 sessionId={}", session.getId(), e);
+
+ // 🔥 失败直接清理
+ remove(session);
+
+ try {
+ session.close();
+ } catch (IOException ex) {
+ log.error("[WebSocket] 关闭连接失败", ex);
+ }
+ }
+ }
+
+
+ /**
+ * 获取session(内部用)
+ */
+ private Session getSession(String sessionId) {
+
+ WsUserInfo userInfo = sessionUserMap.get(sessionId);
+ if (userInfo == null) {
+ return null;
+ }
+
+ // 从deptMap反查(简单处理)
+ for (Set sessions : deptSessionMap.values()) {
+ for (Session s : sessions) {
+ if (s.getId().equals(sessionId)) {
+ return s;
+ }
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index db09a80..78098bc 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -16,7 +16,7 @@ ruoyi:
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
- port: 8082
+ port: 8081
servlet:
# 应用的访问路径
context-path: /
@@ -178,4 +178,7 @@ mqtt:
timeout: 10
# 默认 QoS
- qos: 1
\ No newline at end of file
+ qos: 1
+worn:
+ # PDF 打印字体查找顺序;部署到 Linux 时可把实际存在的中文字体路径放在最前面
+ pdf-font-locations: classpath:fonts/simhei.ttf,/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc,/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc,/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,/usr/share/fonts/truetype/arphic/ukai.ttc,C:/Windows/Fonts/simhei.ttf,C:/Windows/Fonts/simsun.ttc
diff --git a/src/main/resources/fonts/simhei.ttf b/src/main/resources/fonts/simhei.ttf
new file mode 100644
index 0000000..3326815
Binary files /dev/null and b/src/main/resources/fonts/simhei.ttf differ
diff --git a/src/main/resources/mybatis/system/SysDeptMapper.xml b/src/main/resources/mybatis/system/SysDeptMapper.xml
index 488e736..7545619 100644
--- a/src/main/resources/mybatis/system/SysDeptMapper.xml
+++ b/src/main/resources/mybatis/system/SysDeptMapper.xml
@@ -47,7 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by d.parent_id, d.order_num
-