支持从数据库读取rule与application,进行匹配校验,以及拼接返回值

This commit is contained in:
ruisu
2026-06-25 17:25:47 +08:00
parent fb9c7430e9
commit 7747d26ed0
7 changed files with 303 additions and 305 deletions
@@ -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&lt;RiskContext, bool&gt;(内部通过 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}");
}
// 生成可调用委托
+204 -155
View File
@@ -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>
/// 判断某个范围字段是否为空。
/// 为空表示该维度不限制。
+10 -10
View File
@@ -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; }
}
-1
View File
@@ -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";
}