# YJH-MES v2.0.004 — 8Multi di_reg 报工 & 设备状态接口 > **版本**: v2.0.004 > **更新日期**: 2026-03-06 > **项目**: YJH-MES (yjh-mes + mes-ui) > **说明**: 在 8Multi JSON 协议中新增 `di_reg` 字段处理,实现硬件触发报工和设备状态修改 --- ## 一、需求概述 ### 1.1 新增字段 原 8Multi JSON 格式中新增一个字段: ```json "di_reg": %d // 0000-0111 触发报工;1000-1111 修改设备状态 ``` ### 1.2 di_reg 编码规则 | 十进制值 | 二进制 | 动作类型 | 含义 | |---------|--------|---------|------| | 0 | 0000 | 报工 | 对工序路线中**第1道工序**的全部在产工单执行报工 | | 1 | 0001 | 报工 | 对工序路线中**第2道工序**的全部在产工单执行报工 | | 2 | 0010 | 报工 | 对工序路线中**第3道工序**的全部在产工单执行报工 | | 3 | 0011 | 报工 | 对工序路线中**第4道工序**的全部在产工单执行报工 | | 4 | 0100 | 报工 | 对工序路线中**第5道工序**的全部在产工单执行报工 | | 5 | 0101 | 报工 | 对工序路线中**第6道工序**的全部在产工单执行报工 | | 6 | 0110 | 报工 | 对工序路线中**第7道工序**的全部在产工单执行报工 | | 7 | 0111 | 报工 | 对工序路线中**第8道工序**的全部在产工单执行报工 | | 8 | 1000 | 设备状态 | 计划停机(cn_reg = 0) | | 9 | 1001 | 设备状态 | 正常工作(cn_reg = 1) | | 10 | 1010 | 设备状态 | 待机(cn_reg = 2) | | 11 | 1011 | 设备状态 | 故障(cn_reg = 3) | | 12 | 1100 | 设备状态 | 在修(cn_reg = 4) | | 13 | 1101 | 设备状态 | 缺人(cn_reg = 5) | | 14 | 1110 | 设备状态 | 缺料(cn_reg = 6) | | 15 | 1111 | 设备状态 | 清零(cn_reg = 7) | | null/其他 | — | 忽略 | 不触发任何动作 | **判断逻辑**: ``` di_reg < 8 → 报工模式,processSort = di_reg + 1 di_reg >= 8 → 设备状态模式,cn_reg_new = di_reg - 8 ``` --- ## 二、数据库设计 ### 2.1 表设计总览(报工与设备状态两个功能独立) | 功能 | 表 | 说明 | |------|----|------| | 报工结果 | `pro_report`(已有)| 生成报工单,工单状态变更为 D | | 设备状态 | `device_data.cn_reg`(已有)| 本次上报的 cn_reg 被 di_reg 覆盖 | | di_reg 原始值 | `device_data.di_reg`(**新增列**)| 随每次上报落库 | | **触发动作日志** | `device_di_reg_log`(**新建表**)| 记录两类触发事件,用于追溯和审计 | ### 2.2 DDL — device_data 新增列 ```sql ALTER TABLE `device_data` ADD COLUMN `di_reg` INT DEFAULT NULL COMMENT '输入寄存器(0-7报工/8-15设备状态)'; ``` ### 2.3 DDL — device_di_reg_log 新建表 ```sql CREATE TABLE `device_di_reg_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `device_id` BIGINT NOT NULL COMMENT '设备ID(关联device.id)', `device_no` INT NOT NULL COMMENT '设备号(device.device_no)', `di_reg` INT NOT NULL COMMENT 'di_reg原始值(0-15)', `action_type` VARCHAR(16) NOT NULL COMMENT '动作类型: REPORT=报工 / STATUS=设备状态', `process_sort` INT DEFAULT NULL COMMENT '工序顺序(REPORT时有效, = di_reg + 1)', `cn_reg_new` INT DEFAULT NULL COMMENT '新设备状态值(STATUS时有效, = di_reg - 8)', `affected_count` INT DEFAULT 0 COMMENT '本次处理的工单数量(REPORT时)', `work_order_ids` VARCHAR(512) DEFAULT NULL COMMENT '处理的工单ID列表,逗号分隔(REPORT时)', `trigger_time` DATETIME NOT NULL COMMENT '触发时间', `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0正常/1删除)', PRIMARY KEY (`id`), KEY `idx_device_id_time` (`device_id`, `trigger_time`), KEY `idx_action_type_time` (`action_type`, `trigger_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='8Multi di_reg触发动作日志表'; ``` > **字段说明**:`action_type=REPORT` 时 `process_sort`、`affected_count`、`work_order_ids` 有效;`action_type=STATUS` 时 `cn_reg_new` 有效。 --- ## 三、后端实现方案 ### 3.1 涉及文件 ``` yjh-mes/src/main/java/cn/sourceplan/equipment/service/ └── Multi8ProtocolService.java ← 主改动:di_reg 解析分发、日志写入 yjh-mes/src/main/java/cn/sourceplan/equipment/domain/ ├── DeviceData.java ← 新增 diReg 字段 └── DeviceDiRegLog.java ← 新建:日志实体 yjh-mes/src/main/java/cn/sourceplan/equipment/mapper/ └── DeviceDiRegLogMapper.java ← 新建:日志 Mapper(extends BaseMapper) yjh-mes/src/main/java/cn/sourceplan/production/mapper/ └── WorkOrderEntryMapper.java ← 新增 selectActiveEntriesByProcessSort 方法 yjh-mes/src/main/resources/mapper/production/ └── WorkOrderEntryMapper.xml ← 新建:对应 SQL .sql/ └── 2026-03-06_v2.0.004_周启威_8Multi报工、设备状态修改.sql ← DDL 脚本 ``` ### 3.2 Multi8ProtocolService — parse8MultiJson 改动 在 `deviceDataMapper.insert(data)` 之前,解析 `di_reg` 并路由: ```java // 解析 di_reg Integer diReg = parseInt(json.getStr("di_reg")); data.setDiReg(diReg); // di_reg 分发处理 if (diReg != null) { if (diReg >= 0 && diReg <= 7) { // 报工模式:processSort = diReg + 1(全局、全量、幂等) handleAutoReport(device, diReg + 1); } else if (diReg >= 8 && diReg <= 15) { // 设备状态模式:强制覆盖 cn_reg(在 insert 之前覆盖,确保落库值正确) int newCnReg = diReg - 8; data.setCnReg(newCnReg); log.info("di_reg={} → 设备状态覆盖 cn_reg={}", diReg, newCnReg); // saveStatusLog 在 deviceDataMapper.insert(data) 之后调用(见注意事项4) } } ``` ### 3.3 handleAutoReport — 报工逻辑 **三条核心原则**: 1. **报工数量** = 工单计划数量(全量报工,一次完成) 2. **幂等性** = 报工后工单状态置为 D(已完成),下次查询自动过滤,无需额外判断 3. **工单范围** = 全局所有工单,不按工位/车间过滤 ```java /** * di_reg 触发报工 * 全局查找 sort=processSort 且 pro_status IN('A','B') 的工单分录,全量报工后置为已完成 * 幂等保证:已完成(D)的工单在查询时自动过滤,无需重复判断 * * @param device 上报设备(用于写日志) * @param processSort 工序顺序(1-based,= di_reg + 1) */ @Transactional(rollbackFor = Exception.class) // 保证报工单创建与工单状态更新原子性 private void handleAutoReport(MesDevice device, int processSort) { List entries = workOrderEntryMapper.selectActiveEntriesByProcessSort(processSort); if (entries == null || entries.isEmpty()) { log.info("di_reg 报工:未找到第{}道工序的在产工单分录", processSort); saveReportLog(device, processSort - 1, processSort, 0, ""); return; } List processedIds = new ArrayList<>(); for (WorkOrderEntry entry : entries) { try { WorkOrder workOrder = workOrderMapper.selectById(entry.getWorkorderId()); if (workOrder == null || "D".equals(workOrder.getProStatus())) { continue; } // 报工数量 = 工单计划数量(全量) BigDecimal reportQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE; // 构建报工单 Report report = new Report(); report.setWorkOrderEntryId(entry.getId()); report.setWorkOrderId(workOrder.getId()); report.setProcessName(entry.getProcessName()); report.setProcessId(entry.getProcessId()); report.setWorkshopId(entry.getWorkshopId()); report.setWorkshopName(entry.getWorkshopName()); report.setStationId(entry.getStationId()); report.setStationName(entry.getStationName()); report.setReportTime(new Date()); report.setReportChannel("8Multi"); report.setReportQuantity(reportQty); report.setQualifiedQuantity(reportQty); report.setUnqualifiedQuantity(BigDecimal.ZERO); report.setQualityStatus("A"); // 免检 // 创建报工单(insertReportSimple 不处理工单状态,需手动更新) reportService.insertReportSimple(report); // 手动将工单标记为已完成 → 下次同 di_reg 触发时自动幂等跳过 // 注意:两步操作在同一 @Transactional 内,任一失败均回滚 workOrder.setTotalReportedQuantity(reportQty); workOrder.setProStatus("D"); workOrder.setActualEndTime(new Date()); workOrder.setRealFinishDate(new Date()); workOrderMapper.updateById(workOrder); processedIds.add(workOrder.getId()); log.info("di_reg 报工成功:workOrderId={}, processSort={}, qty={}", workOrder.getId(), processSort, reportQty); } catch (Exception e) { log.error("di_reg 报工失败:entryId={}", entry.getId(), e); } } // 写触发日志 String woIds = processedIds.stream().map(String::valueOf) .collect(java.util.stream.Collectors.joining(",")); saveReportLog(device, processSort - 1, processSort, processedIds.size(), woIds); } ``` ### 3.4 日志写入方法 ```java private void saveReportLog(MesDevice device, int diReg, int processSort, int count, String woIds) { DeviceDiRegLog entry = new DeviceDiRegLog(); entry.setDeviceId(device.getId()); entry.setDeviceNo(device.getDeviceNo()); entry.setDiReg(diReg); entry.setActionType("REPORT"); entry.setProcessSort(processSort); entry.setAffectedCount(count); entry.setWorkOrderIds(woIds); entry.setTriggerTime(new Date()); deviceDiRegLogMapper.insert(entry); } private void saveStatusLog(MesDevice device, int diReg, int cnRegNew) { DeviceDiRegLog entry = new DeviceDiRegLog(); entry.setDeviceId(device.getId()); entry.setDeviceNo(device.getDeviceNo()); entry.setDiReg(diReg); entry.setActionType("STATUS"); entry.setCnRegNew(cnRegNew); entry.setTriggerTime(new Date()); deviceDiRegLogMapper.insert(entry); } ``` ### 3.5 WorkOrderEntryMapper — 新增查询方法 **接口**(WorkOrderEntryMapper.java): ```java /** * 查询所有在产工单(非D状态)中 processSort = processSort 的工单分录 */ List selectActiveEntriesByProcessSort(@Param("processSort") int processSort); ``` **XML**(WorkOrderEntryMapper.xml,新建文件): ```xml ``` > **说明**:`sort` 对应 `WorkOrderEntry.sort`(工序顺序,1-based),只取状态为 A(未开始)或 B(生产中)的工单;已完成(D)的工单自动排除,实现天然幂等。 --- ## 四、设备状态覆盖说明 当 `di_reg >= 8` 时,本次上报的 `cn_reg` 值将被 `di_reg - 8` **覆盖**(无论 JSON 中 `cn_reg` 是什么)。这样 OEE 时序图、设备状态页面均可立即反映该状态,无需额外接口。 | di_reg | 覆盖后 cn_reg | 含义 | |--------|------------|------| | 8 | 0 | 计划停机 | | 9 | 1 | 正常工作 | | 10 | 2 | 待机 | | 11 | 3 | 故障 | | 12 | 4 | 在修 | | 13 | 5 | 缺人 | | 14 | 6 | 缺料 | | 15 | 7 | 清零 | --- ## 五、完整 JSON 示例(含 di_reg) ### 5.1 报工示例(第2道工序) ```json { "header": "+YAV", "Card": "8multi", "id": 101126010002, "dt": 300, "a1": 5.2, "a2": 0.0, "q1": 0.0, "q2": 0.0, "t1": 26.5, "t2": 25.0, "dht1": 60.0, "dht2": 58.0, "cn_reg": 1, "f1": 50.00, "f2": 0.00, "c1": 50, "c2": 0, "di_reg": 1, "do_reg": 0, "reg1": 0.000, "reg2": 0.000, "tv": 0, "save": 0, "oee": 100, "end": "EEFF" } ``` → 全局查找 `sort=2` 且 `pro_status IN('A','B')` 的工单分录 → 报工数量 = 工单计划数量 → 工单状态置为 D 再次收到 `di_reg=1` → 同工单已为 D,查询结果为空 → 跳过,**天然幂等** ### 5.2 设备状态示例(设置为故障,di_reg=11) ```json { "cn_reg": 1, "di_reg": 11, ... } ``` → `cn_reg` 强制覆盖为 3(故障),`device_di_reg_log` 写入 STATUS 记录,OEE 状态显示红色 --- ## 六、实施步骤(清单) - [ ] 执行 DDL 脚本 `2026-03-06_v2.0.004_周启威_8Multi报工、设备状态修改.sql`(ALTER + CREATE TABLE) - [ ] `DeviceData.java` 新增 `diReg` 字段(`@TableField("di_reg")`) - [ ] 新建 `DeviceDiRegLog.java` 实体(对应 `device_di_reg_log` 表) - [ ] 新建 `DeviceDiRegLogMapper.java`(`extends BaseMapper`) - [ ] `WorkOrderEntryMapper.java` 新增 `selectActiveEntriesByProcessSort` 方法 - [ ] 新建 `WorkOrderEntryMapper.xml`,添加对应 SQL - [ ] `Multi8ProtocolService.java` 注入 `WorkOrderMapper`、`WorkOrderEntryMapper`、`IReportService`、`DeviceDiRegLogMapper` - [ ] `Multi8ProtocolService.java` 在 `parse8MultiJson` 中新增 `di_reg` 解析分发 - [ ] `Multi8ProtocolService.java` 新增 `handleAutoReport`、`saveReportLog`、`saveStatusLog` 方法 - [ ] 联调测试:di_reg=0 触发报工;di_reg=11 触发故障状态 --- ## 七、注意事项 1. **幂等性**:第一次触发时全量报工并将工单置为 D(已完成);后续同 `di_reg` 触发时,`pro_status IN('A','B')` 查询自动排除 D 状态工单,**无需额外判断**。 2. **报工数量**:固定取 `workOrder.getQuantity()`(计划数量),全量一次性完成,与 c1 计数器无关。 3. **di_reg = null**:旧版固件不携带此字段时,`parseInt` 返回 null,分发代码不执行,**不影响原有解析逻辑**。 4. **`saveStatusLog` 调用时机**:应在 `deviceDataMapper.insert(data)` **之后**调用,避免 insert 失败时日志已落库;`parse8MultiJson` 中需将 `saveStatusLog` 调用挪至 insert 语句后。 5. **`DeviceDiRegLog` 不继承 BaseEntity**:该实体仅需 `@TableName`、`@TableId` 注解,手动设置 `triggerTime`,不依赖框架自动填充的 `create_time`/`create_by` 等字段,DDL 也不必添加这些列。 6. **事务隔离**:`handleAutoReport` 加 `@Transactional`,每个工单报工+状态更新原子提交;但各工单之间彼此独立(外层 try-catch),单个工单失败不影响其他工单。