diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
index 31329c7..a99fa83 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
@@ -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; }
}
}
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
index 44f11b6..be24c64 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
@@ -18,7 +18,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// 书页Id
///
public long JournalPageId { get; set; }
-
+ ///
+ /// 任务信息
+ ///
+ public string Task { get; set; }
///
/// 题号
///
diff --git a/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs b/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
index 5580094..cead891 100644
--- a/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
+++ b/QYZH.InteractiveMagazine.PrintWorker/Consumers/AutoDotCodeConsumer.cs
@@ -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; }
diff --git a/QYZH.InteractiveMagazine.PrintWorker/Program.cs b/QYZH.InteractiveMagazine.PrintWorker/Program.cs
index 0694b35..9f9ed11 100644
--- a/QYZH.InteractiveMagazine.PrintWorker/Program.cs
+++ b/QYZH.InteractiveMagazine.PrintWorker/Program.cs
@@ -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();
var app = builder.Build();
-Log.Information("PrintWorker 已启动,仅消费自动铺码队列");
+Log.Information("{ServiceName} 已启动,仅消费自动铺码队列", serviceName);
await app.RunAsync();
diff --git a/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj b/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj
index 741c9bd..3be3c98 100644
--- a/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj
+++ b/QYZH.InteractiveMagazine.PrintWorker/QYZH.InteractiveMagazine.PrintWorker.csproj
@@ -1,7 +1,9 @@
+ Exe
net8.0
+ win-x64
enable
enable
@@ -28,6 +30,12 @@
Always
+
+ Always
+
+
+ Always
+
diff --git a/QYZH.InteractiveMagazine.PrintWorker/README.md b/QYZH.InteractiveMagazine.PrintWorker/README.md
new file mode 100644
index 0000000..afa8b19
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/README.md
@@ -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
+```
diff --git a/QYZH.InteractiveMagazine.PrintWorker/appsettings.json b/QYZH.InteractiveMagazine.PrintWorker/appsettings.json
index f571c74..028a828 100644
--- a/QYZH.InteractiveMagazine.PrintWorker/appsettings.json
+++ b/QYZH.InteractiveMagazine.PrintWorker/appsettings.json
@@ -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
}
}
diff --git a/QYZH.InteractiveMagazine.PrintWorker/scripts/install-service.ps1 b/QYZH.InteractiveMagazine.PrintWorker/scripts/install-service.ps1
new file mode 100644
index 0000000..5f3727f
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/scripts/install-service.ps1
@@ -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
diff --git a/QYZH.InteractiveMagazine.PrintWorker/scripts/publish-win-x64.ps1 b/QYZH.InteractiveMagazine.PrintWorker/scripts/publish-win-x64.ps1
new file mode 100644
index 0000000..2398b4c
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/scripts/publish-win-x64.ps1
@@ -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"
diff --git a/QYZH.InteractiveMagazine.PrintWorker/scripts/uninstall-service.ps1 b/QYZH.InteractiveMagazine.PrintWorker/scripts/uninstall-service.ps1
new file mode 100644
index 0000000..a10f4f1
--- /dev/null
+++ b/QYZH.InteractiveMagazine.PrintWorker/scripts/uninstall-service.ps1
@@ -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"
diff --git a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
index de9c047..9ec041c 100644
--- a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
+++ b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
@@ -97,6 +97,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
JournalId = task.JournalId,
JournalPageId = task.JournalPageId,
No = task.No,
+ Task =task.Task,
Type = task.Type,
Points = task.Points,
GrowthPoint = task.GrowthPoint,
diff --git a/QYZH.InteractiveMagazine.Service/JournalService.cs b/QYZH.InteractiveMagazine.Service/JournalService.cs
index 6194492..32fbea3 100644
--- a/QYZH.InteractiveMagazine.Service/JournalService.cs
+++ b/QYZH.InteractiveMagazine.Service/JournalService.cs
@@ -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 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 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 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 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 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();
+ 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;
+ }
}
}