15 KiB
15 KiB
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 格式中新增一个字段:
"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 新增列
ALTER TABLE `device_data`
ADD COLUMN `di_reg` INT DEFAULT NULL COMMENT '输入寄存器(0-7报工/8-15设备状态)';
2.3 DDL — device_di_reg_log 新建表
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 并路由:
// 解析 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 — 报工逻辑
三条核心原则:
- 报工数量 = 工单计划数量(全量报工,一次完成)
- 幂等性 = 报工后工单状态置为 D(已完成),下次查询自动过滤,无需额外判断
- 工单范围 = 全局所有工单,不按工位/车间过滤
/**
* 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<WorkOrderEntry> entries = workOrderEntryMapper.selectActiveEntriesByProcessSort(processSort);
if (entries == null || entries.isEmpty()) {
log.info("di_reg 报工:未找到第{}道工序的在产工单分录", processSort);
saveReportLog(device, processSort - 1, processSort, 0, "");
return;
}
List<Long> 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 日志写入方法
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):
/**
* 查询所有在产工单(非D状态)中 processSort = processSort 的工单分录
*/
List<WorkOrderEntry> selectActiveEntriesByProcessSort(@Param("processSort") int processSort);
XML(WorkOrderEntryMapper.xml,新建文件):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.sourceplan.production.mapper.WorkOrderEntryMapper">
<select id="selectActiveEntriesByProcessSort"
resultType="cn.sourceplan.production.domain.WorkOrderEntry">
SELECT e.*
FROM pro_workorder_entry e
INNER JOIN pro_workorder w ON w.id = e.workorder_id
WHERE e.sort = #{processSort}
AND w.pro_status IN ('A', 'B')
</select>
</mapper>
说明:
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道工序)
{
"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)
{
"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<DeviceDiRegLog>) WorkOrderEntryMapper.java新增selectActiveEntriesByProcessSort方法- 新建
WorkOrderEntryMapper.xml,添加对应 SQL Multi8ProtocolService.java注入WorkOrderMapper、WorkOrderEntryMapper、IReportService、DeviceDiRegLogMapperMulti8ProtocolService.java在parse8MultiJson中新增di_reg解析分发Multi8ProtocolService.java新增handleAutoReport、saveReportLog、saveStatusLog方法- 联调测试:di_reg=0 触发报工;di_reg=11 触发故障状态
七、注意事项
- 幂等性:第一次触发时全量报工并将工单置为 D(已完成);后续同
di_reg触发时,pro_status IN('A','B')查询自动排除 D 状态工单,无需额外判断。 - 报工数量:固定取
workOrder.getQuantity()(计划数量),全量一次性完成,与 c1 计数器无关。 - di_reg = null:旧版固件不携带此字段时,
parseInt返回 null,分发代码不执行,不影响原有解析逻辑。 saveStatusLog调用时机:应在deviceDataMapper.insert(data)之后调用,避免 insert 失败时日志已落库;parse8MultiJson中需将saveStatusLog调用挪至 insert 语句后。DeviceDiRegLog不继承 BaseEntity:该实体仅需@TableName、@TableId注解,手动设置triggerTime,不依赖框架自动填充的create_time/create_by等字段,DDL 也不必添加这些列。- 事务隔离:
handleAutoReport加@Transactional,每个工单报工+状态更新原子提交;但各工单之间彼此独立(外层 try-catch),单个工单失败不影响其他工单。