Revert "feat: 风控引擎核心重构 - 表达式树编译 + Roslyn Scripting + 规则模型合并"
This reverts commit 3e357312c2.
This commit is contained in:
@@ -1,297 +0,0 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Roslyn 脚本全局变量容器
|
||||
/// CSharpScript 执行时通过此对象传递 DataMap
|
||||
/// </summary>
|
||||
public class ScriptGlobals
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据字典:key=表名/前缀,value=对应数据对象
|
||||
/// </summary>
|
||||
public Dictionary<string, object> DataMap { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规则编译器(Roslyn 版)
|
||||
///
|
||||
/// 编译时机:规则定义时(零运行时解析开销)
|
||||
/// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool
|
||||
///
|
||||
/// 相比表达式树方案的优势:
|
||||
/// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点
|
||||
/// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法
|
||||
/// - 可调试:生成的脚本代码可直接阅读和理解
|
||||
///
|
||||
/// 示例:
|
||||
/// FormulaJson: { "conditions": [{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}] }
|
||||
/// 生成脚本:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
/// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存)
|
||||
/// </summary>
|
||||
public static class RuleCompiler
|
||||
{
|
||||
/// <summary>
|
||||
/// 变量编码 → 类型全名的映射
|
||||
/// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"])
|
||||
/// </summary>
|
||||
static IYcLogger _logger =LogFactory.GetLogger("RuleCompiler");
|
||||
private static readonly Dictionary<string, string> VariableTypeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["trade"] = "YLErp.DBModels.trade",
|
||||
["swap_position"] = "YLErp.DBModels.swap_position",
|
||||
["client"] = "YLErp.DBModels.client",
|
||||
["credit"] = "YLErp.DBModels.credit",
|
||||
["client_marginrate"] = "YLErp.DBModels.client_marginrate",
|
||||
["market"] = "YLErp.DBModels.market",
|
||||
["calc"] = "YLErp.DBModels.calc",
|
||||
["sys"] = "YLErp.DBModels.sys",
|
||||
["eod_swap_position"] = "YLErp.DBModels.eod_swap_position",
|
||||
["realtime_trade_risk"] = "YLErp.DBModels.realtime_trade_risk",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 将规则编译为可执行委托,并写入 rule.CompiledScript
|
||||
/// 调用时机:规则创建时 / 规则加载时 / 缓存刷新时
|
||||
/// </summary>
|
||||
public static void Compile(this RiskRule rule)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rule.FormulaJson))
|
||||
{
|
||||
throw new ArgumentException("规则 FormulaJson 不能为空", nameof(rule.FormulaJson));
|
||||
}
|
||||
|
||||
// 1. 解析 FormulaJson
|
||||
var conditions = ParseFormulaJson(rule.FormulaJson);
|
||||
|
||||
// 2. 生成 Roslyn C# 脚本代码
|
||||
string scriptCode = BuildScriptCode(conditions);
|
||||
|
||||
// 3. 编译脚本(只编译一次,后续直接执行)
|
||||
var compiled = CompileScript(scriptCode);
|
||||
|
||||
// 4. 写入规则(不持久化,仅内存缓存)
|
||||
rule.CompiledScript = compiled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 FormulaJson,提取条件列表
|
||||
/// </summary>
|
||||
private static List<(string variableCode, string op, object value, string variableType)> ParseFormulaJson(string formulaJson)
|
||||
{
|
||||
var result = new List<(string, string, object, string)>();
|
||||
var jObj = JObject.Parse(formulaJson);
|
||||
var conditions = jObj["conditions"] as JArray;
|
||||
|
||||
if (conditions == null || !conditions.Any())
|
||||
{
|
||||
throw new ArgumentException("FormulaJson 中缺少 conditions");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string variableCode = cond["variableCode"]?.Value<string>();
|
||||
string op = cond["operator"]?.Value<string>();
|
||||
object value = cond["value"]?.Value<object>();
|
||||
string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(variableCode) || string.IsNullOrWhiteSpace(op))
|
||||
{
|
||||
throw new ArgumentException("条件中缺少 variableCode 或 operator");
|
||||
}
|
||||
|
||||
result.Add((variableCode, op, value, variableType));
|
||||
}
|
||||
|
||||
var result = ValidateAndCompileFormula(rule.RuleExpr);
|
||||
if (!result.Success)
|
||||
{
|
||||
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
rule.CompiledScript = result.CompiledScript;
|
||||
RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据条件列表生成 Roslyn C# 脚本代码
|
||||
/// 多条件自动用 &&(AND)连接
|
||||
/// </summary>
|
||||
private static string BuildScriptCode(List<(string variableCode, string op, object value, string variableType)> conditions)
|
||||
{
|
||||
var exprParts = new List<string>();
|
||||
|
||||
foreach (var (variableCode, op, value, variableType) in conditions)
|
||||
{
|
||||
// 解析 variableCode:前缀.属性名
|
||||
var parts = variableCode.Split('.');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
throw new ArgumentException($"variableCode 格式不正确:{variableCode}");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。
|
||||
/// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。
|
||||
/// 数值类型会自动包一层 Convert.ToDecimal,便于与 decimal 阈值比较。
|
||||
/// </summary>
|
||||
private static string BuildMemberAccessExpression(string expression, string variableType)
|
||||
{
|
||||
var parts = expression.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
throw new ArgumentException($"expression 格式不正确:{expression}");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用 Roslyn 编译 C# 脚本代码为可执行委托
|
||||
/// </summary>
|
||||
private static Func<RiskContext, bool> CompileScript(string scriptCode)
|
||||
{
|
||||
// 配置编译选项:引用必要的程序集
|
||||
var options = ScriptOptions.Default
|
||||
.WithReferences(
|
||||
typeof(RiskContext).Assembly, // YLErpDAL
|
||||
typeof(YLErp.DBModels.trade).Assembly // Model 所在程序集
|
||||
)
|
||||
.WithImports("System");
|
||||
|
||||
// 创建脚本(尚未执行,仅编译)
|
||||
var script = CSharpScript.Create<bool>(scriptCode, options, globalsType: typeof(ScriptGlobals));
|
||||
|
||||
// 编译(提前发现语法错误)
|
||||
//后续编译需提供接口,返回前端编译信息,包含编译错误列表
|
||||
var compilation = script.GetCompilation();
|
||||
var diagnostics = compilation.GetDiagnostics();
|
||||
var errors = diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).ToList();
|
||||
if (errors.Any())
|
||||
{
|
||||
string errorMsg = string.Join("; ", errors.Select(e => e.GetMessage()));
|
||||
_logger.Error($"规则编译失败:{errorMsg}\n脚本代码:{scriptCode}");
|
||||
throw new InvalidOperationException($"脚本编译失败:{errorMsg}");
|
||||
}
|
||||
|
||||
// 生成可调用委托
|
||||
var runner = script.CreateDelegate();
|
||||
|
||||
// 包装为同步的 Func<RiskContext, bool>
|
||||
return ctx =>
|
||||
{
|
||||
var globals = new ScriptGlobals { DataMap = ctx.DataMap };
|
||||
try
|
||||
{
|
||||
return runner(globals).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
|
||||
_logger.Error($"规则执行异常:{ex.Message}\n脚本代码:{scriptCode}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化阈值为 C# 字面量
|
||||
/// </summary>
|
||||
private static string FormatValueLiteral(object value, string variableType)
|
||||
{
|
||||
if (value == null) return "null";
|
||||
|
||||
string strValue = value.ToString();
|
||||
|
||||
// 数值类型统一加 m 后缀(decimal)
|
||||
if (variableType == "numeric" || variableType == "number")
|
||||
{
|
||||
if (decimal.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal d))
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture) + "m";
|
||||
}
|
||||
}
|
||||
|
||||
// 布尔
|
||||
if (variableType == "boolean" || variableType == "bool")
|
||||
{
|
||||
if (bool.TryParse(strValue, out bool b))
|
||||
{
|
||||
return b ? "true" : "false";
|
||||
}
|
||||
}
|
||||
|
||||
// 日期
|
||||
if (variableType == "date" && DateTime.TryParse(strValue, out DateTime dt))
|
||||
{
|
||||
return $"DateTime.Parse(\"{dt:yyyy-MM-dd}\")";
|
||||
}
|
||||
|
||||
// 字符串(兜底)
|
||||
return $"\"{strValue.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将操作符统一为 C# 操作符
|
||||
/// </summary>
|
||||
private static string FormatOperator(string op)
|
||||
{
|
||||
return op?.Trim() switch
|
||||
{
|
||||
"=" or "==" => "==",
|
||||
"!=" or "≠" => "!=",
|
||||
">" => ">",
|
||||
"<" => "<",
|
||||
">=" => ">=",
|
||||
"<=" => "<=",
|
||||
_ => throw new ArgumentException($"不支持的操作符:{op}")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using Qdp.Foundation.Utilities;
|
||||
using System.Reflection;
|
||||
using YLErp.BLL;
|
||||
using YLErp.Model;
|
||||
|
||||
@@ -14,45 +12,50 @@ using YLErp.Model;
|
||||
时点执行可配置规则判断。当前目标不是一次性做成最终版,而是在最小可运行链路
|
||||
跑通后,逐步演进为可由前端配置、后端预编译、执行期直接命中的正式版本。
|
||||
|
||||
【当前实现口径】(以本注释和实际代码为准)
|
||||
本模块旨在将现有限额监控升级为完整的风控引擎,实现规则的灵活配置、公式化执行、
|
||||
策略判定与结果输出。
|
||||
|
||||
【设计文档】
|
||||
详细设计文档路径:
|
||||
X:\尹峰\onederiv\风控引擎需求详细设计.md
|
||||
|
||||
核心概念:
|
||||
- RiskRule(风控规则):单条规则包含完整的公式定义 + 校验策略 + 触发时点 + 适用范围。
|
||||
不需要 "一个 Rule 对应多个 Application" 的复杂关系。
|
||||
- Content:用户输入的规则内容字符串,如"名义本金>1亿"
|
||||
- 规则编译:在规则定义时(创建/加载/缓存刷新时)调用 RuleCompiler.Compile(rule),
|
||||
将 Content 解析并编译为 Func<RiskContext, bool> 委托,写入 rule.CompiledScript。
|
||||
判风控时直接执行委托,零解析开销。
|
||||
- Rule(规则):定义"什么情况下触发",由变量 + 操作符 + 阈值组成的条件列表
|
||||
- Application(应用):定义"何时、对谁、怎么处理",关联规则 + 策略/时点/范围
|
||||
- Variable(变量):变量池中的可选项,用于构建规则条件
|
||||
- RiskContext:一次风控检查的数据上下文(DataMap 机制)
|
||||
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings / TriggeredRules)
|
||||
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings)
|
||||
|
||||
当前编译流程:
|
||||
1. 外部准备好规则对象,在 RuleExpr 中直接写最终可执行表达式
|
||||
2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
|
||||
3. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
|
||||
4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
|
||||
5. 执行阶段优先从 RuleCompiledCache 取委托执行
|
||||
|
||||
当前执行流程(EvaluateRisk):
|
||||
1. 从数据库加载规则列表和应用列表
|
||||
2. 按应用状态、TriggerPoints 过滤有效应用
|
||||
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
|
||||
4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
|
||||
5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
|
||||
6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
|
||||
7. 返回 RiskResult
|
||||
|
||||
与旧设计文档的差异:
|
||||
- 删除 RiskRuleApplication 模型,将策略/时点/范围合并到 RiskRule 中
|
||||
- 删除 FormulaJson 实时解析,改为 Content → 表达式树编译
|
||||
- 规则编译跟随规则生命周期(创建/加载时),判风控时零解析
|
||||
|
||||
【与早期方案的主要差异】
|
||||
- 当前不是 Content 解析或表达式树主导,而是 RuleExpr 直编译主导
|
||||
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
|
||||
- 编译器文件已放入 RiskEngine/Compile 目录下
|
||||
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 RuleExpr 直接表达
|
||||
【架构设计】
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ API 层(RiskRuleController / RiskEngineController) │
|
||||
│ - 规则 CRUD API、应用配置 CRUD API、变量池 CRUD API │
|
||||
│ - 风控执行 API(EvaluateRisk / Trial) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────────┐
|
||||
│ 风控引擎层(YLErpDAL/Modules/RiskEngine/) ← 本文件夹所在层 │
|
||||
│ │
|
||||
│ RiskEngineService(本文件) │
|
||||
│ - 内存缓存管理(规则 + 应用 + 变量 + 编译后脚本) │
|
||||
│ - 构造 RiskContext → 解析 FormulaJson → 执行条件判断 → 输出结果 │
|
||||
│ │
|
||||
│ RiskVariableProvider(待实现) │
|
||||
│ - 接收已编译委托 + RiskContext → 执行计算 → 返回变量值 │
|
||||
│ │
|
||||
│ RiskRuleService(待实现) │
|
||||
│ - 规则 / 应用 / 变量 / 日志的 CRUD │
|
||||
│ - CRUD 后触发 RiskEngineService.RefreshCache() │
|
||||
│ │
|
||||
│ RiskAuditLogService(待实现) │
|
||||
│ - 操作日志写入 / 查询 │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────────┐
|
||||
│ 数据访问层 │
|
||||
│ - RiskRuleRepository(规则 + 应用 + 变量 + 日志) │
|
||||
│ - 复用现有 DAL(Trade / Client / SwapPosition / Credit 等) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
【当前代码结构】
|
||||
风控引擎层:YLErpDAL/Modules/RiskEngine/
|
||||
@@ -72,48 +75,92 @@ using YLErp.Model;
|
||||
【当前进度】(截至 2026-06-24)
|
||||
✅ 已完成:
|
||||
1. 创建 RiskEngine 文件夹,与 RiskModule 平级,实现代码隔离
|
||||
2. 核心模型:
|
||||
- RiskRule.cs:合并模型(公式 + 策略 + 时点 + 范围 + CompiledScript)
|
||||
2. 创建核心模型:
|
||||
- RiskContext.cs:风控上下文(TradeId, TriggerPoint, DataMap)
|
||||
- RiskResult.cs:风控结果(Passed, Blocked, NeedApproval, Warnings, TriggeredRules)
|
||||
- RiskFormula.cs:FormulaJson 反序列化模型(辅助)
|
||||
3. RuleCompiler.cs:
|
||||
- ParseContent:解析 Content 字符串(支持 > < >= <= = != ≠)
|
||||
- ParseValue:解析数值(支持 万/亿/千/百分比)
|
||||
- BuildExpression:表达式树构建(反射取值 + decimal 比较)
|
||||
4. RiskEngineService.EvaluateRisk:
|
||||
- 加载示例规则列表
|
||||
- 筛选 TriggerPoint 匹配的规则
|
||||
- 调用 rule.CompiledScript(context) 执行预编译委托
|
||||
- 按 ControlStrategy 聚合结果(禁止/审批/提示)
|
||||
5. 集成到现有流程:
|
||||
- QuotaMonitorService.QuotaCheck() 末尾已接入风控引擎
|
||||
3. RiskEngineService 骨架:
|
||||
- 继承 YLBaseService,保留三个构造函数
|
||||
- Evaluate():1==1 占位方法,验证服务可实例化
|
||||
- EvaluateRisk(RiskContext, triggerPoint):按设计文档预演的入口方法
|
||||
4. 集成到现有流程:
|
||||
- QuotaMonitorService.QuotaCheck() 末尾已插入风控引擎调用
|
||||
- 触发时点:BOOK_CONFIRM(簿记交易确认)
|
||||
6. 编译验证通过
|
||||
- 构造 RiskContext,传入 trade 对象到 DataMap
|
||||
- 按策略(Blocked/NeedApproval/Warnings)映射回 QuotaTrialStatusEnum
|
||||
5. 编译验证通过(YLErpDAL.csproj 0 error, 80 warnings 为旧代码)
|
||||
|
||||
【待办事项 / TODO】
|
||||
⬜ 1. 接入真实规则来源
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 的规则来源
|
||||
⬜ 2. 增加接口层
|
||||
- 提供前端提交 RuleExpr 后的校验接口
|
||||
- 提供规则保存 / 发布后预编译并写缓存的入口
|
||||
⬜ 3. 启动预热与缓存刷新
|
||||
- 服务启动时批量加载有效规则并预编译
|
||||
- 支持规则更新后的缓存刷新 / 删除
|
||||
⬜ 4. 完善执行期策略
|
||||
- 当前保留“缓存未命中时兜底编译”逻辑
|
||||
- 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
|
||||
⬜ 5. 补测试样例
|
||||
- 数值比较、日期比较、维度过滤、非法公式、边界值
|
||||
⬜ 6. 完善 RuleExpr 配套能力
|
||||
- 提供前端可用的表达式编辑、校验与错误提示
|
||||
- 约束可用变量、类型转换和脚本安全边界
|
||||
【待办事项 / TODO】(按优先级排序)
|
||||
⬜ 1. 内存缓存机制
|
||||
- 添加 _cachedRules / _cachedApplications / _cachedVariables / _compiledScripts
|
||||
- 实现 RefreshCache() 方法,从数据库加载 Active 规则/应用/变量
|
||||
- CRUD 操作后即时刷新(由 RiskRuleService 调用)
|
||||
⬜ 2. 数据库表创建(SQL)
|
||||
- glms_risk_rule(规则定义表)
|
||||
- glms_risk_rule_application(规则应用表)
|
||||
- glms_risk_variable(变量池定义表)
|
||||
- glms_risk_rule_audit_log(操作日志表)
|
||||
⬜ 3. 规则筛选逻辑
|
||||
- 按 TriggerPoint 过滤应用配置
|
||||
- 按 Scope(全局/账户/对手方/标的类型/合约类型)过滤适用范围
|
||||
- 去重(同一规则可能被多个应用引用)
|
||||
⬜ 4. 公式解析与执行
|
||||
- 解析 FormulaJson(条件列表,AND 关系)
|
||||
- 数值型 / 日期型 / 布尔型 的条件判断
|
||||
- 变量取值:通过 RiskVariableProvider 执行已编译委托
|
||||
⬜ 5. RiskVariableProvider 实现
|
||||
- 接收已编译委托 + RiskContext,执行计算并返回变量值
|
||||
- 异常捕获与详细错误信息(如"变量 XXX 取值失败:空引用")
|
||||
⬜ 6. RiskContext 数据加载(按需加载)
|
||||
- trade 对象(必须)
|
||||
- swap_position / client / credit / market / calc / client_marginrate(按需)
|
||||
- 参考 QuotaMonitorService 中现有查询模式(AsNoTracking, DataCacheProvider)
|
||||
⬜ 7. 脚本编译引擎
|
||||
- 将变量 ImplementationScript 编译为可执行委托(Expression.Lambda)
|
||||
- 支持安全表达式求值,杜绝注入风险
|
||||
- 按 (id, version) 缓存编译结果
|
||||
⬜ 8. 更多集成点
|
||||
- 平仓审核提交(CLOSE_REVIEW):SwapTrade2Controller / unwindSwapTrade.js
|
||||
- 上传确认书(UPLOAD_CONFIRMATION):ConfirmationGenerateService
|
||||
- 事件发生时(EVENT_TRIGGER):二期实现
|
||||
⬜ 9. 单元测试
|
||||
- 变量计算器取值逻辑
|
||||
- 公式解析与执行
|
||||
- 策略判定逻辑
|
||||
- 完整风控检查流程(API → 结果返回)
|
||||
⬜ 10. 前端规则配置 SPA(独立前端仓库)
|
||||
- 路由:/risk-config(单页面多 Tab:规则管理 / 规则应用 / 变量池 / 操作日志)
|
||||
⬜ 11. 前端"确认交易"弹窗展示优化(当前混在"风险预警"栏目中,待独立)
|
||||
- 问题:风控引擎返回的 Blocked/NeedApproval/Warnings 目前通过 RiskWarningDetails
|
||||
映射到前端 quotaTrial.cshtml,统一显示在"风险预警"栏目(橙色表头)
|
||||
- 方案A(最小改动):前端判断 RiskWarningDetails 是否包含"[风控引擎]"标签,
|
||||
单独渲染"风控检查"栏目(如紫色表头),与原有"风险预警"并列展示
|
||||
- 方案B(干净做法):QuotaTrial 模型新增 RiskEngineDetails 字段,后端往独立字段写,
|
||||
前端新增独立渲染逻辑,彻底分离"风险预警"(旧限额)和"风控检查"(新引擎)
|
||||
- 相关文件:
|
||||
→ QuotaMonitorService.cs(RiskWarningDetails 赋值逻辑)
|
||||
→ quotaTrial.cshtml(前端渲染,检查类别列"风险预警"写死在前端 HTML)
|
||||
|
||||
【关键文件清单】
|
||||
本模块:
|
||||
- RiskEngineService.cs(本文件):引擎核心服务
|
||||
- RiskContext.cs:风控上下文
|
||||
- RiskResult.cs:风控结果
|
||||
|
||||
现有集成点:
|
||||
- QuotaMonitorService.cs(RiskModule 下):QuotaCheck() 末尾已接入
|
||||
|
||||
设计文档:
|
||||
- X:\尹峰\onederiv\风控引擎需求详细设计.md
|
||||
|
||||
【命名空间】
|
||||
YLErp.Modules.RiskEngine(与 RiskModule 平级)
|
||||
|
||||
【注意事项】
|
||||
1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
2. DataMap 中的值由宿主业务代码准备,编译器与执行器本身不负责查库补数
|
||||
3. 当前缓存的是 Func<RiskContext, bool>,这是规则判断函数,不是事件处理器
|
||||
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
|
||||
1. 本期不对接现有审批模块,规则创建后直接可用(Active)
|
||||
2. 旧 QuotaMonitorService 功能保持不变,新旧引擎独立运行
|
||||
3. 变量新增仅需在变量池管理页面添加记录,无需修改引擎核心代码
|
||||
4. 所有查询使用 AsNoTracking() 避免 EF 变更追踪开销
|
||||
5. 数值统一使用 decimal 类型,避免浮点精度问题
|
||||
================================================================================
|
||||
*/
|
||||
|
||||
@@ -138,55 +185,16 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从内存缓存获取规则列表;缓存为空时兜底加载并填充缓存。
|
||||
/// 执行风控检查(第一版:一句简单的 1==1,先跑通)
|
||||
/// </summary>
|
||||
private List<RiskRule> GetRules()
|
||||
public bool Evaluate()
|
||||
{
|
||||
var rules = _cachedRules;
|
||||
if (rules != null)
|
||||
{
|
||||
return rules;
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (_cachedRules != null)
|
||||
{
|
||||
return _cachedRules;
|
||||
}
|
||||
|
||||
var loaded = LoadRulesFromDb();
|
||||
_cachedRules = loaded;
|
||||
return loaded;
|
||||
}
|
||||
// 第一版占位:脚本 1==1,永远返回 true,验证服务能正常实例化和调用
|
||||
return 1 == 1;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -197,369 +205,22 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
|
||||
// 调试模式:模拟触发审批(NeedApproval),验证前端界面展示
|
||||
// TODO: 后续接入真实规则缓存和公式执行逻辑后删除此行
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.Warnings.Add("【风控引擎】规则触发:需审批 - 模拟审批触发(调试模式)");
|
||||
|
||||
// ============================================================
|
||||
// Step 1: 从内存缓存读取规则定义和规则应用(启动时已预热)
|
||||
// ============================================================
|
||||
var rules = GetRules();
|
||||
var applications = GetApplications();
|
||||
_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?.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: 遍历匹配的应用,通过 RuleIds 关联规则并执行预编译委托
|
||||
// ============================================================
|
||||
foreach (var application in matchedApplications)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 3.1 通过 RuleIds 关联规则定义
|
||||
// ------------------------------------------------------------
|
||||
var applicationRuleIds = ParseRuleIds(application.RuleIds);
|
||||
if (!applicationRuleIds.Any())
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未编译,执行编译 - RuleCode: {rule.RuleCode}");
|
||||
RuleCompiler.Compile(rule);
|
||||
}
|
||||
|
||||
// 执行预编译委托
|
||||
bool triggered = false;
|
||||
try
|
||||
{
|
||||
triggered = rule.CompiledScript(context);
|
||||
_logger.Info($"[风控引擎] 规则执行 - RuleCode: {rule.RuleCode}, Triggered: {triggered}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则执行异常 - RuleCode: {rule.RuleCode}, Error: {ex.Message}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.RuleCode}]执行异常:{ex.Message};";
|
||||
continue;
|
||||
}
|
||||
|
||||
var applicationRules = rules
|
||||
.Where(r => applicationRuleIds.Contains(r.Id) && r.Id == 3)
|
||||
.ToList();
|
||||
|
||||
var missingRuleIds = applicationRuleIds
|
||||
.Where(ruleId => applicationRules.All(r => r.Id != ruleId))
|
||||
.ToList();
|
||||
|
||||
foreach (var missingRuleId in missingRuleIds)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {rule.ControlStrategy}");
|
||||
|
||||
foreach (var rule in applicationRules)
|
||||
{
|
||||
var ruleId = rule.Id.ToString();
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
|
||||
{
|
||||
case 1: // 禁止
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleCode = rule.RuleCode,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "禁止",
|
||||
FormulaText = rule.FormulaText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
});
|
||||
break;
|
||||
|
||||
case 2: // 审批
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleCode = rule.RuleCode,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "审批",
|
||||
FormulaText = rule.FormulaText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
});
|
||||
break;
|
||||
|
||||
case 3: // 提示
|
||||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.FormulaText}");
|
||||
break;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.3 执行预编译委托
|
||||
// ------------------------------------------------------------
|
||||
bool triggered = false;
|
||||
try
|
||||
{
|
||||
triggered = compiledScript(context);
|
||||
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
switch (application.ControlStrategy)
|
||||
{
|
||||
case RiskControlStrategy.Block:
|
||||
break;
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = rule.Id.ToString(),
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Block,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
});
|
||||
break;
|
||||
|
||||
case RiskControlStrategy.Approval:
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = ruleId,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Approval,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
});
|
||||
break;
|
||||
|
||||
case RiskControlStrategy.ShowTip:
|
||||
result.ShowTip = true;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = ruleId,
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.ShowTip,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:提示"
|
||||
});
|
||||
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}");
|
||||
_logger.Info($"[风控引擎] EvaluateRisk 被调用 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}, NeedApproval: {result.NeedApproval}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleId = "ENGINE_ERROR",
|
||||
RuleName = "风控引擎执行异常",
|
||||
ControlStrategy = "审批",
|
||||
FormulaText = ex.Message,
|
||||
Message = $"风控引擎异常:{ex.Message},需审批通过"
|
||||
});
|
||||
result.HasError = true;
|
||||
result.ErrorMessage = $"风控引擎执行异常: {ex.Message}";
|
||||
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库加载规则列表
|
||||
/// </summary>
|
||||
private List<RiskRule> LoadRulesFromDb()
|
||||
{
|
||||
// 示例规则:多条件逗号分隔(自动转为 AND)
|
||||
var rule = new RiskRule
|
||||
{
|
||||
Id = 103,
|
||||
RuleCode = "RISK-20260622-0003",
|
||||
RuleName = "测试规则:名义本金超过1亿且保证金比例超过50%禁止",
|
||||
FormulaText = "名义本金>1亿, 保证金比例>50%", // 逗号分隔,解析时自动转为 AND
|
||||
FormulaJson = "{ \"conditions\": [{\"variableCode\":\"trade.StockEqvNotional\",\"operator\":\">\",\"value\":100000000},{\"variableCode\":\"trade.MarginRate\",\"operator\":\">\",\"value\":0.5}] }",
|
||||
Status = 1, // Active
|
||||
ControlStrategy = 1, // 禁止
|
||||
TriggerPoints = "BOOK_CONFIRM",
|
||||
ScopeIsGlobal = true,
|
||||
Version = 1
|
||||
};
|
||||
RuleCompiler.Compile(rule);
|
||||
|
||||
/// <summary>
|
||||
/// 判断应用配置是否命中当前交易。
|
||||
/// 匹配规则遵循设计文档:
|
||||
/// 1. 全局命中时直接返回 true;
|
||||
/// 2. 同一维度内多选按并集处理;
|
||||
/// 3. 不同维度之间按交集处理;
|
||||
/// 4. 某维度留空表示该维度不限制。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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。
|
||||
/// 当前先按 BookId 取值;如果后续真实字段不是 BookId,再统一调整这里即可。
|
||||
/// </summary>
|
||||
private object GetTradeAssetBookId(YLErp.DBModels.trade trade)
|
||||
{
|
||||
var property = trade.GetType().GetProperty("BookId");
|
||||
return property?.GetValue(trade);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取交易的标的类型。
|
||||
/// 当前先按 UnderlyingInstrumentType 取值;如果后续真实字段名不同,再统一调整这里即可。
|
||||
/// </summary>
|
||||
private object GetTradeUnderlyingType(YLErp.DBModels.trade trade)
|
||||
{
|
||||
var property = trade.GetType().GetProperty("UnderlyingInstrumentType");
|
||||
return property?.GetValue(trade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 风控规则定义。
|
||||
/// 只描述“规则是什么”,不包含状态、策略、触发时点、适用范围等应用配置。
|
||||
/// 这些执行维度由 RiskRuleApplication 承载。
|
||||
/// </summary>
|
||||
public class RiskRule
|
||||
{
|
||||
// === 基础信息 ===
|
||||
public int Id { get; set; }
|
||||
public string RuleCode { get; set; }
|
||||
public string RuleName { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 【核心】用户输入的规则内容字符串,如"名义本金>1亿"
|
||||
/// 在规则创建/加载时由 RuleCompiler 解析并编译为 CompiledScript
|
||||
/// </summary>
|
||||
public string FormulaText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 【核心】公式 JSON(条件列表的结构化存储),如
|
||||
/// {"conditions":[{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}]}
|
||||
/// 由 RuleCompiler 解析并编译为 CompiledScript
|
||||
/// </summary>
|
||||
public string FormulaJson { get; set; }
|
||||
|
||||
// === 生命周期 ===
|
||||
public int Version { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
// === 审计字段 ===
|
||||
public int OptId { get; set; }
|
||||
public string OptName { get; set; }
|
||||
public DateTime OptDate { get; set; }
|
||||
public int UpdateOptId { get; set; }
|
||||
public string UpdateOptName { get; set; }
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
// === 运行时内存字段(不持久化到数据库)===
|
||||
/// <summary>
|
||||
/// 【核心】预编译的可执行委托
|
||||
/// 在规则创建/加载时由 RuleCompiler.Compile(this) 生成
|
||||
/// 判风控时直接调用:rule.CompiledScript(context) → bool
|
||||
/// </summary>
|
||||
public Func<RiskContext, bool> CompiledScript { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,6 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="FluentFTP" Version="45.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.12.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
||||
<PackageReference Include="Oracle.EntityFrameworkCore" Version="6.21.90" />
|
||||
@@ -42,7 +40,7 @@
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.0" />
|
||||
<PackageReference Include="Qdp.Pricing.Library.Options" Version="1.0.5" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="YLErp.Office" Version="1.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user