面向 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 '待下发' -- 待下发/已下发/执行中/已完成
);

四、 核心功能使用流程

  1. 产线布局标定
    • 选择产线 → 拖拽工段 / 工位调整坐标 → 绘制工位连接关系 → 保存布局(状态变为 “已标定”);
  2. 工位任务绑定
    • 选中工位 → 输入 ERP 工单编号、任务数量、工艺编码 → 点击 “绑定任务” → 工位显示绑定信息;
  3. 任务执行调度
    • 绑定任务后点击 “启动任务”(状态变为执行中)→ 执行完成点击 “完成任务”(状态变为已完成);
  4. 多指令监控
    • 指令日志表格实时显示所有操作指令(来源端、类型、状态)→ 支持产线 / 工位 / 指令类型筛选。

总结

  1. 核心组件LineLayoutCalibrateControl 实现产线 - 工段 - 工位的可视化拖拽标定,支持布局缩放、连接关系绘制;
  2. 任务调度StationTaskDispatchService 实现 ERP 工单到工位的精准绑定,支持任务启动 / 暂停 / 完成等全生命周期操作;
  3. 多指令适配:通过LayoutCommand模型统一管理各端指令,记录指令执行日志,支持 ERP 计划端、MES 执行端等多端协同。

该组件可直接集成到 WinForm 中间件项目,适配组装产线的工位布局标定与任务调度需求,支持二次扩展(如 IoT 数据对接、AI 任务调度等)。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐