From f897d641b4f2ce0aca3cc035c73da9e32ce8619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=AD=A3=E6=98=93?= Date: Tue, 13 May 2025 16:37:22 +0800 Subject: [PATCH] =?UTF-8?q?MQTT=E5=85=A8=E5=B1=80=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E8=AE=A2=E9=98=85=EF=BC=8C=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=EF=BC=8C=E6=A0=87=E7=AD=BE=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E7=AD=89=E5=8A=9F=E8=83=BD=E5=9F=BA=E6=9C=AC=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/mqtt/MqttController.cs | 52 +++ ...y-6f790ae9-1260-4b42-ac56-6835d3555186.xml | 16 + ZR.Admin.WebApi/Program.cs | 7 + ZR.Admin.WebApi/appsettings.json | 17 + ZR.Common/MqttHelper/MqttService.cs | 424 ++++++++++++++++++ ZR.Common/MqttHelper/MyMqttConfig.cs | 97 ++++ ZR.Common/ZR.Common.csproj | 3 +- .../DTO/backend/QcBackEndPrintMqttEventDto.cs | 33 ++ ZR.Service/ZR.Service.csproj | 4 + ZR.Service/mes/qc/backend/QcBackEndService.cs | 277 ++++++++---- 10 files changed, 843 insertions(+), 87 deletions(-) create mode 100644 ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs create mode 100644 ZR.Admin.WebApi/DataProtection/key-6f790ae9-1260-4b42-ac56-6835d3555186.xml create mode 100644 ZR.Common/MqttHelper/MqttService.cs create mode 100644 ZR.Common/MqttHelper/MyMqttConfig.cs create mode 100644 ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs diff --git a/ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs b/ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs new file mode 100644 index 00000000..389dd860 --- /dev/null +++ b/ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using MQTTnet.Protocol; +using ZR.Common.MqttHelper; +using ZR.Model.MES.wms; +using ZR.Model.MES.wms.Dto; +using ZR.Service.mes.wms.IService; + +namespace ZR.Admin.WebApi.Controllers +{ + /// + /// agv 相关接口 + /// + + [Route("/mqtt")] + public class MqttController : BaseController + { + private readonly MqttService _mqttService; + + public MqttController(MqttService mqttService) + { + _mqttService = mqttService; + } + + /// + /// 1. 发布信息 + /// + /// 主题 + /// 信息 + /// + + [HttpPost("publish")] + public async Task PublishMessage(string topic, string payload) + { + try + { + // 发布消息到MQTT代理服务器 + await _mqttService.PublishAsync( + topic, + payload, + MqttQualityOfServiceLevel.AtLeastOnce, + false + ); + + return Ok("消息已发布"); + } + catch (Exception ex) + { + return StatusCode(500, $"发布消息失败: {ex.Message}"); + } + } + } +} diff --git a/ZR.Admin.WebApi/DataProtection/key-6f790ae9-1260-4b42-ac56-6835d3555186.xml b/ZR.Admin.WebApi/DataProtection/key-6f790ae9-1260-4b42-ac56-6835d3555186.xml new file mode 100644 index 00000000..ef0573f9 --- /dev/null +++ b/ZR.Admin.WebApi/DataProtection/key-6f790ae9-1260-4b42-ac56-6835d3555186.xml @@ -0,0 +1,16 @@ + + + 2025-05-13T00:45:18.9703791Z + 2025-05-13T00:45:18.9163673Z + 2025-08-11T00:45:18.9163673Z + + + + + + + dlysrp3IsJDOznQcS5WVSPFQ68bSJBN8cF8lnJ1+a0QzUsCf9KgzUDoQsgZ/9q/pyoKtTZJCnEoXO+m5nUaj3Q== + + + + \ No newline at end of file diff --git a/ZR.Admin.WebApi/Program.cs b/ZR.Admin.WebApi/Program.cs index 5836a6b1..7c485961 100644 --- a/ZR.Admin.WebApi/Program.cs +++ b/ZR.Admin.WebApi/Program.cs @@ -11,6 +11,7 @@ using ZR.Admin.WebApi.Framework; using ZR.Admin.WebApi.Hubs; using ZR.Admin.WebApi.Middleware; using ZR.Common.Cache; +using ZR.Common.MqttHelper; var builder = WebApplication.CreateBuilder(args); @@ -21,6 +22,12 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); //注入HttpContextAccessor builder.Services.AddSingleton(); +// 注册MyMqttConfig(依赖ILogger和IConfiguration) +builder.Services.AddSingleton(); +// 注册MqttService为单例服务,并作为后台服务运行 !!!! 这样注册就行了 ================ +builder.Services.AddSingleton(); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +/// =============================================================================== // 跨域配置 builder.Services.AddCors(builder.Configuration); // 显示logo diff --git a/ZR.Admin.WebApi/appsettings.json b/ZR.Admin.WebApi/appsettings.json index 0b55ff4e..d1643bae 100644 --- a/ZR.Admin.WebApi/appsettings.json +++ b/ZR.Admin.WebApi/appsettings.json @@ -1,3 +1,20 @@ { //DOANtech123 + "MqttConfig": { + "ClientId": "shgg-mes-server", + "Server": "192.168.23.165", + "Port": 1883, + "Username": "admin", + "Password": "123456", + "Topics": [ + { + "Topic": "devices/#", + "QualityOfServiceLevel": "AtLeastOnce" + }, + { + "Topic": "system/alert", + "QualityOfServiceLevel": "AtLeastOnce" + } + ] + } } \ No newline at end of file diff --git a/ZR.Common/MqttHelper/MqttService.cs b/ZR.Common/MqttHelper/MqttService.cs new file mode 100644 index 00000000..bbe25367 --- /dev/null +++ b/ZR.Common/MqttHelper/MqttService.cs @@ -0,0 +1,424 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Attribute; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Protocol; + +namespace ZR.Common.MqttHelper +{ + public class MqttService : IHostedService, IDisposable + { + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IMqttClient _mqttClient; + private readonly MyMqttConfig _mqttConfig; // 注入配置类 + private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1); + private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); + private Timer _reconnectTimer; + private bool _disposed = false; + private int _reconnectAttempts = 0; + + // 配置常量 + private const string MqttConfigSection = "MqttConfig"; + private const string TopicsConfigKey = "Topics"; + private const int DefaultReconnectDelaySeconds = 5; + private const int MaxReconnectAttempts = 10; + private const int MaxReconnectDelaySeconds = 60; + + public MqttService( + ILogger logger, + IConfiguration configuration, + MyMqttConfig mqttConfig + ) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = + configuration ?? throw new ArgumentNullException(nameof(configuration)); + _mqttConfig = mqttConfig ?? throw new ArgumentNullException(nameof(mqttConfig)); // 注入配置类 + // 创建MQTT客户端 + var factory = new MqttFactory(); + _mqttClient = factory.CreateMqttClient(); + + // 注册事件处理程序 + _mqttClient.ConnectedAsync += OnConnectedAsync; + _mqttClient.DisconnectedAsync += OnDisconnectedAsync; + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + _logger.LogInformation($"MqttService 实例已创建,哈希值: {GetHashCode()}"); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("MQTT服务正在启动..."); + + try + { + await ConnectAsync(cancellationToken); + _logger.LogInformation("MQTT服务已成功启动"); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "MQTT服务启动失败,将在后台尝试重连"); + ScheduleReconnect(); // 即使启动失败,也应该尝试重连 + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("MQTT服务正在停止..."); + + // 取消所有计划的重连 + _disposeCts.Cancel(); + _reconnectTimer?.Dispose(); + + await DisconnectAsync(cancellationToken); + _logger.LogInformation("MQTT服务已停止"); + } + + private async Task ConnectAsync(CancellationToken cancellationToken) + { + await _connectionLock.WaitAsync(cancellationToken); + try + { + if (_mqttClient.IsConnected) + { + _logger.LogDebug("MQTT客户端已连接,跳过连接操作"); + return; + } + // 直接使用MyMqttConfig获取客户端选项 + var options = _mqttConfig.BuildMqttClientOptions(); + _logger.LogInformation($"正在连接到MQTT代理服务器 {options.ChannelOptions}..."); + try + { + await _mqttClient.ConnectAsync(options, cancellationToken); + _reconnectAttempts = 0; // 重置重连计数 + _logger.LogInformation("已成功连接到MQTT代理服务器"); + } + catch (OperationCanceledException) + { + _logger.LogWarning("MQTT连接操作被取消"); + throw; + } + catch (Exception ex) + { + _reconnectAttempts++; + _logger.LogError( + ex, + $"连接MQTT代理服务器失败 (尝试次数: {_reconnectAttempts}/{MaxReconnectAttempts})" + ); + + if (_reconnectAttempts >= MaxReconnectAttempts) + { + _logger.LogCritical("达到最大重连次数,停止尝试"); + return; + } + + ScheduleReconnect(); + throw; + } + } + finally + { + _connectionLock.Release(); + } + } + + private MqttClientOptions BuildMqttClientOptions(IConfigurationSection config) + { + var builder = new MqttClientOptionsBuilder() + .WithClientId(config["ClientId"] ?? Guid.NewGuid().ToString()) + .WithTcpServer(config["Server"], config.GetValue("Port", 1883)) + .WithCleanSession(); + return builder.Build(); + } + + private async Task DisconnectAsync(CancellationToken cancellationToken) + { + if (!_mqttClient.IsConnected) + { + _logger.LogDebug("MQTT客户端未连接,跳过断开操作"); + return; + } + + try + { + await _mqttClient.DisconnectAsync(cancellationToken: cancellationToken); + _logger.LogInformation("已成功断开与MQTT代理服务器的连接"); + } + catch (OperationCanceledException) + { + _logger.LogWarning("MQTT断开操作被取消"); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "断开MQTT连接时出错"); + // 即使断开连接失败,也应该释放资源 + CleanupClientResources(); + } + } + + private Task OnConnectedAsync(MqttClientConnectedEventArgs e) + { + _logger.LogInformation("MQTT连接已建立"); + return SubscribeToTopicsAsync(); + } + + private async Task OnDisconnectedAsync(MqttClientDisconnectedEventArgs e) + { + _logger.LogWarning($"MQTT连接已断开 - 客户端是否之前已连接: {e.ClientWasConnected}"); + + if (_disposed || _disposeCts.IsCancellationRequested) + { + _logger.LogDebug("MQTT断开连接是预期行为,正在关闭服务"); + return; + } + + if (e.ClientWasConnected) + { + ScheduleReconnect(); + } + } + + private async Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) + { + try + { + var payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment); + _logger.LogInformation( + $"收到MQTT消息 - 主题: {e.ApplicationMessage.Topic}, QoS: {e.ApplicationMessage.QualityOfServiceLevel}" + ); + + // 消息处理委托给专用的处理器 + await ProcessMessageAsync(e.ApplicationMessage.Topic, payload); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理MQTT消息时出错"); + } + } + + private async Task ProcessMessageAsync(string topic, string payload) + { + try + { + // 这里可以根据主题路由消息到不同的处理程序 + switch (topic) + { + case string t when t.StartsWith("devices/"): + await HandleDeviceMessage(topic, payload); + break; + case "system/alert": + await HandleSystemAlert(payload); + break; + default: + _logger.LogDebug($"未处理的MQTT主题: {topic}"); + break; + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"处理主题 '{topic}' 的消息时出错"); + } + } + + private async Task SubscribeToTopicsAsync() + { + if (!_mqttClient.IsConnected) + { + _logger.LogWarning("无法订阅主题:MQTT客户端未连接"); + return; + } + + try + { + // 获取配置类生成的订阅选项 + var subscribeOptions = _mqttConfig.GetSubscribeToTopicsAsync(); + + if (subscribeOptions == null || !subscribeOptions.TopicFilters.Any()) + { + _logger.LogInformation("没有配置要订阅的MQTT主题"); + return; + } + + _logger.LogInformation( + $"正在订阅MQTT主题: {string.Join(", ", subscribeOptions.TopicFilters.Select(f => f.Topic))}" + ); + + // 使用强类型的订阅选项进行订阅 + var result = await _mqttClient.SubscribeAsync(subscribeOptions); + + foreach (var item in result.Items) + { + _logger.LogInformation( + $"订阅主题 '{item.TopicFilter.Topic}' 结果: {item.ResultCode}" + ); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "订阅MQTT主题时出错"); + ScheduleReconnect(); + } + } + + private void ScheduleReconnect() + { + // 实现指数退避算法 + var delaySeconds = Math.Min( + DefaultReconnectDelaySeconds * (int)Math.Pow(2, Math.Min(_reconnectAttempts, 5)), + MaxReconnectDelaySeconds + ); + + _logger.LogInformation($"计划在 {delaySeconds} 秒后尝试重新连接MQTT代理服务器"); + + // 使用Timer替代Task.Run,更好地控制资源 + _reconnectTimer?.Dispose(); + _reconnectTimer = new Timer( + async _ => + { + if (_disposed || _disposeCts.IsCancellationRequested) + { + _logger.LogDebug("跳过重连:服务正在关闭"); + return; + } + + try + { + await ConnectAsync(_disposeCts.Token); + } + catch (Exception ex) + { + _logger.LogError(ex, "计划的MQTT重连失败"); + } + }, + null, + TimeSpan.FromSeconds(delaySeconds), + Timeout.InfiniteTimeSpan + ); + } + + private Task HandleDeviceMessage(string topic, string payload) + { + _logger.LogInformation($"处理设备消息: {topic} - {payload}"); + // 这里添加设备消息处理逻辑 + return Task.CompletedTask; + } + + private Task HandleSystemAlert(string payload) + { + _logger.LogWarning($"系统警报: {payload}"); + // 这里添加系统警报处理逻辑 + return Task.CompletedTask; + } + + public async Task PublishAsync( + string topic, + string payload, + MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce, + bool retain = false + ) + { + if (string.IsNullOrEmpty(topic)) + { + throw new ArgumentNullException(nameof(topic), "MQTT主题不能为空"); + } + + if (payload == null) + { + throw new ArgumentNullException(nameof(payload), "MQTT消息内容不能为空"); + } + + await _connectionLock.WaitAsync(_disposeCts.Token); + try + { + if (!_mqttClient.IsConnected) + { + _logger.LogWarning("发布消息前需要连接MQTT代理服务器"); + //await ConnectAsync(_disposeCts.Token); + throw new Exception("发布消息前需要连接MQTT代理服务器"); + } + + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qos) + .WithRetainFlag(retain) + .Build(); + + _logger.LogDebug($"准备发布MQTT消息 - 主题: {topic}, QoS: {qos}, 保留: {retain}"); + + try + { + var result = await _mqttClient.PublishAsync(message, _disposeCts.Token); + _logger.LogInformation($"已发布MQTT消息 - 主题: {topic}, 结果: {result.ReasonCode}"); + } + catch (OperationCanceledException) + { + _logger.LogWarning("MQTT发布操作被取消"); + throw; + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"发布MQTT消息失败 - 主题: {topic}"); + throw; + } + finally + { + _connectionLock.Release(); + } + } + + private void CleanupClientResources() + { + try + { + _mqttClient.ApplicationMessageReceivedAsync -= OnMessageReceivedAsync; + _mqttClient.ConnectedAsync -= OnConnectedAsync; + _mqttClient.DisconnectedAsync -= OnDisconnectedAsync; + + if (_mqttClient.IsConnected) + { + _mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + } + + _mqttClient.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "清理MQTT客户端资源时出错"); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _disposeCts.Cancel(); + _reconnectTimer?.Dispose(); + _connectionLock.Dispose(); + CleanupClientResources(); + _disposeCts.Dispose(); + } + + _disposed = true; + _logger.LogDebug("MQTT服务已释放所有资源"); + } + } + } +} diff --git a/ZR.Common/MqttHelper/MyMqttConfig.cs b/ZR.Common/MqttHelper/MyMqttConfig.cs new file mode 100644 index 00000000..4d85b4e3 --- /dev/null +++ b/ZR.Common/MqttHelper/MyMqttConfig.cs @@ -0,0 +1,97 @@ +using Infrastructure.Attribute; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static Org.BouncyCastle.Math.EC.ECCurve; + +namespace ZR.Common.MqttHelper +{ + [AppService(ServiceLifetime = LifeTime.Singleton)] + public class MyMqttConfig + { + // 配置常量 + private const string MqttConfigSection = "MqttConfig"; + private const string TopicsConfigKey = "Topics"; + private const int DefaultReconnectDelaySeconds = 5; + private const int MaxReconnectAttempts = 10; + private const int MaxReconnectDelaySeconds = 60; + + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public MyMqttConfig(ILogger logger, IConfiguration configuration) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = configuration; + } + + public MqttClientOptions BuildMqttClientOptions() + { + var mqttConfig = _configuration.GetSection(MqttConfigSection); + var builder = new MqttClientOptionsBuilder() + .WithClientId(mqttConfig["ClientId"] ?? Guid.NewGuid().ToString()) + .WithTcpServer(mqttConfig["Server"], mqttConfig.GetValue("Port", 1883)) + .WithCleanSession(); + return builder.Build(); + } + + public MqttClientSubscribeOptions GetSubscribeToTopicsAsync() + { + var topicsSection = _configuration.GetSection($"{MqttConfigSection}:{TopicsConfigKey}"); + var topicConfigs = topicsSection.Get>() ?? new List(); + + if (!topicConfigs.Any()) + { + _logger.LogInformation("没有配置要订阅的MQTT主题"); + return null; // 或返回空配置,根据业务需求决定 + } + + var subscribeOptionsBuilder = new MqttClientSubscribeOptionsBuilder(); + + foreach (var config in topicConfigs) + { + var topicFilter = config.ToMqttTopicFilter(); + subscribeOptionsBuilder.WithTopicFilter(topicFilter); // 直接添加单个主题过滤器 + } + + return subscribeOptionsBuilder.Build(); + } + + + + // 主题订阅配置类 + private class TopicSubscriptionConfig + { + public string Topic { get; set; } + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } = MqttQualityOfServiceLevel.AtLeastOnce; + public bool NoLocal { get; set; } = false; + public bool RetainAsPublished { get; set; } = false; + public MqttRetainHandling RetainHandling { get; set; } = MqttRetainHandling.SendAtSubscribe; + + public MqttTopicFilter ToMqttTopicFilter() + { + return new MqttTopicFilterBuilder() + .WithTopic(Topic) + .WithQualityOfServiceLevel(QualityOfServiceLevel) + .WithNoLocal(NoLocal) + .WithRetainAsPublished(RetainAsPublished) + .WithRetainHandling(RetainHandling) + .Build(); + } + + public override string ToString() + { + return $"{Topic}@QoS{QualityOfServiceLevel}"; + } + } + } +} diff --git a/ZR.Common/ZR.Common.csproj b/ZR.Common/ZR.Common.csproj index b890470b..2d152dd6 100644 --- a/ZR.Common/ZR.Common.csproj +++ b/ZR.Common/ZR.Common.csproj @@ -10,7 +10,8 @@ - + + diff --git a/ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs b/ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs new file mode 100644 index 00000000..6a453019 --- /dev/null +++ b/ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace ZR.Model.Dto +{ + /// + /// 后道外箱标签打印发送消息模板 + /// + public class QcBackEndPrintMqttEventDto + { + public string Path { get; set; } + + public string SiteNo { get; set; } + + public string Name { get; set; } + + public string PartNumber { get; set; } + + public string WorkOrder { get; set; } + + public string Team { get; set; } = "A"; + + public int Sort { get; set; } = 1; + + public string BatchCode { get; set; } + + public int PackageNum { get; set; } = 24; + + public int LabelType { get; set; } = 1; + + public string CreatedTime { get; set; } = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + } +} \ No newline at end of file diff --git a/ZR.Service/ZR.Service.csproj b/ZR.Service/ZR.Service.csproj index 582a50fb..0da804f5 100644 --- a/ZR.Service/ZR.Service.csproj +++ b/ZR.Service/ZR.Service.csproj @@ -21,4 +21,8 @@ + + + + diff --git a/ZR.Service/mes/qc/backend/QcBackEndService.cs b/ZR.Service/mes/qc/backend/QcBackEndService.cs index f5d6a52c..6fc7ca69 100644 --- a/ZR.Service/mes/qc/backend/QcBackEndService.cs +++ b/ZR.Service/mes/qc/backend/QcBackEndService.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Transactions; using Aliyun.OSS; using AutoMapper; @@ -10,7 +11,10 @@ using Infrastructure.Attribute; using Infrastructure.Extensions; using JinianNet.JNTemplate; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Logging; +using MQTTnet.Protocol; using SqlSugar; +using ZR.Common.MqttHelper; using ZR.Model; using ZR.Model.Business; using ZR.Model.Dto; @@ -27,6 +31,15 @@ namespace ZR.Service.Business [AppService(ServiceType = typeof(IQcBackEndService), ServiceLifetime = LifeTime.Transient)] public class QcBackEndService : BaseService, IQcBackEndService { + private readonly MqttService _mqttService; // 注入MqttService + private readonly ILogger _logger; + + public QcBackEndService(MqttService mqttService, ILogger logger) + { + _mqttService = mqttService; + _logger = logger; + } + public QcBackEndLabelAnalysisDto AnalyzeLabelToDto(string label, int type) { QcBackEndLabelAnalysisDto labelAnalysisDto = @@ -158,21 +171,16 @@ namespace ZR.Service.Business public List GetDefectInitOptions() { List defectList = new(); - var predicate = Expressionable.Create().And(it => it.Status == "1"); + var predicate = Expressionable + .Create() + .And(it => it.Status == "1"); /*List groupList = Context .Queryable() .Where(predicate.ToExpression()) .GroupBy(it => it.Group) .Select(it => it.Group) .ToList();*/ - List groupList = new() - { - "油漆", - "设备", - "毛坯", - "程序", - "班组操作" - }; + List groupList = new() { "油漆", "设备", "毛坯", "程序", "班组操作" }; foreach (string group in groupList) { QcBackEndAlterationDefectDto defectDto = new(); @@ -202,20 +210,13 @@ namespace ZR.Service.Business .Create() .And(it => it.Type == "打磨") .And(it => it.Status == "1"); - /* List groupList = Context - .Queryable() - .Where(predicate.ToExpression()) - .GroupBy(it => it.Group) - .Select(it => it.Group) - .ToList();*/ - List groupList = new() - { - "油漆", - "设备", - "毛坯", - "程序", - "班组操作" - }; + /* List groupList = Context + .Queryable() + .Where(predicate.ToExpression()) + .GroupBy(it => it.Group) + .Select(it => it.Group) + .ToList();*/ + List groupList = new() { "油漆", "设备", "毛坯", "程序", "班组操作" }; foreach (string group in groupList) { QcBackEndAlterationDefectDto defectDto = new(); @@ -302,7 +303,9 @@ namespace ZR.Service.Business data.SerialNumber = workorderInfo.SerialNumber; data.StartTime = nowTime; QcBackEndServiceWorkorder newModel = GetNewWorkOrderInfo(data); - QcBackEndServiceWorkorder result = Context.Insertable(newModel).ExecuteReturnEntity(); + QcBackEndServiceWorkorder result = Context + .Insertable(newModel) + .ExecuteReturnEntity(); if (result == null) { Context.Ado.RollbackTran(); @@ -409,7 +412,9 @@ namespace ZR.Service.Business return result; } - public static QcBackEndServiceWorkorder GetNewWorkOrderInfo(QcBackEndWorkorderDetailDto data) + public static QcBackEndServiceWorkorder GetNewWorkOrderInfo( + QcBackEndWorkorderDetailDto data + ) { // 新工单 @@ -453,7 +458,7 @@ namespace ZR.Service.Business { try { - if(string.IsNullOrEmpty(data.DefectCode)) + if (string.IsNullOrEmpty(data.DefectCode)) { throw new Exception("缺陷项传入为空!"); } @@ -643,10 +648,11 @@ namespace ZR.Service.Business bool hasAny = Context .Queryable() .Where(it => it.Label == data.Label) + .Where(it => it.LabelType == 2) .Any(); if (hasAny) { - return "重复扫码!"; + return "内标签重复扫码!"; } // 标签录入 int sort = 0; @@ -685,41 +691,124 @@ namespace ZR.Service.Business return "标签录入系统失败!"; } //TODO 触发箱标签判定 - if(sort > 28 && (sort + 1) % 28 == 0) + CheckAndPrintPackageLabel(newLabelScran); + + return "ok"; + } + + /// + /// 判断是否需要自动出满箱标签 + /// + /// + public void CheckAndPrintPackageLabel(QcBackEndRecordLabelScan newLabelScran) + { + DateTime nowTime = DateTime.Now; + // 找到最大箱容量与模板 + QcBackEndServiceWorkorder workorder = Context + .Queryable() + .Where(it => it.WorkOrder == newLabelScran.WorkOrder) + .First(); + QcBackendBaseOutpackage packageLabelConfig = Context + .Queryable() + .Where(it => workorder.Description.Contains(it.CheckStr)) + .First(); + if (workorder == null) { - /* int packageSort = 0; + throw new Exception("工单异常"); + } + if (packageLabelConfig == null) + { + throw new Exception("该标签打印参数未配置"); + } + int checkSort = newLabelScran.LabelSort ?? 0; + + int maxPackage = packageLabelConfig.PackageNum ?? 0; + if (checkSort >= maxPackage && checkSort % maxPackage == 0) + { + int packageSort = 0; QcBackEndRecordLabelScan packagelabelScan = Context .Queryable() - .Where(it => it.WorkOrder == data.WorkOrder) + .Where(it => it.WorkOrder == newLabelScran.WorkOrder) .Where(it => it.LabelType == 1) .OrderByDescending(it => it.LabelSort) .First(); - if (labelScan != null) + if (packagelabelScan != null) { packageSort = packagelabelScan.LabelSort ?? 0; } QcBackEndRecordLabelScan newPackagePrintLabel = - new() + new() + { + Id = SnowFlakeSingle.Instance.NextId().ToString(), + WorkOrder = newLabelScran.WorkOrder, + PartNumber = newLabelScran.PartNumber, + Team = newLabelScran.Team, + SiteNo = newLabelScran.SiteNo, + ComNo = newLabelScran.ComNo, + Label = + $"Code=BN{newLabelScran.WorkOrder}_{newLabelScran.Team}{packageSort + 1}^ItemNumber={newLabelScran.PartNumber}^Order={newLabelScran.WorkOrder}^Qty={maxPackage}^Type=packageLabel", + LabelType = 1, + LabelSort = packageSort + 1, + ScanTime = $"{nowTime:yyyy-MM-dd HH:mm:ss}", + Type = "1", + Status = "1", + Remark = "自动出满箱标签", + CreatedBy = newLabelScran.CreatedBy, + CreatedTime = newLabelScran.CreatedTime, + }; + int res = Context.Insertable(newPackagePrintLabel).ExecuteCommand(); + if (res > 0) { - Id = SnowFlakeSingle.Instance.NextId().ToString(), - WorkOrder = data.WorkOrder, - PartNumber = data.PartNumber, - Team = data.Team, - SiteNo = data.SiteNo, - ComNo = data.ComNo, - Label = data.Label, - LabelType = 1, - LabelSort = packageSort + 1, - ScanTime = $"{nowTime:yyyy-MM-dd HH:mm:ss}", - Type = "1", - Status = "1", - Remark = "自动出满箱标签", - CreatedBy = data.CreatedBy, - CreatedTime = data.CreatedTime, - }; - int res2 = Context.Insertable(newPackagePrintLabel).ExecuteCommand();*/ + SendPrintPackageLabelAsync(newLabelScran, packageLabelConfig.FileUrl).Wait(); + } + } + } + + /// + /// 发送打印后道外箱标签的mqtt信息 + /// + public async Task SendPrintPackageLabelAsync( + QcBackEndRecordLabelScan newLabelScran, + string path + ) + { + try + { + // 构造主题和消息内容 + string topic = $"shgg_mes/backEnd/print/{newLabelScran.SiteNo}"; + + QcBackEndPrintMqttEventDto mqttEventDto = + new() + { + Path = path, + SiteNo = newLabelScran.SiteNo, + Name = newLabelScran.PartNumber, + WorkOrder = newLabelScran.WorkOrder, + Team = newLabelScran.Team, + Sort = (newLabelScran.LabelSort + 1) ?? 1, + BatchCode = DateTime.Now.ToString("yyyyMMdd"), + PackageNum = 24, + LabelType = newLabelScran.LabelType ?? 1, + CreatedTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + }; + var payload = JsonSerializer.Serialize(mqttEventDto); + + // 调用MqttService的发布方法(支持异步调用) + await _mqttService.PublishAsync( + topic, + payload, + MqttQualityOfServiceLevel.ExactlyOnce, + // 可选:设置消息保留 + retain: false + ); + + _logger.LogInformation($"发送后道外箱标签打印成功:{topic}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"发送后道外箱标签打印失败:{ex.Message}"); + throw; // 或根据业务需求处理异常 } - return "ok"; } public string EndBackEndWorkOrderAndCreateStatistics(string workorder) @@ -847,7 +936,9 @@ namespace ZR.Service.Business return $"{qualifiedRate:F1}%"; } - public QcBackEndServiceWorkorder GenerateVirtualLabel(QcBackEndWorkorderDetailDto workorderDetail) + public QcBackEndServiceWorkorder GenerateVirtualLabel( + QcBackEndWorkorderDetailDto workorderDetail + ) { try { @@ -857,7 +948,10 @@ namespace ZR.Service.Business int qualifiedNumber = workorderDetail.QualifiedNumber ?? -1; if (qualifiedNumber < 0) { - throw new ArgumentException("传入合格数异常!", nameof(workorderDetail.QualifiedNumber)); + throw new ArgumentException( + "传入合格数异常!", + nameof(workorderDetail.QualifiedNumber) + ); } int labelCount = GetLabelCountForWorkOrder(workorderDetail.WorkOrder); @@ -883,37 +977,46 @@ namespace ZR.Service.Business private int GetLabelCountForWorkOrder(string workOrder) { - return Context.Queryable() - .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) - .Count(); + return Context + .Queryable() + .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) + .Count(); } - private void GenerateVirtualLabels(QcBackEndWorkorderDetailDto workOrderDetail, int countToGenerate) + private void GenerateVirtualLabels( + QcBackEndWorkorderDetailDto workOrderDetail, + int countToGenerate + ) { List virtualLabels = new List(); int nextLabelNumber = GetNextLabelNumber(workOrderDetail.WorkOrder); for (int i = 0; i < countToGenerate; i++) { - string uniqueLabel = GenerateUniqueSequentialLabel(workOrderDetail.WorkOrder, nextLabelNumber++); - virtualLabels.Add(new QcBackEndRecordLabelScan - { - Id = SnowFlakeSingle.Instance.NextId().ToString(), - WorkOrder = workOrderDetail.WorkOrder, - PartNumber = workOrderDetail.PartNumber, - Team = workOrderDetail.Team, - SiteNo = workOrderDetail.SiteNo, - ComNo = workOrderDetail.ComNo, - ScanTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"), - Type = "2", - Status = "1", - Remark = "虚拟标签", - CreatedTime = DateTime.UtcNow, - CreatedBy = "系统", - LabelType = 2, - LabelSort = nextLabelNumber, - Label = uniqueLabel - }); + string uniqueLabel = GenerateUniqueSequentialLabel( + workOrderDetail.WorkOrder, + nextLabelNumber++ + ); + virtualLabels.Add( + new QcBackEndRecordLabelScan + { + Id = SnowFlakeSingle.Instance.NextId().ToString(), + WorkOrder = workOrderDetail.WorkOrder, + PartNumber = workOrderDetail.PartNumber, + Team = workOrderDetail.Team, + SiteNo = workOrderDetail.SiteNo, + ComNo = workOrderDetail.ComNo, + ScanTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"), + Type = "2", + Status = "1", + Remark = "虚拟标签", + CreatedTime = DateTime.UtcNow, + CreatedBy = "系统", + LabelType = 2, + LabelSort = nextLabelNumber, + Label = uniqueLabel + } + ); } Context.Insertable(virtualLabels).ExecuteCommand(); @@ -921,20 +1024,22 @@ namespace ZR.Service.Business private void DeleteExcessLabels(string workOrder, int countToDelete) { - var labelsToDelete = Context.Queryable() - .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) - .OrderByDescending(it => it.LabelSort) - .Take(countToDelete) - .ToList(); + var labelsToDelete = Context + .Queryable() + .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) + .OrderByDescending(it => it.LabelSort) + .Take(countToDelete) + .ToList(); Context.Deleteable(labelsToDelete).ExecuteCommand(); } private int GetNextLabelNumber(string workOrder) { - return Context.Queryable() - .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) - .Max(it => it.LabelSort ?? 0); + return Context + .Queryable() + .Where(it => it.WorkOrder == workOrder && it.LabelType == 2) + .Max(it => it.LabelSort ?? 0); } private string GenerateUniqueSequentialLabel(string workOrder, int number) @@ -952,14 +1057,14 @@ namespace ZR.Service.Business private bool IsLabelExists(string workOrder, string label) { - return Context.Queryable() - .Any(it => it.WorkOrder == workOrder && it.LabelType == 2 && it.Label == label); + return Context + .Queryable() + .Any(it => it.WorkOrder == workOrder && it.LabelType == 2 && it.Label == label); } private string GenerateUniqueId() { return Guid.NewGuid().ToString("N").Substring(0, 10); // Generate a 10-character unique ID } - } }