diff --git a/.editorconfig b/.editorconfig
index 40eb71f1..e2964ef3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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
diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs b/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs
new file mode 100644
index 00000000..c8d8ce8a
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs
@@ -0,0 +1,9 @@
+namespace YLErp.DBModels
+{
+ public enum RiskControlStrategy
+ {
+ Block = 1,
+ Approval = 2,
+ ShowTip = 3
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs b/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs
new file mode 100644
index 00000000..b4539183
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs
@@ -0,0 +1,9 @@
+namespace YLErp.DBModels
+{
+ public enum RiskRuleStatus
+ {
+ Active = 1,
+ Disabled = 2,
+ Deleted = 3
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs b/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs
new file mode 100644
index 00000000..28a9e859
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs
@@ -0,0 +1,10 @@
+namespace YLErp.DBModels
+{
+ public enum RiskVariableCategory
+ {
+ BookingElement = 1,
+ MarketData = 2,
+ SystemCalc = 3,
+ BooleanCheck = 4
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs b/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs
new file mode 100644
index 00000000..b5cd9b3d
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs
@@ -0,0 +1,9 @@
+namespace YLErp.DBModels
+{
+ public enum RiskVariableDataType
+ {
+ Numeric = 1,
+ Date = 2,
+ Boolean = 3
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/QuotaTrial.cs b/Framework/YLErp.Core/DBModels/QuotaTrial.cs
index 233f0d3a..c4e95df1 100644
--- a/Framework/YLErp.Core/DBModels/QuotaTrial.cs
+++ b/Framework/YLErp.Core/DBModels/QuotaTrial.cs
@@ -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
/// 1:确认成交时系统调用;
///
public int TrialSource { get; set; }
+
+ [NotMapped]
+ public List ApprovalRuleIds { get; set; } = new List();
}
}
diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs
new file mode 100644
index 00000000..8359f712
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace YLErp.DBModels
+{
+ ///
+ /// 风控规则定义表
+ ///
+ [Table("glms_risk_rule")]
+ public class glms_risk_rule : DBModelWithOperator
+ {
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ /// 结构化条件 JSON(变量+操作符+阈值列表)
+ public string ConditionJson { get; set; }
+ /// 规则执行表达式(C# 布尔表达式,引擎用 Roslyn 编译执行)
+ public string RuleExpr { get; set; }
+ /// 规则说明
+ public string Description { get; set; }
+ /// 规则状态
+ public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
+ /// 版本号(乐观锁)
+ public int Version { get; set; } = 1;
+ /// 最后更新人工号
+ public int? UpdateOptId { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime? UpdateDate { get; set; }
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs
new file mode 100644
index 00000000..89da2889
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs
@@ -0,0 +1,40 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace YLErp.DBModels
+{
+ ///
+ /// 风控规则应用配置表
+ ///
+ [Table("glms_risk_rule_application")]
+ public class glms_risk_rule_application : DBModelWithOperator
+ {
+ /// 关联规则 ID 列表(JSON 数组)
+ public string RuleIds { get; set; }
+ /// 应用说明
+ public string Description { get; set; }
+ /// 应用配置状态
+ public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
+ /// 风控策略(预警/阻断)
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表(JSON 数组)
+ public string TriggerPoints { get; set; }
+ /// 适用范围-资产账簿 ID 列表(JSON 数组)
+ public string ScopeAssetBookIds { get; set; }
+ /// 适用范围-客户 ID 列表(JSON 数组)
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表(JSON 数组)
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表(JSON 数组)
+ public string ScopeTradeTypes { get; set; }
+ /// 是否全局生效(0:否 1:是)
+ public int ScopeIsGlobal { get; set; }
+ /// 版本号(乐观锁)
+ public int Version { get; set; } = 1;
+ /// 最后更新人工号
+ public int? UpdateOptId { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime? UpdateDate { get; set; }
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs
new file mode 100644
index 00000000..0ffdd3fe
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace YLErp.DBModels
+{
+ ///
+ /// 风控操作审计日志表
+ ///
+ [Table("glms_risk_rule_audit_log")]
+ public class glms_risk_rule_audit_log : DBModelBase
+ {
+ /// 操作类型(创建/更新/删除/启用/禁用)
+ public string OperationType { get; set; }
+ /// 操作目标类型(Rule/Variable/Application)
+ public string TargetType { get; set; }
+ /// 操作目标 ID
+ public long TargetId { get; set; }
+ /// 操作目标名称
+ public string TargetName { get; set; }
+ /// 操作详情描述
+ public string OperationDetail { get; set; }
+ /// 操作结果(成功/失败)
+ public string Result { get; set; }
+ /// 操作前数据快照(JSON)
+ public string SnapshotData { get; set; }
+ /// 操作人工号
+ public long OptId { get; set; }
+ /// 操作人姓名
+ public string OptName { get; set; }
+ /// 操作时间
+ public DateTime OptDate { get; set; }
+ }
+}
diff --git a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs
new file mode 100644
index 00000000..2a856819
--- /dev/null
+++ b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace YLErp.DBModels
+{
+ ///
+ /// 风控变量池定义表
+ ///
+ [Table("glms_risk_variable")]
+ public class glms_risk_variable : DBModelWithOperator
+ {
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 值域描述
+ public string ValueDomain { get; set; }
+ /// 变量说明
+ public string Description { get; set; }
+ /// 取值表达式(C# 表达式,用于计算变量值)
+ public string VariableExpr { get; set; }
+ /// 版本号(乐观锁)
+ public int Version { get; set; } = 1;
+ /// 排序序号
+ public int SortOrder { get; set; }
+ /// 最后更新人工号
+ public int? UpdateOptId { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime? UpdateDate { get; set; }
+ }
+}
diff --git a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs
index 18980071..2a3cd093 100644
--- a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs
+++ b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs
@@ -1,4 +1,4 @@
-using YLErp.Models;
+using YLErp.Models;
namespace YLErp.Enums
{
@@ -143,6 +143,11 @@ namespace YLErp.Enums
[Description("预警")]
Warning,
///
+ /// 风控需审批
+ ///
+ [Description("风控需审批")]
+ RiskWarning,
+ ///
/// 不通过
///
[Description("不通过")]
diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql
new file mode 100644
index 00000000..448439e8
--- /dev/null
+++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql
@@ -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;
diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_applications.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_applications.sql
new file mode 100644
index 00000000..4573b7d0
--- /dev/null
+++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_applications.sql
@@ -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 = '接近/触发敲入敲出价';
diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_menu_risk_alert.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_menu_risk_alert.sql
new file mode 100644
index 00000000..68a57b0d
--- /dev/null
+++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_menu_risk_alert.sql
@@ -0,0 +1,50 @@
+-- ============================================================
+-- 异常交易监控页面 - 操作权限(按钮)数据
+-- 依赖:异常交易监控菜单 id=1077 (type=1) 需先插入
+-- 对应 FunctionRight.xml 风险控制下风控相关操作权限(共16个按钮)
+-- type: 0=目录 1=菜单 2=按钮
+-- ============================================================
+
+-- ----------------------------
+-- 1. sys_menu 页面+按钮(type=1,2)
+-- ----------------------------
+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);
diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_rules.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_rules.sql
new file mode 100644
index 00000000..9751a1d9
--- /dev/null
+++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_rules.sql
@@ -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) > 100000000(1亿元)
+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());
diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql
new file mode 100644
index 00000000..934a3a27
--- /dev/null
+++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql
@@ -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());
diff --git a/YLErpDAL/DataBase/YLContext.cs b/YLErpDAL/DataBase/YLContext.cs
index b6ed6e6d..78936a0c 100644
--- a/YLErpDAL/DataBase/YLContext.cs
+++ b/YLErpDAL/DataBase/YLContext.cs
@@ -408,5 +408,10 @@ namespace YLErp.BLL
public DbSet tradeContractOaResult { get; set; }
public DbSet bondPayment { get; set; }
+ public DbSet glms_risk_rule { get; set; }
+ public DbSet glms_risk_rule_application { get; set; }
+ public DbSet glms_risk_rule_audit_log { get; set; }
+ public DbSet glms_risk_variable { get; set; }
+
}
}
\ No newline at end of file
diff --git a/YLErpDAL/Helpers/StringHelper.cs b/YLErpDAL/Helpers/StringHelper.cs
index 411c0c11..62140735 100644
--- a/YLErpDAL/Helpers/StringHelper.cs
+++ b/YLErpDAL/Helpers/StringHelper.cs
@@ -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);
}
+
+ ///
+ /// 将逗号分隔的字符串转换为字符串数组,并去除空白项与重复项。
+ ///
+ /// 逗号分隔的字符串。
+ /// 去除空白项与重复项后的字符串数组。
+ 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();
+ }
}
}
diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs
new file mode 100644
index 00000000..65a88901
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompileResult.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 规则编译结果。
+ /// 用于承载公式校验、Roslyn 编译结果和错误信息。
+ ///
+ public class RuleCompileResult
+ {
+ ///
+ /// 是否编译成功。
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 编译失败时的错误信息。
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 编译成功后的可执行委托。
+ ///
+ public Func CompiledScript { get; set; }
+
+ ///
+ /// 创建成功结果。
+ ///
+ public static RuleCompileResult Ok(Func compiledScript)
+ {
+ return new RuleCompileResult
+ {
+ Success = true,
+ CompiledScript = compiledScript
+ };
+ }
+
+ ///
+ /// 创建失败结果。
+ ///
+ public static RuleCompileResult Fail(string errorMessage)
+ {
+ return new RuleCompileResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs
new file mode 100644
index 00000000..61fcd263
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiledCache.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Concurrent;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 规则编译结果的进程内缓存。
+ /// 当前第一版按规则 Id 缓存 Roslyn 编译后的可执行委托。
+ ///
+ public static class RuleCompiledCache
+ {
+ ///
+ /// 规则编译结果的进程内缓存。ConcurrentDictionary为线程安全的字典,用于缓存编译后的规则委托,性能略低。
+ ///
+ private static readonly ConcurrentDictionary> Cache
+ = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// 写入或更新规则编译结果。
+ ///
+ public static void Set(string ruleCode, Func compiledScript)
+ {
+ if (string.IsNullOrWhiteSpace(ruleCode) || compiledScript == null)
+ {
+ return;
+ }
+
+ Cache[ruleCode] = compiledScript;
+ }
+
+ ///
+ /// 尝试获取规则编译结果。
+ ///
+ public static bool TryGet(string ruleCode, out Func compiledScript)
+ {
+ compiledScript = null;
+
+ if (string.IsNullOrWhiteSpace(ruleCode))
+ {
+ return false;
+ }
+
+ return Cache.TryGetValue(ruleCode, out compiledScript);
+ }
+
+ ///
+ /// 删除指定规则的编译结果。
+ ///
+ public static void Remove(string ruleCode)
+ {
+ if (string.IsNullOrWhiteSpace(ruleCode))
+ {
+ return;
+ }
+
+ Cache.TryRemove(ruleCode, out _);
+ }
+
+ ///
+ /// 清空全部规则编译结果。
+ ///
+ public static void Clear()
+ {
+ Cache.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
new file mode 100644
index 00000000..6d48174c
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
@@ -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
+{
+ ///
+ /// Roslyn 脚本全局变量容器
+ ///
+ public class ScriptGlobals
+ {
+ ///
+ /// 当前交易ID
+ ///
+ public int TradeId { get; set; }
+
+ ///
+ /// 当前触发时点
+ ///
+ public string TriggerPoint { get; set; }
+
+ ///
+ /// 数据库上下文,供规则公式直接查询数据库
+ ///
+ public YLContext DbContext { get; set; }
+ }
+
+ ///
+ /// 规则编译器(Roslyn 版)
+ ///
+ /// 编译时机:规则定义时(零运行时解析开销)
+ /// 执行方式:判风控时直接调用 rule.CompiledScript(context) → bool
+ ///
+ /// 相比表达式树方案的优势:
+ /// - 代码简洁:生成 C# 字符串即可,无需手动拼接 Expression 节点
+ /// - 灵活性高:天然支持多条件 AND/OR、复杂运算、未来可扩展任意 C# 语法
+ /// - 可调试:生成的脚本代码可直接阅读和理解
+ ///
+ /// 示例:
+ /// RuleExpr: Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).StockEqvNotional) > 100000000m
+ /// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存)
+ ///
+ public static class RuleCompiler
+ {
+ static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
+
+ ///
+ /// 校验并编译规则表达式。
+ /// 当前 RuleExpr 要求是 Roslyn 可直接执行的 bool 表达式。
+ /// ruleId 可为空,用于规则未落库时的公式校验。
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 校验并编译规则,编译成功后同时写入规则对象和内存缓存。
+ ///
+ 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;
+ }
+
+ ///
+ /// 用 Roslyn 编译 C# 脚本代码为可执行委托
+ ///
+ private static Func 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(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
+ 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
+ };
+ }
+
+ ///
+ /// 将操作符统一为 C# 操作符
+ ///
+ 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
+ };
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs
new file mode 100644
index 00000000..7d9dccb1
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs
@@ -0,0 +1,17 @@
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 批量操作结果
+ ///
+ public class BatchOperationResult
+ {
+ /// 是否全部成功
+ public bool Success { get; set; }
+ /// 总数量
+ public int TotalCount { get; set; }
+ /// 成功数量
+ public int SuccessCount { get; set; }
+ /// 错误信息
+ public string ErrorMessage { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs
new file mode 100644
index 00000000..b426ea1c
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs
@@ -0,0 +1,29 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 创建应用配置请求
+ ///
+ public class CreateRiskApplicationReq
+ {
+ /// 关联规则 ID 列表(JSON 数组)
+ public string RuleIds { get; set; }
+ /// 应用说明
+ public string Description { get; set; }
+ /// 风控策略
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表(JSON 数组)
+ public string TriggerPoints { get; set; }
+ /// 适用范围-资产账簿 ID 列表
+ public string ScopeAssetBookIds { get; set; }
+ /// 适用范围-客户 ID 列表
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表
+ public string ScopeTradeTypes { get; set; }
+ /// 是否全局生效
+ public int ScopeIsGlobal { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs
new file mode 100644
index 00000000..280c3e17
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs
@@ -0,0 +1,19 @@
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 创建风控规则请求
+ ///
+ public class CreateRiskRuleReq
+ {
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ /// 结构化条件 JSON
+ public string ConditionJson { get; set; }
+ /// 规则执行表达式
+ public string RuleExpr { get; set; }
+ /// 规则说明
+ public string Description { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs
new file mode 100644
index 00000000..e8bff83a
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs
@@ -0,0 +1,27 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 创建变量请求
+ ///
+ public class CreateRiskVariableReq
+ {
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 值域描述
+ public string ValueDomain { get; set; }
+ /// 变量说明
+ public string Description { get; set; }
+ /// 取值表达式
+ public string VariableExpr { get; set; }
+ /// 排序序号
+ public int SortOrder { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs
new file mode 100644
index 00000000..0a501867
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs
@@ -0,0 +1,28 @@
+using BaseOUDAL;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 查询应用配置请求
+ ///
+ public class QueryRiskApplicationReq : BaseSearchReq
+ {
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 应用配置状态
+ public RiskRuleStatus? Status { get; set; }
+ /// 风控策略
+ public RiskControlStrategy? Strategy { get; set; }
+ /// 触发点
+ public string TriggerPoint { get; set; }
+ /// 适用范围-客户 ID 列表
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表
+ public string ScopeTradeTypes { get; set; }
+ /// 适用范围-资产账簿 ID 列表
+ public string ScopeAssetBookIds { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs
new file mode 100644
index 00000000..d1f9d242
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs
@@ -0,0 +1,28 @@
+using System;
+using BaseOUDAL;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 查询审计日志请求
+ ///
+ public class QueryRiskAuditLogReq : BaseSearchReq
+ {
+ /// 操作类型
+ public string OperationType { get; set; }
+ /// 操作类型列表(多选,逗号分隔)
+ public string OperationTypes { get; set; }
+ /// 操作目标类型
+ public string TargetType { get; set; }
+ /// 操作目标名称
+ public string TargetName { get; set; }
+ /// 操作人姓名
+ public string OptName { get; set; }
+ /// 开始日期
+ public DateTime? StartDate { get; set; }
+ /// 结束日期
+ public DateTime? EndDate { get; set; }
+ /// 搜索关键词
+ public string Keyword { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs
new file mode 100644
index 00000000..8339b900
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs
@@ -0,0 +1,18 @@
+using BaseOUDAL;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 查询风控规则请求
+ ///
+ public class QueryRiskRuleReq : BaseSearchReq
+ {
+ /// 搜索关键词
+ public string Keyword { get; set; }
+ /// 规则状态
+ public RiskRuleStatus? Status { get; set; }
+ /// 关联变量 ID
+ public long? VariableId { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs
new file mode 100644
index 00000000..ed4b9901
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs
@@ -0,0 +1,18 @@
+using BaseOUDAL;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 查询变量请求
+ ///
+ public class QueryRiskVariableReq : BaseSearchReq
+ {
+ /// 变量分类
+ public RiskVariableCategory? Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType? DataType { get; set; }
+ /// 变量名称
+ public string VariableName { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs
new file mode 100644
index 00000000..11d5cf7f
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs
@@ -0,0 +1,44 @@
+using System;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 应用配置详情响应
+ ///
+ public class RiskApplicationDetail
+ {
+ /// 应用配置 ID
+ public long Id { get; set; }
+ /// 关联规则 ID 列表(JSON 数组)
+ public string RuleIds { get; set; }
+ /// 应用说明
+ public string Description { get; set; }
+ /// 应用配置状态
+ public RiskRuleStatus Status { get; set; }
+ /// 风控策略
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表(JSON 数组)
+ public string TriggerPoints { get; set; }
+ /// 适用范围-资产账簿 ID 列表
+ public string ScopeAssetBookIds { get; set; }
+ /// 适用范围-客户 ID 列表
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表
+ public string ScopeTradeTypes { get; set; }
+ /// 是否全局生效
+ public int ScopeIsGlobal { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 创建人姓名
+ public string OptName { get; set; }
+ /// 创建时间
+ public DateTime OptDate { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime UpdateDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs
new file mode 100644
index 00000000..f50283ed
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs
@@ -0,0 +1,44 @@
+using System;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 应用配置列表项
+ ///
+ public class RiskApplicationListItem
+ {
+ /// 应用配置 ID
+ public long Id { get; set; }
+ /// 关联规则 ID 列表(JSON 数组)
+ public string RuleIds { get; set; }
+ /// 应用说明
+ public string Description { get; set; }
+ /// 应用配置状态
+ public int Status { get; set; }
+ /// 风控策略
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表(JSON 数组)
+ public string TriggerPoints { get; set; }
+ /// 适用范围-资产账簿 ID 列表
+ public string ScopeAssetBookIds { get; set; }
+ /// 适用范围-客户 ID 列表
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表
+ public string ScopeTradeTypes { get; set; }
+ /// 是否全局生效
+ public int ScopeIsGlobal { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 创建人姓名
+ public string OptName { get; set; }
+ /// 创建时间
+ public DateTime OptDate { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime UpdateDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs
new file mode 100644
index 00000000..e9418ec4
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 审计日志详情响应
+ ///
+ public class RiskAuditLogDetail
+ {
+ /// 日志 ID
+ public long Id { get; set; }
+ /// 操作类型
+ public string OperationType { get; set; }
+ /// 操作目标类型
+ public string TargetType { get; set; }
+ /// 操作目标 ID
+ public long TargetId { get; set; }
+ /// 操作目标名称
+ public string TargetName { get; set; }
+ /// 操作详情描述
+ public string OperationDetail { get; set; }
+ /// 操作结果
+ public string Result { get; set; }
+ /// 操作前数据快照(JSON)
+ public string SnapshotData { get; set; }
+ /// 操作人工号
+ public long OptId { get; set; }
+ /// 操作人姓名
+ public string OptName { get; set; }
+ /// 操作时间
+ public DateTime OptDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs
new file mode 100644
index 00000000..3e2288ab
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 审计日志列表项
+ ///
+ public class RiskAuditLogListItem
+ {
+ /// 日志 ID
+ public long Id { get; set; }
+ /// 操作类型
+ public string OperationType { get; set; }
+ /// 操作目标类型
+ public string TargetType { get; set; }
+ /// 操作目标 ID
+ public long TargetId { get; set; }
+ /// 操作目标名称
+ public string TargetName { get; set; }
+ /// 操作详情描述
+ public string OperationDetail { get; set; }
+ /// 操作结果
+ public string Result { get; set; }
+ /// 操作人姓名
+ public string OptName { get; set; }
+ /// 操作时间
+ public DateTime OptDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs
new file mode 100644
index 00000000..da6b37e4
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs
@@ -0,0 +1,19 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 规则关联应用摘要
+ ///
+ public class RiskRuleApplicationSummary
+ {
+ /// 应用配置 ID
+ public long Id { get; set; }
+ /// 应用配置状态
+ public RiskRuleStatus Status { get; set; }
+ /// 风控策略
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表
+ public string TriggerPoints { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs
new file mode 100644
index 00000000..cdb2e9ec
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 风控规则详情响应
+ ///
+ public class RiskRuleDetail
+ {
+ /// 规则 ID
+ public long Id { get; set; }
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ /// 结构化条件 JSON
+ public string ConditionJson { get; set; }
+ /// 规则执行表达式
+ public string RuleExpr { get; set; }
+ /// 规则说明
+ public string Description { get; set; }
+ /// 规则状态
+ public RiskRuleStatus Status { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 创建人姓名
+ public string OptName { get; set; }
+ /// 创建时间
+ public DateTime OptDate { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime UpdateDate { get; set; }
+ /// 关联应用列表
+ public List Applications { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs
new file mode 100644
index 00000000..b35999a2
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs
@@ -0,0 +1,38 @@
+using System;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 风控规则列表项
+ ///
+ public class RiskRuleListItem
+ {
+ /// 规则 ID
+ public long Id { get; set; }
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ /// 结构化条件 JSON
+ public string ConditionJson { get; set; }
+ /// 规则执行表达式
+ public string RuleExpr { get; set; }
+ /// 规则说明
+ public string Description { get; set; }
+ /// 规则状态
+ public RiskRuleStatus Status { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 创建人姓名
+ public string OptName { get; set; }
+ /// 创建时间
+ public DateTime OptDate { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime UpdateDate { get; set; }
+ /// 关联应用数量
+ public int ApplicationCount { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleSimpleItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleSimpleItem.cs
new file mode 100644
index 00000000..d2d98568
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleSimpleItem.cs
@@ -0,0 +1,15 @@
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 风控规则简要信息
+ ///
+ public class RiskRuleSimpleItem
+ {
+ /// 规则 ID
+ public long Id { get; set; }
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs
new file mode 100644
index 00000000..0e15cd14
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 风控规则版本历史项
+ ///
+ public class RiskRuleVersionItem
+ {
+ /// 版本号
+ public int Version { get; set; }
+ /// 操作类型
+ public string OperationType { get; set; }
+ /// 操作详情
+ public string OperationDetail { get; set; }
+ /// 操作人姓名
+ public string OptName { get; set; }
+ /// 操作时间
+ public DateTime OptDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs
new file mode 100644
index 00000000..c3dee5a8
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs
@@ -0,0 +1,42 @@
+using System;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 变量详情响应
+ ///
+ public class RiskVariableDetail
+ {
+ /// 变量 ID
+ public long Id { get; set; }
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 值域描述
+ public string ValueDomain { get; set; }
+ /// 变量说明
+ public string Description { get; set; }
+ /// 取值表达式
+ public string VariableExpr { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 排序序号
+ public int SortOrder { get; set; }
+ /// 创建人姓名
+ public string OptName { get; set; }
+ /// 创建时间
+ public DateTime OptDate { get; set; }
+ /// 最后更新人姓名
+ public string UpdateOptName { get; set; }
+ /// 最后更新时间
+ public DateTime UpdateDate { get; set; }
+ /// 被引用次数
+ public int ReferenceCount { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs
new file mode 100644
index 00000000..538cdcb9
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs
@@ -0,0 +1,31 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 变量列表项
+ ///
+ public class RiskVariableListItem
+ {
+ /// 变量 ID
+ public long Id { get; set; }
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 值域描述
+ public string ValueDomain { get; set; }
+ /// 变量说明
+ public string Description { get; set; }
+ /// 取值表达式
+ public string VariableExpr { get; set; }
+ /// 版本号
+ public int Version { get; set; }
+ /// 排序序号
+ public int SortOrder { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs
new file mode 100644
index 00000000..f884a03e
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs
@@ -0,0 +1,23 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 变量简要信息
+ ///
+ public class RiskVariableSimpleItem
+ {
+ /// 变量 ID
+ public long Id { get; set; }
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 取值表达式
+ public string VariableExpr { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RuleCondition.cs b/YLErpDAL/Modules/RiskEngine/Dto/RuleCondition.cs
new file mode 100644
index 00000000..071be483
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/RuleCondition.cs
@@ -0,0 +1,23 @@
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 规则条件项
+ ///
+ public class RuleCondition
+ {
+ /// 条件变量 ID
+ public long VariableId { get; set; }
+ /// 操作符(如 Equal、GreaterThan、Between 等)
+ public string Operator { get; set; }
+ /// 阈值类型(FixedValue / VariableRef)
+ public string ThresholdType { get; set; }
+ /// 阈值(固定值或变量引用)
+ public object Value { get; set; }
+ /// 阈值变量 ID(当 ThresholdType 为 VariableRef 时)
+ public long? ThresholdVariableId { get; set; }
+ /// 介于/不介于的下边界是否包含,默认 true
+ public bool IncludeLowerBound { get; set; } = true;
+ /// 介于/不介于的上边界是否包含,默认 true
+ public bool IncludeUpperBound { get; set; } = true;
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs
new file mode 100644
index 00000000..877639d0
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs
@@ -0,0 +1,31 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 更新应用配置请求
+ ///
+ public class UpdateRiskApplicationReq
+ {
+ /// 关联规则 ID 列表(JSON 数组)
+ public string RuleIds { get; set; }
+ /// 应用说明
+ public string Description { get; set; }
+ /// 风控策略
+ public RiskControlStrategy ControlStrategy { get; set; }
+ /// 触发点列表(JSON 数组)
+ public string TriggerPoints { get; set; }
+ /// 适用范围-资产账簿 ID 列表
+ public string ScopeAssetBookIds { get; set; }
+ /// 适用范围-客户 ID 列表
+ public string ScopeClientIds { get; set; }
+ /// 适用范围-标的类型列表
+ public string ScopeUnderlyingTypes { get; set; }
+ /// 适用范围-交易类型列表
+ public string ScopeTradeTypes { get; set; }
+ /// 是否全局生效
+ public int ScopeIsGlobal { get; set; }
+ /// 版本号(乐观锁)
+ public int Version { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs
new file mode 100644
index 00000000..0d19a313
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs
@@ -0,0 +1,21 @@
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 更新风控规则请求
+ ///
+ public class UpdateRiskRuleReq
+ {
+ /// 规则名称
+ public string RuleName { get; set; }
+ /// 规则描述文本
+ public string RuleText { get; set; }
+ /// 结构化条件 JSON
+ public string ConditionJson { get; set; }
+ /// 规则执行表达式
+ public string RuleExpr { get; set; }
+ /// 规则说明
+ public string Description { get; set; }
+ /// 版本号(乐观锁)
+ public int Version { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs
new file mode 100644
index 00000000..c9ca1b3a
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs
@@ -0,0 +1,29 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine.Dto
+{
+ ///
+ /// 更新变量请求
+ ///
+ public class UpdateRiskVariableReq
+ {
+ /// 变量名称
+ public string VariableName { get; set; }
+ /// 变量分类
+ public RiskVariableCategory Category { get; set; }
+ /// 数据类型
+ public RiskVariableDataType DataType { get; set; }
+ /// 单位
+ public string Unit { get; set; }
+ /// 值域描述
+ public string ValueDomain { get; set; }
+ /// 变量说明
+ public string Description { get; set; }
+ /// 取值表达式
+ public string VariableExpr { get; set; }
+ /// 排序序号
+ public int SortOrder { get; set; }
+ /// 版本号(乐观锁)
+ public int Version { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskContext.cs b/YLErpDAL/Modules/RiskEngine/RiskContext.cs
new file mode 100644
index 00000000..9686f980
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskContext.cs
@@ -0,0 +1,25 @@
+using YLErp.BLL;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控上下文:封装一次风控检查所需的全部数据上下文
+ ///
+ public class RiskContext
+ {
+ ///
+ /// 当前交易ID
+ ///
+ public int TradeId { get; set; }
+
+ ///
+ /// 当前触发时点
+ ///
+ public string TriggerPoint { get; set; }
+
+ ///
+ /// 数据库上下文,供规则公式直接查询数据库
+ ///
+ public YLContext DbContext { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
new file mode 100644
index 00000000..efd8b91d
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
@@ -0,0 +1,1013 @@
+using Newtonsoft.Json;
+using Qdp.Foundation.Utilities;
+using System.Reflection;
+using YLErp.BLL;
+using YLErp.Model;
+
+/*
+================================================================================
+ 风控引擎服务 — RiskEngineService 技术方案与当前实现说明
+================================================================================
+
+【项目背景】
+ 当前 TRS 系统已在 QuotaMonitorService 中接入第一版风控引擎,用于在交易关键
+ 时点执行可配置规则判断。当前目标不是一次性做成最终版,而是在最小可运行链路
+ 跑通后,逐步演进为可由前端配置、后端预编译、执行期直接命中的正式版本。
+
+【当前实现口径】(以本注释和实际代码为准)
+
+ 核心概念:
+ - RiskRule(风控规则):规则定义本体,当前重点字段包括 Id、RuleName、
+ RuleText、RuleExpr、Version、CompiledScript。
+ - RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
+ 适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
+ - RuleExpr:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
+ 例如通过 DbContext 和 TradeId 查询数据库后做数值或日期比较。
+ - ConditionJson:当前已在规则定义中保留,但不参与主执行链路。
+ - RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DbContext。
+ - RuleCompiledCache:进程内编译结果缓存,按规则 Id 缓存 Func。
+
+ 当前编译流程:
+ 1. 外部准备好规则对象,在 RuleExpr 中直接写最终可执行表达式
+ 2. 调用 RuleCompiler.ValidateAndCompileRule(rule)
+ 3. 内部使用 Roslyn 编译 RuleExpr,生成 Func
+ 4. 编译成功后写入 rule.CompiledScript,并同步写入 RuleCompiledCache
+ 5. 执行阶段优先从 RuleCompiledCache 取委托执行
+
+ 当前执行流程(EvaluateRisk):
+ 1. 从数据库加载规则列表和应用列表
+ 2. 按应用状态、TriggerPoints 过滤有效应用
+ 3. 按应用范围过滤交易是否命中(全局 / 账户 / 客户 / 标的类型 / 合约类型)
+ 4. 根据应用配置中的 RuleIds 找到对应规则 Id 并关联规则
+ 5. 优先从 RuleCompiledCache 读取已编译委托,未命中时兜底编译一次
+ 6. 执行规则委托,按 ControlStrategy 聚合为 Blocked / NeedApproval / Warnings
+ 7. 返回 RiskResult
+
+ 当前维度匹配规则:
+ - 全局:ScopeIsGlobal=true 时直接命中
+ - 同一维度内多选:并集(OR)
+ - 不同维度之间:交集(AND)
+ - 某维度留空:视为该维度不限制
+
+【与早期方案的主要差异】
+ - 当前不是 Content 解析或表达式树主导,而是 RuleExpr 直编译主导
+ - RiskRule 与 RiskRuleApplication 当前仍是分离模型,没有合并
+ - 编译器文件已放入 RiskEngine/Compile 目录下
+ - 当前已引入 RuleCompileResult、RuleCompiledCache,用于校验结果与进程内缓存
+ - Rule2 这类“左值与右值都来自对象字段”的规则,当前通过 RuleExpr 直接表达
+
+【当前代码结构】
+ 风控引擎层:YLErpDAL/Modules/RiskEngine/
+ - RiskEngineService.cs:执行入口,负责规则筛选、应用过滤、委托执行、结果聚合
+ - RiskRule.cs:规则定义模型,含 CompiledScript 运行时字段
+ - RiskRuleApplication.cs:规则应用模型,承载策略、触发点和适用范围
+ - RiskContext.cs / RiskResult.cs:执行上下文与结果模型
+
+ 编译相关:YLErpDAL/Modules/RiskEngine/Compile/
+ - RuleCompiler.cs:Roslyn 编译入口,提供 ValidateAndCompileFormula / ValidateAndCompileRule
+ - RuleCompileResult.cs:编译结果模型
+ - RuleCompiledCache.cs:编译结果缓存
+
+ 当前集成点:
+ - QuotaMonitorService.cs:构造 RiskContext,并传入当前 TradeId
+
+【当前进度】(截至 2026-06-24)
+ ✅ 已完成:
+ 1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
+ 2. 风控上下文提供 DbContext 和 TradeId,支持规则脚本直接查询数据库
+ 3. RuleExpr 直编译方案已接入,支持数值比较和日期比较
+ 4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
+ 5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
+ 6. RuleCompiler 已支持校验 + 编译 + 写缓存
+ 7. RuleCompiledCache / RuleCompileResult 已落地到 Compile 目录
+ 8. RiskEngineService 已改为执行时优先从 RuleCompiledCache 读取委托
+
+【待办事项 / TODO】
+ ⬜ 1. 接入真实规则来源
+ - 由数据库或前端提交替换当前 LoadRules / LoadApplications 的规则来源
+ ⬜ 2. 增加接口层
+ - 提供前端提交 RuleExpr 后的校验接口
+ - 提供规则保存 / 发布后预编译并写缓存的入口
+ ⬜ 3. 启动预热与缓存刷新
+ - 服务启动时批量加载有效规则并预编译
+ - 支持规则更新后的缓存刷新 / 删除
+ ⬜ 4. 完善执行期策略
+ - 当前保留“缓存未命中时兜底编译”逻辑
+ - 后续可收紧为“执行期只读缓存,未命中按发布失败处理”
+ ⬜ 5. 补测试样例
+ - 数值比较、日期比较、维度过滤、非法公式、边界值
+ ⬜ 6. 完善 RuleExpr 配套能力
+ - 提供前端可用的表达式编辑、校验与错误提示
+ - 约束可用变量、类型转换和脚本安全边界
+
+【注意事项】
+ 1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
+ 2. RuleExpr 可直接使用 DbContext 查询数据库,当前交易通过 TradeId 定位
+ 3. 当前缓存的是 Func,这是规则判断函数,不是事件处理器
+ 4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
+================================================================================
+*/
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控引擎服务(第一版 — 骨架版,先跑通)
+ ///
+ public class RiskEngineService : YLBaseService
+ {
+ IYcLogger _logger = LogFactory.GetLogger("RiskEngineService");
+
+ private static readonly Lazy _instance =
+ new Lazy(() => new RiskEngineService());
+
+ public static RiskEngineService GetInstance() => _instance.Value;
+
+ private RiskEngineService() : base(OptUserInfo.SystemUser)
+ {
+ }
+
+ public RiskEngineService(OptUserInfo userInfo) : base(userInfo)
+ {
+ }
+
+ public RiskEngineService(YLBaseService baseService) : base(baseService)
+ {
+ }
+
+ public RiskEngineService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext)
+ {
+ }
+
+ ///
+ /// 规则内存缓存(启动预热写入,EvaluateRisk 读取)
+ ///
+ private static volatile List _cachedRules;
+ private static volatile List _cachedApplications;
+ private static readonly object _cacheLock = new object();
+
+ ///
+ /// 预热:加载规则与应用到内存,并预编译所有规则到 RuleCompiledCache。
+ /// 项目启动时调用一次;规则/应用更新后调用 RefreshCache 刷新。
+ ///
+ public void Preload()
+ {
+ lock (_cacheLock)
+ {
+ _logger.Info("[风控引擎] Preload 开始 - 加载规则与应用并预编译");
+
+ var rules = LoadRulesFromDb();
+ var applications = LoadApplicationsFromDb();
+ // 预编译所有规则到 RuleCompiledCache
+ foreach (var rule in rules)
+ {
+ var ruleId = rule.Id.ToString();
+ //非活跃的rule不编译
+ if (rule.Status != RiskRuleStatus.Active)
+ {
+ RuleCompiledCache.Remove(rule.Id.ToString());
+ _logger.Info($"[风控引擎] 规则非活跃,已移除预编译缓存 - RuleId: {rule.Id}, Status: {rule.Status}");
+ continue;
+ }
+
+
+ var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
+ if (compileResult.Success)
+ {
+ _logger.Info($"[风控引擎] 规则预编译成功 - RuleId: {rule.Id}");
+ }
+ else
+ {
+ _logger.Info($"[风控引擎] 规则预编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
+ }
+ }
+
+ _cachedRules = rules;
+ _cachedApplications = applications;
+
+ _logger.Info($"[风控引擎] Preload 完成 - 规则数: {rules.Count}, 应用数: {applications.Count}");
+ }
+ }
+
+ ///
+ /// 刷新缓存:刷新规则与应用内存缓存,预编译成功时覆盖旧编译缓存。
+ /// 规则/应用配置更新后调用。
+ ///
+ public void RefreshCache()
+ {
+ _logger.Info("[风控引擎] RefreshCache 被调用 - 刷新规则与应用缓存并重新预加载");
+ lock (_cacheLock)
+ {
+ _cachedRules = null;
+ _cachedApplications = null;
+ RuleCompiledCache.Clear();
+ }
+
+ Preload();
+ }
+
+ ///
+ /// 刷新单条规则缓存:重新加载规则列表,只编译指定规则。
+ ///
+ public RuleCompileResult RefreshOneRuleCache(long ruleId)
+ {
+ lock (_cacheLock)
+ {
+ _logger.Info($"[风控引擎] RefreshOneRuleCache 开始 - RuleId: {ruleId}");
+ // 从数据库加载最新规则列表
+ _cachedRules = LoadRulesFromDb();
+
+ var rule = _cachedRules.FirstOrDefault(r => r.Id == ruleId);
+ if (rule == null)
+ {
+ RuleCompiledCache.Remove(ruleId.ToString());
+ _logger.Info($"[风控引擎] RefreshOneRuleCache 未找到规则,已移除缓存 - RuleId: {ruleId}");
+ return RuleCompileResult.Fail("规则不存在或已删除");
+ }
+
+ if (rule.Status != RiskRuleStatus.Active)
+ {
+ RuleCompiledCache.Remove(ruleId.ToString());
+ _logger.Info($"[风控引擎] RefreshOneRuleCache 规则非活跃,已移除缓存 - RuleId: {ruleId}, Status: {rule.Status}");
+ return RuleCompileResult.Fail("规则不存在或非活跃状态");
+ }
+
+ var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
+ _logger.Info($"[风控引擎] RefreshOneRuleCache 完成 - RuleId: {ruleId}, Success: {compileResult.Success}, Error: {compileResult.ErrorMessage}");
+ return compileResult;
+ }
+ }
+
+ ///
+ /// 仅刷新规则应用列表,不清空规则编译缓存。
+ ///
+ public void RefreshApplication()
+ {
+ lock (_cacheLock)
+ {
+ _cachedApplications = LoadApplicationsFromDb();
+ _logger.Info($"[风控引擎] RefreshApplication 完成 - 应用数: {_cachedApplications.Count}");
+ }
+ }
+
+ ///
+ /// 从内存缓存获取规则列表;缓存为空时兜底加载并填充缓存。
+ ///
+ private List GetRules()
+ {
+ var rules = _cachedRules;
+ if (rules != null)
+ {
+ return rules;
+ }
+
+ lock (_cacheLock)
+ {
+ if (_cachedRules != null)
+ {
+ return _cachedRules;
+ }
+
+ var loaded = LoadRulesFromDb();
+ _cachedRules = loaded;
+ return loaded;
+ }
+ }
+
+ ///
+ /// 从内存缓存获取应用列表;缓存为空时兜底加载并填充缓存。
+ ///
+ private List GetApplications()
+ {
+ var applications = _cachedApplications;
+ if (applications != null)
+ {
+ return applications;
+ }
+
+ lock (_cacheLock)
+ {
+ if (_cachedApplications != null)
+ {
+ return _cachedApplications;
+ }
+
+ var loaded = LoadApplicationsFromDb();
+ _cachedApplications = loaded;
+ return loaded;
+ }
+ }
+
+ ///
+ /// 执行风控检查(最终设计版:预编译委托 + 规则筛选 + 策略判定)
+ ///
+ /// 风控上下文
+ /// 触发时点,如 BOOK_CONFIRM
+ /// 风控结果
+ public RiskResult EvaluateRisk(RiskContext context, string triggerPoint)
+ {
+ var result = new RiskResult();
+
+ try
+ {
+ _logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
+ if (context != null && context.DbContext == null)
+ {
+ context.DbContext = DbContext;
+ }
+
+ // ============================================================
+ // Step 1: 从内存缓存读取规则定义和规则应用(启动时已预热)
+ // ============================================================
+ var rules = GetRules();
+ var applications = GetApplications();
+ _logger.Info($"[风控引擎] 加载规则数: {rules.Count}, 应用数: {applications.Count}");
+
+ // ============================================================
+ // Step 2: 先按 Application 过滤 Active 状态和 TriggerPoint 匹配的应用
+ // ============================================================
+ var activeApps = applications
+ .Where(a => a.Status == RiskRuleStatus.Active)
+ .ToList();
+ var apps = activeApps
+ .Where(a => !string.IsNullOrEmpty(a.TriggerPoints))
+ .ToList();
+ var triggerMatchedApps = apps
+ .Where(a => a.TriggerPoints.Split(',', StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim())
+ .Contains(triggerPoint))
+ .ToList();
+
+ // 再按应用范围过滤。
+ // 统一走通用维度匹配:
+ // 1. 全局命中时直接通过;
+ // 2. 同一维度内多选按并集处理;
+ // 3. 不同维度之间按交集处理;
+ // 4. 某维度留空表示该维度不限制。
+ var trade = context != null && context.TradeId > 0
+ ? DbContext.trade.FirstOrDefault(t => t.id == context.TradeId)
+ : null;
+
+ var matchedApplications = triggerMatchedApps
+ .Where(a => IsApplicationMatched(a, trade))
+ .ToList();
+
+ _logger.Info($"[风控引擎] 匹配 TriggerPoint 的应用数: {matchedApplications.Count}");
+
+ // ============================================================
+ // Step 3: 遍历匹配的应用,通过 RuleIds 关联规则并执行预编译委托
+ // ============================================================
+ foreach (var application in matchedApplications)
+ {
+ // ------------------------------------------------------------
+ // 3.1 通过 RuleIds 关联规则定义
+ // ------------------------------------------------------------
+ var applicationRuleIds = ParseRuleIds(application.RuleIds);
+ if (!applicationRuleIds.Any())
+ {
+ _logger.Info($"[风控引擎] 应用未配置有效规则 - RuleIds: {application.RuleIds}");
+ continue;
+ }
+
+ var applicationRules = rules
+ .Where(r => applicationRuleIds.Contains(r.Id) && r.Status == RiskRuleStatus.Active)
+ .ToList();
+
+ // 区分"规则不存在"与"规则非活跃"两种情况,分别记录日志
+ var ruleDict = rules.Where(r => applicationRuleIds.Contains(r.Id))
+ .ToDictionary(r => r.Id);
+
+ foreach (var ruleId in applicationRuleIds)
+ {
+ if (!ruleDict.TryGetValue(ruleId, out var ruleDef))
+ {
+ _logger.Info($"[风控引擎] 未找到对应规则定义 - RuleId: {ruleId}, ApplicationRuleIds: {application.RuleIds}");
+ }
+ else if (ruleDef.Status != RiskRuleStatus.Active)
+ {
+ _logger.Info($"[风控引擎] 规则非活跃,跳过执行 - RuleId: {ruleId}, Status: {ruleDef.Status}, ApplicationRuleIds: {application.RuleIds}");
+ }
+ }
+
+ foreach (var rule in applicationRules)
+ {
+ var ruleId = rule.Id.ToString();
+
+ // ------------------------------------------------------------
+ // 3.2 优先从编译缓存读取规则委托
+ // ------------------------------------------------------------
+ if (!RuleCompiledCache.TryGet(ruleId, out var compiledScript))
+ {
+ _logger.Info($"[风控引擎] 缓存中未命中已编译规则,执行兜底编译 - RuleId: {rule.Id}");
+
+ var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
+ if (!compileResult.Success)
+ {
+ _logger.Info($"[风控引擎] 规则编译失败 - RuleId: {rule.Id}, Error: {compileResult.ErrorMessage}");
+ continue;
+ }
+
+ compiledScript = compileResult.CompiledScript;
+ }
+
+ // ------------------------------------------------------------
+ // 3.3 执行预编译委托
+ // ------------------------------------------------------------
+ bool triggered = false;
+ try
+ {
+ triggered = compiledScript(context);
+ _logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
+ }
+ catch (Exception ex)
+ {
+ _logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
+ continue;
+ }
+
+ // ------------------------------------------------------------
+ // 3.4 命中后按 Application 的控制策略聚合结果
+ // ------------------------------------------------------------
+ if (triggered)
+ {
+ _logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {application.ControlStrategy}");
+
+ switch (application.ControlStrategy)
+ {
+ case RiskControlStrategy.Block:
+ result.Blocked = true;
+ result.Passed = false;
+ result.TriggeredRules.Add(new TriggeredRuleInfo
+ {
+ RuleId = rule.Id.ToString(),
+ RuleName = rule.RuleName,
+ ControlStrategy = RiskControlStrategy.Block,
+ RuleText = rule.RuleText,
+ Message = $"规则[{rule.RuleName}]触发:禁止"
+ });
+ break;
+
+ case RiskControlStrategy.Approval:
+ result.NeedApproval = true;
+ result.Passed = false;
+ result.TriggeredRules.Add(new TriggeredRuleInfo
+ {
+ RuleId = ruleId,
+ RuleName = rule.RuleName,
+ ControlStrategy = RiskControlStrategy.Approval,
+ RuleText = rule.RuleText,
+ Message = $"规则[{rule.RuleName}]触发:需审批"
+ });
+ break;
+
+ case RiskControlStrategy.ShowTip:
+ result.ShowTip = true;
+ result.TriggeredRules.Add(new TriggeredRuleInfo
+ {
+ RuleId = ruleId,
+ RuleName = rule.RuleName,
+ ControlStrategy = RiskControlStrategy.ShowTip,
+ RuleText = rule.RuleText,
+ Message = $"规则[{rule.RuleName}]触发:提示"
+ });
+ break;
+
+ default:
+ _logger.Info($"[风控引擎] 未知策略类型 - ControlStrategy: {application.ControlStrategy}");
+ break;
+ }
+ }
+ else
+ {
+ _logger.Info($"[风控引擎] 规则未触发 - RuleId: {rule.Id}, TriggerPoint: {triggerPoint}");
+ }
+ }
+ }
+
+ // ============================================================
+ // Step 4: 聚合最终结果
+ // ============================================================
+ if (!result.Blocked && !result.NeedApproval)
+ {
+ result.Passed = true;
+ }
+ else
+ {
+ result.Passed = false;
+ }
+
+ _logger.Info($"[风控引擎] EvaluateRisk 完成 - TradeId: {context?.TradeId}, Passed: {result.Passed}, Blocked: {result.Blocked}, NeedApproval: {result.NeedApproval}, TriggeredRules: {result.TriggeredRules.Count}");
+ }
+ catch (Exception ex)
+ {
+ result.NeedApproval = true;
+ result.Passed = false;
+ result.TriggeredRules.Add(new TriggeredRuleInfo
+ {
+ RuleId = "ENGINE_ERROR",
+ RuleName = "风控引擎执行异常",
+ RuleText = ex.Message,
+ Message = $"风控引擎异常:{ex.Message}"
+ });
+ _logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
+ }
+
+ return result;
+ }
+
+ ///
+ /// 从数据库加载规则列表
+ ///
+ private List LoadRulesFromDb()
+ {
+ var rules = DbContext.glms_risk_rule
+ .AsNoTracking()
+ .OrderByDescending(r => r.UpdateDate ?? r.OptDate)
+ .Select(r => new RiskRule
+ {
+ Status = r.Status,
+ Id = r.id,
+ RuleName = r.RuleName,
+ RuleText = r.RuleText,
+ ConditionJson = r.ConditionJson,
+ RuleExpr = r.RuleExpr,
+ Version = r.Version,
+ OptId = r.OptId ?? 0,
+ OptName = r.OptName,
+ OptDate = r.OptDate ?? DateTime.MinValue,
+ UpdateOptId = r.UpdateOptId ?? 0,
+ UpdateOptName = r.UpdateOptName,
+ UpdateDate = r.UpdateDate ?? r.OptDate ?? DateTime.MinValue
+ }).ToList() ;
+ return rules;
+ //#region 测试本地规则
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000001,
+ // RuleName = "挂钩标的集中度校验(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易;分子查询 trade 表同一标的存续/审批中交易 StockEqvNotional 汇总;分母查询 underlying_manager.ExJson 中债券 IssueSize(亿)。计算逻辑:同一标的总名义本金 ÷ 发行量 × 100%,发行量乘 100000000 还原为元,结果大于 30% 时触发审批。",
+ // RuleExpr = "Convert.ToDecimal(DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.UnderlyingId == DbContext.trade.First(x => x.id == TradeId).UnderlyingId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Sum(t => (double?)t.StockEqvNotional) ?? 0d) / (JsonConvert.DeserializeObject(DbContext.underlying_manager.Where(u => u.UnderlyingCode == DbContext.trade.First(x => x.id == TradeId).UnderlyingCode).Select(u => u.ExJson).FirstOrDefault()).IssueSize.Value * 100000000m) * 100m > 30m",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000003,
+ // RuleName = "名义本金超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StockEqvNotional,对应 trade 表名义本金字段。计算逻辑:StockEqvNotional 大于 100000000 时触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StockEqvNotional > 100000000",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000004,
+ // RuleName = "保证金支付比例超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate,对应 trade 表保证金率字段。计算逻辑:本地测试按数值型比例直接比较,MarginRate 大于 0.5 视为超过 50%,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate > 0.5",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000005,
+ // RuleName = "保证金利率偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate,作为保证金利率本地测试字段。计算逻辑:若 MarginRate 小于 0.02 或大于 0.05,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate < 0.02 || DbContext.trade.First(t => t.id == TradeId).MarginRate > 0.05",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000006,
+ // RuleName = "保证金收取比例低于最低标准(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate 做本地测试比较。计算逻辑:先以 20% 作为本地测试最低标准,MarginRate 小于 0.2 时触发审批,后续接入正式配置后再替换阈值来源。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate < 0.2",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000007,
+ // RuleName = "起息日早于当前日期(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate,对应 trade 表开始日。计算逻辑:StartDate 有值且日期早于系统当天 DateTime.Today 时触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StartDate.HasValue && DbContext.trade.First(t => t.id == TradeId).StartDate.Value.Date < DateTime.Today",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000008,
+ // RuleName = "支付日为银行间交易日(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 SettlementDate,对应 trade 表结算日期;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:若 SettlementDate 有值,且向前修正到最近交易日后的结果不等于原日期,则说明原日期不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).SettlementDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).SettlementDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).SettlementDate.Value.Date",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000009,
+ // RuleName = "到期日为银行间交易日(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 ExerciseDate,对应当前交易里更接近业务到期/行权日的字段;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:ExerciseDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000010,
+ // RuleName = "平仓日为银行间交易日(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 UnWindDate,对应 trade 表平仓日;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:UnWindDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).UnWindDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).UnWindDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).UnWindDate.Value.Date",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000011,
+ // RuleName = "合约期限超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate 和 ExerciseDate。计算逻辑:当 StartDate 和 ExerciseDate 都有值时,用 ExerciseDate.Date 减 StartDate.Date 的总天数,若大于 365 天则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StartDate.HasValue && DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && (DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date - DbContext.trade.First(t => t.id == TradeId).StartDate.Value.Date).TotalDays > 365d",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000012,
+ // RuleName = "债券类净价偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiNetNoFeePrice,对应债券类标的期初交割净价,库内为 1 左右原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期取上一收盘日 net_price,库内为 100 左右报价。计算逻辑:按 ABS(PosiNetNoFeePrice×100-net_price) 计算绝对价差,价差大于 5 元时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiNetNoFeePrice.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().net_price.Value) > 5m",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000013,
+ // RuleName = "债券类收益率偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 InitYtm,对应债券类标的期初成交收益率,库内为原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期取上一收盘日 yield,库内为 1.5 到 2.2 左右百分数。计算逻辑:按 ABS(InitYtm×100-yield) 计算收益率绝对差,差值大于 1 时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).InitYtm.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value) > 1m",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000014,
+ // RuleName = "非债券类价格偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiGrossPrice,对应普通收益互换页面填写的期初标的交割全价%,库内为 1 左右原值;通过 DbContext.eod_stock_price / eod_commodity_future_price 按当前交易标的和交易日前日期取上一日收盘价 ClosePrice。计算逻辑:按 ABS(PosiGrossPrice×100-ClosePrice) 计算绝对价差,价差大于 5 时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiGrossPrice * 100m - Convert.ToDecimal((DbContext.eod_stock_price.Where(e => e.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).Select(e => (double?)e.ClosePrice).FirstOrDefault() ?? DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).Select(e => (double?)e.ClosePrice).FirstOrDefault()).Value)) > 5m",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000015,
+ // RuleName = "单一交易对手累计标的数量超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手存续/审批中交易的去重标的数量。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。",
+ // RuleExpr = "DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Select(t => t.UnderlyingId).Distinct().Count() > 10",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000016,
+ // RuleName = "多头支付固定端利率偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 FixedRate,并结合 BuySell 判断多头方向。计算逻辑:当 BuySell 表示多头且 FixedRate 有值时,先以 2.5% 作为本地测试基准,若 ABS(FixedRate-0.025)/0.025×100% 大于 5%,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).BuySell == \"Buy\" && DbContext.trade.First(t => t.id == TradeId).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == TradeId).FixedRate.Value - 0.025d) / 0.025d) * 100d > 5d",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000017,
+ // RuleName = "空头利率减点借贷加权偏离(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 FixedRate,并结合 BuySell 判断空头方向。计算逻辑:当 BuySell 表示空头且 FixedRate 有值时,先以 2% 作为本地测试基准,若 ABS(FixedRate-0.02)/0.02×100% 大于 5%,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).BuySell == \"Sell\" && DbContext.trade.First(t => t.id == TradeId).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == TradeId).FixedRate.Value - 0.02d) / 0.02d) * 100d > 5d",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000018,
+ // RuleName = "账户授权收支方向不匹配(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OpponentRole 与 BuySell 做本地测试占位判断。计算逻辑:当 OpponentRole 和 BuySell 都有值,且 OpponentRole 为 Pay 且 BuySell 为 Buy 时视为方向不匹配,触发禁止。",
+ // RuleExpr = "!string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == TradeId).OpponentRole) && !string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == TradeId).BuySell) && DbContext.trade.First(t => t.id == TradeId).OpponentRole == \"Pay\" && DbContext.trade.First(t => t.id == TradeId).BuySell == \"Buy\"",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000019,
+ // RuleName = "执行价偏离超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 Strike 和 SpotPrice,分别对应行权价与现价。计算逻辑:当 Strike 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(Strike/SpotPrice-1)×100% 计算执行价相对现价的偏离率,大于 5% 时触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).Strike.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == TradeId).Strike.Value / DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value) - 1d) * 100d > 5d",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000021,
+ // RuleName = "接近/触发敲入敲出价(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 Strike 和 SpotPrice,近似模拟触发价与现价。计算逻辑:当 Strike 和 SpotPrice 都有值且 Strike 不为 0 时,按 ABS(SpotPrice/Strike-1)×100% 计算两者距离,距离小于等于 2% 时视为接近触发价,给出提示。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).Strike.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.HasValue && DbContext.trade.First(t => t.id == TradeId).Strike.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value / DbContext.trade.First(t => t.id == TradeId).Strike.Value) - 1d) * 100d <= 2d",
+ // Version = 1,
+ // Status = RiskRuleStatus.Active,
+ // OptId = 0,
+ // OptName = "system",
+ // OptDate = DateTime.Now,
+ // UpdateOptId = 0,
+ // UpdateOptName = "system",
+ // UpdateDate = DateTime.Now
+ //});
+
+ //return rules;
+ //#endregion
+ }
+
+
+ private List LoadApplicationsFromDb()
+ {
+ var applications = DbContext.glms_risk_rule_application
+ .AsNoTracking()
+ .OrderByDescending(a => a.UpdateDate ?? a.OptDate)
+ .Select(a => new RiskRuleApplication
+ {
+ Id = a.id,
+ RuleIds = a.RuleIds,
+ Status = a.Status,
+ ControlStrategy = a.ControlStrategy,
+ TriggerPoints = a.TriggerPoints,
+ ScopeIsGlobal = a.ScopeIsGlobal,
+ ScopeAssetBookIds = a.ScopeAssetBookIds,
+ ScopeClientIds = a.ScopeClientIds,
+ ScopeUnderlyingTypes = a.ScopeUnderlyingTypes,
+ ScopeTradeTypes = a.ScopeTradeTypes,
+ Version = a.Version,
+ OptId = a.OptId ?? 0,
+ OptName = a.OptName,
+ OptDate = a.OptDate ?? DateTime.MinValue,
+ UpdateOptId = a.UpdateOptId ?? 0,
+ UpdateOptName = a.UpdateOptName,
+ UpdateDate = a.UpdateDate ?? a.OptDate ?? DateTime.MinValue
+ })
+ .ToList();
+ return applications;
+ }
+ // applications.Add(new RiskRuleApplication
+ // {
+ // Id = 1000001,
+ // RuleIds = "1000001",
+ // Status = RiskRuleStatus.Active,
+ // ControlStrategy = RiskControlStrategy.Approval,
+ // TriggerPoints = "BOOK_CONFIRM",
+ // ScopeIsGlobal = true,
+ // ScopeAssetBookIds = string.Empty,
+ // ScopeClientIds = string.Empty,
+ // ScopeUnderlyingTypes = string.Empty,
+ // ScopeTradeTypes = string.Empty,
+ // Version = 1,
+ // OptId = 0,
+ // OptName = "system",
+ //}
+
+ ///
+ /// 判断应用配置是否命中当前交易。
+ /// 匹配规则遵循设计文档:
+ /// 1. 全局命中时直接返回 true;
+ /// 2. 同一维度内多选按并集处理;
+ /// 3. 不同维度之间按交集处理;
+ /// 4. 某维度留空表示该维度不限制。
+ ///
+ private bool IsApplicationMatched(RiskRuleApplication application, YLErp.DBModels.trade trade)
+ {
+ if (application == null)
+ {
+ return false;
+ }
+
+ if (application.ScopeIsGlobal == 1)
+ {
+ return true;
+ }
+
+ if (trade == null)
+ {
+ return false;
+ }
+
+ var accountMatched = IsScopeEmpty(application.ScopeAssetBookIds) || IsValueMatched(application.ScopeAssetBookIds, GetTradeAssetBookId(trade));
+ var clientMatched = IsScopeEmpty(application.ScopeClientIds) || IsValueMatched(application.ScopeClientIds, trade.ClientId);
+ var underlyingTypeMatched = IsScopeEmpty(application.ScopeUnderlyingTypes) || IsValueMatched(application.ScopeUnderlyingTypes, GetTradeUnderlyingType(trade));
+ var tradeTypeMatched = IsScopeEmpty(application.ScopeTradeTypes) || IsValueMatched(application.ScopeTradeTypes, trade.TradeType);
+
+ return accountMatched
+ && clientMatched
+ && underlyingTypeMatched
+ && tradeTypeMatched;
+ }
+
+ ///
+ /// 解析应用配置中的规则ID列表。
+ /// 多个规则ID使用逗号分隔,返回去空格后的 long 集合。
+ ///
+ private List ParseRuleIds(string ruleIds)
+ {
+ if (string.IsNullOrWhiteSpace(ruleIds))
+ {
+ return new List();
+ }
+
+ var ids = new List();
+ foreach (var part in ruleIds.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (long.TryParse(part.Trim(), out long id))
+ {
+ ids.Add(id);
+ }
+ }
+
+ return ids;
+ }
+
+ ///
+ /// 判断某个范围字段是否为空。
+ /// 为空表示该维度不限制。
+ ///
+ private bool IsScopeEmpty(string scopeValue)
+ {
+ return string.IsNullOrWhiteSpace(scopeValue);
+ }
+
+ ///
+ /// 判断单个值是否命中逗号分隔的范围配置。
+ /// 同一维度内多选按并集处理,只要命中任一值即返回 true。
+ ///
+ private bool IsValueMatched(string scopeValue, object currentValue)
+ {
+ if (string.IsNullOrWhiteSpace(scopeValue))
+ {
+ return true;
+ }
+
+ if (currentValue == null)
+ {
+ return false;
+ }
+
+ var currentText = currentValue.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(currentText))
+ {
+ return false;
+ }
+
+ return scopeValue
+ .Split(',', StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim())
+ .Any(s => string.Equals(s, currentText, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// 读取交易的资产簿账户ID。
+ /// 当前先按 BookId 取值;如果后续真实字段不是 BookId,再统一调整这里即可。
+ ///
+ private object GetTradeAssetBookId(YLErp.DBModels.trade trade)
+ {
+ var property = trade.GetType().GetProperty("BookId");
+ return property?.GetValue(trade);
+ }
+
+ ///
+ /// 读取交易的标的类型。
+ /// 当前先按 UnderlyingInstrumentType 取值;如果后续真实字段名不同,再统一调整这里即可。
+ ///
+ private object GetTradeUnderlyingType(YLErp.DBModels.trade trade)
+ {
+ var property = trade.GetType().GetProperty("UnderlyingInstrumentType");
+ return property?.GetValue(trade);
+ }
+}
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskResult.cs b/YLErpDAL/Modules/RiskEngine/RiskResult.cs
new file mode 100644
index 00000000..69edbd57
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskResult.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控结果
+ ///
+ public class RiskResult
+ {
+ ///
+ /// 是否全部通过
+ ///
+ public bool Passed { get; set; } = true;
+
+ ///
+ /// 是否被禁止
+ ///
+ public bool Blocked { get; set; }
+
+ ///
+ /// 是否需要审批
+ ///
+ public bool NeedApproval { get; set; }
+
+ ///
+ /// 是否存在提示
+ ///
+ public bool ShowTip { get; set; }
+
+ ///
+ /// 触发的规则列表
+ ///
+ public List TriggeredRules { get; set; } = new List();
+ }
+
+ ///
+ /// 触发规则详情
+ ///
+ 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; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskRule.cs b/YLErpDAL/Modules/RiskEngine/RiskRule.cs
new file mode 100644
index 00000000..7b5375b6
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskRule.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控规则定义。
+ /// 只描述“规则是什么”,不包含状态、策略、触发时点、适用范围等应用配置。
+ /// 这些执行维度由 RiskRuleApplication 承载。
+ ///
+ public class RiskRule
+ {
+ // === 基础信息 ===
+ public long Id { get; set; }
+ public string RuleName { get; set; }
+ public string RuleText { get; set; }
+
+
+ ///
+ /// 【核心】条件JSON(结构化模式的条件列表存储),如
+ /// {"conditions":[{"variableId":1,"operator":">","value":100000000}]}
+ /// 由 RuleCompiler 解析并编译为 CompiledScript
+ ///
+ public string ConditionJson { get; set; }
+
+ ///
+ /// 【核心】规则表达式(自由文本模式,类C#表达式)
+ /// 由 RuleCompiler 编译为 CompiledScript
+ ///
+ 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; }
+
+ // === 运行时内存字段(不持久化到数据库)===
+ ///
+ /// 【核心】预编译的可执行委托
+ /// 在规则创建/加载时由 RuleCompiler.Compile(this) 生成
+ /// 判风控时直接调用:rule.CompiledScript(context) → bool
+ ///
+ public Func CompiledScript { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs
new file mode 100644
index 00000000..68219350
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskRuleApplication.cs
@@ -0,0 +1,142 @@
+using System;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.RiskEngine
+{
+ ///
+ /// 风控规则应用配置。
+ ///
+ /// 设计原因:
+ /// 依据设计文档,风控规则需要区分“规则定义”和“规则应用”两个层面。
+ /// RiskRule 负责定义“规则是什么”,例如规则名称、规则编码、公式表达式、描述、版本等;
+ /// RiskRuleApplication 负责定义“规则在什么场景下如何执行”,例如启用状态、校验策略、触发时点、适用范围。
+ ///
+ /// 拆分后的好处是:
+ /// 1. 同一条规则可以被多个应用配置复用,避免重复维护相同的公式定义。
+ /// 2. 不同业务场景可以对同一条规则配置不同的执行策略,例如在簿记确认时禁止,在平仓审核时提示。
+ /// 3. 调整触发时点、适用范围、控制策略时,不需要修改规则本身,降低配置耦合。
+ /// 4. 后续如果要做规则管理页面、规则启停、按场景灰度投放,也更容易扩展。
+ ///
+ /// 当前文件先定义第一版最小模型,用于支撑基础流程跑通和示例数据加载。
+ ///
+ public class RiskRuleApplication
+ {
+ ///
+ /// 应用配置主键ID。
+ /// 用于唯一标识一条规则应用记录。
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// 关联的规则ID列表。
+ /// 通过该字段与 RiskRule.Id 关联,多个规则ID使用逗号分隔。
+ ///
+
+ public string RuleIds { get; set; }
+
+ ///
+ /// 应用状态。
+ /// 约定:
+ /// 1 = Active(已生效)
+ /// 2 = Disabled(已停用)
+ /// 3 = Deleted(已删除)
+ /// 在实际执行时,通常只加载 Active 状态的应用配置。
+ ///
+ public RiskRuleStatus Status { get; set; }
+
+ ///
+ /// 校验策略。
+ /// 约定:
+ /// 1 = 禁止
+ /// 2 = 审批
+ /// 3 = 提示
+ /// 当规则命中时,由该字段决定最终处理方式。
+ ///
+ public RiskControlStrategy ControlStrategy { get; set; }
+
+ ///
+ /// 触发时点。
+ /// 多个时点使用逗号分隔,例如:
+ /// BOOK_CONFIRM,CLOSE_REVIEW
+ /// 表示该应用配置会在这些业务时点参与风控判断。
+ ///
+ public string TriggerPoints { get; set; }
+
+ ///
+ /// 是否全局适用。
+ /// true 表示对所有交易都适用,此时通常忽略其他 Scope 字段;
+ /// false 表示只在指定范围内适用,需要结合下方各 Scope 字段进行过滤。
+ ///
+ public int ScopeIsGlobal { get; set; }
+
+ ///
+ /// 适用的资产簿记账户范围。
+ /// 第一版使用逗号分隔的字符串保存,例如:
+ /// 123,456,789
+ /// 后续执行时可按约定解析为账户ID集合。
+ ///
+ public string ScopeAssetBookIds { get; set; }
+
+ ///
+ /// 适用的客户范围。
+ /// 第一版使用逗号分隔的字符串保存客户ID,例如:
+ /// 10001,10002
+ /// 用于在执行前筛选该规则是否适用于当前客户。
+ ///
+ public string ScopeClientIds { get; set; }
+
+ ///
+ /// 适用的标的类型范围。
+ /// 例如:
+ /// 股票,ETF,债券
+ /// 用于控制规则仅对特定标的类型生效。
+ ///
+ public string ScopeUnderlyingTypes { get; set; }
+
+ ///
+ /// 适用的交易类型范围。
+ /// 例如:
+ /// 簿记交易,平仓交易
+ /// 用于控制规则仅在指定交易类型下生效。
+ ///
+ public string ScopeTradeTypes { get; set; }
+
+ public int Version { get; set; }
+
+ ///
+ /// 创建人ID。
+ /// 记录是谁创建了这条应用配置。
+ ///
+ public int OptId { get; set; }
+
+ ///
+ /// 创建人名称。
+ /// 便于界面展示和审计追踪。
+ ///
+ public string OptName { get; set; }
+
+ ///
+ /// 创建时间。
+ /// 记录应用配置的创建时刻。
+ ///
+ public DateTime OptDate { get; set; }
+
+ ///
+ /// 最后更新人ID。
+ /// 记录最近一次修改该应用配置的操作人。
+ ///
+ public int UpdateOptId { get; set; }
+
+ ///
+ /// 最后更新人名称。
+ /// 便于界面展示和审计追踪。
+ ///
+ public string UpdateOptName { get; set; }
+
+ ///
+ /// 最后更新时间。
+ /// 记录最近一次修改该应用配置的时间。
+ ///
+ public DateTime UpdateDate { get; set; }
+ }
+}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs
new file mode 100644
index 00000000..783a6bbc
--- /dev/null
+++ b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs
@@ -0,0 +1,1766 @@
+using BaseOUDAL;
+using Newtonsoft.Json;
+using System.Linq.Expressions;
+using YLErp.BLL;
+using YLErp.DBModels;
+using YLErp.Modules.RiskEngine.Dto;
+
+/*
+================================================================================
+ 风控规则管理服务 — RiskRuleService 技术方案说明
+================================================================================
+
+【职责定位】
+ RiskRuleService 是风控引擎模块的 CRUD 管理服务层,负责规则(Rule)、应用配置
+ (Application)、变量(Variable)和操作审计日志(AuditLog)的全生命周期管理。
+
+ 与 RiskEngineService(引擎执行层)的关系:
+ - 本服务:配置管理(增删改查 + 审计日志)
+ - RiskEngineService:运行时执行(缓存 + 公式解析 + 风控判定)
+ - 交互方式:本服务在每次写操作后调用 RiskEngineService.RefreshCache() 通知引擎刷新
+
+【设计文档】
+ 详细设计文档:.trae/documents/RiskRuleService详细设计.md(v1.1)
+ 需求来源文档:.trae/documents/风控引擎需求详细设计.md(v1.17)
+
+ 核心概念:
+ - Rule(规则):定义"什么情况下触发",支持两种公式模式(互斥):
+ · 结构化模式(ConditionJson 有值):变量 + 操作符 + 阈值/变量 的条件列表,前端拼接 RuleExpr
+ · 自由文本模式(RuleExpr 有值,ConditionJson 为空):前端直接编辑类 C# 表达式
+ - Application(应用配置):定义"何时、对谁、怎么处理",关联多条规则(RuleIds 逗号分隔)+ 策略/时点/范围
+ - Variable(变量):变量池中的可选项,用于构建规则条件(含取值表达式 VariableExpr)
+ - AuditLog(审计日志):记录所有 CRUD 操作,支持版本追溯(SnapshotData 存储版本号快照)
+
+【架构位置】
+ ┌─────────────────────────────────────────────────────────────────┐
+ │ API 层 │
+ │ RiskRuleController(27 个端点) │
+ │ - /api/risk-rules/* 规则 CRUD(10 个端点) │
+ │ - /api/risk-applications/* 应用配置 CRUD(9 个端点) │
+ │ - /api/risk-variables/* 变量池 CRUD(6 个端点) │
+ │ - /api/risk-audit-logs/* 审计日志(3 个端点) │
+ └──────────────────────────┬──────────────────────────────────────┘
+ │
+ ┌──────────────────────────▼──────────────────────────────────────┐
+ │ 服务层(本文件所在层) │
+ │ │
+ │ RiskRuleService(本文件) │
+ │ - 规则 / 应用 / 变量 / 审计日志的 CRUD │
+ │ - ConditionJson 完整校验(结构化模式:变量存在性、操作符、类型匹配)│
+ │ - RuleExpr 基础语法校验(自由文本模式:括号匹配、变量引用校验) │
+ │ - 乐观锁并发控制(Version 字段) │
+ │ - 删除保护(有 Active 引用时拒绝删除) │
+ │ - CRUD 后触发 RiskEngineService.RefreshCache() │
+ │ │
+ │ RiskEngineService(另一团队开发,单例) │
+ │ - 内存缓存管理 + 公式执行 + 风控判定 │
+ │ - RefreshCache():从数据库重新加载 Active 规则/应用/变量 │
+ └──────────────────────────┬──────────────────────────────────────┘
+ │
+ ┌──────────────────────────▼──────────────────────────────────────┐
+ │ 数据访问层(EF Core DbContext 直接访问,无独立 Repository) │
+ │ - glms_risk_rule 规则定义表 │
+ │ - glms_risk_rule_application 规则应用配置表(RuleIds 逗号分隔) │
+ │ - glms_risk_variable 变量池定义表 │
+ │ - glms_risk_rule_audit_log 操作审计日志表 │
+ └─────────────────────────────────────────────────────────────────┘
+
+【方法清单】(共 27 个公开方法 + 9 个私有辅助方法)
+
+ ── 规则管理(10 个) ──────────────────────────────────────────────
+ QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询)
+ GetRuleDetail(long) 获取规则详情(含关联应用摘要)
+ CreateRule(CreateRiskRuleReq) 新建规则(ConditionJson 完整校验)
+ UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1)
+ DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝)
+ EnableRule(long) 启用规则(仅 Disabled → Active)
+ DisableRule(long) 停用规则(仅 Active → Disabled)
+ GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号)
+ BatchDeleteRules(List) 批量删除规则(逐条保护检查,单事务)
+ GetRuleApplications(long) 获取规则关联的应用配置列表
+ GetAllRuleList() 获取所有启用规则(轻量字段,供应用配置下拉)
+
+ ── 应用配置管理(9 个) ───────────────────────────────────────────
+ QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称)
+ GetApplicationDetail(long) 获取应用详情(含关联规则信息)
+ CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验)
+ UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验)
+ DeleteApplication(long) 删除应用(软删除)
+ EnableApplication(long) 启用应用(校验关联规则状态)
+ DisableApplication(long) 停用应用
+ BatchEnableApplications(List) 批量启用(逐条校验关联规则)
+ BatchDisableApplications(List) 批量停用
+
+ ── 变量管理(6 个) ───────────────────────────────────────────────
+ QueryVariableList(QueryRiskVariableReq) 查询变量列表(全量返回,Category/Keyword 筛选)
+ GetVariableDetail(long) 获取变量详情(含 ReferenceCount 引用统计)
+ CreateVariable(CreateRiskVariableReq) 新建变量(VariableExpr 长度校验 ≤10000)
+ UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护)
+ DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除)
+ GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
+
+ ── 审计日志(3 个) ───────────────────────────────────────────────
+ QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页)
+ ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出)
+ GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData)
+
+ ── 私有辅助方法 ───────────────────────────────────────────────────
+ WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数)
+ GetVariableExprSet() 获取变量表达式集合(5 分钟本地缓存)
+ InvalidateVariableCache() 清除变量缓存
+ ValidateConditionJson(conditionJson) ConditionJson 完整校验(结构化模式)
+ ValidateRuleExpr(ruleExpr) RuleExpr 基础语法校验(自由文本模式:括号匹配 + 变量引用校验)
+ ValidateTriggerPoints(triggerPoints) 触发时点校验(5 个合法时点)
+ ValidateScopeFields(req) Scope 字段校验(全局互斥 + ID 格式校验)
+ ValidateIdList(idList, fieldName) ID 列表格式校验
+ TryRefreshCache() 安全调用 RefreshCache(失败仅记日志)
+ GetRuleOrThrow / GetApplicationOrThrow / GetVariableOrThrow 实体获取快捷方法
+
+【关键设计决策】
+ 1. 数据访问:直接使用 EF Core DbContext(无独立 Repository,与项目风格一致)
+ 2. 事务策略:Create* 方法使用 BeginTransaction 包裹双 SaveChanges(实体 + 审计日志同事务)
+ 3. 并发控制:乐观锁(Version 字段),修改时版本不匹配抛 ServiceException
+ 4. 删除保护:强保护模式,有 Active 引用时拒绝删除规则/变量
+ 5. ConditionJson 校验:变量存在性(按 VariableId)+ 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用
+ 6. RuleExpr:自由文本模式下前端直接编辑,后端做基础语法校验(括号匹配 + 变量引用校验)
+ 7. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度
+ 8. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果
+ 9. 批量操作:单事务批量,全部成功或全部失败
+ 10. 变量缓存:5 分钟本地 HashSet 缓存,CRUD 后 InvalidateVariableCache()
+ 11. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照
+ 12. 多规则支持:Application 通过 RuleIds(逗号分隔)关联多条规则,内存解析关联关系
+
+【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql)
+ glms_risk_rule 规则定义表
+ - 普通索引:Status, OptDate
+ - 关键字段:ConditionJson(结构化条件列表), RuleExpr(类 C# 表达式,引擎执行用), Version(乐观锁)
+
+ glms_risk_rule_application 规则应用配置表
+ - 普通索引:Status, OptDate
+ - 关键字段:RuleIds(逗号分隔的规则 ID 列表), ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围)
+
+ glms_risk_variable 变量池定义表
+ - 关键字段:VariableExpr(取值表达式), DataType(数值/日期/布尔)
+
+ glms_risk_rule_audit_log 操作审计日志表
+ - 普通索引:TargetType+TargetId, OperationType, OptDate
+ - 关键字段:SnapshotData(JSON 快照,存储版本号等)
+
+【关键文件清单】
+ 本服务:
+ - RiskRuleService.cs(本文件):规则/应用/变量/日志 CRUD 管理
+ - Dto/ 目录:请求/响应 DTO 模型
+
+ 关联服务:
+ - RiskEngineService.cs:引擎执行层(单例,本服务通过 GetInstance() 获取引用)
+ - RiskRuleController.cs(YLErpWeb/Controllers/):API 控制器(27 个端点)
+
+ 实体模型:
+ - Framework/YLErp.Core/DBModels/glms_risk_rule.cs
+ - Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs
+ - Framework/YLErp.Core/DBModels/glms_risk_variable.cs
+ - Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs
+
+ 枚举定义:
+ - Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs(Active/Disabled/Deleted)
+ - Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs(Block/Approve/Warn)
+ - Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs
+ - Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs(Numeric/Date/Boolean)
+
+【命名空间】
+ YLErp.Modules.RiskEngine
+
+【注意事项】
+ 1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用
+ 2. 所有写操作后必须调用 TryRefreshCache(),确保引擎缓存与数据库一致
+ 3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted)
+ 4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代
+ 5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
+ 6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
+================================================================================
+*/
+
+namespace YLErp.Modules.RiskEngine
+{
+ public class RiskRuleService : YLBaseService
+ {
+ private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService");
+
+ private HashSet _variableExprCache;
+ private DateTime _variableCacheUpdateTime;
+
+ private static readonly HashSet ValidTriggerPoints = new HashSet
+ {
+ "BOOK_CONFIRM",
+ "CLOSE_REVIEW",
+ "UPLOAD_CONFIRMATION",
+ "EVENT_TRIGGER",
+ "FUND_PAYMENT"
+ };
+
+ private static readonly Dictionary> ValidOperatorsByType = new Dictionary>
+ {
+ { RiskVariableDataType.Numeric, new HashSet { ">", "<", ">=", "<=", "=", "≠", "介于", "不介于" } },
+ { RiskVariableDataType.Date, new HashSet { "早于", "晚于", "等于", "不早于", "不晚于", "介于", "不介于" } },
+ { RiskVariableDataType.Boolean, new HashSet { "是", "否" } }
+ };
+
+ public RiskRuleService(OptUserInfo userInfo) : base(userInfo)
+ {
+ }
+
+ public RiskRuleService(YLBaseService baseService) : base(baseService)
+ {
+ }
+
+ public RiskRuleService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext)
+ {
+ }
+
+ #region Private Helpers
+
+ ///
+ /// 写入审计日志
+ ///
+ private void WriteAuditLog(string operationType, string targetType, long targetId,
+ string targetName, string operationDetail, string result = "成功", string snapshotData = null)
+ {
+ var log = new glms_risk_rule_audit_log
+ {
+ OperationType = operationType,
+ TargetType = targetType,
+ TargetId = targetId,
+ TargetName = targetName,
+ OperationDetail = operationDetail,
+ Result = result,
+ SnapshotData = snapshotData,
+ OptId = UserId,
+ OptName = UserName,
+ OptDate = DateTime.Now
+ };
+ DbContext.glms_risk_rule_audit_log.Add(log);
+ }
+
+ ///
+ /// 获取变量表达式集合(5 分钟本地缓存)
+ ///
+ private HashSet GetVariableExprSet()
+ {
+ if (_variableExprCache == null ||
+ DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5))
+ {
+ _variableExprCache = DbContext.glms_risk_variable
+ .Where(v => v.VariableExpr != null && v.VariableExpr != "")
+ .Select(v => v.VariableExpr)
+ .ToHashSet();
+ _variableCacheUpdateTime = DateTime.Now;
+ }
+ return _variableExprCache;
+ }
+
+ ///
+ /// 失效变量表达式缓存
+ ///
+ private void InvalidateVariableCache()
+ {
+ _variableExprCache = null;
+ }
+
+ ///
+ /// 校验 ConditionJson(结构化模式:变量存在性、操作符、类型匹配)
+ ///
+ private void ValidateConditionJson(string conditionJson)
+ {
+ List conditions;
+ try
+ {
+ conditions = JsonConvert.DeserializeObject>(conditionJson);
+ }
+ catch
+ {
+ throw new ServiceException("公式表达式 JSON 格式不合法");
+ }
+
+ if (conditions == null || conditions.Count == 0)
+ throw new ServiceException("公式条件列表不能为空");
+
+ for (int i = 0; i < conditions.Count; i++)
+ {
+ var cond = conditions[i];
+ var condLabel = $"条件{i + 1}";
+
+ var variableDef = DbContext.glms_risk_variable
+ .FirstOrDefault(v => v.id == cond.VariableId);
+ if (variableDef == null)
+ throw new ServiceException($"{condLabel}:变量 ID '{cond.VariableId}' 不存在");
+
+ var validOperators = ValidOperatorsByType[variableDef.DataType];
+ if (!validOperators.Contains(cond.Operator))
+ throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 不适用于{GetDataTypeName(variableDef.DataType)}类型变量");
+
+ if (cond.ThresholdType == "Fixed")
+ {
+ if (cond.Value == null)
+ throw new ServiceException($"{condLabel}:固定阈值不能为空");
+
+ if (cond.Operator == "介于" || cond.Operator == "不介于")
+ {
+ // 介于/不介于:Value 必须为双元素数组 [下限, 上限]
+ if (!(cond.Value is Newtonsoft.Json.Linq.JArray arr && arr.Count == 2))
+ throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 的阈值必须为双元素数组 [下限, 上限]");
+ if (variableDef.DataType == RiskVariableDataType.Numeric)
+ {
+ if (!decimal.TryParse(arr[0].ToString(), out _) || !decimal.TryParse(arr[1].ToString(), out _))
+ throw new ServiceException($"{condLabel}:数值型介于/不介于的下限和上限必须为数字");
+ }
+ if (variableDef.DataType == RiskVariableDataType.Date)
+ {
+ if (!DateTime.TryParse(arr[0].ToString(), out _) || !DateTime.TryParse(arr[1].ToString(), out _))
+ throw new ServiceException($"{condLabel}:日期型介于/不介于的起始和结束日期必须为合法日期");
+ }
+ }
+ else
+ {
+ if (variableDef.DataType == RiskVariableDataType.Numeric)
+ {
+ if (!decimal.TryParse(cond.Value.ToString(), out _))
+ throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字");
+ }
+ if (variableDef.DataType == RiskVariableDataType.Date)
+ {
+ if (!DateTime.TryParse(cond.Value.ToString(), out _))
+ throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期");
+ }
+ }
+ }
+ else if (cond.ThresholdType == "Variable")
+ {
+ if (!cond.ThresholdVariableId.HasValue)
+ throw new ServiceException($"{condLabel}:变量阈值引用的变量 ID 不能为空");
+ var thresholdVarDef = DbContext.glms_risk_variable
+ .FirstOrDefault(v => v.id == cond.ThresholdVariableId.Value);
+ if (thresholdVarDef == null)
+ throw new ServiceException($"{condLabel}:阈值变量 ID '{cond.ThresholdVariableId.Value}' 不存在");
+ if (thresholdVarDef.DataType != variableDef.DataType)
+ throw new ServiceException($"{condLabel}:阈值变量与条件变量的数据类型不一致");
+ }
+ else
+ {
+ throw new ServiceException($"{condLabel}:阈值类型 '{cond.ThresholdType}' 不合法,仅支持 Fixed/Variable");
+ }
+ }
+ }
+
+ ///
+ /// 校验 RuleExpr(自由文本模式:括号匹配)
+ ///
+ private void ValidateRuleExpr(string ruleExpr)
+ {
+ if (ruleExpr.Length > 10000)
+ throw new ServiceException("公式表达式长度不能超过 10000 字符");
+
+ int parenDepth = 0;
+ foreach (char c in ruleExpr)
+ {
+ if (c == '(') parenDepth++;
+ else if (c == ')') parenDepth--;
+ if (parenDepth < 0)
+ throw new ServiceException("公式表达式括号不匹配:多余的右括号");
+ }
+ if (parenDepth != 0)
+ throw new ServiceException("公式表达式括号不匹配:缺少右括号");
+ }
+
+ ///
+ /// 校验触发时点
+ ///
+ private void ValidateTriggerPoints(string triggerPoints)
+ {
+ if (string.IsNullOrWhiteSpace(triggerPoints))
+ throw new ServiceException("触发时点不能为空");
+
+ var points = triggerPoints.Split(',');
+ foreach (var point in points)
+ {
+ var trimmed = point.Trim();
+ if (!ValidTriggerPoints.Contains(trimmed))
+ throw new ServiceException($"触发时点 '{trimmed}' 不合法");
+ }
+
+ if (points.All(p => string.IsNullOrWhiteSpace(p)))
+ throw new ServiceException("至少需要指定一个触发时点");
+ }
+
+ ///
+ /// 校验 Scope 适用范围字段
+ ///
+ private void ValidateScopeFields(CreateRiskApplicationReq req)
+ {
+ if (req.ScopeIsGlobal == 1)
+ {
+ if (!string.IsNullOrEmpty(req.ScopeAssetBookIds) ||
+ !string.IsNullOrEmpty(req.ScopeClientIds) ||
+ !string.IsNullOrEmpty(req.ScopeUnderlyingTypes) ||
+ !string.IsNullOrEmpty(req.ScopeTradeTypes))
+ {
+ throw new ServiceException("全局适用时,不应指定其他适用范围维度");
+ }
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(req.ScopeAssetBookIds))
+ {
+ ValidateIdList(req.ScopeAssetBookIds, "适用账户");
+ }
+
+ if (!string.IsNullOrEmpty(req.ScopeClientIds))
+ {
+ ValidateIdList(req.ScopeClientIds, "适用对手方");
+ }
+ }
+
+ ///
+ /// 校验 ID 列表格式
+ ///
+ private void ValidateIdList(string idList, string fieldName)
+ {
+ var parts = idList.Split(',');
+ foreach (var part in parts)
+ {
+ if (!int.TryParse(part.Trim(), out _))
+ throw new ServiceException($"{fieldName}列表格式不合法,应为逗号分隔的数字 ID");
+ }
+ }
+
+ ///
+ /// 尝试刷新引擎缓存(不抛异常)
+ ///
+ private void TryRefreshCache()
+ {
+ try
+ {
+ RiskEngineService.GetInstance().RefreshCache();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"缓存刷新失败:{ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 获取数据类型中文名
+ ///
+ private string GetDataTypeName(RiskVariableDataType dataType)
+ {
+ return dataType switch
+ {
+ RiskVariableDataType.Numeric => "数值",
+ RiskVariableDataType.Date => "日期",
+ RiskVariableDataType.Boolean => "布尔",
+ _ => "未知"
+ };
+ }
+
+ ///
+ /// 获取规则实体,不存在则抛异常
+ ///
+ private glms_risk_rule GetRuleOrThrow(long ruleId)
+ {
+ var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status != RiskRuleStatus.Deleted);
+ if (rule == null)
+ throw new ServiceException("规则不存在或已删除");
+ return rule;
+ }
+
+ ///
+ /// 获取应用配置实体,不存在则抛异常
+ ///
+ private glms_risk_rule_application GetApplicationOrThrow(long applicationId)
+ {
+ var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted);
+ if (app == null)
+ throw new ServiceException("应用配置不存在或已删除");
+ return app;
+ }
+
+ ///
+ /// 获取变量实体,不存在则抛异常
+ ///
+ private glms_risk_variable GetVariableOrThrow(long variableId)
+ {
+ var variable = DbContext.glms_risk_variable.FirstOrDefault(v => v.id == variableId);
+ if (variable == null)
+ throw new ServiceException("变量不存在");
+ return variable;
+ }
+
+ ///
+ /// 校验规则参数(名称 + ConditionJson + RuleExpr)
+ ///
+ private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr)
+ {
+ if (string.IsNullOrWhiteSpace(ruleName))
+ throw new ServiceException("规则名称不能为空");
+ if (ruleName.Length > 200)
+ throw new ServiceException("规则名称长度不能超过200字符");
+
+ if (string.IsNullOrWhiteSpace(ruleExpr))
+ throw new ServiceException("规则表达式不能为空");
+
+ if (!string.IsNullOrWhiteSpace(conditionJson))
+ {
+ ValidateConditionJson(conditionJson);
+ }
+
+ ValidateRuleExpr(ruleExpr);
+ }
+
+ ///
+ /// 解析逗号分隔的规则 ID 字符串为 List
+ ///
+ private List ParseRuleIds(string ruleIds)
+ {
+ if (string.IsNullOrWhiteSpace(ruleIds))
+ throw new ServiceException("关联规则 ID 不能为空");
+
+ var ids = new List();
+ foreach (var part in ruleIds.Split(','))
+ {
+ if (long.TryParse(part.Trim(), out long id))
+ ids.Add(id);
+ else
+ throw new ServiceException($"规则 ID 列表格式不合法:'{part.Trim()}'");
+ }
+
+ if (ids.Count == 0)
+ throw new ServiceException("关联规则 ID 不能为空");
+
+ return ids;
+ }
+
+ ///
+ /// 校验规则 ID 列表是否全部 Active
+ ///
+ private void ValidateRuleIdsActive(string ruleIds)
+ {
+ var ids = ParseRuleIds(ruleIds);
+ var rules = DbContext.glms_risk_rule
+ .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
+ .ToList();
+
+ if (rules.Count != ids.Count)
+ throw new ServiceException("部分关联规则不存在或已删除");
+
+ foreach (var rule in rules)
+ {
+ if (rule.Status == RiskRuleStatus.Disabled)
+ throw new ServiceException($"关联规则 '{rule.RuleName}' 已停用,无法创建/启用应用配置");
+ }
+ }
+
+ ///
+ /// 根据规则 ID 字符串解析为规则名称字符串
+ ///
+ private string ResolveRuleNames(string ruleIds)
+ {
+ if (string.IsNullOrWhiteSpace(ruleIds))
+ return "";
+
+ var ids = ruleIds.Split(',')
+ .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null)
+ .Where(id => id.HasValue)
+ .Select(id => id.Value)
+ .ToList();
+
+ if (ids.Count == 0)
+ return "";
+
+ var names = DbContext.glms_risk_rule
+ .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
+ .Select(r => r.RuleName)
+ .ToList();
+
+ return string.Join(", ", names);
+ }
+
+ ///
+ /// 构建 Scope 字段的 OR 过滤谓词(逗号分隔值精确匹配)
+ ///
+ private static IQueryable ApplyScopeFilter(
+ IQueryable query,
+ Expression> fieldSelector,
+ string csvValues)
+ {
+ var ids = csvValues.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ if (ids.Length == 0)
+ return query;
+
+ Expression> predicate = null;
+ var param = fieldSelector.Parameters[0];
+ var member = fieldSelector.Body;
+
+ foreach (var trimmed in ids.Select(id => id.Trim()))
+ {
+ var eq = Expression.Equal(member, Expression.Constant(trimmed));
+ var startsExpr = Expression.Call(member, nameof(string.StartsWith), null,
+ Expression.Constant(trimmed + ","));
+ var endsExpr = Expression.Call(member, nameof(string.EndsWith), null,
+ Expression.Constant("," + trimmed));
+ var containsExpr = Expression.Call(member, nameof(string.Contains), null,
+ Expression.Constant("," + trimmed + ","));
+
+ var orExpr = Expression.OrElse(Expression.OrElse(eq, startsExpr),
+ Expression.OrElse(endsExpr, containsExpr));
+ var lambda = Expression.Lambda>(orExpr, param);
+ predicate = predicate == null ? lambda : predicate.Or(lambda);
+ }
+
+ return predicate != null ? query.Where(predicate) : query;
+ }
+
+ ///
+ /// 获取控制策略中文名
+ ///
+ private static string GetStrategyName(RiskControlStrategy strategy)
+ {
+ return strategy switch
+ {
+ RiskControlStrategy.Block => "阻断",
+ RiskControlStrategy.Approval => "审批",
+ RiskControlStrategy.ShowTip => "提醒",
+ _ => strategy.ToString()
+ };
+ }
+
+ private static readonly Dictionary TriggerPointCnMap = new Dictionary
+ {
+ ["BOOK_CONFIRM"] = "交易录入确认",
+ ["CLOSE_REVIEW"] = "平仓审核",
+ ["UPLOAD_CONFIRMATION"] = "上传确认书",
+ ["EVENT_TRIGGER"] = "事件触发",
+ ["FUND_PAYMENT"] = "资金支付"
+ };
+
+ ///
+ /// 将触发时点映射为中文
+ ///
+ private static string MapTriggerPointsToChinese(string triggerPoints)
+ {
+ if (string.IsNullOrWhiteSpace(triggerPoints))
+ return "";
+
+ var parts = triggerPoints.Split(',');
+ var translated = parts.Select(p => TriggerPointCnMap.TryGetValue(p.Trim(), out var cn) ? cn : p.Trim());
+ return string.Join(", ", translated);
+ }
+
+ #endregion
+
+ #region Rule Management
+
+ ///
+ /// 查询规则列表(分页,含 ApplicationCount 子查询)
+ ///
+ public SearchListResult QueryRuleList(QueryRiskRuleReq req)
+ {
+ var query = DbContext.glms_risk_rule
+ .Where(r => r.Status != RiskRuleStatus.Deleted);
+
+ if (!string.IsNullOrWhiteSpace(req.Keyword))
+ {
+ query = query.Where(r => r.RuleName.Contains(req.Keyword)
+ || r.RuleText.Contains(req.Keyword));
+ }
+
+ if (req.Status.HasValue)
+ {
+ query = query.Where(r => r.Status == req.Status.Value);
+ }
+
+ if (req.VariableId.HasValue)
+ {
+ var varIdStr = req.VariableId.Value.ToString();
+ query = query.Where(r => (r.ConditionJson != null && r.ConditionJson.Contains(varIdStr))
+ || (r.RuleExpr != null && r.RuleExpr.Contains(varIdStr)));
+ }
+
+ var result = query.OrderByDescending(r => r.UpdateDate)
+ .Select(r => new RiskRuleListItem
+ {
+ Id = r.id,
+ RuleName = r.RuleName,
+ RuleText = r.RuleText,
+ ConditionJson = r.ConditionJson,
+ RuleExpr = r.RuleExpr,
+ Description = r.Description,
+ Status = r.Status,
+ Version = r.Version,
+ OptName = r.OptName,
+ OptDate = r.OptDate.GetValueOrDefault(),
+ UpdateOptName = r.UpdateOptName,
+ UpdateDate = r.UpdateDate ?? r.OptDate.GetValueOrDefault(),
+ ApplicationCount = DbContext.glms_risk_rule_application.Count(a => a.RuleIds.Contains(r.id.ToString()) && a.Status != RiskRuleStatus.Deleted)
+ });
+
+ return result.ToSearchList(req);
+ }
+
+ ///
+ /// 获取规则详情(含关联应用摘要)
+ ///
+ public RiskRuleDetail GetRuleDetail(long ruleId)
+ {
+ var rule = GetRuleOrThrow(ruleId);
+
+ var ruleIdStr = ruleId.ToString();
+ var applications = DbContext.glms_risk_rule_application
+ .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted)
+ .Select(a => new RiskRuleApplicationSummary
+ {
+ Id = a.id,
+ Status = a.Status,
+ ControlStrategy = a.ControlStrategy,
+ TriggerPoints = a.TriggerPoints
+ })
+ .ToList();
+
+ return new RiskRuleDetail
+ {
+ Id = rule.id,
+ RuleName = rule.RuleName,
+ RuleText = rule.RuleText,
+ ConditionJson = rule.ConditionJson,
+ RuleExpr = rule.RuleExpr,
+ Description = rule.Description,
+ Status = rule.Status,
+ Version = rule.Version,
+ OptName = rule.OptName,
+ OptDate = rule.OptDate.GetValueOrDefault(),
+ UpdateOptName = rule.UpdateOptName,
+ UpdateDate = rule.UpdateDate ?? rule.OptDate.GetValueOrDefault(),
+ Applications = applications
+ };
+ }
+
+ ///
+ /// 创建规则(ConditionJson 完整校验,事务包裹)
+ ///
+ public RiskRuleDetail CreateRule(CreateRiskRuleReq req)
+ {
+ ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
+
+ var compileResult = RuleCompiler.ValidateAndCompileFormula(null, req.RuleExpr);
+ if (!compileResult.Success)
+ throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
+
+ var entity = new glms_risk_rule
+ {
+ RuleName = req.RuleName,
+ RuleText = req.RuleText,
+ ConditionJson = req.ConditionJson,
+ RuleExpr = req.RuleExpr,
+ Description = req.Description,
+ Status = RiskRuleStatus.Active,
+ Version = 1
+ };
+
+ SetDBModelOpt(entity);
+ entity.UpdateOptId = UserId;
+ entity.UpdateOptName = UserName;
+ entity.UpdateDate = DateTime.Now;
+
+ using (var transaction = DbContext.Database.BeginTransaction())
+ {
+ try
+ {
+ DbContext.glms_risk_rule.Add(entity);
+ DbContext.SaveChanges();
+
+ WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, "创建规则",
+ snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
+ DbContext.SaveChanges();
+
+ transaction.Commit();
+ }
+ catch
+ {
+ transaction.Rollback();
+ throw;
+ }
+ }
+
+ RiskEngineService.GetInstance().RefreshOneRuleCache(entity.id);
+
+ return GetRuleDetail(entity.id);
+ }
+
+ ///
+ /// 更新规则(乐观锁校验,Version+1)
+ ///
+ public RiskRuleDetail UpdateRule(long ruleId, UpdateRiskRuleReq req)
+ {
+ var rule = GetRuleOrThrow(ruleId);
+
+ if (rule.Version != req.Version)
+ throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑");
+
+ ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
+
+ var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, req.RuleExpr);
+ if (!compileResult.Success)
+ throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
+
+ rule.RuleName = req.RuleName;
+ rule.RuleText = req.RuleText;
+ rule.ConditionJson = req.ConditionJson;
+ rule.RuleExpr = req.RuleExpr;
+ rule.Description = req.Description;
+ rule.Version = rule.Version + 1;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则");
+ DbContext.SaveChanges();
+
+ RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
+
+ return GetRuleDetail(ruleId);
+ }
+
+ ///
+ /// 删除规则(软删除,有 Active 应用引用时拒绝)
+ ///
+ public void DeleteRule(long ruleId)
+ {
+ var rule = GetRuleOrThrow(ruleId);
+
+ var ruleIdStr = ruleId.ToString();
+ var activeCount = DbContext.glms_risk_rule_application
+ .Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active);
+ if (activeCount > 0)
+ throw new ServiceException($"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用");
+
+ rule.Status = RiskRuleStatus.Deleted;
+ rule.Version = rule.Version + 1;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, "删除规则");
+ DbContext.SaveChanges();
+
+ TryRefreshCache();
+ }
+
+ ///
+ /// 启用规则(仅 Disabled → Active)
+ ///
+ public void EnableRule(long ruleId)
+ {
+ var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Disabled);
+ if (rule == null)
+ throw new ServiceException("仅已停用的规则可以启用");
+
+ rule.Status = RiskRuleStatus.Active;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则");
+ DbContext.SaveChanges();
+
+ TryRefreshCache();
+ }
+
+ ///
+ /// 停用规则(仅 Active → Disabled)
+ ///
+ public void DisableRule(long ruleId)
+ {
+ var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Active);
+ if (rule == null)
+ throw new ServiceException("仅已生效的规则可以停用");
+
+ rule.Status = RiskRuleStatus.Disabled;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, "停用规则");
+ DbContext.SaveChanges();
+
+ TryRefreshCache();
+ }
+
+ ///
+ /// 获取规则版本历史(从审计日志 SnapshotData 提取)
+ ///
+ public List GetRuleVersions(long ruleId)
+ {
+ GetRuleOrThrow(ruleId);
+
+ var logs = DbContext.glms_risk_rule_audit_log
+ .Where(l => l.TargetType == "RULE" && l.TargetId == ruleId)
+ .OrderByDescending(l => l.OptDate)
+ .ToList();
+
+ var result = new List();
+ foreach (var l in logs)
+ {
+ int version = 0;
+ if (!string.IsNullOrEmpty(l.SnapshotData))
+ {
+ try
+ {
+ var snapshot = JsonConvert.DeserializeAnonymousType(l.SnapshotData, new { Version = 0 });
+ if (snapshot != null)
+ version = snapshot.Version;
+ }
+ catch { }
+ }
+
+ result.Add(new RiskRuleVersionItem
+ {
+ Version = version,
+ OperationType = l.OperationType,
+ OperationDetail = l.OperationDetail,
+ OptName = l.OptName,
+ OptDate = l.OptDate
+ });
+ }
+
+ return result;
+ }
+
+ ///
+ /// 批量删除规则(逐条保护检查)
+ ///
+ public BatchOperationResult BatchDeleteRules(List ruleIds)
+ {
+ var rules = DbContext.glms_risk_rule
+ .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
+ .ToList();
+
+ if (rules.Count != ruleIds.Count)
+ throw new ServiceException("部分规则不存在或已删除");
+
+ foreach (var rule in rules)
+ {
+ var ruleIdStr = rule.id.ToString();
+ var activeCount = DbContext.glms_risk_rule_application
+ .Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active);
+ if (activeCount > 0)
+ throw new ServiceException($"规则 '{rule.RuleName}' 仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用");
+ }
+
+ foreach (var rule in rules)
+ {
+ rule.Status = RiskRuleStatus.Deleted;
+ rule.Version = rule.Version + 1;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, "批量删除规则");
+ }
+
+ DbContext.SaveChanges();
+ TryRefreshCache();
+
+ return new BatchOperationResult
+ {
+ Success = true,
+ TotalCount = ruleIds.Count,
+ SuccessCount = ruleIds.Count
+ };
+ }
+
+ ///
+ /// 获取规则关联的应用配置列表
+ ///
+ public List GetRuleApplications(long ruleId)
+ {
+ GetRuleOrThrow(ruleId);
+
+ var ruleIdStr = ruleId.ToString();
+ var apps = DbContext.glms_risk_rule_application
+ .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted)
+ .OrderByDescending(a => a.UpdateDate)
+ .ToList();
+
+ var result = new List();
+ foreach (var app in apps)
+ {
+ result.Add(new RiskApplicationListItem
+ {
+ Id = app.id,
+ RuleIds = app.RuleIds,
+ Status = (int)app.Status,
+ ControlStrategy = app.ControlStrategy,
+ TriggerPoints = app.TriggerPoints,
+ ScopeAssetBookIds = app.ScopeAssetBookIds,
+ ScopeClientIds = app.ScopeClientIds,
+ ScopeUnderlyingTypes = app.ScopeUnderlyingTypes,
+ ScopeTradeTypes = app.ScopeTradeTypes,
+ ScopeIsGlobal = app.ScopeIsGlobal,
+ Version = app.Version,
+ OptName = app.OptName,
+ OptDate = app.OptDate.GetValueOrDefault(),
+ UpdateOptName = app.UpdateOptName,
+ UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault()
+ });
+ }
+
+ return result;
+ }
+
+ ///
+ /// 获取所有启用规则列表(轻量字段,供下拉选择)
+ ///
+ public List GetAllRuleList()
+ {
+ return DbContext.glms_risk_rule
+ .Where(r => r.Status == RiskRuleStatus.Active)
+ .OrderBy(r => r.id)
+ .Select(r => new RiskRuleSimpleItem
+ {
+ Id = r.id,
+ RuleName = r.RuleName,
+ RuleText = r.RuleText
+ })
+ .ToList();
+ }
+
+ #endregion
+
+ #region Application Management
+
+ ///
+ /// 查询应用配置列表(多条件筛选,内存关联规则名称)
+ ///
+ public SearchListResult QueryApplicationList(QueryRiskApplicationReq req)
+ {
+ var query = DbContext.glms_risk_rule_application
+ .Where(a => a.Status != RiskRuleStatus.Deleted);
+
+ if (req.Status.HasValue)
+ {
+ query = query.Where(a => a.Status == req.Status.Value);
+ }
+
+ if (req.Strategy.HasValue)
+ {
+ query = query.Where(a => a.ControlStrategy == req.Strategy.Value);
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.TriggerPoint))
+ {
+ query = query.Where(a => a.TriggerPoints.Contains(req.TriggerPoint));
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.RuleName))
+ {
+ query = query.Where(a => DbContext.glms_risk_rule
+ .Any(r => r.Status != RiskRuleStatus.Deleted
+ && a.RuleIds.Contains(r.id.ToString())
+ && r.RuleName.Contains(req.RuleName)));
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.ScopeClientIds))
+ query = ApplyScopeFilter(query, a => a.ScopeClientIds, req.ScopeClientIds);
+
+ if (!string.IsNullOrWhiteSpace(req.ScopeUnderlyingTypes))
+ query = ApplyScopeFilter(query, a => a.ScopeUnderlyingTypes, req.ScopeUnderlyingTypes);
+
+ if (!string.IsNullOrWhiteSpace(req.ScopeTradeTypes))
+ query = ApplyScopeFilter(query, a => a.ScopeTradeTypes, req.ScopeTradeTypes);
+
+ if (!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds))
+ query = ApplyScopeFilter(query, a => a.ScopeAssetBookIds, req.ScopeAssetBookIds);
+
+ var pagedResult = query.OrderByDescending(a => a.UpdateDate)
+ .Select(a => new RiskApplicationListItem
+ {
+ Id = a.id,
+ RuleIds = a.RuleIds,
+ Description = a.Description,
+ Status = (int)a.Status,
+ ControlStrategy = a.ControlStrategy,
+ TriggerPoints = a.TriggerPoints,
+ ScopeAssetBookIds = a.ScopeAssetBookIds,
+ ScopeClientIds = a.ScopeClientIds,
+ ScopeUnderlyingTypes = a.ScopeUnderlyingTypes,
+ ScopeTradeTypes = a.ScopeTradeTypes,
+ ScopeIsGlobal = a.ScopeIsGlobal,
+ Version = a.Version,
+ OptName = a.OptName,
+ OptDate = a.OptDate.GetValueOrDefault(),
+ UpdateOptName = a.UpdateOptName,
+ UpdateDate = a.UpdateDate ?? a.OptDate.GetValueOrDefault()
+ })
+ .ToSearchList(req);
+
+ return pagedResult;
+ }
+
+ ///
+ /// 获取应用配置详情(含关联规则信息)
+ ///
+ public RiskApplicationDetail GetApplicationDetail(long applicationId)
+ {
+ var app = DbContext.glms_risk_rule_application
+ .FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted);
+ if (app == null)
+ throw new ServiceException("应用配置不存在或已删除");
+
+ return new RiskApplicationDetail
+ {
+ Id = app.id,
+ RuleIds = app.RuleIds,
+ Description = app.Description,
+ Status = app.Status,
+ ControlStrategy = app.ControlStrategy,
+ TriggerPoints = app.TriggerPoints,
+ ScopeAssetBookIds = app.ScopeAssetBookIds,
+ ScopeClientIds = app.ScopeClientIds,
+ ScopeUnderlyingTypes = app.ScopeUnderlyingTypes,
+ ScopeTradeTypes = app.ScopeTradeTypes,
+ ScopeIsGlobal = app.ScopeIsGlobal,
+ Version = app.Version,
+ OptName = app.OptName,
+ OptDate = app.OptDate.GetValueOrDefault(),
+ UpdateOptName = app.UpdateOptName,
+ UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault()
+ };
+ }
+
+ ///
+ /// 创建应用配置(关联规则 Active 校验,事务包裹)
+ ///
+ public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req)
+ {
+ ValidateRuleIdsActive(req.RuleIds);
+ ValidateTriggerPoints(req.TriggerPoints);
+ ValidateScopeFields(req);
+
+ var entity = new glms_risk_rule_application
+ {
+ RuleIds = req.RuleIds,
+ Description = req.Description,
+ Status = RiskRuleStatus.Active,
+ ControlStrategy = req.ControlStrategy,
+ TriggerPoints = req.TriggerPoints,
+ ScopeAssetBookIds = req.ScopeAssetBookIds,
+ ScopeClientIds = req.ScopeClientIds,
+ ScopeUnderlyingTypes = req.ScopeUnderlyingTypes,
+ ScopeTradeTypes = req.ScopeTradeTypes,
+ ScopeIsGlobal = req.ScopeIsGlobal,
+ Version = 1
+ };
+
+ SetDBModelOpt(entity);
+ entity.UpdateOptId = UserId;
+ entity.UpdateOptName = UserName;
+ entity.UpdateDate = DateTime.Now;
+
+ using (var transaction = DbContext.Database.BeginTransaction())
+ {
+ try
+ {
+ DbContext.glms_risk_rule_application.Add(entity);
+ DbContext.SaveChanges();
+
+ WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, ResolveRuleNames(req.RuleIds),
+ $"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}",
+ snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
+ DbContext.SaveChanges();
+
+ transaction.Commit();
+ }
+ catch
+ {
+ transaction.Rollback();
+ throw;
+ }
+ }
+
+ RiskEngineService.GetInstance().RefreshApplication();
+
+ return GetApplicationDetail(entity.id);
+ }
+
+ ///
+ /// 更新应用配置(乐观锁校验)
+ ///
+ public RiskApplicationDetail UpdateApplication(long applicationId, UpdateRiskApplicationReq req)
+ {
+ var app = GetApplicationOrThrow(applicationId);
+
+ if (app.Version != req.Version)
+ throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑");
+
+ if (!string.IsNullOrWhiteSpace(req.RuleIds))
+ ValidateRuleIdsActive(req.RuleIds);
+
+ ValidateTriggerPoints(req.TriggerPoints);
+ var scopeReq = new CreateRiskApplicationReq
+ {
+ ScopeAssetBookIds = req.ScopeAssetBookIds,
+ ScopeClientIds = req.ScopeClientIds,
+ ScopeUnderlyingTypes = req.ScopeUnderlyingTypes,
+ ScopeTradeTypes = req.ScopeTradeTypes,
+ ScopeIsGlobal = req.ScopeIsGlobal
+ };
+ ValidateScopeFields(scopeReq);
+
+ if (!string.IsNullOrWhiteSpace(req.RuleIds))
+ app.RuleIds = req.RuleIds;
+ app.Description = req.Description;
+ app.ControlStrategy = req.ControlStrategy;
+ app.TriggerPoints = req.TriggerPoints;
+ app.ScopeAssetBookIds = req.ScopeAssetBookIds;
+ app.ScopeClientIds = req.ScopeClientIds;
+ app.ScopeUnderlyingTypes = req.ScopeUnderlyingTypes;
+ app.ScopeTradeTypes = req.ScopeTradeTypes;
+ app.ScopeIsGlobal = req.ScopeIsGlobal;
+ app.Version = app.Version + 1;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds),
+ $"修改应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}");
+ DbContext.SaveChanges();
+
+ RiskEngineService.GetInstance().RefreshApplication();
+
+ return GetApplicationDetail(applicationId);
+ }
+
+ ///
+ /// 删除应用配置(软删除)
+ ///
+ public void DeleteApplication(long applicationId)
+ {
+ var app = GetApplicationOrThrow(applicationId);
+
+ app.Status = RiskRuleStatus.Deleted;
+ app.Version = app.Version + 1;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置");
+ DbContext.SaveChanges();
+
+ RiskEngineService.GetInstance().RefreshApplication();
+ }
+
+ ///
+ /// 启用应用配置(校验关联规则状态)
+ ///
+ public void EnableApplication(long applicationId)
+ {
+ var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Disabled);
+ if (app == null)
+ throw new ServiceException("仅已停用的应用配置可以启用");
+
+ ValidateRuleIdsActive(app.RuleIds);
+
+ app.Status = RiskRuleStatus.Active;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置");
+ DbContext.SaveChanges();
+
+ RiskEngineService.GetInstance().RefreshApplication();
+ }
+
+ ///
+ /// 停用应用配置
+ ///
+ public void DisableApplication(long applicationId)
+ {
+ var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Active);
+ if (app == null)
+ throw new ServiceException("仅已生效的应用配置可以停用");
+
+ app.Status = RiskRuleStatus.Disabled;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_DISABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "停用应用配置");
+ DbContext.SaveChanges();
+
+ RiskEngineService.GetInstance().RefreshApplication();
+ }
+
+ ///
+ /// 批量启用应用配置(逐条校验关联规则)
+ ///
+ public BatchOperationResult BatchEnableApplications(List applicationIds)
+ {
+ var apps = DbContext.glms_risk_rule_application
+ .Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Disabled)
+ .ToList();
+
+ if (apps.Count != applicationIds.Count)
+ throw new ServiceException("部分应用配置不存在或当前状态不允许启用");
+
+ foreach (var app in apps)
+ {
+ ValidateRuleIdsActive(app.RuleIds);
+ }
+
+ foreach (var app in apps)
+ {
+ app.Status = RiskRuleStatus.Active;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置");
+ }
+
+ DbContext.SaveChanges();
+ RiskEngineService.GetInstance().RefreshApplication();
+
+ return new BatchOperationResult
+ {
+ Success = true,
+ TotalCount = applicationIds.Count,
+ SuccessCount = applicationIds.Count
+ };
+ }
+
+ ///
+ /// 批量停用应用配置
+ ///
+ public BatchOperationResult BatchDisableApplications(List applicationIds)
+ {
+ var apps = DbContext.glms_risk_rule_application
+ .Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Active)
+ .ToList();
+
+ if (apps.Count != applicationIds.Count)
+ throw new ServiceException("部分应用配置不存在或当前状态不允许停用");
+
+ foreach (var app in apps)
+ {
+ app.Status = RiskRuleStatus.Disabled;
+ app.UpdateOptId = UserId;
+ app.UpdateOptName = UserName;
+ app.UpdateDate = DateTime.Now;
+
+ WriteAuditLog("APP_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置");
+ }
+
+ DbContext.SaveChanges();
+ RiskEngineService.GetInstance().RefreshApplication();
+
+ return new BatchOperationResult
+ {
+ Success = true,
+ TotalCount = applicationIds.Count,
+ SuccessCount = applicationIds.Count
+ };
+ }
+
+ #endregion
+
+ #region Variable Management
+
+ ///
+ /// 查询变量列表(全量返回,Category/Keyword 筛选)
+ ///
+ public SearchListResult QueryVariableList(QueryRiskVariableReq req)
+ {
+ var query = DbContext.glms_risk_variable.AsQueryable();
+
+ if (req.Category.HasValue)
+ {
+ query = query.Where(v => v.Category == req.Category.Value);
+ }
+
+ if (req.DataType.HasValue)
+ {
+ query = query.Where(v => v.DataType == req.DataType.Value);
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.VariableName))
+ {
+ query = query.Where(v => v.VariableName.Contains(req.VariableName));
+ }
+
+ var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableName)
+ .Select(v => new RiskVariableListItem
+ {
+ Id = v.id,
+ VariableName = v.VariableName,
+ Category = v.Category,
+ DataType = v.DataType,
+ Unit = v.Unit,
+ ValueDomain = v.ValueDomain,
+ Description = v.Description,
+ VariableExpr = v.VariableExpr,
+ Version = v.Version,
+ SortOrder = v.SortOrder
+ });
+
+ return result.ToSearchList(req);
+ }
+
+ ///
+ /// 获取变量详情(含 ReferenceCount 引用统计)
+ ///
+ public RiskVariableDetail GetVariableDetail(long variableId)
+ {
+ var variable = GetVariableOrThrow(variableId);
+
+ var varExpr = variable.VariableExpr;
+ var referenceCount = DbContext.glms_risk_rule
+ .Count(r => r.Status == RiskRuleStatus.Active &&
+ ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) ||
+ (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr))));
+
+ return new RiskVariableDetail
+ {
+ Id = variable.id,
+ VariableName = variable.VariableName,
+ Category = variable.Category,
+ DataType = variable.DataType,
+ Unit = variable.Unit,
+ ValueDomain = variable.ValueDomain,
+ Description = variable.Description,
+ VariableExpr = variable.VariableExpr,
+ Version = variable.Version,
+ SortOrder = variable.SortOrder,
+ OptName = variable.OptName,
+ OptDate = variable.OptDate.GetValueOrDefault(),
+ UpdateOptName = variable.UpdateOptName,
+ UpdateDate = variable.UpdateDate ?? variable.OptDate.GetValueOrDefault(),
+ ReferenceCount = referenceCount
+ };
+ }
+
+ ///
+ /// 创建变量(VariableExpr 长度校验 ≤10000,事务包裹)
+ ///
+ public RiskVariableDetail CreateVariable(CreateRiskVariableReq req)
+ {
+ if (string.IsNullOrWhiteSpace(req.VariableName))
+ throw new ServiceException("变量名称不能为空");
+ if (string.IsNullOrWhiteSpace(req.VariableExpr))
+ throw new ServiceException("取值表达式不能为空");
+ if (req.VariableExpr.Length > 10000)
+ throw new ServiceException("取值表达式长度不能超过 10000 字符");
+
+ var entity = new glms_risk_variable
+ {
+ VariableName = req.VariableName,
+ Category = req.Category,
+ DataType = req.DataType,
+ Unit = req.Unit,
+ ValueDomain = req.ValueDomain,
+ Description = req.Description,
+ VariableExpr = req.VariableExpr,
+ SortOrder = req.SortOrder,
+ Version = 1
+ };
+
+ SetDBModelOpt(entity);
+ entity.UpdateOptId = UserId;
+ entity.UpdateOptName = UserName;
+ entity.UpdateDate = DateTime.Now;
+
+ using (var transaction = DbContext.Database.BeginTransaction())
+ {
+ try
+ {
+ DbContext.glms_risk_variable.Add(entity);
+ DbContext.SaveChanges();
+
+ WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, $"创建变量:{req.VariableName}",
+ snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
+ DbContext.SaveChanges();
+
+ transaction.Commit();
+ }
+ catch
+ {
+ transaction.Rollback();
+ throw;
+ }
+ }
+
+ InvalidateVariableCache();
+ TryRefreshCache();
+
+ return GetVariableDetail(entity.id);
+ }
+
+ ///
+ /// 更新变量(乐观锁,DataType 变更保护,级联更新引用规则 RuleExpr)
+ ///
+ public RiskVariableDetail UpdateVariable(long variableId, UpdateRiskVariableReq req)
+ {
+ var variable = GetVariableOrThrow(variableId);
+
+ if (variable.Version != req.Version)
+ throw new ServiceException("变量已被其他用户修改,请重新加载后再编辑");
+
+ if (string.IsNullOrWhiteSpace(req.VariableName))
+ throw new ServiceException("变量名称不能为空");
+ if (string.IsNullOrWhiteSpace(req.VariableExpr))
+ throw new ServiceException("取值表达式不能为空");
+
+ if (req.DataType != variable.DataType)
+ {
+ var varExpr = variable.VariableExpr;
+ var activeRefCount = DbContext.glms_risk_rule
+ .Count(r => r.Status == RiskRuleStatus.Active &&
+ ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) ||
+ (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr))));
+ if (activeRefCount > 0)
+ throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型");
+ }
+
+ var oldVariableExpr = variable.VariableExpr;
+
+ variable.VariableName = req.VariableName;
+ variable.Category = req.Category;
+ variable.DataType = req.DataType;
+ variable.Unit = req.Unit;
+ variable.ValueDomain = req.ValueDomain;
+ variable.Description = req.Description;
+ variable.VariableExpr = req.VariableExpr;
+ variable.SortOrder = req.SortOrder;
+ variable.Version = variable.Version + 1;
+ variable.UpdateOptId = UserId;
+ variable.UpdateOptName = UserName;
+ variable.UpdateDate = DateTime.Now;
+
+ var variableExprChanged = oldVariableExpr != req.VariableExpr;
+
+ if (variableExprChanged)
+ {
+ // 编译校验:变量表达式变更后,需验证所有级联更新的规则表达式可编译
+ var affectedRules = DbContext.glms_risk_rule
+ .Where(r => r.RuleExpr != null && r.RuleExpr.Contains(oldVariableExpr))
+ .ToList();
+
+ var compileTasks = affectedRules.Select(r =>
+ Task.Run(() => RuleCompiler.ValidateAndCompileFormula(
+ r.id, r.RuleExpr.Replace(oldVariableExpr, req.VariableExpr)))
+ );
+ var compileResults = Task.WhenAll(compileTasks).GetAwaiter().GetResult();
+ var failed = compileResults.FirstOrDefault(r => !r.Success);
+ if (failed is not null)
+ throw new ServiceException($"变量表达式变更导致规则编译失败:{failed.ErrorMessage}");
+
+ using (var transaction = DbContext.Database.BeginTransaction())
+ {
+ try
+ {
+ foreach (var rule in affectedRules)
+ {
+ rule.RuleExpr = rule.RuleExpr.Replace(oldVariableExpr, req.VariableExpr);
+ rule.Version = rule.Version + 1;
+ rule.UpdateOptId = UserId;
+ rule.UpdateOptName = UserName;
+ rule.UpdateDate = DateTime.Now;
+ }
+
+ DbContext.SaveChanges();
+
+ WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
+
+ foreach (var rule in affectedRules)
+ {
+ WriteAuditLog("VAR_UPDATE_CASCADE", "RULE", rule.id, rule.RuleName,
+ $"变量 [{variable.VariableName}] VariableExpr 变更,级联更新规则 RuleExpr");
+ }
+
+ DbContext.SaveChanges();
+ transaction.Commit();
+ }
+ catch
+ {
+ transaction.Rollback();
+ throw;
+ }
+ }
+
+ InvalidateVariableCache();
+ foreach (var rule in affectedRules)
+ {
+ RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id);
+ }
+ }
+ else
+ {
+ WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
+ DbContext.SaveChanges();
+ InvalidateVariableCache();
+ }
+
+ return GetVariableDetail(variableId);
+ }
+
+ ///
+ /// 删除变量(有 Active 引用时拒绝)
+ ///
+ public void DeleteVariable(long variableId)
+ {
+ var variable = GetVariableOrThrow(variableId);
+
+ var varExpr = variable.VariableExpr;
+ var activeRefCount = DbContext.glms_risk_rule
+ .Count(r => r.Status == RiskRuleStatus.Active &&
+ ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) ||
+ (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr))));
+ if (activeRefCount > 0)
+ throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除");
+
+ DbContext.glms_risk_variable.Remove(variable);
+
+ WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, $"删除变量:{variable.VariableName}");
+ DbContext.SaveChanges();
+
+ InvalidateVariableCache();
+ TryRefreshCache();
+ }
+
+ ///
+ /// 获取所有变量列表(供下拉选择)
+ ///
+ public List GetAllVariableList()
+ {
+ var result = DbContext.glms_risk_variable
+ .Where(v => v.VariableExpr != null && v.VariableExpr != "")
+ .OrderBy(v => v.SortOrder)
+ .Select(v => new RiskVariableSimpleItem
+ {
+ Id = v.id,
+ VariableName = v.VariableName,
+ Category = v.Category,
+ DataType = v.DataType,
+ Unit = v.Unit,
+ VariableExpr = v.VariableExpr
+ })
+ .ToList();
+
+ return result;
+ }
+
+ #endregion
+
+ #region Audit Log
+
+ ///
+ /// 查询审计日志列表(分页)
+ ///
+ public SearchListResult QueryAuditLogs(QueryRiskAuditLogReq req)
+ {
+ var query = DbContext.glms_risk_rule_audit_log.AsQueryable();
+
+ if (!string.IsNullOrWhiteSpace(req.OperationType))
+ {
+ query = query.Where(l => l.OperationType == req.OperationType);
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.OperationTypes))
+ {
+ var types = req.OperationTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ query = query.Where(l => types.Contains(l.OperationType));
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.TargetType))
+ {
+ query = query.Where(l => l.TargetType == req.TargetType);
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.TargetName))
+ {
+ query = query.Where(l => l.TargetName.Contains(req.TargetName));
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.OptName))
+ {
+ query = query.Where(l => l.OptName.Contains(req.OptName));
+ }
+
+ if (req.StartDate.HasValue)
+ {
+ query = query.Where(l => l.OptDate >= req.StartDate.Value.Date);
+ }
+
+ if (req.EndDate.HasValue)
+ {
+ var end = req.EndDate.Value.Date.AddDays(1);
+ query = query.Where(l => l.OptDate < end);
+ }
+
+ if (!string.IsNullOrWhiteSpace(req.Keyword))
+ {
+ query = query.Where(l => l.TargetName.Contains(req.Keyword)
+ || l.OperationDetail.Contains(req.Keyword));
+ }
+
+ var result = query.OrderByDescending(l => l.OptDate)
+ .Select(l => new RiskAuditLogListItem
+ {
+ Id = l.id,
+ OperationType = l.OperationType,
+ TargetType = l.TargetType,
+ TargetId = l.TargetId,
+ TargetName = l.TargetName,
+ OperationDetail = l.OperationDetail,
+ Result = l.Result,
+ OptName = l.OptName,
+ OptDate = l.OptDate
+ });
+
+ return result.ToSearchList(req);
+ }
+
+ ///
+ /// 导出审计日志为 Excel
+ ///
+ public byte[] ExportAuditLogs(QueryRiskAuditLogReq req)
+ {
+ throw new ServiceException("导出功能暂未实现");
+ }
+
+ ///
+ /// 获取审计日志详情
+ ///
+ public RiskAuditLogDetail GetAuditLogDetail(long logId)
+ {
+ var log = DbContext.glms_risk_rule_audit_log.FirstOrDefault(l => l.id == logId);
+ if (log == null)
+ throw new ServiceException("审计日志不存在");
+
+ return new RiskAuditLogDetail
+ {
+ Id = log.id,
+ OperationType = log.OperationType,
+ TargetType = log.TargetType,
+ TargetId = log.TargetId,
+ TargetName = log.TargetName,
+ OperationDetail = log.OperationDetail,
+ Result = log.Result,
+ SnapshotData = log.SnapshotData,
+ OptId = log.OptId,
+ OptName = log.OptName,
+ OptDate = log.OptDate
+ };
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
index f47b6cee..a0817047 100644
--- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
+++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
@@ -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>((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 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();
+ 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();
+ res.IgnoredRiskRuleIds = currentApprovalRuleIds;
+ //需清除ErrorMsg,不然外部调用会认为失败
+ //触发需审批,交易特批逻辑
+ if (ignoreRiskWarning)
+ {
+ var ignoredRuleIdSet = new HashSet((ignoreRiskRuleIds ?? Enumerable.Empty()).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;
}
+ ///
+ /// 按 quotaTrial.id 校验试算结果是否已超过配置时效
+ ///
+ /// quotaTrial.id
+ /// 有效时长(秒)
+ /// 查不到记录或已超时返回 true,否则返回 false
+ 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);
+ }
+
///
/// 查询试算历史
///
@@ -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;
}
diff --git a/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs b/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs
index 4456edfe..96d6303e 100644
--- a/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs
+++ b/YLErpDAL/Modules/RiskModule/TradeRiskCheckLogService.cs
@@ -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();
}
+ ///
+ /// 查询指定交易最近一次新风控“确认通过”的处理时间,用于控制二次审批放行仅在短时间内有效。
+ ///
+ 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();
+ }
+
///
/// 回退重置
///
diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs
index ad24077c..7bca1074 100644
--- a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs
+++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs
@@ -46,7 +46,7 @@ namespace YLErp.Modules.TradeModule.DealModule
///
/// 2021-02-04改造:批量确认因为耗时太长,用户多页面(交易列表,今日交易)并发确认导致生成多条期权费资金记录
///
- public TradeConfirmResultModel tradeConfirm(IEnumerable tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false)
+ public TradeConfirmResultModel tradeConfirm(IEnumerable tradeids, bool ignoreMoneyCheck = false, bool isSkipApproval = false, bool ignoreRiskWarning = false, IEnumerable 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(),
@@ -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
///
///
private TradeOpenResult ConfirmTrade(int tid, bool isBatch, bool hasTradeProcess,
- bool ignoreMoneyCheck, bool isSkipApproval, List confirmedTradeIds, List changeConfirmPaths, bool continueQuotaCheck = false)
+ bool ignoreMoneyCheck, bool isSkipApproval, List confirmedTradeIds, List changeConfirmPaths, bool continueQuotaCheck = false, bool ignoreRiskWarning = false, IEnumerable 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 confirmedTradeIds;
public List changeConfirmPaths;
public string errorMsg;
+ public string tipMsg;
public string type;
+ public List ignoreRiskRuleIds;
}
}
diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs
index 837864d0..8bfd988e 100644
--- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs
+++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs
@@ -794,11 +794,17 @@ namespace YLErp.Modules.TradeModule.DealModule
public TradeOpenRetCode RetCode;
public int TrialDataId;
+ public List IgnoredRiskRuleIds;
+ public bool OldRiskNeedSpecialApproval;
///
/// 错误信息
///
public string ErrorMsg;
+ ///
+ /// 提示信息
+ ///
+ public string TipMsg;
///
/// 变更确认书路径
@@ -838,6 +844,10 @@ namespace YLErp.Modules.TradeModule.DealModule
///
FundStatus,
///
+ /// 风控预警二次确认
+ ///
+ RiskWarning,
+ ///
/// 无审核权限
///
NoRight
diff --git a/YLErpDAL/Modules/TradeModule/TradeBLL.cs b/YLErpDAL/Modules/TradeModule/TradeBLL.cs
index 5103a1c1..5daff142 100644
--- a/YLErpDAL/Modules/TradeModule/TradeBLL.cs
+++ b/YLErpDAL/Modules/TradeModule/TradeBLL.cs
@@ -25,6 +25,8 @@ namespace YLErp.BLL
///
public const string LackOfMoney = "LackOfMoney";
+ public const string RiskWarningConfirm = "RiskWarningConfirm";
+
public const string IgnoreBreak = "IgnoreBreak";
///
diff --git a/YLErpDAL/YLErpDAL.csproj b/YLErpDAL/YLErpDAL.csproj
index 83cf8334..8b50f99f 100644
--- a/YLErpDAL/YLErpDAL.csproj
+++ b/YLErpDAL/YLErpDAL.csproj
@@ -30,6 +30,8 @@
+
+
@@ -40,7 +42,7 @@
-
+
diff --git a/YLErpWeb/App/HostedTaskService.cs b/YLErpWeb/App/HostedTaskService.cs
index 75bf1339..025420b8 100644
--- a/YLErpWeb/App/HostedTaskService.cs
+++ b/YLErpWeb/App/HostedTaskService.cs
@@ -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;
diff --git a/YLErpWeb/App_Data/FunctionRight.xml b/YLErpWeb/App_Data/FunctionRight.xml
index 7962c464..3d23be45 100644
--- a/YLErpWeb/App_Data/FunctionRight.xml
+++ b/YLErpWeb/App_Data/FunctionRight.xml
@@ -60,6 +60,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/YLErpWeb/App_Data/Menus.txt b/YLErpWeb/App_Data/Menus.txt
index a8aea562..506f2a79 100644
--- a/YLErpWeb/App_Data/Menus.txt
+++ b/YLErpWeb/App_Data/Menus.txt
@@ -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"},
diff --git a/YLErpWeb/Controllers/RiskRuleController.cs b/YLErpWeb/Controllers/RiskRuleController.cs
new file mode 100644
index 00000000..28fbfeec
--- /dev/null
+++ b/YLErpWeb/Controllers/RiskRuleController.cs
@@ -0,0 +1,698 @@
+using Qdp.Foundation.Utilities;
+using YLErp.DBModels;
+using YLErp.Modules.RiskEngine;
+using YLErp.Modules.RiskEngine.Dto;
+
+namespace YLErp.Web.Controllers
+{
+ /// 风控引擎管理控制器,提供规则、应用配置、变量池和审计日志的 CRUD 接口
+ [Route("risk-engine")]
+ public class RiskRuleController : BaseController
+ {
+ private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleController");
+
+ #region Rule Management
+
+ [HttpGet("risk-rules")]
+ [MyAuthorize("风险控制-风控规则查看")]
+ /// 查询风控规则列表(分页)
+ 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("风险控制-风控规则查看")]
+ /// 获取风控规则详情
+ 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("风险控制-风控规则查看")]
+ /// 获取风控规则版本历史
+ 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("风险控制-风控规则新增")]
+ /// 创建风控规则
+ 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("风险控制-风控规则编辑")]
+ /// 更新风控规则
+ 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("风险控制-风控规则删除")]
+ /// 删除风控规则(软删除)
+ 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("风险控制-风控规则删除")]
+ /// 批量删除风控规则
+ public JsonResult BatchDeleteRiskRules([FromBody] List 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("风险控制-风控规则启停")]
+ /// 启用风控规则
+ 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("风险控制-风控规则启停")]
+ /// 停用风控规则
+ 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("风险控制-风控规则查看")]
+ /// 获取规则关联的应用配置列表
+ 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("风险控制-风控规则查看")]
+ /// 获取所有启用规则列表(供下拉选择)
+ 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("风险控制-风控应用查看")]
+ /// 查询应用配置列表(分页)
+ 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("风险控制-风控应用查看")]
+ /// 获取应用配置详情
+ 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("风险控制-风控应用新增")]
+ /// 创建应用配置
+ 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("风险控制-风控应用编辑")]
+ /// 更新应用配置
+ 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("风险控制-风控应用删除")]
+ /// 删除应用配置(软删除)
+ 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("风险控制-风控应用启停")]
+ /// 启用应用配置
+ 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("风险控制-风控应用启停")]
+ /// 停用应用配置
+ 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("风险控制-风控应用启停")]
+ /// 批量启用应用配置
+ public JsonResult BatchEnableRiskApplications([FromBody] List 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("风险控制-风控应用启停")]
+ /// 批量停用应用配置
+ public JsonResult BatchDisableRiskApplications([FromBody] List 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("风险控制-风控变量查看")]
+ /// 查询变量列表
+ 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("风险控制-风控变量查看")]
+ /// 获取变量详情
+ 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("风险控制-风控变量查看")]
+ /// 获取所有变量列表(供下拉选择)
+ 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("风险控制-风控变量新增")]
+ /// 创建变量
+ 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("风险控制-风控变量编辑")]
+ /// 更新变量(含级联更新引用规则的 RuleExpr)
+ 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("风险控制-风控变量删除")]
+ /// 删除变量
+ 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("风险控制-风控应用查看")]
+ /// 获取合约类型列表
+ 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("风险控制-风控日志查看")]
+ /// 查询风控操作日志列表(分页)
+ 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("风险控制-风控日志查看")]
+ /// 获取风控操作日志详情
+ 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("风险控制-风控日志导出")]
+ /// 导出风控操作日志为 Excel
+ 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
+ }
+}
diff --git a/YLErpWeb/Controllers/SystemController.cs b/YLErpWeb/Controllers/SystemController.cs
index 5b7d0681..1ee0bd83 100644
--- a/YLErpWeb/Controllers/SystemController.cs
+++ b/YLErpWeb/Controllers/SystemController.cs
@@ -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);
}
diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs
index 726e93b5..3cead1b9 100644
--- a/YLErpWeb/Controllers/tradeController.cs
+++ b/YLErpWeb/Controllers/tradeController.cs
@@ -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();
+ }
+ }
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()
{
diff --git a/YLErpWeb/Views/trade/quotaTrial.cshtml b/YLErpWeb/Views/trade/quotaTrial.cshtml
index 7751409d..b04ecd25 100644
--- a/YLErpWeb/Views/trade/quotaTrial.cshtml
+++ b/YLErpWeb/Views/trade/quotaTrial.cshtml
@@ -101,6 +101,9 @@
case YLErp.Enums.QuotaTrialStatusEnum.Warning:
预警
break;
+ case YLErp.Enums.QuotaTrialStatusEnum.RiskWarning:
+ 风控需审批
+ break;
case YLErp.Enums.QuotaTrialStatusEnum.Error:
不通过
break;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
index f32dc0ea..7b832f3c 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
@@ -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();
diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/tradeview.js b/YLErpWeb/wwwroot/Scripts/app/trade/tradeview.js
index 85b56acb..67ad44db 100644
--- a/YLErpWeb/wwwroot/Scripts/app/trade/tradeview.js
+++ b/YLErpWeb/wwwroot/Scripts/app/trade/tradeview.js
@@ -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 = ["确认", '取消'];
}