ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于C#的WCS仓储控制系统核心设计与调度实现

基于C#的WCS仓储控制系统核心设计与调度实现 简介这份资源是WCS-HY仓库控制系统WCS的C#实现代码包面向工业自动化与物流仓储领域的系统开发者、集成工程师及对WCS架构感兴趣的进阶学习者适用于输送线控制、立体库调度、分拣系统管理等多种典型场景也可作为高校相关专业课程设计的参考。它涵盖设备调度、任务管理、与WMS对接等核心逻辑可直接作为仓库控制项目的开发参考与二次开发基础。压缩包共2000个文件约251.76MB主要包含C#源码.cs、界面资源.xaml、可执行程序.exe以及依赖库.dll另有配置文件.config、说明文档.txt/XML与调试符号.pdb便于工程分析与问题排查。已有282人学习本套代码其模块化设计覆盖用户界面、逻辑架构、设备驱动、通信协议及第三方系统接口并附带相关配置说明适合用于理解WCS系统全貌、梳理业务流程或快速搭建原型。1. WCS-HY 控制系统是什么先弄明白这套 C# 仓储调度系统能干什么半夜被电话叫醒十有八九是这句堆垛机不动了任务下了半天没反应。你打开界面一看WMS 说任务已经下发WCS-HY 的日志停在“等待设备反馈”。这套夹在 WMS 和设备中间的 C# 仓储控制系统做的就是拆任务、发指令、收状态、扛异常。一句话讲清楚WMS 告诉你“把 A 库位的托盘出到 B 出库口”WCS-HY 要把这句话翻译成堆垛机、输送线、RCSAGV 调度各自能执行的动作并确保它们按顺序、不打架、不丢单。它适合的团队是那种已经有 WMS 和硬件但不想被设备厂商绑死、想自己掌握调度代码的仓储项目。用 C# 做 WCS最大的好处是上位机生态熟、连 PLC 和数据库方便团队招人也容易。下面按我实际做这类系统的路径把任务模型、设备适配、通信接口和现场坑位一次讲透。2. WCS 调度核心怎么拆任务模型、设备适配与最小 C# 调度循环2.1 任务模型把上架、下架、移库先变成数据而不是业务WMS 传过来的从来不是“堆垛机去 3 巷道货位 02-03 取货”而是一张入库单或出库单比如“入库单 20241001-001SKU 数量若干目标库区 A区”。WCS-HY 要先把这单业务拆成一条条可调度的任务记录再拆成设备动作。我见过不少人一上来就写“处理入库”的 if-else结果每种异常分支都要再写一套项目后面全是补丁。正确顺序是先定义任务主表模型把状态、优先级、源/目标库位、设备归属、时间戳这些字段固定下来业务逻辑再去操作这张表。下面是一个我在仓储项目里常用的任务模型字段不追求多但每个都有存在的理由:// 任务主表对一条入库/出库指令的完整描述 public class WcsTask { public long TaskId { get; set; } // 任务唯一IDWCS内部自增 public string OrderNo { get; set; } // WMS传入的业务单号回传给WMS用 public int TaskType { get; set; } // 1入库 2出库 3移库 9盘点 public int Priority { get; set; } // 数字越大越优先比如加急出库给99 public int Status { get; set; } // 状态值对应TaskStatus枚举 public string SourceLocation { get; set; } // 源库位如A-01-02-03 public string TargetLocation { get; set; } // 目标库位如B-02-01-01 public string DeviceId { get; set; } // 当前负责的设备编码如STK-01 public DateTimeOffset CreateTime { get; set; } // 创建时间用UTC避免服务器时区漂移 public DateTimeOffset? StartTime { get; set; } // 实际开始执行时间用于统计效率 public DateTimeOffset? FinishTime { get; set; } // 实际完成时间 } // 状态编号用枚举别让状态散落在各处 public enum TaskStatus { Created 0, // 已创建等待调度线程分配 DispatchReady 1, // 已分配设备准备下发 Executing 2, // 设备正在执行 Paused 3, // 临时暂停比如输送线拥堵 Finished 4, // 已完成 Cancelled 9, // 人工取消 Exception 10 // 异常需要人工介入 }这段代码的逻辑说明就两条。第一源/目标库位用字符串是故意的因为 WMS 给的是逻辑库位设备要的是物理坐标字符串之间留一道转换层后面换设备型号不用改主表结构。第二Priority 用 int 而不是“高/中/低”是为了排序时直接 ORDER BY Priority DESC不用再关联字典表。时间是 DateTimeOffset 而非 DateTime是因为多台服务器和 PLC 时钟不一致时带时区偏移能省掉一堆“差 8 小时”的破事。我在实际项目里还会加一张任务动作明细表因为“入库”这个业务要拆成“取货→输送→入库到位→回报完成”几个动作一个主任务对应多条 Action。Action 表里存动作编码、执行顺序、是否必须回告。这样主任务只负责追踪整单进度动作表负责和设备点对点交互。如果把动作直接堆在主表里遇到移库任务“取出→移送→放入”三个动作字段会越加越乱所以这个拆法是 WCS-HY 这类系统避免返工的关键。2.2 设备适配层C# 接口和委托让 PLC、堆垛机、RCS 都能接WCS-HY 项目里通常不止一类设备有走 OPC UA 的西门子 PLC有走 MODBUS TCP 的输送线电机有带独立调度系统的 AGV/RCS还有品牌堆垛机自带专用协议。调度核心如果直接依赖具体设备类型每加一种设备就要改一遍调度代码这是最典型的返工源。C# 里解决这个问题的自然方式就是接口加事件接口隔离设备差异事件处理异步上报。下面是我常用的设备抽象调度层只认这个接口不认具体品牌// 所有设备的统一抽象不管背后是PLC还是RCS对调度层只暴露这几个方法 public interface IEquipment { string DeviceId { get; } Taskbool SendCommandAsync(WcsAction action, int timeoutMs); Task UpdateFromDeviceAsync(); // 主动向设备要一次状态比如启动时全量同步 event EventHandlerDeviceStatusEventArgs StatusChanged; // 设备状态变化事件 } // 堆垛机实现通过OPC UA写DB块下发指令 public class StackerCrane : IEquipment { public string DeviceId STK-01; public Taskbool SendCommandAsync(WcsAction action, int timeoutMs) { // 把action里的源/目标库位换算成堆垛机坐标命令 // 然后通过OPC UA写入PLC的DB块并等待PLC返回握手位 string command BuildStackerCommand(action); Console.WriteLine($[{DeviceId}] 下发指令: {command}); return Task.FromResult(true); } public Task UpdateFromDeviceAsync() { // 读取PLC中的运行状态、故障位、到位信号 return Task.CompletedTask; } public event EventHandlerDeviceStatusEventArgs StatusChanged delegate { }; }接口背后的具体实现调度层一概不关心。堆垛机走 OPC UA输送线走 MODBUS TCPAGV 那边调 RCS 的 HTTP 接口在接口后面各做各的。C# 的事件在这里比轮询高效得多PLC 的到位信号通过 OPC UA 订阅一到StatusChanged 被触发调度层立刻推进任务状态不用每 200ms 去查一遍“到位没有”。实现类里那个event ... delegate { }是 C# 的惯用法防止没人订阅事件时触发空引用。这里还用到了 C# 委托和事件的组合很多人把委托和接口搞混接口管的是“谁能干什么”委托和事件管的是“发生什么怎么通知”。在 WCS-HY 这种设备满天飞的项目里两者配合是对的姿势。设备适配层建好之后新接入一台设备只需要新写一个类实现 IEquipment并在设备工厂里加一行注册代码调度主循环完全不用动。2.3 一个最小调度循环从数据库取任务到下发设备动作的 C# 代码骨架任务模型和设备接口都定好之后调度主循环就变得很简单核心就干四件事取一条可调度任务、先把状态置为“下发中”、把任务翻译成设备动作发出去、根据发送结果更新状态。注意这里有个明显反直觉的点先改状态再发指令。很多人是先发指令成功后改状态结果在设备反馈延迟的几秒内另一个调度线程又把同一条任务捞出来了。下面是一个基于 BackgroundService 的最小调度循环可以直接跑起来做原型验证public class DispatchService : BackgroundService { private readonly ILoggerDispatchService _logger; private readonly IEquipmentAdapter _adapter; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // 1. 取一条最高优先级的待调度任务 WcsTask task await FindNextDispatchTaskAsync(); if (task null) { await Task.Delay(500, stoppingToken); // 没有任务就歇一会 continue; } // 2. 状态先行先改为DispatchReady防止重复取到 await UpdateTaskStatusAsync(task.TaskId, TaskStatus.DispatchReady); // 3. 根据任务的DeviceId拿到设备实例 IEquipment device _adapter.GetDevice(task.DeviceId); // 4. 把WcsTask翻译成设备能理解的WcsAction WcsAction action ConvertToAction(task); bool sendOk await device.SendCommandAsync(action, timeoutMs: 3000); // 5. 下发成功则进入执行中失败则进异常 TaskStatus next sendOk ? TaskStatus.Executing : TaskStatus.Exception; await UpdateTaskStatusAsync(task.TaskId, next); } catch (Exception ex) { // 调度循环不能挂掉异常必须被接住 _logger.LogError(ex, 调度循环异常当前任务TaskId未知); await Task.Delay(1000, stoppingToken); } } } }参数和细节都值得展开说。Task.Delay(500)是无任务时的轮询间隔我一般设 500ms 到 2s太短会打爆数据库太长会拖慢紧急任务的响应。FindNextDispatchTaskAsync在低并发原型里可以写一句 ORDER BY Priority DESC, CreateTime ASC但上了生产环境必须加行锁不然会有 4.3 节里的重复下发。timeoutMs: 3000是给 OPC UA 写指令的等待时间PLC 扫描周期通常几十毫秒3 秒足够如果设备是 RCS 的 HTTP 接口这个超时我会调到 5 秒因为中间隔着网络和 AGV 自身的任务队列。最需要注意的是那个空 catch 后面的Task.Delay(1000)调度循环里永远不能因为一条脏数据就退出后台服务否则整仓停摆。我把这个最小循环跑通后再往上加东西多线程并发调度、设备繁忙时回退、异常任务重试、暂停恢复。加来加去你会发现最核心的还是状态机没写乱。状态机的事放到第 3 章讲先把通信层打开因为调度循环写得再漂亮连不上 PLC 也是白搭。3. 设备通信层怎么做OPC UA、TCP 多客户端与 WMS/RCS 接口对接3.1 设备通信选型OPC UA、MODBUS TCP、S7 协议怎么选WCS-HY 的通信层选型决定了项目后续一半的排错工作量。我给出的建议不是“哪个好”而是“哪个场景最适合”。下面是我在设备选型会议上常用的对比表通信方式适用设备实时性开发量备注OPC UA西门子 S7-1200/1500、倍福、部分汇川高中跨平台信息模型自带安全新项目首选MODBUS TCP老 PLC、第三方 IO 模块、部分输送线中低报文简单但点位表要自己维护调试靠抓包S7 协议西门子 S7-300/400/1200/1500高低用 s7netplus 库开发快但闭源且有访问限制HTTP/RESTAGV 调度系统RCS、WMS、输送线控制器中低适合任务级交互不适合毫秒级启停控制新项目里只要 PLC 支持 OPC UA我一般直接选 OPC UA。原因是它省掉了自己处理连接管理和点位表映射的大部分麻烦而且调试工具比如 UaExpert比抓 MODBUS 报文直观得多。但老项目里很多 S7-200 和第三方模块只支持 MODBUS TCP这时候就得自己约定点位表绝不能偷懒把点位散落在代码里一定要做一张“逻辑地址↔PLC地址”的映射表后面翻车时能少掉一半头发。3.2 C# 连接西门子 PLC 的 OPC UA 客户端最小连接配置在 C# 里做 OPC UA 客户端最省事的路径是把 PLC 当 OPC UA Server 连。西门子 S7-1200/1500 固件 4.0 以上都内置 OPC UA Server只需要在 PLC 侧启用并开放端口。C# 端我一般直接用官方 OPC Foundation 的 Opc.Ua.Client 库下面是一段最小连接代码我拿它做过 WCS-HY 的通信层原型using Opc.Ua; using Opc.Ua.Client; public class OpcUaPlcClient { private Session _session; private readonly string _endpointUrl opc.tcp://192.168.1.10:4840; private readonly string _appName WCS-HY-Server; public async Task ConnectAsync() { var config new ApplicationConfiguration { ApplicationName _appName, ApplicationUri $urn:{_appName}, SecurityConfiguration new SecurityConfiguration { ApplicationCertificate new CertificateIdentifier() } }; await config.InitializeAsync(); // 先以None模式联调证书互信通过后再切SignAndEncrypt var endpoint new EndpointDescription( _endpointUrl, null, null, MessageSecurityMode.None, new UserIdentity(new AnonymousIdentityToken())); _session await Session.CreateAsync( config, endpoint, false, _appName, 60000); } }这段代码有三个现场必查的参数。第一_endpointUrl的端口西门子默认 4840别写错地址是 PLC 的 IP不是上位机的。第二Session.CreateAsync最后一个参数 60000 是会话超时毫秒现场网络不稳时我会提到 120000否则 WCS 长时间没轮询会话被 PLC 侧断开下次调用直接抛异常。第三一开始用MessageSecurityMode.None联调这时候不用折腾证书但联调通过后一定切回SignAndEncrypt不然 PLC 侧安全策略不满足项目验收都会卡住。常见坑是 PLC 侧没把 WCS 的客户端证书加入信任列表报BadSecurityChecksFailed这个错误码基本只有这个意思别去查代码。OPC UA 读点位不是按“变量名”读而是按 NodeId 读。西门子的典型 NodeId 是ns3;sDB3.DBX10.0这样的字符串。我建议把点位配置放在 JSON 或数据库表里程序启动时加载一次千万别硬编码在 20 个类里。WCS-HY 这类系统点位动辄几百个硬编码代码的人最后都在改错文件。3.3 与 WMS/RCS 对接HTTP 接口还是共享数据库WCS-HY 不是孤岛上游 WMS 要发任务下游 AGV 那边还有个 RCS机器人调度系统。我遇到的大多数项目WMS 和 WCS 之间用共享数据库中转表最省事WCS 和 RCS 之间走 HTTP 接口更合理。原因很简单WMS 与 WCS 同属企业内部系统数据库直连事务可控、不需要开发两套 HTTP 服务就能保证不丢单而 RCS 往往是另一个子系统内部有自己的库WCS 再直连它的库容易把耦合搞大REST 接口反而边界清楚。WMS 到 WCS 的中转表我习惯叫wms_wcs_task_queue表结构保持克制CREATE TABLE wms_wcs_task_queue ( id BIGINT IDENTITY(1,1) PRIMARY KEY, wms_order_no VARCHAR(64) NOT NULL, task_type TINYINT NOT NULL, -- 1入库 2出库 3移库 source_loc VARCHAR(32), target_loc VARCHAR(32), status TINYINT DEFAULT 0, -- 0待处理 1已接收 2已完成 3异常 create_time DATETIME2 NOT NULL DEFAULT SYSDATETIME(), receive_time DATETIME2 NULL, finish_time DATETIME2 NULL, CONSTRAINT uk_wms_order_type UNIQUE (wms_order_no, task_type) );这个表里最重要的是最后那个唯一约束uk_wms_order_type。WMS 因为网络抖动重复插入同一张单据时WCS 侧只需要捕获主键冲突并把新记录忽略掉重复执行的问题在源头就断了。状态字段只保留 0/1/2/3 四个值别加太多中间态中间态放 WCS 自己的任务表里管理中转表越简单越不容易出乱子。WCS 调 RCS 的 HTTP 接口时必须做幂等控制。常见做法是每次请求带一个RequestIdRCS 收到重复请求时通过这个 ID 去重。还有一点容易翻车HTTP 超时后不能简单重发因为 RCS 可能已经把任务收下并开始执行了。保险做法是超时后先查 RCS 的任务状态接口确认这条任务到底收没收到再做决定。这条规则同样适用于 WCS 给 PLC 下发指令原理一样超时不等于失败可能是响应丢了。3.4 用状态机把“下发、执行、完成、异常”变成不会乱跑的代码调度循环写多了之后最容易乱的不是下发逻辑而是状态迁移。典型翻车现场任务从 Executing 直接跳回 Created然后又被调度线程取出来再发一次。给 WCS-HY 写一个状态迁移表是成本最低的防呆手段。状态机有两个硬约束不允许非法迁移不允许把异常状态静默吞掉。我用 C# 实现的时候喜欢用元组字典读起来就是一张表public static class TaskStatusTransition { // 迁移规则表key 是(当前状态, 事件名)value 是目标状态 public static readonly Dictionary(TaskStatus, string), TaskStatus Rules new() { [(TaskStatus.Created, dispatch)] TaskStatus.DispatchReady, [(TaskStatus.DispatchReady, acked)] TaskStatus.Executing, [(TaskStatus.DispatchReady, reject)] TaskStatus.Exception, [(TaskStatus.Executing, done)] TaskStatus.Finished, [(TaskStatus.Executing, timeout)] TaskStatus.Exception, [(TaskStatus.Executing, pause)] TaskStatus.Paused, [(TaskStatus.Paused, resume)] TaskStatus.Executing, [(TaskStatus.Exception, retry)] TaskStatus.DispatchReady, [(TaskStatus.Exception, cancel)] TaskStatus.Cancelled }; public static TaskStatus Move(TaskStatus current, string evt) { if (Rules.TryGetValue((current, evt), out var next)) return next; // 非法迁移必须暴露不能静默维持原状 throw new InvalidOperationException( $非法状态迁移: {current} {evt}); } }这个状态机的价值在 Throw 那一下。很多人写状态迁移时遇到非法事件就返回原状态结果任务卡在 Executing 一个晚上没人知道。正确的做法是抛异常并记录日志让告警系统把人叫起来处理。重试也不该无限循环我设的阈值是同一个任务异常重试最多 3 次超过 3 次就置为 Cancelled 或 Exception留待人工介入。这样既给了瞬时故障自愈的机会又不会让坏任务霸占设备。4. WCS-HY 调试避坑5 个现场常见的 C# 控制系统问题4.1 任务显示已下发设备却一动不动现象WCS 日志清清楚楚写着“堆垛机 STK-01 下发成功”设备端就是没有任何动作连报错都没有。原因排查顺序很重要。第一嫌疑是 PLC 点位映射错WCS 写的 DB 块地址和 PLC 程序里的实际地址对不上。我在一个项目里遇到 C# 往 DB3.DBD12 写目标库位PLC 程序读的是 DB3.DBD16数据写进去了但没人看。第二嫌疑是写的是 Bool 而 PLC 判断的是 Int或者反之。第三是设备处于本地手动模式没有切换到自动/远程这类问题现场每天都在发生。解决方法是先把设备切到手动用 OPC UA 客户端比如 UaExpert直接写那一个点位看设备动不动。如果手动直写能动说明通信链路和点位本身没问题问题在 C# 侧的逻辑映射或坐标换算。如果手动直写也不动问题就在 PLC 程序和设备本身别在 WCS 代码里白白消耗时间。这个排查顺序能省掉一大半无效调试我给现场工程师的要求是不要一上来就怀疑 WCS-HY 代码先手动直写点位一句“设备能不能动”就能把问题劈成两半。4.2 跑一会儿任务积压数据库连接池报警现象WCS-HY 刚启动时一切正常跑两小时后日志出现Timeout expired. The timeout period elapsed任务停在 Created 没有往下走。原因基本是数据库连接没管好。最常见的是调度线程里每次循环都 new 一个 SqlConnection用完没及时释放连接池被打穿另一种是用了同步的ExecuteReader在 UI 线程或 Timer 里阻塞线程一堵后面全排队。解决分两步。第一步把连接字符串的池参数定死至少是下面这种级别Server192.168.1.20;DatabaseWCS_HY;User Idwcs;Password***;Max Pool Size100;Min Pool Size5;Connection Timeout15;Max Pool Size设 100 对 WCS 这种中低频系统够用Min Pool Size 5 是让应用启动时预热几个连接免得高并发瞬间建连卡顿。第二步是把所有数据库操作改成异步async Task一路通到底后台服务里绝不能用.Result或.Wait()硬等。改完这两个地方连接池报警基本消失。如果还积压就去查是不是取任务的 SQL 太慢没加索引给status和priority字段建个联合索引。4.3 同一个任务被下发两次设备做了两遍动作现象出库任务下发了两次托盘被堆垛机从库位取出来又放回去或者输送线同一个目的地址被写了两遍后一位的任务被挤掉。原因有两个。一个是并发调度线程在取任务时没有锁线程 A 和线程 B 同时读到同一条 statusCreated 的任务A 下发成功后改状态B 因为读得早也跟着下发。另一个原因是网络超时后重发设备其实已经执行了但响应在路上丢了C# 侧以为失败于是重发一遍。解决并发重复的关键是取任务时加行锁我这边的标准 SQL 是-- 取一条可调度任务并锁住该行避免并发线程重复读到 SELECT TOP 1 * FROM wcs_task_queue WITH (UPDLOCK, ROWLOCK) WHERE status 0 ORDER BY priority DESC, create_time ASC;UPDLOCK让别的调度线程读同一行时被阻塞ROWLOCK把锁粒度限定在行避免锁住整张表影响 WMS 写入。这样状态先行才真正安全。至于超时重发那条4.5 节那个思路同样适用超时后先查设备状态确认任务是不是已经在执行别盲目重发。我见过最离谱的一次重复是因为两个调度服务实例同时跑在负载均衡后面各发各的后来强制用 Redis 分布式锁才彻底掐死。4.4 TCP 长连接遇到粘包半包指令错乱现象输送线控制器用 TCP 和 WCS-HY 通信WCS 用 TcpListener 接收数据有时候一条完整指令被拆成两段有时候两条指令粘在一起解析出来的动作码完全不对。热词里那条“TcpListener 多客户端”指的就是这个场景WCS 作为服务端同时收几十条输送线连接时粘包问题会被放大成指令风暴。原因一句话TCP 是字节流没有消息边界。发送方可能一次发 10 个字节接收方可能一次收到 14 个字节下一包又只收到 6 个。如果不做帧协议解析必然错乱。解决方法是自定义帧格式我常用的是“帧头长度载荷校验”的结构。解析时要点是没凑齐一帧就继续等绝对不能半帧就开始处理。下面这段代码就是最基础的粘包处理思路生产环境可以替换成高性能的缓冲区private static byte[] TryUnpackFrame(byte[] buffer, ref int offset) { if (buffer.Length - offset 2) return null; // 1. 查找帧头 0xAA 0x55没找到就跳过单字节继续找 if (buffer[offset] ! 0xAA || buffer[offset 1] ! 0x55) { offset; return null; } // 2. 长度字段在帧头后占1字节 int payloadLen buffer[offset 2]; // 3. 帧头2 长度1 载荷 校验1小于这个长度说明还没收完 int frameLen 4 payloadLen; if (buffer.Length - offset frameLen) return null; // 4. 取出载荷跳过完整帧 byte[] payload new byte[payloadLen]; Array.Copy(buffer, offset 3, payload, 0, payloadLen); offset frameLen; return payload; }这个函数每次调用只解析一帧没凑够就返回 null外层 Socket 收到新数据后把 buffer 追加进去再调用一次。注意那个offset用 ref 是为了跳过脏字节避免帧头对不齐时死循环。校验位我在这里没展开生产环境一定要加 CRC不加校验偶尔的电磁干扰会给你造出一个假指令设备动起来你都不知道为什么。4.5 C# 调用 OPC 组件报 AccessViolationException代码 0000005现象WCS-HY 跑着跑着突然崩事件查看器里看到Access violation c0000005C# 侧报 “Attempted to read or write protected memory”。这正好对上热词里那条“c#调用c出现access violation c0000005”在 OPC 互操作场景里非常典型。原因多数不是 C# 代码本身而是 P/Invoke 或原生组件的内存被破坏。常见三种OPC UA Native DLL 依赖缺失比如 OpenSSL 版本不对多个线程同时调用同一个 OPC UA Session而这个 Session 底层不是线程安全的以及证书目录未初始化导致原生层崩溃。解决按顺序来。第一用事件查看器看故障模块文件名确认是哪个原生 DLL 崩的。第二检查 NuGet 包的目标框架是否统一别一个项目里混 net48 和 net6.0-windows 的互操作库。第三OPC UA Session 调用必须串行化我一般用一个 SemaphoreSlim 包住所有读写操作或者把调用集中到一个专用的消息泵线程。这个坑是最难在现场复现的因为它和负载有关修复时最容易犯的错是“加 try-catch 吃异常”那样系统不崩了但任务会莫名卡死比崩溃更难排查。正确的修复是找到并发入口把不该并发的调用串行化。5. 把 WCS-HY 从能跑调到扛得住压测脚本、日志字段和 3 个关键参数5.1 先用任务生成器压一遍调度链路我每次改完调度算法不会直接上现场而是用控制台程序往里灌任务把 WCS-HY 跑一晚上。生成器的逻辑很简单循环插入 5000 条任务覆盖入库、出库、移库三种类型再随机插入一些坏数据比如不存在的库位看调度服务怎么反应。压测关注的三个指标任务平均完成时间、数据库待处理任务积压数、设备动作下发的成功率。如果一晚上下来积压数为 0异常任务都进了 Exception 而不是卡在 Executing我就敢上现场。如果坏数据把某个线程打挂了正好趁这个机会把空 catch 补上。5.2 日志里必须留住的字段把排查时间从小时变成分钟WCS-HY 的日志不能只写“下发成功”“任务完成”这种空话。我要求每一条关键日志都带 TaskId、OrderNo、DeviceId、ActionCode、旧状态、新状态、耗时毫秒。下面是实际项目里的日志写法_logger.LogInformation( 任务状态迁移 {TaskId} {FromStatus} {ToStatus} 设备{DeviceId} 动作{ActionCode} 耗时{ElapsedMs}ms, task.TaskId, oldStatus, newStatus, task.DeviceId, action.ActionCode, sw.ElapsedMilliseconds);这个字段组合能让你在查问题时直接按 TaskId 把整条任务的一生拉出来不用再对着时间戳瞎猜。见过太多现场事故日志里只有一句话“任务异常”然后就没有然后了。不要省这个功夫。5.3 三个关键参数轮询间隔、命令超时、重试次数最后给一张我调参后的初始值表现场按这个起步再根据设备响应微调参数初始值设太小的后果设太大的后果调度轮询间隔500ms数据库压力大小库会被拖垮紧急任务响应慢出库口压车设备命令超时3000ms网络小抖动就误判失败设备卡死时 WCS 迟迟不转移任务异常重试次数3瞬时故障没机会自愈坏任务反复霸占设备这三个参数是 WCS-HY 上线前必须确认的。轮询间隔别小于 200ms大多数 PLC 扫描周期都在 50ms 左右轮询再快也是白等。超时先给 3 秒如果现场网络有丢包提到 5 秒。重试次数 3 次是底线超过 3 次就转人工否则设备会为一个坏任务空转一小时。我自己每次改完调度状态机都会先在测试库模拟 48 小时再上现场有一次就靠这个习惯拦住了一个批量任务重复下发的 bug省了两天返工。这个习惯也分享给你希望帮到你。本文还有配套的精品资源点击获取
返回列表