完整组件化 3C 电子 MES/ERP 协同项目全套方案(含 App.config、分层组件、WinForm、SQLite、GDI、Excel 模板、组轴多线程调度)




一、项目概述
1. 项目定位
面向3C 数码(手机 / 平板)组装产线 的 WinForm 桌面协同系统,打通 ERP 物料 / BOM 与 MES 组轴工序生产流程; 解决原有代码碎片化、业务逻辑分散、线程调度杂乱问题,采用高内聚分层组件架构,所有业务抽离独立类库组件,界面只做交互,无复杂业务代码。
2. 核心业务场景
- ERP 物料、BOM 主数据管理(固定 Excel 模板导入导出,组别 15~17)
- 产线组轴 + 动力设备协同执行引擎(SMT / 焊接 / 组装 / 检测 4 大组轴)
- 多模式多线程调度:单轴、组合并行、流水线联控,自动汇总线程执行结果
- 生产作业完整生命周期:创建→调度→运行→暂停→完成→归档(贴合金蝶 ERP 单据流程)
- GDI 原生自绘生产统计图表(产量柱状图 + 设备负载仪表盘)
- SQLite 本地文件数据库,免服务、开箱即用
- 全局统一
App.config配置文件,数据库、线程、图表、Excel、ERP 参数统一管理
3. 技术栈
- 框架:.NET Framework 4.8 WinForms
- ORM:SqlSugar(SQLite 本地文件库)
- Excel:NPOI 标准固定模板
- 并发:Task 多线程 + 事件总线解耦
- 绘图:原生 GDI + 自定义图表控件
- 配置:App.config 全局参数统一管理
4. 项目分层目录
plaintext
3C_MES_ERP_AxisComponent/
├─ App.config # 全局配置文件
├─ Program.cs # 程序入口
├─ Models/ # 实体、枚举(物料/BOM/组轴/作业/调度日志)
├─ Components/ # 独立业务组件(核心解耦层)
│ ├─ DbComponent/ # SQLite数据库组件
│ ├─ EventBusComponent/ # 全局事件总线(模块解耦)
│ ├─ AxisEngineComponent/ # 组轴执行引擎
│ ├─ ThreadScheduleComponent/ # 多线程调度+结果组装
│ ├─ ExcelTemplateComponent/ # 固定Excel模板导入导出
│ ├─ GdiChartComponent/ # GDI自定义图表控件
│ └─ ErpBomComponent/ # ERP物料BOM业务层
└─ Forms/ # WinForm界面层(轻量化)
二、App.config 全局统一配置文件
所有数据库、线程、图表、Excel、ERP、组轴参数集中配置,无需修改代码切换环境
xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
<!-- 全局自定义参数 -->
<appSettings>
<!-- SQLite本地文件数据库配置 -->
<add key="SqliteDbFileName" value="3C_MES_AxisLocal.db"/>
<add key="DbLogSwitch" value="false"/>
<add key="InitGroupStart" value="15"/>
<add key="InitGroupEnd" value="17"/>
<!-- 组轴&多线程调度配置 -->
<add key="MaxThreadCount" value="8"/>
<add key="AxisTaskDelayMs" value="350"/>
<add key="AxisLinkedGroup1" value="LINK01"/>
<add key="AxisLinkedGroup2" value="LINK02"/>
<!-- Excel固定模板配置 -->
<add key="MaterialTemplateName" value="3C电子物料标准模板.xlsx"/>
<add key="BomTemplateName" value="3C电子BOM标准模板.xlsx"/>
<!-- GDI图表全局绘制参数 -->
<add key="ChartAntiAlias" value="true"/>
<add key="ChartBarWidth" value="26"/>
<add key="ChartTitleFontSize" value="12"/>
<!-- 金蝶K3Cloud ERP接口参数 -->
<add key="K3CloudUrl" value="http://192.168.60/K3Cloud"/>
<add key="K3AcctId" value="5756960b27b1aa"/>
<add key="K3UserName" value="Administrator"/>
<add key="K3Password" value="888888"/>
</appSettings>
<!-- SQLite数据库连接字符串 -->
<connectionStrings>
<add name="SqliteConn" connectionString="Data Source={SqliteDbFileName};Version=3;Pooling=true;Max Pool Size=10;" providerName="System.Data.SQLite"/>
</connectionStrings>
<!-- 第三方程序集绑定重定向 -->
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff13" culture="neutral" version="1.0.118.0"/>
<bindingRedirect oldVersion="0.0.0.0-1.0.118.0" newVersion="1.0.118.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
三、Models 统一实体与枚举(业务数据载体)
csharp
运行
using System;
using System.ComponentModel;
using SqlSugar;
namespace _3C_MES_ERP.Models
{
#region 全局业务枚举
/// <summary>组轴运行状态</summary>
public enum AxisRunState
{
[Description("空闲")] Idle,
[Description("调度中")] Running,
[Description("暂停")] Paused,
[Description("异常停机")] Error
}
/// <summary>动力设备类型(3C产线)</summary>
public enum PowerType
{
[Description("伺服电机")] Servo,
[Description("气动气缸")] Cylinder,
[Description("输送线")] Conveyor,
[Description("检测单元")] Tester
}
/// <summary>生产作业生命周期(ERP标准流程)</summary>
public enum WorkStatus
{
[Description("待创建")] Init,
[Description("已调度")] Dispatched,
[Description("执行中")] Running,
[Description("已暂停")] Paused,
[Description("已完成")] Completed,
[Description("已归档")] Archived
}
/// <summary>调度执行模式</summary>
public enum ScheduleMode
{
[Description("单轴调度")] Single,
[Description("组合并行")] Combine,
[Description("流水线联控")] Linked
}
#endregion
#region 数据库实体
/// <summary>ERP物料档案(组别15~17)</summary>
[SugarTable("T_ERP_Material")]
public class ErpMaterial
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
[SugarColumn(Length = 40, IsNullable = false)]
public string MatCode { get; set; }
[SugarColumn(Length = 80)]
public string MatName { get; set; }
[SugarColumn(Length = 100)]
public string Spec { get; set; }
[SugarColumn(Length = 10)]
public string Unit { get; set; }
public decimal StockQty { get; set; }
public int GroupNo { get; set; } // 固定15/16/17
public string MatType { get; set; }
public DateTime CreateTime { get; set; } = DateTime.Now;
}
/// <summary>ERP BOM结构单</summary>
[SugarTable("T_ERP_BOM")]
public class ErpBom
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string BomNo { get; set; }
public string ParentMatCode { get; set; }
public string ChildMatCode { get; set; }
public decimal UseQty { get; set; }
public string BomVersion { get; set; }
public int GroupNo { get; set; }
}
/// <summary>产线组轴</summary>
[SugarTable("T_AxisGroup")]
public class AxisGroup
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string AxisCode { get; set; } // AX01~AX04
public string AxisName { get; set; }
public AxisRunState RunState { get; set; }
public string BindProcess { get; set; }
public string LinkedGroup { get; set; } // 联控分组
}
/// <summary>动力执行设备</summary>
[SugarTable("T_PowerDevice")]
public class PowerDevice
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string PowerCode { get; set; }
public string PowerName { get; set; }
public PowerType DeviceType { get; set; }
public string BindAxisCode { get; set; }
public decimal LoadRate { get; set; }
public string DeviceStatus { get; set; }
}
/// <summary>生产作业单(对接ERP生产任务)</summary>
[SugarTable("T_WorkOrder")]
public class WorkOrder
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string WorkNo { get; set; }
public string RelBomNo { get; set; }
public int PlanQty { get; set; }
public int CompleteQty { get; set; }
public WorkStatus WorkStatus { get; set; }
public string BindAxisCode { get; set; }
public string BindPowerCode { get; set; }
public DateTime CreateTime { get; set; } = DateTime.Now;
public DateTime? FinishTime { get; set; }
}
/// <summary>调度执行日志(多线程结果持久化载体)</summary>
[SugarTable("T_ScheduleLog")]
public class ScheduleLog
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public ScheduleMode ScheduleMode { get; set; }
public string TargetAxis { get; set; }
public string Operator { get; set; }
public DateTime OperTime { get; set; } = DateTime.Now;
public string FullResult { get; set; } // 组装后的完整线程执行结果
}
/// <summary>GDI图表统计DTO</summary>
public class ChartDataDto
{
public string WorkName { get; set; }
public decimal PlanNum { get; set; }
public decimal ActualNum { get; set; }
public decimal LoadAvg { get; set; }
}
#endregion
}
四、分层独立组件代码(核心业务解耦,消除零散代码)
组件 1:DbComponent 本地 SQLite 数据库组件(读取 App.config)
csharp
运行
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using SqlSugar;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.DbComponent
{
public static class LocalSqlDb
{
private static SqlSugarClient _dbClient;
private static readonly string _dbFile;
private static readonly int _startGroup;
private static readonly int _endGroup;
static LocalSqlDb()
{
// 从App.config读取数据库配置
_dbFile = ConfigurationManager.AppSettings["SqliteDbFileName"];
_start = int.Parse(ConfigurationManager.AppSettings["InitGroupStart"]);
_end = int.Parse(ConfigurationManager.AppSettings["InitGroupEnd"]);
InitSqlSugarClient();
CreateTableAndInitData();
}
/// 初始化SqlSugar连接
private static void InitSqlSugarClient()
{
string connStr = ConfigurationManager.ConnectionStrings["SqliteConn"].ConnectionString.Replace("{SqliteDbFileName}", _dbFile);
_dbClient = new SqlSugarClient(new ConnectionConfig
{
ConnectionString = connStr,
DbType = DbType.Sqlite,
IsAutoCloseConnection = true,
InitKeyType = InitKeyType.Attribute,
AopEvents = new AopEvent
{
OnLogExecuting = (sql, pars) =>
{
if (ConfigurationManager.AppSettings["DbLogSwitch"] == "true")
Console.WriteLine(sql);
}
}
});
}
/// 自动建表 + 初始化15~17组3C测试数据
private static void CreateTableAndInitData()
{
_dbClient.CodeFirst.InitTables(
typeof(ErpMaterial), typeof(ErpBom), typeof(AxisGroup),
typeof(PowerDevice), typeof(WorkOrder), typeof(ScheduleLog));
if (!_dbClient.Queryable<ErpMaterial>().Any()) InitMaterial();
if (!_dbClient.Queryable<ErpBom>().Any()) InitBom();
if (!_dbClient.Queryable<AxisGroup>().Any()) InitAxis();
if (!_dbClient.Queryable<PowerDevice>().Any()) InitPower();
}
#region 预置15~17组业务数据
private static void InitMaterial()
{
Random rand = new Random();
List<ErpMaterial> list = new List<ErpMaterial>();
string[] names = {PCB主板,"芯片","电容","摄像头","电池","外壳"};
string[] types = {"原材料","半成品","成品"};
string[] unit = {"PCS","片","套"};
for (int g = _start; g <= _end; g++)
{
for (int i = 0; i < 3; i++)
{
list.Add(new ErpMaterial
{
MatCode = $"MAT{g:D2}{i:D2}",
MatName = $"{names[rand.Next(names)]}_{g}组",
Spec = $"V{rand.Next(1)}.{rand.Next(9)}",
Unit = unit[rand.Next(unit)],
StockQty = rand.Next(200,12000),
GroupNo = g,
MatType = types[rand.Next(types)]
});
}
}
_dbClient.Insertable(list).ExecuteCommand();
}
private static void InitBom()
{
Random rand = new Random();
List<ErpBom> list = new List<ErpBom>();
for (int g = _start; g <= _end; g++)
{
list.Add(new ErpBom
{
BomNo = $"BOM{g:D2}01",
ParentMatCode = $"MAT{g:D2}00",
ChildMatCode = $"MAT{g:D2}01",
UseQty = Math.Round((decimal)rand.Next(1,5)/10,2),
BomVersion = "V1.0",
GroupNo = g
});
}
_dbClient.Insertable(list).ExecuteCommand();
}
private static void InitAxis()
{
List<AxisGroup> list = new List<AxisGroup>()
{
new AxisGroup{AxisCode="AX01",AxisName="SMT贴片",RunState=AxisRunState.Idle,BindProcess="上料、贴片",LinkedGroup=ConfigurationManager.AppSettings["AxisLinkedGroup1"]},
new AxisGroup{AxisCode="AX02",AxisName="高温焊接",RunState=AxisRunState.Idle,BindProcess="回流焊",LinkedGroup=ConfigurationManager.AppSettings["AxisLinkedGroup1"]},
new AxisGroup{AxisCode="AX03",AxisName="整机组装",RunState=AxisRunState.Idle,BindProcess="锁螺丝、贴合",LinkedGroup=ConfigurationManager.AppSettings["AxisLinkedGroup2"]},
new AxisGroup{AxisCode="AX04",AxisName="功能检测",RunState=AxisRunState.Idle,BindProcess="通电检测",LinkedGroup=ConfigurationManager.AppSettings["AxisLinkedGroup2"]}
};
_dbClient.Insertable(list).ExecuteCommand();
}
private static void InitPower()
{
Random rand = new Random();
List<PowerDevice> list = new List<PowerDevice>();
string[] axisArr = {"AX01","AX02","AX03","AX04"};
var powerTypes = Enum.GetValues(typeof(PowerType)).Cast<PowerType>();
for (int i = 1; i <= _end - _start + 1; i++)
{
list.Add(new PowerDevice
{
PowerCode = $"PU{i:D2}",
PowerName = $"动力单元{i}",
DeviceType = powerTypes.ElementAt(rand.Next(powerTypes.Count())),
BindAxisCode = axisArr[rand.Next(axisArr.Length)],
LoadRate = rand.Next(10,80),
DeviceStatus = "正常"
});
}
_dbClient.Insertable(list).ExecuteCommand();
}
#endregion
// 通用CRUD封装
public static SqlSugarClient GetDb() => _dbClient;
public static bool Insert<T>(T entity) => _dbClient.Insertable(entity).ExecuteCommand() > 0;
public static bool Update<T>(T entity) => _dbClient.Updateable(entity).ExecuteCommand() > 0;
public static List<T> QueryAll<T>() => _dbClient.Queryable<T>().ToList();
public static T QueryById<T>(object id) => _dbClient.Queryable<T>().InSingle(id);
}
}
组件 2:EventBusComponent 全局事件总线(解耦模块,消除跨窗体零散回调)
csharp
运行
using System;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.EventBusComponent
{
public static class GlobalEventBus
{
#region 全局委托定义
public delegate void LogPrintDel(string logInfo);
public delegate void UIRefreshDel();
public delegate AxisMsgDel(string axisCode, string info);
public delegate WorkStateDel(WorkOrder order, WorkStatus status);
public delegate ScheduleResultDel(string fullResult);
#endregion
#region 全局订阅事件
public static event LogPrintDel OnLog;
public static event UIRefreshDel OnUiRefresh;
public static event AxisMsgDel OnAxisNotify;
public static event WorkStateDel OnWorkChange;
public static event ScheduleResultDel OnScheduleComplete;
#endregion
// 输出日志
public static void PublishLog(string msg)
{
string log = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {msg}";
OnLog?.Invoke(log);
}
// 通知界面刷新
public static void PublishRefresh() => OnUiRefresh?.Invoke();
// 推送组轴执行消息
public static void PublishAxisMsg(string axis, string info)
{
PublishLog($"【组轴{axis}】{info}");
OnAxisNotify?.Invoke(axis, info);
}
// 作业状态变更推送
public static void PublishWorkState(WorkOrder order, WorkStatus status)
{
PublishLog($"作业{order.WorkNo} 状态:{status}");
OnWorkChange?.Invoke(order, status);
}
// 推送多线程组装完整调度结果
public static void PublishScheduleResult(string result)
{
OnScheduleComplete?.Invoke(result);
}
}
}
组件 3:AxisEngineComponent 组轴执行引擎(统一收拢零散工序执行代码)
csharp
运行
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Threading;
using _3C_MES_ERP.Components.DbComponent;
using _3C_MES_ERP.Components.EventBusComponent;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.AxisEngineComponent
{
public class AxisExecuteEngine
{
private readonly int _taskDelay;
private readonly Dictionary<string, List<string>> _axisTaskMap = new();
private readonly Dictionary<string, AxisRunState> _axisStateMap = new();
private readonly Dictionary<string, List<string>> _linkGroupMap = new();
public AxisExecuteEngine()
{
_taskDelay = int.Parse(ConfigurationManager.AppSettings["AxisTaskDelayMs"]);
LoadAxisConfigData();
}
/// 加载组轴、联控分组配置
private void LoadAxisConfigData()
{
var axisList = LocalSqlDb.QueryAll<AxisGroup>();
foreach (var axis in axisList)
{
_axisStateMap[axis.AxisCode] = axis.RunState;
_axisTaskMap[axis.AxisCode] = axis.BindProcess.Split('、').ToList();
}
// 构建联控分组映射
var groupQuery = axisList.GroupBy(g => g.LinkedGroup);
foreach (var g in groupQuery)
_linkGroupMap[g.Key] = g.Select(x => x.AxisCode).ToList();
}
/// 单组轴同步执行工序,返回完整执行文本结果
public string RunSingleAxis(string axisCode)
{
if (!_axisStateMap.ContainsKey(axisCode) || _axisStateMap[axisCode] != AxisRunState.Idle)
return $"组轴{axisCode}当前状态不可执行";
_axisStateMap[axisCode] = AxisRunState.Running;
var powerList = LocalSqlDb.QueryAll<PowerDevice>().Where(p => p.BindAxisCode == axisCode).ToList();
var taskList = _axisTaskMap[axisCode];
string resultText = $"====组轴{axisCode}执行开始====\r\n";
foreach (var proc in taskList)
{
if (_axisStateMap[axisCode] == AxisRunState.Paused)
{
result += $"工序{proc}:手动暂停\r\n";
continue;
}
// 动力设备负载上升
foreach (var power in powerList)
{
power.LoadRate += 8;
if (power.LoadRate > 90) power.DeviceStatus = "过载预警";
LocalSqlDb.Update(power);
}
Thread.Sleep(_taskDelay);
resultText += $"工序{proc}执行完成\r\n";
GlobalEventBus.PublishAxisMsg(axisCode, $"工序{proc}完成");
}
// 执行完毕负载回落
foreach (var power in powerList)
{
power.LoadRate = Math.Max(0, power.LoadRate - 15);
power.DeviceStatus = "正常";
LocalSqlDb.Update(power);
}
_axisStateMap[axisCode] = AxisRunState.Idle;
resultText += $"====组轴{axisCode}全部工序结束====\r\n";
return resultText;
}
/// 暂停指定组轴
public void PauseSingleAxis(string axisCode)
{
if (_axisStateMap.ContainsKey(axisCode) && _axisStateMap[axisCode] == AxisRunState.Running)
_axisStateMap[axisCode] = AxisRunState.Paused;
}
/// 紧急停止整个联控分组
public void StopLinkedGroup(string linkCode)
{
if (!_linkGroupMap.TryGetValue(linkCode, out var axisArr)) return;
foreach (var ax in axisArr)
_axisStateMap[ax] = AxisRunState.Error;
GlobalEventBus.PublishLog($"联控组{linkCode}全部设备紧急停机");
}
/// 重置所有组轴为空闲
public void ResetAllAxis()
{
foreach (var key in _axisStateMap.Keys)
_axisStateMap[key] = AxisRunState.Idle;
GlobalEventBus.PublishLog("所有组状态重置完成");
}
public List<string> GetAllAxisCode() => _axisStateMap.Keys.ToList();
public List<string> GetAllLinkGroup() => _linkGroupMap.Keys.ToList();
}
}
组件 4:ThreadScheduleComponent 多线程调度 + 结果组装(收拢零散 Task 代码)
csharp
运行
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using _3C_MES_ERP.Components.AxisEngineComponent;
using _3C_MES_ERP.Components.DbComponent;
using _3C_MES_ERP.Components.EventBusComponent;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.ThreadScheduleComponent
{
public class MultiThreadSchedule
{
private readonly AxisExecuteEngine _axisEngine;
private readonly List<string> _resultBuffer = new();
public MultiThreadSchedule(AxisExecuteEngine engine)
{
_axisEngine = engine;
}
/// 单线程调度
public async Task<string> RunSingleTask(string axisCode, string operatorName)
{
_resultBuffer.Clear();
string singleRes = await Task.Run(() => _axisEngine.RunSingleAxis(axisCode));
_resultBuffer.Add(singleRes);
string fullText = string.Join("\r\n", _resultBuffer);
SaveScheduleLog(ScheduleMode.Single, axisCode, operatorName, fullText);
GlobalEventBus.PublishScheduleResult(fullText);
return fullText;
}
/// 多轴组合并行调度,自动组装全部线程返回结果
public async Task<string> RunCombineTask(List<string> axisList, string operatorName)
{
_resultBuffer.Clear();
List<Task<string>> taskGroup = new List<Task<string>>();
foreach (var ax in axisList)
taskGroup.Add(Task.Run(() => _axisEngine.RunSingleAxis(ax)));
await Task.WhenAll(taskGroup);
// 统一组装所有线程执行输出
string combineResult = "【组合并行调度汇总】\r\n" + string.Join("\r\n-----------------\r\n", taskGroup.Select(t => t.Result));
SaveScheduleLog(ScheduleMode.Combine, string.Join("、", axisList), operatorName, combineResult);
GlobalEventBus.PublishScheduleResult(combineResult);
return combineResult;
}
/// 流水线联控调度(顺序执行,单异常整组停机)
public async Task<string> RunLinkedTask(string linkGroup, string operatorName)
{
_resultBuffer.Clear();
var axisArr = _axisEngine.GetAllLinkGroup().Contains(linkGroup) ? _axisEngine.GetAllLinkGroup()[linkGroup] : new List<string>();
if (!axisArr.Any()) return "该联控分组不存在";
string totalText = $"【联控流水线{linkGroup}】\r\n";
foreach (var ax in axisArr)
{
string res = await Task.Run(() => _axisEngine.RunSingleAxis(ax));
totalText += res + "\r\n";
if (res.Contains("过载预警") || res.Contains("暂停"))
{
_axisEngine.StopLinkedGroup(linkGroup);
totalText += ">>>> 设备异常,流水线整体终止 <<<<\r\n";
break;
}
}
SaveScheduleLog(ScheduleMode.Linked, linkGroup, operatorName, totalText);
GlobalEventBus.PublishScheduleResult(totalText);
return totalText;
}
/// 全部调度停止
public void StopAllSchedule() => _axisEngine.ResetAllAxis();
/// 调度日志入库(组装完整结果持久化)
private void SaveScheduleLog(ScheduleMode mode, string target, string user, string fullResult)
{
var log = new ScheduleLog
{
ScheduleMode = mode,
TargetAxis = target,
Operator = user,
FullResult = fullResult
};
LocalSqlDb.Insert(log);
GlobalEventBus.PublishRefresh();
}
}
}
组件 5:ExcelTemplateComponent 固定模板导入导出(标准化 Excel 逻辑收拢)
csharp
运行
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using _3C_MES_ERP.Components.DbComponent;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.ExcelTemplateComponent
{
public static class ExcelFixedTemplate
{
// 两套标准固定表头(不可修改)
private static readonly List<string> MaterialHeader = new()
{ "物料编码","物料名称","规格型号","单位","库存数量","组别(15/16/17)","物料类型" };
private static readonly List<string> BomHeader = new()
{ "BOM单号","父物料编码","子物料编码","单台用量","版本","组别(15/16/17)" };
#region 下载标准空白模板
public static void DownloadMaterialTemplate() => CreateTemplate(MaterialHeader, ConfigurationManager.AppSettings["MaterialTemplateName"]);
public static void DownloadBomTemplate() => CreateTemplate(BomHeader, ConfigurationManager.AppSettings["BomTemplateName"]);
private static void CreateTemplate(List<string> header, string fileName)
{
using SaveFileDialog sfd = new SaveFileDialog();
s.Filter = "Excel(*.xlsx)|*.xlsx";
s.FileName = fileName;
if (s.ShowDialog() != DialogResult) return;
IWorkbook wb = new XSSFWorkbook();
ISheet sheet = wb.CreateSheet("标准模板");
IRow hRow = sheet.CreateRow(0);
for (int i = 0; i < header.Count; i++)
{
hRow.CreateCell(i).SetCellValue(header[i]);
sheet.SetColumnWidth(i, 22 * 256);
}
// 示例行
IRow demoRow = sheet.CreateRow(1);
header.ForEach((h, idx) => demoRow.CreateCell(idx).SetCellValue($"示例_{h}"));
using FileStream fs = new FileStream(sfd.FileName, FileMode.Create);
wb.Write(fs);
MessageBox.Show("标准模板下载完成");
}
#endregion
#region Excel导入(强制校验组别15~17)
public static (bool Success, string Msg, List<ErpMaterial> ImportMaterial)
{
List<ErpMaterial> dataList = new List<ErpMaterial>();
using OpenFileDialog ofd = new OpenFileDialog();
of.Filter = "*.xlsx;*.xls";
if (of.ShowDialog() != DialogResult) return (false, "已取消", dataList);
try
{
IWorkbook wb;
using FileStream fs = new FileStream(of.FileName, FileMode.Open);
wb = of.FileName.EndsWith(".xlsx") ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs);
ISheet sheet = wb.GetSheetAt(0);
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 : 0;
if (group < 15 || group > 17)
return (false, $"第{r+1}行组别必须15/16/17", dataList);
dataList.Add(new ErpMaterial
{
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, $"读取{dataList}条物料", dataList);
}
catch (Exception ex)
{
return (false, $"解析异常:{ex.Message}", dataList);
}
}
#endregion
#region 数据导出
public static void ExportMaterial() => ExportData(MaterialHeader, LocalSqlDb.QueryAll<ErpMaterial>, m =>
new object[] { m.MatCode, m.MatName, m.Spec, m.Unit, m.StockQty, m.GroupNo, m.MatType });
public static void ExportBom() => ExportData(BomHeader, LocalSqlDb.QueryAll<ErpBom>, b =>
new object[] { b.BomNo, b.ParentMatCode, b.ChildMatCode, b.UseQty, b.BomVersion, b.GroupNo });
private static void ExportData<T>(List<string> header, List<T> source, Func<T, object[]> rowFunc)
{
if (source.Count == 0) { MessageBox.Show("无数据"); return; }
using SaveFileDialog sfd = new SaveFileDialog();
s.Filter = "*.xlsx";
if (s.ShowDialog() != DialogResult) return;
IWorkbook wb = new XSSFWorkbook();
ISheet sheet = wb.CreateSheet("导出数据");
IRow hRow = sheet.CreateRow(0);
for (int i = 0; i < header.Count; i++) hRow.CreateCell(i).SetCellValue(header[i]);
for (int i = 0; i < source.Count; i++)
{
IRow row = sheet.CreateRow(i + 1);
var cells = rowFunc(source[i]);
for (int c = 0; c < cells.Length; c++)
row.CreateCell(c, cells[c]?.ToString() ?? "");
}
using FileStream fs = new FileStream(sfd.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();
}
}
}
组件 6:GdiChartComponent GDI 自绘图表组件(独立绘图逻辑,无窗体混杂代码)
csharp
运行
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.GdiChartComponent
{
public class ProductionGdiChart : Control
{
private List<ChartDataDto> _chartData = new();
private readonly bool _antiAlias;
private readonly int _barWidth;
private readonly int _fontSize;
public ProductionGdiChart()
{
// 读取App.config绘图参数
_antiAlias = bool.Parse(ConfigurationManager.AppSettings["ChartAntiAlias"]);
_barWidth = int.Parse(ConfigurationManager.AppSettings["ChartBarWidth"]);
_fontSize = int.Parse(ConfigurationManager.AppSettings["ChartTitleFontSize"]);
DoubleBuffered = true;
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}
public List<ChartDataDto> DataSource
{
set { _chartData = value; Invalidate(); }
}
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
if (_antiAlias) g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.White);
if (_chart.Count == 0)
{
g.DrawString("暂无生产统计数据", new Font("微软雅黑", _fontSize), Brushes.Gray, Width / 2 - 60, Height / 2);
return;
}
DrawTitle(g);
DrawAxis(g);
DrawBarChart(g);
DrawLoadGauge(g);
}
private void DrawTitle(Graphics g)
{
Font font = new Font("微软雅黑", _fontSize, FontStyle.Bold);
string title = "3C电子产线产量&设备负载报表";
var size = g.MeasureString(title, font);
g.DrawString(title, font, Brushes.Black, (Width - size.Width) / 2, 8);
font.Dispose();
}
private void DrawAxis(Graphics g)
{
int pad = 40;
Pen gridPen = new Pen(Color.LightGray, 1) { DashStyle = DashStyle.Dash };
g.DrawLine(Pens.Black, pad, pad, pad, Height - pad);
g.DrawLine(Pens.Black, pad, Height - pad, Width - pad, Height - pad);
// Y刻度网格
for (int i = 1; i <= 5; i++)
{
int y = Height - pad - (Height - pad) / 5 * i;
g.DrawLine(grid, pad, y, Width - pad, y);
g.DrawString($"{i * 20}%", new Font("微软雅黑", 9), Brushes.Gray, 10, y - 8);
}
gridPen.Dispose();
}
private void DrawBarChart(Graphics g)
{
int pad = 40;
int x = pad + 10;
decimal maxVal = Math.Max(_chart.Max(d => d.PlanNum), _chart.Max(d => d.ActualNum));
float scale = (Height - pad * 2) / (float)maxVal;
foreach (var item in _chartData)
{
float hPlan = (float)item.PlanNum * scale;
g.FillRectangle(Brushes.RoyalBlue, x, Height - pad - hPlan, _barWidth, hPlan);
float hAct = (float)item.ActualNum * scale;
g.FillRectangle(Brushes.SeaGreen, x + _barWidth + 6, Height - pad - hAct, _barWidth, hAct);
g.DrawString(item.WorkName, new Font("微软雅黑", 8), Brushes.Black, x, Height - pad + 5);
x += _barWidth * 2 + 30;
}
}
private void DrawLoadGauge(Graphics g)
{
decimal avgLoad = _chartData.Average(d => d.LoadAvg);
int cx = Width - 130;
int cy = Height / 2;
int r = 80;
using Pen bgPen = new Pen(Color.LightGray, 16) { StartCap = LineCap.Round, EndCap = LineCap.Round };
g.DrawArc(bgPen, cx - r, cy - r, r * 2, r * 2, 135, 270);
using Pen forePen = new Pen(Color.OrangeRed, 16);
g.DrawArc(forePen, cx - r + 8, cy - r + 8, (r - 8) * 2, (r - 8) * 2, 135, (float)(270 * avgLoad / 100));
g.DrawString($"平均负载\n{avgLoad:F1}%", new Font("微软雅黑", 10), Brushes.DarkRed, cx - 40, cy - 25);
}
protected override void Dispose(bool dispos)
{
base.Dispose(dis);
}
}
}
组件 7:ErpBomComponent ERP 物料 BOM 作业业务层(收拢零散单据逻辑)
csharp
运行
using System.Collections.Generic;
using System.Linq;
using _3C_MES_ERP.Components.DbComponent;
using _3C_MES_ERP.Components.EventBusComponent;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Components.ErpBomComponent
{
public class ErpBusinessService
{
#region 物料CRUD
public List<ErpMaterial> GetAllMaterial() => LocalSqlDb.QueryAll<ErpMaterial>();
public bool AddMaterial(ErpMaterial m) => LocalSqlDb.Insert(m);
public bool UpdateMaterial(ErpMaterial m) => LocalSqlDb.Update(m);
#endregion
#region BOM查询
public List<ErpBom> GetAllBom() => LocalSqlDb.QueryAll<ErpBom>();
public List<ErpBom> GetBomByGroup(int groupNo) => LocalSqlDb.QueryAll<ErpBom>().Where(b => b.GroupNo == group).ToList();
#endregion
#region 生产作业单生命周期(ERP标准单据流程)
/// 状态流转校验,禁止非法跨状态操作
private bool CheckWorkFlow(WorkOrder curr, WorkOrder targetStatus)
{
return (curr.WorkStatus, targetStatus) 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
};
}
/// 创建ERP生产作业单
public WorkOrder CreateWork(string bomNo, string axisCode, int planQty)
{
WorkOrder wo = new WorkOrder
{
WorkNo = $"WO{DateTime.Now:yyyyMMddHHmmss}",
RelBomNo = bomNo,
PlanQty = planQty,
CompleteQty = 0,
WorkStatus = WorkStatus.Init,
BindAxisCode = axisCode
};
LocalSqlDb.Insert(wo);
GlobalEventBus.PublishWorkState(wo, WorkStatus.Init);
GlobalEventBus.PublishRefresh();
return wo;
}
/// 调度作业
public bool DispatchWork(WorkOrder wo)
{
if (!CheckWorkFlow(wo, WorkStatus.Dispatched)) return false;
wo.WorkStatus = WorkStatus.Dispatched;
LocalSqlDb.Update(wo);
GlobalEventBus.PublishWorkState(wo, WorkStatus.Dispatched);
return true;
}
/// 暂停作业
public bool PauseWork(WorkOrder wo)
{
if (!CheckWorkFlow(wo, WorkStatus.Paused)) return false;
wo.WorkStatus = WorkStatus.Paused;
LocalSqlDb.Update(wo);
GlobalEventBus.PublishWorkState(wo, WorkStatus.Paused);
return true;
}
/// 完成作业上报产量
public bool FinishWork(WorkOrder wo, int completeQty)
{
if (!CheckWorkFlow(wo, WorkStatus.Completed) || completeQty > wo.PlanQty) return false;
wo.CompleteQty = completeQty;
wo.WorkStatus = WorkStatus.Completed;
wo.FinishTime = DateTime.Now;
LocalSqlDb.Update(wo);
GlobalEventBus.PublishWorkState(wo, WorkStatus.Completed);
return true;
}
/// ERP单据归档
public bool ArchiveWork(WorkOrder wo)
{
if (!CheckWorkFlow(wo, WorkStatus.Archived)) return false;
wo.WorkStatus = WorkStatus.Archived;
LocalSqlDb.Update(wo);
GlobalEventBus.PublishWorkState(wo, WorkStatus.Archived);
return true;
}
public List<WorkOrder> GetAllWorkOrder() => LocalSqlDb.QueryAll<WorkOrder>().OrderByDesc(w => w.CreateTime).ToList();
#endregion
}
}
五、Forms WinForm 界面层(轻量化,无复杂业务,仅调用组件)
csharp
运行
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using _3C_MES_ERP.Components.AxisEngineComponent;
using _3C_MES_ERP.Components.EventBusComponent;
using _3C_MES_ERP.Components.ExcelTemplateComponent;
using _3C_MES_ERP.Components.GdiChartComponent;
using _3C_MES_ERP.Components.ErpBomComponent;
using _3C_MES_ERP.Components.ThreadScheduleComponent;
using _3C_MES_ERP.Models;
namespace _3C_MES_ERP.Forms
{
public partial class MainCooperForm : Form
{
// 全局组件实例(界面仅调用,不写业务逻辑)
private readonly AxisExecuteEngine _axisEngine;
private readonly MultiThreadSchedule _schedule;
private readonly ErpBusinessService _erpBiz;
private readonly ProductionGdiChart _chart;
public MainCooperForm()
{
// 实例化所有底层业务组件
_axisEngine = new AxisExecuteEngine();
_schedule = new MultiThreadSchedule(_axisEngine);
_erpBiz = new ErpBusinessService();
_chart = new ProductionGdiChart { Dock = DockStyle.Fill };
InitializeComponent();
InitMainUI();
BindEventBusSubscribe();
LoadAllTableData();
}
/// 初始化顶部菜单、下拉框UI
private void InitMainUI()
{
Text = "3C电子MES/ERP物料BOM协同平台 | 组轴多线程调度";
WindowState = FormWindowState.Maximized;
// 顶部菜单
MenuStrip menu = new MenuStrip { Dock = DockStyle.Top };
// Excel菜单
ToolStripMenuItem excelMenu = new ToolStripMenuItem("Excel标准模板");
excel.DropDownItems.Add("下载物料模板", null, (s, e) => ExcelFixedTemplate.DownloadMaterialTemplate());
excel.DropDownItems.Add("下载BOM模板", null, (s, e) => ExcelFixedTemplate.DownloadBomTemplate());
ToolStripMenuItem dataMenu = new ToolStripMenuItem("导入导出");
data.DropDownItems.Add("导入物料", null, ImportMatClick);
data.DropDownItems.Add("全部导出", null, (s, e) => { ExcelFixedTemplate.ExportMaterial(); ExcelFixedTemplate.ExportBom(); });
// 调度菜单
ToolStripMenuItem schMenu = new ToolStripMenuItem("调度指令中心");
sch.DropDownItems.Add("单轴调度", null, async (s, e) => await RunSingle());
sch.DropDownItems.Add("组合并行", null, async (s, e) => await RunCombine());
sch.DropDownItems.Add("流水线联控", null, async (s, e) => await RunLink());
sch.DropDownItems.Add("全部停止", null, (s, e) => _schedule.StopAllSchedule());
menu.Items.Add(excelMenu);
menu.Items.Add(dataMenu);
menu.Items.Add(sch);
Controls.Add(menu);
// 下拉框绑定组轴、联控分组
cboSingleAxis.Items.AddRange(_axisEngine.GetAllAxisCode().ToArray());
clbCombine.Items.AddRange(_axisEngine.GetAllAxisCode().ToArray());
cboLinkGroup.Items.AddRange(_axisEngine.GetAllLinkGroup().ToArray());
if (cboSingleAxis.Items.Count > 0) cboSingleAxis.SelectedIndex = 0;
// 图表容器
panelChart.Controls.Add(_chart);
}
/// 订阅全局事件总线(日志、自动刷新、调度结果弹窗)
private void BindEventBusSubscribe()
{
GlobalEventBus.OnLog += log =>
{
if (txtLog.InvokeRequired) txtLog.Invoke(() => txtLog.AppendText(log + "\r\n"));
else txtLog.AppendText(log + "\r\n");
};
GlobalEventBus.OnUiRefresh += LoadAllTableData;
GlobalEventBus.OnScheduleComplete += res => MessageBox.Show("调度执行完成:\r\n" + res);
}
/// 刷新所有物料/BOM/作业表格与图表
private void LoadAllTableData()
{
dgvMaterial.DataSource = _erpBiz.GetAllMaterial();
dgvBom.DataSource = _erpBiz.GetAllBom();
dgvWork.DataSource = _erpBiz.GetAllWorkOrder();
// 图表数据源
List<ChartDataDto> chartSource = new List<ChartDataDto>();
foreach (var wo in _erpBiz.GetAllWorkOrder())
{
chartSource.Add(new ChartDataDto
{
WorkName = wo.WorkNo,
PlanNum = wo.PlanQty,
ActualNum = wo.CompleteQty,
LoadAvg = new Random().Next(10, 85)
});
}
_chart.DataSource = chartSource;
}
#region 三种调度按钮逻辑(仅调用组件,无业务代码)
private async Task RunSingle()
{
if (cboSingleAxis.SelectedItem == null) return;
await _schedule.RunSingleTask(cboSingle.Text, "系统管理员");
}
private async Task RunCombine()
{
List<string> selAxis = new List<string>();
for (int i = 0; i < clbCombine.Items.Count; i++)
if (clbCombine.GetItemChecked(i)) selAxis.Add(clbCombine.Items[i].ToString());
if (selAxis.Count == 0) { MessageBox.Show("请勾选组轴"); return; }
await _schedule.RunCombineTask(selAxis, "系统管理员");
}
private async Task RunLink()
{
if (cboLinkGroup.SelectedItem == null) return;
await _schedule.RunLinkedTask(cboLink.Text, "系统管理员");
}
#endregion
#region ERP作业生命周期按钮
private void btnCreateWork_Click(object sender, EventArgs e)
{
if (!int.TryParse(txtPlanQty.Text, out int qty))
{
MessageBox.Show("请输入合法计划产量");
return;
}
var work = _erpBiz.CreateWork(txtBomNo.Text.Trim(), cboSingle.Text, qty);
MessageBox.Show($"作业创建成功:{work.WorkNo}");
}
private void btnPauseWork_Click(object sender, EventArgs e)
{
var sel = GetSelectWork();
if (sel == null) return;
bool ok = _erpBiz.PauseWork(sel);
MessageBox.Show(ok ? "暂停成功" : "当前状态禁止操作");
}
private void btnArchiveWork_Click(object sender, EventArgs e)
{
var sel = GetSelectWork();
if (sel == null) return;
bool ok = _erpBiz.ArchiveWork(sel);
MessageBox.Show(ok ? "ERP单据归档完成" : "状态不支持归档");
}
private WorkOrder GetSelectWork()
{
if (dgvWork.SelectedRows.Count == 0)
{
MessageBox.Show("请选中作业行");
return null;
}
return dgvWork.SelectedRows[0].DataBoundItem as WorkOrder;
}
#endregion
/// Excel导入物料按钮
private void ImportMatClick(object sender, EventArgs e)
{
var res = ExcelFixedTemplate.ImportMaterial();
if (res.Success && res.ImportMaterial.Count > 0)
LocalSqlDb.Insert(res.ImportMaterial);
MessageBox.Show(res.Msg);
LoadAllTableData();
}
#region 窗体控件定义(VS设计器自动生成)
private DataGridView dgvMaterial, dgvBom, dgvWork;
private ComboBox cboSingleAxis, cboLinkGroup;
private CheckedListBox clbCombine;
private TextBox txtLog, txtBomNo, txtPlanQty;
private Button btnCreateWork, btnPauseWork, btnArchiveWork;
private Panel panelChart;
private void InitializeComponent()
{
this.ClientSize = new Size(1300, 860);
// 省略VS自动布局代码
}
#endregion
}
}
六、Program 程序入口
csharp
运行
using System;
using System.Windows.Forms;
using _3C_MES_ERP.Forms;
namespace _3C_MES_ERP
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainCooperForm());
}
}
}
五、项目完整介绍
1. 项目解决核心痛点
- 业务代码碎片化 :原调度、Excel、绘图、数据库逻辑全部散落在窗体按钮事件内,维护困难;本方案按领域拆独立组件,单一职责,可单独复用。
- 参数硬编码 :所有数据库、线程、图表、ERP、Excel 路径统一放入
App.config,改配置无需修改代码。 - 多线程混乱:统一封装多线程调度组件,自动汇总所有并行线程执行结果并持久化,消除零散 Task/Thread 代码。
- 业务耦合严重:物料 / BOM / 组轴 / 作业分层隔离,通过事件总线解耦模块通信,无跨层硬依赖。
2. 核心业务能力清单
- 本地文件 SQLite 数据库 免数据库服务,程序启动自动建表、生成 15~17 组 3C 电子物料 / BOM 测试数据,SqlSugar 完整封装 CRUD。
- 组轴 + 动力设备协同执行引擎 模拟 3C 产线 SMT / 焊接 / 组装 / 检测四大组轴,工序执行自动变更设备负载,支持单轴暂停、整组紧急停机。
- 三模式多线程调度 ①单轴独立调度;②多轴并行组合调度;③流水线联控调度(一故障全流水线停止);所有线程执行文本自动组装存入调度日志。
- ERP 物料 BOM 完整协同流程 物料档案→BOM 结构→生产作业单,作业生命周期严格贴合金蝶 ERP 单据:创建→调度→运行→暂停→完成→归档,禁止跨状态非法操作。
- 标准化 Excel 固定模板 两套不可修改标准模板,支持下载空白模板、导入校验组别 15~17、批量导出物料 / BOM 数据。
- 原生 GDI 图表控件 不依赖第三方图表库,自绘产量柱状图 + 设备负载仪表盘,绘图参数全部从 App.config 读取,可嵌入任意窗体。
- 全局事件总线 日志、界面刷新、调度通知、作业状态变更统一发布订阅,各组件无直接依赖。
- WinForm 轻量化界面 界面只做展示与按钮交互,无任何复杂业务逻辑,所有操作调用底层组件。
3. 架构优势
- 组件可移植:Db/Excel/GDI/ 调度引擎均可单独提取到其他 WinForm 项目复用;
- 易扩展:新增工序、报表、ERP 接口仅扩展对应组件,不改动其他模块;
- 环境快速切换:修改 App.config 参数即可切换数据库、ERP 地址、线程并发数;
- 可维护:业务按领域拆分,定位 BUG 只需进入对应组件,不用翻遍窗体零散代码;
- 工业场景适配:完全贴合 3C 数码组装 MES+ERP 协同生产业务流程。