Files
zszq-trs/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
T

868 lines
42 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(风控规则):规则定义本体,当前重点字段包括 Id、RuleName、
RuleText、RuleExpr、Version、CompiledScript。
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
- RuleExpr:自由文本规则的编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
例如通过 DbContext 和 TradeId 查询数据库后做数值或日期比较。
- ConditionJson:结构化规则的主执行依据;有值时优先由结构化执行器执行。
- RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DbContext。
- RuleCompiledCache:进程内编译结果缓存,仅用于自由文本 RuleExpr 规则,按规则 Id 缓存 Func<RiskContext, bool>。
当前编译流程:
1. 结构化规则以 ConditionJson 为执行依据,预热时只做 JSON 结构解析校验
2. 自由文本规则在 RuleExpr 中直接写最终可执行表达式
3. 调用 RuleCompiler.ValidateAndCompileRule(rule)
4. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
5. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
当前执行流程(EvaluateRisk):
1. 从数据库加载规则列表和应用列表
2. 按应用状态、TriggerPoints 过滤有效应用
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
5. ConditionJson 有值时走结构化执行器;否则从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
6. 执行规则判断,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
7. 返回 RiskResult
当前维度匹配规则:
- 全局:ScopeIsGlobal=true 时直接命中
- 同一维度内多选:并集(OR)
- 不同维度之间:交集(AND)
- 某维度留空:视为该维度不限制
【与早期方案的主要差异】
- 结构化规则已改为 ConditionJson 执行主导,自由文本规则继续保留 RuleExpr 直编译
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
- 编译器文件已放入 RiskEngine/Compile 目录下
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 RuleExpr 直接表达
【当前代码结构】
风控引擎层:YLErpDAL/Modules/RiskEngine/
- RiskEngineService.cs:执行入口,负责规则筛选、应用过滤、委托执行、结果聚合
- RiskRule.cs:规则定义模型,含 CompiledScript 运行时字段
- RiskRuleApplication.cs:规则应用模型,承载策略、触发点和适用范围
- RiskContext.cs / RiskResult.cs:执行上下文与结果模型
编译相关:YLErpDAL/Modules/RiskEngine/Compile/
- RuleCompiler.csRoslyn 编译入口,提供 ValidateAndCompileFormula / ValidateAndCompileRule
- RuleCompileResult.cs:编译结果模型
- RuleCompiledCache.cs:编译结果缓存
当前集成点:
- QuotaMonitorService.cs:构造 RiskContext,并传入当前 TradeId
【当前进度】(截至 2026-06-24
✅ 已完成:
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
2. 风控上下文提供 DbContext 和 TradeId,支持规则脚本直接查询数据库
3. 结构化规则已支持 ConditionJson 执行,自由文本规则保留 RuleExpr 直编译
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
6. RuleCompiler 已支持校验 + 编译 + 写缓存
7. RuleCompiledCache / RuleCompileResult 已落地到 Compile 目录
8. RiskEngineService 已改为结构化规则优先执行 ConditionJson,自由文本规则再读取 RuleCompiledCache
【待办事项 / TODO】
⬜ 1. 接入真实规则来源
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 的规则来源
⬜ 2. 增加接口层
- 提供前端提交 RuleExpr 后的校验接口
- 提供规则保存 / 发布后预编译并写缓存的入口
⬜ 3. 启动预热与缓存刷新
- 服务启动时批量加载有效规则并预编译
- 支持规则更新后的缓存刷新 / 删除
⬜ 4. 完善执行期策略
- 当前保留“缓存未命中时兜底编译”逻辑
- 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
⬜ 5. 补测试样例
- 数值比较、日期比较、维度过滤、非法公式、边界值
⬜ 6. 完善 RuleExpr 配套能力
- 提供前端可用的表达式编辑、校验与错误提示
- 约束可用变量、类型转换和脚本安全边界
【注意事项】
1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
2. RuleExpr 可直接使用 DbContext 查询数据库,当前交易通过 TradeId 定位
3. 当前缓存的是 Func<RiskContext, bool>,这是规则判断函数,不是事件处理器
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
================================================================================
*/
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)
{
}
/// <summary>
/// 规则内存缓存(启动预热写入,EvaluateRisk 读取)
/// </summary>
private static volatile List<RiskRule> _cachedRules;
private static volatile List<RiskRuleApplication> _cachedApplications;
private static readonly object _cacheLock = new object();
/// <summary>
/// 预热:加载规则与应用到内存,并预编译自由文本规则到 RuleCompiledCache。
/// 结构化规则运行时直接执行 ConditionJson,预热时只做轻量结构校验,不再预编译 RuleExpr。
/// </summary>
public void Preload()
{
lock (_cacheLock)
{
_logger.Info("[风控引擎] Preload 开始 - 加载规则与应用并预编译");
var rules = LoadRulesFromDb();
var applications = LoadApplicationsFromDb();
// 仅预编译自由文本规则;结构化规则运行时由 ConditionJson 执行。
foreach (var rule in rules)
{
var ruleId = rule.Id.ToString();
//非活跃的rule不编译
if (rule.Status != RiskRuleStatus.Active)
{
RuleCompiledCache.Remove(rule.Id.ToString());
_logger.Info($"[风控引擎] 规则非活跃,已移除预编译缓存 - RuleId: {rule.Id}, Status: {rule.Status}");
continue;
}
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
RuleCompiledCache.Remove(rule.Id.ToString());
try
{
// 结构化条件在加载阶段已解析到 ParsedConditions;这里仅确认缓存存在,运行时不再反序列化 ConditionJson。
if (rule.ParsedConditions == null || rule.ParsedConditions.Count == 0)
rule.ParsedConditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
_logger.Info($"[风控引擎] 结构化规则预热校验成功,跳过 RuleExpr 预编译 - RuleId: {rule.Id}");
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] 结构化规则预热校验失败 - RuleId: {rule.Id}, Error: {ex.Message}");
}
continue;
}
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (compileResult.Success)
{
_logger.Info($"[风控引擎] 规则预编译成功 - RuleId: {rule.Id}");
}
else
{
_logger.Info($"[风控引擎] 规则预编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
}
}
_cachedRules = rules;
_cachedApplications = applications;
_logger.Info($"[风控引擎] Preload 完成 - 规则数: {rules.Count}, 应用数: {applications.Count}");
}
}
/// <summary>
/// 刷新缓存:刷新规则与应用内存缓存,预编译成功时覆盖旧编译缓存。
/// 规则/应用配置更新后调用。
/// </summary>
public void RefreshCache()
{
_logger.Info("[风控引擎] RefreshCache 被调用 - 刷新规则与应用缓存并重新预加载");
lock (_cacheLock)
{
_cachedRules = null;
_cachedApplications = null;
RuleCompiledCache.Clear();
StructuredRuleExecutor.ClearVariableCompiledCache();
}
Preload();
}
/// <summary>
/// 刷新单条规则缓存:重新加载规则列表,只编译指定规则。
/// </summary>
public RuleCompileResult RefreshOneRuleCache(long ruleId)
{
lock (_cacheLock)
{
_logger.Info($"[风控引擎] RefreshOneRuleCache 开始 - RuleId: {ruleId}");
// 从数据库加载最新规则列表
_cachedRules = LoadRulesFromDb();
var rule = _cachedRules.FirstOrDefault(r => r.Id == ruleId);
if (rule == null)
{
RuleCompiledCache.Remove(ruleId.ToString());
_logger.Info($"[风控引擎] RefreshOneRuleCache 未找到规则,已移除缓存 - RuleId: {ruleId}");
return RuleCompileResult.Fail("规则不存在或已删除");
}
if (rule.Status != RiskRuleStatus.Active)
{
RuleCompiledCache.Remove(ruleId.ToString());
_logger.Info($"[风控引擎] RefreshOneRuleCache 规则非活跃,已移除缓存 - RuleId: {ruleId}, Status: {rule.Status}");
return RuleCompileResult.Fail("规则不存在或非活跃状态");
}
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
RuleCompiledCache.Remove(ruleId.ToString());
try
{
// 单规则刷新时同步刷新结构化条件缓存,避免后续执行继续解析旧 JSON 或重复反序列化。
rule.ParsedConditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
_logger.Info($"[风控引擎] RefreshOneRuleCache 结构化规则校验成功,跳过 RuleExpr 编译 - RuleId: {ruleId}");
return RuleCompileResult.Ok(null);
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] RefreshOneRuleCache 结构化规则校验失败 - RuleId: {ruleId}, Error: {ex.Message}");
return RuleCompileResult.Fail(ex.Message);
}
}
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
_logger.Info($"[风控引擎] RefreshOneRuleCache 完成 - RuleId: {ruleId}, Success: {compileResult.Success}, Error: {compileResult.ErrorMessage}");
return compileResult;
}
}
/// <summary>
/// 仅刷新规则应用列表,不清空规则编译缓存。
/// </summary>
public void RefreshApplication()
{
lock (_cacheLock)
{
_cachedApplications = LoadApplicationsFromDb();
_logger.Info($"[风控引擎] RefreshApplication 完成 - 应用数: {_cachedApplications.Count}");
}
}
/// <summary>
/// 从内存缓存获取规则列表;缓存为空时兜底加载并填充缓存。
/// </summary>
private List<RiskRule> GetRules()
{
var rules = _cachedRules;
if (rules != null)
{
return rules;
}
lock (_cacheLock)
{
if (_cachedRules != null)
{
return _cachedRules;
}
var loaded = LoadRulesFromDb();
_cachedRules = loaded;
return loaded;
}
}
/// <summary>
/// 从内存缓存获取应用列表;缓存为空时兜底加载并填充缓存。
/// </summary>
private List<RiskRuleApplication> GetApplications()
{
var applications = _cachedApplications;
if (applications != null)
{
return applications;
}
lock (_cacheLock)
{
if (_cachedApplications != null)
{
return _cachedApplications;
}
var loaded = LoadApplicationsFromDb();
_cachedApplications = loaded;
return loaded;
}
}
/// <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
{
using var ruleDbContext = DbContextFactory.GetYLDbContext();
ruleDbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
context ??= new RiskContext();
context.TriggerPoint = triggerPoint;
context.DbContext = ruleDbContext;
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context.TradeId}, TriggerPoint: {triggerPoint}");
// ============================================================
// Step 1: 从内存缓存读取规则定义和规则应用(启动时已预热)
// ============================================================
var rules = GetRules();
var applications = GetApplications();
// 规则列表在本次执行内转为字典,应用按 RuleIds 关联规则时直接按 ID 查找,避免每个应用重复扫描规则列表。
var ruleMap = rules.ToDictionary(r => r.Id);
var variables = new Dictionary<long, glms_risk_variable>();
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}, 应用数: {applications.Count}");
// ============================================================
// Step 2: 先按 Application 过滤 Active 状态和 TriggerPoint 匹配的应用
// ============================================================
var activeApps = applications
.Where(a => a.Status == RiskRuleStatus.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();
// 再按应用范围过滤。
// 统一走通用维度匹配:
// 1. 全局命中时直接通过;
// 2. 同一维度内多选按并集处理;
// 3. 不同维度之间按交集处理;
// 4. 某维度留空表示该维度不限制。
var trade = context.TradeId > 0
? context.DbContext.trade.AsNoTracking().FirstOrDefault(t => t.id == context.TradeId)
: null;
var matchedApplications = triggerMatchedApps
.Where(a => IsApplicationMatched(a, trade))
.ToList();
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的应用数: {matchedApplications.Count}");
// 汇总本次实际命中应用关联的结构化规则,变量定义只在本次检查开始时批量查询一次。
// 同一规则可能被多个应用引用,先对规则 ID 去重,避免重复处理规则定义。
var matchedRuleIds = matchedApplications
.SelectMany(application => ParseRuleIds(application.RuleIds))
.Distinct();
// 只保留当前规则缓存中存在的规则,配置中无效或已删除的规则 ID 仍由后续原有流程记录日志并跳过。
var matchedRules = matchedRuleIds
.Where(ruleMap.ContainsKey)
.Select(ruleId => ruleMap[ruleId]);
// 自由文本规则不依赖变量定义;条件缓存为空的结构化规则继续在执行阶段按原逻辑报错。
var structuredRules = matchedRules
.Where(rule => rule.Status == RiskRuleStatus.Active)
.Where(rule => !string.IsNullOrWhiteSpace(rule.ConditionJson))
.Where(rule => rule.ParsedConditions != null && rule.ParsedConditions.Count > 0);
// 条件中的主变量和变量阈值都需要加载,多条规则引用同一变量时只保留一个 ID。
var requiredVariableIds = structuredRules
.SelectMany(rule => RuleConditionExpressionBuilder.GetReferencedVariableIds(rule.ParsedConditions))
.Distinct()
.ToList();
if (requiredVariableIds.Any())
{
// 仅加载本次检查需要的变量,避免按规则逐次查询,也避免读取整个变量池。
variables = ruleDbContext.glms_risk_variable
.AsNoTracking()
.Where(variable => requiredVariableIds.Contains(variable.id))
.ToDictionary(variable => (long)variable.id);
}
// ============================================================
// Step 3: 遍历匹配的应用,通过 RuleIds 关联规则并执行预编译委托
// ============================================================
foreach (var application in matchedApplications)
{
// ------------------------------------------------------------
// 3.1 通过 RuleIds 关联规则定义
// ------------------------------------------------------------
var applicationRuleIds = ParseRuleIds(application.RuleIds);
if (!applicationRuleIds.Any())
{
_logger.Info($"[风控引擎] 应用未配置有效规则 - RuleIds: {application.RuleIds}");
continue;
}
var applicationRules = new List<RiskRule>();
foreach (var ruleId in applicationRuleIds)
{
// 通过本次执行预构建的规则字典按 ID 查找,避免每个应用都 Where 扫描全量规则列表。
if (!ruleMap.TryGetValue(ruleId, out var ruleDef))
{
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {ruleId}, ApplicationRuleIds: {application.RuleIds}");
continue;
}
if (ruleDef.Status != RiskRuleStatus.Active)
{
_logger.Info($"[风控引擎] 规则非活跃,跳过执行 - RuleId: {ruleId}, Status: {ruleDef.Status}, ApplicationRuleIds: {application.RuleIds}");
continue;
}
applicationRules.Add(ruleDef);
}
foreach (var rule in applicationRules)
{
var ruleId = rule.Id.ToString();
bool triggered = false;
StructuredRuleExecuteResult executeResult = null;
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
{
// 结构化规则优先使用 ConditionJson 执行,避免继续把条件整体拼成 Roslyn bool 公式。
try
{
// 结构化条件在规则加载/刷新时已解析到内存缓存,运行时直接复用,避免每次执行反序列化 ConditionJson。
var conditions = rule.ParsedConditions;
if (conditions == null || conditions.Count == 0)
throw new InvalidOperationException("结构化规则条件缓存为空");
var referencedVariableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions);
var ruleVariables = referencedVariableIds
.Where(variables.ContainsKey)
.ToDictionary(id => id, id => variables[id]);
executeResult = StructuredRuleExecutor.Execute(conditions, ruleVariables, context);
if (!executeResult.Success)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{executeResult.ErrorMessage}";
_logger.Error($"[风控引擎] 结构化规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {executeResult.ErrorMessage}");
AddBlockError(result, ruleId, application.Id, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
triggered = executeResult.Triggered;
_logger.Info($"[风控引擎] 结构化规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 结构化规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, application.Id, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
}
else
{
// ------------------------------------------------------------
// 3.2 优先从编译缓存读取规则委托
// ------------------------------------------------------------
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
{
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (!compileResult.Success)
{
var errorMessage = $"规则[{rule.RuleName}]编译失败,已按阻断处理,请检查规则表达式配置:{compileResult.ErrorMessage}";
_logger.Info($"[风控引擎] 规则编译失败,按阻断处理 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
AddBlockError(result, rule.Id.ToString(), application.Id, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
compiledScript = compileResult.CompiledScript;
}
// ------------------------------------------------------------
// 3.3 执行预编译委托
// ------------------------------------------------------------
try
{
triggered = compiledScript(context);
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, application.Id, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
}
// ------------------------------------------------------------
// 3.4 命中后按 Application 的控制策略聚合结果
// ------------------------------------------------------------
if (triggered)
{
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
// 结构化规则执行器会返回实际值、阈值和变量计算明细;纯文本规则没有该信息时保持原提示。
var triggerMessageDetail = executeResult?.Message;
switch (application.ControlStrategy)
{
case RiskControlStrategy.Block:
result.Blocked = true;
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = rule.Id.ToString(),
ApplicationId = application.Id,
RuleName = rule.RuleName,
ControlStrategy = RiskControlStrategy.Block,
RuleText = rule.RuleText,
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:禁止", triggerMessageDetail)
});
break;
case RiskControlStrategy.Approval:
result.NeedApproval = true;
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = ruleId,
ApplicationId = application.Id,
RuleName = rule.RuleName,
ControlStrategy = RiskControlStrategy.Approval,
RuleText = rule.RuleText,
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:需审批", triggerMessageDetail)
});
break;
case RiskControlStrategy.ShowTip:
result.ShowTip = true;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = ruleId,
ApplicationId = application.Id,
RuleName = rule.RuleName,
ControlStrategy = RiskControlStrategy.ShowTip,
RuleText = rule.RuleText,
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:提示", triggerMessageDetail)
});
break;
default:
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
break;
}
}
else
{
_logger.Info($"[风控引擎] 规则未触发 - RuleId: {rule.Id}, TriggerPoint: {triggerPoint}");
}
}
}
// ============================================================
// Step 4: 聚合最终结果
// ============================================================
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}");
}
catch (Exception ex)
{
var errorMessage = $"风控引擎异常:{ex.Message}";
AddBlockError(result, "ENGINE_ERROR", null, "风控引擎执行异常", ex.Message, errorMessage);
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
}
return result;
}
private static string BuildTriggeredMessage(string baseMessage, string detailMessage)
{
return string.IsNullOrWhiteSpace(detailMessage)
? baseMessage
: $"{baseMessage}。{detailMessage}";
}
private static void AddBlockError(RiskResult result, string ruleId, long? applicationId, string ruleName, string ruleText, string errorMessage)
{
result.Blocked = true;
result.Passed = false;
result.ErrorMessage = string.IsNullOrWhiteSpace(result.ErrorMessage)
? errorMessage
: $"{result.ErrorMessage}{errorMessage}";
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = ruleId,
ApplicationId = applicationId,
RuleName = ruleName,
ControlStrategy = RiskControlStrategy.Block,
RuleText = ruleText,
Message = errorMessage
});
}
/// <summary>
/// 从数据库加载规则列表
/// </summary>
private List<RiskRule> LoadRulesFromDb()
{
using var dbContext = DbContextFactory.GetYLDbContext();
var rules = dbContext.glms_risk_rule
.AsNoTracking()
.OrderByDescending(r => r.UpdateDate ?? r.OptDate)
.Select(r => new RiskRule
{
Status = r.Status,
Id = r.id,
RuleName = r.RuleName,
RuleText = r.RuleText,
ConditionJson = r.ConditionJson,
RuleExpr = r.RuleExpr,
Version = r.Version,
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();
// 结构化规则的 ConditionJson 在加载阶段解析一次并缓存到规则对象,运行时直接复用 ParsedConditions。
// 如果历史脏数据解析失败,不中断整批规则加载,后续执行该规则时仍按单条规则异常处理。
foreach (var rule in rules.Where(r => !string.IsNullOrWhiteSpace(r.ConditionJson)))
{
try
{
rule.ParsedConditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] 结构化规则条件解析失败 - RuleId: {rule.Id}, Error: {ex.Message}");
}
}
return rules;
}
private List<RiskRuleApplication> LoadApplicationsFromDb()
{
using var dbContext = DbContextFactory.GetYLDbContext();
var applications = dbContext.glms_risk_rule_application
.AsNoTracking()
.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();
return applications;
}
// applications.Add(new RiskRuleApplication
// {
// Id = 1000001,
// RuleIds = "1000001",
// Status = RiskRuleStatus.Active,
// ControlStrategy = RiskControlStrategy.Approval,
// TriggerPoints = "BOOK_CONFIRM",
// ScopeIsGlobal = true,
// ScopeAssetBookIds = string.Empty,
// ScopeClientIds = string.Empty,
// ScopeUnderlyingTypes = string.Empty,
// ScopeTradeTypes = string.Empty,
// Version = 1,
// OptId = 0,
// OptName = "system",
//}
/// <summary>
/// 判断应用配置是否命中当前交易。
/// 匹配规则遵循设计文档:
/// 1. 全局命中时直接返回 true;
/// 2. 同一维度内多选按并集处理;
/// 3. 不同维度之间按交集处理;
/// 4. 某维度留空表示该维度不限制。
/// </summary>
private bool IsApplicationMatched(RiskRuleApplication application, YLErp.DBModels.trade trade)
{
if (application == null)
{
return false;
}
if (application.ScopeIsGlobal == 1)
{
return true;
}
var hasAnyScope =
!IsScopeEmpty(application.ScopeAssetBookIds) ||
!IsScopeEmpty(application.ScopeClientIds) ||
!IsScopeEmpty(application.ScopeUnderlyingTypes) ||
!IsScopeEmpty(application.ScopeTradeTypes);
if (!hasAnyScope)
{
_logger.Error($"[风控引擎] 非全局应用未配置任何适用范围,拒绝匹配 - ApplicationId: {application.Id}");
return false;
}
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>
/// 解析应用配置中的规则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>
/// 判断某个范围字段是否为空。
/// 为空表示该维度不限制。
/// </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。
/// </summary>
private object GetTradeAssetBookId(YLErp.DBModels.trade trade)
{
return trade.AssetId;
}
/// <summary>
/// 读取交易的标的类型。
/// </summary>
private object GetTradeUnderlyingType(YLErp.DBModels.trade trade)
{
return trade.UnderlyingAssetClass;
}
}
}