feat: 风控引擎前后端统一操作符
This commit is contained in:
+5
-9
@@ -1,6 +1,4 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
CREATE TABLE `glms_risk_rule` (
|
||||
CREATE TABLE `yltrs_ylcms`.`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 '规则文本',
|
||||
@@ -19,7 +17,7 @@ CREATE TABLE `glms_risk_rule` (
|
||||
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` (
|
||||
CREATE TABLE `yltrs_ylcms`.`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 '描述',
|
||||
@@ -42,7 +40,7 @@ CREATE TABLE `glms_risk_rule_application` (
|
||||
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` (
|
||||
CREATE TABLE `yltrs_ylcms`.`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)',
|
||||
@@ -61,7 +59,7 @@ CREATE TABLE `glms_risk_rule_audit_log` (
|
||||
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` (
|
||||
CREATE TABLE `yltrs_ylcms`.`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',
|
||||
@@ -79,6 +77,4 @@ CREATE TABLE `glms_risk_variable` (
|
||||
`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;
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控变量池定义表' ROW_FORMAT = Dynamic;
|
||||
@@ -0,0 +1,55 @@
|
||||
-- 异常交易监控页面 - 操作权限(按钮)数据
|
||||
-- 依赖:风险控制目录(name='风险控制',type=0)必须已存在
|
||||
-- 对应 FunctionRight.xml 风险控制下风控相关操作权限(共16个按钮)
|
||||
-- type: 0=目录 1=菜单 2=按钮
|
||||
-- 注意:以下 sys_menu 和 sys_role_menu 两个 INSERT 依赖用户变量 @menu_id,必须在同一个 Session 中执行
|
||||
-- ============================================================
|
||||
|
||||
-- ----------------------------
|
||||
-- 3.1. sys_menu 页面+按钮(type=1,2)
|
||||
-- ----------------------------
|
||||
SELECT IFNULL(MAX(id), 0) INTO @menu_id FROM `yltrs_admin`.`sys_menu`;
|
||||
SELECT id INTO @risk_control_id FROM `yltrs_admin`.`sys_menu` WHERE `name` = '风险控制' AND `type` = 0 LIMIT 1;
|
||||
|
||||
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
|
||||
(@menu_id + 1, @risk_control_id, NULL, '异常交易监控', NULL, 1, NULL, NULL, NULL, '异常交易监控', NULL, NULL, NULL, NULL, NULL, NULL, 0, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 2, @menu_id + 1, NULL, '风控规则查看', NULL, 2, NULL, NULL, NULL, '风控规则查看', NULL, NULL, NULL, NULL, NULL, NULL, 1, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 3, @menu_id + 1, NULL, '风控规则新增', NULL, 2, NULL, NULL, NULL, '风控规则新增', NULL, NULL, NULL, NULL, NULL, NULL, 2, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 4, @menu_id + 1, NULL, '风控规则编辑', NULL, 2, NULL, NULL, NULL, '风控规则编辑', NULL, NULL, NULL, NULL, NULL, NULL, 3, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 5, @menu_id + 1, NULL, '风控规则删除', NULL, 2, NULL, NULL, NULL, '风控规则删除', NULL, NULL, NULL, NULL, NULL, NULL, 4, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 6, @menu_id + 1, NULL, '风控规则启停', NULL, 2, NULL, NULL, NULL, '风控规则启停', NULL, NULL, NULL, NULL, NULL, NULL, 5, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 7, @menu_id + 1, NULL, '风控应用查看', NULL, 2, NULL, NULL, NULL, '风控应用查看', NULL, NULL, NULL, NULL, NULL, NULL, 6, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 8, @menu_id + 1, NULL, '风控应用新增', NULL, 2, NULL, NULL, NULL, '风控应用新增', NULL, NULL, NULL, NULL, NULL, NULL, 7, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 9, @menu_id + 1, NULL, '风控应用编辑', NULL, 2, NULL, NULL, NULL, '风控应用编辑', NULL, NULL, NULL, NULL, NULL, NULL, 8, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 10, @menu_id + 1, NULL, '风控应用删除', NULL, 2, NULL, NULL, NULL, '风控应用删除', NULL, NULL, NULL, NULL, NULL, NULL, 9, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 11, @menu_id + 1, NULL, '风控应用启停', NULL, 2, NULL, NULL, NULL, '风控应用启停', NULL, NULL, NULL, NULL, NULL, NULL, 10, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 12, @menu_id + 1, NULL, '风控变量查看', NULL, 2, NULL, NULL, NULL, '风控变量查看', NULL, NULL, NULL, NULL, NULL, NULL, 11, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 13, @menu_id + 1, NULL, '风控变量新增', NULL, 2, NULL, NULL, NULL, '风控变量新增', NULL, NULL, NULL, NULL, NULL, NULL, 12, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 14, @menu_id + 1, NULL, '风控变量编辑', NULL, 2, NULL, NULL, NULL, '风控变量编辑', NULL, NULL, NULL, NULL, NULL, NULL, 13, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 15, @menu_id + 1, NULL, '风控变量删除', NULL, 2, NULL, NULL, NULL, '风控变量删除', NULL, NULL, NULL, NULL, NULL, NULL, 14, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 16, @menu_id + 1, NULL, '风控日志查看', NULL, 2, NULL, NULL, NULL, '风控日志查看', NULL, NULL, NULL, NULL, NULL, NULL, 15, '', 0, NULL, NULL, NULL, NULL),
|
||||
(@menu_id + 17, @menu_id + 1, NULL, '风控日志导出', NULL, 2, NULL, NULL, NULL, '风控日志导出', NULL, NULL, NULL, NULL, NULL, NULL, 16, '', 0, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- 3.2. sys_role_menu 角色1的按钮权限(menu_id 引用上方 sys_menu 自增 id)
|
||||
-- ----------------------------
|
||||
SELECT IFNULL(MAX(id), 0) INTO @role_menu_id FROM `yltrs_admin`.`sys_role_menu`;
|
||||
|
||||
INSERT INTO `yltrs_admin`.`sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES
|
||||
(@role_menu_id + 1, 1, @menu_id + 1),
|
||||
(@role_menu_id + 2, 1, @menu_id + 2),
|
||||
(@role_menu_id + 3, 1, @menu_id + 3),
|
||||
(@role_menu_id + 4, 1, @menu_id + 4),
|
||||
(@role_menu_id + 5, 1, @menu_id + 5),
|
||||
(@role_menu_id + 6, 1, @menu_id + 6),
|
||||
(@role_menu_id + 7, 1, @menu_id + 7),
|
||||
(@role_menu_id + 8, 1, @menu_id + 8),
|
||||
(@role_menu_id + 9, 1, @menu_id + 9),
|
||||
(@role_menu_id + 10, 1, @menu_id + 10),
|
||||
(@role_menu_id + 11, 1, @menu_id + 11),
|
||||
(@role_menu_id + 12, 1, @menu_id + 12),
|
||||
(@role_menu_id + 13, 1, @menu_id + 13),
|
||||
(@role_menu_id + 14, 1, @menu_id + 14),
|
||||
(@role_menu_id + 15, 1, @menu_id + 15),
|
||||
(@role_menu_id + 16, 1, @menu_id + 16),
|
||||
(@role_menu_id + 17, 1, @menu_id + 17);
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ConditionJson 操作符一次性迁移(基于当前生产 seed 数据)
|
||||
-- 原因:ConditionJson 改用稳定 token,避免 >、< 在接口传输中发生 HTML 转义。
|
||||
-- 仅修改规则 3、14、15、16 的 ConditionJson;RuleExpr、RuleText 和自由文本规则不变。
|
||||
|
||||
USE `yltrs_ylcms`;
|
||||
|
||||
-- 执行前确认当前值,并保存查询结果作为人工备份。
|
||||
SELECT id, RuleName, ConditionJson, RuleExpr
|
||||
FROM glms_risk_rule
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
ORDER BY id;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
UPDATE glms_risk_rule
|
||||
SET ConditionJson = REPLACE(ConditionJson, '"Operator":">"', '"Operator":"gt"')
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
AND ConditionJson LIKE '%"Operator":">"%';
|
||||
|
||||
UPDATE glms_risk_rule
|
||||
SET ConditionJson = REPLACE(ConditionJson, '"Operator":"<"', '"Operator":"lt"')
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
AND ConditionJson LIKE '%"Operator":"<"%';
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- 执行后确认:ConditionJson 使用 token,RuleExpr 保持原简洁公式。
|
||||
SELECT id, RuleName, ConditionJson, RuleExpr
|
||||
FROM glms_risk_rule
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
ORDER BY id;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- ConditionJson 操作符迁移回滚(仅用于应用版本整体回退)
|
||||
-- 只恢复规则 3、14、15、16 的 ConditionJson 操作符,不修改 RuleExpr。
|
||||
|
||||
USE `yltrs_ylcms`;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
UPDATE glms_risk_rule
|
||||
SET ConditionJson = REPLACE(ConditionJson, '"Operator":"gt"', '"Operator":">"')
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
AND ConditionJson LIKE '%"Operator":"gt"%';
|
||||
|
||||
UPDATE glms_risk_rule
|
||||
SET ConditionJson = REPLACE(ConditionJson, '"Operator":"lt"', '"Operator":"<"')
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
AND ConditionJson LIKE '%"Operator":"lt"%';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT id, RuleName, ConditionJson, RuleExpr
|
||||
FROM glms_risk_rule
|
||||
WHERE id IN (3, 14, 15, 16)
|
||||
ORDER BY id;
|
||||
@@ -1,135 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 规则应用初始数据(对应设计文档 §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 = '接近/触发敲入敲出价';
|
||||
@@ -1,50 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 异常交易监控页面 - 操作权限(按钮)数据
|
||||
-- 依赖:异常交易监控菜单 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);
|
||||
@@ -0,0 +1,121 @@
|
||||
-- ============================================================
|
||||
-- 规则应用初始数据(对应当前 glms_risk_rule_application 数据库内容)
|
||||
-- 依赖:seed_risk_engine_rules.sql(规则数据需先插入)
|
||||
--
|
||||
-- 本次仅同步依赖脚本文件名,不修改应用业务数据和 RuleIds。
|
||||
-- 每条记录对应一条应用配置,定义"何时、对谁、怎么处理"
|
||||
-- ControlStrategy: 1=Block(禁止), 2=Approval(审批), 3=Warning(提示)
|
||||
-- TriggerPoints: BOOK_CONFIRM=簿记交易确认
|
||||
-- ScopeIsGlobal: 1=全局适用, 0=按维度配置(Scope字段为空字符串表示"全部")
|
||||
-- ============================================================
|
||||
|
||||
-- 插入当前数据库中的 6 条规则应用,保留原始 ID 和 RuleIds
|
||||
INSERT INTO `yltrs_ylcms`.`glms_risk_rule_application` (`id`, `RuleIds`, `Description`, `Status`, `ControlStrategy`, `TriggerPoints`, `ScopeAssetBookIds`, `ScopeClientIds`, `ScopeUnderlyingTypes`, `ScopeTradeTypes`, `ScopeIsGlobal`, `Version`, `OptId`, `OptName`, `OptDate`, `UpdateOptId`, `UpdateOptName`, `UpdateDate`) VALUES
|
||||
(23,
|
||||
'3',
|
||||
'',
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-13 13:24:03',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:49:39'),
|
||||
(24,
|
||||
'12',
|
||||
'',
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-14 15:46:51',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:49:42'),
|
||||
(25,
|
||||
'13',
|
||||
NULL,
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-15 14:06:10',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:00'),
|
||||
(26,
|
||||
'14',
|
||||
'',
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-15 15:19:30',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:05'),
|
||||
(27,
|
||||
'16',
|
||||
NULL,
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
4,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-15 17:44:02',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:54:11'),
|
||||
(28,
|
||||
'15',
|
||||
'',
|
||||
2,
|
||||
2,
|
||||
'BOOK_CONFIRM',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-15 17:49:56',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:03');
|
||||
@@ -0,0 +1,121 @@
|
||||
-- ============================================================
|
||||
-- 规则初始数据(对应当前 glms_risk_rule 数据库内容)
|
||||
-- 依赖:seed_risk_engine_variables.sql(变量池数据需先插入)
|
||||
--
|
||||
-- 变量 ID 映射(seed_risk_engine_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;
|
||||
-- 区间比较使用 Lower/UpperThresholdType、Lower/UpperValue、Lower/UpperThresholdVariableId、IncludeLower/IncludeUpper。
|
||||
-- 修改原因:Operator 使用 gt/lt 等稳定 token,避免 >、< 在接口传输中发生 HTML 转义。
|
||||
-- RuleExpr 保持现有简洁 C# 公式;编译前类型转换由 RiskEngineCompiler 负责。
|
||||
-- 展示字段(VariableName/VariableType/Unit/ThresholdVariableName)从变量接口关联获取
|
||||
-- ============================================================
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 插入当前数据库中的 6 条规则,保留原始 ID 以匹配规则应用表 RuleIds
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO `yltrs_ylcms`.`glms_risk_rule` (`id`, `RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Description`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`, `UpdateOptId`, `UpdateOptName`, `UpdateDate`) VALUES
|
||||
(3,
|
||||
'名义本金超阈值',
|
||||
'合约名义本金 > 1亿',
|
||||
'[{"VariableId":1,"Operator":"gt","ThresholdType":"Fixed","Value":100000000}]',
|
||||
'DbContext.trade.First(t => t.id == TradeId).StockEqvNotional > 100000000',
|
||||
'',
|
||||
2,
|
||||
14,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:33'),
|
||||
(12,
|
||||
'债券类净价偏离',
|
||||
'期初净价与上一收盘日中债估值净价绝对价差大于5元时触发审批',
|
||||
'',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiNetNoFeePrice.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderBy(v => v.credibility).ThenByDescending(v => v.valuation_date).First().net_price.Value) > 5m',
|
||||
NULL,
|
||||
2,
|
||||
9,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:29'),
|
||||
(13,
|
||||
'债券类收益率偏离',
|
||||
'',
|
||||
'',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).InitYtm.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderBy(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value) > 0.4m',
|
||||
'',
|
||||
2,
|
||||
10,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:21'),
|
||||
(14,
|
||||
'非债券类价格偏离',
|
||||
'TRS非债券价格偏离绝对值 > 5元',
|
||||
'[{"VariableId":47,"Operator":"gt","ThresholdType":"Fixed","Value":5}]',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiGrossPrice * 100 - Convert.ToDecimal(DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).First().ClosePrice)) > 5',
|
||||
'',
|
||||
2,
|
||||
8,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:37'),
|
||||
(15,
|
||||
'单一交易对手累计标的数量超阈值',
|
||||
'对手方累计标的数量 > 10个',
|
||||
'[{"VariableId":32,"Operator":"gt","ThresholdType":"Fixed","Value":10}]',
|
||||
'DbContext.swap_position.Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) && !p.IsInitial && p.PosiQuantity > 0 && !p.Invalid && p.PosiDirection > 0 && DbContext.trade.Any(t => t.id == p.SwapTradeId && t.ValidState != "InValid" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId)).Select(p => p.UnderlyingCode).Distinct().Count() > 10',
|
||||
'',
|
||||
2,
|
||||
8,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:50:18'),
|
||||
(16,
|
||||
'多头支付固定端利率偏离',
|
||||
'利息端利率与FR007偏离度 > 3%',
|
||||
'[{"VariableId":48,"Operator":"gt","ThresholdType":"Fixed","Value":3}]',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.InterestDirection == 1).InterestRateDefault)*100m > 3',
|
||||
'',
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 14:54:18');
|
||||
@@ -0,0 +1,112 @@
|
||||
-- ============================================================
|
||||
-- 变量池初始数据(对应当前 glms_risk_variable 数据库内容)
|
||||
-- ============================================================
|
||||
|
||||
-- 4.4.1 簿记要素类(Category=1)
|
||||
-- 4.4.2 行情类(Category=2,统一取上一交易日收盘价)
|
||||
-- 4.4.3 系统计算值类(Category=3)
|
||||
-- 新增变量(ID 45~53):
|
||||
-- 45=期初净价偏离度 46=期初收益率偏离度 47=期初价格偏离度
|
||||
-- 48=利息端利率与FR007偏离度 49=利息端利率与借贷加权费率偏离度
|
||||
-- 50=执行价偏离度 51=执行价 52=客户品种最低保证金率 53=参考价格
|
||||
-- 4.4.4 布尔判断类(Category=4)
|
||||
-- 说明:结构化 RuleExpr 的固定数值不追加 m;本文件 VariableExpr 为人工维护的 C# 表达式,原有 m 后缀保持不变。
|
||||
|
||||
-- 插入当前数据库中的 6 条变量,保留原始 ID 以匹配规则 ConditionJson 中的 VariableId
|
||||
INSERT INTO `yltrs_ylcms`.`glms_risk_variable` (`id`, `VariableName`, `Category`, `DataType`, `Unit`, `ValueDomain`, `Description`, `VariableExpr`, `Version`, `SortOrder`, `OptId`, `OptName`, `OptDate`, `UpdateOptId`, `UpdateOptName`, `UpdateDate`) VALUES
|
||||
(1,
|
||||
'合约名义本金',
|
||||
1,
|
||||
1,
|
||||
'元',
|
||||
'≥ 0',
|
||||
'',
|
||||
'DbContext.trade.First(t => t.id == TradeId).StockEqvNotional',
|
||||
18,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-23 20:13:16',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 10:02:27'),
|
||||
(32,
|
||||
'对手方累计标的数量',
|
||||
3,
|
||||
1,
|
||||
'个',
|
||||
'≥ 0',
|
||||
'当前交易对手方所有存续交易涉及的标的数量(去重)合计(含本笔)',
|
||||
'DbContext.swap_position.Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) && !p.IsInitial && p.PosiQuantity > 0 && !p.Invalid && p.PosiDirection > 0 && DbContext.trade.Any(t => t.id == p.SwapTradeId && t.ValidState != "InValid" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId)).Select(p => p.UnderlyingCode).Distinct().Count()',
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-23 20:13:16',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 11:20:06'),
|
||||
(45,
|
||||
'TRS债券净价偏离绝对值',
|
||||
3,
|
||||
1,
|
||||
'%',
|
||||
'≥ 0',
|
||||
'abs(期初标的交割净价% - 上一收盘日中债估值净价)',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiNetNoFeePrice.Value * 100 - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.valuation_date).First().net_price.Value)',
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 13:28:12'),
|
||||
(46,
|
||||
'TRS债券收益率偏离绝对值',
|
||||
3,
|
||||
1,
|
||||
'%',
|
||||
'≥ 0',
|
||||
'abs(期初标的成交收益率% - 上一收盘日中债估值收益率)',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).InitYtm.Value * 100 - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderBy(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value)',
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 13:28:30'),
|
||||
(47,
|
||||
'TRS非债券价格偏离绝对值',
|
||||
3,
|
||||
1,
|
||||
'元',
|
||||
'≥ 0',
|
||||
'abs(期初标的交割全价% - 上一日标的收盘价)',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).PosiGrossPrice * 100 - Convert.ToDecimal(DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).First().ClosePrice))',
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-17 13:29:43'),
|
||||
(48,
|
||||
'利息端利率与FR007偏离度',
|
||||
3,
|
||||
1,
|
||||
'%',
|
||||
'≥ 0',
|
||||
'ABS(利息端利率-FR007)/FR007*100',
|
||||
'Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.InterestDirection == 1).InterestRateDefault)*100m',
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
'SYSTEM',
|
||||
'2026-06-24 15:51:47',
|
||||
1,
|
||||
'初始用户',
|
||||
'2026-07-16 19:23:05');
|
||||
@@ -1,292 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 规则初始数据(对应设计文档 §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());
|
||||
@@ -1,73 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 变量池初始数据(对应设计文档 §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());
|
||||
@@ -0,0 +1,322 @@
|
||||
using YLErp.Commons;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.RiskEngine.Dto;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
[TestClass]
|
||||
public class RuleConditionExpressionBuilderTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void Build_NumericFixedValue_UsesNormalizedLiteralWithoutSuffix()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "gt",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "123.4500"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount > 123.45",
|
||||
expression);
|
||||
Assert.IsFalse(expression.Contains("123.45m"));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("0", "0")]
|
||||
[DataRow("-0", "0")]
|
||||
[DataRow("123.4500", "123.45")]
|
||||
[DataRow("1e3", "1000")]
|
||||
[DataRow("1e-28", "0.0000000000000000000000000001")]
|
||||
public void Build_NumericFixedValue_NormalizesLikeFrontend(string input, string expectedValue)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = input
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"));
|
||||
|
||||
Assert.AreEqual(
|
||||
$"amount == {expectedValue}",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericFixedValue_RejectsThousandsSeparator()
|
||||
{
|
||||
Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "1,000"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount")));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("gt", ">")]
|
||||
[DataRow("lt", "<")]
|
||||
[DataRow("gte", ">=")]
|
||||
[DataRow("lte", "<=")]
|
||||
[DataRow("eq", "==")]
|
||||
[DataRow("ne", "!=")]
|
||||
public void Build_ComparisonTokens_MapToCSharpOperators(string token, string expectedOperator)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = token,
|
||||
ThresholdType = "Fixed",
|
||||
Value = "0"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"));
|
||||
|
||||
StringAssert.Contains(expression, $" {expectedOperator} ");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericVariableThreshold_UsesStoredExpressions()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "lte",
|
||||
ThresholdType = "Variable",
|
||||
ThresholdVariableId = 2
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "limit.Amount", "元"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount <= limit.Amount",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericVariableThreshold_RejectsDifferentUnit()
|
||||
{
|
||||
var exception = Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "lte",
|
||||
ThresholdType = "Variable",
|
||||
ThresholdVariableId = 2
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
|
||||
|
||||
StringAssert.Contains(exception.Message, "单位不一致");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericRangeVariableBound_RejectsDifferentUnit()
|
||||
{
|
||||
var exception = Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "between",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "0",
|
||||
UpperThresholdType = "Variable",
|
||||
UpperThresholdVariableId = 2,
|
||||
IncludeLower = true,
|
||||
IncludeUpper = true
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
|
||||
|
||||
StringAssert.Contains(exception.Message, "单位不一致");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_FixedDate_UsesDateConstructorTemplate()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "2026-07-21"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
|
||||
|
||||
Assert.AreEqual("trade.TradeDate == new DateTime(2026, 7, 21)", expression);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("isTrue", "trade.IsConfirmed == true")]
|
||||
[DataRow("isFalse", "trade.IsConfirmed == false")]
|
||||
public void Build_BooleanToken_UsesBooleanLiteral(string token, string expected)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition { VariableId = 1, Operator = token },
|
||||
Variable(1, RiskVariableDataType.Boolean, "trade.IsConfirmed"));
|
||||
|
||||
Assert.AreEqual(expected, expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_Between_AllowsMixedFixedAndVariableBounds()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "between",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "-1.25",
|
||||
UpperThresholdType = "Variable",
|
||||
UpperThresholdVariableId = 2,
|
||||
IncludeLower = false,
|
||||
IncludeUpper = true
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "limit.Upper"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount > -1.25 && trade.Amount <= limit.Upper",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NotBetween_NegatesCompleteRangeExpression()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "notBetween",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "2026-01-01",
|
||||
UpperThresholdType = "Fixed",
|
||||
UpperValue = "2026-12-31",
|
||||
IncludeLower = true,
|
||||
IncludeUpper = false
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"!(trade.TradeDate >= new DateTime(2026, 1, 1) && trade.TradeDate < new DateTime(2026, 12, 31))",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_MultipleConditions_UsesExactAndSeparator()
|
||||
{
|
||||
var conditions = new[]
|
||||
{
|
||||
new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "0" },
|
||||
new RuleCondition { VariableId = 2, Operator = "isTrue" }
|
||||
};
|
||||
var variables = Variables(
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"),
|
||||
Variable(2, RiskVariableDataType.Boolean, "isValid"));
|
||||
|
||||
var expression = RuleConditionExpressionBuilder.Build(conditions, variables);
|
||||
|
||||
Assert.AreEqual(
|
||||
"amount > 0 && isValid == true",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_RejectsNonRangeConditionWithRangeFields()
|
||||
{
|
||||
var condition = new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "gt",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "0",
|
||||
IncludeLower = true
|
||||
};
|
||||
|
||||
Assert.ThrowsException<ServiceException>(() => Build(
|
||||
condition,
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferencedVariableIds_IgnoreNullConditionItems()
|
||||
{
|
||||
var conditions = new RuleCondition[]
|
||||
{
|
||||
null,
|
||||
new RuleCondition { VariableId = 7 }
|
||||
};
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new long[] { 7 },
|
||||
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferencedVariableIds_IncludeAllFourFields()
|
||||
{
|
||||
var conditions = new[]
|
||||
{
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
ThresholdVariableId = 2,
|
||||
LowerThresholdVariableId = 3,
|
||||
UpperThresholdVariableId = 4
|
||||
}
|
||||
};
|
||||
|
||||
CollectionAssert.AreEquivalent(
|
||||
new long[] { 1, 2, 3, 4 },
|
||||
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsVariableReferenced_MatchesAllFieldsWithoutIdPrefixCollision()
|
||||
{
|
||||
const string json = "[{\"VariableId\":10,\"ThresholdVariableId\":20,\"LowerThresholdVariableId\":30,\"UpperThresholdVariableId\":40}]";
|
||||
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 10));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 20));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 30));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 40));
|
||||
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 1));
|
||||
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 4));
|
||||
}
|
||||
|
||||
private static string Build(RuleCondition condition, params glms_risk_variable[] variables)
|
||||
{
|
||||
return RuleConditionExpressionBuilder.Build(new[] { condition }, Variables(variables));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<long, glms_risk_variable> Variables(params glms_risk_variable[] variables)
|
||||
{
|
||||
return variables.ToDictionary(variable => (long)variable.id);
|
||||
}
|
||||
|
||||
private static glms_risk_variable Variable(long id, RiskVariableDataType dataType, string expression, string unit = null)
|
||||
{
|
||||
return new glms_risk_variable
|
||||
{
|
||||
id = (int)id,
|
||||
VariableName = $"V{id}",
|
||||
DataType = dataType,
|
||||
VariableExpr = expression,
|
||||
Unit = unit
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ namespace YLErp
|
||||
[ModuleInitializer]
|
||||
internal static void M1()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("YLErp_UNIT_TEST_SKIP_INITIALIZATION"), "1", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
Console.WriteLine("version--" + AppManager.Version);
|
||||
|
||||
var logger = NLog.LogManager.Setup().GetCurrentClassLogger();
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
namespace YLErp.Modules.RiskEngine.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则条件项
|
||||
/// 结构化规则条件项。Operator 使用稳定语义 token,不存储 UI 文案或 C# 操作符。
|
||||
/// </summary>
|
||||
public class RuleCondition
|
||||
{
|
||||
/// <summary>条件变量 ID</summary>
|
||||
public long VariableId { get; set; }
|
||||
/// <summary>操作符(如 Equal、GreaterThan、Between 等)</summary>
|
||||
/// <summary>操作符 token:gt/lt/gte/lte/eq/ne/between/notBetween/isTrue/isFalse</summary>
|
||||
public string Operator { get; set; }
|
||||
/// <summary>阈值类型(FixedValue / VariableRef)</summary>
|
||||
|
||||
/// <summary>普通比较阈值类型:Fixed / Variable</summary>
|
||||
public string ThresholdType { get; set; }
|
||||
/// <summary>阈值(固定值或变量引用)</summary>
|
||||
/// <summary>普通比较固定阈值</summary>
|
||||
public object Value { get; set; }
|
||||
/// <summary>阈值变量 ID(当 ThresholdType 为 VariableRef 时)</summary>
|
||||
/// <summary>普通比较阈值变量 ID</summary>
|
||||
public long? ThresholdVariableId { get; set; }
|
||||
/// <summary>介于/不介于的下边界是否包含,默认 true</summary>
|
||||
public bool IncludeLowerBound { get; set; } = true;
|
||||
/// <summary>介于/不介于的上边界是否包含,默认 true</summary>
|
||||
public bool IncludeUpperBound { get; set; } = true;
|
||||
|
||||
/// <summary>区间下限阈值类型:Fixed / Variable</summary>
|
||||
public string LowerThresholdType { get; set; }
|
||||
/// <summary>区间固定下限</summary>
|
||||
public object LowerValue { get; set; }
|
||||
/// <summary>区间下限变量 ID</summary>
|
||||
public long? LowerThresholdVariableId { get; set; }
|
||||
/// <summary>区间上限阈值类型:Fixed / Variable</summary>
|
||||
public string UpperThresholdType { get; set; }
|
||||
/// <summary>区间固定上限</summary>
|
||||
public object UpperValue { get; set; }
|
||||
/// <summary>区间上限变量 ID</summary>
|
||||
public long? UpperThresholdVariableId { get; set; }
|
||||
/// <summary>是否包含下限</summary>
|
||||
public bool? IncludeLower { get; set; }
|
||||
/// <summary>是否包含上限</summary>
|
||||
public bool? IncludeUpper { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using BaseOUDAL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -208,12 +206,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
"FUND_PAYMENT"
|
||||
};
|
||||
|
||||
private static readonly Dictionary<RiskVariableDataType, HashSet<string>> ValidOperatorsByType = new Dictionary<RiskVariableDataType, HashSet<string>>
|
||||
{
|
||||
{ RiskVariableDataType.Numeric, new HashSet<string> { ">", "<", ">=", "<=", "=", "≠", "介于", "不介于" } },
|
||||
{ RiskVariableDataType.Date, new HashSet<string> { "早于", "晚于", "等于", "不早于", "不晚于", "介于", "不介于" } },
|
||||
{ RiskVariableDataType.Boolean, new HashSet<string> { "是", "否" } }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> AuditOperationTypeNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -361,90 +353,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 ConditionJson(结构化模式:变量存在性、操作符、类型匹配)
|
||||
/// 校验 ConditionJson 并生成服务端期望的简洁 RuleExpr。
|
||||
/// </summary>
|
||||
private void ValidateConditionJson(string conditionJson)
|
||||
private string ValidateConditionJson(string conditionJson)
|
||||
{
|
||||
List<RuleCondition> conditions;
|
||||
try
|
||||
{
|
||||
conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(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");
|
||||
}
|
||||
}
|
||||
return BuildRuleExprFromConditionJson(conditionJson);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 RuleExpr(自由文本模式:括号匹配)
|
||||
/// </summary>
|
||||
@@ -599,7 +513,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
/// <summary>
|
||||
/// 校验规则参数(名称 + ConditionJson + RuleExpr)
|
||||
/// </summary>
|
||||
private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr)
|
||||
private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr, long? ruleId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruleName))
|
||||
throw new ServiceException("规则名称不能为空");
|
||||
@@ -611,7 +525,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(conditionJson))
|
||||
{
|
||||
ValidateConditionJson(conditionJson);
|
||||
var expectedRuleExpr = ValidateConditionJson(conditionJson);
|
||||
if (!string.Equals(expectedRuleExpr, ruleExpr, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.Info($"结构化规则公式一致性校验失败。RuleId={ruleId?.ToString() ?? "NEW"}; Expected={expectedRuleExpr}; Actual={ruleExpr}");
|
||||
throw new ServiceException("公式与结构化条件不一致,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
ValidateRuleExpr(ruleExpr);
|
||||
@@ -816,6 +735,9 @@ namespace YLErp.Modules.RiskEngine
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 汇总逐项结果。业务失败彼此隔离,只有 Succeeded 表示实体已实际变更。
|
||||
/// </summary>
|
||||
private static BatchOperationResult BuildBatchResult(List<BatchOperationItemResult> items)
|
||||
{
|
||||
var result = new BatchOperationResult
|
||||
@@ -836,6 +758,39 @@ namespace YLErp.Modules.RiskEngine
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录批量业务结果。预期业务失败使用 Info,避免触发生产 Error 告警。
|
||||
/// </summary>
|
||||
private void LogBatchResult(string operationName, BatchOperationResult result)
|
||||
{
|
||||
const int maxFailedItems = 20;
|
||||
var failedItems = result.Items
|
||||
.Where(item => item.Status == BatchOperationItemStatus.Failed)
|
||||
.Take(maxFailedItems)
|
||||
.Select(item => $"{item.Id}:{item.Code}")
|
||||
.ToList();
|
||||
var failedSummary = failedItems.Any() ? string.Join(",", failedItems) : "无";
|
||||
var truncated = result.FailedCount > maxFailedItems ? ",...(已截断)" : string.Empty;
|
||||
|
||||
_logger.Info($"[{operationName}] 批量操作完成。Total={result.TotalCount}, Succeeded={result.SucceededCount}, Unchanged={result.UnchangedCount}, Failed={result.FailedCount}, FailedItems={failedSummary}{truncated}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据库提交完成后刷新缓存;刷新失败仅记录 Error 告警,不将已提交操作返回为失败。
|
||||
/// </summary>
|
||||
private void TryRefreshCache(Action refreshAction, string cacheName, IEnumerable<long> affectedIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
refreshAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var ids = affectedIds?.Distinct().Take(20).ToList() ?? new List<long>();
|
||||
_logger.Error($"{cacheName}刷新失败,AffectedIds={string.Join(",", ids)}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rule Management
|
||||
@@ -994,7 +949,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (rule.Version != req.Version)
|
||||
throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑");
|
||||
|
||||
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
|
||||
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr, ruleId);
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, req.RuleExpr);
|
||||
if (!compileResult.Success)
|
||||
@@ -1025,7 +980,6 @@ namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
var rule = GetRuleOrThrow(ruleId);
|
||||
|
||||
|
||||
var activeCount = DbContext.glms_risk_rule_application
|
||||
.Where(a => a.Status == RiskRuleStatus.Active)
|
||||
.Count(RuleIdsMatchExpr(ruleId));
|
||||
@@ -1206,12 +1160,16 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
if (changedIds.Any())
|
||||
{
|
||||
// 所有成功项一次原子提交;提交完成后,缓存失败仅告警,不改变数据库成功结果。
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
foreach (var id in changedIds)
|
||||
RiskEngineService.GetInstance().RefreshOneRuleCache(id);
|
||||
TryRefreshCache(
|
||||
() => changedIds.ForEach(id => RiskEngineService.GetInstance().RefreshOneRuleCache(id)),
|
||||
"风控规则缓存", changedIds);
|
||||
}
|
||||
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量删除规则", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1260,12 +1218,16 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
if (changedIds.Any())
|
||||
{
|
||||
// 仅成功启用项参与同一次提交和审计,编译失败不影响其他规则。
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
foreach (var id in changedIds)
|
||||
RiskEngineService.GetInstance().RefreshOneRuleCache(id);
|
||||
TryRefreshCache(
|
||||
() => changedIds.ForEach(id => RiskEngineService.GetInstance().RefreshOneRuleCache(id)),
|
||||
"风控规则缓存", changedIds);
|
||||
}
|
||||
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量启用规则", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1309,6 +1271,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DISABLED", "规则已停用", cascadesByRule[id]));
|
||||
}
|
||||
|
||||
// 多条规则可能关联同一应用;实际状态修改、审计和顶层级联结果均按应用 ID 去重。
|
||||
var cascadedIds = items.SelectMany(i => i.CascadedApplicationIds).Distinct().ToHashSet();
|
||||
foreach (var app in activeApps.Where(a => cascadedIds.Contains(a.id)))
|
||||
{
|
||||
@@ -1323,14 +1286,19 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded))
|
||||
{
|
||||
// 规则及其级联应用在同一次数据库提交中生效。
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
foreach (var id in activeRules)
|
||||
RiskEngineService.GetInstance().RefreshOneRuleCache(id);
|
||||
TryRefreshCache(
|
||||
() => activeRules.ForEach(id => RiskEngineService.GetInstance().RefreshOneRuleCache(id)),
|
||||
"风控规则缓存", activeRules);
|
||||
if (cascadedIds.Any())
|
||||
RiskEngineService.GetInstance().RefreshApplication();
|
||||
TryRefreshCache(() => RiskEngineService.GetInstance().RefreshApplication(),
|
||||
"风控应用缓存", cascadedIds);
|
||||
}
|
||||
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量停用规则", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1659,9 +1627,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded))
|
||||
{
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
RiskEngineService.GetInstance().RefreshApplication();
|
||||
TryRefreshCache(() => RiskEngineService.GetInstance().RefreshApplication(),
|
||||
"风控应用缓存", items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id));
|
||||
}
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量删除应用配置", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1730,6 +1701,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "应用配置不存在"));
|
||||
continue;
|
||||
}
|
||||
// Deleted 是终态,不能通过批量启用恢复,避免已删除配置重新参与风控执行。
|
||||
if (app.Status == RiskRuleStatus.Deleted)
|
||||
{
|
||||
items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "DELETED_ENTITY", "已删除应用不能启用"));
|
||||
@@ -1761,9 +1733,12 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded))
|
||||
{
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
RiskEngineService.GetInstance().RefreshApplication();
|
||||
TryRefreshCache(() => RiskEngineService.GetInstance().RefreshApplication(),
|
||||
"风控应用缓存", items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id));
|
||||
}
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量启用应用配置", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1807,210 +1782,47 @@ namespace YLErp.Modules.RiskEngine
|
||||
if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded))
|
||||
{
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
RiskEngineService.GetInstance().RefreshApplication();
|
||||
TryRefreshCache(() => RiskEngineService.GetInstance().RefreshApplication(),
|
||||
"风控应用缓存", items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id));
|
||||
}
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量停用应用配置", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 查询 ConditionJson 中引用了指定变量 ID 的规则(仅结构化模式)
|
||||
/// 精确匹配 "VariableId":{id} 或 "ThresholdVariableId":{id},\b 防止 id=1 误匹配 id=10
|
||||
/// 查询 ConditionJson 中四类变量引用字段命中指定变量 ID 的规则(仅结构化模式)。
|
||||
/// </summary>
|
||||
private List<glms_risk_rule> GetRulesByVariableId(long variableId)
|
||||
{
|
||||
var allRules = DbContext.glms_risk_rule
|
||||
.Where(r => r.Status != RiskRuleStatus.Deleted && r.ConditionJson != null)
|
||||
.Where(r => r.Status != RiskRuleStatus.Deleted && r.ConditionJson != null && r.ConditionJson != "")
|
||||
.ToList();
|
||||
|
||||
if (allRules.Count == 0) return allRules;
|
||||
|
||||
var idPattern = $@"""VariableId""\s*:\s*{variableId}\b";
|
||||
var thresholdPattern = $@"""ThresholdVariableId""\s*:\s*{variableId}\b";
|
||||
|
||||
return allRules.Where(r =>
|
||||
Regex.IsMatch(r.ConditionJson, idPattern) ||
|
||||
Regex.IsMatch(r.ConditionJson, thresholdPattern)
|
||||
).ToList();
|
||||
return allRules
|
||||
.Where(rule => RuleConditionExpressionBuilder.IsVariableReferenced(rule.ConditionJson, variableId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据结构化 ConditionJson 和当前变量池定义重新生成可执行 RuleExpr。
|
||||
/// ConditionJson 是结构化规则的事实来源,避免通过字符串 Replace 级联修改表达式。
|
||||
/// 根据结构化 ConditionJson 和当前变量池定义重新生成简洁 RuleExpr。
|
||||
/// </summary>
|
||||
private string BuildRuleExprFromConditionJson(string conditionJson)
|
||||
{
|
||||
List<RuleCondition> conditions;
|
||||
try
|
||||
{
|
||||
conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(conditionJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ServiceException("公式表达式 JSON 格式不合法");
|
||||
}
|
||||
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
throw new ServiceException("公式条件列表不能为空");
|
||||
|
||||
var variableIds = conditions
|
||||
.Select(c => c.VariableId)
|
||||
.Concat(conditions.Where(c => c.ThresholdVariableId.HasValue)
|
||||
.Select(c => c.ThresholdVariableId.Value))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var conditions = RuleConditionExpressionBuilder.DeserializeConditions(conditionJson);
|
||||
var variableIds = RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToList();
|
||||
var variables = DbContext.glms_risk_variable
|
||||
.Where(v => variableIds.Contains(v.id))
|
||||
.Where(variable => variableIds.Contains(variable.id))
|
||||
.ToList()
|
||||
.ToDictionary(v => (long)v.id);
|
||||
.ToDictionary(variable => (long)variable.id);
|
||||
|
||||
var missingVariableIds = variableIds.Where(id => !variables.ContainsKey(id)).ToList();
|
||||
if (missingVariableIds.Any())
|
||||
throw new ServiceException($"公式引用的变量不存在:{string.Join(",", missingVariableIds)}");
|
||||
|
||||
var conditionExpressions = conditions
|
||||
.Select((condition, index) => BuildConditionExpression(condition, variables, index + 1))
|
||||
.ToList();
|
||||
|
||||
return string.Join(" && ", conditionExpressions.Select(expr => $"({expr})"));
|
||||
}
|
||||
|
||||
private string BuildConditionExpression(
|
||||
RuleCondition condition,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
int conditionIndex)
|
||||
{
|
||||
var variable = variables[condition.VariableId];
|
||||
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
|
||||
throw new ServiceException($"条件{conditionIndex}:变量'{variable.VariableName}'的取值表达式为空");
|
||||
|
||||
if (!ValidOperatorsByType.TryGetValue(variable.DataType, out var validOperators) ||
|
||||
!validOperators.Contains(condition.Operator))
|
||||
{
|
||||
throw new ServiceException($"条件{conditionIndex}:操作符'{condition.Operator}'不适用于{GetDataTypeName(variable.DataType)}类型变量");
|
||||
}
|
||||
|
||||
if (variable.DataType == RiskVariableDataType.Boolean)
|
||||
{
|
||||
return condition.Operator switch
|
||||
{
|
||||
"是" => $"({variable.VariableExpr}) == true",
|
||||
"否" => $"({variable.VariableExpr}) == false",
|
||||
_ => throw new ServiceException($"条件{conditionIndex}:不支持的布尔操作符'{condition.Operator}'")
|
||||
};
|
||||
}
|
||||
|
||||
var leftExpression = BuildComparableExpression(variable);
|
||||
if (condition.Operator == "介于" || condition.Operator == "不介于")
|
||||
{
|
||||
if (!string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
|
||||
throw new ServiceException($"条件{conditionIndex}:介于/不介于暂只支持固定上下限");
|
||||
if (condition.Value is not JArray range || range.Count != 2)
|
||||
throw new ServiceException($"条件{conditionIndex}:介于/不介于的阈值必须为双元素数组");
|
||||
|
||||
var lowerExpression = BuildFixedValueExpression(range[0], variable.DataType, conditionIndex);
|
||||
var upperExpression = BuildFixedValueExpression(range[1], variable.DataType, conditionIndex);
|
||||
var lowerOperator = condition.IncludeLowerBound ? ">=" : ">";
|
||||
var upperOperator = condition.IncludeUpperBound ? "<=" : "<";
|
||||
var rangeExpression = $"(({leftExpression}) {lowerOperator} ({lowerExpression}) && ({leftExpression}) {upperOperator} ({upperExpression}))";
|
||||
|
||||
return condition.Operator == "不介于" ? $"!{rangeExpression}" : rangeExpression;
|
||||
}
|
||||
|
||||
var rightExpression = BuildThresholdExpression(condition, variable.DataType, variables, conditionIndex);
|
||||
var comparisonOperator = GetComparisonOperator(condition.Operator, conditionIndex);
|
||||
return $"({leftExpression}) {comparisonOperator} ({rightExpression})";
|
||||
}
|
||||
|
||||
private static string BuildComparableExpression(glms_risk_variable variable)
|
||||
{
|
||||
return variable.DataType == RiskVariableDataType.Numeric
|
||||
? $"Convert.ToDecimal(({variable.VariableExpr}))"
|
||||
: variable.VariableExpr;
|
||||
}
|
||||
|
||||
private string BuildThresholdExpression(
|
||||
RuleCondition condition,
|
||||
RiskVariableDataType dataType,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
int conditionIndex)
|
||||
{
|
||||
if (string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
|
||||
return BuildFixedValueExpression(condition.Value, dataType, conditionIndex);
|
||||
|
||||
if (string.Equals(condition.ThresholdType, "Variable", StringComparison.Ordinal))
|
||||
{
|
||||
if (!condition.ThresholdVariableId.HasValue ||
|
||||
!variables.TryGetValue(condition.ThresholdVariableId.Value, out var thresholdVariable))
|
||||
{
|
||||
throw new ServiceException($"条件{conditionIndex}:阈值变量不存在");
|
||||
}
|
||||
|
||||
if (thresholdVariable.DataType != dataType)
|
||||
throw new ServiceException($"条件{conditionIndex}:阈值变量与条件变量的数据类型不一致");
|
||||
if (string.IsNullOrWhiteSpace(thresholdVariable.VariableExpr))
|
||||
throw new ServiceException($"条件{conditionIndex}:阈值变量'{thresholdVariable.VariableName}'的取值表达式为空");
|
||||
|
||||
return BuildComparableExpression(thresholdVariable);
|
||||
}
|
||||
|
||||
throw new ServiceException($"条件{conditionIndex}:阈值类型'{condition.ThresholdType}'不合法,仅支持 Fixed/Variable");
|
||||
}
|
||||
|
||||
private static string BuildFixedValueExpression(object value, RiskVariableDataType dataType, int conditionIndex)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ServiceException($"条件{conditionIndex}:固定阈值不能为空");
|
||||
|
||||
var rawValue = value is JValue jsonValue
|
||||
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
|
||||
if (dataType == RiskVariableDataType.Numeric)
|
||||
{
|
||||
if (!decimal.TryParse(rawValue, NumberStyles.Number | NumberStyles.AllowExponent,
|
||||
CultureInfo.InvariantCulture, out var numericValue))
|
||||
{
|
||||
throw new ServiceException($"条件{conditionIndex}:数值型阈值必须为数字");
|
||||
}
|
||||
|
||||
return numericValue.ToString(CultureInfo.InvariantCulture) + "m";
|
||||
}
|
||||
|
||||
if (dataType == RiskVariableDataType.Date)
|
||||
{
|
||||
if (!DateTime.TryParse(rawValue, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.RoundtripKind, out var dateValue) &&
|
||||
!DateTime.TryParse(rawValue, out dateValue))
|
||||
{
|
||||
throw new ServiceException($"条件{conditionIndex}:日期型阈值必须为合法日期");
|
||||
}
|
||||
|
||||
return $"new DateTime({dateValue.Ticks}L, DateTimeKind.{dateValue.Kind})";
|
||||
}
|
||||
|
||||
throw new ServiceException($"条件{conditionIndex}:不支持的数据类型");
|
||||
}
|
||||
|
||||
private static string GetComparisonOperator(string ruleOperator, int conditionIndex)
|
||||
{
|
||||
return ruleOperator switch
|
||||
{
|
||||
">" => ">",
|
||||
"<" => "<",
|
||||
">=" => ">=",
|
||||
"<=" => "<=",
|
||||
"=" => "==",
|
||||
"≠" => "!=",
|
||||
"早于" => "<",
|
||||
"晚于" => ">",
|
||||
"等于" => "==",
|
||||
"不早于" => ">=",
|
||||
"不晚于" => "<=",
|
||||
_ => throw new ServiceException($"条件{conditionIndex}:不支持的操作符'{ruleOperator}'")
|
||||
};
|
||||
return RuleConditionExpressionBuilder.Build(conditions, variables);
|
||||
}
|
||||
#region Variable Management
|
||||
|
||||
@@ -2163,6 +1975,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
}
|
||||
|
||||
var oldVariableExpr = variable.VariableExpr;
|
||||
var oldDataType = variable.DataType;
|
||||
|
||||
variable.VariableName = req.VariableName;
|
||||
variable.Category = req.Category;
|
||||
@@ -2177,32 +1990,34 @@ namespace YLErp.Modules.RiskEngine
|
||||
variable.UpdateOptName = UserName;
|
||||
variable.UpdateDate = DateTime.Now;
|
||||
|
||||
var variableExprChanged = oldVariableExpr != req.VariableExpr;
|
||||
var variableDefinitionChanged =
|
||||
oldVariableExpr != req.VariableExpr || oldDataType != req.DataType;
|
||||
|
||||
if (variableExprChanged)
|
||||
if (variableDefinitionChanged)
|
||||
{
|
||||
// 结构化模式:通过 ConditionJson 中的 VariableId 找到受影响的规则
|
||||
var affectedRules = GetRulesByVariableId(variableId)
|
||||
.Where(r => r.RuleExpr != null)
|
||||
.ToList();
|
||||
|
||||
var rebuiltRuleExpressions = new Dictionary<long, string>();
|
||||
foreach (var rule in affectedRules)
|
||||
{
|
||||
var rebuiltRuleExpr = BuildRuleExprFromConditionJson(rule.ConditionJson);
|
||||
ValidateRuleExpr(rebuiltRuleExpr);
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rebuiltRuleExpr);
|
||||
if (!compileResult.Success)
|
||||
throw new ServiceException($"变量表达式变更导致规则'{rule.RuleName}'(ID:{rule.id})编译失败:{compileResult.ErrorMessage}");
|
||||
|
||||
rebuiltRuleExpressions[rule.id] = rebuiltRuleExpr;
|
||||
}
|
||||
|
||||
List<glms_risk_rule> affectedRules;
|
||||
using (var transaction = DbContext.Database.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在同一事务内完成引用发现、公式重建、编译和持久化;任一规则失败时整体回滚。
|
||||
affectedRules = GetRulesByVariableId(variableId)
|
||||
.Where(r => r.RuleExpr != null)
|
||||
.ToList();
|
||||
|
||||
var rebuiltRuleExpressions = new Dictionary<long, string>();
|
||||
foreach (var rule in affectedRules)
|
||||
{
|
||||
var rebuiltRuleExpr = BuildRuleExprFromConditionJson(rule.ConditionJson);
|
||||
ValidateRuleExpr(rebuiltRuleExpr);
|
||||
|
||||
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rebuiltRuleExpr);
|
||||
if (!compileResult.Success)
|
||||
throw new ServiceException($"变量定义变更导致规则'{rule.RuleName}'(ID:{rule.id})编译失败:{compileResult.ErrorMessage}");
|
||||
|
||||
rebuiltRuleExpressions[rule.id] = rebuiltRuleExpr;
|
||||
}
|
||||
|
||||
foreach (var rule in affectedRules)
|
||||
{
|
||||
rule.RuleExpr = rebuiltRuleExpressions[rule.id];
|
||||
@@ -2212,7 +2027,7 @@ namespace YLErp.Modules.RiskEngine
|
||||
rule.UpdateDate = DateTime.Now;
|
||||
|
||||
WriteAuditLog("VAR_CASCADE_UPDATE_RULE", "RULE", rule.id, rule.RuleName,
|
||||
$"因变量'{variable.VariableName}'(ID:{variableId})表达式变更,规则表达式被级联更新");
|
||||
$"因变量'{variable.VariableName}'(ID:{variableId})定义变更,规则表达式被级联更新");
|
||||
}
|
||||
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
@@ -2288,9 +2103,8 @@ namespace YLErp.Modules.RiskEngine
|
||||
continue;
|
||||
}
|
||||
|
||||
var idPattern = $@"""VariableId""\s*:\s*{id}\b";
|
||||
var thresholdPattern = $@"""ThresholdVariableId""\s*:\s*{id}\b";
|
||||
var activeRefCount = activeRules.Count(r => Regex.IsMatch(r.ConditionJson, idPattern) || Regex.IsMatch(r.ConditionJson, thresholdPattern));
|
||||
var activeRefCount = activeRules.Count(rule =>
|
||||
RuleConditionExpressionBuilder.IsVariableReferenced(rule.ConditionJson, id));
|
||||
if (activeRefCount > 0)
|
||||
{
|
||||
items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "ACTIVE_RULE_REFERENCE",
|
||||
@@ -2305,10 +2119,14 @@ namespace YLErp.Modules.RiskEngine
|
||||
|
||||
if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded))
|
||||
{
|
||||
// 只有实际删除项写审计并参与一次提交;缺失和引用阻塞项不影响其他变量。
|
||||
SaveChangesWithConcurrencyCheck();
|
||||
InvalidateVariableCache();
|
||||
TryRefreshCache(InvalidateVariableCache, "风控变量缓存",
|
||||
items.Where(i => i.Status == BatchOperationItemStatus.Succeeded).Select(i => i.Id));
|
||||
}
|
||||
return BuildBatchResult(items);
|
||||
var result = BuildBatchResult(items);
|
||||
LogBatchResult("批量删除变量", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using YLErp.Commons;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.RiskEngine.Dto;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// ConditionJson 契约校验和简洁 RuleExpr 生成器。
|
||||
/// </summary>
|
||||
public static class RuleConditionExpressionBuilder
|
||||
{
|
||||
private static readonly HashSet<string> ComparableOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> BooleanOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"isTrue", "isFalse"
|
||||
};
|
||||
|
||||
private static readonly string[] VariableReferenceFields =
|
||||
{
|
||||
"VariableId", "ThresholdVariableId", "LowerThresholdVariableId", "UpperThresholdVariableId"
|
||||
};
|
||||
|
||||
public static List<RuleCondition> DeserializeConditions(string conditionJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
var conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(conditionJson);
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
throw new ServiceException("公式条件列表不能为空");
|
||||
return conditions;
|
||||
}
|
||||
catch (ServiceException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ServiceException("公式表达式 JSON 格式不合法");
|
||||
}
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<long> GetReferencedVariableIds(IEnumerable<RuleCondition> conditions)
|
||||
{
|
||||
return conditions
|
||||
.SelectMany(condition => condition == null
|
||||
? Array.Empty<long?>()
|
||||
: new long?[]
|
||||
{
|
||||
condition.VariableId,
|
||||
condition.ThresholdVariableId,
|
||||
condition.LowerThresholdVariableId,
|
||||
condition.UpperThresholdVariableId
|
||||
})
|
||||
.Where(id => id.HasValue && id.Value > 0)
|
||||
.Select(id => id.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static bool IsVariableReferenced(string conditionJson, long variableId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conditionJson) || variableId <= 0)
|
||||
return false;
|
||||
|
||||
var idText = variableId.ToString(CultureInfo.InvariantCulture);
|
||||
return VariableReferenceFields.Any(field =>
|
||||
Regex.IsMatch(conditionJson, $@"""{field}""\s*:\s*{idText}\b", RegexOptions.CultureInvariant));
|
||||
}
|
||||
|
||||
public static string Build(
|
||||
IReadOnlyList<RuleCondition> conditions,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables)
|
||||
{
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
throw new ServiceException("公式条件列表不能为空");
|
||||
|
||||
var expressions = conditions
|
||||
.Select((condition, index) => BuildConditionExpression(condition, variables, index + 1));
|
||||
return string.Join(" && ", expressions);
|
||||
}
|
||||
|
||||
private static string BuildConditionExpression(
|
||||
RuleCondition condition,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
int conditionIndex)
|
||||
{
|
||||
var label = $"条件{conditionIndex}";
|
||||
if (condition == null)
|
||||
throw new ServiceException($"{label}不能为空");
|
||||
if (!variables.TryGetValue(condition.VariableId, out var variable))
|
||||
throw new ServiceException($"{label}:变量 ID '{condition.VariableId}' 不存在");
|
||||
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
|
||||
throw new ServiceException($"{label}:变量'{variable.VariableName}'的取值表达式为空");
|
||||
|
||||
if (variable.DataType == RiskVariableDataType.Boolean)
|
||||
return BuildBooleanExpression(condition, variable, label);
|
||||
|
||||
if (variable.DataType != RiskVariableDataType.Numeric && variable.DataType != RiskVariableDataType.Date)
|
||||
throw new ServiceException($"{label}:不支持的数据类型");
|
||||
if (!ComparableOperators.Contains(condition.Operator))
|
||||
throw new ServiceException($"{label}:操作符 '{condition.Operator}' 不适用于{GetDataTypeName(variable.DataType)}类型变量");
|
||||
|
||||
var leftExpression = BuildComparableExpression(variable, label);
|
||||
if (condition.Operator == "between" || condition.Operator == "notBetween")
|
||||
return BuildRangeExpression(condition, variable, leftExpression, variables, label);
|
||||
|
||||
EnsureNoRangeFields(condition, label);
|
||||
var rightExpression = BuildThresholdExpression(
|
||||
condition.ThresholdType,
|
||||
condition.Value,
|
||||
condition.ThresholdVariableId,
|
||||
variable,
|
||||
variables,
|
||||
label,
|
||||
"阈值");
|
||||
var comparisonOperator = GetComparisonOperator(condition.Operator, label);
|
||||
return $"{leftExpression} {comparisonOperator} {rightExpression}";
|
||||
}
|
||||
|
||||
private static string BuildBooleanExpression(RuleCondition condition, glms_risk_variable variable, string label)
|
||||
{
|
||||
if (!BooleanOperators.Contains(condition.Operator))
|
||||
throw new ServiceException($"{label}:操作符 '{condition.Operator}' 不适用于布尔类型变量");
|
||||
EnsureNoThresholdFields(condition, label);
|
||||
return $"{variable.VariableExpr} == {(condition.Operator == "isTrue" ? "true" : "false")}";
|
||||
}
|
||||
|
||||
private static string BuildRangeExpression(
|
||||
RuleCondition condition,
|
||||
glms_risk_variable conditionVariable,
|
||||
string leftExpression,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
string label)
|
||||
{
|
||||
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue)
|
||||
throw new ServiceException($"{label}:区间条件不得携带普通阈值字段");
|
||||
if (!condition.IncludeLower.HasValue || !condition.IncludeUpper.HasValue)
|
||||
throw new ServiceException($"{label}:区间开闭配置不完整");
|
||||
|
||||
var lowerExpression = BuildThresholdExpression(
|
||||
condition.LowerThresholdType,
|
||||
condition.LowerValue,
|
||||
condition.LowerThresholdVariableId,
|
||||
conditionVariable,
|
||||
variables,
|
||||
label,
|
||||
"下限");
|
||||
var upperExpression = BuildThresholdExpression(
|
||||
condition.UpperThresholdType,
|
||||
condition.UpperValue,
|
||||
condition.UpperThresholdVariableId,
|
||||
conditionVariable,
|
||||
variables,
|
||||
label,
|
||||
"上限");
|
||||
|
||||
ValidateFixedRangeOrder(condition, conditionVariable.DataType, label);
|
||||
|
||||
var lowerOperator = condition.IncludeLower.Value ? ">=" : ">";
|
||||
var upperOperator = condition.IncludeUpper.Value ? "<=" : "<";
|
||||
var rangeExpression = $"{leftExpression} {lowerOperator} {lowerExpression} && {leftExpression} {upperOperator} {upperExpression}";
|
||||
return condition.Operator == "notBetween" ? $"!({rangeExpression})" : rangeExpression;
|
||||
}
|
||||
|
||||
private static string BuildThresholdExpression(
|
||||
string thresholdType,
|
||||
object fixedValue,
|
||||
long? thresholdVariableId,
|
||||
glms_risk_variable conditionVariable,
|
||||
IReadOnlyDictionary<long, glms_risk_variable> variables,
|
||||
string label,
|
||||
string thresholdLabel)
|
||||
{
|
||||
var dataType = conditionVariable.DataType;
|
||||
if (string.Equals(thresholdType, "Fixed", StringComparison.Ordinal))
|
||||
{
|
||||
if (thresholdVariableId.HasValue)
|
||||
throw new ServiceException($"{label}:固定{thresholdLabel}不得携带变量 ID");
|
||||
return BuildFixedValueExpression(fixedValue, dataType, label, thresholdLabel);
|
||||
}
|
||||
|
||||
if (string.Equals(thresholdType, "Variable", StringComparison.Ordinal))
|
||||
{
|
||||
if (HasValue(fixedValue))
|
||||
throw new ServiceException($"{label}:变量{thresholdLabel}不得携带固定值");
|
||||
if (!thresholdVariableId.HasValue)
|
||||
throw new ServiceException($"{label}:{thresholdLabel}变量 ID 不能为空");
|
||||
if (!variables.TryGetValue(thresholdVariableId.Value, out var thresholdVariable))
|
||||
throw new ServiceException($"{label}:{thresholdLabel}变量 ID '{thresholdVariableId.Value}' 不存在");
|
||||
if (thresholdVariable.DataType != dataType)
|
||||
throw new ServiceException($"{label}:{thresholdLabel}变量与条件变量的数据类型不一致");
|
||||
if (dataType == RiskVariableDataType.Numeric && !UnitsMatch(conditionVariable.Unit, thresholdVariable.Unit))
|
||||
throw new ServiceException($"{label}:{thresholdLabel}变量与条件变量的单位不一致");
|
||||
return BuildComparableExpression(thresholdVariable, label);
|
||||
}
|
||||
|
||||
throw new ServiceException($"{label}:{thresholdLabel}类型 '{thresholdType}' 不合法,仅支持 Fixed/Variable");
|
||||
}
|
||||
|
||||
private static string BuildComparableExpression(glms_risk_variable variable, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
|
||||
throw new ServiceException($"{label}:变量'{variable.VariableName}'的取值表达式为空");
|
||||
return variable.VariableExpr;
|
||||
}
|
||||
|
||||
private static string BuildFixedValueExpression(
|
||||
object value,
|
||||
RiskVariableDataType dataType,
|
||||
string label,
|
||||
string thresholdLabel)
|
||||
{
|
||||
if (!HasValue(value))
|
||||
throw new ServiceException($"{label}:固定{thresholdLabel}不能为空");
|
||||
|
||||
var rawValue = GetRawValue(value);
|
||||
if (dataType == RiskVariableDataType.Numeric)
|
||||
{
|
||||
if (!decimal.TryParse(rawValue, NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture, out var numericValue))
|
||||
{
|
||||
throw new ServiceException($"{label}:数值型{thresholdLabel}必须为数字");
|
||||
}
|
||||
|
||||
return FormatFixedDecimal(numericValue);
|
||||
}
|
||||
|
||||
if (dataType == RiskVariableDataType.Date)
|
||||
{
|
||||
if (!DateTime.TryParseExact(rawValue, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None, out var dateValue))
|
||||
{
|
||||
throw new ServiceException($"{label}:日期型{thresholdLabel}必须为 yyyy-MM-dd 格式的合法日期");
|
||||
}
|
||||
|
||||
return $"new DateTime({dateValue.Year}, {dateValue.Month}, {dateValue.Day})";
|
||||
}
|
||||
|
||||
throw new ServiceException($"{label}:不支持的数据类型");
|
||||
}
|
||||
|
||||
private static string FormatFixedDecimal(decimal value)
|
||||
{
|
||||
return value.ToString("0.############################", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static void ValidateFixedRangeOrder(RuleCondition condition, RiskVariableDataType dataType, string label)
|
||||
{
|
||||
if (condition.LowerThresholdType != "Fixed" || condition.UpperThresholdType != "Fixed")
|
||||
return;
|
||||
|
||||
var lower = GetRawValue(condition.LowerValue);
|
||||
var upper = GetRawValue(condition.UpperValue);
|
||||
if (dataType == RiskVariableDataType.Numeric &&
|
||||
decimal.TryParse(lower, NumberStyles.Float, CultureInfo.InvariantCulture, out var lowerNumber) &&
|
||||
decimal.TryParse(upper, NumberStyles.Float, CultureInfo.InvariantCulture, out var upperNumber) &&
|
||||
lowerNumber > upperNumber)
|
||||
{
|
||||
throw new ServiceException($"{label}:下限不能大于上限");
|
||||
}
|
||||
|
||||
if (dataType == RiskVariableDataType.Date &&
|
||||
DateTime.TryParseExact(lower, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var lowerDate) &&
|
||||
DateTime.TryParseExact(upper, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var upperDate) &&
|
||||
lowerDate > upperDate)
|
||||
{
|
||||
throw new ServiceException($"{label}:下限不能晚于上限");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureNoRangeFields(RuleCondition condition, string label)
|
||||
{
|
||||
if (condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
|
||||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
|
||||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
|
||||
{
|
||||
throw new ServiceException($"{label}:非区间条件不得携带区间字段");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureNoThresholdFields(RuleCondition condition, string label)
|
||||
{
|
||||
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue ||
|
||||
condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
|
||||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
|
||||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
|
||||
{
|
||||
throw new ServiceException($"{label}:布尔条件不得携带阈值字段");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValue(object value)
|
||||
{
|
||||
if (value == null) return false;
|
||||
if (value is JValue jsonValue)
|
||||
{
|
||||
if (jsonValue.Type == JTokenType.Null || jsonValue.Type == JTokenType.Undefined) return false;
|
||||
if (jsonValue.Type == JTokenType.String) return !string.IsNullOrWhiteSpace(jsonValue.Value<string>());
|
||||
return true;
|
||||
}
|
||||
return value is not string text || !string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
private static string GetRawValue(object value)
|
||||
{
|
||||
return value is JValue jsonValue
|
||||
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static bool UnitsMatch(string left, string right)
|
||||
{
|
||||
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string GetComparisonOperator(string ruleOperator, string label)
|
||||
{
|
||||
return ruleOperator switch
|
||||
{
|
||||
"gt" => ">",
|
||||
"lt" => "<",
|
||||
"gte" => ">=",
|
||||
"lte" => "<=",
|
||||
"eq" => "==",
|
||||
"ne" => "!=",
|
||||
_ => throw new ServiceException($"{label}:不支持的操作符'{ruleOperator}'")
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetDataTypeName(RiskVariableDataType dataType)
|
||||
{
|
||||
return dataType switch
|
||||
{
|
||||
RiskVariableDataType.Numeric => "数值",
|
||||
RiskVariableDataType.Date => "日期",
|
||||
RiskVariableDataType.Boolean => "布尔",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,17 @@ namespace YLErp.Web.Controllers
|
||||
|
||||
private RiskRuleService GetRiskRuleService() => new RiskRuleService(CurUser);
|
||||
|
||||
/// <summary>
|
||||
/// 格式化批量请求上下文,限制 ID 数量以避免异常日志过长。
|
||||
/// </summary>
|
||||
private static string FormatBatchRequestContext(BatchIdsReq req)
|
||||
{
|
||||
var ids = req?.Ids?.Distinct().ToList() ?? new List<long>();
|
||||
var displayedIds = string.Join(",", ids.Take(20));
|
||||
var truncated = ids.Count > 20 ? ",...(已截断)" : string.Empty;
|
||||
return $"Count={ids.Count}, Ids={displayedIds}{truncated}";
|
||||
}
|
||||
|
||||
#region Rule Management
|
||||
|
||||
[HttpGet("risk-rules")]
|
||||
@@ -165,7 +176,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量删除风控规则");
|
||||
_logger.Error(ex, $"批量删除风控规则异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -236,7 +247,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量启用风控规则");
|
||||
_logger.Error(ex, $"批量启用风控规则异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -258,7 +269,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量停用风控规则");
|
||||
_logger.Error(ex, $"批量停用风控规则异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -482,7 +493,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量启用应用配置");
|
||||
_logger.Error(ex, $"批量启用应用配置异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -504,7 +515,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量停用应用配置");
|
||||
_logger.Error(ex, $"批量停用应用配置异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -526,7 +537,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量删除应用配置");
|
||||
_logger.Error(ex, $"批量删除应用配置异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
@@ -684,7 +695,7 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "批量删除变量");
|
||||
_logger.Error(ex, $"批量删除变量异常,{FormatBatchRequestContext(req)}");
|
||||
return Json(new { success = false, message = "系统异常,请联系管理员" });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user