feat:老风控特批使用单独bool量识别,新风控需审批采用新状态RiskWarning,风控流程状态优先级Error>Warning(老风控特批,给前端CODE为ERROR)>RiskWarning>Success(可能带有ShowTIip)。新风控特批时,识别RULEID,不再采用BOOL统一通过

This commit is contained in:
ruisu
2026-07-06 17:41:13 +08:00
parent c8293db227
commit dc0cb9ec7b
11 changed files with 119 additions and 34 deletions
+5 -1
View File
@@ -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
/// <para>1:确认成交时系统调用;</para>
/// </summary>
public int TrialSource { get; set; }
[NotMapped]
public List<string> ApprovalRuleIds { get; set; } = new List<string>();
}
}
@@ -1,4 +1,4 @@
using YLErp.Models;
using YLErp.Models;
namespace YLErp.Enums
{
@@ -143,6 +143,11 @@ namespace YLErp.Enums
[Description("预警")]
Warning,
/// <summary>
/// 风控需审批
/// </summary>
[Description("风控需审批")]
RiskWarning,
/// <summary>
/// 不通过
/// </summary>
[Description("不通过")]
+20 -1
View File
@@ -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);
}
/// <summary>
/// 将逗号分隔的字符串转换为字符串数组,并去除空白项与重复项。
/// </summary>
/// <param name="commaValues">逗号分隔的字符串。</param>
/// <returns>去除空白项与重复项后的字符串数组。</returns>
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();
}
}
}
@@ -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)
@@ -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<string> 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<string>();
res.IgnoredRiskRuleIds = currentApprovalRuleIds;
//需清除ErrorMsg,不然外部调用会认为失败
//触发需审批,交易特批逻辑
if (ignoreRiskWarning)
{
var ignoredRuleIdSet = new HashSet<string>((ignoreRiskRuleIds ?? Enumerable.Empty<string>()).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
@@ -5010,12 +5022,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";
}
@@ -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;
@@ -46,7 +46,7 @@ namespace YLErp.Modules.TradeModule.DealModule
/// <summary>
/// 2021-02-04改造:批量确认因为耗时太长,用户多页面(交易列表,今日交易)并发确认导致生成多条期权费资金记录
/// </summary>
public TradeConfirmResultModel tradeConfirm(IEnumerable<int> tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false)
public TradeConfirmResultModel tradeConfirm(IEnumerable<int> tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false, IEnumerable<string> 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
/// <param name="continueQuotaCheck"></param>
/// <returns></returns>
private TradeOpenResult ConfirmTrade(int tid, bool isBatch, bool hasTradeProcess,
bool ignoreMoneyCheck, bool isSkipApproval, List<int> confirmedTradeIds, List<string> changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false)
bool ignoreMoneyCheck, bool isSkipApproval, List<int> confirmedTradeIds, List<string> changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false, IEnumerable<string> 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<int> confirmedTradeIds;
@@ -625,5 +629,6 @@ namespace YLErp.Modules.TradeModule.DealModule
public string errorMsg;
public string tipMsg;
public string type;
public List<string> ignoreRiskRuleIds;
}
}
@@ -787,6 +787,8 @@ namespace YLErp.Modules.TradeModule.DealModule
public TradeOpenRetCode RetCode;
public int TrialDataId;
public List<string> IgnoredRiskRuleIds;
public bool OldRiskNeedSpecialApproval;
/// <summary>
/// 错误信息
+11 -6
View File
@@ -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 });
}
}
+3
View File
@@ -101,6 +101,9 @@
case YLErp.Enums.QuotaTrialStatusEnum.Warning:
<i class="fa fa-exclamation-triangle status" style="color:gold" aria-hidden="true"></i><span class="status">预警</span>
break;
case YLErp.Enums.QuotaTrialStatusEnum.RiskWarning:
<i class="fa fa-exclamation-triangle status" style="color:orange" aria-hidden="true"></i><span class="status">风控需审批</span>
break;
case YLErp.Enums.QuotaTrialStatusEnum.Error:
<i class="fa fa-ban status" style="color:red" aria-hidden="true"></i><span class="status">不通过</span>
break;
@@ -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];