From 9a4d807700660dfc1a92283997bc04181a609968 Mon Sep 17 00:00:00 2001 From: quowingwang Date: Tue, 20 Jan 2026 09:23:56 +0800 Subject: [PATCH] =?UTF-8?q?PLC=E6=8A=A5=E8=AD=A6=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MES/alarm/AlarmInfoController.cs | 101 ++++++ .../alarm/AlarmProblemDictionaryController.cs | 101 ++++++ .../PLC/Service/PlcHostedService.cs | 9 - RIZO.Admin.WebApi/PLC/Service/PlcService.cs | 287 +++++++++++++++--- RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj | 1 + .../appsettings.Development.json | 6 +- RIZO.Model/MES/alarm/AlarmInfo.cs | 64 ++++ .../MES/alarm/AlarmProblemDictionary.cs | 47 +++ RIZO.Model/MES/alarm/Dto/AlarmInfoDto.cs | 40 +++ .../alarm/Dto/AlarmProblemDictionaryDto.cs | 36 +++ RIZO.Model/RIZO.Model.csproj | 4 + RIZO.Service/MES/alarm/AlarmInfoService.cs | 79 +++++ .../alarm/AlarmProblemDictionaryService.cs | 80 +++++ .../MES/alarm/IService/IAlarmInfoService.cs | 21 ++ .../IAlarmProblemDictionaryService.cs | 23 ++ 15 files changed, 846 insertions(+), 53 deletions(-) create mode 100644 RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmInfoController.cs create mode 100644 RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmProblemDictionaryController.cs create mode 100644 RIZO.Model/MES/alarm/AlarmInfo.cs create mode 100644 RIZO.Model/MES/alarm/AlarmProblemDictionary.cs create mode 100644 RIZO.Model/MES/alarm/Dto/AlarmInfoDto.cs create mode 100644 RIZO.Model/MES/alarm/Dto/AlarmProblemDictionaryDto.cs create mode 100644 RIZO.Service/MES/alarm/AlarmInfoService.cs create mode 100644 RIZO.Service/MES/alarm/AlarmProblemDictionaryService.cs create mode 100644 RIZO.Service/MES/alarm/IService/IAlarmInfoService.cs create mode 100644 RIZO.Service/MES/alarm/IService/IAlarmProblemDictionaryService.cs diff --git a/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmInfoController.cs b/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmInfoController.cs new file mode 100644 index 0000000..695c671 --- /dev/null +++ b/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmInfoController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Mvc; +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; +using RIZO.Service.MES.alarm.IService; + +//创建时间:2026-01-19 +namespace RIZO.Admin.WebApi.Controllers.Mes.alerm +{ + /// + /// PLC报警信息 + /// + [Route("mes/AlarmInfo")] + [AllowAnonymous] + public class AlarmInfoController : BaseController + { + /// + /// PLC报警信息接口 + /// + private readonly IAlarmInfoService _AlarmInfoService; + + public AlarmInfoController(IAlarmInfoService AlarmInfoService) + { + _AlarmInfoService = AlarmInfoService; + } + + /// + /// 查询PLC报警信息列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "alarminfo:list")] + public IActionResult QueryAlarmInfo([FromQuery] AlarmInfoQueryDto parm) + { + var response = _AlarmInfoService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询PLC报警信息详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "alarminfo:query")] + public IActionResult GetAlarmInfo(int Id) + { + var response = _AlarmInfoService.GetInfo(Id); + + var info = response.Adapt(); + return SUCCESS(info); + } + + /// + /// 添加PLC报警信息 + /// + /// + [HttpPost] + [ActionPermissionFilter(Permission = "alarminfo:add")] + [Log(Title = "PLC报警信息", BusinessType = BusinessType.INSERT)] + public IActionResult AddAlarmInfo([FromBody] AlarmInfoDto parm) + { + var modal = parm.Adapt().ToCreate(HttpContext); + + var response = _AlarmInfoService.AddAlarmInfo(modal); + + return SUCCESS(response); + } + + /// + /// 更新PLC报警信息 + /// + /// + [HttpPut] + [ActionPermissionFilter(Permission = "alarminfo:edit")] + [Log(Title = "PLC报警信息", BusinessType = BusinessType.UPDATE)] + public IActionResult UpdateAlarmInfo([FromBody] AlarmInfoDto parm) + { + var modal = parm.Adapt().ToUpdate(HttpContext); + var response = _AlarmInfoService.UpdateAlarmInfo(modal); + + return ToResponse(response); + } + + /// + /// 删除PLC报警信息 + /// + /// + [HttpPost("delete/{ids}")] + [ActionPermissionFilter(Permission = "alarminfo:delete")] + [Log(Title = "PLC报警信息", BusinessType = BusinessType.DELETE)] + public IActionResult DeleteAlarmInfo([FromRoute]string ids) + { + var idArr = Tools.SplitAndConvert(ids); + + return ToResponse(_AlarmInfoService.Delete(idArr)); + } + + } +} \ No newline at end of file diff --git a/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmProblemDictionaryController.cs b/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmProblemDictionaryController.cs new file mode 100644 index 0000000..71cb0ff --- /dev/null +++ b/RIZO.Admin.WebApi/Controllers/MES/alarm/AlarmProblemDictionaryController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Mvc; +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; +using RIZO.Service.MES.alarm.IService; + +//创建时间:2026-01-19 +namespace RIZO.Admin.WebApi.Controllers.Mes.alarm +{ + /// + /// PLC报警信息字典 + /// + [Route("mes/AlarmProblemDictionary")] + [AllowAnonymous] + public class AlarmProblemDictionaryController : BaseController + { + /// + /// PLC报警信息字典接口 + /// + private readonly IAlarmProblemDictionaryService _AlarmProblemDictionaryService; + + public AlarmProblemDictionaryController(IAlarmProblemDictionaryService AlarmProblemDictionaryService) + { + _AlarmProblemDictionaryService = AlarmProblemDictionaryService; + } + + /// + /// 查询PLC报警信息字典列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "alarmproblemdictionary:list")] + public IActionResult QueryAlarmProblemDictionary([FromQuery] AlarmProblemDictionaryQueryDto parm) + { + var response = _AlarmProblemDictionaryService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询PLC报警信息字典详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "alarmproblemdictionary:query")] + public IActionResult GetAlarmProblemDictionary(int Id) + { + var response = _AlarmProblemDictionaryService.GetInfo(Id); + + var info = response.Adapt(); + return SUCCESS(info); + } + + /// + /// 添加PLC报警信息字典 + /// + /// + [HttpPost] + [ActionPermissionFilter(Permission = "alarmproblemdictionary:add")] + [Log(Title = "PLC报警信息字典", BusinessType = BusinessType.INSERT)] + public IActionResult AddAlarmProblemDictionary([FromBody] AlarmProblemDictionaryDto parm) + { + var modal = parm.Adapt().ToCreate(HttpContext); + + var response = _AlarmProblemDictionaryService.AddAlarmProblemDictionary(modal); + + return SUCCESS(response); + } + + /// + /// 更新PLC报警信息字典 + /// + /// + [HttpPut] + [ActionPermissionFilter(Permission = "alarmproblemdictionary:edit")] + [Log(Title = "PLC报警信息字典", BusinessType = BusinessType.UPDATE)] + public IActionResult UpdateAlarmProblemDictionary([FromBody] AlarmProblemDictionaryDto parm) + { + var modal = parm.Adapt().ToUpdate(HttpContext); + var response = _AlarmProblemDictionaryService.UpdateAlarmProblemDictionary(modal); + + return ToResponse(response); + } + + /// + /// 删除PLC报警信息字典 + /// + /// + [HttpPost("delete/{ids}")] + [ActionPermissionFilter(Permission = "alarmproblemdictionary:delete")] + [Log(Title = "PLC报警信息字典", BusinessType = BusinessType.DELETE)] + public IActionResult DeleteAlarmProblemDictionary([FromRoute]string ids) + { + var idArr = Tools.SplitAndConvert(ids); + + return ToResponse(_AlarmProblemDictionaryService.Delete(idArr)); + } + + } +} \ No newline at end of file diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs index 58d2181..921bf7b 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs @@ -234,7 +234,6 @@ namespace RIZO.Admin.WebApi.PLC.Service }); await Task.WhenAll(tasks); - _logger.LogInformation($"批量连接完成 | 已测试设备数:{validConfigs.Count}"); } /// @@ -242,7 +241,6 @@ namespace RIZO.Admin.WebApi.PLC.Service /// private async Task PollPlcDataAsync() { - _logger.LogInformation("开始轮询PLC生产数据..."); // 过滤有效配置(非空IP) var validConfigs = _plcConfigs.Where(c => !string.IsNullOrWhiteSpace(c.Ip)).ToList(); @@ -321,9 +319,6 @@ namespace RIZO.Admin.WebApi.PLC.Service if (success) { - // 日志输出关键业务数据(而非抽象的value) - _logger.LogInformation( - $"[{config.PlcName}] 生产数据读取成功 | IP:{config.Ip} | 产线:{prodData.LineCode} | 产品:{prodData.ProductCode} | 合格标志:{prodData.QualificationFlag} | 生产节拍:{prodData.ProductionCycle}s"); // 数据处理逻辑(示例:可替换为入库/推MQ/存Redis等) await ProcessPlcProductionDataAsync(config, prodData); @@ -364,10 +359,6 @@ namespace RIZO.Admin.WebApi.PLC.Service // 等待所有轮询任务完成 await Task.WhenAll(tasks); - - // 输出本轮轮询汇总日志 - _logger.LogInformation( - $"PLC生产数据轮询完成 | 总设备数:{validConfigs.Count} | 成功:{successCount} | 失败:{failCount} | 跳过:{skipCount}"); } /// diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs index 4c8d93e..91f2f0c 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs @@ -5,6 +5,7 @@ using RIZO.Admin.WebApi.PLC.Model; using RIZO.Common; using S7.Net; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; @@ -26,8 +27,12 @@ namespace RIZO.Admin.WebApi.PLC.Service private readonly List _plcConfigs; private readonly GlobalPlcConfig _globalConfig; - private PlcProductionDataService plcProductionDataService = new PlcProductionDataService(); - private PlantWorkstationService plantWorkstationService = new PlantWorkstationService(); + private PlcProductionDataService _plcProductionDataService = new PlcProductionDataService(); + private PlantWorkstationService _plantWorkstationService = new PlantWorkstationService(); + + // 先在类中添加2个核心优化字段(支撑高频访问) + private readonly SemaphoreSlim _concurrencySemaphore = new SemaphoreSlim(15, 50); // 限制20并发(适配50台PLC) + private readonly ConcurrentDictionary _plcConnPool = new(); // 连接池 // PLC地址映射(严格匹配业务地址清单) private readonly Dictionary _plcStringMap = new() @@ -67,8 +72,86 @@ namespace RIZO.Admin.WebApi.PLC.Service #endregion #region 核心业务方法 + ///// + ///// 读取PLC生产数据(严格匹配业务地址和解析规则) + ///// + ///// PLC IP地址 + ///// 机架号 + ///// 槽位号 + ///// PLC型号(默认S7-1500) + ///// 读取结果(状态+数据+消息) + //public async Task<(bool Success, PlcProductionData Data, string Message)> ReadProductionDataAsync( + // string ip, + // short rack, + // short slot, + // CpuType cpuType = CpuType.S71500) + //{ + // // 参数校验 + // if (string.IsNullOrWhiteSpace(ip)) + // return (false, null, "PLC IP地址不能为空"); + + // Plc plc = null; + // try + // { + // // 初始化PLC连接 + // plc = CreatePlcClient(cpuType, ip, rack, slot); + // await OpenPlcConnectionAsync(plc); + + // if (!plc.IsConnected) + // return (false, null, "PLC连接失败"); + + // // 构建生产数据实体 + // var prodData = new PlcProductionData + // { + // PlcIp = ip.Trim(), + // OccurTime = DateTime.Now, // MES自配时间 + + // // 字符串字段(按规则解析) + // LineCode = await ReadPlcStringAsync(plc, _plcStringMap["LineCode"].Addr, _plcStringMap["LineCode"].Len), + // ProductCode = await ReadPlcStringAsync(plc, _plcStringMap["ProductCode"].Addr, _plcStringMap["ProductCode"].Len), + // ProductName = await ReadPlcStringAsync(plc, _plcStringMap["ProductName"].Addr, _plcStringMap["ProductName"].Len), + // PartCode = await ReadPlcStringAsync(plc, _plcStringMap["PartCode"].Addr, _plcStringMap["PartCode"].Len), + // PartName = await ReadPlcStringAsync(plc, _plcStringMap["PartName"].Addr, _plcStringMap["PartName"].Len), + // ProcessName = await ReadPlcStringAsync(plc, _plcStringMap["ProcessName"].Addr, _plcStringMap["ProcessName"].Len), + // ParamName = await ReadPlcStringAsync(plc, _plcStringMap["ParamName"].Addr, _plcStringMap["ParamName"].Len), + // ParamValue = await ReadPlcStringAsync(plc, _plcStringMap["ParamValue"].Addr, _plcStringMap["ParamValue"].Len), + + // // 修复:改为异步读取整数,确保类型兼容 + // //合格标志(0默认,1合格,2不合格) + // QualificationFlag = (await ReadPlcIntAsync(plc, _plcIntMap["QualificationFlag"])).ToString(), + // //返工标志(0正常,1返工) + // ReworkFlag = (await ReadPlcIntAsync(plc, _plcIntMap["ReworkFlag"])).ToString(), + // //设备自动手动:0-自动,1-手动 + // AutoManual = (await ReadPlcIntAsync(plc, _plcIntMap["AutoManual"])), + // //运行状态:1正常,2异常 + // RunStatus = (await ReadPlcIntAsync(plc, _plcIntMap["RunStatus"])), + // //生产节拍秒 + // ProductionCycle = await ReadPlcIntAsync(plc, _plcIntMap["ProductionCycle"]) + // }; + + // // 空值兜底(避免入库报错) + // prodData.QualificationFlag ??= "0"; + // prodData.ReworkFlag ??= "0"; + // prodData.ProductionCycle ??= 0; + + // // 保存生产数据到MES数据库 + // _plcProductionDataService.AddPlcProductionData(prodData); + // return (true, prodData, "生产数据读取成功"); + // } + // catch (Exception ex) + // { + // return (false, null, $"生产数据读取失败:{ex.Message}"); + // } + // finally + // { + // ReleasePlcConnection(plc); + // } + //} + + + /// - /// 读取PLC生产数据(严格匹配业务地址和解析规则) + /// 读取PLC生产数据(优化版:连接池+并发控制+批量读取+异常防护) /// /// PLC IP地址 /// 机架号 @@ -81,68 +164,189 @@ namespace RIZO.Admin.WebApi.PLC.Service short slot, CpuType cpuType = CpuType.S71500) { - // 参数校验 + // 1. 强化参数校验(源头避免无效请求) if (string.IsNullOrWhiteSpace(ip)) return (false, null, "PLC IP地址不能为空"); + if (rack < 0 || rack > 10) // 工业场景机架号常规范围 + return (false, null, $"PLC机架号{rack}无效(有效值0-10)"); + if (slot < 0 || slot > 4) // 工业场景槽位号常规范围 + return (false, null, $"PLC槽位号{slot}无效(有效值0-4)"); Plc plc = null; + bool isConnReused = false; // 标记是否复用连接 + var poolKey = $"{ip}_{rack}_{slot}_{cpuType}"; // 连接池唯一标识 + + // 2. 并发控制(防止50台PLC同时访问导致端口/线程耗尽) + await _concurrencySemaphore.WaitAsync(); + try { - // 初始化PLC连接 - plc = CreatePlcClient(cpuType, ip, rack, slot); - await OpenPlcConnectionAsync(plc); + // 3. 连接池复用(核心优化:避免高频创建/销毁连接) + if (_plcConnPool.TryGetValue(poolKey, out var poolItem) && poolItem.Client.IsConnected) + { + plc = poolItem.Client; + _plcConnPool.TryUpdate(poolKey, (plc, DateTime.Now), poolItem); // 更新最后使用时间 + isConnReused = true; + } + else + { + // 初始化新连接(保留你的原始逻辑,增加重试) + plc = CreatePlcClient(cpuType, ip, rack, slot); + // 连接重试(应对临时网络波动) + int retryCount = 2; + bool isConnected = false; + while (retryCount-- > 0 && !isConnected) + { + try + { + await OpenPlcConnectionAsync(plc); + isConnected = plc.IsConnected; + } + catch + { + if (retryCount > 0) await Task.Delay(100); // 重试间隔 + } + } - if (!plc.IsConnected) - return (false, null, "PLC连接失败"); + if (!isConnected) + return (false, null, "PLC连接失败(含2次重试)"); - // 构建生产数据实体 + // 新连接加入池 + _plcConnPool.TryAdd(poolKey, (plc, DateTime.Now)); + } + + // 4. 批量并行读取(核心优化:替代串行await,提升效率30%+) + // 4.1 字符串字段批量读取 + var stringTasks = new Dictionary> + { + { "LineCode", ReadPlcStringAsync(plc, _plcStringMap["LineCode"].Addr, _plcStringMap["LineCode"].Len) }, + { "ProductCode", ReadPlcStringAsync(plc, _plcStringMap["ProductCode"].Addr, _plcStringMap["ProductCode"].Len) }, + { "ProductName", ReadPlcStringAsync(plc, _plcStringMap["ProductName"].Addr, _plcStringMap["ProductName"].Len) }, + { "PartCode", ReadPlcStringAsync(plc, _plcStringMap["PartCode"].Addr, _plcStringMap["PartCode"].Len) }, + { "PartName", ReadPlcStringAsync(plc, _plcStringMap["PartName"].Addr, _plcStringMap["PartName"].Len) }, + { "ProcessName", ReadPlcStringAsync(plc, _plcStringMap["ProcessName"].Addr, _plcStringMap["ProcessName"].Len) }, + { "ParamName", ReadPlcStringAsync(plc, _plcStringMap["ParamName"].Addr, _plcStringMap["ParamName"].Len) }, + { "ParamValue", ReadPlcStringAsync(plc, _plcStringMap["ParamValue"].Addr, _plcStringMap["ParamValue"].Len) } + }; + + // 4.2 整数字段批量读取 + var intTasks = new Dictionary> + { + { "QualificationFlag", ReadPlcIntAsync(plc, _plcIntMap["QualificationFlag"]) }, + { "ReworkFlag", ReadPlcIntAsync(plc, _plcIntMap["ReworkFlag"]) }, + { "AutoManual", ReadPlcIntAsync(plc, _plcIntMap["AutoManual"]) }, + { "RunStatus", ReadPlcIntAsync(plc, _plcIntMap["RunStatus"]) }, + { "ProductionCycle", ReadPlcIntAsync(plc, _plcIntMap["ProductionCycle"]) } + }; + + // 等待所有读取完成(并行执行,减少等待时间) + IEnumerable allTasks = stringTasks.Values.Cast() + .Concat(intTasks.Values.Cast()); + await Task.WhenAll(allTasks); + + // 5. 构建数据实体(空值安全处理,单个字段失败不影响整体) var prodData = new PlcProductionData { PlcIp = ip.Trim(), - OccurTime = DateTime.Now, // MES自配时间 + OccurTime = DateTime.Now, - // 字符串字段(按规则解析) - LineCode = await ReadPlcStringAsync(plc, _plcStringMap["LineCode"].Addr, _plcStringMap["LineCode"].Len), - ProductCode = await ReadPlcStringAsync(plc, _plcStringMap["ProductCode"].Addr, _plcStringMap["ProductCode"].Len), - ProductName = await ReadPlcStringAsync(plc, _plcStringMap["ProductName"].Addr, _plcStringMap["ProductName"].Len), - PartCode = await ReadPlcStringAsync(plc, _plcStringMap["PartCode"].Addr, _plcStringMap["PartCode"].Len), - PartName = await ReadPlcStringAsync(plc, _plcStringMap["PartName"].Addr, _plcStringMap["PartName"].Len), - ProcessName = await ReadPlcStringAsync(plc, _plcStringMap["ProcessName"].Addr, _plcStringMap["ProcessName"].Len), - ParamName = await ReadPlcStringAsync(plc, _plcStringMap["ParamName"].Addr, _plcStringMap["ParamName"].Len), - ParamValue = await ReadPlcStringAsync(plc, _plcStringMap["ParamValue"].Addr, _plcStringMap["ParamValue"].Len), + // 字符串字段(空值兜底) + LineCode = await stringTasks["LineCode"] ?? string.Empty, + ProductCode = await stringTasks["ProductCode"] ?? string.Empty, + ProductName = await stringTasks["ProductName"] ?? string.Empty, + PartCode = await stringTasks["PartCode"] ?? string.Empty, + PartName = await stringTasks["PartName"] ?? string.Empty, + ProcessName = await stringTasks["ProcessName"] ?? string.Empty, + ParamName = await stringTasks["ParamName"] ?? string.Empty, + ParamValue = await stringTasks["ParamValue"] ?? string.Empty, - // 修复:改为异步读取整数,确保类型兼容 - //合格标志(0默认,1合格,2不合格) - QualificationFlag = (await ReadPlcIntAsync(plc, _plcIntMap["QualificationFlag"])).ToString(), - //返工标志(0正常,1返工) - ReworkFlag = (await ReadPlcIntAsync(plc, _plcIntMap["ReworkFlag"])).ToString(), - //设备自动手动:0-自动,1-手动 - AutoManual = (await ReadPlcIntAsync(plc, _plcIntMap["AutoManual"])), - //运行状态:1正常,2异常 - RunStatus = (await ReadPlcIntAsync(plc, _plcIntMap["AutoManual"])), - //生产节拍秒 - ProductionCycle = await ReadPlcIntAsync(plc, _plcIntMap["ProductionCycle"]) + // 整数字段(读取失败返回0,不中断流程) + QualificationFlag = (await intTasks["QualificationFlag"]).ToString(), + ReworkFlag = (await intTasks["ReworkFlag"]).ToString(), + AutoManual = await intTasks["AutoManual"], + RunStatus = await intTasks["RunStatus"], + ProductionCycle = await intTasks["ProductionCycle"] }; - // 空值兜底(避免入库报错) + // 空值兜底(保留你的原始逻辑) prodData.QualificationFlag ??= "0"; prodData.ReworkFlag ??= "0"; prodData.ProductionCycle ??= 0; - // 保存生产数据到MES数据库 - plcProductionDataService.AddPlcProductionData(prodData); - return (true, prodData, "生产数据读取成功"); + // 6. 异步保存数据(非阻塞,提升响应速度) + _ = Task.Run(() => _plcProductionDataService.AddPlcProductionData(prodData)) + .ContinueWith(t => + { + if (t.IsFaulted) + { + // 记录保存失败日志(建议替换为ILogger) + Console.WriteLine($"PLC({ip})数据保存失败:{t.Exception?.InnerException?.Message}"); + } + }); + + var successMsg = isConnReused ? "生产数据读取成功(复用连接)" : "生产数据读取成功(新建连接)"; + return (true, prodData, successMsg); } catch (Exception ex) { + // 精细化异常日志(便于定位具体PLC故障) + Console.WriteLine($"PLC({ip})生产数据读取异常:{ex.Message}\n{ex.StackTrace}"); return (false, null, $"生产数据读取失败:{ex.Message}"); } finally { - ReleasePlcConnection(plc); + // 7. 资源安全释放(核心:复用连接不释放,仅新建连接释放;必须释放并发信号量) + if (!isConnReused) + { + ReleasePlcConnection(plc); + } + _concurrencySemaphore.Release(); // 防止死锁的关键 } } + // 可选:添加连接池清理方法(防止长期运行导致连接泄露) + private void CleanupExpiredConnections() + { + var expiredTime = DateTime.Now.AddMinutes(-5); // 5分钟未使用的连接清理 + // 先复制所有Key到列表,避免遍历中修改原集合 + var allKeys = _plcConnPool.Keys.ToList(); + + foreach (var key in allKeys) + { + if (!_plcConnPool.TryGetValue(key, out var poolItem)) + continue; + + if (poolItem.LastUsedTime < expiredTime) + { + if (_plcConnPool.TryRemove(key, out var removedItem)) + { + try + { + if (removedItem.Client != null && removedItem.Client.IsConnected) + removedItem.Client.Close(); + if (removedItem.Client is IDisposable disposable) + disposable.Dispose(); + // 可选:置空引用,帮助GC回收 + removedItem.Client = null; + } + catch + { + // 静默释放,避免清理失败影响主流程 + } + } + } + } + } + + // 在类的构造函数中启动定时清理(可选) + // public PlcService(IOptions globalConfig) + // { + // _globalConfig = globalConfig?.Value ?? throw new ArgumentNullException(nameof(globalConfig)); + // _plcConfigs = initPlcConfigs(_plcConfigs); + // // 每30秒清理一次过期连接 + // new Timer(_ => CleanupExpiredConnections(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); + // } + /// /// 测试单个PLC的连接、读、写功能 /// @@ -391,16 +595,15 @@ namespace RIZO.Admin.WebApi.PLC.Service { if (plc.IsConnected) plc.Close(); + // 正确释放PLC实例(适配S7.Net显式实现IDisposable) + if (plc is IDisposable disposable) + disposable.Dispose(); } catch (Exception ex) { - // 记录日志(此处简化为控制台输出,实际项目替换为日志框架) Console.WriteLine($"PLC连接关闭失败:{ex.Message}"); } - finally - { - Dispose(); - } + // 移除finally中的Dispose(),避免误释放服务 } /// @@ -621,7 +824,7 @@ namespace RIZO.Admin.WebApi.PLC.Service var defaultResult = result ?? new List(); try { - List query = plantWorkstationService.Queryable() + List query = _plantWorkstationService.Queryable() .Where(it => it.Status == 1) .Select(it => new PlcConfig { diff --git a/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj b/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj index cefc6c9..442c097 100644 --- a/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj +++ b/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj @@ -35,6 +35,7 @@ + diff --git a/RIZO.Admin.WebApi/appsettings.Development.json b/RIZO.Admin.WebApi/appsettings.Development.json index fbf0ec3..7662c5a 100644 --- a/RIZO.Admin.WebApi/appsettings.Development.json +++ b/RIZO.Admin.WebApi/appsettings.Development.json @@ -4,7 +4,8 @@ //"Conn": "Data Source=139.224.232.211;User ID=root;Password=RIZOtech123;Initial Catalog=ay2509055-guiyang-fluorescence-lmes;Port=3308;", //"Conn": "Data Source=43.142.238.124;User ID=root;Password=mysql_3AMPxs;Initial Catalog=valeo_lmes;Port=3308;", // "Conn": "Server=43.142.238.124,1433;Database=valeo_lmes_2;User Id=sa;Password=MSSQLP0ss_MBTc7D;Encrypt=True;TrustServerCertificate=True;", - "Conn": "Server=127.0.0.1,1433;Database=valeo_lmes;User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", + //"Conn": "Server=127.0.0.1,1433;Database=valeo_lmes;User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", + "Conn": "Server=192.168.1.48,1433;Database=valeo_lmes;User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", //"Conn": "Data Source=127.0.0.1;User ID=root;Password=123456;Initial Catalog=valeo_lmes;Port=3306;", "DbType": 1, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3,PgSql = 4 "ConfigId": "0", //多租户唯一标识 @@ -18,7 +19,8 @@ //"Conn": "Data Source=139.224.232.211;User ID=root;Password=RIZOtech123;Initial Catalog={dbName};Port=3308;", //"Conn": "Data Source=192.168.1.48;User ID=root;Password=123456;Initial Catalog={dbName};Port=3306;", //"Conn": "Data Source=127.0.0.1;User ID=root;Password=123456;Initial Catalog={dbName};Port=3306;", - "Conn": "Server=127.0.0.1,1433;Database={dbName};User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", + //"Conn": "Server=127.0.0.1,1433;Database={dbName};User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", + "Conn": "Server=192.168.1.48,1433;Database={dbName};User Id=root;Password=123456;Encrypt=True;TrustServerCertificate=True;", "IsAutoCloseConnection": true, "DbName": "valeo_lmes", //代码生成默认连接数据库,Oracle库是实例的名称 "DbType": 1 diff --git a/RIZO.Model/MES/alarm/AlarmInfo.cs b/RIZO.Model/MES/alarm/AlarmInfo.cs new file mode 100644 index 0000000..3274b28 --- /dev/null +++ b/RIZO.Model/MES/alarm/AlarmInfo.cs @@ -0,0 +1,64 @@ + +namespace RIZO.Model.MES.alarm +{ + /// + /// PLC报警信息 + /// + [SugarTable("alarm_info")] + public class AlarmInfo + { + /// + /// ID + /// + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public int Id { get; set; } + + /// + /// 工站编码 + /// + public string WorkstationCode { get; set; } + + /// + /// 报警问题类型 + /// + [SugarColumn(ColumnName = "err_type")] + public string ErrType { get; set; } + + /// + /// 问题编码 + /// + [SugarColumn(ColumnName = "err_code")] + public string ErrCode { get; set; } + + /// + /// 创建人 + /// + [SugarColumn(ColumnName = "created_by")] + public string CreatedBy { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(ColumnName = "created_time")] + public DateTime? CreatedTime { get; set; } + + /// + /// 更新人 + /// + [SugarColumn(ColumnName = "updated_by")] + public string UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnName = "updated_time")] + public DateTime? UpdatedTime { get; set; } + + /// + /// PLCIP + /// + [SugarColumn(ColumnName = "plc_ip")] + public string PlcIp { get; set; } + + } +} \ No newline at end of file diff --git a/RIZO.Model/MES/alarm/AlarmProblemDictionary.cs b/RIZO.Model/MES/alarm/AlarmProblemDictionary.cs new file mode 100644 index 0000000..6931af0 --- /dev/null +++ b/RIZO.Model/MES/alarm/AlarmProblemDictionary.cs @@ -0,0 +1,47 @@ + +namespace RIZO.Model.MES.alarm +{ + /// + /// PLC报警信息字典 + /// + [SugarTable("AlarmProblemDictionary")] + public class AlarmProblemDictionary + { + /// + /// Id + /// + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public int Id { get; set; } + + /// + /// WorkstationCode + /// + public string WorkstationCode { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// DataType + /// + public string DataType { get; set; } + + /// + /// LogicalAddress + /// + public string LogicalAddress { get; set; } + + /// + /// Comment + /// + public string Comment { get; set; } + + /// + /// CreatedTime + /// + public DateTime? CreatedTime { get; set; } + + } +} \ No newline at end of file diff --git a/RIZO.Model/MES/alarm/Dto/AlarmInfoDto.cs b/RIZO.Model/MES/alarm/Dto/AlarmInfoDto.cs new file mode 100644 index 0000000..8628954 --- /dev/null +++ b/RIZO.Model/MES/alarm/Dto/AlarmInfoDto.cs @@ -0,0 +1,40 @@ + +namespace RIZO.Model.MES.alarm.Dto +{ + /// + /// PLC报警信息查询对象 + /// + public class AlarmInfoQueryDto : PagerInfo + { + } + + /// + /// PLC报警信息输入输出对象 + /// + public class AlarmInfoDto + { + [Required(ErrorMessage = "ID不能为空")] + public int Id { get; set; } + + public string WorkstationCode { get; set; } + + public string ErrType { get; set; } + + public string ErrCode { get; set; } + + public string CreatedBy { get; set; } + + public DateTime? CreatedTime { get; set; } + + public string UpdatedBy { get; set; } + + public DateTime? UpdatedTime { get; set; } + + public string PlcIp { get; set; } + + + + [ExcelColumn(Name = "报警问题类型")] + public string ErrTypeLabel { get; set; } + } +} \ No newline at end of file diff --git a/RIZO.Model/MES/alarm/Dto/AlarmProblemDictionaryDto.cs b/RIZO.Model/MES/alarm/Dto/AlarmProblemDictionaryDto.cs new file mode 100644 index 0000000..4d1de7c --- /dev/null +++ b/RIZO.Model/MES/alarm/Dto/AlarmProblemDictionaryDto.cs @@ -0,0 +1,36 @@ + +namespace RIZO.Model.MES.alarm.Dto +{ + /// + /// PLC报警信息字典查询对象 + /// + public class AlarmProblemDictionaryQueryDto : PagerInfo + { + } + + /// + /// PLC报警信息字典输入输出对象 + /// + public class AlarmProblemDictionaryDto + { + [Required(ErrorMessage = "Id不能为空")] + public int Id { get; set; } + + public string WorkstationCode { get; set; } + + public string Name { get; set; } + + public string DataType { get; set; } + + public string LogicalAddress { get; set; } + + public string Comment { get; set; } + + public DateTime? CreatedTime { get; set; } + + + + [ExcelColumn(Name = "DataType")] + public string DataTypeLabel { get; set; } + } +} \ No newline at end of file diff --git a/RIZO.Model/RIZO.Model.csproj b/RIZO.Model/RIZO.Model.csproj index 8dae0f3..a87c5fb 100644 --- a/RIZO.Model/RIZO.Model.csproj +++ b/RIZO.Model/RIZO.Model.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/RIZO.Service/MES/alarm/AlarmInfoService.cs b/RIZO.Service/MES/alarm/AlarmInfoService.cs new file mode 100644 index 0000000..61f0450 --- /dev/null +++ b/RIZO.Service/MES/alarm/AlarmInfoService.cs @@ -0,0 +1,79 @@ +using Infrastructure.Attribute; +using Infrastructure.Extensions; +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; +using RIZO.Repository; +using RIZO.Service.MES.alarm.IService; + +namespace RIZO.Service.MES.alarm +{ + /// + /// PLC报警信息Service业务层处理 + /// + [AppService(ServiceType = typeof(IAlarmInfoService), ServiceLifetime = LifeTime.Transient)] + public class AlarmInfoService : BaseService, IAlarmInfoService + { + /// + /// 查询PLC报警信息列表 + /// + /// + /// + public PagedInfo GetList(AlarmInfoQueryDto parm) + { + var predicate = QueryExp(parm); + + var response = Queryable() + .Where(predicate.ToExpression()) + .ToPage(parm); + + return response; + } + + + /// + /// 获取详情 + /// + /// + /// + public AlarmInfo GetInfo(int Id) + { + var response = Queryable() + .Where(x => x.Id == Id) + .First(); + + return response; + } + + /// + /// 添加PLC报警信息 + /// + /// + /// + public AlarmInfo AddAlarmInfo(AlarmInfo model) + { + return Insertable(model).ExecuteReturnEntity(); + } + + /// + /// 修改PLC报警信息 + /// + /// + /// + public int UpdateAlarmInfo(AlarmInfo model) + { + return Update(model, true); + } + + /// + /// 查询导出表达式 + /// + /// + /// + private static Expressionable QueryExp(AlarmInfoQueryDto parm) + { + var predicate = Expressionable.Create(); + + return predicate; + } + } +} \ No newline at end of file diff --git a/RIZO.Service/MES/alarm/AlarmProblemDictionaryService.cs b/RIZO.Service/MES/alarm/AlarmProblemDictionaryService.cs new file mode 100644 index 0000000..3d5732e --- /dev/null +++ b/RIZO.Service/MES/alarm/AlarmProblemDictionaryService.cs @@ -0,0 +1,80 @@ +using Infrastructure.Attribute; +using Infrastructure.Extensions; +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; +using RIZO.Repository; +using RIZO.Service.Mes.IMesService; +using RIZO.Service.MES.alarm.IService; + +namespace RIZO.Service.MES.alarm +{ + /// + /// PLC报警信息字典Service业务层处理 + /// + [AppService(ServiceType = typeof(IAlarmProblemDictionaryService), ServiceLifetime = LifeTime.Transient)] + public class AlarmProblemDictionaryService : BaseService, IAlarmProblemDictionaryService + { + /// + /// 查询PLC报警信息字典列表 + /// + /// + /// + public PagedInfo GetList(AlarmProblemDictionaryQueryDto parm) + { + var predicate = QueryExp(parm); + + var response = Queryable() + .Where(predicate.ToExpression()) + .ToPage(parm); + + return response; + } + + + /// + /// 获取详情 + /// + /// + /// + public AlarmProblemDictionary GetInfo(int Id) + { + var response = Queryable() + .Where(x => x.Id == Id) + .First(); + + return response; + } + + /// + /// 添加PLC报警信息字典 + /// + /// + /// + public AlarmProblemDictionary AddAlarmProblemDictionary(AlarmProblemDictionary model) + { + return Insertable(model).ExecuteReturnEntity(); + } + + /// + /// 修改PLC报警信息字典 + /// + /// + /// + public int UpdateAlarmProblemDictionary(AlarmProblemDictionary model) + { + return Update(model, true); + } + + /// + /// 查询导出表达式 + /// + /// + /// + private static Expressionable QueryExp(AlarmProblemDictionaryQueryDto parm) + { + var predicate = Expressionable.Create(); + + return predicate; + } + } +} \ No newline at end of file diff --git a/RIZO.Service/MES/alarm/IService/IAlarmInfoService.cs b/RIZO.Service/MES/alarm/IService/IAlarmInfoService.cs new file mode 100644 index 0000000..58064b9 --- /dev/null +++ b/RIZO.Service/MES/alarm/IService/IAlarmInfoService.cs @@ -0,0 +1,21 @@ +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; + +namespace RIZO.Service.MES.alarm.IService +{ + /// + /// PLC报警信息service接口 + /// + public interface IAlarmInfoService : IBaseService + { + PagedInfo GetList(AlarmInfoQueryDto parm); + + AlarmInfo GetInfo(int Id); + + + AlarmInfo AddAlarmInfo(AlarmInfo parm); + int UpdateAlarmInfo(AlarmInfo parm); + + + } +} diff --git a/RIZO.Service/MES/alarm/IService/IAlarmProblemDictionaryService.cs b/RIZO.Service/MES/alarm/IService/IAlarmProblemDictionaryService.cs new file mode 100644 index 0000000..9ccceb0 --- /dev/null +++ b/RIZO.Service/MES/alarm/IService/IAlarmProblemDictionaryService.cs @@ -0,0 +1,23 @@ + + +using RIZO.Model.MES.alarm; +using RIZO.Model.MES.alarm.Dto; + +namespace RIZO.Service.MES.alarm.IService +{ + /// + /// PLC报警信息字典service接口 + /// + public interface IAlarmProblemDictionaryService : IBaseService + { + PagedInfo GetList(AlarmProblemDictionaryQueryDto parm); + + AlarmProblemDictionary GetInfo(int Id); + + + AlarmProblemDictionary AddAlarmProblemDictionary(AlarmProblemDictionary parm); + int UpdateAlarmProblemDictionary(AlarmProblemDictionary parm); + + + } +}