using Microsoft.CodeAnalysis.CSharp.Scripting; 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 { static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler"); /// /// 校验并编译规则表达式。 /// 当前 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); } } /// /// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。 /// 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) { errorMessage = null; var ruleIdText = ruleId.HasValue ? ruleId.Value.ToString() : "未落库"; // 配置编译选项:引用必要的程序集 var options = ScriptOptions.Default .WithReferences( typeof(RiskContext).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"); // 创建脚本(尚未执行,仅编译) 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; } }; } } 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 }; } } }