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:
尹峰
2026-06-26 17:15:29 +08:00
14 changed files with 486 additions and 353 deletions
@@ -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&lt;RiskContext, bool&gt;(内部通过 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}");
}
// 生成可调用委托
+204 -155
View File
@@ -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>
/// 判断某个范围字段是否为空。
/// 为空表示该维度不限制。
+4 -14
View File
@@ -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; }
}
-1
View File
@@ -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
+3 -1
View File
@@ -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>
+33 -6
View File
@@ -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()
{
+53 -23
View File
@@ -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();
}