云
2026-10-20 4eb9a3a99e21522ede60dcb8bee5b5856b515dc7
feat(mes): 添加MES数采功能支持MQTT设备数据采集

- 在application-kyzn.yaml、application-local.yaml和application-test.yaml中添加MES数采MQTT配置
- 新增数采相关错误码定义包括设备编号重复、未绑定等异常情况
- 扩展MesDvMachineryDO实体类增加数采设备编号和启用状态字段
- 更新导入导出Excel VO添加数采设备编号映射
- 修改MesDvMachineryMapper增加数采相关查询方法和条件筛选
- 在设备台账页面请求和响应对象中添加数采相关字段
- 实现数采设备编号唯一性校验逻辑防止重复绑定
- 添加数采报警记录数据对象和相关枚举定义
- 实现数采历史数据清理定时任务配置数据保留策略
- 新增数采监控控制器提供设备状态、实时数据、报警等查询接口
- 添加数采周期过程数据和报警记录的数据访问层实现
- 实现数采报文解析工具支持JSON数据格式化处理
已添加37个文件
已修改13个文件
2325 ■■■■■ 文件已修改
yudao-module-mes/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/MesCollectController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmRespVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCyclePageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCycleRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectDeviceRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogRespVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeHistoryPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeRespVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectTechRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryImportExcelVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryPageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectAlarmDO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectCycleDO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectMessageDO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectOperateLogDO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeDO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeHistoryDO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectTechDO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectAlarmMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectCycleMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectMessageMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectOperateLogMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeHistoryMapper.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectTechMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectAlarmStatusEnum.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectMsgTypeEnum.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectOperateModeEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectRunStatusEnum.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/config/MesCollectProperties.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/mqtt/MesCollectMqttClient.java 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/collect/MesCollectCleanupJob.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataParser.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataService.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectMessageDispatcher.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectQueryService.java 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectTaskResolver.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-kyzn.yaml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-test.yaml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml
@@ -103,6 +103,12 @@
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- æ•°é‡‡ MQTT å®¢æˆ·ç«¯ -->
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/MesCollectController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.*;
import cn.iocoder.yudao.module.mes.service.collect.MesCollectQueryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES æ•°é‡‡ç›‘控")
@RestController
@RequestMapping("/mes/collect")
@Validated
public class MesCollectController {
    @Resource
    private MesCollectQueryService queryService;
    @GetMapping("/device/list")
    @Operation(summary = "获取数采设备状态列表")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<List<MesCollectDeviceRespVO>> getDeviceList() {
        return success(queryService.getDeviceList());
    }
    @GetMapping("/realtime/get")
    @Operation(summary = "获取设备最新实时状态")
    @Parameter(name = "deviceCode", description = "数采设备编号", required = true, example = "C01")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectRealtimeRespVO> getRealtime(@RequestParam("deviceCode") String deviceCode) {
        return success(queryService.getRealtime(deviceCode));
    }
    @GetMapping("/realtime/history-page")
    @Operation(summary = "分页查询实时状态历史")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectRealtimeRespVO>> getRealtimeHistoryPage(
            @Valid MesCollectRealtimeHistoryPageReqVO reqVO) {
        return success(queryService.getRealtimeHistoryPage(reqVO));
    }
    @GetMapping("/cycle/page")
    @Operation(summary = "分页查询周期过程数据")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectCycleRespVO>> getCyclePage(@Valid MesCollectCyclePageReqVO reqVO) {
        return success(queryService.getCyclePage(reqVO));
    }
    @GetMapping("/cycle/get")
    @Operation(summary = "获取周期过程数据明细")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectCycleRespVO> getCycle(@RequestParam("id") Long id) {
        return success(queryService.getCycle(id));
    }
    @GetMapping("/tech/get")
    @Operation(summary = "获取设备最新工艺参数快照")
    @Parameter(name = "deviceCode", description = "数采设备编号", required = true, example = "C01")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectTechRespVO> getTech(@RequestParam("deviceCode") String deviceCode) {
        return success(queryService.getTech(deviceCode));
    }
    @GetMapping("/operate-log/page")
    @Operation(summary = "分页查询参数变更日志")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectOperateLogRespVO>> getOperateLogPage(
            @Valid MesCollectOperateLogPageReqVO reqVO) {
        return success(queryService.getOperateLogPage(reqVO));
    }
    @GetMapping("/alarm/page")
    @Operation(summary = "分页查询报警记录")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectAlarmRespVO>> getAlarmPage(@Valid MesCollectAlarmPageReqVO reqVO) {
        return success(queryService.getAlarmPage(reqVO));
    }
    @PutMapping("/alarm/ack")
    @Operation(summary = "人工确认消除报警")
    @Parameter(name = "id", description = "报警记录编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:collect-alarm:ack')")
    public CommonResult<Boolean> ackAlarm(@RequestParam("id") Long id) {
        queryService.ackAlarm(id);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES æ•°é‡‡æŠ¥è­¦åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectAlarmPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "状态:0未消除 1已消除", example = "0")
    private Integer status;
    @Schema(description = "报警时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] wmTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡æŠ¥è­¦ Response VO")
@Data
public class MesCollectAlarmRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "报警标识:2产生 0消失", example = "2")
    private Integer wmId;
    @Schema(description = "报警内容", example = "安全门未关")
    private String wmMsg;
    @Schema(description = "报警时间")
    private LocalDateTime wmTime;
    @Schema(description = "状态:0未消除 1已消除", example = "0")
    private Integer status;
    @Schema(description = "状态名称", example = "未消除")
    private String statusName;
    @Schema(description = "消除时间")
    private LocalDateTime clearTime;
    @Schema(description = "报文采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCyclePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES æ•°é‡‡å‘¨æœŸè¿‡ç¨‹åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectCyclePageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] collectTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCycleRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å‘¨æœŸè¿‡ç¨‹æ•°æ® Response VO")
@Data
public class MesCollectCycleRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "周期序号(CYCN)", example = "100")
    private Long cycleNo;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "周期时间(ECYCT)")
    private BigDecimal ecyct;
    @Schema(description = "射出起点(EISS)")
    private BigDecimal eiss;
    @Schema(description = "最大射速(EIVM)")
    private BigDecimal eivm;
    @Schema(description = "最大射压(EIPM)")
    private BigDecimal eipm;
    @Schema(description = "转保压时间(ESIPT)")
    private BigDecimal esipt;
    @Schema(description = "转保压压力(ESIPP)")
    private BigDecimal esipp;
    @Schema(description = "转保压位置(ESIPS)")
    private BigDecimal esips;
    @Schema(description = "射出时间(EIPT)")
    private BigDecimal eipt;
    @Schema(description = "射出终点位置(EIPSE)")
    private BigDecimal eipse;
    @Schema(description = "储料时间(EPLST)")
    private BigDecimal eplst;
    @Schema(description = "储料终点(EPLSSE)")
    private BigDecimal eplsse;
    @Schema(description = "最大储料压力(EPLSPM)")
    private BigDecimal eplspm;
    @Schema(description = "生产温度 JSON(ET1..ETn)")
    private String etTemps;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectDeviceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡è®¾å¤‡çŠ¶æ€ Response VO")
@Data
public class MesCollectDeviceRespVO {
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "设备编码", example = "MC_20260818001")
    private String machineryCode;
    @Schema(description = "设备名称", example = "注塑机 1 å·")
    private String machineryName;
    @Schema(description = "工作站编号", example = "1")
    private Long workstationId;
    @Schema(description = "工作站名称", example = "注塑工作站")
    private String workstationName;
    @Schema(description = "数采设备编号", example = "C01")
    private String collectDeviceCode;
    @Schema(description = "是否启用数采", example = "true")
    private Boolean collectEnabled;
    @Schema(description = "最近采集时间")
    private LocalDateTime lastCollectTime;
    @Schema(description = "是否在线(5 åˆ†é’Ÿå†…有数据)", example = "true")
    private Boolean online;
    @Schema(description = "运行状态:1待机 2生产", example = "2")
    private Integer sts;
    @Schema(description = "运行状态名称", example = "生产")
    private String stsName;
    @Schema(description = "运行模式:0手动 1半自动 2电眼自动 3时间自动 4调模使用", example = "2")
    private Integer opm;
    @Schema(description = "运行模式名称", example = "电眼自动")
    private String opmName;
    @Schema(description = "油温", example = "26.5")
    private BigDecimal ot;
    @Schema(description = "各段温度 JSON")
    private String temps;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产任务编码", example = "PT202610200001")
    private String taskCode;
    @Schema(description = "生产任务名称", example = "注塑任务")
    private String taskName;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "生产工单编码", example = "PO202610200001")
    private String workOrderCode;
    @Schema(description = "未消除报警数量", example = "1")
    private Long activeAlarmCount;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES æ•°é‡‡å‚数变更分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectOperateLogPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "变量编码", example = "SIPS")
    private String varId;
    @Schema(description = "修改时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] modifyTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å‚数变更 Response VO")
@Data
public class MesCollectOperateLogRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "变量编码", example = "SIPS")
    private String varId;
    @Schema(description = "修改前的值", example = "37")
    private String oldValue;
    @Schema(description = "修改后的值", example = "37.5")
    private String newValue;
    @Schema(description = "参数修改时间")
    private LocalDateTime modifyTime;
    @Schema(description = "报文采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeHistoryPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES æ•°é‡‡å®žæ—¶åŽ†å²åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectRealtimeHistoryPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] collectTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å®žæ—¶æ•°æ® Response VO")
@Data
public class MesCollectRealtimeRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "运行状态:1待机 2生产", example = "2")
    private Integer sts;
    @Schema(description = "运行状态名称", example = "生产")
    private String stsName;
    @Schema(description = "运行模式:0手动 1半自动 2电眼自动 3时间自动 4调模使用", example = "2")
    private Integer opm;
    @Schema(description = "运行模式名称", example = "电眼自动")
    private String opmName;
    @Schema(description = "附加状态", example = "0")
    private Integer asts;
    @Schema(description = "油温", example = "26.5")
    private BigDecimal ot;
    @Schema(description = "各段温度 JSON")
    private String temps;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectTechRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å·¥è‰ºå‚æ•° Response VO")
@Data
public class MesCollectTechRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "工艺参数完整快照 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryImportExcelVO.java
@@ -52,4 +52,7 @@
    @ExcelProperty("工作站编码")
    private String workstationCode;
    @ExcelProperty("数采设备编号")
    private String collectDeviceCode;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryPageReqVO.java
@@ -38,4 +38,10 @@
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
    @Schema(description = "数采设备编号(MQTT devId)", example = "C01")
    private String collectDeviceCode;
    @Schema(description = "是否启用数采", example = "true")
    private Boolean collectEnabled;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java
@@ -81,6 +81,14 @@
    @ExcelProperty("工作站")
    private String workstationName;
    @Schema(description = "数采设备编号(MQTT devId)", example = "C01")
    @ExcelProperty("数采设备编号")
    private String collectDeviceCode;
    @Schema(description = "是否启用数采", example = "true")
    @ExcelProperty("是否启用数采")
    private Boolean collectEnabled;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java
@@ -58,4 +58,10 @@
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
    @Schema(description = "数采设备编号(MQTT devId)", example = "C01")
    private String collectDeviceCode;
    @Schema(description = "是否启用数采", example = "true")
    private Boolean collectEnabled;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectAlarmDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-报警记录 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_alarm")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectAlarmDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private Integer wmId;
    private String wmMsg;
    private LocalDateTime wmTime;
    private Integer status;
    private LocalDateTime clearTime;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectCycleDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-周期过程数据 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_cycle")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectCycleDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private Long cycleNo;
    private LocalDateTime collectTime;
    private BigDecimal ecyct;
    private BigDecimal eiss;
    private BigDecimal eivm;
    private BigDecimal eipm;
    private BigDecimal esipt;
    private BigDecimal esipp;
    private BigDecimal esips;
    private BigDecimal eipt;
    private BigDecimal eipse;
    private BigDecimal eplst;
    private BigDecimal eplsse;
    private BigDecimal eplspm;
    private String etTemps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectMessageDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-原始报文 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_message")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectMessageDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private String mqttTopic;
    private String msgType;
    private LocalDateTime eventTime;
    private String rawPayload;
    private Integer processStatus;
    private String errorMsg;
    private LocalDateTime receiveTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectOperateLogDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-参数变更日志 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_operate_log")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectOperateLogDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private String varId;
    private String oldValue;
    private String newValue;
    private LocalDateTime modifyTime;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-设备实时状态 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_realtime")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectRealtimeDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private Integer sts;
    private Integer opm;
    private Integer asts;
    private BigDecimal ot;
    private String temps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeHistoryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-实时状态历史 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_realtime_history")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectRealtimeHistoryDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private Integer sts;
    private Integer opm;
    private Integer asts;
    private BigDecimal ot;
    private String temps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectTechDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-工艺参数快照 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_tech")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectTechDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java
@@ -90,5 +90,13 @@
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO#getId()}
     */
    private Long workstationId;
    /**
     * æ•°é‡‡è®¾å¤‡ç¼–号(MQTT devId,如 C01)
     */
    private String collectDeviceCode;
    /**
     * æ˜¯å¦å¯ç”¨æ•°é‡‡
     */
    private Boolean collectEnabled;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectAlarmMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectAlarmPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectAlarmDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-报警记录 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectAlarmMapper extends BaseMapperX<MesCollectAlarmDO> {
    default PageResult<MesCollectAlarmDO> selectPage(MesCollectAlarmPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectAlarmDO>()
                .eqIfPresent(MesCollectAlarmDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectAlarmDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectAlarmDO::getWorkOrderId, reqVO.getWorkOrderId())
                .eqIfPresent(MesCollectAlarmDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesCollectAlarmDO::getWmTime, reqVO.getWmTime())
                .orderByDesc(MesCollectAlarmDO::getWmTime));
    }
    default Long selectActiveCountByDeviceCode(String deviceCode) {
        return selectCount(new LambdaQueryWrapperX<MesCollectAlarmDO>()
                .eq(MesCollectAlarmDO::getDeviceCode, deviceCode)
                .eq(MesCollectAlarmDO::getStatus, 0));
    }
    default int clearActiveByDeviceCode(String deviceCode, LocalDateTime clearTime) {
        return update(null, new LambdaUpdateWrapper<MesCollectAlarmDO>()
                .eq(MesCollectAlarmDO::getDeviceCode, deviceCode)
                .eq(MesCollectAlarmDO::getStatus, 0)
                .set(MesCollectAlarmDO::getStatus, 1)
                .set(MesCollectAlarmDO::getClearTime, clearTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectCycleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectCyclePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectCycleDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-周期过程数据 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectCycleMapper extends BaseMapperX<MesCollectCycleDO> {
    default PageResult<MesCollectCycleDO> selectPage(MesCollectCyclePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectCycleDO>()
                .eqIfPresent(MesCollectCycleDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectCycleDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectCycleDO::getTaskId, reqVO.getTaskId())
                .eqIfPresent(MesCollectCycleDO::getWorkOrderId, reqVO.getWorkOrderId())
                .betweenIfPresent(MesCollectCycleDO::getCollectTime, reqVO.getCollectTime())
                .orderByDesc(MesCollectCycleDO::getCollectTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectMessageMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectMessageDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-原始报文 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectMessageMapper extends BaseMapperX<MesCollectMessageDO> {
    default int deleteBefore(LocalDateTime before, int limit) {
        return delete(new LambdaQueryWrapperX<MesCollectMessageDO>()
                .lt(MesCollectMessageDO::getReceiveTime, before)
                .last("LIMIT " + limit));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectOperateLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectOperateLogPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectOperateLogDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-参数变更日志 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectOperateLogMapper extends BaseMapperX<MesCollectOperateLogDO> {
    default PageResult<MesCollectOperateLogDO> selectPage(MesCollectOperateLogPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectOperateLogDO>()
                .eqIfPresent(MesCollectOperateLogDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectOperateLogDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectOperateLogDO::getWorkOrderId, reqVO.getWorkOrderId())
                .eqIfPresent(MesCollectOperateLogDO::getVarId, reqVO.getVarId())
                .betweenIfPresent(MesCollectOperateLogDO::getModifyTime, reqVO.getModifyTime())
                .orderByDesc(MesCollectOperateLogDO::getModifyTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeHistoryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectRealtimeHistoryPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeHistoryDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-实时状态历史 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectRealtimeHistoryMapper extends BaseMapperX<MesCollectRealtimeHistoryDO> {
    default PageResult<MesCollectRealtimeHistoryDO> selectPage(MesCollectRealtimeHistoryPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
                .eqIfPresent(MesCollectRealtimeHistoryDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getTaskId, reqVO.getTaskId())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getWorkOrderId, reqVO.getWorkOrderId())
                .betweenIfPresent(MesCollectRealtimeHistoryDO::getCollectTime, reqVO.getCollectTime())
                .orderByDesc(MesCollectRealtimeHistoryDO::getCollectTime));
    }
    default int deleteBefore(LocalDateTime before, int limit) {
        return delete(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
                .lt(MesCollectRealtimeHistoryDO::getCollectTime, before)
                .last("LIMIT " + limit));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-设备实时状态 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectRealtimeMapper extends BaseMapperX<MesCollectRealtimeDO> {
    default MesCollectRealtimeDO selectByDeviceCode(String deviceCode) {
        return selectOne(MesCollectRealtimeDO::getDeviceCode, deviceCode);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectTechMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectTechDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-工艺参数快照 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectTechMapper extends BaseMapperX<MesCollectTechDO> {
    default MesCollectTechDO selectByDeviceCode(String deviceCode) {
        return selectOne(MesCollectTechDO::getDeviceCode, deviceCode);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java
@@ -8,6 +8,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES è®¾å¤‡å°è´¦ Mapper
 *
@@ -25,6 +27,8 @@
                .inIfPresent(MesDvMachineryDO::getMachineryTypeId, reqVO.getMachineryTypeIds())
                .eqIfPresent(MesDvMachineryDO::getWorkshopId, reqVO.getWorkshopId())
                .eqIfPresent(MesDvMachineryDO::getWorkstationId, reqVO.getWorkstationId())
                .eqIfPresent(MesDvMachineryDO::getCollectDeviceCode, reqVO.getCollectDeviceCode())
                .eqIfPresent(MesDvMachineryDO::getCollectEnabled, reqVO.getCollectEnabled())
                .eqIfPresent(MesDvMachineryDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesDvMachineryDO::getId));
    }
@@ -33,6 +37,16 @@
        return selectOne(MesDvMachineryDO::getCode, code);
    }
    default MesDvMachineryDO selectByCollectDeviceCode(String collectDeviceCode) {
        return selectOne(MesDvMachineryDO::getCollectDeviceCode, collectDeviceCode);
    }
    default List<MesDvMachineryDO> selectListWithCollectDeviceCode() {
        return selectList(new LambdaQueryWrapperX<MesDvMachineryDO>()
                .isNotNull(MesDvMachineryDO::getCollectDeviceCode)
                .orderByAsc(MesDvMachineryDO::getId));
    }
    default Long selectCountByMachineryTypeId(Long machineryTypeId) {
        return selectCount(MesDvMachineryDO::getMachineryTypeId, machineryTypeId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
@@ -80,6 +80,19 @@
        return selectPage(reqVO, query);
    }
    /**
     * æŸ¥è¯¢å·¥ä½œç«™ä¸Šæ­£åœ¨è¿è¡Œçš„任务:生产中、部分完成、已停工
     */
    default MesProTaskDO selectRunningTaskByWorkstationId(Long workstationId) {
        return selectOne(new LambdaQueryWrapperX<MesProTaskDO>()
                .eq(MesProTaskDO::getWorkstationId, workstationId)
                .in(MesProTaskDO::getStatus, MesProTaskStatusEnum.PRODUCING.getStatus(),
                        MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus())
                .orderByDesc(MesProTaskDO::getLastFeedbackTime)
                .orderByDesc(MesProTaskDO::getId)
                .last("LIMIT 1"));
    }
    default List<MesProTaskDO> selectListByWorkOrderId(Long workOrderId) {
        return selectList(new LambdaQueryWrapperX<MesProTaskDO>()
                .eqIfPresent(MesProTaskDO::getWorkOrderId, workOrderId)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -170,6 +170,7 @@
    ErrorCode DV_MACHINERY_HAS_MAINTEN_RECORD = new ErrorCode(1_040_301_005, "设备已关联保养记录,无法删除");
    ErrorCode DV_MACHINERY_HAS_REPAIR = new ErrorCode(1_040_301_006, "设备已关联维修工单,无法删除");
    ErrorCode DV_MACHINERY_WORKSTATION_NOT_EXISTS = new ErrorCode(1_040_301_008, "工作站编码不存在");
    ErrorCode DV_MACHINERY_COLLECT_CODE_DUPLICATE = new ErrorCode(1_040_301_009, "数采设备编号已被其他设备占用");
    // ========== MES è®¾å¤‡ç®¡ç†-点检保养项目(1-040-304-000) ==========
    ErrorCode DV_SUBJECT_NOT_EXISTS = new ErrorCode(1_040_304_000, "点检保养项目不存在");
@@ -839,4 +840,9 @@
    ErrorCode WMS_APPROVAL_STATUS_INVALID = new ErrorCode(1_040_900_004, "单据状态不正确,无法提交审批");
    ErrorCode WMS_APPROVAL_ALREADY_SUBMITTED = new ErrorCode(1_040_900_005, "单据已提交审批,请勿重复提交");
    // ========== MES è®¾å¤‡ç®¡ç†-数采(1-040-920-000) ==========
    ErrorCode COLLECT_DEVICE_NOT_BOUND = new ErrorCode(1_040_920_000, "数采设备[{}]未绑定设备台账,请先在设备台账维护数采设备编号");
    ErrorCode COLLECT_ALARM_NOT_EXISTS = new ErrorCode(1_040_920_001, "报警记录不存在");
    ErrorCode COLLECT_ALARM_ALREADY_CLEARED = new ErrorCode(1_040_920_002, "该报警已消除,无需重复确认");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectAlarmStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡æŠ¥è­¦çŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectAlarmStatusEnum {
    ACTIVE(0, "未消除"),
    CLEARED(1, "已消除");
    private final Integer status;
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectMsgTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡æŠ¥æ–‡ç±»åž‹
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectMsgTypeEnum {
    REALTIME("realtime", "实时数据"),
    SPC("spc", "周期过程数据"),
    TECH("tech", "工艺参数"),
    OP_LOG("opLog", "参数变更日志"),
    ALARM("alarm", "报警"),
    UNKNOWN("unknown", "未知类型");
    private final String type;
    private final String name;
    public static MesCollectMsgTypeEnum getByType(String type) {
        for (MesCollectMsgTypeEnum value : values()) {
            if (value.type.equalsIgnoreCase(type)) {
                return value;
            }
        }
        return UNKNOWN;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectOperateModeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡è®¾å¤‡è¿è¡Œæ¨¡å¼
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectOperateModeEnum {
    MANUAL(0, "手动"),
    SEMI_AUTO(1, "半自动"),
    EYE_AUTO(2, "电眼自动"),
    TIME_AUTO(3, "时间自动"),
    MOLD_ADJUST(4, "调模使用");
    private final Integer mode;
    private final String name;
    public static String getNameByMode(Integer mode) {
        if (mode == null) {
            return null;
        }
        for (MesCollectOperateModeEnum value : values()) {
            if (value.mode.equals(mode)) {
                return value.name;
            }
        }
        return null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectRunStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡è®¾å¤‡è¿è¡ŒçŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectRunStatusEnum {
    STANDBY(1, "待机"),
    PRODUCING(2, "生产");
    private final Integer status;
    private final String name;
    public static String getNameByStatus(Integer status) {
        if (status == null) {
            return null;
        }
        for (MesCollectRunStatusEnum value : values()) {
            if (value.status.equals(status)) {
                return value.name;
            }
        }
        return null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/config/MesCollectProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.mes.framework.collect.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * MES æ•°é‡‡é…ç½®
 *
 * @author è¶…级管理员
 */
@Component
@ConfigurationProperties(prefix = "yudao.mes.collect")
@Data
public class MesCollectProperties {
    private Mqtt mqtt = new Mqtt();
    private Retention retention = new Retention();
    @Data
    public static class Mqtt {
        /**
         * æ˜¯å¦å¯ç”¨æ•°é‡‡ MQTT è®¢é˜…
         */
        private Boolean enabled = false;
        /**
         * Broker åœ°å€ï¼Œä¾‹å¦‚ tcp://36.213.90.123:1883
         */
        private String url;
        private String username;
        private String password;
        /**
         * å®¢æˆ·ç«¯ ID å‰ç¼€
         */
        private String clientIdPrefix = "mes-collect";
        /**
         * ä¸»é¢˜å‰ç¼€ï¼Œä¾‹å¦‚ /YLCY/IMM
         */
        private String topicPrefix = "/YLCY/IMM";
        private Integer qos = 0;
        private Integer keepAlive = 65;
        private Integer connectionTimeout = 10;
        /**
         * æŠ¥è­¦ä¸»é¢˜åŽç¼€ï¼ŒæŠ¥æ–‡ topic å­—段或主题末段命中时按报警处理
         */
        private String alarmTopicSuffix = "wm";
    }
    @Data
    public static class Retention {
        /**
         * åŽŸå§‹æŠ¥æ–‡ä¿ç•™å¤©æ•°
         */
        private Integer rawDays = 7;
        /**
         * å®žæ—¶åŽ†å²ä¿ç•™å¤©æ•°
         */
        private Integer realtimeDays = 30;
        /**
         * å•次清理条数
         */
        private Integer batchSize = 50000;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/mqtt/MesCollectMqttClient.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
package cn.iocoder.yudao.module.mes.framework.collect.mqtt;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import cn.iocoder.yudao.module.mes.service.collect.MesCollectMessageDispatcher;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * MES æ•°é‡‡ MQTT å®¢æˆ·ç«¯
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectMqttClient implements SmartLifecycle {
    private static final long RECONNECT_DELAY_SECONDS = 30L;
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectMessageDispatcher dispatcher;
    private volatile boolean running = false;
    private MqttClient client;
    private ScheduledExecutorService scheduler;
    @Override
    public void start() {
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        if (!Boolean.TRUE.equals(mqtt.getEnabled())) {
            log.info("[start][数采 MQTT æœªå¯ç”¨]");
            return;
        }
        if (StrUtil.isBlank(mqtt.getUrl())) {
            log.warn("[start][数采 MQTT å·²å¯ç”¨ä½† url ä¸ºç©ºï¼Œè·³è¿‡è¿žæŽ¥]");
            return;
        }
        running = true;
        scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "mes-collect-mqtt-reconnect");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleWithFixedDelay(this::ensureConnected, 0, RECONNECT_DELAY_SECONDS, TimeUnit.SECONDS);
    }
    private void ensureConnected() {
        if (!running) {
            return;
        }
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        try {
            if (client == null) {
                client = new MqttClient(mqtt.getUrl(), buildClientId(), new MemoryPersistence());
                client.setCallback(new MqttCallbackExtended() {
                    @Override
                    public void connectComplete(boolean reconnect, String serverURI) {
                        subscribe();
                    }
                    @Override
                    public void connectionLost(Throwable cause) {
                        log.warn("[connectionLost][数采 MQTT è¿žæŽ¥æ–­å¼€ï¼š{}]", cause == null ? "未知原因" : cause.getMessage());
                    }
                    @Override
                    public void messageArrived(String topic, MqttMessage message) {
                        String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
                        try {
                            dispatcher.dispatch(topic, payload);
                        } catch (Exception e) {
                            log.error("[messageArrived][主题({}) å…¥é˜Ÿå¤±è´¥]", topic, e);
                        }
                    }
                    @Override
                    public void deliveryComplete(IMqttDeliveryToken token) {
                        // å½“前只订阅,不发布
                    }
                });
            }
            if (!client.isConnected()) {
                MqttConnectOptions options = new MqttConnectOptions();
                options.setAutomaticReconnect(true);
                options.setCleanSession(true);
                options.setKeepAliveInterval(mqtt.getKeepAlive());
                options.setConnectionTimeout(mqtt.getConnectionTimeout());
                if (StrUtil.isNotBlank(mqtt.getUsername())) {
                    options.setUserName(mqtt.getUsername());
                }
                if (mqtt.getPassword() != null) {
                    options.setPassword(mqtt.getPassword().toCharArray());
                }
                client.connect(options);
                log.info("[ensureConnected][数采 MQTT å·²è¿žæŽ¥ï¼š{}]", mqtt.getUrl());
            }
        } catch (Exception e) {
            log.warn("[ensureConnected][数采 MQTT è¿žæŽ¥/订阅失败,{} ç§’后重试:{}]",
                    RECONNECT_DELAY_SECONDS, e.getMessage());
        }
    }
    private void subscribe() {
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        String topic = normalizeTopicPrefix(mqtt.getTopicPrefix()) + "+/+";
        try {
            client.subscribe(topic, mqtt.getQos());
            log.info("[subscribe][数采 MQTT è®¢é˜…成功:{}]", topic);
        } catch (Exception e) {
            log.error("[subscribe][数采 MQTT è®¢é˜…失败:{}]", topic, e);
        }
    }
    private String normalizeTopicPrefix(String prefix) {
        if (StrUtil.isBlank(prefix)) {
            return "";
        }
        return prefix.endsWith("/") ? prefix : prefix + "/";
    }
    private String buildClientId() {
        String host;
        try {
            host = InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            host = "host";
        }
        host = host.replaceAll("[^a-zA-Z0-9_-]", "_");
        String clientId = properties.getMqtt().getClientIdPrefix() + "-" + host + "-"
                + Long.toHexString(System.nanoTime());
        return StrUtil.maxLength(clientId, 64);
    }
    @Override
    public void stop() {
        running = false;
        if (scheduler != null) {
            scheduler.shutdownNow();
            scheduler = null;
        }
        if (client != null) {
            try {
                if (client.isConnected()) {
                    client.disconnect();
                }
            } catch (Exception e) {
                log.warn("[stop][数采 MQTT æ–­å¼€å¤±è´¥ï¼š{}]", e.getMessage());
            }
            try {
                client.close(true);
            } catch (Exception e) {
                log.warn("[stop][数采 MQTT å…³é—­å¤±è´¥ï¼š{}]", e.getMessage());
            }
            client = null;
        }
        log.info("[stop][数采 MQTT å·²åœæ­¢]");
    }
    @Override
    public boolean isRunning() {
        return running;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/collect/MesCollectCleanupJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.mes.job.collect;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectMessageMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeHistoryMapper;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡åŽ†å²æ•°æ®æ¸…ç†ä»»åŠ¡
 *
 * <p>原始报文保留 7 å¤©ï¼Œå®žæ—¶åŽ†å²ä¿ç•™ 30 å¤©ï¼Œæ¯æ—¥ 03:30 æ‰§è¡Œã€‚</p>
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectCleanupJob {
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectMessageMapper messageMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Scheduled(cron = "0 30 3 * * ?")
    public void execute() {
        MesCollectProperties.Retention retention = properties.getRetention();
        int batchSize = retention.getBatchSize() == null || retention.getBatchSize() <= 0
                ? 50000 : retention.getBatchSize();
        int rawDeleted = deleteInBatches(() -> messageMapper.deleteBefore(
                LocalDateTime.now().minusDays(retention.getRawDays()), batchSize), batchSize);
        int realtimeDeleted = deleteInBatches(() -> realtimeHistoryMapper.deleteBefore(
                LocalDateTime.now().minusDays(retention.getRealtimeDays()), batchSize), batchSize);
        log.info("[execute][数采历史清理完成:原始报文 {} æ¡ï¼Œå®žæ—¶åŽ†å² {} æ¡]", rawDeleted, realtimeDeleted);
    }
    private int deleteInBatches(DeleteAction action, int batchSize) {
        int total = 0;
        int deleted;
        do {
            deleted = action.delete();
            total += deleted;
        } while (deleted >= batchSize);
        return total;
    }
    @FunctionalInterface
    private interface DeleteAction {
        int delete();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataParser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,212 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import tools.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * MES æ•°é‡‡æŠ¥æ–‡è§£æžå·¥å…·
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectDataParser {
    private static final DateTimeFormatter DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]");
    public JsonNode parsePayload(String payload) {
        return JsonUtils.parseTree(payload);
    }
    public JsonNode parseData(JsonNode root) {
        JsonNode data = root.get("Data");
        if (data != null && data.isTextual()) {
            try {
                data = JsonUtils.parseTree(data.asText());
            } catch (Exception e) {
                log.warn("[parseData][Data å­—段不是合法 JSON,按原文处理:{}]", StrUtil.maxLength(data.asText(), 200));
                return root;
            }
        }
        return data != null && data.isObject() ? data : root;
    }
    public String getText(JsonNode node, String field) {
        JsonNode value = node.get(field);
        return value == null || value.isNull() ? null : value.asText();
    }
    public Integer getInteger(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.intValue();
        }
        try {
            return Integer.valueOf(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    public Long getLong(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.longValue();
        }
        try {
            return Long.valueOf(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    public BigDecimal getDecimal(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.decimalValue();
        }
        try {
            return new BigDecimal(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /**
     * æå–指定前缀 + æ•°å­—的字段,如 T1..Tn、ET1..ETn
     */
    public String extractNumberedFields(JsonNode data, String prefix) {
        Map<String, Object> result = new LinkedHashMap<>();
        data.properties().forEach(entry -> {
            String name = entry.getKey();
            if (!name.startsWith(prefix) || name.length() == prefix.length()) {
                return;
            }
            String suffix = name.substring(prefix.length());
            if (!suffix.chars().allMatch(Character::isDigit)) {
                return;
            }
            JsonNode value = entry.getValue();
            result.put(name, value != null && value.isNumber() ? value.decimalValue() : value == null ? null : value.asText());
        });
        if (result.isEmpty()) {
            return null;
        }
        return JsonUtils.toJsonString(result);
    }
    public LocalDateTime parseEventTime(JsonNode root) {
        LocalDateTime time = parseTimeText(getText(root, "time"));
        if (time != null) {
            return time;
        }
        Long timestamp = getLong(root, "timestamp");
        if (timestamp == null) {
            timestamp = getLong(root, "sendStamp");
        }
        if (timestamp != null && timestamp > 0) {
            Instant instant = timestamp > 1_000_000_000_000L
                    ? Instant.ofEpochMilli(timestamp) : Instant.ofEpochSecond(timestamp);
            return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        }
        return LocalDateTime.now();
    }
    public LocalDateTime parseTimeText(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        try {
            return LocalDateTime.parse(text.trim(), DATE_TIME_FORMATTER);
        } catch (Exception e) {
            return null;
        }
    }
    public String valueToString(JsonNode value) {
        if (value == null || value.isNull()) {
            return null;
        }
        return value.isValueNode() ? value.asText() : value.toString();
    }
    public MesCollectMsgTypeEnum resolveMsgType(String topicField, String mqttTopic,
                                                JsonNode data, String alarmTopicSuffix) {
        MesCollectMsgTypeEnum type = MesCollectMsgTypeEnum.getByType(topicField);
        if (type != MesCollectMsgTypeEnum.UNKNOWN) {
            return type;
        }
        if (StrUtil.isNotBlank(topicField) && StrUtil.equalsIgnoreCase(topicField, alarmTopicSuffix)) {
            return MesCollectMsgTypeEnum.ALARM;
        }
        String suffix = extractTopicSuffix(mqttTopic);
        if (StrUtil.isNotBlank(suffix)) {
            type = MesCollectMsgTypeEnum.getByType(suffix);
            if (type != MesCollectMsgTypeEnum.UNKNOWN) {
                return type;
            }
            if (StrUtil.equalsIgnoreCase(suffix, alarmTopicSuffix)) {
                return MesCollectMsgTypeEnum.ALARM;
            }
        }
        if (data == null) {
            return MesCollectMsgTypeEnum.UNKNOWN;
        }
        if (data.has("wmId")) {
            return MesCollectMsgTypeEnum.ALARM;
        }
        if (data.has("varId")) {
            return MesCollectMsgTypeEnum.OP_LOG;
        }
        if (data.has("CYCN")) {
            return MesCollectMsgTypeEnum.SPC;
        }
        if (data.has("STS") || data.has("OPM")) {
            return MesCollectMsgTypeEnum.REALTIME;
        }
        if (data.has("TS1") || data.has("CP1M")) {
            return MesCollectMsgTypeEnum.TECH;
        }
        return MesCollectMsgTypeEnum.UNKNOWN;
    }
    public String resolveDeviceId(JsonNode root, String mqttTopic) {
        String deviceId = getText(root, "devId");
        if (StrUtil.isNotBlank(deviceId)) {
            return deviceId.trim();
        }
        String[] parts = mqttTopic == null ? new String[0] : mqttTopic.split("/");
        return parts.length >= 2 ? parts[parts.length - 2] : null;
    }
    private String extractTopicSuffix(String mqttTopic) {
        if (StrUtil.isBlank(mqttTopic)) {
            return null;
        }
        String[] parts = mqttTopic.split("/");
        return parts.length == 0 ? null : parts[parts.length - 1];
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.*;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.*;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectAlarmStatusEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import tools.jackson.databind.JsonNode;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡æ•°æ®è½åº“ Service
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectDataService {
    @Resource
    private MesCollectDataParser parser;
    @Resource
    private MesCollectMessageMapper messageMapper;
    @Resource
    private MesCollectRealtimeMapper realtimeMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Resource
    private MesCollectCycleMapper cycleMapper;
    @Resource
    private MesCollectTechMapper techMapper;
    @Resource
    private MesCollectOperateLogMapper collectOperateLogMapper;
    @Resource
    private MesCollectAlarmMapper alarmMapper;
    public Long saveRawMessage(String deviceCode, Long machineryId, String mqttTopic,
                               MesCollectMsgTypeEnum msgType, LocalDateTime eventTime, String payload) {
        MesCollectMessageDO message = MesCollectMessageDO.builder()
                .deviceCode(deviceCode).machineryId(machineryId).mqttTopic(mqttTopic)
                .msgType(msgType.getType()).eventTime(eventTime).rawPayload(payload)
                .processStatus(0).receiveTime(LocalDateTime.now()).build();
        messageMapper.insert(message);
        return message.getId();
    }
    public void markRawMessageFailed(Long id, String error) {
        if (id == null) {
            return;
        }
        messageMapper.updateById(MesCollectMessageDO.builder()
                .id(id).processStatus(1).errorMsg(StrUtil.maxLength(error, 1000)).build());
    }
    public void saveRealtime(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                             LocalDateTime eventTime, JsonNode data) {
        MesCollectRealtimeDO snapshot = MesCollectRealtimeDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime)
                .sts(parser.getInteger(data, "STS"))
                .opm(parser.getInteger(data, "OPM"))
                .asts(parser.getInteger(data, "ASTS"))
                .ot(parser.getDecimal(data, "OT"))
                .temps(parser.extractNumberedFields(data, "T"))
                .dataJson(data.toString())
                .build();
        MesCollectRealtimeDO existing = realtimeMapper.selectByDeviceCode(deviceCode);
        if (existing != null) {
            snapshot.setId(existing.getId());
            realtimeMapper.updateById(snapshot);
        } else {
            try {
                realtimeMapper.insert(snapshot);
            } catch (DuplicateKeyException e) {
                MesCollectRealtimeDO current = realtimeMapper.selectByDeviceCode(deviceCode);
                if (current != null) {
                    snapshot.setId(current.getId());
                    realtimeMapper.updateById(snapshot);
                }
            }
        }
        MesCollectRealtimeHistoryDO history = MesCollectRealtimeHistoryDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime)
                .sts(snapshot.getSts())
                .opm(snapshot.getOpm())
                .asts(snapshot.getAsts())
                .ot(snapshot.getOt())
                .temps(snapshot.getTemps())
                .dataJson(snapshot.getDataJson())
                .build();
        try {
            realtimeHistoryMapper.insert(history);
        } catch (DuplicateKeyException ignored) {
            // åŒä¸€è®¾å¤‡åŒä¸€ç§’的重复报文,直接忽略
        }
    }
    public void saveCycle(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                          LocalDateTime eventTime, JsonNode data) {
        Long cycleNo = parser.getLong(data, "CYCN");
        MesCollectCycleDO cycle = MesCollectCycleDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .cycleNo(cycleNo == null ? 0L : cycleNo)
                .collectTime(eventTime)
                .ecyct(parser.getDecimal(data, "ECYCT"))
                .eiss(parser.getDecimal(data, "EISS"))
                .eivm(parser.getDecimal(data, "EIVM"))
                .eipm(parser.getDecimal(data, "EIPM"))
                .esipt(parser.getDecimal(data, "ESIPT"))
                .esipp(parser.getDecimal(data, "ESIPP"))
                .esips(parser.getDecimal(data, "ESIPS"))
                .eipt(parser.getDecimal(data, "EIPT"))
                .eipse(parser.getDecimal(data, "EIPSE"))
                .eplst(parser.getDecimal(data, "EPLST"))
                .eplsse(parser.getDecimal(data, "EPLSSE"))
                .eplspm(parser.getDecimal(data, "EPLSPM"))
                .etTemps(parser.extractNumberedFields(data, "ET"))
                .dataJson(data.toString())
                .build();
        try {
            cycleMapper.insert(cycle);
        } catch (DuplicateKeyException ignored) {
            // åŒä¸€å‘¨æœŸé‡å¤ä¸ŠæŠ¥ï¼Œç›´æŽ¥å¿½ç•¥
        }
    }
    public void saveTech(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                         LocalDateTime eventTime, JsonNode data) {
        MesCollectTechDO tech = MesCollectTechDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime).dataJson(data.toString()).build();
        MesCollectTechDO existing = techMapper.selectByDeviceCode(deviceCode);
        if (existing != null) {
            tech.setId(existing.getId());
            techMapper.updateById(tech);
        } else {
            try {
                techMapper.insert(tech);
            } catch (DuplicateKeyException e) {
                MesCollectTechDO current = techMapper.selectByDeviceCode(deviceCode);
                if (current != null) {
                    tech.setId(current.getId());
                    techMapper.updateById(tech);
                }
            }
        }
    }
    public void saveOperateLog(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                               LocalDateTime eventTime, JsonNode data) {
        LocalDateTime modifyTime = parser.parseTimeText(parser.getText(data, "modifyTime"));
        collectOperateLogMapper.insert(MesCollectOperateLogDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .varId(parser.getText(data, "varId"))
                .oldValue(StrUtil.maxLength(parser.valueToString(data.get("lastValue")), 500))
                .newValue(StrUtil.maxLength(parser.valueToString(data.get("value")), 500))
                .modifyTime(modifyTime == null ? eventTime : modifyTime)
                .collectTime(eventTime).dataJson(data.toString())
                .build());
    }
    public void saveAlarm(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                          LocalDateTime eventTime, JsonNode data) {
        Integer wmId = parser.getInteger(data, "wmId");
        LocalDateTime wmTime = parser.parseTimeText(parser.getText(data, "wmTime"));
        if (wmTime == null) {
            wmTime = eventTime;
        }
        if (wmId != null && wmId == 0) {
            alarmMapper.clearActiveByDeviceCode(deviceCode, wmTime);
            return;
        }
        alarmMapper.insert(MesCollectAlarmDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .wmId(wmId)
                .wmMsg(StrUtil.maxLength(parser.getText(data, "wmMsg"), 500))
                .wmTime(wmTime)
                .status(MesCollectAlarmStatusEnum.ACTIVE.getStatus())
                .collectTime(eventTime).dataJson(data.toString())
                .build());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectMessageDispatcher.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import tools.jackson.databind.JsonNode;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * MES æ•°é‡‡æŠ¥æ–‡åˆ†å‘器
 *
 * <p>Paho å›žè°ƒçº¿ç¨‹åªè´Ÿè´£å…¥é˜Ÿï¼Œè§£æžä¸Žè½åº“在单线程中串行执行,保证同一设备报警等状态按顺序处理。</p>
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectMessageDispatcher {
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectDataParser parser;
    @Resource
    private MesCollectDataService dataService;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesCollectTaskResolver taskResolver;
    private final ExecutorService executor = new ThreadPoolExecutor(1, 1,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10_000),
            new ThreadFactory() {
                private final AtomicInteger index = new AtomicInteger();
                @Override
                public Thread newThread(Runnable runnable) {
                    Thread thread = new Thread(runnable, "mes-collect-dispatch-" + index.incrementAndGet());
                    thread.setDaemon(true);
                    return thread;
                }
            }, new ThreadPoolExecutor.CallerRunsPolicy());
    /**
     * è®¾å¤‡æœªç»‘定台账时的告警缓存,避免每条报文重复刷日志
     */
    private final ConcurrentHashMap<String, Long> unboundWarnCache = new ConcurrentHashMap<>();
    public void dispatch(String topic, String payload) {
        executor.execute(() -> process(topic, payload));
    }
    private void process(String topic, String payload) {
        JsonNode root;
        try {
            root = parser.parsePayload(payload);
        } catch (Exception e) {
            log.warn("[process][主题({}) æŠ¥æ–‡ä¸æ˜¯åˆæ³• JSON:{}]", topic, StrUtil.maxLength(payload, 200));
            dataService.markRawMessageFailed(
                    dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN,
                            LocalDateTime.now(), payload),
                    "报文不是合法 JSON:" + e.getMessage());
            return;
        }
        JsonNode data = parser.parseData(root);
        MesCollectMsgTypeEnum msgType = parser.resolveMsgType(parser.getText(root, "topic"), topic, data,
                properties.getMqtt().getAlarmTopicSuffix());
        String deviceCode = parser.resolveDeviceId(root, topic);
        LocalDateTime eventTime = parser.parseEventTime(root);
        MesDvMachineryDO machinery = StrUtil.isBlank(deviceCode) ? null
                : machineryMapper.selectByCollectDeviceCode(deviceCode);
        if (machinery == null && StrUtil.isNotBlank(deviceCode)) {
            warnUnboundDevice(deviceCode);
        }
        Long machineryId = machinery == null ? null : machinery.getId();
        Long taskId = null;
        Long workOrderId = null;
        if (machinery != null && machinery.getWorkstationId() != null) {
            MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
            if (task != null) {
                taskId = task.getId();
                workOrderId = task.getWorkOrderId();
            }
        }
        Long rawId = dataService.saveRawMessage(deviceCode, machineryId, topic, msgType, eventTime, payload);
        try {
            switch (msgType) {
                case REALTIME -> dataService.saveRealtime(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case SPC -> dataService.saveCycle(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case TECH -> dataService.saveTech(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case OP_LOG -> dataService.saveOperateLog(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case ALARM -> dataService.saveAlarm(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                default -> log.debug("[process][忽略未知类型报文,主题({})]", topic);
            }
        } catch (Exception e) {
            dataService.markRawMessageFailed(rawId, e.getMessage());
            log.error("[process][主题({}) è®¾å¤‡({}) å¤„理失败]", topic, deviceCode, e);
        }
    }
    private void warnUnboundDevice(String deviceCode) {
        long now = System.currentTimeMillis();
        Long last = unboundWarnCache.get(deviceCode);
        if (last != null && now - last < 60_000L) {
            return;
        }
        unboundWarnCache.put(deviceCode, now);
        log.warn("[process][数采设备({}) æœªç»‘定任何设备台账,数据将按设备编号落库,machineryId ä¸ºç©º]", deviceCode);
    }
    @PreDestroy
    public void destroy() {
        executor.shutdownNow();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectQueryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.*;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectAlarmStatusEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectOperateModeEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectRunStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_ALREADY_CLEARED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_NOT_EXISTS;
/**
 * MES æ•°é‡‡æ•°æ®æŸ¥è¯¢ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectQueryService {
    /**
     * è¶…过该时长未采集数据视为离线
     */
    private static final long ONLINE_TIMEOUT_MINUTES = 5L;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesCollectRealtimeMapper realtimeMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Resource
    private MesCollectCycleMapper cycleMapper;
    @Resource
    private MesCollectTechMapper techMapper;
    @Resource
    private MesCollectOperateLogMapper collectOperateLogMapper;
    @Resource
    private MesCollectAlarmMapper alarmMapper;
    @Resource
    private MesProWorkOrderMapper workOrderMapper;
    @Resource
    private MesCollectTaskResolver taskResolver;
    @Resource
    private MesMdWorkstationService workstationService;
    /**
     * èŽ·å–å·²é…ç½®æ•°é‡‡ç¼–ç çš„è®¾å¤‡å®žæ—¶çŠ¶æ€åˆ—è¡¨
     */
    public List<MesCollectDeviceRespVO> getDeviceList() {
        List<MesDvMachineryDO> machineries = machineryMapper.selectListWithCollectDeviceCode();
        Map<Long, MesMdWorkstationDO> workstationMap = workstationService.getWorkstationMap(
                machineries.stream().map(MesDvMachineryDO::getWorkstationId).filter(Objects::nonNull).toList());
        return machineries.stream().map(machinery -> {
            MesCollectDeviceRespVO vo = new MesCollectDeviceRespVO();
            vo.setMachineryId(machinery.getId());
            vo.setMachineryCode(machinery.getCode());
            vo.setMachineryName(machinery.getName());
            vo.setCollectDeviceCode(machinery.getCollectDeviceCode());
            vo.setCollectEnabled(Boolean.TRUE.equals(machinery.getCollectEnabled()));
            MesMdWorkstationDO workstation = workstationMap.get(machinery.getWorkstationId());
            if (workstation != null) {
                vo.setWorkstationId(workstation.getId());
                vo.setWorkstationName(workstation.getName());
            }
            MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(machinery.getCollectDeviceCode());
            if (realtime != null) {
                vo.setLastCollectTime(realtime.getCollectTime());
                vo.setOnline(realtime.getCollectTime() != null
                        && realtime.getCollectTime().isAfter(LocalDateTime.now().minusMinutes(ONLINE_TIMEOUT_MINUTES)));
                vo.setSts(realtime.getSts());
                vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
                vo.setOpm(realtime.getOpm());
                vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
                vo.setOt(realtime.getOt());
                vo.setTemps(realtime.getTemps());
            } else {
                vo.setOnline(false);
            }
            MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
            if (task != null) {
                vo.setTaskId(task.getId());
                vo.setTaskCode(task.getCode());
                vo.setTaskName(task.getName());
                vo.setWorkOrderId(task.getWorkOrderId());
                MesProWorkOrderDO workOrder = task.getWorkOrderId() != null
                        ? workOrderMapper.selectById(task.getWorkOrderId()) : null;
                if (workOrder != null) {
                    vo.setWorkOrderCode(workOrder.getCode());
                }
            }
            vo.setActiveAlarmCount(alarmMapper.selectActiveCountByDeviceCode(machinery.getCollectDeviceCode()));
            return vo;
        }).toList();
    }
    /**
     * èŽ·å–æŒ‡å®šæ•°é‡‡è®¾å¤‡çš„å®žæ—¶çŠ¶æ€å¿«ç…§
     */
    public MesCollectRealtimeRespVO getRealtime(String deviceCode) {
        MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(deviceCode);
        if (realtime == null) {
            return null;
        }
        MesCollectRealtimeRespVO vo = BeanUtils.toBean(realtime, MesCollectRealtimeRespVO.class);
        vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
        vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
        return vo;
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å®žæ—¶çŠ¶æ€åŽ†å²
     */
    public PageResult<MesCollectRealtimeRespVO> getRealtimeHistoryPage(MesCollectRealtimeHistoryPageReqVO reqVO) {
        PageResult<MesCollectRealtimeHistoryDO> pageResult = realtimeHistoryMapper.selectPage(reqVO);
        PageResult<MesCollectRealtimeRespVO> result = BeanUtils.toBean(pageResult, MesCollectRealtimeRespVO.class);
        result.getList().forEach(vo -> {
            vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(vo.getSts()));
            vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(vo.getOpm()));
        });
        return result;
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å‘¨æœŸè¿‡ç¨‹æ•°æ®
     */
    public PageResult<MesCollectCycleRespVO> getCyclePage(MesCollectCyclePageReqVO reqVO) {
        return BeanUtils.toBean(cycleMapper.selectPage(reqVO), MesCollectCycleRespVO.class);
    }
    /**
     * èŽ·å–å•æ¡å‘¨æœŸè¿‡ç¨‹æ•°æ®
     */
    public MesCollectCycleRespVO getCycle(Long id) {
        MesCollectCycleDO cycle = cycleMapper.selectById(id);
        return cycle == null ? null : BeanUtils.toBean(cycle, MesCollectCycleRespVO.class);
    }
    /**
     * èŽ·å–æŒ‡å®šæ•°é‡‡è®¾å¤‡çš„æœ€æ–°å·¥è‰ºå‚æ•°å¿«ç…§
     */
    public MesCollectTechRespVO getTech(String deviceCode) {
        MesCollectTechDO tech = techMapper.selectByDeviceCode(deviceCode);
        return tech == null ? null : BeanUtils.toBean(tech, MesCollectTechRespVO.class);
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å‚数变更日志
     */
    public PageResult<MesCollectOperateLogRespVO> getOperateLogPage(MesCollectOperateLogPageReqVO reqVO) {
        return BeanUtils.toBean(collectOperateLogMapper.selectPage(reqVO), MesCollectOperateLogRespVO.class);
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢æŠ¥è­¦è®°å½•
     */
    public PageResult<MesCollectAlarmRespVO> getAlarmPage(MesCollectAlarmPageReqVO reqVO) {
        PageResult<MesCollectAlarmDO> pageResult = alarmMapper.selectPage(reqVO);
        PageResult<MesCollectAlarmRespVO> result = BeanUtils.toBean(pageResult, MesCollectAlarmRespVO.class);
        result.getList().forEach(vo -> vo.setStatusName(MesCollectAlarmStatusEnum.ACTIVE.getStatus().equals(vo.getStatus())
                ? MesCollectAlarmStatusEnum.ACTIVE.getName() : MesCollectAlarmStatusEnum.CLEARED.getName()));
        return result;
    }
    /**
     * äººå·¥ç¡®è®¤æ¶ˆé™¤æŠ¥è­¦
     */
    public void ackAlarm(Long id) {
        MesCollectAlarmDO alarm = alarmMapper.selectById(id);
        if (alarm == null) {
            throw exception(COLLECT_ALARM_NOT_EXISTS);
        }
        if (MesCollectAlarmStatusEnum.CLEARED.getStatus().equals(alarm.getStatus())) {
            throw exception(COLLECT_ALARM_ALREADY_CLEARED);
        }
        MesCollectAlarmDO updateObj = new MesCollectAlarmDO();
        updateObj.setId(id);
        updateObj.setStatus(MesCollectAlarmStatusEnum.CLEARED.getStatus());
        updateObj.setClearTime(LocalDateTime.now());
        alarmMapper.updateById(updateObj);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectTaskResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
 * MES æ•°é‡‡-生产任务关联解析
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectTaskResolver {
    private static final long CACHE_MILLIS = 30_000L;
    @Resource
    private MesProTaskMapper taskMapper;
    private final ConcurrentHashMap<Long, CachedTask> cache = new ConcurrentHashMap<>();
    /**
     * æŸ¥è¯¢å·¥ä½œç«™ä¸Šæ­£åœ¨è¿è¡Œçš„生产任务(生产中/部分完成/已停工)
     */
    public MesProTaskDO resolve(Long workstationId) {
        if (workstationId == null) {
            return null;
        }
        long now = System.currentTimeMillis();
        CachedTask cached = cache.get(workstationId);
        if (cached != null && now - cached.time < CACHE_MILLIS) {
            return cached.task;
        }
        MesProTaskDO task = taskMapper.selectRunningTaskByWorkstationId(workstationId);
        cache.put(workstationId, new CachedTask(task, now));
        return task;
    }
    private record CachedTask(MesProTaskDO task, long time) {
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java
@@ -82,6 +82,8 @@
        workshopService.getWorkshop(createReqVO.getWorkshopId());
        // æ ¡éªŒç¼–码唯一
        validateMachineryCodeUnique(null, createReqVO.getCode());
        // æ ¡éªŒæ•°é‡‡è®¾å¤‡ç¼–号唯一
        validateCollectDeviceCodeUnique(null, createReqVO.getCollectDeviceCode());
        // æ’å…¥
        MesDvMachineryDO machinery = BeanUtils.toBean(createReqVO, MesDvMachineryDO.class);
@@ -103,6 +105,8 @@
        workshopService.getWorkshop(updateReqVO.getWorkshopId());
        // æ ¡éªŒç¼–码唯一
        validateMachineryCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        // æ ¡éªŒæ•°é‡‡è®¾å¤‡ç¼–号唯一
        validateCollectDeviceCodeUnique(updateReqVO.getId(), updateReqVO.getCollectDeviceCode());
        // æ›´æ–°
        MesDvMachineryDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMachineryDO.class);
@@ -148,6 +152,19 @@
        }
        if (ObjUtil.notEqual(machinery.getId(), id)) {
            throw exception(DV_MACHINERY_CODE_DUPLICATE);
        }
    }
    private void validateCollectDeviceCodeUnique(Long id, String collectDeviceCode) {
        if (StrUtil.isBlank(collectDeviceCode)) {
            return;
        }
        MesDvMachineryDO machinery = machineryMapper.selectByCollectDeviceCode(collectDeviceCode);
        if (machinery == null) {
            return;
        }
        if (ObjUtil.notEqual(machinery.getId(), id)) {
            throw exception(DV_MACHINERY_COLLECT_CODE_DUPLICATE);
        }
    }
@@ -278,6 +295,15 @@
            // 3.5 åˆ¤æ–­ï¼šåˆ›å»º or æ›´æ–°
            MesDvMachineryDO existMachinery = machineryMapper.selectByCode(importItem.getCode());
            if (StrUtil.isNotBlank(importItem.getCollectDeviceCode())) {
                MesDvMachineryDO collectOwner = machineryMapper.selectByCollectDeviceCode(importItem.getCollectDeviceCode());
                if (collectOwner != null && (existMachinery == null
                        || ObjUtil.notEqual(collectOwner.getId(), existMachinery.getId()))) {
                    respVO.getFailureCodes().put(key, "数采设备编号[" + importItem.getCollectDeviceCode() + "]已被设备["
                            + collectOwner.getCode() + "]占用");
                    return;
                }
            }
            if (existMachinery == null) {
                // 3.5.1 åˆ›å»º
                MesDvMachineryDO machinery = BeanUtils.toBean(importItem, MesDvMachineryDO.class);
yudao-server/src/main/resources/application-kyzn.yaml
@@ -235,3 +235,20 @@
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'xd@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
  mes:
    collect:
      mqtt: # æ³¨å¡‘机数采:EMQX è®¢é˜…,设备以 devId(如 C01)经设备台账 collect_device_code ç»‘定
        enabled: true # å…³æŽ‰åŽä¸å†è¿žæŽ¥ Broker,仅保留历史数据查询
        url: tcp://36.213.90.123:1883
        username: admin
        password: public
        client-id-prefix: mes-collect
        topic-prefix: /YLCY/IMM
        qos: 0
        keep-alive: 65
        connection-timeout: 10
        alarm-topic-suffix: wm # æŠ¥æ–‡ topic å­—段或主题末段命中该后缀即按报警处理
      retention:
        raw-days: 7 # åŽŸå§‹æŠ¥æ–‡ä¿ç•™å¤©æ•°
        realtime-days: 30 # å®žæ—¶çŠ¶æ€åŽ†å²ä¿ç•™å¤©æ•°
        batch-size: 50000 # å•次清理条数
yudao-server/src/main/resources/application-local.yaml
@@ -65,7 +65,7 @@
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/mom-hm?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          url: jdbc:mysql://localhost:3306/mom-kyzn?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: 123456
@@ -222,7 +222,24 @@
  database-backup: # æ•°æ®åº“备份配置
    host: localhost # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 3306 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    database: mom-kyzn # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: '123456' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
  mes:
    collect:
      mqtt: # æ³¨å¡‘机数采:本地默认不连生产 Broker,需要联调时把 enabled æ”¹ä¸º true
        enabled: false
        url: tcp://36.213.90.123:1883
        username: admin
        password: public
        client-id-prefix: mes-collect-local
        topic-prefix: /YLCY/IMM
        qos: 0
        keep-alive: 65
        connection-timeout: 10
        alarm-topic-suffix: wm
      retention:
        raw-days: 7
        realtime-days: 30
        batch-size: 50000
yudao-server/src/main/resources/application-test.yaml
@@ -259,3 +259,20 @@
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'xd@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
  mes:
    collect:
      mqtt: # æ³¨å¡‘机数采:测试环境默认不连生产 Broker,需要联调时把 enabled æ”¹ä¸º true
        enabled: false
        url: tcp://36.213.90.123:1883
        username: admin
        password: public
        client-id-prefix: mes-collect-test
        topic-prefix: /YLCY/IMM
        qos: 0
        keep-alive: 65
        connection-timeout: 10
        alarm-topic-suffix: wm
      retention:
        raw-days: 7
        realtime-days: 30
        batch-size: 50000