merge and fix conflict

This commit is contained in:
尹峰
2026-06-23 16:06:01 +08:00
4 changed files with 566 additions and 102 deletions
+227 -101
View File
@@ -1,4 +1,6 @@
using Newtonsoft.Json;
using Qdp.Foundation.Utilities;
using System.Reflection;
using YLErp.BLL;
using YLErp.Model;
@@ -15,137 +17,143 @@ using YLErp.Model;
本模块旨在将现有限额监控升级为完整的风控引擎,实现规则的灵活配置、公式化执行、
策略判定与结果输出。
设计文档】
详细设计文档路径:
X:\尹峰\onederiv\风控引擎需求详细设计.md
最终设计】(以本注释为准,与设计文档不同的部分以本注释为准)
核心概念:
- Rule(规则):定义"什么情况下触发",由变量 + 操作符 + 阈值组成的条件列表
- Application(应用):定义"何时、对谁、怎么处理",关联规则 + 策略/时点/范围
- Variable(变量):变量池中的可选项,用于构建规则条件
- RiskRule风控规则):单条规则包含完整的公式定义 + 校验策略 + 触发时点 + 适用范围。
不需要 "一个 Rule 对应多个 Application" 的复杂关系。
- Content:用户输入的规则内容字符串,如"名义本金>1亿"
- 规则编译:在规则定义时(创建/加载/缓存刷新时)调用 RuleCompiler.Compile(rule)
将 Content 解析并编译为 Func<RiskContext, bool> 委托,写入 rule.CompiledScript。
判风控时直接执行委托,零解析开销。
- RiskContext:一次风控检查的数据上下文(DataMap 机制)
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings / TriggeredRules
编译流程:
1. 解析 Content:提取变量名、条件符、阈值
2. 变量名映射:用户输入的"名义本金" → "trade.StockEqvNotional"
3. 构建表达式树:Func<RiskContext, bool> = ctx => ...
4. 编译为委托:rule.CompiledScript = lambda.Compile()
5. 判风控时直接调用:rule.CompiledScript(context) → bool
执行流程(EvaluateRisk):
1. 从缓存/数据库加载 Active 状态的 RiskRule 列表
2. 筛选 TriggerPoints 包含当前 triggerPoint 的规则
3. 筛选 Scope 匹配当前交易上下文的规则(全局/账户/对手方/标的类型/合约类型)
4. 对每条规则:执行 rule.CompiledScript(context) 判断是否触发
5. 按 ControlStrategy(禁止/审批/提示)聚合结果
6. 返回 RiskResult
与旧设计文档的差异:
- 删除 RiskRuleApplication 模型,将策略/时点/范围合并到 RiskRule 中
- 删除 FormulaJson 实时解析,改为 Content → 表达式树编译
- 规则编译跟随规则生命周期(创建/加载时),判风控时零解析
【架构设计】
┌─────────────────────────────────────────────────────────────────┐
│ API 层(RiskRuleController / RiskEngineController
│ - 规则 CRUD API、应用配置 CRUD API、变量池 CRUD API
│ - 风控执行 APIEvaluateRisk / Trial
│ - 规则 CRUD API、风控执行 APIEvaluateRisk / Trial
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────┐
│ 风控引擎层(YLErpDAL/Modules/RiskEngine/ ← 本文件夹所在层 │
│ │
│ RiskEngineService(本文件) │
│ - 内存缓存管理(规则 + 应用 + 变量 + 编译后脚本)
│ - 构造 RiskContext → 解析 FormulaJson → 执行条件判断 → 输出结果
│ - 内存缓存管理(规则列表)
│ - EvaluateRisk:筛选规则 → 执行预编译委托 → 聚合结果
│ │
│ RiskVariableProvider(待实现)
│ - 接收已编译委托 + RiskContext → 执行计算 → 返回变量值
│ RuleCompiler.cs
│ - ParseContent:解析 Content 字符串 → 变量 + 条件符 + 阈值
│ - BuildExpression:构建表达式树(反射取值 + decimal 比较) │
│ - Compile(RiskRule):编译为 Func<RiskContext, bool> 委托 │
│ │
│ RiskRuleService(待实现)
│ - 规则 / 应用 / 变量 / 日志的 CRUD
│ - CRUD 后触发 RiskEngineService.RefreshCache()
│ RiskRule.cs
│ - 合并模型:公式定义 + 校验策略 + 触发时点 + 适用范围
│ - CompiledScript:预编译的可执行委托(运行时内存字段)
│ │
│ RiskAuditLogService(待实现)
│ - 操作日志写入 / 查询
│ RiskContext.cs / RiskResult.cs / RiskFormula.cs
│ - 数据上下文 / 结果模型 / 公式模型(辅助)
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────┐
│ 数据访问层 │
│ - RiskRuleRepository(规则 + 应用 + 变量 + 日志)
│ - RiskRuleRepository(规则表)
│ - 复用现有 DALTrade / Client / SwapPosition / Credit 等) │
└─────────────────────────────────────────────────────────────────┘
【当前进度】(截至 2026-06-22
✅ 已完成:
1. 创建 RiskEngine 文件夹,与 RiskModule 平级,实现代码隔离
2. 创建核心模型:
2. 核心模型:
- RiskRule.cs:合并模型(公式 + 策略 + 时点 + 范围 + CompiledScript
- RiskContext.cs:风控上下文(TradeId, TriggerPoint, DataMap
- RiskResult.cs:风控结果(Passed, Blocked, NeedApproval, Warnings, TriggeredRules
3. RiskEngineService 骨架:
- 继承 YLBaseService,保留三个构造函数
- Evaluate():1==1 占位方法,验证服务可实例化
- EvaluateRisk(RiskContext, triggerPoint):按设计文档预演的入口方法
4. 集成到现有流程:
- QuotaMonitorService.QuotaCheck() 末尾已插入风控引擎调用
- RiskFormula.csFormulaJson 反序列化模型(辅助)
3. RuleCompiler.cs
- ParseContent:解析 Content 字符串(支持 > < >= <= = != ≠)
- ParseValue:解析数值(支持 万/亿/千/百分比)
- BuildExpression:表达式树构建(反射取值 + decimal 比较)
4. RiskEngineService.EvaluateRisk
- 加载示例规则列表
- 筛选 TriggerPoint 匹配的规则
- 调用 rule.CompiledScript(context) 执行预编译委托
- 按 ControlStrategy 聚合结果(禁止/审批/提示)
5. 集成到现有流程:
- QuotaMonitorService.QuotaCheck() 末尾已接入风控引擎
- 触发时点:BOOK_CONFIRM(簿记交易确认)
- 构造 RiskContext,传入 trade 对象到 DataMap
- 按策略(Blocked/NeedApproval/Warnings)映射回 QuotaTrialStatusEnum
5. 编译验证通过(YLErpDAL.csproj 0 error, 80 warnings 为旧代码)
6. 编译验证通过
【待办事项 / TODO】(按优先级排序)
⬜ 1. 内存缓存机制
- 添加 _cachedRules / _cachedApplications / _cachedVariables / _compiledScripts
- 实现 RefreshCache() 方法,从数据库加载 Active 规则/应用/变量
- CRUD 操作后即时刷新(由 RiskRuleService 调用
- _cachedRules: Dictionary<(int id, int version), RiskRule>
- RefreshCache()从数据库加载 Active 规则 + 逐个 Compile()
- 线程安全(lock / Interlocked.Exchange
⬜ 2. 数据库表创建(SQL
- glms_risk_rule规则定义表
- glms_risk_rule_application(规则应用表)
- glms_risk_variable(变量池定义表)
- glms_risk_rule_audit_log(操作日志表)
- glms_risk_rule合并模型:公式 + 策略 + 时点 + 范围
⬜ 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.csRiskWarningDetails 赋值逻辑)
→ quotaTrial.cshtml(前端渲染,检查类别列"风险预警"写死在前端 HTML
- 去重
⬜ 4. 更多变量映射(VariableNameMap
- 行情类:market.LastClosePrice, market.FR007 等
- 系统计算类:calc.CounterpartyTotalNotional, calc.CreditUsageRate 等
- 布尔判断类:calc.IsLongDirection, calc.IsMarginPay 等
⬜ 5. 多条件支持(AND / OR
- Content 解析:支持 "名义本金>1亿 且 保证金比例>50%"
- 表达式树构建:多个条件合并为单一委托
⬜ 6. 更多集成点
- 平仓审核提交(CLOSE_REVIEW
- 上传确认书(UPLOAD_CONFIRMATION
- 事件发生时(EVENT_TRIGGER
⬜ 7. 前端"确认交易"弹窗展示优化(当前混在"风险预警"栏目中)
- 方案:QuotaTrial 模型新增 RiskEngineDetails 字段,前端独立渲染"风控检查"栏目
⬜ 8. 单元测试
⬜ 9. 前端规则配置 SPA(独立前端仓库)
10. 【暂不实现】考虑迁移到 CSScriptLib
- 原因:当前已采用 RoslynMicrosoft.CodeAnalysis.CSharp.Scripting),无需额外包
- 灵活性:Roslyn 已足够支持多条件 AND/OR、复杂表达式,CSScriptLib 并无明显优势
- 优先级:低,当前版本暂不引入
【关键文件清单】
本模块:
- RiskEngineService.cs(本文件):引擎核心服务
- RiskContext.cs:风控上下文
- RiskResult.cs风控结果
- RiskEngineService.cs(本文件):引擎核心服务EvaluateRisk 入口
- RuleCompiler.cs:规则编译器,Content → 表达式树 → 委托
- RiskRule.cs合并后的规则模型(含 CompiledScript
- RiskContext.cs / RiskResult.cs / RiskFormula.cs
现有集成点:
- QuotaMonitorService.csRiskModule 下):QuotaCheck() 末尾已接入
设计文档:
- X:\尹峰\onederiv\风控引擎需求详细设计.md
- X:\尹峰\onederiv\风控引擎需求详细设计.md(旧设计,仅供参考)
【命名空间】
YLErp.Modules.RiskEngine(与 RiskModule 平级)
【注意事项】
1. 本期不对接现有审批模块,规则创建后直接可用(Active)
2. 旧 QuotaMonitorService 功能保持不变,新旧引擎独立运行
3. 变量新增仅需在变量池管理页面添加记录,无需修改引擎核心代码
4. 所有查询使用 AsNoTracking() 避免 EF 变更追踪开销
5. 数值统一使用 decimal 类型,避免浮点精度问题
1. 规则编译跟随规则生命周期(创建/加载时),判风控时零解析开销
2. 数值统一使用 decimal 类型,避免浮点精度问题
3. 反射取值使用 BindingFlags.IgnoreCase,兼容大小写不敏感的属性名
4. 异常不阻断原有流程,只记录日志并返回 HasError=true
================================================================================
*/
@@ -185,16 +193,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 执行风控检查(第一版:一句简单的 1==1,先跑通
/// </summary>
public bool Evaluate()
{
// 第一版占位:脚本 1==1,永远返回 true,验证服务能正常实例化和调用
return 1 == 1;
}
/// <summary>
/// 执行风控检查(按设计文档预演版)
/// 执行风控检查(最终设计版:预编译委托 + 规则筛选 + 策略判定
/// </summary>
/// <param name="context">风控上下文</param>
/// <param name="triggerPoint">触发时点,如 BOOK_CONFIRM</param>
@@ -205,22 +204,149 @@ namespace YLErp.Modules.RiskEngine
try
{
// 调试模式:模拟触发审批(NeedApproval),验证前端界面展示
// TODO: 后续接入真实规则缓存和公式执行逻辑后删除此行
result.NeedApproval = true;
result.Passed = false;
result.Warnings.Add("【风控引擎】规则触发:需审批 - 模拟审批触发(调试模式)");
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
_logger.Info($"[风控引擎] EvaluateRisk 被调用 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}, NeedApproval: {result.NeedApproval}");
// ============================================================
// Step 1: 加载规则列表(TODO: 后续接入真实内存缓存)
// ============================================================
var rules = LoadRules();
_logger.Info($"[风控引擎] 加载规则数: {rules.Count}");
// ============================================================
// Step 2: 筛选匹配当前 triggerPoint 且状态为 Active 的规则
// ============================================================
var matchedRules = rules
.Where(r => r.Status == 1) // Active
.Where(r => !string.IsNullOrEmpty(r.TriggerPoints))
.Where(r => r.TriggerPoints.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Contains(triggerPoint))
.ToList();
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的规则数: {matchedRules.Count}");
// ============================================================
// Step 3: 遍历匹配的规则,执行预编译委托
// ============================================================
foreach (var rule in matchedRules)
{
// 确保规则已编译
if (rule.CompiledScript == null)
{
_logger.Info($"[风控引擎] 规则未编译,执行编译 - 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;
}
if (triggered)
{
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {rule.ControlStrategy}");
// 按 ControlStrategy 生成结果
switch (rule.ControlStrategy)
{
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;
default:
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {rule.ControlStrategy}");
break;
}
}
}
// ============================================================
// Step 4: 聚合最终结果
// ============================================================
if (!result.Blocked && !result.NeedApproval && result.Warnings.Count == 0 && !result.HasError)
{
result.Passed = true;
}
_logger.Info($"[风控引擎] EvaluateRisk 完成 - TradeId: {context?.TradeId}, Passed: {result.Passed}, Blocked: {result.Blocked}, NeedApproval: {result.NeedApproval}, TriggeredRules: {result.TriggeredRules.Count}");
}
catch (Exception ex)
{
result.HasError = true;
result.ErrorMessage = $"风控引擎执行异常: {ex.Message}";
result.NeedApproval = true;
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleCode = "ENGINE_ERROR",
RuleName = "风控引擎执行异常",
ControlStrategy = "审批",
FormulaText = ex.Message,
Message = $"风控引擎异常:{ex.Message},需审批通过"
});
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
}
return result;
}
/// <summary>
/// 加载规则列表(TODO: 后续接入真实内存缓存和数据库查询)
/// 当前返回示例规则用于验证流程
/// </summary>
private List<RiskRule> LoadRules()
{
// 示例规则:多条件逗号分隔(自动转为 AND)
var rule = new RiskRule
{
Id = 103,
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);
return new List<RiskRule> { rule };
}
}
}
+80
View File
@@ -0,0 +1,80 @@
using System;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控规则(合并模型:规则定义 + 应用配置合一)
///
/// 最终设计说明:
/// 业务上不需要 "一个 Rule 对应多个 Application" 的复杂关系。
/// 每条规则自身包含:公式定义 + 校验策略 + 触发时点 + 适用范围。
/// 规则创建时即完成脚本编译,判风控时直接执行已编译委托(零解析开销)。
/// </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; }
// === 校验策略(原 Application.ControlStrategy===
/// <summary>
/// 校验策略:1=禁止, 2=审批, 3=提示
/// </summary>
public int ControlStrategy { get; set; }
// === 触发时点(原 Application.TriggerPoints===
/// <summary>
/// 触发时点,逗号分隔(如 BOOK_CONFIRM,CLOSE_REVIEW
/// </summary>
public string TriggerPoints { get; set; }
// === 适用范围(原 Application.Scope*===
/// <summary>
/// 是否全局适用(true=所有交易适用,忽略其他 Scope 字段)
/// </summary>
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; }
// === 生命周期 ===
/// <summary>
/// 状态:1=已生效(Active), 2=已停用(Disabled), 3=已删除(Deleted)
/// </summary>
public int Status { 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; }
}
}
+256
View File
@@ -0,0 +1,256 @@
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&lt;RiskContext, bool&gt;(内部通过 Roslyn 编译缓存)
/// </summary>
public static class RuleCompiler
{
/// <summary>
/// 变量编码 → 类型全名的映射
/// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"])
/// </summary>
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");
}
foreach (var cond in conditions)
{
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));
}
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);
// 生成单条件表达式
// Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
string expr = $"Convert.ToDecimal((({typeFullName})DataMap[\"{prefix}\"]).{propertyName}) {csharpOp} {valueLiteral}";
exprParts.Add(expr);
}
// 多条件用 && 连接
return string.Join(" && ", exprParts);
}
/// <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()));
throw new InvalidOperationException($"脚本编译失败:{errorMsg}\n脚本代码:{scriptCode}");
}
// 生成可调用委托
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)
{
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
// TODO: 记录日志
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}")
};
}
}
}
+3 -1
View File
@@ -30,6 +30,8 @@
<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" />
@@ -40,7 +42,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="6.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="YLErp.Office" Version="1.1.2" />
</ItemGroup>