177 lines
6.5 KiB
C#
177 lines
6.5 KiB
C#
using System.Text.Json;
|
|
|
|
namespace FixedAssetPrintAgent;
|
|
|
|
internal sealed class PendingPrintResult
|
|
{
|
|
public int JobId { get; set; }
|
|
public string ClaimToken { get; set; } = string.Empty;
|
|
public string ServerUrl { get; set; } = string.Empty;
|
|
public int TerminalId { get; set; }
|
|
public string Phase { get; set; } = "claimed";
|
|
public string? ErrorMessage { get; set; }
|
|
}
|
|
|
|
internal interface IPrintResultJournal
|
|
{
|
|
Task<PendingPrintResult?> LoadAsync();
|
|
Task SaveAsync(PendingPrintResult result);
|
|
Task ClearAsync();
|
|
}
|
|
|
|
internal sealed class PrintResultJournal(string storageRoot) : IPrintResultJournal
|
|
{
|
|
private readonly string _filePath = Path.Combine(storageRoot, "pending-print-result.json");
|
|
private readonly JsonSerializerOptions _options = new(JsonSerializerDefaults.Web);
|
|
|
|
public async Task<PendingPrintResult?> LoadAsync()
|
|
{
|
|
if (!File.Exists(_filePath)) return null;
|
|
await using var stream = File.OpenRead(_filePath);
|
|
var result = await JsonSerializer.DeserializeAsync<PendingPrintResult>(stream, _options);
|
|
if (result is null || result.JobId <= 0 || string.IsNullOrWhiteSpace(result.ClaimToken) ||
|
|
result.Phase is not ("claimed" or "dispatching" or "completed_pending_ack" or "unknown_pending_ack" or "failed_pending_ack"))
|
|
throw new InvalidOperationException("本地打印恢复记录损坏,请先核实上一任务出纸情况。");
|
|
return result;
|
|
}
|
|
|
|
public async Task SaveAsync(PendingPrintResult result)
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
|
var temporaryPath = $"{_filePath}.{Guid.NewGuid():N}.tmp";
|
|
try
|
|
{
|
|
await using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
|
{
|
|
await JsonSerializer.SerializeAsync(stream, result, _options);
|
|
stream.Flush(true);
|
|
}
|
|
File.Move(temporaryPath, _filePath, true);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
|
|
}
|
|
}
|
|
|
|
public Task ClearAsync()
|
|
{
|
|
File.Delete(_filePath);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
internal sealed class PrintAgentRequestException(int statusCode, string message) : Exception(message)
|
|
{
|
|
public int StatusCode { get; } = statusCode;
|
|
}
|
|
|
|
internal static class PrintResultRecovery
|
|
{
|
|
// Acknowledgment retries never enter the printer. Inspect a conflicting result
|
|
// before clearing the journal; only a fenced attempt or final server state wins.
|
|
public static async Task AcknowledgeAsync(
|
|
PendingPrintResult pending,
|
|
IPrintResultJournal journal,
|
|
Func<int, string, string, string?, Task> report,
|
|
Func<int, Task<PrintJobState>> readState,
|
|
Action<string> log)
|
|
{
|
|
var status = pending.Phase == "completed_pending_ack" ? "completed" :
|
|
pending.Phase == "failed_pending_ack" ? "failed" : "result_unknown";
|
|
try
|
|
{
|
|
await report(pending.JobId, pending.ClaimToken, status, pending.ErrorMessage ??
|
|
(status == "result_unknown" ? "打印过程曾中断,请人工核实出纸结果。" : null));
|
|
await journal.ClearAsync();
|
|
log(status == "completed" ? $"任务 {pending.JobId} 完成结果已确认。" :
|
|
$"任务 {pending.JobId} 结果已上报,请到系统查看。" );
|
|
}
|
|
catch (PrintAgentRequestException ex) when (ex.StatusCode == 409)
|
|
{
|
|
var current = await readState(pending.JobId);
|
|
if (!current.HasValidState() || current.Id != pending.JobId)
|
|
throw new InvalidOperationException("服务器任务状态响应不完整,保留恢复记录并等待核实。");
|
|
if (current.ClaimToken != pending.ClaimToken || current.Status is "completed" or "cancelled" or "failed")
|
|
{
|
|
await journal.ClearAsync();
|
|
log($"任务 {pending.JobId} 已由服务器处理,请核对系统中的结果。");
|
|
return;
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public static async Task ExecuteAsync(
|
|
PrintJobDto job,
|
|
AgentConfiguration configuration,
|
|
IPrintResultJournal journal,
|
|
Func<int, string, string, string?, Task> report,
|
|
Func<int, Task<PrintJobState>> readState,
|
|
Func<Task> dispatch,
|
|
Action<string> log,
|
|
TimeSpan? renewalInterval = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(job.ClaimToken))
|
|
throw new InvalidOperationException("服务器缺少领取凭据,已阻止实际出纸,请更新服务器。");
|
|
var pending = new PendingPrintResult
|
|
{
|
|
JobId = job.Id, ClaimToken = job.ClaimToken, ServerUrl = configuration.ServerUrl,
|
|
TerminalId = configuration.TerminalId, Phase = "claimed"
|
|
};
|
|
await journal.SaveAsync(pending);
|
|
if (job.JobType != "asset_label")
|
|
{
|
|
pending.Phase = "failed_pending_ack";
|
|
pending.ErrorMessage = "不支持的打印任务类型,未开始打印。";
|
|
await journal.SaveAsync(pending);
|
|
await AcknowledgeAsync(pending, journal, report, readState, log);
|
|
return;
|
|
}
|
|
await report(job.Id, job.ClaimToken, "printing", null);
|
|
// Persist before any spool call. On restart this phase becomes result_unknown.
|
|
pending.Phase = "dispatching";
|
|
await journal.SaveAsync(pending);
|
|
using var cancellation = new CancellationTokenSource();
|
|
var renewal = RenewAsync(job, report, log, renewalInterval ?? TimeSpan.FromSeconds(30), cancellation.Token);
|
|
try
|
|
{
|
|
await dispatch();
|
|
pending.Phase = "completed_pending_ack";
|
|
pending.ErrorMessage = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Some labels may already be spooled. A partial failure cannot be retried
|
|
// as a whole batch until an administrator verifies the actual output.
|
|
pending.Phase = "unknown_pending_ack";
|
|
pending.ErrorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
cancellation.Cancel();
|
|
await renewal;
|
|
}
|
|
await journal.SaveAsync(pending);
|
|
// This is deliberately outside the dispatch catch. Network failure after
|
|
// printing keeps completed_pending_ack and cannot become a print failure.
|
|
await AcknowledgeAsync(pending, journal, report, readState, log);
|
|
}
|
|
|
|
private static async Task RenewAsync(
|
|
PrintJobDto job, Func<int, string, string, string?, Task> report,
|
|
Action<string> log, TimeSpan interval, CancellationToken cancellation)
|
|
{
|
|
while (!cancellation.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(interval, cancellation);
|
|
if (!cancellation.IsCancellationRequested) await report(job.Id, job.ClaimToken, "printing", null);
|
|
}
|
|
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { return; }
|
|
catch (Exception ex) { log($"任务 {job.Id} 续期失败:{ex.Message}"); }
|
|
}
|
|
}
|
|
}
|