Files
2026-09-02 16:31:50 +08:00

356 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。