diff --git a/.editorconfig b/.editorconfig index 40eb71f1..e2964ef3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -160,3 +160,17 @@ csharp_preserve_single_line_statements = true # CA1819: Properties should not return arrays dotnet_diagnostic.CA1819.severity = silent + +# JavaScript 和 TypeScript 文件 +[*.{js,jsx,ts,tsx}] +# 缩进和间距 +indent_size = 4 +indent_style = space +tab_width = 4 + +# 新行首选项 +end_of_line = crlf +insert_final_newline = false + +# 拖尾逗号不添加 +trailing_comma = none diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs b/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs new file mode 100644 index 00000000..1fd37054 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs @@ -0,0 +1,9 @@ +namespace YLErp.DBModels +{ + public enum RiskControlStrategy + { + Block = 1, + Approval = 2, + Warning = 3 + } +} diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs b/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs new file mode 100644 index 00000000..b4539183 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs @@ -0,0 +1,9 @@ +namespace YLErp.DBModels +{ + public enum RiskRuleStatus + { + Active = 1, + Disabled = 2, + Deleted = 3 + } +} diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs b/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs new file mode 100644 index 00000000..28a9e859 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs @@ -0,0 +1,10 @@ +namespace YLErp.DBModels +{ + public enum RiskVariableCategory + { + BookingElement = 1, + MarketData = 2, + SystemCalc = 3, + BooleanCheck = 4 + } +} diff --git a/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs b/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs new file mode 100644 index 00000000..b5cd9b3d --- /dev/null +++ b/Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs @@ -0,0 +1,9 @@ +namespace YLErp.DBModels +{ + public enum RiskVariableDataType + { + Numeric = 1, + Date = 2, + Boolean = 3 + } +} diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs new file mode 100644 index 00000000..cdb5896d --- /dev/null +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; + +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 RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active; + public int Version { get; set; } = 1; + public int? UpdateOptId { get; set; } + public string UpdateOptName { get; set; } + public DateTime? UpdateDate { get; set; } + } +} diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs new file mode 100644 index 00000000..0c731e27 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations.Schema; + +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 RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active; + public RiskControlStrategy ControlStrategy { get; set; } + public string TriggerPoints { get; set; } + public string ScopeAssetBookIds { get; set; } + public string ScopeClientIds { get; set; } + public string ScopeUnderlyingTypes { get; set; } + public string ScopeTradeTypes { get; set; } + public bool ScopeIsGlobal { get; set; } + public int Version { get; set; } = 1; + public int? UpdateOptId { get; set; } + public string UpdateOptName { get; set; } + public DateTime? UpdateDate { get; set; } + } +} diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs new file mode 100644 index 00000000..660927b0 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace YLErp.DBModels +{ + [Table("glms_risk_rule_audit_log")] + public class glms_risk_rule_audit_log : DBModelBase + { + public string OperationType { get; set; } + 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; } + public long OptId { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + } +} diff --git a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs new file mode 100644 index 00000000..8eff1191 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations.Schema; + +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 int Version { get; set; } = 1; + public int SortOrder { get; set; } + public int? UpdateOptId { get; set; } + public string UpdateOptName { get; set; } + public DateTime? UpdateDate { get; set; } + } +} diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql new file mode 100644 index 00000000..8ba5327f --- /dev/null +++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql @@ -0,0 +1,92 @@ +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 '公式可读文本', + `Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted', + `Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)', + `OptId` int NULL DEFAULT NULL COMMENT '创建人Id', + `OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称', + `OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间', + `UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id', + `UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称', + `UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间', + PRIMARY KEY (`id`) USING BTREE, + 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', + `Status` tinyint NOT NULL DEFAULT 1 COMMENT '状态: 1=Active, 2=Disabled, 3=Deleted', + `ControlStrategy` tinyint NOT NULL COMMENT '控制策略: 1=Block, 2=Approval, 3=Warning', + `TriggerPoints` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发时点(逗号分隔)', + `ScopeAssetBookIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用账户Id列表', + `ScopeClientIds` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用对手方Id列表', + `ScopeUnderlyingTypes` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用标的类型', + `ScopeTradeTypes` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '适用合约类型', + `ScopeIsGlobal` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否全局适用: 0=否, 1=是', + `Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)', + `OptId` int NULL DEFAULT NULL COMMENT '创建人Id', + `OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称', + `OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间', + `UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id', + `UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称', + `UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间', + PRIMARY KEY (`id`) USING BTREE, + 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; + +CREATE TABLE `glms_risk_rule_audit_log` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id', + `OperationType` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作类型(RULE_CREATE/RULE_UPDATE等)', + `TargetType` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '目标类型(RULE/APPLICATION/VARIABLE)', + `TargetId` bigint NOT NULL COMMENT '目标记录Id', + `TargetName` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '目标名称', + `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)', + `OptId` bigint NOT NULL COMMENT '操作人Id', + `OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人名称', + `OptDate` datetime NOT NULL COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_opt_date`(`OptDate` ASC) USING BTREE, + INDEX `idx_operation_type`(`OperationType` ASC) USING BTREE, + INDEX `idx_target_type`(`TargetType` ASC) USING BTREE, + INDEX `idx_target_id`(`TargetId` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控规则操作审计日志表' ROW_FORMAT = Dynamic; + +CREATE TABLE `glms_risk_variable` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id', + `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=是', + `Version` int NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)', + `SortOrder` int NOT NULL DEFAULT 0 COMMENT '排序', + `OptId` int NULL DEFAULT NULL COMMENT '创建人Id', + `OptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人名称', + `OptDate` datetime NULL DEFAULT NULL COMMENT '创建时间', + `UpdateOptId` int NULL DEFAULT NULL COMMENT '最后修改人Id', + `UpdateOptName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人名称', + `UpdateDate` datetime NULL DEFAULT NULL COMMENT '最后修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_variable_code`(`VariableCode` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '风控变量池定义表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/YLErpDAL/DataBase/YLContext.cs b/YLErpDAL/DataBase/YLContext.cs index b6ed6e6d..ce383a6d 100644 --- a/YLErpDAL/DataBase/YLContext.cs +++ b/YLErpDAL/DataBase/YLContext.cs @@ -1,4 +1,4 @@ -using BaseOUDAL; +using BaseOUDAL; using YLErp.Core.DBModels; using YLErp.Model; @@ -408,5 +408,10 @@ namespace YLErp.BLL public DbSet tradeContractOaResult { get; set; } public DbSet bondPayment { get; set; } + public DbSet glms_risk_rule { get; set; } + public DbSet glms_risk_rule_application { get; set; } + public DbSet glms_risk_rule_audit_log { get; set; } + public DbSet glms_risk_variable { get; set; } + } } \ No newline at end of file diff --git a/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs new file mode 100644 index 00000000..0aed2a39 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs @@ -0,0 +1,10 @@ +namespace YLErp.Modules.RiskEngine.Dto +{ + public class BatchOperationResult + { + public bool Success { get; set; } + public int TotalCount { get; set; } + public int SuccessCount { get; set; } + public string ErrorMessage { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs new file mode 100644 index 00000000..f1b80da9 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskApplicationReq.cs @@ -0,0 +1,16 @@ +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class CreateRiskApplicationReq + { + public long RuleId { get; set; } + public RiskControlStrategy ControlStrategy { get; set; } + public string TriggerPoints { get; set; } + public string ScopeAssetBookIds { get; set; } + public string ScopeClientIds { get; set; } + public string ScopeUnderlyingTypes { get; set; } + public string ScopeTradeTypes { get; set; } + public bool ScopeIsGlobal { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs new file mode 100644 index 00000000..ceceee6a --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskRuleReq.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs new file mode 100644 index 00000000..5299a549 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/CreateRiskVariableReq.cs @@ -0,0 +1,19 @@ +using YLErp.DBModels; + +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 int SortOrder { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs b/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs new file mode 100644 index 00000000..af603b66 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/FormulaCondition.cs @@ -0,0 +1,15 @@ +namespace YLErp.Modules.RiskEngine.Dto +{ + public class FormulaCondition + { + public string VariableCode { 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 string ThresholdVariableName { get; set; } + public string Unit { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/FormulaDefinition.cs b/YLErpDAL/Modules/RiskEngine/Dto/FormulaDefinition.cs new file mode 100644 index 00000000..cd85d0d7 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/FormulaDefinition.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class FormulaDefinition + { + public string LogicOperator { get; set; } + public List Conditions { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs new file mode 100644 index 00000000..f195ee7c --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs @@ -0,0 +1,13 @@ +using BaseOUDAL; +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class QueryRiskApplicationReq : BaseSearchReq + { + public string Keyword { get; set; } + public RiskRuleStatus? Status { get; set; } + public RiskControlStrategy? Strategy { get; set; } + public string TriggerPoint { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs new file mode 100644 index 00000000..690712fa --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskAuditLogReq.cs @@ -0,0 +1,14 @@ +using System; +using BaseOUDAL; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class QueryRiskAuditLogReq : BaseSearchReq + { + public string OperationType { get; set; } + public string TargetType { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public string Keyword { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs new file mode 100644 index 00000000..362316e4 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskRuleReq.cs @@ -0,0 +1,12 @@ +using BaseOUDAL; +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class QueryRiskRuleReq : BaseSearchReq + { + public string Keyword { get; set; } + public RiskRuleStatus? Status { get; set; } + public string VariableCode { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs new file mode 100644 index 00000000..cc9dc45a --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskVariableReq.cs @@ -0,0 +1,11 @@ +using BaseOUDAL; +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class QueryRiskVariableReq : BaseSearchReq + { + public RiskVariableCategory? Category { get; set; } + public string Keyword { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs new file mode 100644 index 00000000..a791dde8 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationDetail.cs @@ -0,0 +1,29 @@ +using System; +using YLErp.DBModels; + +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 RuleName { get; set; } + public string FormulaJson { get; set; } + public string FormulaText { get; set; } + public RiskRuleStatus Status { get; set; } + public RiskControlStrategy ControlStrategy { get; set; } + public string TriggerPoints { get; set; } + public string ScopeAssetBookIds { get; set; } + public string ScopeClientIds { get; set; } + public string ScopeUnderlyingTypes { get; set; } + public string ScopeTradeTypes { get; set; } + public bool ScopeIsGlobal { get; set; } + public int Version { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + public string UpdateOptName { get; set; } + public DateTime UpdateDate { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs new file mode 100644 index 00000000..c04e7595 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskApplicationListItem.cs @@ -0,0 +1,28 @@ +using System; +using YLErp.DBModels; + +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 RuleName { get; set; } + public string FormulaText { get; set; } + public RiskRuleStatus Status { get; set; } + public RiskControlStrategy ControlStrategy { get; set; } + public string TriggerPoints { get; set; } + public string ScopeAssetBookIds { get; set; } + public string ScopeClientIds { get; set; } + public string ScopeUnderlyingTypes { get; set; } + public string ScopeTradeTypes { get; set; } + public bool ScopeIsGlobal { get; set; } + public int Version { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + public string UpdateOptName { get; set; } + public DateTime UpdateDate { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs new file mode 100644 index 00000000..ea08ebe8 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogDetail.cs @@ -0,0 +1,20 @@ +using System; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class RiskAuditLogDetail + { + public long Id { get; set; } + public string OperationType { get; set; } + 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; } + public long OptId { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs new file mode 100644 index 00000000..aabc739a --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskAuditLogListItem.cs @@ -0,0 +1,18 @@ +using System; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class RiskAuditLogListItem + { + public long Id { get; set; } + public string OperationType { get; set; } + 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; } + public DateTime OptDate { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs new file mode 100644 index 00000000..3be19da8 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleApplicationSummary.cs @@ -0,0 +1,13 @@ +using YLErp.DBModels; + +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 new file mode 100644 index 00000000..37251f3d --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleDetail.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using YLErp.DBModels; + +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 RiskRuleStatus Status { get; set; } + public int Version { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + public string UpdateOptName { get; set; } + public DateTime UpdateDate { get; set; } + public List Applications { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs new file mode 100644 index 00000000..509bfe33 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleListItem.cs @@ -0,0 +1,21 @@ +using System; +using YLErp.DBModels; + +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 RiskRuleStatus Status { get; set; } + public int Version { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + public string UpdateOptName { get; set; } + public DateTime UpdateDate { get; set; } + public int ApplicationCount { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs new file mode 100644 index 00000000..b0842d6e --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskRuleVersionItem.cs @@ -0,0 +1,13 @@ +using System; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class RiskRuleVersionItem + { + public int Version { get; set; } + public string OperationType { get; set; } + public string OperationDetail { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs new file mode 100644 index 00000000..7470d12e --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableDetail.cs @@ -0,0 +1,27 @@ +using System; +using YLErp.DBModels; + +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 int Version { get; set; } + public int SortOrder { get; set; } + public string OptName { get; set; } + public DateTime OptDate { get; set; } + public string UpdateOptName { get; set; } + public DateTime UpdateDate { get; set; } + public int ReferenceCount { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs new file mode 100644 index 00000000..56ab0beb --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableListItem.cs @@ -0,0 +1,19 @@ +using YLErp.DBModels; + +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 int Version { get; set; } + public int SortOrder { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs new file mode 100644 index 00000000..a414930f --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/RiskVariableSimpleItem.cs @@ -0,0 +1,14 @@ +using YLErp.DBModels; + +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; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs new file mode 100644 index 00000000..096f320e --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskApplicationReq.cs @@ -0,0 +1,16 @@ +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class UpdateRiskApplicationReq + { + public RiskControlStrategy ControlStrategy { get; set; } + public string TriggerPoints { get; set; } + public string ScopeAssetBookIds { get; set; } + public string ScopeClientIds { get; set; } + public string ScopeUnderlyingTypes { get; set; } + public string ScopeTradeTypes { get; set; } + public bool ScopeIsGlobal { get; set; } + public int ExpectedVersion { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs new file mode 100644 index 00000000..1cbd7002 --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskRuleReq.cs @@ -0,0 +1,11 @@ +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 int ExpectedVersion { get; set; } + } +} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs new file mode 100644 index 00000000..f7db36bd --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/Dto/UpdateRiskVariableReq.cs @@ -0,0 +1,19 @@ +using YLErp.DBModels; + +namespace YLErp.Modules.RiskEngine.Dto +{ + public class UpdateRiskVariableReq + { + public string VariableName { get; set; } + public RiskVariableCategory Category { get; set; } + public RiskVariableDataType DataType { get; set; } + public string Unit { get; set; } + public string ValueDomain { get; set; } + public string Description { get; set; } + public string ImplementationScript { get; set; } + public string SourceExpression { get; set; } + public bool IsImplemented { 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 25ce5868..4f5bd585 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs @@ -157,6 +157,16 @@ namespace YLErp.Modules.RiskEngine public class RiskEngineService : YLBaseService { IYcLogger _logger = LogFactory.GetLogger("RiskEngineService"); + + private static readonly Lazy _instance = + new Lazy(() => new RiskEngineService()); + + public static RiskEngineService GetInstance() => _instance.Value; + + private RiskEngineService() : base((OptUserInfo)null) + { + } + public RiskEngineService(OptUserInfo userInfo) : base(userInfo) { } @@ -169,6 +179,11 @@ namespace YLErp.Modules.RiskEngine { } + public void RefreshCache() + { + _logger.Info("[风控引擎] RefreshCache 被调用(当前为桩实现,待缓存机制完成后替换)"); + } + /// /// 执行风控检查(第一版:一句简单的 1==1,先跑通) /// diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs new file mode 100644 index 00000000..58ebe35d --- /dev/null +++ b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs @@ -0,0 +1,1414 @@ +using BaseOUDAL; +using Newtonsoft.Json; +using Qdp.Foundation.Utilities; +using YLErp.BLL; +using YLErp.DBModels; +using YLErp.Model; +using YLErp.Modules.RiskEngine.Dto; + +/* +================================================================================ + 风控规则管理服务 — RiskRuleService 技术方案说明 +================================================================================ + +【职责定位】 + RiskRuleService 是风控引擎模块的 CRUD 管理服务层,负责规则(Rule)、应用配置 + (Application)、变量(Variable)和操作审计日志(AuditLog)的全生命周期管理。 + + 与 RiskEngineService(引擎执行层)的关系: + - 本服务:配置管理(增删改查 + 审计日志) + - RiskEngineService:运行时执行(缓存 + 公式解析 + 风控判定) + - 交互方式:本服务在每次写操作后调用 RiskEngineService.RefreshCache() 通知引擎刷新 + +【设计文档】 + 详细设计文档:.trae/documents/RiskRuleService详细设计.md(v1.1) + 需求来源文档:.trae/documents/风控引擎需求详细设计.md(v1.14) + + 核心概念: + - Rule(规则):定义"什么情况下触发",由变量 + 操作符 + 阈值组成的条件列表(FormulaJson) + - Application(应用配置):定义"何时、对谁、怎么处理",关联规则 + 策略/时点/范围(1个规则可配N个应用) + - Variable(变量):变量池中的可选项,用于构建规则条件(含实现脚本 ImplementationScript) + - AuditLog(审计日志):记录所有 CRUD 操作,支持版本追溯(SnapshotData 存储版本号快照) + +【架构位置】 + ┌─────────────────────────────────────────────────────────────────┐ + │ API 层 │ + │ RiskRuleController(27 个端点) │ + │ - /api/risk-rules/* 规则 CRUD(10 个端点) │ + │ - /api/risk-applications/* 应用配置 CRUD(9 个端点) │ + │ - /api/risk-variables/* 变量池 CRUD(6 个端点) │ + │ - /api/risk-audit-logs/* 审计日志(3 个端点) │ + └──────────────────────────┬──────────────────────────────────────┘ + │ + ┌──────────────────────────▼──────────────────────────────────────┐ + │ 服务层(本文件所在层) │ + │ │ + │ RiskRuleService(本文件) │ + │ - 规则 / 应用 / 变量 / 审计日志的 CRUD │ + │ - FormulaJson 完整校验(变量存在性、操作符、类型匹配、阈值引用) │ + │ - 乐观锁并发控制(Version 字段) │ + │ - 删除保护(有 Active 引用时拒绝删除) │ + │ - CRUD 后触发 RiskEngineService.RefreshCache() │ + │ │ + │ RiskEngineService(另一团队开发,单例) │ + │ - 内存缓存管理 + 公式执行 + 风控判定 │ + │ - RefreshCache():从数据库重新加载 Active 规则/应用/变量 │ + └──────────────────────────┬──────────────────────────────────────┘ + │ + ┌──────────────────────────▼──────────────────────────────────────┐ + │ 数据访问层(EF Core DbContext 直接访问,无独立 Repository) │ + │ - glms_risk_rule 规则定义表 │ + │ - glms_risk_rule_application 规则应用配置表(RuleId 外键) │ + │ - glms_risk_variable 变量池定义表 │ + │ - glms_risk_rule_audit_log 操作审计日志表 │ + └─────────────────────────────────────────────────────────────────┘ + +【方法清单】(共 27 个公开方法 + 10 个私有辅助方法) + + ── 规则管理(10 个) ────────────────────────────────────────────── + QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询) + GetRuleDetail(long) 获取规则详情(含关联应用摘要) + CreateRule(CreateRiskRuleReq) 新建规则(RuleCode 自动生成,FormulaJson 完整校验) + UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1) + DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝) + EnableRule(long) 启用规则(仅 Disabled → Active) + DisableRule(long) 停用规则(仅 Active → Disabled) + GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号) + BatchDeleteRules(List) 批量删除规则(逐条保护检查,单事务) + GetRuleApplications(long) 获取规则关联的应用配置列表 + + ── 应用配置管理(9 个) ─────────────────────────────────────────── + QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(Join 规则表,多条件筛选) + GetApplicationDetail(long) 获取应用详情(含关联规则信息) + CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验) + UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验) + DeleteApplication(long) 删除应用(软删除) + EnableApplication(long) 启用应用(校验关联规则状态) + DisableApplication(long) 停用应用 + BatchEnableApplications(List) 批量启用(逐条校验关联规则) + BatchDisableApplications(List) 批量停用 + + ── 变量管理(6 个) ─────────────────────────────────────────────── + QueryVariableList(QueryRiskVariableReq) 查询变量列表(全量返回,Category/Keyword 筛选) + GetVariableDetail(long) 获取变量详情(含 ReferenceCount 引用统计) + CreateVariable(CreateRiskVariableReq) 新建变量(编码唯一性校验,脚本长度校验 ≤10000) + UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护) + DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除) + GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉) + + ── 审计日志(3 个) ─────────────────────────────────────────────── + QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页) + ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出) + GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData) + + ── 私有辅助方法 ─────────────────────────────────────────────────── + WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数) + GenerateCode(prefix, existingCodes) 编码生成(RISK/APP-YYYYMMDD-NNNN,每日上限 9999) + GetVariableCodeSet() 获取变量编码集合(5 分钟本地缓存) + InvalidateVariableCache() 清除变量缓存 + ValidateFormulaJson(formulaJson) FormulaJson 完整校验 + ValidateTriggerPoints(triggerPoints) 触发时点校验(5 个合法时点) + ValidateScopeFields(req) Scope 字段校验(全局互斥 + ID 格式校验) + ValidateIdList(idList, fieldName) ID 列表格式校验 + TryRefreshCache() 安全调用 RefreshCache(失败仅记日志) + GetRuleOrThrow / GetApplicationOrThrow / GetVariableOrThrow 实体获取快捷方法 + +【关键设计决策】 + 1. 数据访问:直接使用 EF Core DbContext(无独立 Repository,与项目风格一致) + 2. 事务策略:Create* 方法使用 BeginTransaction 包裹双 SaveChanges(实体 + 审计日志同事务) + 3. 并发控制:乐观锁(Version 字段),修改时版本不匹配抛 ServiceException + 4. 删除保护:强保护模式,有 Active 引用时拒绝删除规则/变量 + 5. 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 存储版本快照 + +【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql) + glms_risk_rule 规则定义表 + - 唯一索引:RuleCode + - 普通索引:Status, OptDate + - 关键字段:FormulaJson(公式定义), FormulaText(可读文本), Version(乐观锁) + + glms_risk_rule_application 规则应用配置表 + - 唯一索引:ApplicationCode + - 普通索引:RuleId, Status, OptDate + - 外键:RuleId → glms_risk_rule.id + - 关键字段:ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围) + + glms_risk_variable 变量池定义表 + - 唯一索引:VariableCode + - 关键字段:ImplementationScript(实现脚本), DataType(数值/日期/布尔), IsImplemented + + glms_risk_rule_audit_log 操作审计日志表 + - 普通索引:TargetType+TargetId, OperationType, OptDate + - 关键字段:SnapshotData(JSON 快照,存储版本号等) + +【关键文件清单】 + 本服务: + - RiskRuleService.cs(本文件):规则/应用/变量/日志 CRUD 管理 + - Dto/ 目录:请求/响应 DTO 模型 + + 关联服务: + - RiskEngineService.cs:引擎执行层(单例,本服务通过 GetInstance() 获取引用) + - RiskRuleController.cs(YLErpWeb/Controllers/):API 控制器(27 个端点) + + 实体模型: + - Framework/YLErp.Core/DBModels/glms_risk_rule.cs + - Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs + - Framework/YLErp.Core/DBModels/glms_risk_variable.cs + - Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs + + 枚举定义: + - Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.cs(Active/Disabled/Deleted) + - Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs(Block/Approve/Warn) + - Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs + - Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs(Numeric/Date/Boolean) + +【命名空间】 + YLErp.Modules.RiskEngine + +【注意事项】 + 1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用 + 2. 所有写操作后必须调用 TryRefreshCache(),确保引擎缓存与数据库一致 + 3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted) + 4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代 + 5. ImplementationScript 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库 + 6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性) +================================================================================ +*/ + +namespace YLErp.Modules.RiskEngine +{ + public class RiskRuleService : YLBaseService + { + private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService"); + private readonly RiskEngineService _riskEngineService; + + private HashSet _variableCodeCache; + private DateTime _variableCacheUpdateTime; + + private static readonly HashSet ValidTriggerPoints = new HashSet + { + "BOOK_CONFIRM", + "CLOSE_REVIEW", + "UPLOAD_CONFIRMATION", + "EVENT_TRIGGER", + "FUND_PAYMENT" + }; + + private static readonly Dictionary> ValidOperatorsByType = new Dictionary> + { + { RiskVariableDataType.Numeric, new HashSet { ">", "<", ">=", "<=", "=", "≠", "介于", "不介于" } }, + { RiskVariableDataType.Date, new HashSet { "<", ">", "=", ">=", "<=", "介于", "不介于" } }, + { RiskVariableDataType.Boolean, new HashSet { "是", "否" } } + }; + + public RiskRuleService(OptUserInfo userInfo) : base(userInfo) + { + _riskEngineService = RiskEngineService.GetInstance(); + } + + public RiskRuleService(YLBaseService baseService) : base(baseService) + { + _riskEngineService = RiskEngineService.GetInstance(); + } + + public RiskRuleService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext) + { + _riskEngineService = RiskEngineService.GetInstance(); + } + + #region Private Helpers + + private void WriteAuditLog(string operationType, string targetType, long targetId, + string targetName, string targetCode, string operationDetail, string result = "成功", string snapshotData = null) + { + var log = new glms_risk_rule_audit_log + { + OperationType = operationType, + TargetType = targetType, + TargetId = targetId, + TargetName = targetName, + TargetCode = targetCode, + OperationDetail = operationDetail, + Result = result, + SnapshotData = snapshotData, + OptId = UserId, + OptName = UserName, + OptDate = DateTime.Now + }; + DbContext.glms_risk_rule_audit_log.Add(log); + } + + private string GenerateCode(string prefix, IQueryable existingCodes) + { + 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 || + DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5)) + { + _variableCodeCache = DbContext.glms_risk_variable + .Where(v => v.IsImplemented) + .Select(v => v.VariableCode) + .ToHashSet(); + _variableCacheUpdateTime = DateTime.Now; + } + return _variableCodeCache; + } + + private void InvalidateVariableCache() + { + _variableCodeCache = null; + } + + private void ValidateFormulaJson(string formulaJson) + { + FormulaDefinition formula; + try + { + formula = JsonConvert.DeserializeObject(formulaJson); + } + catch + { + throw new ServiceException("公式表达式 JSON 格式不合法"); + } + + if (formula.Conditions == null || formula.Conditions.Count == 0) + throw new ServiceException("公式条件列表不能为空"); + + 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); + + var validOperators = ValidOperatorsByType[variableDef.DataType]; + if (!validOperators.Contains(cond.Operator)) + throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 不适用于{GetDataTypeName(variableDef.DataType)}类型变量"); + + if (cond.ThresholdType == "fixed") + { + if (cond.Value == null) + throw new ServiceException($"{condLabel}:固定阈值不能为空"); + if (variableDef.DataType == RiskVariableDataType.Numeric) + { + if (!decimal.TryParse(cond.Value.ToString(), out _)) + throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字"); + } + if (variableDef.DataType == RiskVariableDataType.Date) + { + if (!DateTime.TryParse(cond.Value.ToString(), out _)) + throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期"); + } + } + else if (cond.ThresholdType == "variable") + { + if (string.IsNullOrEmpty(cond.ThresholdVariableCode)) + throw new ServiceException($"{condLabel}:变量阈值引用的变量编码不能为空"); + if (!allVariableCodes.Contains(cond.ThresholdVariableCode)) + throw new ServiceException($"{condLabel}:阈值变量编码 '{cond.ThresholdVariableCode}' 不存在或未实现"); + var thresholdVarDef = DbContext.glms_risk_variable + .FirstOrDefault(v => v.VariableCode == cond.ThresholdVariableCode); + if (thresholdVarDef.DataType != variableDef.DataType) + throw new ServiceException($"{condLabel}:阈值变量与条件变量的数据类型不一致"); + } + else + { + throw new ServiceException($"{condLabel}:阈值类型 '{cond.ThresholdType}' 不合法,仅支持 fixed/variable"); + } + } + } + + private void ValidateTriggerPoints(string triggerPoints) + { + if (string.IsNullOrWhiteSpace(triggerPoints)) + throw new ServiceException("触发时点不能为空"); + + var points = triggerPoints.Split(','); + foreach (var point in points) + { + var trimmed = point.Trim(); + if (!ValidTriggerPoints.Contains(trimmed)) + throw new ServiceException($"触发时点 '{trimmed}' 不合法"); + } + + if (points.All(p => string.IsNullOrWhiteSpace(p))) + throw new ServiceException("至少需要指定一个触发时点"); + } + + private void ValidateScopeFields(CreateRiskApplicationReq req) + { + if (req.ScopeIsGlobal) + { + if (!string.IsNullOrEmpty(req.ScopeAssetBookIds) || + !string.IsNullOrEmpty(req.ScopeClientIds) || + !string.IsNullOrEmpty(req.ScopeUnderlyingTypes) || + !string.IsNullOrEmpty(req.ScopeTradeTypes)) + { + throw new ServiceException("全局适用时,不应指定其他适用范围维度"); + } + return; + } + + if (!string.IsNullOrEmpty(req.ScopeAssetBookIds)) + { + ValidateIdList(req.ScopeAssetBookIds, "适用账户"); + } + + if (!string.IsNullOrEmpty(req.ScopeClientIds)) + { + ValidateIdList(req.ScopeClientIds, "适用对手方"); + } + } + + private void ValidateIdList(string idList, string fieldName) + { + var parts = idList.Split(','); + foreach (var part in parts) + { + if (!int.TryParse(part.Trim(), out _)) + throw new ServiceException($"{fieldName}列表格式不合法,应为逗号分隔的数字 ID"); + } + } + + private void TryRefreshCache() + { + try + { + _riskEngineService.RefreshCache(); + } + catch (Exception ex) + { + _logger.Error($"缓存刷新失败:{ex.Message}", ex); + } + } + + private string GetDataTypeName(RiskVariableDataType dataType) + { + return dataType switch + { + RiskVariableDataType.Numeric => "数值", + RiskVariableDataType.Date => "日期", + RiskVariableDataType.Boolean => "布尔", + _ => "未知" + }; + } + + private glms_risk_rule GetRuleOrThrow(long ruleId) + { + var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status != RiskRuleStatus.Deleted); + if (rule == null) + throw new ServiceException("规则不存在或已删除"); + return rule; + } + + private glms_risk_rule_application GetApplicationOrThrow(long applicationId) + { + var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted); + if (app == null) + throw new ServiceException("应用配置不存在或已删除"); + return app; + } + + private glms_risk_variable GetVariableOrThrow(long variableId) + { + var variable = DbContext.glms_risk_variable.FirstOrDefault(v => v.id == variableId); + if (variable == null) + throw new ServiceException("变量不存在"); + return variable; + } + + private void ValidateRuleParams(string ruleName, string formulaJson, string formulaText) + { + 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); + } + + #endregion + + #region Rule Management + + public SearchListResult QueryRuleList(QueryRiskRuleReq req) + { + var query = DbContext.glms_risk_rule + .Where(r => r.Status != RiskRuleStatus.Deleted); + + if (!string.IsNullOrWhiteSpace(req.Keyword)) + { + query = query.Where(r => r.RuleCode.Contains(req.Keyword) + || r.RuleName.Contains(req.Keyword) + || r.RuleDescription.Contains(req.Keyword)); + } + + if (req.Status.HasValue) + { + query = query.Where(r => r.Status == req.Status.Value); + } + + if (!string.IsNullOrWhiteSpace(req.VariableCode)) + { + query = query.Where(r => r.FormulaJson.Contains(req.VariableCode)); + } + + 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, + 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) + }); + + return result.ToSearchList(req); + } + + public RiskRuleDetail GetRuleDetail(long ruleId) + { + var rule = GetRuleOrThrow(ruleId); + + var applications = DbContext.glms_risk_rule_application + .Where(a => a.RuleId == ruleId && a.Status != RiskRuleStatus.Deleted) + .Select(a => new RiskRuleApplicationSummary + { + Id = a.id, + ApplicationCode = a.ApplicationCode, + Status = a.Status, + ControlStrategy = a.ControlStrategy, + TriggerPoints = a.TriggerPoints + }) + .ToList(); + + return new RiskRuleDetail + { + Id = rule.id, + RuleCode = rule.RuleCode, + RuleName = rule.RuleName, + RuleDescription = rule.RuleDescription, + FormulaJson = rule.FormulaJson, + FormulaText = rule.FormulaText, + Status = rule.Status, + Version = rule.Version, + OptName = rule.OptName, + OptDate = rule.OptDate.GetValueOrDefault(), + UpdateOptName = rule.UpdateOptName, + UpdateDate = rule.UpdateDate ?? rule.OptDate.GetValueOrDefault(), + Applications = applications + }; + } + + public RiskRuleDetail CreateRule(CreateRiskRuleReq req) + { + ValidateRuleParams(req.RuleName, req.FormulaJson, req.FormulaText); + + var ruleCode = GenerateCode("RISK", DbContext.glms_risk_rule.Select(r => r.RuleCode)); + + var entity = new glms_risk_rule + { + RuleCode = ruleCode, + RuleName = req.RuleName, + RuleDescription = req.RuleDescription, + FormulaJson = req.FormulaJson, + FormulaText = req.FormulaText, + Status = RiskRuleStatus.Active, + Version = 1 + }; + + SetDBModelOpt(entity); + entity.UpdateOptId = UserId; + entity.UpdateOptName = UserName; + entity.UpdateDate = DateTime.Now; + + using (var transaction = DbContext.Database.BeginTransaction()) + { + try + { + DbContext.glms_risk_rule.Add(entity); + DbContext.SaveChanges(); + + WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, entity.RuleCode, "创建规则", + snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); + DbContext.SaveChanges(); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + TryRefreshCache(); + + return GetRuleDetail(entity.id); + } + + public RiskRuleDetail UpdateRule(long ruleId, UpdateRiskRuleReq req) + { + var rule = GetRuleOrThrow(ruleId); + + if (rule.Version != req.ExpectedVersion) + throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑"); + + ValidateRuleParams(req.RuleName, req.FormulaJson, req.FormulaText); + + rule.RuleName = req.RuleName; + rule.RuleDescription = req.RuleDescription; + rule.FormulaJson = req.FormulaJson; + rule.FormulaText = req.FormulaText; + rule.Version = rule.Version + 1; + rule.UpdateOptId = UserId; + rule.UpdateOptName = UserName; + rule.UpdateDate = DateTime.Now; + + WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, rule.RuleCode, "修改规则"); + DbContext.SaveChanges(); + + TryRefreshCache(); + + return GetRuleDetail(ruleId); + } + + public void DeleteRule(long ruleId) + { + var rule = GetRuleOrThrow(ruleId); + + var activeCount = DbContext.glms_risk_rule_application + .Count(a => a.RuleId == ruleId && a.Status == RiskRuleStatus.Active); + if (activeCount > 0) + throw new ServiceException($"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用"); + + rule.Status = RiskRuleStatus.Deleted; + rule.Version = rule.Version + 1; + rule.UpdateOptId = UserId; + rule.UpdateOptName = UserName; + rule.UpdateDate = DateTime.Now; + + WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "删除规则"); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public void EnableRule(long ruleId) + { + var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Disabled); + if (rule == null) + throw new ServiceException("仅已停用的规则可以启用"); + + rule.Status = RiskRuleStatus.Active; + rule.Version = rule.Version + 1; + rule.UpdateOptId = UserId; + rule.UpdateOptName = UserName; + rule.UpdateDate = DateTime.Now; + + WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "启用规则", + snapshotData: JsonConvert.SerializeObject(new { Version = rule.Version })); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public void DisableRule(long ruleId) + { + var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Active); + if (rule == null) + throw new ServiceException("仅已生效的规则可以停用"); + + rule.Status = RiskRuleStatus.Disabled; + rule.Version = rule.Version + 1; + rule.UpdateOptId = UserId; + rule.UpdateOptName = UserName; + rule.UpdateDate = DateTime.Now; + + WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "停用规则"); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public List GetRuleVersions(long ruleId) + { + GetRuleOrThrow(ruleId); + + var logs = DbContext.glms_risk_rule_audit_log + .Where(l => l.TargetType == "RULE" && l.TargetId == ruleId) + .OrderByDescending(l => l.OptDate) + .ToList(); + + var result = new List(); + foreach (var l in logs) + { + int version = 0; + if (!string.IsNullOrEmpty(l.SnapshotData)) + { + try + { + var snapshot = JsonConvert.DeserializeAnonymousType(l.SnapshotData, new { Version = 0 }); + if (snapshot != null) + version = snapshot.Version; + } + catch { } + } + + result.Add(new RiskRuleVersionItem + { + Version = version, + OperationType = l.OperationType, + OperationDetail = l.OperationDetail, + OptName = l.OptName, + OptDate = l.OptDate + }); + } + + return result; + } + + public BatchOperationResult BatchDeleteRules(List ruleIds) + { + var rules = DbContext.glms_risk_rule + .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + .ToList(); + + if (rules.Count != ruleIds.Count) + throw new ServiceException("部分规则不存在或已删除"); + + foreach (var rule in rules) + { + var activeCount = DbContext.glms_risk_rule_application + .Count(a => a.RuleId == rule.id && a.Status == RiskRuleStatus.Active); + if (activeCount > 0) + throw new ServiceException($"规则 '{rule.RuleName}' 仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用"); + } + + foreach (var rule in rules) + { + rule.Status = RiskRuleStatus.Deleted; + rule.Version = rule.Version + 1; + rule.UpdateOptId = UserId; + rule.UpdateOptName = UserName; + rule.UpdateDate = DateTime.Now; + + WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, rule.RuleCode, "批量删除规则"); + } + + DbContext.SaveChanges(); + TryRefreshCache(); + + return new BatchOperationResult + { + Success = true, + TotalCount = ruleIds.Count, + SuccessCount = ruleIds.Count + }; + } + + public List GetRuleApplications(long ruleId) + { + 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() + }; + + return result.ToList(); + } + + #endregion + + #region Application Management + + 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)); + } + + if (req.Status.HasValue) + { + query = query.Where(x => x.app.Status == req.Status.Value); + } + + if (req.Strategy.HasValue) + { + query = query.Where(x => x.app.ControlStrategy == req.Strategy.Value); + } + + if (!string.IsNullOrWhiteSpace(req.TriggerPoint)) + { + query = query.Where(x => x.app.TriggerPoints.Contains(req.TriggerPoint)); + } + + var result = query.OrderByDescending(x => x.app.UpdateDate) + .Select(x => new RiskApplicationListItem + { + 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() + }); + + return result.ToSearchList(req); + } + + 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) + throw new ServiceException("应用配置不存在或已删除"); + + return detail; + } + + 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("关联规则已停用,无法创建应用配置"); + + 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, + Status = RiskRuleStatus.Active, + ControlStrategy = req.ControlStrategy, + TriggerPoints = req.TriggerPoints, + ScopeAssetBookIds = req.ScopeAssetBookIds, + ScopeClientIds = req.ScopeClientIds, + ScopeUnderlyingTypes = req.ScopeUnderlyingTypes, + ScopeTradeTypes = req.ScopeTradeTypes, + ScopeIsGlobal = req.ScopeIsGlobal, + Version = 1 + }; + + SetDBModelOpt(entity); + entity.UpdateOptId = UserId; + entity.UpdateOptName = UserName; + entity.UpdateDate = DateTime.Now; + + using (var transaction = DbContext.Database.BeginTransaction()) + { + try + { + DbContext.glms_risk_rule_application.Add(entity); + DbContext.SaveChanges(); + + WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, entity.ApplicationCode, entity.ApplicationCode, + $"创建应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}", + snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); + DbContext.SaveChanges(); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + TryRefreshCache(); + + return GetApplicationDetail(entity.id); + } + + public RiskApplicationDetail UpdateApplication(long applicationId, UpdateRiskApplicationReq req) + { + var app = GetApplicationOrThrow(applicationId); + + if (app.Version != req.ExpectedVersion) + throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑"); + + ValidateTriggerPoints(req.TriggerPoints); + var scopeReq = new CreateRiskApplicationReq + { + ScopeAssetBookIds = req.ScopeAssetBookIds, + ScopeClientIds = req.ScopeClientIds, + ScopeUnderlyingTypes = req.ScopeUnderlyingTypes, + ScopeTradeTypes = req.ScopeTradeTypes, + ScopeIsGlobal = req.ScopeIsGlobal + }; + ValidateScopeFields(scopeReq); + + app.ControlStrategy = req.ControlStrategy; + app.TriggerPoints = req.TriggerPoints; + app.ScopeAssetBookIds = req.ScopeAssetBookIds; + app.ScopeClientIds = req.ScopeClientIds; + app.ScopeUnderlyingTypes = req.ScopeUnderlyingTypes; + app.ScopeTradeTypes = req.ScopeTradeTypes; + app.ScopeIsGlobal = req.ScopeIsGlobal; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APPLICATION_UPDATE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, + $"修改应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}"); + DbContext.SaveChanges(); + + TryRefreshCache(); + + return GetApplicationDetail(applicationId); + } + + public void DeleteApplication(long applicationId) + { + var app = GetApplicationOrThrow(applicationId); + + app.Status = RiskRuleStatus.Deleted; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APPLICATION_DELETE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "删除应用配置"); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public void EnableApplication(long applicationId) + { + var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Disabled); + if (app == null) + throw new ServiceException("仅已停用的应用配置可以启用"); + + 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("关联规则已停用,无法启用应用配置"); + + app.Status = RiskRuleStatus.Active; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "启用应用配置"); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public void DisableApplication(long applicationId) + { + var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Active); + if (app == null) + throw new ServiceException("仅已生效的应用配置可以停用"); + + app.Status = RiskRuleStatus.Disabled; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APPLICATION_DISABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "停用应用配置"); + DbContext.SaveChanges(); + + TryRefreshCache(); + } + + public BatchOperationResult BatchEnableApplications(List applicationIds) + { + var apps = DbContext.glms_risk_rule_application + .Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Disabled) + .ToList(); + + if (apps.Count != applicationIds.Count) + throw new ServiceException("部分应用配置不存在或当前状态不允许启用"); + + foreach (var app in apps) + { + 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}' 的关联规则未生效,无法启用"); + } + + foreach (var app in apps) + { + app.Status = RiskRuleStatus.Active; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APPLICATION_BATCH_ENABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量启用应用配置"); + } + + DbContext.SaveChanges(); + TryRefreshCache(); + + return new BatchOperationResult + { + Success = true, + TotalCount = applicationIds.Count, + SuccessCount = applicationIds.Count + }; + } + + public BatchOperationResult BatchDisableApplications(List applicationIds) + { + var apps = DbContext.glms_risk_rule_application + .Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Active) + .ToList(); + + if (apps.Count != applicationIds.Count) + throw new ServiceException("部分应用配置不存在或当前状态不允许停用"); + + foreach (var app in apps) + { + app.Status = RiskRuleStatus.Disabled; + app.Version = app.Version + 1; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + + WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量停用应用配置"); + } + + DbContext.SaveChanges(); + TryRefreshCache(); + + return new BatchOperationResult + { + Success = true, + TotalCount = applicationIds.Count, + SuccessCount = applicationIds.Count + }; + } + + #endregion + + #region Variable Management + + public SearchListResult QueryVariableList(QueryRiskVariableReq req) + { + var query = DbContext.glms_risk_variable.AsQueryable(); + + if (req.Category.HasValue) + { + query = query.Where(v => v.Category == req.Category.Value); + } + + if (!string.IsNullOrWhiteSpace(req.Keyword)) + { + query = query.Where(v => v.VariableCode.Contains(req.Keyword) + || v.VariableName.Contains(req.Keyword)); + } + + var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableCode) + .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, + Version = v.Version, + SortOrder = v.SortOrder + }); + + return result.ToSearchList(req); + } + + public RiskVariableDetail GetVariableDetail(long variableId) + { + var variable = GetVariableOrThrow(variableId); + + var referenceCount = DbContext.glms_risk_rule + .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + + 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, + Version = variable.Version, + SortOrder = variable.SortOrder, + OptName = variable.OptName, + OptDate = variable.OptDate.GetValueOrDefault(), + UpdateOptName = variable.UpdateOptName, + UpdateDate = variable.UpdateDate ?? variable.OptDate.GetValueOrDefault(), + ReferenceCount = referenceCount + }; + } + + 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}' 已存在"); + + 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, + SortOrder = req.SortOrder, + Version = 1 + }; + + SetDBModelOpt(entity); + entity.UpdateOptId = UserId; + entity.UpdateOptName = UserName; + entity.UpdateDate = DateTime.Now; + + using (var transaction = DbContext.Database.BeginTransaction()) + { + try + { + DbContext.glms_risk_variable.Add(entity); + DbContext.SaveChanges(); + + WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, req.VariableCode, $"创建变量:{req.VariableCode}", + snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); + DbContext.SaveChanges(); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + InvalidateVariableCache(); + TryRefreshCache(); + + return GetVariableDetail(entity.id); + } + + public RiskVariableDetail UpdateVariable(long variableId, UpdateRiskVariableReq req) + { + var variable = GetVariableOrThrow(variableId); + + if (variable.Version != req.ExpectedVersion) + throw new ServiceException("变量已被其他用户修改,请重新加载后再编辑"); + + if (string.IsNullOrWhiteSpace(req.VariableName)) + throw new ServiceException("变量名称不能为空"); + if (string.IsNullOrWhiteSpace(req.ImplementationScript)) + throw new ServiceException("实现脚本不能为空"); + + if (req.DataType != variable.DataType) + { + var activeRefCount = DbContext.glms_risk_rule + .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + if (activeRefCount > 0) + throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型"); + } + + variable.VariableName = req.VariableName; + variable.Category = req.Category; + variable.DataType = req.DataType; + variable.Unit = req.Unit; + variable.ValueDomain = req.ValueDomain; + variable.Description = req.Description; + variable.ImplementationScript = req.ImplementationScript; + variable.SourceExpression = req.SourceExpression; + variable.IsImplemented = req.IsImplemented; + 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}"); + DbContext.SaveChanges(); + + InvalidateVariableCache(); + TryRefreshCache(); + + return GetVariableDetail(variableId); + } + + public void DeleteVariable(long variableId) + { + var variable = GetVariableOrThrow(variableId); + + var activeRefCount = DbContext.glms_risk_rule + .Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode)); + if (activeRefCount > 0) + throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除"); + + DbContext.glms_risk_variable.Remove(variable); + + WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, variable.VariableCode, $"删除变量:{variable.VariableCode}"); + DbContext.SaveChanges(); + + InvalidateVariableCache(); + TryRefreshCache(); + } + + public List GetAllVariableList() + { + var result = DbContext.glms_risk_variable + .Where(v => v.IsImplemented) + .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 + }) + .ToList(); + + return result; + } + + #endregion + + #region Audit Log + + public SearchListResult QueryAuditLogs(QueryRiskAuditLogReq req) + { + var query = DbContext.glms_risk_rule_audit_log.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(req.OperationType)) + { + query = query.Where(l => l.OperationType == req.OperationType); + } + + if (!string.IsNullOrWhiteSpace(req.TargetType)) + { + query = query.Where(l => l.TargetType == req.TargetType); + } + + if (req.StartDate.HasValue) + { + query = query.Where(l => l.OptDate >= req.StartDate.Value); + } + + if (req.EndDate.HasValue) + { + query = query.Where(l => l.OptDate <= req.EndDate.Value); + } + + if (!string.IsNullOrWhiteSpace(req.Keyword)) + { + query = query.Where(l => l.TargetName.Contains(req.Keyword) + || l.TargetCode.Contains(req.Keyword) + || l.OperationDetail.Contains(req.Keyword)); + } + + var result = query.OrderByDescending(l => l.OptDate) + .Select(l => new RiskAuditLogListItem + { + Id = l.id, + OperationType = l.OperationType, + TargetType = l.TargetType, + TargetId = l.TargetId, + TargetName = l.TargetName, + TargetCode = l.TargetCode, + OperationDetail = l.OperationDetail, + Result = l.Result, + OptName = l.OptName, + OptDate = l.OptDate + }); + + return result.ToSearchList(req); + } + + public byte[] ExportAuditLogs(QueryRiskAuditLogReq req) + { + throw new ServiceException("导出功能暂未实现"); + } + + public RiskAuditLogDetail GetAuditLogDetail(long logId) + { + var log = DbContext.glms_risk_rule_audit_log.FirstOrDefault(l => l.id == logId); + if (log == null) + throw new ServiceException("审计日志不存在"); + + return new RiskAuditLogDetail + { + Id = log.id, + OperationType = log.OperationType, + TargetType = log.TargetType, + TargetId = log.TargetId, + TargetName = log.TargetName, + TargetCode = log.TargetCode, + OperationDetail = log.OperationDetail, + Result = log.Result, + SnapshotData = log.SnapshotData, + OptId = log.OptId, + OptName = log.OptName, + OptDate = log.OptDate + }; + } + + #endregion + } +} diff --git a/YLErpWeb/App_Data/FunctionRight.xml b/YLErpWeb/App_Data/FunctionRight.xml index 2f01d359..1e03e280 100644 --- a/YLErpWeb/App_Data/FunctionRight.xml +++ b/YLErpWeb/App_Data/FunctionRight.xml @@ -1,28 +1,29 @@ - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -30,13 +31,13 @@ - - - - - - - + + + + + + + @@ -52,14 +53,31 @@ - + - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -71,39 +89,39 @@ - + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + @@ -111,62 +129,62 @@ - - - - + + + + - - + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - + - + - - - - - - - + + + + + + + @@ -194,28 +212,28 @@ - + - + - - - - - - - + + + + + + + - - + + - + diff --git a/YLErpWeb/Common/UserInfo.cs b/YLErpWeb/Common/UserInfo.cs index d9c507eb..e5071920 100644 --- a/YLErpWeb/Common/UserInfo.cs +++ b/YLErpWeb/Common/UserInfo.cs @@ -1,4 +1,4 @@ -using YLErp.DBModels.Consts; +using YLErp.DBModels.Consts; using YLErp.Model.Enum; using YLErp.Modules.ClientModule; using YLErp.Modules.ClientModule.Models; @@ -280,7 +280,7 @@ namespace YLErp.Web if (user.Roles.Any(n => n.Name == "权限管理员")) { - var rightStrs = new string[] { "部门管理", "系统管理", "角色管理", "角色新增", "角色修改", "用户管理", "用户新增", "用户修改", "用户禁用", "基础缓存" }; + var rightStrs = new string[] { "部门管理", "系统管理", "角色管理", "角色新增", "角色修改", "用户管理", "用户新增", "用户修改", "用户禁用", "基础缓存", "风控规则查看", "风控规则新增", "风控规则编辑", "风控规则删除", "风控规则启停", "风控应用查看", "风控应用新增", "风控应用编辑", "风控应用删除", "风控应用启停", "风控变量查看", "风控变量新增", "风控变量编辑", "风控变量删除", "风控日志查看", "风控日志导出" }; var query1 = db.Functions.Where(o => rightStrs.Contains(o.Name)).Select(n => n.Id); var query2 = from roleFunc in db.RoleFunctions join roleUsr in db.RoleUsers on roleFunc.RoleId equals roleUsr.RoleId diff --git a/YLErpWeb/Controllers/RiskRuleController.cs b/YLErpWeb/Controllers/RiskRuleController.cs new file mode 100644 index 00000000..d65997dc --- /dev/null +++ b/YLErpWeb/Controllers/RiskRuleController.cs @@ -0,0 +1,616 @@ +using Qdp.Foundation.Utilities; +using YLErp.Modules.RiskEngine; +using YLErp.Modules.RiskEngine.Dto; + +namespace YLErp.Web.Controllers +{ + [Route("risk-engine")] + public class RiskRuleController : BaseController + { + private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleController"); + + #region Rule Management + + [HttpGet("risk-rules")] + [MyAuthorize("风控规则查看")] + public JsonResult QueryRiskRules(QueryRiskRuleReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.QueryRuleList(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询风控规则列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-rules/{id}")] + [MyAuthorize("风控规则查看")] + public JsonResult GetRiskRule(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetRuleDetail(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询风控规则详情"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-rules/{id}/versions")] + [MyAuthorize("风控规则查看")] + public JsonResult GetRiskRuleVersions(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetRuleVersions(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询风控规则版本历史"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-rules")] + [MyAuthorize("风控规则新增")] + public JsonResult CreateRiskRule([FromBody] CreateRiskRuleReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.CreateRule(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "创建风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPut("risk-rules/{id}")] + [MyAuthorize("风控规则编辑")] + public JsonResult UpdateRiskRule(long id, [FromBody] UpdateRiskRuleReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.UpdateRule(id, req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "更新风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpDelete("risk-rules/{id}")] + [MyAuthorize("风控规则删除")] + public JsonResult DeleteRiskRule(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.DeleteRule(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "删除风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpDelete("risk-rules/batch")] + [MyAuthorize("风控规则删除")] + public JsonResult BatchDeleteRiskRules([FromBody] List ids) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.BatchDeleteRules(ids); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "批量删除风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-rules/{id}/enable")] + [MyAuthorize("风控规则启停")] + public JsonResult EnableRiskRule(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.EnableRule(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "启用风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-rules/{id}/disable")] + [MyAuthorize("风控规则启停")] + public JsonResult DisableRiskRule(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.DisableRule(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "停用风控规则"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-rules/{id}/applications")] + [MyAuthorize("风控规则查看")] + public JsonResult GetRuleApplications(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetRuleApplications(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询规则关联应用列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + #endregion + + #region Application Management + + [HttpGet("risk-applications")] + [MyAuthorize("风控应用查看")] + public JsonResult QueryRiskApplications(QueryRiskApplicationReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.QueryApplicationList(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询应用配置列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-applications/{id}")] + [MyAuthorize("风控应用查看")] + public JsonResult GetRiskApplication(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetApplicationDetail(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询应用配置详情"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-applications")] + [MyAuthorize("风控应用新增")] + public JsonResult CreateRiskApplication([FromBody] CreateRiskApplicationReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.CreateApplication(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "创建应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPut("risk-applications/{id}")] + [MyAuthorize("风控应用编辑")] + public JsonResult UpdateRiskApplication(long id, [FromBody] UpdateRiskApplicationReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.UpdateApplication(id, req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "更新应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpDelete("risk-applications/{id}")] + [MyAuthorize("风控应用删除")] + public JsonResult DeleteRiskApplication(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.DeleteApplication(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "删除应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-applications/{id}/enable")] + [MyAuthorize("风控应用启停")] + public JsonResult EnableRiskApplication(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.EnableApplication(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "启用应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-applications/{id}/disable")] + [MyAuthorize("风控应用启停")] + public JsonResult DisableRiskApplication(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.DisableApplication(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "停用应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-applications/batch-enable")] + [MyAuthorize("风控应用启停")] + public JsonResult BatchEnableRiskApplications([FromBody] List ids) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.BatchEnableApplications(ids); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "批量启用应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-applications/batch-disable")] + [MyAuthorize("风控应用启停")] + public JsonResult BatchDisableRiskApplications([FromBody] List ids) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.BatchDisableApplications(ids); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "批量停用应用配置"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + #endregion + + #region Variable Management + + [HttpGet("risk-variables")] + [MyAuthorize("风控变量查看")] + public JsonResult QueryRiskVariables(QueryRiskVariableReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.QueryVariableList(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询变量列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-variables/{id}")] + [MyAuthorize("风控变量查看")] + public JsonResult GetRiskVariable(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetVariableDetail(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询变量详情"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-variables/list")] + [MyAuthorize("风控变量查看")] + public JsonResult GetAllRiskVariables() + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetAllVariableList(); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询全部变量列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPost("risk-variables")] + [MyAuthorize("风控变量新增")] + public JsonResult CreateRiskVariable([FromBody] CreateRiskVariableReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.CreateVariable(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "创建变量"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpPut("risk-variables/{id}")] + [MyAuthorize("风控变量编辑")] + public JsonResult UpdateRiskVariable(long id, [FromBody] UpdateRiskVariableReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.UpdateVariable(id, req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "更新变量"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpDelete("risk-variables/{id}")] + [MyAuthorize("风控变量删除")] + public JsonResult DeleteRiskVariable(long id) + { + try + { + var service = new RiskRuleService(CurUser); + service.DeleteVariable(id); + return Json(new { success = true }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "删除变量"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + #endregion + + #region Audit Log + + [HttpGet("risk-audit-logs")] + [MyAuthorize("风控日志查看")] + public JsonResult QueryRiskAuditLogs(QueryRiskAuditLogReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.QueryAuditLogs(req); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询风控操作日志列表"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-audit-logs/{id}")] + [MyAuthorize("风控日志查看")] + public JsonResult GetRiskAuditLogDetail(long id) + { + try + { + var service = new RiskRuleService(CurUser); + var result = service.GetAuditLogDetail(id); + return Json(new { success = true, data = result }); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "查询风控操作日志详情"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + [HttpGet("risk-audit-logs/export")] + [MyAuthorize("风控日志导出")] + public IActionResult ExportRiskAuditLogs(QueryRiskAuditLogReq req) + { + try + { + var service = new RiskRuleService(CurUser); + var bytes = service.ExportAuditLogs(req); + return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "风控操作日志.xlsx"); + } + catch (ServiceException ex) + { + return Json(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + _logger.Error(ex, "导出风控操作日志"); + return Json(new { success = false, message = "系统异常,请联系管理员" }); + } + } + + #endregion + } +} diff --git a/YLErpWeb/appsettings.dev.json b/YLErpWeb/appsettings.dev.json index 378136e1..f53778f2 100644 --- a/YLErpWeb/appsettings.dev.json +++ b/YLErpWeb/appsettings.dev.json @@ -7,15 +7,15 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" + "ylcms": "server=10.250.202.35;uid=onederiv@OneDeriv#wx_dtpp_test;pooling=true;port=2883;pwd=Onederiv@2026;database=yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "yladmin": "server=10.250.202.35;uid=onederiv@OneDeriv#wx_dtpp_test;pooling=true;port=2883;pwd=Onederiv@2026;database=yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "ylclient": "server=10.250.202.35;uid=onederiv@OneDeriv#wx_dtpp_test;pooling=true;port=2883;pwd=Onederiv@2026;database=yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "bondoms": "server=10.250.202.35;uid=onederiv@OneDeriv#wx_dtpp_test;pooling=true;port=2883;pwd=Onederiv@2026;database=bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" }, "AppSettings": { "VirtualPathRoot": "", "UseRightAligned": "", - "PluginFolder": "D:\\workspace\\glms\\zszq-trs\\Plugins\\YLErp.Plugins.GuoLian\\obj\\Debug\\net6.0" + "PluginFolder": "D:\\workspace\\onederiv\\trs\\Plugins\\YLErp.Plugins.GuoLian\\obj\\Debug\\net6.0" // 改为本地目录地址 }, "LibreOffice": { "ExePath": "", @@ -75,12 +75,15 @@ "SendKafakaCron": "10 0 * * *", "MaxThreadPercent": 0.6, //占系统资源最多60%的线程数量 "oa_confg": { - "BaseUrl": "http://ip:port", - "SubUrl": "", - "UploadUrl": "/api/oa/upload", // OA附件上传接口路径 - "SystemToken": "your-system-token", // OA系统鉴权Token - "SystemName": "OTC_TRADE", // 调用方系统标识 - "IsNextFlow": 0 + "BaseUrl": "http://10.250.202.40:13590", + "SubUrl": "/gateway/oaflow/createOaFlow", + "QueryUrl": "/gateway/oaflow/getRequestData", + "DownloadFileUrl": "/gateway/oaflow/downloadSingleFile", + "SystemToken": "QIkox8sv3rVLtiT1wmI4Dp8wSjX9D84Glkp121vRino9fLobWnDiy8ppmqJcv1676TYuxZsGrYelx0whRCMaPyums2VnzIYrpqCFf5tyc3/x22Elf25xgRmd5pH8tLcnPuVOjMrUf0GsoytQCGFkhnrPajhsKq/mSwYQ4OA=", //OA系统key + "SystemName": "onederiv", + "IsNextFlow": 1, //是否自动提交 默认0 + "FlowId": 779115, + "UploadUrl": "http://10.250.202.40:13590/gateway/oaflow/uploadDoc" }, "GeneralSSO": { "enable": true, //是否启用通用SSO登录