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 - select d.dept_id from sys_dept d left join sys_role_dept rd on d.dept_id = rd.dept_id @@ -169,5 +169,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND dept_name LIKE CONCAT('%', #{keyword}, '%') + \ No newline at end of file diff --git a/src/main/resources/mybatis/unique/WornUniqueCodeMapper.xml b/src/main/resources/mybatis/unique/WornUniqueCodeMapper.xml index 2a85c7a..20178a2 100644 --- a/src/main/resources/mybatis/unique/WornUniqueCodeMapper.xml +++ b/src/main/resources/mybatis/unique/WornUniqueCodeMapper.xml @@ -45,21 +45,27 @@ FROM worn_unique_code t LEFT JOIN sys_dept d ON t.project_id = d.dept_id + AND (t.is_delete = '0' OR t.is_delete IS NULL) - ${params.dataScope} + + AND t.project_id IN + + #{id} + + AND t.code = #{code} + AND t.bill_no = #{billNo} - - AND t.project_id = #{projectId} - - + + AND t.status = #{status} + AND t.rfid_code = #{rfidCode} @@ -70,6 +76,7 @@ SELECT id, @@ -196,7 +205,7 @@ WHERE uc.code = #{code} AND (uc.is_delete = '0' OR uc.is_delete IS NULL) - AND (ucm.is_delete = '0' OR ucm.is_delete IS NULL) + AND (ucm.id IS NULL OR ucm.is_delete = '0') AND (m.is_delete = '0' OR m.is_delete IS NULL) ORDER BY ucm.id ASC diff --git a/src/main/resources/mybatis/worn/MqttSensorDataMapper.xml b/src/main/resources/mybatis/worn/MqttSensorDataMapper.xml index e488b1d..da330fc 100644 --- a/src/main/resources/mybatis/worn/MqttSensorDataMapper.xml +++ b/src/main/resources/mybatis/worn/MqttSensorDataMapper.xml @@ -7,6 +7,8 @@ + + @@ -18,7 +20,6 @@ - @@ -31,33 +32,57 @@ select - id, device_id, topic, project, warehouse, payload, data_json, - battery, temperature, humidity, nh3, h2s, concentration, - water_status, - remark, create_by, create_time, update_by, update_time, is_delete + id, + device_id, + dept_id, + topic, + project, + warehouse, + payload, + data_json, + battery, + temperature, + humidity, + nh3, + h2s, + concentration, + water_status, + remark, + create_by, + create_time, + update_by, + update_time, + is_delete from mqtt_sensor_data - - and device_id = #{deviceId} - and event_type = #{eventType} - and event_desc = #{eventDesc} - and level = #{level} - and status = #{status} - and is_delete = #{isDelete} + + and device_id = #{deviceId} + and dept_id = #{deptId} + + + + AND dept_id IN + + #{deptId} + + + + and event_type = #{eventType} + and event_desc = #{eventDesc} + and level = #{level} + and status = #{status} + and is_delete = #{isDelete} + order by create_time desc - + - - and device_id = #{deviceId} - and metric_type = #{metricType} - and warn_min = #{warnMin} - and warn_max = #{warnMax} - and alarm_min = #{alarmMin} - and alarm_max = #{alarmMax} - and unit = #{unit} - and status = #{status} - and is_delete = #{isDelete} + + and device_id = #{deviceId} + and dept_id = #{deptId} + + + and dept_id in + + #{deptId} + + + + and metric_type = #{metricType} + and warn_min = #{warnMin} + and warn_max = #{warnMax} + and alarm_min = #{alarmMin} + and alarm_max = #{alarmMax} + and unit = #{unit} + and status = #{status} + and is_delete = #{isDelete} - + select * from mqtt_sensor_threshold @@ -123,4 +137,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and is_delete = '0' limit 1 + + + + + + \ No newline at end of file diff --git a/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml b/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml new file mode 100644 index 0000000..d53645e --- /dev/null +++ b/src/main/resources/mybatis/worn/MqttTopicConfigMapper.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + select id, project, warehouse, dept_id, topic_up, topic_down_prefix, + status, remark, create_by, create_time, update_by, update_time, is_delete + from mqtt_topic_config + + + + + + + + + + + + + + insert into mqtt_topic_config + + project, + warehouse, + dept_id, + topic_up, + topic_down_prefix, + status, + remark, + create_by, + create_time, + update_by, + update_time, + is_delete, + + + #{project}, + #{warehouse}, + #{deptId}, + #{topicUp}, + #{topicDownPrefix}, + #{status}, + #{remark}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{isDelete}, + + + + + + update mqtt_topic_config + + project = #{project}, + warehouse = #{warehouse}, + dept_id = #{deptId}, + topic_up = #{topicUp}, + topic_down_prefix = #{topicDownPrefix}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = #{updateTime}, + is_delete = #{isDelete}, + + where id = #{id} + + + + + delete from mqtt_topic_config where id = #{id} + + + + + delete from mqtt_topic_config where id in + + #{id} + + + + \ No newline at end of file diff --git a/src/main/resources/mybatis/worn/WornInboundItemMapper.xml b/src/main/resources/mybatis/worn/WornInboundItemMapper.xml index 3b852ac..b96ef2a 100644 --- a/src/main/resources/mybatis/worn/WornInboundItemMapper.xml +++ b/src/main/resources/mybatis/worn/WornInboundItemMapper.xml @@ -35,6 +35,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -193,22 +194,44 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" parameterType="Long" resultMap="WornInboundItemResult"> SELECT - id, - bill_id, - bill_no, - material_id, - quantity, - unique_code, - remark, - status, - create_by, - create_time, - update_by, - update_time, - is_delete - FROM worn_inbound_item - WHERE bill_id = #{billId} - AND (is_delete = '0' OR is_delete IS NULL) - ORDER BY id ASC + i.id, + i.bill_id, + i.bill_no, + i.material_id, + i.quantity, + i.unique_code, + i.remark, + i.status, + i.create_by, + i.create_time, + i.update_by, + i.update_time, + i.is_delete, + b.inbound_time, + b.warehouse_code, + b.warehouse_name, + b.area_code, + b.area_name, + b.remark AS bill_remark, + b.status AS bill_status, + b.bill_type, + m.material_name, + m.material_short_name, + m.material_code, + m.specification, + m.model, + m.weight, + m.unit_id, + mu.unit_name, + u.nick_name AS operator_name + FROM worn_inbound_item i + LEFT JOIN worn_inbound_bill b ON i.bill_id = b.id + LEFT JOIN worn_material m ON i.material_id = m.id + LEFT JOIN worn_material_unit mu ON m.unit_id = mu.id + LEFT JOIN sys_user u ON CAST(b.create_by AS UNSIGNED) = u.user_id + WHERE i.bill_id = #{billId} + AND (i.is_delete = '0' OR i.is_delete IS NULL) + AND (b.is_delete = '0' OR b.is_delete IS NULL) + ORDER BY i.id ASC - \ No newline at end of file +