组装产线工站流程层级标定层级
核心组件实现产线 - 工段 - 工位的可视化拖拽标定,支持布局缩放、连接关系绘制;任务调度实现 ERP 工单到工位的精准绑定,支持任务启动 / 暂停 / 完成等全生命周期操作;多指令适配:通过模型统一管理各端指令,记录指令执行日志,支持 ERP 计划端、MES 执行端等多端协同。该组件可直接集成到 WinForm 中间件项目,适配组装产线的工位布局标定与任务调度需求,支持二次扩展(如 IoT 数据
·
面向 ERP/MES 系统、聚焦组装产线工站流程化层级布局标定 + 工位任务绑定的 WinForm 中间件组件,核心适配 “多指令端 / 应用调度模式”,实现从 “产线层级布局可视化标定→工位任务精准绑定→多端指令调度→状态闭环监控” 的全流程能力,解决组装产线工位任务分配不精准、多端指令协同效率低、布局与任务脱节的问题。
以下是可直接落地的完整设计 + 代码实现,包含核心布局标定组件、任务绑定模块、多指令调度中心三大核心模块。
一、 核心设计思路
1. 层级布局模型(适配组装产线特性)
| 层级 | 核心属性 | 布局标定要素 | 任务绑定规则 |
|---|---|---|---|
| 产线 | 产线编码 / 名称 / 类型 / 尺寸 | 画布尺寸、坐标系原点、缩放比例 | 承接 ERP 工单,拆解为工段任务 |
| 工段 | 工段编码 / 所属产线 / 排序 / 尺寸 | 相对产线的 X/Y 坐标、宽高、排列方式 | 按工艺路线拆分任务,分配到下属工位 |
| 工位 | 工位编码 / 所属工段 / 类型 / 设备组 | 相对工段的 X/Y 坐标、宽高、连接关系 | 绑定具体工序任务、工艺参数、执行规则 |
2. 多指令端调度模型
| 指令端类型 | 核心指令 | 调度规则 | 联动动作 |
|---|---|---|---|
| ERP 计划端 | 工单下发 / 任务调整 / 工单取消 | 仅对 “已标定布局” 的产线 / 工位生效 | 触发工位任务绑定,更新工位生命周期状态 |
| MES 执行端 | 任务启动 / 暂停 / 完成 / 返工 | 仅对 “已绑定任务” 的工位生效 | 同步工位执行状态,更新产线 / 工段进度 |
| 工艺配置端 | 工艺绑定 / 参数调整 / 路线变更 | 仅对 “初始化 / 已配置” 状态的工位生效 | 更新工位工艺属性,重新标定任务执行规则 |
| 布局标定端 | 布局保存 / 布局重置 / 工位新增 / 删除 | 仅对 “未投产” 状态的产线生效 | 同步布局数据到 MES,更新工位物理位置属性 |
二、 核心代码实现
1. 基础数据模型(布局 + 任务 + 指令)
csharp
运行
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Dapper;
using Newtonsoft.Json;
// 核心:产线层级布局模型(用于标定)
public class ProductionLineLayout
{
/// <summary>
/// 产线编码
/// </summary>
public string LineCode { get; set; }
/// <summary>
/// 产线名称
/// </summary>
public string LineName { get; set; }
/// <summary>
/// 布局画布宽度(像素)
/// </summary>
public int CanvasWidth { get; set; }
/// <summary>
/// 布局画布高度(像素)
/// </summary>
public int CanvasHeight { get; set; }
/// <summary>
/// 布局标定状态(未标定/已标定/已投产)
/// </summary>
public string LayoutStatus { get; set; }
/// <summary>
/// 最后标定时间
/// </summary>
public DateTime LastCalibrateTime { get; set; }
/// <summary>
/// 标定人
/// </summary>
public string Calibrator { get; set; }
/// <summary>
/// 下属工段布局列表
/// </summary>
public List<WorkSectionLayout> SectionLayouts { get; set; } = new List<WorkSectionLayout>();
}
// 工段布局模型
public class WorkSectionLayout
{
/// <summary>
/// 工段编码
/// </summary>
public string SectionCode { get; set; }
/// <summary>
/// 工段名称
/// </summary>
public string SectionName { get; set; }
/// <summary>
/// 相对产线的X坐标(像素)
/// </summary>
public int X { get; set; }
/// <summary>
/// 相对产线的Y坐标(像素)
/// </summary>
public int Y { get; set; }
/// <summary>
/// 工段宽度(像素)
/// </summary>
public int Width { get; set; }
/// <summary>
/// 工段高度(像素)
/// </summary>
public int Height { get; set; }
/// <summary>
/// 工段排序号
/// </summary>
public int SortNo { get; set; }
/// <summary>
/// 下属工位布局列表
/// </summary>
public List<WorkStationLayout> StationLayouts { get; set; } = new List<WorkStationLayout>();
}
// 工位布局模型(核心:布局标定+任务绑定)
public class WorkStationLayout
{
/// <summary>
/// 工位编码
/// </summary>
public string StationCode { get; set; }
/// <summary>
/// 工位名称
/// </summary>
public string StationName { get; set; }
/// <summary>
/// 相对工段的X坐标(像素)
/// </summary>
public int X { get; set; }
/// <summary>
/// 相对工段的Y坐标(像素)
/// </summary>
public int Y { get; set; }
/// <summary>
/// 工位宽度(像素)
/// </summary>
public int Width { get; set; }
/// <summary>
/// 工位高度(像素)
/// </summary>
public int Height { get; set; }
/// <summary>
/// 工位类型(加工/装配/检测)
/// </summary>
public string StationType { get; set; }
/// <summary>
/// 绑定的工艺编码
/// </summary>
public string ProcessCode { get; set; }
/// <summary>
/// 绑定的ERP工单编号
/// </summary>
public string BindOrderNo { get; set; }
/// <summary>
/// 绑定的任务数量
/// </summary>
public decimal BindTaskQty { get; set; }
/// <summary>
/// 任务执行状态(待执行/执行中/已完成/暂停)
/// </summary>
public string TaskStatus { get; set; }
/// <summary>
/// 工位连接关系(如:上工序工位编码,多个用,分隔)
/// </summary>
public string PreStationCodes { get; set; }
}
// 多指令调度模型
public class LayoutCommand
{
/// <summary>
/// 指令ID
/// </summary>
public string CommandId { get; set; }
/// <summary>
/// 指令来源端(ERP_PLAN/MES_EXEC/PROCESS_CONFIG/LAYOUT_CALIB)
/// </summary>
public string SourceTerminal { get; set; }
/// <summary>
/// 指令目标端(同上)
/// </summary>
public string TargetTerminal { get; set; }
/// <summary>
/// 指令类型(布局保存/任务绑定/任务调整/工艺绑定/工单下发)
/// </summary>
public string CommandType { get; set; }
/// <summary>
/// 关联产线编码
/// </summary>
public string LineCode { get; set; }
/// <summary>
/// 关联工位编码
/// </summary>
public string StationCode { get; set; }
/// <summary>
/// 指令内容(JSON格式)
/// </summary>
public string CommandContent { get; set; }
/// <summary>
/// 指令状态(待执行/已执行/执行失败)
/// </summary>
public string CommandStatus { get; set; }
/// <summary>
/// 指令创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 指令执行时间
/// </summary>
public DateTime? ExecuteTime { get; set; }
}
// 配置辅助类
public static class ConfigHelper
{
/// <summary>
/// 获取数据库连接字符串
/// </summary>
public static string GetConnStr()
{
// 实际项目中建议从配置文件读取
return "Data Source=.;Initial Catalog=MES_AssemblyLine;Integrated Security=True;";
}
}
// 日志辅助类
public static class LogHelper
{
/// <summary>
/// 记录信息日志
/// </summary>
public static void Info(string message)
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [INFO] {message}");
// 实际项目中可集成log4net/NLog
}
/// <summary>
/// 记录错误日志
/// </summary>
public static void Error(string message, Exception ex = null)
{
var errorMsg = ex == null ? message : $"{message} | 异常:{ex.Message}";
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [ERROR] {errorMsg}");
}
}
2. 核心:产线层级布局标定组件(WinForm 可视化)
csharp
运行
using System;
using System.Drawing;
using System.Windows.Forms;
/// <summary>
/// 产线层级布局标定核心控件(支持拖拽标定、任务绑定)
/// </summary>
public partial class LineLayoutCalibrateControl : UserControl
{
// 当前编辑的产线布局
private ProductionLineLayout _currentLineLayout;
// 选中的工段/工位
private WorkSectionLayout _selectedSection;
private WorkStationLayout _selectedStation;
// 拖拽状态
private bool _isDragging;
private Point _dragStartPoint;
// 画布缩放比例
private float _scale = 1.0f;
public LineLayoutCalibrateControl()
{
InitializeComponent();
// 开启双缓冲,避免闪烁
this.DoubleBuffered = true;
// 绑定鼠标事件
this.MouseDown += LayoutCanvas_MouseDown;
this.MouseMove += LayoutCanvas_MouseMove;
this.MouseUp += LayoutCanvas_MouseUp;
this.Paint += LayoutCanvas_Paint;
this.MouseWheel += LayoutCanvas_MouseWheel;
}
#region 核心功能:加载/保存布局
/// <summary>
/// 加载产线布局数据(从数据库)
/// </summary>
public void LoadLineLayout(string lineCode)
{
try
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
// 1. 加载产线基础布局
_currentLineLayout = conn.QuerySingle<ProductionLineLayout>(
"SELECT LineCode, LineName, CanvasWidth, CanvasHeight, LayoutStatus, LastCalibrateTime, Calibrator " +
"FROM ProductionLineLayout WHERE LineCode = @LineCode", new { LineCode = lineCode });
if (_currentLineLayout == null)
{
// 无布局数据,初始化默认值
_currentLineLayout = new ProductionLineLayout
{
LineCode = lineCode,
LineName = $"产线{lineCode}",
CanvasWidth = 1200,
CanvasHeight = 600,
LayoutStatus = "未标定",
LastCalibrateTime = DateTime.Now,
Calibrator = Environment.UserName
};
}
// 2. 加载工段布局
_currentLineLayout.SectionLayouts = conn.Query<WorkSectionLayout>(
"SELECT SectionCode, SectionName, X, Y, Width, Height, SortNo " +
"FROM WorkSectionLayout WHERE LineCode = @LineCode ORDER BY SortNo",
new { LineCode = lineCode }).ToList();
// 3. 加载每个工段的工位布局
foreach (var section in _currentLineLayout.SectionLayouts)
{
section.StationLayouts = conn.Query<WorkStationLayout>(
"SELECT StationCode, StationName, X, Y, Width, Height, StationType, ProcessCode, " +
"BindOrderNo, BindTaskQty, TaskStatus, PreStationCodes " +
"FROM WorkStationLayout WHERE SectionCode = @SectionCode",
new { SectionCode = section.SectionCode }).ToList();
}
LogHelper.Info($"产线{lineCode}布局数据加载完成,状态:{_currentLineLayout.LayoutStatus}");
this.Refresh();
}
}
catch (Exception ex)
{
LogHelper.Error($"加载产线{lineCode}布局失败", ex);
MessageBox.Show($"布局加载失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 保存产线布局标定结果
/// </summary>
public bool SaveLineLayout()
{
if (_currentLineLayout == null)
{
MessageBox.Show("请先加载产线布局");
return false;
}
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
using (var tran = conn.BeginTransaction())
{
try
{
// 1. 保存产线布局
var lineExist = conn.ExecuteScalar<int>(
"SELECT COUNT(1) FROM ProductionLineLayout WHERE LineCode = @LineCode",
new { _currentLineLayout.LineCode }, tran) > 0;
if (lineExist)
{
conn.Execute(
"UPDATE ProductionLineLayout SET LineName=@LineName, CanvasWidth=@CanvasWidth, CanvasHeight=@CanvasHeight, " +
"LayoutStatus=@LayoutStatus, LastCalibrateTime=@LastCalibrateTime, Calibrator=@Calibrator " +
"WHERE LineCode=@LineCode",
new
{
_currentLineLayout.LineName,
_currentLineLayout.CanvasWidth,
_currentLineLayout.CanvasHeight,
_currentLineLayout.LayoutStatus,
_currentLineLayout.LastCalibrateTime = DateTime.Now,
_currentLineLayout.Calibrator = Environment.UserName,
_currentLineLayout.LineCode
}, tran);
}
else
{
conn.Execute(
"INSERT INTO ProductionLineLayout (LineCode, LineName, CanvasWidth, CanvasHeight, LayoutStatus, LastCalibrateTime, Calibrator) " +
"VALUES (@LineCode, @LineName, @CanvasWidth, @CanvasHeight, @LayoutStatus, @LastCalibrateTime, @Calibrator)",
new
{
_currentLineLayout.LineCode,
_currentLineLayout.LineName,
_currentLineLayout.CanvasWidth,
_currentLineLayout.CanvasHeight,
_currentLineLayout.LayoutStatus,
_currentLineLayout.LastCalibrateTime = DateTime.Now,
_currentLineLayout.Calibrator = Environment.UserName
}, tran);
}
// 2. 保存工段布局(先删后增,简化逻辑)
conn.Execute("DELETE FROM WorkSectionLayout WHERE LineCode = @LineCode",
new { _currentLineLayout.LineCode }, tran);
foreach (var section in _currentLineLayout.SectionLayouts)
{
conn.Execute(
"INSERT INTO WorkSectionLayout (SectionCode, SectionName, LineCode, X, Y, Width, Height, SortNo) " +
"VALUES (@SectionCode, @SectionName, @LineCode, @X, @Y, @Width, @Height, @SortNo)",
new
{
section.SectionCode,
section.SectionName,
_currentLineLayout.LineCode,
section.X,
section.Y,
section.Width,
section.Height,
section.SortNo
}, tran);
// 3. 保存工位布局(先删后增)
conn.Execute("DELETE FROM WorkStationLayout WHERE SectionCode = @SectionCode",
new { section.SectionCode }, tran);
foreach (var station in section.StationLayouts)
{
conn.Execute(
"INSERT INTO WorkStationLayout (StationCode, StationName, SectionCode, X, Y, Width, Height, " +
"StationType, ProcessCode, BindOrderNo, BindTaskQty, TaskStatus, PreStationCodes) " +
"VALUES (@StationCode, @StationName, @SectionCode, @X, @Y, @Width, @Height, " +
"@StationType, @ProcessCode, @BindOrderNo, @BindTaskQty, @TaskStatus, @PreStationCodes)",
new
{
station.StationCode,
station.StationName,
section.SectionCode,
station.X,
station.Y,
station.Width,
station.Height,
station.StationType,
station.ProcessCode,
station.BindOrderNo,
station.BindTaskQty,
station.TaskStatus,
station.PreStationCodes
}, tran);
}
}
// 4. 更新布局状态为已标定
_currentLineLayout.LayoutStatus = "已标定";
conn.Execute(
"UPDATE ProductionLineLayout SET LayoutStatus='已标定' WHERE LineCode=@LineCode",
new { _currentLineLayout.LineCode }, tran);
tran.Commit();
LogHelper.Info($"产线{_currentLineLayout.LineCode}布局标定结果保存成功");
this.Refresh();
return true;
}
catch (Exception ex)
{
tran.Rollback();
LogHelper.Error($"保存产线{_currentLineLayout.LineCode}布局失败", ex);
MessageBox.Show($"布局保存失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
}
#endregion
#region 可视化绘制:产线-工段-工位布局
private void LayoutCanvas_Paint(object sender, PaintEventArgs e)
{
if (_currentLineLayout == null) return;
var g = e.Graphics;
// 缩放画布
g.ScaleTransform(_scale, _scale);
// 1. 绘制产线背景
g.FillRectangle(Brushes.LightGray, new Rectangle(0, 0, _currentLineLayout.CanvasWidth, _currentLineLayout.CanvasHeight));
g.DrawRectangle(Pens.Black, new Rectangle(0, 0, _currentLineLayout.CanvasWidth, _currentLineLayout.CanvasHeight));
// 产线标题
g.DrawString($"{_currentLineLayout.LineName}(布局状态:{_currentLineLayout.LayoutStatus})",
new Font("微软雅黑", 12, FontStyle.Bold), Brushes.Black, new PointF(10, 10));
// 2. 绘制工段
foreach (var section in _currentLineLayout.SectionLayouts)
{
// 工段矩形(相对产线坐标)
var sectionRect = new Rectangle(section.X, section.Y, section.Width, section.Height);
// 选中的工段高亮
var sectionBrush = _selectedSection == section ? Brushes.LightSkyBlue : Brushes.LightCyan;
g.FillRectangle(sectionBrush, sectionRect);
g.DrawRectangle(Pens.DarkBlue, sectionRect);
// 工段名称
g.DrawString($"工段:{section.SectionName}({section.SectionCode})",
new Font("微软雅黑", 10, FontStyle.Bold), Brushes.DarkBlue,
new PointF(section.X + 5, section.Y + 5));
// 工段排序号
g.DrawString($"排序:{section.SortNo}",
new Font("微软雅黑", 8), Brushes.Gray,
new PointF(section.X + section.Width - 30, section.Y + 5));
// 3. 绘制工位(相对工段坐标 → 转换为绝对坐标)
foreach (var station in section.StationLayouts)
{
var stationAbsX = section.X + station.X;
var stationAbsY = section.Y + station.Y;
var stationRect = new Rectangle(stationAbsX, stationAbsY, station.Width, station.Height);
// 工位状态配色:待执行-浅黄/执行中-浅绿/已完成-浅蓝/暂停-浅红
var stationBrush = station.TaskStatus switch
{
"执行中" => Brushes.LightGreen,
"已完成" => Brushes.LightBlue,
"暂停" => Brushes.LightPink,
_ => Brushes.LightYellow // 待执行
};
// 选中的工位高亮边框
var stationPen = _selectedStation == station ? Pens.Red : Pens.Black;
g.FillRectangle(stationBrush, stationRect);
g.DrawRectangle(stationPen, stationRect);
// 工位名称+编码
g.DrawString($"{station.StationName}\n({station.StationCode})",
new Font("微软雅黑", 8), Brushes.Black,
new PointF(stationAbsX + 5, stationAbsY + 5));
// 绑定的工单+任务数
if (!string.IsNullOrEmpty(station.BindOrderNo))
{
g.DrawString($"工单:{station.BindOrderNo}\n任务数:{station.BindTaskQty}",
new Font("微软雅黑", 7), Brushes.DarkRed,
new PointF(stationAbsX + 5, stationAbsY + 30));
}
}
// 4. 绘制工位连接关系(工序流转线)
DrawStationConnections(g, section);
}
}
/// <summary>
/// 绘制工位间的连接关系(工序流转线)
/// </summary>
private void DrawStationConnections(Graphics g, WorkSectionLayout section)
{
foreach (var station in section.StationLayouts)
{
if (string.IsNullOrEmpty(station.PreStationCodes)) continue;
// 遍历上工序工位
foreach (var preStationCode in station.PreStationCodes.Split(','))
{
var preStation = section.StationLayouts.FirstOrDefault(s => s.StationCode == preStationCode);
if (preStation == null) continue;
// 计算绝对坐标
var preX = section.X + preStation.X + preStation.Width / 2;
var preY = section.Y + preStation.Y + preStation.Height / 2;
var currX = section.X + station.X + station.Width / 2;
var currY = section.Y + station.Y + station.Height / 2;
// 绘制流转线+箭头
g.DrawLine(Pens.Green, preX, preY, currX, currY);
DrawArrow(g, new Point(preX, preY), new Point(currX, currY), Pens.Green);
}
}
}
/// <summary>
/// 绘制箭头
/// </summary>
private void DrawArrow(Graphics g, Point start, Point end, Pen pen)
{
var angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
int arrowSize = 8;
var p1 = new Point(
(int)(end.X - arrowSize * Math.Cos(angle - Math.PI / 6)),
(int)(end.Y - arrowSize * Math.Sin(angle - Math.PI / 6)));
var p2 = new Point(
(int)(end.X - arrowSize * Math.Cos(angle + Math.PI / 6)),
(int)(end.Y - arrowSize * Math.Sin(angle + Math.PI / 6)));
g.DrawLine(pen, end, p1);
g.DrawLine(pen, end, p2);
}
#endregion
#region 交互功能:拖拽标定布局
private void LayoutCanvas_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left || _currentLineLayout == null) return;
// 转换鼠标坐标(反缩放)
var mouseX = e.X / _scale;
var mouseY = e.Y / _scale;
// 1. 检查是否点击到工位
_selectedStation = null;
_selectedSection = null;
foreach (var section in _currentLineLayout.SectionLayouts)
{
foreach (var station in section.StationLayouts)
{
var stationAbsX = section.X + station.X;
var stationAbsY = section.Y + station.Y;
var stationRect = new Rectangle(stationAbsX, stationAbsY, station.Width, station.Height);
if (stationRect.Contains(mouseX, mouseY))
{
_selectedStation = station;
_selectedSection = section;
_isDragging = true;
_dragStartPoint = new Point(
(int)(mouseX - stationAbsX),
(int)(mouseY - stationAbsY));
// 触发工位选中事件
StationSelected?.Invoke(this, new StationSelectedEventArgs(station));
this.Refresh();
return;
}
}
// 2. 检查是否点击到工段
var sectionRect = new Rectangle(section.X, section.Y, section.Width, section.Height);
if (sectionRect.Contains(mouseX, mouseY))
{
_selectedSection = section;
_selectedStation = null;
// 触发工段选中事件
SectionSelected?.Invoke(this, new SectionSelectedEventArgs(section));
this.Refresh();
return;
}
}
}
private void LayoutCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (!_isDragging || _selectedStation == null || _selectedSection == null) return;
// 转换鼠标坐标(反缩放)
var mouseX = e.X / _scale;
var mouseY = e.Y / _scale;
// 计算工位新的相对坐标(相对于工段)
_selectedStation.X = (int)(mouseX - _selectedSection.X - _dragStartPoint.X);
_selectedStation.Y = (int)(mouseY - _selectedSection.Y - _dragStartPoint.Y);
// 边界限制(不能超出工段范围)
_selectedStation.X = Math.Max(0, Math.Min(_selectedStation.X, _selectedSection.Width - _selectedStation.Width));
_selectedStation.Y = Math.Max(0, Math.Min(_selectedStation.Y, _selectedSection.Height - _selectedStation.Height));
this.Refresh();
}
private void LayoutCanvas_MouseUp(object sender, MouseEventArgs e)
{
_isDragging = false;
}
/// <summary>
/// 鼠标滚轮缩放画布
/// </summary>
private void LayoutCanvas_MouseWheel(object sender, MouseEventArgs e)
{
// 缩放增量:0.1
_scale += e.Delta > 0 ? 0.1f : -0.1f;
// 限制缩放范围
_scale = Math.Max(0.5f, Math.Min(_scale, 2.0f));
this.Refresh();
}
#endregion
#region 事件定义:工段/工位选中
/// <summary>
/// 工段选中事件
/// </summary>
public event EventHandler<SectionSelectedEventArgs> SectionSelected;
/// <summary>
/// 工位选中事件
/// </summary>
public event EventHandler<StationSelectedEventArgs> StationSelected;
}
// 工段选中事件参数
public class SectionSelectedEventArgs : EventArgs
{
public WorkSectionLayout SelectedSection { get; set; }
public SectionSelectedEventArgs(WorkSectionLayout section)
{
SelectedSection = section;
}
}
// 工位选中事件参数
public class StationSelectedEventArgs : EventArgs
{
public WorkStationLayout SelectedStation { get; set; }
public StationSelectedEventArgs(WorkStationLayout station)
{
SelectedStation = station;
}
}
3. 工位任务绑定与多指令调度模块
csharp
运行
/// <summary>
/// 工位任务绑定与多指令调度服务
/// </summary>
public class StationTaskDispatchService
{
private readonly string _connStr;
public StationTaskDispatchService()
{
_connStr = ConfigHelper.GetConnStr();
}
#region 核心功能:工位任务绑定(ERP工单→工位)
/// <summary>
/// 绑定ERP工单到指定工位
/// </summary>
public bool BindOrderToStation(string stationCode, string orderNo, decimal taskQty, string processCode)
{
try
{
using (var conn = new SqlConnection(_connStr))
{
// 1. 校验工位布局状态(必须已标定)
var stationLayout = conn.QuerySingle<WorkStationLayout>(
"SELECT s.*, l.LayoutStatus FROM WorkStationLayout s " +
"LEFT JOIN ProductionLineLayout l ON s.LineCode = l.LineCode " +
"WHERE s.StationCode = @StationCode", new { StationCode = stationCode });
if (stationLayout == null)
{
LogHelper.Error($"工位{stationCode}不存在");
return false;
}
if (stationLayout.LayoutStatus != "已标定")
{
LogHelper.Error($"工位{stationCode}布局未标定,无法绑定任务");
return false;
}
// 2. 校验ERP工单有效性
var orderValid = conn.ExecuteScalar<int>(
"SELECT COUNT(1) FROM ERPAssemblyOrder WHERE OrderNo = @OrderNo AND OrderStatus = '待下发'",
new { OrderNo = orderNo }) > 0;
if (!orderValid)
{
LogHelper.Error($"ERP工单{orderNo}无效或已下发");
return false;
}
// 3. 更新工位任务绑定信息
conn.Execute(
"UPDATE WorkStationLayout SET BindOrderNo=@OrderNo, BindTaskQty=@TaskQty, " +
"ProcessCode=@ProcessCode, TaskStatus='待执行' WHERE StationCode=@StationCode",
new { OrderNo = orderNo, TaskQty = taskQty, ProcessCode = processCode, StationCode = stationCode });
// 4. 生成任务绑定指令(记录调度日志)
var command = new LayoutCommand
{
CommandId = Guid.NewGuid().ToString("N"),
SourceTerminal = "ERP_PLAN",
TargetTerminal = "MES_EXEC",
CommandType = "任务绑定",
LineCode = stationLayout.LineCode,
StationCode = stationCode,
CommandContent = JsonConvert.SerializeObject(new
{
OrderNo = orderNo,
TaskQty = taskQty,
ProcessCode = processCode,
BindTime = DateTime.Now
}),
CommandStatus = "已执行",
CreateTime = DateTime.Now,
ExecuteTime = DateTime.Now
};
// 5. 保存指令日志
SaveCommandLog(command);
LogHelper.Info($"工位{stationCode}成功绑定ERP工单{orderNo},任务数量:{taskQty}");
return true;
}
}
catch (Exception ex)
{
LogHelper.Error($"绑定工单{orderNo}到工位{stationCode}失败", ex);
return false;
}
}
/// <summary>
/// 调整工位任务数量
/// </summary>
public bool AdjustStationTaskQty(string stationCode, decimal newTaskQty)
{
try
{
using (var conn = new SqlConnection(_connStr))
{
// 1. 校验工位是否已绑定任务
var station = conn.QuerySingle<WorkStationLayout>(
"SELECT * FROM WorkStationLayout WHERE StationCode = @StationCode",
new { StationCode = stationCode });
if (station == null || string.IsNullOrEmpty(station.BindOrderNo))
{
LogHelper.Error($"工位{stationCode}未绑定任何任务");
return false;
}
// 2. 任务执行中不允许调整
if (station.TaskStatus == "执行中")
{
LogHelper.Error($"工位{stationCode}任务正在执行,无法调整数量");
return false;
}
// 3. 更新任务数量
conn.Execute(
"UPDATE WorkStationLayout SET BindTaskQty=@NewTaskQty WHERE StationCode=@StationCode",
new { NewTaskQty = newTaskQty, StationCode = stationCode });
// 4. 生成任务调整指令
var command = new LayoutCommand
{
CommandId = Guid.NewGuid().ToString("N"),
SourceTerminal = "MES_EXEC",
TargetTerminal = "MES_EXEC",
CommandType = "任务调整",
LineCode = station.LineCode,
StationCode = stationCode,
CommandContent = JsonConvert.SerializeObject(new
{
OldTaskQty = station.BindTaskQty,
NewTaskQty = newTaskQty,
AdjustTime = DateTime.Now
}),
CommandStatus = "已执行",
CreateTime = DateTime.Now,
ExecuteTime = DateTime.Now
};
SaveCommandLog(command);
LogHelper.Info($"工位{stationCode}任务数量调整为:{newTaskQty}");
return true;
}
}
catch (Exception ex)
{
LogHelper.Error($"调整工位{stationCode}任务数量失败", ex);
return false;
}
}
/// <summary>
/// 启动工位任务
/// </summary>
public bool StartStationTask(string stationCode)
{
return UpdateStationTaskStatus(stationCode, "执行中", "任务启动");
}
/// <summary>
/// 完成工位任务
/// </summary>
public bool CompleteStationTask(string stationCode)
{
return UpdateStationTaskStatus(stationCode, "已完成", "任务完成");
}
/// <summary>
/// 暂停工位任务
/// </summary>
public bool PauseStationTask(string stationCode)
{
return UpdateStationTaskStatus(stationCode, "暂停", "任务暂停");
}
/// <summary>
/// 更新工位任务状态
/// </summary>
private bool UpdateStationTaskStatus(string stationCode, string newStatus, string commandType)
{
try
{
using (var conn = new SqlConnection(_connStr))
{
// 1. 更新工位任务状态
conn.Execute(
"UPDATE WorkStationLayout SET TaskStatus=@NewStatus WHERE StationCode=@StationCode",
new { NewStatus = newStatus, StationCode = stationCode });
// 2. 生成状态变更指令
var station = conn.QuerySingle<WorkStationLayout>(
"SELECT * FROM WorkStationLayout WHERE StationCode = @StationCode",
new { StationCode = stationCode });
var command = new LayoutCommand
{
CommandId = Guid.NewGuid().ToString("N"),
SourceTerminal = "MES_EXEC",
TargetTerminal = "MES_EXEC",
CommandType = commandType,
LineCode = station.LineCode,
StationCode = stationCode,
CommandContent = JsonConvert.SerializeObject(new
{
OldStatus = station.TaskStatus,
NewStatus = newStatus,
UpdateTime = DateTime.Now
}),
CommandStatus = "已执行",
CreateTime = DateTime.Now,
ExecuteTime = DateTime.Now
};
SaveCommandLog(command);
LogHelper.Info($"工位{stationCode}任务状态更新为:{newStatus}");
return true;
}
}
catch (Exception ex)
{
LogHelper.Error($"更新工位{stationCode}任务状态失败", ex);
return false;
}
}
#endregion
#region 多指令调度:指令日志/执行/查询
/// <summary>
/// 保存指令日志
/// </summary>
private void SaveCommandLog(LayoutCommand command)
{
using (var conn = new SqlConnection(_connStr))
{
conn.Execute(
"INSERT INTO LayoutCommandLog (CommandId, SourceTerminal, TargetTerminal, CommandType, " +
"LineCode, StationCode, CommandContent, CommandStatus, CreateTime, ExecuteTime) " +
"VALUES (@CommandId, @SourceTerminal, @TargetTerminal, @CommandType, " +
"@LineCode, @StationCode, @CommandContent, @CommandStatus, @CreateTime, @ExecuteTime)",
command);
}
}
/// <summary>
/// 执行外部指令(如ERP下发/工艺配置)
/// </summary>
public bool ExecuteExternalCommand(LayoutCommand command)
{
try
{
command.CommandStatus = "执行中";
command.ExecuteTime = DateTime.Now;
// 根据指令类型执行不同逻辑
switch (command.CommandType)
{
case "工单下发":
var orderContent = JsonConvert.DeserializeObject<OrderDispatchContent>(command.CommandContent);
return BindOrderToStation(command.StationCode, orderContent.OrderNo, orderContent.TaskQty, orderContent.ProcessCode);
case "工艺绑定":
var processContent = JsonConvert.DeserializeObject<ProcessBindContent>(command.CommandContent);
using (var conn = new SqlConnection(_connStr))
{
conn.Execute(
"UPDATE WorkStationLayout SET ProcessCode=@ProcessCode WHERE StationCode=@StationCode",
new { ProcessCode = processContent.ProcessCode, StationCode = command.StationCode });
}
break;
case "布局重置":
using (var conn = new SqlConnection(_connStr))
{
conn.Execute(
"UPDATE ProductionLineLayout SET LayoutStatus='未标定' WHERE LineCode=@LineCode",
new { LineCode = command.LineCode });
}
break;
default:
LogHelper.Warn($"不支持的指令类型:{command.CommandType}");
command.CommandStatus = "执行失败";
SaveCommandLog(command);
return false;
}
command.CommandStatus = "已执行";
SaveCommandLog(command);
LogHelper.Info($"外部指令{command.CommandId}执行成功");
return true;
}
catch (Exception ex)
{
command.CommandStatus = "执行失败";
command.CommandContent += $"|执行失败原因:{ex.Message}";
SaveCommandLog(command);
LogHelper.Error($"执行外部指令{command.CommandId}失败", ex);
return false;
}
}
/// <summary>
/// 查询指令日志(适配WinForm展示)
/// </summary>
public DataTable QueryCommandLogs(string lineCode = "", string stationCode = "", string commandType = "")
{
var dt = new DataTable();
dt.Columns.Add("指令ID", typeof(string));
dt.Columns.Add("来源端", typeof(string));
dt.Columns.Add("目标端", typeof(string));
dt.Columns.Add("指令类型", typeof(string));
dt.Columns.Add("产线编码", typeof(string));
dt.Columns.Add("工位编码", typeof(string));
dt.Columns.Add("指令状态", typeof(string));
dt.Columns.Add("创建时间", typeof(DateTime));
dt.Columns.Add("执行时间", typeof(DateTime));
using (var conn = new SqlConnection(_connStr))
{
string sql = "SELECT CommandId, SourceTerminal, TargetTerminal, CommandType, " +
"LineCode, StationCode, CommandStatus, CreateTime, ExecuteTime " +
"FROM LayoutCommandLog WHERE 1=1 ";
var param = new DynamicParameters();
if (!string.IsNullOrEmpty(lineCode))
{
sql += "AND LineCode = @LineCode ";
param.Add("LineCode", lineCode);
}
if (!string.IsNullOrEmpty(stationCode))
{
sql += "AND StationCode = @StationCode ";
param.Add("StationCode", stationCode);
}
if (!string.IsNullOrEmpty(commandType))
{
sql += "AND CommandType = @CommandType ";
param.Add("CommandType", commandType);
}
sql += "ORDER BY CreateTime DESC";
var logs = conn.Query(sql, param);
foreach (var log in logs)
{
dt.Rows.Add(
log.CommandId,
log.SourceTerminal,
log.TargetTerminal,
log.CommandType,
log.LineCode,
log.StationCode,
log.CommandStatus,
log.CreateTime,
log.ExecuteTime ?? DBNull.Value
);
}
}
return dt;
}
#endregion
}
// 指令内容DTO
public class OrderDispatchContent
{
public string OrderNo { get; set; }
public decimal TaskQty { get; set; }
public string ProcessCode { get; set; }
}
public class ProcessBindContent
{
public string ProcessCode { get; set; }
public string ProcessName { get; set; }
}
4. 主界面组件(整合布局标定 + 任务绑定 + 指令调度)
csharp
运行
/// <summary>
/// 组装产线工位布局标定与任务调度主界面
/// </summary>
public partial class MainLayoutDispatchForm : Form
{
private readonly LineLayoutCalibrateControl _layoutControl;
private readonly StationTaskDispatchService _dispatchService;
private string _currentLineCode;
public MainLayoutDispatchForm()
{
InitializeComponent();
// 1. 初始化布局标定控件
_layoutControl = new LineLayoutCalibrateControl();
_layoutControl.Dock = DockStyle.Fill;
panelLayoutCanvas.Controls.Add(_layoutControl);
// 2. 绑定选中事件
_layoutControl.SectionSelected += OnSectionSelected;
_layoutControl.StationSelected += OnStationSelected;
// 3. 初始化调度服务
_dispatchService = new StationTaskDispatchService();
// 4. 加载产线列表
LoadLineList();
}
/// <summary>
/// 加载产线列表
/// </summary>
private void LoadLineList()
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
var lines = conn.Query<dynamic>(
"SELECT LineCode, LineName FROM ProductionLineLayout ORDER BY LineCode");
cboLineCode.DataSource = lines;
cboLineCode.DisplayMember = "LineName";
cboLineCode.ValueMember = "LineCode";
}
}
/// <summary>
/// 产线选择变更
/// </summary>
private void cboLineCode_SelectedIndexChanged(object sender, EventArgs e)
{
if (cboLineCode.SelectedValue == null) return;
_currentLineCode = cboLineCode.SelectedValue.ToString();
// 加载产线布局
_layoutControl.LoadLineLayout(_currentLineCode);
// 加载指令日志
RefreshCommandLogs();
}
/// <summary>
/// 工段选中事件处理
/// </summary>
private void OnSectionSelected(object sender, SectionSelectedEventArgs e)
{
// 显示工段信息
lblSectionInfo.Text = $"当前选中工段:{e.SelectedSection.SectionName}({e.SelectedSection.SectionCode})";
// 清空工位信息
lblStationInfo.Text = "未选中工位";
// 禁用工位操作按钮
btnBindTask.Enabled = false;
btnStartTask.Enabled = false;
btnPauseTask.Enabled = false;
btnCompleteTask.Enabled = false;
}
/// <summary>
/// 工位选中事件处理
/// </summary>
private void OnStationSelected(object sender, StationSelectedEventArgs e)
{
// 显示工位信息
lblStationInfo.Text = $"当前选中工位:{e.SelectedStation.StationName}({e.SelectedStation.StationCode})\n" +
$"绑定工单:{e.SelectedStation.BindOrderNo}\n" +
$"任务数量:{e.SelectedStation.BindTaskQty}\n" +
$"任务状态:{e.SelectedStation.TaskStatus}";
// 填充任务绑定表单
txtStationCode.Text = e.SelectedStation.StationCode;
txtProcessCode.Text = e.SelectedStation.ProcessCode;
txtOrderNo.Text = e.SelectedStation.BindOrderNo;
numTaskQty.Value = (decimal)e.SelectedStation.BindTaskQty;
// 启用工位操作按钮
btnBindTask.Enabled = true;
btnStartTask.Enabled = e.SelectedStation.TaskStatus == "待执行";
btnPauseTask.Enabled = e.SelectedStation.TaskStatus == "执行中";
btnCompleteTask.Enabled = e.SelectedStation.TaskStatus == "执行中";
}
/// <summary>
/// 保存布局按钮
/// </summary>
private void btnSaveLayout_Click(object sender, EventArgs e)
{
var result = _layoutControl.SaveLineLayout();
if (result)
{
MessageBox.Show("产线布局标定结果保存成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
/// <summary>
/// 绑定任务按钮
/// </summary>
private void btnBindTask_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(txtStationCode.Text) || string.IsNullOrEmpty(txtOrderNo.Text))
{
MessageBox.Show("请填写工位编码和ERP工单编号");
return;
}
var result = _dispatchService.BindOrderToStation(
txtStationCode.Text,
txtOrderNo.Text,
numTaskQty.Value,
txtProcessCode.Text);
if (result)
{
MessageBox.Show("工位任务绑定成功!");
// 刷新布局
_layoutControl.LoadLineLayout(_currentLineCode);
// 刷新指令日志
RefreshCommandLogs();
}
else
{
MessageBox.Show("任务绑定失败", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 启动任务按钮
/// </summary>
private void btnStartTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.StartStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务启动成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
/// <summary>
/// 暂停任务按钮
/// </summary>
private void btnPauseTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.PauseStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务暂停成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
/// <summary>
/// 完成任务按钮
/// </summary>
private void btnCompleteTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.CompleteStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务完成成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
/// <summary>
/// 刷新指令日志
/// </summary>
private void RefreshCommandLogs()
{
var dt = _dispatchService.QueryCommandLogs(_currentLineCode);
dgvCommandLogs.DataSource = dt;
}
}
三、 数据库表结构(核心)
sql
-- 产线布局表
CREATE TABLE ProductionLineLayout (
LineCode VARCHAR(50) PRIMARY KEY,
LineName VARCHAR(100) NOT NULL,
CanvasWidth INT DEFAULT 1200,
CanvasHeight INT DEFAULT 600,
LayoutStatus VARCHAR(20) DEFAULT '未标定', -- 未标定/已标定/已投产
LastCalibrateTime DATETIME,
Calibrator VARCHAR(50)
);
-- 工段布局表
CREATE TABLE WorkSectionLayout (
SectionCode VARCHAR(50) PRIMARY KEY,
SectionName VARCHAR(100) NOT NULL,
LineCode VARCHAR(50) NOT NULL,
X INT NOT NULL DEFAULT 0,
Y INT NOT NULL DEFAULT 0,
Width INT NOT NULL DEFAULT 200,
Height INT NOT NULL DEFAULT 300,
SortNo INT NOT NULL DEFAULT 0,
FOREIGN KEY (LineCode) REFERENCES ProductionLineLayout(LineCode)
);
-- 工位布局表(核心)
CREATE TABLE WorkStationLayout (
StationCode VARCHAR(50) PRIMARY KEY,
StationName VARCHAR(100) NOT NULL,
SectionCode VARCHAR(50) NOT NULL,
X INT NOT NULL DEFAULT 0,
Y INT NOT NULL DEFAULT 0,
Width INT NOT NULL DEFAULT 80,
Height INT NOT NULL DEFAULT 60,
StationType VARCHAR(20) DEFAULT '加工', -- 加工/装配/检测
ProcessCode VARCHAR(50),
BindOrderNo VARCHAR(50),
BindTaskQty DECIMAL(18,2) DEFAULT 0,
TaskStatus VARCHAR(20) DEFAULT '待执行', -- 待执行/执行中/已完成/暂停
PreStationCodes VARCHAR(200), -- 上工序工位编码
FOREIGN KEY (SectionCode) REFERENCES WorkSectionLayout(SectionCode)
);
-- 布局指令日志表
CREATE TABLE LayoutCommandLog (
CommandId VARCHAR(50) PRIMARY KEY,
SourceTerminal VARCHAR(50) NOT NULL, -- ERP_PLAN/MES_EXEC/PROCESS_CONFIG/LAYOUT_CALIB
TargetTerminal VARCHAR(50) NOT NULL,
CommandType VARCHAR(50) NOT NULL, -- 布局保存/任务绑定/任务调整/工艺绑定/工单下发
LineCode VARCHAR(50) NOT NULL,
StationCode VARCHAR(50),
CommandContent NVARCHAR(MAX),
CommandStatus VARCHAR(20) DEFAULT '待执行', -- 待执行/执行中/已执行/执行失败
CreateTime DATETIME DEFAULT GETDATE(),
ExecuteTime DATETIME
);
-- ERP组装工单表(简化)
CREATE TABLE ERPAssemblyOrder (
OrderNo VARCHAR(50) PRIMARY KEY,
ProductCode VARCHAR(50) NOT NULL,
AssemblyQty DECIMAL(18,2) NOT NULL,
PlanStartTime DATETIME NOT NULL,
PlanEndTime DATETIME NOT NULL,
OrderStatus VARCHAR(20) DEFAULT '待下发' -- 待下发/已下发/执行中/已完成
);
四、 核心功能使用流程
- 产线布局标定:
- 选择产线 → 拖拽工段 / 工位调整坐标 → 绘制工位连接关系 → 保存布局(状态变为 “已标定”);
- 工位任务绑定:
- 选中工位 → 输入 ERP 工单编号、任务数量、工艺编码 → 点击 “绑定任务” → 工位显示绑定信息;
- 任务执行调度:
- 绑定任务后点击 “启动任务”(状态变为执行中)→ 执行完成点击 “完成任务”(状态变为已完成);
- 多指令监控:
- 指令日志表格实时显示所有操作指令(来源端、类型、状态)→ 支持产线 / 工位 / 指令类型筛选。
总结
- 核心组件:
LineLayoutCalibrateControl实现产线 - 工段 - 工位的可视化拖拽标定,支持布局缩放、连接关系绘制; - 任务调度:
StationTaskDispatchService实现 ERP 工单到工位的精准绑定,支持任务启动 / 暂停 / 完成等全生命周期操作; - 多指令适配:通过
LayoutCommand模型统一管理各端指令,记录指令执行日志,支持 ERP 计划端、MES 执行端等多端协同。
该组件可直接集成到 WinForm 中间件项目,适配组装产线的工位布局标定与任务调度需求,支持二次扩展(如 IoT 数据对接、AI 任务调度等)。
更多推荐


所有评论(0)