feat: 变量删除时校验是否有规则引用,有则阻止
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user