Files
zszq-trs/YLErpWeb/Middleware/SwapCalcTraceMiddleware.cs
T
hjhan 97029897c4 feat(swap): SwapCalcTrace 支持运行时按请求开关 + 响应头可见(无需重启)
- 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) 即可在响应头看到逐步计息过程。
2026-08-08 15:57:00 +08:00

104 lines
3.7 KiB
C#

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
{
/// <summary>
/// 计息计算过程追踪中间件(运行时排障用,无需重启、无需改前端)。
///
/// 开启方式(三选一):
/// 1) 请求 URL 追加 ?swaptrace=1
/// 2) 请求头带 X-SwapCalc-Trace: 1
/// 3) 站点根目录 App_Data/swapcalctrace.on 标记文件存在(对所有请求开启,谨慎使用)
///
/// 开启后,SwapCalcTrace 会在计息函数中逐日记录过程。本中间件在响应发出前把追踪文本写入:
/// - 响应头 X-SwapCalc-Trace(开发者在浏览器"网络"面板即可看到,超长则写入日志并在头部给文件路径)
/// - 服务器日志 logs/swapcalctrace-YYYYMMDD.log(完整文本,便于 tail/检索)
///
/// 性能影响可忽略(仅被显式开启的少数请求触发,且每次只是拼字符串)。
/// </summary>
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;
}
}
}
}