131 lines
5.3 KiB
C#
131 lines
5.3 KiB
C#
using System.Net.WebSockets;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Nodes;
|
||
using Microsoft.Extensions.Configuration;
|
||
using YY.Admin.Core.Services;
|
||
|
||
namespace YY.Admin.Services.Service.Print;
|
||
|
||
/// <summary>
|
||
/// PrintDot 本地桥接器 WebSocket 客户端。
|
||
/// URL 从 appsettings.json 的 PrintDot:Url 读取,可在运行时通过 <see cref="PrintDotSettings"/> 覆盖。
|
||
/// </summary>
|
||
public class PrintDotService : IPrintDotService
|
||
{
|
||
private readonly IConfiguration _config;
|
||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||
|
||
public PrintDotService(IConfiguration config)
|
||
{
|
||
_config = config;
|
||
}
|
||
|
||
private string ResolveWsUrl()
|
||
{
|
||
var url = PrintDotSettings.Current?.WsUrl
|
||
?? _config.GetValue<string>("PrintDot:Url")
|
||
?? "ws://127.0.0.1:1122/ws";
|
||
return url.TrimEnd('/');
|
||
}
|
||
|
||
public async Task<IReadOnlyList<PrintDotPrinter>> GetPrintersAsync(CancellationToken ct = default)
|
||
{
|
||
var wsUrl = ResolveWsUrl();
|
||
using var ws = new ClientWebSocket();
|
||
await ws.ConnectAsync(new Uri(wsUrl), ct);
|
||
|
||
// 连接后服务端立即推送 printer_list
|
||
var json = await ReceiveTextAsync(ws, ct);
|
||
var doc = JsonNode.Parse(json);
|
||
if (doc?["type"]?.GetValue<string>() == "printer_list")
|
||
{
|
||
var arr = doc["data"]?.AsArray();
|
||
if (arr != null)
|
||
{
|
||
return arr
|
||
.Where(n => n != null)
|
||
.Select(n => new PrintDotPrinter(
|
||
Name: n!["name"]?.GetValue<string>()?.Trim() ?? string.Empty,
|
||
IsDefault: n["isDefault"]?.GetValue<bool>() ?? false))
|
||
.Where(p => !string.IsNullOrWhiteSpace(p.Name))
|
||
.ToList();
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
|
||
public async Task PrintAsync(string printerName, string pdfBase64, string jobName = "QH-MES", int copies = 1, CancellationToken ct = default)
|
||
{
|
||
// 去掉 data: 前缀
|
||
var content = pdfBase64.Trim();
|
||
var comma = content.IndexOf(',');
|
||
if (content.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
|
||
content = content[(comma + 1)..];
|
||
|
||
var payload = new
|
||
{
|
||
printer = printerName,
|
||
content,
|
||
job = new { name = jobName, copies = Math.Max(1, copies) }
|
||
};
|
||
|
||
var wsUrl = ResolveWsUrl();
|
||
using var ws = new ClientWebSocket();
|
||
await ws.ConnectAsync(new Uri(wsUrl), ct);
|
||
|
||
// 先等服务端推送 printer_list 再发任务
|
||
await ReceiveTextAsync(ws, ct);
|
||
|
||
var msg = JsonSerializer.Serialize(payload, JsonOpts);
|
||
await ws.SendAsync(Encoding.UTF8.GetBytes(msg), WebSocketMessageType.Text, true, ct);
|
||
|
||
// 等待打印结果(最多 3 分钟)
|
||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||
cts.CancelAfter(TimeSpan.FromMinutes(3));
|
||
while (ws.State == WebSocketState.Open)
|
||
{
|
||
var response = await ReceiveTextAsync(ws, cts.Token);
|
||
var resDoc = JsonNode.Parse(response);
|
||
var status = resDoc?["status"]?.GetValue<string>();
|
||
if (status == null) continue;
|
||
if (status == "success") return;
|
||
var rawMsg = resDoc?["message"]?.GetValue<string>() ?? "PrintDot 打印失败";
|
||
throw new InvalidOperationException(EnhanceErrorMessage(rawMsg));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 PrintDot 返回的部分英文错误转换为带本地处理步骤的中文提示。
|
||
/// 与 web 端 printDotBridge.ts::enhancePrintDotErrorMessage 行为一致,方便桌面端用户自助排查。
|
||
/// </summary>
|
||
private static string EnhanceErrorMessage(string raw)
|
||
{
|
||
var m = (raw ?? string.Empty).Trim();
|
||
// 缺 SumatraPDF:是 PrintDot 客户端最常见的初始化错误
|
||
if (System.Text.RegularExpressions.Regex.IsMatch(m, @"SumatraPDF\.exe not found", System.Text.RegularExpressions.RegexOptions.IgnoreCase)
|
||
|| System.Text.RegularExpressions.Regex.IsMatch(m, "SUMATRAPDF_PATH", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||
{
|
||
return m + "。\n本地处理:PrintDot 依赖 SumatraPDF 静默打印 PDF。请安装 SumatraPDF 后任选其一:\n" +
|
||
"① 将 SumatraPDF.exe 放在 PrintDot 客户端 exe 同目录;\n" +
|
||
"② 或将 SumatraPDF 安装目录加入系统 PATH;\n" +
|
||
"③ 或设置用户/系统环境变量 SUMATRAPDF_PATH 指向 SumatraPDF.exe 的完整路径;\n" +
|
||
"然后重启 PrintDot 桥接器即可。";
|
||
}
|
||
return m;
|
||
}
|
||
|
||
private static async Task<string> ReceiveTextAsync(ClientWebSocket ws, CancellationToken ct)
|
||
{
|
||
var buffer = new ArraySegment<byte>(new byte[64 * 1024]);
|
||
using var ms = new System.IO.MemoryStream();
|
||
WebSocketReceiveResult result;
|
||
do
|
||
{
|
||
result = await ws.ReceiveAsync(buffer, ct);
|
||
ms.Write(buffer.Array!, buffer.Offset, result.Count);
|
||
} while (!result.EndOfMessage);
|
||
return Encoding.UTF8.GetString(ms.ToArray());
|
||
}
|
||
}
|