@ -1,18 +1,26 @@
using Newtonsoft.Json ;
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 ;
/// <summary>
/// 自动铺码消费者
/// </summary>
public class AutoDotCodeConsumer : IQueueConsumer
public class AutoDotCodeConsumer ( IConfiguration configuration ,
IServiceScopeFactory scopeFactory ,
ILogger < AutoDotCodeConsumer > logger ,
IHttpClientFactory httpClientFactory ,
OssService ossService
) : IQueueConsumer
{
private readonly ILogger < AutoDotCodeConsumer > _logger ;
public AutoDotCodeConsumer ( ILogger < AutoDotCodeConsumer > logger )
{
_logger = logger ;
}
public string Exchange = > "ex.journal" ;
@ -20,20 +28,312 @@ public class AutoDotCodeConsumer : IQueueConsumer
public string RoutingKey = > "rk.journal.dotcode.auto" ;
public async Task HandleAsync ( byte [ ] message )
public async Task HandleAsync ( byte [ ] body , CancellationToken cancellationToken = default )
{
var body = Encoding . UTF8 . GetString ( message ) ;
_ logger. LogInformation ( "收到自动铺码消息: {Message}" , body ) ;
var message = Encoding . UTF8 . GetString ( body ) ;
logger . LogInformation ( "收到自动铺码消息: {Message}" , message ) ;
// TODO: 在此编写具体的铺码处理逻辑
using var scope = scopeFactory . CreateScope ( ) ;
var dBContext = scope . ServiceProvider . GetRequiredService < ISqlSugarClient > ( ) ;
try
{
var journalPagePrintDtoMessage = JsonConvert . DeserializeObject < JournalPagePrintDto > ( 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 pdfSteam = await httpClientFactory . CreateClient ( ) . GetStreamAsync ( journalPagePrintDtoMessage . JournalPdfUrl , cancellationToken ) ;
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" , 683790963662917 ) ;
var dotfile = await dBContext . Queryable < DotFile > ( ) . FirstAsync ( x = > x . Id = = dotId , cancellationToken ) ;
if ( dotfile = = null )
{
logger . LogError ( $"配置打印数据错误, 点阵文件不存在, dotId: {dotId}" ) ;
return ;
}
var exePath = AppDomain . CurrentDomain . BaseDirectory + "PrintToolV2.7\\PrintTool.exe" ;
var xmlPath = AppDomain . CurrentDomain . BaseDirectory + $"PrintToolV2.7\\{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 < DotFileDetail > ( ) . 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 cmd = $"PrintTool.exe -sMode=Generate -sPDF={uploadFilePath} -sLIC={xmlPath} -pStart=1 -oPDF={downloadFilePath} -dPageAddr=1 -dPrint={dPrint} -dDotSize=40 -dType=0 -dOutFile=0 -dControlPageNum={pageStr}" ;
logger . LogInformation ( $"执行PrintTool.exe 的命令: {cmd}" ) ;
string output ;
using ( var p = new Process ( ) )
{
p . StartInfo = new ProcessStartInfo
{
WorkingDirectory = AppDomain . CurrentDomain . BaseDirectory + "PrintToolV2.7" ,
FileName = "cmd.exe" ,
Arguments = "/c " + cmd , // /c参数表示执行后关闭
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 < string [ ] > ( 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 = "BookPagePdf/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 > ( ) ? ? 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 < DotFile > ( )
. 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 < CallbackUpdateJournalStatusResponse > ExecuteUpdateJournalStatus ( JournalPagePrintDto request , ISqlSugarClient dBContext )
{
var client = httpClientFactory . CreateClient ( ) ;
var callbackUrl = configuration . GetValue < string > ( "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 < CallbackUpdateJournalStatusResponse > ( responseContent ) ? ? new CallbackUpdateJournalStatusResponse ( ) ;
}
return new CallbackUpdateJournalStatusResponse ( ) ;
}
public Task OnErrorAsync ( byte [ ] message , Exception exception )
{
var body = Encoding . UTF8 . GetString ( message ) ;
_ logger. LogError ( exception , "处理自动铺码消息失败: {Message}" , body ) ;
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 ; }
}