diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs
new file mode 100644
index 00000000..65a88901
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 规则编译结果。
+ /// 用于承载公式校验、Roslyn 编译结果和错误信息。
+ ///
+ public class RuleCompileResult
+ {
+ ///
+ /// 是否编译成功。
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 编译失败时的错误信息。
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 编译成功后的可执行委托。
+ ///
+ public Func CompiledScript { get; set; }
+
+ ///
+ /// 创建成功结果。
+ ///
+ public static RuleCompileResult Ok(Func compiledScript)
+ {
+ return new RuleCompileResult
+ {
+ Success = true,
+ CompiledScript = compiledScript
+ };
+ }
+
+ ///
+ /// 创建失败结果。
+ ///
+ public static RuleCompileResult Fail(string errorMessage)
+ {
+ return new RuleCompileResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs
new file mode 100644
index 00000000..59fe9099
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Concurrent;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 规则编译结果的进程内缓存。
+ /// 当前第一版按 RuleCode 缓存 Roslyn 编译后的可执行委托。
+ ///
+ public static class RuleCompiledCache
+ {
+ ///
+ /// 规则编译结果的进程内缓存。ConcurrentDictionary为线程安全的字典,用于缓存编译后的规则委托,性能略低。
+ ///
+ private static readonly ConcurrentDictionary> Cache
+ = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// 写入或更新规则编译结果。
+ ///
+ public static void Set(string ruleCode, Func compiledScript)
+ {
+ if (string.IsNullOrWhiteSpace(ruleCode) || compiledScript == null)
+ {
+ return;
+ }
+
+ Cache[ruleCode] = compiledScript;
+ }
+
+ ///
+ /// 尝试获取规则编译结果。
+ ///
+ public static bool TryGet(string ruleCode, out Func compiledScript)
+ {
+ compiledScript = null;
+
+ if (string.IsNullOrWhiteSpace(ruleCode))
+ {
+ return false;
+ }
+
+ return Cache.TryGetValue(ruleCode, out compiledScript);
+ }
+
+ ///
+ /// 删除指定规则的编译结果。
+ ///
+ public static void Remove(string ruleCode)
+ {
+ if (string.IsNullOrWhiteSpace(ruleCode))
+ {
+ return;
+ }
+
+ Cache.TryRemove(ruleCode, out _);
+ }
+
+ ///
+ /// 清空全部规则编译结果。
+ ///
+ public static void Clear()
+ {
+ Cache.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
similarity index 61%
rename from YLErpDAL/Modules/RiskEngine/RuleCompiler.cs
rename to YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
index c44a8d27..5f003ff1 100644
--- a/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
@@ -42,6 +42,7 @@ namespace YLErp.Modules.RiskEngine
/// 变量编码 → 类型全名的映射
/// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"])
///
+ static IYcLogger _logger =LogFactory.GetLogger("RuleCompiler");
private static readonly Dictionary VariableTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["trade"] = "YLErp.DBModels.trade",
@@ -62,22 +63,59 @@ namespace YLErp.Modules.RiskEngine
///
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);
+ }
+ }
+
+ ///
+ /// 校验并编译公式表达式。
+ /// 当前 FormulaExp 要求是 Roslyn 可直接执行的 bool 表达式。
+ ///
+ 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);
+ ///
+ /// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
+ ///
+ 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;
}
///
@@ -118,45 +156,74 @@ namespace YLErp.Modules.RiskEngine
/// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式,
/// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr
///
- 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();
+
+ // 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);
+ //}
+
+ ///
+ /// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。
+ /// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。
+ /// 数值类型会自动包一层 Convert.ToDecimal,便于与 decimal 阈值比较。
+ ///
+ private static string BuildMemberAccessExpression(string expression, string variableType)
{
- var exprParts = new List();
-
- 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;
}
///
@@ -199,7 +266,7 @@ namespace YLErp.Modules.RiskEngine
catch (Exception ex)
{
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
- // TODO: 记录日志
+ _logger.Error($"规则执行异常:{ex.Message}\n脚本代码:{scriptCode}");
return false;
}
};
diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
index 38dca7dc..0f763cf9 100644
--- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
+++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
@@ -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 委托,写入 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。
- 编译流程:
- 1. 解析 Content:提取变量名、条件符、阈值
- 2. 变量名映射:用户输入的"名义本金" → "trade.StockEqvNotional"
- 3. 构建表达式树:Func = ctx => ...
- 4. 编译为委托:rule.CompiledScript = lambda.Compile()
- 5. 判风控时直接调用:rule.CompiledScript(context) → bool
+ 当前编译流程:
+ 1. 外部准备好规则对象,FormulaExp 中直接写最终可执行表达式
+ 2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
+ 3. 内部使用 Roslyn 编译 FormulaExp,生成 Func
+ 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 委托 │
- │ │
- │ 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,这是规则判断函数,不是事件处理器
+ 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
///
private List 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 { 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 { Rule1, Rule2 };
+ }
+ ///
+ /// 加载application
+ /// 当前返回示例规则用于验证流程
+ ///
+ private List 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 { application1, application2 };
+ }
+
+ ///
+ /// 判断应用配置是否命中当前交易。
+ /// 匹配规则遵循设计文档:
+ /// 1. 全局命中时直接返回 true;
+ /// 2. 同一维度内多选按并集处理;
+ /// 3. 不同维度之间按交集处理;
+ /// 4. 某维度留空表示该维度不限制。
+ ///
+ 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;
+ }
+
+ ///
+ /// 判断某个范围字段是否为空。
+ /// 为空表示该维度不限制。
+ ///
+ private bool IsScopeEmpty(string scopeValue)
+ {
+ return string.IsNullOrWhiteSpace(scopeValue);
+ }
+
+ ///
+ /// 判断单个值是否命中逗号分隔的范围配置。
+ /// 同一维度内多选按并集处理,只要命中任一值即返回 true。
+ ///
+ 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));
+ }
+
+ ///
+ /// 读取交易的资产簿账户ID。
+ /// 当前先按 BookId 取值;如果后续真实字段不是 BookId,再统一调整这里即可。
+ ///
+ private object GetTradeAssetBookId(YLErp.DBModels.trade trade)
+ {
+ var property = trade.GetType().GetProperty("BookId");
+ return property?.GetValue(trade);
+ }
+
+ ///
+ /// 读取交易的标的类型。
+ /// 当前先按 UnderlyingInstrumentType 取值;如果后续真实字段名不同,再统一调整这里即可。
+ ///
+ private object GetTradeUnderlyingType(YLErp.DBModels.trade trade)
+ {
+ var property = trade.GetType().GetProperty("UnderlyingInstrumentType");
+ return property?.GetValue(trade);
}
}
}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskRule.cs b/YLErpDAL/Modules/RiskEngine/RiskRule.cs
index 95f5fd7a..b01c9f1a 100644
--- a/YLErpDAL/Modules/RiskEngine/RiskRule.cs
+++ b/YLErpDAL/Modules/RiskEngine/RiskRule.cs
@@ -3,12 +3,9 @@ using System;
namespace YLErp.Modules.RiskEngine
{
///
- /// 风控规则(合并模型:规则定义 + 应用配置合一)
- ///
- /// 最终设计说明:
- /// 业务上不需要 "一个 Rule 对应多个 Application" 的复杂关系。
- /// 每条规则自身包含:公式定义 + 校验策略 + 触发时点 + 适用范围。
- /// 规则创建时即完成脚本编译,判风控时直接执行已编译委托(零解析开销)。
+ /// 风控规则定义。
+ /// 只描述“规则是什么”,不包含状态、策略、触发时点、适用范围等应用配置。
+ /// 这些执行维度由 RiskRuleApplication 承载。
///
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; }
///
/// 【核心】条件JSON(结构化模式的条件列表存储),如
@@ -30,33 +28,7 @@ namespace YLErp.Modules.RiskEngine
///
public string RuleExpr { get; set; }
- // === 校验策略(原 Application.ControlStrategy)===
- ///
- /// 校验策略:1=禁止, 2=审批, 3=提示
- ///
- public int ControlStrategy { get; set; }
-
- // === 触发时点(原 Application.TriggerPoints)===
- ///
- /// 触发时点,逗号分隔(如 BOOK_CONFIRM,CLOSE_REVIEW)
- ///
- public string TriggerPoints { get; set; }
-
- // === 适用范围(原 Application.Scope*)===
- ///
- /// 是否全局适用(true=所有交易适用,忽略其他 Scope 字段)
- ///
- 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; }
-
// === 生命周期 ===
- ///
- /// 状态:1=已生效(Active), 2=已停用(Disabled), 3=已删除(Deleted)
- ///
- public int Status { get; set; }
public int Version { get; set; }
public bool IsDeleted { get; set; }
diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs
new file mode 100644
index 00000000..2a554d1e
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs
@@ -0,0 +1,188 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控规则应用配置。
+ ///
+ /// 设计原因:
+ /// 依据设计文档,风控规则需要区分“规则定义”和“规则应用”两个层面。
+ /// RiskRule 负责定义“规则是什么”,例如规则名称、规则编码、公式表达式、描述、版本等;
+ /// RiskRuleApplication 负责定义“规则在什么场景下如何执行”,例如启用状态、校验策略、触发时点、适用范围。
+ ///
+ /// 拆分后的好处是:
+ /// 1. 同一条规则可以被多个应用配置复用,避免重复维护相同的公式定义。
+ /// 2. 不同业务场景可以对同一条规则配置不同的执行策略,例如在簿记确认时禁止,在平仓审核时提示。
+ /// 3. 调整触发时点、适用范围、控制策略时,不需要修改规则本身,降低配置耦合。
+ /// 4. 后续如果要做规则管理页面、规则启停、按场景灰度投放,也更容易扩展。
+ ///
+ /// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
+ ///
+ ///
+ /// 规则应用状态枚举。
+ /// 用于描述一条规则应用配置当前是否参与风控执行。
+ ///
+ public enum RiskRuleApplicationStatusEnum
+ {
+ ///
+ /// 已生效。
+ /// 当前应用配置可正常参与风控判断。
+ ///
+ Active = 1,
+
+ ///
+ /// 已停用。
+ /// 当前应用配置暂不参与风控判断,但配置仍被保留。
+ ///
+ Disabled = 2,
+
+ ///
+ /// 已删除。
+ /// 当前应用配置逻辑上已删除,通常不再参与任何执行。
+ ///
+ Deleted = 3
+ }
+
+ ///
+ /// 风控控制策略枚举。
+ /// 用于描述规则命中后的处理方式。
+ ///
+ public enum RiskControlStrategyEnum
+ {
+ ///
+ /// 禁止。
+ /// 规则命中后直接拦截,不允许继续后续流程。
+ ///
+ Block = 1,
+
+ ///
+ /// 审批。
+ /// 规则命中后不直接放行,需要进入审批流程。
+ ///
+ Approval = 2,
+
+ ///
+ /// 提示。
+ /// 规则命中后只给出风险提示,不阻断流程。
+ ///
+ Warning = 3
+ }
+ public class RiskRuleApplication
+ {
+ ///
+ /// 应用配置主键ID。
+ /// 用于唯一标识一条规则应用记录。
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// 关联的规则编码。
+ /// 通过该字段与 RiskRule.RuleCode 关联,表示当前应用配置对应哪一条规则定义。
+ /// 第一版先用字符串编码关联,后续如有需要也可以扩展为 RuleId。
+ ///
+ public string RuleCode { get; set; }
+
+ ///
+ /// 应用状态。
+ /// 约定:
+ /// 1 = Active(已生效)
+ /// 2 = Disabled(已停用)
+ /// 3 = Deleted(已删除)
+ /// 在实际执行时,通常只加载 Active 状态的应用配置。
+ ///
+ public RiskRuleApplicationStatusEnum Status { get; set; }
+
+ ///
+ /// 校验策略。
+ /// 约定:
+ /// 1 = 禁止
+ /// 2 = 审批
+ /// 3 = 提示
+ /// 当规则命中时,由该字段决定最终处理方式。
+ ///
+ public RiskControlStrategyEnum ControlStrategy { get; set; }
+
+ ///
+ /// 触发时点。
+ /// 多个时点使用逗号分隔,例如:
+ /// BOOK_CONFIRM,CLOSE_REVIEW
+ /// 表示该应用配置会在这些业务时点参与风控判断。
+ ///
+ public string TriggerPoints { get; set; }
+
+ ///
+ /// 是否全局适用。
+ /// true 表示对所有交易都适用,此时通常忽略其他 Scope 字段;
+ /// false 表示只在指定范围内适用,需要结合下方各 Scope 字段进行过滤。
+ ///
+ public bool ScopeIsGlobal { get; set; }
+
+ ///
+ /// 适用的资产簿记账户范围。
+ /// 第一版使用逗号分隔的字符串保存,例如:
+ /// 123,456,789
+ /// 后续执行时可按约定解析为账户ID集合。
+ ///
+ public string ScopeAssetBookIds { get; set; }
+
+ ///
+ /// 适用的客户范围。
+ /// 第一版使用逗号分隔的字符串保存客户ID,例如:
+ /// 10001,10002
+ /// 用于在执行前筛选该规则是否适用于当前客户。
+ ///
+ public string ScopeClientIds { get; set; }
+
+ ///
+ /// 适用的标的类型范围。
+ /// 例如:
+ /// 股票,ETF,债券
+ /// 用于控制规则仅对特定标的类型生效。
+ ///
+ public string ScopeUnderlyingTypes { get; set; }
+
+ ///
+ /// 适用的交易类型范围。
+ /// 例如:
+ /// 簿记交易,平仓交易
+ /// 用于控制规则仅在指定交易类型下生效。
+ ///
+ public string ScopeTradeTypes { get; set; }
+
+ ///
+ /// 创建人ID。
+ /// 记录是谁创建了这条应用配置。
+ ///
+ public int OptId { get; set; }
+
+ ///
+ /// 创建人名称。
+ /// 便于界面展示和审计追踪。
+ ///
+ public string OptName { get; set; }
+
+ ///
+ /// 创建时间。
+ /// 记录应用配置的创建时刻。
+ ///
+ public DateTime OptDate { get; set; }
+
+ ///
+ /// 最后更新人ID。
+ /// 记录最近一次修改该应用配置的操作人。
+ ///
+ public int UpdateOptId { get; set; }
+
+ ///
+ /// 最后更新人名称。
+ /// 便于界面展示和审计追踪。
+ ///
+ public string UpdateOptName { get; set; }
+
+ ///
+ /// 最后更新时间。
+ /// 记录最近一次修改该应用配置的时间。
+ ///
+ public DateTime UpdateDate { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
index 385f2299..fa5132ab 100644
--- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
+++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
@@ -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");