Files
zszq-trs/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
T
ruisu 8fb3c4392c feat:风控触发时显示应用ID、
新老风控组合校验:
老风控Error/Warning,新风控需审批——点击特批即两边都放行
老风控Error/Warning,新风控禁止——不展示
老风控Error/Warning——展示特批,且只特批老风控。
新增风控流程日志
2026-08-05 14:39:01 +08:00

582 lines
23 KiB
C#

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;
using System.Linq;
using YLErp.BLL;
using YLErp.QdpModule;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// Roslyn 脚本全局变量容器
/// </summary>
public class ScriptGlobals
{
/// <summary>
/// 当前交易ID
/// </summary>
public int TradeId { get; set; }
/// <summary>
/// 当前触发时点
/// </summary>
public string TriggerPoint { get; set; }
/// <summary>
/// 数据库上下文,供规则公式直接查询数据库
/// </summary>
public YLContext DbContext { get; set; }
}
/// <summary>
/// 规则编译器(Roslyn 版)
///
/// 编译时机:规则定义时(零运行时解析开销)
/// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool
///
/// 相比表达式树方案的优势:
/// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点
/// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法
/// - 可调试:生成的脚本代码可直接阅读和理解
///
/// 示例:
/// RuleExpr: Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).StockEqvNotional) > 100000000m
/// 编译结果:Func&lt;RiskContext, bool&gt;(内部通过 Roslyn 编译缓存)
/// </summary>
public static class 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>
/// 校验并编译规则表达式。
/// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
/// ruleId 可为空,用于规则未落库时的公式校验。
/// </summary>
public static RuleCompileResult ValidateAndCompileFormula(long? ruleId, string formulaExp)
{
var ruleIdText = ruleId.HasValue ? ruleId.Value.ToString() : "未落库";
if (string.IsNullOrWhiteSpace(formulaExp))
{
_logger.Error($"规则表达式为空,无法编译 - RuleId: {ruleIdText}");
return RuleCompileResult.Fail("规则 RuleExpr 不能为空");
}
try
{
var compiled = CompileScript(ruleId, formulaExp, out string compileErrorMessage);
if (compiled == null)
{
return RuleCompileResult.Fail(compileErrorMessage ?? "脚本编译失败");
}
return RuleCompileResult.Ok(compiled);
}
catch (Exception ex)
{
_logger.Error($"规则表达式校验异常 - RuleId: {ruleIdText}, Error: {ex.Message}\n脚本代码:{formulaExp}");
return RuleCompileResult.Fail(ex.Message);
}
}
/// <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>
public static RuleCompileResult ValidateAndCompileRule(RiskRule rule)
{
if (rule == null)
{
_logger.Error("规则对象为空,无法编译 - RuleId: 0");
return RuleCompileResult.Fail("规则不能为空");
}
if (rule.Id <= 0)
{
_logger.Error($"规则 Id 非法,无法编译 - RuleId: {rule.Id}");
return RuleCompileResult.Fail("规则 Id 不能为空");
}
if (rule.Status != RiskRuleStatus.Active)
{
_logger.Error($"规则状态非启用,跳过编译 - RuleId: {rule.Id}, Status: {rule.Status}");
return RuleCompileResult.Fail("规则 状态为未启动");
}
var result = ValidateAndCompileFormula(rule.Id, rule.RuleExpr);
if (!result.Success)
{
_logger.Error($"规则校验失败 - RuleId: {rule.Id}, Error: {result.ErrorMessage}");
rule.CompiledScript = null;
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
}
rule.CompiledScript = result.CompiledScript;
RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript);
return result;
}
/// <summary>
/// 用 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(
typeof(RiskContext).Assembly,
typeof(RiskCalendarHelper).Assembly,
typeof(YLContext).Assembly,
typeof(YLErp.DBModels.trade).Assembly,
typeof(QdpCalendarHelper).Assembly,
typeof(JsonConvert).Assembly,
typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly,
typeof(Queryable).Assembly
)
.WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule", "YLErp.Modules.RiskEngine");
// 创建脚本(尚未执行,仅编译)
var script = CSharpScript.Create<T>(scriptCode, options, globalsType: typeof(ScriptGlobals));
// 编译(提前发现语法错误)
//后续编译需提供接口,返回前端编译信息,包含编译错误列表
var compilation = script.GetCompilation();
var diagnostics = compilation.GetDiagnostics();
var errors = diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).ToList();
if (errors.Any())
{
errorMessage = string.Join("; ", errors.Select(e => e.GetMessage()));
_logger.Error($"规则编译失败 - RuleId: {ruleIdText}, Error: {errorMessage}\n脚本代码:{scriptCode}");
return null;
}
// 生成可调用委托
var runner = script.CreateDelegate();
// 包装为同步的 Func<RiskContext, T>
return ctx =>
{
try
{
var globals = new ScriptGlobals
{
TradeId = ctx.TradeId,
TriggerPoint = ctx.TriggerPoint,
DbContext = ctx.DbContext
};
return runner(globals).GetAwaiter().GetResult();
}
catch (Exception ex)
{
// 脚本执行异常(如空引用、类型转换失败)交由执行层按阻断处理
_logger.Error($"规则执行异常 - RuleId: {ruleIdText}, Error: {ex.Message}\n脚本代码:{scriptCode}");
throw;
}
};
}
/// <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
{
public bool Success { get; set; }
public string Expression { get; set; }
public string ErrorMessage { get; set; }
public static BuildMemberAccessResult Ok(string expression)
{
return new BuildMemberAccessResult
{
Success = true,
Expression = expression
};
}
/// <summary>
/// 将操作符统一为 C# 操作符
/// </summary>
private static string FormatOperator(string op)
{
return op?.Trim() switch
{
// 数值型 C# 符号
"=" or "==" => "==",
"!=" or "≠" => "!=",
">" => ">",
"<" => "<",
">=" => ">=",
"<=" => "<=",
// 日期型中文语义 → C# 符号
"早于" => "<",
"晚于" => ">",
"等于" => "==",
"不早于" => ">=",
"不晚于" => "<=",
_ => throw new ArgumentException($"不支持的操作符:{op}")
};
}
public static BuildMemberAccessResult Fail(string errorMessage)
{
return new BuildMemberAccessResult
{
Success = false,
ErrorMessage = errorMessage
};
}
}
}