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; } }