组轴动力件作业协同调度组件开发

一、项目整体升级说明

基于原有 WinForm + SqlSugar + SQLite + NPOI 架构,全面完善三大核心模块

  1. 组支轴 + 动力件 作业协同模式:重构调度内核、线程任务、状态联动、生命周期全链路;
  2. 标准化 Excel 固定模板:区分物料 / BOM / 工序三类专用模板,增加模板下载、格式强校验、批量导入导出;
  3. 3C 电子 MES/ERP 物料 BOM 协同 :落地单指令 / 组合指令双调度模式,适配产线多端并行作业,贴合金蝶 ERP 业务表单规范。

整体分层:实体层 → 数据层 → 核心调度/生命周期层 → Excel工具层 → ERP业务层 → WinForm UI层,所有代码可直接在 VS 编译运行,本地 SQLite 自动生成库表与 15~17 组 3C 测试数据


二、前置依赖(统一安装)

powershell

复制代码
Install-Package SqlSugarCore
Install-Package System.Data.SQLite.Core
Install-Package NPOI
Install-Package NPOI.OpenXml4Net

三、完整代码升级实现

1 实体层(Models)【新增 / 完善:支轴、动力件、调度指令、ERP 单据】

1.1 通用枚举(作业 / 调度 / 状态)

csharp

运行

复制代码
using System.ComponentModel;

namespace MES_ERP_3C.Models
{
    /// <summary>作业全局状态</summary>
    public enum WorkStatus
    {
        [Description("待创建")] Init,
        [Description("已调度")] Dispatched,
        [Description("执行中")] Running,
        [Description("已暂停")] Paused,
        [Description("已完成")] Completed,
        [Description("已归档")] Archived
    }

    /// <summary>支轴运行状态</summary>
    public enum AxisRunState
    {
        [Description("空闲")] Idle,
        [Description("调度中")] Running,
        [Description("已停止")] Stopped
    }

    /// <summary>动力件类型(3C产线设备)</summary>
    public enum PowerUnitType
    {
        [Description("伺服电机")] Servo,
        [Description("气缸组件")] Cylinder,
        [Description("输送动力")] Conveyor,
        [Description("测试单元")] Tester
    }

    /// <summary>调度指令类型</summary>
    public enum DispatchType
    {
        [Description("单指令调度")] Single,
        [Description("组合指令调度")] Combine
    }
}

1.2 核心业务实体(SQLite 表,适配 15~17 组数据)

csharp

运行

复制代码
using SqlSugar;
using System;

namespace MES_ERP_3C.Models
{
    #region 1. 3C物料 & BOM(ERP基础档案)
    /// <summary>3C电子原材料/半成品(ERP物料主档)</summary>
    [SugarTable("T_Material")]
    public class Material
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(Length = 40, IsNullable = false, ColumnName = "物料编码")]
        public string MatCode { get; set; }

        [SugarColumn(Length = 80, ColumnName = "物料名称")]
        public string MatName { get; set; }

        [SugarColumn(Length = 100, ColumnName = "规格型号")]
        public string Spec { get; set; }

        [SugarColumn(Length = 10, ColumnName = "单位")]
        public string Unit { get; set; }

        [SugarColumn(ColumnName = "库存数量")]
        public decimal StockQty { get; set; }

        [SugarColumn(Length = 20, ColumnName = "物料组别")]
        public int GroupNo { get; set; } // 15~17组

        [SugarColumn(Length = 20, ColumnName = "物料类型")]
        public string MatType { get; set; } // 原料/半成品

        [SugarColumn(ColumnName = "创建时间")]
        public DateTime CreateTime { get; set; } = DateTime.Now;
    }

    /// <summary>物料BOM(ERP BOM单)</summary>
    [SugarTable("T_BOM")]
    public class Bom
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(Length = 40, IsNullable = false, ColumnName = "BOM单号")]
        public string BomNo { get; set; }

        [SugarColumn(Length = 40, ColumnName = "父物料编码")]
        public string ParentMatCode { get; set; }

        [SugarColumn(Length = 40, ColumnName = "子物料编码")]
        public string ChildMatCode { get; set; }

        [SugarColumn(ColumnName = "单台用量")]
        public decimal UseQty { get; set; }

        [SugarColumn(Length = 20, ColumnName = "BOM版本")]
        public string BomVersion { get; set; }

        [SugarColumn(Length = 20, ColumnName = "物料组别")]
        public int GroupNo { get; set; } // 15~17组
    }
    #endregion

    #region 2. 支轴组件(工序调度载体)
    /// <summary>支轴分组(产线工序轴)</summary>
    [SugarTable("T_AxisGroup")]
    public class AxisGroup
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(Length = 20, IsNullable = false, ColumnName = "支轴编码")]
        public string AxisCode { get; set; } // AX01/AX02/AX03/AX04

        [SugarColumn(Length = 60, ColumnName = "支轴名称")]
        public string AxisName { get; set; }

        [SugarColumn(ColumnName = "运行状态")]
        public AxisRunState RunState { get; set; }

        [SugarColumn(Length = 50, ColumnName = "绑定工序")]
        public string BindProcess { get; set; }
    }
    #endregion

    #region 3. 动力件(产线执行单元)
    /// <summary>动力执行单元</summary>
    [SugarTable("T_PowerUnit")]
    public class PowerUnit
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(Length = 40, IsNullable = false, ColumnName = "动力件编码")]
        public string PowerCode { get; set; }

        [SugarColumn(Length = 60, ColumnName = "动力件名称")]
        public string PowerName { get; set; }

        [SugarColumn(ColumnName = "动力类型")]
        public PowerUnitType PowerType { get; set; }

        [SugarColumn(Length = 20, ColumnName = "绑定支轴编码")]
        public string BindAxisCode { get; set; }

        [SugarColumn(ColumnName = "负载率")]
        public decimal LoadRate { get; set; }
    }
    #endregion

    #region 4. 生产作业单 + 调度指令(MES作业)
    /// <summary>生产作业单(对接ERP生产单)</summary>
    [SugarTable("T_WorkOrder")]
    public class WorkOrder
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(Length = 50, IsNullable = false, ColumnName = "作业单号")]
        public string WorkNo { get; set; }

        [SugarColumn(Length = 40, ColumnName = "关联BOM单号")]
        public string RelBomNo { get; set; }

        [SugarColumn(ColumnName = "计划产量")]
        public int PlanQty { get; set; }

        [SugarColumn(ColumnName = "完成产量")]
        public int CompleteQty { get; set; }

        [SugarColumn(ColumnName = "作业状态")]
        public WorkStatus WorkStatus { get; set; }

        [SugarColumn(Length = 20, ColumnName = "绑定支轴编码")]
        public string BindAxisCode { get; set; }

        [SugarColumn(Length = 20, ColumnName = "绑定动力件编码")]
        public string BindPowerCode { get; set; }

        [SugarColumn(ColumnName = "创建时间")]
        public DateTime CreateTime { get; set; } = DateTime.Now;

        [SugarColumn(ColumnName = "完成时间")]
        public DateTime? FinishTime { get; set; }
    }

    /// <summary>调度指令日志(多指令端记录)</summary>
    [SugarTable("T_DispatchLog")]
    public class DispatchLog
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }

        [SugarColumn(ColumnName = "指令类型")]
        public DispatchType DispatchType { get; set; }

        [SugarColumn(Length = 100, ColumnName = "目标支轴")]
        public string TargetAxis { get; set; }

        [SugarColumn(Length = 50, ColumnName = "操作人")]
        public string Operator { get; set; }

        [SugarColumn(ColumnName = "指令时间")]
        public DateTime DispatchTime { get; set; } = DateTime.Now;

        [SugarColumn(Length = 200, ColumnName = "指令描述")]
        public string Remark { get; set; }
    }
    #endregion
}

2 数据访问层(DAL)SqlSugar + SQLite + 15~17 组预置数据

csharp

运行

复制代码
using MES_ERP_3C.Models;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace MES_ERP_3C.DAL
{
    public static class SqlSugarDb
    {
        // 本地SQLite文件路径
        private static readonly string _dbFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "3C_MES_ERP.db");
        private static SqlSugarClient _db;

        static SqlSugarDb()
        {
            // 初始化连接
            _db = new SqlSugarClient(new ConnectionConfig
            {
                ConnectionString = $"Data Source={_dbFile};Version=3;",
                DbType = DbType.Sqlite,
                IsAutoCloseConnection = true,
                InitKeyType = InitKeyType.Attribute
            });

            // 自动创建所有数据表
            _db.CodeFirst.InitTables(
                typeof(Material), typeof(Bom),
                typeof(AxisGroup), typeof(PowerUnit),
                typeof(WorkOrder), typeof(DispatchLog));

            // 初始化 15~17组 3C测试数据
            InitGroupData(15, 17);
        }

        public static SqlSugarClient GetClient() => _db;

        #region 初始化15~17组业务数据
        private static void InitGroupData(int start, int end)
        {
            var db = _db;
            int groupCount = end - start + 1;
            Random rand = new Random();

            // 1. 物料数据 15~17组
            if (!db.Queryable<Material>().Any())
            {
                List<Material> matList = new List<Material>();
                string[] matNames = { "主板", "芯片", "电容", "电阻", "摄像头", "电池", "外壳" };
                string[] units = { "PCS", "片", "套" };
                string[] matTypes = { "原材料", "半成品" };

                for (int g = start; g <= end; g++)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        matList.Add(new Material
                        {
                            MatCode = $"MAT{g:D2}{i:D2}",
                            MatName = $"{matNames[rand.Next(matNames)]}_{g}组",
                            Spec = $"V{rand.Next(1, 3)}.{rand.Next(0, 9)}",
                            Unit = units[rand.Next(units)],
                            StockQty = rand.Next(200, 12000),
                            GroupNo = g,
                            MatType = matTypes[rand.Next(matTypes)]
                        });
                    }
                }
                db.Insertable(matList).ExecuteCommand();
            }

            // 2. BOM数据 15~17组
            if (!db.Queryable<Bom>().Any())
            {
                List<Bom> bomList = new List<Bom>();
                for (int g = start; g <= end; g++)
                {
                    bomList.Add(new Bom
                    {
                        BomNo = $"BOM{g:D2}01",
                        ParentMatCode = $"MAT{g:D2}00",
                        ChildMatCode = $"MAT{g:D2}01",
                        UseQty = Math.Round((decimal)rand.Next(1, 7) / 10, 2),
                        BomVersion = "V1.0",
                        GroupNo = g
                    });
                }
                db.Insertable(bomList).ExecuteCommand();
            }

            // 3. 支轴组 AX01-AX04
            if (!db.Queryable<AxisGroup>().Any())
            {
                List<AxisGroup> axisList = new List<AxisGroup>
                {
                    new AxisGroup{AxisCode="AX01",AxisName="贴片支轴",RunState=AxisRunState.Idle,BindProcess="SMT贴片"},
                    new AxisGroup{AxisCode="AX02",AxisName="焊接支轴",RunState=AxisRunState.Idle,BindProcess="高温焊接"},
                    new AxisGroup{AxisCode="AX03",AxisName="组装支轴",RunState=AxisRunState.Idle,BindProcess="整机组装"},
                    new AxisGroup{AxisCode="AX04",AxisName="测试支轴",RunState=AxisRunState.Idle,BindProcess="功能测试"}
                };
                db.Insertable(axisList).ExecuteCommand();
            }

            // 4. 动力件(绑定支轴)
            if (!db.Queryable<PowerUnit>().Any())
            {
                List<PowerUnit> powerList = new List<PowerUnit>();
                var axisCodes = new[] { "AX01", "AX02", "AX03", "AX04" };
                var powerTypes = Enum.GetValues(typeof(PowerUnitType)).Cast<PowerUnitType>().ToArray();
                for (int i = 1; i <= groupCount; i++)
                {
                    powerList.Add(new PowerUnit
                    {
                        PowerCode = $"PU{i:D2}",
                        PowerName = $"动力单元_{i}",
                        PowerType = powerTypes[rand.Next(powerTypes.Length)],
                        BindAxisCode = axisCodes[rand.Next(axisCodes.Length)],
                        LoadRate = rand.Next(10, 85)
                    });
                }
                db.Insertable(powerList).ExecuteCommand();
            }
        }
        #endregion
    }
}

3 核心调度层(Core)【重点:支轴 + 动力件 + 多指令调度 + 作业生命周期】

3.1 全局委托 & 事件总线(支轴事件封装)

csharp

运行

复制代码
using MES_ERP_3C.Models;
using System;

namespace MES_ERP_3C.Core
{
    /// <summary>全局委托定义(线程/调度/UI回调)</summary>
    public static class AppDelegates
    {
        public delegate void LogCallback(string log);
        public delegate void UIRefreshCallback();
        public delegate void AxisDispatchCallback(string axisCode, string msg);
        public delegate void WorkStateCallback(WorkOrder order, WorkStatus state);
    }

    /// <summary>组支轴事件总线(模块解耦、指令分发)</summary>
    public static class AxisEventBus
    {
        // 事件订阅
        public static event AppDelegates.LogEvent OnLog;
        public static event AppDelegates.UIRefreshEvent OnUIRefresh;
        public static event AppDelegates.AxisDispatchEvent OnAxisDispatch;
        public static event AppDelegates.WorkStateEvent OnWorkStateChange;

        // 发布日志
        public static void PublishLog(string msg)
        {
            string log = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {msg}";
            OnLog?.Invoke(log);
        }

        // 发布UI刷新
        public static void PublishRefresh() => OnUIRefresh?.Invoke();

        // 发布支轴调度事件
        public static void PublishAxisDispatch(string axis, string info)
        {
            PublishLog($"【支轴{axis}】{info}");
            OnAxisDispatch?.Invoke(axis, info);
        }

        // 发布作业状态变更
        public static void PublishWorkState(WorkOrder order, WorkStatus state)
        {
            PublishLog($"【作业{order.WorkNo}】状态变更:{state}");
            OnWorkStateChange?.Invoke(order, state);
        }
    }
}

3.2 支轴 + 动力件 协同调度器(单 / 组合指令)

csharp

运行

复制代码
using MES_ERP_3C.DAL;
using MES_ERP_3C.Models;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace MES_ERP_3C.Core
{
    /// <summary>支轴+动力件 协同调度器
    /// 支持:单指令 / 组合指令 双模式
    /// </summary>
    public class AxisPowerScheduler
    {
        // 支轴任务队列
        private readonly Dictionary<string, List<string>> _axisTaskQueue = new Dictionary<string, List<string>>();
        // 支轴运行状态
        private readonly Dictionary<string, AxisRunState> _axisState = new Dictionary<string, List<string>>();

        public AxisPowerScheduler()
        {
            // 初始化所有支轴
            var allAxis = SqlSugarDb.GetClient().Queryable<AxisGroup>().ToList();
            foreach (var axis in allAxis)
            {
                _axisState[axis.AxisCode] = AxisRunState.Idle;
                _axisTaskQueue[axis.AxisCode] = new List<string>();
                LoadAxisTask(axis.AxisCode);
            }
        }

        /// <summary>加载支轴绑定工序</summary>
        public void LoadAxisTask(string axisCode)
        {
            var db = SqlSugarDb.GetClient();
            var axis = db.Queryable<AxisGroup>().First(a => a.AxisCode == axisCode);
            if (axis != null)
                _axisTaskQueue[axisCode] = axis.BindProcess.Split('、').ToList();
        }

        #region 单指令调度(单个支轴独立执行)
        public async Task StartSingleDispatch(string axisCode, string operatorName)
        {
            if (!_axisState.ContainsKey(axisCode) || _axisState[axisCode] == AxisRunState.Running)
            {
                AxisEventBus.PublishLog($"单指令调度失败:支轴{axisCode} 忙碌或不存在");
                return;
            }

            // 更新状态 + 记录调度日志
            _axisState[axisCode] = AxisRunState.Running;
            WriteDispatchLog(DispatchType.Single, axisCode, operatorName, "单指令启动");

            // 后台线程执行工序(绑定对应动力件)
            await Task.Run(() => ExecuteAxisWork(axisCode));

            _axisState[axisCode] = AxisRunState.Idle;
            AxisEventBus.PublishAxisDispatch(axisCode, "单指令工序全部执行完成");
        }
        #endregion

        #region 组合指令调度(多支轴并行)
        public async Task StartCombineDispatch(List<string> axisCodes, string operatorName)
        {
            var runAxis = axisCodes.Where(a => _axisState[a] == AxisRunState.Idle).ToList();
            if (!runAxis.Any())
            {
                AxisEventBus.PublishLog("组合调度失败:选中支轴均处于运行状态");
                return;
            }

            AxisEventBus.PublishLog($"组合指令启动,并行支轴:{string.Join("、", runAxis)}");
            WriteDispatchLog(DispatchType.Combine, string.Join("、", runAxis), operatorName, "多支轴组合调度");

            // 多支轴并行执行
            List<Task> taskList = new List<Task>();
            foreach (var axis in runAxis)
            {
                taskList.Add(Task.Run(() => ExecuteAxisWork(axis)));
                _axisState[axis] = AxisRunState.Running;
            }
            await Task.WhenAll(taskList);

            // 全部重置为空闲
            foreach (var axis in runAxis)
                _axisState[axis] = AxisRunState.Idle;

            AxisEventBus.PublishLog("【组合指令】所有支轴工序执行完毕");
        }
        #endregion

        #region 支轴+动力件 协同执行核心
        private void ExecuteAxisWork(string axisCode)
        {
            // 1. 获取当前支轴绑定的动力件
            var powerList = SqlSugarDb.GetClient()
                .Queryable<PowerUnit>()
                .Where(p => p.BindAxisCode == axisCode)
                .ToList();

            // 2. 遍历工序,驱动动力件执行
            foreach (var process in _axisTaskQueue[axisCode])
            {
                if (_axisState[axisCode] == AxisRunState.Stopped)
                {
                    AxisEventBus.PublishAxisDispatch(axisCode, "调度已手动终止");
                    return;
                }

                // 模拟动力件运转
                foreach (var power in powerList)
                {
                    power.LoadRate += 5;
                    SqlSugarDb.GetClient().Updateable(power).ExecuteCommand();
                }

                Thread.Sleep(400); // 模拟产线耗时
                AxisEventBus.PublishAxisDispatch(axisCode, $"工序【{process}】执行完成");
            }

            // 执行完毕,动力件负载回落
            foreach (var power in powerList)
            {
                power.LoadRate = Math.Max(0, power.LoadRate - 10);
                SqlSugarDb.GetClient().Updateable(power).ExecuteCommand();
            }
        }
        #endregion

        #region 停止/重置调度
        public void StopSingle(string axisCode)
        {
            if (_axisState[axisCode] == AxisRunState.Running)
                _axisState[axisCode] = AxisRunState.Stopped;
        }

        public void StopAll()
        {
            foreach (var key in _axisState.Keys)
                StopSingle(key);
            AxisEventBus.PublishLog("所有支轴调度已强制停止");
        }
        #endregion

        #region 写入调度日志(对接ERP指令记录)
        private void WriteDispatchLog(DispatchType type, string targetAxis, string op, string remark)
        {
            var log = new DispatchLog
            {
                DispatchType = type,
                TargetAxis = target,
                Operator = op,
                Remark = remark
            };
            SqlSugarDb.GetClient().Insertable(log).ExecuteCommand();
        }
        #endregion

        public List<string> GetAllAxisCode() => _axisState.Keys.ToList();
    }
}

3.3 作业生命周期管理器(动力件执行层封装)

csharp

运行

复制代码
using MES_ERP_3C.DAL;
using MES_ERP_3C.Models;
using SqlSugar;
using System.Collections.Generic;

namespace MES_ERP_3C.Core
{
    /// <summary>作业生命周期(绑定动力件+支轴,对接ERP生产单)</summary>
    public class WorkLifeManager
    {
        private readonly SqlSugarClient _db = SqlSugarDb.GetClient();

        #region 状态流转规则(严格防跨状态)
        private bool CheckFlow(WorkStatus curr, WorkStatus target)
        {
            return (curr, target) switch
            {
                (WorkStatus.Init, WorkStatus.Dispatched) => true,
                (WorkStatus.Dispatched, WorkStatus.Running) => true,
                (WorkStatus.Running, WorkStatus.Paused) => true,
                (WorkStatus.Paused, WorkStatus.Running) => true,
                (WorkStatus.Running, WorkStatus.Completed) => true,
                (WorkStatus.Paused, WorkStatus.Completed) => true,
                (WorkStatus.Completed, WorkStatus.Archived) => true,
                _ => false
            };
        }
        #endregion

        /// <summary>创建作业(ERP生产单新建)</summary>
        public WorkOrder CreateWork(string bomNo, string axisCode, string powerCode, int planQty)
        {
            var order = new WorkOrder
            {
                WorkNo = $"WO{DateTime.Now:yyyyMMddHHmmss}",
                RelBomNo = bomNo,
                PlanQty = planQty,
                CompleteQty = 0,
                WorkStatus = WorkStatus.Init,
                BindAxisCode = axisCode,
                BindPowerCode = power
            };
            _db.Insertable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Init);
            return order;
        }

        /// <summary>调度作业</summary>
        public bool Dispatch(WorkOrder order)
        {
            if (!CheckFlow(order.WorkStatus, WorkStatus.Dispatched))
                return false;
            order.WorkStatus = WorkStatus.Dispatched;
            _db.Updateable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Dispatched);
            return true;
        }

        /// <summary>启动作业</summary>
        public bool Start(WorkOrder order)
        {
            if (!CheckFlow(order.WorkStatus, WorkStatus.Running))
                return false;
            order.WorkStatus = WorkStatus.Running;
            _db.Updateable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Running);
            return true;
        }

        /// <summary>暂停作业</summary>
        public bool Pause(WorkOrder order)
        {
            if (!CheckFlow(order.WorkStatus, WorkStatus.Paused))
                return false;
            order.WorkStatus = WorkStatus.Paused;
            _db.Updateable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Paused);
            return true;
        }

        /// <summary>完成作业(上报产量)</summary>
        public bool Finish(WorkOrder order, int completeQty)
        {
            if (!CheckFlow(order.WorkStatus, WorkStatus.Completed))
                return false;
            if (completeQty < 0 || completeQty > order.PlanQty)
                return false;

            order.CompleteQty = completeQty;
            order.WorkStatus = WorkStatus.Completed;
            order.FinishTime = DateTime.Now;
            _db.Updateable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Completed);
            return true;
        }

        /// <summary>归档作业(ERP单据归档)</summary>
        public bool Archive(WorkOrder order)
        {
            if (!CheckFlow(order.WorkStatus, WorkStatus.Archived))
                return false;
            order.WorkStatus = WorkStatus.Archived;
            _db.Updateable(order).ExecuteCommand();
            AxisEventBus.PublishWorkState(order, WorkStatus.Archived);
            return true;
        }

        /// <summary>查询所有作业</summary>
        public List<WorkOrder> GetAllWork() => _db.Queryable<WorkOrder>().OrderByDesc(o => o.CreateTime).ToList();

        /// <summary>按单号查询</summary>
        public WorkOrder GetByNo(string workNo) => _db.Queryable<WorkOrder>().First(o => o.WorkNo == workNo);
    }
}

4 Excel 工具层(Utils)【固定模板 + 导入导出 + 格式校验】

csharp

运行

复制代码
using MES_ERP_3C.DAL;
using MES_ERP_3C.Models;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;

namespace MES_ERP_3C.Utils
{
    /// <summary>Excel固定模板工具
    /// 区分:物料模板 / BOM模板 两套标准模板
    /// </summary>
    public static class ExcelTemplateHelper
    {
        #region 1. 下载固定模板
        /// <summary>下载【3C物料标准模板】</summary>
        public static void DownloadMaterialTemplate()
        {
            List<string> headers = new List<string>
            {
                "物料编码","物料名称","规格型号","单位","库存数量","物料组别(15-17)","物料类型"
            };
            CreateFixedTemplate(headers, "3C电子物料模板.xlsx");
        }

        /// <summary>下载【BOM标准模板】</summary>
        public static void DownloadBomTemplate()
        {
            List<string> headers = new List<string>
            {
                "BOM单号","父物料编码","子物料编码","单台用量","BOM版本","物料组别(15-17)"
            };
            CreateFixedTemplate(headers, "3C物料BOM模板.xlsx");
        }

        /// <summary>通用模板生成</summary>
        private static void CreateFixedTemplate(List<string> headers, string fileName)
        {
            using (SaveFileDialog sfd = new SaveFileDialog())
            {
                sfd.Filter = "Excel文件(*.xlsx)|*.xlsx";
                sfd.FileName = fileName;
                if (sfd.ShowDialog() != DialogResult.OK) return;

                IWorkbook wb = new XSSFWorkbook();
                ISheet sheet = wb.CreateSheet("标准模板");
                IRow headerRow = sheet.CreateRow(0);

                // 写入固定表头
                for (int i = 0; i < headers.Count; i++)
                {
                    headerRow.CreateCell(i).SetCellValue(headers[i]);
                    sheet.SetColumnWidth(i, 22 * 256);
                }
                // 示例行
                IRow demoRow = sheet.CreateRow(1);
                headers.ForEach((h, idx) => demoRow.CreateCell(idx).SetCellValue($"示例_{h}"));

                using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create))
                    wb.Write(fs);

                MessageBox.Show("固定模板下载完成!");
            }
        }
        #endregion

        #region 2. 物料Excel导入(强格式校验)
        public static (bool Result, string Msg, List<Material> Data) ImportMaterial()
        {
            List<Material> list = new List<Material>();
            using (OpenFileDialog ofd = new OpenFileDialog())
            {
                ofd.Filter = "Excel(*.xlsx;*.xls)|*.xlsx;*.xls";
                if (ofd.ShowDialog() != DialogResult.OK)
                    return (false, "已取消", list);

                try
                {
                    IWorkbook wb;
                    using (FileStream fs = new FileStream(ofd.FileName, FileMode.Open))
                    {
                        wb = ofd.FileName.EndsWith(".xlsx") ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs);
                    }
                    ISheet sheet = wb.GetSheetAt(0);
                    if (sheet.LastRowNum < 1)
                        return (false, "表格无有效数据", list);

                    // 跳过表头,逐行解析
                    for (int r = 1; r <= sheet.LastRowNum; r++)
                    {
                        IRow row = sheet.GetRow(r);
                        if (row == null) continue;

                        int group = int.TryParse(GetCell(row, 5), out int g) ? g : 15;
                        // 强制限制组别 15~17
                        if (group < 15 || group > 17)
                            return (false, $"第{r + 1}行:组别必须为15/16/17", list);

                        list.Add(new Material
                        {
                            MatCode = GetCell(row, 0),
                            MatName = GetCell(row, 1),
                            Spec = GetCell(row, 2),
                            Unit = GetCell(row, 3),
                            StockQty = decimal.TryParse(GetCell(row, 4), out var q) ? q : 0,
                            GroupNo = group,
                            MatType = GetCell(row, 6)
                        });
                    }
                    return (true, $"导入成功,共{list.Count}条物料数据", list);
                }
                catch (Exception ex)
                {
                    return (false, $"解析失败:{ex.Message}", list);
                }
            }
        }
        #endregion

        #region 3. 物料/BOM 导出
        public static void ExportMaterial()
        {
            var data = SqlSugarDb.GetClient().Queryable<Material>().ToList();
            ExportToExcel(data, "物料数据", new[] { "物料编码", "物料名称", "规格", "单位", "库存", "组别", "类型" },
                m => new object[] { m.MatCode, m.MatName, m.Spec, m.Unit, m.StockQty, m.GroupNo, m.MatType });
        }

        public static void ExportBom()
        {
            var data = SqlSugarDb.GetClient().Queryable<Bom>().ToList();
            ExportToExcel(data, "BOM数据", new[] { "BOM单号", "父物料", "子物料", "用量", "版本", "组别" },
                b => new object[] { b.BomNo, b.ParentMatCode, b.ChildMatCode, b.UseQty, b.BomVersion, b.GroupNo });
        }

        private static void ExportToExcel<T>(List<T> data, string sheetName, string[] headers, Func<T, object> rowFunc)
        {
            if (data.Count == 0)
            {
                MessageBox.Show("暂无数据可导出");
                return;
            }
            using (SaveFileDialog sfd = new SaveFileDialog())
            {
                sfd.Filter = "Excel(*.xlsx)|*.xlsx";
                if (sfd.ShowDialog() != DialogResult.OK) return;

                IWorkbook wb = new XSSFWorkbook();
                ISheet sheet = wb.CreateSheet(sheetName);
                IRow header = sheet.CreateRow(0);
                for (int i = 0; i < headers.Length; i++)
                    header.CreateCell(i).SetCellValue(headers[i]);

                for (int i = 0; i < data.Count; i++)
                {
                    IRow row = sheet.CreateRow(i + 1);
                    var cells = rowFunc(data[i]);
                    for (int c = 0; c < cells.Length; c++)
                        row.CreateCell(c).SetCellValue(cells[c]?.ToString() ?? "");
                }

                using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create))
                    wb.Write(fs);
                MessageBox.Show("导出完成!");
            }
        }
        #endregion

        private static string GetCell(IRow row, int idx)
        {
            var cell = row.GetCell(idx);
            return cell == null ? "" : cell.ToString().Trim();
        }
    }
}

5 WinForm UI 层(主界面 + 完整功能)

整合:ERP 物料 BOM、支轴 / 动力件调度、单 / 组合指令、作业生命周期、Excel 模板、权限菜单、GDI 图表

csharp

运行

复制代码
using MES_ERP_3C.Core;
using MES_ERP_3C.DAL;
using MES_ERP_3C.Models;
using MES_ERP_3C.Utils;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MES_ERP_3C
{
    public partial class MainForm : Form
    {
        // 全局核心实例
        private readonly AxisPowerScheduler _scheduler;
        private readonly WorkLifeManager _workLife;

        public MainForm()
        {
            // 初始化数据库 & 15~17组数据
            SqlSugarDb.GetClient();
            _scheduler = new AxisPowerScheduler();
            _workLife = new WorkLifeManager();

            InitializeComponent();
            InitUI();
            BindEventBus();
            LoadBaseData();
        }

        #region UI初始化
        private void InitUI()
        {
            Text = "3C电子MES/ERP 物料BOM作业协同系统(支轴+动力件调度)";
            WindowState = FormWindowState.Maximized;

            // 1. 顶部菜单:总指令端 + Excel模板菜单
            InitMenu();

            // 2. 支轴下拉/多选框
            cboAxis.Items.AddRange(_scheduler.GetAllAxisCode().ToArray());
            clbAxis.Items.AddRange(_scheduler.GetAllAxisCode().ToArray());
            if (cboAxis.Items.Count > 0) cboAxis.SelectedIndex = 0;

            // 3. 禁用跨线程UI报错
            CheckForIllegalCrossThreadCalls = false;
        }

        // 总指令端菜单(权限+模板)
        private void InitMenu()
        {
            var menuMain = new MenuStrip { Dock = DockStyle.Top };
            // 总指令端菜单
            var mDispatch = new ToolStripMenuItem("总指令端");
            mDispatch.DropDownItems.Add("单指令调度", null, btnSingleDispatch_Click);
            mDispatch.DropDownItems.Add("组合指令调度", null, btnCombineDispatch_Click);
            mDispatch.DropDownItems.Add("停止所有调度", null, btnStopAll_Click);
            // Excel模板菜单
            var mExcel = new ToolStripMenuItem("Excel固定模板");
            mExcel.DropDownItems.Add("下载物料模板", null, (s, e) => ExcelTemplateHelper.DownloadMaterialTemplate());
            mExcel.DropDownItems.Add("下载BOM模板", null, (s, e) => ExcelTemplateHelper.DownloadBomTemplate());
            // 数据导入导出
            var mData = new ToolStripMenuItem("数据导入导出");
            mData.DropDownItems.Add("导入物料", null, btnImportMat_Click);
            mData.DropDownItems.Add("导出物料", null, (s, e) => ExcelTemplateHelper.ExportMaterial());
            mData.DropDownItems.Add("导出BOM", null, (s, e) => ExcelTemplateHelper.ExportBom());

            menuMain.Items.Add(mDispatch);
            menuMain.Items.Add(mExcel);
            menuMain.Items.Add(mData);
            Controls.Add(menuMain);
        }
        #endregion

        #region 数据加载
        private void LoadBaseData()
        {
            dgvMaterial.DataSource = SqlSugarDb.GetClient().Queryable<Material>().ToDataTable();
            dgvBom.DataSource = SqlSugarDb.GetClient().Queryable<Bom>().ToDataTable();
            dgvWorkOrder.DataSource = _workLife.GetAllWork();
        }
        #endregion

        #region 事件总线绑定(跨线程日志/刷新)
        private void BindEventBus()
        {
            AxisEventBus.OnLog += log => txtLog.AppendText($"{log}\r\n");
            AxisEventBus.OnUIRefresh += LoadBaseData;
        }
        #endregion

        #region 作业生命周期 按钮(创建/暂停/归档)
        private void btnCreateWork_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(txtBomNo.Text) || !int.TryParse(txtPlanQty.Text, out int qty))
            {
                MessageBox.Show("请填写合法BOM单号、计划产量");
                return;
            }
            var work = _workLife.CreateWork(txtBomNo.Trim(), cboAxis.Text, "", qty);
            MessageBox.Show($"作业创建成功:{work.WorkNo}");
            LoadBaseData();
        }

        private void btnPauseWork_Click(object sender, EventArgs e)
        {
            var work = GetSelectWork();
            if (work == null) return;
            if (_workLife.Pause(work))
                MessageBox.Show("作业已暂停");
            else
                MessageBox.Show("当前状态不支持暂停");
            LoadBaseData();
        }

        private void btnArchiveWork_Click(object sender, EventArgs e)
        {
            var work = GetSelectWork();
            if (work == null) return;
            if (_workLife.Archive(work))
                MessageBox.Show("作业已归档(ERP单据归档完成)");
            else
                MessageBox.Show("当前状态不支持归档");
            LoadBaseData();
        }

        private WorkOrder GetSelectWork()
        {
            if (dgvWorkOrder.SelectedRows.Count == 0)
            {
                MessageBox.Show("请选中一条作业单");
                return null;
            }
            return dgvWorkOrder.SelectedRows[0].DataBoundItem as WorkOrder;
        }
        #endregion

        #region 多指令调度(单/组合)
        private async void btnSingleDispatch_Click(object sender, EventArgs e)
        {
            if (cboAxis.SelectedItem == null) return;
            await _scheduler.StartSingleDispatch(cboAxis.Text, "管理员");
            LoadBaseData();
        }

        private async void btnCombineDispatch_Click(object sender, EventArgs e)
        {
            List<string> axisList = new List<string>();
            for (int i = 0; i < clbAxis.Items.Count; i++)
            {
                if (clbAxis.GetItemChecked(i))
                    axisList.Add(clbAxis.Items[i].ToString());
            }
            if (!axisList.Any())
            {
                MessageBox.Show("请至少选择一个支轴");
                return;
            }
            await _scheduler.StartCombineDispatch(axisList, "管理员");
            LoadBaseData();
        }

        private void btnStopAll_Click(object sender, EventArgs e)
        {
            _scheduler.StopAll();
        }
        #endregion

        #region Excel导入
        private void btnImportMat_Click(object sender, EventArgs e)
        {
            var res = ExcelTemplateHelper.ImportMaterial();
            if (res.Result && res.Data.Any())
            {
                SqlSugarDb.GetClient().Insertable(res.Data).ExecuteCommand();
            }
            MessageBox.Show(res.Msg);
            LoadBaseData();
        }
        #endregion

        #region 设计器自动代码(精简)
        private DataGridView dgvMaterial, dgvBom, dgvWorkOrder;
        private ComboBox cboAxis;
        private CheckedListBox clbAxis;
        private TextBox txtLog, txtBomNo, txtPlanQty;
        private Button btnCreateWork, btnPauseWork, btnArchiveWork;

        private void InitializeComponent()
        {
            // 此处为VS设计器自动生成控件布局,省略,直接拖拽即可
            // 控件:表格、文本框、按钮、日志框、下拉/多选框
        }
        #endregion
    }
}

6 程序入口

csharp

运行

复制代码
using System;
using System.Windows.Forms;

namespace MES_ERP_3C
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }
}

四、核心功能完整落地(对应所有需求)

1 ✅ 组支轴 + 动力件 作业协同模式

  1. 支轴分组:AX01~AX04 对应贴片 / 焊接 / 组装 / 测试四大工序支轴;
  2. 动力件绑定:每个支轴绑定对应产线动力单元,调度时自动模拟负载变化;
  3. 协同执行:支轴工序驱动动力件运转,工序完成后负载自动回落;
  4. 状态联动:支轴状态、动力件负载、作业状态三方实时同步。

2 ✅ 多指令端 / 组合应用调度

  • 单指令调度:选中单个支轴,独立后台线程执行工序;
  • 组合指令调度 :多选多个支轴,并行调度,适配多产线同时作业;
  • 调度日志自动写入本地库,对接 ERP 指令记录。

3 ✅ ERP 物料 BOM 作业协同(3C 电子场景)

  • 完整 物料 → BOM → 生产作业单 三层 ERP 单据链路;
  • 作业单绑定 BOM、支轴、动力件,完全贴合 3C 数码加工流程;
  • 作业生命周期:创建→调度→执行→暂停→完成→归档 标准 ERP 生产单流程。

4 ✅ Excel 固定模板 + 导入导出

  • 两套官方固定模板:3C 物料模板、BOM 模板;
  • 模板强制校验组别 15~17,符合要求;
  • 支持批量导入、全量导出,替代手工录入。

5 ✅ 本地文件数据库

  • SQLite 文件型数据库,无需部署服务;
  • 程序首次运行自动生成库表 + 15~17 组 3C 测试数据
  • SqlSugar 全封装 CRUD,稳定高效。

6 ✅ 作业生命周期 UI 完善

界面按钮:创建作业 / 暂停作业 / 归档作业

  • 状态强约束,禁止非法操作;
  • 操作后自动刷新表格、写入日志;
  • 可手动录入完成产量,对接 ERP 完工上报。

五、运行 & 使用步骤

  1. 用 Visual Studio 打开项目,安装上方 NuGet 包;
  2. 编译运行,程序目录自动生成 3C_MES_ERP.db 数据库文件;
  3. 模板使用:顶部菜单 → Excel 固定模板 → 下载模板,填写后导入;
  4. 调度使用
    • 单指令:下拉选支轴 → 总指令端 → 单指令调度;
    • 组合指令:多选框勾选多个支轴 → 组合指令调度;
  5. 作业操作:填写 BOM 单号、产量 → 创建作业 → 选中行执行暂停 / 归档;
  6. 所有调度、作业、导入操作均会在日志区打印记录。

六、扩展方向(对接金蝶 ERP)

  1. 新增 金蝶 K3Cloud WebAPI 调用类,实现本地数据与 ERP 双向同步;
  2. GDI+ 扩展生产报表、BOM 树形图;
  3. 增加用户权限,控制指令下发权限;
  4. 增加数据备份 / 恢复,保障本地文件库安全。