From 97029897c4b4888fddc555f58a6a923cada477c5 Mon Sep 17 00:00:00 2001 From: hjhan Date: Sat, 8 Aug 2026 15:57:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(swap):=20SwapCalcTrace=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=BF=90=E8=A1=8C=E6=97=B6=E6=8C=89=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=BC=80=E5=85=B3=20+=20=E5=93=8D=E5=BA=94=E5=A4=B4=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=EF=BC=88=E6=97=A0=E9=9C=80=E9=87=8D=E5=90=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SwapCalcTrace 改为双模式:保留全局静态开关(单元测试用 IsEnabled/Dump), 新增请求级 AsyncLocal 缓冲(EnableForRequest/DumpForRequest/ClearForRequest), 由中间件按请求开关,不跨请求串扰。 - 新增 SwapCalcTraceMiddleware:开启方式三选一——URL ?swaptrace=1 / 请求头 X-SwapCalc-Trace:1 / 标记文件 App_Data/swapcalctrace.on(无需重启); Response.OnStarting 时把逐日计息过程写入响应头 X-SwapCalc-Trace(超长落 logs/swapcalctrace-YYYYMMDD.log 并在头给路径),同时写服务器日志。 开发者在浏览器“网络”面板即可查看,零前端改码、零行为影响、性能可忽略。 - Program.cs 在 ExceptionMiddleware 后注册该中间件。 - 删除 CalcDailyCompoundInterestByEod 遗留垃圾日志 LogFactory.GetLogger("test").Error("lksafhasdhfjas")。 - CalcDailySimpleInterest 两处追踪守卫改为 IsEnabled||IsRequestEnabled。 - 既有 PrepaidPrincipalCloseTraceTest / ClosingChainTraceTest 2/2 仍全绿。 回答用户两点:① dynomicPrincipal 公式字段含义;② 原 Trace 需重启才能开, 现改为前端 otcdebug 式(?swaptrace=1) 即可在响应头看到逐步计息过程。 --- YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs | 58 ++++++---- .../Modules/SwapModule/SwapDealService.cs | 5 +- .../Middleware/SwapCalcTraceMiddleware.cs | 103 ++++++++++++++++++ YLErpWeb/Program.cs | 3 + 4 files changed, 142 insertions(+), 27 deletions(-) create mode 100644 YLErpWeb/Middleware/SwapCalcTraceMiddleware.cs 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 路径区分大小写,需要对静态文件做处理