Merge branch 'feature/p132_74-risk-engine' of https://gitee.glmszq.com/gsty/onederiv/trs into feature/p132_74-risk-engine

This commit is contained in:
尹峰
2026-07-21 15:05:40 +08:00
3 changed files with 124 additions and 28 deletions
@@ -4465,12 +4465,20 @@ namespace YLErp.Modules.RiskModule
return result;
}
public bool QuotaCheck(ref TradeOpenResult res)
/// <summary>
/// 交易确认风控检查入口:执行完整风控试算(老风控 + 新风控),并把试算结果转换为交易确认结果。
/// 返回 true 表示确认流程可以继续;返回 false 表示需要阻断、提示或审批。
/// </summary>
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<string> ignoreRiskRuleIds = null)
/// <summary>
/// 交易确认风控检查入口:执行完整风控试算(老风控 + 新风控)。
/// ignoreRiskWarning/ignoreRiskRuleIds 只用于新风控“需审批”规则的二次确认,不影响老风控结果。
/// </summary>
public bool CheckTradeConfirmRisk(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable<string> 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);
}
/// <summary>
/// 交易确认新风控检查入口:只执行新风控试算。
/// 用于老风控已被特批放行(ignoreMoneyCheck = true)的场景,避免老风控特批误跳过新风控。
/// </summary>
public bool CheckTradeConfirmNewRisk(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable<string> 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);
}
/// <summary>
/// 将风控试算结果转换为交易确认结果。
/// 这里不重新执行风控,只负责设置 RetCode、ErrorMsg、TipMsg、TrialDataId 等返回给确认流程的字段。
/// </summary>
private bool ApplyQuotaCheckResult(ref TradeOpenResult res, QuotaTrial quotaObj, bool ignoreRiskWarning, IEnumerable<string> ignoreRiskRuleIds = null)
{
if (quotaObj.TrialStatus == QuotaTrialStatusEnum.Success)//这次计算结果是通过,直接通过
{
if (!string.IsNullOrWhiteSpace(quotaObj.AvailableForClient)) // 返回用户资金情况
@@ -5026,12 +5064,13 @@ namespace YLErp.Modules.RiskModule
}
/// <summary>
/// 交易试算
/// 完整风控试算:先执行老风控资金/限额试算,再叠加新风控试算结果。
/// 后续老风控下线时,调用方可以保留该入口,内部改为直接调用 RunNewRiskTrial。
/// </summary>
/// <param name="tradeId">交易Id,结构化交易请传主交易Id</param>
/// <param name="trialSource">试算来源,0:手动点击试算按钮;1:确认成交时自动调用;</param>
/// <returns></returns>
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;
}
/// <summary>
/// 新风控独立试算:不执行老风控资金/限额检查,只调用新风控引擎。
/// 用于老风控特批后的确认流程,也可作为后续老风控下线后的主试算入口。
/// </summary>
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;
}
/// <summary>
/// 将新风控引擎结果合并到 QuotaTrial。
/// 如果传入的是完整试算结果,则是在老风控结果基础上叠加新风控;如果传入的是新建结果,则表示只跑新风控。
/// </summary>
private QuotaTrial ApplyNewRiskTrial(int tradeId, QuotaTrial result)
{
// ===== 新风控引擎接入点(设计文档 4.6.1) =====
try
{
@@ -5318,7 +5403,6 @@ namespace YLErp.Modules.RiskModule
}
// ===== 新风控引擎接入结束 =====
SaveQuotaTrial(result);
return result;
}
/// <summary>
@@ -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 = "确认交易";
+1 -1
View File
@@ -8428,7 +8428,7 @@ namespace YLErp.Web.Controllers
/// <returns></returns>
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);
}