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 路径区分大小写,需要对静态文件做处理