From bec5bfd0dd49d7a819ff36395fac77466cdbfc8a Mon Sep 17 00:00:00 2001 From: xiangning Date: Tue, 7 Jul 2026 08:55:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(scripting):=20T3=20code=20quality=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: 脚本运行时异常未捕获导致 500(新增 catch-all Failed 日志+包装) - I1: DateTime.TryParse 误判日期样字符串(改为严格 ISO-8601 正则前置) - M1: IScriptExecutionService XML 注释补 Logger - M2: TestExecuteAsync → ExecuteTestAsync 命名一致性 - M7: 递归深度 ++ 移入 try 块保证 finally 配对 T3 code quality review fixes --- .../Services/IScriptExecutionService.cs | 4 +- .../Services/ScriptExecutionService.cs | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/modules/scripting/Application/Services/IScriptExecutionService.cs b/src/modules/scripting/Application/Services/IScriptExecutionService.cs index 0cc9eb7..c042291 100644 --- a/src/modules/scripting/Application/Services/IScriptExecutionService.cs +++ b/src/modules/scripting/Application/Services/IScriptExecutionService.cs @@ -2,7 +2,7 @@ using Scripting.Application.DTOs; namespace Scripting.Application.Services; -/// 脚本执行服务:编译+运行 C# 脚本,注入 MediatR/CurrentUser/CancellationToken,并写执行日志。 +/// 脚本执行服务:编译+运行 C# 脚本,注入 MediatR/CurrentUser/CancellationToken/Logger,并写执行日志。 public interface IScriptExecutionService { /// 按 Id 执行已发布脚本(按钮动作链用)。 @@ -12,5 +12,5 @@ public interface IScriptExecutionService Task ExecuteByNameAsync(string scriptName, string inputJson, CancellationToken ct); /// 测试运行:不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。 - Task TestExecuteAsync(Guid scriptId, string inputJson, CancellationToken ct); + Task ExecuteTestAsync(Guid scriptId, string inputJson, CancellationToken ct); } diff --git a/src/modules/scripting/Application/Services/ScriptExecutionService.cs b/src/modules/scripting/Application/Services/ScriptExecutionService.cs index 8eac9dc..0f8b603 100644 --- a/src/modules/scripting/Application/Services/ScriptExecutionService.cs +++ b/src/modules/scripting/Application/Services/ScriptExecutionService.cs @@ -57,6 +57,12 @@ public partial class ScriptExecutionService( RegexOptions.Compiled | RegexOptions.CultureInvariant)] private static partial Regex ForbiddenApiRegex { get; } + /// 严格 ISO-8601 日期正则:要求 YYYY-MM-DD(可带时间/时区)。防止 "2026"/"2026-07" 等日期样字符串误判为 DateTime。 + [GeneratedRegex( + @"^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$", + RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex Iso8601Regex { get; } + // ── 公共入口 ────────────────────────────────────────────────────────── /// @@ -68,7 +74,7 @@ public partial class ScriptExecutionService( => ExecuteAsync(null, scriptName, inputJson, requirePublished: true, isTest: false, ct); /// - public Task TestExecuteAsync(Guid scriptId, string inputJson, CancellationToken ct) + public Task ExecuteTestAsync(Guid scriptId, string inputJson, CancellationToken ct) => ExecuteAsync(scriptId, null, inputJson, requirePublished: false, isTest: true, ct); // ── 核心执行 ────────────────────────────────────────────────────────── @@ -117,12 +123,13 @@ public partial class ScriptExecutionService( throw new BusinessException($"脚本包含禁用 API: {forbiddenMatch.Value}"); } - // 深度计数 +1(finally 归位) - _recursionDepth.Value++; var stopwatch = Stopwatch.StartNew(); var logger = new ScriptLogger(); try { + // 深度计数 +1(finally 归位),置于 try 内保证与 finally 配对 + _recursionDepth.Value++; + // 超时令牌:默认 5s,上限 30s var timeoutMs = function.TimeoutMs <= 0 ? DefaultTimeoutMs : Math.Min(function.TimeoutMs, MaxTimeoutMs); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); @@ -170,6 +177,17 @@ public partial class ScriptExecutionService( inputJson, null, ex.Message, stopwatch.ElapsedMilliseconds, logger.ToText(), ct); throw new BusinessException($"脚本执行失败: {ex.Message}"); } + catch (Exception ex) + { + // 脚本运行时异常(NullReference/DivideByZero/IndexOutOfRange/InvalidCast/KeyNotFound 等): + // 统一记录 Failed 日志并包装为 BusinessException,避免原始异常冒泡成 500 带堆栈。 + // 超时场景下 ct 可能已取消,用 CancellationToken.None 保证日志落库 + stopwatch.Stop(); + await AddLogAsync(function, isTest, ScriptExecutionStatus.Failed, + inputJson, null, ex.Message, stopwatch.ElapsedMilliseconds, logger.ToText(), + ct.IsCancellationRequested ? CancellationToken.None : ct); + throw new BusinessException($"脚本执行失败: {ex.Message}"); + } finally { _recursionDepth.Value--; @@ -301,16 +319,17 @@ public partial class ScriptExecutionService( { var str = element.GetString() ?? string.Empty; - // ISO 8601 日期优先 - if (DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt)) - { - return dt; - } - // Guid + // Guid 优先(Guid 串不会匹配 ISO-8601 日期正则) if (Guid.TryParse(str, out var guid)) { return guid; } + // 严格 ISO-8601 日期:先正则白名单,再 TryParse,避免 "2026"/"2026-07" 等日期样字符串误判 + if (Iso8601Regex.IsMatch(str) + && DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt)) + { + return dt; + } return str; }