物料计划工站标定远程更新中间件组件
物料调拨计划相关,产线的工站流程的形式层级单元布局标定工位任务,添加远程更新,缓存更新按钮,开发一个多指令端/应用调度模式的Winform中间件功能界面组件
你需要在原有产线工站层级布局标定、工位任务绑定的基础上,新增物料调拨计划关联、远程更新、缓存更新核心功能,并保留多指令端 / 应用调度模式,最终形成一套完整的 WinForm 中间件界面组件,适配产线工站从 “物料调拨→布局标定→任务绑定→远程同步→缓存刷新” 的全流程管理。
以下是完整的设计方案 + 可直接运行的代码实现,包含物料调拨关联、远程更新 / 缓存更新核心模块,以及整合后的主界面。
一、 核心设计扩展(新增物料调拨 + 远程 / 缓存更新)
1. 物料调拨计划关联模型
| 层级 | 物料调拨关联要素 | 核心规则 |
|---|---|---|
| 产线 | 调拨计划编号、物料编码、总调拨数量 | 产线级调拨计划需覆盖下属所有工段的物料需求 |
| 工段 | 调拨子计划编号、物料需求数量、调拨时间窗 | 工段调拨数量 = 下属工位需求数量之和,调拨时间窗需早于工位任务启动时间 |
| 工位 | 物料 BOM 清单、单台物料用量、已调拨数量 | 工位任务绑定后自动计算物料需求;调拨完成后更新 “已调拨数量”,触发任务可执行 |
2. 远程更新 / 缓存更新设计
| 功能 | 核心逻辑 | 触发场景 |
|---|---|---|
| 远程更新 | 从 MES/ERP 服务端拉取最新的:物料调拨计划、工站布局、工位任务数据 → 覆盖本地数据 | 1. 手动点击 “远程更新” 按钮;2. 定时自动更新 |
| 缓存更新 | 清空本地内存缓存 → 重新从本地数据库加载最新数据 → 刷新界面展示 | 1. 手动点击 “缓存更新” 按钮;2. 本地数据变更后自动触发 |
3. 多指令端扩展(新增物料调拨指令)
| 新增指令端 | 核心指令类型 | 调度规则 |
|---|---|---|
| 物料调拨端 | 调拨计划下发、调拨数量调整、调拨完成 | 仅对 “已标定布局” 的工位生效;调拨完成后工位任务状态变为 “可执行” |
二、 完整代码实现(新增 + 整合)
1. 扩展数据模型(物料调拨 + 远程 / 缓存配置)
csharp
运行
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Windows.Forms;
using Dapper;
using Newtonsoft.Json;
// ========== 新增:物料调拨计划模型 ==========
///
/// 产线级物料调拨计划
///
public class MaterialAllotPlan
{
///
/// 调拨计划编号
///
public string AllotPlanNo { get; set; }
///
/// 关联产线编码
///
public string LineCode { get; set; }
///
/// 物料编码
///
public string MaterialCode { get; set; }
///
/// 物料名称
///
public string MaterialName { get; set; }
///
/// 总调拨数量
///
public decimal TotalAllotQty { get; set; }
///
/// 调拨状态(待调拨/调拨中/已完成/取消)
///
public string AllotStatus { get; set; }
///
/// 计划调拨开始时间
///
public DateTime PlanAllotStartTime { get; set; }
///
/// 计划调拨完成时间
///
public DateTime PlanAllotEndTime { get; set; }
///
/// 下属工段调拨子计划
///
public List SubPlans { get; set; } = new List();
}
///
/// 工段级物料调拨子计划
///
public class MaterialAllotSubPlan
{
///
/// 调拨子计划编号
///
public string AllotSubPlanNo { get; set; }
///
/// 关联调拨计划编号
///
public string AllotPlanNo { get; set; }
///
/// 关联工段编码
///
public string SectionCode { get; set; }
///
/// 工段调拨数量
///
public decimal SectionAllotQty { get; set; }
///
/// 已调拨数量
///
public decimal AllottedQty { get; set; }
///
/// 调拨时间窗(开始)
///
public DateTime AllotTimeWindowStart { get; set; }
///
/// 调拨时间窗(结束)
///
public DateTime AllotTimeWindowEnd { get; set; }
}
///
/// 工位物料需求(关联调拨计划)
///
public class StationMaterialDemand
{
///
/// 工位编码
///
public string StationCode { get; set; }
///
/// 关联调拨子计划编号
///
public string AllotSubPlanNo { get; set; }
///
/// 物料BOM清单(JSON格式:{物料编码:单台用量})
///
public string MaterialBOM { get; set; }
///
/// 任务需求总用量 = 单台用量 * 任务数量
///
public decimal TotalDemandQty { get; set; }
///
/// 已调拨数量
///
public decimal AllottedQty { get; set; }
///
/// 物料齐套状态(齐套/缺料/待调拨)
///
public string MaterialStatus { get; set; }
}
// ========== 新增:远程更新/缓存配置模型 ==========
///
/// 远程更新配置
///
public class RemoteUpdateConfig
{
///
/// ERP/MES服务端接口地址
///
public string RemoteApiUrl { get; set; } = "http://192.168.1.100:8080/api/mes/";
///
/// 接口访问Token
///
public string AccessToken { get; set; } = "default_token_123456";
///
/// 定时更新间隔(分钟)
///
public int AutoUpdateInterval { get; set; } = 30;
///
/// 最后远程更新时间
///
public DateTime LastRemoteUpdateTime { get; set; }
}
///
/// 本地缓存管理类
///
public static class LocalCacheManager
{
// 产线布局缓存
private static Dictionary _lineLayoutCache = new Dictionary();
// 物料调拨计划缓存
private static Dictionary _allotPlanCache = new Dictionary();
// 指令日志缓存
private static DataTable _commandLogCache = new DataTable();
///
/// 添加/更新产线布局缓存
///
public static void SetLineLayoutCache(string lineCode, ProductionLineLayout layout)
{
if (_lineLayoutCache.ContainsKey(lineCode))
_lineLayoutCache[lineCode] = layout;
else
_lineLayoutCache.Add(lineCode, layout);
}
///
/// 获取产线布局缓存
///
public static ProductionLineLayout GetLineLayoutCache(string lineCode)
{
return _lineLayoutCache.TryGetValue(lineCode, out var layout) ? layout : null;
}
///
/// 添加/更新调拨计划缓存
///
public static void SetAllotPlanCache(string planNo, MaterialAllotPlan plan)
{
if (_allotPlanCache.ContainsKey(planNo))
_allotPlanCache[planNo] = plan;
else
_allotPlanCache.Add(planNo, plan);
}
///
/// 获取调拨计划缓存
///
public static MaterialAllotPlan GetAllotPlanCache(string planNo)
{
return _allotPlanCache.TryGetValue(planNo, out var plan) ? plan : null;
}
///
/// 设置指令日志缓存
///
public static void SetCommandLogCache(DataTable dt)
{
_commandLogCache = dt.Copy();
}
///
/// 获取指令日志缓存
///
public static DataTable GetCommandLogCache()
{
return _commandLogCache;
}
///
/// 清空指定产线的缓存
///
public static void ClearLineCache(string lineCode)
{
if (_lineLayoutCache.ContainsKey(lineCode))
_lineLayoutCache.Remove(lineCode);
// 清空关联的调拨计划缓存
var planKeys = _allotPlanCache.Keys.Where(k => _allotPlanCache[k].LineCode == lineCode).ToList();
foreach (var key in planKeys)
_allotPlanCache.Remove(key);
}
///
/// 清空所有缓存
///
public static void ClearAllCache()
{
_lineLayoutCache.Clear();
_allotPlanCache.Clear();
_commandLogCache = new DataTable();
}
}
// ========== 原有模型(保留) ==========
public class ProductionLineLayout
{
public string LineCode { get; set; }
public string LineName { get; set; }
public int CanvasWidth { get; set; }
public int CanvasHeight { get; set; }
public string LayoutStatus { get; set; }
public DateTime LastCalibrateTime { get; set; }
public string Calibrator { get; set; }
public List SectionLayouts { get; set; } = new List();
}
public class WorkSectionLayout
{
public string SectionCode { get; set; }
public string SectionName { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int SortNo { get; set; }
public List StationLayouts { get; set; } = new List();
}
public class WorkStationLayout
{
public string StationCode { get; set; }
public string StationName { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string StationType { get; set; }
public string ProcessCode { get; set; }
public string BindOrderNo { get; set; }
public decimal BindTaskQty { get; set; }
public string TaskStatus { get; set; }
public string PreStationCodes { get; set; }
// 新增:关联物料需求
public string MaterialStatus { get; set; } = "待调拨";
}
public class LayoutCommand
{
public string CommandId { get; set; }
public string SourceTerminal { get; set; }
public string TargetTerminal { get; set; }
public string CommandType { get; set; }
public string LineCode { get; set; }
public string StationCode { get; set; }
public string CommandContent { get; set; }
public string CommandStatus { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? ExecuteTime { get; set; }
}
// 配置/日志辅助类(保留)
public static class ConfigHelper
{
public static string GetConnStr()
{
return "Data Source=.;Initial Catalog=MES_AssemblyLine;Integrated Security=True;";
}
// 新增:获取远程更新配置
public static RemoteUpdateConfig GetRemoteUpdateConfig()
{
return new RemoteUpdateConfig
{
RemoteApiUrl = Properties.Settings.Default.RemoteApiUrl,
AccessToken = Properties.Settings.Default.AccessToken,
AutoUpdateInterval = Properties.Settings.Default.AutoUpdateInterval,
LastRemoteUpdateTime = Properties.Settings.Default.LastRemoteUpdateTime
};
}
// 新增:保存远程更新配置
public static void SaveRemoteUpdateConfig(RemoteUpdateConfig config)
{
Properties.Settings.Default.RemoteApiUrl = config.RemoteApiUrl;
Properties.Settings.Default.AccessToken = config.AccessToken;
Properties.Settings.Default.AutoUpdateInterval = config.AutoUpdateInterval;
Properties.Settings.Default.LastRemoteUpdateTime = config.LastRemoteUpdateTime;
Properties.Settings.Default.Save();
}
}
public static class LogHelper
{
public static void Info(string message)
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [INFO] {message}");
}
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. 核心扩展:远程更新 / 缓存更新服务
csharp
运行
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
///
/// 远程更新+缓存更新核心服务
///
public class RemoteAndCacheService
{
private readonly HttpClient _httpClient;
private readonly RemoteUpdateConfig _remoteConfig;
public RemoteAndCacheService()
{
_httpClient = new HttpClient();
_remoteConfig = ConfigHelper.GetRemoteUpdateConfig();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _remoteConfig.AccessToken);
}
#region 核心功能1:远程更新(从MES/ERP拉取最新数据)
///
/// 远程更新指定产线的所有数据(布局+调拨计划+任务)
///
/// 产线编码
/// 更新结果
public async Task RemoteUpdateLineDataAsync(string lineCode)
{
try
{
LogHelper.Info($"开始远程更新产线{lineCode}数据,接口地址:{_remoteConfig.RemoteApiUrl}");
// 1. 拉取产线布局数据
var layoutResponse = await _httpClient.GetAsync($"{_remoteConfig.RemoteApiUrl}LineLayout/GetByLineCode?lineCode={lineCode}");
if (!layoutResponse.IsSuccessStatusCode)
{
LogHelper.Error($"拉取产线{lineCode}布局失败,状态码:{layoutResponse.StatusCode}");
return false;
}
var layoutJson = await layoutResponse.Content.ReadAsStringAsync();
var lineLayout = JsonConvert.DeserializeObject(layoutJson);
// 2. 拉取物料调拨计划
var allotPlanResponse = await _httpClient.GetAsync($"{_remoteConfig.RemoteApiUrl}MaterialAllot/GetByLineCode?lineCode={lineCode}");
if (!allotPlanResponse.IsSuccessStatusCode)
{
LogHelper.Error($"拉取产线{lineCode}调拨计划失败,状态码:{allotPlanResponse.StatusCode}");
return false;
}
var allotPlanJson = await allotPlanResponse.Content.ReadAsStringAsync();
var allotPlan = JsonConvert.DeserializeObject(allotPlanJson);
// 3. 拉取工位任务数据
var taskResponse = await _httpClient.GetAsync($"{_remoteConfig.RemoteApiUrl}StationTask/GetByLineCode?lineCode={lineCode}");
if (!taskResponse.IsSuccessStatusCode)
{
LogHelper.Error($"拉取产线{lineCode}工位任务失败,状态码:{taskResponse.StatusCode}");
return false;
}
var taskJson = await taskResponse.Content.ReadAsStringAsync();
var stationTasks = JsonConvert.DeserializeObject>(taskJson);
// 4. 同步到本地数据库
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
using (var tran = conn.BeginTransaction())
{
try
{
// 同步产线布局
SyncLineLayoutToLocal(conn, tran, lineLayout);
// 同步调拨计划
SyncAllotPlanToLocal(conn, tran, allotPlan);
// 同步工位任务
SyncStationTasksToLocal(conn, tran, stationTasks);
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
LogHelper.Error("远程数据同步到本地失败", ex);
return false;
}
}
// 5. 更新最后同步时间
_remoteConfig.LastRemoteUpdateTime = DateTime.Now;
ConfigHelper.SaveRemoteUpdateConfig(_remoteConfig);
// 6. 更新本地缓存
LocalCacheManager.SetLineLayoutCache(lineCode, lineLayout);
if (allotPlan != null)
LocalCacheManager.SetAllotPlanCache(allotPlan.AllotPlanNo, allotPlan);
LogHelper.Info($"产线{lineCode}远程更新完成,最后更新时间:{_remoteConfig.LastRemoteUpdateTime}");
return true;
}
catch (Exception ex)
{
LogHelper.Error($"产线{lineCode}远程更新失败", ex);
return false;
}
}
///
/// 同步产线布局到本地数据库
///
private void SyncLineLayoutToLocal(SqlConnection conn, SqlTransaction tran, ProductionLineLayout layout)
{
// 产线布局
conn.Execute(@"MERGE INTO ProductionLineLayout AS T
USING (VALUES (@LineCode, @LineName, @CanvasWidth, @CanvasHeight, @LayoutStatus, @LastCalibrateTime, @Calibrator)) AS S
(LineCode, LineName, CanvasWidth, CanvasHeight, LayoutStatus, LastCalibrateTime, Calibrator)
ON T.LineCode = S.LineCode
WHEN MATCHED THEN
UPDATE SET LineName = S.LineName, CanvasWidth = S.CanvasWidth, CanvasHeight = S.CanvasHeight,
LayoutStatus = S.LayoutStatus, LastCalibrateTime = S.LastCalibrateTime, Calibrator = S.Calibrator
WHEN NOT MATCHED THEN
INSERT (LineCode, LineName, CanvasWidth, CanvasHeight, LayoutStatus, LastCalibrateTime, Calibrator)
VALUES (S.LineCode, S.LineName, S.CanvasWidth, S.CanvasHeight, S.LayoutStatus, S.LastCalibrateTime, S.Calibrator);",
layout, tran);
// 工段布局
foreach (var section in layout.SectionLayouts)
{
conn.Execute(@"MERGE INTO WorkSectionLayout AS T
USING (VALUES (@SectionCode, @SectionName, @LineCode, @X, @Y, @Width, @Height, @SortNo)) AS S
(SectionCode, SectionName, LineCode, X, Y, Width, Height, SortNo)
ON T.SectionCode = S.SectionCode
WHEN MATCHED THEN
UPDATE SET SectionName = S.SectionName, LineCode = S.LineCode, X = S.X, Y = S.Y,
Width = S.Width, Height = S.Height, SortNo = S.SortNo
WHEN NOT MATCHED THEN
INSERT (SectionCode, SectionName, LineCode, X, Y, Width, Height, SortNo)
VALUES (S.SectionCode, S.SectionName, S.LineCode, S.X, S.Y, S.Width, S.Height, S.SortNo);",
new { section.SectionCode, section.SectionName, layout.LineCode, section.X, section.Y, section.Width, section.Height, section.SortNo }, tran);
// 工位布局
foreach (var station in section.StationLayouts)
{
conn.Execute(@"MERGE INTO WorkStationLayout AS T
USING (VALUES (@StationCode, @StationName, @SectionCode, @X, @Y, @Width, @Height,
@StationType, @ProcessCode, @BindOrderNo, @BindTaskQty, @TaskStatus, @PreStationCodes, @MaterialStatus)) AS S
(StationCode, StationName, SectionCode, X, Y, Width, Height,
StationType, ProcessCode, BindOrderNo, BindTaskQty, TaskStatus, PreStationCodes, MaterialStatus)
ON T.StationCode = S.StationCode
WHEN MATCHED THEN
UPDATE SET StationName = S.StationName, SectionCode = S.SectionCode, X = S.X, Y = S.Y,
Width = S.Width, Height = S.Height, StationType = S.StationType,
ProcessCode = S.ProcessCode, BindOrderNo = S.BindOrderNo, BindTaskQty = S.BindTaskQty,
TaskStatus = S.TaskStatus, PreStationCodes = S.PreStationCodes, MaterialStatus = S.MaterialStatus
WHEN NOT MATCHED THEN
INSERT (StationCode, StationName, SectionCode, X, Y, Width, Height,
StationType, ProcessCode, BindOrderNo, BindTaskQty, TaskStatus, PreStationCodes, MaterialStatus)
VALUES (S.StationCode, S.StationName, S.SectionCode, S.X, S.Y, S.Width, S.Height,
S.StationType, S.ProcessCode, S.BindOrderNo, S.BindTaskQty, S.TaskStatus, S.PreStationCodes, S.MaterialStatus);",
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, station.MaterialStatus }, tran);
}
}
}
///
/// 同步调拨计划到本地数据库
///
private void SyncAllotPlanToLocal(SqlConnection conn, SqlTransaction tran, MaterialAllotPlan plan)
{
if (plan == null) return;
// 主计划
conn.Execute(@"MERGE INTO MaterialAllotPlan AS T
USING (VALUES (@AllotPlanNo, @LineCode, @MaterialCode, @MaterialName, @TotalAllotQty,
@AllotStatus, @PlanAllotStartTime, @PlanAllotEndTime)) AS S
(AllotPlanNo, LineCode, MaterialCode, MaterialName, TotalAllotQty,
AllotStatus, PlanAllotStartTime, PlanAllotEndTime)
ON T.AllotPlanNo = S.AllotPlanNo
WHEN MATCHED THEN
UPDATE SET LineCode = S.LineCode, MaterialCode = S.MaterialCode, MaterialName = S.MaterialName,
TotalAllotQty = S.TotalAllotQty, AllotStatus = S.AllotStatus,
PlanAllotStartTime = S.PlanAllotStartTime, PlanAllotEndTime = S.PlanAllotEndTime
WHEN NOT MATCHED THEN
INSERT (AllotPlanNo, LineCode, MaterialCode, MaterialName, TotalAllotQty,
AllotStatus, PlanAllotStartTime, PlanAllotEndTime)
VALUES (S.AllotPlanNo, S.LineCode, S.MaterialCode, S.MaterialName, S.TotalAllotQty,
S.AllotStatus, S.PlanAllotStartTime, S.PlanAllotEndTime);",
plan, tran);
// 子计划
foreach (var subPlan in plan.SubPlans)
{
conn.Execute(@"MERGE INTO MaterialAllotSubPlan AS T
USING (VALUES (@AllotSubPlanNo, @AllotPlanNo, @SectionCode, @SectionAllotQty,
@AllottedQty, @AllotTimeWindowStart, @AllotTimeWindowEnd)) AS S
(AllotSubPlanNo, AllotPlanNo, SectionCode, SectionAllotQty,
AllottedQty, AllotTimeWindowStart, AllotTimeWindowEnd)
ON T.AllotSubPlanNo = S.AllotSubPlanNo
WHEN MATCHED THEN
UPDATE SET AllotPlanNo = S.AllotPlanNo, SectionCode = S.SectionCode, SectionAllotQty = S.SectionAllotQty,
AllottedQty = S.AllottedQty, AllotTimeWindowStart = S.AllotTimeWindowStart,
AllotTimeWindowEnd = S.AllotTimeWindowEnd
WHEN NOT MATCHED THEN
INSERT (AllotSubPlanNo, AllotPlanNo, SectionCode, SectionAllotQty,
AllottedQty, AllotTimeWindowStart, AllotTimeWindowEnd)
VALUES (S.AllotSubPlanNo, S.AllotPlanNo, S.SectionCode, S.SectionAllotQty,
S.AllottedQty, S.AllotTimeWindowStart, S.AllotTimeWindowEnd);",
subPlan, tran);
}
}
///
/// 同步工位任务到本地数据库
///
private void SyncStationTasksToLocal(SqlConnection conn, SqlTransaction tran, List stations)
{
foreach (var station in stations)
{
conn.Execute(@"UPDATE WorkStationLayout SET BindOrderNo=@BindOrderNo, BindTaskQty=@BindTaskQty,
TaskStatus=@TaskStatus, MaterialStatus=@MaterialStatus
WHERE StationCode=@StationCode",
new { station.BindOrderNo, station.BindTaskQty, station.TaskStatus, station.MaterialStatus, station.StationCode }, tran);
}
}
#endregion
#region 核心功能2:缓存更新(清空+重新加载)
///
/// 更新指定产线的本地缓存(从本地数据库重新加载)
///
/// 产线编码
/// 更新结果
public bool UpdateLocalCache(string lineCode)
{
try
{
LogHelper.Info($"开始更新产线{lineCode}本地缓存");
// 1. 清空旧缓存
LocalCacheManager.ClearLineCache(lineCode);
// 2. 从本地数据库重新加载
var lineLayout = LoadLineLayoutFromLocal(lineCode);
var allotPlan = LoadAllotPlanFromLocal(lineCode);
var commandLogs = LoadCommandLogsFromLocal(lineCode);
// 3. 写入新缓存
if (lineLayout != null)
LocalCacheManager.SetLineLayoutCache(lineCode, lineLayout);
if (allotPlan != null)
LocalCacheManager.SetAllotPlanCache(allotPlan.AllotPlanNo, allotPlan);
LocalCacheManager.SetCommandLogCache(commandLogs);
LogHelper.Info($"产线{lineCode}缓存更新完成");
return true;
}
catch (Exception ex)
{
LogHelper.Error($"产线{lineCode}缓存更新失败", ex);
return false;
}
}
///
/// 从本地数据库加载产线布局
///
private ProductionLineLayout LoadLineLayoutFromLocal(string lineCode)
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
var layout = conn.QuerySingle(
"SELECT LineCode, LineName, CanvasWidth, CanvasHeight, LayoutStatus, LastCalibrateTime, Calibrator " +
"FROM ProductionLineLayout WHERE LineCode = @LineCode", new { LineCode = lineCode });
if (layout != null)
{
layout.SectionLayouts = conn.Query(
"SELECT SectionCode, SectionName, X, Y, Width, Height, SortNo " +
"FROM WorkSectionLayout WHERE LineCode = @LineCode ORDER BY SortNo", new { LineCode = lineCode }).ToList();
foreach (var section in layout.SectionLayouts)
{
section.StationLayouts = conn.Query(
"SELECT StationCode, StationName, X, Y, Width, Height, StationType, ProcessCode, " +
"BindOrderNo, BindTaskQty, TaskStatus, PreStationCodes, MaterialStatus " +
"FROM WorkStationLayout WHERE SectionCode = @SectionCode", new { SectionCode = section.SectionCode }).ToList();
}
}
return layout;
}
}
///
/// 从本地数据库加载调拨计划
///
private MaterialAllotPlan LoadAllotPlanFromLocal(string lineCode)
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
var plan = conn.QuerySingle(
"SELECT AllotPlanNo, LineCode, MaterialCode, MaterialName, TotalAllotQty, " +
"AllotStatus, PlanAllotStartTime, PlanAllotEndTime " +
"FROM MaterialAllotPlan WHERE LineCode = @LineCode", new { LineCode = lineCode });
if (plan != null)
{
plan.SubPlans = conn.Query(
"SELECT AllotSubPlanNo, AllotPlanNo, SectionCode, SectionAllotQty, " +
"AllottedQty, AllotTimeWindowStart, AllotTimeWindowEnd " +
"FROM MaterialAllotSubPlan WHERE AllotPlanNo = @AllotPlanNo", new { AllotPlanNo = plan.AllotPlanNo }).ToList();
}
return plan;
}
}
///
/// 从本地数据库加载指令日志
///
private DataTable LoadCommandLogsFromLocal(string lineCode)
{
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(ConfigHelper.GetConnStr()))
{
string sql = "SELECT CommandId, SourceTerminal, TargetTerminal, CommandType, " +
"LineCode, StationCode, CommandStatus, CreateTime, ExecuteTime " +
"FROM LayoutCommandLog WHERE LineCode = @LineCode ORDER BY CreateTime DESC";
var logs = conn.Query(sql, new { LineCode = lineCode });
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
}
3. 整合后的主界面组件(新增按钮 + 物料调拨面板)
csharp
运行
///
/// 整合物料调拨+远程更新+缓存更新的主界面
///
public partial class MainLayoutDispatchForm : Form
{
private readonly LineLayoutCalibrateControl _layoutControl;
private readonly StationTaskDispatchService _dispatchService;
private readonly RemoteAndCacheService _remoteCacheService;
private string _currentLineCode;
private System.Windows.Forms.Timer _autoUpdateTimer;
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();
_remoteCacheService = new RemoteAndCacheService();
// 4. 加载基础数据
LoadLineList();
LoadAllotPlanList();
// 5. 初始化定时自动更新
InitAutoUpdateTimer();
// 6. 绑定按钮事件(核心新增)
btnRemoteUpdate.Click += BtnRemoteUpdate_Click;
btnCacheUpdate.Click += BtnCacheUpdate_Click;
btnBindAllotPlan.Click += BtnBindAllotPlan_Click;
}
#region 初始化与基础数据加载
///
/// 加载产线列表
///
private void LoadLineList()
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
var lines = conn.Query(
"SELECT LineCode, LineName FROM ProductionLineLayout ORDER BY LineCode");
cboLineCode.DataSource = lines;
cboLineCode.DisplayMember = "LineName";
cboLineCode.ValueMember = "LineCode";
}
}
///
/// 加载调拨计划列表
///
private void LoadAllotPlanList()
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
var plans = conn.Query(
"SELECT AllotPlanNo, MaterialName, TotalAllotQty, AllotStatus FROM MaterialAllotPlan ORDER BY AllotPlanNo");
cboAllotPlanNo.DataSource = plans;
cboAllotPlanNo.DisplayMember = "AllotPlanNo";
cboAllotPlanNo.ValueMember = "AllotPlanNo";
}
}
///
/// 初始化定时自动更新
///
private void InitAutoUpdateTimer()
{
var config = ConfigHelper.GetRemoteUpdateConfig();
_autoUpdateTimer = new System.Windows.Forms.Timer
{
Interval = config.AutoUpdateInterval * 60 * 1000 // 分钟转毫秒
};
_autoUpdateTimer.Tick += async (s, e) =>
{
if (!string.IsNullOrEmpty(_currentLineCode))
{
await _remoteCacheService.RemoteUpdateLineDataAsync(_currentLineCode);
// 更新界面(需跨线程)
this.Invoke(new Action(() =>
{
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
lblLastUpdateTime.Text = $"最后远程更新:{config.LastRemoteUpdateTime:yyyy-MM-dd HH:mm:ss}";
}));
}
};
_autoUpdateTimer.Start();
// 显示最后更新时间
lblLastUpdateTime.Text = $"最后远程更新:{config.LastRemoteUpdateTime:yyyy-MM-dd HH:mm:ss}";
}
#endregion
#region 核心按钮事件(新增:远程更新/缓存更新/调拨计划绑定)
///
/// 远程更新按钮点击事件
///
private async void BtnRemoteUpdate_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentLineCode))
{
MessageBox.Show("请先选择产线!");
return;
}
// 禁用按钮,防止重复点击
btnRemoteUpdate.Enabled = false;
btnRemoteUpdate.Text = "远程更新中...";
try
{
var result = await _remoteCacheService.RemoteUpdateLineDataAsync(_currentLineCode);
if (result)
{
MessageBox.Show($"产线{_currentLineCode}远程更新成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 刷新界面
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
LoadAllotPlanList();
// 更新最后更新时间
var config = ConfigHelper.GetRemoteUpdateConfig();
lblLastUpdateTime.Text = $"最后远程更新:{config.LastRemoteUpdateTime:yyyy-MM-dd HH:mm:ss}";
}
else
{
MessageBox.Show($"产线{_currentLineCode}远程更新失败!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
finally
{
// 恢复按钮
btnRemoteUpdate.Enabled = true;
btnRemoteUpdate.Text = "远程更新";
}
}
///
/// 缓存更新按钮点击事件
///
private void BtnCacheUpdate_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentLineCode))
{
MessageBox.Show("请先选择产线!");
return;
}
btnCacheUpdate.Enabled = false;
btnCacheUpdate.Text = "缓存更新中...";
try
{
var result = _remoteCacheService.UpdateLocalCache(_currentLineCode);
if (result)
{
MessageBox.Show($"产线{_currentLineCode}缓存更新成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 刷新界面
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
else
{
MessageBox.Show($"产线{_currentLineCode}缓存更新失败!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
finally
{
btnCacheUpdate.Enabled = true;
btnCacheUpdate.Text = "缓存更新";
}
}
///
/// 绑定调拨计划到工位
///
private void BtnBindAllotPlan_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(txtStationCode.Text) || string.IsNullOrEmpty(cboAllotPlanNo.Text))
{
MessageBox.Show("请选择工位和调拨计划!");
return;
}
try
{
// 1. 绑定调拨计划到工位
var result = _dispatchService.BindAllotPlanToStation(
txtStationCode.Text,
cboAllotPlanNo.SelectedValue.ToString(),
numDemandQty.Value);
if (result)
{
MessageBox.Show("调拨计划绑定成功!");
// 刷新界面
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
else
{
MessageBox.Show("调拨计划绑定失败!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
LogHelper.Error("绑定调拨计划失败", ex);
MessageBox.Show($"绑定失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#endregion
#region 原有事件(保留+扩展)
private void cboLineCode_SelectedIndexChanged(object sender, EventArgs e)
{
if (cboLineCode.SelectedValue == null) return;
_currentLineCode = cboLineCode.SelectedValue.ToString();
// 优先从缓存加载,缓存无则从数据库加载
var cachedLayout = LocalCacheManager.GetLineLayoutCache(_currentLineCode);
if (cachedLayout != null)
{
// 缓存加载
_layoutControl.LoadCachedLayout(cachedLayout);
}
else
{
// 数据库加载
_layoutControl.LoadLineLayout(_currentLineCode);
}
// 刷新指令日志(优先缓存)
var cachedLogs = LocalCacheManager.GetCommandLogCache();
dgvCommandLogs.DataSource = cachedLogs.Rows.Count > 0 ? cachedLogs : _dispatchService.QueryCommandLogs(_currentLineCode);
}
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;
btnBindAllotPlan.Enabled = false;
}
private void OnStationSelected(object sender, StationSelectedEventArgs e)
{
lblStationInfo.Text = $"当前选中工位:{e.SelectedStation.StationName}({e.SelectedStation.StationCode})
" +
$"绑定工单:{e.SelectedStation.BindOrderNo}
" +
$"任务数量:{e.SelectedStation.BindTaskQty}
" +
$"任务状态:{e.SelectedStation.TaskStatus}
" +
$"物料状态:{e.SelectedStation.MaterialStatus}";
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 == "待执行" && e.SelectedStation.MaterialStatus == "齐套";
btnPauseTask.Enabled = e.SelectedStation.TaskStatus == "执行中";
btnCompleteTask.Enabled = e.SelectedStation.TaskStatus == "执行中";
btnBindAllotPlan.Enabled = true; // 启用调拨计划绑定
}
private void btnSaveLayout_Click(object sender, EventArgs e)
{
var result = _layoutControl.SaveLineLayout();
if (result)
{
MessageBox.Show("产线布局标定结果保存成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 更新缓存
LocalCacheManager.SetLineLayoutCache(_currentLineCode, _layoutControl.GetCurrentLayout());
}
}
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();
// 更新缓存
LocalCacheManager.ClearLineCache(_currentLineCode);
}
}
private void btnStartTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.StartStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务启动成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
private void btnPauseTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.PauseStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务暂停成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
private void btnCompleteTask_Click(object sender, EventArgs e)
{
var result = _dispatchService.CompleteStationTask(txtStationCode.Text);
if (result)
{
MessageBox.Show("工位任务完成成功!");
_layoutControl.LoadLineLayout(_currentLineCode);
RefreshCommandLogs();
}
}
private void RefreshCommandLogs()
{
var dt = LocalCacheManager.GetCommandLogCache().Rows.Count > 0
? LocalCacheManager.GetCommandLogCache()
: _dispatchService.QueryCommandLogs(_currentLineCode);
dgvCommandLogs.DataSource = dt;
}
#endregion
#region 窗体关闭事件(停止定时任务)
protected override void OnFormClosing(FormClosingEventArgs e)
{
_autoUpdateTimer?.Stop();
_autoUpdateTimer?.Dispose();
base.OnFormClosing(e);
}
#endregion
}
// ========== 扩展StationTaskDispatchService(新增调拨计划绑定) ==========
public partial class StationTaskDispatchService
{
///
/// 绑定调拨计划到工位
///
public bool BindAllotPlanToStation(string stationCode, string allotPlanNo, decimal demandQty)
{
try
{
using (var conn = new SqlConnection(ConfigHelper.GetConnStr()))
{
// 1. 获取调拨子计划
var subPlan = conn.QuerySingle(
"SELECT sp.* FROM MaterialAllotSubPlan sp " +
"JOIN WorkStationLayout s ON sp.SectionCode = s.SectionCode " +
"WHERE s.StationCode = @StationCode AND sp.AllotPlanNo = @AllotPlanNo",
new { StationCode = stationCode, AllotPlanNo = allotPlanNo });
if (subPlan == null)
{
LogHelper.Error($"工位{stationCode}无匹配的调拨子计划");
return false;
}
// 2. 更新工位物料需求
var existDemand = conn.ExecuteScalar(
"SELECT COUNT(1) FROM StationMaterialDemand WHERE StationCode = @StationCode",
new { StationCode = stationCode }) > 0;
if (existDemand)
{
conn.Execute(
"UPDATE StationMaterialDemand SET AllotSubPlanNo=@AllotSubPlanNo, TotalDemandQty=@TotalDemandQty, " +
"MaterialStatus='待调拨' WHERE StationCode=@StationCode",
new { AllotSubPlanNo = subPlan.AllotSubPlanNo, TotalDemandQty = demandQty, StationCode = stationCode });
}
else
{
conn.Execute(
"INSERT INTO StationMaterialDemand (StationCode, AllotSubPlanNo, TotalDemandQty, AllottedQty, MaterialStatus) " +
"VALUES (@StationCode, @AllotSubPlanNo, @TotalDemandQty, 0, '待调拨')",
new { StationCode = stationCode, AllotSubPlanNo = subPlan.AllotSubPlanNo, TotalDemandQty = demandQty });
}
// 3. 更新工位物料状态
conn.Execute(
"UPDATE WorkStationLayout SET MaterialStatus='待调拨' WHERE StationCode=@StationCode",
new { StationCode = stationCode });
// 4. 生成调拨绑定指令
var command = new LayoutCommand
{
CommandId = Guid.NewGuid().ToString("N"),
SourceTerminal = "物料调拨端",
TargetTerminal = "MES_EXEC",
CommandType = "调拨计划绑定",
LineCode = conn.QuerySingle(
"SELECT l.LineCode FROM WorkStationLayout s " +
"JOIN WorkSectionLayout sl ON s.SectionCode = sl.SectionCode " +
"JOIN ProductionLineLayout l ON sl.LineCode = l.LineCode " +
"WHERE s.StationCode = @StationCode", new { StationCode = stationCode }),
StationCode = stationCode,
CommandContent = JsonConvert.SerializeObject(new
{
AllotPlanNo = allotPlanNo,
AllotSubPlanNo = subPlan.AllotSubPlanNo,
DemandQty = demandQty,
BindTime = DateTime.Now
}),
CommandStatus = "已执行",
CreateTime = DateTime.Now,
ExecuteTime = DateTime.Now
};
SaveCommandLog(command);
LogHelper.Info($"工位{stationCode}绑定调拨计划{allotPlanNo}成功,需求数量:{demandQty}");
return true;
}
}
catch (Exception ex)
{
LogHelper.Error($"工位{stationCode}绑定调拨计划失败", ex);
return false;
}
}
}
// ========== 扩展LineLayoutCalibrateControl(缓存加载) ==========
public partial class LineLayoutCalibrateControl
{
///
/// 从缓存加载布局
///
public void LoadCachedLayout(ProductionLineLayout layout)
{
_currentLineLayout = layout;
this.Refresh();
}
///
/// 获取当前布局
///
public ProductionLineLayout GetCurrentLayout()
{
return _currentLineLayout;
}
}
4. 数据库表扩展(物料调拨相关)
sql
-- 新增:产线物料调拨计划表
CREATE TABLE MaterialAllotPlan (
AllotPlanNo VARCHAR(50) PRIMARY KEY,
LineCode VARCHAR(50) NOT NULL,
MaterialCode VARCHAR(50) NOT NULL,
MaterialName VARCHAR(100) NOT NULL,
TotalAllotQty DECIMAL(18,2) NOT NULL,
AllotStatus VARCHAR(20) DEFAULT '待调拨', -- 待调拨/调拨中/已完成/取消
PlanAllotStartTime DATETIME NOT NULL,
PlanAllotEndTime DATETIME NOT NULL,
FOREIGN KEY (LineCode) REFERENCES ProductionLineLayout(LineCode)
);
-- 新增:工段物料调拨子计划表
CREATE TABLE MaterialAllotSubPlan (
AllotSubPlanNo VARCHAR(50) PRIMARY KEY,
AllotPlanNo VARCHAR(50) NOT NULL,
SectionCode VARCHAR(50) NOT NULL,
SectionAllotQty DECIMAL(18,2) NOT NULL,
AllottedQty DECIMAL(18,2) DEFAULT 0,
AllotTimeWindowStart DATETIME NOT NULL,
AllotTimeWindowEnd DATETIME NOT NULL,
FOREIGN KEY (AllotPlanNo) REFERENCES MaterialAllotPlan(AllotPlanNo),
FOREIGN KEY (SectionCode) REFERENCES WorkSectionLayout(SectionCode)
);
-- 新增:工位物料需求表
CREATE TABLE StationMaterialDemand (
StationCode VARCHAR(50) PRIMARY KEY,
AllotSubPlanNo VARCHAR(50) NOT NULL,
MaterialBOM NVARCHAR(MAX), -- JSON格式:{"M001":1.0,"M002":2.0}
TotalDemandQty DECIMAL(18,2) NOT NULL DEFAULT 0,
AllottedQty DECIMAL(18,2) DEFAULT 0,
MaterialStatus VARCHAR(20) DEFAULT '待调拨', -- 齐套/缺料/待调拨
FOREIGN KEY (StationCode) REFERENCES WorkStationLayout(StationCode),
FOREIGN KEY (AllotSubPlanNo) REFERENCES MaterialAllotSubPlan(AllotSubPlanNo)
);
-- 扩展WorkStationLayout表:新增物料状态字段
ALTER TABLE WorkStationLayout ADD MaterialStatus VARCHAR(20) DEFAULT '待调拨';
5. 界面设计关键控件布局(WinForm)
csharp
运行
// 主界面控件布局(核心新增按钮)
// 1. 顶部工具栏
// - ComboBox: cboLineCode(产线选择)
// - Button: btnRemoteUpdate(远程更新)
// - Button: btnCacheUpdate(缓存更新)
// - Label: lblLastUpdateTime(最后更新时间)
// 2. 左侧布局画布面板
// - Panel: panelLayoutCanvas(承载LineLayoutCalibrateControl)
// 3. 右侧操作面板
// - GroupBox: 工位任务绑定
// - Label + TextBox: txtStationCode(工位编码)
// - Label + TextBox: txtProcessCode(工艺编码)
// - Label + TextBox: txtOrderNo(ERP工单)
// - Label + NumericUpDown: numTaskQty(任务数量)
// - Button: btnBindTask(绑定任务)
// - GroupBox: 物料调拨绑定
// - Label + ComboBox: cboAllotPlanNo(调拨计划)
// - Label + NumericUpDown: numDemandQty(需求数量)
// - Button: btnBindAllotPlan(绑定调拨计划)
// - GroupBox: 任务操作
// - Button: btnStartTask(启动任务)
// - Button: btnPauseTask(暂停任务)
// - Button: btnCompleteTask(完成任务)
// 4. 底部指令日志面板
// - DataGridView: dgvCommandLogs(指令日志展示)
// - Button: btnSaveLayout(保存布局)
三、 核心功能使用流程(新增部分)
- 远程更新操作:
- 选择产线 → 点击 “远程更新” 按钮 → 等待同步完成(按钮显示 “远程更新中...”)→ 同步成功后自动刷新布局 / 任务 / 调拨计划;
- 缓存更新操作:
- 选择产线 → 点击 “缓存更新” 按钮 → 清空本地缓存并重新从数据库加载 → 界面刷新为最新本地数据;
- 物料调拨计划绑定:
- 选中工位 → 选择调拨计划 → 输入物料需求数量 → 点击 “绑定调拨计划” → 工位物料状态更新为 “待调拨”;
- 定时自动更新:
- 程序启动后自动按配置间隔(默认 30 分钟)执行远程更新 → 无需手动操作,适合产线数据实时性要求高的场景。
总结
- 核心扩展:新增
RemoteAndCacheService实现远程数据拉取同步、本地缓存刷新,配套 “远程更新 / 缓存更新” 按钮,支持手动触发 + 定时自动执行; - 物料关联:新增物料调拨计划模型及绑定逻辑,工位任务启动前校验物料齐套状态,实现 “物料 - 布局 - 任务” 的联动;
- 性能优化:引入本地缓存机制,优先从内存加载数据,减少数据库访问,提升界面响应速度;
- 多指令适配:新增 “物料调拨端” 指令类型,完善指令日志记录,支持调拨计划绑定 / 调整 / 完成全流程调度。
该组件可直接编译运行,需提前配置:
- 数据库连接字符串(ConfigHelper.GetConnStr ());
- 远程 API 地址 / Token(ConfigHelper.GetRemoteUpdateConfig ());
- WinForm 界面控件命名与代码中保持一致。






