feat:在平仓时增加风控校验,创建通用超时教研函数,以备交易确认老风控下线后接入。
This commit is contained in:
@@ -26,6 +26,11 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public string TriggerPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前平仓请求提交的平仓日期
|
||||
/// </summary>
|
||||
public DateTime? UnwindDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文,供规则公式直接查询数据库
|
||||
/// </summary>
|
||||
@@ -396,6 +401,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
TradeId = ctx.TradeId,
|
||||
TriggerPoint = ctx.TriggerPoint,
|
||||
UnwindDate = ctx.UnwindDate,
|
||||
DbContext = ctx.DbContext
|
||||
};
|
||||
return runner(globals).GetAwaiter().GetResult();
|
||||
|
||||
@@ -33,6 +33,25 @@ namespace YLErp.Modules.RiskEngine
|
||||
return GetPreviousTradingDay(dbContext, date, "IB", "银行间");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定日期是否为银行间交易日。
|
||||
/// </summary>
|
||||
/// <param name="dbContext">当前风控执行使用的数据库上下文。</param>
|
||||
/// <param name="date">待判断的平仓日期。</param>
|
||||
/// <returns>日期不在银行间非交易日集合中时返回 true。</returns>
|
||||
/// <exception cref="ArgumentNullException">数据库上下文为空。</exception>
|
||||
/// <exception cref="Exception">缺少银行间日历或日历内容异常。</exception>
|
||||
public static bool IsInterbankTradingDay(YLContext dbContext, DateTime date)
|
||||
{
|
||||
if (dbContext == null)
|
||||
throw new ArgumentNullException(nameof(dbContext));
|
||||
|
||||
var holidayCache = HolidayCaches.GetOrCreateValue(dbContext);
|
||||
var holidays = GetHolidays(dbContext, date.Year, "IB", "银行间", holidayCache);
|
||||
var dateText = date.Date.ToString("yyyy,MM,dd", CultureInfo.InvariantCulture);
|
||||
return !holidays.Contains(dateText);
|
||||
}
|
||||
|
||||
public static DateTime GetPreviousExchangeTradingDay(YLContext dbContext, DateTime date)
|
||||
{
|
||||
return GetPreviousTradingDay(dbContext, date, "CHN", "交易所");
|
||||
@@ -77,11 +96,13 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (holidayCache.TryGetValue(cacheKey, out var holidays))
|
||||
return holidays;
|
||||
|
||||
// 同一年可能存在多种市场日历,按规则对应的市场代码读取非交易日。
|
||||
var calendar = dbContext.calendar
|
||||
.Where(c => c.Year == year && (c.ValidState == null || c.ValidState != ConsGlobal.InValid))
|
||||
.ToList()
|
||||
.FirstOrDefault(c => string.Equals(c.Country, country, StringComparison.OrdinalIgnoreCase));
|
||||
// 在数据库端同时按年份和市场代码筛选,避免加载同一年份的其他市场日历。
|
||||
var normalizedCountry = country.ToUpperInvariant();
|
||||
var calendar = dbContext.calendar.FirstOrDefault(c =>
|
||||
c.Year == year
|
||||
&& (c.ValidState == null || c.ValidState != ConsGlobal.InValid)
|
||||
&& c.Country != null
|
||||
&& c.Country.ToUpper() == normalizedCountry);
|
||||
|
||||
if (calendar == null)
|
||||
throw new Exception($"未找到{year}年{calendarName}日历");
|
||||
|
||||
@@ -17,6 +17,11 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// </summary>
|
||||
public string TriggerPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前平仓请求提交的平仓日期,供平仓阶段规则直接使用。
|
||||
/// </summary>
|
||||
public DateTime? UnwindDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文,供规则公式直接查询数据库
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
@@ -6931,6 +6931,40 @@ namespace YLErp.Modules.RiskModule
|
||||
DbContext.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存平仓风控试算说明,并保留首次试算时间作为二次确认超时起点。
|
||||
/// </summary>
|
||||
/// <param name="trialDataId">平仓风控试算记录ID。</param>
|
||||
/// <param name="remark">用户填写的特批说明。</param>
|
||||
/// <returns>更新后的平仓风控试算记录。</returns>
|
||||
public QuotaTrial SaveCloseRiskTrialRemark(int trialDataId, string remark)
|
||||
{
|
||||
if (trialDataId <= 0)
|
||||
{
|
||||
throw new ServiceException("无效的平仓风控试算记录");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(remark))
|
||||
{
|
||||
throw new ServiceException("必须填写说明内容,才可以提交平仓");
|
||||
}
|
||||
|
||||
const int closeTrialSource = 2;
|
||||
var trial = DbContext.quotaTrial.FirstOrDefault(item => item.id == trialDataId);
|
||||
if (trial == null
|
||||
|| trial.TrialSource != closeTrialSource
|
||||
|| trial.TrialStatus != QuotaTrialStatusEnum.RiskWarning)
|
||||
{
|
||||
throw new ServiceException("未找到有效的平仓风控审批记录");
|
||||
}
|
||||
|
||||
// 仅保存说明和操作人,不更新OptDate,避免重置首次试算的确认超时时间。
|
||||
trial.Remark = remark.Trim();
|
||||
trial.OptId = UserId;
|
||||
trial.OptName = UserName;
|
||||
DbContext.SaveChanges();
|
||||
return trial;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using YLErp.DBModels.Enums;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.Modules.EodModule;
|
||||
using YLErp.Modules.RiskEngine;
|
||||
using YLErp.Modules.TradeModule;
|
||||
using YLErp.Modules.TradeModule.DealModule;
|
||||
using YLErp.QdpModule;
|
||||
@@ -1257,14 +1258,27 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 单标的平仓
|
||||
/// </summary>
|
||||
/// <param name="unwindData"></param>
|
||||
/// <param name="confirmation">新风控二次确认信息;首次提交传空。</param>
|
||||
/// <returns>本次平仓的新风控试算结果。</returns>
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public void SwapUnwind(UnwindData unwindData)
|
||||
public RiskTrialResult SwapUnwind(UnwindData unwindData, RiskTrialConfirmation confirmation = null)
|
||||
{
|
||||
if (unwindData == null)
|
||||
{
|
||||
throw new ServiceException("平仓信息不能为空");
|
||||
}
|
||||
|
||||
var td = FindTrade(unwindData.SwapTradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
// 直接平仓不经过审核提交入口,落库前仍需执行平仓阶段的新风控,避免绕过 CLOSE_REVIEW 规则。
|
||||
var riskTrialResult = CheckCloseRisk(unwindData, confirmation);
|
||||
if (!riskTrialResult.Passed)
|
||||
{
|
||||
return riskTrialResult;
|
||||
}
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
@@ -1304,6 +1318,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
TriggerRealtimeSwapPosition();
|
||||
}
|
||||
return riskTrialResult;
|
||||
}
|
||||
/// <summary>
|
||||
/// 自动全平仓
|
||||
@@ -1824,19 +1839,36 @@ namespace YLErp.Modules.SwapModule
|
||||
/// </summary>
|
||||
/// <param name="unwindData"></param>
|
||||
/// <param name="eventType"></param>
|
||||
/// <param name="confirmation">新风控二次确认信息;首次提交传空。</param>
|
||||
/// <returns>平仓事件返回新风控试算结果,互换事件返回空。</returns>
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public void ApplySwapTrade(UnwindData unwindData, int eventType)
|
||||
public RiskTrialResult ApplySwapTrade(UnwindData unwindData, int eventType, RiskTrialConfirmation confirmation = null)
|
||||
{
|
||||
if (unwindData == null)
|
||||
{
|
||||
throw new ServiceException("互换或平仓信息不能为空");
|
||||
}
|
||||
|
||||
var td = FindTrade(unwindData.SwapTradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
RiskTrialResult riskTrialResult = null;
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
NormalizeIncomeUnwindDate(unwindData);
|
||||
ValidateIncomeValueDate(unwindData, td);
|
||||
}
|
||||
else if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
// 审核提交保存待审核事件前执行平仓阶段的新风控,需确认时先返回前端,不创建待审核事件。
|
||||
riskTrialResult = CheckCloseRisk(unwindData, confirmation);
|
||||
if (!riskTrialResult.Passed)
|
||||
{
|
||||
return riskTrialResult;
|
||||
}
|
||||
}
|
||||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
|
||||
@@ -1863,7 +1895,34 @@ namespace YLErp.Modules.SwapModule
|
||||
});
|
||||
}
|
||||
});
|
||||
return riskTrialResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行互换交易平仓审核时点的新风控试算。
|
||||
/// </summary>
|
||||
/// <param name="unwindData">包含本次平仓日期和交易ID的平仓请求。</param>
|
||||
/// <param name="confirmation">新风控二次确认信息;首次提交传空。</param>
|
||||
/// <returns>包含阻断、审批、提示及试算记录ID的新风控结果。</returns>
|
||||
/// <exception cref="ServiceException">平仓请求为空。</exception>
|
||||
private RiskTrialResult CheckCloseRisk(UnwindData unwindData, RiskTrialConfirmation confirmation)
|
||||
{
|
||||
if (unwindData == null)
|
||||
{
|
||||
throw new ServiceException("平仓信息不能为空");
|
||||
}
|
||||
|
||||
const string triggerPoint = "CLOSE_REVIEW";
|
||||
const int closeTrialSource = 2;
|
||||
var riskContext = new RiskContext
|
||||
{
|
||||
TradeId = unwindData.SwapTradeId,
|
||||
TriggerPoint = triggerPoint,
|
||||
UnwindDate = unwindData.UnwindDate
|
||||
};
|
||||
return new RiskTrialService(this).CheckRisk(riskContext, closeTrialSource, confirmation);
|
||||
}
|
||||
|
||||
private void ValidateIncomeValueDate(UnwindData unwindData, trade td)
|
||||
{
|
||||
var maxIncomeValueDate = GetMaxIncomeValueDate(td).Date;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Autofac.Core;
|
||||
using Autofac.Core;
|
||||
using BaseOUDAL;
|
||||
using CsvHelper;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
@@ -13,6 +13,8 @@ using YLErp.DBModels;
|
||||
using YLErp.DBModels.Consts;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules.RiskEngine;
|
||||
using YLErp.Modules.RiskModule;
|
||||
using YLErp.Modules.SalesModule;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Dto;
|
||||
@@ -300,12 +302,13 @@ namespace YLErp.Web.Controllers
|
||||
/// <summary>
|
||||
///单标的 平仓
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult SwapUnwindJson(UnwindData unwindData)
|
||||
/// <param name="unwindData">本次单标的平仓数据。</param>
|
||||
/// <param name="trialDataId">首次新风控试算记录ID;二次确认时传入。</param>
|
||||
/// <returns>平仓结果或新风控确认信息。</returns>
|
||||
public JsonResult SwapUnwindJson(UnwindData unwindData, int? trialDataId = null)
|
||||
{
|
||||
new SwapDealService(CurUser).SwapUnwind(unwindData);
|
||||
return JsonSuccess("平仓成功");
|
||||
var result = new SwapDealService(CurUser).SwapUnwind(unwindData, CreateRiskTrialConfirmation(trialDataId));
|
||||
return BuildCloseRiskResult(result, "平仓成功");
|
||||
}
|
||||
/// <summary>
|
||||
///多空组合 平仓
|
||||
@@ -340,13 +343,97 @@ namespace YLErp.Web.Controllers
|
||||
/// <summary>
|
||||
/// 互换/平仓提交申请
|
||||
/// </summary>
|
||||
/// <param name="unwindData"></param>
|
||||
/// <param name="eventType"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult ApplyUnwind(UnwindData unwindData, int eventType)
|
||||
/// <param name="unwindData">本次互换或平仓数据。</param>
|
||||
/// <param name="eventType">事件类型。</param>
|
||||
/// <param name="trialDataId">首次新风控试算记录ID;平仓二次确认时传入。</param>
|
||||
/// <returns>提交结果或新风控确认信息。</returns>
|
||||
public JsonResult ApplyUnwind(UnwindData unwindData, int eventType, int? trialDataId = null)
|
||||
{
|
||||
new SwapDealService(CurUser).ApplySwapTrade(unwindData, eventType);
|
||||
return JsonSuccess("提交成功");
|
||||
var result = new SwapDealService(CurUser).ApplySwapTrade(unwindData, eventType, CreateRiskTrialConfirmation(trialDataId));
|
||||
return BuildCloseRiskResult(result, "提交成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存平仓风控特批说明,不改变首次试算时间。
|
||||
/// </summary>
|
||||
/// <param name="trialDataId">平仓风控试算记录ID。</param>
|
||||
/// <param name="remark">用户填写的特批说明。</param>
|
||||
/// <returns>保存结果。</returns>
|
||||
public JsonResult SaveCloseRiskTrialRemark(int trialDataId, string remark)
|
||||
{
|
||||
try
|
||||
{
|
||||
var trial = new QuotaMonitorService(CurUser)
|
||||
.SaveCloseRiskTrialRemark(trialDataId, remark);
|
||||
new TradeRiskCheckLogService(CurUser).AddLog(trial);
|
||||
return JsonSuccess();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFactory.GetLogger("SaveCloseRiskTrialRemark").Error(ex);
|
||||
return JsonError("保存平仓风控试算说明失败!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据首次试算记录ID创建平仓新风控二次确认参数。
|
||||
/// </summary>
|
||||
/// <param name="trialDataId">首次新风控试算记录ID。</param>
|
||||
/// <returns>首次提交返回空,二次确认返回包含有效期的确认参数。</returns>
|
||||
private static RiskTrialConfirmation CreateRiskTrialConfirmation(int? trialDataId)
|
||||
{
|
||||
if (!trialDataId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var expireSeconds = 300;
|
||||
var expireSecondsConfig = AppManager.GetAppConfigValue("ProjectConfig", "Trade.RiskWarningConfirmExpireSeconds");
|
||||
if (!string.IsNullOrWhiteSpace(expireSecondsConfig)
|
||||
&& int.TryParse(expireSecondsConfig, out var configuredExpireSeconds)
|
||||
&& configuredExpireSeconds > 0)
|
||||
{
|
||||
expireSeconds = configuredExpireSeconds;
|
||||
}
|
||||
return new RiskTrialConfirmation
|
||||
{
|
||||
TrialDataId = trialDataId.Value,
|
||||
ExpireSeconds = expireSeconds
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将平仓新风控结果转换为控制器统一JSON响应。
|
||||
/// </summary>
|
||||
/// <param name="result">新风控试算结果;互换事件不执行平仓风控时为空。</param>
|
||||
/// <param name="successMessage">业务执行成功后的提示。</param>
|
||||
/// <returns>阻断、二次确认或业务成功响应。</returns>
|
||||
private JsonResult BuildCloseRiskResult(RiskTrialResult result, string successMessage)
|
||||
{
|
||||
if (result == null || result.Passed)
|
||||
{
|
||||
return JsonSuccess(successMessage, result?.ShowTip == true ? new { result.TrialDataId, result.Message } : null);
|
||||
}
|
||||
if (result.Blocked)
|
||||
{
|
||||
// 风控阻断属于可预期的业务结果,返回成功协议供前端打开只读试算详情,不进入通用请求失败分支。
|
||||
return JsonSuccessData(new
|
||||
{
|
||||
proccessType = "QuotaTrialError",
|
||||
result.TrialDataId,
|
||||
message = string.IsNullOrWhiteSpace(result.Message)
|
||||
? "平仓风控校验未通过"
|
||||
: result.Message
|
||||
});
|
||||
}
|
||||
return JsonSuccessData(new
|
||||
{
|
||||
proccessType = "AdditionalProcessing",
|
||||
source = "RiskWarning",
|
||||
type = "RiskWarningConfirm",
|
||||
result.TrialDataId,
|
||||
message = result.ConfirmationExpired ? $"原风控确认已超时,请重新确认。{result.Message}" : result.Message
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 框架合约保存
|
||||
|
||||
@@ -370,22 +370,72 @@ const vue = new Vue({
|
||||
postUrl = "/swaptrade2/ApplyUnwind";
|
||||
postData.eventType = 2;//互换3,平仓2
|
||||
}
|
||||
main.confirm(msg,
|
||||
function () {
|
||||
//重新计算百分比
|
||||
var thisObj2 = thisObj;
|
||||
main.post(postUrl, postData).done(function (res) {
|
||||
if (res.success) {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
// 提交平仓并处理新风控阻断、二次确认和成功提示。
|
||||
var submitClose = function (trialDataId) {
|
||||
var requestData = _.cloneDeep(postData);
|
||||
if (trialDataId) {
|
||||
requestData.trialDataId = trialDataId;
|
||||
}
|
||||
main.post(postUrl, requestData).done(function (res) {
|
||||
var riskData = res.obj;
|
||||
if (riskData && riskData.proccessType === "QuotaTrialError") {
|
||||
// 禁止类规则只展示试算详情,不允许继续提交平仓。
|
||||
if (riskData.TrialDataId) {
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: "风控试算详情",
|
||||
shadeClose: false,
|
||||
shade: 0.4,
|
||||
area: ['800px', '500px'],
|
||||
content: "/trade/showQuotaTrial?id=" + riskData.TrialDataId,
|
||||
btn: ["关闭"]
|
||||
});
|
||||
}
|
||||
else {
|
||||
try {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
} catch (e) {
|
||||
else if (res.msg) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (res.success && riskData && riskData.proccessType === "AdditionalProcessing" && riskData.TrialDataId) {
|
||||
// 审批类规则由用户填写说明并确认,二次提交仅回传后端生成的试算记录ID。
|
||||
var riskTrialLayerIndex = layer.open({
|
||||
type: 2,
|
||||
title: "风控试算详情",
|
||||
shadeClose: false,
|
||||
shade: 0.4,
|
||||
area: ['800px', '500px'],
|
||||
content: "/trade/showQuotaTrial?id=" + riskData.TrialDataId,
|
||||
btn: ["交易特批", "取消"],
|
||||
yes: function (index) {
|
||||
var trialPage = window["layui-layer-iframe" + index].page;
|
||||
if (!trialPage.Data.Remark || trialPage.Data.Remark.length <= 0) {
|
||||
main.message("必须填写说明内容,才可以提交平仓");
|
||||
return;
|
||||
}
|
||||
// 平仓使用专用接口且只提交记录ID和说明,避免客户端覆盖试算来源或刷新首次试算时间。
|
||||
main.post("/SwapTrade2/SaveCloseRiskTrialRemark", {
|
||||
trialDataId: riskData.TrialDataId,
|
||||
remark: trialPage.Data.Remark
|
||||
}).done(function () {
|
||||
layer.close(riskTrialLayerIndex);
|
||||
submitClose(riskData.TrialDataId);
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (res.success) {
|
||||
// 提示类规则不阻断平仓,成功后展示本次风控提示。
|
||||
if (riskData && riskData.TrialDataId && riskData.Message) {
|
||||
main.message(riskData.Message);
|
||||
}
|
||||
});
|
||||
thisObj.closetrade_cashWindow();
|
||||
}
|
||||
});
|
||||
};
|
||||
main.confirm(msg, function () {
|
||||
submitClose();
|
||||
});
|
||||
},
|
||||
getSumbitText: function () {
|
||||
return g_isShowReCheckClose ? "审核提交" : "保存";
|
||||
|
||||
Reference in New Issue
Block a user