feat:老风控特批使用单独bool量识别,新风控需审批采用新状态RiskWarning,风控流程状态优先级Error>Warning(老风控特批,给前端CODE为ERROR)>RiskWarning>Success(可能带有ShowTIip)。新风控特批时,识别RULEID,不再采用BOOL统一通过
This commit is contained in:
@@ -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("不通过")]
|
||||
|
||||
@@ -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>
|
||||
/// 错误信息
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user