Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
glz de9a0c2978 refactor(work-service,print-worker): 优化代码结构与错误处理
1. 提取评分prompt构建逻辑减少重复代码
2. 移除冗余的参考答案图片循环逻辑
3. 重构打印服务的错误回调逻辑,统一失败处理流程
4. 新增打印服务的任务取消处理逻辑
2026-07-08 17:07:41 +08:00

370 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 自动铺码消费者。
/// </summary>
public class AutoDotCodeConsumer(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
IHostEnvironment hostEnvironment,
ILogger<AutoDotCodeConsumer> logger,
IHttpClientFactory httpClientFactory,
OssService ossService) : IQueueConsumer
{
/// <summary>
/// 交换机名称。
/// </summary>
public string Exchange => "ex.journal";
/// <summary>
/// 队列名称。
/// </summary>
public string QueueName => "mq.journal.dotcode.auto";
/// <summary>
/// 路由键。
/// </summary>
public string RoutingKey => "rk.journal.dotcode.auto";
/// <summary>
/// 处理自动铺码消息。
/// </summary>
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<ISqlSugarClient>();
JournalPagePrintDto? request = null;
try
{
request = JsonSerializer.Deserialize<JournalPagePrintDto>(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);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
await using var pdfStream = ossService.GetObjectStream(journalPdfKey);
if (pdfStream == null)
{
logger.LogError("获取书籍上传 PDF 文件失败OSS Key: {OssKey}", journalPdfKey);
await TryCallbackCodeFailAsync(request, cancellationToken);
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<DotFile>().FirstAsync(x => x.Id == dotId, cancellationToken);
if (dotFile == null)
{
logger.LogError("配置打印数据错误点阵文件不存在DotId: {DotId}", dotId);
await TryCallbackCodeFailAsync(request, cancellationToken);
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 TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
var xmlPath = Path.Combine(printToolDirectory, dotFile.FileName);
if (!File.Exists(xmlPath))
{
logger.LogError("铺码授权文件不存在,路径:{XmlPath}", xmlPath);
await TryCallbackCodeFailAsync(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<DotFileDetail>()
.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 TryCallbackCodeFailAsync(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 TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
var statusModel = new JournalPagePrintDto
{
JournalId = request.JournalId,
PageNo = dotFileDetailPageName
};
if (!ValidatePrintOutput(printResult.Output, dotFileDetailPageName, dPrint))
{
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
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>() ?? 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<DotFile>()
.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 (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogWarning("自动铺码处理已取消JournalId: {JournalId}", request?.JournalId);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "自动铺码处理失败");
if (request != null)
{
await TryCallbackCodeFailAsync(request, CancellationToken.None);
}
}
}
/// <summary>
/// 处理消费异常。
/// </summary>
public Task OnErrorAsync(byte[] message, Exception exception)
{
var body = Encoding.UTF8.GetString(message);
logger.LogError(exception, "处理自动铺码消息失败: {Message}", body);
return Task.CompletedTask;
}
private async Task<PrintToolResult> 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<string[]>(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<CallbackUpdateJournalStatusResponse> ExecuteUpdateJournalStatusAsync(JournalPagePrintDto request, CancellationToken cancellationToken)
{
var client = httpClientFactory.CreateClient();
var callbackUrl = configuration.GetValue<string>("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<CallbackUpdateJournalStatusResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new CallbackUpdateJournalStatusResponse();
}
return new CallbackUpdateJournalStatusResponse();
}
private async Task TryCallbackCodeFailAsync(JournalPagePrintDto request, CancellationToken cancellationToken, string[]? pageNo = null)
{
try
{
var callbackResponse = await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request, pageNo), cancellationToken);
if (!callbackResponse.IsSuccess)
{
logger.LogError("回调接口修改书籍为铺码失败状态失败JournalId: {JournalId},接口返回消息:{Message}", request.JournalId, callbackResponse.Message);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception callbackEx)
{
logger.LogError(callbackEx, "铺码失败后请求回调接口失败JournalId: {JournalId}", request.JournalId);
}
}
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 object? Code { get; set; }
public bool Result { get; set; }
public bool IsSuccess { get; set; }
}