diff --git a/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs b/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
new file mode 100644
index 0000000..5580094
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
@@ -0,0 +1,338 @@
+using QYZH.InteractiveMagazine.Common.Extensions;
+using QYZH.InteractiveMagazine.Infrastructure.OSS;
+using QYZH.InteractiveMagazine.Models.Dto.Journal;
+using QYZH.InteractiveMagazine.Models.Entity;
+using QYZH.InteractiveMagazine.Models.Enum;
+using SqlSugar;
+using System.Diagnostics;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+
+namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
+
+///
+/// 自动铺码消费者。
+///
+public class AutoDotCodeConsumer(
+ IConfiguration configuration,
+ IServiceScopeFactory scopeFactory,
+ IHostEnvironment hostEnvironment,
+ ILogger logger,
+ IHttpClientFactory httpClientFactory,
+ OssService ossService) : IQueueConsumer
+{
+ ///
+ /// 交换机名称。
+ ///
+ public string Exchange => "ex.journal";
+
+ ///
+ /// 队列名称。
+ ///
+ public string QueueName => "mq.journal.dotcode.auto";
+
+ ///
+ /// 路由键。
+ ///
+ public string RoutingKey => "rk.journal.dotcode.auto";
+
+ ///
+ /// 处理自动铺码消息。
+ ///
+ public async Task HandleAsync(byte[] body, CancellationToken cancellationToken = default)
+ {
+ var message = Encoding.UTF8.GetString(body);
+ logger.LogInformation("收到自动铺码消息: {Message}", message);
+
+ using var scope = scopeFactory.CreateScope();
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+
+ try
+ {
+ var request = JsonSerializer.Deserialize(message, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ if (request == null)
+ {
+ return;
+ }
+
+ var workDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "JournalPagePdf");
+ var uploadDirectory = Path.Combine(workDirectory, "upload");
+ var downloadDirectory = Path.Combine(workDirectory, "download");
+ Directory.CreateDirectory(uploadDirectory);
+ Directory.CreateDirectory(downloadDirectory);
+
+ var uploadFileName = $"upload_{request.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}.pdf";
+ var uploadFilePath = Path.Combine(uploadDirectory, uploadFileName);
+
+ var journalPdfKey = request.JournalPdfUrl?.RemoveDomain();
+ if (string.IsNullOrWhiteSpace(journalPdfKey))
+ {
+ logger.LogError("书籍上传 PDF 文件地址为空,JournalId: {JournalId}", request.JournalId);
+ return;
+ }
+
+ await using var pdfStream = ossService.GetObjectStream(journalPdfKey);
+ if (pdfStream == null)
+ {
+ logger.LogError("获取书籍上传 PDF 文件失败,OSS Key: {OssKey}", journalPdfKey);
+ return;
+ }
+
+ await using (var fs = new FileStream(uploadFilePath, FileMode.CreateNew, FileAccess.Write))
+ {
+ await pdfStream.CopyToAsync(fs, cancellationToken);
+ logger.LogInformation("书籍 PDF 已下载到本地,长度:{Length}", fs.Length);
+ }
+
+ var dotId = configuration.GetValue("PrintConfig:DotId", 765837655859269);
+ var dotFile = await dbContext.Queryable().FirstAsync(x => x.Id == dotId, cancellationToken);
+ if (dotFile == null)
+ {
+ logger.LogError("配置打印数据错误,点阵文件不存在,DotId: {DotId}", dotId);
+ return;
+ }
+
+ var printToolDirectory = Path.Combine(hostEnvironment.ContentRootPath, "PrintToolV2.7");
+ var exePath = Path.Combine(printToolDirectory, "PrintTool.exe");
+ if (!File.Exists(exePath))
+ {
+ logger.LogError("PrintTool.exe 不存在,路径:{ExePath}", exePath);
+ await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
+ return;
+ }
+
+ var xmlPath = Path.Combine(printToolDirectory, dotFile.FileName);
+ if (!File.Exists(xmlPath))
+ {
+ logger.LogError("铺码授权文件不存在,路径:{XmlPath}", xmlPath);
+ await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
+ return;
+ }
+
+ var downloadFileName = $"download_{request.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}_dot.pdf";
+ var downloadFilePath = Path.Combine(downloadDirectory, downloadFileName);
+ var dPrint = configuration.GetValue("PrintConfig:DPrint", 0);
+ var pageNumMax = request.PageNum.Max(x => x);
+ var dotFileDetailList = await dbContext.Queryable()
+ .Where(x => x.DotId == dotId && !x.IsUse)
+ .OrderBy(x => x.Id)
+ .Take(pageNumMax)
+ .ToListAsync(cancellationToken);
+
+ if (dotFileDetailList.Count < pageNumMax)
+ {
+ logger.LogError("点阵页码余量不足,DotId: {DotId}, Need: {Need}, Available: {Available}", dotId, pageNumMax, dotFileDetailList.Count);
+ await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
+ return;
+ }
+
+ var dotFileDetailPageName = dotFileDetailList.Select(x => x.PageName).OrderBy(x => x).ToArray();
+ var pageStr = "{" + $"[{dotFileDetailList[0].PageName},{pageNumMax}]" + "}";
+ var arguments = $"-sMode=Generate -sPDF=\"{uploadFilePath}\" -sLIC=\"{xmlPath}\" -pStart=1 -oPDF=\"{downloadFilePath}\" -dPageAddr=1 -dPrint={dPrint} -dDotSize=40 -dType=0 -dOutFile=0 -dControlPageNum={pageStr}";
+ var printResult = await ExecutePrintToolAsync(exePath, printToolDirectory, arguments, cancellationToken);
+
+ if (printResult.Timeout || printResult.ExitCode != 0)
+ {
+ logger.LogError("执行 PrintTool.exe 失败,退出码:{ExitCode},错误信息:{ErrorMessage}", printResult.ExitCode, printResult.Error);
+ await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request, dotFileDetailPageName), cancellationToken);
+ return;
+ }
+
+ var statusModel = new JournalPagePrintDto
+ {
+ JournalId = request.JournalId,
+ PageNo = dotFileDetailPageName
+ };
+
+ if (!ValidatePrintOutput(printResult.Output, dotFileDetailPageName, dPrint))
+ {
+ statusModel.Status = JournalStatusEnum.CodeFail;
+ await ExecuteUpdateJournalStatusAsync(statusModel, cancellationToken);
+ return;
+ }
+
+ var tempOssDownloadKey = "journal/download/" + downloadFileName;
+ const int bufferSize = 1 * 1024 * 1024;
+ await using var downloadFs = new FileStream(downloadFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan | FileOptions.Asynchronous);
+ var downloadBookPagePdfName = ossService.PutObject(tempOssDownloadKey, downloadFs);
+ var ossDomain = configuration.GetSection("AliyunOSSConfigs:Domain").Get() ?? string.Empty;
+
+ statusModel.DownloadJournalPagePdfName = ossDomain + downloadBookPagePdfName;
+ await downloadFs.DisposeAsync();
+
+ TryDeleteFile(uploadFilePath);
+ TryDeleteFile(downloadFilePath);
+
+ statusModel.Status = JournalStatusEnum.CodeSuccess;
+ var callbackResponse = await ExecuteUpdateJournalStatusAsync(statusModel, cancellationToken);
+ if (callbackResponse.IsSuccess)
+ {
+ await dbContext.Ado.UseTranAsync(async () =>
+ {
+ foreach (var dotFileDetail in dotFileDetailList)
+ {
+ dotFileDetail.IsUse = true;
+ dotFileDetail.UpdatedAt = DateTime.Now;
+ }
+
+ await dbContext.Updateable(dotFileDetailList).ExecuteCommandAsync();
+ await dbContext.Updateable()
+ .SetColumns(x => x.TotalUse == x.TotalUse + dotFileDetailList.Count)
+ .SetColumns(x => x.UpdatedAt == DateTime.Now)
+ .Where(x => x.Id == dotId)
+ .ExecuteCommandAsync();
+ });
+ }
+ else
+ {
+ logger.LogError("回调接口修改书籍状态失败,JournalId: {JournalId},接口返回消息:{Message}", statusModel.JournalId, callbackResponse.Message);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "自动铺码处理失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 处理消费异常。
+ ///
+ public Task OnErrorAsync(byte[] message, Exception exception)
+ {
+ var body = Encoding.UTF8.GetString(message);
+ logger.LogError(exception, "处理自动铺码消息失败: {Message}", body);
+ return Task.CompletedTask;
+ }
+
+ private async Task ExecutePrintToolAsync(string exePath, string workingDirectory, string arguments, CancellationToken cancellationToken)
+ {
+ var printToolTimeoutSeconds = configuration.GetValue("PrintConfig:TimeoutSeconds", 300);
+ var printToolTimeout = TimeSpan.FromSeconds(printToolTimeoutSeconds);
+ var cmd = $"\"{exePath}\" {arguments}";
+ logger.LogInformation("执行 PrintTool.exe 的命令: {Command},超时时间:{TimeoutSeconds} 秒", cmd, printToolTimeoutSeconds);
+
+ using var process = new Process();
+ process.StartInfo = new ProcessStartInfo
+ {
+ WorkingDirectory = workingDirectory,
+ FileName = exePath,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ process.Start();
+ var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
+ var timeout = false;
+
+ try
+ {
+ await process.WaitForExitAsync(cancellationToken).WaitAsync(printToolTimeout, cancellationToken);
+ }
+ catch (TimeoutException)
+ {
+ timeout = true;
+ logger.LogError("执行 PrintTool.exe 超时,准备终止进程树,超时时间:{TimeoutSeconds} 秒,命令:{Command}", printToolTimeoutSeconds, cmd);
+ process.Kill(entireProcessTree: true);
+ await process.WaitForExitAsync(CancellationToken.None);
+ }
+
+ var output = await outputTask;
+ var error = await errorTask;
+ var exitCode = process.ExitCode;
+
+ logger.LogInformation("执行 PrintTool.exe 后,退出码:{ExitCode},stdout:{Output}", exitCode, output);
+ if (!string.IsNullOrWhiteSpace(error))
+ {
+ logger.LogWarning("执行 PrintTool.exe 后,stderr:{ErrorMessage}", error);
+ }
+
+ return new PrintToolResult(output, error, exitCode, timeout);
+ }
+
+ private bool ValidatePrintOutput(string output, string[] dotFileDetailPageName, int dPrint)
+ {
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ logger.LogInformation("执行铺码程序没有任何输出");
+ return false;
+ }
+
+ if (dPrint == 0)
+ {
+ var pageNoList = JsonSerializer.Deserialize(output) ?? [];
+ return pageNoList.OrderBy(x => x).SequenceEqual(dotFileDetailPageName.OrderBy(x => x), StringComparer.Ordinal);
+ }
+
+ return dotFileDetailPageName.AsParallel().Any(ip => output.Contains(ip, StringComparison.Ordinal));
+ }
+
+ private async Task ExecuteUpdateJournalStatusAsync(JournalPagePrintDto request, CancellationToken cancellationToken)
+ {
+ var client = httpClientFactory.CreateClient();
+ var callbackUrl = configuration.GetValue("PrintConfig:CallBackApiUrl");
+ var callbackRequest = new HttpRequestMessage(HttpMethod.Post, callbackUrl)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json")
+ };
+
+ var response = await client.SendAsync(callbackRequest, cancellationToken);
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
+ logger.LogInformation("回调接口返回内容:{ResponseContent}", responseContent);
+ return JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? new CallbackUpdateJournalStatusResponse();
+ }
+
+ return new CallbackUpdateJournalStatusResponse();
+ }
+
+ private static JournalPagePrintDto BuildFailResponse(JournalPagePrintDto request, string[]? pageNo = null)
+ {
+ return new JournalPagePrintDto
+ {
+ JournalId = request.JournalId,
+ PageNo = pageNo ?? [],
+ Status = JournalStatusEnum.CodeFail
+ };
+ }
+
+ private void TryDeleteFile(string filePath)
+ {
+ try
+ {
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "删除临时文件失败,路径:{FilePath}", filePath);
+ }
+ }
+}
+
+internal sealed record PrintToolResult(string Output, string Error, int ExitCode, bool Timeout);
+
+internal class CallbackUpdateJournalStatusResponse
+{
+ public string? Message { get; set; }
+
+ public string? Code { get; set; }
+
+ public bool Result { get; set; }
+
+ public bool IsSuccess { get; set; }
+}
diff --git a/QYZH.InteractiveMagazine.PrintWorker/Consumers/IQueueConsumer.cs b/QYZH.InteractiveMagazine.PrintWorker/Consumers/IQueueConsumer.cs
new file mode 100644
index 0000000..8aa709d
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/Consumers/IQueueConsumer.cs
@@ -0,0 +1,32 @@
+namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
+
+///
+/// 队列消费者接口。
+///
+public interface IQueueConsumer
+{
+ ///
+ /// 交换机名称。
+ ///
+ string Exchange { get; }
+
+ ///
+ /// 队列名称。
+ ///
+ string QueueName { get; }
+
+ ///
+ /// 路由键。
+ ///
+ string RoutingKey { get; }
+
+ ///
+ /// 处理消息。
+ ///
+ Task HandleAsync(byte[] message, CancellationToken cancellationToken = default);
+
+ ///
+ /// 处理消费异常。
+ ///
+ Task OnErrorAsync(byte[] message, Exception exception);
+}
diff --git a/QYZH.InteractiveMagazine.PrintWorker/Consumers/RabbitMQHostedService.cs b/QYZH.InteractiveMagazine.PrintWorker/Consumers/RabbitMQHostedService.cs
new file mode 100644
index 0000000..721dbdc
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/Consumers/RabbitMQHostedService.cs
@@ -0,0 +1,105 @@
+using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
+
+namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
+
+///
+/// RabbitMQ 消费者后台服务。
+///
+public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger logger) : BackgroundService
+{
+ ///
+ /// 启动所有已注册队列消费者。
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ using var scope = serviceProvider.CreateScope();
+ var consumers = scope.ServiceProvider.GetServices().ToList();
+
+ if (consumers.Count == 0)
+ {
+ logger.LogWarning("未注册任何队列消费者");
+ return;
+ }
+
+ var tasks = consumers.Select(c => StartConsumerAsync(c, stoppingToken)).ToList();
+ await Task.WhenAll(tasks);
+ }
+
+ private async Task StartConsumerAsync(IQueueConsumer queueConsumer, CancellationToken stoppingToken)
+ {
+ 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(exchange, queueName, routingKey, async (channel, ea) =>
+ {
+ 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);
+
+ using var messageScope = serviceProvider.CreateScope();
+ var consumer = messageScope.ServiceProvider
+ .GetServices()
+ .First(c => c.QueueName == queueName);
+
+ try
+ {
+ await consumer.HandleAsync(ea.Body.ToArray(), stoppingToken);
+ await channel.BasicAckAsync(ea.DeliveryTag, false, stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "消费者 {QueueName} 处理消息异常", queueName);
+ var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, ea.Body.ToArray(), stoppingToken);
+ await channel.BasicNackAsync(ea.DeliveryTag, false, !dlqSent, stoppingToken);
+
+ try
+ {
+ await consumer.OnErrorAsync(ea.Body.ToArray(), ex);
+ }
+ catch (Exception errorEx)
+ {
+ logger.LogError(errorEx, "消费者 {QueueName} OnErrorAsync 执行异常", queueName);
+ }
+ }
+ }, stoppingToken);
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("消费者 {QueueName} 已停止", queueName);
+ }
+ catch (Exception ex)
+ {
+ 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.PrintWorker/PrintToolV2.7/PrintTool.exe
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/PrintTool.exe
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/PrintTool.exe
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1714.0.0.0_10.xml
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1761.172.8.16_1000.xml
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.211.21.48_10000.xml b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1761.211.21.48_10000.xml
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/sublic_license_1761.211.21.48_10000.xml
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/sublic_license_1761.211.21.48_10000.xml
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdlicense.dll b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/wfdlicense.dll
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdlicense.dll
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/wfdlicense.dll
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdprint.dll b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/wfdprint.dll
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/wfdprint.dll
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/wfdprint.dll
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/命令行方式调用方法.txt b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/命令行方式调用方法.txt
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/命令行方式调用方法.txt
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/命令行方式调用方法.txt
diff --git a/QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/返回值说明.txt b/QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/返回值说明.txt
similarity index 100%
rename from QYZH.InteractiveMagazine.WorkService/PrintToolV2.7/返回值说明.txt
rename to QYZH.InteractiveMagazine.PrintWorker/PrintToolV2.7/返回值说明.txt
diff --git a/QYZH.InteractiveMagazine.PrintWorker/Program.cs b/QYZH.InteractiveMagazine.PrintWorker/Program.cs
new file mode 100644
index 0000000..0694b35
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/Program.cs
@@ -0,0 +1,59 @@
+using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
+using QYZH.InteractiveMagazine.Infrastructure.Redis;
+using QYZH.InteractiveMagazine.Infrastructure.SDK;
+using QYZH.InteractiveMagazine.PrintWorker.Consumers;
+using Serilog;
+using SqlSugar;
+using SqlSugar.IOC;
+using Yitter.IdGenerator;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Configuration
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
+ .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
+
+Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .Enrich.FromLogContext()
+ .CreateLogger();
+
+builder.Services.AddSerilog();
+builder.Services.AddWindowsService(options => options.ServiceName = "QYZH InteractiveMagazine PrintWorker");
+
+YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 3 });
+
+builder.Services.AddSqlSugar(new IocConfig
+{
+ ConfigId = 0,
+ DbType = IocDbType.MySql,
+ ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"),
+ IsAutoCloseConnection = true
+});
+
+SugarIocServices.ConfigurationSugar(db =>
+{
+ db.Aop.OnLogExecuting = (sql, pars) =>
+ {
+ Log.Information("[SQL] {Sql}", UtilMethods.GetSqlString((DbType)IocDbType.MySql, sql, pars));
+ };
+ db.Aop.OnError = ex =>
+ {
+ Log.Error(ex, "[SQL Error] {Message}", ex.Message);
+ };
+});
+
+builder.Services.AddScoped(_ => DbScoped.SugarScope);
+builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
+builder.Services.AddHttpClient();
+builder.Services.AddSDKService(builder.Configuration);
+builder.Services.AddRabbitMQ(builder.Configuration);
+
+builder.Services.AddScoped();
+builder.Services.AddHostedService();
+
+var app = builder.Build();
+
+Log.Information("PrintWorker 已启动,仅消费自动铺码队列");
+
+await app.RunAsync();
diff --git a/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj b/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj
new file mode 100644
index 0000000..741c9bd
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/QYZH.InteractiveMagazine.PrintWorker/appsettings.json b/QYZH.InteractiveMagazine.PrintWorker/appsettings.json
new file mode 100644
index 0000000..f571c74
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/appsettings.json
@@ -0,0 +1,85 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "server=192.168.20.150;port=13306;database=InteractiveMagazine;user=user;password=n68792bu!y99r905;charset=utf8mb4;"
+ },
+ "RedisSettings": {
+ "ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
+ "Sentinels": [],
+ "ExpireSecondRange": [ 3600, 7200 ]
+ },
+ "RabbitMq": {
+ "HostName": "192.168.20.150",
+ "Port": 5672,
+ "UserName": "smartschool",
+ "Password": "@ss%&*otz%d*pq2S",
+ "VirtualHost": "InteractiveMagazine"
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning",
+ "Hangfire": "Information"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console"
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "logs/worker-log-.txt",
+ "rollingInterval": "Day"
+ }
+ }
+ ]
+ },
+ "HangfireJobs": {
+ "_说明": "定时任务配置。新增任务只需在 Jobs 数组中追加一项。Cron格式:分 时 日 月 星期(5位)。常用:* * * * * 每分钟 | */5 * * * * 每5分钟 | 0 * * * * 每小时 | 0 9 * * * 每天9点 | 0 0 * * 1 每周一午夜",
+ "Jobs": [
+ {
+ "Name": "sample-job",
+ "JobType": "QYZH.InteractiveMagazine.WorkService.Jobs.SampleJob",
+ "MethodName": "ExecuteAsync",
+ "Cron": "* * * * *",
+ "Enabled": false,
+ "Description": "示例任务(默认关闭,仅用于验证框架运行)"
+ }
+ ]
+ },
+ "AiChat": {
+ "ApiKey": "Ollama",
+ "BaseUrl": "http://172.16.10.130:11434/v1/",
+ "Model": "qwen2.5vl:7b",
+ "TimeoutSeconds": 300,
+ "MaxTokens": 2000,
+ "Temperature": 0.5,
+ "ScoreMaxRetryCount": 3,
+ "ScoreRetryDelayMilliseconds": 1000,
+ "MaxImageBytes": 10485760
+ },
+ "AllowedHosts": "*",
+ "AliyunOSSConfigs": {
+ "AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
+ "AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf",
+ "VodBucketName": "outin-5277bbb52bec11f08dbd00163e169e2b.oss-cn-beijing.aliyuncs.com",
+ "BucketName": "qyzh2025test",
+ "Region": "beijing",
+ "RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole",
+ "DurationSeconds": 3600, //过期时间(秒)
+ "Endpoint": "oss-cn-beijing.aliyuncs.com",
+ "ProjectName": "InteractiveMagazine",
+ "Domain": "http://oss-test.qyzhjy.com/"
+ },
+ "PrintConfig": {
+ "TimeoutSeconds": 300,
+ "DPrint": 0, //打印场景,0:普通激光打印机,1:工业印刷,默认为0(可选)
+ "CallBackApiUrl": "http://localhost:8080/api/page/callbackupdatepageno", // 开发环境 打印成功回调API地址修改打印状态
+ //"CallBackApiUrl": "http://192.168.20.150:8000/api/icr/page/callbackupdatepageno", // 测试环境 打印成功回调API地址修改打印状态
+ //"CallBackApiUrl": "https://zyb.qyzhjy.com/zybback/api/icr/page/callbackupdatepageno", // 正式环境 打印成功回调API地址修改打印状态
+ //这个ID执行的xml文件是:sublic_license_1761.172.8.16_1000.xml,如果想执行sublic_license_1761.211.21.48_10000.xml 换成 765837655859269 ---暂时没有导入页码
+ "DotId": 765837655859269
+ }
+}
diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs
deleted file mode 100644
index 9e8024f..0000000
--- a/QYZH.InteractiveMagazine.WorkService/Consumers/AutoDotCodeConsumer.cs
+++ /dev/null
@@ -1,355 +0,0 @@
-using Newtonsoft.Json;
-using QYZH.InteractiveMagazine.Common.Extensions;
-using QYZH.InteractiveMagazine.Infrastructure.OSS;
-using QYZH.InteractiveMagazine.Models.Dto.Journal;
-using QYZH.InteractiveMagazine.Models.Entity;
-using QYZH.InteractiveMagazine.Models.Enum;
-using SqlSugar;
-using System.Diagnostics;
-using System.Net;
-using System.Text;
-using System.Threading.Channels;
-
-namespace QYZH.InteractiveMagazine.WorkService.Consumers;
-
-///
-/// 自动铺码消费者
-///
-public class AutoDotCodeConsumer(IConfiguration configuration,
- IServiceScopeFactory scopeFactory,
- IWebHostEnvironment webHostEnvironment,
- ILogger logger,
- IHttpClientFactory httpClientFactory,
- OssService ossService
- ) : IQueueConsumer
-{
-
- public string Exchange => "ex.journal";
-
- public string QueueName => "mq.journal.dotcode.auto";
-
- public string RoutingKey => "rk.journal.dotcode.auto";
-
- public async Task HandleAsync(byte[] body, CancellationToken cancellationToken = default)
- {
- var message = Encoding.UTF8.GetString(body);
- logger.LogInformation("收到自动铺码消息: {Message}", message);
-
- // TODO: 在此编写具体的铺码处理逻辑
- using var scope = scopeFactory.CreateScope();
- var dBContext = scope.ServiceProvider.GetRequiredService();
-
- try
- {
- var journalPagePrintDtoMessage = JsonConvert.DeserializeObject(message);
- if (journalPagePrintDtoMessage == null)
- return;
-
- await dBContext.Ado.BeginTranAsync();
-
- #region 创建文件夹以及下载PDF文件
-
- var currentDomainDic = AppDomain.CurrentDomain.BaseDirectory + "JournalPagePdf";
- if (!Directory.Exists(currentDomainDic))
- Directory.CreateDirectory(currentDomainDic);
-
- var uploadPdfDic = currentDomainDic + "/upload/";
- if (!Directory.Exists(uploadPdfDic))
- Directory.CreateDirectory(uploadPdfDic);
-
- var downloadDic = currentDomainDic + "/download/";
- if (!Directory.Exists(downloadDic))
- Directory.CreateDirectory(downloadDic);
-
- var uploadFileName = $"upload_{journalPagePrintDtoMessage.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}.pdf";
-
- var uploadFilePath = uploadPdfDic + uploadFileName;
-
- // 获取上传成功的书籍页码pdf文件
- var journalPdfKey = journalPagePrintDtoMessage.JournalPdfUrl?.RemoveDomain();
- if (string.IsNullOrWhiteSpace(journalPdfKey))
- {
- logger.LogError("书籍上传的PDF文件地址为空,JournalId: {JournalId}", journalPagePrintDtoMessage.JournalId);
- return;
- }
-
- await using var pdfSteam = ossService.GetObjectStream(journalPdfKey);
- if (pdfSteam == null)
- {
- logger.LogError("获取书籍上传的PDF文件失败,OSS Key: {OssKey}", journalPdfKey);
- return;
- }
-
- await using (var fs = new FileStream(uploadFilePath, FileMode.CreateNew, FileAccess.Write))
- {
- await pdfSteam.CopyToAsync(fs, cancellationToken);
- logger.LogInformation($" 获取书籍上传的PDF文件成功下载到本地,长度为:{fs.Length}");
- }
-
- #endregion
-
-
- #region 调用铺码程序
-
- // 从配置中获取点阵文件Id
- var dotId = configuration.GetValue("PrintConfig:DotId", 765837655859269);
-
- var dotfile = await dBContext.Queryable().FirstAsync(x => x.Id == dotId, cancellationToken);
- if (dotfile == null)
- {
- logger.LogError($"配置打印数据错误,点阵文件不存在,dotId: {dotId}");
- return;
- }
-
- var printToolDirectory = Path.Combine(webHostEnvironment.ContentRootPath, "PrintToolV2.7");
- var exePath = Path.Combine(printToolDirectory, "PrintTool.exe");
-
- var xmlPath = Path.Combine(printToolDirectory, dotfile.FileName);
-
-
- #region 注释说明
-
- //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
-
- #endregion
-
- var downloadFileName = $"download_{journalPagePrintDtoMessage.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}_dot.pdf";
-
- // 生成成功的PDF文件路径
- var downloadFilePath = downloadDic + downloadFileName;
-
- // -dPrint = 打印场景,0:普通激光打印机,1:工业印刷,默认为0(可选)
- var dPrint = configuration.GetValue("PrintConfig:DPrint", 0);
-
- // 获取书籍页码中的最大页数,作为铺码程序需要铺的页数(连续数量)
- var pageNumMax = journalPagePrintDtoMessage.PageNum.Max(x => x);
-
- // 根据点阵文件Id获取对应的页码详情列表,按照Id升序排序,取前N条(N为书籍页数)
- var dotFileDetailList = await dBContext.Queryable().Where(x => x.DotId == dotId && !x.IsUse).OrderBy(x => x.Id).Take(pageNumMax).ToListAsync(cancellationToken);
-
- // 获取页码详情列表中的页地址,组成一个数组
- var dotFileDetailPageName = dotFileDetailList.Select(x => x.PageName).OrderBy(x => x).ToArray();
-
- // -dControlPageNum ={ [页地址, 连续数量],[页地址, 连续数量]...}
- // string pageStr = "{" + string.Join(",", item.Pages.Select(s => $"[{s.PageAddress},{s.PageNum}]")) + "}";
-
- // 从第一个开始执行,连续铺码N条(N为书籍页数)
- string pageStr = "{" + $"[{dotFileDetailList[0].PageName},{pageNumMax}]" + "}";
-
- var arguments = $"-sMode=Generate -sPDF=\"{uploadFilePath}\" -sLIC=\"{xmlPath}\" -pStart=1 -oPDF=\"{downloadFilePath}\" -dPageAddr=1 -dPrint={dPrint} -dDotSize=40 -dType=0 -dOutFile=0 -dControlPageNum={pageStr}";
- var cmd = $"\"{exePath}\" {arguments}";
-
- logger.LogInformation($"执行PrintTool.exe 的命令: {cmd}");
-
- string output;
- using (var p = new Process())
- {
- p.StartInfo = new ProcessStartInfo
- {
- WorkingDirectory = printToolDirectory,
- FileName = exePath,
- Arguments = arguments,
- UseShellExecute = false, //是否使用操作系统shell启动
- RedirectStandardInput = true, //接受来自调用程序的输入信息
- RedirectStandardOutput = true, //由调用程序获取输出信息
- RedirectStandardError = true, //重定向标准错误输出
- CreateNoWindow = true, //不显示程序窗口
- };
-
- p.Start();
- output = await p.StandardOutput.ReadToEndAsync(cancellationToken);
-
- logger.LogInformation("执行 ProcessStartInfo 执行命令后,output 输出值:{Output}", output);
-
- var exeErrorMsg = await p.StandardError.ReadToEndAsync(cancellationToken);
- if (!string.IsNullOrWhiteSpace(exeErrorMsg))
- {
- logger.LogInformation($"执行 ProcessStartInfo 执行命令后,返回的错误信息为:{exeErrorMsg}");
- }
-
- await p.WaitForExitAsync(cancellationToken);
- p.Kill();
- }
- logger.LogInformation("执行 PrintTool.exe 文件成功");
-
- #endregion
-
- var journalPagePrintDtoModel = new JournalPagePrintDto
- {
- JournalId = journalPagePrintDtoMessage.JournalId,
- PageNo = dotFileDetailPageName,
- };
-
- #region 根据配置的dPrint 验证铺码程序执行的结果 output数据
-
- if (string.IsNullOrWhiteSpace(output))
- {
- logger.LogInformation("执行铺码程序没有任何输出,请联系管理员");
- journalPagePrintDtoModel.Status = JournalStatusEnum.CodeFail;
- await ExecuteUpdateJournalStatus(journalPagePrintDtoModel, dBContext);
- return;
- }
-
- // 如果是激光打印场景,output肯定和 dotFileDetailPageName 是一一对应的关系,如果是工业印刷场景,output可能会有其他信息,所以需要验证output中是否包含dotFileDetailPageName中的页码
- if (dPrint == 0)
- {
- var pageNoList = JsonConvert.DeserializeObject(output) ?? [];
- var isequalArray = pageNoList.OrderBy(x => x).SequenceEqual(dotFileDetailPageName.OrderBy(x => x), StringComparer.Ordinal);
- if (!isequalArray)
- {
- logger.LogInformation("执行铺码程序输出的页码与期望的页码不一致,请联系管理员");
- journalPagePrintDtoModel.Status = JournalStatusEnum.CodeFail;
- await ExecuteUpdateJournalStatus(journalPagePrintDtoModel, dBContext);
- return;
- }
-
- }
- else if (dPrint == 1)// 如果是工业印刷场景,验证页码是否打印过,如果打印过则直接使用原来的页码,不再执行铺码程序
- {
- // 并行验证页码是否存在于铺码程序的输出中,存在则说明打印过,不存在则说明没有打印过,说明铺码程序没有执行成功
- var existPageNo = dotFileDetailPageName.AsParallel().Any(ip => output.Contains(ip, StringComparison.Ordinal));
- if (!existPageNo)
- {
- logger.LogInformation("执行铺码程序没有任何输出,请联系管理员");
- journalPagePrintDtoModel.Status = JournalStatusEnum.CodeFail;
- await ExecuteUpdateJournalStatus(journalPagePrintDtoModel, dBContext);
- return;
- }
- }
-
- #endregion
-
- #region 把本地文集上传到OOS上
-
- var tempOssDownloadKey = "journal/download/" + downloadFileName;
-
- const int bufferSize = 1 * 1024 * 1024; // 1MB
-
- await using var downloadFs = new FileStream(downloadFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan | FileOptions.Asynchronous);
-
- //把本地文件上传到OOS上
- var downloadBookPagePdfName = ossService.PutObject(tempOssDownloadKey, downloadFs);
-
- var ossDomain = configuration.GetSection("AliyunOSSConfigs:Domain").Get() ?? string.Empty;
-
- journalPagePrintDtoModel.DownloadJournalPagePdfName = ossDomain + downloadBookPagePdfName;
-
- // 这里一定要释放上面的流,否则下面无法删除文件
- await downloadFs.DisposeAsync();
-
- #endregion
-
- ////删除临时文件
- File.Delete(uploadFilePath);
-
- File.Delete(downloadFilePath);
-
- #region 调用API,成功后修改页码状态为已使用
-
- journalPagePrintDtoModel.Status = JournalStatusEnum.CodeSuccess;
-
- // 调用回调接口修改书籍状态为铺码成功
- var callbackResponse = await ExecuteUpdateJournalStatus(journalPagePrintDtoModel, dBContext);
-
- if (callbackResponse != null && callbackResponse.IsSuccess)
- {
- logger.LogInformation($"回调接口成功修改书籍状态为铺码成功,书籍ID:{journalPagePrintDtoModel.JournalId}");
-
- foreach (var dotFileDetail in dotFileDetailList)
- {
- dotFileDetail.IsUse = true;
- dotFileDetail.UpdatedAt = DateTime.Now;
- }
-
- await dBContext.Updateable(dotFileDetailList).ExecuteCommandAsync();
-
- await dBContext.Updateable()
- .SetColumns(x => x.TotalUse == x.TotalUse + dotFileDetailList.Count)
- .SetColumns(x => x.UpdatedAt == DateTime.Now)
- .Where(x => x.Id == dotId).ExecuteCommandAsync();
-
- ////手动确认消息已处理(由于下方 autoAck 设为 false)
- //await channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, multiple: false, cancellationToken: cancellationToken);
- }
- else
- {
- logger.LogError($"回调接口修改书籍状态为铺码成功没有成功,书籍ID:{journalPagePrintDtoModel.JournalId},接口返回消息:{callbackResponse?.Message}");
- }
-
- #endregion
-
-
- await dBContext.Ado.CommitTranAsync();
-
- }
- catch (Exception ex)
- {
- await dBContext.Ado.RollbackTranAsync();
- logger.LogError(ex, "铺码错误,回调接口修改书籍状态为铺码失败没有成功");
- }
- await Task.CompletedTask;
- }
- private async Task ExecuteUpdateJournalStatus(JournalPagePrintDto request, ISqlSugarClient dBContext)
- {
- var client = httpClientFactory.CreateClient();
- var callbackUrl = configuration.GetValue("PrintConfig:CallBackApiUrl");
- var callbackRequest = new HttpRequestMessage(HttpMethod.Post, callbackUrl)
- {
- Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")
- };
- var response = await client.SendAsync(callbackRequest);
- if (response.StatusCode == HttpStatusCode.OK)
- {
- var responseContent = await response.Content.ReadAsStringAsync();
- logger.LogInformation($"回调接口成功修改书籍状态为铺码失败,接口返回内容:{responseContent}");
- return JsonConvert.DeserializeObject(responseContent) ?? new CallbackUpdateJournalStatusResponse();
- }
- return new CallbackUpdateJournalStatusResponse();
- }
- public Task OnErrorAsync(byte[] message, Exception exception)
- {
- var body = Encoding.UTF8.GetString(message);
- logger.LogError(exception, "处理自动铺码消息失败: {Message}", body);
- return Task.CompletedTask;
- }
-}
-internal class CallbackUpdateJournalStatusResponse
-{
- public string? Message { get; set; }
-
- public string? Code { get; set; }
-
- public bool Result { get; set; }
-
- public bool IsSuccess { get; set; }
-}
diff --git a/QYZH.InteractiveMagazine.WorkService/Dockerfile b/QYZH.InteractiveMagazine.WorkService/Dockerfile
index c4ca07a..b9b6927 100644
--- a/QYZH.InteractiveMagazine.WorkService/Dockerfile
+++ b/QYZH.InteractiveMagazine.WorkService/Dockerfile
@@ -1,16 +1,10 @@
-# 使用 ASP.NET Core 8.0 运行时基础镜像
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
-# 设置工作目录
WORKDIR /app
-# 将当前目录(发布文件夹)的所有内容复制到容器内的 /app 目录
COPY . .
-# 设置时区(可选)
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
-# 启动应用
-ENTRYPOINT ["dotnet", "QYZH.InteractiveMagazine.WorkService.dll"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "QYZH.InteractiveMagazine.WorkService.dll"]
diff --git a/QYZH.InteractiveMagazine.WorkService/Program.cs b/QYZH.InteractiveMagazine.WorkService/Program.cs
index bafdc3b..16ccff2 100644
--- a/QYZH.InteractiveMagazine.WorkService/Program.cs
+++ b/QYZH.InteractiveMagazine.WorkService/Program.cs
@@ -77,7 +77,6 @@ builder.Services.AddRabbitMQ(builder.Configuration);
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
builder.Services.AddScoped();
-builder.Services.AddScoped();
// 注册消费者后台服务
builder.Services.AddHostedService();
diff --git a/QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj b/QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj
index 0789df2..b931f2a 100644
--- a/QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj
+++ b/QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj
@@ -22,58 +22,4 @@
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
-
diff --git a/QYZH.InteractiveMagazine.slnx b/QYZH.InteractiveMagazine.slnx
index ca05cf1..99e2027 100644
--- a/QYZH.InteractiveMagazine.slnx
+++ b/QYZH.InteractiveMagazine.slnx
@@ -3,6 +3,7 @@
+