Merge remote-tracking branch 'origin/feature/p132_74-risk-engine' into spike/merge-p132_74-to-1.4.2

This commit is contained in:
hjhan
2026-07-14 14:48:11 +08:00
66 changed files with 6085 additions and 35 deletions
+14
View File
@@ -160,3 +160,17 @@ csharp_preserve_single_line_statements = true
# CA1819: Properties should not return arrays
dotnet_diagnostic.CA1819.severity = silent
# JavaScript 和 TypeScript 文件
[*.{js,jsx,ts,tsx}]
# 缩进和间距
indent_size = 4
indent_style = space
tab_width = 4
# 新行首选项
end_of_line = crlf
insert_final_newline = false
# 拖尾逗号不添加
trailing_comma = none
@@ -0,0 +1,9 @@
namespace YLErp.DBModels
{
public enum RiskControlStrategy
{
Block = 1,
Approval = 2,
ShowTip = 3
}
}
@@ -0,0 +1,9 @@
namespace YLErp.DBModels
{
public enum RiskRuleStatus
{
Active = 1,
Disabled = 2,
Deleted = 3
}
}
@@ -0,0 +1,10 @@
namespace YLErp.DBModels
{
public enum RiskVariableCategory
{
BookingElement = 1,
MarketData = 2,
SystemCalc = 3,
BooleanCheck = 4
}
}
@@ -0,0 +1,9 @@
namespace YLErp.DBModels
{
public enum RiskVariableDataType
{
Numeric = 1,
Date = 2,
Boolean = 3
}
}
+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>();
}
}
@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 风控规则定义表
/// </summary>
[Table("glms_risk_rule")]
public class glms_risk_rule : DBModelWithOperator
{
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
/// <summary>结构化条件 JSON(变量+操作符+阈值列表)</summary>
public string ConditionJson { get; set; }
/// <summary>规则执行表达式(C# 布尔表达式,引擎用 Roslyn 编译执行)</summary>
public string RuleExpr { get; set; }
/// <summary>规则说明</summary>
public string Description { get; set; }
/// <summary>规则状态</summary>
public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; } = 1;
/// <summary>最后更新人工号</summary>
public int? UpdateOptId { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime? UpdateDate { get; set; }
}
}
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 风控规则应用配置表
/// </summary>
[Table("glms_risk_rule_application")]
public class glms_risk_rule_application : DBModelWithOperator
{
/// <summary>关联规则 ID 列表(JSON 数组)</summary>
public string RuleIds { get; set; }
/// <summary>应用说明</summary>
public string Description { get; set; }
/// <summary>应用配置状态</summary>
public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
/// <summary>风控策略(预警/阻断)</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表(JSON 数组)</summary>
public string TriggerPoints { get; set; }
/// <summary>适用范围-资产账簿 ID 列表(JSON 数组)</summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>适用范围-客户 ID 列表(JSON 数组)</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表(JSON 数组)</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表(JSON 数组)</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>是否全局生效(0:否 1:是)</summary>
public int ScopeIsGlobal { get; set; }
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; } = 1;
/// <summary>最后更新人工号</summary>
public int? UpdateOptId { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime? UpdateDate { get; set; }
}
}
@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 风控操作审计日志表
/// </summary>
[Table("glms_risk_rule_audit_log")]
public class glms_risk_rule_audit_log : DBModelBase
{
/// <summary>操作类型(创建/更新/删除/启用/禁用)</summary>
public string OperationType { get; set; }
/// <summary>操作目标类型(Rule/Variable/Application</summary>
public string TargetType { get; set; }
/// <summary>操作目标 ID</summary>
public long TargetId { get; set; }
/// <summary>操作目标名称</summary>
public string TargetName { get; set; }
/// <summary>操作详情描述</summary>
public string OperationDetail { get; set; }
/// <summary>操作结果(成功/失败)</summary>
public string Result { get; set; }
/// <summary>操作前数据快照(JSON</summary>
public string SnapshotData { get; set; }
/// <summary>操作人工号</summary>
public long OptId { get; set; }
/// <summary>操作人姓名</summary>
public string OptName { get; set; }
/// <summary>操作时间</summary>
public DateTime OptDate { get; set; }
}
}
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 风控变量池定义表
/// </summary>
[Table("glms_risk_variable")]
public class glms_risk_variable : DBModelWithOperator
{
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>值域描述</summary>
public string ValueDomain { get; set; }
/// <summary>变量说明</summary>
public string Description { get; set; }
/// <summary>取值表达式(C# 表达式,用于计算变量值)</summary>
public string VariableExpr { get; set; }
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; } = 1;
/// <summary>排序序号</summary>
public int SortOrder { get; set; }
/// <summary>最后更新人工号</summary>
public int? UpdateOptId { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime? UpdateDate { get; set; }
}
}
@@ -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("不通过")]
@@ -0,0 +1,84 @@
SET FOREIGN_KEY_CHECKS=0;
CREATE TABLE `glms_risk_rule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`RuleName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '规则名称',
`RuleText` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规则文本',
`ConditionJson` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '条件JSON(结构化模式, RuleCondition数组)',
`RuleExpr` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '规则表达式(自由文本模式, 类C#表达式)',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
`OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称',
`OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间',
`UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id',
`UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称',
`UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_status`(`Status` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则定义表' ROW_FORMAT = Dynamic;
CREATE TABLE `glms_risk_rule_application` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`RuleIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '关联规则Id列表(逗号分隔)',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`ControlStrategy` tinyint NOT NULL COMMENT '控制策略: 1=Block, 2=Approval, 3=Warning',
`TriggerPoints` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发时点(逗号分隔)',
`ScopeAssetBookIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用账户Id列表',
`ScopeClientIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用对手方Id列表',
`ScopeUnderlyingTypes` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用标的类型',
`ScopeTradeTypes` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用合约类型',
`ScopeIsGlobal` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否全局适用: 0=否, 1=是',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
`OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称',
`OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间',
`UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id',
`UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称',
`UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_status`(`Status` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则应用配置表' ROW_FORMAT = Dynamic;
CREATE TABLE `glms_risk_rule_audit_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`OperationType` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作类型(RULE_CREATE/RULE_UPDATE等)',
`TargetType` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '目标类型(RULE/APPLICATION/VARIABLE)',
`TargetId` bigint NOT NULL COMMENT '目标记录Id',
`TargetName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '目标名称',
`OperationDetail` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '操作详情',
`Result` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作结果',
`SnapshotData` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '快照数据(JSON)',
`OptId` bigint NOT NULL COMMENT '操作人Id',
`OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人名称',
`OptDate` datetime NOT NULL COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_opt_date`(`OptDate` ASC) USING BTREE,
INDEX `idx_operation_type`(`OperationType` ASC) USING BTREE,
INDEX `idx_target_type`(`TargetType` ASC) USING BTREE,
INDEX `idx_target_id`(`TargetId` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则操作审计日志表' ROW_FORMAT = Dynamic;
CREATE TABLE `glms_risk_variable` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`VariableName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '变量名称',
`Category` tinyint NOT NULL COMMENT '分类: 1=BookingElement, 2=MarketData, 3=SystemCalc, 4=BooleanCheck',
`DataType` tinyint NOT NULL COMMENT '数据类型: 1=Numeric, 2=Date, 3=Boolean',
`Unit` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
`ValueDomain` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '值域描述',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`VariableExpr` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '变量表达式(C#表达式)',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`SortOrder` int NOT NULL DEFAULT 0 COMMENT '排序',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
`OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称',
`OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间',
`UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id',
`UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称',
`UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控变量池定义表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS=1;
@@ -0,0 +1,135 @@
-- ============================================================
-- 规则应用初始数据(对应设计文档 §4.9.1 通用规则预置参考)
-- 依赖:seed_rules.sql(规则数据需先插入)
--
-- 每条规则对应一条应用配置(1:1),定义"何时、对谁、怎么处理"
-- ControlStrategy: 1=Block(禁止), 2=Approval(审批), 3=Warning(提示)
-- TriggerPoints: BOOK_CONFIRM=簿记交易确认
-- ScopeIsGlobal: 1=全局适用, 0=按维度配置(Scope字段为NULL表示"全部"
-- ============================================================
-- 规则1:挂钩标的集中度超阈值(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '挂钩标的集中度超阈值';
-- 规则2:挂钩标的到期日小于合约到期日(禁止,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 1, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '挂钩标的到期日小于合约到期日';
-- 规则3:名义本金超阈值(审批,账户/合约类型)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '名义本金超阈值';
-- 规则4:保证金支付比例超阈值(审批,账户/标的类型)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '保证金支付比例超阈值';
-- 规则5:保证金利率偏离(审批,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '保证金利率偏离';
-- 规则6:保证金收取比例低于最低标准(审批,账户/标的类型)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '保证金收取比例低于最低标准';
-- 规则7:起息日早于当前日期(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '起息日早于当前日期';
-- 规则8:支付日为银行间交易日(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '支付日为银行间交易日';
-- 规则9:到期日为银行间交易日(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '到期日为银行间交易日';
-- 规则10:平仓日为银行间交易日(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '平仓日为银行间交易日';
-- 规则11:合约期限超阈值(审批,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '合约期限超阈值';
-- 规则12:债券类净价偏离(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '债券类净价偏离';
-- 规则13:债券类收益率偏离(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '债券类收益率偏离';
-- 规则14:非债券类价格偏离(审批,全局)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '非债券类价格偏离';
-- 规则15:单一交易对手累计标的数量超阈值(审批,对手方)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '单一交易对手累计标的数量超阈值';
-- 规则16:多头支付固定端利率偏离(审批,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '多头支付固定端利率偏离';
-- 规则17:空头利率减点借贷加权偏离(审批,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '空头利率减点借贷加权偏离';
-- 规则18:账户授权收支方向不匹配(禁止,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 1, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '账户授权收支方向不匹配';
-- 规则19:执行价偏离超阈值(审批,账户)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 2, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '执行价偏离超阈值';
-- 规则20:希腊字母限额超阈值(提示,账户/标的,预留接口)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 3, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 0, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '希腊字母限额超阈值';
-- 规则21:接近/触发敲入敲出价(提示,全局,预留接口)
INSERT INTO `glms_risk_rule_application` (`RuleIds`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`)
SELECT
CAST(r.id AS CHAR), 1, 3, 'BOOK_CONFIRM', NULL, NULL, NULL, NULL, 1, 1, 0, 'SYSTEM', NOW()
FROM `glms_risk_rule` r WHERE r.RuleName = '接近/触发敲入敲出价';
@@ -0,0 +1,50 @@
-- ============================================================
-- 异常交易监控页面 - 操作权限(按钮)数据
-- 依赖:异常交易监控菜单 id=1077 (type=1) 需先插入
-- 对应 FunctionRight.xml 风险控制下风控相关操作权限(共16个按钮)
-- type: 0=目录 1=菜单 2=按钮
-- ============================================================
-- ----------------------------
-- 1. sys_menu 页面+按钮(type=12
-- ----------------------------
INSERT INTO `yltrs_admin`.`sys_menu` (`id`, `pid`, `pids`, `name`, `code`, `type`, `icon`, `router`, `component`, `permission`, `application`, `open_type`, `visible`, `link`, `redirect`, `weight`, `sort`, `remark`, `status`, `create_time`, `create_user`, `update_time`, `update_user`) VALUES
(1077, 99, NULL, '异常交易监控', NULL, 1, NULL, NULL, NULL, '异常交易监控', NULL, NULL, NULL, NULL, NULL, NULL, 0, '', 0, NULL, NULL, NULL, NULL),
(1078, 1077, NULL, '风控规则查看', NULL, 2, NULL, NULL, NULL, '风控规则查看', NULL, NULL, NULL, NULL, NULL, NULL, 1, '', 0, NULL, NULL, NULL, NULL),
(1079, 1077, NULL, '风控规则新增', NULL, 2, NULL, NULL, NULL, '风控规则新增', NULL, NULL, NULL, NULL, NULL, NULL, 2, '', 0, NULL, NULL, NULL, NULL),
(1080, 1077, NULL, '风控规则编辑', NULL, 2, NULL, NULL, NULL, '风控规则编辑', NULL, NULL, NULL, NULL, NULL, NULL, 3, '', 0, NULL, NULL, NULL, NULL),
(1081, 1077, NULL, '风控规则删除', NULL, 2, NULL, NULL, NULL, '风控规则删除', NULL, NULL, NULL, NULL, NULL, NULL, 4, '', 0, NULL, NULL, NULL, NULL),
(1082, 1077, NULL, '风控规则启停', NULL, 2, NULL, NULL, NULL, '风控规则启停', NULL, NULL, NULL, NULL, NULL, NULL, 5, '', 0, NULL, NULL, NULL, NULL),
(1083, 1077, NULL, '风控应用查看', NULL, 2, NULL, NULL, NULL, '风控应用查看', NULL, NULL, NULL, NULL, NULL, NULL, 6, '', 0, NULL, NULL, NULL, NULL),
(1084, 1077, NULL, '风控应用新增', NULL, 2, NULL, NULL, NULL, '风控应用新增', NULL, NULL, NULL, NULL, NULL, NULL, 7, '', 0, NULL, NULL, NULL, NULL),
(1085, 1077, NULL, '风控应用编辑', NULL, 2, NULL, NULL, NULL, '风控应用编辑', NULL, NULL, NULL, NULL, NULL, NULL, 8, '', 0, NULL, NULL, NULL, NULL),
(1086, 1077, NULL, '风控应用删除', NULL, 2, NULL, NULL, NULL, '风控应用删除', NULL, NULL, NULL, NULL, NULL, NULL, 9, '', 0, NULL, NULL, NULL, NULL),
(1087, 1077, NULL, '风控应用启停', NULL, 2, NULL, NULL, NULL, '风控应用启停', NULL, NULL, NULL, NULL, NULL, NULL, 10, '', 0, NULL, NULL, NULL, NULL),
(1088, 1077, NULL, '风控变量查看', NULL, 2, NULL, NULL, NULL, '风控变量查看', NULL, NULL, NULL, NULL, NULL, NULL, 11, '', 0, NULL, NULL, NULL, NULL),
(1089, 1077, NULL, '风控变量新增', NULL, 2, NULL, NULL, NULL, '风控变量新增', NULL, NULL, NULL, NULL, NULL, NULL, 12, '', 0, NULL, NULL, NULL, NULL),
(1090, 1077, NULL, '风控变量编辑', NULL, 2, NULL, NULL, NULL, '风控变量编辑', NULL, NULL, NULL, NULL, NULL, NULL, 13, '', 0, NULL, NULL, NULL, NULL),
(1091, 1077, NULL, '风控变量删除', NULL, 2, NULL, NULL, NULL, '风控变量删除', NULL, NULL, NULL, NULL, NULL, NULL, 14, '', 0, NULL, NULL, NULL, NULL),
(1092, 1077, NULL, '风控日志查看', NULL, 2, NULL, NULL, NULL, '风控日志查看', NULL, NULL, NULL, NULL, NULL, NULL, 15, '', 0, NULL, NULL, NULL, NULL),
(1093, 1077, NULL, '风控日志导出', NULL, 2, NULL, NULL, NULL, '风控日志导出', NULL, NULL, NULL, NULL, NULL, NULL, 16, '', 0, NULL, NULL, NULL, NULL);
-- ----------------------------
-- 2. sys_role_menu 角色1的按钮权限
-- ----------------------------
INSERT INTO `yltrs_admin`.`sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES
(1097, 1, 1077),
(1098, 1, 1078),
(1099, 1, 1079),
(1100, 1, 1080),
(1101, 1, 1081),
(1102, 1, 1082),
(1103, 1, 1083),
(1104, 1, 1084),
(1105, 1, 1085),
(1106, 1, 1086),
(1107, 1, 1087),
(1108, 1, 1088),
(1109, 1, 1089),
(1110, 1, 1090),
(1111, 1, 1091),
(1112, 1, 1092),
(1113, 1, 1093);
@@ -0,0 +1,292 @@
-- ============================================================
-- 规则初始数据(对应设计文档 §4.9.1 通用规则预置参考)
-- 依赖:seed_variables.sql(变量池数据需先插入,ID 1~44)
--
-- 变量 ID 映射(seed_variables.sql 自增):
-- 1=合约名义本金 2=合约起息日 3=合约到期日 4=合约平仓日
-- 5=合约支付日 6=期初净价 7=期初全价 8=期初收益率
-- 9=期初价格 10=期末全价 11=期末价格 12=保证金利率
-- 13=保证金比例 14=客户授信额度
-- 15=上一收盘日中债估值净价 16=上一收盘日中债估值全价 17=上一收盘日中债估值收益率
-- 18=上一日收盘价 19=借贷加权费率 20=FR007 21=当前日期
-- 22=挂钩标的到期日 23=标的发行余额
-- 24=挂钩标的集中度 25=授信占用率 26=合约期限 27=Delta
-- 28=Gamma 29=Vega 30=Theta 31=利息端利率
-- 32=对手方累计标的数量 33=同一标的累计名义本金 34=同一客户累计名义本金
-- 35=总持仓名义本金
-- 36=到期日是否银行间交易日 37=平仓日是否银行间交易日 38=支付日是否银行间交易日
-- 39=利息端/浮动端方向是否同向 40=关键业务要素是否一致
-- 41=多空方向为多头 42=多空方向为空头
-- 43=保证金收支方向为支付 44=保证金收支方向为收取
-- 45=期初净价偏离度 46=期初收益率偏离度 47=期初价格偏离度
-- 48=利息端利率与FR007偏离度 49=利息端利率与借贷加权费率偏离度
-- 50=执行价偏离度 51=执行价 52=客户品种最低保证金率 53=参考价格
--
-- ConditionJson 精简设计:只存 VariableId/Operator/ThresholdType/Value/ThresholdVariableId
-- 展示字段(VariableName/VariableType/Unit/ThresholdVariableName)从变量接口关联获取
-- ============================================================
-- ============================================================
-- 插入 21 条通用规则
-- ============================================================
-- 规则1:挂钩标的集中度超阈值(审批,全局)
-- 条件:挂钩标的集中度(ID=24) > 30%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('挂钩标的集中度超阈值',
'挂钩标的集中度超过阈值(默认30%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 24,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 30
)),
'calc.UnderlyingConcentration > 30',
1, 1, 0, 'SYSTEM', NOW());
-- 规则2:挂钩标的到期日小于合约到期日(禁止,全局)
-- 条件:挂钩标的到期日(ID=22) < 合约到期日(ID=3)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('挂钩标的到期日小于合约到期日',
'挂钩标的到期日早于合约到期日时禁止交易',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 22,
'Operator', '早于', 'ThresholdType', 'Variable',
'ThresholdVariableId', 3
)),
'market.UnderlyingMaturityDate < trade.ExerciseDate',
1, 1, 0, 'SYSTEM', NOW());
-- 规则3:名义本金超阈值(审批,账户/合约类型)
-- 条件:合约名义本金(ID=1) > 1000000001亿元)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('名义本金超阈值',
'合约名义本金超过阈值(默认1亿元)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 1,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 100000000
)),
'trade.StockEqvNotional > 100000000',
1, 1, 0, 'SYSTEM', NOW());
-- 规则4:保证金支付比例超阈值(审批,账户/标的类型)
-- 条件:保证金比例(ID=13) > 50%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金支付比例超阈值',
'保证金比例超过阈值(默认50%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 13,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 50
)),
'trade.MarginRate > 50',
1, 1, 0, 'SYSTEM', NOW());
-- 规则5:保证金利率偏离(审批,账户)
-- 条件:保证金利率(ID=12) 不介于 [2%, 5%]
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金利率偏离',
'保证金利率不在配置区间内(默认2%~5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 12,
'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());
-- 规则6:保证金收取比例低于最低标准(审批,账户/标的类型)
-- 条件:保证金比例(ID=13) < 客户品种最低保证金率(ID=52)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金收取比例低于最低标准',
'保证金比例低于客户品种最低保证金率时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 13,
'Operator', '<', 'ThresholdType', 'Variable',
'ThresholdVariableId', 52
)),
'trade.MarginRate < config.MinMarginRate',
1, 1, 0, 'SYSTEM', NOW());
-- 规则7:起息日早于当前日期(审批,全局)
-- 条件:合约起息日(ID=2) < 当前日期(ID=21)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('起息日早于当前日期',
'合约起息日早于当前日期时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 2,
'Operator', '早于', 'ThresholdType', 'Variable',
'ThresholdVariableId', 21
)),
'trade.StartDate < sys.CurrentDate',
1, 1, 0, 'SYSTEM', NOW());
-- 规则8:支付日为银行间交易日(审批,全局)
-- 条件:支付日是否银行间交易日(ID=38) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('支付日为银行间交易日',
'支付日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 38,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsSettlementDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则9:到期日为银行间交易日(审批,全局)
-- 条件:到期日是否银行间交易日(ID=36) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('到期日为银行间交易日',
'到期日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 36,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsExerciseDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则10:平仓日为银行间交易日(审批,全局)
-- 条件:平仓日是否银行间交易日(ID=37) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('平仓日为银行间交易日',
'平仓日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 37,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsUnwindDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则11:合约期限超阈值(审批,账户)
-- 条件:合约期限(ID=26) > 365天
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('合约期限超阈值',
'合约期限超过阈值(默认365天)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 26,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 365
)),
'calc.MaturityDays > 365',
1, 1, 0, 'SYSTEM', NOW());
-- 规则12:债券类净价偏离(审批,全局)
-- 条件:期初净价偏离度(ID=45) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('债券类净价偏离',
'期初净价与上一收盘日中债估值净价偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 45,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(swap_position.PosiNetNoFeePrice - market.CBValuationNetPrice) / market.CBValuationNetPrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则13:债券类收益率偏离(审批,全局)
-- 条件:期初收益率偏离度(ID=46) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('债券类收益率偏离',
'期初收益率与上一收盘日中债估值收益率偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 46,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.InitYtm - market.CBValuationYtm) / market.CBValuationYtm * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则14:非债券类价格偏离(审批,全局)
-- 条件:期初价格偏离度(ID=47) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('非债券类价格偏离',
'期初价格与上一日收盘价偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 47,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.SpotPrice - market.LastClosePrice) / market.LastClosePrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则15:单一交易对手累计标的数量超阈值(审批,对手方)
-- 条件:对手方累计标的数量(ID=32) > 10个
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('单一交易对手累计标的数量超阈值',
'对手方累计标的数量超过阈值(默认10个)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 32,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 10
)),
'calc.CounterpartyUnderlyingCount > 10',
1, 1, 0, 'SYSTEM', NOW());
-- 规则16:多头支付固定端利率偏离(审批,账户)
-- 条件:多空方向为多头(ID=41) AND 利息端利率与FR007偏离度(ID=48) > 3%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('多头支付固定端利率偏离',
'多头方向支付固定端场景下,利息端利率与FR007偏离度超阈值(默认3%)时触发审批',
JSON_ARRAY(
JSON_OBJECT(
'VariableId', 41,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
),
JSON_OBJECT(
'VariableId', 48,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 3
)
),
'calc.IsLongDirection == true && Math.Abs(calc.InterestRate - market.FR007) / market.FR007 * 100 > 3',
1, 1, 0, 'SYSTEM', NOW());
-- 规则17:空头利率减点借贷加权偏离(审批,账户)
-- 条件:多空方向为空头(ID=42) AND 利息端利率与借贷加权费率偏离度(ID=49) > 2%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('空头利率减点借贷加权偏离',
'空头方向场景下,利息端利率与借贷加权费率偏离度超阈值(默认2%)时触发审批',
JSON_ARRAY(
JSON_OBJECT(
'VariableId', 42,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
),
JSON_OBJECT(
'VariableId', 49,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 2
)
),
'calc.IsShortDirection == true && Math.Abs(calc.InterestRate - market.BondLendingRate) / market.BondLendingRate * 100 > 2',
1, 1, 0, 'SYSTEM', NOW());
-- 规则18:账户授权收支方向不匹配(禁止,账户)
-- 条件:保证金收支方向为支付(ID=43)(实际需结合账户授权方向判断,此处简化)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('账户授权收支方向不匹配',
'保证金收支方向与账户授权方向不匹配时禁止交易',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 43,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsMarginPay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则19:执行价偏离超阈值(审批,账户)
-- 条件:执行价偏离度(ID=50) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('执行价偏离超阈值',
'执行价与参考价格偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 50,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.StrikePrice - market.ReferencePrice) / market.ReferencePrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则20:希腊字母限额超阈值(提示,预留接口,一期不纳入)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('希腊字母限额超阈值',
'Delta/Gamma/Vega/Theta任一超阈值时提示(预留接口,一期不纳入)',
NULL,
NULL,
1, 1, 0, 'SYSTEM', NOW());
-- 规则21:接近/触发敲入敲出价(提示,预留接口,需确认具体判断逻辑)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('接近/触发敲入敲出价',
'标的价格接近敲入/敲出价时提示(预留接口,需确认具体判断逻辑)',
NULL,
NULL,
1, 1, 0, 'SYSTEM', NOW());
@@ -0,0 +1,73 @@
-- ============================================================
-- 变量池初始数据(对应设计文档 §4.4 变量池完整清单)
-- ============================================================
-- 4.4.1 簿记要素类(Category=1
INSERT INTO `glms_risk_variable` (`VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `SortOrder`, `OptId`, `OptName`, `OptDate`) VALUES
('合约名义本金', 1, 1, '', '≥ 0', 'trade.StockEqvNotional 或 swap_position.PosiNotionalValue', 'trade.StockEqvNotional', 101, 0, 'SYSTEM', NOW()),
('合约起息日', 1, 2, NULL, NULL, 'trade.StartDate 或 swap_position.PosiStartDate', 'trade.StartDate', 102, 0, 'SYSTEM', NOW()),
('合约到期日', 1, 2, NULL, NULL, 'trade.ExerciseDate 或 swap_position.PosiMatuirityDate', 'trade.ExerciseDate', 103, 0, 'SYSTEM', NOW()),
('合约平仓日', 1, 2, NULL, NULL, 'trade.UnWindDate', 'trade.UnWindDate', 104, 0, 'SYSTEM', NOW()),
('合约支付日', 1, 2, NULL, NULL, 'trade.SettlementDate', 'trade.SettlementDate', 105, 0, 'SYSTEM', NOW()),
('期初净价', 1, 1, '', '≥ 0', 'swap_position.PosiNetNoFeePrice(债券 TRS', 'swap_position.PosiNetNoFeePrice', 106, 0, 'SYSTEM', NOW()),
('期初全价', 1, 1, '', '≥ 0', 'swap_position.PosiNetFeePrice(债券 TRS/ trade.SpotPrice(其他)', 'swap_position.PosiNetFeePrice', 107, 0, 'SYSTEM', NOW()),
('期初收益率', 1, 1, '%', NULL, 'trade.InitYtm 或 swap_position.InitYtm', 'trade.InitYtm', 108, 0, 'SYSTEM', NOW()),
('期初价格', 1, 1, '', '≥ 0', 'trade.SpotPrice(非债券类)', 'trade.SpotPrice', 109, 0, 'SYSTEM', NOW()),
('期末全价', 1, 1, '', '≥ 0', 'eod_swap_position.UnderlyingPrice(债券 TRS', 'eod_swap_position.UnderlyingPrice', 110, 0, 'SYSTEM', NOW()),
('期末价格', 1, 1, '', '≥ 0', 'trade.FinalPrice', 'trade.FinalPrice', 111, 0, 'SYSTEM', NOW()),
('保证金利率', 1, 1, '%', NULL, 'client_marginrate.InitMarginRebateRate', 'client_marginrate.InitMarginRebateRate', 112, 0, 'SYSTEM', NOW()),
('保证金比例', 1, 1, '%', '0~100', 'trade.MarginRate', 'trade.MarginRate', 113, 0, 'SYSTEM', NOW()),
('客户授信额度', 1, 1, '', '≥ 0', 'credit.Credit', 'credit.Credit', 114, 0, 'SYSTEM', NOW());
-- 4.4.2 行情类(Category=2,统一取上一交易日收盘价)
INSERT INTO `glms_risk_variable` (`VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `SortOrder`, `OptId`, `OptName`, `OptDate`) VALUES
('上一收盘日中债估值净价', 2, 1, '', '≥ 0', '资讯数据', 'market.CBValuationNetPrice', 201, 0, 'SYSTEM', NOW()),
('上一收盘日中债估值全价', 2, 1, '', '≥ 0', '资讯数据', 'market.CBValuationFullPrice', 202, 0, 'SYSTEM', NOW()),
('上一收盘日中债估值收益率', 2, 1, '%', NULL, '资讯数据', 'market.CBValuationYtm', 203, 0, 'SYSTEM', NOW()),
('上一日收盘价', 2, 1, '', '≥ 0', '行情数据,按标的区分', 'market.LastClosePrice', 204, 0, 'SYSTEM', NOW()),
('借贷加权费率', 2, 1, '%', NULL, 'CMDM 标的债券借贷费率行情表', 'market.BondLendingRate', 205, 0, 'SYSTEM', NOW()),
('FR007', 2, 1, '%', NULL, '上一交易日收盘价', 'market.FR007', 206, 0, 'SYSTEM', NOW()),
('当前日期', 2, 2, NULL, NULL, 'DateTime.Today', 'sys.CurrentDate', 207, 0, 'SYSTEM', NOW()),
('挂钩标的到期日', 2, 2, NULL, NULL, '资讯数据', 'market.UnderlyingMaturityDate', 208, 0, 'SYSTEM', NOW()),
('标的发行余额', 2, 1, '', '≥ 0', '资讯数据', 'market.UnderlyingIssueBalance', 209, 0, 'SYSTEM', NOW());
-- 4.4.3 系统计算值类(Category=3
INSERT INTO `glms_risk_variable` (`VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `SortOrder`, `OptId`, `OptName`, `OptDate`) VALUES
('挂钩标的集中度', 3, 1, '%', '0~100', '同一标的存续交易总名义本金 ÷ 标的发行余额 × 100', 'calc.UnderlyingConcentration', 301, 0, 'SYSTEM', NOW()),
('授信占用率', 3, 1, '%', '0~100', '(已占用授信 + 本笔授信占用) ÷ 授信总额 × 100', 'calc.CreditUsageRate', 302, 0, 'SYSTEM', NOW()),
('合约期限', 3, 1, '', '≥ 0', '(ExerciseDate - StartDate).Days', 'calc.MaturityDays', 303, 0, 'SYSTEM', NOW()),
('Delta', 3, 1, NULL, NULL, 'realtime_trade_risk.Delta(预留接口,一期不纳入)', 'realtime_trade_risk.Delta', 304, 0, 'SYSTEM', NOW()),
('Gamma', 3, 1, NULL, NULL, 'realtime_trade_risk.Gamma(预留接口,一期不纳入)', 'realtime_trade_risk.Gamma', 305, 0, 'SYSTEM', NOW()),
('Vega', 3, 1, NULL, NULL, 'realtime_trade_risk.Vega(预留接口,一期不纳入)', 'realtime_trade_risk.Vega', 306, 0, 'SYSTEM', NOW()),
('Theta', 3, 1, NULL, NULL, 'realtime_trade_risk.Theta(预留接口,一期不纳入)', 'realtime_trade_risk.Theta', 307, 0, 'SYSTEM', NOW()),
('利息端利率', 3, 1, '%', NULL, '固定利率 或 FR007 ± 加点', 'calc.InterestRate', 308, 0, 'SYSTEM', NOW()),
('对手方累计标的数量', 3, 1, '', '≥ 0', 'COUNT(DISTINCT UnderlyingId) 该对手方所有存续交易,含本笔', 'calc.CounterpartyUnderlyingCount', 309, 0, 'SYSTEM', NOW()),
('同一标的累计名义本金', 3, 1, '', '≥ 0', 'SUM(该标的所有存续交易的 StockEqvNotional),含本笔', 'calc.SameUnderlyingTotalNotional', 310, 0, 'SYSTEM', NOW()),
('同一客户累计名义本金', 3, 1, '', '≥ 0', 'SUM(该客户所有存续交易的 StockEqvNotional),含本笔', 'calc.SameClientTotalNotional', 311, 0, 'SYSTEM', NOW()),
('总持仓名义本金', 3, 1, '', '≥ 0', 'SUM(所有存续交易的 StockEqvNotional)', 'calc.TotalPositionNotional', 312, 0, 'SYSTEM', NOW());
-- 新增变量(ID 45~53):
-- 45=期初净价偏离度 46=期初收益率偏离度 47=期初价格偏离度
-- 48=利息端利率与FR007偏离度 49=利息端利率与借贷加权费率偏离度
-- 50=执行价偏离度 51=执行价 52=客户品种最低保证金率 53=参考价格
INSERT INTO `glms_risk_variable` (`VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `SortOrder`, `OptId`, `OptName`, `OptDate`) VALUES
('期初净价偏离度', 3, 1, '%', '≥ 0', 'ABS(期初净价-中债估值净价)/中债估值净价*100', 'Math.Abs(swap_position.PosiNetNoFeePrice - market.CBValuationNetPrice) / market.CBValuationNetPrice * 100', 313, 0, 'SYSTEM', NOW()),
('期初收益率偏离度', 3, 1, '%', '≥ 0', 'ABS(期初收益率-中债估值收益率)/中债估值收益率*100', 'Math.Abs(trade.InitYtm - market.CBValuationYtm) / market.CBValuationYtm * 100', 314, 0, 'SYSTEM', NOW()),
('期初价格偏离度', 3, 1, '%', '≥ 0', 'ABS(期初价格-上一日收盘价)/上一日收盘价*100', 'Math.Abs(trade.SpotPrice - market.LastClosePrice) / market.LastClosePrice * 100', 315, 0, 'SYSTEM', NOW()),
('利息端利率与FR007偏离度', 3, 1, '%', '≥ 0', 'ABS(利息端利率-FR007)/FR007*100', 'Math.Abs(calc.InterestRate - market.FR007) / market.FR007 * 100', 316, 0, 'SYSTEM', NOW()),
('利息端利率与借贷加权费率偏离度', 3, 1, '%', '≥ 0', 'ABS(利息端利率-借贷加权费率)/借贷加权费率*100', 'Math.Abs(calc.InterestRate - market.BondLendingRate) / market.BondLendingRate * 100', 317, 0, 'SYSTEM', NOW()),
('执行价偏离度', 3, 1, '%', '≥ 0', 'ABS(执行价-参考价格)/参考价格*100', 'Math.Abs(trade.StrikePrice - market.ReferencePrice) / market.ReferencePrice * 100', 318, 0, 'SYSTEM', NOW()),
('执行价', 1, 1, '', '≥ 0', 'trade.StrikePrice', 'trade.StrikePrice', 115, 0, 'SYSTEM', NOW()),
('客户品种最低保证金率', 1, 1, '%', '0~100', '客户品种最低保证金率(由账户/标的配置决定)', 'config.MinMarginRate', 116, 0, 'SYSTEM', NOW()),
('参考价格', 2, 1, '', '≥ 0', '执行价参考价格(行情数据)', 'market.ReferencePrice', 210, 0, 'SYSTEM', NOW());
-- 4.4.4 布尔判断类(Category=4
INSERT INTO `glms_risk_variable` (`VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `SortOrder`, `OptId`, `OptName`, `OptDate`) VALUES
('到期日是否银行间交易日', 4, 3, NULL, NULL, '查询银行间交易日历', 'calc.IsExerciseDateTradingDay', 401, 0, 'SYSTEM', NOW()),
('平仓日是否银行间交易日', 4, 3, NULL, NULL, '查询银行间交易日历', 'calc.IsUnwindDateTradingDay', 402, 0, 'SYSTEM', NOW()),
('支付日是否银行间交易日', 4, 3, NULL, NULL, '查询银行间交易日历', 'calc.IsSettlementDateTradingDay', 403, 0, 'SYSTEM', NOW()),
('利息端/浮动端方向是否同向', 4, 3, NULL, NULL, '利息端"收取"↔浮动端"多头",利息端"支付"↔浮动端"空头"', 'calc.IsInterestFloatSameDirection', 404, 0, 'SYSTEM', NOW()),
('关键业务要素是否一致', 4, 3, NULL, NULL, '交易确认书 vs 簿记要素(大模型方案)', 'calc.IsKeyElementsConsistent', 405, 0, 'SYSTEM', NOW()),
('多空方向为多头', 4, 3, NULL, NULL, 'trade.BuySell == "买入" 或浮动端为多头', 'calc.IsLongDirection', 406, 0, 'SYSTEM', NOW()),
('多空方向为空头', 4, 3, NULL, NULL, '与多头互斥', 'calc.IsShortDirection', 407, 0, 'SYSTEM', NOW()),
('保证金收支方向为支付', 4, 3, NULL, NULL, '保证金方向为支付', 'calc.IsMarginPay', 408, 0, 'SYSTEM', NOW()),
('保证金收支方向为收取', 4, 3, NULL, NULL, '与支付互斥', 'calc.IsMarginReceive', 409, 0, 'SYSTEM', NOW());
+5
View File
@@ -408,5 +408,10 @@ namespace YLErp.BLL
public DbSet<trade_contract_oa_result> tradeContractOaResult { get; set; }
public DbSet<BondPayment> bondPayment { get; set; }
public DbSet<glms_risk_rule> glms_risk_rule { get; set; }
public DbSet<glms_risk_rule_application> glms_risk_rule_application { get; set; }
public DbSet<glms_risk_rule_audit_log> glms_risk_rule_audit_log { get; set; }
public DbSet<glms_risk_variable> glms_risk_variable { get; set; }
}
}
+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();
}
}
}
@@ -0,0 +1,50 @@
using System;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 规则编译结果。
/// 用于承载公式校验、Roslyn 编译结果和错误信息。
/// </summary>
public class RuleCompileResult
{
/// <summary>
/// 是否编译成功。
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 编译失败时的错误信息。
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 编译成功后的可执行委托。
/// </summary>
public Func<RiskContext, bool> CompiledScript { get; set; }
/// <summary>
/// 创建成功结果。
/// </summary>
public static RuleCompileResult Ok(Func<RiskContext, bool> compiledScript)
{
return new RuleCompileResult
{
Success = true,
CompiledScript = compiledScript
};
}
/// <summary>
/// 创建失败结果。
/// </summary>
public static RuleCompileResult Fail(string errorMessage)
{
return new RuleCompileResult
{
Success = false,
ErrorMessage = errorMessage
};
}
}
}
@@ -0,0 +1,67 @@
using System;
using System.Collections.Concurrent;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 规则编译结果的进程内缓存。
/// 当前第一版按规则 Id 缓存 Roslyn 编译后的可执行委托。
/// </summary>
public static class RuleCompiledCache
{
/// <summary>
/// 规则编译结果的进程内缓存。ConcurrentDictionary为线程安全的字典,用于缓存编译后的规则委托,性能略低。
/// </summary>
private static readonly ConcurrentDictionary<string, Func<RiskContext, bool>> Cache
= new ConcurrentDictionary<string, Func<RiskContext, bool>>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 写入或更新规则编译结果。
/// </summary>
public static void Set(string ruleCode, Func<RiskContext, bool> compiledScript)
{
if (string.IsNullOrWhiteSpace(ruleCode) || compiledScript == null)
{
return;
}
Cache[ruleCode] = compiledScript;
}
/// <summary>
/// 尝试获取规则编译结果。
/// </summary>
public static bool TryGet(string ruleCode, out Func<RiskContext, bool> compiledScript)
{
compiledScript = null;
if (string.IsNullOrWhiteSpace(ruleCode))
{
return false;
}
return Cache.TryGetValue(ruleCode, out compiledScript);
}
/// <summary>
/// 删除指定规则的编译结果。
/// </summary>
public static void Remove(string ruleCode)
{
if (string.IsNullOrWhiteSpace(ruleCode))
{
return;
}
Cache.TryRemove(ruleCode, out _);
}
/// <summary>
/// 清空全部规则编译结果。
/// </summary>
public static void Clear()
{
Cache.Clear();
}
}
}
@@ -0,0 +1,228 @@
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Newtonsoft.Json;
using System;
using System.Linq;
using YLErp.BLL;
using YLErp.QdpModule;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// Roslyn 脚本全局变量容器
/// </summary>
public class ScriptGlobals
{
/// <summary>
/// 当前交易ID
/// </summary>
public int TradeId { get; set; }
/// <summary>
/// 当前触发时点
/// </summary>
public string TriggerPoint { get; set; }
/// <summary>
/// 数据库上下文,供规则公式直接查询数据库
/// </summary>
public YLContext DbContext { get; set; }
}
/// <summary>
/// 规则编译器(Roslyn 版)
///
/// 编译时机:规则定义时(零运行时解析开销)
/// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool
///
/// 相比表达式树方案的优势:
/// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点
/// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法
/// - 可调试:生成的脚本代码可直接阅读和理解
///
/// 示例:
/// RuleExpr: Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).StockEqvNotional) > 100000000m
/// 编译结果:Func&lt;RiskContext, bool&gt;(内部通过 Roslyn 编译缓存)
/// </summary>
public static class RuleCompiler
{
static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
/// <summary>
/// 校验并编译规则表达式。
/// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
/// ruleId 可为空,用于规则未落库时的公式校验。
/// </summary>
public static RuleCompileResult ValidateAndCompileFormula(long? ruleId, string formulaExp)
{
var ruleIdText = ruleId.HasValue ? ruleId.Value.ToString() : "未落库";
if (string.IsNullOrWhiteSpace(formulaExp))
{
_logger.Error($"规则表达式为空,无法编译 - RuleId: {ruleIdText}");
return RuleCompileResult.Fail("规则 RuleExpr 不能为空");
}
try
{
var compiled = CompileScript(ruleId, formulaExp, out string compileErrorMessage);
if (compiled == null)
{
return RuleCompileResult.Fail(compileErrorMessage ?? "脚本编译失败");
}
return RuleCompileResult.Ok(compiled);
}
catch (Exception ex)
{
_logger.Error($"规则表达式校验异常 - RuleId: {ruleIdText}, Error: {ex.Message}\n脚本代码:{formulaExp}");
return RuleCompileResult.Fail(ex.Message);
}
}
/// <summary>
/// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
/// </summary>
public static RuleCompileResult ValidateAndCompileRule(RiskRule rule)
{
if (rule == null)
{
_logger.Error("规则对象为空,无法编译 - RuleId: 0");
return RuleCompileResult.Fail("规则不能为空");
}
if (rule.Id <= 0)
{
_logger.Error($"规则 Id 非法,无法编译 - RuleId: {rule.Id}");
return RuleCompileResult.Fail("规则 Id 不能为空");
}
if (rule.Status != RiskRuleStatus.Active)
{
_logger.Error($"规则状态非启用,跳过编译 - RuleId: {rule.Id}, Status: {rule.Status}");
return RuleCompileResult.Fail("规则 状态为未启动");
}
var result = ValidateAndCompileFormula(rule.Id, rule.RuleExpr);
if (!result.Success)
{
_logger.Error($"规则校验失败 - RuleId: {rule.Id}, Error: {result.ErrorMessage}");
rule.CompiledScript = null;
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
}
rule.CompiledScript = result.CompiledScript;
RuleCompiledCache.Set(rule.Id.ToString(), result.CompiledScript);
return result;
}
/// <summary>
/// 用 Roslyn 编译 C# 脚本代码为可执行委托
/// </summary>
private static Func<RiskContext, bool> CompileScript(long? ruleId, string scriptCode, out string errorMessage)
{
errorMessage = null;
var ruleIdText = ruleId.HasValue ? ruleId.Value.ToString() : "未落库";
// 配置编译选项:引用必要的程序集
var options = ScriptOptions.Default
.WithReferences(
typeof(RiskContext).Assembly,
typeof(YLContext).Assembly,
typeof(YLErp.DBModels.trade).Assembly,
typeof(QdpCalendarHelper).Assembly,
typeof(JsonConvert).Assembly,
typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly,
typeof(Queryable).Assembly
)
.WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule");
// 创建脚本(尚未执行,仅编译)
var script = CSharpScript.Create<bool>(scriptCode, options, globalsType: typeof(ScriptGlobals));
// 编译(提前发现语法错误)
//后续编译需提供接口,返回前端编译信息,包含编译错误列表
var compilation = script.GetCompilation();
var diagnostics = compilation.GetDiagnostics();
var errors = diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).ToList();
if (errors.Any())
{
errorMessage = string.Join("; ", errors.Select(e => e.GetMessage()));
_logger.Error($"规则编译失败 - RuleId: {ruleIdText}, Error: {errorMessage}\n脚本代码:{scriptCode}");
return null;
}
// 生成可调用委托
var runner = script.CreateDelegate();
// 包装为同步的 Func<RiskContext, bool>
return ctx =>
{
try
{
var globals = new ScriptGlobals
{
TradeId = ctx.TradeId,
TriggerPoint = ctx.TriggerPoint,
DbContext = ctx.DbContext
};
return runner(globals).GetAwaiter().GetResult();
}
catch (Exception ex)
{
// 脚本执行异常(如空引用、类型转换失败)视为规则不触发
_logger.Error($"规则执行异常 - RuleId: {ruleIdText}, Error: {ex.Message}\n脚本代码:{scriptCode}");
return false;
}
};
}
}
public class BuildMemberAccessResult
{
public bool Success { get; set; }
public string Expression { get; set; }
public string ErrorMessage { get; set; }
public static BuildMemberAccessResult Ok(string expression)
{
return new BuildMemberAccessResult
{
Success = true,
Expression = expression
};
}
/// <summary>
/// 将操作符统一为 C# 操作符
/// </summary>
private static string FormatOperator(string op)
{
return op?.Trim() switch
{
// 数值型 C# 符号
"=" or "==" => "==",
"!=" or "≠" => "!=",
">" => ">",
"<" => "<",
">=" => ">=",
"<=" => "<=",
// 日期型中文语义 → C# 符号
"早于" => "<",
"晚于" => ">",
"等于" => "==",
"不早于" => ">=",
"不晚于" => "<=",
_ => throw new ArgumentException($"不支持的操作符:{op}")
};
}
public static BuildMemberAccessResult Fail(string errorMessage)
{
return new BuildMemberAccessResult
{
Success = false,
ErrorMessage = errorMessage
};
}
}
}
@@ -0,0 +1,17 @@
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 批量操作结果
/// </summary>
public class BatchOperationResult
{
/// <summary>是否全部成功</summary>
public bool Success { get; set; }
/// <summary>总数量</summary>
public int TotalCount { get; set; }
/// <summary>成功数量</summary>
public int SuccessCount { get; set; }
/// <summary>错误信息</summary>
public string ErrorMessage { get; set; }
}
}
@@ -0,0 +1,29 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 创建应用配置请求
/// </summary>
public class CreateRiskApplicationReq
{
/// <summary>关联规则 ID 列表(JSON 数组)</summary>
public string RuleIds { get; set; }
/// <summary>应用说明</summary>
public string Description { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表(JSON 数组)</summary>
public string TriggerPoints { get; set; }
/// <summary>适用范围-资产账簿 ID 列表</summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>适用范围-客户 ID 列表</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>是否全局生效</summary>
public int ScopeIsGlobal { get; set; }
}
}
@@ -0,0 +1,19 @@
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 创建风控规则请求
/// </summary>
public class CreateRiskRuleReq
{
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
/// <summary>结构化条件 JSON</summary>
public string ConditionJson { get; set; }
/// <summary>规则执行表达式</summary>
public string RuleExpr { get; set; }
/// <summary>规则说明</summary>
public string Description { get; set; }
}
}
@@ -0,0 +1,27 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 创建变量请求
/// </summary>
public class CreateRiskVariableReq
{
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>值域描述</summary>
public string ValueDomain { get; set; }
/// <summary>变量说明</summary>
public string Description { get; set; }
/// <summary>取值表达式</summary>
public string VariableExpr { get; set; }
/// <summary>排序序号</summary>
public int SortOrder { get; set; }
}
}
@@ -0,0 +1,28 @@
using BaseOUDAL;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 查询应用配置请求
/// </summary>
public class QueryRiskApplicationReq : BaseSearchReq
{
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>应用配置状态</summary>
public RiskRuleStatus? Status { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy? Strategy { get; set; }
/// <summary>触发点</summary>
public string TriggerPoint { get; set; }
/// <summary>适用范围-客户 ID 列表</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>适用范围-资产账簿 ID 列表</summary>
public string ScopeAssetBookIds { get; set; }
}
}
@@ -0,0 +1,28 @@
using System;
using BaseOUDAL;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 查询审计日志请求
/// </summary>
public class QueryRiskAuditLogReq : BaseSearchReq
{
/// <summary>操作类型</summary>
public string OperationType { get; set; }
/// <summary>操作类型列表(多选,逗号分隔)</summary>
public string OperationTypes { get; set; }
/// <summary>操作目标类型</summary>
public string TargetType { get; set; }
/// <summary>操作目标名称</summary>
public string TargetName { get; set; }
/// <summary>操作人姓名</summary>
public string OptName { get; set; }
/// <summary>开始日期</summary>
public DateTime? StartDate { get; set; }
/// <summary>结束日期</summary>
public DateTime? EndDate { get; set; }
/// <summary>搜索关键词</summary>
public string Keyword { get; set; }
}
}
@@ -0,0 +1,18 @@
using BaseOUDAL;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 查询风控规则请求
/// </summary>
public class QueryRiskRuleReq : BaseSearchReq
{
/// <summary>搜索关键词</summary>
public string Keyword { get; set; }
/// <summary>规则状态</summary>
public RiskRuleStatus? Status { get; set; }
/// <summary>关联变量 ID</summary>
public long? VariableId { get; set; }
}
}
@@ -0,0 +1,18 @@
using BaseOUDAL;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 查询变量请求
/// </summary>
public class QueryRiskVariableReq : BaseSearchReq
{
/// <summary>变量分类</summary>
public RiskVariableCategory? Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType? DataType { get; set; }
/// <summary>变量名称</summary>
public string VariableName { get; set; }
}
}
@@ -0,0 +1,44 @@
using System;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 应用配置详情响应
/// </summary>
public class RiskApplicationDetail
{
/// <summary>应用配置 ID</summary>
public long Id { get; set; }
/// <summary>关联规则 ID 列表(JSON 数组)</summary>
public string RuleIds { get; set; }
/// <summary>应用说明</summary>
public string Description { get; set; }
/// <summary>应用配置状态</summary>
public RiskRuleStatus Status { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表(JSON 数组)</summary>
public string TriggerPoints { get; set; }
/// <summary>适用范围-资产账簿 ID 列表</summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>适用范围-客户 ID 列表</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>是否全局生效</summary>
public int ScopeIsGlobal { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>创建人姓名</summary>
public string OptName { get; set; }
/// <summary>创建时间</summary>
public DateTime OptDate { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime UpdateDate { get; set; }
}
}
@@ -0,0 +1,44 @@
using System;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 应用配置列表项
/// </summary>
public class RiskApplicationListItem
{
/// <summary>应用配置 ID</summary>
public long Id { get; set; }
/// <summary>关联规则 ID 列表(JSON 数组)</summary>
public string RuleIds { get; set; }
/// <summary>应用说明</summary>
public string Description { get; set; }
/// <summary>应用配置状态</summary>
public int Status { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表(JSON 数组)</summary>
public string TriggerPoints { get; set; }
/// <summary>适用范围-资产账簿 ID 列表</summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>适用范围-客户 ID 列表</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>是否全局生效</summary>
public int ScopeIsGlobal { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>创建人姓名</summary>
public string OptName { get; set; }
/// <summary>创建时间</summary>
public DateTime OptDate { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime UpdateDate { get; set; }
}
}
@@ -0,0 +1,33 @@
using System;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 审计日志详情响应
/// </summary>
public class RiskAuditLogDetail
{
/// <summary>日志 ID</summary>
public long Id { get; set; }
/// <summary>操作类型</summary>
public string OperationType { get; set; }
/// <summary>操作目标类型</summary>
public string TargetType { get; set; }
/// <summary>操作目标 ID</summary>
public long TargetId { get; set; }
/// <summary>操作目标名称</summary>
public string TargetName { get; set; }
/// <summary>操作详情描述</summary>
public string OperationDetail { get; set; }
/// <summary>操作结果</summary>
public string Result { get; set; }
/// <summary>操作前数据快照(JSON</summary>
public string SnapshotData { get; set; }
/// <summary>操作人工号</summary>
public long OptId { get; set; }
/// <summary>操作人姓名</summary>
public string OptName { get; set; }
/// <summary>操作时间</summary>
public DateTime OptDate { get; set; }
}
}
@@ -0,0 +1,29 @@
using System;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 审计日志列表项
/// </summary>
public class RiskAuditLogListItem
{
/// <summary>日志 ID</summary>
public long Id { get; set; }
/// <summary>操作类型</summary>
public string OperationType { get; set; }
/// <summary>操作目标类型</summary>
public string TargetType { get; set; }
/// <summary>操作目标 ID</summary>
public long TargetId { get; set; }
/// <summary>操作目标名称</summary>
public string TargetName { get; set; }
/// <summary>操作详情描述</summary>
public string OperationDetail { get; set; }
/// <summary>操作结果</summary>
public string Result { get; set; }
/// <summary>操作人姓名</summary>
public string OptName { get; set; }
/// <summary>操作时间</summary>
public DateTime OptDate { get; set; }
}
}
@@ -0,0 +1,19 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 规则关联应用摘要
/// </summary>
public class RiskRuleApplicationSummary
{
/// <summary>应用配置 ID</summary>
public long Id { get; set; }
/// <summary>应用配置状态</summary>
public RiskRuleStatus Status { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表</summary>
public string TriggerPoints { get; set; }
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 风控规则详情响应
/// </summary>
public class RiskRuleDetail
{
/// <summary>规则 ID</summary>
public long Id { get; set; }
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
/// <summary>结构化条件 JSON</summary>
public string ConditionJson { get; set; }
/// <summary>规则执行表达式</summary>
public string RuleExpr { get; set; }
/// <summary>规则说明</summary>
public string Description { get; set; }
/// <summary>规则状态</summary>
public RiskRuleStatus Status { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>创建人姓名</summary>
public string OptName { get; set; }
/// <summary>创建时间</summary>
public DateTime OptDate { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime UpdateDate { get; set; }
/// <summary>关联应用列表</summary>
public List<RiskRuleApplicationSummary> Applications { get; set; }
}
}
@@ -0,0 +1,38 @@
using System;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 风控规则列表项
/// </summary>
public class RiskRuleListItem
{
/// <summary>规则 ID</summary>
public long Id { get; set; }
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
/// <summary>结构化条件 JSON</summary>
public string ConditionJson { get; set; }
/// <summary>规则执行表达式</summary>
public string RuleExpr { get; set; }
/// <summary>规则说明</summary>
public string Description { get; set; }
/// <summary>规则状态</summary>
public RiskRuleStatus Status { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>创建人姓名</summary>
public string OptName { get; set; }
/// <summary>创建时间</summary>
public DateTime OptDate { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime UpdateDate { get; set; }
/// <summary>关联应用数量</summary>
public int ApplicationCount { get; set; }
}
}
@@ -0,0 +1,15 @@
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 风控规则简要信息
/// </summary>
public class RiskRuleSimpleItem
{
/// <summary>规则 ID</summary>
public long Id { get; set; }
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
}
}
@@ -0,0 +1,21 @@
using System;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 风控规则版本历史项
/// </summary>
public class RiskRuleVersionItem
{
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>操作类型</summary>
public string OperationType { get; set; }
/// <summary>操作详情</summary>
public string OperationDetail { get; set; }
/// <summary>操作人姓名</summary>
public string OptName { get; set; }
/// <summary>操作时间</summary>
public DateTime OptDate { get; set; }
}
}
@@ -0,0 +1,42 @@
using System;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 变量详情响应
/// </summary>
public class RiskVariableDetail
{
/// <summary>变量 ID</summary>
public long Id { get; set; }
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>值域描述</summary>
public string ValueDomain { get; set; }
/// <summary>变量说明</summary>
public string Description { get; set; }
/// <summary>取值表达式</summary>
public string VariableExpr { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>排序序号</summary>
public int SortOrder { get; set; }
/// <summary>创建人姓名</summary>
public string OptName { get; set; }
/// <summary>创建时间</summary>
public DateTime OptDate { get; set; }
/// <summary>最后更新人姓名</summary>
public string UpdateOptName { get; set; }
/// <summary>最后更新时间</summary>
public DateTime UpdateDate { get; set; }
/// <summary>被引用次数</summary>
public int ReferenceCount { get; set; }
}
}
@@ -0,0 +1,31 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 变量列表项
/// </summary>
public class RiskVariableListItem
{
/// <summary>变量 ID</summary>
public long Id { get; set; }
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>值域描述</summary>
public string ValueDomain { get; set; }
/// <summary>变量说明</summary>
public string Description { get; set; }
/// <summary>取值表达式</summary>
public string VariableExpr { get; set; }
/// <summary>版本号</summary>
public int Version { get; set; }
/// <summary>排序序号</summary>
public int SortOrder { get; set; }
}
}
@@ -0,0 +1,23 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 变量简要信息
/// </summary>
public class RiskVariableSimpleItem
{
/// <summary>变量 ID</summary>
public long Id { get; set; }
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>取值表达式</summary>
public string VariableExpr { get; set; }
}
}
@@ -0,0 +1,23 @@
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 规则条件项
/// </summary>
public class RuleCondition
{
/// <summary>条件变量 ID</summary>
public long VariableId { get; set; }
/// <summary>操作符(如 Equal、GreaterThan、Between 等)</summary>
public string Operator { get; set; }
/// <summary>阈值类型(FixedValue / VariableRef</summary>
public string ThresholdType { get; set; }
/// <summary>阈值(固定值或变量引用)</summary>
public object Value { get; set; }
/// <summary>阈值变量 ID(当 ThresholdType 为 VariableRef 时)</summary>
public long? ThresholdVariableId { get; set; }
/// <summary>介于/不介于的下边界是否包含,默认 true</summary>
public bool IncludeLowerBound { get; set; } = true;
/// <summary>介于/不介于的上边界是否包含,默认 true</summary>
public bool IncludeUpperBound { get; set; } = true;
}
}
@@ -0,0 +1,31 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 更新应用配置请求
/// </summary>
public class UpdateRiskApplicationReq
{
/// <summary>关联规则 ID 列表(JSON 数组)</summary>
public string RuleIds { get; set; }
/// <summary>应用说明</summary>
public string Description { get; set; }
/// <summary>风控策略</summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>触发点列表(JSON 数组)</summary>
public string TriggerPoints { get; set; }
/// <summary>适用范围-资产账簿 ID 列表</summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>适用范围-客户 ID 列表</summary>
public string ScopeClientIds { get; set; }
/// <summary>适用范围-标的类型列表</summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>适用范围-交易类型列表</summary>
public string ScopeTradeTypes { get; set; }
/// <summary>是否全局生效</summary>
public int ScopeIsGlobal { get; set; }
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; }
}
}
@@ -0,0 +1,21 @@
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 更新风控规则请求
/// </summary>
public class UpdateRiskRuleReq
{
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>规则描述文本</summary>
public string RuleText { get; set; }
/// <summary>结构化条件 JSON</summary>
public string ConditionJson { get; set; }
/// <summary>规则执行表达式</summary>
public string RuleExpr { get; set; }
/// <summary>规则说明</summary>
public string Description { get; set; }
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; }
}
}
@@ -0,0 +1,29 @@
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine.Dto
{
/// <summary>
/// 更新变量请求
/// </summary>
public class UpdateRiskVariableReq
{
/// <summary>变量名称</summary>
public string VariableName { get; set; }
/// <summary>变量分类</summary>
public RiskVariableCategory Category { get; set; }
/// <summary>数据类型</summary>
public RiskVariableDataType DataType { get; set; }
/// <summary>单位</summary>
public string Unit { get; set; }
/// <summary>值域描述</summary>
public string ValueDomain { get; set; }
/// <summary>变量说明</summary>
public string Description { get; set; }
/// <summary>取值表达式</summary>
public string VariableExpr { get; set; }
/// <summary>排序序号</summary>
public int SortOrder { get; set; }
/// <summary>版本号(乐观锁)</summary>
public int Version { get; set; }
}
}
@@ -0,0 +1,25 @@
using YLErp.BLL;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控上下文:封装一次风控检查所需的全部数据上下文
/// </summary>
public class RiskContext
{
/// <summary>
/// 当前交易ID
/// </summary>
public int TradeId { get; set; }
/// <summary>
/// 当前触发时点
/// </summary>
public string TriggerPoint { get; set; }
/// <summary>
/// 数据库上下文,供规则公式直接查询数据库
/// </summary>
public YLContext DbContext { get; set; }
}
}
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控结果
/// </summary>
public class RiskResult
{
/// <summary>
/// 是否全部通过
/// </summary>
public bool Passed { get; set; } = true;
/// <summary>
/// 是否被禁止
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// 是否需要审批
/// </summary>
public bool NeedApproval { get; set; }
/// <summary>
/// 是否存在提示
/// </summary>
public bool ShowTip { get; set; }
/// <summary>
/// 触发的规则列表
/// </summary>
public List<TriggeredRuleInfo> TriggeredRules { get; set; } = new List<TriggeredRuleInfo>();
}
/// <summary>
/// 触发规则详情
/// </summary>
public class TriggeredRuleInfo
{
public string RuleId { get; set; }
public string RuleName { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string RuleText { get; set; }
public string Message { get; set; }
}
}
+51
View File
@@ -0,0 +1,51 @@
using System;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控规则定义。
/// 只描述“规则是什么”,不包含状态、策略、触发时点、适用范围等应用配置。
/// 这些执行维度由 RiskRuleApplication 承载。
/// </summary>
public class RiskRule
{
// === 基础信息 ===
public long Id { get; set; }
public string RuleName { get; set; }
public string RuleText { get; set; }
/// <summary>
/// 【核心】条件JSON(结构化模式的条件列表存储),如
/// {"conditions":[{"variableId":1,"operator":">","value":100000000}]}
/// 由 RuleCompiler 解析并编译为 CompiledScript
/// </summary>
public string ConditionJson { get; set; }
/// <summary>
/// 【核心】规则表达式(自由文本模式,类C#表达式)
/// 由 RuleCompiler 编译为 CompiledScript
/// </summary>
public string RuleExpr { get; set; }
// === 生命周期 ===
public int Version { get; set; }
public RiskRuleStatus Status { get; set; }
// === 审计字段 ===
public int OptId { get; set; }
public string OptName { get; set; }
public DateTime OptDate { get; set; }
public int UpdateOptId { get; set; }
public string UpdateOptName { get; set; }
public DateTime UpdateDate { get; set; }
// === 运行时内存字段(不持久化到数据库)===
/// <summary>
/// 【核心】预编译的可执行委托
/// 在规则创建/加载时由 RuleCompiler.Compile(this) 生成
/// 判风控时直接调用:rule.CompiledScript(context) → bool
/// </summary>
public Func<RiskContext, bool> CompiledScript { get; set; }
}
}
@@ -0,0 +1,142 @@
using System;
using YLErp.DBModels;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控规则应用配置。
///
/// 设计原因:
/// 依据设计文档,风控规则需要区分“规则定义”和“规则应用”两个层面。
/// RiskRule 负责定义“规则是什么”,例如规则名称、规则编码、公式表达式、描述、版本等;
/// RiskRuleApplication 负责定义“规则在什么场景下如何执行”,例如启用状态、校验策略、触发时点、适用范围。
///
/// 拆分后的好处是:
/// 1. 同一条规则可以被多个应用配置复用,避免重复维护相同的公式定义。
/// 2. 不同业务场景可以对同一条规则配置不同的执行策略,例如在簿记确认时禁止,在平仓审核时提示。
/// 3. 调整触发时点、适用范围、控制策略时,不需要修改规则本身,降低配置耦合。
/// 4. 后续如果要做规则管理页面、规则启停、按场景灰度投放,也更容易扩展。
///
/// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
/// </summary>
public class RiskRuleApplication
{
/// <summary>
/// 应用配置主键ID。
/// 用于唯一标识一条规则应用记录。
/// </summary>
public long Id { get; set; }
/// <summary>
/// 关联的规则ID列表。
/// 通过该字段与 RiskRule.Id 关联,多个规则ID使用逗号分隔。
/// </summary>
public string RuleIds { get; set; }
/// <summary>
/// 应用状态。
/// 约定:
/// 1 = Active(已生效)
/// 2 = Disabled(已停用)
/// 3 = Deleted(已删除)
/// 在实际执行时,通常只加载 Active 状态的应用配置。
/// </summary>
public RiskRuleStatus Status { get; set; }
/// <summary>
/// 校验策略。
/// 约定:
/// 1 = 禁止
/// 2 = 审批
/// 3 = 提示
/// 当规则命中时,由该字段决定最终处理方式。
/// </summary>
public RiskControlStrategy ControlStrategy { get; set; }
/// <summary>
/// 触发时点。
/// 多个时点使用逗号分隔,例如:
/// BOOK_CONFIRM,CLOSE_REVIEW
/// 表示该应用配置会在这些业务时点参与风控判断。
/// </summary>
public string TriggerPoints { get; set; }
/// <summary>
/// 是否全局适用。
/// true 表示对所有交易都适用,此时通常忽略其他 Scope 字段;
/// false 表示只在指定范围内适用,需要结合下方各 Scope 字段进行过滤。
/// </summary>
public int ScopeIsGlobal { get; set; }
/// <summary>
/// 适用的资产簿记账户范围。
/// 第一版使用逗号分隔的字符串保存,例如:
/// 123,456,789
/// 后续执行时可按约定解析为账户ID集合。
/// </summary>
public string ScopeAssetBookIds { get; set; }
/// <summary>
/// 适用的客户范围。
/// 第一版使用逗号分隔的字符串保存客户ID,例如:
/// 10001,10002
/// 用于在执行前筛选该规则是否适用于当前客户。
/// </summary>
public string ScopeClientIds { get; set; }
/// <summary>
/// 适用的标的类型范围。
/// 例如:
/// 股票,ETF,债券
/// 用于控制规则仅对特定标的类型生效。
/// </summary>
public string ScopeUnderlyingTypes { get; set; }
/// <summary>
/// 适用的交易类型范围。
/// 例如:
/// 簿记交易,平仓交易
/// 用于控制规则仅在指定交易类型下生效。
/// </summary>
public string ScopeTradeTypes { get; set; }
public int Version { get; set; }
/// <summary>
/// 创建人ID。
/// 记录是谁创建了这条应用配置。
/// </summary>
public int OptId { get; set; }
/// <summary>
/// 创建人名称。
/// 便于界面展示和审计追踪。
/// </summary>
public string OptName { get; set; }
/// <summary>
/// 创建时间。
/// 记录应用配置的创建时刻。
/// </summary>
public DateTime OptDate { get; set; }
/// <summary>
/// 最后更新人ID。
/// 记录最近一次修改该应用配置的操作人。
/// </summary>
public int UpdateOptId { get; set; }
/// <summary>
/// 最后更新人名称。
/// 便于界面展示和审计追踪。
/// </summary>
public string UpdateOptName { get; set; }
/// <summary>
/// 最后更新时间。
/// 记录最近一次修改该应用配置的时间。
/// </summary>
public DateTime UpdateDate { get; set; }
}
}
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,7 @@ using YLErp.Modules.TradeDalModule;
using YLErp.Modules.TradeModule;
using YLErp.Modules.TradeModule.DealModule;
using YLErp.QdpModule;
using YLErp.Modules.RiskEngine;
using static YLErp.ConsGlobal;
namespace YLErp.Modules.RiskModule
@@ -1012,7 +1013,7 @@ namespace YLErp.Modules.RiskModule
// 互换DV汇总:直接取eod_swap_position中已计算好的dv01
var eodSwapDV = Convert.ToDouble(posiQuery.Sum(p => p.dv01));
var setValue = new Action<QuotaMonitor_Global, List<QuotaSetting>>((obj, settings) =>
{
obj.StockEqvNotional = Convert.ToDouble(posiQuery.Sum(s => s.PosiNotionalValue));
@@ -4464,7 +4465,12 @@ namespace YLErp.Modules.RiskModule
public bool QuotaCheck(ref TradeOpenResult res)
{
if (res == null && res.Trade == null)
return QuotaCheck(ref res, false);
}
public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable<string> ignoreRiskRuleIds = null)
{
if (res == null || res.Trade == null)
{
throw new ArgumentNullException(nameof(res));
}
@@ -4473,7 +4479,7 @@ namespace YLErp.Modules.RiskModule
var tradeNumber = DbContext.trade.Where(O => O.id == tradeId).Select(O => O.TradeNumber).FirstOrDefault();
var trialService = new QuotaMonitorService(this);
var quotaObj = trialService.QuotaCheck(tradeId, 1);
if (quotaObj.TrialStatus == QuotaTrialStatusEnum.Success)//这次计算结果是通过,直接通过
if (quotaObj.TrialStatus == QuotaTrialStatusEnum.Success)//这次计算结果是通过,直接通过
{
if (!string.IsNullOrWhiteSpace(quotaObj.AvailableForClient)) // 返回用户资金情况
{
@@ -4481,6 +4487,12 @@ namespace YLErp.Modules.RiskModule
res.TrialDataId = quotaObj.id;
return false;
}
//成功状态下,如果触发TIp检查,提示用户
if (!string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails))
{
res.TrialDataId = quotaObj.id;
res.TipMsg = quotaObj.RiskWarningDetails;
}
return true;
}
var lastTrial = trialService.QueryLastQuotaTrial(tradeId, true);
@@ -4491,6 +4503,41 @@ namespace YLErp.Modules.RiskModule
//否则的情况是上次没算,这次是预警,或上次算了,结果是不通过\通过或预警,这次是预警或不通过,提示用户;
res.RetCode = TradeOpenRetCode.QuotaTrialError;
res.TrialDataId = quotaObj.id;
//汇总各类检查详情,避免只展示RiskWarningDetails而漏掉其它老风控检查
var detailParts = new List<string>();
if (!string.IsNullOrWhiteSpace(quotaObj.FundCheckDetails))
detailParts.Add($"应付资金检查:{quotaObj.FundCheckDetails}");
if (!string.IsNullOrWhiteSpace(quotaObj.QuotaCheckDetails))
detailParts.Add($"限额检查:{quotaObj.QuotaCheckDetails}");
if (!string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails))
detailParts.Add($"风险预警:{quotaObj.RiskWarningDetails}");
if (!string.IsNullOrWhiteSpace(quotaObj.QuotaWarningDetails))
detailParts.Add($"限额预警:{quotaObj.QuotaWarningDetails}");
res.ErrorMsg = string.Join("\n", detailParts);
//确认本次为需审批后,二次特批。因为只靠点击“交易特批”的ignoreRiskWarning,不能保证本次校验通过。
var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.RiskWarning
&& !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails)
&& (quotaObj.ApprovalRuleIds?.Any() ?? false);
//当前为老风控特批,不是新风控需审批
res.OldRiskNeedSpecialApproval = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning;
if (isRiskApprovalWarning)
{
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;
}
@@ -4882,6 +4929,22 @@ namespace YLErp.Modules.RiskModule
return result;
}
/// <summary>
/// 按 quotaTrial.id 校验试算结果是否已超过配置时效
/// </summary>
/// <param name="trialDataId">quotaTrial.id</param>
/// <param name="expireSeconds">有效时长(秒)</param>
/// <returns>查不到记录或已超时返回 true,否则返回 false</returns>
public bool IsQuotaTrialExpired(int trialDataId, int expireSeconds)
{
var quotaTrial = QueryQuotaTrial(trialDataId);
if (quotaTrial == null || !quotaTrial.OptDate.HasValue)
{
return true;
}
return DateTime.Now - quotaTrial.OptDate.Value > TimeSpan.FromSeconds(expireSeconds);
}
/// <summary>
/// 查询试算历史
/// </summary>
@@ -4904,14 +4967,14 @@ namespace YLErp.Modules.RiskModule
_logger.Info($"[限额试算] 开始执行 - tradeId: {tradeId}, trialSource: {trialSource}");
var result = new QuotaTrial();
trade tradeObj = DbContext.trade.AsNoTracking().Where(O => O.id == tradeId).FirstOrDefault();
// 检查交易对象是否存在
if (tradeObj == null)
{
_logger.Info($"[限额试算] 找不到交易记录 - tradeId: {tradeId}");
throw new ArgumentNullException(nameof(tradeObj), $"找不到ID为 {tradeId} 的交易记录");
}
_logger.Info($"[限额试算] 交易信息 - tradeId: {tradeObj.id}, TradeNumber: {tradeObj.TradeNumber}, TradeType: {tradeObj.TradeType}, ClientId: {tradeObj.ClientId}");
//// 检查关键字段是否为null
@@ -5110,6 +5173,75 @@ namespace YLErp.Modules.RiskModule
result.AvailableForClient = availableMsg ?? "";
result.TrialSource = trialSource;
_logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}");
// ===== 新风控引擎接入点(设计文档 4.6.1) =====
try
{
var riskEngine = new RiskEngineService(this);
var riskContext = new RiskContext
{
TradeId = tradeId,
TriggerPoint = "BOOK_CONFIRM"
};
var riskResult = riskEngine.EvaluateRisk(riskContext, "BOOK_CONFIRM");
_logger.Info($"[风控引擎] 簿记交易确认 - TradeId: {tradeId}, Passed: {riskResult.Passed}, Blocked: {riskResult.Blocked}, NeedApproval: {riskResult.NeedApproval}, ShowTip: {riskResult.ShowTip}");
// 按策略映射回 QuotaTrialStatusEnum
if (riskResult.Blocked)
{
result.TrialStatus = QuotaTrialStatusEnum.Error;
result.RiskWarningDetails += "[风控引擎] 规则触发:禁止\n";
foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Block))
{
result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n";
}
}
if (riskResult.NeedApproval)
{
if (result.TrialStatus != QuotaTrialStatusEnum.Error && result.TrialStatus != QuotaTrialStatusEnum.Warning)
{
result.TrialStatus = QuotaTrialStatusEnum.RiskWarning;
}
result.RiskWarningDetails += "[风控引擎] 规则触发:需审批\n";
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";
}
}
if (riskResult.ShowTip)
{
var warningMessages = riskResult.TriggeredRules
.Where(r => r.ControlStrategy == RiskControlStrategy.ShowTip)
.Select(r => r.Message)
.Where(r => !string.IsNullOrWhiteSpace(r))
.ToList();
if (warningMessages.Count > 0)
{
result.RiskWarningDetails += "[风控引擎] 提示:" + string.Join("", warningMessages) + "\n";
}
}
}
catch (Exception ex)
{
_logger.Error($"[风控引擎] 执行异常 - TradeId: {tradeId}, Error: {ex.Message}");
// 第一版:异常不阻断原有流程,只记录日志
}
// ===== 新风控引擎接入结束 =====
SaveQuotaTrial(result);
return result;
}
@@ -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;
@@ -146,6 +149,59 @@ namespace YLErp.Modules.RiskModule
DbContext.SaveChanges();
}
/// <summary>
/// 查询指定交易最近一次新风控“确认通过”的处理时间,用于控制二次审批放行仅在短时间内有效。
/// </summary>
public DateTime? QueryLatestRiskWarningDecisionTime(int tradeId)
{
var tradeNumber = DbContext.trade.Where(x => x.id == tradeId).Select(x => x.TradeNumber).FirstOrDefault();
if (string.IsNullOrWhiteSpace(tradeNumber))
{
return null;
}
return DbContext.trade_risk_check_log
.Where(x => x.trade_number == tradeNumber
&& x.trial_result == (int)QuotaTrialStatusEnum.RiskWarning
&& x.remark.Contains("风控预警处理结果:确认通过"))
.OrderByDescending(x => x.create_time)
.Select(x => (DateTime?)x.create_time)
.FirstOrDefault();
}
public void AddWarningDecisionLog(int quotaTrialId, string decision)
{
var quotaTrial = DbContext.quotaTrial.FirstOrDefault(x => x.id == quotaTrialId);
if (quotaTrial == null)
{
return;
}
var td = DbContext.trade.FirstOrDefault(x => x.id == quotaTrial.TradeId);
trade_risk_check_log log = new trade_risk_check_log();
if (td != null)
{
var client = DataCacheProvider.GetClientDataSource().GetData(td.ClientId);
log.client_number = client?.Number;
}
var worker = new IdWorker(1, 1);
log.id = worker.NextId();
log.trial_result = (int)quotaTrial.TrialStatus;
log.client_name = quotaTrial.ClientName;
log.trader = td?.TraderName;
log.trade_number = quotaTrial.TradeNumber;
log.risk_warning = string.IsNullOrWhiteSpace(quotaTrial.RiskWarningDetails)
? $"[风控预警处理]{decision}"
: quotaTrial.RiskWarningDetails + Environment.NewLine + $"[风控预警处理]{decision}";
log.limit_warning = quotaTrial.QuotaCheckDetails;
log.remark = string.IsNullOrWhiteSpace(quotaTrial.Remark)
? $"风控预警处理结果:{decision}"
: quotaTrial.Remark + ";风控预警处理结果:" + decision;
log.create_user = UserId;
log.create_time = DateTime.Now;
DbContext.trade_risk_check_log.Add(log);
DbContext.SaveChanges();
}
/// <summary>
/// 回退重置
/// </summary>
@@ -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)
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))
{
@@ -76,6 +76,7 @@ namespace YLErp.Modules.TradeModule.DealModule
var hasTradeProcess = HasTradeProcess();
var sbError = new StringBuilder();
var sbTip = new StringBuilder();
var result = new TradeConfirmResultModel
{
confirmedTradeIds = new List<int>(),
@@ -99,7 +100,9 @@ namespace YLErp.Modules.TradeModule.DealModule
isSkipApproval: isSkipApproval,
confirmedTradeIds: result.confirmedTradeIds,
changeConfirmPaths: result.changeConfirmPaths,
continueQuotaCheck: idCache.Contains(pid));
continueQuotaCheck: idCache.Contains(pid),
ignoreRiskWarning: ignoreRiskWarning,
ignoreRiskRuleIds: ignoreRiskRuleIds);
if (pid > 0)
{
idCache.Add(pid);
@@ -114,11 +117,14 @@ namespace YLErp.Modules.TradeModule.DealModule
result.LackOfMoney = true;
break;
}
if (temp.RetCode == TradeOpenRetCode.QuotaTrialError || temp.RetCode == TradeOpenRetCode.FundStatus)
//此处LackOfMoney仅仅代表需要前端二次处理,不代表资金缺乏,延用老逻辑
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;
}
@@ -128,6 +134,12 @@ namespace YLErp.Modules.TradeModule.DealModule
sbError.AppendLine(temp.ErrorMsg);
}
if (!string.IsNullOrEmpty(temp.TipMsg))
{
result.TrialDataId = temp.TrialDataId;
sbTip.AppendLine(temp.TipMsg);
}
Helpers.LockHelper.Release(lockKeyId);
}
}
@@ -135,6 +147,7 @@ namespace YLErp.Modules.TradeModule.DealModule
DbContext.SaveChanges();
result.errorMsg = sbError.ToString();
result.tipMsg = sbTip.ToString();
return result;
}
@@ -152,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 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());
@@ -207,7 +220,7 @@ namespace YLErp.Modules.TradeModule.DealModule
}
var trialService = new QuotaMonitorService(this);
if (!trialService.QuotaCheck(ref result))
if (!trialService.QuotaCheck(ref result, ignoreRiskWarning, ignoreRiskRuleIds))
{
return result;
}
@@ -223,6 +236,10 @@ namespace YLErp.Modules.TradeModule.DealModule
history = "特权账户批量确认自动" + history;
}
}
else if (ignoreRiskWarning)
{
history = "风控预警确认-" + history;
}
//进入交易审批流程
if (hasTradeProcess && !isSkipApproval)
{
@@ -604,11 +621,14 @@ namespace YLErp.Modules.TradeModule.DealModule
public class TradeConfirmResultModel
{
public bool LackOfMoney;
public bool OldRiskNeedSpecialApproval;
public int TrialDataId;
public int TradeId;
public List<int> confirmedTradeIds;
public List<string> changeConfirmPaths;
public string errorMsg;
public string tipMsg;
public string type;
public List<string> ignoreRiskRuleIds;
}
}
@@ -794,11 +794,17 @@ namespace YLErp.Modules.TradeModule.DealModule
public TradeOpenRetCode RetCode;
public int TrialDataId;
public List<string> IgnoredRiskRuleIds;
public bool OldRiskNeedSpecialApproval;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMsg;
/// <summary>
/// 提示信息
/// </summary>
public string TipMsg;
/// <summary>
/// 变更确认书路径
@@ -838,6 +844,10 @@ namespace YLErp.Modules.TradeModule.DealModule
/// </summary>
FundStatus,
/// <summary>
/// 风控预警二次确认
/// </summary>
RiskWarning,
/// <summary>
/// 无审核权限
/// </summary>
NoRight
+2
View File
@@ -25,6 +25,8 @@ namespace YLErp.BLL
/// </summary>
public const string LackOfMoney = "LackOfMoney";
public const string RiskWarningConfirm = "RiskWarningConfirm";
public const string IgnoreBreak = "IgnoreBreak";
/// <summary>
+3 -1
View File
@@ -30,6 +30,8 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentFTP" Version="45.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.12.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.10.0" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
<PackageReference Include="Oracle.EntityFrameworkCore" Version="6.21.90" />
@@ -40,7 +42,7 @@
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.0" />
<PackageReference Include="Qdp.Pricing.Library.Options" Version="1.0.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="YLErp.Office" Version="1.1.2" />
</ItemGroup>
+13
View File
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting.Server.Features;
using YLErp.DBModels.Consts;
using YLErp.Modules.AppModule;
using YLErp.Modules.EodModule.SettlementModule;
using YLErp.Modules.RiskEngine;
using YLErp.Modules.RiskModule;
using YLErp.Modules.SuperviseReportModule.SAC.Service;
using YLErp.Modules.TradeRiskCalcModule;
@@ -124,6 +125,18 @@ namespace YLErp.Web.App
PS.SetConfig(ConsAppConfig.YLErpWebUrlConfig, address);
}
// 预热风控引擎:加载规则与应用到内存,并预编译所有规则
Task.Run(() =>
{
try
{
RiskEngineService.GetInstance().Preload();
}
catch (Exception ex)
{
LogFactory.GetLogger("HostedTaskService").Error("[风控引擎] 启动预热失败", ex);
}
});
});
return Task.CompletedTask;
+17
View File
@@ -60,6 +60,23 @@
<FunctionSub Name="VaR计算" Title="VaR计算"></FunctionSub>
<FunctionSub Name="观察事件" Title="观察事件"></FunctionSub>
<FunctionSub Name="风险预警/禁止查询" Title="风险预警/禁止查询"></FunctionSub>
<FunctionSub Name="异常交易监控" Title="异常交易监控"></FunctionSub>
<FunctionSub Name="风控规则查看" Type="Operate" Note="是否有权限查询风控规则"></FunctionSub>
<FunctionSub Name="风控规则新增" Type="Operate" Note="是否有权限新增风控规则"></FunctionSub>
<FunctionSub Name="风控规则编辑" Type="Operate" Note="是否有权限编辑风控规则"></FunctionSub>
<FunctionSub Name="风控规则删除" Type="Operate" Note="是否有权限删除/批量删除风控规则"></FunctionSub>
<FunctionSub Name="风控规则启停" Type="Operate" Note="是否有权限启用/停用风控规则"></FunctionSub>
<FunctionSub Name="风控应用查看" Type="Operate" Note="是否有权限查询风控应用"></FunctionSub>
<FunctionSub Name="风控应用新增" Type="Operate" Note="是否有权限新增应用配置"></FunctionSub>
<FunctionSub Name="风控应用编辑" Type="Operate" Note="是否有权限编辑应用配置"></FunctionSub>
<FunctionSub Name="风控应用删除" Type="Operate" Note="是否有权限删除应用配置"></FunctionSub>
<FunctionSub Name="风控应用启停" Type="Operate" Note="是否有权限启用/停用/批量操作应用配置"></FunctionSub>
<FunctionSub Name="风控变量查看" Type="Operate" Note="是否有权限查询风控变量"></FunctionSub>
<FunctionSub Name="风控变量新增" Type="Operate" Note="是否有权限新增变量"></FunctionSub>
<FunctionSub Name="风控变量编辑" Type="Operate" Note="是否有权限编辑变量"></FunctionSub>
<FunctionSub Name="风控变量删除" Type="Operate" Note="是否有权限删除变量"></FunctionSub>
<FunctionSub Name="风控日志查看" Type="Operate" Note="是否有权限查询风控日志"></FunctionSub>
<FunctionSub Name="风控日志导出" Type="Operate" Note="是否有权限导出日志Excel"></FunctionSub>
</FunctionParent>
<FunctionParent Name="结算管理" Title="结算财务">
<FunctionSub Name="成交簿记" Title="成交簿记"></FunctionSub>
+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"},
+698
View File
@@ -0,0 +1,698 @@
using Qdp.Foundation.Utilities;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine;
using YLErp.Modules.RiskEngine.Dto;
namespace YLErp.Web.Controllers
{
/// <summary>风控引擎管理控制器,提供规则、应用配置、变量池和审计日志的 CRUD 接口</summary>
[Route("risk-engine")]
public class RiskRuleController : BaseController
{
private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleController");
#region Rule Management
[HttpGet("risk-rules")]
[MyAuthorize("风险控制-风控规则查看")]
/// <summary>查询风控规则列表(分页)</summary>
public JsonResult QueryRiskRules(QueryRiskRuleReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.QueryRuleList(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询风控规则列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-rules/{id}")]
[MyAuthorize("风险控制-风控规则查看")]
/// <summary>获取风控规则详情</summary>
public JsonResult GetRiskRule(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetRuleDetail(id);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询风控规则详情");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-rules/{id}/versions")]
[MyAuthorize("风险控制-风控规则查看")]
/// <summary>获取风控规则版本历史</summary>
public JsonResult GetRiskRuleVersions(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetRuleVersions(id);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询风控规则版本历史");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-rules")]
[MyAuthorize("风险控制-风控规则新增")]
/// <summary>创建风控规则</summary>
public JsonResult CreateRiskRule([FromBody] CreateRiskRuleReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.CreateRule(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "创建风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPut("risk-rules/{id}")]
[MyAuthorize("风险控制-风控规则编辑")]
/// <summary>更新风控规则</summary>
public JsonResult UpdateRiskRule(long id, [FromBody] UpdateRiskRuleReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.UpdateRule(id, req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "更新风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpDelete("risk-rules/{id}")]
[MyAuthorize("风险控制-风控规则删除")]
/// <summary>删除风控规则(软删除)</summary>
public JsonResult DeleteRiskRule(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.DeleteRule(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "删除风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpDelete("risk-rules/batch")]
[MyAuthorize("风险控制-风控规则删除")]
/// <summary>批量删除风控规则</summary>
public JsonResult BatchDeleteRiskRules([FromBody] List<long> ids)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.BatchDeleteRules(ids);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "批量删除风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-rules/{id}/enable")]
[MyAuthorize("风险控制-风控规则启停")]
/// <summary>启用风控规则</summary>
public JsonResult EnableRiskRule(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.EnableRule(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "启用风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-rules/{id}/disable")]
[MyAuthorize("风险控制-风控规则启停")]
/// <summary>停用风控规则</summary>
public JsonResult DisableRiskRule(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.DisableRule(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "停用风控规则");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-rules/{id}/applications")]
[MyAuthorize("风险控制-风控规则查看")]
/// <summary>获取规则关联的应用配置列表</summary>
public JsonResult GetRuleApplications(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetRuleApplications(id);
return Json(new { success = true, data = new { rows = result, total = result.Count, records = result.Count, page = 1 } });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询规则关联应用列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-rules/list")]
[MyAuthorize("风险控制-风控规则查看")]
/// <summary>获取所有启用规则列表(供下拉选择)</summary>
public JsonResult GetAllRiskRules()
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetAllRuleList();
return Json(new { success = true, data = new { rows = result, total = result.Count, records = result.Count, page = 1 } });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询规则下拉列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
#region Application Management
[HttpGet("risk-applications")]
[MyAuthorize("风险控制-风控应用查看")]
/// <summary>查询应用配置列表(分页)</summary>
public JsonResult QueryRiskApplications(QueryRiskApplicationReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.QueryApplicationList(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询应用配置列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-applications/{id}")]
[MyAuthorize("风险控制-风控应用查看")]
/// <summary>获取应用配置详情</summary>
public JsonResult GetRiskApplication(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetApplicationDetail(id);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询应用配置详情");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-applications")]
[MyAuthorize("风险控制-风控应用新增")]
/// <summary>创建应用配置</summary>
public JsonResult CreateRiskApplication([FromBody] CreateRiskApplicationReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.CreateApplication(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "创建应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPut("risk-applications/{id}")]
[MyAuthorize("风险控制-风控应用编辑")]
/// <summary>更新应用配置</summary>
public JsonResult UpdateRiskApplication(long id, [FromBody] UpdateRiskApplicationReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.UpdateApplication(id, req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "更新应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpDelete("risk-applications/{id}")]
[MyAuthorize("风险控制-风控应用删除")]
/// <summary>删除应用配置(软删除)</summary>
public JsonResult DeleteRiskApplication(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.DeleteApplication(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "删除应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-applications/{id}/enable")]
[MyAuthorize("风险控制-风控应用启停")]
/// <summary>启用应用配置</summary>
public JsonResult EnableRiskApplication(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.EnableApplication(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "启用应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-applications/{id}/disable")]
[MyAuthorize("风险控制-风控应用启停")]
/// <summary>停用应用配置</summary>
public JsonResult DisableRiskApplication(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.DisableApplication(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "停用应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-applications/batch-enable")]
[MyAuthorize("风险控制-风控应用启停")]
/// <summary>批量启用应用配置</summary>
public JsonResult BatchEnableRiskApplications([FromBody] List<long> ids)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.BatchEnableApplications(ids);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "批量启用应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-applications/batch-disable")]
[MyAuthorize("风险控制-风控应用启停")]
/// <summary>批量停用应用配置</summary>
public JsonResult BatchDisableRiskApplications([FromBody] List<long> ids)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.BatchDisableApplications(ids);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "批量停用应用配置");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
#region Variable Management
[HttpGet("risk-variables")]
[MyAuthorize("风险控制-风控变量查看")]
/// <summary>查询变量列表</summary>
public JsonResult QueryRiskVariables(QueryRiskVariableReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.QueryVariableList(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询变量列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-variables/{id}")]
[MyAuthorize("风险控制-风控变量查看")]
/// <summary>获取变量详情</summary>
public JsonResult GetRiskVariable(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetVariableDetail(id);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询变量详情");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-variables/list")]
[MyAuthorize("风险控制-风控变量查看")]
/// <summary>获取所有变量列表(供下拉选择)</summary>
public JsonResult GetAllRiskVariables()
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetAllVariableList();
return Json(new { success = true, data = new { rows = result, total = result.Count, records = result.Count, page = 1 } });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询全部变量列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPost("risk-variables")]
[MyAuthorize("风险控制-风控变量新增")]
/// <summary>创建变量</summary>
public JsonResult CreateRiskVariable([FromBody] CreateRiskVariableReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.CreateVariable(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "创建变量");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpPut("risk-variables/{id}")]
[MyAuthorize("风险控制-风控变量编辑")]
/// <summary>更新变量(含级联更新引用规则的 RuleExpr</summary>
public JsonResult UpdateRiskVariable(long id, [FromBody] UpdateRiskVariableReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.UpdateVariable(id, req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "更新变量");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpDelete("risk-variables/{id}")]
[MyAuthorize("风险控制-风控变量删除")]
/// <summary>删除变量</summary>
public JsonResult DeleteRiskVariable(long id)
{
try
{
var service = new RiskRuleService(CurUser);
service.DeleteVariable(id);
return Json(new { success = true });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "删除变量");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
#region Trade Types
[HttpGet("trade-types")]
[MyAuthorize("风险控制-风控应用查看")]
/// <summary>获取合约类型列表</summary>
public JsonResult GetTradeTypes()
{
try
{
var rows = ConsTrade.AllTradeTypes.Select(t => new { label = t, value = t }).ToList();
var total = rows.Count;
return Json(new {
success = true,
data = new {
page = 1,
records = total,
rows = rows,
total = total
}
});
}
catch (Exception ex)
{
_logger.Error(ex, "查询合约类型列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
#region Audit Log
[HttpGet("risk-audit-logs")]
[MyAuthorize("风险控制-风控日志查看")]
/// <summary>查询风控操作日志列表(分页)</summary>
public JsonResult QueryRiskAuditLogs(QueryRiskAuditLogReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.QueryAuditLogs(req);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询风控操作日志列表");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-audit-logs/{id}")]
[MyAuthorize("风险控制-风控日志查看")]
/// <summary>获取风控操作日志详情</summary>
public JsonResult GetRiskAuditLogDetail(long id)
{
try
{
var service = new RiskRuleService(CurUser);
var result = service.GetAuditLogDetail(id);
return Json(new { success = true, data = result });
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "查询风控操作日志详情");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
[HttpGet("risk-audit-logs/export")]
[MyAuthorize("风险控制-风控日志导出")]
/// <summary>导出风控操作日志为 Excel</summary>
public IActionResult ExportRiskAuditLogs(QueryRiskAuditLogReq req)
{
try
{
var service = new RiskRuleService(CurUser);
var bytes = service.ExportAuditLogs(req);
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "风控操作日志.xlsx");
}
catch (ServiceException ex)
{
return Json(new { success = false, message = ex.Message });
}
catch (Exception ex)
{
_logger.Error(ex, "导出风控操作日志");
return Json(new { success = false, message = "系统异常,请联系管理员" });
}
}
#endregion
}
}
+3
View File
@@ -15,6 +15,7 @@ using YLErp.BLL;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.Modules.DictionaryModule;
using YLErp.Modules.RiskEngine;
using YLErp.Modules.SystemModule;
using YLErp.Web.Models;
@@ -1356,6 +1357,8 @@ namespace YLErp.Web.Controllers
Providers.MemoryCacheProvider.Default.FlushAll();
Server.CacheProvider.FlushAll();
Modules.DataCacheModule.DataCacheManager.ResetDataCache();
// 刷新风控引擎缓存
RiskEngineService.GetInstance().RefreshCache();
return Json(1);
}
+71 -7
View File
@@ -2447,7 +2447,8 @@ namespace YLErp.Web.Controllers
}
public JsonResult tradeConfirm(string tradeids, string additionalProcessing, bool isSkipCheck = false)
// trialDataId: 二次确认时回传的 quotaTrial.id,用于超时校验;首次确认不传,为 null 时不做超时判断
public JsonResult tradeConfirm(string tradeids, string additionalProcessing, string ignoreRiskRuleIds, int? trialDataId, bool isSkipCheck = false)
{
var tradeidArr = DataConvert.ConvertCommaValuesToInt32Array(tradeids);
if (tradeids is null || !tradeids.Any(n => n > 0))
@@ -2455,19 +2456,67 @@ namespace YLErp.Web.Controllers
return JsonError("请至少选择一项要生成确认书的交易,参数tradeids:" + tradeids);
}
var config = valuedateBLL.SystemDate;
var riskWarningConfirmExpireSeconds = 300;
var riskWarningConfirmExpireSecondsConfig = AppManager.GetAppConfigValue("ProjectConfig", "Trade.RiskWarningConfirmExpireSeconds");
if (!string.IsNullOrWhiteSpace(riskWarningConfirmExpireSecondsConfig)
&& int.TryParse(riskWarningConfirmExpireSecondsConfig, out var configuredRiskWarningConfirmExpireSeconds)
&& configuredRiskWarningConfirmExpireSeconds > 0)
{
riskWarningConfirmExpireSeconds = configuredRiskWarningConfirmExpireSeconds;
}
var ignoreMoneyCheck = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.LackOfMoney;
var ignoreRiskWarning = tradeidArr.Count() == 1 && additionalProcessing == tradeBLL.RiskWarningConfirm;
var ignoreRiskRuleIdArr = StringHelper.ConvertCommaValuesToStringArray(ignoreRiskRuleIds);
// 老风控交易特批与新风控二次审批统一基于 quotaTrial 做超时校验,超时后清空放行标记并重新校验。
// 仅 swapTradeView.js 会回传 trialDataId 并触发 RiskWarningConfirm,按 quotaTrial.OptDate 精确校验;
// tradeview.js / tradeConfirmList.js / swapTradeConfirmList.js / quotaMonitor.js 只发 LackOfMoney,不传 trialDataId,不进入超时校验。
if (ignoreMoneyCheck || ignoreRiskWarning)
{
var trialExpired = false;
if (trialDataId.HasValue)
{
// swapTradeView.js:按 quotaTrial.OptDate 校验,查不到记录也视为过期
trialExpired = new QuotaMonitorService(CurUser).IsQuotaTrialExpired(trialDataId.Value, riskWarningConfirmExpireSeconds);
}
if (trialExpired)
{
ignoreMoneyCheck = false;
ignoreRiskWarning = false;
ignoreRiskRuleIdArr = Array.Empty<string>();
}
}
var isSkipApproval = false;
if (CurUser._特批批量确认 && isSkipCheck)
{
ignoreMoneyCheck = true;
isSkipApproval = true;
}
var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval);
var result = new TradeConfirmService(CurUser).tradeConfirm(tradeidArr, ignoreMoneyCheck, isSkipApproval, ignoreRiskWarning, ignoreRiskRuleIdArr);
//如果客户缺少资金而操作者有交易特批权限
if (!ignoreMoneyCheck && result.LackOfMoney)
if (!ignoreMoneyCheck && !ignoreRiskWarning && result.LackOfMoney)
{
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = PS.Config.ErpElement.Company == Configuration.CompanyEnum. || config.SpecialOperateForTrade == 1 ? tradeBLL.LackOfMoney : "", TrialDataId = result.TrialDataId, message = result.errorMsg, typecode = result.type });
if (result.type == TradeOpenRetCode.RiskWarning.ToString())
{
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", 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", 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 });
}
}
//生成交易确认书
@@ -2475,7 +2524,7 @@ namespace YLErp.Web.Controllers
{
new TradeContractGenerateService(CurUser).GenerateContractsAsync(result.confirmedTradeIds, "确认书");
}
//有ErrorMsg则认为失败
if (!string.IsNullOrEmpty(result.errorMsg))
{
if (result.changeConfirmPaths?.Count > 0)
@@ -2486,11 +2535,12 @@ namespace YLErp.Web.Controllers
}
else
{
var successMsg = string.IsNullOrWhiteSpace(result.tipMsg) ? "操作完成" : result.tipMsg;
if (result.changeConfirmPaths?.Count > 0)
{
return JsonSuccess("操作完成", new { generateChangeSuccess = true, url = result.changeConfirmPaths });
return JsonSuccess(successMsg, new { generateChangeSuccess = true, url = result.changeConfirmPaths, TrialDataId = result.TrialDataId });
}
return JsonSuccess("操作完成");
return JsonSuccess(successMsg, new { TrialDataId = result.TrialDataId });
}
}
@@ -8399,6 +8449,20 @@ namespace YLErp.Web.Controllers
return JsonSuccess();
}
public JsonResult SaveRiskWarningDecisionLog(int quotaTrialId, string decision)
{
try
{
new TradeRiskCheckLogService(CurUser).AddWarningDecisionLog(quotaTrialId, decision);
}
catch (Exception ex)
{
LogFactory.GetLogger("SaveRiskWarningDecisionLog").Error(ex);
return JsonError("保存风控预警处理结果失败!");
}
return JsonSuccess();
}
[MyAuthorize("风险控制-接口审批列表")]
public ActionResult InterfaceApproval()
{
+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,18 +49,86 @@ function showSwapRate(timeRate, isMarginLeg) {
$("#swapIntervalModal").modal("show");
}
var layerIndex = -1;
var confirmFunc = function (id, additionalProcessing) {
var confirmFunc = function (id, additionalProcessing, ignoreRiskRuleIds, trialDataId) {
var pData = { tradeids: id };
if (!main.isEmpty(additionalProcessing)) {
pData.additionalProcessing = additionalProcessing;
}
if (ignoreRiskRuleIds && ignoreRiskRuleIds.length > 0) {
pData.ignoreRiskRuleIds = ignoreRiskRuleIds.join(',');
}
if (trialDataId) {
pData.trialDataId = trialDataId;
}
main.post("/trade/tradeConfirm", pData).done(function (data) {
if (!data.success) {
main.message(data.msg);
return;
}
if (data.obj && data.obj.proccessType === "QuotaTrialError") {
//限额试算不通过(老风控检查失败),展示详情表格但不允许审批
if (data.obj.TrialDataId) {
var quotaTrialErrorLayerSetting = {
type: 2,
title: "提示",
shadeClose: false,
shade: 0.4,
area: ['800px', '500px'],
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
yes: function (index) {
layer.close(layerIndex);
},
cancel: function () {
if (window.parent && window.parent.reloadtrade) {
window.parent.reloadtrade();
}
layer.close(layerIndex);
}
};
quotaTrialErrorLayerSetting.btn = ["关闭"];
layerIndex = layer.open(quotaTrialErrorLayerSetting);
}
else if (data.obj.message) {
main.message(data.obj.message);
}
return;
}
if (data.obj && data.obj.proccessType === "AdditionalProcessing") {
if (data.obj.TrialDataId) {
// 交易特批来源,优先使用后端新增的 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 currentTrialDataId = data.obj.TrialDataId;
var saveQuotaTrial = function (obj) {
var saveSuccess = true;
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
if (!res || !res.success) {
saveSuccess = false;
main.message(res.msg);
}
});
}
return saveSuccess;
};
var saveRiskWarningDecisionLog = function (quotaTrialId, decision) {
var saveSuccess = true;
main.post("/trade/SaveRiskWarningDecisionLog", { quotaTrialId: quotaTrialId, decision: decision }, { async: false }).done(function (res) {
if (!res || !res.success) {
saveSuccess = false;
main.message(res.msg);
}
});
return saveSuccess;
};
var layerSetting = {
type: 2,
title: "提示",
@@ -70,19 +138,30 @@ var confirmFunc = function (id, additionalProcessing) {
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
yes: function (index) {
var obj = window["layui-layer-iframe" + index].page;
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
if (!res || !res.success) {
main.message(res.msg);
}
});
layer.close(layerIndex);
confirmFunc(id, "LackOfMoney");
if (!obj.Data.Remark || obj.Data.Remark.length <= 0) {
main.message("必须填写说明内容,才可以录入交易");
return;
}
main.message("必须填写说明内容,才可以录入交易");
if (!saveQuotaTrial(obj)) {
return;
}
if (isRiskWarningConfirm) {
if (!saveRiskWarningDecisionLog(obj.Data.id, "确认通过")) {
return;
}
}
if (isOldRiskSpecialApproval) {
obj.Data.Remark = (obj.Data.Remark || "").trim();
}
layer.close(layerIndex);
confirmFunc(id, additionalProcessingType, currentIgnoreRiskRuleIds, currentTrialDataId);
},
cancel: function () {
var iframeWindow = window["layui-layer-iframe" + this.index];
var pageObj = iframeWindow && iframeWindow.page;
if (isRiskWarningConfirm && pageObj && pageObj.Data && pageObj.Data.id) {
saveRiskWarningDecisionLog(pageObj.Data.id, "取消不通过");
}
if (window.parent && window.parent.reloadtrade) {
window.parent.reloadtrade();
}
@@ -92,8 +171,11 @@ var confirmFunc = function (id, additionalProcessing) {
if (data.obj.type === "LackOfMoney") {
layerSetting.btn = [page.buttonStr, '取消'];
}
// 只展示"资金状况"的情况
if (data.obj.typecode === "FundStatus") {
else if (isRiskWarningConfirm) {
layerSetting.btn = ["交易特批", '取消'];
}
// 只展示“资金状况”的情况
else if (data.obj.typecode === "FundStatus") {
layerSetting.btn = ["确认", '取消'];
}
@@ -106,6 +188,38 @@ var confirmFunc = function (id, additionalProcessing) {
}
return;
}
if (data.msg && data.msg !== "操作完成") {
//新风控 ShowTip 提示:交易已确认成功,但触发提示规则,弹窗展示试算详情
if (data.obj && data.obj.TrialDataId) {
var showTipLayerSetting = {
type: 2,
title: "提示",
shadeClose: false,
shade: 0.4,
area: ['800px', '500px'],
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
yes: function (index) {
layer.close(layerIndex);
closetradeWindow();
},
cancel: function () {
if (window.parent && window.parent.reloadtrade) {
window.parent.reloadtrade();
}
layer.close(layerIndex);
closetradeWindow();
}
};
showTipLayerSetting.btn = ["关闭"];
layerIndex = layer.open(showTipLayerSetting);
}
else {
main.alert(data.msg, function () {
closetradeWindow();
});
}
return;
}
if (window.parent && window.parent.reloadtrade) {
window.parent.reloadtrade();
}
@@ -115,6 +229,8 @@ var confirmFunc = function (id, additionalProcessing) {
}
}
closetradeWindow();
}).fail(function (err) {
console.log("[swapTradeView.tradeConfirm] fail, err=", err);
});
};
function confirmTrade(id) {
@@ -197,7 +313,7 @@ function unWindSwap(id) {
main.open(title,
srcurl,
{
area: ["90%", "800px"],
area: ["90%", "780px"],
end: function () {
if (window.parent && window.parent.reloadtrade) {
window.parent.reloadtrade();
@@ -128,7 +128,7 @@ var confirmFunc = function (id, additionalProcessing) {
if (data.obj.type === "LackOfMoney") {
layerSetting.btn = [page.buttonStr, '取消'];
}
// 只展示"资金状况"的情况
// 只展示资金状况的情况
if (data.obj.typecode === "FundStatus") {
layerSetting.btn = ["确认", '取消'];
}