!21 feature/p132_74-risk-engine VS glms/feature/1.4.2

Merge pull request !21 from feature/p132_74-risk-engine
This commit is contained in:
尹峰
2026-07-17 08:17:47 +00:00
committed by Gitee
14 changed files with 2235 additions and 99 deletions
@@ -7,7 +7,7 @@ CREATE TABLE `glms_risk_rule` (
`ConditionJson` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '条件JSON(结构化模式, RuleCondition数组)',
`RuleExpr` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '规则表达式(自由文本模式, 类C#表达式)',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`Status` tinyint NOT NULL DEFAULT 2 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
`OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称',
@@ -23,7 +23,7 @@ CREATE TABLE `glms_risk_rule_application` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`RuleIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '关联规则Id列表(逗号分隔)',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`Status` tinyint NOT NULL DEFAULT 2 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`ControlStrategy` tinyint NOT NULL COMMENT '控制策略: 1=Block, 2=Approval, 3=Warning',
`TriggerPoints` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发时点(逗号分隔)',
`ScopeAssetBookIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用账户Id列表',
@@ -168,9 +168,9 @@ namespace YLErp.Modules.RiskEngine
}
catch (Exception ex)
{
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
// 脚本执行异常(如空引用、类型转换失败)交由执行层按阻断处理
_logger.Error($"规则执行异常 - RuleId: {ruleIdText}, Error: {ex.Message}\n脚本代码:{scriptCode}");
return false;
throw;
}
};
}
@@ -14,4 +14,19 @@ namespace YLErp.Modules.RiskEngine.Dto
/// <summary>错误信息</summary>
public string ErrorMessage { get; set; }
}
/// <summary>
/// 批量删除变量结果
/// </summary>
public class BatchDeleteVariablesResult : BatchOperationResult
{
/// <summary>成功删除的变量 ID</summary>
public List<long> DeletedIds { get; set; }
/// <summary>不存在的变量 ID</summary>
public List<long> MissingIds { get; set; }
/// <summary>因被生效规则引用而跳过的变量 ID</summary>
public List<long> BlockedIds { get; set; }
/// <summary>被跳过变量的原因,Key 为变量 ID</summary>
public Dictionary<long, string> BlockedReasons { get; set; }
}
}
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using Qdp.Foundation.Utilities;
using System.Reflection;
using YLErp.BLL;
@@ -309,11 +309,14 @@ namespace YLErp.Modules.RiskEngine
try
{
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
if (context != null && context.DbContext == null)
{
context.DbContext = DbContext;
}
using var ruleDbContext = DbContextFactory.GetYLDbContext();
ruleDbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
context ??= new RiskContext();
context.TriggerPoint = triggerPoint;
context.DbContext = ruleDbContext;
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context.TradeId}, TriggerPoint: {triggerPoint}");
// ============================================================
// Step 1: 从内存缓存读取规则定义和规则应用(启动时已预热)
@@ -343,8 +346,8 @@ namespace YLErp.Modules.RiskEngine
// 2. 同一维度内多选按并集处理;
// 3. 不同维度之间按交集处理;
// 4. 某维度留空表示该维度不限制。
var trade = context != null && context.TradeId > 0
? DbContext.trade.FirstOrDefault(t => t.id == context.TradeId)
var trade = context.TradeId > 0
? context.DbContext.trade.AsNoTracking().FirstOrDefault(t => t.id == context.TradeId)
: null;
var matchedApplications = triggerMatchedApps
@@ -402,7 +405,9 @@ namespace YLErp.Modules.RiskEngine
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (!compileResult.Success)
{
_logger.Info($"[风控引擎] 规则编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
var errorMessage = $"规则[{rule.RuleName}]编译失败,已按阻断处理,请检查规则表达式配置:{compileResult.ErrorMessage}";
_logger.Info($"[风控引擎] 规则编译失败,按阻断处理 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
AddBlockError(result, rule.Id.ToString(), rule.RuleName, rule.RuleText, errorMessage);
continue;
}
@@ -420,7 +425,9 @@ namespace YLErp.Modules.RiskEngine
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
var errorMessage = $"规则[{rule.RuleName}]执行异常:{ex.Message}";
_logger.Error($"[风控引擎] 规则执行异常,按阻断处理 - RuleId: {rule.Id}, Error: {ex.Message}");
AddBlockError(result, ruleId, rule.RuleName, rule.RuleText, errorMessage);
continue;
}
@@ -499,21 +506,31 @@ namespace YLErp.Modules.RiskEngine
}
catch (Exception ex)
{
result.NeedApproval = true;
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = "ENGINE_ERROR",
RuleName = "风控引擎执行异常",
RuleText = ex.Message,
Message = $"风控引擎异常:{ex.Message}"
});
var errorMessage = $"风控引擎异常:{ex.Message}";
AddBlockError(result, "ENGINE_ERROR", "风控引擎执行异常", ex.Message, errorMessage);
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
}
return result;
}
private static void AddBlockError(RiskResult result, string ruleId, string ruleName, string ruleText, string errorMessage)
{
result.Blocked = true;
result.Passed = false;
result.ErrorMessage = string.IsNullOrWhiteSpace(result.ErrorMessage)
? errorMessage
: $"{result.ErrorMessage}{errorMessage}";
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleId = ruleId,
RuleName = ruleName,
ControlStrategy = RiskControlStrategy.Block,
RuleText = ruleText,
Message = errorMessage
});
}
/// <summary>
/// 从数据库加载规则列表
/// </summary>
@@ -706,8 +723,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000012,
// RuleName = "债券类净价偏离(本地)",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiNetNoFeePrice对应债券类标的期初交割净价,库内为 1 左右原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期上一收盘日 net_price,库内为 100 左右报价。计算逻辑:按 ABS(PosiNetNoFeePrice×100-net_price) 计算绝对价差,价差大于 5 元时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiNetNoFeePrice.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().net_price.Value) > 5m",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、PosiDirection=2 且有标的代码的浮动支付端 PosiNetNoFeePrice 和 UnderlyingCodePosiNetNoFeePrice 对应债券类标的期初交割净价,库内为 1 左右原值;通过 DbContext.china_bond_valuation 按该浮动支付端标的和交易日前日期优先取 credibility=1 的上一收盘日 net_price,库内为 100 左右报价。计算逻辑:按 ABS(PosiNetNoFeePrice×100-net_price) 计算绝对价差,价差大于 5 元时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiNetNoFeePrice.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderBy(v => v.credibility).ThenByDescending(v => v.valuation_date).First().net_price.Value) > 5m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -722,8 +739,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000013,
// RuleName = "债券类收益率偏离(本地)",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 InitYtm对应债券类标的期初成交收益率,库内为原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期上一收盘日 yield,库内为 1.5 到 2.2 左右百分数。计算逻辑:按 ABS(InitYtm×100-yield) 计算收益率绝对差,差值大于 1 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).InitYtm.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value) > 1m",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、PosiDirection=2 且有标的代码的浮动支付端 InitYtm 和 UnderlyingCodeInitYtm 对应债券类标的期初成交收益率,库内为原值;通过 DbContext.china_bond_valuation 按该浮动支付端标的和交易日前日期优先取 credibility=1 的上一收盘日 yield,库内为 1.5 到 2.2 左右百分数。计算逻辑:按 ABS(InitYtm×100-yield) 计算收益率绝对差,差值大于 1 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).InitYtm.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderBy(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value) > 1m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -738,8 +755,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000014,
// RuleName = "非债券类价格偏离(本地)",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiGrossPrice对应普通收益互换页面填写的期初标的价格,库内为 1 左右原值;通过 DbContext.eod_commodity_future_price 按当前交易标的和交易日前日期取上一日收盘价 ClosePrice。计算逻辑:按 ABS(PosiGrossPrice×100-ClosePrice) 计算绝对价差,价差大于 5 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiGrossPrice * 100m - Convert.ToDecimal(DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).First().ClosePrice)) > 5m",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、PosiDirection=2 且有标的代码的浮动支付端 PosiGrossPrice 和 UnderlyingCodePosiGrossPrice 对应普通收益互换页面填写的期初标的价格,库内为 1 左右原值;通过 DbContext.eod_commodity_future_price 按该浮动支付端标的和交易日前日期取上一日收盘价 ClosePrice。注意:eod_commodity_future_price 模型属性 UnderlyingCode 实际映射数据库列 FutureContractId,数据库排查时应使用 FutureContractId 与 swap_position.UnderlyingCode 关联。计算逻辑:按 ABS(PosiGrossPrice×100-ClosePrice) 计算绝对价差,价差大于 5 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiGrossPrice * 100m - Convert.ToDecimal(DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).First().ClosePrice)) > 5m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -754,8 +771,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000015,
// RuleName = "单一交易对手累计标的数量超阈值(本地)",
// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手存续/审批中交易对应的 swap_position.UnderlyingCode 去重数量。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。",
// RuleExpr= DbContext.swap_position.Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) && p.IsInitial && !p.Invalid && DbContext.trade.Any(t => t.id == p.SwapTradeId && t.ValidState != "InValid" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == "审批中"))).Select(p => p.UnderlyingCode).Distinct().Count() > 10
// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手有效交易对应的实时存续持仓 swap_position.UnderlyingCode 去重数量。实时存续持仓口径:IsInitial=false、PosiQuantity>0、Invalid=false、PosiDirection>0 且 UnderlyingCode 非空。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。",
// RuleExpr= DbContext.swap_position.Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) && !p.IsInitial && p.PosiQuantity > 0 && !p.Invalid && p.PosiDirection > 0 && DbContext.trade.Any(t => t.id == p.SwapTradeId && t.ValidState != "InValid" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId)).Select(p => p.UnderlyingCode).Distinct().Count() > 10
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -770,8 +787,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000016,
// RuleName = "多头支付固定端利率偏离(本地)",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取利息端支付方向记录的 InterestRateDefault,对应互换交易-利息端-利率文本框数值;通过 DbContext.eod_commodity_future_price 按 FutureContractId=FR007(模型属性 UnderlyingCode)和交易日前日期取最近一条 ClosePrice。计算逻辑:按 ABS(InterestRateDefault-FR007) 计算利率差,差值小于 5% 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.InterestDirection == 2).InterestRateDefault - Convert.ToDecimal(DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == "FR007" && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).First().ClosePrice)) < 0.05m",
// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取利息端收入固定利息方向记录的 InterestRateDefault。InterestRateDefault 只代表利率文本框中 + 号后的点差,不包含 FR007 基准利率,库内为小数原值,界面按百分比显示。计算逻辑:按 ABS(InterestRateDefault×100) 计算点差百分比绝对值,绝对值小于 5 时触发审批。",
// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.InterestDirection == 1).InterestRateDefault * 100m) < 5m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -27,6 +27,11 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public bool ShowTip { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 触发的规则列表
/// </summary>
+260 -23
View File
@@ -1,5 +1,7 @@
using BaseOUDAL;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
@@ -105,9 +107,10 @@ using YLErp.Modules.RiskEngine.Dto;
DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除)
GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
── 审计日志(3 个) ───────────────────────────────────────────────
── 审计日志(4 个) ───────────────────────────────────────────────
QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页)
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出)
BuildAuditLogExportFileName(...) 根据查询条件生成导出文件名
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志 Excel(ID 保持整数文本、类型中文映射)
GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData
── 私有辅助方法 ───────────────────────────────────────────────────
@@ -180,7 +183,7 @@ using YLErp.Modules.RiskEngine.Dto;
1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用
2. 规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication(),变量变更不触发引擎缓存刷新
3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted
4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代
4. ExportAuditLogs 按查询条件导出,并生成包含查询条件的文件名
5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
================================================================================
@@ -291,6 +294,7 @@ namespace YLErp.Modules.RiskEngine
var sanitized = Regex.Replace(value.Trim(), @"[\\/:*?""<>|\r\n]+", "_");
return sanitized.Length <= 40 ? sanitized : sanitized.Substring(0, 40);
}
/// <summary>
/// 写入审计日志
/// </summary>
@@ -801,9 +805,10 @@ namespace YLErp.Modules.RiskEngine
if (req.VariableId.HasValue)
{
var varIdStr = req.VariableId.Value.ToString();
query = query.Where(r => (r.ConditionJson != null && r.ConditionJson.Contains(varIdStr))
|| (r.RuleExpr != null && r.RuleExpr.Contains(varIdStr)));
var referencedRuleIds = GetRulesByVariableId(req.VariableId.Value)
.Select(r => (long)r.id)
.ToList();
query = query.Where(r => referencedRuleIds.Contains(r.id));
}
var result = query.OrderByDescending(r => r.UpdateDate)
@@ -822,7 +827,8 @@ namespace YLErp.Modules.RiskEngine
UpdateOptName = r.UpdateOptName,
UpdateDate = r.UpdateDate ?? r.OptDate.GetValueOrDefault(),
ApplicationCount = DbContext.glms_risk_rule_application
.Count(a => a.RuleIds != null &&
.Count(a => a.Status != RiskRuleStatus.Deleted &&
a.RuleIds != null &&
(a.RuleIds == r.id.ToString() ||
a.RuleIds.StartsWith(r.id.ToString() + ",") ||
a.RuleIds.EndsWith("," + r.id.ToString()) ||
@@ -841,6 +847,7 @@ namespace YLErp.Modules.RiskEngine
var applications = DbContext.glms_risk_rule_application
.Where(a => a.Status != RiskRuleStatus.Deleted)
.Where(RuleIdsMatchExpr(ruleId))
.Select(a => new RiskRuleApplicationSummary
{
@@ -1946,6 +1953,183 @@ namespace YLErp.Modules.RiskEngine
).ToList();
}
/// <summary>
/// 根据结构化 ConditionJson 和当前变量池定义重新生成可执行 RuleExpr。
/// ConditionJson 是结构化规则的事实来源,避免通过字符串 Replace 级联修改表达式。
/// </summary>
private string BuildRuleExprFromConditionJson(string conditionJson)
{
List<RuleCondition> conditions;
try
{
conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(conditionJson);
}
catch
{
throw new ServiceException("公式表达式 JSON 格式不合法");
}
if (conditions == null || conditions.Count == 0)
throw new ServiceException("公式条件列表不能为空");
var variableIds = conditions
.Select(c => c.VariableId)
.Concat(conditions.Where(c => c.ThresholdVariableId.HasValue)
.Select(c => c.ThresholdVariableId.Value))
.Distinct()
.ToList();
var variables = DbContext.glms_risk_variable
.Where(v => variableIds.Contains(v.id))
.ToList()
.ToDictionary(v => (long)v.id);
var missingVariableIds = variableIds.Where(id => !variables.ContainsKey(id)).ToList();
if (missingVariableIds.Any())
throw new ServiceException($"公式引用的变量不存在:{string.Join(",", missingVariableIds)}");
var conditionExpressions = conditions
.Select((condition, index) => BuildConditionExpression(condition, variables, index + 1))
.ToList();
return string.Join(" && ", conditionExpressions.Select(expr => $"({expr})"));
}
private string BuildConditionExpression(
RuleCondition condition,
IReadOnlyDictionary<long, glms_risk_variable> variables,
int conditionIndex)
{
var variable = variables[condition.VariableId];
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
throw new ServiceException($"条件{conditionIndex}:变量'{variable.VariableName}'的取值表达式为空");
if (!ValidOperatorsByType.TryGetValue(variable.DataType, out var validOperators) ||
!validOperators.Contains(condition.Operator))
{
throw new ServiceException($"条件{conditionIndex}:操作符'{condition.Operator}'不适用于{GetDataTypeName(variable.DataType)}类型变量");
}
if (variable.DataType == RiskVariableDataType.Boolean)
{
return condition.Operator switch
{
"是" => $"({variable.VariableExpr}) == true",
"否" => $"({variable.VariableExpr}) == false",
_ => throw new ServiceException($"条件{conditionIndex}:不支持的布尔操作符'{condition.Operator}'")
};
}
var leftExpression = BuildComparableExpression(variable);
if (condition.Operator == "介于" || condition.Operator == "不介于")
{
if (!string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
throw new ServiceException($"条件{conditionIndex}:介于/不介于暂只支持固定上下限");
if (condition.Value is not JArray range || range.Count != 2)
throw new ServiceException($"条件{conditionIndex}:介于/不介于的阈值必须为双元素数组");
var lowerExpression = BuildFixedValueExpression(range[0], variable.DataType, conditionIndex);
var upperExpression = BuildFixedValueExpression(range[1], variable.DataType, conditionIndex);
var lowerOperator = condition.IncludeLowerBound ? ">=" : ">";
var upperOperator = condition.IncludeUpperBound ? "<=" : "<";
var rangeExpression = $"(({leftExpression}) {lowerOperator} ({lowerExpression}) && ({leftExpression}) {upperOperator} ({upperExpression}))";
return condition.Operator == "不介于" ? $"!{rangeExpression}" : rangeExpression;
}
var rightExpression = BuildThresholdExpression(condition, variable.DataType, variables, conditionIndex);
var comparisonOperator = GetComparisonOperator(condition.Operator, conditionIndex);
return $"({leftExpression}) {comparisonOperator} ({rightExpression})";
}
private static string BuildComparableExpression(glms_risk_variable variable)
{
return variable.DataType == RiskVariableDataType.Numeric
? $"Convert.ToDecimal(({variable.VariableExpr}))"
: variable.VariableExpr;
}
private string BuildThresholdExpression(
RuleCondition condition,
RiskVariableDataType dataType,
IReadOnlyDictionary<long, glms_risk_variable> variables,
int conditionIndex)
{
if (string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
return BuildFixedValueExpression(condition.Value, dataType, conditionIndex);
if (string.Equals(condition.ThresholdType, "Variable", StringComparison.Ordinal))
{
if (!condition.ThresholdVariableId.HasValue ||
!variables.TryGetValue(condition.ThresholdVariableId.Value, out var thresholdVariable))
{
throw new ServiceException($"条件{conditionIndex}:阈值变量不存在");
}
if (thresholdVariable.DataType != dataType)
throw new ServiceException($"条件{conditionIndex}:阈值变量与条件变量的数据类型不一致");
if (string.IsNullOrWhiteSpace(thresholdVariable.VariableExpr))
throw new ServiceException($"条件{conditionIndex}:阈值变量'{thresholdVariable.VariableName}'的取值表达式为空");
return BuildComparableExpression(thresholdVariable);
}
throw new ServiceException($"条件{conditionIndex}:阈值类型'{condition.ThresholdType}'不合法,仅支持 Fixed/Variable");
}
private static string BuildFixedValueExpression(object value, RiskVariableDataType dataType, int conditionIndex)
{
if (value == null)
throw new ServiceException($"条件{conditionIndex}:固定阈值不能为空");
var rawValue = value is JValue jsonValue
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
: Convert.ToString(value, CultureInfo.InvariantCulture);
if (dataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(rawValue, NumberStyles.Number | NumberStyles.AllowExponent,
CultureInfo.InvariantCulture, out var numericValue))
{
throw new ServiceException($"条件{conditionIndex}:数值型阈值必须为数字");
}
return numericValue.ToString(CultureInfo.InvariantCulture) + "m";
}
if (dataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(rawValue, CultureInfo.InvariantCulture,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.RoundtripKind, out var dateValue) &&
!DateTime.TryParse(rawValue, out dateValue))
{
throw new ServiceException($"条件{conditionIndex}:日期型阈值必须为合法日期");
}
return $"new DateTime({dateValue.Ticks}L, DateTimeKind.{dateValue.Kind})";
}
throw new ServiceException($"条件{conditionIndex}:不支持的数据类型");
}
private static string GetComparisonOperator(string ruleOperator, int conditionIndex)
{
return ruleOperator switch
{
">" => ">",
"<" => "<",
">=" => ">=",
"<=" => "<=",
"=" => "==",
"≠" => "!=",
"早于" => "<",
"晚于" => ">",
"等于" => "==",
"不早于" => ">=",
"不晚于" => "<=",
_ => throw new ServiceException($"条件{conditionIndex}:不支持的操作符'{ruleOperator}'")
};
}
#region Variable Management
/// <summary>
@@ -1995,11 +2179,8 @@ namespace YLErp.Modules.RiskEngine
{
var variable = GetVariableOrThrow(variableId);
var varExpr = variable.VariableExpr;
var referenceCount = DbContext.glms_risk_rule
.Count(r => r.Status == RiskRuleStatus.Active &&
((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) ||
(r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr))));
var referenceCount = GetRulesByVariableId(variableId)
.Count(r => r.Status == RiskRuleStatus.Active);
return new RiskVariableDetail
{
@@ -2123,14 +2304,18 @@ namespace YLErp.Modules.RiskEngine
.Where(r => r.RuleExpr != null)
.ToList();
var compileTasks = affectedRules.Select(r =>
Task.Run(() => RuleCompiler.ValidateAndCompileFormula(
r.id, r.RuleExpr.Replace(oldVariableExpr, req.VariableExpr)))
);
var compileResults = Task.WhenAll(compileTasks).GetAwaiter().GetResult();
var failed = compileResults.FirstOrDefault(r => !r.Success);
if (failed is not null)
throw new ServiceException($"变量表达式变更导致规则编译失败:{failed.ErrorMessage}");
var rebuiltRuleExpressions = new Dictionary<long, string>();
foreach (var rule in affectedRules)
{
var rebuiltRuleExpr = BuildRuleExprFromConditionJson(rule.ConditionJson);
ValidateRuleExpr(rebuiltRuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rebuiltRuleExpr);
if (!compileResult.Success)
throw new ServiceException($"变量表达式变更导致规则'{rule.RuleName}'(ID:{rule.id})编译失败:{compileResult.ErrorMessage}");
rebuiltRuleExpressions[rule.id] = rebuiltRuleExpr;
}
using (var transaction = DbContext.Database.BeginTransaction())
{
@@ -2138,7 +2323,7 @@ namespace YLErp.Modules.RiskEngine
{
foreach (var rule in affectedRules)
{
rule.RuleExpr = rule.RuleExpr.Replace(oldVariableExpr, req.VariableExpr);
rule.RuleExpr = rebuiltRuleExpressions[rule.id];
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
@@ -2198,6 +2383,58 @@ namespace YLErp.Modules.RiskEngine
InvalidateVariableCache();
}
/// <summary>
/// 批量删除变量(不存在或被生效规则引用的变量将被跳过)
/// </summary>
public BatchDeleteVariablesResult BatchDeleteVariables(List<long> variableIds)
{
if (variableIds == null || variableIds.Count == 0)
throw new ServiceException("变量ID列表不能为空");
var distinctIds = variableIds.Distinct().ToList();
var variables = DbContext.glms_risk_variable
.Where(v => distinctIds.Contains(v.id))
.ToList();
var foundIds = variables.Select(v => (long)v.id).ToList();
var missingIds = distinctIds.Except(foundIds).ToList();
var blockedIds = new List<long>();
var blockedReasons = new Dictionary<long, string>();
var deletedIds = new List<long>();
foreach (var variable in variables)
{
var activeRefCount = GetRulesByVariableId(variable.id)
.Count(r => r.Status == RiskRuleStatus.Active);
if (activeRefCount > 0)
{
blockedIds.Add(variable.id);
blockedReasons[variable.id] = $"该变量被 {activeRefCount} 个生效中的规则引用,无法删除";
continue;
}
DbContext.glms_risk_variable.Remove(variable);
WriteAuditLog("VAR_BATCH_DELETE", "VARIABLE", variable.id, variable.VariableName, $"批量删除变量:{variable.VariableName}");
deletedIds.Add(variable.id);
}
if (deletedIds.Count > 0)
{
DbContext.SaveChanges();
InvalidateVariableCache();
}
return new BatchDeleteVariablesResult
{
Success = true,
TotalCount = distinctIds.Count,
SuccessCount = deletedIds.Count,
DeletedIds = deletedIds,
MissingIds = missingIds,
BlockedIds = blockedIds,
BlockedReasons = blockedReasons
};
}
/// <summary>
/// 获取所有变量列表(供下拉选择)
/// </summary>
@@ -2314,13 +2551,13 @@ namespace YLErp.Modules.RiskEngine
}
var operationTypeNames = operationTypes
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAuditOperationTypeName)
.Select(type => SanitizeFileNamePart(GetAuditOperationTypeName(type)))
.ToList();
if (operationTypeNames.Any())
conditions.Add($"操作类型-{string.Join("+", operationTypeNames)}");
if (!string.IsNullOrWhiteSpace(req.TargetType))
conditions.Add($"目标类型-{GetAuditTargetTypeName(req.TargetType)}");
conditions.Add($"目标类型-{SanitizeFileNamePart(GetAuditTargetTypeName(req.TargetType))}");
if (!string.IsNullOrWhiteSpace(req.TargetName))
conditions.Add($"目标名称-{SanitizeFileNamePart(req.TargetName)}");
if (!string.IsNullOrWhiteSpace(req.OptName))
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ using Qdp.Foundation.Utilities;
using Qdp.Pricing.Ecosystem.Trade.FixedIncome;
using Qdp.Pricing.Library.Base.Utilities;
using Qdp.Pricing.Library.Common.Products.Rates;
using Snowflake.Core;
using System.Collections.Generic;
using System.Data;
using System.Linq;
@@ -57,6 +58,7 @@ namespace YLErp.Modules.RiskModule
{
public class QuotaMonitorService : YLBaseService
{
private const string RiskCheckTriggerRemark = "触发风控";
IYcLogger _logger = LogFactory.GetLogger("QuotaMonitorService");
static QuotaMonitorService()
{
@@ -4500,11 +4502,6 @@ namespace YLErp.Modules.RiskModule
}
return true;
}
var lastTrial = trialService.QueryLastQuotaTrial(tradeId, true);
if (quotaObj.TrialStatus == QuotaTrialStatusEnum.Success)
{
new TradeRiskCheckLogService(UserInfo).AddLog(quotaObj);
}
//否则的情况是上次没算,这次是预警,或上次算了,结果是不通过\通过或预警,这次是预警或不通过,提示用户;
res.RetCode = TradeOpenRetCode.QuotaTrialError;
res.TrialDataId = quotaObj.id;
@@ -4544,6 +4541,10 @@ namespace YLErp.Modules.RiskModule
}
}
res.RetCode = isRiskApprovalWarning ? TradeOpenRetCode.RiskWarning : TradeOpenRetCode.QuotaTrialError;
var isOldRiskErrorSpecialApproval = !isRiskApprovalWarning
&& quotaObj.OldRiskTrialStatus == QuotaTrialStatusEnum.Error
&& res.OldRiskNeedSpecialApproval;
AddRiskCheckLog(quotaObj, isOldRiskErrorSpecialApproval);
return false;
}
@@ -4935,8 +4936,64 @@ namespace YLErp.Modules.RiskModule
return result;
}
private DateTime? QueryLatestRiskCheckTriggerTime(int tradeId)
{
var tradeNumber = DbContext.trade.Where(x => x.id == tradeId).Select(x => x.TradeNumber).FirstOrDefault();
if (string.IsNullOrWhiteSpace(tradeNumber))
{
return null;
}
return DbContext.trade_risk_check_log
.Where(x => x.trade_number == tradeNumber && x.remark.StartsWith(RiskCheckTriggerRemark))
.OrderByDescending(x => x.create_time)
.Select(x => (DateTime?)x.create_time)
.FirstOrDefault();
}
private void AddRiskCheckLog(QuotaTrial quotaTrial, bool isOldRiskErrorSpecialApproval = false)
{
var td = DbContext.trade.FirstOrDefault(x => x.id == quotaTrial.TradeId);
var 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;
var riskWarning = string.Empty;
if (!string.IsNullOrWhiteSpace(quotaTrial.QuotaWarningDetails))
{
riskWarning = "限额预警: " + quotaTrial.QuotaWarningDetails;
}
if (!string.IsNullOrWhiteSpace(quotaTrial.RiskWarningDetails))
{
if (!string.IsNullOrWhiteSpace(riskWarning))
{
riskWarning += Environment.NewLine;
}
riskWarning += "风险预警: " + quotaTrial.RiskWarningDetails;
}
log.risk_warning = riskWarning;
log.limit_warning = quotaTrial.QuotaCheckDetails;
log.remark = RiskCheckTriggerRemark;
if (isOldRiskErrorSpecialApproval)
{
log.remark += ";老风控Error允许交易特批";
}
log.create_user = UserId;
log.create_time = DateTime.Now;
DbContext.trade_risk_check_log.Add(log);
DbContext.SaveChanges();
}
/// <summary>
/// 按 quotaTrial.id 校验试算结果是否已超过配置时效
/// 按 quotaTrial.id 校验最近一次触发风控的记录是否已超过配置时效
/// </summary>
/// <param name="trialDataId">quotaTrial.id</param>
/// <param name="expireSeconds">有效时长(秒)</param>
@@ -4944,11 +5001,17 @@ namespace YLErp.Modules.RiskModule
public bool IsQuotaTrialExpired(int trialDataId, int expireSeconds)
{
var quotaTrial = QueryQuotaTrial(trialDataId);
if (quotaTrial == null || !quotaTrial.OptDate.HasValue)
if (quotaTrial == null)
{
return true;
}
return DateTime.Now - quotaTrial.OptDate.Value > TimeSpan.FromSeconds(expireSeconds);
var latestRiskCheckTriggerTime = QueryLatestRiskCheckTriggerTime(quotaTrial.TradeId);
var riskCheckTime = latestRiskCheckTriggerTime ?? quotaTrial.OptDate;
if (!riskCheckTime.HasValue)
{
return true;
}
return DateTime.Now - riskCheckTime.Value > TimeSpan.FromSeconds(expireSeconds);
}
/// <summary>
@@ -5202,7 +5265,12 @@ 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}\n";
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}";
if (!string.IsNullOrWhiteSpace(triggeredRule.Message))
{
result.RiskWarningDetails += $";信息:{triggeredRule.Message}";
}
result.RiskWarningDetails += "\n";
}
}
if (riskResult.NeedApproval)
@@ -95,13 +95,13 @@ namespace YLErp.Modules.RiskModule
switch ((QuotaTrialStatusEnum)item.trial_result)
{
case QuotaTrialStatusEnum.Warning:
exportModel.trial_result = "预警";
exportModel.trial_result = "老风控触发特批";
break;
case QuotaTrialStatusEnum.RiskWarning:
exportModel.trial_result = "风控需审批";
exportModel.trial_result = "风控需审批";
break;
case QuotaTrialStatusEnum.Error:
exportModel.trial_result = "限额";
exportModel.trial_result = "禁止";
break;
default:
exportModel.trial_result = "通过";
@@ -140,7 +140,20 @@ namespace YLErp.Modules.RiskModule
log.client_name = quotaTrial.ClientName;
log.trader = td?.TraderName;
log.trade_number = quotaTrial.TradeNumber;
log.risk_warning = quotaTrial.QuotaWarningDetails;
var riskWarning = string.Empty;
if (!string.IsNullOrWhiteSpace(quotaTrial.QuotaWarningDetails))
{
riskWarning = "限额预警: " + quotaTrial.QuotaWarningDetails;
}
if (!string.IsNullOrWhiteSpace(quotaTrial.RiskWarningDetails))
{
if (!string.IsNullOrWhiteSpace(riskWarning))
{
riskWarning += Environment.NewLine;
}
riskWarning += "风控预警: " + quotaTrial.RiskWarningDetails;
}
log.risk_warning = riskWarning;
log.limit_warning = quotaTrial.QuotaCheckDetails;
log.remark = quotaTrial.Remark;
log.create_user = UserId;
@@ -1,4 +1,4 @@
using YLErp.Modules.RiskModule;
using YLErp.Modules.RiskModule;
using YLErp.Modules.RiskModule.Dto;
using YLErp.Modules.SwapModule;
@@ -41,8 +41,9 @@ namespace YLErp.Web.Controllers
{
new SelectListItem() {Value="-1",Text="全部",Selected=true},
new SelectListItem() {Value="0",Text="通过"},
new SelectListItem() {Value="1",Text="预警"},
new SelectListItem() {Value="2",Text="限额"}
new SelectListItem() {Value="1",Text="老风控触发特批"},
new SelectListItem() {Value="2",Text="新风控需审批"},
new SelectListItem() {Value="3",Text="禁止"}
};
return list;
}
+25 -1
View File
@@ -685,6 +685,28 @@ namespace YLErp.Web.Controllers
}
}
[HttpDelete("risk-variables/batch")]
[MyAuthorize("风险控制-风控变量删除")]
/// <summary>批量删除变量</summary>
public async Task<JsonResult> BatchDeleteRiskVariables([FromBody] BatchIdsReq req)
{
try
{
var service = GetRiskRuleService();
var result = await Task.Run(() => service.BatchDeleteVariables(req?.Ids));
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "批量删除变量");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
#region Trade Types
@@ -773,7 +795,9 @@ namespace YLErp.Web.Controllers
var service = GetRiskRuleService();
var bytes = await Task.Run(() => service.ExportAuditLogs(req));
var fileName = service.BuildAuditLogExportFileName(req);
return File(bytes, xlsxMimeType, fileName);
var encodedFileName = Uri.EscapeDataString(fileName);
Response.Headers["Content-Disposition"] = $"attachment; filename*=UTF-8''{encodedFileName}";
return File(bytes, xlsxMimeType);
}
catch (ServiceException ex)
{
+23 -2
View File
@@ -2467,8 +2467,8 @@ namespace YLErp.Web.Controllers
var ignoreMoneyCheck = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.LackOfMoney;
var ignoreRiskWarning = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.RiskWarningConfirm;
var ignoreRiskRuleIdArr = StringHelper.ConvertCommaValuesToStringArray(ignoreRiskRuleIds);
// 老风控交易特批与新风控二次审批统一基于 quotaTrial 做超时校验,超时后清空放行标记并重新校验。
// 仅 swapTradeView.js 会回传 trialDataId 并触发 RiskWarningConfirm,按 quotaTrial.OptDate 精确校验;
// 老风控交易特批与新风控二次审批统一基于 trialDataId 做超时校验,超时后清空放行标记并重新校验。
// 当前仅 swapTradeView.js 会回传 trialDataId,按最近一次“触发风控”日志时间校验;查不到日志时回退 quotaTrial.OptDate
// tradeview.js / tradeConfirmList.js / swapTradeConfirmList.js / quotaMonitor.js 只发 LackOfMoney,不传 trialDataId,不进入超时校验。
if (ignoreMoneyCheck || ignoreRiskWarning)
{
@@ -2535,6 +2535,27 @@ namespace YLErp.Web.Controllers
}
else
{
// 特批放行只有在交易确认最终成功后才记录,避免前端点击特批但后端超时重检失败时误记通过。
if (trialDataId.HasValue)
{
var decision = string.Empty;
if (ignoreRiskWarning && ignoreMoneyCheck)
{
decision = "新风控特批、老风控交易特批通过";
}
else if (ignoreRiskWarning)
{
decision = "新风控特批通过";
}
else if (ignoreMoneyCheck)
{
decision = "老风控交易特批通过";
}
if (!string.IsNullOrWhiteSpace(decision))
{
new TradeRiskCheckLogService(CurUser).AddWarningDecisionLog(trialDataId.Value, decision);
}
}
var successMsg = string.IsNullOrWhiteSpace(result.tipMsg) ? "操作完成" : result.tipMsg;
if (result.changeConfirmPaths?.Count > 0)
{
@@ -1,4 +1,4 @@
var query_data = {};
var query_data = {};
const colModelGrid = function () {
function dateFmt(cellValue) {
@@ -15,8 +15,9 @@ const colModelGrid = function () {
name: 'trial_result', label: '预警结果', index: 'trial_result', width: 80,
formatter(cellValue) {
if (cellValue === 0) return '通过';
if (cellValue === 1) return '预警';
if (cellValue === 2) return '限额';
if (cellValue === 1) return '老风控触发特批';
if (cellValue === 2) return '新风控需审批';
if (cellValue === 3) return '禁止';
return cellValue;
}
},
@@ -45,7 +46,7 @@ var g_grid = {};
$(function () {
$(".datepicker").datepicker({ changeMonth: true, changeYear: true, showButtonPanel: true, showOtherMonths: true, selectOtherMonths: true });
$("#TrialResult").val([1, 2]);
$("#TrialResult").val([1, 2, 3]);
$("#TrialResult").selectpicker('refresh');
PostData = {
TrialResults: $("#TrialResult").val(),
@@ -119,16 +119,6 @@ var confirmFunc = function (id, additionalProcessing, ignoreRiskRuleIds, trialDa
}
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: "提示",
@@ -145,11 +135,6 @@ var confirmFunc = function (id, additionalProcessing, ignoreRiskRuleIds, trialDa
if (!saveQuotaTrial(obj)) {
return;
}
if (isRiskWarningConfirm) {
if (!saveRiskWarningDecisionLog(obj.Data.id, "确认通过")) {
return;
}
}
if (isOldRiskSpecialApproval) {
obj.Data.Remark = (obj.Data.Remark || "").trim();
}
@@ -157,11 +142,6 @@ var confirmFunc = function (id, additionalProcessing, ignoreRiskRuleIds, trialDa
confirmFunc(id, additionalProcessingType, currentIgnoreRiskRuleIds, currentTrialDataId);
},
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();
}