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