refactor(work-service): 重构自动铺码消费者,新增微信登录逻辑与配置更新

1. 更新微信公众号配置AppId和AppSecret
2. 修复微信授权控制器临时返回逻辑,启用真实登录流程
3. 新增期刊打印DTO类,定义铺码数据结构
4. 重构AutoDotCodeConsumer,新增依赖注入与完整铺码业务逻辑
5. 调整队列消费者接口与实现,添加取消令牌参数
6. 新增铺码回调响应实体类,完善异常处理与事务管理
This commit is contained in:
glz
2026-06-23 16:53:18 +08:00
parent 2d3fceb265
commit 16ad80f2a8
7 changed files with 361 additions and 20 deletions

View File

@ -0,0 +1,42 @@
using QYZH.InteractiveMagazine.Models.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalPagePrintDto
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 状态
/// </summary>
public JournalStatusEnum? Status { get; set; }
/// <summary>
/// 书PDF URL这里带有 OSS 域名 https://oss.qyzhjy.com/
/// </summary>
public string? JournalPdfUrl { get; set; }
/// <summary>
/// 页码数组
/// </summary>
public int[] PageNum { get; set; } = [];
/// <summary>
/// 点阵码打印页面数组
/// </summary>
public string[] PageNo { get; set; } = [];
/// <summary>
/// 下载书页PDF的文件名铺码程序已经添加好这里带有 OSS 域名 https://oss.qyzhjy.com/
/// </summary>
public string? DownloadJournalPagePdfName { get; set; }
}
}

View File

@ -33,9 +33,8 @@ public class WeChatAuthController : WeChatBaseController
{
try
{
//var result = await _weChatAuthService.LoginAsync(input);
//return Success(result);
return BaseResponse<WeChatLoginOutput>.Fail("微信登录失败,请稍后重试");
var result = await _weChatAuthService.LoginAsync(input);
return Success(result);
}
catch (BusinessException ex)
{

View File

@ -22,8 +22,8 @@
"ClientProvidedName": "Custom connection name"
},
"WeChatSettings": {
"AppId": "wx7922cc9b6023f3ac",
"AppSecret": "95a1d61b7be03d1dab81fe19b7483dcf"
"AppId": "wx5d8f281c2fd6594c",
"AppSecret": "974f45e4a0aaa075278db0477d0bd8b6"
},
"Serilog": {
"MinimumLevel": {

View File

@ -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>();
await Task.CompletedTask;
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; }
}

View File

@ -23,7 +23,7 @@ public interface IQueueConsumer
/// <summary>
/// 处理消息
/// </summary>
Task HandleAsync(byte[] message);
Task HandleAsync(byte[] message, CancellationToken cancellationToken = default);
/// <summary>
/// 处理消费异常

View File

@ -20,7 +20,7 @@ public class JournalTaskReceiveConsumer : IQueueConsumer
public string RoutingKey => "rk.journal.task.receive";
public async Task HandleAsync(byte[] message)
public async Task HandleAsync(byte[] message, CancellationToken cancellationToken = default)
{
var body = Encoding.UTF8.GetString(message);
_logger.LogInformation("收到期刊任务消息: {Message}", body);

View File

@ -60,7 +60,7 @@ public class RabbitMQHostedService : BackgroundService
try
{
await consumer.HandleAsync(ea.Body.ToArray());
await consumer.HandleAsync(ea.Body.ToArray(), stoppingToken);
await channel.BasicAckAsync(ea.DeliveryTag, false, stoppingToken);
}
catch (Exception ex)