1. 新增 21 条通用风控规则及应用配置种子数据(seed_rules.sql / seed_applications.sql) 2. 新增 9 个风控变量(偏离度类、执行价、最低保证金率、参考价格) 3. DTO 字段重命名:ExpectedVersion → Version,Keyword → RuleName/VariableName 4. 规则列表返回 ConditionJson 和 RuleExpr 字段 5. 应用列表查询支持按规则名称数据库级筛选(替代内存过滤) 6. 启用/停用操作不再递增 Version 号 7. RiskEngineService 单例构造使用 SystemUser 替代 null 8. RiskRuleService 移除冗余实例字段,改用 GetInstance() 直接调用
352 lines
18 KiB
C#
352 lines
18 KiB
C#
using Newtonsoft.Json;
|
||
using Qdp.Foundation.Utilities;
|
||
using System.Reflection;
|
||
using YLErp.BLL;
|
||
using YLErp.Model;
|
||
|
||
/*
|
||
================================================================================
|
||
风控引擎服务 — RiskEngineService 技术方案与进度说明
|
||
================================================================================
|
||
|
||
【项目背景】
|
||
当前 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)
|
||
|
||
编译流程:
|
||
1. 解析 Content:提取变量名、条件符、阈值
|
||
2. 变量名映射:用户输入的"名义本金" → "trade.StockEqvNotional"
|
||
3. 构建表达式树:Func<RiskContext, bool> = ctx => ...
|
||
4. 编译为委托:rule.CompiledScript = lambda.Compile()
|
||
5. 判风控时直接调用:rule.CompiledScript(context) → bool
|
||
|
||
执行流程(EvaluateRisk):
|
||
1. 从缓存/数据库加载 Active 状态的 RiskRule 列表
|
||
2. 筛选 TriggerPoints 包含当前 triggerPoint 的规则
|
||
3. 筛选 Scope 匹配当前交易上下文的规则(全局/账户/对手方/标的类型/合约类型)
|
||
4. 对每条规则:执行 rule.CompiledScript(context) 判断是否触发
|
||
5. 按 ControlStrategy(禁止/审批/提示)聚合结果
|
||
6. 返回 RiskResult
|
||
|
||
与旧设计文档的差异:
|
||
- 删除 RiskRuleApplication 模型,将策略/时点/范围合并到 RiskRule 中
|
||
- 删除 FormulaJson 实时解析,改为 ConditionJson/RuleExpr → 表达式树编译
|
||
- 规则编译跟随规则生命周期(创建/加载时),判风控时零解析
|
||
|
||
【架构设计】
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 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 等) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
【当前进度】(截至 2026-06-22)
|
||
✅ 已完成:
|
||
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. 编译验证通过
|
||
|
||
【待办事项 / 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 平级)
|
||
|
||
【注意事项】
|
||
1. 规则编译跟随规则生命周期(创建/加载时),判风控时零解析开销
|
||
2. 数值统一使用 decimal 类型,避免浮点精度问题
|
||
3. 反射取值使用 BindingFlags.IgnoreCase,兼容大小写不敏感的属性名
|
||
4. 异常不阻断原有流程,只记录日志并返回 HasError=true
|
||
================================================================================
|
||
*/
|
||
|
||
namespace YLErp.Modules.RiskEngine
|
||
{
|
||
/// <summary>
|
||
/// 风控引擎服务(第一版 — 骨架版,先跑通)
|
||
/// </summary>
|
||
public class RiskEngineService : YLBaseService
|
||
{
|
||
IYcLogger _logger = LogFactory.GetLogger("RiskEngineService");
|
||
|
||
private static readonly Lazy<RiskEngineService> _instance =
|
||
new Lazy<RiskEngineService>(() => new RiskEngineService());
|
||
|
||
public static RiskEngineService GetInstance() => _instance.Value;
|
||
|
||
private RiskEngineService() : base(OptUserInfo.SystemUser)
|
||
{
|
||
}
|
||
|
||
public RiskEngineService(OptUserInfo userInfo) : base(userInfo)
|
||
{
|
||
}
|
||
|
||
public RiskEngineService(YLBaseService baseService) : base(baseService)
|
||
{
|
||
}
|
||
|
||
public RiskEngineService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext)
|
||
{
|
||
}
|
||
|
||
public void RefreshCache()
|
||
{
|
||
_logger.Info("[风控引擎] RefreshCache 被调用(当前为桩实现,待缓存机制完成后替换)");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行风控检查(最终设计版:预编译委托 + 规则筛选 + 策略判定)
|
||
/// </summary>
|
||
/// <param name="context">风控上下文</param>
|
||
/// <param name="triggerPoint">触发时点,如 BOOK_CONFIRM</param>
|
||
/// <returns>风控结果</returns>
|
||
public RiskResult EvaluateRisk(RiskContext context, string triggerPoint)
|
||
{
|
||
var result = new RiskResult();
|
||
|
||
try
|
||
{
|
||
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
|
||
|
||
// ============================================================
|
||
// Step 1: 加载规则列表(TODO: 后续接入真实内存缓存)
|
||
// ============================================================
|
||
var rules = LoadRules();
|
||
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}");
|
||
|
||
// ============================================================
|
||
// Step 2: 筛选匹配当前 triggerPoint 且状态为 Active 的规则
|
||
// ============================================================
|
||
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))
|
||
.ToList();
|
||
|
||
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的规则数: {matchedRules.Count}");
|
||
|
||
// ============================================================
|
||
// Step 3: 遍历匹配的规则,执行预编译委托
|
||
// ============================================================
|
||
foreach (var rule in matchedRules)
|
||
{
|
||
// 确保规则已编译
|
||
if (rule.CompiledScript == null)
|
||
{
|
||
_logger.Info($"[风控引擎] 规则未编译,执行编译 - RuleId: {rule.Id}");
|
||
RuleCompiler.Compile(rule);
|
||
}
|
||
|
||
// 执行预编译委托
|
||
bool triggered = false;
|
||
try
|
||
{
|
||
triggered = rule.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;
|
||
}
|
||
|
||
if (triggered)
|
||
{
|
||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {rule.ControlStrategy}");
|
||
|
||
// 按 ControlStrategy 生成结果
|
||
switch (rule.ControlStrategy)
|
||
{
|
||
case 1: // 禁止
|
||
result.Blocked = true;
|
||
result.Passed = false;
|
||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||
{
|
||
RuleName = rule.RuleName,
|
||
ControlStrategy = "禁止",
|
||
RuleText = rule.RuleText,
|
||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||
});
|
||
break;
|
||
|
||
case 2: // 审批
|
||
result.NeedApproval = true;
|
||
result.Passed = false;
|
||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||
{
|
||
RuleName = rule.RuleName,
|
||
ControlStrategy = "审批",
|
||
RuleText = rule.RuleText,
|
||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||
});
|
||
break;
|
||
|
||
case 3: // 提示
|
||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
|
||
break;
|
||
|
||
default:
|
||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {rule.ControlStrategy}");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 4: 聚合最终结果
|
||
// ============================================================
|
||
if (!result.Blocked && !result.NeedApproval && result.Warnings.Count == 0 && !result.HasError)
|
||
{
|
||
result.Passed = true;
|
||
}
|
||
|
||
_logger.Info($"[风控引擎] EvaluateRisk 完成 - TradeId: {context?.TradeId}, Passed: {result.Passed}, Blocked: {result.Blocked}, NeedApproval: {result.NeedApproval}, TriggeredRules: {result.TriggeredRules.Count}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.NeedApproval = true;
|
||
result.Passed = false;
|
||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||
{
|
||
RuleCode = "ENGINE_ERROR",
|
||
RuleName = "风控引擎执行异常",
|
||
ControlStrategy = "审批",
|
||
RuleText = ex.Message,
|
||
Message = $"风控引擎异常:{ex.Message},需审批通过"
|
||
});
|
||
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载规则列表(TODO: 后续接入真实内存缓存和数据库查询)
|
||
/// 当前返回示例规则用于验证流程
|
||
/// </summary>
|
||
private List<RiskRule> LoadRules()
|
||
{
|
||
// 示例规则:多条件逗号分隔(自动转为 AND)
|
||
var rule = 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,
|
||
Version = 1
|
||
};
|
||
RuleCompiler.Compile(rule);
|
||
|
||
return new List<RiskRule> { rule };
|
||
}
|
||
}
|
||
}
|