372 lines
15 KiB
Markdown
372 lines
15 KiB
Markdown
# 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<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 日志写入方法
|
||
|
||
```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<WorkOrderEntry> selectActiveEntriesByProcessSort(@Param("processSort") int processSort);
|
||
```
|
||
|
||
**XML**(WorkOrderEntryMapper.xml,新建文件):
|
||
```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道工序)
|
||
```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<DeviceDiRegLog>`)
|
||
- [ ] `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),单个工单失败不影响其他工单。
|
||
|