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 { /// /// Roslyn 脚本全局变量容器 /// public class ScriptGlobals { /// /// 当前交易ID /// public int TradeId { get; set; } /// /// 当前触发时点 /// public string TriggerPoint { get; set; } /// /// 数据库上下文,供规则公式直接查询数据库 /// public YLContext DbContext { get; set; } } /// /// 规则编译器(Roslyn 版) /// /// 编译时机:规则定义时(零运行时解析开销) /// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool /// /// 相比表达式树方案的优势: /// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点 /// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法 /// - 可调试:生成的脚本代码可直接阅读和理解 /// /// 示例: /// RuleExpr: Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).StockEqvNotional) > 100000000m /// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存) /// public static class RuleCompiler { private static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler"); /// /// 高风险方法黑名单。 /// 这里按方法名做语法层拦截,覆盖数据库写入、原生 SQL、反射、文件、进程、服务定位等入口。 /// private static readonly HashSet ForbiddenInvocationNames = new HashSet(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" }; /// /// 高风险成员黑名单。 /// DbContext.Database、ChangeTracker 等成员会绕过只读查询约束或暴露底层状态,禁止脚本访问。 /// private static readonly HashSet ForbiddenMemberNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "Database", "ChangeTracker", "Assembly", "AppDomain" }; /// /// 高风险类型或标识符黑名单。 /// 用于拦截 File、Process、Environment 等直接作为类型或变量名出现的绕过方式。 /// private static readonly HashSet ForbiddenTypeOrIdentifierNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "Activator", "Assembly", "AppDomain", "Environment", "File", "Directory", "Path", "Process", "HttpClient", "WebClient", "WebRequest", "Socket", "Thread", "GC" }; /// /// 校验并编译规则表达式。 /// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。 /// ruleId 可为空,用于规则未落库时的公式校验。 /// 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); } } /// /// 校验变量取值表达式。 /// 变量表达式只要求能编译为 object,不在保存时执行,避免依赖真实交易和数据库数据。 /// 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); } /// /// 编译变量取值表达式。 /// 结构化规则执行时使用,表达式返回值由执行器按变量 DataType 统一转换。 /// public static Func CompileValueExpression(string variableExpr, out string errorMessage) { if (string.IsNullOrWhiteSpace(variableExpr)) { errorMessage = "变量取值表达式不能为空"; return null; } return CompileVariableExpression(variableExpr, out errorMessage); } /// /// 编译变量取值表达式。优先兼容原有单表达式写法;失败后再按多语句脚本编译。 /// 多语句脚本支持用 return 表达最终值,编译前会转换为 Roslyn Script 稳定支持的末尾表达式。 /// private static Func CompileVariableExpression(string variableExpr, out string errorMessage) { if (IsLikelyVariableScript(variableExpr)) { var normalizedScript = NormalizeVariableScript(variableExpr); var scriptCompiled = CompileScript(null, normalizedScript, out errorMessage); return scriptCompiled; } // 原有变量表达式大多是单个查询表达式,包成 object 后可直接作为脚本返回值。 var expressionScript = $"(object)({variableExpr})"; var compiled = CompileScript(null, expressionScript, out var expressionErrorMessage); if (compiled != null) { errorMessage = null; return compiled; } // 单表达式编译失败时,再按多语句脚本兜底,兼容包含局部变量、if、throw 的复杂变量。 var normalizedFallbackScript = NormalizeVariableScript(variableExpr); compiled = CompileScript(null, normalizedFallbackScript, out var scriptErrorMessage); if (compiled != null) { errorMessage = null; return compiled; } errorMessage = $"单表达式编译失败:{expressionErrorMessage};多语句脚本编译失败:{scriptErrorMessage}"; return null; } /// /// 粗略判断变量表达式是否更像多语句脚本。 /// 多语句脚本不能再包成 (object)(...),需要直接按 C# Script 编译。 /// 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; } /// /// 规范化多语句变量脚本。 /// Roslyn Script 更稳定的返回方式是“最后一行表达式”,这里把末尾 return 表达式转换为末尾表达式。 /// 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() .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; } /// /// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。 /// 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; } /// /// 用 Roslyn 编译 C# 布尔表达式为可执行委托。 /// private static Func CompileScript(long? ruleId, string scriptCode, out string errorMessage) { return CompileScript(ruleId, scriptCode, out errorMessage); } /// /// 用 Roslyn 编译 C# 脚本代码为指定返回类型的可执行委托。 /// 规则公式编译为 bool,变量取值表达式编译为 object。 /// private static Func CompileScript(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(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 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; } }; } /// /// 脚本安全校验。 /// 在 Roslyn 编译前先扫描语法树,禁止写库、反射、文件、网络、进程等高风险语法入口。 /// 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().Any()) { errorMessage = "规则表达式不允许包含赋值语句"; return false; } if (root.DescendantNodes().OfType().Any(IsIncrementOrDecrement) || root.DescendantNodes().OfType().Any(IsIncrementOrDecrement)) { errorMessage = "规则表达式不允许包含自增或自减语句"; return false; } foreach (var memberAccess in root.DescendantNodes().OfType()) { 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()) { 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()) { var typeName = GetTypeName(objectCreation.Type); if (!string.IsNullOrWhiteSpace(typeName) && ForbiddenTypeOrIdentifierNames.Contains(typeName)) { errorMessage = $"规则表达式不允许创建高风险类型:{typeName}"; return false; } } foreach (var invocation in root.DescendantNodes().OfType()) { var methodName = GetInvocationName(invocation.Expression); if (!string.IsNullOrWhiteSpace(methodName) && ForbiddenInvocationNames.Contains(methodName)) { errorMessage = $"规则表达式不允许调用高风险方法:{methodName}"; return false; } } errorMessage = null; return true; } /// /// 判断前缀一元表达式是否为自增/自减。 /// private static bool IsIncrementOrDecrement(PrefixUnaryExpressionSyntax expression) { return expression.IsKind(SyntaxKind.PreIncrementExpression) || expression.IsKind(SyntaxKind.PreDecrementExpression); } /// /// 判断后缀一元表达式是否为自增/自减。 /// private static bool IsIncrementOrDecrement(PostfixUnaryExpressionSyntax expression) { return expression.IsKind(SyntaxKind.PostIncrementExpression) || expression.IsKind(SyntaxKind.PostDecrementExpression); } /// /// 判断标识符是否是成员访问右侧名称,例如 p.Path 中的 Path。 /// private static bool IsMemberAccessName(IdentifierNameSyntax identifier) { return identifier.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == identifier; } /// /// 从调用表达式中提取方法名,兼容 SaveChanges() 和 DbContext.SaveChanges() 两种写法。 /// private static string GetInvocationName(ExpressionSyntax expression) { return expression switch { MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText, IdentifierNameSyntax identifier => identifier.Identifier.ValueText, _ => null }; } /// /// 从类型语法中提取类型短名,用于匹配高风险类型黑名单。 /// 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 }; } /// /// 将操作符统一为 C# 操作符 /// 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 }; } } }