fix(risk-engine): 修复多个风控引擎问题

- ScopeIsGlobal 类型由 bool 改为 int 以兼容前端传输 0/1
- 修复 QueryApplicationList 中 Status 投影 EF Core enum 转换失败
- 新增 Scope 字段查询支持(逗号分隔集合精确匹配)
- 新增变量列表 DataType 筛选
- 修复审计日志日期查询范围(EndDate 纳入整天数据)
- 统一 Application 审计日志 OperationType 前缀(APP_xxx)
- 审计日志 OperationDetail 枚举值映射为中文
This commit is contained in:
尹峰
2026-06-29 18:09:19 +08:00
parent 04c01f2f47
commit d7c09c793a
14 changed files with 173 additions and 27 deletions
@@ -13,7 +13,7 @@ namespace YLErp.DBModels
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
public int Version { get; set; } = 1;
public int? UpdateOptId { get; set; }
public string UpdateOptName { get; set; }
@@ -50,7 +50,7 @@ INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr
'挂钩标的到期日早于合约到期日时禁止交易',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 22,
'Operator', '<', 'ThresholdType', 'Variable',
'Operator', '早于', 'ThresholdType', 'Variable',
'ThresholdVariableId', 3
)),
'market.UnderlyingMaturityDate < trade.ExerciseDate',
@@ -87,7 +87,8 @@ INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr
'保证金利率不在配置区间内(默认2%~5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 12,
'Operator', '不介于', 'ThresholdType', 'Fixed', 'Value', JSON_ARRAY(2, 5)
'Operator', '不介于', 'ThresholdType', 'Fixed', 'Value', JSON_ARRAY(2, 5),
'IncludeLowerBound', true, 'IncludeUpperBound', true
)),
'!(client_marginrate.InitMarginRebateRate >= 2 && client_marginrate.InitMarginRebateRate <= 5)',
1, 1, 0, 'SYSTEM', NOW());
@@ -112,7 +113,7 @@ INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr
'合约起息日早于当前日期时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 2,
'Operator', '<', 'ThresholdType', 'Variable',
'Operator', '早于', 'ThresholdType', 'Variable',
'ThresholdVariableId', 21
)),
'trade.StartDate < sys.CurrentDate',
@@ -304,12 +304,19 @@ namespace YLErp.Modules.RiskEngine
{
return op?.Trim() switch
{
// 数值型 C# 符号
"=" or "==" => "==",
"!=" or "≠" => "!=",
">" => ">",
"<" => "<",
">=" => ">=",
"<=" => "<=",
// 日期型中文语义 → C# 符号
"早于" => "<",
"晚于" => ">",
"等于" => "==",
"不早于" => ">=",
"不晚于" => "<=",
_ => throw new ArgumentException($"不支持的操作符:{op}")
};
}
@@ -11,6 +11,6 @@ namespace YLErp.Modules.RiskEngine.Dto
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
}
}
@@ -9,5 +9,9 @@ namespace YLErp.Modules.RiskEngine.Dto
public RiskRuleStatus? Status { get; set; }
public RiskControlStrategy? Strategy { get; set; }
public string TriggerPoint { get; set; }
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public string ScopeAssetBookIds { get; set; }
}
}
@@ -6,6 +6,7 @@ namespace YLErp.Modules.RiskEngine.Dto
public class QueryRiskVariableReq : BaseSearchReq
{
public RiskVariableCategory? Category { get; set; }
public RiskVariableDataType? DataType { get; set; }
public string VariableName { get; set; }
}
}
@@ -14,7 +14,7 @@ namespace YLErp.Modules.RiskEngine.Dto
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
public int Version { get; set; }
public string OptName { get; set; }
public DateTime OptDate { get; set; }
@@ -7,14 +7,14 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public long Id { get; set; }
public string RuleIds { get; set; }
public RiskRuleStatus Status { get; set; }
public int Status { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
public string ScopeAssetBookIds { get; set; }
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
public int Version { get; set; }
public string OptName { get; set; }
public DateTime OptDate { get; set; }
@@ -7,5 +7,8 @@ namespace YLErp.Modules.RiskEngine.Dto
public string ThresholdType { get; set; }
public object Value { get; set; }
public long? ThresholdVariableId { get; set; }
// 介于/不介于 的边界包含控制,默认 true
public bool IncludeLowerBound { get; set; } = true;
public bool IncludeUpperBound { get; set; } = true;
}
}
@@ -11,7 +11,7 @@ namespace YLErp.Modules.RiskEngine.Dto
public string ScopeClientIds { get; set; }
public string ScopeUnderlyingTypes { get; set; }
public string ScopeTradeTypes { get; set; }
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
public int Version { get; set; }
}
}
@@ -436,7 +436,7 @@ namespace YLErp.Modules.RiskEngine
return false;
}
if (application.ScopeIsGlobal)
if (application.ScopeIsGlobal == 1)
{
return true;
}
@@ -67,7 +67,7 @@ namespace YLErp.Modules.RiskEngine
/// true 表示对所有交易都适用,此时通常忽略其他 Scope 字段;
/// false 表示只在指定范围内适用,需要结合下方各 Scope 字段进行过滤。
/// </summary>
public bool ScopeIsGlobal { get; set; }
public int ScopeIsGlobal { get; set; }
/// <summary>
/// 适用的资产簿记账户范围。
+145 -16
View File
@@ -1,6 +1,7 @@
using BaseOUDAL;
using Newtonsoft.Json;
using Qdp.Foundation.Utilities;
using System.Linq.Expressions;
using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Model;
@@ -202,7 +203,7 @@ namespace YLErp.Modules.RiskEngine
private static readonly Dictionary<RiskVariableDataType, HashSet<string>> ValidOperatorsByType = new Dictionary<RiskVariableDataType, HashSet<string>>
{
{ RiskVariableDataType.Numeric, new HashSet<string> { ">", "<", ">=", "<=", "=", "≠", "介于", "不介于" } },
{ RiskVariableDataType.Date, new HashSet<string> { "<", ">", "=", ">=", "<=", "介于", "不介于" } },
{ RiskVariableDataType.Date, new HashSet<string> { "早于", "晚于", "等于", "不早于", "不晚于", "介于", "不介于" } },
{ RiskVariableDataType.Boolean, new HashSet<string> { "是", "否" } }
};
@@ -291,15 +292,35 @@ namespace YLErp.Modules.RiskEngine
{
if (cond.Value == null)
throw new ServiceException($"{condLabel}:固定阈值不能为空");
if (variableDef.DataType == RiskVariableDataType.Numeric)
if (cond.Operator == "介于" || cond.Operator == "不介于")
{
if (!decimal.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字");
// 介于/不介于:Value 必须为双元素数组 [下限, 上限]
if (!(cond.Value is Newtonsoft.Json.Linq.JArray arr && arr.Count == 2))
throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 的阈值必须为双元素数组 [下限, 上限]");
if (variableDef.DataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(arr[0].ToString(), out _) || !decimal.TryParse(arr[1].ToString(), out _))
throw new ServiceException($"{condLabel}:数值型介于/不介于的下限和上限必须为数字");
}
if (variableDef.DataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(arr[0].ToString(), out _) || !DateTime.TryParse(arr[1].ToString(), out _))
throw new ServiceException($"{condLabel}:日期型介于/不介于的起始和结束日期必须为合法日期");
}
}
if (variableDef.DataType == RiskVariableDataType.Date)
else
{
if (!DateTime.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期");
if (variableDef.DataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字");
}
if (variableDef.DataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期");
}
}
}
else if (cond.ThresholdType == "Variable")
@@ -356,7 +377,7 @@ namespace YLErp.Modules.RiskEngine
private void ValidateScopeFields(CreateRiskApplicationReq req)
{
if (req.ScopeIsGlobal)
if (req.ScopeIsGlobal == 1)
{
if (!string.IsNullOrEmpty(req.ScopeAssetBookIds) ||
!string.IsNullOrEmpty(req.ScopeClientIds) ||
@@ -513,6 +534,36 @@ namespace YLErp.Modules.RiskEngine
return string.Join(", ", names);
}
private static string GetStrategyName(RiskControlStrategy strategy)
{
return strategy switch
{
RiskControlStrategy.Block => "阻断",
RiskControlStrategy.Approval => "审批",
RiskControlStrategy.ShowTip => "提醒",
_ => strategy.ToString()
};
}
private static readonly Dictionary<string, string> TriggerPointCnMap = new Dictionary<string, string>
{
["BOOK_CONFIRM"] = "交易录入确认",
["CLOSE_REVIEW"] = "平仓审核",
["UPLOAD_CONFIRMATION"] = "上传确认书",
["EVENT_TRIGGER"] = "事件触发",
["FUND_PAYMENT"] = "资金支付"
};
private static string MapTriggerPointsToChinese(string triggerPoints)
{
if (string.IsNullOrWhiteSpace(triggerPoints))
return "";
var parts = triggerPoints.Split(',');
var translated = parts.Select(p => TriggerPointCnMap.TryGetValue(p.Trim(), out var cn) ? cn : p.Trim());
return string.Join(", ", translated);
}
#endregion
#region Rule Management
@@ -813,7 +864,7 @@ namespace YLErp.Modules.RiskEngine
{
Id = app.id,
RuleIds = app.RuleIds,
Status = app.Status,
Status = (int)app.Status,
ControlStrategy = app.ControlStrategy,
TriggerPoints = app.TriggerPoints,
ScopeAssetBookIds = app.ScopeAssetBookIds,
@@ -878,12 +929,84 @@ namespace YLErp.Modules.RiskEngine
&& r.RuleName.Contains(req.RuleName)));
}
if (!string.IsNullOrWhiteSpace(req.ScopeClientIds))
{
var ids = req.ScopeClientIds.Split(',', StringSplitOptions.RemoveEmptyEntries);
Expression<Func<glms_risk_rule_application, bool>> scopePredicate = null;
foreach (var id in ids)
{
var idCopy = id;
Expression<Func<glms_risk_rule_application, bool>> expr = a =>
a.ScopeClientIds == idCopy ||
a.ScopeClientIds.StartsWith(idCopy + ",") ||
a.ScopeClientIds.EndsWith("," + idCopy) ||
a.ScopeClientIds.Contains("," + idCopy + ",");
scopePredicate = scopePredicate == null ? expr : scopePredicate.Or(expr);
}
if (scopePredicate != null)
query = query.Where(scopePredicate);
}
if (!string.IsNullOrWhiteSpace(req.ScopeUnderlyingTypes))
{
var types = req.ScopeUnderlyingTypes.Split(',', StringSplitOptions.RemoveEmptyEntries);
Expression<Func<glms_risk_rule_application, bool>> scopePredicate = null;
foreach (var type in types)
{
var typeCopy = type;
Expression<Func<glms_risk_rule_application, bool>> expr = a =>
a.ScopeUnderlyingTypes == typeCopy ||
a.ScopeUnderlyingTypes.StartsWith(typeCopy + ",") ||
a.ScopeUnderlyingTypes.EndsWith("," + typeCopy) ||
a.ScopeUnderlyingTypes.Contains("," + typeCopy + ",");
scopePredicate = scopePredicate == null ? expr : scopePredicate.Or(expr);
}
if (scopePredicate != null)
query = query.Where(scopePredicate);
}
if (!string.IsNullOrWhiteSpace(req.ScopeTradeTypes))
{
var types = req.ScopeTradeTypes.Split(',', StringSplitOptions.RemoveEmptyEntries);
Expression<Func<glms_risk_rule_application, bool>> scopePredicate = null;
foreach (var type in types)
{
var typeCopy = type;
Expression<Func<glms_risk_rule_application, bool>> expr = a =>
a.ScopeTradeTypes == typeCopy ||
a.ScopeTradeTypes.StartsWith(typeCopy + ",") ||
a.ScopeTradeTypes.EndsWith("," + typeCopy) ||
a.ScopeTradeTypes.Contains("," + typeCopy + ",");
scopePredicate = scopePredicate == null ? expr : scopePredicate.Or(expr);
}
if (scopePredicate != null)
query = query.Where(scopePredicate);
}
if (!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds))
{
var ids = req.ScopeAssetBookIds.Split(',', StringSplitOptions.RemoveEmptyEntries);
Expression<Func<glms_risk_rule_application, bool>> scopePredicate = null;
foreach (var id in ids)
{
var idCopy = id;
Expression<Func<glms_risk_rule_application, bool>> expr = a =>
a.ScopeAssetBookIds == idCopy ||
a.ScopeAssetBookIds.StartsWith(idCopy + ",") ||
a.ScopeAssetBookIds.EndsWith("," + idCopy) ||
a.ScopeAssetBookIds.Contains("," + idCopy + ",");
scopePredicate = scopePredicate == null ? expr : scopePredicate.Or(expr);
}
if (scopePredicate != null)
query = query.Where(scopePredicate);
}
var pagedResult = query.OrderByDescending(a => a.UpdateDate)
.Select(a => new RiskApplicationListItem
{
Id = a.id,
RuleIds = a.RuleIds,
Status = a.Status,
Status = (int)a.Status,
ControlStrategy = a.ControlStrategy,
TriggerPoints = a.TriggerPoints,
ScopeAssetBookIds = a.ScopeAssetBookIds,
@@ -962,7 +1085,7 @@ namespace YLErp.Modules.RiskEngine
DbContext.SaveChanges();
WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, ResolveRuleNames(req.RuleIds),
$"创建应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}",
$"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}",
snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
DbContext.SaveChanges();
@@ -1016,7 +1139,7 @@ namespace YLErp.Modules.RiskEngine
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds),
$"修改应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}");
$"修改应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}");
DbContext.SaveChanges();
TryRefreshCache();
@@ -1034,7 +1157,7 @@ namespace YLErp.Modules.RiskEngine
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APPLICATION_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置");
WriteAuditLog("APP_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置");
DbContext.SaveChanges();
TryRefreshCache();
@@ -1127,7 +1250,7 @@ namespace YLErp.Modules.RiskEngine
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置");
WriteAuditLog("APP_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置");
}
DbContext.SaveChanges();
@@ -1154,6 +1277,11 @@ namespace YLErp.Modules.RiskEngine
query = query.Where(v => v.Category == req.Category.Value);
}
if (req.DataType.HasValue)
{
query = query.Where(v => v.DataType == req.DataType.Value);
}
if (!string.IsNullOrWhiteSpace(req.VariableName))
{
query = query.Where(v => v.VariableName.Contains(req.VariableName));
@@ -1365,12 +1493,13 @@ namespace YLErp.Modules.RiskEngine
if (req.StartDate.HasValue)
{
query = query.Where(l => l.OptDate >= req.StartDate.Value);
query = query.Where(l => l.OptDate >= req.StartDate.Value.Date);
}
if (req.EndDate.HasValue)
{
query = query.Where(l => l.OptDate <= req.EndDate.Value);
var end = req.EndDate.Value.Date.AddDays(1);
query = query.Where(l => l.OptDate < end);
}
if (!string.IsNullOrWhiteSpace(req.Keyword))
+1
View File
@@ -25,6 +25,7 @@
{Name:"资金监控",Rights:["风险控制-资金监控"],Url:"client/clientRiskMonitor"},
{Name:"市场风险",Rights:["风险控制-市场风险"],Url:"risk/RiskExposureReport"},
{Name:"限额监控",Rights:["风险控制-限额监控"],Url:"risk/quotaMonitor"},
{Name:"异常交易监控",Rights:["风险控制-异常交易监控"],Url:"v3//risk/risk-engine-config"},
{Name:"白名单券池",Rights:["风险控制-白名单券池"],Url:"v3/data/underlying-pool"},
{Name:"日终持仓风险",Rights:["风险控制-日终持仓风险"],Url:"trade/EodPositionRisks"},
{Name:"日终持仓风险_互换",Rights:["风险控制-日终持仓风险_互换"],Url:"swaptrade2/EodPositionRisks"},