Merge branch 'feature/p132_74-risk-engine' of https://gitee.glmszq.com/gsty/onederiv/trs into feature/p132_74-risk-engine
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则编译结果。
|
||||
/// 用于承载公式校验、Roslyn 编译结果和错误信息。
|
||||
/// </summary>
|
||||
public class RuleCompileResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否编译成功。
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编译失败时的错误信息。
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编译成功后的可执行委托。
|
||||
/// </summary>
|
||||
public Func<RiskContext, bool> CompiledScript { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建成功结果。
|
||||
/// </summary>
|
||||
public static RuleCompileResult Ok(Func<RiskContext, bool> compiledScript)
|
||||
{
|
||||
return new RuleCompileResult
|
||||
{
|
||||
Success = true,
|
||||
CompiledScript = compiledScript
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建失败结果。
|
||||
/// </summary>
|
||||
public static RuleCompileResult Fail(string errorMessage)
|
||||
{
|
||||
return new RuleCompileResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则编译结果的进程内缓存。
|
||||
/// 当前第一版按 RuleCode 缓存 Roslyn 编译后的可执行委托。
|
||||
/// </summary>
|
||||
public static class RuleCompiledCache
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则编译结果的进程内缓存。ConcurrentDictionary为线程安全的字典,用于缓存编译后的规则委托,性能略低。
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, Func<RiskContext, bool>> Cache
|
||||
= new ConcurrentDictionary<string, Func<RiskContext, bool>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 写入或更新规则编译结果。
|
||||
/// </summary>
|
||||
public static void Set(string ruleCode, Func<RiskContext, bool> compiledScript)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruleCode) || compiledScript == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cache[ruleCode] = compiledScript;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取规则编译结果。
|
||||
/// </summary>
|
||||
public static bool TryGet(string ruleCode, out Func<RiskContext, bool> compiledScript)
|
||||
{
|
||||
compiledScript = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ruleCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Cache.TryGetValue(ruleCode, out compiledScript);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定规则的编译结果。
|
||||
/// </summary>
|
||||
public static void Remove(string ruleCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruleCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cache.TryRemove(ruleCode, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空全部规则编译结果。
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
Cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-46
@@ -42,6 +42,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 变量编码 → 类型全名的映射
|
||||
/// 用于 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",
|
||||
@@ -62,22 +63,59 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public static void Compile(this RiskRule rule)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rule.ConditionJson))
|
||||
var result = ValidateAndCompileRule(rule);
|
||||
if (!result.Success)
|
||||
{
|
||||
throw new ArgumentException("规则 ConditionJson 不能为空", nameof(rule.ConditionJson));
|
||||
throw new InvalidOperationException(result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并编译公式表达式。
|
||||
/// 当前 FormulaExp 要求是 Roslyn 可直接执行的 bool 表达式。
|
||||
/// </summary>
|
||||
public static RuleCompileResult ValidateAndCompileFormula(string formulaExp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(formulaExp))
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 FormulaExp 不能为空");
|
||||
}
|
||||
|
||||
// 1. 解析 ConditionJson
|
||||
var conditions = ParseConditionJson(rule.ConditionJson);
|
||||
try
|
||||
{
|
||||
var compiled = CompileScript(formulaExp);
|
||||
return RuleCompileResult.Ok(compiled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RuleCompileResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成 Roslyn C# 脚本代码
|
||||
string scriptCode = BuildScriptCode(conditions);
|
||||
/// <summary>
|
||||
/// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
|
||||
/// </summary>
|
||||
public static RuleCompileResult ValidateAndCompileRule(RiskRule rule)
|
||||
{
|
||||
if (rule == null)
|
||||
{
|
||||
return RuleCompileResult.Fail("规则不能为空");
|
||||
}
|
||||
|
||||
// 3. 编译脚本(只编译一次,后续直接执行)
|
||||
var compiled = CompileScript(scriptCode);
|
||||
if (string.IsNullOrWhiteSpace(rule.RuleCode))
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 Code 不能为空");
|
||||
}
|
||||
|
||||
// 4. 写入规则(不持久化,仅内存缓存)
|
||||
rule.CompiledScript = compiled;
|
||||
var result = ValidateAndCompileFormula(rule.RuleExpr);
|
||||
if (!result.Success)
|
||||
{
|
||||
return RuleCompileResult.Fail($"规则[{rule.RuleCode}]编译失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
rule.CompiledScript = result.CompiledScript;
|
||||
RuleCompiledCache.Set(rule.RuleCode, result.CompiledScript);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,45 +156,74 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式,
|
||||
/// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr
|
||||
/// </summary>
|
||||
private static string BuildScriptCode(List<(long variableId, string op, object value, string variableType)> conditions)
|
||||
//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。
|
||||
/// 数值类型会自动包一层 Convert.ToDecimal,便于与 decimal 阈值比较。
|
||||
/// </summary>
|
||||
private static string BuildMemberAccessExpression(string expression, string variableType)
|
||||
{
|
||||
var exprParts = new List<string>();
|
||||
|
||||
foreach (var (variableId, op, value, variableType) in conditions)
|
||||
var parts = expression.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
// 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);
|
||||
|
||||
// 生成单条件表达式
|
||||
// Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
string expr = $"Convert.ToDecimal((({typeFullName})DataMap[\"{prefix}\"]).{propertyName}) {csharpOp} {valueLiteral}";
|
||||
exprParts.Add(expr);
|
||||
throw new ArgumentException($"expression 格式不正确:{expression}");
|
||||
}
|
||||
|
||||
// 多条件用 && 连接
|
||||
return string.Join(" && ", exprParts);
|
||||
string prefix = parts[0].Trim().ToLower();
|
||||
if (!VariableTypeMap.TryGetValue(prefix, out string typeFullName))
|
||||
{
|
||||
throw new ArgumentException($"未知前缀:{prefix}");
|
||||
}
|
||||
|
||||
// 除前缀外,其余部分都视为成员访问路径,便于后续扩展多级属性访问
|
||||
string memberAccess = string.Join(".", parts.Skip(1).Select(p => p.Trim()));
|
||||
string objectExpr = $"(({typeFullName})DataMap[\"{prefix}\"]).{memberAccess}";
|
||||
|
||||
if (variableType == "numeric" || variableType == "number")
|
||||
{
|
||||
return $"Convert.ToDecimal({objectExpr})";
|
||||
}
|
||||
|
||||
return objectExpr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,7 +266,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
|
||||
// TODO: 记录日志
|
||||
_logger.Error($"规则执行异常:{ex.Message}\n脚本代码:{scriptCode}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -6,156 +6,105 @@ using YLErp.Model;
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
风控引擎服务 — RiskEngineService 技术方案与进度说明
|
||||
风控引擎服务 — RiskEngineService 技术方案与当前实现说明
|
||||
================================================================================
|
||||
|
||||
【项目背景】
|
||||
当前 TRS 系统的限额监控模块(QuotaMonitorService)采用"固定指标 + 上下限阈值"
|
||||
的硬编码模式,存在规则维度单一、无法自由组合、不支持公式化配置、缺少三级控制
|
||||
策略(禁止/审批/提示)等局限。
|
||||
当前 TRS 系统已在 QuotaMonitorService 中接入第一版风控引擎,用于在交易关键
|
||||
时点执行可配置规则判断。当前目标不是一次性做成最终版,而是在最小可运行链路
|
||||
跑通后,逐步演进为可由前端配置、后端预编译、执行期直接命中的正式版本。
|
||||
|
||||
本模块旨在将现有限额监控升级为完整的风控引擎,实现规则的灵活配置、公式化执行、
|
||||
策略判定与结果输出。
|
||||
|
||||
【最终设计】(以本注释为准,与设计文档不同的部分以本注释为准)
|
||||
【当前实现口径】(以本注释和实际代码为准)
|
||||
|
||||
核心概念:
|
||||
- RiskRule(风控规则):单条规则包含完整的公式定义 + 校验策略 + 触发时点 + 适用范围。
|
||||
不需要 "一个 Rule 对应多个 Application" 的复杂关系。
|
||||
- Content:用户输入的规则内容字符串,如"名义本金>1亿"
|
||||
- ConditionJson:条件JSON(结构化模式),如 {"conditions":[{"variableId":1,"operator":">","value":100000000}]}
|
||||
- RuleExpr:规则表达式(自由文本模式,类C#表达式)
|
||||
- 规则编译:在规则定义时(创建/加载/缓存刷新时)调用 RuleCompiler.Compile(rule),
|
||||
将 ConditionJson/RuleExpr 解析并编译为 Func<RiskContext, bool> 委托,写入 rule.CompiledScript。
|
||||
判风控时直接执行委托,零解析开销。
|
||||
- RiskContext:一次风控检查的数据上下文(DataMap 机制)
|
||||
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings / TriggeredRules)
|
||||
- RiskRule(风控规则):规则定义本体,当前重点字段包括 RuleCode、RuleName、
|
||||
FormulaText、FormulaExp、Version、CompiledScript。
|
||||
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
|
||||
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
|
||||
- FormulaExp:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
|
||||
例如直接访问 DataMap["trade"] 后做数值或日期比较。
|
||||
- FormulaJson:当前已在规则定义和样例代码中停用,不参与主执行链路。
|
||||
- RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DataMap。
|
||||
- RuleCompiledCache:进程内编译结果缓存,按 RuleCode 缓存 Func<RiskContext, bool>。
|
||||
|
||||
编译流程:
|
||||
1. 解析 Content:提取变量名、条件符、阈值
|
||||
2. 变量名映射:用户输入的"名义本金" → "trade.StockEqvNotional"
|
||||
3. 构建表达式树:Func<RiskContext, bool> = ctx => ...
|
||||
4. 编译为委托:rule.CompiledScript = lambda.Compile()
|
||||
5. 判风控时直接调用:rule.CompiledScript(context) → bool
|
||||
当前编译流程:
|
||||
1. 外部准备好规则对象,FormulaExp 中直接写最终可执行表达式
|
||||
2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
|
||||
3. 内部使用 Roslyn 编译 FormulaExp,生成 Func<RiskContext, bool>
|
||||
4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
|
||||
5. 执行阶段优先从 RuleCompiledCache 取委托执行
|
||||
|
||||
执行流程(EvaluateRisk):
|
||||
1. 从缓存/数据库加载 Active 状态的 RiskRule 列表
|
||||
2. 筛选 TriggerPoints 包含当前 triggerPoint 的规则
|
||||
3. 筛选 Scope 匹配当前交易上下文的规则(全局/账户/对手方/标的类型/合约类型)
|
||||
4. 对每条规则:执行 rule.CompiledScript(context) 判断是否触发
|
||||
5. 按 ControlStrategy(禁止/审批/提示)聚合结果
|
||||
6. 返回 RiskResult
|
||||
当前执行流程(EvaluateRisk):
|
||||
1. 加载示例规则列表和示例应用列表
|
||||
2. 按应用状态、TriggerPoints 过滤有效应用
|
||||
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
|
||||
4. 根据命中的 RuleCode 找到对应规则
|
||||
5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
|
||||
6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
|
||||
7. 返回 RiskResult
|
||||
|
||||
与旧设计文档的差异:
|
||||
- 删除 RiskRuleApplication 模型,将策略/时点/范围合并到 RiskRule 中
|
||||
- 删除 FormulaJson 实时解析,改为 ConditionJson/RuleExpr → 表达式树编译
|
||||
- 规则编译跟随规则生命周期(创建/加载时),判风控时零解析
|
||||
当前维度匹配规则:
|
||||
- 全局:ScopeIsGlobal=true 时直接命中
|
||||
- 同一维度内多选:并集(OR)
|
||||
- 不同维度之间:交集(AND)
|
||||
- 某维度留空:视为该维度不限制
|
||||
|
||||
【架构设计】
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ API 层(RiskRuleController / RiskEngineController) │
|
||||
│ - 规则 CRUD API、风控执行 API(EvaluateRisk / Trial) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────────┐
|
||||
│ 风控引擎层(YLErpDAL/Modules/RiskEngine/) ← 本文件夹所在层 │
|
||||
│ │
|
||||
│ RiskEngineService(本文件) │
|
||||
│ - 内存缓存管理(规则列表) │
|
||||
│ - EvaluateRisk:筛选规则 → 执行预编译委托 → 聚合结果 │
|
||||
│ │
|
||||
│ RuleCompiler.cs │
|
||||
│ - ParseContent:解析 Content 字符串 → 变量 + 条件符 + 阈值 │
|
||||
│ - BuildExpression:构建表达式树(反射取值 + decimal 比较) │
|
||||
│ - Compile(RiskRule):编译为 Func<RiskContext, bool> 委托 │
|
||||
│ │
|
||||
│ RiskRule.cs │
|
||||
│ - 合并模型:公式定义 + 校验策略 + 触发时点 + 适用范围 │
|
||||
│ - CompiledScript:预编译的可执行委托(运行时内存字段) │
|
||||
│ │
|
||||
│ RiskContext.cs / RiskResult.cs / RiskFormula.cs │
|
||||
│ - 数据上下文 / 结果模型 / 公式模型(辅助) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────────┐
|
||||
│ 数据访问层 │
|
||||
│ - RiskRuleRepository(规则表) │
|
||||
│ - 复用现有 DAL(Trade / Client / SwapPosition / Credit 等) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
【与早期方案的主要差异】
|
||||
- 当前不是 Content 解析或表达式树主导,而是 FormulaExp 直编译主导
|
||||
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
|
||||
- 编译器文件已放入 RiskEngine/Compile 目录下
|
||||
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 FormulaExp 直接表达
|
||||
|
||||
【当前进度】(截至 2026-06-22)
|
||||
【当前代码结构】
|
||||
风控引擎层:YLErpDAL/Modules/RiskEngine/
|
||||
- RiskEngineService.cs:执行入口,负责规则筛选、应用过滤、委托执行、结果聚合
|
||||
- RiskRule.cs:规则定义模型,含 CompiledScript 运行时字段
|
||||
- RiskRuleApplication.cs:规则应用模型,承载策略、触发点和适用范围
|
||||
- RiskContext.cs / RiskResult.cs:执行上下文与结果模型
|
||||
|
||||
编译相关:YLErpDAL/Modules/RiskEngine/Compile/
|
||||
- RuleCompiler.cs:Roslyn 编译入口,提供 ValidateAndCompileFormula / ValidateAndCompileRule
|
||||
- RuleCompileResult.cs:编译结果模型
|
||||
- RuleCompiledCache.cs:编译结果缓存
|
||||
|
||||
当前集成点:
|
||||
- QuotaMonitorService.cs:构造 RiskContext,并将 trade 对象放入 DataMap["trade"]
|
||||
|
||||
【当前进度】(截至 2026-06-24)
|
||||
✅ 已完成:
|
||||
1. 创建 RiskEngine 文件夹,与 RiskModule 平级,实现代码隔离
|
||||
2. 核心模型:
|
||||
- RiskRule.cs:合并模型(公式 + 策略 + 时点 + 范围 + CompiledScript)
|
||||
- RiskContext.cs:风控上下文(TradeId, TriggerPoint, DataMap)
|
||||
- RiskResult.cs:风控结果(Passed, Blocked, NeedApproval, Warnings, TriggeredRules)
|
||||
- RiskFormula.cs:ConditionJson 反序列化模型(辅助)
|
||||
3. RuleCompiler.cs:
|
||||
- ParseContent:解析 Content 字符串(支持 > < >= <= = != ≠)
|
||||
- ParseValue:解析数值(支持 万/亿/千/百分比)
|
||||
- BuildExpression:表达式树构建(反射取值 + decimal 比较)
|
||||
4. RiskEngineService.EvaluateRisk:
|
||||
- 加载示例规则列表
|
||||
- 筛选 TriggerPoint 匹配的规则
|
||||
- 调用 rule.CompiledScript(context) 执行预编译委托
|
||||
- 按 ControlStrategy 聚合结果(禁止/审批/提示)
|
||||
5. 集成到现有流程:
|
||||
- QuotaMonitorService.QuotaCheck() 末尾已接入风控引擎
|
||||
- 触发时点:BOOK_CONFIRM(簿记交易确认)
|
||||
6. 编译验证通过
|
||||
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
|
||||
2. 风控上下文通过 DataMap 传入 trade 对象,支持规则脚本直接访问交易字段
|
||||
3. FormulaExp 直编译方案已接入,支持数值比较和日期比较
|
||||
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
|
||||
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
|
||||
6. RuleCompiler 已支持校验 + 编译 + 写缓存
|
||||
7. RuleCompiledCache / RuleCompileResult 已落地到 Compile 目录
|
||||
8. RiskEngineService 已改为执行时优先从 RuleCompiledCache 读取委托
|
||||
|
||||
【待办事项 / TODO】(按优先级排序)
|
||||
⬜ 1. 内存缓存机制
|
||||
- _cachedRules: Dictionary<(int id, int version), RiskRule>
|
||||
- RefreshCache():从数据库加载 Active 规则 + 逐个 Compile()
|
||||
- 线程安全(lock / Interlocked.Exchange)
|
||||
⬜ 2. 数据库表创建(SQL)
|
||||
- glms_risk_rule(合并模型:公式 + 策略 + 时点 + 范围)
|
||||
⬜ 3. 规则筛选逻辑
|
||||
- 按 Scope(全局/账户/对手方/标的类型/合约类型)过滤适用范围
|
||||
- 去重
|
||||
⬜ 4. 更多变量映射(VariableNameMap)
|
||||
- 行情类:market.LastClosePrice, market.FR007 等
|
||||
- 系统计算类:calc.CounterpartyTotalNotional, calc.CreditUsageRate 等
|
||||
- 布尔判断类:calc.IsLongDirection, calc.IsMarginPay 等
|
||||
⬜ 5. 多条件支持(AND / OR)
|
||||
- Content 解析:支持 "名义本金>1亿 且 保证金比例>50%"
|
||||
- 表达式树构建:多个条件合并为单一委托
|
||||
⬜ 6. 更多集成点
|
||||
- 平仓审核提交(CLOSE_REVIEW)
|
||||
- 上传确认书(UPLOAD_CONFIRMATION)
|
||||
- 事件发生时(EVENT_TRIGGER)
|
||||
⬜ 7. 前端"确认交易"弹窗展示优化(当前混在"风险预警"栏目中)
|
||||
- 方案:QuotaTrial 模型新增 RiskEngineDetails 字段,前端独立渲染"风控检查"栏目
|
||||
⬜ 8. 单元测试
|
||||
⬜ 9. 前端规则配置 SPA(独立前端仓库)
|
||||
⬜ 10. 【暂不实现】考虑迁移到 CSScriptLib
|
||||
- 原因:当前已采用 Roslyn(Microsoft.CodeAnalysis.CSharp.Scripting),无需额外包
|
||||
- 灵活性:Roslyn 已足够支持多条件 AND/OR、复杂表达式,CSScriptLib 并无明显优势
|
||||
- 优先级:低,当前版本暂不引入
|
||||
|
||||
【关键文件清单】
|
||||
本模块:
|
||||
- RiskEngineService.cs(本文件):引擎核心服务,EvaluateRisk 入口
|
||||
- RuleCompiler.cs:规则编译器,Content → 表达式树 → 委托
|
||||
- RiskRule.cs:合并后的规则模型(含 CompiledScript)
|
||||
- RiskContext.cs / RiskResult.cs / RiskFormula.cs
|
||||
|
||||
现有集成点:
|
||||
- QuotaMonitorService.cs(RiskModule 下):QuotaCheck() 末尾已接入
|
||||
|
||||
设计文档:
|
||||
- X:\尹峰\onederiv\风控引擎需求详细设计.md(旧设计,仅供参考)
|
||||
|
||||
【命名空间】
|
||||
YLErp.Modules.RiskEngine(与 RiskModule 平级)
|
||||
【待办事项 / TODO】
|
||||
⬜ 1. 接入真实规则来源
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 示例数据
|
||||
⬜ 2. 增加接口层
|
||||
- 提供前端提交 FormulaExp 后的校验接口
|
||||
- 提供规则保存 / 发布后预编译并写缓存的入口
|
||||
⬜ 3. 启动预热与缓存刷新
|
||||
- 服务启动时批量加载有效规则并预编译
|
||||
- 支持规则更新后的缓存刷新 / 删除
|
||||
⬜ 4. 完善执行期策略
|
||||
- 当前保留“缓存未命中时兜底编译”逻辑
|
||||
- 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
|
||||
⬜ 5. 补测试样例
|
||||
- 数值比较、日期比较、维度过滤、非法公式、边界值
|
||||
⬜ 6. 完善 FormulaExp 配套能力
|
||||
- 提供前端可用的表达式编辑、校验与错误提示
|
||||
- 约束可用变量、类型转换和脚本安全边界
|
||||
|
||||
【注意事项】
|
||||
1. 规则编译跟随规则生命周期(创建/加载时),判风控时零解析开销
|
||||
2. 数值统一使用 decimal 类型,避免浮点精度问题
|
||||
3. 反射取值使用 BindingFlags.IgnoreCase,兼容大小写不敏感的属性名
|
||||
4. 异常不阻断原有流程,只记录日志并返回 HasError=true
|
||||
1. 当前 FormulaExp 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
2. DataMap 中的值由宿主业务代码准备,编译器与执行器本身不负责查库补数
|
||||
3. 当前缓存的是 Func<RiskContext, bool>,这是规则判断函数,不是事件处理器
|
||||
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
|
||||
================================================================================
|
||||
*/
|
||||
|
||||
@@ -209,41 +158,86 @@ namespace YLErp.Modules.RiskEngine
|
||||
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
|
||||
|
||||
// ============================================================
|
||||
// Step 1: 加载规则列表(TODO: 后续接入真实内存缓存)
|
||||
// Step 1: 加载规则定义和规则应用(TODO: 后续接入真实内存缓存)
|
||||
// ============================================================
|
||||
var rules = LoadRules();
|
||||
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}");
|
||||
var applications = LoadApplications();
|
||||
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}, 应用数: {applications.Count}");
|
||||
|
||||
// ============================================================
|
||||
// Step 2: 筛选匹配当前 triggerPoint 且状态为 Active 的规则
|
||||
// Step 2: 先按 Application 过滤 Active 状态和 TriggerPoint 匹配的应用
|
||||
// ============================================================
|
||||
var matchedRules = rules
|
||||
.Where(r => r.Status == 1) // Active
|
||||
.Where(r => !string.IsNullOrEmpty(r.TriggerPoints))
|
||||
.Where(r => r.TriggerPoints.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Contains(triggerPoint))
|
||||
var activeApps = applications
|
||||
.Where(a => a.Status == RiskRuleApplicationStatusEnum.Active)
|
||||
.ToList();
|
||||
var apps = activeApps
|
||||
.Where(a => !string.IsNullOrEmpty(a.TriggerPoints))
|
||||
.ToList();
|
||||
var triggerMatchedApps = apps
|
||||
.Where(a => a.TriggerPoints.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Contains(triggerPoint))
|
||||
.ToList();
|
||||
|
||||
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的规则数: {matchedRules.Count}");
|
||||
// 再按应用范围过滤。
|
||||
// 统一走通用维度匹配:
|
||||
// 1. 全局命中时直接通过;
|
||||
// 2. 同一维度内多选按并集处理;
|
||||
// 3. 不同维度之间按交集处理;
|
||||
// 4. 某维度留空表示该维度不限制。
|
||||
var trade = context?.DataMap != null && context.DataMap.ContainsKey("trade")
|
||||
? context.DataMap["trade"] as YLErp.DBModels.trade
|
||||
: null;
|
||||
|
||||
var matchedApplications = triggerMatchedApps
|
||||
.Where(a => IsApplicationMatched(a, trade))
|
||||
.ToList();
|
||||
|
||||
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的应用数: {matchedApplications.Count}");
|
||||
|
||||
// ============================================================
|
||||
// Step 3: 遍历匹配的规则,执行预编译委托
|
||||
// Step 3: 遍历匹配的应用,通过 RuleCode 关联规则并执行预编译委托
|
||||
// ============================================================
|
||||
foreach (var rule in matchedRules)
|
||||
foreach (var application in matchedApplications)
|
||||
{
|
||||
// 确保规则已编译
|
||||
if (rule.CompiledScript == null)
|
||||
// ------------------------------------------------------------
|
||||
// 3.1 通过 RuleCode 关联规则定义
|
||||
// ------------------------------------------------------------
|
||||
var rule = rules.FirstOrDefault(r => r.RuleCode == application.RuleCode);
|
||||
if (rule == null)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未编译,执行编译 - RuleId: {rule.Id}");
|
||||
RuleCompiler.Compile(rule);
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleCode: {application.RuleCode}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"未找到规则定义[{application.RuleCode}];";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行预编译委托
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(rule.RuleCode, out var compiledScript))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleCode: {rule.RuleCode}");
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则编译失败 - RuleCode: {rule.RuleCode}, Error: {compileResult.ErrorMessage}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.RuleCode}]编译失败:{compileResult.ErrorMessage};";
|
||||
continue;
|
||||
}
|
||||
|
||||
compiledScript = compileResult.CompiledScript;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.3 执行预编译委托
|
||||
// ------------------------------------------------------------
|
||||
bool triggered = false;
|
||||
try
|
||||
{
|
||||
triggered = rule.CompiledScript(context);
|
||||
triggered = compiledScript(context);
|
||||
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -254,14 +248,16 @@ namespace YLErp.Modules.RiskEngine
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {rule.ControlStrategy}");
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
// 按 ControlStrategy 生成结果
|
||||
switch (rule.ControlStrategy)
|
||||
switch (application.ControlStrategy)
|
||||
{
|
||||
case 1: // 禁止
|
||||
case RiskControlStrategyEnum.Block:
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
@@ -273,7 +269,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
});
|
||||
break;
|
||||
|
||||
case 2: // 审批
|
||||
case RiskControlStrategyEnum.Approval:
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
@@ -285,15 +281,19 @@ namespace YLErp.Modules.RiskEngine
|
||||
});
|
||||
break;
|
||||
|
||||
case 3: // 提示
|
||||
case RiskControlStrategyEnum.Warning:
|
||||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {rule.ControlStrategy}");
|
||||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未触发 - RuleCode: {rule.RuleCode}, TriggerPoint: {triggerPoint}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -330,22 +330,162 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
private List<RiskRule> LoadRules()
|
||||
{
|
||||
// 示例规则:多条件逗号分隔(自动转为 AND)
|
||||
var rule = new RiskRule
|
||||
//名义本金:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap[\"trade\"]).StockEqvNotional)
|
||||
//挂钩标的到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).MaturityDate
|
||||
//合约到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate
|
||||
|
||||
// 示例规则1:先保留一个最小可运行样例,只验证名义本金阈值判断
|
||||
var Rule1 = new RiskRule
|
||||
{
|
||||
Id = 103,
|
||||
RuleName = "测试规则:名义本金超过1亿且保证金比例超过50%禁止",
|
||||
RuleText = "名义本金>1亿, 保证金比例>50%",
|
||||
ConditionJson = "{ \"conditions\": [{\"variableId\":1,\"operator\":\">\",\"value\":100000000},{\"variableId\":2,\"operator\":\">\",\"value\":0.5}] }",
|
||||
Status = 1, // Active
|
||||
ControlStrategy = 1, // 禁止
|
||||
TriggerPoints = "BOOK_CONFIRM",
|
||||
ScopeIsGlobal = true,
|
||||
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
|
||||
};
|
||||
RuleCompiler.Compile(rule);
|
||||
|
||||
return new List<RiskRule> { rule };
|
||||
// 示例规则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 };
|
||||
}
|
||||
/// <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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断应用配置是否命中当前交易。
|
||||
/// 匹配规则遵循设计文档:
|
||||
/// 1. 全局命中时直接返回 true;
|
||||
/// 2. 同一维度内多选按并集处理;
|
||||
/// 3. 不同维度之间按交集处理;
|
||||
/// 4. 某维度留空表示该维度不限制。
|
||||
/// </summary>
|
||||
private bool IsApplicationMatched(RiskRuleApplication application, YLErp.DBModels.trade trade)
|
||||
{
|
||||
if (application == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (application.ScopeIsGlobal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trade == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var accountMatched = IsScopeEmpty(application.ScopeAssetBookIds) || IsValueMatched(application.ScopeAssetBookIds, GetTradeAssetBookId(trade));
|
||||
var clientMatched = IsScopeEmpty(application.ScopeClientIds) || IsValueMatched(application.ScopeClientIds, trade.ClientId);
|
||||
var underlyingTypeMatched = IsScopeEmpty(application.ScopeUnderlyingTypes) || IsValueMatched(application.ScopeUnderlyingTypes, GetTradeUnderlyingType(trade));
|
||||
var tradeTypeMatched = IsScopeEmpty(application.ScopeTradeTypes) || IsValueMatched(application.ScopeTradeTypes, trade.TradeType);
|
||||
|
||||
return accountMatched
|
||||
&& clientMatched
|
||||
&& underlyingTypeMatched
|
||||
&& tradeTypeMatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个范围字段是否为空。
|
||||
/// 为空表示该维度不限制。
|
||||
/// </summary>
|
||||
private bool IsScopeEmpty(string scopeValue)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(scopeValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断单个值是否命中逗号分隔的范围配置。
|
||||
/// 同一维度内多选按并集处理,只要命中任一值即返回 true。
|
||||
/// </summary>
|
||||
private bool IsValueMatched(string scopeValue, object currentValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scopeValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentValue == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentText = currentValue.ToString()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(currentText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return scopeValue
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Any(s => string.Equals(s, currentText, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取交易的资产簿账户ID。
|
||||
/// 当前先按 BookId 取值;如果后续真实字段不是 BookId,再统一调整这里即可。
|
||||
/// </summary>
|
||||
private object GetTradeAssetBookId(YLErp.DBModels.trade trade)
|
||||
{
|
||||
var property = trade.GetType().GetProperty("BookId");
|
||||
return property?.GetValue(trade);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取交易的标的类型。
|
||||
/// 当前先按 UnderlyingInstrumentType 取值;如果后续真实字段名不同,再统一调整这里即可。
|
||||
/// </summary>
|
||||
private object GetTradeUnderlyingType(YLErp.DBModels.trade trade)
|
||||
{
|
||||
var property = trade.GetType().GetProperty("UnderlyingInstrumentType");
|
||||
return property?.GetValue(trade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@ using System;
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 风控规则(合并模型:规则定义 + 应用配置合一)
|
||||
///
|
||||
/// 最终设计说明:
|
||||
/// 业务上不需要 "一个 Rule 对应多个 Application" 的复杂关系。
|
||||
/// 每条规则自身包含:公式定义 + 校验策略 + 触发时点 + 适用范围。
|
||||
/// 规则创建时即完成脚本编译,判风控时直接执行已编译委托(零解析开销)。
|
||||
/// 风控规则定义。
|
||||
/// 只描述“规则是什么”,不包含状态、策略、触发时点、适用范围等应用配置。
|
||||
/// 这些执行维度由 RiskRuleApplication 承载。
|
||||
/// </summary>
|
||||
public class RiskRule
|
||||
{
|
||||
@@ -16,6 +13,7 @@ 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(结构化模式的条件列表存储),如
|
||||
@@ -30,33 +28,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public string RuleExpr { get; set; }
|
||||
|
||||
// === 校验策略(原 Application.ControlStrategy)===
|
||||
/// <summary>
|
||||
/// 校验策略:1=禁止, 2=审批, 3=提示
|
||||
/// </summary>
|
||||
public int ControlStrategy { get; set; }
|
||||
|
||||
// === 触发时点(原 Application.TriggerPoints)===
|
||||
/// <summary>
|
||||
/// 触发时点,逗号分隔(如 BOOK_CONFIRM,CLOSE_REVIEW)
|
||||
/// </summary>
|
||||
public string TriggerPoints { get; set; }
|
||||
|
||||
// === 适用范围(原 Application.Scope*)===
|
||||
/// <summary>
|
||||
/// 是否全局适用(true=所有交易适用,忽略其他 Scope 字段)
|
||||
/// </summary>
|
||||
public bool ScopeIsGlobal { get; set; }
|
||||
public string ScopeAssetBookIds { get; set; }
|
||||
public string ScopeClientIds { get; set; }
|
||||
public string ScopeUnderlyingTypes { get; set; }
|
||||
public string ScopeTradeTypes { get; set; }
|
||||
|
||||
// === 生命周期 ===
|
||||
/// <summary>
|
||||
/// 状态:1=已生效(Active), 2=已停用(Disabled), 3=已删除(Deleted)
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
public int Version { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 风控规则应用配置。
|
||||
///
|
||||
/// 设计原因:
|
||||
/// 依据设计文档,风控规则需要区分“规则定义”和“规则应用”两个层面。
|
||||
/// RiskRule 负责定义“规则是什么”,例如规则名称、规则编码、公式表达式、描述、版本等;
|
||||
/// RiskRuleApplication 负责定义“规则在什么场景下如何执行”,例如启用状态、校验策略、触发时点、适用范围。
|
||||
///
|
||||
/// 拆分后的好处是:
|
||||
/// 1. 同一条规则可以被多个应用配置复用,避免重复维护相同的公式定义。
|
||||
/// 2. 不同业务场景可以对同一条规则配置不同的执行策略,例如在簿记确认时禁止,在平仓审核时提示。
|
||||
/// 3. 调整触发时点、适用范围、控制策略时,不需要修改规则本身,降低配置耦合。
|
||||
/// 4. 后续如果要做规则管理页面、规则启停、按场景灰度投放,也更容易扩展。
|
||||
///
|
||||
/// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
|
||||
/// </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>
|
||||
/// 应用配置主键ID。
|
||||
/// 用于唯一标识一条规则应用记录。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的规则编码。
|
||||
/// 通过该字段与 RiskRule.RuleCode 关联,表示当前应用配置对应哪一条规则定义。
|
||||
/// 第一版先用字符串编码关联,后续如有需要也可以扩展为 RuleId。
|
||||
/// </summary>
|
||||
public string RuleCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用状态。
|
||||
/// 约定:
|
||||
/// 1 = Active(已生效)
|
||||
/// 2 = Disabled(已停用)
|
||||
/// 3 = Deleted(已删除)
|
||||
/// 在实际执行时,通常只加载 Active 状态的应用配置。
|
||||
/// </summary>
|
||||
public RiskRuleApplicationStatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 校验策略。
|
||||
/// 约定:
|
||||
/// 1 = 禁止
|
||||
/// 2 = 审批
|
||||
/// 3 = 提示
|
||||
/// 当规则命中时,由该字段决定最终处理方式。
|
||||
/// </summary>
|
||||
public RiskControlStrategyEnum ControlStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发时点。
|
||||
/// 多个时点使用逗号分隔,例如:
|
||||
/// BOOK_CONFIRM,CLOSE_REVIEW
|
||||
/// 表示该应用配置会在这些业务时点参与风控判断。
|
||||
/// </summary>
|
||||
public string TriggerPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否全局适用。
|
||||
/// true 表示对所有交易都适用,此时通常忽略其他 Scope 字段;
|
||||
/// false 表示只在指定范围内适用,需要结合下方各 Scope 字段进行过滤。
|
||||
/// </summary>
|
||||
public bool ScopeIsGlobal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用的资产簿记账户范围。
|
||||
/// 第一版使用逗号分隔的字符串保存,例如:
|
||||
/// 123,456,789
|
||||
/// 后续执行时可按约定解析为账户ID集合。
|
||||
/// </summary>
|
||||
public string ScopeAssetBookIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用的客户范围。
|
||||
/// 第一版使用逗号分隔的字符串保存客户ID,例如:
|
||||
/// 10001,10002
|
||||
/// 用于在执行前筛选该规则是否适用于当前客户。
|
||||
/// </summary>
|
||||
public string ScopeClientIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用的标的类型范围。
|
||||
/// 例如:
|
||||
/// 股票,ETF,债券
|
||||
/// 用于控制规则仅对特定标的类型生效。
|
||||
/// </summary>
|
||||
public string ScopeUnderlyingTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用的交易类型范围。
|
||||
/// 例如:
|
||||
/// 簿记交易,平仓交易
|
||||
/// 用于控制规则仅在指定交易类型下生效。
|
||||
/// </summary>
|
||||
public string ScopeTradeTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID。
|
||||
/// 记录是谁创建了这条应用配置。
|
||||
/// </summary>
|
||||
public int OptId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人名称。
|
||||
/// 便于界面展示和审计追踪。
|
||||
/// </summary>
|
||||
public string OptName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// 记录应用配置的创建时刻。
|
||||
/// </summary>
|
||||
public DateTime OptDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人ID。
|
||||
/// 记录最近一次修改该应用配置的操作人。
|
||||
/// </summary>
|
||||
public int UpdateOptId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人名称。
|
||||
/// 便于界面展示和审计追踪。
|
||||
/// </summary>
|
||||
public string UpdateOptName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新时间。
|
||||
/// 记录最近一次修改该应用配置的时间。
|
||||
/// </summary>
|
||||
public DateTime UpdateDate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4724,7 +4724,7 @@ namespace YLErp.Modules.RiskModule
|
||||
{
|
||||
_logger.Info($"[限额试算] 开始执行 - tradeId: {tradeId}, trialSource: {trialSource}");
|
||||
var result = new QuotaTrial();
|
||||
var tradeObj = DbContext.trade.AsNoTracking().Where(O => O.id == tradeId).FirstOrDefault();
|
||||
trade tradeObj = DbContext.trade.AsNoTracking().Where(O => O.id == tradeId).FirstOrDefault();
|
||||
|
||||
// 检查交易对象是否存在
|
||||
if (tradeObj == null)
|
||||
@@ -4939,6 +4939,11 @@ namespace YLErp.Modules.RiskModule
|
||||
TriggerPoint = "BOOK_CONFIRM"
|
||||
};
|
||||
// 构造 DataMap(第一版只塞 trade 对象,后续按需扩展)
|
||||
// 当前先用 demo 值验证“名义本金超过1亿”的规则是否能命中
|
||||
tradeObj.StockEqvNotional = 1000;
|
||||
tradeObj.TradeType = "收益互换";
|
||||
tradeObj.MaturityDate = new DateTime(2026, 6, 19);
|
||||
tradeObj.ExerciseDate = new DateTime(2026, 6, 21);
|
||||
riskContext.DataMap["trade"] = tradeObj;
|
||||
var riskResult = riskEngine.EvaluateRisk(riskContext, "BOOK_CONFIRM");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user