From 37e275d81e2b84d347f32c1c6965365ef5776d9f Mon Sep 17 00:00:00 2001 From: ruisu Date: Mon, 20 Jul 2026 15:06:58 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=8B=86=E5=88=86=E6=96=B0?= =?UTF-8?q?=E8=80=81=E9=A3=8E=E6=8E=A7=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E4=B8=BA=E4=B9=8B=E5=90=8E=E8=80=81=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E4=B8=8B=E7=BA=BF=E5=81=9A=E5=87=86=E5=A4=87=E3=80=82=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E4=BF=AE=E5=A4=8D=E8=80=81=E9=A3=8E=E6=8E=A7=E7=89=B9?= =?UTF-8?q?=E6=89=B9=E8=B7=B3=E8=BF=87=E6=96=B0=E9=A3=8E=E6=8E=A7=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/RiskModule/QuotaMonitorService.cs | 104 ++++++++++++++++-- .../DealModule/TradeConfirmService.cs | 46 +++++--- YLErpWeb/Controllers/tradeController.cs | 2 +- 3 files changed, 124 insertions(+), 28 deletions(-) diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs index eab45af4..e2315e10 100644 --- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs +++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs @@ -4465,12 +4465,20 @@ namespace YLErp.Modules.RiskModule return result; } - public bool QuotaCheck(ref TradeOpenResult res) + /// + /// 交易确认风控检查入口:执行完整风控试算(老风控 + 新风控),并把试算结果转换为交易确认结果。 + /// 返回 true 表示确认流程可以继续;返回 false 表示需要阻断、提示或审批。 + /// + public bool CheckTradeConfirmRisk(ref TradeOpenResult res) { - return QuotaCheck(ref res, false); + return CheckTradeConfirmRisk(ref res, false); } - public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) + /// + /// 交易确认风控检查入口:执行完整风控试算(老风控 + 新风控)。 + /// ignoreRiskWarning/ignoreRiskRuleIds 只用于新风控“需审批”规则的二次确认,不影响老风控结果。 + /// + public bool CheckTradeConfirmRisk(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) { if (res == null) { @@ -4483,9 +4491,39 @@ namespace YLErp.Modules.RiskModule } var tradeId = (res.Trade.ParentTradeId == 0 || res.Trade.IsGroup != 0) ? res.Trade.id : res.Trade.ParentTradeId; - var tradeNumber = DbContext.trade.Where(O => O.id == tradeId).Select(O => O.TradeNumber).FirstOrDefault(); var trialService = new QuotaMonitorService(this); - var quotaObj = trialService.QuotaCheck(tradeId, 1); + var quotaObj = trialService.RunQuotaTrial(tradeId, 1); + return ApplyQuotaCheckResult(ref res, quotaObj, ignoreRiskWarning, ignoreRiskRuleIds); + } + + /// + /// 交易确认新风控检查入口:只执行新风控试算。 + /// 用于老风控已被特批放行(ignoreMoneyCheck = true)的场景,避免老风控特批误跳过新风控。 + /// + public bool CheckTradeConfirmNewRisk(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) + { + if (res == null) + { + throw new ArgumentNullException(nameof(res)); + } + + if (res.Trade == null) + { + throw new ArgumentNullException(nameof(res.Trade)); + } + + var tradeId = (res.Trade.ParentTradeId == 0 || res.Trade.IsGroup != 0) ? res.Trade.id : res.Trade.ParentTradeId; + var trialService = new QuotaMonitorService(this); + var quotaObj = trialService.RunNewRiskTrial(tradeId, 1); + return ApplyQuotaCheckResult(ref res, quotaObj, ignoreRiskWarning, ignoreRiskRuleIds); + } + + /// + /// 将风控试算结果转换为交易确认结果。 + /// 这里不重新执行风控,只负责设置 RetCode、ErrorMsg、TipMsg、TrialDataId 等返回给确认流程的字段。 + /// + private bool ApplyQuotaCheckResult(ref TradeOpenResult res, QuotaTrial quotaObj, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) + { if (quotaObj.TrialStatus == QuotaTrialStatusEnum.Success)//这次计算结果是通过,直接通过 { if (!string.IsNullOrWhiteSpace(quotaObj.AvailableForClient)) // 返回用户资金情况 @@ -5026,12 +5064,13 @@ namespace YLErp.Modules.RiskModule } /// - /// 交易试算 + /// 完整风控试算:先执行老风控资金/限额试算,再叠加新风控试算结果。 + /// 后续老风控下线时,调用方可以保留该入口,内部改为直接调用 RunNewRiskTrial。 /// /// 交易Id,结构化交易请传主交易Id /// 试算来源,0:手动点击试算按钮;1:确认成交时自动调用; /// - public QuotaTrial QuotaCheck(int tradeId, int trialSource) + public QuotaTrial RunQuotaTrial(int tradeId, int trialSource) { _logger.Info($"[限额试算] 开始执行 - tradeId: {tradeId}, trialSource: {trialSource}"); var result = new QuotaTrial(); @@ -5076,8 +5115,20 @@ namespace YLErp.Modules.RiskModule tradeList = tradeList.Where(O => !string.IsNullOrEmpty(O.UnderlyingCode) && O.TradeType != "结构化交易" && O.TradeType != "现金流交易").ToList(); if (tradeList.Count == 0) { + // 老风控没有可试算的标的时,老逻辑认为试算通过;这里仍需继续执行新风控,避免普通确认漏掉新风控检查。 //("当前交易不需要试算"); - return new QuotaTrial() { TrialStatus = QuotaTrialStatusEnum.Success }; + result.TrialStatus = QuotaTrialStatusEnum.Success; + result.TradeId = tradeObj.id; + result.TradeNumber = tradeObj.TradeNumber ?? ""; + result.ClientName = tradeObj.ClientName ?? ""; + result.FundCheckDetails = fundMsg ?? ""; + result.RiskWarningDetails = riskWarningMsg ?? ""; + result.AvailableForClient = availableMsg ?? ""; + result.TrialSource = trialSource; + result.OldRiskTrialStatus = result.TrialStatus; + result = ApplyNewRiskTrial(tradeId, result); + SaveQuotaTrial(result); + return result; } string quotaWarningMsg = "", quotaMsg = ""; bool quotaWarningStatus = true, quotaStatus = true; @@ -5243,8 +5294,42 @@ namespace YLErp.Modules.RiskModule result.AvailableForClient = availableMsg ?? ""; result.TrialSource = trialSource; result.OldRiskTrialStatus = result.TrialStatus; - _logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}"); + // 保留老风控原始状态,随后叠加新风控结果;这样二次特批时可以区分是老风控失败还是新风控需审批。 + result = ApplyNewRiskTrial(tradeId, result); + SaveQuotaTrial(result); + return result; + } + /// + /// 新风控独立试算:不执行老风控资金/限额检查,只调用新风控引擎。 + /// 用于老风控特批后的确认流程,也可作为后续老风控下线后的主试算入口。 + /// + public QuotaTrial RunNewRiskTrial(int tradeId, int trialSource) + { + var result = new QuotaTrial + { + TradeId = tradeId, + TrialSource = trialSource, + TrialStatus = QuotaTrialStatusEnum.Success, + OldRiskTrialStatus = QuotaTrialStatusEnum.Success + }; + var tradeObj = DbContext.trade.AsNoTracking().Where(O => O.id == tradeId).FirstOrDefault(); + if (tradeObj != null) + { + result.TradeNumber = tradeObj.TradeNumber ?? ""; + result.ClientName = tradeObj.ClientName ?? ""; + } + result = ApplyNewRiskTrial(tradeId, result); + SaveQuotaTrial(result); + return result; + } + + /// + /// 将新风控引擎结果合并到 QuotaTrial。 + /// 如果传入的是完整试算结果,则是在老风控结果基础上叠加新风控;如果传入的是新建结果,则表示只跑新风控。 + /// + private QuotaTrial ApplyNewRiskTrial(int tradeId, QuotaTrial result) + { // ===== 新风控引擎接入点(设计文档 4.6.1) ===== try { @@ -5318,7 +5403,6 @@ namespace YLErp.Modules.RiskModule } // ===== 新风控引擎接入结束 ===== - SaveQuotaTrial(result); return result; } /// diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs index 7bca1074..018cac68 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs @@ -203,26 +203,38 @@ namespace YLErp.Modules.TradeModule.DealModule result.ErrorMsg = $"未找到客户信息:{td.ClientName}"; return result; } - if (!ignoreMoneyCheck && !continueQuotaCheck) + if (!continueQuotaCheck) { - result = CheckTradeConfirmRule(result, td, isSkipApproval); - - if (!string.IsNullOrWhiteSpace(result.ErrorMsg)) - { - return result; - } - - result = CheckClientRating(result, client, td); - - if (!string.IsNullOrWhiteSpace(result.ErrorMsg)) - { - return result; - } - var trialService = new QuotaMonitorService(this); - if (!trialService.QuotaCheck(ref result, ignoreRiskWarning, ignoreRiskRuleIds)) + if (ignoreMoneyCheck) { - return result; + // ignoreMoneyCheck 表示老风控资金/限额检查已被特批放行,只跳过老风控;新风控仍需独立执行。 + if (!trialService.CheckTradeConfirmNewRisk(ref result, ignoreRiskWarning, ignoreRiskRuleIds)) + { + return result; + } + } + else + { + // 正常确认流程:先执行原有交易确认规则和客户评级检查,再执行完整风控(老风控 + 新风控)。 + result = CheckTradeConfirmRule(result, td, isSkipApproval); + + if (!string.IsNullOrWhiteSpace(result.ErrorMsg)) + { + return result; + } + + result = CheckClientRating(result, client, td); + + if (!string.IsNullOrWhiteSpace(result.ErrorMsg)) + { + return result; + } + + if (!trialService.CheckTradeConfirmRisk(ref result, ignoreRiskWarning, ignoreRiskRuleIds)) + { + return result; + } } } var history = "确认交易"; diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index 0a162c3c..cf12800c 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -8428,7 +8428,7 @@ namespace YLErp.Web.Controllers /// public ActionResult quotaTrial(int id, int trialSource = 0) { - var quotaTrial = new QuotaMonitorService(CurUser).QuotaCheck(id, trialSource); + var quotaTrial = new QuotaMonitorService(CurUser).RunQuotaTrial(id, trialSource); return View(quotaTrial); }