feat: 新增打印任务相关功能并完善服务配置
1. 调整回调响应的Code字段类型为object以兼容更多返回值 2. 为JournalDto新增Url和QuestionNo属性 3. 为JournalPageTaskOutput新增Task字段并完善注释 4. 新增打印工作服务的发布、安装卸载脚本与README文档 5. 调整项目配置,添加运行时标识与资源拷贝配置 6. 优化程序启动逻辑,修复工作目录与日志路径问题 7. 简化日志配置,移除多余的Hangfire与AI配置项 8. 完善期刊删除逻辑,关联删除相关子数据 9. 新增期刊发布时的书页任务排序与回调地址补全逻辑
This commit is contained in:
@ -146,7 +146,9 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
{
|
||||
public long BookId { get; set; }
|
||||
public long PageId { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string? PageNo { get; set; }
|
||||
public string? Layout { get; set; }
|
||||
public string QuestionNo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
/// 书页Id
|
||||
/// </summary>
|
||||
public long JournalPageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务信息
|
||||
/// </summary>
|
||||
public string Task { get; set; }
|
||||
/// <summary>
|
||||
/// 题号
|
||||
/// </summary>
|
||||
|
||||
@ -330,7 +330,7 @@ internal class CallbackUpdateJournalStatusResponse
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
|
||||
public string? Code { get; set; }
|
||||
public object? Code { get; set; }
|
||||
|
||||
public bool Result { get; set; }
|
||||
|
||||
|
||||
@ -5,21 +5,34 @@ using QYZH.InteractiveMagazine.PrintWorker.Consumers;
|
||||
using Serilog;
|
||||
using SqlSugar;
|
||||
using SqlSugar.IOC;
|
||||
using System.Text;
|
||||
using Yitter.IdGenerator;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
Directory.SetCurrentDirectory(AppContext.BaseDirectory);
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings
|
||||
{
|
||||
Args = args,
|
||||
ContentRootPath = AppContext.BaseDirectory
|
||||
});
|
||||
const string serviceName = "QYZH.InteractiveMagazine.PrintWorker";
|
||||
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
|
||||
var logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
var logFilePath = Path.Combine(logDirectory, "printworker-log-.txt");
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day, encoding: Encoding.UTF8)
|
||||
.Enrich.FromLogContext()
|
||||
.CreateLogger();
|
||||
|
||||
builder.Services.AddSerilog();
|
||||
builder.Services.AddWindowsService(options => options.ServiceName = "QYZH InteractiveMagazine PrintWorker");
|
||||
builder.Services.AddWindowsService(options => options.ServiceName = serviceName);
|
||||
|
||||
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 3 });
|
||||
|
||||
@ -54,6 +67,6 @@ builder.Services.AddHostedService<RabbitMQHostedService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
Log.Information("PrintWorker 已启动,仅消费自动铺码队列");
|
||||
Log.Information("{ServiceName} 已启动,仅消费自动铺码队列", serviceName);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
@ -28,6 +30,12 @@
|
||||
<None Include="PrintToolV2.7\**\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="scripts\**\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="README.md">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
30
QYZH.InteractiveMagazine.PrintWorker/README.md
Normal file
30
QYZH.InteractiveMagazine.PrintWorker/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# PrintWorker Windows Service
|
||||
|
||||
## 发布
|
||||
|
||||
```powershell
|
||||
cd C:\项目\QYZH.InteractiveMagazine\QYZH.InteractiveMagazine.PrintWorker
|
||||
.\scripts\publish-win-x64.ps1
|
||||
```
|
||||
|
||||
发布目录默认是 `.\publish\win-x64`,其中包含 `PrintToolV2.7`、`appsettings.json` 和 `QYZH.InteractiveMagazine.PrintWorker.exe`。
|
||||
|
||||
## 安装服务
|
||||
|
||||
用管理员 PowerShell 执行:
|
||||
|
||||
```powershell
|
||||
cd C:\Web\QYZH.InteractiveMagazine.PrintWorker
|
||||
.\scripts\install-service.ps1
|
||||
```
|
||||
|
||||
默认服务名:`QYZH.InteractiveMagazine.PrintWorker`。
|
||||
|
||||
## 卸载服务
|
||||
|
||||
用管理员 PowerShell 执行:
|
||||
|
||||
```powershell
|
||||
cd C:\项目\QYZH.InteractiveMagazine\QYZH.InteractiveMagazine.PrintWorker
|
||||
.\scripts\uninstall-service.ps1
|
||||
```
|
||||
@ -19,48 +19,15 @@
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning",
|
||||
"Hangfire": "Information"
|
||||
"System": "Warning"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -68,18 +35,15 @@
|
||||
"BucketName": "qyzh2025test",
|
||||
"Region": "beijing",
|
||||
"RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole",
|
||||
"DurationSeconds": 3600, //过期时间(秒)
|
||||
"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 ---暂时没有导入页码
|
||||
"DPrint": 0,
|
||||
"CallBackApiUrl": "http://localhost:8080/api/page/callbackupdatepageno",
|
||||
"DotId": 765837655859269
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
param(
|
||||
[string]$ServiceName = "QYZH.InteractiveMagazine.PrintWorker",
|
||||
[string]$DisplayName = "QYZH InteractiveMagazine PrintWorker",
|
||||
[string]$Description = "InteractiveMagazine automatic dot-code print worker.",
|
||||
[string]$PublishPath = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
throw "Please run this script in an elevated PowerShell window."
|
||||
}
|
||||
|
||||
$exePath = Resolve-Path (Join-Path $PublishPath "QYZH.InteractiveMagazine.PrintWorker.exe")
|
||||
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existing) {
|
||||
if ($existing.Status -ne "Stopped") {
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
$existing.WaitForStatus("Stopped", "00:00:30")
|
||||
}
|
||||
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
New-Service `
|
||||
-Name $ServiceName `
|
||||
-BinaryPathName "`"$exePath`"" `
|
||||
-DisplayName $DisplayName `
|
||||
-Description $Description `
|
||||
-StartupType Automatic
|
||||
|
||||
Start-Service -Name $ServiceName
|
||||
Get-Service -Name $ServiceName
|
||||
@ -0,0 +1,28 @@
|
||||
param(
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Output = ".\publish\win-x64",
|
||||
[switch]$SelfContained
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$projectPath = Join-Path $PSScriptRoot "..\QYZH.InteractiveMagazine.PrintWorker.csproj"
|
||||
$publishArgs = @(
|
||||
"publish",
|
||||
$projectPath,
|
||||
"-c", $Configuration,
|
||||
"-r", "win-x64",
|
||||
"-o", $Output
|
||||
)
|
||||
|
||||
if ($SelfContained) {
|
||||
$publishArgs += "--self-contained"
|
||||
$publishArgs += "true"
|
||||
} else {
|
||||
$publishArgs += "--self-contained"
|
||||
$publishArgs += "false"
|
||||
}
|
||||
|
||||
dotnet @publishArgs
|
||||
|
||||
Write-Host "PrintWorker published to: $Output"
|
||||
@ -0,0 +1,23 @@
|
||||
param(
|
||||
[string]$ServiceName = "QYZH.InteractiveMagazine.PrintWorker"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
throw "Please run this script in an elevated PowerShell window."
|
||||
}
|
||||
|
||||
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
if (-not $existing) {
|
||||
Write-Host "Service not found: $ServiceName"
|
||||
return
|
||||
}
|
||||
|
||||
if ($existing.Status -ne "Stopped") {
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
$existing.WaitForStatus("Stopped", "00:00:30")
|
||||
}
|
||||
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
Write-Host "Service deleted: $ServiceName"
|
||||
@ -97,6 +97,7 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
||||
JournalId = task.JournalId,
|
||||
JournalPageId = task.JournalPageId,
|
||||
No = task.No,
|
||||
Task =task.Task,
|
||||
Type = task.Type,
|
||||
Points = task.Points,
|
||||
GrowthPoint = task.GrowthPoint,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
@ -225,19 +226,19 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
||||
await base.DeleteAsync(d => ids.Contains(d.Id));
|
||||
await JournalPageRepository.DeleteAsync(d => ids.Contains(d.JournalId));
|
||||
await JournalCatalogRepository.DeleteAsync(d => ids.Contains(d.JournalId));
|
||||
|
||||
|
||||
// 先查询要删除的 JournalPageTask Id 列表
|
||||
var taskIds = await JournalPageTaskRepository.Queryable()
|
||||
.Where(d => ids.Contains(d.JournalId))
|
||||
.Select(d => d.Id)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
// 删除 JournalPageTaskAnswer
|
||||
if (taskIds.Any())
|
||||
{
|
||||
await JournalPageTaskAnswerRepository.DeleteAsync(d => taskIds.Contains(d.JournalPageTaskId));
|
||||
}
|
||||
|
||||
|
||||
// 再删除 JournalPageTask
|
||||
await JournalPageTaskRepository.DeleteAsync(d => ids.Contains(d.JournalId));
|
||||
return true;
|
||||
@ -344,6 +345,13 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
||||
.Where(x => x.JournalId == id && !x.IsDeleted)
|
||||
.OrderBy(x => x.PageNum)
|
||||
.OrderBy(x => x.Sort)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.PageNo,
|
||||
x.Layout,
|
||||
x.Url
|
||||
})
|
||||
.ToListAsync();
|
||||
BusinessException.ThrowIf(pages.Count == 0, "书籍未添加任何书页,无法发布", ResultCode.UNPROCESSABLE_ENTITY);
|
||||
|
||||
@ -351,6 +359,22 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
||||
BusinessException.ThrowIf(!book.EndTime.HasValue, "发布结束时间不能为空", ResultCode.UNPROCESSABLE_ENTITY);
|
||||
BusinessException.ThrowIf(book.EndTime < book.StartTime, "发布结束时间不能早于开始时间", ResultCode.UNPROCESSABLE_ENTITY);
|
||||
|
||||
var pageIds = pages.Select(x => x.Id).ToList();
|
||||
var pageQuestions = await JournalPageTaskRepository.Queryable()
|
||||
.Where(x => x.JournalId == id && pageIds.Contains(x.JournalPageId) && !x.IsDeleted)
|
||||
.Select(x => new
|
||||
{
|
||||
x.JournalPageId,
|
||||
x.Id,
|
||||
x.No
|
||||
})
|
||||
.ToListAsync();
|
||||
var questionIdsByPageId = pageQuestions
|
||||
.GroupBy(x => x.JournalPageId)
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => string.Join(',', x.OrderBy(q => ParseTaskNo(q.No)).Select(q => q.Id)));
|
||||
|
||||
var bookMessageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
|
||||
{
|
||||
Exchange = JournalExchange,
|
||||
@ -377,7 +401,9 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
||||
BookId = id,
|
||||
PageId = page.Id,
|
||||
PageNo = page.PageNo,
|
||||
Layout = page.Layout
|
||||
Layout = page.Layout,
|
||||
Url = DomainHelper.OssFullUrl(page.Url),
|
||||
QuestionNo = questionIdsByPageId.GetValueOrDefault(page.Id) ?? string.Empty
|
||||
}
|
||||
});
|
||||
BusinessException.ThrowIf(!pageMessageSent, $"发布书页消息发送失败,PageId: {page.Id}", ResultCode.GLOBAL_ERROR);
|
||||
@ -388,5 +414,16 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
|
||||
.SetColumns(s => s.UpdatedAt, DateTime.Now)
|
||||
.Where(w => w.Id == id)
|
||||
.ExecuteCommandAsync() > 0;
|
||||
|
||||
static (int First, int Second, int Third, int Fourth) ParseTaskNo(string? no)
|
||||
{
|
||||
var parts = no?.Split('-') ?? Array.Empty<string>();
|
||||
return (GetNoPart(parts, 0), GetNoPart(parts, 1), GetNoPart(parts, 2), GetNoPart(parts, 3));
|
||||
}
|
||||
|
||||
static int GetNoPart(string[] parts, int index)
|
||||
{
|
||||
return parts.Length > index && int.TryParse(parts[index], out var value) ? value : int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user