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; /// /// PrintDot 本地桥接器 WebSocket 客户端。 /// URL 从 appsettings.json 的 PrintDot:Url 读取,可在运行时通过 覆盖。 /// 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("PrintDot:Url") ?? "ws://127.0.0.1:1122/ws"; return url.TrimEnd('/'); } public async Task> 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() == "printer_list") { var arr = doc["data"]?.AsArray(); if (arr != null) { return arr .Where(n => n != null) .Select(n => new PrintDotPrinter( Name: n!["name"]?.GetValue()?.Trim() ?? string.Empty, IsDefault: n["isDefault"]?.GetValue() ?? 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(); if (status == null) continue; if (status == "success") return; var rawMsg = resDoc?["message"]?.GetValue() ?? "PrintDot 打印失败"; throw new InvalidOperationException(EnhanceErrorMessage(rawMsg)); } } /// /// 将 PrintDot 返回的部分英文错误转换为带本地处理步骤的中文提示。 /// 与 web 端 printDotBridge.ts::enhancePrintDotErrorMessage 行为一致,方便桌面端用户自助排查。 /// 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 ReceiveTextAsync(ClientWebSocket ws, CancellationToken ct) { var buffer = new ArraySegment(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()); } }