diff --git a/Framework/YLErp.Core/DBModels/QuotaTrial.cs b/Framework/YLErp.Core/DBModels/QuotaTrial.cs index 233f0d3a..c4e95df1 100644 --- a/Framework/YLErp.Core/DBModels/QuotaTrial.cs +++ b/Framework/YLErp.Core/DBModels/QuotaTrial.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using YLErp.Enums; namespace YLErp.DBModels @@ -62,5 +63,8 @@ namespace YLErp.DBModels /// 1:确认成交时系统调用; /// public int TrialSource { get; set; } + + [NotMapped] + public List ApprovalRuleIds { get; set; } = new List(); } } diff --git a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs index 18980071..2a3cd093 100644 --- a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs +++ b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs @@ -1,4 +1,4 @@ -using YLErp.Models; +using YLErp.Models; namespace YLErp.Enums { @@ -143,6 +143,11 @@ namespace YLErp.Enums [Description("预警")] Warning, /// + /// 风控需审批 + /// + [Description("风控需审批")] + RiskWarning, + /// /// 不通过 /// [Description("不通过")] diff --git a/YLErpDAL/Helpers/StringHelper.cs b/YLErpDAL/Helpers/StringHelper.cs index 411c0c11..62140735 100644 --- a/YLErpDAL/Helpers/StringHelper.cs +++ b/YLErpDAL/Helpers/StringHelper.cs @@ -1,4 +1,4 @@ -namespace YLErp.Helpers +namespace YLErp.Helpers { public static class StringHelper { @@ -49,5 +49,24 @@ var decExp = Math.Pow(10, decimalSize + 1); return (Math.Floor(value.Value * decExp) / decExp).ToString("f" + decimalSize); } + + /// + /// 将逗号分隔的字符串转换为字符串数组,并去除空白项与重复项。 + /// + /// 逗号分隔的字符串。 + /// 去除空白项与重复项后的字符串数组。 + public static string[] ConvertCommaValuesToStringArray(string commaValues) + { + if (string.IsNullOrWhiteSpace(commaValues)) + { + return new string[0]; + } + + return commaValues.Split(',') + .Select(o => o?.Trim()) + .Where(o => !string.IsNullOrWhiteSpace(o)) + .Distinct() + .ToArray(); + } } } diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs index 30296d9d..fea278d0 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs @@ -161,18 +161,18 @@ namespace YLErp.Modules.RiskEngine 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; } + //已存在编译缓存的规则直接跳过,避免重复编译 + if (RuleCompiledCache.TryGet(ruleId, out _)) + { + _logger.Info($"[风控引擎] 规则已存在编译缓存,跳过预编译 - RuleId: {rule.Id}"); + continue; + } var compileResult = RuleCompiler.ValidateAndCompileRule(rule); if (!compileResult.Success) diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs index 2d7a1215..189382a8 100644 --- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs +++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs @@ -4335,7 +4335,7 @@ namespace YLErp.Modules.RiskModule return QuotaCheck(ref res, false); } - public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning) + public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) { if (res == null || res.Trade == null) { @@ -4381,14 +4381,27 @@ namespace YLErp.Modules.RiskModule 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) + var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.RiskWarning + && !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails) + && (quotaObj.ApprovalRuleIds?.Any() ?? false); + //当前为老风控特批,不是新风控需审批 + res.OldRiskNeedSpecialApproval = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning; + if (isRiskApprovalWarning) { - res.ErrorMsg = string.Empty; - return true; + var currentApprovalRuleIds = quotaObj.ApprovalRuleIds ?? new List(); + res.IgnoredRiskRuleIds = currentApprovalRuleIds; + //需清除ErrorMsg,不然外部调用会认为失败 + //触发需审批,交易特批逻辑 + if (ignoreRiskWarning) + { + var ignoredRuleIdSet = new HashSet((ignoreRiskRuleIds ?? Enumerable.Empty()).Where(o => !string.IsNullOrWhiteSpace(o))); + var newApprovalRuleIds = currentApprovalRuleIds.Where(o => !ignoredRuleIdSet.Contains(o)).ToList(); + if (!newApprovalRuleIds.Any()) + { + res.ErrorMsg = string.Empty; + return true; + } + } } res.RetCode = isRiskApprovalWarning ? TradeOpenRetCode.RiskWarning : TradeOpenRetCode.QuotaTrialError; return false; @@ -4959,7 +4972,6 @@ namespace YLErp.Modules.RiskModule result.RiskWarningDetails = riskWarningMsg ?? ""; result.QuotaWarningDetails = quotaWarningMsg ?? ""; result.AvailableForClient = availableMsg ?? ""; - result.TrialSource = trialSource; // ===== 新风控引擎接入点(设计文档 4.6.1) ===== try @@ -4970,7 +4982,6 @@ namespace YLErp.Modules.RiskModule TradeId = tradeId, TriggerPoint = "BOOK_CONFIRM" }; - tradeObj.StockEqvNotional = 10000000000; riskContext.DataMap["trade"] = tradeObj; var sameUnderlyingTotalNotional = DbContext.trade .Where(t => t.ValidState != "InValid" @@ -5010,12 +5021,25 @@ namespace YLErp.Modules.RiskModule } if (riskResult.NeedApproval) { - if (result.TrialStatus != QuotaTrialStatusEnum.Error) + if (result.TrialStatus != QuotaTrialStatusEnum.Error && result.TrialStatus != QuotaTrialStatusEnum.Warning) { - result.TrialStatus = QuotaTrialStatusEnum.Warning; + result.TrialStatus = QuotaTrialStatusEnum.RiskWarning; } result.RiskWarningDetails += "[风控引擎] 规则触发:需审批\n"; - foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval)) + var approvalTriggeredRules = riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval).ToList(); + var approvalTriggeredRuleIds = approvalTriggeredRules.Select(r => r.RuleId) + .Where(r => !string.IsNullOrWhiteSpace(r)) + .ToList(); + var showTipTriggeredRules = riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.ShowTip).ToList(); + var showTipTriggeredRuleIds = showTipTriggeredRules.Select(r => r.RuleId) + .Where(r => !string.IsNullOrWhiteSpace(r)) + .ToList(); + // 二次确认时,需审批规则允许按首次命中结果忽略;同时已展示过的提示规则也不再重复展示 + var processedRiskRuleIds = approvalTriggeredRuleIds.Concat(showTipTriggeredRuleIds) + .Distinct() + .ToList(); + result.ApprovalRuleIds = processedRiskRuleIds; + foreach (var triggeredRule in approvalTriggeredRules) { result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n"; } diff --git a/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs b/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs index 53ac78c3..a5d508d8 100644 --- a/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs +++ b/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs @@ -97,6 +97,9 @@ namespace YLErp.Modules.RiskModule case QuotaTrialStatusEnum.Warning: exportModel.trial_result = "预警"; break; + case QuotaTrialStatusEnum.RiskWarning: + exportModel.trial_result = "风控需审批"; + break; case QuotaTrialStatusEnum.Error: exportModel.trial_result = "限额"; break; diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs index bedb5873..7bca1074 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs @@ -46,7 +46,7 @@ namespace YLErp.Modules.TradeModule.DealModule /// /// 2021-02-04改造:批量确认因为耗时太长,用户多页面(交易列表,今日交易)并发确认导致生成多条期权费资金记录 /// - public TradeConfirmResultModel tradeConfirm(IEnumerable tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false) + public TradeConfirmResultModel tradeConfirm(IEnumerable tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false, IEnumerable ignoreRiskRuleIds = null) { if (tradeids is null || !tradeids.Any(n => n > 0)) { @@ -101,7 +101,8 @@ namespace YLErp.Modules.TradeModule.DealModule confirmedTradeIds: result.confirmedTradeIds, changeConfirmPaths: result.changeConfirmPaths, continueQuotaCheck: idCache.Contains(pid), - ignoreRiskWarning: ignoreRiskWarning); + ignoreRiskWarning: ignoreRiskWarning, + ignoreRiskRuleIds: ignoreRiskRuleIds); if (pid > 0) { idCache.Add(pid); @@ -120,8 +121,10 @@ namespace YLErp.Modules.TradeModule.DealModule if (temp.RetCode == TradeOpenRetCode.QuotaTrialError || temp.RetCode == TradeOpenRetCode.FundStatus || temp.RetCode == TradeOpenRetCode.RiskWarning) { result.LackOfMoney = true; + result.OldRiskNeedSpecialApproval = temp.OldRiskNeedSpecialApproval; result.TrialDataId = temp.TrialDataId; result.TradeId = temp.Trade.id; + result.ignoreRiskRuleIds = temp.IgnoredRiskRuleIds; sbError.AppendLine(temp.ErrorMsg); break; } @@ -162,7 +165,7 @@ namespace YLErp.Modules.TradeModule.DealModule /// /// private TradeOpenResult ConfirmTrade(int tid, bool isBatch, bool hasTradeProcess, - bool ignoreMoneyCheck, bool isSkipApproval, List confirmedTradeIds, List changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false) + bool ignoreMoneyCheck, bool isSkipApproval, List confirmedTradeIds, List changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false, IEnumerable ignoreRiskRuleIds = null) { var td = DbContext.trade.FirstOrDefault(t => t.id == tid); var result = new TradeOpenResult(td ?? new trade()); @@ -217,7 +220,7 @@ namespace YLErp.Modules.TradeModule.DealModule } var trialService = new QuotaMonitorService(this); - if (!trialService.QuotaCheck(ref result, ignoreRiskWarning)) + if (!trialService.QuotaCheck(ref result, ignoreRiskWarning, ignoreRiskRuleIds)) { return result; } @@ -618,6 +621,7 @@ namespace YLErp.Modules.TradeModule.DealModule public class TradeConfirmResultModel { public bool LackOfMoney; + public bool OldRiskNeedSpecialApproval; public int TrialDataId; public int TradeId; public List confirmedTradeIds; @@ -625,5 +629,6 @@ namespace YLErp.Modules.TradeModule.DealModule public string errorMsg; public string tipMsg; public string type; + public List ignoreRiskRuleIds; } } diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs index 448967ec..0915baff 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs @@ -787,6 +787,8 @@ namespace YLErp.Modules.TradeModule.DealModule public TradeOpenRetCode RetCode; public int TrialDataId; + public List IgnoredRiskRuleIds; + public bool OldRiskNeedSpecialApproval; /// /// 错误信息 diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index 06e7dae4..98a3d539 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -2447,7 +2447,7 @@ namespace YLErp.Web.Controllers } - public JsonResult tradeConfirm(string tradeids, string additionalProcessing, bool isSkipCheck = false) + public JsonResult tradeConfirm(string tradeids, string additionalProcessing, string ignoreRiskRuleIds, bool isSkipCheck = false) { var tradeidArr = DataConvert.ConvertCommaValuesToInt32Array(tradeids); if (tradeids is null || !tradeids.Any(n => n > 0)) @@ -2457,32 +2457,37 @@ 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 ignoreRiskRuleIdArr = StringHelper.ConvertCommaValuesToStringArray(ignoreRiskRuleIds); var isSkipApproval = false; if (CurUser.交易管理_特批批量确认 && isSkipCheck) { ignoreMoneyCheck = true; isSkipApproval = true; } - var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval, ignoreRiskWarning); + var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval, ignoreRiskWarning, ignoreRiskRuleIdArr); //如果客户缺少资金而操作者有交易特批权限 if (!ignoreMoneyCheck && !ignoreRiskWarning && result.LackOfMoney) { if (result.type == TradeOpenRetCode.RiskWarning.ToString()) { - return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = tradeBLL.RiskWarningConfirm, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); + return JsonSuccessData(new { proccessType = "AdditionalProcessing", source = "RiskWarning", type = tradeBLL.RiskWarningConfirm, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type, ignoreRiskRuleIds = result.ignoreRiskRuleIds }); } if (result.type == TradeOpenRetCode.FundStatus.ToString()) { - return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = "", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); + return JsonSuccessData(new { proccessType = "AdditionalProcessing", source = "FundStatus", 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 }); + return JsonSuccessData(new { proccessType = "AdditionalProcessing", source = "LackOfMoney", type = tradeBLL.LackOfMoney, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); } - //限额试算不通过(老风控,新风控检查失败),展示详情但不允许审批 + //限额试算不通过时,延用master老逻辑:老风控走AdditionalProcessing并允许前端展示“交易特批”;新风控检查失败仍只展示详情不允许审批 if (result.type == TradeOpenRetCode.QuotaTrialError.ToString()) { + if (result.OldRiskNeedSpecialApproval) + { + return JsonSuccessData(new { proccessType = "AdditionalProcessing", source = "OldRiskQuotaTrialError", type = tradeBLL.LackOfMoney, TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); + } return JsonSuccessData(new { proccessType = "QuotaTrialError", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type }); } } diff --git a/YLErpWeb/Views/trade/quotaTrial.cshtml b/YLErpWeb/Views/trade/quotaTrial.cshtml index 7751409d..b04ecd25 100644 --- a/YLErpWeb/Views/trade/quotaTrial.cshtml +++ b/YLErpWeb/Views/trade/quotaTrial.cshtml @@ -101,6 +101,9 @@ case YLErp.Enums.QuotaTrialStatusEnum.Warning: 预警 break; + case YLErp.Enums.QuotaTrialStatusEnum.RiskWarning: + 风控需审批 + break; case YLErp.Enums.QuotaTrialStatusEnum.Error: 不通过 break; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js index ad78eeb4..2f6eb85f 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js @@ -49,11 +49,14 @@ function showSwapRate(timeRate, isMarginLeg) { $("#swapIntervalModal").modal("show"); } var layerIndex = -1; -var confirmFunc = function (id, additionalProcessing) { +var confirmFunc = function (id, additionalProcessing, ignoreRiskRuleIds) { var pData = { tradeids: id }; if (!main.isEmpty(additionalProcessing)) { pData.additionalProcessing = additionalProcessing; } + if (ignoreRiskRuleIds && ignoreRiskRuleIds.length > 0) { + pData.ignoreRiskRuleIds = ignoreRiskRuleIds.join(','); + } main.post("/trade/tradeConfirm", pData).done(function (data) { if (!data.success) { main.message(data.msg); @@ -90,8 +93,16 @@ var confirmFunc = function (id, additionalProcessing) { } if (data.obj && data.obj.proccessType === "AdditionalProcessing") { if (data.obj.TrialDataId) { - var isRiskWarningConfirm = data.obj.type === "RiskWarningConfirm"; + // 交易特批来源,优先使用后端新增的 source 字段区分老风控/新风控,同时兼容旧返回结构。 + var additionalProcessingSource = data.obj.source || ""; + // 新风控需审批场景:source=RiskWarning 或 type=RiskWarningConfirm,二次确认时需要继续透传 ignoreRiskRuleIds。 + var isRiskWarningConfirm = data.obj.type === "RiskWarningConfirm" || additionalProcessingSource === "RiskWarning"; + // 老风控兼容特批场景:source=OldRiskQuotaTrialError 或 type=LackOfMoney,保持 master 原有“交易特批”按钮逻辑。 + var isOldRiskSpecialApproval = additionalProcessingSource === "OldRiskQuotaTrialError" || data.obj.type === "LackOfMoney"; + // 新风控特批走 RiskWarningConfirm,老风控特批继续走 LackOfMoney,兼容当前 confirmFunc 的入参约定。 var additionalProcessingType = isRiskWarningConfirm ? "RiskWarningConfirm" : "LackOfMoney"; + // 新风控需审批时由后端返回本次命中的审批规则 id,前端二次确认时原样透传。 + var currentIgnoreRiskRuleIds = data.obj.ignoreRiskRuleIds || []; var saveQuotaTrial = function (obj) { var saveSuccess = true; if (obj.Data.Remark && obj.Data.Remark.length > 0) { @@ -135,8 +146,11 @@ var confirmFunc = function (id, additionalProcessing) { return; } } + if (isOldRiskSpecialApproval) { + obj.Data.Remark = (obj.Data.Remark || "").trim(); + } layer.close(layerIndex); - confirmFunc(id, additionalProcessingType); + confirmFunc(id, additionalProcessingType, currentIgnoreRiskRuleIds); }, cancel: function () { var iframeWindow = window["layui-layer-iframe" + this.index];