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 委托,写入 rule.CompiledScript。 判风控时直接执行委托,零解析开销。 - RiskContext:一次风控检查的数据上下文(DataMap 机制) - RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings / TriggeredRules) 编译流程: 1. 解析 Content:提取变量名、条件符、阈值 2. 变量名映射:用户输入的"名义本金" → "trade.StockEqvNotional" 3. 构建表达式树:Func = 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 委托 │ │ │ │ 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 { /// /// 风控引擎服务(第一版 — 骨架版,先跑通) /// public class RiskEngineService : YLBaseService { IYcLogger _logger = LogFactory.GetLogger("RiskEngineService"); private static readonly Lazy _instance = new Lazy(() => 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 被调用(当前为桩实现,待缓存机制完成后替换)"); } /// /// 执行风控检查(最终设计版:预编译委托 + 规则筛选 + 策略判定) /// /// 风控上下文 /// 触发时点,如 BOOK_CONFIRM /// 风控结果 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; } /// /// 加载规则列表(TODO: 后续接入真实内存缓存和数据库查询) /// 当前返回示例规则用于验证流程 /// private List 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 { rule }; } } }