feat: 规则双模式公式表达式(RuleExpr)& 字段精简重构

- 新增 RuleExpr 字段,支持结构化拼接和自由文本两种模式,二选一互斥
- 删除 RuleCode/FormulaText/ApplicationCode/SourceExpression/IsImplemented/VariableCode/TargetCode
- 重命名 FormulaJson→ConditionJson, FormulaExpr→RuleExpr, ImplementationScript→VariableExpr, RuleDescription→RuleText
- RuleId(bigint)→RuleIds(varchar) 多规则支持
- FormulaCondition: variableCode(string)→variableId(long), thresholdVariableCode→thresholdVariableId
- 新增 44 条变量池 seed 数据
- 设计文档同步更新
This commit is contained in:
尹峰
2026-06-24 09:44:13 +08:00
parent 5b77bbf53e
commit 28ed20e527
31 changed files with 646 additions and 402 deletions
@@ -5,11 +5,10 @@ namespace YLErp.DBModels
[Table("glms_risk_rule")]
public class glms_risk_rule : DBModelWithOperator
{
public string RuleCode { get; set; }
public string RuleName { get; set; }
public string RuleDescription { get; set; }
public string FormulaJson { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public string ConditionJson { get; set; }
public string RuleExpr { get; set; }
public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
public int Version { get; set; } = 1;
public int? UpdateOptId { get; set; }
@@ -5,8 +5,7 @@ namespace YLErp.DBModels
[Table("glms_risk_rule_application")]
public class glms_risk_rule_application : DBModelWithOperator
{
public string ApplicationCode { get; set; }
public long RuleId { get; set; }
public string RuleIds { get; set; }
public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active;
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
@@ -9,7 +9,6 @@ namespace YLErp.DBModels
public string TargetType { get; set; }
public long TargetId { get; set; }
public string TargetName { get; set; }
public string TargetCode { get; set; }
public string OperationDetail { get; set; }
public string Result { get; set; }
public string SnapshotData { get; set; }
@@ -5,16 +5,13 @@ namespace YLErp.DBModels
[Table("glms_risk_variable")]
public class glms_risk_variable : DBModelWithOperator
{
public string VariableCode { get; set; }
public string VariableName { get; set; }
public RiskVariableCategory Category { get; set; }
public RiskVariableDataType DataType { get; set; }
public string Unit { get; set; }
public string ValueDomain { get; set; }
public string Description { get; set; }
public string ImplementationScript { get; set; }
public string SourceExpression { get; set; }
public bool IsImplemented { get; set; }
public string VariableExpr { get; set; }
public int Version { get; set; } = 1;
public int SortOrder { get; set; }
public int? UpdateOptId { get; set; }
@@ -2,11 +2,10 @@ SET FOREIGN_KEY_CHECKS=0;
CREATE TABLE `glms_risk_rule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`RuleCode` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '规则编码(RISK-YYYYMMDD-NNNN)',
`RuleName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '规则名称',
`RuleDescription` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规则描述',
`FormulaJson` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '公式JSON表达式',
`FormulaText` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '公式可读文本',
`RuleText` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规则文本',
`ConditionJson` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '条件JSON(结构化模式, FormulaCondition数组)',
`RuleExpr` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '规则表达式(自由文本模式, 类C#表达式)',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
@@ -16,14 +15,12 @@ CREATE TABLE `glms_risk_rule` (
`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,
UNIQUE INDEX `uk_rule_code`(`RuleCode` ASC) USING BTREE,
INDEX `idx_status`(`Status` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则定义表' ROW_FORMAT = Dynamic;
CREATE TABLE `glms_risk_rule_application` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`ApplicationCode` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '应用编码(APP-YYYYMMDD-NNNN)',
`RuleId` bigint NOT NULL COMMENT '关联规则Id',
`RuleIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '关联规则Id列表(逗号分隔)',
`Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted',
`ControlStrategy` tinyint NOT NULL COMMENT '控制策略: 1=Block, 2=Approval, 3=Warning',
`TriggerPoints` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发时点(逗号分隔)',
@@ -40,8 +37,6 @@ CREATE TABLE `glms_risk_rule_application` (
`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,
UNIQUE INDEX `uk_application_code`(`ApplicationCode` ASC) USING BTREE,
INDEX `idx_rule_id`(`RuleId` ASC) USING BTREE,
INDEX `idx_status`(`Status` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则应用配置表' ROW_FORMAT = Dynamic;
@@ -51,7 +46,6 @@ CREATE TABLE `glms_risk_rule_audit_log` (
`TargetType` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '目标类型(RULE/APPLICATION/VARIABLE)',
`TargetId` bigint NOT NULL COMMENT '目标记录Id',
`TargetName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '目标名称',
`TargetCode` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '目标编码',
`OperationDetail` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '操作详情',
`Result` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作结果',
`SnapshotData` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '快照数据(JSON)',
@@ -67,16 +61,13 @@ CREATE TABLE `glms_risk_rule_audit_log` (
CREATE TABLE `glms_risk_variable` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`VariableCode` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '变量编码',
`VariableName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '变量名称',
`Category` tinyint NOT NULL COMMENT '分类: 1=BookingElement, 2=MarketData, 3=SystemCalc, 4=BooleanCheck',
`DataType` tinyint NOT NULL COMMENT '数据类型: 1=Numeric, 2=Date, 3=Boolean',
`Unit` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
`ValueDomain` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '值域描述',
`Description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`ImplementationScript` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '实现脚本(C#表达式)',
`SourceExpression` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '来源表达式',
`IsImplemented` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已实现: 0=否, 1=是',
`VariableExpr` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '变量表达式(C#表达式)',
`Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`SortOrder` int NOT NULL DEFAULT 0 COMMENT '排序',
`OptId` int NULL DEFAULT NULL COMMENT '创建人Id',
@@ -85,8 +76,7 @@ CREATE TABLE `glms_risk_variable` (
`UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id',
`UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称',
`UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_variable_code`(`VariableCode` ASC) USING BTREE
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控变量池定义表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS=1;
@@ -0,0 +1,59 @@
-- ============================================================
-- 变量池初始数据(对应设计文档 §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());
-- 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());
@@ -4,7 +4,7 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public class CreateRiskApplicationReq
{
public long RuleId { get; set; }
public string RuleIds { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
public string ScopeAssetBookIds { get; set; }
@@ -3,8 +3,8 @@ namespace YLErp.Modules.RiskEngine.Dto
public class CreateRiskRuleReq
{
public string RuleName { get; set; }
public string RuleDescription { get; set; }
public string FormulaJson { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public string ConditionJson { get; set; }
public string RuleExpr { get; set; }
}
}
@@ -4,16 +4,13 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public class CreateRiskVariableReq
{
public string VariableCode { get; set; }
public string VariableName { get; set; }
public RiskVariableCategory Category { get; set; }
public RiskVariableDataType DataType { get; set; }
public string Unit { get; set; }
public string ValueDomain { get; set; }
public string Description { get; set; }
public string ImplementationScript { get; set; }
public string SourceExpression { get; set; }
public bool IsImplemented { get; set; }
public string VariableExpr { get; set; }
public int SortOrder { get; set; }
}
}
@@ -2,13 +2,13 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public class FormulaCondition
{
public string VariableCode { get; set; }
public long VariableId { get; set; }
public string VariableName { get; set; }
public string VariableType { get; set; }
public string Operator { get; set; }
public string ThresholdType { get; set; }
public object Value { get; set; }
public string ThresholdVariableCode { get; set; }
public long? ThresholdVariableId { get; set; }
public string ThresholdVariableName { get; set; }
public string Unit { get; set; }
}
@@ -7,6 +7,6 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public string Keyword { get; set; }
public RiskRuleStatus? Status { get; set; }
public string VariableCode { get; set; }
public long? VariableId { get; set; }
}
}
@@ -6,12 +6,10 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskApplicationDetail
{
public long Id { get; set; }
public string ApplicationCode { get; set; }
public long RuleId { get; set; }
public string RuleCode { get; set; }
public string RuleIds { get; set; }
public string RuleName { get; set; }
public string FormulaJson { get; set; }
public string FormulaText { get; set; }
public string ConditionJson { get; set; }
public string RuleNames { get; set; }
public RiskRuleStatus Status { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
@@ -6,11 +6,9 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskApplicationListItem
{
public long Id { get; set; }
public string ApplicationCode { get; set; }
public long RuleId { get; set; }
public string RuleCode { get; set; }
public string RuleIds { get; set; }
public string RuleName { get; set; }
public string FormulaText { get; set; }
public string RuleNames { get; set; }
public RiskRuleStatus Status { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
@@ -9,7 +9,6 @@ namespace YLErp.Modules.RiskEngine.Dto
public string TargetType { get; set; }
public long TargetId { get; set; }
public string TargetName { get; set; }
public string TargetCode { get; set; }
public string OperationDetail { get; set; }
public string Result { get; set; }
public string SnapshotData { get; set; }
@@ -9,7 +9,6 @@ namespace YLErp.Modules.RiskEngine.Dto
public string TargetType { get; set; }
public long TargetId { get; set; }
public string TargetName { get; set; }
public string TargetCode { get; set; }
public string OperationDetail { get; set; }
public string Result { get; set; }
public string OptName { get; set; }
@@ -5,7 +5,6 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskRuleApplicationSummary
{
public long Id { get; set; }
public string ApplicationCode { get; set; }
public RiskRuleStatus Status { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
@@ -7,11 +7,10 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskRuleDetail
{
public long Id { get; set; }
public string RuleCode { get; set; }
public string RuleName { get; set; }
public string RuleDescription { get; set; }
public string FormulaJson { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public string ConditionJson { get; set; }
public string RuleExpr { get; set; }
public RiskRuleStatus Status { get; set; }
public int Version { get; set; }
public string OptName { get; set; }
@@ -6,10 +6,8 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskRuleListItem
{
public long Id { get; set; }
public string RuleCode { get; set; }
public string RuleName { get; set; }
public string RuleDescription { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public RiskRuleStatus Status { get; set; }
public int Version { get; set; }
public string OptName { get; set; }
@@ -6,16 +6,13 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskVariableDetail
{
public long Id { get; set; }
public string VariableCode { get; set; }
public string VariableName { get; set; }
public RiskVariableCategory Category { get; set; }
public RiskVariableDataType DataType { get; set; }
public string Unit { get; set; }
public string ValueDomain { get; set; }
public string Description { get; set; }
public string ImplementationScript { get; set; }
public string SourceExpression { get; set; }
public bool IsImplemented { get; set; }
public string VariableExpr { get; set; }
public int Version { get; set; }
public int SortOrder { get; set; }
public string OptName { get; set; }
@@ -5,14 +5,13 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskVariableListItem
{
public long Id { get; set; }
public string VariableCode { get; set; }
public string VariableName { get; set; }
public RiskVariableCategory Category { get; set; }
public RiskVariableDataType DataType { get; set; }
public string Unit { get; set; }
public string ValueDomain { get; set; }
public string Description { get; set; }
public bool IsImplemented { get; set; }
public string VariableExpr { get; set; }
public int Version { get; set; }
public int SortOrder { get; set; }
}
@@ -5,10 +5,10 @@ namespace YLErp.Modules.RiskEngine.Dto
public class RiskVariableSimpleItem
{
public long Id { get; set; }
public string VariableCode { get; set; }
public string VariableName { get; set; }
public RiskVariableCategory Category { get; set; }
public RiskVariableDataType DataType { get; set; }
public string Unit { get; set; }
public string VariableExpr { get; set; }
}
}
@@ -4,6 +4,7 @@ namespace YLErp.Modules.RiskEngine.Dto
{
public class UpdateRiskApplicationReq
{
public string RuleIds { get; set; }
public RiskControlStrategy ControlStrategy { get; set; }
public string TriggerPoints { get; set; }
public string ScopeAssetBookIds { get; set; }
@@ -3,9 +3,9 @@ namespace YLErp.Modules.RiskEngine.Dto
public class UpdateRiskRuleReq
{
public string RuleName { get; set; }
public string RuleDescription { get; set; }
public string FormulaJson { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public string ConditionJson { get; set; }
public string RuleExpr { get; set; }
public int ExpectedVersion { get; set; }
}
}
@@ -10,9 +10,7 @@ namespace YLErp.Modules.RiskEngine.Dto
public string Unit { get; set; }
public string ValueDomain { get; set; }
public string Description { get; set; }
public string ImplementationScript { get; set; }
public string SourceExpression { get; set; }
public bool IsImplemented { get; set; }
public string VariableExpr { get; set; }
public int SortOrder { get; set; }
public int ExpectedVersion { get; set; }
}
@@ -23,8 +23,10 @@ using YLErp.Model;
- RiskRule(风控规则):单条规则包含完整的公式定义 + 校验策略 + 触发时点 + 适用范围。
不需要 "一个 Rule 对应多个 Application" 的复杂关系。
- Content:用户输入的规则内容字符串,如"名义本金>1亿"
- ConditionJson:条件JSON(结构化模式),如 {"conditions":[{"variableId":1,"operator":">","value":100000000}]}
- RuleExpr:规则表达式(自由文本模式,类C#表达式)
- 规则编译:在规则定义时(创建/加载/缓存刷新时)调用 RuleCompiler.Compile(rule)
将 Content 解析并编译为 Func<RiskContext, bool> 委托,写入 rule.CompiledScript。
将 ConditionJson/RuleExpr 解析并编译为 Func<RiskContext, bool> 委托,写入 rule.CompiledScript。
判风控时直接执行委托,零解析开销。
- RiskContext:一次风控检查的数据上下文(DataMap 机制)
- RiskResult:风控检查结果(Passed / Blocked / NeedApproval / Warnings / TriggeredRules
@@ -46,7 +48,7 @@ using YLErp.Model;
与旧设计文档的差异:
- 删除 RiskRuleApplication 模型,将策略/时点/范围合并到 RiskRule 中
- 删除 FormulaJson 实时解析,改为 Content → 表达式树编译
- 删除 FormulaJson 实时解析,改为 ConditionJson/RuleExpr → 表达式树编译
- 规则编译跟随规则生命周期(创建/加载时),判风控时零解析
【架构设计】
@@ -88,7 +90,7 @@ using YLErp.Model;
- RiskRule.cs:合并模型(公式 + 策略 + 时点 + 范围 + CompiledScript
- RiskContext.cs:风控上下文(TradeId, TriggerPoint, DataMap
- RiskResult.cs:风控结果(Passed, Blocked, NeedApproval, Warnings, TriggeredRules
- RiskFormula.csFormulaJson 反序列化模型(辅助)
- RiskFormula.csConditionJson 反序列化模型(辅助)
3. RuleCompiler.cs
- ParseContent:解析 Content 字符串(支持 > < >= <= = != ≠)
- ParseValue:解析数值(支持 万/亿/千/百分比)
@@ -233,7 +235,7 @@ namespace YLErp.Modules.RiskEngine
// 确保规则已编译
if (rule.CompiledScript == null)
{
_logger.Info($"[风控引擎] 规则未编译,执行编译 - RuleCode: {rule.RuleCode}");
_logger.Info($"[风控引擎] 规则未编译,执行编译 - RuleId: {rule.Id}");
RuleCompiler.Compile(rule);
}
@@ -242,19 +244,19 @@ namespace YLErp.Modules.RiskEngine
try
{
triggered = rule.CompiledScript(context);
_logger.Info($"[风控引擎] 规则执行 - RuleCode: {rule.RuleCode}, Triggered: {triggered}");
_logger.Info($"[风控引擎] 规则执行 - RuleId: {rule.Id}, Triggered: {triggered}");
}
catch (Exception ex)
{
_logger.Info($"[风控引擎] 规则执行异常 - RuleCode: {rule.RuleCode}, Error: {ex.Message}");
_logger.Info($"[风控引擎] 规则执行异常 - RuleId: {rule.Id}, Error: {ex.Message}");
result.HasError = true;
result.ErrorMessage += $"规则[{rule.RuleCode}]执行异常:{ex.Message}";
result.ErrorMessage += $"规则[{rule.Id}]执行异常:{ex.Message}";
continue;
}
if (triggered)
{
_logger.Info($"[风控引擎] 规则触发 - RuleCode: {rule.RuleCode}, Strategy: {rule.ControlStrategy}");
_logger.Info($"[风控引擎] 规则触发 - RuleId: {rule.Id}, Strategy: {rule.ControlStrategy}");
// 按 ControlStrategy 生成结果
switch (rule.ControlStrategy)
@@ -264,10 +266,9 @@ namespace YLErp.Modules.RiskEngine
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleCode = rule.RuleCode,
RuleName = rule.RuleName,
ControlStrategy = "禁止",
FormulaText = rule.FormulaText,
RuleText = rule.RuleText,
Message = $"规则[{rule.RuleName}]触发:禁止"
});
break;
@@ -277,16 +278,15 @@ namespace YLErp.Modules.RiskEngine
result.Passed = false;
result.TriggeredRules.Add(new TriggeredRuleInfo
{
RuleCode = rule.RuleCode,
RuleName = rule.RuleName,
ControlStrategy = "审批",
FormulaText = rule.FormulaText,
RuleText = rule.RuleText,
Message = $"规则[{rule.RuleName}]触发:需审批"
});
break;
case 3: // 提示
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.FormulaText}");
result.Warnings.Add($"规则[{rule.RuleName}]触发:提示 - {rule.RuleText}");
break;
default:
@@ -315,7 +315,7 @@ namespace YLErp.Modules.RiskEngine
RuleCode = "ENGINE_ERROR",
RuleName = "风控引擎执行异常",
ControlStrategy = "审批",
FormulaText = ex.Message,
RuleText = ex.Message,
Message = $"风控引擎异常:{ex.Message},需审批通过"
});
_logger.Error($"[风控引擎] EvaluateRisk 异常 - TradeId: {context?.TradeId}, Error: {ex.Message}");
@@ -334,10 +334,9 @@ namespace YLErp.Modules.RiskEngine
var rule = new RiskRule
{
Id = 103,
RuleCode = "RISK-20260622-0003",
RuleName = "测试规则:名义本金超过1亿且保证金比例超过50%禁止",
FormulaText = "名义本金>1亿, 保证金比例>50%", // 逗号分隔,解析时自动转为 AND
FormulaJson = "{ \"conditions\": [{\"variableCode\":\"trade.StockEqvNotional\",\"operator\":\">\",\"value\":100000000},{\"variableCode\":\"trade.MarginRate\",\"operator\":\">\",\"value\":0.5}] }",
RuleText = "名义本金>1亿, 保证金比例>50%",
ConditionJson = "{ \"conditions\": [{\"variableId\":1,\"operator\":\">\",\"value\":100000000},{\"variableId\":2,\"operator\":\">\",\"value\":0.5}] }",
Status = 1, // Active
ControlStrategy = 1, // 禁止
TriggerPoints = "BOOK_CONFIRM",
+1 -1
View File
@@ -51,7 +51,7 @@ namespace YLErp.Modules.RiskEngine
public string RuleCode { get; set; }
public string RuleName { get; set; }
public string ControlStrategy { get; set; }
public string FormulaText { get; set; }
public string RuleText { get; set; }
public string Message { get; set; }
}
}
+10 -11
View File
@@ -14,22 +14,21 @@ namespace YLErp.Modules.RiskEngine
{
// === 基础信息 ===
public int Id { get; set; }
public string RuleCode { get; set; }
public string RuleName { get; set; }
public string Description { get; set; }
public string RuleText { get; set; }
/// <summary>
/// 【核心】用户输入的规则内容字符串,如"名义本金>1亿"
/// 在规则创建/加载时由 RuleCompiler 解析并编译为 CompiledScript
/// </summary>
public string FormulaText { get; set; }
/// <summary>
/// 【核心】公式 JSON(条件列表的结构化存储),如
/// {"conditions":[{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}]}
/// 【核心】条件JSON(结构化模式的条件列表存储),如
/// {"conditions":[{"variableId":1,"operator":">","value":100000000}]}
/// 由 RuleCompiler 解析并编译为 CompiledScript
/// </summary>
public string FormulaJson { get; set; }
public string ConditionJson { get; set; }
/// <summary>
/// 【核心】规则表达式(自由文本模式,类C#表达式)
/// 由 RuleCompiler 编译为 CompiledScript
/// </summary>
public string RuleExpr { get; set; }
// === 校验策略(原 Application.ControlStrategy===
/// <summary>
File diff suppressed because it is too large Load Diff
+25 -19
View File
@@ -32,7 +32,7 @@ namespace YLErp.Modules.RiskEngine
/// - 可调试:生成的脚本代码可直接阅读和理解
///
/// 示例:
/// FormulaJson: { "conditions": [{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}] }
/// ConditionJson: { "conditions": [{"variableId":1,"operator":">","value":100000000}] }
/// 生成脚本:Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
/// 编译结果:Func&lt;RiskContext, bool&gt;(内部通过 Roslyn 编译缓存)
/// </summary>
@@ -62,13 +62,13 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public static void Compile(this RiskRule rule)
{
if (string.IsNullOrWhiteSpace(rule.FormulaJson))
if (string.IsNullOrWhiteSpace(rule.ConditionJson))
{
throw new ArgumentException("规则 FormulaJson 不能为空", nameof(rule.FormulaJson));
throw new ArgumentException("规则 ConditionJson 不能为空", nameof(rule.ConditionJson));
}
// 1. 解析 FormulaJson
var conditions = ParseFormulaJson(rule.FormulaJson);
// 1. 解析 ConditionJson
var conditions = ParseConditionJson(rule.ConditionJson);
// 2. 生成 Roslyn C# 脚本代码
string scriptCode = BuildScriptCode(conditions);
@@ -81,32 +81,32 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 解析 FormulaJson,提取条件列表
/// 解析 ConditionJson,提取条件列表
/// </summary>
private static List<(string variableCode, string op, object value, string variableType)> ParseFormulaJson(string formulaJson)
private static List<(long variableId, string op, object value, string variableType)> ParseConditionJson(string conditionJson)
{
var result = new List<(string, string, object, string)>();
var jObj = JObject.Parse(formulaJson);
var result = new List<(long, string, object, string)>();
var jObj = JObject.Parse(conditionJson);
var conditions = jObj["conditions"] as JArray;
if (conditions == null || !conditions.Any())
{
throw new ArgumentException("FormulaJson 中缺少 conditions");
throw new ArgumentException("ConditionJson 中缺少 conditions");
}
foreach (var cond in conditions)
{
string variableCode = cond["variableCode"]?.Value<string>();
long variableId = cond["variableId"]?.Value<long>() ?? 0;
string op = cond["operator"]?.Value<string>();
object value = cond["value"]?.Value<object>();
string variableType = cond["variableType"]?.Value<string>() ?? "numeric";
if (string.IsNullOrWhiteSpace(variableCode) || string.IsNullOrWhiteSpace(op))
if (variableId <= 0 || string.IsNullOrWhiteSpace(op))
{
throw new ArgumentException("条件中缺少 variableCode 或 operator");
throw new ArgumentException("条件中缺少 variableId 或 operator");
}
result.Add((variableCode, op, value, variableType));
result.Add((variableId, op, value, variableType));
}
return result;
@@ -115,18 +115,24 @@ namespace YLErp.Modules.RiskEngine
/// <summary>
/// 根据条件列表生成 Roslyn C# 脚本代码
/// 多条件自动用 &&AND)连接
/// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式,
/// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr
/// </summary>
private static string BuildScriptCode(List<(string variableCode, string op, object value, string variableType)> conditions)
private static string BuildScriptCode(List<(long variableId, string op, object value, string variableType)> conditions)
{
var exprParts = new List<string>();
foreach (var (variableCode, op, value, variableType) in conditions)
foreach (var (variableId, op, value, variableType) in conditions)
{
// 解析 variableCode:前缀.属性名
var parts = variableCode.Split('.');
// TODO: 通过 variableId 查变量池获取 VariableExpr(如 "trade.StockEqvNotional"
// 当前暂用 variableId.ToString() 占位,后续接入变量池查询
string variableExpr = variableId.ToString();
// 解析 variableExpr:前缀.属性名
var parts = variableExpr.Split('.');
if (parts.Length != 2)
{
throw new ArgumentException($"variableCode 格式不正确:{variableCode}");
throw new ArgumentException($"variableExpr 格式不正确:{variableExpr}variableId={variableId}");
}
string prefix = parts[0].Trim().ToLower();
+124 -1
View File
@@ -1,2 +1,125 @@
var main = main || {};
main.formatOptions = { "trading": { "umprice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "umpriceP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "umpricePR": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradeSinglePrice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "premiumRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "premiumRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradePrice": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "StockEqvNotional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notionalP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "volatility": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "volatilityP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "greek": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "marginRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "marginRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 } } };
main.formatOptions={
"trading": {
"umprice": {
"precision": 9,
"grouping": true,
"rounded": true,
"percent": false,
"minDecimals": 9,
"maxDecimals": 0
},
"umpriceP": {
"percent": true,
"precision": 2,
"grouping": false,
"rounded": true,
"minDecimals": 2,
"maxDecimals": 0
},
"umpricePR": {
"precision": 4,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 4,
"maxDecimals": 2
},
"tradeSinglePrice": {
"precision": 2,
"grouping": true,
"rounded": true,
"percent": false,
"minDecimals": 2,
"maxDecimals": 0
},
"premiumRateP": {
"percent": true,
"precision": 2,
"grouping": false,
"rounded": true,
"minDecimals": 2,
"maxDecimals": 0
},
"premiumRate": {
"precision": 4,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 4,
"maxDecimals": 2
},
"tradePrice": {
"precision": 2,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 2,
"maxDecimals": 0
},
"StockEqvNotional": {
"precision": 2,
"grouping": true,
"rounded": true,
"percent": false,
"minDecimals": 2,
"maxDecimals": 0
},
"notional": {
"precision": 2,
"grouping": true,
"rounded": true,
"percent": false,
"minDecimals": 2,
"maxDecimals": 0
},
"notionalP": {
"percent": true,
"precision": 2,
"grouping": false,
"rounded": true,
"minDecimals": 2,
"maxDecimals": 0
},
"volatility": {
"precision": 4,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 4,
"maxDecimals": 2
},
"volatilityP": {
"percent": true,
"precision": 2,
"grouping": false,
"rounded": true,
"minDecimals": 2,
"maxDecimals": 0
},
"greek": {
"precision": 2,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 2,
"maxDecimals": 0
},
"marginRateP": {
"percent": true,
"precision": 2,
"grouping": false,
"rounded": true,
"minDecimals": 2,
"maxDecimals": 0
},
"marginRate": {
"precision": 4,
"grouping": false,
"rounded": true,
"percent": false,
"minDecimals": 4,
"maxDecimals": 2
}
}
};
+8
View File
@@ -180,6 +180,14 @@ namespace YLErp.Web
return;
}
var hasRight = false;
#if DEBUG
if (context.Controller is Controllers.RiskRuleController)
{
hasRight = true;
base.OnActionExecuting(context);
return;
}
#endif
var isAjaxRequest = Request.Headers["X-Requested-With"] == "XMLHttpRequest";
var typedHeaders = Request.GetTypedHeaders();