支持从数据库读取rule与application,进行匹配校验,以及拼接返回值
This commit is contained in:
@@ -5,7 +5,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则编译结果的进程内缓存。
|
||||
/// 当前第一版按 RuleCode 缓存 Roslyn 编译后的可执行委托。
|
||||
/// 当前第一版按规则 Id 缓存 Roslyn 编译后的可执行委托。
|
||||
/// </summary>
|
||||
public static class RuleCompiledCache
|
||||
{
|
||||
|
||||
@@ -32,8 +32,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// - 可调试:生成的脚本代码可直接阅读和理解
|
||||
///
|
||||
/// 示例:
|
||||
/// ConditionJson: { "conditions": [{"variableId":1,"operator":">","value":100000000}] }
|
||||
/// 生成脚本:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
/// RuleExpr: Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
/// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存)
|
||||
/// </summary>
|
||||
public static class RuleCompiler
|
||||
@@ -71,14 +70,14 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并编译公式表达式。
|
||||
/// 当前 FormulaExp 要求是 Roslyn 可直接执行的 bool 表达式。
|
||||
/// 校验并编译规则表达式。
|
||||
/// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
|
||||
/// </summary>
|
||||
public static RuleCompileResult ValidateAndCompileFormula(string formulaExp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(formulaExp))
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 FormulaExp 不能为空");
|
||||
return RuleCompileResult.Fail("规则 RuleExpr 不能为空");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -101,100 +100,87 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
return RuleCompileResult.Fail("规则不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rule.RuleCode))
|
||||
|
||||
if (rule.Id <= 0)
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 Code 不能为空");
|
||||
return RuleCompileResult.Fail("规则 Id 不能为空");
|
||||
}
|
||||
|
||||
var result = ValidateAndCompileFormula(rule.RuleExpr);
|
||||
if (!result.Success)
|
||||
{
|
||||
return RuleCompileResult.Fail($"规则[{rule.RuleCode}]编译失败:{result.ErrorMessage}");
|
||||
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
rule.CompiledScript = result.CompiledScript;
|
||||
RuleCompiledCache.Set(rule.RuleCode, result.CompiledScript);
|
||||
RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 ConditionJson,提取条件列表
|
||||
/// 解析 ConditionJson,提取条件列表。
|
||||
/// 要求 expression 明确描述实际取值路径。
|
||||
/// 当前主执行链不再使用该方法,仅作为早期方案保留。
|
||||
/// </summary>
|
||||
private static List<(long variableId, string op, object value, string variableType)> ParseConditionJson(string conditionJson)
|
||||
// private static List<(string variableCode, string expression, string op, object value, string variableType)> ParseConditionJson(string conditionJson)
|
||||
// {
|
||||
// var result = new List<(string, string, string, object, string)>();
|
||||
// var jObj = JObject.Parse(formulaJson);
|
||||
// var conditions = jObj["conditions"] as JArray;
|
||||
|
||||
// if (conditions == null || !conditions.Any())
|
||||
// {
|
||||
// throw new ArgumentException("ConditionJson 中缺少 conditions");
|
||||
// }
|
||||
|
||||
// foreach (var cond in conditions)
|
||||
// {
|
||||
// string variableCode = cond["variableCode"]?.Value<string>();
|
||||
// string expression = cond["expression"]?.Value<string>();
|
||||
// string op = cond["operator"]?.Value<string>();
|
||||
// object value = cond["value"]?.Value<object>();
|
||||
// string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
|
||||
|
||||
// if (string.IsNullOrWhiteSpace(expression) || string.IsNullOrWhiteSpace(op))
|
||||
// {
|
||||
// throw new ArgumentException("条件中缺少 expression 或 operator");
|
||||
// }
|
||||
|
||||
// result.Add((variableCode, expression, op, value, variableType));
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 根据条件列表生成 Roslyn C# 脚本代码。
|
||||
/// 多条件自动用 &&(AND)连接。
|
||||
/// 左值表达式统一基于 expression 构建。
|
||||
/// </summary>
|
||||
private static string BuildScriptCode(List<(string variableCode, string expression, string op, object value, string variableType)> conditions)
|
||||
{
|
||||
var result = new List<(long, string, object, string)>();
|
||||
var jObj = JObject.Parse(conditionJson);
|
||||
var conditions = jObj["conditions"] as JArray;
|
||||
var exprParts = new List<string>();
|
||||
|
||||
if (conditions == null || !conditions.Any())
|
||||
foreach (var (_, expression, op, value, variableType) in conditions)
|
||||
{
|
||||
throw new ArgumentException("ConditionJson 中缺少 conditions");
|
||||
// 先根据 expression 构建左值访问表达式,再拼接比较符和右值字面量
|
||||
string leftExpr = BuildMemberAccessExpression(expression, variableType);
|
||||
|
||||
// 格式化阈值
|
||||
string valueLiteral = FormatValueLiteral(value, variableType);
|
||||
|
||||
// 格式化操作符
|
||||
string csharpOp = FormatOperator(op);
|
||||
|
||||
// 生成单条件表达式
|
||||
string expr = $"{leftExpr} {csharpOp} {valueLiteral}";
|
||||
exprParts.Add(expr);
|
||||
}
|
||||
|
||||
foreach (var cond in conditions)
|
||||
{
|
||||
long variableId = cond["variableId"]?.Value<long>() ?? 0;
|
||||
string op = cond["operator"]?.Value<string>();
|
||||
object value = cond["value"]?.Value<object>();
|
||||
string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
|
||||
|
||||
if (variableId <= 0 || string.IsNullOrWhiteSpace(op))
|
||||
{
|
||||
throw new ArgumentException("条件中缺少 variableId 或 operator");
|
||||
}
|
||||
|
||||
result.Add((variableId, op, value, variableType));
|
||||
}
|
||||
|
||||
return result;
|
||||
// 多条件用 && 连接
|
||||
return string.Join(" && ", exprParts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据条件列表生成 Roslyn C# 脚本代码
|
||||
/// 多条件自动用 &&(AND)连接
|
||||
/// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式,
|
||||
/// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr
|
||||
/// </summary>
|
||||
//private static string BuildScriptCode(List<(long variableId, string op, object value, string variableType)> conditions)
|
||||
//{
|
||||
// var exprParts = new List<string>();
|
||||
|
||||
// foreach (var (variableId, op, value, variableType) in conditions)
|
||||
// {
|
||||
// // TODO: 通过 variableId 查变量池获取 VariableExpr(如 "trade.StockEqvNotional")
|
||||
// // 当前暂用 variableId.ToString() 占位,后续接入变量池查询
|
||||
// string variableExpr = variableId.ToString();
|
||||
|
||||
// // 解析 variableExpr:前缀.属性名
|
||||
// var parts = variableExpr.Split('.');
|
||||
// if (parts.Length != 2)
|
||||
// {
|
||||
// throw new ArgumentException($"variableExpr 格式不正确:{variableExpr}(variableId={variableId})");
|
||||
// }
|
||||
|
||||
// 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);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。
|
||||
/// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。
|
||||
@@ -243,13 +229,15 @@ namespace YLErp.Modules.RiskEngine
|
||||
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())
|
||||
{
|
||||
string errorMsg = string.Join("; ", errors.Select(e => e.GetMessage()));
|
||||
throw new InvalidOperationException($"脚本编译失败:{errorMsg}\n脚本代码:{scriptCode}");
|
||||
_logger.Error($"规则编译失败:{errorMsg}\n脚本代码:{scriptCode}");
|
||||
throw new InvalidOperationException($"脚本编译失败:{errorMsg}");
|
||||
}
|
||||
|
||||
// 生成可调用委托
|
||||
|
||||
@@ -17,28 +17,28 @@ using YLErp.Model;
|
||||
【当前实现口径】(以本注释和实际代码为准)
|
||||
|
||||
核心概念:
|
||||
- RiskRule(风控规则):规则定义本体,当前重点字段包括 RuleCode、RuleName、
|
||||
FormulaText、FormulaExp、Version、CompiledScript。
|
||||
- RiskRule(风控规则):规则定义本体,当前重点字段包括 Id、RuleName、
|
||||
RuleText、RuleExpr、Version、CompiledScript。
|
||||
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
|
||||
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
|
||||
- FormulaExp:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
|
||||
- RuleExpr:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
|
||||
例如直接访问 DataMap["trade"] 后做数值或日期比较。
|
||||
- FormulaJson:当前已在规则定义和样例代码中停用,不参与主执行链路。
|
||||
- ConditionJson:当前已在规则定义中保留,但不参与主执行链路。
|
||||
- RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DataMap。
|
||||
- RuleCompiledCache:进程内编译结果缓存,按 RuleCode 缓存 Func<RiskContext, bool>。
|
||||
- RuleCompiledCache:进程内编译结果缓存,按规则 Id 缓存 Func<RiskContext, bool>。
|
||||
|
||||
当前编译流程:
|
||||
1. 外部准备好规则对象,FormulaExp 中直接写最终可执行表达式
|
||||
1. 外部准备好规则对象,在 RuleExpr 中直接写最终可执行表达式
|
||||
2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
|
||||
3. 内部使用 Roslyn 编译 FormulaExp,生成 Func<RiskContext, bool>
|
||||
3. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
|
||||
4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
|
||||
5. 执行阶段优先从 RuleCompiledCache 取委托执行
|
||||
|
||||
当前执行流程(EvaluateRisk):
|
||||
1. 加载示例规则列表和示例应用列表
|
||||
1. 从数据库加载规则列表和应用列表
|
||||
2. 按应用状态、TriggerPoints 过滤有效应用
|
||||
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
|
||||
4. 根据命中的 RuleCode 找到对应规则
|
||||
4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
|
||||
5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
|
||||
6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
|
||||
7. 返回 RiskResult
|
||||
@@ -50,11 +50,11 @@ using YLErp.Model;
|
||||
- 某维度留空:视为该维度不限制
|
||||
|
||||
【与早期方案的主要差异】
|
||||
- 当前不是 Content 解析或表达式树主导,而是 FormulaExp 直编译主导
|
||||
- 当前不是 Content 解析或表达式树主导,而是 RuleExpr 直编译主导
|
||||
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
|
||||
- 编译器文件已放入 RiskEngine/Compile 目录下
|
||||
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 FormulaExp 直接表达
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 RuleExpr 直接表达
|
||||
|
||||
【当前代码结构】
|
||||
风控引擎层:YLErpDAL/Modules/RiskEngine/
|
||||
@@ -75,7 +75,7 @@ using YLErp.Model;
|
||||
✅ 已完成:
|
||||
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
|
||||
2. 风控上下文通过 DataMap 传入 trade 对象,支持规则脚本直接访问交易字段
|
||||
3. FormulaExp 直编译方案已接入,支持数值比较和日期比较
|
||||
3. RuleExpr 直编译方案已接入,支持数值比较和日期比较
|
||||
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
|
||||
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
|
||||
6. RuleCompiler 已支持校验 + 编译 + 写缓存
|
||||
@@ -84,9 +84,9 @@ using YLErp.Model;
|
||||
|
||||
【待办事项 / TODO】
|
||||
⬜ 1. 接入真实规则来源
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 示例数据
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 的规则来源
|
||||
⬜ 2. 增加接口层
|
||||
- 提供前端提交 FormulaExp 后的校验接口
|
||||
- 提供前端提交 RuleExpr 后的校验接口
|
||||
- 提供规则保存 / 发布后预编译并写缓存的入口
|
||||
⬜ 3. 启动预热与缓存刷新
|
||||
- 服务启动时批量加载有效规则并预编译
|
||||
@@ -96,12 +96,12 @@ using YLErp.Model;
|
||||
- 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
|
||||
⬜ 5. 补测试样例
|
||||
- 数值比较、日期比较、维度过滤、非法公式、边界值
|
||||
⬜ 6. 完善 FormulaExp 配套能力
|
||||
⬜ 6. 完善 RuleExpr 配套能力
|
||||
- 提供前端可用的表达式编辑、校验与错误提示
|
||||
- 约束可用变量、类型转换和脚本安全边界
|
||||
|
||||
【注意事项】
|
||||
1. 当前 FormulaExp 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
2. DataMap 中的值由宿主业务代码准备,编译器与执行器本身不负责查库补数
|
||||
3. 当前缓存的是 Func<RiskContext, bool>,这是规则判断函数,不是事件处理器
|
||||
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
|
||||
@@ -168,7 +168,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
// Step 2: 先按 Application 过滤 Active 状态和 TriggerPoint 匹配的应用
|
||||
// ============================================================
|
||||
var activeApps = applications
|
||||
.Where(a => a.Status == RiskRuleApplicationStatusEnum.Active)
|
||||
.Where(a => a.Status == RiskRuleStatus.Active)
|
||||
.ToList();
|
||||
var apps = activeApps
|
||||
.Where(a => !string.IsNullOrEmpty(a.TriggerPoints))
|
||||
@@ -196,113 +196,139 @@ namespace YLErp.Modules.RiskEngine
|
||||
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的应用数: {matchedApplications.Count}");
|
||||
|
||||
// ============================================================
|
||||
// Step 3: 遍历匹配的应用,通过 RuleCode 关联规则并执行预编译委托
|
||||
// Step 3: 遍历匹配的应用,通过 RuleIds 关联规则并执行预编译委托
|
||||
// ============================================================
|
||||
foreach (var application in matchedApplications)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 3.1 通过 RuleCode 关联规则定义
|
||||
// 3.1 通过 RuleIds 关联规则定义
|
||||
// ------------------------------------------------------------
|
||||
var rule = rules.FirstOrDefault(r => r.RuleCode == application.RuleCode);
|
||||
if (rule == null)
|
||||
var applicationRuleIds = ParseRuleIds(application.RuleIds);
|
||||
if (!applicationRuleIds.Any())
|
||||
{
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleCode: {application.RuleCode}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"未找到规则定义[{application.RuleCode}];";
|
||||
_logger.Info($"[风控引擎] 应用未配置有效规则 - RuleIds: {application.RuleIds}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(rule.RuleCode, out var compiledScript))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleCode: {rule.RuleCode}");
|
||||
var applicationRules = rules
|
||||
.Where(r => applicationRuleIds.Contains(r.Id) && r.Id == 3)
|
||||
.ToList();
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
var missingRuleIds = applicationRuleIds
|
||||
.Where(ruleId => applicationRules.All(r => r.Id != ruleId))
|
||||
.ToList();
|
||||
|
||||
foreach (var missingRuleId in missingRuleIds)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {missingRuleId}, ApplicationRuleIds: {application.RuleIds}");
|
||||
}
|
||||
|
||||
foreach (var rule in applicationRules)
|
||||
{
|
||||
var ruleId = rule.Id.ToString();
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则编译失败 - RuleCode: {rule.RuleCode}, Error: {compileResult.ErrorMessage}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.RuleCode}]编译失败:{compileResult.ErrorMessage};";
|
||||
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
|
||||
continue;
|
||||
}
|
||||
|
||||
compiledScript = compileResult.CompiledScript;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.3 执行预编译委托
|
||||
// ------------------------------------------------------------
|
||||
bool triggered = false;
|
||||
try
|
||||
{
|
||||
triggered = compiledScript(context);
|
||||
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
compiledScript = compileResult.CompiledScript;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.3 执行预编译委托
|
||||
// ------------------------------------------------------------
|
||||
bool triggered = false;
|
||||
try
|
||||
{
|
||||
triggered = compiledScript(context);
|
||||
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.Id}]执行异常:{ex.Message};";
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
switch (application.ControlStrategy)
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
case RiskControlStrategyEnum.Block:
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "禁止",
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
});
|
||||
break;
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
case RiskControlStrategyEnum.Approval:
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "审批",
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
});
|
||||
break;
|
||||
switch (application.ControlStrategy)
|
||||
{
|
||||
case RiskControlStrategy.Block:
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = rule.Id.ToString(),
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Block,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
});
|
||||
break;
|
||||
|
||||
case RiskControlStrategyEnum.Warning:
|
||||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
|
||||
break;
|
||||
case RiskControlStrategy.Approval:
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = ruleId,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Approval,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
|
||||
break;
|
||||
case RiskControlStrategy.Warning:
|
||||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = ruleId,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Warning,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:提示"
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未触发 - RuleId: {rule.Id}, TriggerPoint: {triggerPoint}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未触发 - RuleCode: {rule.RuleCode}, TriggerPoint: {triggerPoint}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 4: 聚合最终结果
|
||||
// ============================================================
|
||||
if (!result.Blocked && !result.NeedApproval && result.Warnings.Count == 0 && !result.HasError)
|
||||
if (!result.Blocked && !result.NeedApproval)
|
||||
{
|
||||
result.Passed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Passed = false;
|
||||
}
|
||||
|
||||
_logger.Info($"[风控引擎] EvaluateRisk 完成 - TradeId: {context?.TradeId}, Passed: {result.Passed}, Blocked: {result.Blocked}, NeedApproval: {result.NeedApproval}, TriggeredRules: {result.TriggeredRules.Count}");
|
||||
}
|
||||
@@ -312,11 +338,10 @@ namespace YLErp.Modules.RiskEngine
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleCode = "ENGINE_ERROR",
|
||||
RuleId = "ENGINE_ERROR",
|
||||
RuleName = "风控引擎执行异常",
|
||||
ControlStrategy = "审批",
|
||||
RuleText = ex.Message,
|
||||
Message = $"风控引擎异常:{ex.Message},需审批通过"
|
||||
Message = $"风控引擎异常:{ex.Message}"
|
||||
});
|
||||
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
|
||||
}
|
||||
@@ -325,74 +350,75 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载规则列表(TODO: 后续接入真实内存缓存和数据库查询)
|
||||
/// 当前返回示例规则用于验证流程
|
||||
/// 加载规则列表(TODO: 后续接入真实内存缓存)
|
||||
/// 当前从数据库读取规则用于验证流程
|
||||
/// </summary>
|
||||
private List<RiskRule> LoadRules()
|
||||
{
|
||||
//名义本金:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap[\"trade\"]).StockEqvNotional)
|
||||
//挂钩标的到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).MaturityDate
|
||||
//合约到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate
|
||||
var rules = DbContext.glms_risk_rule
|
||||
.AsNoTracking()
|
||||
.Where(r => r.Status != RiskRuleStatus.Deleted)
|
||||
.OrderByDescending(r => r.UpdateDate ?? r.OptDate)
|
||||
.Select(r => new RiskRule
|
||||
{
|
||||
Id = r.id,
|
||||
RuleName = r.RuleName,
|
||||
RuleText = r.RuleText,
|
||||
ConditionJson = r.ConditionJson,
|
||||
RuleExpr = r.RuleExpr,
|
||||
Version = r.Version,
|
||||
IsDeleted = r.Status == RiskRuleStatus.Deleted,
|
||||
OptId = r.OptId ?? 0,
|
||||
OptName = r.OptName,
|
||||
OptDate = r.OptDate ?? DateTime.MinValue,
|
||||
UpdateOptId = r.UpdateOptId ?? 0,
|
||||
UpdateOptName = r.UpdateOptName,
|
||||
UpdateDate = r.UpdateDate ?? r.OptDate ?? DateTime.MinValue
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 示例规则1:先保留一个最小可运行样例,只验证名义本金阈值判断
|
||||
var Rule1 = new RiskRule
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
Id = 103,
|
||||
RuleName = "测试规则:名义本金超过1亿",
|
||||
RuleText = "名义本金>1亿",
|
||||
// FormulaJson = "{ \"conditions\": [" +
|
||||
// "{\"variableCode\":\"StockEqvNotional\",\"expression\":\"trade.StockEqvNotional\",\"operator\":\">\",\"value\":100000000,\"variableType\":\"number\"}" +
|
||||
// "] }",
|
||||
RuleExpr = "Convert.ToDecimal(((YLErp.DBModels.trade)DataMap[\"trade\"]).StockEqvNotional) > 100000000m",
|
||||
Version = 1
|
||||
};
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则预编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
// 示例规则2:挂钩标的到期日小于合约到期日
|
||||
var Rule2 = new RiskRule
|
||||
{
|
||||
Id = 104,
|
||||
RuleCode = "RISK-20260623-0004",
|
||||
RuleName = "挂钩标的到期日小于合约到期日",
|
||||
RuleText = "挂钩标的到期日<合约到期日",
|
||||
//FormulaJson = "{ \"conditions\": [{\"variableCode\":\"MaturityDate\",\"expression\":\"trade.MaturityDate\",\"operator\":\"<\",\"value\":\"trade.ExerciseDate\",\"variableType\":\"date\"}] }",
|
||||
RuleExpr = "((YLErp.DBModels.trade)DataMap[\"trade\"]).MaturityDate < ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate",
|
||||
Version = 1
|
||||
};
|
||||
|
||||
// 当前样例规则在加载阶段预编译并写入缓存,便于执行阶段直接取用
|
||||
RuleCompiler.Compile(Rule1);
|
||||
RuleCompiler.Compile(Rule2);
|
||||
|
||||
return new List<RiskRule> { Rule1, Rule2 };
|
||||
return rules;
|
||||
}
|
||||
/// <summary>
|
||||
/// 加载application
|
||||
/// 当前返回示例规则用于验证流程
|
||||
/// 当前从数据库读取应用配置用于验证流程
|
||||
/// </summary>
|
||||
private List<RiskRuleApplication> LoadApplications()
|
||||
{
|
||||
var application1 = new RiskRuleApplication
|
||||
{
|
||||
Id = 1,
|
||||
RuleCode = "RISK-20260622-0003",
|
||||
Status = RiskRuleApplicationStatusEnum.Active,
|
||||
ControlStrategy = RiskControlStrategyEnum.Block,
|
||||
TriggerPoints = "BOOK_CONFIRM",
|
||||
ScopeIsGlobal = true,
|
||||
};
|
||||
|
||||
var application2 = new RiskRuleApplication
|
||||
{
|
||||
Id = 2,
|
||||
RuleCode = "RISK-20260623-0004",
|
||||
Status = RiskRuleApplicationStatusEnum.Active,
|
||||
ControlStrategy = RiskControlStrategyEnum.Block,
|
||||
TriggerPoints = "BOOK_CLICK,BOOK_CONFIRM",
|
||||
ScopeIsGlobal = false,
|
||||
ScopeTradeTypes = "收益互换",
|
||||
};
|
||||
|
||||
return new List<RiskRuleApplication> { application1, application2 };
|
||||
return DbContext.glms_risk_rule_application
|
||||
.AsNoTracking()
|
||||
.Where(a => a.Status != RiskRuleStatus.Deleted)
|
||||
.OrderByDescending(a => a.UpdateDate ?? a.OptDate)
|
||||
.Select(a => new RiskRuleApplication
|
||||
{
|
||||
Id = a.id,
|
||||
RuleIds = a.RuleIds,
|
||||
Status = a.Status,
|
||||
ControlStrategy = a.ControlStrategy,
|
||||
TriggerPoints = a.TriggerPoints,
|
||||
ScopeIsGlobal = a.ScopeIsGlobal,
|
||||
ScopeAssetBookIds = a.ScopeAssetBookIds,
|
||||
ScopeClientIds = a.ScopeClientIds,
|
||||
ScopeUnderlyingTypes = a.ScopeUnderlyingTypes,
|
||||
ScopeTradeTypes = a.ScopeTradeTypes,
|
||||
Version = a.Version,
|
||||
OptId = a.OptId ?? 0,
|
||||
OptName = a.OptName,
|
||||
OptDate = a.OptDate ?? DateTime.MinValue,
|
||||
UpdateOptId = a.UpdateOptId ?? 0,
|
||||
UpdateOptName = a.UpdateOptName,
|
||||
UpdateDate = a.UpdateDate ?? a.OptDate ?? DateTime.MinValue
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -431,6 +457,29 @@ namespace YLErp.Modules.RiskEngine
|
||||
&& tradeTypeMatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析应用配置中的规则ID列表。
|
||||
/// 多个规则ID使用逗号分隔,返回去空格后的 long 集合。
|
||||
/// </summary>
|
||||
private List<long> ParseRuleIds(string ruleIds)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruleIds))
|
||||
{
|
||||
return new List<long>();
|
||||
}
|
||||
|
||||
var ids = new List<long>();
|
||||
foreach (var part in ruleIds.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (long.TryParse(part.Trim(), out long id))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个范围字段是否为空。
|
||||
/// 为空表示该维度不限制。
|
||||
|
||||
@@ -22,15 +22,15 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public bool NeedApproval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否存在执行异常
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
///// <summary>
|
||||
///// 是否存在执行异常
|
||||
///// </summary>
|
||||
//public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详细错误信息
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
///// <summary>
|
||||
///// 详细错误信息
|
||||
///// </summary>
|
||||
//public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发的规则列表
|
||||
@@ -48,9 +48,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public class TriggeredRuleInfo
|
||||
{
|
||||
public string RuleCode { get; set; }
|
||||
public string RuleId { get; set; }
|
||||
public string RuleName { get; set; }
|
||||
public string ControlStrategy { get; set; }
|
||||
public RiskControlStrategy ControlStrategy { get; set; }
|
||||
public string RuleText { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
public int Id { get; set; }
|
||||
public string RuleName { get; set; }
|
||||
public string RuleText { get; set; }
|
||||
public string RuleCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 【核心】条件JSON(结构化模式的条件列表存储),如
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
@@ -18,55 +19,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
///
|
||||
/// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 规则应用状态枚举。
|
||||
/// 用于描述一条规则应用配置当前是否参与风控执行。
|
||||
/// </summary>
|
||||
public enum RiskRuleApplicationStatusEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 已生效。
|
||||
/// 当前应用配置可正常参与风控判断。
|
||||
/// </summary>
|
||||
Active = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已停用。
|
||||
/// 当前应用配置暂不参与风控判断,但配置仍被保留。
|
||||
/// </summary>
|
||||
Disabled = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已删除。
|
||||
/// 当前应用配置逻辑上已删除,通常不再参与任何执行。
|
||||
/// </summary>
|
||||
Deleted = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 风控控制策略枚举。
|
||||
/// 用于描述规则命中后的处理方式。
|
||||
/// </summary>
|
||||
public enum RiskControlStrategyEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 禁止。
|
||||
/// 规则命中后直接拦截,不允许继续后续流程。
|
||||
/// </summary>
|
||||
Block = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 审批。
|
||||
/// 规则命中后不直接放行,需要进入审批流程。
|
||||
/// </summary>
|
||||
Approval = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 提示。
|
||||
/// 规则命中后只给出风险提示,不阻断流程。
|
||||
/// </summary>
|
||||
Warning = 3
|
||||
}
|
||||
public class RiskRuleApplication
|
||||
{
|
||||
/// <summary>
|
||||
@@ -76,11 +28,11 @@ namespace YLErp.Modules.RiskEngine
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的规则编码。
|
||||
/// 通过该字段与 RiskRule.RuleCode 关联,表示当前应用配置对应哪一条规则定义。
|
||||
/// 第一版先用字符串编码关联,后续如有需要也可以扩展为 RuleId。
|
||||
/// 关联的规则ID列表。
|
||||
/// 通过该字段与 RiskRule.Id 关联,多个规则ID使用逗号分隔。
|
||||
/// </summary>
|
||||
public string RuleCode { get; set; }
|
||||
|
||||
public string RuleIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用状态。
|
||||
@@ -90,7 +42,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 3 = Deleted(已删除)
|
||||
/// 在实际执行时,通常只加载 Active 状态的应用配置。
|
||||
/// </summary>
|
||||
public RiskRuleApplicationStatusEnum Status { get; set; }
|
||||
public RiskRuleStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 校验策略。
|
||||
@@ -100,7 +52,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 3 = 提示
|
||||
/// 当规则命中时,由该字段决定最终处理方式。
|
||||
/// </summary>
|
||||
public RiskControlStrategyEnum ControlStrategy { get; set; }
|
||||
public RiskControlStrategy ControlStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发时点。
|
||||
@@ -149,6 +101,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public string ScopeTradeTypes { get; set; }
|
||||
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID。
|
||||
/// 记录是谁创建了这条应用配置。
|
||||
@@ -185,4 +139,4 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public DateTime UpdateDate { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4940,7 +4940,7 @@ namespace YLErp.Modules.RiskModule
|
||||
};
|
||||
// 构造 DataMap(第一版只塞 trade 对象,后续按需扩展)
|
||||
// 当前先用 demo 值验证“名义本金超过1亿”的规则是否能命中
|
||||
tradeObj.StockEqvNotional = 1000;
|
||||
tradeObj.StockEqvNotional = 1000000000;
|
||||
tradeObj.TradeType = "收益互换";
|
||||
tradeObj.MaturityDate = new DateTime(2026, 6, 19);
|
||||
tradeObj.ExerciseDate = new DateTime(2026, 6, 21);
|
||||
@@ -4954,13 +4954,21 @@ namespace YLErp.Modules.RiskModule
|
||||
{
|
||||
result.TrialStatus = QuotaTrialStatusEnum.Error;
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:禁止\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Block))
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
|
||||
}
|
||||
}
|
||||
else if (riskResult.NeedApproval)
|
||||
if (riskResult.NeedApproval)
|
||||
{
|
||||
result.TrialStatus = QuotaTrialStatusEnum.Warning;
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:需审批\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval))
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
|
||||
}
|
||||
}
|
||||
else if (riskResult.Warnings != null && riskResult.Warnings.Count > 0)
|
||||
if (riskResult.Warnings != null && riskResult.Warnings.Count > 0)
|
||||
{
|
||||
result.RiskWarningDetails += "[风控引擎] 提示:" + string.Join(";", riskResult.Warnings) + "\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user