feat: 风控引擎前后端统一操作符

This commit is contained in:
尹峰
2026-07-23 18:30:05 +08:00
parent 15c249a423
commit 3651d3d2df
18 changed files with 1313 additions and 887 deletions
@@ -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=12
-- ----------------------------
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 的 ConditionJsonRuleExpr、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 使用 tokenRuleExpr 保持原简洁公式。
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=12
-- ----------------------------
INSERT INTO `yltrs_admin`.`sys_menu` (`id`, `pid`, `pids`, `name`, `code`, `type`, `icon`, `router`, `component`, `permission`, `application`, `open_type`, `visible`, `link`, `redirect`, `weight`, `sort`, `remark`, `status`, `create_time`, `create_user`, `update_time`, `update_user`) VALUES
(1077, 99, NULL, '异常交易监控', NULL, 1, NULL, NULL, NULL, '异常交易监控', NULL, NULL, NULL, NULL, NULL, NULL, 0, '', 0, NULL, NULL, NULL, NULL),
(1078, 1077, NULL, '风控规则查看', NULL, 2, NULL, NULL, NULL, '风控规则查看', NULL, NULL, NULL, NULL, NULL, NULL, 1, '', 0, NULL, NULL, NULL, NULL),
(1079, 1077, NULL, '风控规则新增', NULL, 2, NULL, NULL, NULL, '风控规则新增', NULL, NULL, NULL, NULL, NULL, NULL, 2, '', 0, NULL, NULL, NULL, NULL),
(1080, 1077, NULL, '风控规则编辑', NULL, 2, NULL, NULL, NULL, '风控规则编辑', NULL, NULL, NULL, NULL, NULL, NULL, 3, '', 0, NULL, NULL, NULL, NULL),
(1081, 1077, NULL, '风控规则删除', NULL, 2, NULL, NULL, NULL, '风控规则删除', NULL, NULL, NULL, NULL, NULL, NULL, 4, '', 0, NULL, NULL, NULL, NULL),
(1082, 1077, NULL, '风控规则启停', NULL, 2, NULL, NULL, NULL, '风控规则启停', NULL, NULL, NULL, NULL, NULL, NULL, 5, '', 0, NULL, NULL, NULL, NULL),
(1083, 1077, NULL, '风控应用查看', NULL, 2, NULL, NULL, NULL, '风控应用查看', NULL, NULL, NULL, NULL, NULL, NULL, 6, '', 0, NULL, NULL, NULL, NULL),
(1084, 1077, NULL, '风控应用新增', NULL, 2, NULL, NULL, NULL, '风控应用新增', NULL, NULL, NULL, NULL, NULL, NULL, 7, '', 0, NULL, NULL, NULL, NULL),
(1085, 1077, NULL, '风控应用编辑', NULL, 2, NULL, NULL, NULL, '风控应用编辑', NULL, NULL, NULL, NULL, NULL, NULL, 8, '', 0, NULL, NULL, NULL, NULL),
(1086, 1077, NULL, '风控应用删除', NULL, 2, NULL, NULL, NULL, '风控应用删除', NULL, NULL, NULL, NULL, NULL, NULL, 9, '', 0, NULL, NULL, NULL, NULL),
(1087, 1077, NULL, '风控应用启停', NULL, 2, NULL, NULL, NULL, '风控应用启停', NULL, NULL, NULL, NULL, NULL, NULL, 10, '', 0, NULL, NULL, NULL, NULL),
(1088, 1077, NULL, '风控变量查看', NULL, 2, NULL, NULL, NULL, '风控变量查看', NULL, NULL, NULL, NULL, NULL, NULL, 11, '', 0, NULL, NULL, NULL, NULL),
(1089, 1077, NULL, '风控变量新增', NULL, 2, NULL, NULL, NULL, '风控变量新增', NULL, NULL, NULL, NULL, NULL, NULL, 12, '', 0, NULL, NULL, NULL, NULL),
(1090, 1077, NULL, '风控变量编辑', NULL, 2, NULL, NULL, NULL, '风控变量编辑', NULL, NULL, NULL, NULL, NULL, NULL, 13, '', 0, NULL, NULL, NULL, NULL),
(1091, 1077, NULL, '风控变量删除', NULL, 2, NULL, NULL, NULL, '风控变量删除', NULL, NULL, NULL, NULL, NULL, NULL, 14, '', 0, NULL, NULL, NULL, NULL),
(1092, 1077, NULL, '风控日志查看', NULL, 2, NULL, NULL, NULL, '风控日志查看', NULL, NULL, NULL, NULL, NULL, NULL, 15, '', 0, NULL, NULL, NULL, NULL),
(1093, 1077, NULL, '风控日志导出', NULL, 2, NULL, NULL, NULL, '风控日志导出', NULL, NULL, NULL, NULL, NULL, NULL, 16, '', 0, NULL, NULL, NULL, NULL);
-- ----------------------------
-- 2. sys_role_menu 角色1的按钮权限
-- ----------------------------
INSERT INTO `yltrs_admin`.`sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES
(1097, 1, 1077),
(1098, 1, 1078),
(1099, 1, 1079),
(1100, 1, 1080),
(1101, 1, 1081),
(1102, 1, 1082),
(1103, 1, 1083),
(1104, 1, 1084),
(1105, 1, 1085),
(1106, 1, 1086),
(1107, 1, 1087),
(1108, 1, 1088),
(1109, 1, 1089),
(1110, 1, 1090),
(1111, 1, 1091),
(1112, 1, 1092),
(1113, 1, 1093);
@@ -0,0 +1,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) > 1000000001亿元)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('名义本金超阈值',
'合约名义本金超过阈值(默认1亿元)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 1,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 100000000
)),
'trade.StockEqvNotional > 100000000',
1, 1, 0, 'SYSTEM', NOW());
-- 规则4:保证金支付比例超阈值(审批,账户/标的类型)
-- 条件:保证金比例(ID=13) > 50%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金支付比例超阈值',
'保证金比例超过阈值(默认50%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 13,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 50
)),
'trade.MarginRate > 50',
1, 1, 0, 'SYSTEM', NOW());
-- 规则5:保证金利率偏离(审批,账户)
-- 条件:保证金利率(ID=12) 不介于 [2%, 5%]
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金利率偏离',
'保证金利率不在配置区间内(默认2%~5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 12,
'Operator', '不介于', 'ThresholdType', 'Fixed', 'Value', JSON_ARRAY(2, 5),
'IncludeLowerBound', true, 'IncludeUpperBound', true
)),
'!(client_marginrate.InitMarginRebateRate >= 2 && client_marginrate.InitMarginRebateRate <= 5)',
1, 1, 0, 'SYSTEM', NOW());
-- 规则6:保证金收取比例低于最低标准(审批,账户/标的类型)
-- 条件:保证金比例(ID=13) < 客户品种最低保证金率(ID=52)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('保证金收取比例低于最低标准',
'保证金比例低于客户品种最低保证金率时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 13,
'Operator', '<', 'ThresholdType', 'Variable',
'ThresholdVariableId', 52
)),
'trade.MarginRate < config.MinMarginRate',
1, 1, 0, 'SYSTEM', NOW());
-- 规则7:起息日早于当前日期(审批,全局)
-- 条件:合约起息日(ID=2) < 当前日期(ID=21)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('起息日早于当前日期',
'合约起息日早于当前日期时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 2,
'Operator', '早于', 'ThresholdType', 'Variable',
'ThresholdVariableId', 21
)),
'trade.StartDate < sys.CurrentDate',
1, 1, 0, 'SYSTEM', NOW());
-- 规则8:支付日为银行间交易日(审批,全局)
-- 条件:支付日是否银行间交易日(ID=38) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('支付日为银行间交易日',
'支付日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 38,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsSettlementDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则9:到期日为银行间交易日(审批,全局)
-- 条件:到期日是否银行间交易日(ID=36) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('到期日为银行间交易日',
'到期日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 36,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsExerciseDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则10:平仓日为银行间交易日(审批,全局)
-- 条件:平仓日是否银行间交易日(ID=37) = 是
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('平仓日为银行间交易日',
'平仓日为银行间交易日时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 37,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsUnwindDateTradingDay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则11:合约期限超阈值(审批,账户)
-- 条件:合约期限(ID=26) > 365天
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('合约期限超阈值',
'合约期限超过阈值(默认365天)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 26,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 365
)),
'calc.MaturityDays > 365',
1, 1, 0, 'SYSTEM', NOW());
-- 规则12:债券类净价偏离(审批,全局)
-- 条件:期初净价偏离度(ID=45) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('债券类净价偏离',
'期初净价与上一收盘日中债估值净价偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 45,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(swap_position.PosiNetNoFeePrice - market.CBValuationNetPrice) / market.CBValuationNetPrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则13:债券类收益率偏离(审批,全局)
-- 条件:期初收益率偏离度(ID=46) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('债券类收益率偏离',
'期初收益率与上一收盘日中债估值收益率偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 46,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.InitYtm - market.CBValuationYtm) / market.CBValuationYtm * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则14:非债券类价格偏离(审批,全局)
-- 条件:期初价格偏离度(ID=47) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('非债券类价格偏离',
'期初价格与上一日收盘价偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 47,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.SpotPrice - market.LastClosePrice) / market.LastClosePrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则15:单一交易对手累计标的数量超阈值(审批,对手方)
-- 条件:对手方累计标的数量(ID=32) > 10个
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('单一交易对手累计标的数量超阈值',
'对手方累计标的数量超过阈值(默认10个)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 32,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 10
)),
'calc.CounterpartyUnderlyingCount > 10',
1, 1, 0, 'SYSTEM', NOW());
-- 规则16:多头支付固定端利率偏离(审批,账户)
-- 条件:多空方向为多头(ID=41) AND 利息端利率与FR007偏离度(ID=48) > 3%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('多头支付固定端利率偏离',
'多头方向支付固定端场景下,利息端利率与FR007偏离度超阈值(默认3%)时触发审批',
JSON_ARRAY(
JSON_OBJECT(
'VariableId', 41,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
),
JSON_OBJECT(
'VariableId', 48,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 3
)
),
'calc.IsLongDirection == true && Math.Abs(calc.InterestRate - market.FR007) / market.FR007 * 100 > 3',
1, 1, 0, 'SYSTEM', NOW());
-- 规则17:空头利率减点借贷加权偏离(审批,账户)
-- 条件:多空方向为空头(ID=42) AND 利息端利率与借贷加权费率偏离度(ID=49) > 2%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('空头利率减点借贷加权偏离',
'空头方向场景下,利息端利率与借贷加权费率偏离度超阈值(默认2%)时触发审批',
JSON_ARRAY(
JSON_OBJECT(
'VariableId', 42,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
),
JSON_OBJECT(
'VariableId', 49,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 2
)
),
'calc.IsShortDirection == true && Math.Abs(calc.InterestRate - market.BondLendingRate) / market.BondLendingRate * 100 > 2',
1, 1, 0, 'SYSTEM', NOW());
-- 规则18:账户授权收支方向不匹配(禁止,账户)
-- 条件:保证金收支方向为支付(ID=43)(实际需结合账户授权方向判断,此处简化)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('账户授权收支方向不匹配',
'保证金收支方向与账户授权方向不匹配时禁止交易',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 43,
'Operator', '', 'ThresholdType', 'Fixed', 'Value', true
)),
'calc.IsMarginPay == true',
1, 1, 0, 'SYSTEM', NOW());
-- 规则19:执行价偏离超阈值(审批,账户)
-- 条件:执行价偏离度(ID=50) > 5%
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('执行价偏离超阈值',
'执行价与参考价格偏离度超阈值(默认5%)时触发审批',
JSON_ARRAY(JSON_OBJECT(
'VariableId', 50,
'Operator', '>', 'ThresholdType', 'Fixed', 'Value', 5
)),
'Math.Abs(trade.StrikePrice - market.ReferencePrice) / market.ReferencePrice * 100 > 5',
1, 1, 0, 'SYSTEM', NOW());
-- 规则20:希腊字母限额超阈值(提示,预留接口,一期不纳入)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('希腊字母限额超阈值',
'Delta/Gamma/Vega/Theta任一超阈值时提示(预留接口,一期不纳入)',
NULL,
NULL,
1, 1, 0, 'SYSTEM', NOW());
-- 规则21:接近/触发敲入敲出价(提示,预留接口,需确认具体判断逻辑)
INSERT INTO `glms_risk_rule` (`RuleName`, `RuleText`, `ConditionJson`, `RuleExpr`, `Status`, `Version`, `OptId`, `OptName`, `OptDate`) VALUES
('接近/触发敲入敲出价',
'标的价格接近敲入/敲出价时提示(预留接口,需确认具体判断逻辑)',
NULL,
NULL,
1, 1, 0, 'SYSTEM', NOW());
@@ -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
};
}
}
}
+3
View File
@@ -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>操作符 tokengt/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; }
}
}
+130 -312
View File
@@ -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 => "布尔",
_ => "未知"
};
}
}
}
+18 -7
View File
@@ -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 = "系统异常,请联系管理员" });
}
}