Merge branch 'feature/p132_74-risk-engine' of https://gitee.glmszq.com/gsty/onederiv/trs into feature/p132_74-risk-engine
This commit is contained in:
@@ -4,6 +4,6 @@ namespace YLErp.DBModels
|
||||
{
|
||||
Block = 1,
|
||||
Approval = 2,
|
||||
Warning = 3
|
||||
ShowTip = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则编译结果的进程内缓存。
|
||||
/// 当前第一版按 RuleCode 缓存 Roslyn 编译后的可执行委托。
|
||||
/// 当前第一版按规则 Id 缓存 Roslyn 编译后的可执行委托。
|
||||
/// </summary>
|
||||
public static class RuleCompiledCache
|
||||
{
|
||||
|
||||
@@ -32,8 +32,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// - 可调试:生成的脚本代码可直接阅读和理解
|
||||
///
|
||||
/// 示例:
|
||||
/// ConditionJson: { "conditions": [{"variableId":1,"operator":">","value":100000000}] }
|
||||
/// 生成脚本:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
/// RuleExpr: Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
|
||||
/// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存)
|
||||
/// </summary>
|
||||
public static class RuleCompiler
|
||||
@@ -71,14 +70,14 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并编译公式表达式。
|
||||
/// 当前 FormulaExp 要求是 Roslyn 可直接执行的 bool 表达式。
|
||||
/// 校验并编译规则表达式。
|
||||
/// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
|
||||
/// </summary>
|
||||
public static RuleCompileResult ValidateAndCompileFormula(string formulaExp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(formulaExp))
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 FormulaExp 不能为空");
|
||||
return RuleCompileResult.Fail("规则 RuleExpr 不能为空");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -101,100 +100,87 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
return RuleCompileResult.Fail("规则不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rule.RuleCode))
|
||||
|
||||
if (rule.Id <= 0)
|
||||
{
|
||||
return RuleCompileResult.Fail("规则 Code 不能为空");
|
||||
return RuleCompileResult.Fail("规则 Id 不能为空");
|
||||
}
|
||||
|
||||
var result = ValidateAndCompileFormula(rule.RuleExpr);
|
||||
if (!result.Success)
|
||||
{
|
||||
return RuleCompileResult.Fail($"规则[{rule.RuleCode}]编译失败:{result.ErrorMessage}");
|
||||
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
rule.CompiledScript = result.CompiledScript;
|
||||
RuleCompiledCache.Set(rule.RuleCode, result.CompiledScript);
|
||||
RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 ConditionJson,提取条件列表
|
||||
/// 解析 ConditionJson,提取条件列表。
|
||||
/// 要求 expression 明确描述实际取值路径。
|
||||
/// 当前主执行链不再使用该方法,仅作为早期方案保留。
|
||||
/// </summary>
|
||||
private static List<(long variableId, string op, object value, string variableType)> ParseConditionJson(string conditionJson)
|
||||
// private static List<(string variableCode, string expression, string op, object value, string variableType)> ParseConditionJson(string conditionJson)
|
||||
// {
|
||||
// var result = new List<(string, string, string, object, string)>();
|
||||
// var jObj = JObject.Parse(formulaJson);
|
||||
// var conditions = jObj["conditions"] as JArray;
|
||||
|
||||
// if (conditions == null || !conditions.Any())
|
||||
// {
|
||||
// throw new ArgumentException("ConditionJson 中缺少 conditions");
|
||||
// }
|
||||
|
||||
// foreach (var cond in conditions)
|
||||
// {
|
||||
// string variableCode = cond["variableCode"]?.Value<string>();
|
||||
// string expression = cond["expression"]?.Value<string>();
|
||||
// string op = cond["operator"]?.Value<string>();
|
||||
// object value = cond["value"]?.Value<object>();
|
||||
// string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
|
||||
|
||||
// if (string.IsNullOrWhiteSpace(expression) || string.IsNullOrWhiteSpace(op))
|
||||
// {
|
||||
// throw new ArgumentException("条件中缺少 expression 或 operator");
|
||||
// }
|
||||
|
||||
// result.Add((variableCode, expression, op, value, variableType));
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 根据条件列表生成 Roslyn C# 脚本代码。
|
||||
/// 多条件自动用 &&(AND)连接。
|
||||
/// 左值表达式统一基于 expression 构建。
|
||||
/// </summary>
|
||||
private static string BuildScriptCode(List<(string variableCode, string expression, string op, object value, string variableType)> conditions)
|
||||
{
|
||||
var result = new List<(long, string, object, string)>();
|
||||
var jObj = JObject.Parse(conditionJson);
|
||||
var conditions = jObj["conditions"] as JArray;
|
||||
var exprParts = new List<string>();
|
||||
|
||||
if (conditions == null || !conditions.Any())
|
||||
foreach (var (_, expression, op, value, variableType) in conditions)
|
||||
{
|
||||
throw new ArgumentException("ConditionJson 中缺少 conditions");
|
||||
// 先根据 expression 构建左值访问表达式,再拼接比较符和右值字面量
|
||||
string leftExpr = BuildMemberAccessExpression(expression, variableType);
|
||||
|
||||
// 格式化阈值
|
||||
string valueLiteral = FormatValueLiteral(value, variableType);
|
||||
|
||||
// 格式化操作符
|
||||
string csharpOp = FormatOperator(op);
|
||||
|
||||
// 生成单条件表达式
|
||||
string expr = $"{leftExpr} {csharpOp} {valueLiteral}";
|
||||
exprParts.Add(expr);
|
||||
}
|
||||
|
||||
foreach (var cond in conditions)
|
||||
{
|
||||
long variableId = cond["variableId"]?.Value<long>() ?? 0;
|
||||
string op = cond["operator"]?.Value<string>();
|
||||
object value = cond["value"]?.Value<object>();
|
||||
string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
|
||||
|
||||
if (variableId <= 0 || string.IsNullOrWhiteSpace(op))
|
||||
{
|
||||
throw new ArgumentException("条件中缺少 variableId 或 operator");
|
||||
}
|
||||
|
||||
result.Add((variableId, op, value, variableType));
|
||||
}
|
||||
|
||||
return result;
|
||||
// 多条件用 && 连接
|
||||
return string.Join(" && ", exprParts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据条件列表生成 Roslyn C# 脚本代码
|
||||
/// 多条件自动用 &&(AND)连接
|
||||
/// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式,
|
||||
/// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr
|
||||
/// </summary>
|
||||
//private static string BuildScriptCode(List<(long variableId, string op, object value, string variableType)> conditions)
|
||||
//{
|
||||
// var exprParts = new List<string>();
|
||||
|
||||
// foreach (var (variableId, op, value, variableType) in conditions)
|
||||
// {
|
||||
// // TODO: 通过 variableId 查变量池获取 VariableExpr(如 "trade.StockEqvNotional")
|
||||
// // 当前暂用 variableId.ToString() 占位,后续接入变量池查询
|
||||
// string variableExpr = variableId.ToString();
|
||||
|
||||
// // 解析 variableExpr:前缀.属性名
|
||||
// var parts = variableExpr.Split('.');
|
||||
// if (parts.Length != 2)
|
||||
// {
|
||||
// throw new ArgumentException($"variableExpr 格式不正确:{variableExpr}(variableId={variableId})");
|
||||
// }
|
||||
|
||||
// string prefix = parts[0].Trim().ToLower();
|
||||
// string propertyName = parts[1].Trim();
|
||||
|
||||
// if (!VariableTypeMap.TryGetValue(prefix, out string typeFullName))
|
||||
// {
|
||||
// throw new ArgumentException($"未知前缀:{prefix}");
|
||||
// }
|
||||
// // 格式化阈值
|
||||
// string valueLiteral = FormatValueLiteral(value, variableType);
|
||||
|
||||
// // 格式化操作符
|
||||
// string csharpOp = FormatOperator(op);
|
||||
|
||||
// // 生成单条件表达式
|
||||
// string expr = $"{leftExpr} {csharpOp} {valueLiteral}";
|
||||
// exprParts.Add(expr);
|
||||
// }
|
||||
|
||||
// // 多条件用 && 连接
|
||||
// return string.Join(" && ", exprParts);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。
|
||||
/// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。
|
||||
@@ -243,13 +229,15 @@ namespace YLErp.Modules.RiskEngine
|
||||
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}");
|
||||
_logger.Error($"规则编译失败:{errorMsg}\n脚本代码:{scriptCode}");
|
||||
throw new InvalidOperationException($"脚本编译失败:{errorMsg}");
|
||||
}
|
||||
|
||||
// 生成可调用委托
|
||||
|
||||
@@ -17,28 +17,28 @@ using YLErp.Model;
|
||||
【当前实现口径】(以本注释和实际代码为准)
|
||||
|
||||
核心概念:
|
||||
- RiskRule(风控规则):规则定义本体,当前重点字段包括 RuleCode、RuleName、
|
||||
FormulaText、FormulaExp、Version、CompiledScript。
|
||||
- RiskRule(风控规则):规则定义本体,当前重点字段包括 Id、RuleName、
|
||||
RuleText、RuleExpr、Version、CompiledScript。
|
||||
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
|
||||
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
|
||||
- FormulaExp:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
|
||||
- RuleExpr:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
|
||||
例如直接访问 DataMap["trade"] 后做数值或日期比较。
|
||||
- FormulaJson:当前已在规则定义和样例代码中停用,不参与主执行链路。
|
||||
- ConditionJson:当前已在规则定义中保留,但不参与主执行链路。
|
||||
- RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DataMap。
|
||||
- RuleCompiledCache:进程内编译结果缓存,按 RuleCode 缓存 Func<RiskContext, bool>。
|
||||
- RuleCompiledCache:进程内编译结果缓存,按规则 Id 缓存 Func<RiskContext, bool>。
|
||||
|
||||
当前编译流程:
|
||||
1. 外部准备好规则对象,FormulaExp 中直接写最终可执行表达式
|
||||
1. 外部准备好规则对象,在 RuleExpr 中直接写最终可执行表达式
|
||||
2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
|
||||
3. 内部使用 Roslyn 编译 FormulaExp,生成 Func<RiskContext, bool>
|
||||
3. 内部使用 Roslyn 编译 RuleExpr,生成 Func<RiskContext, bool>
|
||||
4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
|
||||
5. 执行阶段优先从 RuleCompiledCache 取委托执行
|
||||
|
||||
当前执行流程(EvaluateRisk):
|
||||
1. 加载示例规则列表和示例应用列表
|
||||
1. 从数据库加载规则列表和应用列表
|
||||
2. 按应用状态、TriggerPoints 过滤有效应用
|
||||
3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
|
||||
4. 根据命中的 RuleCode 找到对应规则
|
||||
4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
|
||||
5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
|
||||
6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
|
||||
7. 返回 RiskResult
|
||||
@@ -50,11 +50,11 @@ using YLErp.Model;
|
||||
- 某维度留空:视为该维度不限制
|
||||
|
||||
【与早期方案的主要差异】
|
||||
- 当前不是 Content 解析或表达式树主导,而是 FormulaExp 直编译主导
|
||||
- 当前不是 Content 解析或表达式树主导,而是 RuleExpr 直编译主导
|
||||
- RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
|
||||
- 编译器文件已放入 RiskEngine/Compile 目录下
|
||||
- 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 FormulaExp 直接表达
|
||||
- Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 RuleExpr 直接表达
|
||||
|
||||
【当前代码结构】
|
||||
风控引擎层:YLErpDAL/Modules/RiskEngine/
|
||||
@@ -75,7 +75,7 @@ using YLErp.Model;
|
||||
✅ 已完成:
|
||||
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
|
||||
2. 风控上下文通过 DataMap 传入 trade 对象,支持规则脚本直接访问交易字段
|
||||
3. FormulaExp 直编译方案已接入,支持数值比较和日期比较
|
||||
3. RuleExpr 直编译方案已接入,支持数值比较和日期比较
|
||||
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
|
||||
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
|
||||
6. RuleCompiler 已支持校验 + 编译 + 写缓存
|
||||
@@ -84,9 +84,9 @@ using YLErp.Model;
|
||||
|
||||
【待办事项 / TODO】
|
||||
⬜ 1. 接入真实规则来源
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 示例数据
|
||||
- 由数据库或前端提交替换当前 LoadRules / LoadApplications 的规则来源
|
||||
⬜ 2. 增加接口层
|
||||
- 提供前端提交 FormulaExp 后的校验接口
|
||||
- 提供前端提交 RuleExpr 后的校验接口
|
||||
- 提供规则保存 / 发布后预编译并写缓存的入口
|
||||
⬜ 3. 启动预热与缓存刷新
|
||||
- 服务启动时批量加载有效规则并预编译
|
||||
@@ -96,12 +96,12 @@ using YLErp.Model;
|
||||
- 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
|
||||
⬜ 5. 补测试样例
|
||||
- 数值比较、日期比较、维度过滤、非法公式、边界值
|
||||
⬜ 6. 完善 FormulaExp 配套能力
|
||||
⬜ 6. 完善 RuleExpr 配套能力
|
||||
- 提供前端可用的表达式编辑、校验与错误提示
|
||||
- 约束可用变量、类型转换和脚本安全边界
|
||||
|
||||
【注意事项】
|
||||
1. 当前 FormulaExp 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
|
||||
2. DataMap 中的值由宿主业务代码准备,编译器与执行器本身不负责查库补数
|
||||
3. 当前缓存的是 Func<RiskContext, bool>,这是规则判断函数,不是事件处理器
|
||||
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
|
||||
@@ -168,7 +168,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
// Step 2: 先按 Application 过滤 Active 状态和 TriggerPoint 匹配的应用
|
||||
// ============================================================
|
||||
var activeApps = applications
|
||||
.Where(a => a.Status == RiskRuleApplicationStatusEnum.Active)
|
||||
.Where(a => a.Status == RiskRuleStatus.Active)
|
||||
.ToList();
|
||||
var apps = activeApps
|
||||
.Where(a => !string.IsNullOrEmpty(a.TriggerPoints))
|
||||
@@ -196,113 +196,139 @@ namespace YLErp.Modules.RiskEngine
|
||||
_logger.Info($"[风控引擎] 匹配 TriggerPoint 的应用数: {matchedApplications.Count}");
|
||||
|
||||
// ============================================================
|
||||
// Step 3: 遍历匹配的应用,通过 RuleCode 关联规则并执行预编译委托
|
||||
// Step 3: 遍历匹配的应用,通过 RuleIds 关联规则并执行预编译委托
|
||||
// ============================================================
|
||||
foreach (var application in matchedApplications)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 3.1 通过 RuleCode 关联规则定义
|
||||
// 3.1 通过 RuleIds 关联规则定义
|
||||
// ------------------------------------------------------------
|
||||
var rule = rules.FirstOrDefault(r => r.RuleCode == application.RuleCode);
|
||||
if (rule == null)
|
||||
var applicationRuleIds = ParseRuleIds(application.RuleIds);
|
||||
if (!applicationRuleIds.Any())
|
||||
{
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleCode: {application.RuleCode}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"未找到规则定义[{application.RuleCode}];";
|
||||
_logger.Info($"[风控引擎] 应用未配置有效规则 - RuleIds: {application.RuleIds}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(rule.RuleCode, out var compiledScript))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleCode: {rule.RuleCode}");
|
||||
var applicationRules = rules
|
||||
.Where(r => applicationRuleIds.Contains(r.Id) && r.Id == 3)
|
||||
.ToList();
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
var missingRuleIds = applicationRuleIds
|
||||
.Where(ruleId => applicationRules.All(r => r.Id != ruleId))
|
||||
.ToList();
|
||||
|
||||
foreach (var missingRuleId in missingRuleIds)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {missingRuleId}, ApplicationRuleIds: {application.RuleIds}");
|
||||
}
|
||||
|
||||
foreach (var rule in applicationRules)
|
||||
{
|
||||
var ruleId = rule.Id.ToString();
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.2 优先从编译缓存读取规则委托
|
||||
// ------------------------------------------------------------
|
||||
if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则编译失败 - RuleCode: {rule.RuleCode}, Error: {compileResult.ErrorMessage}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.RuleCode}]编译失败:{compileResult.ErrorMessage};";
|
||||
_logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
|
||||
continue;
|
||||
}
|
||||
|
||||
compiledScript = compileResult.CompiledScript;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
|
||||
compiledScript = compileResult.CompiledScript;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 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}");
|
||||
result.HasError = true;
|
||||
result.ErrorMessage += $"规则[{rule.Id}]执行异常:{ex.Message};";
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
switch (application.ControlStrategy)
|
||||
// ------------------------------------------------------------
|
||||
// 3.4 命中后按 Application 的控制策略聚合结果
|
||||
// ------------------------------------------------------------
|
||||
if (triggered)
|
||||
{
|
||||
case RiskControlStrategyEnum.Block:
|
||||
result.Blocked = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "禁止",
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:禁止"
|
||||
});
|
||||
break;
|
||||
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
|
||||
|
||||
case RiskControlStrategyEnum.Approval:
|
||||
result.NeedApproval = true;
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleName = rule.RuleName,
|
||||
ControlStrategy = "审批",
|
||||
RuleText = rule.RuleText,
|
||||
Message = $"规则[{rule.RuleName}]触发:需审批"
|
||||
});
|
||||
break;
|
||||
switch (application.ControlStrategy)
|
||||
{
|
||||
case RiskControlStrategy.Block:
|
||||
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 RiskControlStrategyEnum.Warning:
|
||||
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
|
||||
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;
|
||||
|
||||
default:
|
||||
_logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则未触发 - RuleCode: {rule.RuleCode}, TriggerPoint: {triggerPoint}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 4: 聚合最终结果
|
||||
// ============================================================
|
||||
if (!result.Blocked && !result.NeedApproval && result.Warnings.Count == 0 && !result.HasError)
|
||||
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}");
|
||||
}
|
||||
@@ -312,11 +338,10 @@ namespace YLErp.Modules.RiskEngine
|
||||
result.Passed = false;
|
||||
result.TriggeredRules.Add(new TriggeredRuleInfo
|
||||
{
|
||||
RuleCode = "ENGINE_ERROR",
|
||||
RuleId = "ENGINE_ERROR",
|
||||
RuleName = "风控引擎执行异常",
|
||||
ControlStrategy = "审批",
|
||||
RuleText = ex.Message,
|
||||
Message = $"风控引擎异常:{ex.Message},需审批通过"
|
||||
Message = $"风控引擎异常:{ex.Message}"
|
||||
});
|
||||
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
|
||||
}
|
||||
@@ -325,74 +350,75 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载规则列表(TODO: 后续接入真实内存缓存和数据库查询)
|
||||
/// 当前返回示例规则用于验证流程
|
||||
/// 加载规则列表(TODO: 后续接入真实内存缓存)
|
||||
/// 当前从数据库读取规则用于验证流程
|
||||
/// </summary>
|
||||
private List<RiskRule> LoadRules()
|
||||
{
|
||||
//名义本金:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap[\"trade\"]).StockEqvNotional)
|
||||
//挂钩标的到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).MaturityDate
|
||||
//合约到期日:((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate
|
||||
var rules = DbContext.glms_risk_rule
|
||||
.AsNoTracking()
|
||||
.Where(r => r.Status != RiskRuleStatus.Deleted)
|
||||
.OrderByDescending(r => r.UpdateDate ?? r.OptDate)
|
||||
.Select(r => new RiskRule
|
||||
{
|
||||
Id = r.id,
|
||||
RuleName = r.RuleName,
|
||||
RuleText = r.RuleText,
|
||||
ConditionJson = r.ConditionJson,
|
||||
RuleExpr = r.RuleExpr,
|
||||
Version = r.Version,
|
||||
IsDeleted = r.Status == RiskRuleStatus.Deleted,
|
||||
OptId = r.OptId ?? 0,
|
||||
OptName = r.OptName,
|
||||
OptDate = r.OptDate ?? DateTime.MinValue,
|
||||
UpdateOptId = r.UpdateOptId ?? 0,
|
||||
UpdateOptName = r.UpdateOptName,
|
||||
UpdateDate = r.UpdateDate ?? r.OptDate ?? DateTime.MinValue
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 示例规则1:先保留一个最小可运行样例,只验证名义本金阈值判断
|
||||
var Rule1 = new RiskRule
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
Id = 103,
|
||||
RuleName = "测试规则:名义本金超过1亿",
|
||||
RuleText = "名义本金>1亿",
|
||||
// FormulaJson = "{ \"conditions\": [" +
|
||||
// "{\"variableCode\":\"StockEqvNotional\",\"expression\":\"trade.StockEqvNotional\",\"operator\":\">\",\"value\":100000000,\"variableType\":\"number\"}" +
|
||||
// "] }",
|
||||
RuleExpr = "Convert.ToDecimal(((YLErp.DBModels.trade)DataMap[\"trade\"]).StockEqvNotional) > 100000000m",
|
||||
Version = 1
|
||||
};
|
||||
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
_logger.Info($"[风控引擎] 规则预编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
// 示例规则2:挂钩标的到期日小于合约到期日
|
||||
var Rule2 = new RiskRule
|
||||
{
|
||||
Id = 104,
|
||||
RuleCode = "RISK-20260623-0004",
|
||||
RuleName = "挂钩标的到期日小于合约到期日",
|
||||
RuleText = "挂钩标的到期日<合约到期日",
|
||||
//FormulaJson = "{ \"conditions\": [{\"variableCode\":\"MaturityDate\",\"expression\":\"trade.MaturityDate\",\"operator\":\"<\",\"value\":\"trade.ExerciseDate\",\"variableType\":\"date\"}] }",
|
||||
RuleExpr = "((YLErp.DBModels.trade)DataMap[\"trade\"]).MaturityDate < ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate",
|
||||
Version = 1
|
||||
};
|
||||
|
||||
// 当前样例规则在加载阶段预编译并写入缓存,便于执行阶段直接取用
|
||||
RuleCompiler.Compile(Rule1);
|
||||
RuleCompiler.Compile(Rule2);
|
||||
|
||||
return new List<RiskRule> { Rule1, Rule2 };
|
||||
return rules;
|
||||
}
|
||||
/// <summary>
|
||||
/// 加载application
|
||||
/// 当前返回示例规则用于验证流程
|
||||
/// 当前从数据库读取应用配置用于验证流程
|
||||
/// </summary>
|
||||
private List<RiskRuleApplication> LoadApplications()
|
||||
{
|
||||
var application1 = new RiskRuleApplication
|
||||
{
|
||||
Id = 1,
|
||||
RuleCode = "RISK-20260622-0003",
|
||||
Status = RiskRuleApplicationStatusEnum.Active,
|
||||
ControlStrategy = RiskControlStrategyEnum.Block,
|
||||
TriggerPoints = "BOOK_CONFIRM",
|
||||
ScopeIsGlobal = true,
|
||||
};
|
||||
|
||||
var application2 = new RiskRuleApplication
|
||||
{
|
||||
Id = 2,
|
||||
RuleCode = "RISK-20260623-0004",
|
||||
Status = RiskRuleApplicationStatusEnum.Active,
|
||||
ControlStrategy = RiskControlStrategyEnum.Block,
|
||||
TriggerPoints = "BOOK_CLICK,BOOK_CONFIRM",
|
||||
ScopeIsGlobal = false,
|
||||
ScopeTradeTypes = "收益互换",
|
||||
};
|
||||
|
||||
return new List<RiskRuleApplication> { application1, application2 };
|
||||
return DbContext.glms_risk_rule_application
|
||||
.AsNoTracking()
|
||||
.Where(a => a.Status != RiskRuleStatus.Deleted)
|
||||
.OrderByDescending(a => a.UpdateDate ?? a.OptDate)
|
||||
.Select(a => new RiskRuleApplication
|
||||
{
|
||||
Id = a.id,
|
||||
RuleIds = a.RuleIds,
|
||||
Status = a.Status,
|
||||
ControlStrategy = a.ControlStrategy,
|
||||
TriggerPoints = a.TriggerPoints,
|
||||
ScopeIsGlobal = a.ScopeIsGlobal,
|
||||
ScopeAssetBookIds = a.ScopeAssetBookIds,
|
||||
ScopeClientIds = a.ScopeClientIds,
|
||||
ScopeUnderlyingTypes = a.ScopeUnderlyingTypes,
|
||||
ScopeTradeTypes = a.ScopeTradeTypes,
|
||||
Version = a.Version,
|
||||
OptId = a.OptId ?? 0,
|
||||
OptName = a.OptName,
|
||||
OptDate = a.OptDate ?? DateTime.MinValue,
|
||||
UpdateOptId = a.UpdateOptId ?? 0,
|
||||
UpdateOptName = a.UpdateOptName,
|
||||
UpdateDate = a.UpdateDate ?? a.OptDate ?? DateTime.MinValue
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -431,6 +457,29 @@ namespace YLErp.Modules.RiskEngine
|
||||
&& 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>
|
||||
/// 判断某个范围字段是否为空。
|
||||
/// 为空表示该维度不限制。
|
||||
|
||||
@@ -23,24 +23,14 @@ namespace YLErp.Modules.RiskEngine
|
||||
public bool NeedApproval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否存在执行异常
|
||||
/// 是否存在提示
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详细错误信息
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
public bool ShowTip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发的规则列表
|
||||
/// </summary>
|
||||
public List<TriggeredRuleInfo> TriggeredRules { get; set; } = new List<TriggeredRuleInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// 提示类警告信息
|
||||
/// </summary>
|
||||
public List<string> Warnings { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,9 +38,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public class TriggeredRuleInfo
|
||||
{
|
||||
public string RuleCode { get; set; }
|
||||
public string RuleId { get; set; }
|
||||
public string RuleName { get; set; }
|
||||
public string ControlStrategy { get; set; }
|
||||
public RiskControlStrategy ControlStrategy { get; set; }
|
||||
public string RuleText { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
public int Id { get; set; }
|
||||
public string RuleName { get; set; }
|
||||
public string RuleText { get; set; }
|
||||
public string RuleCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 【核心】条件JSON(结构化模式的条件列表存储),如
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
@@ -18,55 +19,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
///
|
||||
/// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 规则应用状态枚举。
|
||||
/// 用于描述一条规则应用配置当前是否参与风控执行。
|
||||
/// </summary>
|
||||
public enum RiskRuleApplicationStatusEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 已生效。
|
||||
/// 当前应用配置可正常参与风控判断。
|
||||
/// </summary>
|
||||
Active = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已停用。
|
||||
/// 当前应用配置暂不参与风控判断,但配置仍被保留。
|
||||
/// </summary>
|
||||
Disabled = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已删除。
|
||||
/// 当前应用配置逻辑上已删除,通常不再参与任何执行。
|
||||
/// </summary>
|
||||
Deleted = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 风控控制策略枚举。
|
||||
/// 用于描述规则命中后的处理方式。
|
||||
/// </summary>
|
||||
public enum RiskControlStrategyEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 禁止。
|
||||
/// 规则命中后直接拦截,不允许继续后续流程。
|
||||
/// </summary>
|
||||
Block = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 审批。
|
||||
/// 规则命中后不直接放行,需要进入审批流程。
|
||||
/// </summary>
|
||||
Approval = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 提示。
|
||||
/// 规则命中后只给出风险提示,不阻断流程。
|
||||
/// </summary>
|
||||
Warning = 3
|
||||
}
|
||||
public class RiskRuleApplication
|
||||
{
|
||||
/// <summary>
|
||||
@@ -76,11 +28,11 @@ namespace YLErp.Modules.RiskEngine
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的规则编码。
|
||||
/// 通过该字段与 RiskRule.RuleCode 关联,表示当前应用配置对应哪一条规则定义。
|
||||
/// 第一版先用字符串编码关联,后续如有需要也可以扩展为 RuleId。
|
||||
/// 关联的规则ID列表。
|
||||
/// 通过该字段与 RiskRule.Id 关联,多个规则ID使用逗号分隔。
|
||||
/// </summary>
|
||||
public string RuleCode { get; set; }
|
||||
|
||||
public string RuleIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用状态。
|
||||
@@ -90,7 +42,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 3 = Deleted(已删除)
|
||||
/// 在实际执行时,通常只加载 Active 状态的应用配置。
|
||||
/// </summary>
|
||||
public RiskRuleApplicationStatusEnum Status { get; set; }
|
||||
public RiskRuleStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 校验策略。
|
||||
@@ -100,7 +52,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// 3 = 提示
|
||||
/// 当规则命中时,由该字段决定最终处理方式。
|
||||
/// </summary>
|
||||
public RiskControlStrategyEnum ControlStrategy { get; set; }
|
||||
public RiskControlStrategy ControlStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发时点。
|
||||
@@ -149,6 +101,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public string ScopeTradeTypes { get; set; }
|
||||
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID。
|
||||
/// 记录是谁创建了这条应用配置。
|
||||
@@ -185,4 +139,4 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public DateTime UpdateDate { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4332,7 +4332,12 @@ namespace YLErp.Modules.RiskModule
|
||||
|
||||
public bool QuotaCheck(ref TradeOpenResult res)
|
||||
{
|
||||
if (res == null && res.Trade == null)
|
||||
return QuotaCheck(ref res, false);
|
||||
}
|
||||
|
||||
public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning)
|
||||
{
|
||||
if (res == null || res.Trade == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(res));
|
||||
}
|
||||
@@ -4349,6 +4354,12 @@ namespace YLErp.Modules.RiskModule
|
||||
res.TrialDataId = quotaObj.id;
|
||||
return false;
|
||||
}
|
||||
//成功状态下,如果触发TIp检查,提示用户
|
||||
if (!string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails))
|
||||
{
|
||||
res.TrialDataId = quotaObj.id;
|
||||
res.TipMsg = quotaObj.RiskWarningDetails;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var lastTrial = trialService.QueryLastQuotaTrial(tradeId, true);
|
||||
@@ -4357,8 +4368,17 @@ namespace YLErp.Modules.RiskModule
|
||||
new TradeRiskCheckLogService(UserInfo).AddLog(quotaObj);
|
||||
}
|
||||
//否则的情况是上次没算,这次是预警,或上次算了,结果是不通过\通过或预警,这次是预警或不通过,提示用户;
|
||||
res.RetCode = TradeOpenRetCode.QuotaTrialError;
|
||||
res.TrialDataId = quotaObj.id;
|
||||
res.ErrorMsg = quotaObj.RiskWarningDetails;
|
||||
var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning
|
||||
&& !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails);
|
||||
//需清除ErrorMsg,不然外部调用会认为失败
|
||||
if (ignoreRiskWarning && isRiskApprovalWarning)
|
||||
{
|
||||
res.ErrorMsg = string.Empty;
|
||||
return true;
|
||||
}
|
||||
res.RetCode = isRiskApprovalWarning ? TradeOpenRetCode.RiskWarning : TradeOpenRetCode.QuotaTrialError;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4940,29 +4960,48 @@ namespace YLErp.Modules.RiskModule
|
||||
};
|
||||
// 构造 DataMap(第一版只塞 trade 对象,后续按需扩展)
|
||||
// 当前先用 demo 值验证“名义本金超过1亿”的规则是否能命中
|
||||
tradeObj.StockEqvNotional = 1000;
|
||||
tradeObj.StockEqvNotional = 1000000000;
|
||||
tradeObj.TradeType = "收益互换";
|
||||
tradeObj.MaturityDate = new DateTime(2026, 6, 19);
|
||||
tradeObj.ExerciseDate = new DateTime(2026, 6, 21);
|
||||
riskContext.DataMap["trade"] = tradeObj;
|
||||
var riskResult = riskEngine.EvaluateRisk(riskContext, "BOOK_CONFIRM");
|
||||
|
||||
_logger.Info($"[风控引擎] 簿记交易确认 - TradeId: {tradeId}, Passed: {riskResult.Passed}, Blocked: {riskResult.Blocked}, NeedApproval: {riskResult.NeedApproval}");
|
||||
_logger.Info($"[风控引擎] 簿记交易确认 - TradeId: {tradeId}, Passed: {riskResult.Passed}, Blocked: {riskResult.Blocked}, NeedApproval: {riskResult.NeedApproval}, ShowTip: {riskResult.ShowTip}");
|
||||
|
||||
// 按策略映射回 QuotaTrialStatusEnum
|
||||
if (riskResult.Blocked)
|
||||
{
|
||||
result.TrialStatus = QuotaTrialStatusEnum.Error;
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:禁止\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Block))
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
|
||||
}
|
||||
}
|
||||
else if (riskResult.NeedApproval)
|
||||
if (riskResult.NeedApproval)
|
||||
{
|
||||
result.TrialStatus = QuotaTrialStatusEnum.Warning;
|
||||
if (result.TrialStatus != QuotaTrialStatusEnum.Error)
|
||||
{
|
||||
result.TrialStatus = QuotaTrialStatusEnum.Warning;
|
||||
}
|
||||
result.RiskWarningDetails += "[风控引擎] 规则触发:需审批\n";
|
||||
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval))
|
||||
{
|
||||
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
|
||||
}
|
||||
}
|
||||
else if (riskResult.Warnings != null && riskResult.Warnings.Count > 0)
|
||||
if (riskResult.ShowTip)
|
||||
{
|
||||
result.RiskWarningDetails += "[风控引擎] 提示:" + string.Join(";", riskResult.Warnings) + "\n";
|
||||
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 += "[风控引擎] 提示:" + string.Join(";", warningMessages) + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using MoreLinq;
|
||||
using OfficeOpenXml;
|
||||
using Snowflake.Core;
|
||||
@@ -146,6 +146,39 @@ namespace YLErp.Modules.RiskModule
|
||||
DbContext.SaveChanges();
|
||||
}
|
||||
|
||||
public void AddWarningDecisionLog(int quotaTrialId, string decision)
|
||||
{
|
||||
var quotaTrial = DbContext.quotaTrial.FirstOrDefault(x => x.id == quotaTrialId);
|
||||
if (quotaTrial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var td = DbContext.trade.FirstOrDefault(x => x.id == quotaTrial.TradeId);
|
||||
trade_risk_check_log log = new trade_risk_check_log();
|
||||
if (td != null)
|
||||
{
|
||||
var client = DataCacheProvider.GetClientDataSource().GetData(td.ClientId);
|
||||
log.client_number = client?.Number;
|
||||
}
|
||||
var worker = new IdWorker(1, 1);
|
||||
log.id = worker.NextId();
|
||||
log.trial_result = (int)quotaTrial.TrialStatus;
|
||||
log.client_name = quotaTrial.ClientName;
|
||||
log.trader = td?.TraderName;
|
||||
log.trade_number = quotaTrial.TradeNumber;
|
||||
log.risk_warning = string.IsNullOrWhiteSpace(quotaTrial.RiskWarningDetails)
|
||||
? $"[风控预警处理]{decision}"
|
||||
: quotaTrial.RiskWarningDetails + Environment.NewLine + $"[风控预警处理]{decision}";
|
||||
log.limit_warning = quotaTrial.QuotaCheckDetails;
|
||||
log.remark = string.IsNullOrWhiteSpace(quotaTrial.Remark)
|
||||
? $"风控预警处理结果:{decision}"
|
||||
: quotaTrial.Remark + ";风控预警处理结果:" + decision;
|
||||
log.create_user = UserId;
|
||||
log.create_time = DateTime.Now;
|
||||
DbContext.trade_risk_check_log.Add(log);
|
||||
DbContext.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回退重置
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using DocumentFormat.OpenXml.Office2010.Excel;
|
||||
using OfficeOpenXml.Drawing;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
@@ -46,7 +46,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// <summary>
|
||||
/// 2021-02-04改造:批量确认因为耗时太长,用户多页面(交易列表,今日交易)并发确认导致生成多条期权费资金记录
|
||||
/// </summary>
|
||||
public TradeConfirmResultModel tradeConfirm(IEnumerable<int> tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false)
|
||||
public TradeConfirmResultModel tradeConfirm(IEnumerable<int> tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false)
|
||||
{
|
||||
if (tradeids is null || !tradeids.Any(n => n > 0))
|
||||
{
|
||||
@@ -76,6 +76,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
var hasTradeProcess = HasTradeProcess();
|
||||
|
||||
var sbError = new StringBuilder();
|
||||
var sbTip = new StringBuilder();
|
||||
var result = new TradeConfirmResultModel
|
||||
{
|
||||
confirmedTradeIds = new List<int>(),
|
||||
@@ -99,7 +100,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
isSkipApproval: isSkipApproval,
|
||||
confirmedTradeIds: result.confirmedTradeIds,
|
||||
changeConfirmPaths: result.changeConfirmPaths,
|
||||
continueQuotaCheck: idCache.Contains(pid));
|
||||
continueQuotaCheck: idCache.Contains(pid),
|
||||
ignoreRiskWarning: ignoreRiskWarning);
|
||||
if (pid > 0)
|
||||
{
|
||||
idCache.Add(pid);
|
||||
@@ -114,7 +116,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
result.LackOfMoney = true;
|
||||
break;
|
||||
}
|
||||
if (temp.RetCode == TradeOpenRetCode.QuotaTrialError || temp.RetCode == TradeOpenRetCode.FundStatus)
|
||||
//此处LackOfMoney仅仅代表需要前端二次处理,不代表资金缺乏,延用老逻辑
|
||||
if (temp.RetCode == TradeOpenRetCode.QuotaTrialError || temp.RetCode == TradeOpenRetCode.FundStatus || temp.RetCode == TradeOpenRetCode.RiskWarning)
|
||||
{
|
||||
result.LackOfMoney = true;
|
||||
result.TrialDataId = temp.TrialDataId;
|
||||
@@ -128,6 +131,11 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
sbError.AppendLine(temp.ErrorMsg);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(temp.TipMsg))
|
||||
{
|
||||
sbTip.AppendLine(temp.TipMsg);
|
||||
}
|
||||
|
||||
Helpers.LockHelper.Release(lockKeyId);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +143,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
DbContext.SaveChanges();
|
||||
|
||||
result.errorMsg = sbError.ToString();
|
||||
result.tipMsg = sbTip.ToString();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -152,7 +161,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// <param name="continueQuotaCheck"></param>
|
||||
/// <returns></returns>
|
||||
private TradeOpenResult ConfirmTrade(int tid, bool isBatch, bool hasTradeProcess,
|
||||
bool ignoreMoneyCheck, bool isSkipApproval, List<int> confirmedTradeIds, List<string> changeConfirmPaths, bool continueQuotaCheck = false)
|
||||
bool ignoreMoneyCheck, bool isSkipApproval, List<int> confirmedTradeIds, List<string> changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false)
|
||||
{
|
||||
var td = DbContext.trade.FirstOrDefault(t => t.id == tid);
|
||||
var result = new TradeOpenResult(td ?? new trade());
|
||||
@@ -207,7 +216,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
}
|
||||
|
||||
var trialService = new QuotaMonitorService(this);
|
||||
if (!trialService.QuotaCheck(ref result))
|
||||
if (!trialService.QuotaCheck(ref result, ignoreRiskWarning))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -223,6 +232,10 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
history = "特权账户批量确认自动" + history;
|
||||
}
|
||||
}
|
||||
else if (ignoreRiskWarning)
|
||||
{
|
||||
history = "风控预警确认-" + history;
|
||||
}
|
||||
//进入交易审批流程
|
||||
if (hasTradeProcess && !isSkipApproval)
|
||||
{
|
||||
@@ -609,6 +622,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
public List<int> confirmedTradeIds;
|
||||
public List<string> changeConfirmPaths;
|
||||
public string errorMsg;
|
||||
public string tipMsg;
|
||||
public string type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using YLErp.BLL;
|
||||
using YLErp.BLL.Eod;
|
||||
using YLErp.Model;
|
||||
@@ -792,6 +792,10 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string ErrorMsg;
|
||||
/// <summary>
|
||||
/// 提示信息
|
||||
/// </summary>
|
||||
public string TipMsg;
|
||||
|
||||
/// <summary>
|
||||
/// 变更确认书路径
|
||||
@@ -831,6 +835,10 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// </summary>
|
||||
FundStatus,
|
||||
/// <summary>
|
||||
/// 风控预警二次确认
|
||||
/// </summary>
|
||||
RiskWarning,
|
||||
/// <summary>
|
||||
/// 无审核权限
|
||||
/// </summary>
|
||||
NoRight
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
@@ -25,6 +25,8 @@ namespace YLErp.BLL
|
||||
/// </summary>
|
||||
public const string LackOfMoney = "LackOfMoney";
|
||||
|
||||
public const string RiskWarningConfirm = "RiskWarningConfirm";
|
||||
|
||||
public const string IgnoreBreak = "IgnoreBreak";
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2456,18 +2456,30 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
var config = valuedateBLL.SystemDate;
|
||||
var ignoreMoneyCheck = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.LackOfMoney;
|
||||
var ignoreRiskWarning = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.RiskWarningConfirm;
|
||||
var isSkipApproval = false;
|
||||
if (CurUser.交易管理_特批批量确认 && isSkipCheck)
|
||||
{
|
||||
ignoreMoneyCheck = true;
|
||||
isSkipApproval = true;
|
||||
}
|
||||
var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval);
|
||||
var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval, ignoreRiskWarning);
|
||||
|
||||
//如果客户缺少资金而操作者有交易特批权限
|
||||
if (!ignoreMoneyCheck && result.LackOfMoney)
|
||||
if (!ignoreMoneyCheck && !ignoreRiskWarning && result.LackOfMoney)
|
||||
{
|
||||
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = PS.Config.ErpElement.Company == Configuration.CompanyEnum.天风 || config.SpecialOperateForTrade == 1 ? tradeBLL.LackOfMoney : "", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });
|
||||
if (result.type == TradeOpenRetCode.RiskWarning.ToString())
|
||||
{
|
||||
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = tradeBLL.RiskWarningConfirm, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });
|
||||
}
|
||||
if (result.type == TradeOpenRetCode.FundStatus.ToString())
|
||||
{
|
||||
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = "", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });
|
||||
}
|
||||
if (result.type == TradeOpenRetCode.LackOfMoney.ToString() || ((PS.Config.ErpElement.Company == Configuration.CompanyEnum.天风 || config.SpecialOperateForTrade == 1) && string.IsNullOrWhiteSpace(result.type)))
|
||||
{
|
||||
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = tradeBLL.LackOfMoney, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });
|
||||
}
|
||||
}
|
||||
|
||||
//生成交易确认书
|
||||
@@ -2475,7 +2487,7 @@ namespace YLErp.Web.Controllers
|
||||
{
|
||||
new TradeContractGenerateService(CurUser).GenerateContractsAsync(result.confirmedTradeIds, "确认书");
|
||||
}
|
||||
|
||||
//有ErrorMsg则认为失败
|
||||
if (!string.IsNullOrEmpty(result.errorMsg))
|
||||
{
|
||||
if (result.changeConfirmPaths?.Count > 0)
|
||||
@@ -2486,11 +2498,12 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var successMsg = string.IsNullOrWhiteSpace(result.tipMsg) ? "操作完成" : result.tipMsg;
|
||||
if (result.changeConfirmPaths?.Count > 0)
|
||||
{
|
||||
return JsonSuccess("操作完成", new { generateChangeSuccess = true, url = result.changeConfirmPaths });
|
||||
return JsonSuccess(successMsg, new { generateChangeSuccess = true, url = result.changeConfirmPaths });
|
||||
}
|
||||
return JsonSuccess("操作完成");
|
||||
return JsonSuccess(successMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8396,6 +8409,20 @@ namespace YLErp.Web.Controllers
|
||||
return JsonSuccess();
|
||||
}
|
||||
|
||||
public JsonResult SaveRiskWarningDecisionLog(int quotaTrialId, string decision)
|
||||
{
|
||||
try
|
||||
{
|
||||
new TradeRiskCheckLogService(CurUser).AddWarningDecisionLog(quotaTrialId, decision);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFactory.GetLogger("SaveRiskWarningDecisionLog").Error(ex);
|
||||
return JsonError("保存风控预警处理结果失败!");
|
||||
}
|
||||
return JsonSuccess();
|
||||
}
|
||||
|
||||
[MyAuthorize("风险控制-接口审批列表")]
|
||||
public ActionResult InterfaceApproval()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const inputFormatDouble2 = Object.freeze({ precision: 2, append: '' });
|
||||
const inputFormatDouble2 = Object.freeze({ precision: 2, append: '' });
|
||||
|
||||
function deletetrade(id) { //无效化
|
||||
main.confirm(page.ConfirmInfo, function () {
|
||||
@@ -85,6 +85,34 @@ var confirmFunc = function (id, additionalProcessing) {
|
||||
}
|
||||
if (data.obj && data.obj.proccessType === "AdditionalProcessing") {
|
||||
if (data.obj.TrialDataId) {
|
||||
var isRiskWarningConfirm = data.obj.type === "RiskWarningConfirm";
|
||||
var additionalProcessingType = isRiskWarningConfirm ? "RiskWarningConfirm" : "LackOfMoney";
|
||||
var saveQuotaTrial = function (obj) {
|
||||
var saveSuccess = true;
|
||||
if (obj.IsTF && (!obj.Data.Remark || obj.Data.Remark.length <= 0)) {
|
||||
main.message("必须填写说明内容,才可以录入交易");
|
||||
return false;
|
||||
}
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
saveSuccess = false;
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
return saveSuccess;
|
||||
};
|
||||
var saveRiskWarningDecisionLog = function (quotaTrialId, decision) {
|
||||
var saveSuccess = true;
|
||||
main.post("/trade/SaveRiskWarningDecisionLog", { quotaTrialId: quotaTrialId, decision: decision }, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
saveSuccess = false;
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
return saveSuccess;
|
||||
};
|
||||
var layerSetting = {
|
||||
type: 2,
|
||||
title: "提示",
|
||||
@@ -94,31 +122,23 @@ var confirmFunc = function (id, additionalProcessing) {
|
||||
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
|
||||
yes: function (index) {
|
||||
var obj = window["layui-layer-iframe" + index].page;
|
||||
if (obj.IsTF) {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
layer.close(layerIndex);
|
||||
confirmFunc(id, "LackOfMoney");
|
||||
if (!saveQuotaTrial(obj)) {
|
||||
return;
|
||||
}
|
||||
if (isRiskWarningConfirm) {
|
||||
if (!saveRiskWarningDecisionLog(obj.Data.id, "确认通过")) {
|
||||
return;
|
||||
}
|
||||
main.message("必须填写说明内容,才可以录入交易");
|
||||
} else {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
layer.close(layerIndex);
|
||||
confirmFunc(id, "LackOfMoney");
|
||||
}
|
||||
layer.close(layerIndex);
|
||||
confirmFunc(id, additionalProcessingType);
|
||||
},
|
||||
cancel: function () {
|
||||
var iframeWindow = window["layui-layer-iframe" + this.index];
|
||||
var pageObj = iframeWindow && iframeWindow.page;
|
||||
if (isRiskWarningConfirm && pageObj && pageObj.Data && pageObj.Data.id) {
|
||||
saveRiskWarningDecisionLog(pageObj.Data.id, "取消不通过");
|
||||
}
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
window.parent.reloadtrade();
|
||||
}
|
||||
@@ -128,8 +148,10 @@ var confirmFunc = function (id, additionalProcessing) {
|
||||
if (data.obj.type === "LackOfMoney") {
|
||||
layerSetting.btn = [page.buttonStr, '取消'];
|
||||
}
|
||||
// 只展示“资金状况”的情况
|
||||
if (data.obj.typecode === "FundStatus") {
|
||||
else if (isRiskWarningConfirm) {
|
||||
layerSetting.btn = ["交易特批", '取消'];
|
||||
}
|
||||
else if (data.obj.typecode === "FundStatus") {
|
||||
layerSetting.btn = ["确认", '取消'];
|
||||
}
|
||||
|
||||
@@ -167,6 +189,14 @@ var confirmFunc = function (id, additionalProcessing) {
|
||||
});
|
||||
});
|
||||
}
|
||||
if (data.msg && data.msg !== "操作完成") {
|
||||
main.alert(data.msg, function () {
|
||||
if (blClose) {
|
||||
closetradeWindow();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (blClose) {
|
||||
closetradeWindow();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user