@@ -0,0 +1,17 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Model | |||||
{ | |||||
public class Attendance | |||||
{ | |||||
public int AttendanceStatus { get; set; } | |||||
public string AttendanceTime { get; set; } =default!; | |||||
public string Imei { get; set; } = default!; | |||||
} | |||||
} |
@@ -9,5 +9,7 @@ namespace NearCardAttendance.Model | |||||
public class ServiceConfig | public class ServiceConfig | ||||
{ | { | ||||
public string XinHuaLeYuUrl { get; set; } = default!; | public string XinHuaLeYuUrl { get; set; } = default!; | ||||
public string KafkaServerAddress { get; set; } = default!; | |||||
} | } | ||||
} | } |
@@ -0,0 +1,15 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.MessageQueue.Kafka | |||||
{ | |||||
public interface IKafkaService | |||||
{ | |||||
Task PublishAsync<T>(string topicName, T message) where T : class; | |||||
Task SubscribeAsync<T>(IEnumerable<string> topics, Action<T> messageFunc, CancellationToken cancellationToken = default) where T : class; | |||||
} | |||||
} |
@@ -0,0 +1,141 @@ | |||||
using Confluent.Kafka; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using Newtonsoft.Json; | |||||
using NearCardAttendance.Common; | |||||
using NearCardAttendance.Model; | |||||
using NearCardAttendance.Service.MessageQueue.Model; | |||||
namespace NearCardAttendance.Service.MessageQueue.Kafka | |||||
{ | |||||
public class KafkaService : IKafkaService | |||||
{ | |||||
private readonly ILogger<KafkaService> _logger; | |||||
private readonly ServiceConfig _configService; | |||||
public KafkaService(IOptions<ServiceConfig> _optConfigService, ILogger<KafkaService> logger) | |||||
{ | |||||
_configService = _optConfigService.Value; | |||||
_logger = logger; | |||||
} | |||||
public async Task PublishAsync<T>(string topicName, T message) where T : class | |||||
{ | |||||
try | |||||
{ | |||||
Type messageType = typeof(T); | |||||
var config = new ProducerConfig | |||||
{ | |||||
BootstrapServers = _configService.KafkaServerAddress, | |||||
EnableIdempotence = true, | |||||
Acks = Acks.All, | |||||
MessageSendMaxRetries = 3 | |||||
}; | |||||
if (message.GetType().Equals(typeof(EventData))) | |||||
{ | |||||
using var producer = new ProducerBuilder<string, string>(config).Build(); | |||||
string imei = messageType.GetProperty("IMEI")!.GetValue(message)!.ToString()!; | |||||
//var tailNo = long.Parse(messageType.GetProperty("IMEI")!.GetValue(message)!.ToString()!) % 100; | |||||
//int tailNo = SafeType.SafeInt(imei.Substring(imei.Length - 2)); | |||||
var messageId = messageType.GetProperty("MessageId")!.GetValue(message)!.ToString()!; | |||||
//await producer.ProduceAsync(new TopicPartition(topicName, new Partition(tailNo)), new Message<string, string> | |||||
//{ | |||||
// Key = messageId, | |||||
// Value = JsonConvert.SerializeObject(message), | |||||
//}); | |||||
await producer.ProduceAsync(topicName, new Message<string, string> | |||||
{ | |||||
Key = messageId, | |||||
Value = JsonConvert.SerializeObject(message), | |||||
}); | |||||
// TopicPartition topicPartition = new TopicPartition(topicName, new Partition(tailNo)); | |||||
} | |||||
else | |||||
{ | |||||
using var producer = new ProducerBuilder<string, string>(config).Build(); | |||||
await producer.ProduceAsync(topicName, new Message<string, string> | |||||
{ | |||||
Key = Guid.NewGuid().ToString(), | |||||
Value = JsonConvert.SerializeObject(message) | |||||
}); | |||||
} | |||||
} | |||||
catch (ProduceException<Null, string> ex) | |||||
{ | |||||
_logger.LogError($"推送到kafka失败,topic: {topicName},\n message:{JsonConvert.SerializeObject(message)}: \n{ex.Error.Reason}"); | |||||
} | |||||
} | |||||
public async Task SubscribeAsync<T>(IEnumerable<string> topics, Action<T> messageFunc, CancellationToken cancellationToken = default) where T : class | |||||
{ | |||||
var config = new ConsumerConfig | |||||
{ | |||||
BootstrapServers = _configService.KafkaServerAddress, | |||||
GroupId = "Consumer", | |||||
EnableAutoCommit = false, // 禁止AutoCommit | |||||
Acks = Acks.Leader, // 假设只需要Leader响应即可 | |||||
AutoOffsetReset = AutoOffsetReset.Earliest // 从最早的开始消费起 | |||||
}; | |||||
using (var consumer = new ConsumerBuilder<Ignore, string>(config).Build()) | |||||
{ | |||||
consumer.Subscribe(topics); | |||||
try | |||||
{ | |||||
while (true) | |||||
{ | |||||
try | |||||
{ | |||||
var consumeResult = consumer.Consume(cancellationToken); | |||||
Console.WriteLine($"Consumed message '{consumeResult.Message?.Value}' at: '{consumeResult?.TopicPartitionOffset}'."); | |||||
if (consumeResult!.IsPartitionEOF) | |||||
{ | |||||
Console.WriteLine($" - {DateTime.Now:yyyy-MM-dd HH:mm:ss} 已经到底了:{consumeResult.Topic}, partition {consumeResult.Partition}, offset {consumeResult.Offset}."); | |||||
continue; | |||||
} | |||||
T? messageResult = null; | |||||
try | |||||
{ | |||||
messageResult = JsonConvert.DeserializeObject<T>(consumeResult.Message!.Value)!; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
var errorMessage = $" - {DateTime.Now:yyyy-MM-dd HH:mm:ss}【Exception 消息反序列化失败,Value:{consumeResult.Message!.Value}】 :{ex.StackTrace?.ToString()}"; | |||||
Console.WriteLine(errorMessage); | |||||
messageResult = null; | |||||
} | |||||
if (messageResult != null/* && consumeResult.Offset % commitPeriod == 0*/) | |||||
{ | |||||
messageFunc(messageResult); | |||||
try | |||||
{ | |||||
consumer.Commit(consumeResult); | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
Console.WriteLine(e.Message); | |||||
} | |||||
} | |||||
} | |||||
catch (ConsumeException e) | |||||
{ | |||||
Console.WriteLine($"Consume error: {e.Error.Reason}"); | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) | |||||
{ | |||||
Console.WriteLine("Closing consumer."); | |||||
consumer.Close(); | |||||
} | |||||
} | |||||
await Task.CompletedTask; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,14 @@ | |||||
namespace NearCardAttendance.Service.MessageQueue.Model | |||||
{ | |||||
public class EventData | |||||
{ | |||||
public string EventTime { get; set; } = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); | |||||
public string TopicName { get; set; } = default!; | |||||
public string MessageId { get; set; } = default!; | |||||
public string IMEI{ get; set; } = default!; | |||||
public string Content { get; set; } = default!; | |||||
//public T Message { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json.Linq; | |||||
namespace NearCardAttendance.Service.MessageQueue.Model | |||||
{ | |||||
public class IMEIMessage | |||||
{ | |||||
[JsonProperty("imei")] | |||||
public string IMEI { get; set; } = default!; | |||||
[JsonProperty("content")] | |||||
public JObject Content { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,24 @@ | |||||
using Microsoft.Extensions.Logging; | |||||
using Newtonsoft.Json; | |||||
using NearCardAttendance.Service.MessageQueue.Kafka; | |||||
using NearCardAttendance.Service.MessageQueue.Model; | |||||
namespace NearCardAttendance.Service.MessageQueue | |||||
{ | |||||
public class MqProcessLogic | |||||
{ | |||||
private readonly ILogger<MqProcessLogic> _logger; | |||||
private readonly KafkaService _serviceKafka; | |||||
public MqProcessLogic(ILogger<MqProcessLogic> logger, KafkaService serviceKafka) | |||||
{ | |||||
_logger = logger; | |||||
_serviceKafka = serviceKafka; | |||||
} | |||||
public async Task ProcessIMEIEventMessageAsync(EventData eventData) | |||||
{ | |||||
await _serviceKafka.PublishAsync(eventData.TopicName, eventData); | |||||
_logger.LogInformation($"推送消息 {eventData.MessageId} 内容:{JsonConvert.SerializeObject(eventData)}"); | |||||
} | |||||
} | |||||
} |
@@ -9,6 +9,7 @@ | |||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | <PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | ||||
<PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | <PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | ||||
<PackageReference Include="Confluent.Kafka" Version="2.2.0" /> | |||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
@@ -18,6 +18,9 @@ using TelpoDataService.Util; | |||||
using TelpoDataService.Util.Clients; | using TelpoDataService.Util.Clients; | ||||
using TelpoDataService.Util.Models; | using TelpoDataService.Util.Models; | ||||
using TelpoDataService.Util.QueryObjects; | using TelpoDataService.Util.QueryObjects; | ||||
using NearCardAttendance.Service.MessageQueue; | |||||
using NearCardAttendance.Service.MessageQueue.Model; | |||||
using Newtonsoft.Json.Linq; | |||||
namespace NearCardAttendance.Service.TcpServer.Handler | namespace NearCardAttendance.Service.TcpServer.Handler | ||||
{ | { | ||||
@@ -25,6 +28,7 @@ namespace NearCardAttendance.Service.TcpServer.Handler | |||||
{ | { | ||||
private readonly ILogger<RegisterHandler> _logger; | private readonly ILogger<RegisterHandler> _logger; | ||||
private readonly HttpHelper _httpHelper = default!; | private readonly HttpHelper _httpHelper = default!; | ||||
private readonly MqProcessLogic _serviceMqProcess; | |||||
//private readonly IDisposable _loggerScope = default!; | //private readonly IDisposable _loggerScope = default!; | ||||
private readonly ServiceConfig _configService; | private readonly ServiceConfig _configService; | ||||
private readonly GpsCardAccessorClient<GpsDeviceConfig> _deviceConfigApiClient; | private readonly GpsCardAccessorClient<GpsDeviceConfig> _deviceConfigApiClient; | ||||
@@ -32,12 +36,13 @@ namespace NearCardAttendance.Service.TcpServer.Handler | |||||
//private readonly TcpClientsManager _managerTcpClients; | //private readonly TcpClientsManager _managerTcpClients; | ||||
//private readonly ScheduleResendManager _managerScheduleResend; | //private readonly ScheduleResendManager _managerScheduleResend; | ||||
public RegisterHandler(ILogger<RegisterHandler> logger, GpsCardAccessorClient<GpsDeviceConfig> deviceConfigApiClient,HttpHelper httpHelper, IOptions<ServiceConfig> configService) | |||||
public RegisterHandler(ILogger<RegisterHandler> logger, GpsCardAccessorClient<GpsDeviceConfig> deviceConfigApiClient,HttpHelper httpHelper, IOptions<ServiceConfig> configService, MqProcessLogic serviceMqProcess) | |||||
{ | { | ||||
_logger = logger; | _logger = logger; | ||||
_httpHelper = httpHelper; | _httpHelper = httpHelper; | ||||
_configService = configService.Value; | _configService = configService.Value; | ||||
_deviceConfigApiClient= deviceConfigApiClient; | _deviceConfigApiClient= deviceConfigApiClient; | ||||
_serviceMqProcess = serviceMqProcess; | |||||
} | } | ||||
public override void ChannelActive(IChannelHandlerContext context) | public override void ChannelActive(IChannelHandlerContext context) | ||||
@@ -267,15 +272,43 @@ namespace NearCardAttendance.Service.TcpServer.Handler | |||||
if (config!=null) | if (config!=null) | ||||
{ | { | ||||
var url = _configService.XinHuaLeYuUrl; | |||||
//var url = _configService.XinHuaLeYuUrl; | |||||
var url = $"{_configService.XinHuaLeYuUrl}/user/electronicCardAttendance/receiveTbAttendanceRecord"; | |||||
var data = new | var data = new | ||||
{ | { | ||||
attendanceStatus = 2, //考勤状态: 0.进 1.出 2.未知 | attendanceStatus = 2, //考勤状态: 0.进 1.出 2.未知 | ||||
attendanceTime = DateTime.ParseExact(startTime, "yyyyMMddHHmmss", null).ToString("yyyy-MM-dd HH:mm:ss"), | attendanceTime = DateTime.ParseExact(startTime, "yyyyMMddHHmmss", null).ToString("yyyy-MM-dd HH:mm:ss"), | ||||
imei = config.Imei | imei = config.Imei | ||||
}; | }; | ||||
var eventData = new EventData | |||||
{ | |||||
TopicName = "topics.storage.near_card_attendance", | |||||
MessageId = $"{config.Imei}-{parser.SeqNo}-{Guid.NewGuid().ToString("N")[^4..]}", | |||||
IMEI = config.Imei, | |||||
Content = JsonConvert.SerializeObject(data) | |||||
}; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | var res = await _httpHelper.HttpToPostAsync(url, data); | ||||
_logger.LogInformation($"{nameof(HandleSignRecsAsync)} 推送 {JsonConvert.SerializeObject(data)} 结果,{res}"); | |||||
if (!string.IsNullOrEmpty(res)) | |||||
{ | |||||
JObject resObj = (JObject)JsonConvert.DeserializeObject(res!)!; | |||||
if ((bool)resObj["success"]!) | |||||
{ | |||||
_logger.LogInformation($"{nameof(HandleSignRecsAsync)} 推送 {JsonConvert.SerializeObject(data)} 结果,{res}"); | |||||
} | |||||
else | |||||
{ | |||||
await _serviceMqProcess.ProcessIMEIEventMessageAsync(eventData); | |||||
_logger.LogInformation($"HTTP 响应业务失败 {res},{nameof(HandleSignRecsAsync)} 推送 {JsonConvert.SerializeObject(eventData)}"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
await _serviceMqProcess.ProcessIMEIEventMessageAsync(eventData); | |||||
_logger.LogInformation($"HTTP 响应超时,{nameof(HandleSignRecsAsync)} 推送 {JsonConvert.SerializeObject(eventData)}"); | |||||
} | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -328,15 +361,53 @@ namespace NearCardAttendance.Service.TcpServer.Handler | |||||
if (config != null) | if (config != null) | ||||
{ | { | ||||
//var url = "https://midplat.xinhualeyu.com/dev-api/user/electronicCardAttendance/receiveTbAttendanceRecord"; | //var url = "https://midplat.xinhualeyu.com/dev-api/user/electronicCardAttendance/receiveTbAttendanceRecord"; | ||||
var url = _configService.XinHuaLeYuUrl; | |||||
#if DEBUG | |||||
var url = $"{_configService.XinHuaLeYuUrl}/user/electronicCardAttendance/receiveTbAttendanceRecord1"; | |||||
#else | |||||
var url = $"{_configService.XinHuaLeYuUrl}/user/electronicCardAttendance/receiveTbAttendanceRecord"; | |||||
#endif | |||||
var data = new | var data = new | ||||
{ | { | ||||
attendanceStatus = int.TryParse(optType, out int type) ? type : 0, | attendanceStatus = int.TryParse(optType, out int type) ? type : 0, | ||||
attendanceTime = DateTime.ParseExact(startTime, "yyyyMMddHHmmss", null).ToString("yyyy-MM-dd HH:mm:ss"), | attendanceTime = DateTime.ParseExact(startTime, "yyyyMMddHHmmss", null).ToString("yyyy-MM-dd HH:mm:ss"), | ||||
imei = config.Imei | imei = config.Imei | ||||
}; | }; | ||||
var eventData = new EventData | |||||
{ | |||||
TopicName = "topics.storage.near_card_attendance", | |||||
MessageId = $"{config.Imei}-{parser.SeqNo}-{Guid.NewGuid().ToString("N")[^4..]}", | |||||
IMEI = config.Imei, | |||||
Content = JsonConvert.SerializeObject(data) | |||||
}; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | var res = await _httpHelper.HttpToPostAsync(url, data); | ||||
_logger.LogInformation($"{nameof(HandleStdtSchoolRecsAsync)} 推送 {JsonConvert.SerializeObject(data)} 结果,{res}"); | |||||
if (!string.IsNullOrEmpty(res)) | |||||
{ | |||||
JObject resObj = (JObject)JsonConvert.DeserializeObject(res!)!; | |||||
if ((bool)resObj["success"]!) | |||||
{ | |||||
_logger.LogInformation($"{nameof(HandleStdtSchoolRecsAsync)} 推送 {JsonConvert.SerializeObject(data)} 结果,{res}"); | |||||
} | |||||
else | |||||
{ | |||||
await _serviceMqProcess.ProcessIMEIEventMessageAsync(eventData); | |||||
_logger.LogInformation($"HTTP 响应业务失败 {res},{nameof(HandleStdtSchoolRecsAsync)} 推送 {JsonConvert.SerializeObject(eventData)}"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
await _serviceMqProcess.ProcessIMEIEventMessageAsync(eventData); | |||||
_logger.LogInformation($"HTTP 响应超时,{nameof(HandleStdtSchoolRecsAsync)} 推送 {JsonConvert.SerializeObject(eventData)}"); | |||||
} | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -30,6 +30,7 @@ | |||||
<PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | <PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | ||||
<PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | <PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | ||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" /> | <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" /> | ||||
<PackageReference Include="Confluent.Kafka" Version="2.2.0" /> | |||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> | <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> | ||||
<PackageReference Include="Serilog.Expressions" Version="3.4.0" /> | <PackageReference Include="Serilog.Expressions" Version="3.4.0" /> | ||||
@@ -14,6 +14,8 @@ using Serilog; | |||||
using NearCardAttendance.Model; | using NearCardAttendance.Model; | ||||
using TelpoDataService.Util.Clients; | using TelpoDataService.Util.Clients; | ||||
using System.Text; | using System.Text; | ||||
using NearCardAttendance.Service.MessageQueue.Kafka; | |||||
using NearCardAttendance.Service.MessageQueue; | |||||
namespace NearCardAttendance.TcpServer | namespace NearCardAttendance.TcpServer | ||||
{ | { | ||||
@@ -110,8 +112,9 @@ namespace NearCardAttendance.TcpServer | |||||
.AddHostedService<Server>() | .AddHostedService<Server>() | ||||
; | ; | ||||
#endregion | #endregion | ||||
services.AddSingleton<MqProcessLogic>(); | |||||
services.AddSingleton<KafkaService>(); | |||||
}); | }); | ||||
} | } | ||||
} | } |
@@ -1,13 +1,20 @@ | |||||
using DotNetty.Transport.Bootstrapping; | |||||
using Confluent.Kafka; | |||||
using DotNetty.Transport.Bootstrapping; | |||||
using DotNetty.Transport.Channels; | using DotNetty.Transport.Channels; | ||||
using Microsoft.Extensions.Hosting; | using Microsoft.Extensions.Hosting; | ||||
using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||
using Microsoft.Extensions.Options; | |||||
using NearCardAttendance.Common.helper; | |||||
using NearCardAttendance.Model; | |||||
using Newtonsoft.Json.Linq; | |||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | using System.Net; | ||||
using System.Text; | using System.Text; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using NearCardAttendance.Service.MessageQueue.Model; | |||||
namespace NearCardAttendance.TcpServer | namespace NearCardAttendance.TcpServer | ||||
{ | { | ||||
@@ -19,16 +26,23 @@ namespace NearCardAttendance.TcpServer | |||||
// private IChannel _serverChannel = default!; | // private IChannel _serverChannel = default!; | ||||
private CancellationTokenSource _tokenSource = null!; | private CancellationTokenSource _tokenSource = null!; | ||||
private readonly ServiceConfig _configService; | |||||
private readonly HttpHelper _httpHelper = default!; | |||||
private int _messageCount = 0; | |||||
public Server( | public Server( | ||||
ILogger<Server> logger, | ILogger<Server> logger, | ||||
ServerBootstrap serverBootstrap, | ServerBootstrap serverBootstrap, | ||||
IServiceProvider serviceProvider) | |||||
IServiceProvider serviceProvider, HttpHelper httpHelper, IOptions<ServiceConfig> _optConfigService) | |||||
{ | { | ||||
_logger = logger; | _logger = logger; | ||||
//_serviceProvider = serviceProvider; | //_serviceProvider = serviceProvider; | ||||
_configService = _optConfigService.Value; | |||||
_serverBootstrap = serverBootstrap; | _serverBootstrap = serverBootstrap; | ||||
_httpHelper = httpHelper; | |||||
} | } | ||||
public override Task StartAsync(CancellationToken cancellationToken) | public override Task StartAsync(CancellationToken cancellationToken) | ||||
{ | { | ||||
@@ -60,7 +74,51 @@ namespace NearCardAttendance.TcpServer | |||||
//_logger.LogInformation("DotNetty server started on {0}.", address); | //_logger.LogInformation("DotNetty server started on {0}.", address); | ||||
_logger.LogInformation("DotNetty server started on {0}.", endPoint); | _logger.LogInformation("DotNetty server started on {0}.", endPoint); | ||||
#region kafka | |||||
var kafkaConsumer = CreateKafkaConsumer(); | |||||
kafkaConsumer.Subscribe("topics.storage.near_card_attendance"); | |||||
var tasks = new List<Task>(); | |||||
List<ConsumeResult<Ignore, string>> consumeBatchResult = new List<ConsumeResult<Ignore, string>>(); | |||||
try | |||||
{ | |||||
while (!stoppingToken.IsCancellationRequested) | |||||
{ | |||||
var consumeResult = kafkaConsumer.Consume(stoppingToken); | |||||
if (consumeResult != null) | |||||
{ | |||||
_messageCount++; | |||||
consumeBatchResult.Add(consumeResult); | |||||
//// 30条消息为一批 | |||||
#if DEBUG | |||||
if (!await ProcessBatchMessageAsync(consumeBatchResult, kafkaConsumer)) | |||||
{ // 返回结果错误暂停5分钟 | |||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | |||||
} | |||||
#else | |||||
if (_messageCount % 30 == 0) | |||||
{ | |||||
if (!await ProcessBatchMessageAsync(consumeBatchResult, kafkaConsumer)) | |||||
{ // 返回结果错误暂停5分钟 | |||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | |||||
} | |||||
} | |||||
#endif | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) | |||||
{ | |||||
_logger.LogWarning("Worker exit"); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"An error occurred: {ex.Message}"); | |||||
} | |||||
#endregion | |||||
// Wait until the service is stopped | // Wait until the service is stopped | ||||
stoppingToken.WaitHandle.WaitOne(); | stoppingToken.WaitHandle.WaitOne(); | ||||
@@ -71,6 +129,102 @@ namespace NearCardAttendance.TcpServer | |||||
await _serverChannel.CloseAsync(); | await _serverChannel.CloseAsync(); | ||||
_logger.LogInformation("DotNetty server stopped."); | _logger.LogInformation("DotNetty server stopped."); | ||||
} | } | ||||
private async Task<bool> ProcessBatchMessageAsync(List<ConsumeResult<Ignore, string>> consumeBatchResult, IConsumer<Ignore, string> kafkaConsumer) | |||||
{ | |||||
try | |||||
{ | |||||
var url = $"{_configService.XinHuaLeYuUrl}/user/electronicCardAttendance/receiveTbAttendanceRecordException"; | |||||
var list = new List<object>(); | |||||
//consumeBatchResult.ForEach(x => { | |||||
// JObject msg = (JObject)JsonConvert.DeserializeObject(x.Message.Value)!; | |||||
// list.Add(new | |||||
// { | |||||
// attendanceStatus = int.TryParse(msg["data"]!["attendanceStatus"]!.ToString(), out int status) ? status : 0, | |||||
// attendanceTime = msg["data"]!["attendanceTime"]!.ToString(), | |||||
// imei = msg["data"]!["imei"]!.ToString() | |||||
// }) ; | |||||
//}); | |||||
consumeBatchResult.ForEach(x => { | |||||
EventData msg = JsonConvert.DeserializeObject<EventData>(x.Message.Value)!; | |||||
JObject content = (JObject)JsonConvert.DeserializeObject(msg.Content)!; | |||||
list.Add(new | |||||
{ | |||||
attendanceStatus = int.TryParse(content!["attendanceStatus"]!.ToString(), out int status) ? status : 0, | |||||
attendanceTime = content!["attendanceTime"]!.ToString(), | |||||
imei = content!["imei"]!.ToString() | |||||
}); | |||||
}); | |||||
var data = new | |||||
{ | |||||
data = list | |||||
}; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | |||||
if (!string.IsNullOrEmpty(res)) | |||||
{ | |||||
JObject resObj = (JObject)JsonConvert.DeserializeObject(res!)!; | |||||
if ((bool)resObj["success"]!) | |||||
{ | |||||
_logger.LogInformation($"{nameof(ProcessBatchMessageAsync)} 推送 {JsonConvert.SerializeObject(data)} 结果,{res}"); | |||||
consumeBatchResult.ForEach(x => | |||||
{ | |||||
kafkaConsumer.Commit(x); | |||||
_logger.LogInformation($"完成消费:{JsonConvert.SerializeObject(x.Message.Value)}"); | |||||
}); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"处理消息出错 \n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
return false; | |||||
} | |||||
private IConsumer<Ignore, string> CreateKafkaConsumer() | |||||
{ | |||||
var consumerConfig = new ConsumerConfig | |||||
{ | |||||
GroupId = "near_card_attendance", | |||||
BootstrapServers = _configService.KafkaServerAddress, | |||||
AutoOffsetReset = AutoOffsetReset.Earliest, | |||||
EnableAutoCommit = false, // 关闭自动提交偏移量 | |||||
CancellationDelayMaxMs = 1//set CancellationDelayMaxMs | |||||
}; | |||||
return new ConsumerBuilder<Ignore, string>(consumerConfig) | |||||
.SetErrorHandler((_, e) => | |||||
{ | |||||
//Console.WriteLine($"消费者创建出错,代码:{e.Code} |原因: {e.Reason}"); | |||||
_logger.LogInformation($"消费者创建出错,代码:{e.Code} |原因: {e.Reason}"); | |||||
}) | |||||
.SetPartitionsAssignedHandler((c, partitions) => | |||||
{ | |||||
//// 在这里手动指定要消费的分区 | |||||
//var partitionsToConsume = new List<TopicPartitionOffset> | |||||
//{ | |||||
// new TopicPartitionOffset("topics.storage.near_card_attendance", partitionIndex, Offset.Unset) | |||||
//}; | |||||
////c.Assign(partitionsToConsume); | |||||
//Console.WriteLine($"Assigned partitions: {string.Join(", ", partitionsToConsume)}"); | |||||
//return partitionsToConsume; | |||||
}) | |||||
.SetPartitionsRevokedHandler((c, partitions) => | |||||
{ | |||||
}) | |||||
.Build(); | |||||
} | |||||
} | } | ||||
@@ -0,0 +1,288 @@ | |||||
using Confluent.Kafka; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using NearCardAttendance.Model; | |||||
using Newtonsoft.Json; | |||||
using Serilog.Context; | |||||
using Serilog; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using NearCardAttendance.Common.helper; | |||||
using Newtonsoft.Json.Linq; | |||||
using NearCardAttendance.Service.MessageQueue.Model; | |||||
namespace NearCardAttendance.TcpServer | |||||
{ | |||||
public class Worker : BackgroundService | |||||
{ | |||||
private readonly ILogger<Worker> _logger; | |||||
private readonly ServiceConfig _configService; | |||||
private readonly HttpHelper _httpHelper = default!; | |||||
private int _messageCount = 0; | |||||
public Worker(ILogger<Worker> logger, IOptions<ServiceConfig> _optConfigService,HttpHelper httpHelper) | |||||
{ | |||||
_logger = logger; | |||||
_configService = _optConfigService.Value; | |||||
_httpHelper = httpHelper; | |||||
} | |||||
/** | |||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | |||||
{ | |||||
var kafkaConsumer = CreateKafkaConsumer(); | |||||
kafkaConsumer.Subscribe("topics.storage.near_card_attendance"); | |||||
// process messages every 50 messages or every minute | |||||
// 每隔1分钟或没50条消息就批量处理一次 | |||||
var tasks = new List<Task>(); | |||||
#region 每分钟触发一次 | |||||
var messageCounter = 0; // 消息计数器 | |||||
var timer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds); // 定时器,每分钟触发一次 | |||||
timer.AutoReset = true; | |||||
// 定时器事件处理程序,用于定时处理消息 | |||||
timer.Elapsed += async (sender, e) => | |||||
{ | |||||
// 如果计数器大于 0,则处理消息 | |||||
if (messageCounter > 0) | |||||
{ | |||||
await ProcessMessagesAsync(tasks); | |||||
messageCounter = 0; // 处理完成后重置计数器 | |||||
} | |||||
}; | |||||
timer.Start(); // 启动定时器 | |||||
#endregion | |||||
try | |||||
{ | |||||
while (!stoppingToken.IsCancellationRequested) | |||||
{ | |||||
var consumeResult = kafkaConsumer.Consume(stoppingToken); | |||||
if (consumeResult != null) | |||||
{ | |||||
//tasks.Add(ProcessMessageAsync(consumeResult, kafkaConsumer)); | |||||
// 处理消息 | |||||
tasks.Add(ProcessMessageAsync(consumeResult, kafkaConsumer)); | |||||
messageCounter++; // 增加消息计数器 | |||||
// 如果消息计数达到 30 条,则处理消息并重置计数器 | |||||
if (messageCounter >= 30) | |||||
{ | |||||
await ProcessMessagesAsync(tasks); | |||||
messageCounter = 0; // 处理完成后重置计数器 | |||||
} | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) | |||||
{ | |||||
_logger.LogWarning("Worker exit"); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"An error occurred: {ex.Message}"); | |||||
} | |||||
await Task.WhenAll(tasks); | |||||
} | |||||
// 异步处理消息的方法 | |||||
private async Task ProcessMessagesAsync(List<Task> tasks) | |||||
{ | |||||
await Task.WhenAll(tasks); // 等待所有任务完成 | |||||
tasks.Clear(); // 清空任务列表 | |||||
} | |||||
private async Task ProcessMessageAsync(ConsumeResult<Ignore, string> consumeResult, IConsumer<Ignore, string> kafkaConsumer) | |||||
{ | |||||
// 处理消息的逻辑 | |||||
if (consumeResult != null) | |||||
{ | |||||
try | |||||
{ | |||||
var url = _configService.XinHuaLeYuUrl; | |||||
var data = new { }; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//kafkaConsumer.Commit(consumeResult); | |||||
_logger.LogError($"消费出错:{consumeResult.Message.Value},\n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
} | |||||
} | |||||
*/ | |||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | |||||
{ | |||||
var kafkaConsumer = CreateKafkaConsumer(); | |||||
kafkaConsumer.Subscribe("topics.storage.near_card_attendance"); | |||||
var tasks = new List<Task>(); | |||||
List<ConsumeResult<Ignore, string>> consumeBatchResult = new List<ConsumeResult<Ignore, string>>(); | |||||
try | |||||
{ | |||||
while (!stoppingToken.IsCancellationRequested) | |||||
{ | |||||
var consumeResult = kafkaConsumer.Consume(stoppingToken); | |||||
if (consumeResult != null) | |||||
{ | |||||
_messageCount++; | |||||
consumeBatchResult.Add(consumeResult); | |||||
//// 30条消息为一批 | |||||
if (_messageCount % 30 == 0) | |||||
{ | |||||
if (!await ProcessBatchMessageAsync(consumeBatchResult, kafkaConsumer)) | |||||
{ // 返回结果错误暂停5分钟 | |||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) | |||||
{ | |||||
_logger.LogWarning("Worker exit"); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"An error occurred: {ex.Message}"); | |||||
} | |||||
} | |||||
private async Task<bool> ProcessMessageAsync(ConsumeResult<Ignore, string> consumeResult, IConsumer<Ignore, string> kafkaConsumer) | |||||
{ | |||||
try | |||||
{ | |||||
var url = _configService.XinHuaLeYuUrl; | |||||
var data = new { }; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | |||||
if (!string.IsNullOrEmpty(res)) | |||||
{ | |||||
JObject resObj = (JObject)JsonConvert.DeserializeObject(res!)!; | |||||
if ((bool)resObj["success"]!) | |||||
{ | |||||
kafkaConsumer.Commit(consumeResult); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//kafkaConsumer.Commit(consumeResult); | |||||
_logger.LogError($"消费出错:{consumeResult.Message.Value},\n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
return false; | |||||
} | |||||
private async Task<bool> ProcessBatchMessageAsync(List<ConsumeResult<Ignore, string>> consumeBatchResult, IConsumer<Ignore, string> kafkaConsumer) | |||||
{ | |||||
try | |||||
{ | |||||
var url =$"{_configService.XinHuaLeYuUrl}/user/electronicCardAttendance/receiveTbAttendanceRecordException"; | |||||
var list = new List<object>(); | |||||
//consumeBatchResult.ForEach(x => { | |||||
// JObject msg = (JObject)JsonConvert.DeserializeObject(x.Message.Value)!; | |||||
// list.Add(new | |||||
// { | |||||
// attendanceStatus = int.TryParse(msg["data"]!["attendanceStatus"]!.ToString(), out int status) ? status : 0, | |||||
// attendanceTime = msg["data"]!["attendanceTime"]!.ToString(), | |||||
// imei = msg["data"]!["imei"]!.ToString() | |||||
// }) ; | |||||
//}); | |||||
consumeBatchResult.ForEach(x => { | |||||
EventData msg = JsonConvert.DeserializeObject<EventData>(x.Message.Value)!; | |||||
JObject content = (JObject)JsonConvert.DeserializeObject(msg.Content)!; | |||||
list.Add(new | |||||
{ | |||||
attendanceStatus = int.TryParse(content!["attendanceStatus"]!.ToString(), out int status) ? status : 0, | |||||
attendanceTime = content!["attendanceTime"]!.ToString(), | |||||
imei = content!["imei"]!.ToString() | |||||
}); | |||||
}); | |||||
var data = new { | |||||
data= list | |||||
}; | |||||
var res = await _httpHelper.HttpToPostAsync(url, data); | |||||
if (!string.IsNullOrEmpty(res)) | |||||
{ | |||||
JObject resObj = (JObject)JsonConvert.DeserializeObject(res!)!; | |||||
if ((bool)resObj["success"]!) | |||||
{ | |||||
consumeBatchResult.ForEach(x => | |||||
{ | |||||
kafkaConsumer.Commit(x); | |||||
}); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"处理消息出错 \n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
return false; | |||||
} | |||||
/// <summary> | |||||
/// 创建消费者 | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
private IConsumer<Ignore, string> CreateKafkaConsumer() | |||||
{ | |||||
var consumerConfig = new ConsumerConfig | |||||
{ | |||||
GroupId = "near_card_attendance", | |||||
BootstrapServers = _configService.KafkaServerAddress, | |||||
AutoOffsetReset = AutoOffsetReset.Earliest, | |||||
EnableAutoCommit = false, // 关闭自动提交偏移量 | |||||
CancellationDelayMaxMs = 1//set CancellationDelayMaxMs | |||||
}; | |||||
return new ConsumerBuilder<Ignore, string>(consumerConfig) | |||||
.SetErrorHandler((_, e) => | |||||
{ | |||||
//Console.WriteLine($"消费者创建出错,代码:{e.Code} |原因: {e.Reason}"); | |||||
_logger.LogInformation($"消费者创建出错,代码:{e.Code} |原因: {e.Reason}"); | |||||
}) | |||||
.SetPartitionsAssignedHandler((c, partitions) => | |||||
{ | |||||
//// 在这里手动指定要消费的分区 | |||||
//var partitionsToConsume = new List<TopicPartitionOffset> | |||||
//{ | |||||
// new TopicPartitionOffset("topics.storage.near_card_attendance", partitionIndex, Offset.Unset) | |||||
//}; | |||||
////c.Assign(partitionsToConsume); | |||||
//Console.WriteLine($"Assigned partitions: {string.Join(", ", partitionsToConsume)}"); | |||||
//return partitionsToConsume; | |||||
}) | |||||
.SetPartitionsRevokedHandler((c, partitions) => | |||||
{ | |||||
}) | |||||
.Build(); | |||||
} | |||||
} | |||||
} |
@@ -1,7 +1,8 @@ | |||||
{ | { | ||||
"AllowedHosts": "*", | "AllowedHosts": "*", | ||||
"ServiceConfig": { | "ServiceConfig": { | ||||
"XinHuaLeYuUrl": "https://midplat.xinhualeyu.com/dev-api/user/electronicCardAttendance/receiveTbAttendanceRecord", | |||||
"TelpoDataUrl": "https://ai.ssjlai.com/data" | |||||
"XinHuaLeYuUrl": "https://midplat.xinhualeyu.com/dev-api", | |||||
"TelpoDataUrl": "https://ai.ssjlai.com/data", | |||||
"KafkaServerAddress": "192.168.2.121:9092" | |||||
} | } | ||||
} | } |
@@ -1,7 +1,8 @@ | |||||
{ | { | ||||
"AllowedHosts": "*", | "AllowedHosts": "*", | ||||
"ServiceConfig": { | "ServiceConfig": { | ||||
"XinHuaLeYuUrl": "https://midplat.xinhualeyu.com/dev-api/user/electronicCardAttendance/receiveTbAttendanceRecord", | |||||
"TelpoDataUrl": "https://ai.ssjlai.com/data" | |||||
"XinHuaLeYuUrl": "https://midplat.xinhualeyu.com/dev-api", | |||||
"TelpoDataUrl": "https://ai.ssjlai.com/data", | |||||
"KafkaServerAddress": "172.19.42.53:9092" | |||||
} | } | ||||
} | } |