fix(scripting): T3 code quality 修复

- 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
This commit is contained in:
xiangning 2026-07-07 08:55:16 +08:00
parent 624579cf78
commit bec5bfd0dd
2 changed files with 30 additions and 11 deletions

View File

@ -2,7 +2,7 @@ using Scripting.Application.DTOs;
namespace Scripting.Application.Services; namespace Scripting.Application.Services;
/// <summary>脚本执行服务:编译+运行 C# 脚本,注入 MediatR/CurrentUser/CancellationToken,并写执行日志。</summary> /// <summary>脚本执行服务:编译+运行 C# 脚本,注入 MediatR/CurrentUser/CancellationToken/Logger,并写执行日志。</summary>
public interface IScriptExecutionService public interface IScriptExecutionService
{ {
/// <summary>按 Id 执行已发布脚本(按钮动作链用)。</summary> /// <summary>按 Id 执行已发布脚本(按钮动作链用)。</summary>
@ -12,5 +12,5 @@ public interface IScriptExecutionService
Task<ScriptExecutionResult> ExecuteByNameAsync(string scriptName, string inputJson, CancellationToken ct); Task<ScriptExecutionResult> ExecuteByNameAsync(string scriptName, string inputJson, CancellationToken ct);
/// <summary>测试运行:不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。</summary> /// <summary>测试运行:不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。</summary>
Task<ScriptExecutionResult> TestExecuteAsync(Guid scriptId, string inputJson, CancellationToken ct); Task<ScriptExecutionResult> ExecuteTestAsync(Guid scriptId, string inputJson, CancellationToken ct);
} }

View File

@ -57,6 +57,12 @@ public partial class ScriptExecutionService(
RegexOptions.Compiled | RegexOptions.CultureInvariant)] RegexOptions.Compiled | RegexOptions.CultureInvariant)]
private static partial Regex ForbiddenApiRegex { get; } private static partial Regex ForbiddenApiRegex { get; }
/// <summary>严格 ISO-8601 日期正则:要求 YYYY-MM-DD(可带时间/时区)。防止 "2026"/"2026-07" 等日期样字符串误判为 DateTime。</summary>
[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; }
// ── 公共入口 ────────────────────────────────────────────────────────── // ── 公共入口 ──────────────────────────────────────────────────────────
/// <inheritdoc /> /// <inheritdoc />
@ -68,7 +74,7 @@ public partial class ScriptExecutionService(
=> ExecuteAsync(null, scriptName, inputJson, requirePublished: true, isTest: false, ct); => ExecuteAsync(null, scriptName, inputJson, requirePublished: true, isTest: false, ct);
/// <inheritdoc /> /// <inheritdoc />
public Task<ScriptExecutionResult> TestExecuteAsync(Guid scriptId, string inputJson, CancellationToken ct) public Task<ScriptExecutionResult> ExecuteTestAsync(Guid scriptId, string inputJson, CancellationToken ct)
=> ExecuteAsync(scriptId, null, inputJson, requirePublished: false, isTest: true, ct); => ExecuteAsync(scriptId, null, inputJson, requirePublished: false, isTest: true, ct);
// ── 核心执行 ────────────────────────────────────────────────────────── // ── 核心执行 ──────────────────────────────────────────────────────────
@ -117,12 +123,13 @@ public partial class ScriptExecutionService(
throw new BusinessException($"脚本包含禁用 API: {forbiddenMatch.Value}"); throw new BusinessException($"脚本包含禁用 API: {forbiddenMatch.Value}");
} }
// 深度计数 +1(finally 归位)
_recursionDepth.Value++;
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var logger = new ScriptLogger(); var logger = new ScriptLogger();
try try
{ {
// 深度计数 +1(finally 归位),置于 try 内保证与 finally 配对
_recursionDepth.Value++;
// 超时令牌:默认 5s,上限 30s // 超时令牌:默认 5s,上限 30s
var timeoutMs = function.TimeoutMs <= 0 ? DefaultTimeoutMs : Math.Min(function.TimeoutMs, MaxTimeoutMs); var timeoutMs = function.TimeoutMs <= 0 ? DefaultTimeoutMs : Math.Min(function.TimeoutMs, MaxTimeoutMs);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
@ -170,6 +177,17 @@ public partial class ScriptExecutionService(
inputJson, null, ex.Message, stopwatch.ElapsedMilliseconds, logger.ToText(), ct); inputJson, null, ex.Message, stopwatch.ElapsedMilliseconds, logger.ToText(), ct);
throw new BusinessException($"脚本执行失败: {ex.Message}"); 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 finally
{ {
_recursionDepth.Value--; _recursionDepth.Value--;
@ -301,16 +319,17 @@ public partial class ScriptExecutionService(
{ {
var str = element.GetString() ?? string.Empty; var str = element.GetString() ?? string.Empty;
// ISO 8601 日期优先 // Guid 优先(Guid 串不会匹配 ISO-8601 日期正则)
if (DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt))
{
return dt;
}
// Guid
if (Guid.TryParse(str, out var guid)) if (Guid.TryParse(str, out var guid))
{ {
return 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; return str;
} }