using System; using System.Collections.Generic; using System.Text; using YLErp.Helpers; namespace YLErp.Modules.SwapModule { /// /// 计息计算过程追踪器。采用两层设计: /// /// ① 常驻关键日志(Critical):记录"走了哪条分支 / orginPv 重映射 / preEod.id==0 播种 / /// closePercent 语义翻转(A→B) / 计息基数 dynomicPrincipal 分解 / ValueDate 地板 / /// 最终四舍五入输出 / 逐日累加明细"。这些信息**无条件**经 IYcLogger.Info 落盘, /// 与开关无关——出问题时事后翻日志即可定位,不必事前开开关(否则"出问题了已经晚了")。 /// /// ② 开关态明细(Header/Line/Day,经 IsEnabled 或请求开关):仅在主动排障时把上述信息 /// 额外汇进内存 buffer,便于单元测试 Dump() 断言,或运行时在响应头 X-SwapCalc-Trace /// 一次性看全貌(URL 追加 ?swaptrace=1 / 请求头 X-SwapCalc-Trace:1 / 标记文件 /// App_Data/swapcalctrace.on 三选一,无需重启)。开关态不影响日志落盘。 /// /// 关键日志常驻后,本类的"零成本"只针对开关态内存 buffer;Critical 的日志写入是常态成本, /// 但因交易笔数极少、单行极小,可忽略。 /// public static class SwapCalcTrace { private static readonly IYcLogger _logger = LogFactory.GetLogger("SwapCalc"); // ---- 全局模式(单元测试,保持 ThreadStatic 兼容既有测试) ---- public static bool IsEnabled { get; set; } = false; [ThreadStatic] private static List _globalLines; private static List GlobalLines => _globalLines ??= new List(); // ---- 请求模式(运行时排障,按请求异步流隔离,不会跨请求串扰) ---- private static readonly AsyncLocal _reqBuf = new AsyncLocal(); private static readonly AsyncLocal _reqOn = new AsyncLocal(); public static void EnableForRequest(bool on = true) => _reqOn.Value = on; public static bool IsRequestEnabled => _reqOn.Value; public static void ClearForRequest() => _reqBuf.Value = null; public static void Reset() { GlobalLines.Clear(); _reqBuf.Value = null; } /// /// 常驻关键日志:始终落盘(IYcLogger.Info);开关打开时额外进 buffer 供实时查看/单测断言。 /// 这是事后定位根因的主通道,不应依赖开关。 /// public static void Critical(string s) { _logger.Info(s); if (IsEnabled) GlobalLines.Add(s); if (_reqOn.Value) { _reqBuf.Value ??= new StringBuilder(); _reqBuf.Value.AppendLine(s); } } // ---- 以下为开关态(verbose)工具,仅用于单测 Dump 与 ?swaptrace=1 的逐日明细汇总 ---- public static void Header(string title) => Record($"== {title} =="); public static void Line(string text) => Record(text); /// 逐日明细。同样常驻落盘——这是"为何 accrued N 天而非 M 天"的直接证据,必须事后可查。 public static void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated) => Critical($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); private static void Record(string s) { if (IsEnabled) GlobalLines.Add(s); if (_reqOn.Value) { _reqBuf.Value ??= new StringBuilder(); _reqBuf.Value.AppendLine(s); } } public static string Dump() => string.Join(Environment.NewLine, GlobalLines); public static string DumpForRequest() => _reqBuf.Value?.ToString() ?? ""; } }