From 917eacc7b7b2cf980a7d331bd0c39b5fbf440635 Mon Sep 17 00:00:00 2001 From: ruisu Date: Thu, 2 Jul 2026 16:46:28 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E5=AD=98=E5=88=B7=E6=96=B0=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96rule=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8EXception=E3=80=82=E9=9D=9E?= =?UTF-8?q?ACTIVE=E7=9A=84rule=E4=B8=8D=E5=86=8D=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RiskEngine/Compile/RuleCompiler.cs | 218 +++------- .../Modules/RiskEngine/RiskEngineService.cs | 387 ++++++++++++++++-- YLErpDAL/Modules/RiskEngine/RiskRule.cs | 3 +- .../Modules/RiskModule/QuotaMonitorService.cs | 32 +- YLErpWeb/Controllers/RiskRuleController.cs | 24 ++ YLErpWeb/Controllers/tradeController.cs | 2 +- 6 files changed, 455 insertions(+), 211 deletions(-) diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs index 0d2f778f..aed0f953 100644 --- a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs +++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs @@ -41,7 +41,7 @@ namespace YLErp.Modules.RiskEngine /// 变量编码 → 类型全名的映射 /// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"]) /// - static IYcLogger _logger =LogFactory.GetLogger("RuleCompiler"); + static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler"); private static readonly Dictionary VariableTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["trade"] = "YLErp.DBModels.trade", @@ -50,43 +50,37 @@ namespace YLErp.Modules.RiskEngine ["credit"] = "YLErp.DBModels.credit", ["client_marginrate"] = "YLErp.DBModels.client_marginrate", ["market"] = "YLErp.DBModels.market", - ["calc"] = "YLErp.DBModels.calc", + ["underlying_manager"] = "YLErp.DBModels.underlying_manager", ["sys"] = "YLErp.DBModels.sys", ["eod_swap_position"] = "YLErp.DBModels.eod_swap_position", ["realtime_trade_risk"] = "YLErp.DBModels.realtime_trade_risk", }; - /// - /// 将规则编译为可执行委托,并写入 rule.CompiledScript - /// 调用时机:规则创建时 / 规则加载时 / 缓存刷新时 - /// - public static void Compile(this RiskRule rule) - { - var result = ValidateAndCompileRule(rule); - if (!result.Success) - { - throw new InvalidOperationException(result.ErrorMessage); - } - } - /// /// 校验并编译规则表达式。 /// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。 /// - public static RuleCompileResult ValidateAndCompileFormula(string formulaExp) + public static RuleCompileResult ValidateAndCompileFormula(int ruleId, string formulaExp) { if (string.IsNullOrWhiteSpace(formulaExp)) { + _logger.Error($"规则表达式为空,无法编译 - RuleId: {ruleId}"); return RuleCompileResult.Fail("规则 RuleExpr 不能为空"); } try { - var compiled = CompileScript(formulaExp); + var compiled = CompileScript(ruleId, formulaExp, out string compileErrorMessage); + if (compiled == null) + { + return RuleCompileResult.Fail(compileErrorMessage ?? "脚本编译失败"); + } + return RuleCompileResult.Ok(compiled); } catch (Exception ex) { + _logger.Error($"规则表达式校验异常 - RuleId: {ruleId}, Error: {ex.Message}\n脚本代码:{formulaExp}"); return RuleCompileResult.Fail(ex.Message); } } @@ -98,17 +92,27 @@ namespace YLErp.Modules.RiskEngine { if (rule == null) { + _logger.Error("规则对象为空,无法编译 - RuleId: 0"); return RuleCompileResult.Fail("规则不能为空"); } - + if (rule.Id <= 0) { + _logger.Error($"规则 Id 非法,无法编译 - RuleId: {rule.Id}"); return RuleCompileResult.Fail("规则 Id 不能为空"); } + if (rule.Status != RiskRuleStatus.Active) + { + _logger.Error($"规则状态非启用,跳过编译 - RuleId: {rule.Id}, Status: {rule.Status}"); + return RuleCompileResult.Fail("规则 状态为未启动"); + } - var result = ValidateAndCompileFormula(rule.RuleExpr); + var result = ValidateAndCompileFormula(rule.Id, rule.RuleExpr); if (!result.Success) { + _logger.Error($"规则校验失败 - RuleId: {rule.Id}, Error: {result.ErrorMessage}"); + rule.CompiledScript = null; + RuleCompiledCache.Remove(rule.Id.ToString()); return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}"); } @@ -117,111 +121,18 @@ namespace YLErp.Modules.RiskEngine return result; } - /// - /// 解析 ConditionJson,提取条件列表。 - /// 要求 expression 明确描述实际取值路径。 - /// 当前主执行链不再使用该方法,仅作为早期方案保留。 - /// - // 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 expression = cond["expression"]?.Value(); - // string op = cond["operator"]?.Value(); - // object value = cond["value"]?.Value(); - // string variableType = cond["variableType"]?.Value() ?? "numeric"; - - // if (string.IsNullOrWhiteSpace(expression) || string.IsNullOrWhiteSpace(op)) - // { - // throw new ArgumentException("条件中缺少 expression 或 operator"); - // } - - // result.Add((variableCode, expression, op, value, variableType)); - // } - - // return result; - // } - - /// - /// 根据条件列表生成 Roslyn C# 脚本代码。 - /// 多条件自动用 &&(AND)连接。 - /// 左值表达式统一基于 expression 构建。 - /// - private static string BuildScriptCode(List<(string variableCode, string expression, string op, object value, string variableType)> conditions) - { - var exprParts = new List(); - - foreach (var (_, expression, op, value, variableType) in conditions) - { - // 先根据 expression 构建左值访问表达式,再拼接比较符和右值字面量 - string leftExpr = BuildMemberAccessExpression(expression, variableType); - - // 格式化阈值 - string valueLiteral = FormatValueLiteral(value, variableType); - - // 格式化操作符 - string csharpOp = FormatOperator(op); - - // 生成单条件表达式 - string expr = $"{leftExpr} {csharpOp} {valueLiteral}"; - exprParts.Add(expr); - } - - // 多条件用 && 连接 - return string.Join(" && ", exprParts); - } - - /// - /// 将前端传入的 expression 转成 Roslyn 可执行的成员访问表达式。 - /// 例如 trade.StockEqvNotional 会转成 ((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional。 - /// 数值类型会自动包一层 Convert.ToDecimal,便于与 decimal 阈值比较。 - /// - private static string BuildMemberAccessExpression(string expression, string variableType) - { - var parts = expression.Split('.'); - if (parts.Length < 2) - { - throw new ArgumentException($"expression 格式不正确:{expression}"); - } - - string prefix = parts[0].Trim().ToLower(); - if (!VariableTypeMap.TryGetValue(prefix, out string typeFullName)) - { - throw new ArgumentException($"未知前缀:{prefix}"); - } - - // 除前缀外,其余部分都视为成员访问路径,便于后续扩展多级属性访问 - string memberAccess = string.Join(".", parts.Skip(1).Select(p => p.Trim())); - string objectExpr = $"(({typeFullName})DataMap[\"{prefix}\"]).{memberAccess}"; - - if (variableType == "numeric" || variableType == "number") - { - return $"Convert.ToDecimal({objectExpr})"; - } - - return objectExpr; - } - /// /// 用 Roslyn 编译 C# 脚本代码为可执行委托 /// - private static Func CompileScript(string scriptCode) + private static Func CompileScript(int ruleId, string scriptCode, out string errorMessage) { + errorMessage = null; + // 配置编译选项:引用必要的程序集 var options = ScriptOptions.Default .WithReferences( - typeof(RiskContext).Assembly, // YLErpDAL - typeof(YLErp.DBModels.trade).Assembly // Model 所在程序集 + typeof(RiskContext).Assembly, + typeof(YLErp.DBModels.trade).Assembly ) .WithImports("System"); @@ -235,9 +146,9 @@ namespace YLErp.Modules.RiskEngine var errors = diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).ToList(); if (errors.Any()) { - string errorMsg = string.Join("; ", errors.Select(e => e.GetMessage())); - _logger.Error($"规则编译失败:{errorMsg}\n脚本代码:{scriptCode}"); - throw new InvalidOperationException($"脚本编译失败:{errorMsg}"); + errorMessage = string.Join("; ", errors.Select(e => e.GetMessage())); + _logger.Error($"规则编译失败 - RuleId: {ruleId}, Error: {errorMessage}\n脚本代码:{scriptCode}"); + return null; } // 生成可调用委托 @@ -246,71 +157,44 @@ namespace YLErp.Modules.RiskEngine // 包装为同步的 Func return ctx => { - var globals = new ScriptGlobals { DataMap = ctx.DataMap }; try { + var globals = new ScriptGlobals { DataMap = ctx.DataMap }; return runner(globals).GetAwaiter().GetResult(); } catch (Exception ex) { // 脚本执行异常(如空引用、类型转换失败)视为规则不触发 - _logger.Error($"规则执行异常:{ex.Message}\n脚本代码:{scriptCode}"); + _logger.Error($"规则执行异常 - RuleId: {ruleId}, Error: {ex.Message}\n脚本代码:{scriptCode}"); return false; } }; } + } - /// - /// 格式化阈值为 C# 字面量 - /// - private static string FormatValueLiteral(object value, string variableType) + public class BuildMemberAccessResult + { + public bool Success { get; set; } + + public string Expression { get; set; } + + public string ErrorMessage { get; set; } + + public static BuildMemberAccessResult Ok(string expression) { - if (value == null) return "null"; - - string strValue = value.ToString(); - - // 数值类型统一加 m 后缀(decimal) - if (variableType == "numeric" || variableType == "number") + return new BuildMemberAccessResult { - if (decimal.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal d)) - { - return d.ToString(CultureInfo.InvariantCulture) + "m"; - } - } - - // 布尔 - if (variableType == "boolean" || variableType == "bool") - { - if (bool.TryParse(strValue, out bool b)) - { - return b ? "true" : "false"; - } - } - - // 日期 - if (variableType == "date" && DateTime.TryParse(strValue, out DateTime dt)) - { - return $"DateTime.Parse(\"{dt:yyyy-MM-dd}\")"; - } - - // 字符串(兜底) - return $"\"{strValue.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""; + Success = true, + Expression = expression + }; } - /// - /// 将操作符统一为 C# 操作符 - /// - private static string FormatOperator(string op) + public static BuildMemberAccessResult Fail(string errorMessage) { - return op?.Trim() switch + return new BuildMemberAccessResult { - "=" or "==" => "==", - "!=" or "≠" => "!=", - ">" => ">", - "<" => "<", - ">=" => ">=", - "<=" => "<=", - _ => throw new ArgumentException($"不支持的操作符:{op}") + Success = false, + ErrorMessage = errorMessage }; } } diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs index 845149d3..8f4c3470 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs @@ -157,20 +157,20 @@ namespace YLErp.Modules.RiskEngine var rules = LoadRulesFromDb(); var applications = LoadApplicationsFromDb(); - List res = new List(); - foreach(var i in applications) - { - if(i.Status!= RiskRuleStatus.Active) - { - res.Add(i.Id); - } - } // 预编译所有规则到 RuleCompiledCache foreach (var rule in rules) { var ruleId = rule.Id.ToString(); + //已存在编译缓存的规则直接跳过,避免重复编译 if (RuleCompiledCache.TryGet(ruleId, out _)) { + _logger.Info($"[风控引擎] 规则已存在编译缓存,跳过预编译 - RuleId: {rule.Id}"); + continue; + } + //非活跃的rule不编译 + if (rule.Status != RiskRuleStatus.Active) + { + _logger.Info($"[风控引擎] 规则非活跃,跳过预编译 - RuleId: {rule.Id}, Status: {rule.Status}"); continue; } @@ -321,16 +321,23 @@ namespace YLErp.Modules.RiskEngine } var applicationRules = rules - .Where(r => applicationRuleIds.Contains(r.Id) && r.Id == 3) + .Where(r => applicationRuleIds.Contains(r.Id) && r.Status == RiskRuleStatus.Active) .ToList(); - var missingRuleIds = applicationRuleIds - .Where(ruleId => applicationRules.All(r => r.Id != ruleId)) - .ToList(); + // 区分"规则不存在"与"规则非活跃"两种情况,分别记录日志 + var ruleDict = rules.Where(r => applicationRuleIds.Contains(r.Id)) + .ToDictionary(r => r.Id); - foreach (var missingRuleId in missingRuleIds) + foreach (var ruleId in applicationRuleIds) { - _logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {missingRuleId}, ApplicationRuleIds: {application.RuleIds}"); + if (!ruleDict.TryGetValue(ruleId, out var ruleDef)) + { + _logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {ruleId}, ApplicationRuleIds: {application.RuleIds}"); + } + else if (ruleDef.Status != RiskRuleStatus.Active) + { + _logger.Info($"[风控引擎] 规则非活跃,跳过执行 - RuleId: {ruleId}, Status: {ruleDef.Status}, ApplicationRuleIds: {application.RuleIds}"); + } } foreach (var rule in applicationRules) @@ -379,7 +386,6 @@ namespace YLErp.Modules.RiskEngine switch (application.ControlStrategy) { case RiskControlStrategy.Block: - break; result.Blocked = true; result.Passed = false; result.TriggeredRules.Add(new TriggeredRuleInfo @@ -465,36 +471,324 @@ namespace YLErp.Modules.RiskEngine /// private List LoadRulesFromDb() { - return DbContext.glms_risk_rule + var rules = DbContext.glms_risk_rule .AsNoTracking() - .Where(r => r.Status != RiskRuleStatus.Deleted) .OrderByDescending(r => r.UpdateDate ?? r.OptDate) .Select(r => new RiskRule { + Status = r.Status, 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(); + }).ToList() ; + return rules; + //#region 测试本地规则 + //rules.Add(new RiskRule + //{ + // Id = 1000001, + // RuleName = "挂钩标的集中度校验(本地)", + // RuleText = "取值字段:分子取 QuotaMonitorService 已注入 DataMap[same_underlying_total_notional],该值按存续交易口径汇总同一标的 StockEqvNotional;分母取 DataMap[underlying_manager].IssueSize,债券标的场景下该值由 underlying_manager.ExJson 反序列化回填,含义按当前业务测试口径使用发行量(亿)。为什么这么取:当前上下文已经稳定注入了这两个值,且与集中度规则最接近正式口径。计算逻辑:同一标的总名义本金 ÷ 发行量 × 100%,发行量需先乘 100000000 还原为元,结果大于 30% 时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"same_underlying_total_notional\") && DataMap[\"same_underlying_total_notional\"] != null && DataMap.ContainsKey(\"underlying_manager\") && DataMap[\"underlying_manager\"] != null && ((YLErp.DBModels.underlying_manager)DataMap[\"underlying_manager\"]).IssueSize.HasValue && ((YLErp.DBModels.underlying_manager)DataMap[\"underlying_manager\"]).IssueSize.Value > 0 && Convert.ToDecimal(DataMap[\"same_underlying_total_notional\"]) / (((YLErp.DBModels.underlying_manager)DataMap[\"underlying_manager\"]).IssueSize.Value * 100000000m) * 100m > 30m", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000004, + // RuleName = "保证金支付比例超阈值(本地)", + // RuleText = "取值字段:直接取 DataMap[trade].MarginRate,对应 trade 表保证金率字段。为什么这么取:该字段已经在当前上下文稳定注入,且 seed 规则 4 的判断核心也是保证金比例。计算逻辑:本地测试按数值型比例直接比较,MarginRate 大于 0.5 视为超过 50%,触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).MarginRate > 0.5", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000005, + // RuleName = "保证金利率偏离(本地)", + // RuleText = "取值字段:正式口径应取收益互换扩展数据中的保证金利率字段,但当前 RiskContext 未注入 trade_swap,因此本地测试先取 DataMap[trade].MarginRate 近似代替。为什么这么取:当前上下文只有 trade 可直接取值,先保证规则链路可验证。计算逻辑:若近似保证金利率不在 2% 到 5% 区间内,即小于 0.02 或大于 0.05,则触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((((YLErp.DBModels.trade)DataMap[\"trade\"]).MarginRate < 0.02) || (((YLErp.DBModels.trade)DataMap[\"trade\"]).MarginRate > 0.05))", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000006, + // RuleName = "保证金收取比例低于最低标准(本地)", + // RuleText = "取值字段:正式口径应取配置项 MinMarginRate 或客户/品种最低保证金率,当前上下文未注入配置对象,因此本地测试仍取 DataMap[trade].MarginRate 做比较。为什么这么取:trade.MarginRate 是当前唯一稳定可得且能反映保证金比例的字段。计算逻辑:先以 20% 作为本地测试最低标准,MarginRate 小于 0.2 时触发审批,后续接入正式配置后再替换阈值来源。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).MarginRate < 0.2", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000007, + // RuleName = "起息日早于当前日期(本地)", + // RuleText = "取值字段:取 DataMap[trade].StartDate,对应 trade 表开始日。为什么这么取:seed 规则 7 直接使用 StartDate 与当前日期比较,当前上下文也已注入 trade。计算逻辑:StartDate 有值且日期早于系统当天 DateTime.Today 时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.Value.Date < DateTime.Today", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000008, + // RuleName = "支付日为银行间交易日(本地)", + // RuleText = "取值字段:取 DataMap[trade].SettlementDate,对应 trade 表结算日期。为什么这么取:当前代码中支付相关日期可直接从 trade 取得,且项目已有 QdpCalendarHelper.GetNonHolidayDefore 可用于交易日校验。计算逻辑:若 SettlementDate 有值,且向前修正到最近交易日后的结果不等于原日期,则说明原日期不是银行间交易日,触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SettlementDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap[\"trade\"]).SettlementDate.Value.Date) != ((YLErp.DBModels.trade)DataMap[\"trade\"]).SettlementDate.Value.Date", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000009, + // RuleName = "到期日为银行间交易日(本地)", + // RuleText = "取值字段:取 DataMap[trade].ExerciseDate,对应当前交易里更接近业务到期/行权日的字段。为什么这么取:TradeBase 中 MaturityDate 注释已提示容易与 ExerciseDate 混淆,当前测试按交易实际到期处理字段 ExerciseDate 落地,避免先取错口径。计算逻辑:ExerciseDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.Value.Date) != ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.Value.Date", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000010, + // RuleName = "平仓日为银行间交易日(本地)", + // RuleText = "取值字段:取 DataMap[trade].UnWindDate,对应 trade 表平仓日。为什么这么取:当前上下文已注入该字段,且 seed 规则中的平仓日判断在本地最接近该口径。计算逻辑:UnWindDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).UnWindDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap[\"trade\"]).UnWindDate.Value.Date) != ((YLErp.DBModels.trade)DataMap[\"trade\"]).UnWindDate.Value.Date", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000011, + // RuleName = "合约期限超阈值(本地)", + // RuleText = "取值字段:取 DataMap[trade].StartDate 和 DataMap[trade].ExerciseDate。为什么这么取:seed 规则 11 本质是计算合约剩余天数,当前 trade 中最稳定可得且最接近交易起止区间的就是开始日和到期/行权日。计算逻辑:当 StartDate 和 ExerciseDate 都有值时,用 ExerciseDate.Date 减 StartDate.Date 的总天数,若大于 365 天则触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.HasValue && (((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.Value.Date - ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.Value.Date).TotalDays > 365d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000012, + // RuleName = "债券类净价偏离(本地)", + // RuleText = "取值字段:当前上下文未注入债券估值净价 market.CBValuationPrice,因此本地测试先取 DataMap[trade].SpotPrice 作为可直接获得的价格字段。为什么这么取:债券类正式估值未接入前,需要用交易上已有价格先验证偏离类规则链路。计算逻辑:若 SpotPrice 有值且不为 0,则按 ABS(SpotPrice-100)/100×100% 计算相对面值 100 的偏离率,偏离率大于 5% 时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && Math.Abs(((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value - 100d) / 100d * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000013, + // RuleName = "债券类收益率偏离(本地)", + // RuleText = "取值字段:正式口径应比较 DataMap[trade].InitYtm 与市场估值收益率 market.CBValuationYtm,但当前未注入 market,因此本地测试直接取 trade.InitYtm。为什么这么取:InitYtm 是 trade 上已有且与收益率偏离最接近的字段。计算逻辑:先以 2.5% 作为本地测试基准收益率,若 InitYtm 有值且 ABS(InitYtm-0.025)/0.025×100% 大于 5%,则触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).InitYtm.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).InitYtm.Value - 0.025d) / 0.025d) * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000014, + // RuleName = "非债券类价格偏离(本地)", + // RuleText = "取值字段:正式口径应比较 trade.TradePrice 与市场参考价 market.ReferencePrice,当前未注入 market,因此本地测试直接取 trade.TradePrice 与 trade.SpotPrice 互相比对。为什么这么取:这两个字段都来自 trade,且能够表达成交价相对现价的偏离。计算逻辑:当 TradePrice 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(TradePrice/SpotPrice-1)×100% 计算偏离率,大于 5% 时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).TradePrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).TradePrice.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value) - 1d) * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000015, + // RuleName = "单一交易对手累计标的数量超阈值(本地)", + // RuleText = "取值字段:取 DataMap[trade].ClientId 作为交易对手标识,并在表达式里直接查询 trade 表的 UnderlyingId。为什么这么取:当前上下文尚未预先注入该聚合值,但 DbContext 在脚本环境可用,且项目已有存续口径可以复用。计算逻辑:按 ValidState 不等于 InValid、ParentTradeId 等于 0、TradeStatus 属于 NeedMarginTradeStatusList 或审批中 的存续口径,统计同一 ClientId 下去重后的 UnderlyingId 数量,超过 10 个时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.ClientId == ((YLErp.DBModels.trade)DataMap[\"trade\"]).ClientId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Select(t => t.UnderlyingId).Distinct().Count() > 10", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000016, + // RuleName = "多头支付固定端利率偏离(本地)", + // RuleText = "取值字段:正式口径应同时取 trade_swap 固定端方向、固定利率以及市场基准利率,当前未注入 trade_swap 和 market,因此本地测试先取 DataMap[trade].FixedRate,并结合 BuySell 判断多头方向。为什么这么取:FixedRate 和 BuySell 都已在 trade 上可取,能先验证方向类利率规则链路。计算逻辑:当 BuySell 表示多头且 FixedRate 有值时,先以 2.5% 作为本地测试基准,若 ABS(FixedRate-0.025)/0.025×100% 大于 5%,则触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Buy\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.Value - 0.025d) / 0.025d) * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000017, + // RuleName = "空头利率减点借贷加权偏离(本地)", + // RuleText = "取值字段:正式口径应取 trade_swap 空头端利率减点、借贷成本和加权基准,当前未注入这些对象,因此本地测试仍取 DataMap[trade].FixedRate,并结合 BuySell 判断空头方向。为什么这么取:当前 trade 上只有 FixedRate 可稳定表达利率数值,先用于验证空头分支规则。计算逻辑:当 BuySell 表示空头且 FixedRate 有值时,先以 2% 作为本地测试基准,若 ABS(FixedRate-0.02)/0.02×100% 大于 5%,则触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Sell\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.Value - 0.02d) / 0.02d) * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000018, + // RuleName = "账户授权收支方向不匹配(本地)", + // RuleText = "取值字段:正式口径应取账户授权配置中的收支方向和当前交易实际收支方向,当前上下文未注入账户授权对象,因此本地测试先用 trade.OpponentRole 与 trade.BuySell 做占位判断。为什么这么取:当前只有 trade 上的方向类字段可直接取得,先用于验证禁止类规则是否能命中。计算逻辑:当 OpponentRole 和 BuySell 都有值,且出现本地定义的不允许组合时触发禁止;当前测试口径先将 OpponentRole 为 Pay 且 BuySell 为 Buy 视为方向不匹配。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && !string.IsNullOrWhiteSpace(((YLErp.DBModels.trade)DataMap[\"trade\"]).OpponentRole) && !string.IsNullOrWhiteSpace(((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell) && ((YLErp.DBModels.trade)DataMap[\"trade\"]).OpponentRole == \"Pay\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Buy\"", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000019, + // RuleName = "执行价偏离超阈值(本地)", + // RuleText = "取值字段:取 DataMap[trade].Strike 和 DataMap[trade].SpotPrice,分别对应行权价与现价。为什么这么取:当前项目已有使用 Strike 与 SpotPrice 做偏离判断的场景,且这两个字段都已在 trade 上可直接获取。计算逻辑:当 Strike 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(Strike/SpotPrice-1)×100% 计算执行价相对现价的偏离率,大于 5% 时触发审批。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value) - 1d) * 100d > 5d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //rules.Add(new RiskRule + //{ + // Id = 1000021, + // RuleName = "接近/触发敲入敲出价(本地)", + // RuleText = "取值字段:正式口径应取产品条款中的敲入价/敲出价以及现价,当前上下文未注入条款对象,因此本地测试先取 DataMap[trade].Strike 和 DataMap[trade].SpotPrice 近似模拟触发价与现价。为什么这么取:这两个字段当前即可直接取得,适合先验证提示类规则链路。计算逻辑:当 Strike 和 SpotPrice 都有值且 Strike 不为 0 时,按 ABS(SpotPrice/Strike-1)×100% 计算两者距离,距离小于等于 2% 时视为接近触发价,给出提示。", + // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value) - 1d) * 100d <= 2d", + // Version = 1, + // Status = RiskRuleStatus.Active, + // OptId = 0, + // OptName = "system", + // OptDate = DateTime.Now, + // UpdateOptId = 0, + // UpdateOptName = "system", + // UpdateDate = DateTime.Now + //}); + + //return rules; + //#endregion } - /// - /// 从数据库加载应用配置 - /// + + private List LoadApplicationsFromDb() { - return DbContext.glms_risk_rule_application + var applications = DbContext.glms_risk_rule_application .AsNoTracking() - .Where(a => a.Status != RiskRuleStatus.Deleted) .OrderByDescending(a => a.UpdateDate ?? a.OptDate) .Select(a => new RiskRuleApplication { @@ -517,16 +811,33 @@ namespace YLErp.Modules.RiskEngine UpdateDate = a.UpdateDate ?? a.OptDate ?? DateTime.MinValue }) .ToList(); + return applications; } + // applications.Add(new RiskRuleApplication + // { + // Id = 1000001, + // RuleIds = "1000001", + // Status = RiskRuleStatus.Active, + // ControlStrategy = RiskControlStrategy.Approval, + // TriggerPoints = "BOOK_CONFIRM", + // ScopeIsGlobal = true, + // ScopeAssetBookIds = string.Empty, + // ScopeClientIds = string.Empty, + // ScopeUnderlyingTypes = string.Empty, + // ScopeTradeTypes = string.Empty, + // Version = 1, + // OptId = 0, + // OptName = "system", + //} - /// - /// 判断应用配置是否命中当前交易。 - /// 匹配规则遵循设计文档: - /// 1. 全局命中时直接返回 true; - /// 2. 同一维度内多选按并集处理; - /// 3. 不同维度之间按交集处理; - /// 4. 某维度留空表示该维度不限制。 - /// + /// + /// 判断应用配置是否命中当前交易。 + /// 匹配规则遵循设计文档: + /// 1. 全局命中时直接返回 true; + /// 2. 同一维度内多选按并集处理; + /// 3. 不同维度之间按交集处理; + /// 4. 某维度留空表示该维度不限制。 + /// private bool IsApplicationMatched(RiskRuleApplication application, YLErp.DBModels.trade trade) { if (application == null) @@ -559,17 +870,17 @@ namespace YLErp.Modules.RiskEngine /// 解析应用配置中的规则ID列表。 /// 多个规则ID使用逗号分隔,返回去空格后的 long 集合。 /// - private List ParseRuleIds(string ruleIds) + private List ParseRuleIds(string ruleIds) { if (string.IsNullOrWhiteSpace(ruleIds)) { - return new List(); + return new List(); } - var ids = new List(); + var ids = new List(); foreach (var part in ruleIds.Split(',', StringSplitOptions.RemoveEmptyEntries)) { - if (long.TryParse(part.Trim(), out long id)) + if (int.TryParse(part.Trim(), out int id)) { ids.Add(id); } @@ -634,5 +945,5 @@ namespace YLErp.Modules.RiskEngine var property = trade.GetType().GetProperty("UnderlyingInstrumentType"); return property?.GetValue(trade); } - } +} } diff --git a/YLErpDAL/Modules/RiskEngine/RiskRule.cs b/YLErpDAL/Modules/RiskEngine/RiskRule.cs index 5558c2b4..8e8ef067 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRule.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRule.cs @@ -13,6 +13,7 @@ namespace YLErp.Modules.RiskEngine public int Id { get; set; } public string RuleName { get; set; } public string RuleText { get; set; } + /// /// 【核心】条件JSON(结构化模式的条件列表存储),如 @@ -29,7 +30,7 @@ namespace YLErp.Modules.RiskEngine // === 生命周期 === public int Version { get; set; } - public bool IsDeleted { get; set; } + public RiskRuleStatus Status { get; set; } // === 审计字段 === public int OptId { get; set; } diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs index b5b7dd9a..2d7a1215 100644 --- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs +++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs @@ -4380,9 +4380,11 @@ namespace YLErp.Modules.RiskModule if (!string.IsNullOrWhiteSpace(quotaObj.QuotaWarningDetails)) detailParts.Add($"限额预警:{quotaObj.QuotaWarningDetails}"); res.ErrorMsg = string.Join("\n", detailParts); + //确认本次为需审批后,二次特批。因为只靠点击“交易特批”的ignoreRiskWarning,不能保证本次校验通过。 var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning && !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails); //需清除ErrorMsg,不然外部调用会认为失败 + //触发需审批,交易特批逻辑 if (ignoreRiskWarning && isRiskApprovalWarning) { res.ErrorMsg = string.Empty; @@ -4755,14 +4757,14 @@ namespace YLErp.Modules.RiskModule _logger.Info($"[限额试算] 开始执行 - tradeId: {tradeId}, trialSource: {trialSource}"); var result = new QuotaTrial(); trade tradeObj = DbContext.trade.AsNoTracking().Where(O => O.id == tradeId).FirstOrDefault(); - + // 检查交易对象是否存在 if (tradeObj == null) { _logger.Info($"[限额试算] 找不到交易记录 - tradeId: {tradeId}"); throw new ArgumentNullException(nameof(tradeObj), $"找不到ID为 {tradeId} 的交易记录"); } - + _logger.Info($"[限额试算] 交易信息 - tradeId: {tradeObj.id}, TradeNumber: {tradeObj.TradeNumber}, TradeType: {tradeObj.TradeType}, ClientId: {tradeObj.ClientId}"); //// 检查关键字段是否为null @@ -4968,8 +4970,30 @@ namespace YLErp.Modules.RiskModule TradeId = tradeId, TriggerPoint = "BOOK_CONFIRM" }; - // 构造 DataMap(第一版只塞 trade 对象,后续按需扩展) + tradeObj.StockEqvNotional = 10000000000; riskContext.DataMap["trade"] = tradeObj; + var sameUnderlyingTotalNotional = DbContext.trade + .Where(t => t.ValidState != "InValid" + && t.UnderlyingId == tradeObj.UnderlyingId + && t.ParentTradeId == 0 + && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == "审批中")) + .Sum(t => (double?)t.StockEqvNotional) ?? 0d; + riskContext.DataMap["same_underlying_total_notional"] = sameUnderlyingTotalNotional; + var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(tradeObj.UnderlyingCode); + if (underlying != null) + { + if (underlying.IsBond() && !string.IsNullOrEmpty(underlying.ExJson)) + { + var bond = JsonHelper.Deserialize(underlying.ExJson); + if (bond != null) + { + underlying.IssueSize = bond.IssueSize; + underlying.UnderlyingFullName = bond.UnderlyingFullName; + underlying.UnderlyingIssuer = bond.UnderlyingIssuer; + } + } + riskContext.DataMap["underlying_manager"] = underlying; + } var riskResult = riskEngine.EvaluateRisk(riskContext, "BOOK_CONFIRM"); _logger.Info($"[风控引擎] 簿记交易确认 - TradeId: {tradeId}, Passed: {riskResult.Passed}, Blocked: {riskResult.Blocked}, NeedApproval: {riskResult.NeedApproval}, ShowTip: {riskResult.ShowTip}"); @@ -5016,7 +5040,7 @@ namespace YLErp.Modules.RiskModule } // ===== 新风控引擎接入结束 ===== - _logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}"); + //_logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}"); SaveQuotaTrial(result); return result; } diff --git a/YLErpWeb/Controllers/RiskRuleController.cs b/YLErpWeb/Controllers/RiskRuleController.cs index 55ba98e9..99158235 100644 --- a/YLErpWeb/Controllers/RiskRuleController.cs +++ b/YLErpWeb/Controllers/RiskRuleController.cs @@ -568,6 +568,30 @@ namespace YLErp.Web.Controllers #endregion + #region Cache Management + + [HttpGet("riskCache/refresh")] + [MyAuthorize("风险控制-异常交易监控")] + public JsonResult RefreshRiskEngineCache() + { + try + { + RiskEngineService.GetInstance().RefreshCache(); + return Json(new { success = true, message = "风控缓存刷新成功" }); + } + 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 [HttpGet("trade-types")] diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index 9959628b..06e7dae4 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -2480,7 +2480,7 @@ namespace YLErp.Web.Controllers { return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = tradeBLL.LackOfMoney, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); } - //限额试算不通过(老风控检查失败),展示详情但不允许审批 + //限额试算不通过(老风控,新风控检查失败),展示详情但不允许审批 if (result.type == TradeOpenRetCode.QuotaTrialError.ToString()) { return JsonSuccessData(new { proccessType = "QuotaTrialError", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });