From 2505d93459abcf5a857b299e60c2f415b2f63948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E6=BD=9C?= <1620556043@qq.com> Date: Wed, 11 Jun 2025 20:45:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E7=BB=83=E4=B9=A0=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatePracticeSettingDto.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Src/CodeSpirit.ExamApi/Dtos/PracticeSetting/CreatePracticeSettingDto.cs b/Src/CodeSpirit.ExamApi/Dtos/PracticeSetting/CreatePracticeSettingDto.cs index ee1398f..f1bb2e8 100644 --- a/Src/CodeSpirit.ExamApi/Dtos/PracticeSetting/CreatePracticeSettingDto.cs +++ b/Src/CodeSpirit.ExamApi/Dtos/PracticeSetting/CreatePracticeSettingDto.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using CodeSpirit.Amis.Attributes.FormFields; using CodeSpirit.ExamApi.Data.Models.Enums; namespace CodeSpirit.ExamApi.Dtos.PracticeSetting; @@ -27,10 +28,21 @@ public class CreatePracticeSettingDto /// /// 试卷ID /// - [Required(ErrorMessage = "试卷ID不能为空")] - [DisplayName("试卷ID")] + [DisplayName("试卷")] + [Required(ErrorMessage = "请选择试卷")] + [AmisSelectField( + Source = "${ROOT_API}/api/exam/ExamPapers/select-published", + ValueField = "id", + LabelField = "name", + Multiple = false, + JoinValues = false, + ExtractValue = true, + Searchable = true, + Clearable = true, + Placeholder = "请选择试卷" + )] public long ExamPaperId { get; set; } - + /// /// 练习模式 /// @@ -56,11 +68,11 @@ public class CreatePracticeSettingDto /// 是否显示答案解析 /// [DisplayName("显示答案解析")] - public bool ShowAnalysis { get; set; } = true; + public bool? ShowAnalysis { get; set; } = false; /// /// 是否随机排序题目 /// [DisplayName("随机排序题目")] - public bool RandomizeQuestions { get; set; } = false; + public bool? RandomizeQuestions { get; set; } = false; } \ No newline at end of file -- Gitee From 5fec31725778681e1d8b197ed6bf3a0bc13c4007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E6=BD=9C?= <1620556043@qq.com> Date: Mon, 16 Jun 2025 15:40:21 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=80=83=E7=94=9F?= =?UTF-8?q?=E6=97=B6=E9=80=9A=E8=BF=87=E6=B6=88=E6=81=AF=E9=98=9F=E5=88=97?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=85=B3=E8=81=94=E7=94=A8=E6=88=B7=E7=9A=84?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/StudentService.cs | 44 ++++++++++++++ .../EventHandlers/UserDeletedEventHandler.cs | 58 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 1 + .../EventBus/Events/UserDeletedEvent.cs | 14 +++++ 4 files changed, 117 insertions(+) create mode 100644 Src/CodeSpirit.IdentityApi/EventHandlers/UserDeletedEventHandler.cs create mode 100644 Src/CodeSpirit.Shared/EventBus/Events/UserDeletedEvent.cs diff --git a/Src/CodeSpirit.ExamApi/Services/Implementations/StudentService.cs b/Src/CodeSpirit.ExamApi/Services/Implementations/StudentService.cs index d37f5e1..01ca199 100644 --- a/Src/CodeSpirit.ExamApi/Services/Implementations/StudentService.cs +++ b/Src/CodeSpirit.ExamApi/Services/Implementations/StudentService.cs @@ -166,8 +166,10 @@ public class StudentService : BaseCRUDIService x.StudentId == id) .ExecuteDeleteAsync(); + await OnDeleting(student); await Repository.DeleteAsync(student); await Repository.SaveChangesAsync(); + await OnDeleted(student); } catch (Exception ex) { @@ -242,6 +244,13 @@ public class StudentService : BaseCRUDIService /// 发布用户创建事件 /// @@ -270,6 +279,41 @@ public class StudentService : BaseCRUDIService + /// 发布用户删除事件 + /// + /// + /// + private async Task PublishUserDeletedEventAsync(Student student) + { + try + { + _logger.LogInformation("准备发布用户删除事件: 学生ID={StudentId}, 用户ID={UserId}", + student.Id, student.UserId); + + if (student.UserId <= 0) + { + _logger.LogError("无法发布用户删除事件:用户ID无效: {UserId}", student.UserId); + return; + } + + var @event = new UserDeletedEvent + { + UserId = student.UserId, + }; + + _logger.LogInformation("正在发布用户删除事件: {@Event}", @event); + await _eventBus.PublishAsync(@event); + _logger.LogInformation("用户删除事件发布成功: 用户ID={UserId}", student.UserId); + } + catch (Exception ex) + { + _logger.LogError(ex, "发布用户删除事件失败: 学生ID={StudentId}, 用户ID={UserId}, 错误信息: {ErrorMessage}", + student.Id, student.UserId, ex.Message); + throw; // 重新抛出异常,让调用者知道发布失败 + } + } + /// /// 获取导入项ID /// diff --git a/Src/CodeSpirit.IdentityApi/EventHandlers/UserDeletedEventHandler.cs b/Src/CodeSpirit.IdentityApi/EventHandlers/UserDeletedEventHandler.cs new file mode 100644 index 0000000..1343933 --- /dev/null +++ b/Src/CodeSpirit.IdentityApi/EventHandlers/UserDeletedEventHandler.cs @@ -0,0 +1,58 @@ +using CodeSpirit.Core.Extensions; +using CodeSpirit.Core; +using CodeSpirit.IdentityApi.Services; +using CodeSpirit.Shared.EventBus.Events; +using CodeSpirit.Shared.EventBus.Interfaces; + +namespace CodeSpirit.IdentityApi.EventHandlers +{ + public class UserDeletedEventHandler : IEventHandler + { + private readonly IUserService _userService; + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + public UserDeletedEventHandler( + IUserService userService, + ILogger logger) + { + _userService = userService; + _logger = logger; + _logger.LogInformation("UserDeletedEventHandler 已初始化"); + } + + /// + /// 处理用户删除事件 + /// + public async Task HandleAsync(UserDeletedEvent @event) + { + try + { + _logger.LogInformation("开始处理用户删除事件: {@Event}", @event); + + if (@event == null) + { + _logger.LogError("收到的事件为空"); + return; + } + + if (@event.UserId <= 0) + { + _logger.LogError("事件中的用户ID无效: {UserId}", @event.UserId); + return; + } + + _logger.LogInformation("正在删除用户: {UserId}", @event.UserId); + await _userService.DeleteAsync(@event.UserId); + _logger.LogInformation("用户删除成功: {UserId}", @event.UserId); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理用户删除事件失败: {@Event}", @event); + throw new AppServiceException(500, $"处理用户删除事件失败: {ex.Message}"); + } + } + } +} diff --git a/Src/CodeSpirit.IdentityApi/ServiceCollectionExtensions.cs b/Src/CodeSpirit.IdentityApi/ServiceCollectionExtensions.cs index fbbee13..735c18c 100644 --- a/Src/CodeSpirit.IdentityApi/ServiceCollectionExtensions.cs +++ b/Src/CodeSpirit.IdentityApi/ServiceCollectionExtensions.cs @@ -242,6 +242,7 @@ public static class ServiceCollectionExtensions // 注册事件处理器 builder.Services.AddEventHandler(); + builder.Services.AddEventHandler(); return builder; } diff --git a/Src/CodeSpirit.Shared/EventBus/Events/UserDeletedEvent.cs b/Src/CodeSpirit.Shared/EventBus/Events/UserDeletedEvent.cs new file mode 100644 index 0000000..b3366b1 --- /dev/null +++ b/Src/CodeSpirit.Shared/EventBus/Events/UserDeletedEvent.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace CodeSpirit.Shared.EventBus.Events; + +/// +/// 用户删除 +/// +public class UserDeletedEvent +{ + /// + /// 用户ID + /// + public long UserId { get; set; } +} \ No newline at end of file -- Gitee From 915ff5c76d1a6a3043ed2cda171aef0c71994eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E6=BD=9C?= <1620556043@qq.com> Date: Mon, 16 Jun 2025 15:46:13 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=B8=BA=E6=AF=8F=E4=B8=AA=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E8=80=85=E5=88=9B=E5=BB=BA=E7=8B=AC=E7=AB=8B=E7=9A=84?= =?UTF-8?q?=E9=80=9A=E9=81=93=EF=BC=8C=E9=81=BF=E5=85=8D=E9=80=9A=E9=81=93?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RabbitMQEventSubscriber.cs | 120 +++++++++++------- 1 file changed, 74 insertions(+), 46 deletions(-) diff --git a/Src/CodeSpirit.Shared/EventBus/Implementations/RabbitMQEventSubscriber.cs b/Src/CodeSpirit.Shared/EventBus/Implementations/RabbitMQEventSubscriber.cs index 028dcb7..3cf9bb2 100644 --- a/Src/CodeSpirit.Shared/EventBus/Implementations/RabbitMQEventSubscriber.cs +++ b/Src/CodeSpirit.Shared/EventBus/Implementations/RabbitMQEventSubscriber.cs @@ -24,6 +24,7 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber private readonly Dictionary> _eventHandlers; private readonly List _queueNames = new(); private readonly Dictionary _consumerTags = new(); + private readonly Dictionary _consumerChannels = new(); /// /// 构造函数 @@ -1145,6 +1146,20 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber try { + // 关闭所有消费者通道 + foreach (var channel in _consumerChannels.Values) + { + try + { + channel.Dispose(); + } + catch (Exception ex) + { + _subscriberLogger.LogWarning(ex, "关闭消费者通道时出错"); + } + } + _consumerChannels.Clear(); + UnbindAllQueues(); base.Dispose(); } @@ -1213,19 +1228,31 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber try { - // 确保通道可用,这里使用最简单的创建方式,避免复杂的逻辑干扰 - _channel?.Dispose(); // 先释放旧通道 - _channel = _connection.CreateModel(); + // 为每个消费者创建独立的通道 + if (_consumerChannels.TryGetValue(queueName, out var existingChannel)) + { + try + { + existingChannel.Dispose(); + } + catch (Exception ex) + { + _subscriberLogger.LogWarning(ex, "关闭已存在的通道时出错: {QueueName}", queueName); + } + } + + var channel = _connection.CreateModel(); + _consumerChannels[queueName] = channel; // 声明交换机 - _channel.ExchangeDeclare( + channel.ExchangeDeclare( exchange: _exchangeName, type: ExchangeType.Topic, // 或考虑改为 ExchangeType.Fanout durable: true, autoDelete: false); // 声明死信交换机 - _channel.ExchangeDeclare( + channel.ExchangeDeclare( exchange: _deadLetterExchangeName, type: ExchangeType.Topic, durable: true, @@ -1238,7 +1265,7 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber {"x-dead-letter-routing-key", eventName} }; - _channel.QueueDeclare( + channel.QueueDeclare( queue: queueName, durable: true, exclusive: false, @@ -1246,25 +1273,25 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber arguments: arguments); // 获取队列消息数量 - var queueInfo = _channel.QueueDeclare(queueName, true, false, false, arguments); + var queueInfo = channel.QueueDeclare(queueName, true, false, false, arguments); _subscriberLogger.LogInformation("查询队列状态: {QueueName}, 消息数量: {MessageCount}", queueName, queueInfo.MessageCount); // 绑定队列到交换机 - _channel.QueueBind( + channel.QueueBind( queue: queueName, exchange: _exchangeName, routingKey: eventName); - + // 设置QoS - 非常重要,确保消费者不会一次接收太多消息 - _channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); // 降低为1,确保逐条处理 + channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); // 清除旧消费者(如果有) if (_consumerTags.TryGetValue(queueName, out var oldTag)) { try { - _channel.BasicCancel(oldTag); + channel.BasicCancel(oldTag); _subscriberLogger.LogInformation("已取消旧消费者: {QueueName}, ConsumerTag: {ConsumerTag}", queueName, oldTag); } catch (Exception ex) @@ -1275,8 +1302,8 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber } // 创建消费者 - 使用最简单直接的方式 - var consumer = new EventingBasicConsumer(_channel); // 使用同步版本,避免异步问题 - + var consumer = new EventingBasicConsumer(channel); // 使用同步版本,避免异步问题 + // 注册接收消息事件 consumer.Received += (sender, args) => { try @@ -1285,24 +1312,24 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber var body = args.Body.ToArray(); var message = Encoding.UTF8.GetString(body); var deliveryTag = args.DeliveryTag; - + // 记录消息接收 _subscriberLogger.LogInformation("收到消息: {EventName}, DeliveryTag: {DeliveryTag}, 内容: {MessageContent}", eventName, deliveryTag, message); - + // 第二步:处理消息 (同步调用,确保消息被处理) var success = ProcessEventAsync(eventName, message).GetAwaiter().GetResult(); - + // 第三步:确认或拒绝消息 if (success) { - _channel.BasicAck(deliveryTag, false); + channel.BasicAck(deliveryTag, false); _subscriberLogger.LogInformation("消息已确认: {EventName}, DeliveryTag: {DeliveryTag}", eventName, deliveryTag); } else { // 如果处理失败,不重新入队,让它进入死信队列 - _channel.BasicNack(deliveryTag, false, false); + channel.BasicNack(deliveryTag, false, false); _subscriberLogger.LogWarning("消息处理失败,已拒绝: {EventName}, DeliveryTag: {DeliveryTag}", eventName, deliveryTag); } } @@ -1312,7 +1339,7 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber try { // 出现异常,重新入队 - _channel.BasicNack(args.DeliveryTag, false, true); + channel.BasicNack(args.DeliveryTag, false, true); } catch { @@ -1324,51 +1351,52 @@ public class RabbitMQEventSubscriber : RabbitMQEventBusBase, IEventSubscriber // 注册消费者取消事件 consumer.Shutdown += (sender, args) => { _subscriberLogger.LogWarning("消费者已关闭: {QueueName}, 原因: {Reason}", queueName, args.ReplyText); + // 尝试重新创建消费者 + Task.Run(async () => { + try + { + await Task.Delay(1000); // 等待1秒后重试 + await Subscribe(); + } + catch (Exception ex) + { + _subscriberLogger.LogError(ex, "重新创建消费者失败: {QueueName}", queueName); + } + }); }; // 开始消费 - var consumerTag = _channel.BasicConsume( + var consumerTag = channel.BasicConsume( queue: queueName, autoAck: false, // 关键:禁用自动确认,必须手动确认 consumer: consumer); // 保存消费者标签 _consumerTags[queueName] = consumerTag; - + // 记录成功订阅 _subscriberLogger.LogInformation("成功订阅事件: {EventName}, 处理器: {HandlerType}, 消费者标签: {ConsumerTag}", eventName, handlerType.Name, consumerTag); - + // 确认消费者正在运行 _subscriberLogger.LogInformation("消费者已启动,正在监听队列: {QueueName}, 消费者标签: {ConsumerTag}, 通道ID: {ChannelId}", - queueName, consumerTag, _channel.ChannelNumber); - - // 手动获取一条消息测试是否工作 (可选,仅用于调试) - var result = _channel.BasicGet(queueName, false); // false = 不自动确认 - if (result != null) - { - _subscriberLogger.LogInformation("主动获取到消息: DeliveryTag={DeliveryTag}, 长度={BodyLength}字节", - result.DeliveryTag, result.Body.Length); - - // 获取消息内容 - var messageBody = Encoding.UTF8.GetString(result.Body.ToArray()); - _subscriberLogger.LogInformation("消息内容: {MessageContent}", messageBody); - - // 测试完后确认这条消息 - _channel.BasicAck(result.DeliveryTag, false); - _subscriberLogger.LogInformation("已确认测试消息: DeliveryTag={DeliveryTag}", result.DeliveryTag); - } - else - { - _subscriberLogger.LogWarning("未能主动获取消息,队列可能为空: {QueueName}", queueName); - } + queueName, consumerTag, channel.ChannelNumber); } catch (Exception ex) { _subscriberLogger.LogError(ex, "订阅事件失败: {EventName}, 队列: {QueueName}", eventName, queueName); - // 发生错误时,通道可能需要重新创建 - _channel?.Dispose(); - _channel = null; + if (_consumerChannels.TryGetValue(queueName, out var channel)) + { + try + { + channel.Dispose(); + } + catch + { + // 忽略关闭通道时的错误 + } + _consumerChannels.Remove(queueName); + } } } } \ No newline at end of file -- Gitee