using System; using System.IO; using System.Text; namespace Aivfo.OperationLog { /// /// 本地文件写入:调试级日志(14§4)+ 运行兜底(Kafka 发不出、队列满)+ 组件自身错误。 /// 按天滚动;内部加锁串行写;全 try 兜底,绝不抛给业务。 /// internal sealed class LocalFileWriter { private readonly string _dir; private readonly object _lock = new object(); public LocalFileWriter(string dir) { _dir = string.IsNullOrEmpty(dir) ? "oplog_local" : dir; try { Directory.CreateDirectory(_dir); } catch { /* ignore */ } } /// 调试级一行(结构化文本,不进 Kafka)。 public void WriteDebug(OperationLogMessage m) { var line = $"{DateTimeOffset.FromUnixTimeMilliseconds(m.Time):yyyy-MM-dd HH:mm:ss.fff}\t" + $"[DEBUG]\ttrace={m.TraceId}\tmodule={m.Module}\top={m.Operation}\t" + $"house={m.HouseSn}\twell={m.WellSn}\tresult={m.Result}\t" + $"in={Truncate(m.Input)}\tout={Truncate(m.Output)}\terr={m.Error}"; Write("debug", line); } /// 兜底:完整 JSON 落本地(Kafka 不可用 / 队列满丢弃前保留)。 public void WriteFallback(string json) { Write("fallback", json); } /// 组件自身错误(连不上 Kafka、序列化失败等)。 public void WriteSelfError(string msg) { Write("error", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}\t{msg}"); } private void Write(string kind, string line) { try { var path = Path.Combine(_dir, $"oplog-{kind}-{DateTime.Now:yyyyMMdd}.log"); lock (_lock) { File.AppendAllText(path, line + Environment.NewLine, Encoding.UTF8); } } catch { /* 本地写失败也不能拖垮业务 */ } } // ===== G3-3 / 14§11 兜底补送原语 ===== /// 列出待补送的兜底文件(不含正在补送的 .resending)。按文件名升序≈时间升序。 public string[] ListFallbackFiles() { try { lock (_lock) { if (!Directory.Exists(_dir)) return Array.Empty(); var files = Directory.GetFiles(_dir, "oplog-fallback-*.log"); Array.Sort(files, StringComparer.OrdinalIgnoreCase); return files; } } catch { return Array.Empty(); } } /// /// 认领一个兜底文件:原子重命名为 .resending(避免与 append 竞争、避免并发重复补送)。 /// 成功返回新路径,失败(已被认领/被占用)返回 null。 /// public string ClaimFallback(string path) { try { lock (_lock) { if (!File.Exists(path)) return null; var dst = path + "." + DateTime.Now.ToString("HHmmssfff") + ".resending"; File.Move(path, dst); return dst; } } catch { return null; } } /// 读取认领文件的所有非空行。 public string[] ReadLines(string path) { try { return File.ReadAllLines(path, Encoding.UTF8); } catch { return Array.Empty(); } } /// 删除文件(补送成功后清理认领文件)。 public void DeleteQuietly(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { /* ignore */ } } private static string Truncate(string s, int max = 500) { if (string.IsNullOrEmpty(s)) return s; return s.Length <= max ? s : s.Substring(0, max) + "...(" + s.Length + ")"; } } }