feat: 改造为冷轧双机架二级控制系统

- 删除 ruoyi-flowable 模块,移除所有 Flowable 依赖
- 新建 ruoyi-mill 业务模块:
  * protocol: iXComPCS UDP 电文编解码器(7条电文完整定义)
  * udp: UdpServer/UdpSender/TelegramDispatcher
  * domain: MillPlan / MillProductionResult
  * service/controller: 生产计划、产出管理骨架
- 前端改造为工业深色主题(深海军蓝 + 工业青色)
- 系统标题更新为"冷轧双机架二级控制系统"
- application.yml 新增 mill.udp 配置节

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 12:58:22 +08:00
parent 30764b5854
commit 7648a56a0d
66 changed files with 1579 additions and 6169 deletions

View File

@@ -0,0 +1,52 @@
package com.ruoyi.mill.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.mill.domain.MillPlan;
import com.ruoyi.mill.service.IMillPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/mill/plan")
public class MillPlanController extends BaseController {
@Autowired
private IMillPlanService millPlanService;
@PreAuthorize("@ss.hasPermi('mill:plan:list')")
@GetMapping("/list")
public TableDataInfo list(MillPlan query) {
startPage();
List<MillPlan> list = millPlanService.selectMillPlanList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('mill:plan:query')")
@GetMapping("/{planNo}")
public AjaxResult getInfo(@PathVariable String planNo) {
return AjaxResult.success(millPlanService.selectMillPlanByPlanNo(planNo));
}
@PreAuthorize("@ss.hasPermi('mill:plan:add')")
@PostMapping
public AjaxResult add(@RequestBody MillPlan plan) {
return toAjax(millPlanService.insertMillPlan(plan));
}
@PreAuthorize("@ss.hasPermi('mill:plan:edit')")
@PutMapping
public AjaxResult edit(@RequestBody MillPlan plan) {
return toAjax(millPlanService.updateMillPlan(plan));
}
@PreAuthorize("@ss.hasPermi('mill:plan:cancel')")
@PutMapping("/cancel/{planNo}/{matSeqNo}")
public AjaxResult cancel(@PathVariable String planNo, @PathVariable String matSeqNo) {
return toAjax(millPlanService.cancelMillPlan(planNo, matSeqNo));
}
}

View File

@@ -0,0 +1,52 @@
package com.ruoyi.mill.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.mill.domain.MillProductionResult;
import com.ruoyi.mill.service.IMillProductionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/mill/production")
public class MillProductionController extends BaseController {
@Autowired
private IMillProductionService productionService;
@PreAuthorize("@ss.hasPermi('mill:production:list')")
@GetMapping("/list")
public TableDataInfo list(MillProductionResult query) {
startPage();
List<MillProductionResult> list = productionService.selectResultList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('mill:production:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return AjaxResult.success(productionService.selectResultById(id));
}
/** 录入产出结果并自动通过 UDP 上报 K12F03 */
@PreAuthorize("@ss.hasPermi('mill:production:add')")
@PostMapping
public AjaxResult add(@RequestBody MillProductionResult result) {
return toAjax(productionService.saveAndReport(result));
}
/** 删除计划钢卷并发送 K12F02 */
@PreAuthorize("@ss.hasPermi('mill:production:delete')")
@DeleteMapping("/coil")
public AjaxResult deleteCoil(@RequestParam String planNo,
@RequestParam Integer seqNo,
@RequestParam String unitCode,
@RequestParam String inMatNo,
@RequestParam String causeDesc) {
return toAjax(productionService.deleteCoilAndNotify(planNo, seqNo, unitCode, inMatNo, causeDesc));
}
}

View File

@@ -0,0 +1,128 @@
package com.ruoyi.mill.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import java.math.BigDecimal;
/**
* 冷轧生产计划 — 对应 2FK101 作业命令信息
*/
public class MillPlan extends BaseEntity {
@Excel(name = "计划号")
private String planNo;
@Excel(name = "序号")
private String matSeqNo;
@Excel(name = "机组代码")
private String unitCode;
@Excel(name = "计划类型")
private String planType;
/** 计划状态: 0-待生产 1-生产中 2-完成 3-撤销 */
@Excel(name = "计划状态")
private Integer planStatus;
@Excel(name = "入口钢卷号")
private String inMatNo;
@Excel(name = "入口厚度(mm)")
private BigDecimal inMatThick;
@Excel(name = "入口最大厚度(mm)")
private BigDecimal inMatThickMax;
@Excel(name = "入口最小厚度(mm)")
private BigDecimal inMatThickMin;
@Excel(name = "入口宽度(mm)")
private BigDecimal inMatWidth;
@Excel(name = "入口重量(t)")
private BigDecimal inMatWt;
@Excel(name = "入口长度(m)")
private BigDecimal inMatLen;
@Excel(name = "入口内径(mm)")
private BigDecimal inMatInDia;
@Excel(name = "入口外径(mm)")
private BigDecimal inMatDia;
@Excel(name = "炉号")
private String pono;
@Excel(name = "钢种")
private String sgSign;
@Excel(name = "出口材料号")
private String outMatNo;
@Excel(name = "内部钢卷号")
private String custInMatNo;
@Excel(name = "备注")
private String remark;
// ---- getters / setters ----
public String getPlanNo() { return planNo; }
public void setPlanNo(String v) { this.planNo = v; }
public String getMatSeqNo() { return matSeqNo; }
public void setMatSeqNo(String v) { this.matSeqNo = v; }
public String getUnitCode() { return unitCode; }
public void setUnitCode(String v) { this.unitCode = v; }
public String getPlanType() { return planType; }
public void setPlanType(String v) { this.planType = v; }
public Integer getPlanStatus() { return planStatus; }
public void setPlanStatus(Integer v) { this.planStatus = v; }
public String getInMatNo() { return inMatNo; }
public void setInMatNo(String v) { this.inMatNo = v; }
public BigDecimal getInMatThick() { return inMatThick; }
public void setInMatThick(BigDecimal v) { this.inMatThick = v; }
public BigDecimal getInMatThickMax() { return inMatThickMax; }
public void setInMatThickMax(BigDecimal v) { this.inMatThickMax = v; }
public BigDecimal getInMatThickMin() { return inMatThickMin; }
public void setInMatThickMin(BigDecimal v) { this.inMatThickMin = v; }
public BigDecimal getInMatWidth() { return inMatWidth; }
public void setInMatWidth(BigDecimal v) { this.inMatWidth = v; }
public BigDecimal getInMatWt() { return inMatWt; }
public void setInMatWt(BigDecimal v) { this.inMatWt = v; }
public BigDecimal getInMatLen() { return inMatLen; }
public void setInMatLen(BigDecimal v) { this.inMatLen = v; }
public BigDecimal getInMatInDia() { return inMatInDia; }
public void setInMatInDia(BigDecimal v) { this.inMatInDia = v; }
public BigDecimal getInMatDia() { return inMatDia; }
public void setInMatDia(BigDecimal v) { this.inMatDia = v; }
public String getPono() { return pono; }
public void setPono(String v) { this.pono = v; }
public String getSgSign() { return sgSign; }
public void setSgSign(String v) { this.sgSign = v; }
public String getOutMatNo() { return outMatNo; }
public void setOutMatNo(String v) { this.outMatNo = v; }
public String getCustInMatNo() { return custInMatNo; }
public void setCustInMatNo(String v) { this.custInMatNo = v; }
public String getRemark() { return remark; }
public void setRemark(String v) { this.remark = v; }
}

View File

@@ -0,0 +1,158 @@
package com.ruoyi.mill.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import java.math.BigDecimal;
/**
* 冷轧产出信息 — 对应 K12F03 生产信息电文
*/
public class MillProductionResult extends BaseEntity {
/** 操作标识: I-新增 U-修改 D-删除 */
@Excel(name = "操作标识")
private String flag;
@Excel(name = "计划号")
private String planNo;
@Excel(name = "序号")
private Integer seqNo;
@Excel(name = "机组代码")
private String unitCode;
@Excel(name = "产出序号")
private String procSeqNo;
@Excel(name = "产出类型")
private String annealFlag;
@Excel(name = "入口钢卷号")
private String inMatNo;
@Excel(name = "入口重量(t)")
private Integer inMatWt;
@Excel(name = "入口厚度(mm)")
private BigDecimal inMatThick;
@Excel(name = "入口宽度(mm)")
private BigDecimal inMatWidth;
@Excel(name = "出口钢卷号")
private String outMatNo;
@Excel(name = "内部钢卷号")
private String custMatNo;
@Excel(name = "出口实际重量(t)")
private Integer outMatActWt;
@Excel(name = "出口实际厚度(mm)")
private BigDecimal outMatActThick;
@Excel(name = "出口实际宽度(mm)")
private BigDecimal outMatActWidth;
@Excel(name = "出口实际长度(m)")
private BigDecimal outMatActLen;
@Excel(name = "出口内径(mm)")
private BigDecimal outMatActInnerDia;
@Excel(name = "出口外径(mm)")
private BigDecimal outMatActOuterDia;
@Excel(name = "生产班次")
private String prodShiftNo;
@Excel(name = "生产班组")
private String prodShiftGroup;
@Excel(name = "开始时刻")
private String startProdTime;
@Excel(name = "结束时刻")
private String endProdTime;
@Excel(name = "炉台号")
private String socketNo;
@Excel(name = "备注")
private String remark;
// ---- getters / setters ----
public String getFlag() { return flag; }
public void setFlag(String v) { this.flag = v; }
public String getPlanNo() { return planNo; }
public void setPlanNo(String v) { this.planNo = v; }
public Integer getSeqNo() { return seqNo; }
public void setSeqNo(Integer v) { this.seqNo = v; }
public String getUnitCode() { return unitCode; }
public void setUnitCode(String v) { this.unitCode = v; }
public String getProcSeqNo() { return procSeqNo; }
public void setProcSeqNo(String v) { this.procSeqNo = v; }
public String getAnnealFlag() { return annealFlag; }
public void setAnnealFlag(String v) { this.annealFlag = v; }
public String getInMatNo() { return inMatNo; }
public void setInMatNo(String v) { this.inMatNo = v; }
public Integer getInMatWt() { return inMatWt; }
public void setInMatWt(Integer v) { this.inMatWt = v; }
public BigDecimal getInMatThick() { return inMatThick; }
public void setInMatThick(BigDecimal v) { this.inMatThick = v; }
public BigDecimal getInMatWidth() { return inMatWidth; }
public void setInMatWidth(BigDecimal v) { this.inMatWidth = v; }
public String getOutMatNo() { return outMatNo; }
public void setOutMatNo(String v) { this.outMatNo = v; }
public String getCustMatNo() { return custMatNo; }
public void setCustMatNo(String v) { this.custMatNo = v; }
public Integer getOutMatActWt() { return outMatActWt; }
public void setOutMatActWt(Integer v) { this.outMatActWt = v; }
public BigDecimal getOutMatActThick() { return outMatActThick; }
public void setOutMatActThick(BigDecimal v) { this.outMatActThick = v; }
public BigDecimal getOutMatActWidth() { return outMatActWidth; }
public void setOutMatActWidth(BigDecimal v) { this.outMatActWidth = v; }
public BigDecimal getOutMatActLen() { return outMatActLen; }
public void setOutMatActLen(BigDecimal v) { this.outMatActLen = v; }
public BigDecimal getOutMatActInnerDia() { return outMatActInnerDia; }
public void setOutMatActInnerDia(BigDecimal v) { this.outMatActInnerDia = v; }
public BigDecimal getOutMatActOuterDia() { return outMatActOuterDia; }
public void setOutMatActOuterDia(BigDecimal v) { this.outMatActOuterDia = v; }
public String getProdShiftNo() { return prodShiftNo; }
public void setProdShiftNo(String v) { this.prodShiftNo = v; }
public String getProdShiftGroup() { return prodShiftGroup; }
public void setProdShiftGroup(String v) { this.prodShiftGroup = v; }
public String getStartProdTime() { return startProdTime; }
public void setStartProdTime(String v) { this.startProdTime = v; }
public String getEndProdTime() { return endProdTime; }
public void setEndProdTime(String v) { this.endProdTime = v; }
public String getSocketNo() { return socketNo; }
public void setSocketNo(String v) { this.socketNo = v; }
public String getRemark() { return remark; }
public void setRemark(String v) { this.remark = v; }
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.mill.protocol;
/**
* 电文字段描述符(与 Excel 字段定义一一对应)
*/
public class FieldDef {
private final String name;
private final FieldType type;
/** 字符型长度字节数整型固定4浮点固定4长整型固定8 */
private final int length;
/** 浮点精度(仅用于格式化输出,编解码不依赖此值) */
private final int precision;
public FieldDef(String name, FieldType type, int length, int precision) {
this.name = name;
this.type = type;
this.length = length;
this.precision = precision;
}
public String getName() { return name; }
public FieldType getType() { return type; }
public int getLength() { return type == FieldType.STRING ? length : type == FieldType.LONG ? 8 : 4; }
public int getPrecision() { return precision; }
}

View File

@@ -0,0 +1,105 @@
package com.ruoyi.mill.protocol;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* iXComPCS UDP 电文编解码器第18章协议
*
* 编码规则:
* 字符型 → 固定长度,右侧以 0x20(空格) 填充,超长截断
* 整型 → 4 字节,大端序 (BIG_ENDIAN)
* 浮点型 → 4 字节 IEEE-754小端序 (LITTLE_ENDIAN)
* 长整型 → 8 字节,大端序
*/
public class TelegramCodec {
private TelegramCodec() {}
/** 将字段 Map 按字段定义顺序序列化为字节数组 */
public static byte[] encode(List<FieldDef> schema, Map<String, Object> data) {
int totalLen = calcTotalLength(schema);
ByteBuffer buf = ByteBuffer.allocate(totalLen);
for (FieldDef fd : schema) {
Object val = data.getOrDefault(fd.getName(), defaultValue(fd));
switch (fd.getType()) {
case STRING: {
String s = val == null ? "" : val.toString();
byte[] bytes = s.getBytes(StandardCharsets.US_ASCII);
byte[] fixed = new byte[fd.getLength()];
Arrays.fill(fixed, (byte) 0x20);
System.arraycopy(bytes, 0, fixed, 0, Math.min(bytes.length, fixed.length));
buf.put(fixed);
break;
}
case INT: {
int i = val instanceof Number ? ((Number) val).intValue() : 0;
buf.order(ByteOrder.BIG_ENDIAN).putInt(i);
break;
}
case FLOAT: {
float f = val instanceof Number ? ((Number) val).floatValue() : 0f;
buf.order(ByteOrder.LITTLE_ENDIAN).putFloat(f);
break;
}
case LONG: {
long l = val instanceof Number ? ((Number) val).longValue() : 0L;
buf.order(ByteOrder.BIG_ENDIAN).putLong(l);
break;
}
}
}
return buf.array();
}
/** 将字节数组按字段定义顺序反序列化为字段 Map */
public static Map<String, Object> decode(List<FieldDef> schema, byte[] raw) {
ByteBuffer buf = ByteBuffer.wrap(raw);
Map<String, Object> result = new LinkedHashMap<>();
for (FieldDef fd : schema) {
if (buf.remaining() < fd.getLength()) break;
switch (fd.getType()) {
case STRING: {
byte[] bytes = new byte[fd.getLength()];
buf.get(bytes);
String s = new String(bytes, StandardCharsets.US_ASCII).stripTrailing();
result.put(fd.getName(), s);
break;
}
case INT: {
result.put(fd.getName(), buf.order(ByteOrder.BIG_ENDIAN).getInt());
break;
}
case FLOAT: {
result.put(fd.getName(), buf.order(ByteOrder.LITTLE_ENDIAN).getFloat());
break;
}
case LONG: {
result.put(fd.getName(), buf.order(ByteOrder.BIG_ENDIAN).getLong());
break;
}
}
}
return result;
}
private static int calcTotalLength(List<FieldDef> schema) {
return schema.stream().mapToInt(FieldDef::getLength).sum();
}
private static Object defaultValue(FieldDef fd) {
switch (fd.getType()) {
case INT: return 0;
case FLOAT: return 0.0f;
case LONG: return 0L;
default: return "";
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.mill.protocol;
/**
* iXComPCS 电文字段类型定义(对应 Sheet2 编码规则)
* 字符型: 空格右填充的定长 ASCII 字符串
* 整型: 4字节有符号整数大端序
* 浮点型: 4字节 IEEE-754 单精度,小端序
* 长整型: 8字节有符号整数大端序
*/
public enum FieldType {
STRING, // 字符型 ' '
INT, // 整型 '>'
FLOAT, // 浮点型 '<'
LONG // 长整型 '|'
}

View File

@@ -0,0 +1,192 @@
package com.ruoyi.mill.protocol;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.ruoyi.mill.protocol.FieldType.*;
/**
* 7 条电文的字段 Schema 定义(与 电文/*.xlsx 完全对应)
*
* L3→L2接收:
* 2FK101 作业命令信息
* 2FK102 作业命令撤销
* 2FK103 作业命令应答
* 2FK104 产出信息应答
*
* L2→L3发送:
* K12F01 计划信息应答
* K12F02 计划钢卷删除
* K12F03 生产信息电文
*/
public final class TelegramSchema {
private TelegramSchema() {}
private static FieldDef s(String name, int len) { return new FieldDef(name, STRING, len, 0); }
private static FieldDef i(String name) { return new FieldDef(name, INT, 4, 0); }
private static FieldDef f(String name, int len, int precision) { return new FieldDef(name, FLOAT, len, precision); }
// ─────────────────────────────────────────────────────────────
// 2FK101 作业命令信息 L3→L2
// ─────────────────────────────────────────────────────────────
public static final String ID_2FK101 = "2FK101";
public static final List<FieldDef> SCHEMA_2FK101 = Collections.unmodifiableList(Arrays.asList(
s("PLAN_NO", 20),
s("MAT_SEQ_NO", 3),
s("UNIT_CODE", 4),
s("PLAN_TYPE", 1),
s("IN_MAT_NO", 20),
f("IN_MAT_THICK", 9, 3),
f("IN_MAT_THICK_MAX", 9, 3),
f("IN_MAT_THICK_MIN", 9, 3),
f("IN_MAT_WIDTH", 10, 3),
f("IN_MAT_WT", 5, 0),
f("IN_MAT_LEN", 12, 3),
f("IN_MAT_IN_DIA", 8, 2),
f("IN_MAT_DIA", 8, 2),
s("PONO", 10),
s("SG_SIGN", 24),
s("OUT_MAT_NO", 20),
s("CUST_IN_MAT_NO", 20),
s("REMARK", 250),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20),
f("SPARE_6", 13, 3),
f("SPARE_7", 13, 3),
f("SPARE_8", 13, 3),
f("SPARE_9", 13, 3),
f("SPARE_10", 13, 3)
));
// ─────────────────────────────────────────────────────────────
// 2FK102 作业命令撤销 L3→L2
// ─────────────────────────────────────────────────────────────
public static final String ID_2FK102 = "2FK102";
public static final List<FieldDef> SCHEMA_2FK102 = Collections.unmodifiableList(Arrays.asList(
s("PLAN_NO", 20),
i("MAT_SEQ_NO"),
s("UNIT_CODE", 4),
s("IN_MAT_NO", 20),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
// ─────────────────────────────────────────────────────────────
// 2FK103 作业命令应答 L3→L2
// ─────────────────────────────────────────────────────────────
public static final String ID_2FK103 = "2FK103";
public static final List<FieldDef> SCHEMA_2FK103 = Collections.unmodifiableList(Arrays.asList(
s("TC_NO", 6),
s("PLAN_NO", 20),
i("SEQ_NO"),
s("UNIT_CODE", 4),
s("IN_MAT_NO", 20),
s("FLAG", 1),
s("DEAL_RESULT", 50),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
// ─────────────────────────────────────────────────────────────
// 2FK104 产出信息应答 L3→L2
// ─────────────────────────────────────────────────────────────
public static final String ID_2FK104 = "2FK104";
public static final List<FieldDef> SCHEMA_2FK104 = Collections.unmodifiableList(Arrays.asList(
s("TC_NO", 6),
s("PLAN_NO", 20),
i("SEQ_NO"),
s("UNIT_CODE", 4),
s("OUT_MAT_NO", 20),
s("FLAG", 1),
s("DEAL_RESULT", 50),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
// ─────────────────────────────────────────────────────────────
// K12F01 计划信息应答 L2→L3
// ─────────────────────────────────────────────────────────────
public static final String ID_K12F01 = "K12F01";
public static final List<FieldDef> SCHEMA_K12F01 = Collections.unmodifiableList(Arrays.asList(
s("TC_NO", 6),
s("PLAN_NO", 20),
i("SEQ_NO"),
s("UNIT_CODE", 4),
s("IN_MAT_NO", 20),
s("FLAG", 1),
s("DEAL_RESULT", 50),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
// ─────────────────────────────────────────────────────────────
// K12F02 计划钢卷删除 L2→L3
// ─────────────────────────────────────────────────────────────
public static final String ID_K12F02 = "K12F02";
public static final List<FieldDef> SCHEMA_K12F02 = Collections.unmodifiableList(Arrays.asList(
s("PLAN_NO", 20),
i("SEQ_NO"),
s("UNIT_CODE", 4),
s("IN_MAT_NO", 20),
s("CAUSE_DESC", 50),
s("DO_TIME", 14),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
// ─────────────────────────────────────────────────────────────
// K12F03 生产信息电文 L2→L3
// ─────────────────────────────────────────────────────────────
public static final String ID_K12F03 = "K12F03";
public static final List<FieldDef> SCHEMA_K12F03 = Collections.unmodifiableList(Arrays.asList(
s("FLAG", 1),
s("PLAN_NO", 20),
i("SEQ_NO"),
s("UNIT_CODE", 4),
s("PROC_SEQ_NO", 4),
s("ANNEAL_FLAG", 16),
s("IN_MAT_NO", 20),
i("IN_MAT_WT"),
f("IN_MAT_THICK", 9, 3),
f("IN_MAT_WIDTH", 10, 3),
s("OUT_MAT_NO", 20),
s("CUST_MAT_NO", 20),
i("OUT_MAT_ACT_WT"),
f("OUT_MAT_ACT_THICK", 9, 3),
f("OUT_MAT_ACT_WIDTH", 10, 3),
f("OUT_MAT_ACT_LEN", 15, 3),
f("OUT_MAT_ACT_INNER_DIA", 8, 2),
f("OUT_MAT_ACT_OUTER_DIA", 8, 2),
s("PROD_SHIFT_NO", 1),
s("PROD_SHIFT_GROUP", 1),
s("START_PROD_TIME", 14),
s("END_PROD_TIME", 14),
s("SOCKET_NO", 2),
s("REMARK", 100),
s("SPARE_1", 20),
s("SPARE_2", 20),
s("SPARE_3", 20),
s("SPARE_4", 20),
s("SPARE_5", 20)
));
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.mill.service;
import com.ruoyi.mill.domain.MillPlan;
import java.util.List;
public interface IMillPlanService {
List<MillPlan> selectMillPlanList(MillPlan query);
MillPlan selectMillPlanByPlanNo(String planNo);
int insertMillPlan(MillPlan plan);
int updateMillPlan(MillPlan plan);
/** 撤销计划L3 下发 2FK102 后调用) */
int cancelMillPlan(String planNo, String matSeqNo);
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.mill.service;
import com.ruoyi.mill.domain.MillProductionResult;
import java.util.List;
public interface IMillProductionService {
List<MillProductionResult> selectResultList(MillProductionResult query);
MillProductionResult selectResultById(Long id);
/** 保存产出并通过 UDP 发送 K12F03 */
int saveAndReport(MillProductionResult result);
/** 删除计划钢卷并发送 K12F02 */
int deleteCoilAndNotify(String planNo, Integer seqNo, String unitCode,
String inMatNo, String causeDesc);
}

View File

@@ -0,0 +1,64 @@
package com.ruoyi.mill.service.impl;
import com.ruoyi.mill.domain.MillPlan;
import com.ruoyi.mill.service.IMillPlanService;
import com.ruoyi.mill.udp.UdpSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class MillPlanServiceImpl implements IMillPlanService {
@Autowired
private UdpSender udpSender;
// TODO: 注入 MillPlanMapper 并替换以下占位实现
@Override
public List<MillPlan> selectMillPlanList(MillPlan query) {
return new ArrayList<>();
}
@Override
public MillPlan selectMillPlanByPlanNo(String planNo) {
return null;
}
@Override
public int insertMillPlan(MillPlan plan) {
// TODO: mapper.insert(plan)
// 写入成功后回复 K12F01 计划信息应答
sendK12F01Ack(plan.getPlanNo(), plan.getMatSeqNo(),
plan.getUnitCode(), plan.getInMatNo(), "1", "OK");
return 1;
}
@Override
public int updateMillPlan(MillPlan plan) {
return 0; // TODO
}
@Override
public int cancelMillPlan(String planNo, String matSeqNo) {
// TODO: mapper 软删除
return 1;
}
private void sendK12F01Ack(String planNo, String seqNo, String unitCode,
String inMatNo, String flag, String dealResult) {
Map<String, Object> data = new HashMap<>();
data.put("TC_NO", "K12F01");
data.put("PLAN_NO", planNo);
data.put("SEQ_NO", 0);
data.put("UNIT_CODE", unitCode);
data.put("IN_MAT_NO", inMatNo);
data.put("FLAG", flag);
data.put("DEAL_RESULT", dealResult);
udpSender.sendK12F01(data);
}
}

View File

@@ -0,0 +1,83 @@
package com.ruoyi.mill.service.impl;
import com.ruoyi.mill.domain.MillProductionResult;
import com.ruoyi.mill.service.IMillProductionService;
import com.ruoyi.mill.udp.UdpSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class MillProductionServiceImpl implements IMillProductionService {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Autowired
private UdpSender udpSender;
// TODO: 注入 MillProductionResultMapper
@Override
public List<MillProductionResult> selectResultList(MillProductionResult query) {
return new ArrayList<>();
}
@Override
public MillProductionResult selectResultById(Long id) {
return null;
}
@Override
public int saveAndReport(MillProductionResult r) {
// TODO: mapper.insert(r)
// 保存成功后发送 K12F03
Map<String, Object> data = new HashMap<>();
data.put("FLAG", "I");
data.put("PLAN_NO", r.getPlanNo());
data.put("SEQ_NO", r.getSeqNo() == null ? 0 : r.getSeqNo());
data.put("UNIT_CODE", r.getUnitCode());
data.put("PROC_SEQ_NO", r.getProcSeqNo());
data.put("ANNEAL_FLAG", r.getAnnealFlag());
data.put("IN_MAT_NO", r.getInMatNo());
data.put("IN_MAT_WT", r.getInMatWt() == null ? 0 : r.getInMatWt());
data.put("IN_MAT_THICK", r.getInMatThick() == null ? 0f : r.getInMatThick().floatValue());
data.put("IN_MAT_WIDTH", r.getInMatWidth() == null ? 0f : r.getInMatWidth().floatValue());
data.put("OUT_MAT_NO", r.getOutMatNo());
data.put("CUST_MAT_NO", r.getCustMatNo());
data.put("OUT_MAT_ACT_WT", r.getOutMatActWt() == null ? 0 : r.getOutMatActWt());
data.put("OUT_MAT_ACT_THICK", r.getOutMatActThick() == null ? 0f : r.getOutMatActThick().floatValue());
data.put("OUT_MAT_ACT_WIDTH", r.getOutMatActWidth() == null ? 0f : r.getOutMatActWidth().floatValue());
data.put("OUT_MAT_ACT_LEN", r.getOutMatActLen() == null ? 0f : r.getOutMatActLen().floatValue());
data.put("OUT_MAT_ACT_INNER_DIA", r.getOutMatActInnerDia() == null ? 0f : r.getOutMatActInnerDia().floatValue());
data.put("OUT_MAT_ACT_OUTER_DIA", r.getOutMatActOuterDia() == null ? 0f : r.getOutMatActOuterDia().floatValue());
data.put("PROD_SHIFT_NO", r.getProdShiftNo());
data.put("PROD_SHIFT_GROUP", r.getProdShiftGroup());
data.put("START_PROD_TIME", r.getStartProdTime());
data.put("END_PROD_TIME", r.getEndProdTime());
data.put("SOCKET_NO", r.getSocketNo());
data.put("REMARK", r.getRemark());
udpSender.sendK12F03(data);
return 1;
}
@Override
public int deleteCoilAndNotify(String planNo, Integer seqNo, String unitCode,
String inMatNo, String causeDesc) {
// TODO: mapper 软删除
Map<String, Object> data = new HashMap<>();
data.put("PLAN_NO", planNo);
data.put("SEQ_NO", seqNo == null ? 0 : seqNo);
data.put("UNIT_CODE", unitCode);
data.put("IN_MAT_NO", inMatNo);
data.put("CAUSE_DESC", causeDesc);
data.put("DO_TIME", LocalDateTime.now().format(FMT));
udpSender.sendK12F02(data);
return 1;
}
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.mill.udp;
import com.ruoyi.mill.protocol.TelegramCodec;
import com.ruoyi.mill.protocol.TelegramSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 接收到的 L3→L2 电文分发处理器
* 根据电文号前6字节路由到对应业务处理方法
*/
@Component
public class TelegramDispatcher {
private static final Logger log = LoggerFactory.getLogger(TelegramDispatcher.class);
public void dispatch(String telegramId, byte[] payload) {
log.info("[UDP-RECV] telegramId={} payloadLen={}", telegramId, payload.length);
switch (telegramId) {
case TelegramSchema.ID_2FK101:
handle2FK101(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK101, payload));
break;
case TelegramSchema.ID_2FK102:
handle2FK102(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK102, payload));
break;
case TelegramSchema.ID_2FK103:
handle2FK103(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK103, payload));
break;
case TelegramSchema.ID_2FK104:
handle2FK104(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK104, payload));
break;
default:
log.warn("[UDP-RECV] 未知电文号: {}", telegramId);
}
}
/** 2FK101 作业命令信息 — L3 下发生产计划 */
private void handle2FK101(Map<String, Object> data) {
log.info("[2FK101] 作业命令: planNo={} inMatNo={} unitCode={}",
data.get("PLAN_NO"), data.get("IN_MAT_NO"), data.get("UNIT_CODE"));
// TODO: 写入计划表,触发 K12F01 应答
}
/** 2FK102 作业命令撤销 — L3 撤销某条计划 */
private void handle2FK102(Map<String, Object> data) {
log.info("[2FK102] 作业撤销: planNo={} inMatNo={}", data.get("PLAN_NO"), data.get("IN_MAT_NO"));
// TODO: 从计划表软删除,记录撤销原因
}
/** 2FK103 作业命令应答 — L3 对 L2 发出计划的回执 */
private void handle2FK103(Map<String, Object> data) {
log.info("[2FK103] 命令应答: tcNo={} flag={} result={}",
data.get("TC_NO"), data.get("FLAG"), data.get("DEAL_RESULT"));
// TODO: 更新计划状态
}
/** 2FK104 产出信息应答 — L3 对 L2 上报产出的回执 */
private void handle2FK104(Map<String, Object> data) {
log.info("[2FK104] 产出应答: tcNo={} flag={} result={}",
data.get("TC_NO"), data.get("FLAG"), data.get("DEAL_RESULT"));
// TODO: 更新产出确认状态
}
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.mill.udp;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* UDP 通信参数配置application.yml: mill.udp.*
*/
@Component
@ConfigurationProperties(prefix = "mill.udp")
public class UdpProperties {
/** 本地监听端口(接收 L3 下发电文) */
private int localPort = 9001;
/** L3 系统 IP */
private String remoteHost = "127.0.0.1";
/** L3 系统端口 */
private int remotePort = 9000;
/** 接收缓冲区大小(字节) */
private int bufferSize = 4096;
public int getLocalPort() { return localPort; }
public void setLocalPort(int p) { this.localPort = p; }
public String getRemoteHost() { return remoteHost; }
public void setRemoteHost(String h){ this.remoteHost = h; }
public int getRemotePort() { return remotePort; }
public void setRemotePort(int p) { this.remotePort = p; }
public int getBufferSize() { return bufferSize; }
public void setBufferSize(int s) { this.bufferSize = s; }
}

View File

@@ -0,0 +1,62 @@
package com.ruoyi.mill.udp;
import com.ruoyi.mill.protocol.TelegramCodec;
import com.ruoyi.mill.protocol.TelegramSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
/**
* UDP 发送服务 — L2→L3 上行电文
* 帧结构前6字节电文号 + 电文体
*/
@Component
public class UdpSender {
private static final Logger log = LoggerFactory.getLogger(UdpSender.class);
@Autowired
private UdpProperties props;
/** K12F01 计划信息应答 */
public void sendK12F01(Map<String, Object> data) {
send(TelegramSchema.ID_K12F01, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F01, data));
}
/** K12F02 计划钢卷删除 */
public void sendK12F02(Map<String, Object> data) {
send(TelegramSchema.ID_K12F02, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F02, data));
}
/** K12F03 生产信息电文 */
public void sendK12F03(Map<String, Object> data) {
send(TelegramSchema.ID_K12F03, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F03, data));
}
private void send(String tcNo, byte[] payload) {
try (DatagramSocket socket = new DatagramSocket()) {
// 帧 = 6字节电文号 + payload
byte[] tcNoBytes = Arrays.copyOf(
tcNo.getBytes(StandardCharsets.US_ASCII), 6);
byte[] frame = new byte[6 + payload.length];
System.arraycopy(tcNoBytes, 0, frame, 0, 6);
System.arraycopy(payload, 0, frame, 6, payload.length);
InetAddress addr = InetAddress.getByName(props.getRemoteHost());
DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, props.getRemotePort());
socket.send(pkt);
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
props.getRemoteHost(), props.getRemotePort(), frame.length);
} catch (Exception e) {
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
}
}
}

View File

@@ -0,0 +1,77 @@
package com.ruoyi.mill.udp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* UDP 接收服务
* 监听来自 L3 的下行电文,解析电文号后交 TelegramDispatcher 处理
*
* 电文帧结构iXComPCS 第18章
* 前6字节 ASCII = 电文号 (TC_NO)
* 后续字节 = 电文体 (payload)
*/
@Component
public class UdpServer {
private static final Logger log = LoggerFactory.getLogger(UdpServer.class);
private static final int TC_NO_LEN = 6;
@Autowired
private UdpProperties props;
@Autowired
private TelegramDispatcher dispatcher;
private DatagramSocket socket;
private volatile boolean running;
private final ExecutorService executor = Executors.newSingleThreadExecutor(
r -> { Thread t = new Thread(r, "udp-receiver"); t.setDaemon(true); return t; });
@PostConstruct
public void start() throws Exception {
socket = new DatagramSocket(props.getLocalPort());
running = true;
executor.submit(this::receiveLoop);
log.info("[UDP-SERVER] 已启动,监听端口: {}", props.getLocalPort());
}
@PreDestroy
public void stop() {
running = false;
if (socket != null && !socket.isClosed()) socket.close();
executor.shutdownNow();
log.info("[UDP-SERVER] 已停止");
}
private void receiveLoop() {
byte[] buf = new byte[props.getBufferSize()];
while (running) {
try {
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
socket.receive(pkt);
byte[] data = Arrays.copyOf(pkt.getData(), pkt.getLength());
if (data.length < TC_NO_LEN) {
log.warn("[UDP-SERVER] 收到过短数据包,长度={}", data.length);
continue;
}
String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim();
byte[] payload = Arrays.copyOfRange(data, TC_NO_LEN, data.length);
dispatcher.dispatch(tcNo, payload);
} catch (Exception e) {
if (running) log.error("[UDP-SERVER] 接收异常", e);
}
}
}
}