feat: 变量删除时校验是否有规则引用,有则阻止

This commit is contained in:
尹峰
2026-07-30 13:29:16 +08:00
5 changed files with 2105 additions and 106 deletions
@@ -1,4 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Scripting;
using Newtonsoft.Json;
using System;
@@ -46,7 +49,119 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public static class RuleCompiler
{
static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
private static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
/// <summary>
/// 高风险方法黑名单。
/// 这里按方法名做语法层拦截,覆盖数据库写入、原生 SQL、反射、文件、进程、服务定位等入口。
/// </summary>
private static readonly HashSet<string> ForbiddenInvocationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"SaveChanges",
"SaveChangesAsync",
"Add",
"AddAsync",
"AddRange",
"AddRangeAsync",
"AddOrUpdate",
"Update",
"UpdateRange",
"Remove",
"RemoveRange",
"Attach",
"AttachRange",
"Set",
"Entry",
"ExecuteSqlRaw",
"ExecuteSqlRawAsync",
"ExecuteSqlInterpolated",
"ExecuteSqlInterpolatedAsync",
"ExecuteSql",
"ExecuteSqlAsync",
"ExecuteSqlCommand",
"ExecuteSqlCommandAsync",
"ExecuteStoreCommand",
"ExecuteStoreQuery",
"ExecuteDelete",
"ExecuteDeleteAsync",
"ExecuteUpdate",
"ExecuteUpdateAsync",
"FromSqlRaw",
"FromSqlRawAsync",
"FromSqlInterpolated",
"FromSqlInterpolatedAsync",
"BulkInsert",
"BulkUpdate",
"BulkDelete",
"BulkMerge",
"GetType",
"Invoke",
"InvokeMember",
"GetMethod",
"GetProperty",
"GetField",
"CreateInstance",
"Load",
"LoadFrom",
"ReadAllText",
"ReadAllLines",
"ReadAllBytes",
"WriteAllText",
"WriteAllLines",
"WriteAllBytes",
"AppendAllText",
"Delete",
"Move",
"Copy",
"Open",
"OpenRead",
"OpenWrite",
"Create",
"CreateText",
"Start",
"Kill",
"Exit",
"GetEnvironmentVariable",
"SetEnvironmentVariable",
"GetService",
"GetRequiredService",
"CreateScope",
"Sleep"
};
/// <summary>
/// 高风险成员黑名单。
/// DbContext.Database、ChangeTracker 等成员会绕过只读查询约束或暴露底层状态,禁止脚本访问。
/// </summary>
private static readonly HashSet<string> ForbiddenMemberNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Database",
"ChangeTracker",
"Assembly",
"AppDomain"
};
/// <summary>
/// 高风险类型或标识符黑名单。
/// 用于拦截 File、Process、Environment 等直接作为类型或变量名出现的绕过方式。
/// </summary>
private static readonly HashSet<string> ForbiddenTypeOrIdentifierNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Activator",
"Assembly",
"AppDomain",
"Environment",
"File",
"Directory",
"Path",
"Process",
"HttpClient",
"WebClient",
"WebRequest",
"Socket",
"Thread",
"GC"
};
/// <summary>
/// 校验并编译规则表达式。
@@ -79,6 +194,109 @@ namespace YLErp.Modules.RiskEngine
}
}
/// <summary>
/// 校验变量取值表达式。
/// 变量表达式只要求能编译为 object,不在保存时执行,避免依赖真实交易和数据库数据。
/// </summary>
public static RuleCompileResult ValidateVariableExpression(string variableExpr)
{
if (string.IsNullOrWhiteSpace(variableExpr))
return RuleCompileResult.Fail("变量取值表达式不能为空");
var compiled = CompileVariableExpression(variableExpr, out var compileErrorMessage);
if (compiled == null)
return RuleCompileResult.Fail(compileErrorMessage ?? "变量取值表达式编译失败");
return RuleCompileResult.Ok(null);
}
/// <summary>
/// 编译变量取值表达式。
/// 结构化规则执行时使用,表达式返回值由执行器按变量 DataType 统一转换。
/// </summary>
public static Func<RiskContext, object> CompileValueExpression(string variableExpr, out string errorMessage)
{
if (string.IsNullOrWhiteSpace(variableExpr))
{
errorMessage = "变量取值表达式不能为空";
return null;
}
return CompileVariableExpression(variableExpr, out errorMessage);
}
/// <summary>
/// 编译变量取值表达式。优先兼容原有单表达式写法;失败后再按多语句脚本编译。
/// 多语句脚本支持用 return 表达最终值,编译前会转换为 Roslyn Script 稳定支持的末尾表达式。
/// </summary>
private static Func<RiskContext, object> CompileVariableExpression(string variableExpr, out string errorMessage)
{
if (IsLikelyVariableScript(variableExpr))
{
var normalizedScript = NormalizeVariableScript(variableExpr);
var scriptCompiled = CompileScript<object>(null, normalizedScript, out errorMessage);
return scriptCompiled;
}
// 原有变量表达式大多是单个查询表达式,包成 object 后可直接作为脚本返回值。
var expressionScript = $"(object)({variableExpr})";
var compiled = CompileScript<object>(null, expressionScript, out var expressionErrorMessage);
if (compiled != null)
{
errorMessage = null;
return compiled;
}
// 单表达式编译失败时,再按多语句脚本兜底,兼容包含局部变量、if、throw 的复杂变量。
var normalizedFallbackScript = NormalizeVariableScript(variableExpr);
compiled = CompileScript<object>(null, normalizedFallbackScript, out var scriptErrorMessage);
if (compiled != null)
{
errorMessage = null;
return compiled;
}
errorMessage = $"单表达式编译失败:{expressionErrorMessage};多语句脚本编译失败:{scriptErrorMessage}";
return null;
}
/// <summary>
/// 粗略判断变量表达式是否更像多语句脚本。
/// 多语句脚本不能再包成 (object)(...),需要直接按 C# Script 编译。
/// </summary>
private static bool IsLikelyVariableScript(string variableExpr)
{
return variableExpr.IndexOf(';') >= 0 ||
variableExpr.IndexOf("return", StringComparison.OrdinalIgnoreCase) >= 0 ||
variableExpr.IndexOf("throw", StringComparison.OrdinalIgnoreCase) >= 0 ||
variableExpr.IndexOf("if", StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
/// 规范化多语句变量脚本。
/// Roslyn Script 更稳定的返回方式是“最后一行表达式”,这里把末尾 return 表达式转换为末尾表达式。
/// </summary>
private static string NormalizeVariableScript(string variableExpr)
{
var syntaxTree = CSharpSyntaxTree.ParseText(variableExpr, new CSharpParseOptions(kind: SourceCodeKind.Script));
var root = syntaxTree.GetCompilationUnitRoot();
var lastStatement = root.Members
.OfType<GlobalStatementSyntax>()
.Select(statement => statement.Statement)
.LastOrDefault();
if (lastStatement is ReturnStatementSyntax returnStatement && returnStatement.Expression != null)
{
var returnText = returnStatement.ToFullString();
var expressionText = returnStatement.Expression.ToFullString();
var index = variableExpr.LastIndexOf(returnText, StringComparison.Ordinal);
if (index >= 0)
return variableExpr.Substring(0, index) + expressionText;
}
return variableExpr;
}
/// <summary>
/// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
/// </summary>
@@ -115,13 +333,28 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 用 Roslyn 编译 C# 脚本代码为可执行委托
/// 用 Roslyn 编译 C# 布尔表达式为可执行委托
/// </summary>
private static Func<RiskContext, bool> CompileScript(long? ruleId, string scriptCode, out string errorMessage)
{
return CompileScript<bool>(ruleId, scriptCode, out errorMessage);
}
/// <summary>
/// 用 Roslyn 编译 C# 脚本代码为指定返回类型的可执行委托。
/// 规则公式编译为 bool,变量取值表达式编译为 object。
/// </summary>
private static Func<RiskContext, T> CompileScript<T>(long? ruleId, string scriptCode, out string errorMessage)
{
errorMessage = null;
var ruleIdText = ruleId.HasValue ? ruleId.Value.ToString() : "未落库";
if (!ValidateScriptSafety(scriptCode, out errorMessage))
{
_logger.Error($"规则脚本安全校验失败 - RuleId: {ruleIdText}, Error: {errorMessage}\n脚本代码:{scriptCode}");
return null;
}
// 配置编译选项:引用必要的程序集
var options = ScriptOptions.Default
.WithReferences(
@@ -136,7 +369,7 @@ namespace YLErp.Modules.RiskEngine
.WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule");
// 创建脚本(尚未执行,仅编译)
var script = CSharpScript.Create<bool>(scriptCode, options, globalsType: typeof(ScriptGlobals));
var script = CSharpScript.Create<T>(scriptCode, options, globalsType: typeof(ScriptGlobals));
// 编译(提前发现语法错误)
//后续编译需提供接口,返回前端编译信息,包含编译错误列表
@@ -153,7 +386,7 @@ namespace YLErp.Modules.RiskEngine
// 生成可调用委托
var runner = script.CreateDelegate();
// 包装为同步的 Func<RiskContext, bool>
// 包装为同步的 Func<RiskContext, T>
return ctx =>
{
try
@@ -174,6 +407,125 @@ namespace YLErp.Modules.RiskEngine
}
};
}
/// <summary>
/// 脚本安全校验。
/// 在 Roslyn 编译前先扫描语法树,禁止写库、反射、文件、网络、进程等高风险语法入口。
/// </summary>
private static bool ValidateScriptSafety(string scriptCode, out string errorMessage)
{
var tree = CSharpSyntaxTree.ParseText(scriptCode, new CSharpParseOptions(kind: SourceCodeKind.Script));
var root = tree.GetRoot();
// 允许变量声明初始化(如 decimal a = ...),但禁止后续赋值或复合赋值,避免脚本修改对象状态。
if (root.DescendantNodes().OfType<AssignmentExpressionSyntax>().Any())
{
errorMessage = "规则表达式不允许包含赋值语句";
return false;
}
if (root.DescendantNodes().OfType<PrefixUnaryExpressionSyntax>().Any(IsIncrementOrDecrement) ||
root.DescendantNodes().OfType<PostfixUnaryExpressionSyntax>().Any(IsIncrementOrDecrement))
{
errorMessage = "规则表达式不允许包含自增或自减语句";
return false;
}
foreach (var memberAccess in root.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
{
var memberName = memberAccess.Name.Identifier.ValueText;
if (!string.IsNullOrWhiteSpace(memberName) && ForbiddenMemberNames.Contains(memberName))
{
errorMessage = $"规则表达式不允许访问高风险成员:{memberName}";
return false;
}
}
// 只拦截独立标识符,避免把实体字段 p.File、p.Path 误判为高风险类型名。
foreach (var identifier in root.DescendantNodes().OfType<IdentifierNameSyntax>())
{
var identifierName = identifier.Identifier.ValueText;
if (!IsMemberAccessName(identifier) && !string.IsNullOrWhiteSpace(identifierName) && ForbiddenTypeOrIdentifierNames.Contains(identifierName))
{
errorMessage = $"规则表达式不允许访问高风险类型或标识符:{identifierName}";
return false;
}
}
foreach (var objectCreation in root.DescendantNodes().OfType<ObjectCreationExpressionSyntax>())
{
var typeName = GetTypeName(objectCreation.Type);
if (!string.IsNullOrWhiteSpace(typeName) && ForbiddenTypeOrIdentifierNames.Contains(typeName))
{
errorMessage = $"规则表达式不允许创建高风险类型:{typeName}";
return false;
}
}
foreach (var invocation in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
var methodName = GetInvocationName(invocation.Expression);
if (!string.IsNullOrWhiteSpace(methodName) && ForbiddenInvocationNames.Contains(methodName))
{
errorMessage = $"规则表达式不允许调用高风险方法:{methodName}";
return false;
}
}
errorMessage = null;
return true;
}
/// <summary>
/// 判断前缀一元表达式是否为自增/自减。
/// </summary>
private static bool IsIncrementOrDecrement(PrefixUnaryExpressionSyntax expression)
{
return expression.IsKind(SyntaxKind.PreIncrementExpression) || expression.IsKind(SyntaxKind.PreDecrementExpression);
}
/// <summary>
/// 判断后缀一元表达式是否为自增/自减。
/// </summary>
private static bool IsIncrementOrDecrement(PostfixUnaryExpressionSyntax expression)
{
return expression.IsKind(SyntaxKind.PostIncrementExpression) || expression.IsKind(SyntaxKind.PostDecrementExpression);
}
/// <summary>
/// 判断标识符是否是成员访问右侧名称,例如 p.Path 中的 Path。
/// </summary>
private static bool IsMemberAccessName(IdentifierNameSyntax identifier)
{
return identifier.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == identifier;
}
/// <summary>
/// 从调用表达式中提取方法名,兼容 SaveChanges() 和 DbContext.SaveChanges() 两种写法。
/// </summary>
private static string GetInvocationName(ExpressionSyntax expression)
{
return expression switch
{
MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText,
IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
_ => null
};
}
/// <summary>
/// 从类型语法中提取类型短名,用于匹配高风险类型黑名单。
/// </summary>
private static string GetTypeName(TypeSyntax type)
{
return type switch
{
IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText,
AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText,
_ => type?.ToString()
};
}
}
public class BuildMemberAccessResult
+123 -44
View File
@@ -21,26 +21,26 @@ using YLErp.Model;
RuleText、RuleExpr、Version、CompiledScript。
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
- RuleExpr当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
- RuleExpr自由文本规则的编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
例如通过 DbContext 和 TradeId 查询数据库后做数值或日期比较。
- ConditionJson当前已在规则定义中保留,但不参与主执行链路
- ConditionJson结构化规则的主执行依据;有值时优先由结构化执行器执行
- RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DbContext。
- RuleCompiledCache:进程内编译结果缓存,按规则 Id 缓存 Func<RiskContext, bool>。
- RuleCompiledCache:进程内编译结果缓存,仅用于自由文本 RuleExpr 规则,按规则 Id 缓存 Func<RiskContext, bool>。
当前编译流程:
1. 外部准备好规则对象,在 RuleExpr 中直接写最终可执行表达式
2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
3. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
5. 执行阶段优先从 RuleCompiledCache 取委托执行
1. 结构化规则以 ConditionJson 为执行依据,预热时只做 JSON 结构解析校验
2. 自由文本规则在 RuleExpr 中直接写最终可执行表达式
3. 调用 RuleCompiler.ValidateAndCompileRule(rule)
4. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
5. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
当前执行流程(EvaluateRisk):
1. 从数据库加载规则列表和应用列表
2. 按应用状态、TriggerPoints 过滤有效应用
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
5. ConditionJson 有值时走结构化执行器;否则从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
6. 执行规则判断,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
7. 返回 RiskResult
当前维度匹配规则:
@@ -50,7 +50,7 @@ using YLErp.Model;
- 某维度留空:视为该维度不限制
【与早期方案的主要差异】
- 当前不是 Content 解析或表达式树主导,而是 RuleExpr 直编译主导
- 结构化规则已改为 ConditionJson 执行主导,自由文本规则继续保留 RuleExpr 直编译
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
- 编译器文件已放入 RiskEngine/Compile 目录下
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
@@ -75,12 +75,12 @@ using YLErp.Model;
✅ 已完成:
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
2. 风控上下文提供 DbContext 和 TradeId,支持规则脚本直接查询数据库
3. RuleExpr 直编译方案已接入,支持数值比较和日期比较
3. 结构化规则已支持 ConditionJson 执行,自由文本规则保留 RuleExpr 直编译
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
6. RuleCompiler 已支持校验 + 编译 + 写缓存
7. RuleCompiledCache / RuleCompileResult 已落地到 Compile 目录
8. RiskEngineService 已改为执行时优先从 RuleCompiledCache 读取委托
8. RiskEngineService 已改为结构化规则优先执行 ConditionJson,自由文本规则再读取 RuleCompiledCache
【待办事项 / TODO】
⬜ 1. 接入真实规则来源
@@ -146,8 +146,8 @@ namespace YLErp.Modules.RiskEngine
private static readonly object _cacheLock = new object();
/// <summary>
/// 预热:加载规则与应用到内存,并预编译所有规则到 RuleCompiledCache。
/// 项目启动时调用一次;规则/应用更新后调用 RefreshCache 刷新
/// 预热:加载规则与应用到内存,并预编译自由文本规则到 RuleCompiledCache。
/// 结构化规则运行时直接执行 ConditionJson,预热时只做轻量结构校验,不再预编译 RuleExpr
/// </summary>
public void Preload()
{
@@ -157,7 +157,7 @@ namespace YLErp.Modules.RiskEngine
var rules = LoadRulesFromDb();
var applications = LoadApplicationsFromDb();
// 预编译所有规则到 RuleCompiledCache
// 预编译自由文本规则;结构化规则运行时由 ConditionJson 执行。
foreach (var rule in rules)
{
var ruleId = rule.Id.ToString();
@@ -169,6 +169,20 @@ namespace YLErp.Modules.RiskEngine
continue;
}
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
RuleCompiledCache.Remove(rule.Id.ToString());
try
{
RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
_logger.Info($"[风控引擎] 结构化规则预热校验成功,跳过 RuleExpr 预编译 - RuleId: {rule.Id}");
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] 结构化规则预热校验失败 - RuleId: {rule.Id}, Error: {ex.Message}");
}
continue;
}
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (compileResult.Success)
@@ -231,6 +245,22 @@ namespace YLErp.Modules.RiskEngine
return RuleCompileResult.Fail("规则不存在或非活跃状态");
}
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
RuleCompiledCache.Remove(ruleId.ToString());
try
{
RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
_logger.Info($"[风控引擎] RefreshOneRuleCache 结构化规则校验成功,跳过 RuleExpr 编译 - RuleId: {ruleId}");
return RuleCompileResult.Ok(null);
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] RefreshOneRuleCache 结构化规则校验失败 - RuleId: {ruleId}, Error: {ex.Message}");
return RuleCompileResult.Fail(ex.Message);
}
}
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
_logger.Info($"[风控引擎] RefreshOneRuleCache 完成 - RuleId: {ruleId}, Success: {compileResult.Success}, Error: {compileResult.ErrorMessage}");
return compileResult;
@@ -323,6 +353,7 @@ namespace YLErp.Modules.RiskEngine
// ============================================================
var rules = GetRules();
var applications = GetApplications();
var variables = new Dictionary<long, glms_risk_variable>();
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}, 应用数: {applications.Count}");
// ============================================================
@@ -395,40 +426,88 @@ namespace YLErp.Modules.RiskEngine
{
var ruleId = rule.Id.ToString();
// ------------------------------------------------------------
// 3.2 优先从编译缓存读取规则委托
// ------------------------------------------------------------
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
bool triggered = false;
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (!compileResult.Success)
// 结构化规则优先使用 ConditionJson 执行,避免继续把条件整体拼成 Roslyn bool 公式。
try
{
var errorMessage = $"规则[{rule.RuleName}]编译失败,已按阻断处理,请检查规则表达式配置:{compileResult.ErrorMessage}";
_logger.Info($"[风控引擎] 规则编译失败,按阻断处理 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
AddBlockError(result, rule.Id.ToString(), rule.RuleName, rule.RuleText, errorMessage);
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
var referencedVariableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions);
var missingVariableIds = referencedVariableIds
.Where(id => !variables.ContainsKey(id))
.ToList();
if (missingVariableIds.Any())
{
// 按本次结构化规则实际引用的变量懒加载,避免每次风控检查全量读取变量池。
var loadedVariables = ruleDbContext.glms_risk_variable
.AsNoTracking()
.Where(v => missingVariableIds.Contains(v.id))
.ToDictionary(v => v.id);
foreach (var variable in loadedVariables)
variables[variable.Key] = variable.Value;
}
var ruleVariables = referencedVariableIds
.Where(variables.ContainsKey)
.ToDictionary(id => id, id => variables[id]);
var executeResult = StructuredRuleExecutor.Execute(conditions, ruleVariables, context);
if (!executeResult.Success)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{executeResult.ErrorMessage}";
_logger.Error($"[风控引擎] 结构化规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {executeResult.ErrorMessage}");
AddBlockError(result, ruleId, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
triggered = executeResult.Triggered;
_logger.Info($"[风控引擎] 结构化规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 结构化规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
compiledScript = compileResult.CompiledScript;
}
else
{
// ------------------------------------------------------------
// 3.2 优先从编译缓存读取规则委托
// ------------------------------------------------------------
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
{
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
// ------------------------------------------------------------
// 3.3 执行预编译委托
// ------------------------------------------------------------
bool triggered = false;
try
{
triggered = compiledScript(context);
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, rule.RuleName, rule.RuleText, errorMessage);
continue;
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (!compileResult.Success)
{
var errorMessage = $"规则[{rule.RuleName}]编译失败,已按阻断处理,请检查规则表达式配置:{compileResult.ErrorMessage}";
_logger.Info($"[风控引擎] 规则编译失败,按阻断处理 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
AddBlockError(result, rule.Id.ToString(), rule.RuleName, rule.RuleText, errorMessage);
continue;
}
compiledScript = compileResult.CompiledScript;
}
// ------------------------------------------------------------
// 3.3 执行预编译委托
// ------------------------------------------------------------
try
{
triggered = compiledScript(context);
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
}
// ------------------------------------------------------------
+61 -53
View File
@@ -183,7 +183,7 @@ using YLErp.Modules.RiskEngine.Dto;
2. 规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication(),变量变更不触发引擎缓存刷新
3. 变量/规则/应用删除均为软删除(Status=Deleted),新建变量默认 Active
4. ExportAuditLogs 按查询条件导出,并生成包含查询条件的文件名
5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
5. VariableExpr 保存时校验长度,并通过 Roslyn 做编译与安全校验
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
================================================================================
*/
@@ -355,11 +355,12 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 校验 ConditionJson 并生成服务端期望的简洁 RuleExpr
/// 校验 ConditionJson 结构和引用变量是否存在
/// 结构化规则运行时以 ConditionJson 为准,RuleExpr 只作为兼容/展示字段。
/// </summary>
private string ValidateConditionJson(string conditionJson)
private void ValidateConditionJson(string conditionJson)
{
return BuildRuleExprFromConditionJson(conditionJson);
ValidateConditionJsonVariables(conditionJson);
}
/// <summary>
/// 校验 RuleExpr(自由文本模式:括号匹配)
@@ -514,7 +515,8 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 校验规则参数(名称 + ConditionJson + RuleExpr
/// 校验规则参数(名称 + ConditionJson / RuleExpr
/// 自由文本规则继续使用 RuleExpr;结构化规则允许保留 RuleExpr 作为兼容/展示字段,运行时以 ConditionJson 为准。
/// </summary>
private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr, long? ruleId = null)
{
@@ -523,22 +525,31 @@ namespace YLErp.Modules.RiskEngine
if (ruleName.Length > 200)
throw new ServiceException("规则名称长度不能超过200字符");
if (!string.IsNullOrWhiteSpace(conditionJson))
{
ValidateConditionJson(conditionJson);
return;
}
if (string.IsNullOrWhiteSpace(ruleExpr))
throw new ServiceException("规则表达式不能为空");
if (!string.IsNullOrWhiteSpace(conditionJson))
{
var expectedRuleExpr = ValidateConditionJson(conditionJson);
if (!string.Equals(expectedRuleExpr, ruleExpr, StringComparison.Ordinal))
{
_logger.Info($"结构化规则公式一致性校验失败。RuleId={ruleId?.ToString() ?? "NEW"}; Expected={expectedRuleExpr}; Actual={ruleExpr}");
throw new ServiceException("公式与结构化条件不一致,请刷新后重试");
}
}
ValidateRuleExpr(ruleExpr);
}
/// <summary>
/// 校验规则编译:结构化规则运行时以 ConditionJson 为准,只有自由文本规则必须编译 RuleExpr。
/// </summary>
private void ValidateRuleCompile(long? ruleId, string conditionJson, string ruleExpr)
{
if (!string.IsNullOrWhiteSpace(conditionJson))
return;
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, ruleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
}
/// <summary>
/// 解析逗号分隔的规则 ID 字符串为 List
/// </summary>
@@ -979,10 +990,7 @@ namespace YLErp.Modules.RiskEngine
public RiskRuleDetail CreateRule(CreateRiskRuleReq req)
{
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(null, req.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
ValidateRuleCompile(null, req.ConditionJson, req.RuleExpr);
var entity = new glms_risk_rule
{
@@ -1036,10 +1044,7 @@ namespace YLErp.Modules.RiskEngine
throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑");
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr, ruleId);
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, req.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
ValidateRuleCompile(ruleId, req.ConditionJson, req.RuleExpr);
rule.RuleName = req.RuleName;
rule.RuleText = req.RuleText;
@@ -1122,9 +1127,7 @@ namespace YLErp.Modules.RiskEngine
if (rule == null)
throw new ServiceException("仅已停用的规则可以启用");
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, rule.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
ValidateRuleCompile(ruleId, rule.ConditionJson, rule.RuleExpr);
rule.Status = RiskRuleStatus.Active;
@@ -1320,11 +1323,13 @@ namespace YLErp.Modules.RiskEngine
continue;
}
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rule.RuleExpr);
if (!compileResult.Success)
try
{
items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "COMPILE_FAILED",
$"规则表达式编译失败:{compileResult.ErrorMessage}"));
ValidateRuleCompile(rule.id, rule.ConditionJson, rule.RuleExpr);
}
catch (ServiceException ex)
{
items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "COMPILE_FAILED", ex.Message));
continue;
}
@@ -1923,23 +1928,36 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 根据结构化 ConditionJson 和当前变量池定义重新生成简洁 RuleExpr
/// 校验结构化 ConditionJson 引用的变量是否存在,并返回未删除的变量定义
/// </summary>
private string BuildRuleExprFromConditionJson(string conditionJson)
private List<glms_risk_variable> ValidateConditionJsonVariables(string conditionJson)
{
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(conditionJson);
var variableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToList();
var variables = DbContext.glms_risk_variable
.Where(variable => variableIds.Contains(variable.id) && variable.Status != RiskRuleStatus.Deleted)
.ToList()
.ToDictionary(variable => (long)variable.id);
.ToList();
var missingVariableIds = variableIds.Where(id => !variables.ContainsKey(id)).ToList();
var variableMap = variables.ToDictionary(variable => (long)variable.id);
var missingVariableIds = variableIds.Where(id => !variableMap.ContainsKey(id)).ToList();
if (missingVariableIds.Any())
throw new ServiceException($"公式引用的变量不存在:{string.Join(",", missingVariableIds)}");
return RuleConditionExpressionBuilder.Build(conditions, variables);
return variables;
}
/// <summary>
/// 校验变量取值表达式是否可编译且不包含写库等危险调用。
/// 这里只做静态编译与安全检查,不执行表达式,避免依赖真实交易数据。
/// </summary>
private void ValidateVariableExpr(string variableExpr)
{
var compileResult = RuleCompiler.ValidateVariableExpression(variableExpr);
if (!compileResult.Success)
throw new ServiceException($"变量取值表达式校验失败:{compileResult.ErrorMessage}");
}
#region Variable Management
/// <summary>
@@ -2031,6 +2049,7 @@ namespace YLErp.Modules.RiskEngine
throw new ServiceException("取值表达式不能为空");
if (req.VariableExpr.Length > 10000)
throw new ServiceException("取值表达式长度不能超过 10000 字符");
ValidateVariableExpr(req.VariableExpr);
var entity = new glms_risk_variable
{
@@ -2090,6 +2109,9 @@ namespace YLErp.Modules.RiskEngine
throw new ServiceException("变量名称不能为空");
if (string.IsNullOrWhiteSpace(req.VariableExpr))
throw new ServiceException("取值表达式不能为空");
if (req.VariableExpr.Length > 10000)
throw new ServiceException("取值表达式长度不能超过 10000 字符");
ValidateVariableExpr(req.VariableExpr);
if (req.DataType != variable.DataType)
{
@@ -2125,34 +2147,20 @@ namespace YLErp.Modules.RiskEngine
{
try
{
// 在同一事务内完成引用发现、公式重建、编译和持久化;任一规则失败时整体回滚
// 结构化规则运行时直接按 ConditionJson 读取变量定义,不再把变量表达式展开回 RuleExpr
// 因此变量定义变化时只标记引用规则受影响,避免多语句变量表达式被拼成非法公式。
affectedRules = GetRulesByVariableId(variableId)
.Where(r => r.RuleExpr != null)
.ToList();
var rebuiltRuleExpressions = new Dictionary<long, string>();
foreach (var rule in affectedRules)
{
var rebuiltRuleExpr = BuildRuleExprFromConditionJson(rule.ConditionJson);
ValidateRuleExpr(rebuiltRuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rebuiltRuleExpr);
if (!compileResult.Success)
throw new ServiceException($"变量定义变更导致规则'{rule.RuleName}'(ID:{rule.id})编译失败:{compileResult.ErrorMessage}");
rebuiltRuleExpressions[rule.id] = rebuiltRuleExpr;
}
foreach (var rule in affectedRules)
{
rule.RuleExpr = rebuiltRuleExpressions[rule.id];
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("VAR_CASCADE_UPDATE_RULE", "RULE", rule.id, rule.RuleName,
$"因变量'{variable.VariableName}'(ID:{variableId})定义变更,规则表达式被级联更新");
$"因变量'{variable.VariableName}'(ID:{variableId})定义变更,结构化规则已标记更新");
}
SaveChangesWithConcurrencyCheck();
@@ -0,0 +1,461 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using YLErp.Commons;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine.Dto;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 结构化规则执行器。
/// 用于执行现有 ConditionJson:变量取值、按 DataType 转换并比较阈值;复杂业务计算放在变量表达式中完成。
/// </summary>
public static class StructuredRuleExecutor
{
private static readonly ConcurrentDictionary<string, Func<RiskContext, object>> VariableCompiledCache = new ConcurrentDictionary<string, Func<RiskContext, object>>();
private static readonly HashSet<string> ComparableOperators = new HashSet<string>(StringComparer.Ordinal)
{
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
};
private static readonly HashSet<string> BooleanOperators = new HashSet<string>(StringComparer.Ordinal)
{
"isTrue", "isFalse"
};
public static StructuredRuleExecuteResult Execute(
IReadOnlyList<RuleCondition> conditions,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context)
{
try
{
if (conditions == null || conditions.Count == 0)
return StructuredRuleExecuteResult.Fail("公式条件列表不能为空");
if (variables == null)
return StructuredRuleExecuteResult.Fail("变量列表不能为空");
for (var i = 0; i < conditions.Count; i++)
{
var conditionResult = ExecuteCondition(conditions[i], variables, context, i + 1);
if (!conditionResult.Success)
return conditionResult;
if (!conditionResult.Triggered)
return StructuredRuleExecuteResult.Ok(false);
}
return StructuredRuleExecuteResult.Ok(true);
}
catch (Exception ex)
{
return StructuredRuleExecuteResult.Fail(ex.Message);
}
}
private static StructuredRuleExecuteResult ExecuteCondition(
RuleCondition condition,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
int conditionIndex)
{
var label = $"条件{conditionIndex}";
if (condition == null)
return StructuredRuleExecuteResult.Fail($"{label}不能为空");
if (!variables.TryGetValue(condition.VariableId, out var variable))
return StructuredRuleExecuteResult.Fail($"{label}:变量 ID '{condition.VariableId}' 不存在");
if (variable.DataType == RiskVariableDataType.Boolean)
return ExecuteBooleanCondition(condition, variable, context, label);
if (variable.DataType != RiskVariableDataType.Numeric && variable.DataType != RiskVariableDataType.Date)
return StructuredRuleExecuteResult.Fail($"{label}:不支持的数据类型");
if (!ComparableOperators.Contains(condition.Operator))
return StructuredRuleExecuteResult.Fail($"{label}:操作符 '{condition.Operator}' 不适用于{GetDataTypeName(variable.DataType)}类型变量");
var left = GetVariableValue(variable, variable.DataType, context, label, "条件变量");
if (!left.Success)
return StructuredRuleExecuteResult.Fail(left.ErrorMessage);
if (condition.Operator == "between" || condition.Operator == "notBetween")
return ExecuteRangeCondition(condition, variable, left.Value, variables, context, label);
var rangeFieldCheck = EnsureNoRangeFields(condition, label);
if (!rangeFieldCheck.Success)
return rangeFieldCheck;
var right = GetThresholdValue(
condition.ThresholdType,
condition.Value,
condition.ThresholdVariableId,
variable,
variables,
context,
label,
"阈值");
if (!right.Success)
return StructuredRuleExecuteResult.Fail(right.ErrorMessage);
var compareResult = Compare(left.Value, right.Value, variable.DataType, condition.Operator, label);
if (!compareResult.Success)
return compareResult;
return StructuredRuleExecuteResult.Ok(compareResult.Triggered);
}
private static StructuredRuleExecuteResult ExecuteBooleanCondition(
RuleCondition condition,
glms_risk_variable variable,
RiskContext context,
string label)
{
if (!BooleanOperators.Contains(condition.Operator))
return StructuredRuleExecuteResult.Fail($"{label}:操作符 '{condition.Operator}' 不适用于布尔类型变量");
var thresholdFieldCheck = EnsureNoThresholdFields(condition, label);
if (!thresholdFieldCheck.Success)
return thresholdFieldCheck;
var value = GetVariableValue(variable, RiskVariableDataType.Boolean, context, label, "条件变量");
if (!value.Success)
return StructuredRuleExecuteResult.Fail(value.ErrorMessage);
var expected = condition.Operator == "isTrue";
return StructuredRuleExecuteResult.Ok((bool)value.Value == expected);
}
private static StructuredRuleExecuteResult ExecuteRangeCondition(
RuleCondition condition,
glms_risk_variable variable,
object leftValue,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
string label)
{
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue)
return StructuredRuleExecuteResult.Fail($"{label}:区间条件不得携带普通阈值字段");
if (!condition.IncludeLower.HasValue || !condition.IncludeUpper.HasValue)
return StructuredRuleExecuteResult.Fail($"{label}:区间开闭配置不完整");
var fixedRangeCheck = ValidateFixedRangeOrder(condition, variable.DataType, label);
if (!fixedRangeCheck.Success)
return fixedRangeCheck;
var lower = GetThresholdValue(condition.LowerThresholdType, condition.LowerValue, condition.LowerThresholdVariableId, variable, variables, context, label, "下限");
if (!lower.Success)
return StructuredRuleExecuteResult.Fail(lower.ErrorMessage);
var upper = GetThresholdValue(condition.UpperThresholdType, condition.UpperValue, condition.UpperThresholdVariableId, variable, variables, context, label, "上限");
if (!upper.Success)
return StructuredRuleExecuteResult.Fail(upper.ErrorMessage);
var lowerOperator = condition.IncludeLower == true ? "gte" : "gt";
var upperOperator = condition.IncludeUpper == true ? "lte" : "lt";
var lowerCompare = Compare(leftValue, lower.Value, variable.DataType, lowerOperator, label);
if (!lowerCompare.Success)
return lowerCompare;
var upperCompare = Compare(leftValue, upper.Value, variable.DataType, upperOperator, label);
if (!upperCompare.Success)
return upperCompare;
var inRange = lowerCompare.Triggered && upperCompare.Triggered;
return StructuredRuleExecuteResult.Ok(condition.Operator == "notBetween" ? !inRange : inRange);
}
private static ValueExecuteResult GetThresholdValue(
string thresholdType,
object fixedValue,
long? thresholdVariableId,
glms_risk_variable conditionVariable,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
string label,
string thresholdLabel)
{
if (string.Equals(thresholdType, "Fixed", StringComparison.Ordinal))
{
if (thresholdVariableId.HasValue)
return ValueExecuteResult.Fail($"{label}:固定{thresholdLabel}不得携带变量 ID");
var fixedResult = ConvertValue(fixedValue, conditionVariable.DataType, label, $"固定{thresholdLabel}");
if (!fixedResult.Success)
return ValueExecuteResult.Fail($"{fixedResult.ErrorMessage},原始值:{FormatRawValue(fixedValue)}");
return fixedResult;
}
if (string.Equals(thresholdType, "Variable", StringComparison.Ordinal))
{
if (HasValue(fixedValue))
return ValueExecuteResult.Fail($"{label}:变量{thresholdLabel}不得携带固定值");
if (!thresholdVariableId.HasValue)
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量 ID 不能为空");
if (!variables.TryGetValue(thresholdVariableId.Value, out var thresholdVariable))
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量 ID '{thresholdVariableId.Value}' 不存在");
if (thresholdVariable.DataType != conditionVariable.DataType)
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量与条件变量的数据类型不一致");
if (conditionVariable.DataType == RiskVariableDataType.Numeric && !UnitsMatch(conditionVariable.Unit, thresholdVariable.Unit))
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量与条件变量的单位不一致");
return GetVariableValue(thresholdVariable, conditionVariable.DataType, context, label, thresholdLabel);
}
return ValueExecuteResult.Fail($"{label}{thresholdLabel}类型 '{thresholdType}' 不合法,仅支持 Fixed/Variable");
}
private static ValueExecuteResult GetVariableValue(
glms_risk_variable variable,
RiskVariableDataType dataType,
RiskContext context,
string label,
string valueLabel)
{
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})的取值表达式为空");
var compiled = GetCompiledVariableExpression(variable, out var errorMessage);
if (compiled == null)
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})编译失败:{errorMessage}");
object rawValue;
try
{
rawValue = compiled(context);
}
catch (Exception ex)
{
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})执行失败:{ex.Message}");
}
var converted = ConvertValue(rawValue, dataType, label, valueLabel);
if (!converted.Success)
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id}){converted.ErrorMessage},原始值:{FormatRawValue(rawValue)}");
return converted;
}
private static Func<RiskContext, object> GetCompiledVariableExpression(glms_risk_variable variable, out string errorMessage)
{
var cacheKey = $"{variable.id}:{variable.Version}:{variable.UpdateDate?.Ticks ?? 0}:{StringComparer.Ordinal.GetHashCode(variable.VariableExpr ?? string.Empty)}";
if (VariableCompiledCache.TryGetValue(cacheKey, out var compiled))
{
errorMessage = null;
return compiled;
}
// 变量表达式只负责取原始值,类型转换统一在结构化执行器中处理;这里缓存编译结果,避免每次执行重复 Roslyn 编译。
compiled = RuleCompiler.CompileValueExpression(variable.VariableExpr, out errorMessage);
if (compiled != null)
VariableCompiledCache[cacheKey] = compiled;
return compiled;
}
private static ValueExecuteResult ConvertValue(object value, RiskVariableDataType dataType, string label, string valueLabel)
{
if (!HasValue(value))
return ValueExecuteResult.Fail($"{valueLabel}为空");
if (dataType == RiskVariableDataType.Numeric)
{
if (value is decimal decimalValue)
return ValueExecuteResult.Ok(decimalValue);
if (decimal.TryParse(GetRawValue(value), NumberStyles.Float, CultureInfo.InvariantCulture, out var numericValue))
return ValueExecuteResult.Ok(numericValue);
return ValueExecuteResult.Fail($"{valueLabel}必须为数字");
}
if (dataType == RiskVariableDataType.Date)
{
if (value is DateTime dateValue)
return ValueExecuteResult.Ok(dateValue.Date);
if (DateTime.TryParseExact(GetRawValue(value), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var fixedDate))
return ValueExecuteResult.Ok(fixedDate.Date);
return ValueExecuteResult.Fail($"{valueLabel}必须为 yyyy-MM-dd 格式的合法日期");
}
if (dataType == RiskVariableDataType.Boolean)
{
if (value is bool boolValue)
return ValueExecuteResult.Ok(boolValue);
if (bool.TryParse(GetRawValue(value), out var parsedBool))
return ValueExecuteResult.Ok(parsedBool);
return ValueExecuteResult.Fail($"{valueLabel}必须为布尔值");
}
return ValueExecuteResult.Fail($"{label}:不支持的数据类型");
}
private static StructuredRuleExecuteResult Compare(object left, object right, RiskVariableDataType dataType, string ruleOperator, string label)
{
var compare = dataType switch
{
RiskVariableDataType.Numeric => ((decimal)left).CompareTo((decimal)right),
RiskVariableDataType.Date => ((DateTime)left).CompareTo((DateTime)right),
_ => throw new ServiceException($"{label}:不支持的数据类型")
};
var result = ruleOperator switch
{
"gt" => compare > 0,
"lt" => compare < 0,
"gte" => compare >= 0,
"lte" => compare <= 0,
"eq" => compare == 0,
"ne" => compare != 0,
_ => throw new ServiceException($"{label}:不支持的操作符'{ruleOperator}'")
};
return StructuredRuleExecuteResult.Ok(result);
}
private static StructuredRuleExecuteResult EnsureNoRangeFields(RuleCondition condition, string label)
{
if (condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
{
return StructuredRuleExecuteResult.Fail($"{label}:非区间条件不得携带区间字段");
}
return StructuredRuleExecuteResult.Ok(false);
}
private static StructuredRuleExecuteResult EnsureNoThresholdFields(RuleCondition condition, string label)
{
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue ||
condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
{
return StructuredRuleExecuteResult.Fail($"{label}:布尔条件不得携带阈值字段");
}
return StructuredRuleExecuteResult.Ok(false);
}
private static StructuredRuleExecuteResult ValidateFixedRangeOrder(RuleCondition condition, RiskVariableDataType dataType, string label)
{
if (condition.LowerThresholdType != "Fixed" || condition.UpperThresholdType != "Fixed")
return StructuredRuleExecuteResult.Ok(false);
var lower = GetRawValue(condition.LowerValue);
var upper = GetRawValue(condition.UpperValue);
if (dataType == RiskVariableDataType.Numeric &&
decimal.TryParse(lower, NumberStyles.Float, CultureInfo.InvariantCulture, out var lowerNumber) &&
decimal.TryParse(upper, NumberStyles.Float, CultureInfo.InvariantCulture, out var upperNumber) &&
lowerNumber > upperNumber)
{
return StructuredRuleExecuteResult.Fail($"{label}:下限不能大于上限");
}
if (dataType == RiskVariableDataType.Date &&
DateTime.TryParseExact(lower, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var lowerDate) &&
DateTime.TryParseExact(upper, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var upperDate) &&
lowerDate > upperDate)
{
return StructuredRuleExecuteResult.Fail($"{label}:下限不能晚于上限");
}
return StructuredRuleExecuteResult.Ok(false);
}
private static bool HasValue(object value)
{
if (value == null) return false;
if (value is JValue jsonValue)
{
if (jsonValue.Type == JTokenType.Null || jsonValue.Type == JTokenType.Undefined) return false;
if (jsonValue.Type == JTokenType.String) return !string.IsNullOrWhiteSpace(jsonValue.Value<string>());
return true;
}
return value is not string text || !string.IsNullOrWhiteSpace(text);
}
private static string GetRawValue(object value)
{
return value is JValue jsonValue
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
: Convert.ToString(value, CultureInfo.InvariantCulture);
}
private static string FormatRawValue(object value)
{
if (value == null)
return "<null>";
var rawValue = GetRawValue(value);
return string.IsNullOrWhiteSpace(rawValue) ? "<empty>" : rawValue;
}
private static bool UnitsMatch(string left, string right)
{
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
}
private static string GetDataTypeName(RiskVariableDataType dataType)
{
return dataType switch
{
RiskVariableDataType.Numeric => "数值",
RiskVariableDataType.Date => "日期",
RiskVariableDataType.Boolean => "布尔",
_ => "未知"
};
}
}
public class StructuredRuleExecuteResult
{
public bool Success { get; set; }
public bool Triggered { get; set; }
public string ErrorMessage { get; set; }
public static StructuredRuleExecuteResult Ok(bool triggered)
{
return new StructuredRuleExecuteResult
{
Success = true,
Triggered = triggered
};
}
public static StructuredRuleExecuteResult Fail(string errorMessage)
{
return new StructuredRuleExecuteResult
{
Success = false,
Triggered = false,
ErrorMessage = errorMessage
};
}
}
internal class ValueExecuteResult
{
public bool Success { get; set; }
public object Value { get; set; }
public string ErrorMessage { get; set; }
public static ValueExecuteResult Ok(object value)
{
return new ValueExecuteResult
{
Success = true,
Value = value
};
}
public static ValueExecuteResult Fail(string errorMessage)
{
return new ValueExecuteResult
{
Success = false,
ErrorMessage = errorMessage
};
}
}
}
File diff suppressed because it is too large Load Diff