fix conflict
This commit is contained in:
@@ -366,7 +366,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly,
|
||||
typeof(Queryable).Assembly
|
||||
)
|
||||
.WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule");
|
||||
.WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule", "YLErp.Modules.RiskEngine");
|
||||
|
||||
// 创建脚本(尚未执行,仅编译)
|
||||
var script = CSharpScript.Create<T>(scriptCode, options, globalsType: typeof(ScriptGlobals));
|
||||
|
||||
@@ -174,7 +174,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
RuleCompiledCache.Remove(rule.Id.ToString());
|
||||
try
|
||||
{
|
||||
RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
|
||||
// 结构化条件在加载阶段已解析到 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)
|
||||
@@ -214,6 +216,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
_cachedRules = null;
|
||||
_cachedApplications = null;
|
||||
RuleCompiledCache.Clear();
|
||||
StructuredRuleExecutor.ClearVariableCompiledCache();
|
||||
}
|
||||
|
||||
Preload();
|
||||
@@ -250,7 +253,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
RuleCompiledCache.Remove(ruleId.ToString());
|
||||
try
|
||||
{
|
||||
RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
|
||||
// 单规则刷新时同步刷新结构化条件缓存,避免后续执行继续解析旧 JSON 或重复反序列化。
|
||||
rule.ParsedConditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
|
||||
_logger.Info($"[风控引擎] RefreshOneRuleCache 结构化规则校验成功,跳过 RuleExpr 编译 - RuleId: {ruleId}");
|
||||
return RuleCompileResult.Ok(null);
|
||||
}
|
||||
@@ -353,6 +357,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
// ============================================================
|
||||
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}");
|
||||
|
||||
@@ -402,24 +408,23 @@ namespace YLErp.Modules.RiskEngine
|
||||
continue;
|
||||
}
|
||||
|
||||
var applicationRules = rules
|
||||
.Where(r => applicationRuleIds.Contains(r.Id) && r.Status == RiskRuleStatus.Active)
|
||||
.ToList();
|
||||
|
||||
// 区分"规则不存在"与"规则非活跃"两种情况,分别记录日志
|
||||
var ruleDict = rules.Where(r => applicationRuleIds.Contains(r.Id))
|
||||
.ToDictionary(r => r.Id);
|
||||
|
||||
var applicationRules = new List<RiskRule>();
|
||||
foreach (var ruleId in applicationRuleIds)
|
||||
{
|
||||
if (!ruleDict.TryGetValue(ruleId, out var ruleDef))
|
||||
// 通过本次执行预构建的规则字典按 ID 查找,避免每个应用都 Where 扫描全量规则列表。
|
||||
if (!ruleMap.TryGetValue(ruleId, out var ruleDef))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {ruleId}, ApplicationRuleIds: {application.RuleIds}");
|
||||
continue;
|
||||
}
|
||||
else if (ruleDef.Status != RiskRuleStatus.Active)
|
||||
|
||||
if (ruleDef.Status != RiskRuleStatus.Active)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则非活跃,跳过执行 - RuleId: {ruleId}, Status: {ruleDef.Status}, ApplicationRuleIds: {application.RuleIds}");
|
||||
continue;
|
||||
}
|
||||
|
||||
applicationRules.Add(ruleDef);
|
||||
}
|
||||
|
||||
foreach (var rule in applicationRules)
|
||||
@@ -427,12 +432,17 @@ namespace YLErp.Modules.RiskEngine
|
||||
var ruleId = rule.Id.ToString();
|
||||
|
||||
bool triggered = false;
|
||||
StructuredRuleExecuteResult executeResult = null;
|
||||
if (!string.IsNullOrWhiteSpace(rule.ConditionJson))
|
||||
{
|
||||
// 结构化规则优先使用 ConditionJson 执行,避免继续把条件整体拼成 Roslyn bool 公式。
|
||||
try
|
||||
{
|
||||
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(rule.ConditionJson);
|
||||
// 结构化条件在规则加载/刷新时已解析到内存缓存,运行时直接复用,避免每次执行反序列化 ConditionJson。
|
||||
var conditions = rule.ParsedConditions;
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
throw new InvalidOperationException("结构化规则条件缓存为空");
|
||||
|
||||
var referencedVariableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions);
|
||||
var missingVariableIds = referencedVariableIds
|
||||
.Where(id => !variables.ContainsKey(id))
|
||||
@@ -452,7 +462,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
.Where(variables.ContainsKey)
|
||||
.ToDictionary(id => id, id => variables[id]);
|
||||
|
||||
var executeResult = StructuredRuleExecutor.Execute(conditions, ruleVariables, context);
|
||||
executeResult = StructuredRuleExecutor.Execute(conditions, ruleVariables, context);
|
||||
if (!executeResult.Success)
|
||||
{
|
||||
var errorMessage = $"规则[{rule.RuleName}]执行异常:{executeResult.ErrorMessage}";
|
||||
@@ -516,6 +526,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (triggered)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
|
||||
// 结构化规则执行器会返回实际值、阈值和变量计算明细;纯文本规则没有该信息时保持原提示。
|
||||
var triggerMessageDetail = executeResult?.Message;
|
||||
|
||||
switch (application.ControlStrategy)
|
||||
{
|
||||
@@ -528,7 +540,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Block,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:禁止", triggerMessageDetail)
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -541,7 +553,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.Approval,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:需审批", triggerMessageDetail)
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -553,7 +565,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = RiskControlStrategy.ShowTip,
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:提示"
|
||||
Message = BuildTriggeredMessage($"规则[{rule.RuleName}]触发:提示", triggerMessageDetail)
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -593,6 +605,13 @@ namespace YLErp.Modules.RiskEngine
|
||||
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, string ruleName, string ruleText, string errorMessage)
|
||||
{
|
||||
result.Blocked = true;
|
||||
@@ -635,7 +654,22 @@ namespace YLErp.Modules.RiskEngine
|
||||
UpdateOptId = r.UpdateOptId ?? 0,
|
||||
UpdateOptName = r.UpdateOptName,
|
||||
UpdateDate = r.UpdateDate ?? r.OptDate ?? DateTime.MinValue
|
||||
}).ToList() ;
|
||||
}).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;
|
||||
//#region 测试本地规则
|
||||
//rules.Add(new RiskRule
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YLErp.Modules.RiskEngine.Dto;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
@@ -47,5 +49,11 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 判风控时直接调用:rule.CompiledScript(context) → bool
|
||||
/// </summary>
|
||||
public Func<RiskContext, bool> CompiledScript { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结构化规则已解析条件缓存。
|
||||
/// 规则加载/刷新时由 ConditionJson 解析一次,运行时直接复用,避免每次风控执行反序列化 JSON。
|
||||
/// </summary>
|
||||
public IReadOnlyList<RuleCondition> ParsedConditions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,12 +355,15 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 ConditionJson 结构和引用变量是否存在。
|
||||
/// 校验 ConditionJson 结构、引用变量以及操作符/阈值/类型一致性。
|
||||
/// 结构化规则运行时以 ConditionJson 为准,RuleExpr 只作为兼容/展示字段。
|
||||
/// </summary>
|
||||
private void ValidateConditionJson(string conditionJson)
|
||||
{
|
||||
ValidateConditionJsonVariables(conditionJson);
|
||||
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(conditionJson);
|
||||
var variables = ValidateConditionJsonVariables(conditions);
|
||||
var variableMap = variables.ToDictionary(variable => (long)variable.id);
|
||||
RuleConditionExpressionBuilder.Build(conditions, variableMap);
|
||||
}
|
||||
/// <summary>
|
||||
/// 校验 RuleExpr(自由文本模式:括号匹配)
|
||||
@@ -1930,9 +1933,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// <summary>
|
||||
/// 校验结构化 ConditionJson 引用的变量是否存在,并返回未删除的变量定义。
|
||||
/// </summary>
|
||||
private List<glms_risk_variable> ValidateConditionJsonVariables(string conditionJson)
|
||||
private List<glms_risk_variable> ValidateConditionJsonVariables(IReadOnlyList<RuleCondition> conditions)
|
||||
{
|
||||
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(conditionJson);
|
||||
var variableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToList();
|
||||
var variables = DbContext.glms_risk_variable
|
||||
.Where(variable => variableIds.Contains(variable.id) && variable.Status != RiskRuleStatus.Deleted)
|
||||
@@ -2178,6 +2180,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
InvalidateVariableCache();
|
||||
StructuredRuleExecutor.RemoveVariableCompiledCache(variableId);
|
||||
foreach (var rule in affectedRules)
|
||||
{
|
||||
RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id);
|
||||
@@ -2188,6 +2191,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
InvalidateVariableCache();
|
||||
StructuredRuleExecutor.RemoveVariableCompiledCache(variableId);
|
||||
}
|
||||
|
||||
return GetVariableDetail(variableId);
|
||||
@@ -2214,6 +2218,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
|
||||
InvalidateVariableCache();
|
||||
StructuredRuleExecutor.RemoveVariableCompiledCache(variableId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2264,8 +2269,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
// 只有实际删除项写审计并参与一次提交;缺失和引用阻塞项不影响其他变量。
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
TryRefreshCache(InvalidateVariableCache, "风控变量缓存",
|
||||
items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id));
|
||||
var changedIds = items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id).ToList();
|
||||
TryRefreshCache(() =>
|
||||
{
|
||||
InvalidateVariableCache();
|
||||
StructuredRuleExecutor.RemoveVariableCompiledCache(changedIds);
|
||||
}, "风控变量缓存", changedIds);
|
||||
}
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量删除变量", result);
|
||||
|
||||
@@ -16,18 +16,76 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public static class StructuredRuleExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// 变量表达式编译缓存。Key 包含变量 ID、版本、更新时间和表达式哈希,避免变量变更后复用旧委托。
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, Func<RiskContext, object>> VariableCompiledCache = new ConcurrentDictionary<string, Func<RiskContext, object>>();
|
||||
|
||||
/// <summary>
|
||||
/// 按变量 ID 增量移除变量表达式编译缓存。
|
||||
/// </summary>
|
||||
public static void RemoveVariableCompiledCache(long variableId)
|
||||
{
|
||||
var keyPrefix = variableId + ":";
|
||||
// 缓存 Key 以变量 ID 开头,变量修改/删除时只清理该变量相关的编译结果。
|
||||
foreach (var key in VariableCompiledCache.Keys.Where(k => k.StartsWith(keyPrefix, StringComparison.Ordinal)))
|
||||
{
|
||||
VariableCompiledCache.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按变量 ID 集合批量移除变量表达式编译缓存。
|
||||
/// </summary>
|
||||
public static void RemoveVariableCompiledCache(IEnumerable<long> variableIds)
|
||||
{
|
||||
if (variableIds == null)
|
||||
return;
|
||||
|
||||
var idSet = new HashSet<long>(variableIds.Distinct());
|
||||
if (idSet.Count == 0)
|
||||
return;
|
||||
|
||||
// 批量移除时只扫描一次缓存 Key,避免变量数较多时重复遍历整个缓存。
|
||||
foreach (var key in VariableCompiledCache.Keys)
|
||||
{
|
||||
var separatorIndex = key.IndexOf(':');
|
||||
if (separatorIndex <= 0)
|
||||
continue;
|
||||
if (long.TryParse(key.Substring(0, separatorIndex), NumberStyles.None, CultureInfo.InvariantCulture, out var variableId) && idSet.Contains(variableId))
|
||||
{
|
||||
VariableCompiledCache.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空全部变量表达式编译缓存。仅用于风控缓存全量刷新场景。
|
||||
/// </summary>
|
||||
public static void ClearVariableCompiledCache()
|
||||
{
|
||||
VariableCompiledCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数值和日期类型支持的比较操作符。
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> ComparableOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 布尔类型支持的判断操作符。
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> BooleanOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"isTrue", "isFalse"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 执行一组结构化条件。当前规则条件之间按 AND 关系处理,任一条件不满足则整条规则不触发。
|
||||
/// </summary>
|
||||
public static StructuredRuleExecuteResult Execute(
|
||||
IReadOnlyList<RuleCondition> conditions,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
@@ -40,16 +98,21 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (variables == null)
|
||||
return StructuredRuleExecuteResult.Fail("变量列表不能为空");
|
||||
|
||||
var triggerMessages = new List<string>();
|
||||
for (var i = 0; i < conditions.Count; i++)
|
||||
{
|
||||
var conditionResult = ExecuteCondition(conditions[i], variables, context, i + 1);
|
||||
if (!conditionResult.Success)
|
||||
return conditionResult;
|
||||
// 结构化规则当前按 AND 执行,任一条件不命中即可短路返回,避免后续变量表达式继续查库。
|
||||
if (!conditionResult.Triggered)
|
||||
return StructuredRuleExecuteResult.Ok(false);
|
||||
if (!string.IsNullOrWhiteSpace(conditionResult.Message))
|
||||
triggerMessages.Add(conditionResult.Message);
|
||||
}
|
||||
|
||||
return StructuredRuleExecuteResult.Ok(true);
|
||||
// 所有条件都命中时,合并每个条件的实际值/阈值说明,供最终风控结果展示。
|
||||
return StructuredRuleExecuteResult.Ok(true, string.Join(";", triggerMessages));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -57,6 +120,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行单个结构化条件,负责按变量数据类型分派到布尔、普通比较或区间比较逻辑。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult ExecuteCondition(
|
||||
RuleCondition condition,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
@@ -69,6 +135,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (!variables.TryGetValue(condition.VariableId, out var variable))
|
||||
return StructuredRuleExecuteResult.Fail($"{label}:变量 ID '{condition.VariableId}' 不存在");
|
||||
|
||||
// 布尔变量没有阈值概念,只能走 isTrue/isFalse 分支,避免被后续数值/日期比较逻辑误处理。
|
||||
if (variable.DataType == RiskVariableDataType.Boolean)
|
||||
return ExecuteBooleanCondition(condition, variable, context, label);
|
||||
|
||||
@@ -77,12 +144,14 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (!ComparableOperators.Contains(condition.Operator))
|
||||
return StructuredRuleExecuteResult.Fail($"{label}:操作符 '{condition.Operator}' 不适用于{GetDataTypeName(variable.DataType)}类型变量");
|
||||
|
||||
// 左侧条件变量统一先执行并转换成声明的数据类型;后续普通比较和区间比较都复用这个结果。
|
||||
var left = GetVariableValue(variable, variable.DataType, context, label, "条件变量");
|
||||
if (!left.Success)
|
||||
return StructuredRuleExecuteResult.Fail(left.ErrorMessage);
|
||||
|
||||
// 区间操作符使用下限/上限字段,不能继续走普通阈值字段。
|
||||
if (condition.Operator == "between" || condition.Operator == "notBetween")
|
||||
return ExecuteRangeCondition(condition, variable, left.Value, variables, context, label);
|
||||
return ExecuteRangeCondition(condition, variable, left, variables, context, label);
|
||||
|
||||
var rangeFieldCheck = EnsureNoRangeFields(condition, label);
|
||||
if (!rangeFieldCheck.Success)
|
||||
@@ -104,9 +173,15 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (!compareResult.Success)
|
||||
return compareResult;
|
||||
|
||||
return StructuredRuleExecuteResult.Ok(compareResult.Triggered);
|
||||
return StructuredRuleExecuteResult.Ok(
|
||||
compareResult.Triggered,
|
||||
// 未命中时不构造提示,减少字符串拼接开销;只有真正触发才展示实际值和阈值。
|
||||
compareResult.Triggered ? BuildCompareMessage(variable, left, right, condition.Operator) : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行布尔条件。布尔变量只允许 isTrue/isFalse,不允许配置固定值、变量阈值或区间字段。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult ExecuteBooleanCondition(
|
||||
RuleCondition condition,
|
||||
glms_risk_variable variable,
|
||||
@@ -124,13 +199,19 @@ namespace YLErp.Modules.RiskEngine
|
||||
return StructuredRuleExecuteResult.Fail(value.ErrorMessage);
|
||||
|
||||
var expected = condition.Operator == "isTrue";
|
||||
return StructuredRuleExecuteResult.Ok((bool)value.Value == expected);
|
||||
var triggered = (bool)value.Value == expected;
|
||||
return StructuredRuleExecuteResult.Ok(
|
||||
triggered,
|
||||
triggered ? BuildBooleanMessage(variable, value, expected) : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行区间条件。下限和上限可以分别来自固定值或变量,并根据开闭区间转换为对应比较操作符。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult ExecuteRangeCondition(
|
||||
RuleCondition condition,
|
||||
glms_risk_variable variable,
|
||||
object leftValue,
|
||||
ValueExecuteResult left,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
RiskContext context,
|
||||
string label)
|
||||
@@ -140,6 +221,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (!condition.IncludeLower.HasValue || !condition.IncludeUpper.HasValue)
|
||||
return StructuredRuleExecuteResult.Fail($"{label}:区间开闭配置不完整");
|
||||
|
||||
// 固定上下限可以提前比较顺序;变量上下限依赖运行时查询结果,留到后面按实际值比较。
|
||||
var fixedRangeCheck = ValidateFixedRangeOrder(condition, variable.DataType, label);
|
||||
if (!fixedRangeCheck.Success)
|
||||
return fixedRangeCheck;
|
||||
@@ -152,19 +234,27 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (!upper.Success)
|
||||
return StructuredRuleExecuteResult.Fail(upper.ErrorMessage);
|
||||
|
||||
// 将区间开闭配置转换为普通比较:闭区间使用 >=/<=,开区间使用 >/<。
|
||||
var lowerOperator = condition.IncludeLower == true ? "gte" : "gt";
|
||||
var upperOperator = condition.IncludeUpper == true ? "lte" : "lt";
|
||||
var lowerCompare = Compare(leftValue, lower.Value, variable.DataType, lowerOperator, label);
|
||||
var lowerCompare = Compare(left.Value, lower.Value, variable.DataType, lowerOperator, label);
|
||||
if (!lowerCompare.Success)
|
||||
return lowerCompare;
|
||||
var upperCompare = Compare(leftValue, upper.Value, variable.DataType, upperOperator, label);
|
||||
var upperCompare = Compare(left.Value, upper.Value, variable.DataType, upperOperator, label);
|
||||
if (!upperCompare.Success)
|
||||
return upperCompare;
|
||||
|
||||
var inRange = lowerCompare.Triggered && upperCompare.Triggered;
|
||||
return StructuredRuleExecuteResult.Ok(condition.Operator == "notBetween" ? !inRange : inRange);
|
||||
// between 要求落在区间内,notBetween 则取反;提示同样只在命中时构造。
|
||||
var triggered = condition.Operator == "notBetween" ? !inRange : inRange;
|
||||
return StructuredRuleExecuteResult.Ok(
|
||||
triggered,
|
||||
triggered ? BuildRangeMessage(variable, left, lower, upper, condition.IncludeLower == true, condition.IncludeUpper == true, condition.Operator) : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取阈值。固定阈值直接转换;变量阈值先校验类型和单位,再执行阈值变量表达式。
|
||||
/// </summary>
|
||||
private static ValueExecuteResult GetThresholdValue(
|
||||
string thresholdType,
|
||||
object fixedValue,
|
||||
@@ -195,6 +285,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
return ValueExecuteResult.Fail($"{label}:{thresholdLabel}变量 ID '{thresholdVariableId.Value}' 不存在");
|
||||
if (thresholdVariable.DataType != conditionVariable.DataType)
|
||||
return ValueExecuteResult.Fail($"{label}:{thresholdLabel}变量与条件变量的数据类型不一致");
|
||||
// 数值变量允许配置单位;变量阈值必须与条件变量单位一致,避免“金额”和“比例”等不同口径被直接比较。
|
||||
if (conditionVariable.DataType == RiskVariableDataType.Numeric && !UnitsMatch(conditionVariable.Unit, thresholdVariable.Unit))
|
||||
return ValueExecuteResult.Fail($"{label}:{thresholdLabel}变量与条件变量的单位不一致");
|
||||
|
||||
@@ -204,6 +295,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return ValueExecuteResult.Fail($"{label}:{thresholdLabel}类型 '{thresholdType}' 不合法,仅支持 Fixed/Variable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行变量取值表达式,并将表达式返回值转换为变量声明的数据类型。
|
||||
/// </summary>
|
||||
private static ValueExecuteResult GetVariableValue(
|
||||
glms_risk_variable variable,
|
||||
RiskVariableDataType dataType,
|
||||
@@ -228,13 +322,21 @@ namespace YLErp.Modules.RiskEngine
|
||||
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})执行失败:{ex.Message}");
|
||||
}
|
||||
|
||||
var converted = ConvertValue(rawValue, dataType, label, valueLabel);
|
||||
// 变量表达式可选择返回 RiskVariableValueDetail:Value 参与比较,Message 只用于命中说明。
|
||||
// 普通变量表达式仍返回原始值,不需要修改历史变量配置。
|
||||
var detail = rawValue as RiskVariableValueDetail;
|
||||
var actualRawValue = detail?.Value ?? rawValue;
|
||||
var converted = ConvertValue(actualRawValue, dataType, label, valueLabel);
|
||||
if (!converted.Success)
|
||||
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id}){converted.ErrorMessage},原始值:{FormatRawValue(rawValue)}");
|
||||
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id}){converted.ErrorMessage},原始值:{FormatRawValue(actualRawValue)}");
|
||||
|
||||
converted.DetailMessage = detail?.Message;
|
||||
return converted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取变量表达式的编译委托。缓存 Key 绑定变量版本信息,变量内容变化后会自然生成新 Key。
|
||||
/// </summary>
|
||||
private static Func<RiskContext, object> GetCompiledVariableExpression(glms_risk_variable variable, out string errorMessage)
|
||||
{
|
||||
var cacheKey = $"{variable.id}:{variable.Version}:{variable.UpdateDate?.Ticks ?? 0}:{StringComparer.Ordinal.GetHashCode(variable.VariableExpr ?? string.Empty)}";
|
||||
@@ -251,6 +353,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按变量数据类型统一转换值。日期固定值只接受 yyyy-MM-dd,避免不同区域格式导致比较结果不一致。
|
||||
/// </summary>
|
||||
private static ValueExecuteResult ConvertValue(object value, RiskVariableDataType dataType, string label, string valueLabel)
|
||||
{
|
||||
if (!HasValue(value))
|
||||
@@ -260,6 +365,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
if (value is decimal decimalValue)
|
||||
return ValueExecuteResult.Ok(decimalValue);
|
||||
// 数据库字段可能返回 double/int/string/JValue,统一按 InvariantCulture 转 decimal,避免服务器区域设置影响小数点解析。
|
||||
if (decimal.TryParse(GetRawValue(value), NumberStyles.Float, CultureInfo.InvariantCulture, out var numericValue))
|
||||
return ValueExecuteResult.Ok(numericValue);
|
||||
return ValueExecuteResult.Fail($"{valueLabel}必须为数字");
|
||||
@@ -286,8 +392,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
return ValueExecuteResult.Fail($"{label}:不支持的数据类型");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个已转换为同一数据类型的值,并应用结构化规则操作符。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult Compare(object left, object right, RiskVariableDataType dataType, string ruleOperator, string label)
|
||||
{
|
||||
// 前置 ConvertValue 已保证左右值类型一致,这里只做最终大小关系计算。
|
||||
var compare = dataType switch
|
||||
{
|
||||
RiskVariableDataType.Numeric => ((decimal)left).CompareTo((decimal)right),
|
||||
@@ -309,6 +419,59 @@ namespace YLErp.Modules.RiskEngine
|
||||
return StructuredRuleExecuteResult.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建普通比较条件的命中说明。变量表达式如果返回 RiskVariableValueDetail,会优先带出自定义明细。
|
||||
/// </summary>
|
||||
private static string BuildCompareMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult right, string ruleOperator)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
AddDetail(parts, left.DetailMessage);
|
||||
parts.Add($"{variable.VariableName}为{FormatDisplayValue(left.Value)}");
|
||||
AddDetail(parts, right.DetailMessage);
|
||||
parts.Add($"阈值为{FormatDisplayValue(right.Value)}");
|
||||
parts.Add($"比较关系:{GetOperatorName(ruleOperator)}");
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建区间条件的命中说明,包含实际值、上下限和区间开闭配置。
|
||||
/// </summary>
|
||||
private static string BuildRangeMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult lower, ValueExecuteResult upper, bool includeLower, bool includeUpper, string ruleOperator)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
AddDetail(parts, left.DetailMessage);
|
||||
parts.Add($"{variable.VariableName}为{FormatDisplayValue(left.Value)}");
|
||||
AddDetail(parts, lower.DetailMessage);
|
||||
AddDetail(parts, upper.DetailMessage);
|
||||
parts.Add($"阈值区间为{(includeLower ? "[" : "(")}{FormatDisplayValue(lower.Value)}, {FormatDisplayValue(upper.Value)}{(includeUpper ? "]" : ")")}");
|
||||
parts.Add(ruleOperator == "notBetween" ? "要求不在区间内" : "要求在区间内");
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建布尔条件的命中说明。
|
||||
/// </summary>
|
||||
private static string BuildBooleanMessage(glms_risk_variable variable, ValueExecuteResult value, bool expected)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
AddDetail(parts, value.DetailMessage);
|
||||
parts.Add($"{variable.VariableName}为{FormatDisplayValue(value.Value)}");
|
||||
parts.Add($"期望为{FormatDisplayValue(expected)}");
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加变量表达式返回的自定义明细,避免空明细污染最终提示。
|
||||
/// </summary>
|
||||
private static void AddDetail(List<string> parts, string detailMessage)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(detailMessage))
|
||||
parts.Add(detailMessage.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验非区间条件没有携带区间字段,避免前端残留字段影响规则语义。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult EnsureNoRangeFields(RuleCondition condition, string label)
|
||||
{
|
||||
if (condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
|
||||
@@ -321,6 +484,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return StructuredRuleExecuteResult.Ok(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验布尔条件没有携带阈值或区间字段,布尔判断只由变量值和 isTrue/isFalse 决定。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult EnsureNoThresholdFields(RuleCondition condition, string label)
|
||||
{
|
||||
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue ||
|
||||
@@ -334,6 +500,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return StructuredRuleExecuteResult.Ok(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当区间上下限都是固定值时,提前校验下限不能大于上限;变量上下限留到运行时按实际值比较。
|
||||
/// </summary>
|
||||
private static StructuredRuleExecuteResult ValidateFixedRangeOrder(RuleCondition condition, RiskVariableDataType dataType, string label)
|
||||
{
|
||||
if (condition.LowerThresholdType != "Fixed" || condition.UpperThresholdType != "Fixed")
|
||||
@@ -360,6 +529,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return StructuredRuleExecuteResult.Ok(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断普通对象或 JObject/JValue 字段是否有有效值,空字符串、null、undefined 都视为无值。
|
||||
/// </summary>
|
||||
private static bool HasValue(object value)
|
||||
{
|
||||
if (value == null) return false;
|
||||
@@ -372,6 +544,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
return value is not string text || !string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用固定区域性取出原始字符串,保证数字和日期解析不受服务器区域设置影响。
|
||||
/// </summary>
|
||||
private static string GetRawValue(object value)
|
||||
{
|
||||
return value is JValue jsonValue
|
||||
@@ -379,6 +554,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化错误信息中的原始值,便于区分 null、空字符串和普通值。
|
||||
/// </summary>
|
||||
private static string FormatRawValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
@@ -387,11 +565,48 @@ namespace YLErp.Modules.RiskEngine
|
||||
return string.IsNullOrWhiteSpace(rawValue) ? "<empty>" : rawValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化命中说明中的值,日期只展示日期部分,布尔值转为中文。
|
||||
/// </summary>
|
||||
private static string FormatDisplayValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return "<null>";
|
||||
if (value is DateTime dateValue)
|
||||
return dateValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
if (value is bool boolValue)
|
||||
return boolValue ? "是" : "否";
|
||||
return Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取操作符中文说明,用于正常命中时解释实际值与阈值的关系。
|
||||
/// </summary>
|
||||
private static string GetOperatorName(string ruleOperator)
|
||||
{
|
||||
return ruleOperator switch
|
||||
{
|
||||
"gt" => "大于阈值",
|
||||
"lt" => "小于阈值",
|
||||
"gte" => "大于等于阈值",
|
||||
"lte" => "小于等于阈值",
|
||||
"eq" => "等于阈值",
|
||||
"ne" => "不等于阈值",
|
||||
_ => ruleOperator
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比较数值变量单位。空单位按空字符串处理,前后空格不参与比较。
|
||||
/// </summary>
|
||||
private static bool UnitsMatch(string left, string right)
|
||||
{
|
||||
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据类型中文名称,用于拼接用户可读的错误信息。
|
||||
/// </summary>
|
||||
private static string GetDataTypeName(RiskVariableDataType dataType)
|
||||
{
|
||||
return dataType switch
|
||||
@@ -404,6 +619,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结构化规则执行结果。Success 表示执行过程是否成功,Triggered 表示规则条件是否命中。
|
||||
/// </summary>
|
||||
public class StructuredRuleExecuteResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
@@ -412,12 +630,18 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public static StructuredRuleExecuteResult Ok(bool triggered)
|
||||
/// <summary>
|
||||
/// 正常命中时的可读说明,用于展示实际值、阈值和变量表达式返回的计算明细。
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
public static StructuredRuleExecuteResult Ok(bool triggered, string message = null)
|
||||
{
|
||||
return new StructuredRuleExecuteResult
|
||||
{
|
||||
Success = true,
|
||||
Triggered = triggered
|
||||
Triggered = triggered,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
@@ -432,12 +656,36 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变量表达式可选返回模型。Value 参与规则比较,Message 用于正常命中时展示计算明细。
|
||||
/// </summary>
|
||||
public class RiskVariableValueDetail
|
||||
{
|
||||
public RiskVariableValueDetail(object value, string message)
|
||||
{
|
||||
Value = value;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public object Value { get; }
|
||||
|
||||
public string Message { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个值取值或转换结果,用于在内部传递转换后的值和错误信息。
|
||||
/// </summary>
|
||||
internal class ValueExecuteResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public object Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变量表达式返回的计算明细,仅用于正常命中提示,不参与比较。
|
||||
/// </summary>
|
||||
public string DetailMessage { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public static ValueExecuteResult Ok(object value)
|
||||
|
||||
@@ -1440,6 +1440,41 @@ ORDER BY
|
||||
挂钩标的到期日 < 合约到期日
|
||||
```
|
||||
|
||||
变量 Roslyn 示例:配置两个 Date 类型变量;规则前端配置“挂钩标的到期日 < 当前交易合约到期日”。
|
||||
|
||||
变量 1:挂钩标的到期日,DataType 为 Date。
|
||||
|
||||
```csharp
|
||||
var underlyingCode = DbContext.trade
|
||||
.Where(t => t.id == TradeId)
|
||||
.Select(t => t.UnderlyingCode)
|
||||
.FirstOrDefault();
|
||||
|
||||
var maturityDate = DbContext.underlying_manager
|
||||
.Where(u => u.UnderlyingCode == underlyingCode)
|
||||
.Select(u => u.MaturityDate)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!maturityDate.HasValue)
|
||||
throw new Exception("挂钩标的到期日为空");
|
||||
|
||||
return maturityDate.Value.Date;
|
||||
```
|
||||
|
||||
变量 2:当前交易合约到期日,DataType 为 Date。
|
||||
|
||||
```csharp
|
||||
var exerciseDate = DbContext.trade
|
||||
.Where(t => t.id == TradeId)
|
||||
.Select(t => t.ExerciseDate)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!exerciseDate.HasValue)
|
||||
throw new Exception("当前交易合约到期日为空");
|
||||
|
||||
return exerciseDate.Value.Date;
|
||||
```
|
||||
|
||||
规则字段口径:
|
||||
|
||||
```text
|
||||
@@ -1455,7 +1490,7 @@ ORDER BY
|
||||
// Id = 1000002,
|
||||
// RuleName = "挂钩标的到期日小于合约到期日(本地)",
|
||||
// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 UnderlyingCode 和 ExerciseDate,ExerciseDate 对应合约到期日;通过 DbContext.underlying_manager 按 UnderlyingCode 取 MaturityDate,MaturityDate 对应挂钩标的到期日。计算逻辑:挂钩标的到期日小于合约到期日时触发禁止。",
|
||||
// RuleExpr = "DbContext.underlying_manager.First(u => u.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode).MaturityDate.Value < DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value",
|
||||
// RuleExpr = "DbContext.underlying_manager.First(u => u.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode).MaturityDate.HasValue && DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && DateTime.Compare(DbContext.underlying_manager.First(u => u.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode).MaturityDate.Value.Date, DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date) < 0",
|
||||
// Version = 1,
|
||||
// Status = RiskRuleStatus.Active,
|
||||
// OptId = 0,
|
||||
@@ -1600,7 +1635,11 @@ decimal openingNotional = (decimal)openingNotionalRaw.Value;
|
||||
if (openingNotional == 0m)
|
||||
throw new Exception("开仓名义本金为0,不能作为除数");
|
||||
|
||||
return marginPaymentAmount / openingNotional;
|
||||
decimal marginPaymentRatio = marginPaymentAmount / openingNotional;
|
||||
|
||||
return new RiskVariableValueDetail(
|
||||
marginPaymentRatio,
|
||||
$"保证金支付金额为{marginPaymentAmount},开仓名义本金为{openingNotional}");
|
||||
```
|
||||
|
||||
规则字段口径:
|
||||
@@ -1708,6 +1747,30 @@ ORDER BY
|
||||
ABS((预付金返息率 - 1) * 100) > 阈值
|
||||
```
|
||||
|
||||
变量 Roslyn 示例:变量名为“保证金利率偏离”,DataType 为 Numeric;规则前端仍配置“保证金利率偏离 > 阈值”。`InterestRateDefault` 实体字段类型为 decimal,可直接参与 decimal 计算。
|
||||
|
||||
```csharp
|
||||
var marginPosition = DbContext.swap_position
|
||||
.Where(p => p.SwapTradeId == TradeId
|
||||
&& p.IsInitial
|
||||
&& !p.Invalid
|
||||
&& (p.InterestMode == 5 || p.InterestMode == 6))
|
||||
.OrderBy(p => p.HappenDate)
|
||||
.ThenBy(p => p.id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (marginPosition == null)
|
||||
throw new Exception("预付金记录不存在");
|
||||
|
||||
decimal interestRateRawValue = marginPosition.InterestRateDefault;
|
||||
decimal interestRatePercentValue = interestRateRawValue * 100m;
|
||||
decimal interestRateDeviation = Math.Abs((interestRateRawValue - 1m) * 100m);
|
||||
|
||||
return new RiskVariableValueDetail(
|
||||
interestRateDeviation,
|
||||
$"预付金返息率原值为{interestRateRawValue},页面百分比口径为{interestRatePercentValue},保证金利率偏离为{interestRateDeviation}");
|
||||
```
|
||||
|
||||
规则字段口径:
|
||||
|
||||
```text
|
||||
@@ -1812,6 +1875,38 @@ ORDER BY
|
||||
保证金收取金额 ÷ 开仓名义本金 < 20%
|
||||
```
|
||||
|
||||
变量 Roslyn 示例:变量名为“保证金收取比例”,DataType 为 Numeric;规则前端仍配置“保证金收取比例 < 0.2”。字段类型需为 decimal / decimal?,否则需要字段层调整或显式类型转换。
|
||||
|
||||
```csharp
|
||||
decimal marginReceiveAmount = DbContext.swap_position
|
||||
.Where(p => p.SwapTradeId == TradeId
|
||||
&& p.IsInitial
|
||||
&& !p.Invalid
|
||||
&& (p.InterestMode == 5 || p.InterestMode == 6)
|
||||
&& p.InterestDirection == 1)
|
||||
.Select(p => p.InterestPrincipalFix)
|
||||
.Sum();
|
||||
|
||||
double? openingNotionalRaw = DbContext.trade
|
||||
.Where(t => t.id == TradeId)
|
||||
.Select(t => t.OriginalStockEqvNotional)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!openingNotionalRaw.HasValue)
|
||||
throw new Exception("开仓名义本金为空");
|
||||
|
||||
decimal openingNotional = (decimal)openingNotionalRaw.Value;
|
||||
|
||||
if (openingNotional == 0m)
|
||||
throw new Exception("开仓名义本金为0,不能作为除数");
|
||||
|
||||
decimal marginReceiveRatio = marginReceiveAmount / openingNotional;
|
||||
|
||||
return new RiskVariableValueDetail(
|
||||
marginReceiveRatio,
|
||||
$"保证金收取金额为{marginReceiveAmount},开仓名义本金为{openingNotional}");
|
||||
```
|
||||
|
||||
规则字段口径:
|
||||
|
||||
```text
|
||||
|
||||
@@ -5350,12 +5350,7 @@ namespace YLErp.Modules.RiskModule
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:禁止\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Block))
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}";
|
||||
if (!string.IsNullOrWhiteSpace(triggeredRule.Message))
|
||||
{
|
||||
result.RiskWarningDetails += $";信息:{triggeredRule.Message}";
|
||||
}
|
||||
result.RiskWarningDetails += "\n";
|
||||
result.RiskWarningDetails += BuildTriggeredRuleDetail(triggeredRule);
|
||||
}
|
||||
}
|
||||
if (riskResult.NeedApproval)
|
||||
@@ -5380,19 +5375,15 @@ namespace YLErp.Modules.RiskModule
|
||||
result.ApprovalRuleIds = processedRiskRuleIds;
|
||||
foreach (var triggeredRule in approvalTriggeredRules)
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
|
||||
result.RiskWarningDetails += BuildTriggeredRuleDetail(triggeredRule);
|
||||
}
|
||||
}
|
||||
if (riskResult.ShowTip)
|
||||
{
|
||||
var warningMessages = riskResult.TriggeredRules
|
||||
.Where(r => r.ControlStrategy == RiskControlStrategy.ShowTip)
|
||||
.Select(r => r.Message)
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r))
|
||||
.ToList();
|
||||
if (warningMessages.Count > 0)
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:提示\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.ShowTip))
|
||||
{
|
||||
result.RiskWarningDetails += "[风控引擎] 提示:" + string.Join(";", warningMessages) + "\n";
|
||||
result.RiskWarningDetails += BuildTriggeredRuleDetail(triggeredRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5405,6 +5396,21 @@ namespace YLErp.Modules.RiskModule
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建新风控触发规则明细。禁止、审批、提示三类规则都可能带结构化命中明细,统一格式避免不同策略展示字段不一致。
|
||||
/// </summary>
|
||||
private static string BuildTriggeredRuleDetail(TriggeredRuleInfo triggeredRule)
|
||||
{
|
||||
var detail = $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}";
|
||||
if (!string.IsNullOrWhiteSpace(triggeredRule.Message))
|
||||
{
|
||||
detail += $";信息:{triggeredRule.Message}";
|
||||
}
|
||||
|
||||
return detail + "\n";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验标的白名单
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user