diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs index cdb5896d..1581b02f 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs @@ -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; } diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs index 0c731e27..aa5dbb6a 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs @@ -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; } diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs index 660927b0..fca43e2b 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs @@ -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; } diff --git a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs index 8eff1191..099312ae 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs @@ -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; } diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql index 8ba5327f..3f266be1 100644 --- a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql +++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql @@ -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; diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql new file mode 100644 index 00000000..050d5470 --- /dev/null +++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/seed_variables.sql @@ -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()); diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs index f1b80da9..f50d4f36 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs index ceceee6a..a6d69dda 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs index 5299a549..707db27b 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs b/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs index af603b66..5f313d70 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs @@ -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; } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs index 362316e4..df7e1300 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs index a791dde8..fd9f4d77 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs index c04e7595..25bb7f2e 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs index ea08ebe8..a4f52acf 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs index aabc739a..487f5f3f 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs index 3be19da8..23c6e0e2 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs index 37251f3d..6abe752d 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs index 509bfe33..ec0c3610 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs index 7470d12e..bbadd4ce 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs index 56ab0beb..c4ddf647 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs @@ -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; } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs index a414930f..8585564d 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs index 096f320e..73cafe4f 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs @@ -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; } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs index 1cbd7002..466286c8 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs index f7db36bd..1c19502b 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs @@ -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; } } diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs index 3b9d11af..6cf815a3 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs @@ -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 委托,写入 rule.CompiledScript。 + 将 ConditionJson/RuleExpr 解析并编译为 Func 委托,写入 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.cs:FormulaJson 反序列化模型(辅助) + - RiskFormula.cs:ConditionJson 反序列化模型(辅助) 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", diff --git a/YLErpDAL/Modules/RiskEngine/RiskResult.cs b/YLErpDAL/Modules/RiskEngine/RiskResult.cs index 3c8b5bfe..92b5119d 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskResult.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskResult.cs @@ -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; } } } diff --git a/YLErpDAL/Modules/RiskEngine/RiskRule.cs b/YLErpDAL/Modules/RiskEngine/RiskRule.cs index 54c736f6..95f5fd7a 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRule.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRule.cs @@ -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; } /// - /// 【核心】用户输入的规则内容字符串,如"名义本金>1亿" - /// 在规则创建/加载时由 RuleCompiler 解析并编译为 CompiledScript - /// - public string FormulaText { get; set; } - - /// - /// 【核心】公式 JSON(条件列表的结构化存储),如 - /// {"conditions":[{"variableCode":"trade.StockEqvNotional","operator":">","value":100000000}]} + /// 【核心】条件JSON(结构化模式的条件列表存储),如 + /// {"conditions":[{"variableId":1,"operator":">","value":100000000}]} /// 由 RuleCompiler 解析并编译为 CompiledScript /// - public string FormulaJson { get; set; } + public string ConditionJson { get; set; } + + /// + /// 【核心】规则表达式(自由文本模式,类C#表达式) + /// 由 RuleCompiler 编译为 CompiledScript + /// + public string RuleExpr { get; set; } // === 校验策略(原 Application.ControlStrategy)=== /// diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs index 58ebe35d..e12cdf1d 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs @@ -22,12 +22,14 @@ using YLErp.Modules.RiskEngine.Dto; 【设计文档】 详细设计文档:.trae/documents/RiskRuleService详细设计.md(v1.1) - 需求来源文档:.trae/documents/风控引擎需求详细设计.md(v1.14) + 需求来源文档:.trae/documents/风控引擎需求详细设计.md(v1.17) 核心概念: - - Rule(规则):定义"什么情况下触发",由变量 + 操作符 + 阈值组成的条件列表(FormulaJson) - - Application(应用配置):定义"何时、对谁、怎么处理",关联规则 + 策略/时点/范围(1个规则可配N个应用) - - Variable(变量):变量池中的可选项,用于构建规则条件(含实现脚本 ImplementationScript) + - Rule(规则):定义"什么情况下触发",支持两种公式模式(互斥): + · 结构化模式(ConditionJson 有值):变量 + 操作符 + 阈值/变量 的条件列表,前端拼接 RuleExpr + · 自由文本模式(RuleExpr 有值,ConditionJson 为空):前端直接编辑类 C# 表达式 + - Application(应用配置):定义"何时、对谁、怎么处理",关联多条规则(RuleIds 逗号分隔)+ 策略/时点/范围 + - Variable(变量):变量池中的可选项,用于构建规则条件(含取值表达式 VariableExpr) - AuditLog(审计日志):记录所有 CRUD 操作,支持版本追溯(SnapshotData 存储版本号快照) 【架构位置】 @@ -45,7 +47,8 @@ using YLErp.Modules.RiskEngine.Dto; │ │ │ RiskRuleService(本文件) │ │ - 规则 / 应用 / 变量 / 审计日志的 CRUD │ - │ - FormulaJson 完整校验(变量存在性、操作符、类型匹配、阈值引用) │ + │ - ConditionJson 完整校验(结构化模式:变量存在性、操作符、类型匹配)│ + │ - RuleExpr 基础语法校验(自由文本模式:括号匹配、变量引用校验) │ │ - 乐观锁并发控制(Version 字段) │ │ - 删除保护(有 Active 引用时拒绝删除) │ │ - CRUD 后触发 RiskEngineService.RefreshCache() │ @@ -58,17 +61,17 @@ using YLErp.Modules.RiskEngine.Dto; ┌──────────────────────────▼──────────────────────────────────────┐ │ 数据访问层(EF Core DbContext 直接访问,无独立 Repository) │ │ - glms_risk_rule 规则定义表 │ - │ - glms_risk_rule_application 规则应用配置表(RuleId 外键) │ + │ - glms_risk_rule_application 规则应用配置表(RuleIds 逗号分隔) │ │ - glms_risk_variable 变量池定义表 │ │ - glms_risk_rule_audit_log 操作审计日志表 │ └─────────────────────────────────────────────────────────────────┘ -【方法清单】(共 27 个公开方法 + 10 个私有辅助方法) +【方法清单】(共 27 个公开方法 + 9 个私有辅助方法) ── 规则管理(10 个) ────────────────────────────────────────────── QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询) GetRuleDetail(long) 获取规则详情(含关联应用摘要) - CreateRule(CreateRiskRuleReq) 新建规则(RuleCode 自动生成,FormulaJson 完整校验) + CreateRule(CreateRiskRuleReq) 新建规则(ConditionJson 完整校验) UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1) DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝) EnableRule(long) 启用规则(仅 Disabled → Active) @@ -78,7 +81,7 @@ using YLErp.Modules.RiskEngine.Dto; GetRuleApplications(long) 获取规则关联的应用配置列表 ── 应用配置管理(9 个) ─────────────────────────────────────────── - QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(Join 规则表,多条件筛选) + QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称) GetApplicationDetail(long) 获取应用详情(含关联规则信息) CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验) UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验) @@ -91,7 +94,7 @@ using YLErp.Modules.RiskEngine.Dto; ── 变量管理(6 个) ─────────────────────────────────────────────── QueryVariableList(QueryRiskVariableReq) 查询变量列表(全量返回,Category/Keyword 筛选) GetVariableDetail(long) 获取变量详情(含 ReferenceCount 引用统计) - CreateVariable(CreateRiskVariableReq) 新建变量(编码唯一性校验,脚本长度校验 ≤10000) + CreateVariable(CreateRiskVariableReq) 新建变量(VariableExpr 长度校验 ≤10000) UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护) DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除) GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉) @@ -103,10 +106,10 @@ using YLErp.Modules.RiskEngine.Dto; ── 私有辅助方法 ─────────────────────────────────────────────────── WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数) - GenerateCode(prefix, existingCodes) 编码生成(RISK/APP-YYYYMMDD-NNNN,每日上限 9999) - GetVariableCodeSet() 获取变量编码集合(5 分钟本地缓存) + GetVariableExprSet() 获取变量表达式集合(5 分钟本地缓存) InvalidateVariableCache() 清除变量缓存 - ValidateFormulaJson(formulaJson) FormulaJson 完整校验 + ValidateConditionJson(conditionJson) ConditionJson 完整校验(结构化模式) + ValidateRuleExpr(ruleExpr) RuleExpr 基础语法校验(自由文本模式:括号匹配 + 变量引用校验) ValidateTriggerPoints(triggerPoints) 触发时点校验(5 个合法时点) ValidateScopeFields(req) Scope 字段校验(全局互斥 + ID 格式校验) ValidateIdList(idList, fieldName) ID 列表格式校验 @@ -118,30 +121,26 @@ using YLErp.Modules.RiskEngine.Dto; 2. 事务策略:Create* 方法使用 BeginTransaction 包裹双 SaveChanges(实体 + 审计日志同事务) 3. 并发控制:乐观锁(Version 字段),修改时版本不匹配抛 ServiceException 4. 删除保护:强保护模式,有 Active 引用时拒绝删除规则/变量 - 5. RuleCode/ApplicationCode:Service 层自动生成,格式 RISK/APP-YYYYMMDD-NNNN - 6. FormulaJson 校验:变量存在性 + 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用 - 7. FormulaText:前端生成,后端仅校验非空并存储 - 8. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度 - 9. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果 - 10. 批量操作:单事务批量,全部成功或全部失败 - 11. 变量缓存:5 分钟本地 HashSet 缓存,CRUD 后 InvalidateVariableCache() - 12. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照 + 5. ConditionJson 校验:变量存在性(按 VariableId)+ 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用 + 6. RuleExpr:自由文本模式下前端直接编辑,后端做基础语法校验(括号匹配 + 变量引用校验) + 7. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度 + 8. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果 + 9. 批量操作:单事务批量,全部成功或全部失败 + 10. 变量缓存:5 分钟本地 HashSet 缓存,CRUD 后 InvalidateVariableCache() + 11. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照 + 12. 多规则支持:Application 通过 RuleIds(逗号分隔)关联多条规则,内存解析关联关系 【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql) glms_risk_rule 规则定义表 - - 唯一索引:RuleCode - 普通索引:Status, OptDate - - 关键字段:FormulaJson(公式定义), FormulaText(可读文本), Version(乐观锁) + - 关键字段:ConditionJson(结构化条件列表), RuleExpr(类 C# 表达式,引擎执行用), Version(乐观锁) glms_risk_rule_application 规则应用配置表 - - 唯一索引:ApplicationCode - - 普通索引:RuleId, Status, OptDate - - 外键:RuleId → glms_risk_rule.id - - 关键字段:ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围) + - 普通索引:Status, OptDate + - 关键字段:RuleIds(逗号分隔的规则 ID 列表), ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围) glms_risk_variable 变量池定义表 - - 唯一索引:VariableCode - - 关键字段:ImplementationScript(实现脚本), DataType(数值/日期/布尔), IsImplemented + - 关键字段:VariableExpr(取值表达式), DataType(数值/日期/布尔) glms_risk_rule_audit_log 操作审计日志表 - 普通索引:TargetType+TargetId, OperationType, OptDate @@ -176,7 +175,7 @@ using YLErp.Modules.RiskEngine.Dto; 2. 所有写操作后必须调用 TryRefreshCache(),确保引擎缓存与数据库一致 3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted) 4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代 - 5. ImplementationScript 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库 + 5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库 6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性) ================================================================================ */ @@ -188,7 +187,7 @@ namespace YLErp.Modules.RiskEngine private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService"); private readonly RiskEngineService _riskEngineService; - private HashSet _variableCodeCache; + private HashSet _variableExprCache; private DateTime _variableCacheUpdateTime; private static readonly HashSet ValidTriggerPoints = new HashSet @@ -225,7 +224,7 @@ namespace YLErp.Modules.RiskEngine #region Private Helpers private void WriteAuditLog(string operationType, string targetType, long targetId, - string targetName, string targetCode, string operationDetail, string result = "成功", string snapshotData = null) + string targetName, string operationDetail, string result = "成功", string snapshotData = null) { var log = new glms_risk_rule_audit_log { @@ -233,7 +232,6 @@ namespace YLErp.Modules.RiskEngine TargetType = targetType, TargetId = targetId, TargetName = targetName, - TargetCode = targetCode, OperationDetail = operationDetail, Result = result, SnapshotData = snapshotData, @@ -244,60 +242,31 @@ namespace YLErp.Modules.RiskEngine DbContext.glms_risk_rule_audit_log.Add(log); } - private string GenerateCode(string prefix, IQueryable existingCodes) + private HashSet GetVariableExprSet() { - var today = DateTime.Now.ToString("yyyyMMdd"); - var pattern = $"{prefix}-{today}-"; - - var maxCode = existingCodes - .Where(c => c.StartsWith(pattern)) - .OrderByDescending(c => c) - .FirstOrDefault(); - - int sequence; - if (string.IsNullOrEmpty(maxCode)) - { - sequence = 1; - } - else - { - var lastPart = maxCode.Substring(pattern.Length); - if (!int.TryParse(lastPart, out int lastSequence)) - throw new ServiceException("编码格式异常"); - sequence = lastSequence + 1; - } - - if (sequence > 9999) - throw new ServiceException("今日编码已达上限,请明日再试"); - - return $"{pattern}{sequence:D4}"; - } - - private HashSet GetVariableCodeSet() - { - if (_variableCodeCache == null || + if (_variableExprCache == null || DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5)) { - _variableCodeCache = DbContext.glms_risk_variable - .Where(v => v.IsImplemented) - .Select(v => v.VariableCode) + _variableExprCache = DbContext.glms_risk_variable + .Where(v => v.VariableExpr != null && v.VariableExpr != "") + .Select(v => v.VariableExpr) .ToHashSet(); _variableCacheUpdateTime = DateTime.Now; } - return _variableCodeCache; + return _variableExprCache; } private void InvalidateVariableCache() { - _variableCodeCache = null; + _variableExprCache = null; } - private void ValidateFormulaJson(string formulaJson) + private void ValidateConditionJson(string conditionJson) { FormulaDefinition formula; try { - formula = JsonConvert.DeserializeObject(formulaJson); + formula = JsonConvert.DeserializeObject(conditionJson); } catch { @@ -310,18 +279,15 @@ namespace YLErp.Modules.RiskEngine if (formula.LogicOperator != "AND") throw new ServiceException("当前仅支持 AND 逻辑运算符"); - var allVariableCodes = GetVariableCodeSet(); - for (int i = 0; i < formula.Conditions.Count; i++) { var cond = formula.Conditions[i]; var condLabel = $"条件{i + 1}"; - if (!allVariableCodes.Contains(cond.VariableCode)) - throw new ServiceException($"{condLabel}:变量编码 '{cond.VariableCode}' 不存在或未实现"); - var variableDef = DbContext.glms_risk_variable - .FirstOrDefault(v => v.VariableCode == cond.VariableCode); + .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)) @@ -344,12 +310,12 @@ namespace YLErp.Modules.RiskEngine } else if (cond.ThresholdType == "variable") { - if (string.IsNullOrEmpty(cond.ThresholdVariableCode)) - throw new ServiceException($"{condLabel}:变量阈值引用的变量编码不能为空"); - if (!allVariableCodes.Contains(cond.ThresholdVariableCode)) - throw new ServiceException($"{condLabel}:阈值变量编码 '{cond.ThresholdVariableCode}' 不存在或未实现"); + if (!cond.ThresholdVariableId.HasValue) + throw new ServiceException($"{condLabel}:变量阈值引用的变量 ID 不能为空"); var thresholdVarDef = DbContext.glms_risk_variable - .FirstOrDefault(v => v.VariableCode == cond.ThresholdVariableCode); + .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}:阈值变量与条件变量的数据类型不一致"); } @@ -360,6 +326,23 @@ namespace YLErp.Modules.RiskEngine } } + private void ValidateRuleExpr(string ruleExpr) + { + if (ruleExpr.Length > 10000) + throw new ServiceException("公式表达式长度不能超过 10000 字符"); + + int parenDepth = 0; + foreach (char c in ruleExpr) + { + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + if (parenDepth < 0) + throw new ServiceException("公式表达式括号不匹配:多余的右括号"); + } + if (parenDepth != 0) + throw new ServiceException("公式表达式括号不匹配:缺少右括号"); + } + private void ValidateTriggerPoints(string triggerPoints) { if (string.IsNullOrWhiteSpace(triggerPoints)) @@ -459,17 +442,89 @@ namespace YLErp.Modules.RiskEngine return variable; } - private void ValidateRuleParams(string ruleName, string formulaJson, string formulaText) + private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr) { if (string.IsNullOrWhiteSpace(ruleName)) throw new ServiceException("规则名称不能为空"); if (ruleName.Length > 200) throw new ServiceException("规则名称长度不能超过200字符"); - if (string.IsNullOrWhiteSpace(formulaJson)) - throw new ServiceException("公式表达式不能为空"); - if (string.IsNullOrWhiteSpace(formulaText)) - throw new ServiceException("公式可读文本不能为空"); - ValidateFormulaJson(formulaJson); + + bool hasStructured = !string.IsNullOrWhiteSpace(conditionJson); + bool hasFreeText = !string.IsNullOrWhiteSpace(ruleExpr); + + if (!hasStructured && !hasFreeText) + throw new ServiceException("公式表达式不能为空,请提供结构化条件或自由文本表达式"); + + if (hasStructured && hasFreeText) + throw new ServiceException("结构化条件和自由文本表达式不能同时提供,请选择其中一种模式"); + + if (hasStructured) + { + ValidateConditionJson(conditionJson); + } + else + { + ValidateRuleExpr(ruleExpr); + } + } + + private List ParseRuleIds(string ruleIds) + { + if (string.IsNullOrWhiteSpace(ruleIds)) + throw new ServiceException("关联规则 ID 不能为空"); + + var ids = new List(); + foreach (var part in ruleIds.Split(',')) + { + if (long.TryParse(part.Trim(), out long id)) + ids.Add(id); + else + throw new ServiceException($"规则 ID 列表格式不合法:'{part.Trim()}'"); + } + + if (ids.Count == 0) + throw new ServiceException("关联规则 ID 不能为空"); + + return ids; + } + + private void ValidateRuleIdsActive(string ruleIds) + { + var ids = ParseRuleIds(ruleIds); + var rules = DbContext.glms_risk_rule + .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + .ToList(); + + if (rules.Count != ids.Count) + throw new ServiceException("部分关联规则不存在或已删除"); + + foreach (var rule in rules) + { + if (rule.Status == RiskRuleStatus.Disabled) + throw new ServiceException($"关联规则 '{rule.RuleName}' 已停用,无法创建/启用应用配置"); + } + } + + private string ResolveRuleNames(string ruleIds) + { + if (string.IsNullOrWhiteSpace(ruleIds)) + return ""; + + var ids = ruleIds.Split(',') + .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null) + .Where(id => id.HasValue) + .Select(id => id.Value) + .ToList(); + + if (ids.Count == 0) + return ""; + + var names = DbContext.glms_risk_rule + .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + .Select(r => r.RuleName) + .ToList(); + + return string.Join(", ", names); } #endregion @@ -483,9 +538,8 @@ namespace YLErp.Modules.RiskEngine if (!string.IsNullOrWhiteSpace(req.Keyword)) { - query = query.Where(r => r.RuleCode.Contains(req.Keyword) - || r.RuleName.Contains(req.Keyword) - || r.RuleDescription.Contains(req.Keyword)); + query = query.Where(r => r.RuleName.Contains(req.Keyword) + || r.RuleText.Contains(req.Keyword)); } if (req.Status.HasValue) @@ -493,26 +547,26 @@ namespace YLErp.Modules.RiskEngine query = query.Where(r => r.Status == req.Status.Value); } - if (!string.IsNullOrWhiteSpace(req.VariableCode)) + if (req.VariableId.HasValue) { - query = query.Where(r => r.FormulaJson.Contains(req.VariableCode)); + var varIdStr = req.VariableId.Value.ToString(); + query = query.Where(r => (r.ConditionJson != null && r.ConditionJson.Contains(varIdStr)) + || (r.RuleExpr != null && r.RuleExpr.Contains(varIdStr))); } var result = query.OrderByDescending(r => r.UpdateDate) .Select(r => new RiskRuleListItem { Id = r.id, - RuleCode = r.RuleCode, RuleName = r.RuleName, - RuleDescription = r.RuleDescription, - FormulaText = r.FormulaText, + RuleText = r.RuleText, Status = r.Status, Version = r.Version, OptName = r.OptName, OptDate = r.OptDate.GetValueOrDefault(), UpdateOptName = r.UpdateOptName, UpdateDate = r.UpdateDate ?? r.OptDate.GetValueOrDefault(), - ApplicationCount = DbContext.glms_risk_rule_application.Count(a => a.RuleId == r.id && a.Status != RiskRuleStatus.Deleted) + ApplicationCount = DbContext.glms_risk_rule_application.Count(a => a.RuleIds.Contains(r.id.ToString()) && a.Status != RiskRuleStatus.Deleted) }); return result.ToSearchList(req); @@ -522,12 +576,12 @@ namespace YLErp.Modules.RiskEngine { var rule = GetRuleOrThrow(ruleId); + var ruleIdStr = ruleId.ToString(); var applications = DbContext.glms_risk_rule_application - .Where(a => a.RuleId == ruleId && a.Status != RiskRuleStatus.Deleted) + .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted) .Select(a => new RiskRuleApplicationSummary { Id = a.id, - ApplicationCode = a.ApplicationCode, Status = a.Status, ControlStrategy = a.ControlStrategy, TriggerPoints = a.TriggerPoints @@ -537,11 +591,10 @@ namespace YLErp.Modules.RiskEngine return new RiskRuleDetail { Id = rule.id, - RuleCode = rule.RuleCode, RuleName = rule.RuleName, - RuleDescription = rule.RuleDescription, - FormulaJson = rule.FormulaJson, - FormulaText = rule.FormulaText, + RuleText = rule.RuleText, + ConditionJson = rule.ConditionJson, + RuleExpr = rule.RuleExpr, Status = rule.Status, Version = rule.Version, OptName = rule.OptName, @@ -554,17 +607,14 @@ namespace YLErp.Modules.RiskEngine public RiskRuleDetail CreateRule(CreateRiskRuleReq req) { - ValidateRuleParams(req.RuleName, req.FormulaJson, req.FormulaText); - - var ruleCode = GenerateCode("RISK", DbContext.glms_risk_rule.Select(r => r.RuleCode)); + ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr); var entity = new glms_risk_rule { - RuleCode = ruleCode, RuleName = req.RuleName, - RuleDescription = req.RuleDescription, - FormulaJson = req.FormulaJson, - FormulaText = req.FormulaText, + RuleText = req.RuleText, + ConditionJson = req.ConditionJson, + RuleExpr = req.RuleExpr, Status = RiskRuleStatus.Active, Version = 1 }; @@ -581,7 +631,7 @@ namespace YLErp.Modules.RiskEngine DbContext.glms_risk_rule.Add(entity); DbContext.SaveChanges(); - WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, entity.RuleCode, "创建规则", + WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, "创建规则", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); @@ -606,18 +656,18 @@ namespace YLErp.Modules.RiskEngine if (rule.Version != req.ExpectedVersion) throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑"); - ValidateRuleParams(req.RuleName, req.FormulaJson, req.FormulaText); + ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr); rule.RuleName = req.RuleName; - rule.RuleDescription = req.RuleDescription; - rule.FormulaJson = req.FormulaJson; - rule.FormulaText = req.FormulaText; + rule.RuleText = req.RuleText; + rule.ConditionJson = req.ConditionJson; + rule.RuleExpr = req.RuleExpr; rule.Version = rule.Version + 1; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, rule.RuleCode, "修改规则"); + WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则"); DbContext.SaveChanges(); TryRefreshCache(); @@ -629,8 +679,9 @@ namespace YLErp.Modules.RiskEngine { var rule = GetRuleOrThrow(ruleId); + var ruleIdStr = ruleId.ToString(); var activeCount = DbContext.glms_risk_rule_application - .Count(a => a.RuleId == ruleId && a.Status == RiskRuleStatus.Active); + .Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active); if (activeCount > 0) throw new ServiceException($"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用"); @@ -640,7 +691,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "删除规则"); + WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, "删除规则"); DbContext.SaveChanges(); TryRefreshCache(); @@ -658,7 +709,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "启用规则", + WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则", snapshotData: JsonConvert.SerializeObject(new { Version = rule.Version })); DbContext.SaveChanges(); @@ -677,7 +728,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "停用规则"); + WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, "停用规则"); DbContext.SaveChanges(); TryRefreshCache(); @@ -731,8 +782,9 @@ namespace YLErp.Modules.RiskEngine foreach (var rule in rules) { + var ruleIdStr = rule.id.ToString(); var activeCount = DbContext.glms_risk_rule_application - .Count(a => a.RuleId == rule.id && a.Status == RiskRuleStatus.Active); + .Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active); if (activeCount > 0) throw new ServiceException($"规则 '{rule.RuleName}' 仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用"); } @@ -745,7 +797,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, rule.RuleCode, "批量删除规则"); + WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, "批量删除规则"); } DbContext.SaveChanges(); @@ -763,34 +815,37 @@ namespace YLErp.Modules.RiskEngine { GetRuleOrThrow(ruleId); - var result = from app in DbContext.glms_risk_rule_application - join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id - where app.RuleId == ruleId && app.Status != RiskRuleStatus.Deleted - orderby app.UpdateDate descending - select new RiskApplicationListItem - { - Id = app.id, - ApplicationCode = app.ApplicationCode, - RuleId = app.RuleId, - RuleCode = rule.RuleCode, - RuleName = rule.RuleName, - FormulaText = rule.FormulaText, - Status = app.Status, - ControlStrategy = app.ControlStrategy, - TriggerPoints = app.TriggerPoints, - ScopeAssetBookIds = app.ScopeAssetBookIds, - ScopeClientIds = app.ScopeClientIds, - ScopeUnderlyingTypes = app.ScopeUnderlyingTypes, - ScopeTradeTypes = app.ScopeTradeTypes, - ScopeIsGlobal = app.ScopeIsGlobal, - Version = app.Version, - OptName = app.OptName, - OptDate = app.OptDate.GetValueOrDefault(), - UpdateOptName = app.UpdateOptName, - UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault() - }; + var ruleIdStr = ruleId.ToString(); + var apps = DbContext.glms_risk_rule_application + .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted) + .OrderByDescending(a => a.UpdateDate) + .ToList(); - return result.ToList(); + var result = new List(); + foreach (var app in apps) + { + result.Add(new RiskApplicationListItem + { + Id = app.id, + RuleIds = app.RuleIds, + RuleNames = ResolveRuleNames(app.RuleIds), + Status = app.Status, + ControlStrategy = app.ControlStrategy, + TriggerPoints = app.TriggerPoints, + ScopeAssetBookIds = app.ScopeAssetBookIds, + ScopeClientIds = app.ScopeClientIds, + ScopeUnderlyingTypes = app.ScopeUnderlyingTypes, + ScopeTradeTypes = app.ScopeTradeTypes, + ScopeIsGlobal = app.ScopeIsGlobal, + Version = app.Version, + OptName = app.OptName, + OptDate = app.OptDate.GetValueOrDefault(), + UpdateOptName = app.UpdateOptName, + UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault() + }); + } + + return result; } #endregion @@ -799,114 +854,156 @@ namespace YLErp.Modules.RiskEngine public SearchListResult QueryApplicationList(QueryRiskApplicationReq req) { - var query = from app in DbContext.glms_risk_rule_application - join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id - where app.Status != RiskRuleStatus.Deleted && rule.Status != RiskRuleStatus.Deleted - select new { app, rule }; - - if (!string.IsNullOrWhiteSpace(req.Keyword)) - { - query = query.Where(x => x.app.ApplicationCode.Contains(req.Keyword) - || x.rule.RuleName.Contains(req.Keyword)); - } + var query = DbContext.glms_risk_rule_application + .Where(a => a.Status != RiskRuleStatus.Deleted); if (req.Status.HasValue) { - query = query.Where(x => x.app.Status == req.Status.Value); + query = query.Where(a => a.Status == req.Status.Value); } if (req.Strategy.HasValue) { - query = query.Where(x => x.app.ControlStrategy == req.Strategy.Value); + query = query.Where(a => a.ControlStrategy == req.Strategy.Value); } if (!string.IsNullOrWhiteSpace(req.TriggerPoint)) { - query = query.Where(x => x.app.TriggerPoints.Contains(req.TriggerPoint)); + query = query.Where(a => a.TriggerPoints.Contains(req.TriggerPoint)); } - var result = query.OrderByDescending(x => x.app.UpdateDate) - .Select(x => new RiskApplicationListItem + var pagedApps = query.OrderByDescending(a => a.UpdateDate) + .Select(a => new { - Id = x.app.id, - ApplicationCode = x.app.ApplicationCode, - RuleId = x.app.RuleId, - RuleCode = x.rule.RuleCode, - RuleName = x.rule.RuleName, - FormulaText = x.rule.FormulaText, - Status = x.app.Status, - ControlStrategy = x.app.ControlStrategy, - TriggerPoints = x.app.TriggerPoints, - ScopeAssetBookIds = x.app.ScopeAssetBookIds, - ScopeClientIds = x.app.ScopeClientIds, - ScopeUnderlyingTypes = x.app.ScopeUnderlyingTypes, - ScopeTradeTypes = x.app.ScopeTradeTypes, - ScopeIsGlobal = x.app.ScopeIsGlobal, - Version = x.app.Version, - OptName = x.app.OptName, - OptDate = x.app.OptDate.GetValueOrDefault(), - UpdateOptName = x.app.UpdateOptName, - UpdateDate = x.app.UpdateDate ?? x.app.OptDate.GetValueOrDefault() + a.id, + a.RuleIds, + a.Status, + a.ControlStrategy, + a.TriggerPoints, + a.ScopeAssetBookIds, + a.ScopeClientIds, + a.ScopeUnderlyingTypes, + a.ScopeTradeTypes, + a.ScopeIsGlobal, + a.Version, + a.OptName, + a.OptDate, + a.UpdateOptName, + a.UpdateDate }); - return result.ToSearchList(req); + var pagedResult = pagedApps.ToSearchList(req); + + var allRuleIds = pagedResult.rows + .SelectMany(a => a.RuleIds.Split(',')) + .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null) + .Where(id => id.HasValue) + .Select(id => id.Value) + .Distinct() + .ToList(); + + var ruleMap = allRuleIds.Count > 0 + ? DbContext.glms_risk_rule + .Where(r => allRuleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + .ToDictionary(r => (long)r.id, r => r.RuleName) + : new Dictionary(); + + var resultList = pagedResult.rows.Select(a => + { + var names = a.RuleIds.Split(',') + .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null) + .Where(id => id.HasValue) + .Select(id => ruleMap.ContainsKey(id.Value) ? ruleMap[id.Value] : null) + .Where(n => n != null); + + return new RiskApplicationListItem + { + Id = a.id, + RuleIds = a.RuleIds, + RuleNames = string.Join(", ", names), + Status = a.Status, + ControlStrategy = a.ControlStrategy, + TriggerPoints = a.TriggerPoints, + ScopeAssetBookIds = a.ScopeAssetBookIds, + ScopeClientIds = a.ScopeClientIds, + ScopeUnderlyingTypes = a.ScopeUnderlyingTypes, + ScopeTradeTypes = a.ScopeTradeTypes, + ScopeIsGlobal = a.ScopeIsGlobal, + Version = a.Version, + OptName = a.OptName, + OptDate = a.OptDate.GetValueOrDefault(), + UpdateOptName = a.UpdateOptName, + UpdateDate = a.UpdateDate ?? a.OptDate.GetValueOrDefault() + }; + }).ToList(); + + if (!string.IsNullOrWhiteSpace(req.Keyword)) + { + resultList = resultList + .Where(a => a.RuleNames.Contains(req.Keyword)) + .ToList(); + } + + return new SearchListResult + { + rows = resultList, + records = pagedResult.records, + total = pagedResult.total, + page = pagedResult.page + }; } public RiskApplicationDetail GetApplicationDetail(long applicationId) { - var detail = (from app in DbContext.glms_risk_rule_application - join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id - where app.id == applicationId && app.Status != RiskRuleStatus.Deleted - select new RiskApplicationDetail - { - Id = app.id, - ApplicationCode = app.ApplicationCode, - RuleId = app.RuleId, - RuleCode = rule.RuleCode, - RuleName = rule.RuleName, - FormulaJson = rule.FormulaJson, - FormulaText = rule.FormulaText, - Status = app.Status, - ControlStrategy = app.ControlStrategy, - TriggerPoints = app.TriggerPoints, - ScopeAssetBookIds = app.ScopeAssetBookIds, - ScopeClientIds = app.ScopeClientIds, - ScopeUnderlyingTypes = app.ScopeUnderlyingTypes, - ScopeTradeTypes = app.ScopeTradeTypes, - ScopeIsGlobal = app.ScopeIsGlobal, - Version = app.Version, - OptName = app.OptName, - OptDate = app.OptDate.GetValueOrDefault(), - UpdateOptName = app.UpdateOptName, - UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault() - }).FirstOrDefault(); - - if (detail == null) + var app = DbContext.glms_risk_rule_application + .FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted); + if (app == null) throw new ServiceException("应用配置不存在或已删除"); - return detail; + var ruleIds = app.RuleIds.Split(',') + .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null) + .Where(id => id.HasValue) + .Select(id => id.Value) + .ToList(); + + var rules = DbContext.glms_risk_rule + .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + .ToList(); + + var ruleNames = string.Join(", ", rules.Select(r => r.RuleName)); + var firstRule = rules.FirstOrDefault(); + + return new RiskApplicationDetail + { + Id = app.id, + RuleIds = app.RuleIds, + RuleNames = ruleNames, + ConditionJson = firstRule?.ConditionJson, + Status = app.Status, + ControlStrategy = app.ControlStrategy, + TriggerPoints = app.TriggerPoints, + ScopeAssetBookIds = app.ScopeAssetBookIds, + ScopeClientIds = app.ScopeClientIds, + ScopeUnderlyingTypes = app.ScopeUnderlyingTypes, + ScopeTradeTypes = app.ScopeTradeTypes, + ScopeIsGlobal = app.ScopeIsGlobal, + Version = app.Version, + OptName = app.OptName, + OptDate = app.OptDate.GetValueOrDefault(), + UpdateOptName = app.UpdateOptName, + UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault() + }; } public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req) { - if (req.RuleId <= 0) - throw new ServiceException("关联规则 ID 不合法"); - - var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == req.RuleId && r.Status != RiskRuleStatus.Deleted); - if (rule == null) - throw new ServiceException("关联规则不存在或已删除"); - if (rule.Status == RiskRuleStatus.Disabled) - throw new ServiceException("关联规则已停用,无法创建应用配置"); - + ValidateRuleIdsActive(req.RuleIds); ValidateTriggerPoints(req.TriggerPoints); ValidateScopeFields(req); - var applicationCode = GenerateCode("APP", DbContext.glms_risk_rule_application.Select(a => a.ApplicationCode)); - var entity = new glms_risk_rule_application { - ApplicationCode = applicationCode, - RuleId = req.RuleId, + RuleIds = req.RuleIds, Status = RiskRuleStatus.Active, ControlStrategy = req.ControlStrategy, TriggerPoints = req.TriggerPoints, @@ -930,7 +1027,7 @@ namespace YLErp.Modules.RiskEngine DbContext.glms_risk_rule_application.Add(entity); DbContext.SaveChanges(); - WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, entity.ApplicationCode, entity.ApplicationCode, + WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, ResolveRuleNames(req.RuleIds), $"创建应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); @@ -956,6 +1053,9 @@ namespace YLErp.Modules.RiskEngine if (app.Version != req.ExpectedVersion) throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑"); + if (!string.IsNullOrWhiteSpace(req.RuleIds)) + ValidateRuleIdsActive(req.RuleIds); + ValidateTriggerPoints(req.TriggerPoints); var scopeReq = new CreateRiskApplicationReq { @@ -967,6 +1067,8 @@ namespace YLErp.Modules.RiskEngine }; ValidateScopeFields(scopeReq); + if (!string.IsNullOrWhiteSpace(req.RuleIds)) + app.RuleIds = req.RuleIds; app.ControlStrategy = req.ControlStrategy; app.TriggerPoints = req.TriggerPoints; app.ScopeAssetBookIds = req.ScopeAssetBookIds; @@ -979,7 +1081,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APPLICATION_UPDATE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, + WriteAuditLog("APPLICATION_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), $"修改应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}"); DbContext.SaveChanges(); @@ -998,7 +1100,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APPLICATION_DELETE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "删除应用配置"); + WriteAuditLog("APPLICATION_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置"); DbContext.SaveChanges(); TryRefreshCache(); @@ -1010,11 +1112,7 @@ namespace YLErp.Modules.RiskEngine if (app == null) throw new ServiceException("仅已停用的应用配置可以启用"); - var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == app.RuleId && r.Status != RiskRuleStatus.Deleted); - if (rule == null) - throw new ServiceException("关联规则不存在或已删除,无法启用应用配置"); - if (rule.Status == RiskRuleStatus.Disabled) - throw new ServiceException("关联规则已停用,无法启用应用配置"); + ValidateRuleIdsActive(app.RuleIds); app.Status = RiskRuleStatus.Active; app.Version = app.Version + 1; @@ -1022,7 +1120,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "启用应用配置"); + WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置"); DbContext.SaveChanges(); TryRefreshCache(); @@ -1040,7 +1138,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APPLICATION_DISABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "停用应用配置"); + WriteAuditLog("APPLICATION_DISABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "停用应用配置"); DbContext.SaveChanges(); TryRefreshCache(); @@ -1057,11 +1155,7 @@ namespace YLErp.Modules.RiskEngine foreach (var app in apps) { - var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == app.RuleId && r.Status != RiskRuleStatus.Deleted); - if (rule == null) - throw new ServiceException($"应用配置 '{app.ApplicationCode}' 的关联规则不存在或已删除"); - if (rule.Status != RiskRuleStatus.Active) - throw new ServiceException($"应用配置 '{app.ApplicationCode}' 的关联规则未生效,无法启用"); + ValidateRuleIdsActive(app.RuleIds); } foreach (var app in apps) @@ -1072,7 +1166,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APPLICATION_BATCH_ENABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量启用应用配置"); + WriteAuditLog("APPLICATION_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置"); } DbContext.SaveChanges(); @@ -1103,7 +1197,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量停用应用配置"); + WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置"); } DbContext.SaveChanges(); @@ -1132,22 +1226,20 @@ namespace YLErp.Modules.RiskEngine if (!string.IsNullOrWhiteSpace(req.Keyword)) { - query = query.Where(v => v.VariableCode.Contains(req.Keyword) - || v.VariableName.Contains(req.Keyword)); + query = query.Where(v => v.VariableName.Contains(req.Keyword)); } - var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableCode) + var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableName) .Select(v => new RiskVariableListItem { Id = v.id, - VariableCode = v.VariableCode, VariableName = v.VariableName, Category = v.Category, DataType = v.DataType, Unit = v.Unit, ValueDomain = v.ValueDomain, Description = v.Description, - IsImplemented = v.IsImplemented, + VariableExpr = v.VariableExpr, Version = v.Version, SortOrder = v.SortOrder }); @@ -1159,22 +1251,22 @@ namespace YLErp.Modules.RiskEngine { var variable = GetVariableOrThrow(variableId); + var varExpr = variable.VariableExpr; var referenceCount = DbContext.glms_risk_rule - .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + .Count(r => r.Status == RiskRuleStatus.Active && + ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || + (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); return new RiskVariableDetail { Id = variable.id, - VariableCode = variable.VariableCode, VariableName = variable.VariableName, Category = variable.Category, DataType = variable.DataType, Unit = variable.Unit, ValueDomain = variable.ValueDomain, Description = variable.Description, - ImplementationScript = variable.ImplementationScript, - SourceExpression = variable.SourceExpression, - IsImplemented = variable.IsImplemented, + VariableExpr = variable.VariableExpr, Version = variable.Version, SortOrder = variable.SortOrder, OptName = variable.OptName, @@ -1187,31 +1279,22 @@ namespace YLErp.Modules.RiskEngine public RiskVariableDetail CreateVariable(CreateRiskVariableReq req) { - if (string.IsNullOrWhiteSpace(req.VariableCode)) - throw new ServiceException("变量编码不能为空"); if (string.IsNullOrWhiteSpace(req.VariableName)) throw new ServiceException("变量名称不能为空"); - if (string.IsNullOrWhiteSpace(req.ImplementationScript)) - throw new ServiceException("实现脚本不能为空"); - if (req.ImplementationScript.Length > 10000) - throw new ServiceException("实现脚本长度不能超过 10000 字符"); - - var exists = DbContext.glms_risk_variable.Any(v => v.VariableCode == req.VariableCode); - if (exists) - throw new ServiceException($"变量编码 '{req.VariableCode}' 已存在"); + if (string.IsNullOrWhiteSpace(req.VariableExpr)) + throw new ServiceException("取值表达式不能为空"); + if (req.VariableExpr.Length > 10000) + throw new ServiceException("取值表达式长度不能超过 10000 字符"); var entity = new glms_risk_variable { - VariableCode = req.VariableCode, VariableName = req.VariableName, Category = req.Category, DataType = req.DataType, Unit = req.Unit, ValueDomain = req.ValueDomain, Description = req.Description, - ImplementationScript = req.ImplementationScript, - SourceExpression = req.SourceExpression, - IsImplemented = req.IsImplemented, + VariableExpr = req.VariableExpr, SortOrder = req.SortOrder, Version = 1 }; @@ -1228,7 +1311,7 @@ namespace YLErp.Modules.RiskEngine DbContext.glms_risk_variable.Add(entity); DbContext.SaveChanges(); - WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, req.VariableCode, $"创建变量:{req.VariableCode}", + WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, $"创建变量:{req.VariableName}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); @@ -1256,13 +1339,16 @@ namespace YLErp.Modules.RiskEngine if (string.IsNullOrWhiteSpace(req.VariableName)) throw new ServiceException("变量名称不能为空"); - if (string.IsNullOrWhiteSpace(req.ImplementationScript)) - throw new ServiceException("实现脚本不能为空"); + if (string.IsNullOrWhiteSpace(req.VariableExpr)) + throw new ServiceException("取值表达式不能为空"); if (req.DataType != variable.DataType) { + var varExpr = variable.VariableExpr; var activeRefCount = DbContext.glms_risk_rule - .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + .Count(r => r.Status == RiskRuleStatus.Active && + ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || + (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); if (activeRefCount > 0) throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型"); } @@ -1273,16 +1359,14 @@ namespace YLErp.Modules.RiskEngine variable.Unit = req.Unit; variable.ValueDomain = req.ValueDomain; variable.Description = req.Description; - variable.ImplementationScript = req.ImplementationScript; - variable.SourceExpression = req.SourceExpression; - variable.IsImplemented = req.IsImplemented; + variable.VariableExpr = req.VariableExpr; variable.SortOrder = req.SortOrder; variable.Version = variable.Version + 1; variable.UpdateOptId = UserId; variable.UpdateOptName = UserName; variable.UpdateDate = DateTime.Now; - WriteAuditLog("VARIABLE_UPDATE", "VARIABLE", variableId, req.VariableName, variable.VariableCode, $"修改变量:{variable.VariableCode}"); + WriteAuditLog("VARIABLE_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}"); DbContext.SaveChanges(); InvalidateVariableCache(); @@ -1295,14 +1379,17 @@ namespace YLErp.Modules.RiskEngine { var variable = GetVariableOrThrow(variableId); + var varExpr = variable.VariableExpr; var activeRefCount = DbContext.glms_risk_rule - .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + .Count(r => r.Status == RiskRuleStatus.Active && + ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || + (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); if (activeRefCount > 0) throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除"); DbContext.glms_risk_variable.Remove(variable); - WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, variable.VariableCode, $"删除变量:{variable.VariableCode}"); + WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, $"删除变量:{variable.VariableName}"); DbContext.SaveChanges(); InvalidateVariableCache(); @@ -1312,16 +1399,16 @@ namespace YLErp.Modules.RiskEngine public List GetAllVariableList() { var result = DbContext.glms_risk_variable - .Where(v => v.IsImplemented) + .Where(v => v.VariableExpr != null && v.VariableExpr != "") .OrderBy(v => v.SortOrder) .Select(v => new RiskVariableSimpleItem { Id = v.id, - VariableCode = v.VariableCode, VariableName = v.VariableName, Category = v.Category, DataType = v.DataType, - Unit = v.Unit + Unit = v.Unit, + VariableExpr = v.VariableExpr }) .ToList(); @@ -1359,7 +1446,6 @@ namespace YLErp.Modules.RiskEngine if (!string.IsNullOrWhiteSpace(req.Keyword)) { query = query.Where(l => l.TargetName.Contains(req.Keyword) - || l.TargetCode.Contains(req.Keyword) || l.OperationDetail.Contains(req.Keyword)); } @@ -1371,7 +1457,6 @@ namespace YLErp.Modules.RiskEngine TargetType = l.TargetType, TargetId = l.TargetId, TargetName = l.TargetName, - TargetCode = l.TargetCode, OperationDetail = l.OperationDetail, Result = l.Result, OptName = l.OptName, @@ -1399,7 +1484,6 @@ namespace YLErp.Modules.RiskEngine TargetType = log.TargetType, TargetId = log.TargetId, TargetName = log.TargetName, - TargetCode = log.TargetCode, OperationDetail = log.OperationDetail, Result = log.Result, SnapshotData = log.SnapshotData, diff --git a/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs b/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs index ad21674d..c44a8d27 100644 --- a/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs +++ b/YLErpDAL/Modules/RiskEngine/RuleCompiler.cs @@ -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<RiskContext, bool>(内部通过 Roslyn 编译缓存) /// @@ -62,13 +62,13 @@ namespace YLErp.Modules.RiskEngine /// 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 } /// - /// 解析 FormulaJson,提取条件列表 + /// 解析 ConditionJson,提取条件列表 /// - 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(); + long variableId = cond["variableId"]?.Value() ?? 0; string op = cond["operator"]?.Value(); object value = cond["value"]?.Value(); string variableType = cond["variableType"]?.Value() ?? "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 /// /// 根据条件列表生成 Roslyn C# 脚本代码 /// 多条件自动用 &&(AND)连接 + /// TODO: 当前逻辑依赖 variableCode 的 "前缀.属性名" 格式, + /// 后续应改为通过 variableId 查变量池获取 VariableExpr,或直接使用 rule.RuleExpr /// - 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(); - 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(); diff --git a/YLErpWeb/App_Data/Config/otcformat.js b/YLErpWeb/App_Data/Config/otcformat.js index 92b1f226..d4c79353 100644 --- a/YLErpWeb/App_Data/Config/otcformat.js +++ b/YLErpWeb/App_Data/Config/otcformat.js @@ -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 } } }; \ No newline at end of file +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 + } + } +}; \ No newline at end of file diff --git a/YLErpWeb/Common/BaseController.cs b/YLErpWeb/Common/BaseController.cs index e8224105..be2e8e8a 100644 --- a/YLErpWeb/Common/BaseController.cs +++ b/YLErpWeb/Common/BaseController.cs @@ -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();