Files
zszq-trs/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
T
2026-07-02 17:04:20 +08:00

225 lines
8.6 KiB
C#

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
{
/// <summary>
/// Roslyn 脚本全局变量容器
/// CSharpScript 执行时通过此对象传递 DataMap
/// </summary>
public class ScriptGlobals
{
/// <summary>
/// 数据字典:key=表名/前缀,value=对应数据对象
/// </summary>
public Dictionary<string, object> DataMap { get; set; }
}
/// <summary>
/// 规则编译器(Roslyn 版)
///
/// 编译时机:规则定义时(零运行时解析开销)
/// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool
///
/// 相比表达式树方案的优势:
/// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点
/// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法
/// - 可调试:生成的脚本代码可直接阅读和理解
///
/// 示例:
/// RuleExpr: Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
/// 编译结果:Func&lt;RiskContext, bool&gt;(内部通过 Roslyn 编译缓存)
/// </summary>
public static class RuleCompiler
{
/// <summary>
/// 变量编码 → 类型全名的映射
/// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"])
/// </summary>
static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
private static readonly Dictionary<string, string> VariableTypeMap = new Dictionary<string, string>(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",
["underlying_manager"] = "YLErp.DBModels.underlying_manager",
["sys"] = "YLErp.DBModels.sys",
["eod_swap_position"] = "YLErp.DBModels.eod_swap_position",
["realtime_trade_risk"] = "YLErp.DBModels.realtime_trade_risk",
};
/// <summary>
/// 校验并编译规则表达式。
/// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
/// </summary>
public static RuleCompileResult ValidateAndCompileFormula(long ruleId, string formulaExp)
{
if (string.IsNullOrWhiteSpace(formulaExp))
{
_logger.Error($"规则表达式为空,无法编译 - RuleId: {ruleId}");
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: {ruleId}, Error: {ex.Message}\n脚本代码:{formulaExp}");
return RuleCompileResult.Fail(ex.Message);
}
}
/// <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;
RuleCompiledCache.Remove(rule.Id.ToString());
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)
{
errorMessage = null;
// 配置编译选项:引用必要的程序集
var options = ScriptOptions.Default
.WithReferences(
typeof(RiskContext).Assembly,
typeof(YLErp.DBModels.trade).Assembly
)
.WithImports("System");
// 创建脚本(尚未执行,仅编译)
var script = CSharpScript.Create<bool>(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: {ruleId}, Error: {errorMessage}\n脚本代码:{scriptCode}");
return null;
}
// 生成可调用委托
var runner = script.CreateDelegate();
// 包装为同步的 Func<RiskContext, bool>
return ctx =>
{
try
{
var globals = new ScriptGlobals { DataMap = ctx.DataMap };
return runner(globals).GetAwaiter().GetResult();
}
catch (Exception ex)
{
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
_logger.Error($"规则执行异常 - RuleId: {ruleId}, Error: {ex.Message}\n脚本代码:{scriptCode}");
return false;
}
};
}
}
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
};
}
}
}