Files
zszq-trs/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
T
尹峰 c2fc699e57 feat(risk-engine): 完善风控规则种子数据与 CRUD 接口优化
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() 直接调用
2026-06-24 18:23:43 +08:00

352 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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、风控执行 APIEvaluateRisk / 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(规则表) │
│ - 复用现有 DALTrade / 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.csConditionJson 反序列化模型(辅助)
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
- 原因:当前已采用 RoslynMicrosoft.CodeAnalysis.CSharp.Scripting),无需额外包
- 灵活性:Roslyn 已足够支持多条件 AND/OR、复杂表达式,CSScriptLib 并无明显优势
- 优先级:低,当前版本暂不引入
【关键文件清单】
本模块:
- RiskEngineService.cs(本文件):引擎核心服务,EvaluateRisk 入口
- RuleCompiler.cs:规则编译器,Content → 表达式树 → 委托
- RiskRule.cs:合并后的规则模型(含 CompiledScript
- RiskContext.cs / RiskResult.cs / RiskFormula.cs
现有集成点:
- QuotaMonitorService.csRiskModule 下):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 };
}
}
}