229 lines
8.5 KiB
C#
229 lines
8.5 KiB
C#
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
|
|
{
|
|
/// <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<RiskContext, bool>(内部通过 Roslyn 编译缓存)
|
|
/// </summary>
|
|
public static class RuleCompiler
|
|
{
|
|
static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
|
|
|
|
/// <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>
|
|
/// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
|
|
/// </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)
|
|
{
|
|
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<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: {ruleIdText}, Error: {errorMessage}\n脚本代码:{scriptCode}");
|
|
return null;
|
|
}
|
|
|
|
// 生成可调用委托
|
|
var runner = script.CreateDelegate();
|
|
|
|
// 包装为同步的 Func<RiskContext, bool>
|
|
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}");
|
|
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
|
|
};
|
|
}
|
|
}
|
|
}
|