Files

356 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-09-02 16:31:50 +08:00
# 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)` 主流程:
1. 查物料信息是否存在、查库位是否存在
2. 校验同「合格证号」是否已有未完成任务(防重扫)
3. 按 **放置策略 + 责任链** 计算目标库位并**锁定**(先锁库再写任务,保证一致性)
4. 写入 `ERPTask`,触发 AGV 搬运任务
### 4.2 出库(销售出库)`Business/StockOut`
`StockOutService.Stockout` 主流程:
1. 校验销售订单号是否重复
2. 计算可出库存:同批次库存 − 锁定/禁用 − 未完结任务 − 撤单数
3. 生成 `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. 部署与启动
1. 还原数据库:执行 `../dbo.sql`
2. 配置 `appSettingsWebhost.json`(数据库连接、ERP 类型、电梯 IP/端口)
3. 编译 `WMS.sln`(.NET 8 SDK)
4. 启动:`JSMachine.WMS.WebHost` 为 Windows 服务(`WebHostEngine1.Start()` 调用 `UseWindowsService`),
也可直接运行调试;服务监听 8078 端口
5. 前端:`../wms_front`(Web)、`../wmsjstpda`(PDA 手持终端)
---
## 11. 注意事项
1. **鉴权未启用**:`Startup.cs` 中 JWT/IdentityServer 认证逻辑整体被注释,`UseAuthentication/UseAuthorization` 空转。
2. **测试覆盖薄**:`RPCTest` 仅含 RPC 调用测试(MSTest)。
3. **两个工程未入解决方案**:`Modules`(WPF)、`MqttClient` 需手动加载。
4. **并发控制**:入库责任链用 `SemaphoreSlim(1,1)` 串行化,保证库位锁定的数据一致性。
---
## 12. 代码级深入分析
### 12.1 入库库位计算(策略 + 责任链)
入口 `StockInService` 调用 `StrategyFactory.CreateStockInStrategy`:一楼强制走「不混放」,其余楼层按 `PlcaementMod` 决定策略。
责任链节点职责:
| 节点 | 职责 |
|------|------|
| `StockInByExist` | 优先放「已有同批次」的列,同批次堆满一列再开新列(最多 2 列) |
| `StockInByNew` | 放「整列全空」的新列,取最底层 |
| `StockInByMixing` | 与其他批次混放;仍无则落中转区兜底 |
`StockInByExist` 关键逻辑:
1. `GetStorageRackExceptStockout` 先剔除「当天有出库任务的列」,避免入库与掏货出库冲突
2. `SemaphoreSlim(1,1)` 串行化,防并发抢库位
3. 列被锁时的分支:有未锁空列→直接取;空列≤1→开新列;一楼已开两列被锁→`return null` 等待解锁;物料已在中转区→`return null` 防重复
4. 选定后**先 `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. 技术债与改进点
**并发/性能**
1. `_SemaphoreSlim(1,1)` 静态单例全局串行化所有入库计算,高并发吞吐瓶颈。
2. `GetStorageRackExceptStockout` 每次查当天全部出库任务,宜加索引/缓存。
3. 服务端扫码回读靠 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)。