Files
zszq-trs/YLErpDAL/Modules/RiskEngine/RiskTrialService.cs
T

269 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using Qdp.Foundation.Utilities;
using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Enums;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 新风控通用试算服务,负责按业务触发时点执行规则、保存试算结果并校验二次确认。
/// </summary>
public class RiskTrialService : YLBaseService
{
private readonly IYcLogger _logger = LogFactory.GetLogger("RiskTrialService");
/// <summary>
/// 使用当前业务服务的用户和数据库上下文创建新风控试算服务。
/// </summary>
/// <param name="baseService">当前业务服务。</param>
public RiskTrialService(YLBaseService baseService) : base(baseService)
{
}
/// <summary>
/// 执行一次独立于老风控的新风控试算并保存试算记录。
/// </summary>
/// <param name="context">包含交易、触发时点及业务日期的风控上下文。</param>
/// <param name="trialSource">试算来源,由具体业务场景约定。</param>
/// <param name="confirmation">二次确认信息;首次试算传空。</param>
/// <returns>包含阻断、审批、提示及试算记录ID的统一结果。</returns>
public RiskTrialResult CheckRisk(RiskContext context, int trialSource, RiskTrialConfirmation confirmation = null)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.TradeId <= 0)
{
throw new ArgumentException("交易ID必须大于0", nameof(context));
}
if (string.IsNullOrWhiteSpace(context.TriggerPoint))
{
throw new ArgumentException("风控触发时点不能为空", nameof(context));
}
var confirmedRuleIds = new HashSet<string>();
var confirmationExpired = false;
if (confirmation != null)
{
confirmationExpired = !TryGetConfirmedRuleIds(context, trialSource, confirmation, confirmedRuleIds);
}
var riskResult = new RiskEngineService(this).EvaluateRisk(context, context.TriggerPoint);
var currentApprovalRules = riskResult.TriggeredRules
.Where(rule => rule.ControlStrategy == RiskControlStrategy.Approval)
.ToList();
var pendingApprovalRules = confirmationExpired
? currentApprovalRules
: currentApprovalRules.Where(rule => !confirmedRuleIds.Contains(rule.RuleId)).ToList();
var trial = CreateQuotaTrial(context, trialSource, riskResult, pendingApprovalRules);
new Modules.RiskModule.QuotaMonitorService(this).SaveQuotaTrial(trial);
var result = new RiskTrialResult
{
Blocked = riskResult.Blocked,
NeedApproval = pendingApprovalRules.Any(),
ShowTip = riskResult.ShowTip,
ConfirmationExpired = confirmationExpired,
TrialDataId = trial.id,
Message = trial.RiskWarningDetails,
ApprovalRuleIds = pendingApprovalRules
.Select(rule => rule.RuleId)
.Where(ruleId => !string.IsNullOrWhiteSpace(ruleId))
.Distinct()
.ToList()
};
result.Passed = !result.Blocked && !result.NeedApproval;
_logger.Info($"[新风控试算] TradeId: {context.TradeId}, TriggerPoint: {context.TriggerPoint}, TrialSource: {trialSource}, TrialDataId: {result.TrialDataId}, Passed: {result.Passed}, Blocked: {result.Blocked}, NeedApproval: {result.NeedApproval}, ShowTip: {result.ShowTip}, ConfirmationExpired: {result.ConfirmationExpired}");
return result;
}
/// <summary>
/// 校验二次确认记录,并恢复首次试算中已确认的审批规则ID。
/// </summary>
private bool TryGetConfirmedRuleIds(RiskContext context, int trialSource, RiskTrialConfirmation confirmation, ISet<string> confirmedRuleIds)
{
if (confirmation.TrialDataId <= 0 || confirmation.ExpireSeconds <= 0)
{
return false;
}
var trial = DbContext.quotaTrial.FirstOrDefault(item => item.id == confirmation.TrialDataId);
if (trial == null
|| trial.TradeId != context.TradeId
|| trial.TrialSource != trialSource
|| !trial.OptDate.HasValue
|| !HasMatchingRequestContext(trial.RiskWarningDetails, context))
{
return false;
}
if (DateTime.Now - trial.OptDate.Value > TimeSpan.FromSeconds(confirmation.ExpireSeconds))
{
return false;
}
foreach (var ruleId in ParseApprovalRuleIds(trial.RiskWarningDetails))
{
confirmedRuleIds.Add(ruleId);
}
return confirmedRuleIds.Count > 0;
}
/// <summary>
/// 校验二次提交与首次试算的触发点和平仓日期一致,避免复用其他业务参数生成的审批记录。
/// </summary>
private static bool HasMatchingRequestContext(string details, RiskContext context)
{
var expectedContext = BuildRequestContext(context);
return (details ?? string.Empty)
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Any(line => string.Equals(line, expectedContext, StringComparison.Ordinal));
}
/// <summary>
/// 从系统生成的审批规则明细中恢复规则ID,避免直接信任前端提交的放行规则列表。
/// </summary>
private static IEnumerable<string> ParseApprovalRuleIds(string details)
{
const string approvalTitle = "[风控引擎] 规则触发:需审批";
const string ruleIdPrefix = "规则ID:";
var inApprovalSection = false;
foreach (var line in (details ?? string.Empty).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("[风控引擎] 规则触发:", StringComparison.Ordinal))
{
inApprovalSection = line.StartsWith(approvalTitle, StringComparison.Ordinal);
continue;
}
if (!inApprovalSection)
{
continue;
}
var ruleIdStart = line.IndexOf(ruleIdPrefix, StringComparison.Ordinal);
if (ruleIdStart < 0)
{
continue;
}
ruleIdStart += ruleIdPrefix.Length;
var ruleIdEnd = line.IndexOf('', ruleIdStart);
var ruleId = (ruleIdEnd < 0 ? line.Substring(ruleIdStart) : line.Substring(ruleIdStart, ruleIdEnd - ruleIdStart)).Trim();
if (!string.IsNullOrWhiteSpace(ruleId))
{
yield return ruleId;
}
}
}
/// <summary>
/// 将新风控引擎结果转换为现有试算记录,供详情页面和二次确认复用。
/// </summary>
private QuotaTrial CreateQuotaTrial(RiskContext context, int trialSource, RiskResult riskResult, IReadOnlyCollection<TriggeredRuleInfo> pendingApprovalRules)
{
// 业务入口已通过Find加载交易,优先复用当前上下文跟踪的实体,避免重复查询数据库。
var trade = DbContext.trade.Local.FirstOrDefault(item => item.id == context.TradeId)
?? DbContext.trade.Find(context.TradeId);
var trial = new QuotaTrial
{
TradeId = context.TradeId,
TradeNumber = trade?.TradeNumber ?? string.Empty,
ClientName = trade?.ClientName ?? string.Empty,
TrialSource = trialSource,
TrialStatus = riskResult.Blocked
? QuotaTrialStatusEnum.Error
: pendingApprovalRules.Any() ? QuotaTrialStatusEnum.RiskWarning : QuotaTrialStatusEnum.Success,
NewRiskBlocked = riskResult.Blocked,
NewRiskNeedApproval = pendingApprovalRules.Any(),
RiskWarningDetails = BuildRiskDetails(context, riskResult, pendingApprovalRules)
};
trial.ApprovalRuleIds = pendingApprovalRules
.Select(rule => rule.RuleId)
.Where(ruleId => !string.IsNullOrWhiteSpace(ruleId))
.Distinct()
.ToList();
return trial;
}
/// <summary>
/// 按控制策略生成与现有试算详情页面兼容的规则明细。
/// </summary>
private static string BuildRiskDetails(RiskContext context, RiskResult riskResult, IReadOnlyCollection<TriggeredRuleInfo> pendingApprovalRules)
{
var lines = new List<string>
{
BuildRequestContext(context)
};
AppendRuleDetails(lines, "禁止", riskResult.TriggeredRules.Where(rule => rule.ControlStrategy == RiskControlStrategy.Block));
AppendRuleDetails(lines, "需审批", pendingApprovalRules);
AppendRuleDetails(lines, "提示", riskResult.TriggeredRules.Where(rule => rule.ControlStrategy == RiskControlStrategy.ShowTip));
return string.Join("\n", lines);
}
/// <summary>
/// 生成首次试算的业务请求标识,供二次确认严格校验业务参数。
/// </summary>
private static string BuildRequestContext(RiskContext context)
{
return $"[风控引擎] 请求上下文:触发点:{context.TriggerPoint ?? string.Empty};平仓日期:{context.UnwindDate?.Date:yyyy-MM-dd}";
}
/// <summary>
/// 追加指定控制策略的规则明细。
/// </summary>
private static void AppendRuleDetails(ICollection<string> lines, string strategyName, IEnumerable<TriggeredRuleInfo> rules)
{
var ruleList = rules.ToList();
if (!ruleList.Any())
{
return;
}
lines.Add($"[风控引擎] 规则触发:{strategyName}");
foreach (var rule in ruleList)
{
var detail = $"应用ID:{rule.ApplicationId?.ToString() ?? "-"};规则ID:{rule.RuleId};规则名称:{rule.RuleName};规则说明:{rule.RuleText}";
if (!string.IsNullOrWhiteSpace(rule.Message))
{
detail += $";信息:{rule.Message}";
}
lines.Add(detail);
}
}
}
/// <summary>
/// 新风控二次确认参数。
/// </summary>
public class RiskTrialConfirmation
{
/// <summary>
/// 首次风控试算记录ID。
/// </summary>
public int TrialDataId { get; set; }
/// <summary>
/// 首次试算允许确认放行的有效时长,单位为秒。
/// </summary>
public int ExpireSeconds { get; set; }
}
/// <summary>
/// 新风控通用试算结果。
/// </summary>
public class RiskTrialResult
{
public bool Passed { get; set; }
public bool Blocked { get; set; }
public bool NeedApproval { get; set; }
public bool ShowTip { get; set; }
public bool ConfirmationExpired { get; set; }
public int TrialDataId { get; set; }
public string Message { get; set; }
public List<string> ApprovalRuleIds { get; set; } = new List<string>();
}
}