diff --git a/QYZH.InteractiveMagazine.IService/IJournalPageService.cs b/QYZH.InteractiveMagazine.IService/IJournalPageService.cs index 7b1e1bc..654f4b8 100644 --- a/QYZH.InteractiveMagazine.IService/IJournalPageService.cs +++ b/QYZH.InteractiveMagazine.IService/IJournalPageService.cs @@ -18,12 +18,6 @@ public interface IJournalPageService: IBaseService /// Task UpdatePageNoAsync(long JournalId); - /// - /// 打印书页(全部) - /// - /// - /// - Task PrintJournalPageAsync(long JournalId); Task DetailAsync(long id); Task DeleteAsync(long id); diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs index c60c127..39f75df 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/IRabbitMQService.cs @@ -9,6 +9,6 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ Task SendBatchAsync(IEnumerable @params, CancellationToken cancellationToken = default); - Task ReceiveAsync(string queueName, Func callback, CancellationToken cancellationToken = default); + Task ReceiveAsync(string exchange, string queueName, string routingKey, Func callback, CancellationToken cancellationToken = default); } } diff --git a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs index 51023e8..2a168e8 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/RabbitMQ/RabbitMQService.cs @@ -26,18 +26,17 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ { using var channel = await _connection.CreateChannel(); - // 启用发布者确认 - //var channelOpts = new CreateChannelOptions - //( - // publisherConfirmationsEnabled: true, - // publisherConfirmationTrackingEnabled: true, - // outstandingPublisherConfirmationsRateLimiter: new ThrottlingRateLimiter(MAX_OUTSTANDING_CONFIRMS) - //); + // 声明 Exchange(持久化) + await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, autoDelete: false, arguments: null); // 声明队列(持久化) - await channel.QueueDeclareAsync(queue: param.RoutingKey, durable: true, exclusive: false, autoDelete: false, arguments: null); + await channel.QueueDeclareAsync(queue: param.Queue, durable: true, exclusive: false, autoDelete: false, arguments: null); + + // 绑定队列到 Exchange + await channel.QueueBindAsync(queue: param.Queue, exchange: param.Exchange, routingKey: param.RoutingKey, arguments: null); + // 清空队列 - if (param.Purge) await channel.QueuePurgeAsync(param.RoutingKey); + if (param.Purge) await channel.QueuePurgeAsync(param.Queue); // 消息序列化 var mesjson = JsonSerializer.Serialize(param.Data, options); @@ -128,12 +127,20 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ } - public async Task ReceiveAsync(string queueName, Func callback, CancellationToken cancellationToken = default) + public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func callback, CancellationToken cancellationToken = default) { var channel = await _connection.CreateChannel(); //await channel.BasicQosAsync(0, 10, false); // 一次最多接收10条未确认的消息 + + // 声明 Exchange(持久化) + await channel.ExchangeDeclareAsync(exchange: exchange, type: "direct", durable: true, autoDelete: false, arguments: null); + + // 声明队列(持久化) await channel.QueueDeclareAsync(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); + // 绑定队列到 Exchange + await channel.QueueBindAsync(queue: queueName, exchange: exchange, routingKey: routingKey, arguments: null); + var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (model, ea) => { diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs index 1ce5927..0f1b907 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs @@ -71,6 +71,19 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal /// 回答时间秒 /// public int AnswerTime { get; set; } + /// + /// Desc:基础Prompt配置ID(为空则使用IsDefault=1的模板) + /// Default: + /// Nullable:True + /// + public string BasePrompt { get; set; } + + /// + /// Desc:个性化Prompt(追加到基础Prompt后面,可为空) + /// Default: + /// Nullable:True + /// + public string CustomPrompt { get; set; } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs index 06e9f5b..ee69e2c 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs @@ -126,7 +126,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Default: /// Nullable:True /// - public long? BasePromptId { get; set; } + public string BasePrompt { get; set; } /// /// Desc:个性化Prompt(追加到基础Prompt后面,可为空) diff --git a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs index b03a565..88d562d 100644 --- a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs +++ b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs @@ -7,24 +7,14 @@ namespace QYZH.InteractiveMagazine.Models.Enum; /// public enum JournalPageTaskTypeEnum { - /// - /// 单选题 - /// - [Description("单选题")] - SingleChoice = 1, - /// - /// 多选题 - /// - [Description("多选题")] - MultipleChoice = 2, /// /// 填空题 /// - [Description("填空题")] - FillInBlank = 3, + [Description("客观题")] + FillInBlank = 1, /// /// 问答题 /// - [Description("问答题")] + [Description("主观题")] TaskAnswer = 4 } diff --git a/QYZH.InteractiveMagazine.Service/JournalPageService.cs b/QYZH.InteractiveMagazine.Service/JournalPageService.cs index d882303..cbcce20 100644 --- a/QYZH.InteractiveMagazine.Service/JournalPageService.cs +++ b/QYZH.InteractiveMagazine.Service/JournalPageService.cs @@ -108,20 +108,7 @@ public class JournalPageService(BaseRepository JournalRepository, /// public async Task UpdatePageNoAsync(long JournalId) { - //这里不能修改 Exchange = "icr.direct" 否则会报错 - var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "icr.direct", Queue = "mq.Journal.updatePageNo.queue", RoutingKey = "mq.Journal.updatePageNo", Data = JournalId });//发生消息 - return msRes; - } - - /// - /// 打印书页(全部) - /// - /// - /// - public async Task PrintJournalPageAsync(long JournalId) - { - //这里不能修改 Exchange = "icr.direct" 否则会报错 - var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "icr.direct", Queue = "mq.Journal.printJournalPage.queue", RoutingKey = "mq.Journal.printJournalPage", Data = JournalId });//发生消息 + var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "ex.journal", Queue = "mq.journal.dotcode.auto", RoutingKey = "rk.journal.dotcode.auto", Data = JournalId });//发生消息 return msRes; } diff --git a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs index 48277a1..e0e147c 100644 --- a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs +++ b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs @@ -66,6 +66,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O task.Expression = input.Expression; task.Persuasiveness = input.Persuasiveness; task.AnswerTime = input.AnswerTime; + var no = input.No.Split('-').Select(int.Parse).ToArray(); if (no.Last() >= 2) diff --git a/QYZH.InteractiveMagazine.Service/JournalService.cs b/QYZH.InteractiveMagazine.Service/JournalService.cs index ca99118..25dedce 100644 --- a/QYZH.InteractiveMagazine.Service/JournalService.cs +++ b/QYZH.InteractiveMagazine.Service/JournalService.cs @@ -18,6 +18,7 @@ namespace QYZH.InteractiveMagazine.Service; public class JournalService(BaseRepository JournalPageRepository, BaseRepository JournalPageTaskRepository, + BaseRepository JournalCatalogRepository, BaseRepository dotFileRepository, BaseRepository dotFileDetailRepository, OssService ossService, ILogger logger) : BaseRepository, IJournalService { @@ -208,6 +209,7 @@ public class JournalService(BaseRepository JournalPageRepository, { await base.DeleteAsync(d => ids.Contains(d.Id)); await JournalPageRepository.DeleteAsync(d => ids.Contains(d.JournalId)); + await JournalCatalogRepository.DeleteAsync(d => ids.Contains(d.JournalId)); await JournalPageTaskRepository.DeleteAsync(d => ids.Contains(d.JournalId)); return true; }); diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs new file mode 100644 index 0000000..46da0a2 --- /dev/null +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace QYZH.InteractiveMagazine.WorkService.Consumers; + +/// +/// 自动铺码消费者 +/// +public class AutoDotCodeConsumer : IQueueConsumer +{ + private readonly ILogger _logger; + + public AutoDotCodeConsumer(ILogger logger) + { + _logger = logger; + } + + public string Exchange => "ex.journal"; + + public string QueueName => "mq.journal.dotcode.auto"; + + public string RoutingKey => "rk.journal.dotcode.auto"; + + public async Task HandleAsync(byte[] message) + { + var body = Encoding.UTF8.GetString(message); + _logger.LogInformation("收到自动铺码消息: {Message}", body); + + // TODO: 在此编写具体的铺码处理逻辑 + + await Task.CompletedTask; + } + + public Task OnErrorAsync(byte[] message, Exception exception) + { + var body = Encoding.UTF8.GetString(message); + _logger.LogError(exception, "处理自动铺码消息失败: {Message}", body); + return Task.CompletedTask; + } +} diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/IQueueConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/IQueueConsumer.cs index 002ff9d..974474c 100644 --- a/QYZH.InteractiveMagazine.WorkService/Consumers/IQueueConsumer.cs +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/IQueueConsumer.cs @@ -5,11 +5,21 @@ namespace QYZH.InteractiveMagazine.WorkService.Consumers; /// public interface IQueueConsumer { + /// + /// 交换机名称 + /// + string Exchange { get; } + /// /// 监听的队列名称 /// string QueueName { get; } + /// + /// 路由键 + /// + string RoutingKey { get; } + /// /// 处理消息 /// diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs index af30191..8dff60e 100644 --- a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs @@ -14,7 +14,11 @@ public class JournalTaskReceiveConsumer : IQueueConsumer _logger = logger; } - public string QueueName => "mq.Journal.task.receive"; + public string Exchange => "ex.journal"; + + public string QueueName => "mq.journal.task.receive"; + + public string RoutingKey => "rk.journal.task.receive"; public async Task HandleAsync(byte[] message) { diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/RabbitMQHostedService.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/RabbitMQHostedService.cs index dee36af..3aef0a4 100644 --- a/QYZH.InteractiveMagazine.WorkService/Consumers/RabbitMQHostedService.cs +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/RabbitMQHostedService.cs @@ -29,20 +29,29 @@ public class RabbitMQHostedService : BackgroundService _logger.LogInformation("发现 {Count} 个队列消费者,开始启动...", consumers.Count); - var tasks = consumers.Select(c => StartConsumerAsync(c.QueueName, stoppingToken)).ToList(); + var tasks = consumers.Select(c => StartConsumerAsync(c, stoppingToken)).ToList(); await Task.WhenAll(tasks); } - private async Task StartConsumerAsync(string queueName, CancellationToken stoppingToken) + private async Task StartConsumerAsync(IQueueConsumer queueConsumer, CancellationToken stoppingToken) { - _logger.LogInformation("正在启动消费者: {QueueName}", queueName); + var queueName = queueConsumer.QueueName; + var exchange = queueConsumer.Exchange; + var routingKey = queueConsumer.RoutingKey; + _logger.LogInformation("正在启动消费者: {QueueName}, Exchange: {Exchange}, RoutingKey: {RoutingKey}", queueName, exchange, routingKey); try { var rabbitMQService = _serviceProvider.GetRequiredService(); - await rabbitMQService.ReceiveAsync(queueName, async (channel, ea) => + await rabbitMQService.ReceiveAsync(exchange, queueName, routingKey, async (channel, ea) => { + // 声明死信队列及绑定(幂等操作,确保DLQ存在) + var dlqName = $"{queueName}.dlq"; + var dlqRoutingKey = $"{routingKey}.dlq"; + await channel.QueueDeclareAsync(queue: dlqName, durable: true, exclusive: false, autoDelete: false, arguments: null); + await channel.QueueBindAsync(queue: dlqName, exchange: exchange, routingKey: dlqRoutingKey, arguments: null); + // 每条消息创建独立 scope,确保消费者内可注入 Scoped 服务 using var messageScope = _serviceProvider.CreateScope(); var consumer = messageScope.ServiceProvider @@ -57,7 +66,15 @@ public class RabbitMQHostedService : BackgroundService catch (Exception ex) { _logger.LogError(ex, "消费者 {QueueName} 处理消息异常", queueName); - await channel.BasicNackAsync(ea.DeliveryTag, false, true, stoppingToken); + + // 发送到死信队列,成功则从主队列移除,失败则重新入队 + var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, ea.Body.ToArray(), stoppingToken); + await channel.BasicNackAsync(ea.DeliveryTag, false, !dlqSent, stoppingToken); + + if (dlqSent) + { + _logger.LogInformation("消息已转入死信队列 {DlqName}", dlqName); + } try { @@ -79,4 +96,32 @@ public class RabbitMQHostedService : BackgroundService _logger.LogError(ex, "消费者 {QueueName} 启动失败", queueName); } } + + /// + /// 将失败消息发送到死信队列 + /// + private async Task SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, byte[] body, CancellationToken cancellationToken) + { + try + { + var rabbitMQConnection = _serviceProvider.GetRequiredService(); + using var channel = await rabbitMQConnection.CreateChannel(); + await channel.QueueDeclareAsync(queue: dlqName, durable: true, exclusive: false, autoDelete: false, arguments: null); + await channel.QueueBindAsync(queue: dlqName, exchange: exchange, routingKey: dlqRoutingKey, arguments: null); + + var properties = new RabbitMQ.Client.BasicProperties + { + Persistent = true + }; + await channel.BasicPublishAsync(exchange, dlqRoutingKey, false, properties, body, cancellationToken); + + _logger.LogInformation("消息已发送到死信队列: {DlqName}", dlqName); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "发送消息到死信队列 {DlqName} 失败", dlqName); + return false; + } + } } diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/PrintTool.exe b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/PrintTool.exe new file mode 100644 index 0000000..23206b0 Binary files /dev/null and b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/PrintTool.exe differ diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml new file mode 100644 index 0000000..5e49d38 --- /dev/null +++ b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml new file mode 100644 index 0000000..bba3846 --- /dev/null +++ b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdlicense.dll b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdlicense.dll new file mode 100644 index 0000000..90f8a24 Binary files /dev/null and b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdlicense.dll differ diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdprint.dll b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdprint.dll new file mode 100644 index 0000000..99aec2f Binary files /dev/null and b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdprint.dll differ diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/命令行方式调用方法.txt b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/命令行方式调用方法.txt new file mode 100644 index 0000000..46083fb --- /dev/null +++ b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/命令行方式调用方法.txt @@ -0,0 +1,31 @@ +调用PrintTool.exe增加参数,说明如下: +1、-sMode=Generate (必须) +2、-sPDF=源文件pdf格式的完整路径(必须) +3、-sLIC=铺码资源文件完整路径(必须) +4、-oPDF=生成pdf文件的完整路径(必须) +5、-dType=点阵形状,0:方点,1:圆点,默认为0(可选) +6、-dPrint=打印场景,0:普通激光打印机,1:工业印刷,默认为0(可选) +7、-pStart=数字,整数,可以制定资源文件从第几个编号开始铺码(非必须,默认0,表示从资源的剩余页码开始,每次铺码成功后点阵资源会相应减少;大于0时点阵资源不会减少,pStart=1时表示从第一页开始铺码) +8、-dPageAddr=是否显示点阵页码地址,0:不显示,1:显示,默认为0(可选) +9、-sPrinter=打印机名称(必须) +10、-dPageStart=数字,设置打印起始页码,1表示从第一页开始打印,0:全部打印,默认0(可选) +11、-dPageEnd=数字,设置打印结束页码,0:全部打印,默认0(可选) +12、-dCopy=数字,设置打印份数,默认1(可选) +13、-dKValue=数字,设置码点颜色深度,取值范围50-100,默认100,比如取值90表示k值为90%(可选) +14、-dDotSize=数字,设置码点大小,取值范围30-50,默认40(可选),方点仅支持40 +15、-dOutFile=数字,0:只生成带点阵pdf文件,1:只生成纯点阵文件,2:既生成纯点阵文件也生成带点阵pdf文件,默认为0(可选) + +16、-dControlPageNum={[页地址,连续数量],[页地址,连续数量]...} 可一个pdf有多段页码段,默认从第一页开始(可选)(最后一段若是不想数pdf剩下多少页,可直接放0默认用最后字段铺完剩下的页) + +说明: +当-dOutFile=2时,纯点阵文件名为输入的-oPDF参数,带点阵pdf文件名为在-oPDF参数后加上"_dp",即"D:\pdf\28_dot_dp.pdf"。 + +示例: +制作点阵: +-sMode=Generate -sPDF="D:\pdf\28.pdf" -sLIC="D:\Root licnese segment 70_70.0.0.0_100.xml" -oPDF="D:\pdf\28_dot.pdf" -pStart=1 + +制作纯点阵文件: +-sMode=Generate -sPDF="D:\pdf\28.pdf" -sLIC="D:\Root licnese segment 70_70.0.0.0_100.xml" -oPDF="D:\pdf\28_dot.pdf" -dOutFile=1 + +打印: +-sMode=Print -sPDF="D:\pdf\28_dot.pdf" -sPrinter="HP LaserJet Professional M1216nfh MFP (副本 1)" -dPageStart=2 -dPageEnd=3 -dCopy=5 diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/返回值说明.txt b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/返回值说明.txt new file mode 100644 index 0000000..837560a --- /dev/null +++ b/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/返回值说明.txt @@ -0,0 +1,13 @@ +0:成功 +-1:未知错误 +-2、-105:输入参数错误 +-6:读取pdf信息失败 +-5、-7:加载授权文件失败 +-8:授权文件页码数量不够或页面尺寸不匹配 +-10:生成码点失败 +-11到-15:生成pdf失败 +-30:程序异常,触发保护机制 +-31: 传入起始页码不在资源范围内 +-32:传入起始页码+连续铺码数量大于资源文件剩余可使用页数 +-33:中间页码连续数量为0 +-34:预计铺码数量大于PDF的页数 diff --git a/QYZH.InteractiveMagazine.WorkService/Program.cs b/QYZH.InteractiveMagazine.WorkService/Program.cs index 44d3285..7a7fe43 100644 --- a/QYZH.InteractiveMagazine.WorkService/Program.cs +++ b/QYZH.InteractiveMagazine.WorkService/Program.cs @@ -63,6 +63,7 @@ builder.Services.AddRabbitMQ(builder.Configuration); // 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册) builder.Services.AddScoped(); +builder.Services.AddScoped(); // 注册消费者后台服务 builder.Services.AddHostedService();