PaperMES — 原纸物流 MES 系统(武威金士泰 AGV)
造纸厂原纸(纸卷/纸板)智能仓储物流系统。打通「生产线 → 立体库 → 跨楼层搬运 → 销售出库」全流程, 由 AGV 自动导引车 + 电梯 PLC + 条码扫描 协同完成原纸的自动存取与调度。
解决方案文件:
WMS.sln(Visual Studio 2022,.NET 8)
1. 技术栈
| 类别 | 技术 |
|---|---|
| 运行时 | .NET 8(主工程)、.NET 6-windows(WPF 客户端 Modules)、.NET 6(MqttClient) |
| Web | ASP.NET Core,以 Windows 服务 方式部署(UseWindowsService),监听 http://*:8078 |
| ORM | SqlSugar(SQL Server,库 wms_jst2.0) |
| IoC | Autofac(按程序集批量注册 *Repository/*Service/*Strategy/*Handler) |
| 任务调度 | Quartz |
| 通信 | DotNetty(TCP/UDP 条码)、NModbus(电梯 PLC)、RestSharp(HTTP 调 ERP/RCS)、SignalR.Client、MQTT |
| 其他 | AutoMapper、FluentValidation、NLog、Polly、SSE(服务端推送)、NPOI(Excel)、FastReport |
2. 解决方案结构
逻辑分组
| 分组 | 包含项目 | 职责 |
|---|---|---|
| DataBase | Domain、App | 领域模型与数据访问 |
| Utils | Infrastructure、Business、Common、Job | 基础设施、业务、公共、后台作业 |
| CommunicationMiddleware | PLC、RPC、Netty | 通信中间件 |
| WebHost | WebHost、Authorization | Web 服务与鉴权 |
| UnitTest | RPCTest | 单元测试 |
12 个工程项目
| 项目 | 说明 |
|---|---|
JSMachine.WMS.Domain |
领域层:实体(AGVTask/ERPTask/RcsTask/StorageRack/ElevatorPlcQueue…)+ 仓储接口/实现(SqlSugar) |
JSMachine.WMS.App |
应用层:Dto + IService/ServiceImpl(每个实体一个服务) |
JSMachine.WMS.Business |
业务层:入库/出库/RCS 回调/SSE/公共(核心业务 + 大量设计模式) |
JSMachine.WMS.Infrastructure |
基础设施:AppSettings、SqlSugar、Lambda 表达式树、日志、序列化、加密等 |
JSMachine.WMS.Common |
公共:缓存、枚举、Dto、Helper、Htmls |
JSMachine.WMS.Job |
后台作业(Quartz):AGV/电梯/ERP/中转区/数据清理等定时任务 |
JSMachine.WMS.PLC |
电梯 PLC(Modbus 读写,NModbus) |
JSMachine.WMS.RPC |
对外 HTTP 调用:ERP(晨龙/旺泰/新荣/IBS)、PMS(ACS/DCS)、RCS、IBS |
JSMachine.WMS.Netty |
基于 DotNetty:条码扫描客户端/服务端/UDP(心跳、设备动态) |
JSMachine.WMS.WebHost |
Web 宿主(入口 Program.cs → WebHostEngine1.Start()),Controllers/Filters/Validators |
JSMachine.WMS.Authorization |
身份认证/授权(IdentityServer4,当前未启用) |
JSMachine.WMS.RPCTest |
RPC 调用测试(MSTest) |
目录下另有
JSMachine.WMS.Modules(WPF 客户端,Prism + HandyControl)、JSMachine.WMS.MqttClient, 未纳入WMS.sln,需手动加载。
3. 分层架构(DDD)
WebHost(表现层,端口 8078) + Authorization(认证)
↓
Application(应用层:Dto + IService/ServiceImpl)
↓
Business(业务层:入库/出库/RCS回调/SSE)
↓
Domain(领域层:Entity + IRepository/Repository)
Infrastructure(基础设施:SqlSugar/配置/日志/Lambda)
Common(公共:缓存/枚举/Helper)
↓
通信中间件:PLC / RPC / Netty / MqttClient
后台作业:Job(Quartz)
依赖方向自上而下;Domain 定义实体与仓储接口,Infrastructure 提供 SqlSugar 实现。
4. 核心业务流程
4.1 入库(PDA 扫码入库)Business/StockIn
StockInService.StockIn(PDAInParam) 主流程:
- 查物料信息是否存在、查库位是否存在
- 校验同「合格证号」是否已有未完成任务(防重扫)
- 按 放置策略 + 责任链 计算目标库位并锁定(先锁库再写任务,保证一致性)
- 写入
ERPTask,触发 AGV 搬运任务
4.2 出库(销售出库)Business/StockOut
StockOutService.Stockout 主流程:
- 校验销售订单号是否重复
- 计算可出库存:同批次库存 − 锁定/禁用 − 未完结任务 − 撤单数
- 生成
ERPTask出库任务
4.3 AGV 状态回调 Business/RcsCallBack
RCS 上报 AGV 任务状态 → RcsCallBackService.DealwithExcutionStatus:
失败(推 PDA 报警)→ 取消 → 成功,按状态码分发到不同处理器(见第 6 节)。
5. 对接外部系统
| 系统 | 项目 | 说明 |
|---|---|---|
| ERP(四选一) | RPC/ErpRPC |
企望(0) / IBS(1) / 晨龙(2) / 新荣(3),当前用 新荣;含生产入库、销售出库、生产退库、物料查询等接口 |
| RCS 机器人调度 | RPC/RcsRPC |
创建任务 ics/taskOrder/addTask、取消任务 ics/out/task/cancelTask |
| PMS/DCS/ACS 备料 | RPC/PmsRPC |
AcsPaperPreparService、DcsPaperPreparService |
| IBS 原料查询 | RPC/IBSRPC |
纸卷基础信息查询 |
6. 设计模式应用
6.1 责任链模式(入库库位计算)
责任流动方向:StockInByExist → StockInByNew → StockInByMixing
| 节点 | 职责 |
|---|---|
StockInByExist |
优先放「已有同批次」的列;同批次放满一列再开新列(最多 2 列,超出走中转区) |
StockInByNew |
放「全新空白列」 |
StockInByMixing |
与其他批次货物混放 |
6.2 策略模式(入库放置策略)
StrategyFactory.CreateStockInStrategy 根据 PlcaementMod 配置返回不同策略:
| 模式 | 配置值 | 策略 | 责任链 |
|---|---|---|---|
| 完全不混放 | 0 | NoMixingStrategy |
Exist → New |
| 混合模式(默认) | 1 | MixedModeStrategy |
Exist → New → Mixing |
| 完全混放 | 2 | CompleteMixingStrategy |
直接找空列 |
一楼(
FirstFloor)强制走「不混放」逻辑。
6.3 状态模式 + 策略模式(RCS 回调)
RcsCallbackHandlerFactory.CreateHandler(status, destWarehouse) 按 AGV 状态码 分发:
| 状态码 | 处理器 | 说明 |
|---|---|---|
| 21 | PickupCompletedStateHandler |
取货完成 |
| 23 | 按 Transport 二次分发 |
放货完成 |
状态 23 按目标区域二次分发:
| 目标区域(Transport) | 处理器 |
|---|---|
| 北区/南区/出库中区/退库区 | StockOutWharfHandler(出库月台) |
| 库区(NormalArea) | StockAreaHandler |
| 中转区(TransitArea) | TransitAreaHandler |
| 电梯(Elevator) | ElevatorDeliveryHandler |
7. 电梯 PLC 通信(PLC)
ElevatorPlc : ModbusEngineBase 初始化时按 IP 匹配电梯编号,注册到 PLCManager 后连接(NModbus,端口 502/503)。
每台电梯 8 个通道(读/写寄存器位):
| 通道 | 网带号 | 读数据位 | 写数据位 |
|---|---|---|---|
| 入库放货通道2 | 1001 | 0 | 8 |
| 入库放货通道1 | 1011 | 2 | 10 |
| 入库取货通道2 | 2004 | 5 | 13 |
| 入库取货通道1 | 2014 | 7 | 15 |
| 出库放货通道2 | 2001 | 4 | 12 |
| 出库放货通道1 | 2011 | 6 | 14 |
| 出库取货通道2 | 1006 | 1 | 9 |
| 出库取货通道1 | 1016 | 3 | 11 |
- 读:
ElevatorStatusRead.ReadElevatorStatus(no),读 20 个 ushort(寄存器地址 1 起) - 写:
ElevatorWrite.WriteElevator(status),逐通道写「放货/取货完成」信号
8. 后台作业(Quartz,Job)
AgvTaskJob、ElevatorJob、ElevatorOutJob(电梯出库)、ErpTaskJob、RcsCallbackJob、
StockInSecondFloorJob(二楼入库)、TransferAreaJob(中转区)、DataCleanJob(数据清理)、XinRongErpJob(新荣 ERP 定时回传)。
通过 JobEngine + JobListener 启动。
9. 配置说明(JSMachine.WMS.WebHost/appSettingsWebhost.json)
| 配置项 | 说明 | 当前值 |
|---|---|---|
urls |
服务监听地址 | http://*:8078 |
DataBaseCon |
SQL Server 连接串 | wms_jst2.0(本地) |
ErpConfig.ErpType |
ERP 类型 | 3(新荣) |
ErpConfig.RetryNum |
上传重试次数 | 5 |
RcsConfig |
RCS 创建/取消任务 URL | 见上 |
ElevatorConfig |
电梯列表 | 2 台(端口 502/503) |
PlcaementMod |
放置模式 | 1(混合模式) |
⚠️ 配置文件包含明文数据库密码,生产部署前应改为环境变量/加密配置。
10. 部署与启动
- 还原数据库:执行
../dbo.sql - 配置
appSettingsWebhost.json(数据库连接、ERP 类型、电梯 IP/端口) - 编译
WMS.sln(.NET 8 SDK) - 启动:
JSMachine.WMS.WebHost为 Windows 服务(WebHostEngine1.Start()调用UseWindowsService), 也可直接运行调试;服务监听 8078 端口 - 前端:
../wms_front(Web)、../wmsjstpda(PDA 手持终端)
11. 注意事项
- 鉴权未启用:
Startup.cs中 JWT/IdentityServer 认证逻辑整体被注释,UseAuthentication/UseAuthorization空转。 - 测试覆盖薄:
RPCTest仅含 RPC 调用测试(MSTest)。 - 两个工程未入解决方案:
Modules(WPF)、MqttClient需手动加载。 - 并发控制:入库责任链用
SemaphoreSlim(1,1)串行化,保证库位锁定的数据一致性。
12. 代码级深入分析
12.1 入库库位计算(策略 + 责任链)
入口 StockInService 调用 StrategyFactory.CreateStockInStrategy:一楼强制走「不混放」,其余楼层按 PlcaementMod 决定策略。
责任链节点职责:
| 节点 | 职责 |
|---|---|
StockInByExist |
优先放「已有同批次」的列,同批次堆满一列再开新列(最多 2 列) |
StockInByNew |
放「整列全空」的新列,取最底层 |
StockInByMixing |
与其他批次混放;仍无则落中转区兜底 |
StockInByExist 关键逻辑:
GetStorageRackExceptStockout先剔除「当天有出库任务的列」,避免入库与掏货出库冲突SemaphoreSlim(1,1)串行化,防并发抢库位- 列被锁时的分支:有未锁空列→直接取;空列≤1→开新列;一楼已开两列被锁→
return null等待解锁;物料已在中转区→return null防重复 - 选定后先
Lock再返回,保证 DB 一致性
12.2 电梯 PLC 通信
ElevatorPlc.Init 时序关键:先 PLCManager.AddPLC 再 Connect,否则 Job 先执行拿不到实例无法重连。
每台电梯 8 通道寄存器映射(读位/写位):
| 动作 | 通道2 网带 | 通道1 网带 | 读位 | 写位 |
|---|---|---|---|---|
| 入库放货 | 1001 | 1011 | 0 / 2 | 8 / 10 |
| 入库取货 | 2004 | 2014 | 5 / 7 | 13 / 15 |
| 出库放货 | 2001 | 2011 | 4 / 6 | 12 / 14 |
| 出库取货 | 1006 | 1016 | 1 / 3 | 9 / 11 |
电梯编号靠库位名称是否含「1号」判定(StorageRackName.Contains("1号") ? 1 : 2)。
12.3 RCS 回调双重分派(状态 + 策略)
RcsCallbackHandlerFactory.CreateHandler(status, destWarehouse):
- 状态 21 →
PickupCompletedStateHandler(取货完成) - 状态 23 → 按
Transport二次分发到StockOutWharfHandler/StockAreaHandler/TransitAreaHandler/ElevatorDeliveryHandler
ElevatorDeliveryHandler 完整链路:写 PLC 放货信号 → 写 PLC 队列 → 电梯计数 → 更新 AGV 任务完成 → 解锁库位 → SSE 广播刷新前端。
12.4 后台作业调度(Job,Quartz)
声明式注册:JobEngine 反射扫描所有「实现 IJob 且带 [Job] 特性」的类注册到 Quartz,新增作业只需打特性。
9 个作业(均 [DisallowConcurrentExecution] 禁止并发):
| 作业 | 间隔 | 职责 |
|---|---|---|
| AgvTaskJob | 1s | 把 Initial 状态 AGV 任务下发给 RCS |
| ErpTaskJob | 2s | ERP 任务拆解为 AGV 搬运任务(一楼 AGV 限流 ≤4) |
| RcsCallbackJob | 1s | 消费 RCS 回调队列,幂等去重后处理状态流转 |
| ElevatorJob | 0.5s | 轮询 PLC 入库取货信号,上升沿触发,入队二楼 |
| ElevatorOutJob | 0.5s | 轮询 PLC 出库取货信号,生成一楼出库任务 |
| StockInSecondFloorJob | 2s | 消费二楼入库队列,策略工厂算库位,无位回写队列自旋 |
| TransferAreaJob | 2s | 调度中转区滞留物料 |
| XinRongErpJob | 2s | 已完成任务回传新荣 ERP(含重试) |
| DataCleanJob | 24h | 清理 12 个月前历史数据 + 日志文件 |
作业间通信用 ConcurrentQueue 静态缓存做生产者-消费者(StockInSecondFloorCache、RcsCallbackCache);PLC 信号用 static bool 前值做上升沿检测。
12.5 Netty 条码协议
三套同构实现(客户端/服务端/UDP):
| 角色 | 场景 |
|---|---|
| NettyAsClient | WMS 主动连条码设备(基恩士/倍加福/喷码机),收扫码结果写内存缓存 |
| NettyAsServer | WMS 监听端口,发 LON\r\n 触发扫码,轮询缓存回读结果 |
| NettyUdp | 广播监听 503 端口,收设备心跳/动态 |
分帧:DelimiterBasedFrameDecoder(分隔符)或 LengthFieldBasedFrameDecoder(2 字节长度域)。分发:MEF [InheritedExport] 装配全部 Handler,责任链逐个 Handle,首个返回 true 终止。
12.6 出库库存计算
可出库存公式:物理在库 + 在途AGV任务数 + 撤单数 − 已下达出库命令总数 ≥ 本次出库数
CalculateOutMaterial 决策树:优先出「拖数最少」的列 → 锁定列等待解锁 → 挡路不同批次则触发掏货中转。
12.7 RPC 对接层
四家 ERP 中只有新荣(ErpType=3)完整可用,其余三家接口签名已漂移断链。认证方式:辰龙/企望 MD5 签名、IBS Header AppId/AppKey、新荣 body 内 user_key。RCS 返回 code==1000 成功。
12.8 WebHost 与认证
60+ HTTP 接口(入库/出库/撤单/搬运/库位维护/统计/SSE 推送)。当前认证完全关闭:JWT/IdentityServer 全部注释,登录为明文密码比对 + 无签名 GUID token,所有接口匿名可访问。限流过滤器、FluentValidation 规则均未启用。
12.9 基础设施与遗留模块
- 动态 Lambda 查询(Infrastructure/LambdaHelp):把「字段+操作符+值」动态编译为
Expression<Func<T,bool>>,组内 OR、组间 AND,供 SqlSugar 动态筛选/排序——全项目最核心的通用能力。 - ViKey 加密狗:P/Invoke 硬件授权读取。
- Modules(WPF):Prism + HandyControl + FastReport,生产现场操作终端(设备连接监控 + 新卷入库查询/标签打印)。
- MqttClient:空壳工程(仅
Class1),MQTT 未实现。 - Infrastructure 从 DCS/Accelerider 项目迁移而来,保留旧命名空间/AssemblyTitle。
13. 技术债与改进点
并发/性能
_SemaphoreSlim(1,1)静态单例全局串行化所有入库计算,高并发吞吐瓶颈。GetStorageRackExceptStockout每次查当天全部出库任务,宜加索引/缓存。- 服务端扫码回读靠 100ms 轮询 + 2s 超时阻塞。
逻辑 bug(已定位)
4. RcsCallBackService.HandleStocOutFirstFloor 守卫误写 Floor==SecondFloor,一楼销售出库失败报警分支失效。
5. CalculateDestWarehouse_SaleOutMiddle 双重 OrderBy 覆盖,前一排序失效。
6. LambdaBuider 中 long 类型判断误写 PropertyInfo == typeof(long),恒 false。
死代码/未接线
7. 策略模式整套(StorageAreaStrategy/ElevatorStrategy/TaskCompletionStrategyFactory)无调用方,与状态模式功能重叠。
8. Job:JobListener 未注册、ScannerSceneAttribute 孤儿、JobGroup 枚举未用、PlateList 遗留。
9. Netty:两个 Decoder.cs 死代码且有 bug、PenmoReadCmd 无 Handler、UDP 心跳未落地。
10. RCS 回调 RcsCallBackService 内 4 个私有方法、StockOutCreationDetail、FailedStateHandler 均未引用。
11. ScheduleType 仅实现 Simple,Corn/Calendar/XML/JSON 未落地。
安全
12. 认证完全关闭,60+ 接口匿名可访问;登录明文密码 + 无签名 token;CORS 全开;IdentityServer 开发签名 + 弱口令 123456。
13. 配置含明文数据库密码。
工程/维护
14. 三家 ERP(辰龙/企望/IBS)接口签名漂移断链,仅新荣可用;ErpType/PmsType 特性未接线,实现选择依赖 Autofac「最后注册生效」。
15. PmsRPC(ACS/DCS)为占位半成品;IBSRPC 缺 await 不可用。
16. Modules.csproj 引用不存在的 ..\Server.MqttClient\;MqttClient 空壳。
17. Infrastructure 保留 DCS/Accelerider 迁移残留(命名空间/AssemblyTitle)。