diff --git a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs index b9ee5a96..2a8c8843 100644 --- a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs +++ b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs @@ -5,48 +5,58 @@ using System.Text; namespace YLErp.Modules.SwapModule { /// - /// 计息计算过程追踪器(默认关闭,零运行时成本)。 + /// 计息计算过程追踪器。默认完全关闭,零运行时成本;开启后逐日记录: + /// 计息起点、日终归档 ValueDate 地板、计息基数 dynomicPrincipal、当日利率、当日利息、累计利息。 + /// 便于定位"算出来一个数却不对"的根因,也便于把一次真实平仓的逐步过程打印出来与 Excel 对账。 /// - /// 在 CalcDailySimpleInterest / CalcDailyCompoundInterest 等计息函数中调用 - /// SwapCalcTrace.Record(...),开启后逐日记录:计息起点、日终归档 ValueDate 地板、 - /// 计息基数、当日利率、当日利息、累计利息。便于定位"算出来一个数却不对"的根因, - /// 也便于把一次真实平仓的逐步过程打印出来与 Excel 对账。 - /// - /// 用法(单元测试或临时排障): - /// SwapCalcTrace.IsEnabled = true; - /// SwapCalcTrace.Reset(); - /// ... 调用计息 ... - /// Console.WriteLine(SwapCalcTrace.Dump()); - /// SwapCalcTrace.IsEnabled = false; + /// 两种开启方式(互不冲突): + /// 1) 全局(单元测试):SwapCalcTrace.IsEnabled = true; SwapCalcTrace.Reset(); ...; SwapCalcTrace.Dump(); + /// 2) 请求(运行时排障,无需重启):经 SwapCalcTraceMiddleware,在 URL 追加 ?swaptrace=1 + /// 或请求头 X-SwapCalc-Trace: 1 即可,追踪文本会出现在响应头 X-SwapCalc-Trace(超长落 logs/ 并在头部给路径)。 /// public static class SwapCalcTrace { + // ---- 全局模式(单元测试,保持 ThreadStatic 兼容既有测试) ---- public static bool IsEnabled { get; set; } = false; [ThreadStatic] - private static List _lines; + private static List _globalLines; - private static List Lines => _lines ??= new List(); + private static List GlobalLines => _globalLines ??= new List(); - public static void Reset() => Lines.Clear(); + // ---- 请求模式(运行时排障,按请求异步流隔离,不会跨请求串扰) ---- + private static readonly AsyncLocal _reqBuf = new AsyncLocal(); + private static readonly AsyncLocal _reqOn = new AsyncLocal(); - public static void Header(string title) + 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() { - if (IsEnabled) Lines.Add($"== {title} =="); + GlobalLines.Clear(); + _reqBuf.Value = null; } - public static void Line(string text) - { - if (IsEnabled) Lines.Add(text); - } + public static void Header(string title) => Record($"== {title} =="); + public static void Line(string text) => Record(text); /// 记录某一计息日的明细。 public static void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated) + => Record($" [{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) - Lines.Add($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); + if (IsEnabled) GlobalLines.Add(s); + if (_reqOn.Value) + { + _reqBuf.Value ??= new StringBuilder(); + _reqBuf.Value.AppendLine(s); + } } - public static string Dump() => string.Join(Environment.NewLine, Lines); + public static string Dump() => string.Join(Environment.NewLine, GlobalLines); + + public static string DumpForRequest() => _reqBuf.Value?.ToString() ?? ""; } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7f313ff8..3bec844e 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1360,7 +1360,7 @@ namespace YLErp.Modules.SwapModule decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; decimal tdDynomicPrincipal = dynomicPrincipal; var calcDays = (endDate - startDate).Days; - if (SwapCalcTrace.IsEnabled) + if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled) { SwapCalcTrace.Header($"CalcDailySimpleInterest posId={position.id} mode={position.InterestMode} type={(position.InterestType == (int)InterestTypeEnum.复利 ? "复利" : "单利")}"); SwapCalcTrace.Line($" PosiStartDate={startDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} calcDays={calcDays} calcFirst={calcFirst} calcLast={calcLast}"); @@ -1409,7 +1409,7 @@ namespace YLErp.Modules.SwapModule } interest += interest1; tdinterest += tdinterest1; - if (SwapCalcTrace.IsEnabled) + if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled) SwapCalcTrace.Day(i, accrueDate, (decimal)floatRate, flowEvent.InterestPrincipal, interest1, interest); } } @@ -1438,7 +1438,6 @@ namespace YLErp.Modules.SwapModule decimal tdDynomicPrincipal = posiPrincipal; double floatRate = Convert.ToDouble(floateRate); var days = (endDate - tradeDate).Days; - LogFactory.GetLogger("test").Error("lksafhasdhfjas"); if (days % interestPeriod == 0) { LogFactory.GetLogger("test").Error("kluausdyfh"); diff --git a/YLErpWeb/Middleware/SwapCalcTraceMiddleware.cs b/YLErpWeb/Middleware/SwapCalcTraceMiddleware.cs new file mode 100644 index 00000000..d1776a0f --- /dev/null +++ b/YLErpWeb/Middleware/SwapCalcTraceMiddleware.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using System; +using System.IO; +using System.Threading.Tasks; +using YLErp.Helpers; +using YLErp.Modules.SwapModule; + +namespace YLErp.Web.Middleware +{ + /// + /// 计息计算过程追踪中间件(运行时排障用,无需重启、无需改前端)。 + /// + /// 开启方式(三选一): + /// 1) 请求 URL 追加 ?swaptrace=1 + /// 2) 请求头带 X-SwapCalc-Trace: 1 + /// 3) 站点根目录 App_Data/swapcalctrace.on 标记文件存在(对所有请求开启,谨慎使用) + /// + /// 开启后,SwapCalcTrace 会在计息函数中逐日记录过程。本中间件在响应发出前把追踪文本写入: + /// - 响应头 X-SwapCalc-Trace(开发者在浏览器"网络"面板即可看到,超长则写入日志并在头部给文件路径) + /// - 服务器日志 logs/swapcalctrace-YYYYMMDD.log(完整文本,便于 tail/检索) + /// + /// 性能影响可忽略(仅被显式开启的少数请求触发,且每次只是拼字符串)。 + /// + public class SwapCalcTraceMiddleware + { + private readonly RequestDelegate _next; + private readonly IWebHostEnvironment _env; + + public SwapCalcTraceMiddleware(RequestDelegate next, IWebHostEnvironment env) + { + _next = next; + _env = env; + } + + public async Task InvokeAsync(HttpContext context) + { + bool on = context.Request.Query["swaptrace"] == "1" + || context.Request.Headers["X-SwapCalc-Trace"] == "1"; + if (!on) on = MarkerFileExists(); + if (on) SwapCalcTrace.EnableForRequest(true); + + try + { + if (on) + { + // OnStarting 在响应体写出前触发,确保整个请求(含计息)都已被追踪 + context.Response.OnStarting(() => + { + FlushTrace(context); + return Task.CompletedTask; + }); + } + await _next(context); + } + finally + { + if (on) SwapCalcTrace.ClearForRequest(); + } + } + + private void FlushTrace(HttpContext context) + { + var trace = SwapCalcTrace.DumpForRequest(); + if (string.IsNullOrEmpty(trace)) return; + + var logger = LogFactory.GetLogger("SwapCalcTrace"); + logger.Info($"[SwapCalcTrace] {context.Request.Path}\n{trace}"); + + var headerVal = trace; + if (trace.Length > 4000) + { + try + { + var dir = Path.Combine(_env.ContentRootPath, "logs"); + Directory.CreateDirectory(dir); + var file = Path.Combine(dir, $"swapcalctrace-{DateTime.Now:yyyyMMdd}.log"); + File.AppendAllText(file, + $"=== {DateTime.Now:yyyy-MM-dd HH:mm:ss} {context.Request.Path} ===\n{trace}\n\n"); + headerVal = $"(trace truncated, full in {file})\n" + trace.Substring(0, 4000); + } + catch + { + headerVal = trace.Substring(0, 4000); + } + } + + try { context.Response.Headers["X-SwapCalc-Trace"] = headerVal; } catch { } + } + + private bool MarkerFileExists() + { + try + { + return File.Exists(Path.Combine(_env.ContentRootPath, "App_Data", "swapcalctrace.on")); + } + catch + { + return false; + } + } + } +} diff --git a/YLErpWeb/Program.cs b/YLErpWeb/Program.cs index 5cc71026..696fcd84 100644 --- a/YLErpWeb/Program.cs +++ b/YLErpWeb/Program.cs @@ -171,6 +171,9 @@ try app.UseMiddleware(); + // 计息计算过程追踪(?swaptrace=1 / X-SwapCalc-Trace:1 开启,详见 SwapCalcTraceMiddleware) + app.UseMiddleware(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { // Linux 路径区分大小写,需要对静态文件做处理