MQTT全局服务订阅,基本功能创建,标签打印等功能基本实现
This commit is contained in:
parent
4d1ec06430
commit
f897d641b4
52
ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs
Normal file
52
ZR.Admin.WebApi/Controllers/mqtt/MqttController.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// agv 相关接口
|
||||
/// </summary>
|
||||
|
||||
[Route("/mqtt")]
|
||||
public class MqttController : BaseController
|
||||
{
|
||||
private readonly MqttService _mqttService;
|
||||
|
||||
public MqttController(MqttService mqttService)
|
||||
{
|
||||
_mqttService = mqttService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1. 发布信息
|
||||
/// </summary>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="payload">信息</param>
|
||||
/// <returns></returns>
|
||||
|
||||
[HttpPost("publish")]
|
||||
public async Task<IActionResult> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<key id="6f790ae9-1260-4b42-ac56-6835d3555186" version="1">
|
||||
<creationDate>2025-05-13T00:45:18.9703791Z</creationDate>
|
||||
<activationDate>2025-05-13T00:45:18.9163673Z</activationDate>
|
||||
<expirationDate>2025-08-11T00:45:18.9163673Z</expirationDate>
|
||||
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<descriptor>
|
||||
<encryption algorithm="AES_256_CBC" />
|
||||
<validation algorithm="HMACSHA256" />
|
||||
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
|
||||
<!-- Warning: the key below is in an unencrypted form. -->
|
||||
<value>dlysrp3IsJDOznQcS5WVSPFQ68bSJBN8cF8lnJ1+a0QzUsCf9KgzUDoQsgZ/9q/pyoKtTZJCnEoXO+m5nUaj3Q==</value>
|
||||
</masterKey>
|
||||
</descriptor>
|
||||
</descriptor>
|
||||
</key>
|
||||
@ -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<IHttpContextAccessor, HttpContextAccessor>();
|
||||
// 注册MyMqttConfig(依赖ILogger和IConfiguration)
|
||||
builder.Services.AddSingleton<MyMqttConfig>();
|
||||
// 注册MqttService为单例服务,并作为后台服务运行 !!!! 这样注册就行了 ================
|
||||
builder.Services.AddSingleton<MqttService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<MqttService>());
|
||||
/// ===============================================================================
|
||||
// 跨域配置
|
||||
builder.Services.AddCors(builder.Configuration);
|
||||
// 显示logo
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
424
ZR.Common/MqttHelper/MqttService.cs
Normal file
424
ZR.Common/MqttHelper/MqttService.cs
Normal file
@ -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<MqttService> _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<MqttService> 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<int>("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服务已释放所有资源");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
ZR.Common/MqttHelper/MyMqttConfig.cs
Normal file
97
ZR.Common/MqttHelper/MyMqttConfig.cs
Normal file
@ -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<MyMqttConfig> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public MyMqttConfig(ILogger<MyMqttConfig> 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<int>("Port", 1883))
|
||||
.WithCleanSession();
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public MqttClientSubscribeOptions GetSubscribeToTopicsAsync()
|
||||
{
|
||||
var topicsSection = _configuration.GetSection($"{MqttConfigSection}:{TopicsConfigKey}");
|
||||
var topicConfigs = topicsSection.Get<List<TopicSubscriptionConfig>>() ?? new List<TopicSubscriptionConfig>();
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,8 @@
|
||||
<PackageReference Include="JinianNet.JNTemplate" Version="2.3.3" />
|
||||
<PackageReference Include="MailKit" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageReference Include="MiniExcel" Version="1.31.0" />
|
||||
<PackageReference Include="MiniExcel" Version="1.41.1" />
|
||||
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
|
||||
<PackageReference Include="NLog" Version="5.2.0" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
33
ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs
Normal file
33
ZR.Model/MES/qc/DTO/backend/QcBackEndPrintMqttEventDto.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ZR.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 后道外箱标签打印发送消息模板
|
||||
/// </summary>
|
||||
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");
|
||||
|
||||
}
|
||||
}
|
||||
@ -21,4 +21,8 @@
|
||||
<ProjectReference Include="..\ZR.Repository\ZR.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="mqtt\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -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<QcBackEndServiceWorkorder>, IQcBackEndService
|
||||
{
|
||||
private readonly MqttService _mqttService; // 注入MqttService
|
||||
private readonly ILogger<QcBackEndService> _logger;
|
||||
|
||||
public QcBackEndService(MqttService mqttService, ILogger<QcBackEndService> logger)
|
||||
{
|
||||
_mqttService = mqttService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public QcBackEndLabelAnalysisDto AnalyzeLabelToDto(string label, int type)
|
||||
{
|
||||
QcBackEndLabelAnalysisDto labelAnalysisDto =
|
||||
@ -158,21 +171,16 @@ namespace ZR.Service.Business
|
||||
public List<QcBackEndAlterationDefectDto> GetDefectInitOptions()
|
||||
{
|
||||
List<QcBackEndAlterationDefectDto> defectList = new();
|
||||
var predicate = Expressionable.Create<QcBackEndBaseDefect>().And(it => it.Status == "1");
|
||||
var predicate = Expressionable
|
||||
.Create<QcBackEndBaseDefect>()
|
||||
.And(it => it.Status == "1");
|
||||
/*List<string> groupList = Context
|
||||
.Queryable<QcBackEndBaseDefect>()
|
||||
.Where(predicate.ToExpression())
|
||||
.GroupBy(it => it.Group)
|
||||
.Select(it => it.Group)
|
||||
.ToList();*/
|
||||
List<string> groupList = new()
|
||||
{
|
||||
"油漆",
|
||||
"设备",
|
||||
"毛坯",
|
||||
"程序",
|
||||
"班组操作"
|
||||
};
|
||||
List<string> groupList = new() { "油漆", "设备", "毛坯", "程序", "班组操作" };
|
||||
foreach (string group in groupList)
|
||||
{
|
||||
QcBackEndAlterationDefectDto defectDto = new();
|
||||
@ -202,20 +210,13 @@ namespace ZR.Service.Business
|
||||
.Create<QcBackEndBaseDefect>()
|
||||
.And(it => it.Type == "打磨")
|
||||
.And(it => it.Status == "1");
|
||||
/* List<string> groupList = Context
|
||||
.Queryable<QcBackEndBaseDefect>()
|
||||
.Where(predicate.ToExpression())
|
||||
.GroupBy(it => it.Group)
|
||||
.Select(it => it.Group)
|
||||
.ToList();*/
|
||||
List<string> groupList = new()
|
||||
{
|
||||
"油漆",
|
||||
"设备",
|
||||
"毛坯",
|
||||
"程序",
|
||||
"班组操作"
|
||||
};
|
||||
/* List<string> groupList = Context
|
||||
.Queryable<QcBackEndBaseDefect>()
|
||||
.Where(predicate.ToExpression())
|
||||
.GroupBy(it => it.Group)
|
||||
.Select(it => it.Group)
|
||||
.ToList();*/
|
||||
List<string> 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<QcBackEndRecordLabelScan>()
|
||||
.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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否需要自动出满箱标签
|
||||
/// </summary>
|
||||
/// <param name="workorder"></param>
|
||||
public void CheckAndPrintPackageLabel(QcBackEndRecordLabelScan newLabelScran)
|
||||
{
|
||||
DateTime nowTime = DateTime.Now;
|
||||
// 找到最大箱容量与模板
|
||||
QcBackEndServiceWorkorder workorder = Context
|
||||
.Queryable<QcBackEndServiceWorkorder>()
|
||||
.Where(it => it.WorkOrder == newLabelScran.WorkOrder)
|
||||
.First();
|
||||
QcBackendBaseOutpackage packageLabelConfig = Context
|
||||
.Queryable<QcBackendBaseOutpackage>()
|
||||
.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<QcBackEndRecordLabelScan>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送打印后道外箱标签的mqtt信息
|
||||
/// </summary>
|
||||
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<QcBackEndRecordLabelScan>()
|
||||
.Where(it => it.WorkOrder == workOrder && it.LabelType == 2)
|
||||
.Count();
|
||||
return Context
|
||||
.Queryable<QcBackEndRecordLabelScan>()
|
||||
.Where(it => it.WorkOrder == workOrder && it.LabelType == 2)
|
||||
.Count();
|
||||
}
|
||||
|
||||
private void GenerateVirtualLabels(QcBackEndWorkorderDetailDto workOrderDetail, int countToGenerate)
|
||||
private void GenerateVirtualLabels(
|
||||
QcBackEndWorkorderDetailDto workOrderDetail,
|
||||
int countToGenerate
|
||||
)
|
||||
{
|
||||
List<QcBackEndRecordLabelScan> virtualLabels = new List<QcBackEndRecordLabelScan>();
|
||||
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<QcBackEndRecordLabelScan>()
|
||||
.Where(it => it.WorkOrder == workOrder && it.LabelType == 2)
|
||||
.OrderByDescending(it => it.LabelSort)
|
||||
.Take(countToDelete)
|
||||
.ToList();
|
||||
var labelsToDelete = Context
|
||||
.Queryable<QcBackEndRecordLabelScan>()
|
||||
.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<QcBackEndRecordLabelScan>()
|
||||
.Where(it => it.WorkOrder == workOrder && it.LabelType == 2)
|
||||
.Max(it => it.LabelSort ?? 0);
|
||||
return Context
|
||||
.Queryable<QcBackEndRecordLabelScan>()
|
||||
.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<QcBackEndRecordLabelScan>()
|
||||
.Any(it => it.WorkOrder == workOrder && it.LabelType == 2 && it.Label == label);
|
||||
return Context
|
||||
.Queryable<QcBackEndRecordLabelScan>()
|
||||
.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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user