临时数据提交
This commit is contained in:
@ -49,6 +49,8 @@ public interface IJournalService : IBaseService<Journal>
|
|||||||
|
|
||||||
Task<bool> StatusAsync(long id, JournalStatusEnum status);
|
Task<bool> StatusAsync(long id, JournalStatusEnum status);
|
||||||
|
|
||||||
|
Task<bool> PublishAsync(long id);
|
||||||
|
|
||||||
Task<DotMatrixOutput> PrintCodeAsync(long id);
|
Task<DotMatrixOutput> PrintCodeAsync(long id);
|
||||||
|
|
||||||
Task<bool> ResultReportAsync(DotMatrixNoteJournalReportInput input);
|
Task<bool> ResultReportAsync(DotMatrixNoteJournalReportInput input);
|
||||||
|
|||||||
@ -120,8 +120,33 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DownloadJournalPagePdfName { get; set; }
|
public string? DownloadJournalPagePdfName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布开始时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布结束时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public string CreatedBy { get; set; }
|
public string CreatedBy { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class JournalPublishBookMessage
|
||||||
|
{
|
||||||
|
public long BookId { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JournalPublishBookPageMessage
|
||||||
|
{
|
||||||
|
public long BookId { get; set; }
|
||||||
|
public long PageId { get; set; }
|
||||||
|
public string? PageNo { get; set; }
|
||||||
|
public string? Layout { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,16 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PdfPreviewUrl { get; set; }
|
public string? PdfPreviewUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:发布开始时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:发布结束时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public partial class JournalEditDto: JournalAddDto
|
public partial class JournalEditDto: JournalAddDto
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,5 +29,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TaskBankTypeEnum Type { get; set; }
|
public TaskBankTypeEnum Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否需要AI批改,默认需要
|
||||||
|
/// </summary>
|
||||||
|
public bool NeedAiProcess { get; set; } = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
|||||||
/// Prompt配置
|
/// Prompt配置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Prompt { get; set; }
|
public string Prompt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否需要AI批改
|
||||||
|
/// </summary>
|
||||||
|
public bool NeedAiProcess { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -71,6 +71,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Prompt { get; set; }
|
public string Prompt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否需要AI批改,false时由人工批改
|
||||||
|
/// </summary>
|
||||||
|
public bool NeedAiProcess { get; set; } = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class JournalPageTaskComplementInput
|
public class JournalPageTaskComplementInput
|
||||||
|
|||||||
@ -106,6 +106,20 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string DownloadJournalPagePdfName { get; set; }
|
public string DownloadJournalPagePdfName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:发布日期开始时间
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:发布日期结束时间
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:书籍描述/简介
|
/// Desc:书籍描述/简介
|
||||||
|
|||||||
@ -3,9 +3,9 @@ using SqlSugar;
|
|||||||
|
|
||||||
namespace QYZH.InteractiveMagazine.Models.Entity
|
namespace QYZH.InteractiveMagazine.Models.Entity
|
||||||
{
|
{
|
||||||
///<summary>
|
/// <summary>
|
||||||
///杂志任务用户作答记录;
|
/// 杂志任务用户作答记录
|
||||||
///</summary>
|
/// </summary>
|
||||||
[SugarTable("JournalPageTaskUserAnswer")]
|
[SugarTable("JournalPageTaskUserAnswer")]
|
||||||
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
|
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
|
||||||
{
|
{
|
||||||
@ -56,7 +56,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// Default:0
|
/// Default:0
|
||||||
/// Nullable:False
|
/// Nullable:False
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Points { get; set; }
|
public int Points { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:成长值
|
/// Desc:成长值
|
||||||
@ -72,6 +72,34 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float Score { get; set; }
|
public float Score { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:理解力评分
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public float Comprehension { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:判断力评分
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public float Judgment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:表达力评分
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public float Expression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:说服力评分
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public float Persuasiveness { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:题目作答图片地址
|
/// Desc:题目作答图片地址
|
||||||
/// Default:
|
/// Default:
|
||||||
@ -211,6 +239,5 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// Nullable:True
|
/// Nullable:True
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AssignmentStatus { get; set; }
|
public string AssignmentStatus { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Points awarded.
|
/// Points awarded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Points { get; set; }
|
public int Points { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Growth points awarded.
|
/// Growth points awarded.
|
||||||
@ -58,6 +58,26 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float Score { get; set; }
|
public float Score { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comprehension score.
|
||||||
|
/// </summary>
|
||||||
|
public float Comprehension { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Judgment score.
|
||||||
|
/// </summary>
|
||||||
|
public float Judgment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expression score.
|
||||||
|
/// </summary>
|
||||||
|
public float Expression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persuasiveness score.
|
||||||
|
/// </summary>
|
||||||
|
public float Persuasiveness { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Question answer image URL.
|
/// Question answer image URL.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
/// <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>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var 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);
|
||||||
|
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<DotFile>().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<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 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>() ?? 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 (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "自动铺码处理失败");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列消费者接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface IQueueConsumer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 交换机名称。
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列名称。
|
||||||
|
/// </summary>
|
||||||
|
string QueueName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由键。
|
||||||
|
/// </summary>
|
||||||
|
string RoutingKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理消息。
|
||||||
|
/// </summary>
|
||||||
|
Task HandleAsync(byte[] message, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理消费异常。
|
||||||
|
/// </summary>
|
||||||
|
Task OnErrorAsync(byte[] message, Exception exception);
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RabbitMQ 消费者后台服务。
|
||||||
|
/// </summary>
|
||||||
|
public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<RabbitMQHostedService> logger) : BackgroundService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 启动所有已注册队列消费者。
|
||||||
|
/// </summary>
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var consumers = scope.ServiceProvider.GetServices<IQueueConsumer>().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<IRabbitMQService>();
|
||||||
|
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<IQueueConsumer>()
|
||||||
|
.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<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, byte[] body, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rabbitMQConnection = serviceProvider.GetRequiredService<IRabbitMQConnection>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
QYZH.InteractiveMagazine.PrintWorker/Program.cs
Normal file
59
QYZH.InteractiveMagazine.PrintWorker/Program.cs
Normal file
@ -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<ISqlSugarClient>(_ => 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<IQueueConsumer, AutoDotCodeConsumer>();
|
||||||
|
builder.Services.AddHostedService<RabbitMQHostedService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
Log.Information("PrintWorker 已启动,仅消费自动铺码队列");
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Infrastructure\QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.5.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
|
<PackageReference Include="SqlSugar.IOC" Version="2.0.1" />
|
||||||
|
<PackageReference Include="SqlSugarCoreNoDrive" Version="5.1.4.213" />
|
||||||
|
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Include="PrintToolV2.7\**\*">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
85
QYZH.InteractiveMagazine.PrintWorker/appsettings.json
Normal file
85
QYZH.InteractiveMagazine.PrintWorker/appsettings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
|||||||
var map = input.Adapt<JournalPageTask>();
|
var map = input.Adapt<JournalPageTask>();
|
||||||
map.Id = YitIdHelper.NextId();
|
map.Id = YitIdHelper.NextId();
|
||||||
map.GroupId = map.Id;
|
map.GroupId = map.Id;
|
||||||
|
map.NeedAiProcess = input.NeedAiProcess;
|
||||||
var no = input.No.Split('-').Select(int.Parse).ToArray();
|
var no = input.No.Split('-').Select(int.Parse).ToArray();
|
||||||
if (no.Last() >= 2)
|
if (no.Last() >= 2)
|
||||||
{
|
{
|
||||||
@ -59,7 +60,6 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
|||||||
|
|
||||||
task.Type = input.Type;
|
task.Type = input.Type;
|
||||||
task.Task = input.Task;
|
task.Task = input.Task;
|
||||||
task.No = input.No;
|
|
||||||
task.Points = input.Points;
|
task.Points = input.Points;
|
||||||
task.GrowthPoint = (int?)input.GrowthPoint;
|
task.GrowthPoint = (int?)input.GrowthPoint;
|
||||||
task.Comprehension = input.Comprehension;
|
task.Comprehension = input.Comprehension;
|
||||||
@ -68,6 +68,7 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
|||||||
task.Persuasiveness = input.Persuasiveness;
|
task.Persuasiveness = input.Persuasiveness;
|
||||||
task.AnswerTime = input.AnswerTime;
|
task.AnswerTime = input.AnswerTime;
|
||||||
task.Prompt = input.Prompt;
|
task.Prompt = input.Prompt;
|
||||||
|
task.NeedAiProcess = input.NeedAiProcess;
|
||||||
|
|
||||||
|
|
||||||
var no = input.No.Split('-').Select(int.Parse).ToArray();
|
var no = input.No.Split('-').Select(int.Parse).ToArray();
|
||||||
@ -104,7 +105,8 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
|||||||
Expression = task.Expression,
|
Expression = task.Expression,
|
||||||
Persuasiveness = task.Persuasiveness,
|
Persuasiveness = task.Persuasiveness,
|
||||||
AnswerTime = task.AnswerTime,
|
AnswerTime = task.AnswerTime,
|
||||||
Prompt = task.Prompt
|
Prompt = task.Prompt,
|
||||||
|
NeedAiProcess = task.NeedAiProcess
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查询答案列表
|
// 查询答案列表
|
||||||
|
|||||||
@ -21,8 +21,17 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
|||||||
BaseRepository<JournalPageTaskAnswer> JournalPageTaskAnswerRepository,
|
BaseRepository<JournalPageTaskAnswer> JournalPageTaskAnswerRepository,
|
||||||
BaseRepository<JournalCatalog> JournalCatalogRepository,
|
BaseRepository<JournalCatalog> JournalCatalogRepository,
|
||||||
BaseRepository<DotFile> dotFileRepository,
|
BaseRepository<DotFile> dotFileRepository,
|
||||||
BaseRepository<DotFileDetail> dotFileDetailRepository, OssService ossService, ILogger<JournalService> logger) : BaseRepository<Journal>, IJournalService
|
BaseRepository<DotFileDetail> dotFileDetailRepository,
|
||||||
|
OssService ossService,
|
||||||
|
IRabbitMQService rabbitMqService,
|
||||||
|
ILogger<JournalService> logger) : BaseRepository<Journal>, IJournalService
|
||||||
{
|
{
|
||||||
|
private const string JournalExchange = "ex.journal";
|
||||||
|
private const string PublishBookQueue = "mq.journal.publish.book";
|
||||||
|
private const string PublishBookRoutingKey = "rk.journal.publish.book";
|
||||||
|
private const string PublishBookPageQueue = "mq.journal.publish.bookpage";
|
||||||
|
private const string PublishBookPageRoutingKey = "rk.journal.publish.bookpage";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询List
|
/// 查询List
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -82,6 +91,8 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
|||||||
Cover = input.Cover?.RemoveDomain(),
|
Cover = input.Cover?.RemoveDomain(),
|
||||||
BackCover = input.BackCover?.RemoveDomain(),
|
BackCover = input.BackCover?.RemoveDomain(),
|
||||||
PdfPreviewUrl = input.PdfPreviewUrl?.RemoveDomain(),
|
PdfPreviewUrl = input.PdfPreviewUrl?.RemoveDomain(),
|
||||||
|
StartTime = input.StartTime,
|
||||||
|
EndTime = input.EndTime,
|
||||||
CreatedBy = "System",
|
CreatedBy = "System",
|
||||||
UpdatedBy = "System",
|
UpdatedBy = "System",
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
@ -167,6 +178,9 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
|||||||
Journal.Height = input.Height;
|
Journal.Height = input.Height;
|
||||||
Journal.Name = input.Name;
|
Journal.Name = input.Name;
|
||||||
Journal.Title = input.Title;
|
Journal.Title = input.Title;
|
||||||
|
Journal.StartTime = input.StartTime;
|
||||||
|
Journal.EndTime = input.EndTime;
|
||||||
|
Journal.UpdatedAt = DateTime.Now;
|
||||||
|
|
||||||
var res = await UseTranAsync(async () =>
|
var res = await UseTranAsync(async () =>
|
||||||
{
|
{
|
||||||
@ -320,4 +334,59 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
|||||||
var res = await base.Updateable().SetColumns(s => s.Status, status).Where(w => w.Id == id).ExecuteCommandAsync() > 0;
|
var res = await base.Updateable().SetColumns(s => s.Status, status).Where(w => w.Id == id).ExecuteCommandAsync() > 0;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PublishAsync(long id)
|
||||||
|
{
|
||||||
|
var book = await base.GetByIdAsync(id);
|
||||||
|
BusinessException.ThrowIf(book == null || book.IsDeleted, "杂志不存在", ResultCode.NOT_FOUND);
|
||||||
|
|
||||||
|
var pages = await JournalPageRepository.Queryable()
|
||||||
|
.Where(x => x.JournalId == id && !x.IsDeleted)
|
||||||
|
.OrderBy(x => x.PageNum)
|
||||||
|
.OrderBy(x => x.Sort)
|
||||||
|
.ToListAsync();
|
||||||
|
BusinessException.ThrowIf(pages.Count == 0, "书籍未添加任何书页,无法发布", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
|
|
||||||
|
BusinessException.ThrowIf(!book.StartTime.HasValue, "发布开始时间不能为空", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
|
BusinessException.ThrowIf(!book.EndTime.HasValue, "发布结束时间不能为空", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
|
BusinessException.ThrowIf(book.EndTime < book.StartTime, "发布结束时间不能早于开始时间", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
|
|
||||||
|
var bookMessageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
|
||||||
|
{
|
||||||
|
Exchange = JournalExchange,
|
||||||
|
Queue = PublishBookQueue,
|
||||||
|
RoutingKey = PublishBookRoutingKey,
|
||||||
|
Data = new JournalPublishBookMessage
|
||||||
|
{
|
||||||
|
BookId = id,
|
||||||
|
StartTime = book.StartTime.Value,
|
||||||
|
EndTime = book.EndTime.Value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BusinessException.ThrowIf(!bookMessageSent, "发布书籍消息发送失败", ResultCode.GLOBAL_ERROR);
|
||||||
|
|
||||||
|
foreach (var page in pages)
|
||||||
|
{
|
||||||
|
var pageMessageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
|
||||||
|
{
|
||||||
|
Exchange = JournalExchange,
|
||||||
|
Queue = PublishBookPageQueue,
|
||||||
|
RoutingKey = PublishBookPageRoutingKey,
|
||||||
|
Data = new JournalPublishBookPageMessage
|
||||||
|
{
|
||||||
|
BookId = id,
|
||||||
|
PageId = page.Id,
|
||||||
|
PageNo = page.PageNo,
|
||||||
|
Layout = page.Layout
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BusinessException.ThrowIf(!pageMessageSent, $"发布书页消息发送失败,PageId: {page.Id}", ResultCode.GLOBAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.Updateable()
|
||||||
|
.SetColumns(s => s.Status, JournalStatusEnum.Published)
|
||||||
|
.SetColumns(s => s.UpdatedAt, DateTime.Now)
|
||||||
|
.Where(w => w.Id == id)
|
||||||
|
.ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -141,6 +141,17 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
|
|||||||
return BaseResponse<bool>.Success(data);
|
return BaseResponse<bool>.Success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 书籍发布
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost, Route("journal/publish/{id:long}")]
|
||||||
|
public async Task<BaseResponse<bool>> Publish(long id)
|
||||||
|
{
|
||||||
|
var data = await JournalService.PublishAsync(id);
|
||||||
|
return BaseResponse<bool>.Success(data);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 书籍铺码
|
/// 书籍铺码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -200,17 +211,6 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
|
|||||||
|
|
||||||
#region 书籍目录
|
#region 书籍目录
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 书籍目录详情
|
|
||||||
///// </summary>
|
|
||||||
///// <returns></returns>
|
|
||||||
//[HttpGet, Route("catalog/tree/{JournalId:long}")]
|
|
||||||
//public async Task<BaseResponse<List<IcrJournalCatalog>>> CatalogDetailAsync(long JournalId)
|
|
||||||
//{
|
|
||||||
// var data = await _JournalCatalogService.DetailAsync(JournalId);
|
|
||||||
// return BaseResponse<List<IcrJournalCatalog>>.Success(data);
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 书籍目录详情
|
/// 书籍目录详情
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -312,18 +312,6 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
|
|||||||
return BaseResponse<bool>.Success(data);
|
return BaseResponse<bool>.Success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 书籍目录复制
|
|
||||||
///// </summary>
|
|
||||||
///// <param name="input"></param>
|
|
||||||
///// <returns></returns>
|
|
||||||
//[HttpPost, Route("catalog/copy")]
|
|
||||||
//[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
|
|
||||||
//public async Task<BaseResponse> CatalogCopy(CopyInput input)
|
|
||||||
//{
|
|
||||||
// var data = await _JournalCatalogServices.CopyAsync(input);
|
|
||||||
// return ApiResult(data, "书籍目录删除失败!");
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 书籍目录移动
|
/// 书籍目录移动
|
||||||
@ -411,30 +399,6 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 打印书页(全部) 暂时废弃这个方,直接使用 Adobe Reader X 软件打印就可以了
|
|
||||||
///// </summary>
|
|
||||||
///// <returns></returns>
|
|
||||||
//[HttpPost, Route("page/printJournalPage/{JournalId:long}")]
|
|
||||||
//public async Task<BaseResponse<bool>> PrintJournalPage([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId)
|
|
||||||
//{
|
|
||||||
// var data = await JournalPageService.PrintJournalPageAsync(JournalId);
|
|
||||||
// return BaseResponse<bool>.Success(data);
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 书页删除
|
|
||||||
///// </summary>
|
|
||||||
///// <returns></returns>
|
|
||||||
//[HttpPost, Route("page/delete/{id:long}")]
|
|
||||||
//[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
|
|
||||||
//public async Task<IActionResult> PageDelete(long id)
|
|
||||||
//{
|
|
||||||
// var data = await _JournalPageServices.DeleteAsync(id);
|
|
||||||
// return ApiResult(data, "更新书页失败!");
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 书页详情
|
/// 书页详情
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -446,17 +410,6 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
|
|||||||
return BaseResponse<JournalPageV2Output>.Success(data);
|
return BaseResponse<JournalPageV2Output>.Success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 不包含有文章的书页
|
|
||||||
///// </summary>
|
|
||||||
///// <returns></returns>
|
|
||||||
//[HttpGet, Route("page/no-article/{JournalId:long}")]
|
|
||||||
//[ProducesResponseType(typeof(BaseResponse<List<JournalPageNoArticleOutput>>), 200)]
|
|
||||||
//public async Task<IActionResult> PageNoArticleAsync(long JournalId)
|
|
||||||
//{
|
|
||||||
// var data = await _JournalPageService.PageNoArticleAsync(JournalId);
|
|
||||||
// return BaseResponse<List<JournalPageNoArticleOutput>>(data, "查询书页失败!");
|
|
||||||
//}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 自动铺码消费者
|
|
||||||
/// </summary>
|
|
||||||
public class AutoDotCodeConsumer(IConfiguration configuration,
|
|
||||||
IServiceScopeFactory scopeFactory,
|
|
||||||
IWebHostEnvironment webHostEnvironment,
|
|
||||||
ILogger<AutoDotCodeConsumer> 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<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 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<DotFile>().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<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 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<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 = "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>() ?? 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);
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@ -1,21 +1,27 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Enum;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Channels;
|
using System.Text.Json;
|
||||||
using Yitter.IdGenerator;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace QYZH.InteractiveMagazine.WorkService.Consumers;
|
namespace QYZH.InteractiveMagazine.WorkService.Consumers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 期刊任务接收消费者(示例)
|
/// 期刊任务接收消费者
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JournalTaskReceiveConsumer(ILogger<JournalTaskReceiveConsumer> logger, IConfiguration configuration,
|
public class JournalTaskReceiveConsumer(
|
||||||
|
ILogger<JournalTaskReceiveConsumer> logger,
|
||||||
|
IConfiguration configuration,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IWebHostEnvironment webHostEnvironment,
|
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
OssService ossService) : IQueueConsumer
|
OssService ossService) : IQueueConsumer
|
||||||
{
|
{
|
||||||
|
private const int DefaultAiScoreMaxRetryCount = 3;
|
||||||
|
private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
|
||||||
|
private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
|
||||||
|
|
||||||
public string Exchange => "ex.journal";
|
public string Exchange => "ex.journal";
|
||||||
|
|
||||||
@ -30,21 +36,98 @@ public class JournalTaskReceiveConsumer(ILogger<JournalTaskReceiveConsumer> logg
|
|||||||
|
|
||||||
using var scope = scopeFactory.CreateScope();
|
using var scope = scopeFactory.CreateScope();
|
||||||
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
||||||
// TODO: 在此编写具体的消息处理逻辑
|
var data = JsonSerializer.Deserialize<QuestionData>(message, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
}) ?? throw new InvalidOperationException("期刊任务消息内容为空");
|
||||||
|
|
||||||
|
if (data.Questions == null || data.Questions.Length == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("期刊任务消息没有题目,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskIds = data.Questions.Select(q => q.Id).Distinct().ToList();
|
||||||
|
var tasks = await client.Queryable<JournalPageTask>()
|
||||||
|
.Where(t => taskIds.Contains(t.Id) && !t.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var taskMap = tasks.ToDictionary(t => t.Id);
|
||||||
|
var referenceAnswers = await client.Queryable<JournalPageTaskAnswer>()
|
||||||
|
.Where(a => taskIds.Contains(a.JournalPageTaskId) && !a.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var referenceAnswerMap = referenceAnswers
|
||||||
|
.GroupBy(a => a.JournalPageTaskId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
var page = await client.Queryable<JournalPage>()
|
||||||
|
.Where(p => p.Id == data.PageId && !p.IsDeleted)
|
||||||
|
.FirstAsync(cancellationToken);
|
||||||
|
|
||||||
|
var answerEntities = new List<JournalPageTaskUserAnswer>();
|
||||||
|
foreach (var question in data.Questions)
|
||||||
|
{
|
||||||
|
if (!taskMap.TryGetValue(question.Id, out var task))
|
||||||
|
{
|
||||||
|
logger.LogWarning("未找到期刊任务,TaskId: {TaskId}, UserId: {UserId}", question.Id, data.UserId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task.NeedAiProcess)
|
||||||
|
{
|
||||||
|
logger.LogInformation("期刊任务配置为人工批改,跳过AI评分,TaskId: {TaskId}, UserId: {UserId}", task.Id, data.UserId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceAnswerMap.TryGetValue(task.Id, out var taskReferenceAnswers);
|
||||||
|
var scoreResult = await ScoreQuestionAsync(task, question, taskReferenceAnswers ?? [], cancellationToken);
|
||||||
|
answerEntities.Add(BuildAnswerEntity(data, question, task, page, scoreResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answerEntities.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("期刊任务消息没有可入库的答题记录,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}", data.UserId, data.JournalId, data.PageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Ado.BeginTran();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = System.Text.Json.JsonSerializer.Deserialize<QuestionData>(message);
|
foreach (var answer in answerEntities)
|
||||||
|
{
|
||||||
|
var existing = await client.Queryable<JournalPageTaskUserAnswer>()
|
||||||
|
.Where(a => a.UserId == answer.UserId && a.JournalPageTaskId == answer.JournalPageTaskId && !a.IsDeleted)
|
||||||
|
.FirstAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await client.Insertable(answer).ExecuteCommandAsync(cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.Insertable(BuildAnswerSnapshot(existing)).ExecuteCommandAsync(cancellationToken);
|
||||||
|
|
||||||
|
answer.Id = existing.Id;
|
||||||
|
answer.CreatedBy = existing.CreatedBy;
|
||||||
|
answer.CreatedAt = existing.CreatedAt;
|
||||||
|
answer.UpdatedBy = answer.UserId.ToString();
|
||||||
|
answer.UpdatedAt = DateTime.Now;
|
||||||
|
|
||||||
|
await client.Updateable(answer)
|
||||||
|
.IgnoreColumns(a => new { a.CreatedBy, a.CreatedAt })
|
||||||
|
.Where(a => a.Id == existing.Id)
|
||||||
|
.ExecuteCommandAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
client.Ado.CommitTran();
|
client.Ado.CommitTran();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
client.Ado.RollbackTran();
|
client.Ado.RollbackTran();
|
||||||
logger.LogError(ex.Message + ex.StackTrace);
|
throw;
|
||||||
}
|
}
|
||||||
await Task.CompletedTask;
|
|
||||||
|
logger.LogInformation("期刊任务答题记录保存完成,UserId: {UserId}, JournalId: {JournalId}, PageId: {PageId}, Count: {Count}",
|
||||||
|
data.UserId, data.JournalId, data.PageId, answerEntities.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnErrorAsync(byte[] body, Exception exception)
|
public Task OnErrorAsync(byte[] body, Exception exception)
|
||||||
@ -54,32 +137,684 @@ public class JournalTaskReceiveConsumer(ILogger<JournalTaskReceiveConsumer> logg
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<JournalAnswerScoreResult> ScoreQuestionAsync(
|
||||||
|
JournalPageTask task,
|
||||||
|
Question question,
|
||||||
|
List<JournalPageTaskAnswer> referenceAnswers,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var apiKey = configuration["AiChat:ApiKey"];
|
||||||
|
var baseUrl = configuration["AiChat:BaseUrl"];
|
||||||
|
var model = configuration["AiChat:Model"];
|
||||||
|
var timeoutSeconds = configuration.GetValue<int>("AiChat:TimeoutSeconds");
|
||||||
|
var maxTokens = configuration.GetValue<int>("AiChat:MaxTokens");
|
||||||
|
var temperature = configuration.GetValue<double>("AiChat:Temperature");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("AI聊天服务配置不完整,请检查 AiChat 配置节点");
|
||||||
|
}
|
||||||
|
|
||||||
|
var answerImages = await BuildAnswerImageContentsAsync(question, cancellationToken);
|
||||||
|
if (answerImages.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"题目 {question.Id} 缺少答案图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
var referenceAnswerImages = await BuildReferenceAnswerImageContentsAsync(task.Id, referenceAnswers, cancellationToken);
|
||||||
|
|
||||||
|
var content = new List<object>
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
type = "text",
|
||||||
|
text = BuildScorePrompt(task, question, answerImages.Count, referenceAnswers, referenceAnswerImages.Count)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var answerImage in answerImages)
|
||||||
|
{
|
||||||
|
content.Add(new
|
||||||
|
{
|
||||||
|
type = "image_url",
|
||||||
|
image_url = new { url = answerImage.DataUrl }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceAnswerImages.Count > 0)
|
||||||
|
{
|
||||||
|
content.Add(new
|
||||||
|
{
|
||||||
|
type = "text",
|
||||||
|
text = $"以下为参考答案图片,共 {referenceAnswerImages.Count} 张。参考答案不是必有,评分时以题目Prompt和学生答案为主。"
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var referenceAnswerImage in referenceAnswerImages)
|
||||||
|
{
|
||||||
|
content.Add(new
|
||||||
|
{
|
||||||
|
type = "image_url",
|
||||||
|
image_url = new { url = referenceAnswerImage.DataUrl }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
messages = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
role = "system",
|
||||||
|
content = "你是专业的学生作答评分助手。必须只返回合法 JSON,不要返回 Markdown、解释或代码块。"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
role = "user",
|
||||||
|
content
|
||||||
|
}
|
||||||
|
},
|
||||||
|
max_tokens = maxTokens > 0 ? maxTokens : 2000,
|
||||||
|
temperature = temperature > 0 ? temperature : 0.2,
|
||||||
|
stream = false,
|
||||||
|
response_format = new { type = "json_object" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
});
|
||||||
|
|
||||||
|
var responseContent = await SendAiScoreRequestWithRetryAsync(
|
||||||
|
task.Id,
|
||||||
|
$"{baseUrl.TrimEnd('/')}/chat/completions",
|
||||||
|
apiKey,
|
||||||
|
requestJson,
|
||||||
|
timeoutSeconds > 0 ? timeoutSeconds : 300,
|
||||||
|
cancellationToken);
|
||||||
|
var resultJson = ExtractAssistantContent(responseContent);
|
||||||
|
var scoreResult = ParseScoreResult(resultJson);
|
||||||
|
scoreResult.Result = TrimResult(scoreResult.Result);
|
||||||
|
return scoreResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildScorePrompt(
|
||||||
|
JournalPageTask task,
|
||||||
|
Question question,
|
||||||
|
int answerImageCount,
|
||||||
|
List<JournalPageTaskAnswer> referenceAnswers,
|
||||||
|
int referenceAnswerImageCount)
|
||||||
|
{
|
||||||
|
var referenceAnswerTexts = referenceAnswers
|
||||||
|
.Select(a => a.Answer?.Trim())
|
||||||
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var prompt = new StringBuilder();
|
||||||
|
prompt.AppendLine("参考答案不是必有;若无参考答案,以题目 Prompt 和学生答案为准评分。");
|
||||||
|
prompt.AppendLine($"参考答案图片数量:{referenceAnswerImageCount}");
|
||||||
|
if (referenceAnswerTexts.Count > 0)
|
||||||
|
{
|
||||||
|
prompt.AppendLine("参考答案文本:");
|
||||||
|
for (var i = 0; i < referenceAnswerTexts.Count; i++)
|
||||||
|
{
|
||||||
|
prompt.AppendLine($"{i + 1}. {referenceAnswerTexts[i]}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prompt.AppendLine("参考答案文本:无");
|
||||||
|
}
|
||||||
|
prompt.AppendLine();
|
||||||
|
prompt.AppendLine("请根据题目评分 Prompt 和学生答案图片进行评分。");
|
||||||
|
prompt.AppendLine();
|
||||||
|
prompt.AppendLine($"题目内容:{task.Task}");
|
||||||
|
prompt.AppendLine("评分Prompt:");
|
||||||
|
prompt.AppendLine(task.Prompt);
|
||||||
|
prompt.AppendLine();
|
||||||
|
prompt.AppendLine("题目配置:");
|
||||||
|
prompt.AppendLine($"- 成长值上限:{task.GrowthPoint ?? 0}");
|
||||||
|
prompt.AppendLine($"- 积分上限:{task.Points}");
|
||||||
|
prompt.AppendLine($"- 理解力上限:{task.Comprehension}");
|
||||||
|
prompt.AppendLine($"- 判断力上限:{task.Judgment}");
|
||||||
|
prompt.AppendLine($"- 表达力上限:{task.Expression}");
|
||||||
|
prompt.AppendLine($"- 说服力上限:{task.Persuasiveness}");
|
||||||
|
prompt.AppendLine();
|
||||||
|
prompt.AppendLine($"学生答案图片数量:{answerImageCount}");
|
||||||
|
prompt.AppendLine();
|
||||||
|
prompt.AppendLine("只返回如下 JSON 字段:");
|
||||||
|
prompt.AppendLine("{");
|
||||||
|
prompt.AppendLine(" \"Score\": 0,");
|
||||||
|
prompt.AppendLine(" \"GrowthPoint\": 0,");
|
||||||
|
prompt.AppendLine(" \"Points\": 0,");
|
||||||
|
prompt.AppendLine(" \"Comprehension\": 0,");
|
||||||
|
prompt.AppendLine(" \"Judgment\": 0,");
|
||||||
|
prompt.AppendLine(" \"Expression\": 0,");
|
||||||
|
prompt.AppendLine(" \"Persuasiveness\": 0,");
|
||||||
|
prompt.AppendLine(" \"Result\": \"50字内的中文评语\"");
|
||||||
|
prompt.AppendLine("}");
|
||||||
|
return prompt.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<AnswerImageContent>> BuildAnswerImageContentsAsync(Question question, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var imageUrls = question.AnswerUrl?.Where(url => !string.IsNullOrWhiteSpace(url)).Distinct().ToList() ?? [];
|
||||||
|
var maxImageBytes = configuration.GetValue<long>("AiChat:MaxImageBytes");
|
||||||
|
if (maxImageBytes <= 0)
|
||||||
|
{
|
||||||
|
maxImageBytes = DefaultMaxImageBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<AnswerImageContent>();
|
||||||
|
foreach (var imageUrl in imageUrls)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
await using var imageStream = ossService.GetObjectStream(imageUrl);
|
||||||
|
if (imageStream == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"答案图片读取失败,TaskId: {question.Id}, Url: {imageUrl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await imageStream.CopyToAsync(memoryStream, cancellationToken);
|
||||||
|
if (memoryStream.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"答案图片内容为空,TaskId: {question.Id}, Url: {imageUrl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryStream.Length > maxImageBytes)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"答案图片超过大小限制,TaskId: {question.Id}, Url: {imageUrl}, Size: {memoryStream.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageBytes = memoryStream.ToArray();
|
||||||
|
var mimeType = GetImageMimeType(imageUrl, imageBytes);
|
||||||
|
result.Add(new AnswerImageContent($"data:{mimeType};base64,{Convert.ToBase64String(imageBytes)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<AnswerImageContent>> BuildReferenceAnswerImageContentsAsync(
|
||||||
|
long taskId,
|
||||||
|
List<JournalPageTaskAnswer> referenceAnswers,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var imageUrls = referenceAnswers
|
||||||
|
.Select(a => a.AnswerUrL)
|
||||||
|
.Where(url => !string.IsNullOrWhiteSpace(url))
|
||||||
|
.Distinct()
|
||||||
|
.Select(url => url!)
|
||||||
|
.ToList();
|
||||||
|
var maxImageBytes = configuration.GetValue<long>("AiChat:MaxImageBytes");
|
||||||
|
if (maxImageBytes <= 0)
|
||||||
|
{
|
||||||
|
maxImageBytes = DefaultMaxImageBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<AnswerImageContent>();
|
||||||
|
foreach (var imageUrl in imageUrls)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var imageStream = ossService.GetObjectStream(imageUrl);
|
||||||
|
if (imageStream == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"参考答案图片读取失败,TaskId: {taskId}, Url: {imageUrl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await imageStream.CopyToAsync(memoryStream, cancellationToken);
|
||||||
|
if (memoryStream.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"参考答案图片内容为空,TaskId: {taskId}, Url: {imageUrl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryStream.Length > maxImageBytes)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"参考答案图片超过大小限制,TaskId: {taskId}, Url: {imageUrl}, Size: {memoryStream.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageBytes = memoryStream.ToArray();
|
||||||
|
var mimeType = GetImageMimeType(imageUrl, imageBytes);
|
||||||
|
result.Add(new AnswerImageContent($"data:{mimeType};base64,{Convert.ToBase64String(imageBytes)}"));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "参考答案图片读取失败,已跳过,TaskId: {TaskId}, Url: {Url}", taskId, imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> SendAiScoreRequestWithRetryAsync(
|
||||||
|
long taskId,
|
||||||
|
string requestUrl,
|
||||||
|
string apiKey,
|
||||||
|
string requestJson,
|
||||||
|
int timeoutSeconds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var maxRetryCount = configuration.GetValue<int>("AiChat:ScoreMaxRetryCount");
|
||||||
|
if (maxRetryCount <= 0)
|
||||||
|
{
|
||||||
|
maxRetryCount = DefaultAiScoreMaxRetryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
var retryDelayMilliseconds = configuration.GetValue<int>("AiChat:ScoreRetryDelayMilliseconds");
|
||||||
|
if (retryDelayMilliseconds <= 0)
|
||||||
|
{
|
||||||
|
retryDelayMilliseconds = DefaultAiScoreRetryDelayMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception? lastException = null;
|
||||||
|
for (var attempt = 1; attempt <= maxRetryCount; attempt++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var httpClient = httpClientFactory.CreateClient();
|
||||||
|
httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
request.Content = new StringContent(requestJson, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return responseContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("AI评分调用失败,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}, StatusCode: {StatusCode}, Response: {Response}",
|
||||||
|
taskId, attempt, maxRetryCount, response.StatusCode, responseContent);
|
||||||
|
|
||||||
|
if (!ShouldRetry(response.StatusCode) || attempt == maxRetryCount)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"AI评分调用失败:{response.StatusCode},响应:{responseContent}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < maxRetryCount)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
logger.LogWarning(ex, "AI评分调用异常,准备重试,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}", taskId, attempt, maxRetryCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = TimeSpan.FromMilliseconds(retryDelayMilliseconds * attempt);
|
||||||
|
await Task.Delay(delay, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"AI评分调用失败,TaskId: {taskId}", lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRetry(System.Net.HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
var status = (int)statusCode;
|
||||||
|
return status == 408 || status == 429 || status >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetImageMimeType(string imageUrl, byte[] imageBytes)
|
||||||
|
{
|
||||||
|
if (imageBytes.Length >= 4)
|
||||||
|
{
|
||||||
|
if (imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47)
|
||||||
|
{
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageBytes[0] == 0xFF && imageBytes[1] == 0xD8)
|
||||||
|
{
|
||||||
|
return "image/jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageBytes[0] == 0x47 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46)
|
||||||
|
{
|
||||||
|
return "image/gif";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageBytes[0] == 0x52 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46 && imageBytes[3] == 0x46)
|
||||||
|
{
|
||||||
|
return "image/webp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(imageUrl).ToLowerInvariant();
|
||||||
|
return extension switch
|
||||||
|
{
|
||||||
|
".png" => "image/png",
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".gif" => "image/gif",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
_ => "image/jpeg"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractAssistantContent(string responseContent)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(responseContent);
|
||||||
|
var message = document.RootElement.GetProperty("choices")[0].GetProperty("message");
|
||||||
|
if (!message.TryGetProperty("content", out var contentElement))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("AI评分响应缺少 content");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return contentElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentElement.GetRawText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalAnswerScoreResult ParseScoreResult(string resultJson)
|
||||||
|
{
|
||||||
|
var cleanedJson = CleanJsonContent(resultJson);
|
||||||
|
var result = JsonSerializer.Deserialize<JournalAnswerScoreResult>(cleanedJson, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ?? throw new InvalidOperationException("AI评分结果解析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanJsonContent(string content)
|
||||||
|
{
|
||||||
|
var text = content.Trim();
|
||||||
|
if (text.StartsWith("```", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var firstLineEnd = text.IndexOf('\n');
|
||||||
|
if (firstLineEnd >= 0)
|
||||||
|
{
|
||||||
|
text = text[(firstLineEnd + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
var fenceIndex = text.LastIndexOf("```", StringComparison.Ordinal);
|
||||||
|
if (fenceIndex >= 0)
|
||||||
|
{
|
||||||
|
text = text[..fenceIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = text.IndexOf('{');
|
||||||
|
var end = text.LastIndexOf('}');
|
||||||
|
if (start >= 0 && end > start)
|
||||||
|
{
|
||||||
|
text = text[start..(end + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalPageTaskUserAnswer BuildAnswerEntity(
|
||||||
|
QuestionData data,
|
||||||
|
Question question,
|
||||||
|
JournalPageTask task,
|
||||||
|
JournalPage? page,
|
||||||
|
JournalAnswerScoreResult scoreResult)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var growthPoint = Math.Max(0, scoreResult.GrowthPoint);
|
||||||
|
var points = Math.Max(0, scoreResult.Points);
|
||||||
|
|
||||||
|
return new JournalPageTaskUserAnswer
|
||||||
|
{
|
||||||
|
JournalId = data.JournalId,
|
||||||
|
JournalPageId = data.PageId,
|
||||||
|
JournalPageTaskId = task.Id,
|
||||||
|
JournalPageTaskGroupId = task.GroupId,
|
||||||
|
UserId = data.UserId,
|
||||||
|
Result = scoreResult.Result,
|
||||||
|
Points = points,
|
||||||
|
GrowthPoints = growthPoint,
|
||||||
|
Score = Math.Max(0, scoreResult.Score),
|
||||||
|
Comprehension = Math.Max(0, scoreResult.Comprehension),
|
||||||
|
Judgment = Math.Max(0, scoreResult.Judgment),
|
||||||
|
Expression = Math.Max(0, scoreResult.Expression),
|
||||||
|
Persuasiveness = Math.Max(0, scoreResult.Persuasiveness),
|
||||||
|
QuestionAnswerUrl = question.Url,
|
||||||
|
AnswerUrl = JsonSerializer.Serialize(question.AnswerUrl ?? []),
|
||||||
|
PageAnswerUrl = data.PageAnswerUrl,
|
||||||
|
Revision = 0,
|
||||||
|
AnswerStatus = (int)UserAnswerStatusEnum.Complete,
|
||||||
|
AnswerStartTime = question.AnswerStartTime,
|
||||||
|
AnswerEndTime = question.AnswerEndTime,
|
||||||
|
AnswerSeconds = question.AnswerTime,
|
||||||
|
ImageRecognition = 0,
|
||||||
|
JournalPageNum = page?.PageNum ?? 0,
|
||||||
|
Modify = 0,
|
||||||
|
LastTag = 0,
|
||||||
|
DotPageNum = page?.PageNum ?? 0,
|
||||||
|
PageResultUrl = string.Empty,
|
||||||
|
Type = task.Type.ToString(),
|
||||||
|
DotPageNo = page?.PageNo ?? string.Empty,
|
||||||
|
PageAnswerDotUrl = string.Empty,
|
||||||
|
BreakCount = question.BreakCount,
|
||||||
|
BreakTimes = JsonSerializer.Serialize(question.BreakTimes ?? []),
|
||||||
|
AssignmentStatus = UserAnswerStatusEnum.Complete.ToString(),
|
||||||
|
Status = (int)UserAnswerStatusEnum.Complete,
|
||||||
|
CreatedBy = data.UserId.ToString(),
|
||||||
|
CreatedAt = data.CreatedTime == default ? now : data.CreatedTime,
|
||||||
|
UpdatedBy = data.UserId.ToString(),
|
||||||
|
UpdatedAt = now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalPageTaskUserAnswerSnapshot BuildAnswerSnapshot(JournalPageTaskUserAnswer answer)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
return new JournalPageTaskUserAnswerSnapshot
|
||||||
|
{
|
||||||
|
JournalPageTaskUserAnswerId = answer.Id,
|
||||||
|
JournalId = answer.JournalId,
|
||||||
|
JournalPageId = answer.JournalPageId,
|
||||||
|
JournalPageTaskId = answer.JournalPageTaskId,
|
||||||
|
JournalPageTaskGroupId = answer.JournalPageTaskGroupId,
|
||||||
|
UserId = answer.UserId,
|
||||||
|
Result = answer.Result,
|
||||||
|
Points = answer.Points,
|
||||||
|
GrowthPoints = answer.GrowthPoints,
|
||||||
|
Score = answer.Score,
|
||||||
|
Comprehension = answer.Comprehension,
|
||||||
|
Judgment = answer.Judgment,
|
||||||
|
Expression = answer.Expression,
|
||||||
|
Persuasiveness = answer.Persuasiveness,
|
||||||
|
QuestionAnswerUrl = answer.QuestionAnswerUrl,
|
||||||
|
AnswerUrl = answer.AnswerUrl,
|
||||||
|
PageAnswerUrl = answer.PageAnswerUrl,
|
||||||
|
Revision = answer.Revision,
|
||||||
|
AnswerStatus = answer.AnswerStatus,
|
||||||
|
AnswerStartTime = answer.AnswerStartTime,
|
||||||
|
AnswerEndTime = answer.AnswerEndTime,
|
||||||
|
AnswerSeconds = answer.AnswerSeconds,
|
||||||
|
ImageRecognition = answer.ImageRecognition,
|
||||||
|
JournalPageNum = answer.JournalPageNum,
|
||||||
|
Modify = answer.Modify,
|
||||||
|
LastTag = answer.LastTag,
|
||||||
|
DotPageNum = answer.DotPageNum,
|
||||||
|
PageResultUrl = answer.PageResultUrl,
|
||||||
|
Type = answer.Type,
|
||||||
|
DotPageNo = answer.DotPageNo,
|
||||||
|
PageAnswerDotUrl = answer.PageAnswerDotUrl,
|
||||||
|
BreakCount = answer.BreakCount,
|
||||||
|
BreakTimes = answer.BreakTimes,
|
||||||
|
AssignmentStatus = answer.AssignmentStatus,
|
||||||
|
Status = answer.Status,
|
||||||
|
CreatedBy = answer.UpdatedBy ?? answer.CreatedBy ?? string.Empty,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedBy = answer.UpdatedBy ?? string.Empty,
|
||||||
|
UpdatedAt = now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimResult(string? result)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(result))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Length <= 50 ? result : result[..50];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊题目作答消息
|
||||||
|
/// </summary>
|
||||||
public class QuestionData
|
public class QuestionData
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户ID
|
||||||
|
/// </summary>
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊ID
|
||||||
|
/// </summary>
|
||||||
public long JournalId { get; set; }
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页ID
|
||||||
|
/// </summary>
|
||||||
public long PageId { get; set; }
|
public long PageId { get; set; }
|
||||||
public string PageAnswerUrl { get; set; }
|
|
||||||
public Question[] Questions { get; set; }
|
/// <summary>
|
||||||
|
/// 页面作答图片地址
|
||||||
|
/// </summary>
|
||||||
|
public string PageAnswerUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 题目作答列表
|
||||||
|
/// </summary>
|
||||||
|
public Question[] Questions { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
public DateTime CreatedTime { get; set; }
|
public DateTime CreatedTime { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 题目作答数据
|
||||||
|
/// </summary>
|
||||||
public class Question
|
public class Question
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 题目ID
|
||||||
|
/// </summary>
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public string Url { get; set; }
|
|
||||||
public string[] AnswerUrl { get; set; }
|
/// <summary>
|
||||||
|
/// 题目图片地址
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 答案图片地址
|
||||||
|
/// </summary>
|
||||||
|
public string[] AnswerUrl { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作答开始时间
|
||||||
|
/// </summary>
|
||||||
public DateTime AnswerStartTime { get; set; }
|
public DateTime AnswerStartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作答结束时间
|
||||||
|
/// </summary>
|
||||||
public DateTime AnswerEndTime { get; set; }
|
public DateTime AnswerEndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作答耗时秒数
|
||||||
|
/// </summary>
|
||||||
public int AnswerTime { get; set; }
|
public int AnswerTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 中断次数
|
||||||
|
/// </summary>
|
||||||
public int BreakCount { get; set; }
|
public int BreakCount { get; set; }
|
||||||
public List<BreakTime> BreakTimes { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 中断记录
|
||||||
|
/// </summary>
|
||||||
|
public List<BreakTime> BreakTimes { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 中断时间记录
|
||||||
|
/// </summary>
|
||||||
public class BreakTime
|
public class BreakTime
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 中断时间
|
||||||
|
/// </summary>
|
||||||
public DateTime Time { get; set; }
|
public DateTime Time { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 等待时间
|
||||||
|
/// </summary>
|
||||||
public long WaitTime { get; set; }
|
public long WaitTime { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI评分结果
|
||||||
|
/// </summary>
|
||||||
|
public class JournalAnswerScoreResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 题目得分
|
||||||
|
/// </summary>
|
||||||
|
public float Score { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 积分
|
||||||
|
/// </summary>
|
||||||
|
public int Points { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 理解力评分
|
||||||
|
/// </summary>
|
||||||
|
public float Comprehension { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断力评分
|
||||||
|
/// </summary>
|
||||||
|
public float Judgment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表达力评分
|
||||||
|
/// </summary>
|
||||||
|
public float Expression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 说服力评分
|
||||||
|
/// </summary>
|
||||||
|
public float Persuasiveness { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 50字内评语
|
||||||
|
/// </summary>
|
||||||
|
public string Result { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AnswerImageContent(string DataUrl);
|
||||||
|
|||||||
10
QYZH.InteractiveMagazine.WorkService/Dockerfile
Normal file
10
QYZH.InteractiveMagazine.WorkService/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
|
|
||||||
|
WORKDIR /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"]
|
||||||
@ -77,7 +77,6 @@ builder.Services.AddRabbitMQ(builder.Configuration);
|
|||||||
|
|
||||||
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
|
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
|
||||||
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
|
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
|
||||||
builder.Services.AddScoped<IQueueConsumer, AutoDotCodeConsumer>();
|
|
||||||
|
|
||||||
// 注册消费者后台服务
|
// 注册消费者后台服务
|
||||||
builder.Services.AddHostedService<RabbitMQHostedService>();
|
builder.Services.AddHostedService<RabbitMQHostedService>();
|
||||||
|
|||||||
@ -22,58 +22,4 @@
|
|||||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="PrintToolV2.7\bin\gsdll32.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\gsdll32_1.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\gsdll32_10.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\gsdll32_9.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\gswin32c.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\info.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\libiconv2.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\print.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\bin\tk.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\PrintTool.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\sublic_license_1714.0.0.0_10.xml">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\sublic_license_1761.172.8.16_1000.xml">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\sublic_license_1761.211.21.48_10000.xml">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\wfdlicense.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\wfdprint.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\命令行方式调用方法.txt">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="PrintToolV2.7\返回值说明.txt">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -49,6 +49,17 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": "*",
|
"AllowedHosts": "*",
|
||||||
"AliyunOSSConfigs": {
|
"AliyunOSSConfigs": {
|
||||||
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
|
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
<Project Path="QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
|
||||||
|
|||||||
Reference in New Issue
Block a user