using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace YLErp.Modules.RiskEngine { /// /// Roslyn 脚本全局变量容器 /// CSharpScript 执行时通过此对象传递 DataMap /// public class ScriptGlobals { /// /// 数据字典:key=表名/前缀,value=对应数据对象 /// public Dictionary DataMap { get; set; } } /// /// 规则编译器(Roslyn 版) /// /// 编译时机:规则定义时(零运行时解析开销) /// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool /// /// 相比表达式树方案的优势: /// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点 /// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法 /// - 可调试:生成的脚本代码可直接阅读和理解 /// /// 示例: /// FormulaJson: { "conditions": [{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}] } /// 生成脚本:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m /// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存) /// public static class RuleCompiler { /// /// 变量编码 → 类型全名的映射 /// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"]) /// static IYcLogger _logger =LogFactory.GetLogger("RuleCompiler"); private static readonly Dictionary VariableTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["trade"] = "YLErp.DBModels.trade", ["swap_position"] = "YLErp.DBModels.swap_position", ["client"] = "YLErp.DBModels.client", ["credit"] = "YLErp.DBModels.credit", ["client_marginrate"] = "YLErp.DBModels.client_marginrate", ["market"] = "YLErp.DBModels.market", ["calc"] = "YLErp.DBModels.calc", ["sys"] = "YLErp.DBModels.sys", ["eod_swap_position"] = "YLErp.DBModels.eod_swap_position", ["realtime_trade_risk"] = "YLErp.DBModels.realtime_trade_risk", }; /// /// 将规则编译为可执行委托,并写入 rule.CompiledScript /// 调用时机:规则创建时 / 规则加载时 / 缓存刷新时 /// public static void Compile(this RiskRule rule) { if (string.IsNullOrWhiteSpace(rule.FormulaJson)) { throw new ArgumentException("规则 FormulaJson 不能为空", nameof(rule.FormulaJson)); } // 1. 解析 FormulaJson var conditions = ParseFormulaJson(rule.FormulaJson); // 2. 生成 Roslyn C# 脚本代码 string scriptCode = BuildScriptCode(conditions); // 3. 编译脚本(只编译一次,后续直接执行) var compiled = CompileScript(scriptCode); // 4. 写入规则(不持久化,仅内存缓存) rule.CompiledScript = compiled; } /// /// 解析 FormulaJson,提取条件列表 /// private static List<(string variableCode, string op, object value, string variableType)> ParseFormulaJson(string formulaJson) { var result = new List<(string, string, object, string)>(); var jObj = JObject.Parse(formulaJson); var conditions = jObj["conditions"] as JArray; if (conditions == null || !conditions.Any()) { throw new ArgumentException("FormulaJson 中缺少 conditions"); } try { string variableCode = cond["variableCode"]?.Value(); string op = cond["operator"]?.Value(); object value = cond["value"]?.Value(); string variableType = cond["variableType"]?.Value() ?? "numeric"; if (string.IsNullOrWhiteSpace(variableCode) || string.IsNullOrWhiteSpace(op)) { throw new ArgumentException("条件中缺少 variableCode 或 operator"); } result.Add((variableCode, op, value, variableType)); } var result = ValidateAndCompileFormula(rule.RuleExpr); if (!result.Success) { return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}"); } rule.CompiledScript = result.CompiledScript; RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript); return result; } /// /// 根据条件列表生成 Roslyn C# 脚本代码 /// 多条件自动用 &&(AND)连接 /// private static string BuildScriptCode(List<(string variableCode, string op, object value, string variableType)> conditions) { var exprParts = new List(); foreach (var (variableCode, op, value, variableType) in conditions) { // 解析 variableCode:前缀.属性名 var parts = variableCode.Split('.'); if (parts.Length != 2) { throw new ArgumentException($"variableCode 格式不正确:{variableCode}"); } string prefix = parts[0].Trim().ToLower(); string propertyName = parts[1].Trim(); if (!VariableTypeMap.TryGetValue(prefix, out string typeFullName)) { throw new ArgumentException($"未知前缀:{prefix}"); } // 格式化阈值 string valueLiteral = FormatValueLiteral(value, variableType); // 格式化操作符 string csharpOp = FormatOperator(op); // 生成单条件表达式 string expr = $"{leftExpr} {csharpOp} {valueLiteral}"; exprParts.Add(expr); } // 多条件用 && 连接 return string.Join(" && ", exprParts); } /// /// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。 /// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。 /// 数值类型会自动包一层 Convert.ToDecimal,便于与 decimal 阈值比较。 /// private static string BuildMemberAccessExpression(string expression, string variableType) { var parts = expression.Split('.'); if (parts.Length < 2) { throw new ArgumentException($"expression 格式不正确:{expression}"); } string prefix = parts[0].Trim().ToLower(); if (!VariableTypeMap.TryGetValue(prefix, out string typeFullName)) { throw new ArgumentException($"未知前缀:{prefix}"); } // 除前缀外,其余部分都视为成员访问路径,便于后续扩展多级属性访问 string memberAccess = string.Join(".", parts.Skip(1).Select(p => p.Trim())); string objectExpr = $"(({typeFullName})DataMap[\"{prefix}\"]).{memberAccess}"; if (variableType == "numeric" || variableType == "number") { return $"Convert.ToDecimal({objectExpr})"; } return objectExpr; } /// /// 用 Roslyn 编译 C# 脚本代码为可执行委托 /// private static Func CompileScript(string scriptCode) { // 配置编译选项:引用必要的程序集 var options = ScriptOptions.Default .WithReferences( typeof(RiskContext).Assembly, // YLErpDAL typeof(YLErp.DBModels.trade).Assembly // Model 所在程序集 ) .WithImports("System"); // 创建脚本(尚未执行,仅编译) 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()) { string errorMsg = string.Join("; ", errors.Select(e => e.GetMessage())); _logger.Error($"规则编译失败:{errorMsg}\n脚本代码:{scriptCode}"); throw new InvalidOperationException($"脚本编译失败:{errorMsg}"); } // 生成可调用委托 var runner = script.CreateDelegate(); // 包装为同步的 Func return ctx => { var globals = new ScriptGlobals { DataMap = ctx.DataMap }; try { return runner(globals).GetAwaiter().GetResult(); } catch (Exception ex) { // 脚本执行异常(如空引用、类型转换失败)视为规则不触发 _logger.Error($"规则执行异常:{ex.Message}\n脚本代码:{scriptCode}"); return false; } }; } /// /// 格式化阈值为 C# 字面量 /// private static string FormatValueLiteral(object value, string variableType) { if (value == null) return "null"; string strValue = value.ToString(); // 数值类型统一加 m 后缀(decimal) if (variableType == "numeric" || variableType == "number") { if (decimal.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal d)) { return d.ToString(CultureInfo.InvariantCulture) + "m"; } } // 布尔 if (variableType == "boolean" || variableType == "bool") { if (bool.TryParse(strValue, out bool b)) { return b ? "true" : "false"; } } // 日期 if (variableType == "date" && DateTime.TryParse(strValue, out DateTime dt)) { return $"DateTime.Parse(\"{dt:yyyy-MM-dd}\")"; } // 字符串(兜底) return $"\"{strValue.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""; } /// /// 将操作符统一为 C# 操作符 /// private static string FormatOperator(string op) { return op?.Trim() switch { "=" or "==" => "==", "!=" or "≠" => "!=", ">" => ">", "<" => "<", ">=" => ">=", "<=" => "<=", _ => throw new ArgumentException($"不支持的操作符:{op}") }; } } }