using BaseOUDAL; using Newtonsoft.Json; using System.Linq.Expressions; using YLErp.BLL; using YLErp.DBModels; 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.17) 核心概念: - Rule(规则):定义"什么情况下触发",支持两种公式模式(互斥): · 结构化模式(ConditionJson 有值):变量 + 操作符 + 阈值/变量 的条件列表,前端拼接 RuleExpr · 自由文本模式(RuleExpr 有值,ConditionJson 为空):前端直接编辑类 C# 表达式 - Application(应用配置):定义"何时、对谁、怎么处理",关联多条规则(RuleIds 逗号分隔)+ 策略/时点/范围 - Variable(变量):变量池中的可选项,用于构建规则条件(含取值表达式 VariableExpr) - 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 │ │ - ConditionJson 完整校验(结构化模式:变量存在性、操作符、类型匹配)│ │ - RuleExpr 基础语法校验(自由文本模式:括号匹配、变量引用校验) │ │ - 乐观锁并发控制(Version 字段) │ │ - 删除保护(有 Active 引用时拒绝删除) │ │ - CRUD 后触发 RiskEngineService.RefreshCache() │ │ │ │ RiskEngineService(另一团队开发,单例) │ │ - 内存缓存管理 + 公式执行 + 风控判定 │ │ - RefreshCache():从数据库重新加载 Active 规则/应用/变量 │ └──────────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────────▼──────────────────────────────────────┐ │ 数据访问层(EF Core DbContext 直接访问,无独立 Repository) │ │ - glms_risk_rule 规则定义表 │ │ - glms_risk_rule_application 规则应用配置表(RuleIds 逗号分隔) │ │ - glms_risk_variable 变量池定义表 │ │ - glms_risk_rule_audit_log 操作审计日志表 │ └─────────────────────────────────────────────────────────────────┘ 【方法清单】(共 27 个公开方法 + 9 个私有辅助方法) ── 规则管理(10 个) ────────────────────────────────────────────── QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询) GetRuleDetail(long) 获取规则详情(含关联应用摘要) CreateRule(CreateRiskRuleReq) 新建规则(ConditionJson 完整校验) UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1) DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝) EnableRule(long) 启用规则(仅 Disabled → Active) DisableRule(long) 停用规则(仅 Active → Disabled) GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号) BatchDeleteRules(List) 批量删除规则(逐条保护检查,单事务) GetRuleApplications(long) 获取规则关联的应用配置列表 GetAllRuleList() 获取所有启用规则(轻量字段,供应用配置下拉) ── 应用配置管理(9 个) ─────────────────────────────────────────── QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称) 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) 新建变量(VariableExpr 长度校验 ≤10000) UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护) DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除) GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉) ── 审计日志(3 个) ─────────────────────────────────────────────── QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页) ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出) GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData) ── 私有辅助方法 ─────────────────────────────────────────────────── WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数) GetVariableExprSet() 获取变量表达式集合(5 分钟本地缓存) InvalidateVariableCache() 清除变量缓存 ValidateConditionJson(conditionJson) ConditionJson 完整校验(结构化模式) ValidateRuleExpr(ruleExpr) RuleExpr 基础语法校验(自由文本模式:括号匹配 + 变量引用校验) 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. ConditionJson 校验:变量存在性(按 VariableId)+ 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用 6. RuleExpr:自由文本模式下前端直接编辑,后端做基础语法校验(括号匹配 + 变量引用校验) 7. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度 8. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果 9. 批量操作:单事务批量,全部成功或全部失败 10. 变量缓存:5 分钟本地 HashSet 缓存,CRUD 后 InvalidateVariableCache() 11. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照 12. 多规则支持:Application 通过 RuleIds(逗号分隔)关联多条规则,内存解析关联关系 【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql) glms_risk_rule 规则定义表 - 普通索引:Status, OptDate - 关键字段:ConditionJson(结构化条件列表), RuleExpr(类 C# 表达式,引擎执行用), Version(乐观锁) glms_risk_rule_application 规则应用配置表 - 普通索引:Status, OptDate - 关键字段:RuleIds(逗号分隔的规则 ID 列表), ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围) glms_risk_variable 变量池定义表 - 关键字段:VariableExpr(取值表达式), DataType(数值/日期/布尔) 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. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库 6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性) ================================================================================ */ namespace YLErp.Modules.RiskEngine { public class RiskRuleService : YLBaseService { private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService"); private HashSet _variableExprCache; 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) { } public RiskRuleService(YLBaseService baseService) : base(baseService) { } public RiskRuleService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext) { } #region Private Helpers /// /// 写入审计日志 /// private void WriteAuditLog(string operationType, string targetType, long targetId, string targetName, string operationDetail, string result = "成功", string snapshotData = null) { var log = new glms_risk_rule_audit_log { OperationType = operationType, TargetType = targetType, TargetId = targetId, TargetName = targetName, OperationDetail = operationDetail, Result = result, SnapshotData = snapshotData, OptId = UserId, OptName = UserName, OptDate = DateTime.Now }; DbContext.glms_risk_rule_audit_log.Add(log); } /// /// 获取变量表达式集合(5 分钟本地缓存) /// private HashSet GetVariableExprSet() { if (_variableExprCache == null || DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5)) { _variableExprCache = DbContext.glms_risk_variable .Where(v => v.VariableExpr != null && v.VariableExpr != "") .Select(v => v.VariableExpr) .ToHashSet(); _variableCacheUpdateTime = DateTime.Now; } return _variableExprCache; } /// /// 失效变量表达式缓存 /// private void InvalidateVariableCache() { _variableExprCache = null; } /// /// 校验 ConditionJson(结构化模式:变量存在性、操作符、类型匹配) /// private void ValidateConditionJson(string conditionJson) { List conditions; try { conditions = JsonConvert.DeserializeObject>(conditionJson); } catch { throw new ServiceException("公式表达式 JSON 格式不合法"); } if (conditions == null || conditions.Count == 0) throw new ServiceException("公式条件列表不能为空"); for (int i = 0; i < conditions.Count; i++) { var cond = conditions[i]; var condLabel = $"条件{i + 1}"; var variableDef = DbContext.glms_risk_variable .FirstOrDefault(v => v.id == cond.VariableId); if (variableDef == null) throw new ServiceException($"{condLabel}:变量 ID '{cond.VariableId}' 不存在"); var validOperators = ValidOperatorsByType[variableDef.DataType]; if (!validOperators.Contains(cond.Operator)) throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 不适用于{GetDataTypeName(variableDef.DataType)}类型变量"); if (cond.ThresholdType == "Fixed") { if (cond.Value == null) throw new ServiceException($"{condLabel}:固定阈值不能为空"); if (cond.Operator == "介于" || cond.Operator == "不介于") { // 介于/不介于:Value 必须为双元素数组 [下限, 上限] if (!(cond.Value is Newtonsoft.Json.Linq.JArray arr && arr.Count == 2)) throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 的阈值必须为双元素数组 [下限, 上限]"); if (variableDef.DataType == RiskVariableDataType.Numeric) { if (!decimal.TryParse(arr[0].ToString(), out _) || !decimal.TryParse(arr[1].ToString(), out _)) throw new ServiceException($"{condLabel}:数值型介于/不介于的下限和上限必须为数字"); } if (variableDef.DataType == RiskVariableDataType.Date) { if (!DateTime.TryParse(arr[0].ToString(), out _) || !DateTime.TryParse(arr[1].ToString(), out _)) throw new ServiceException($"{condLabel}:日期型介于/不介于的起始和结束日期必须为合法日期"); } } else { if (variableDef.DataType == RiskVariableDataType.Numeric) { if (!decimal.TryParse(cond.Value.ToString(), out _)) throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字"); } if (variableDef.DataType == RiskVariableDataType.Date) { if (!DateTime.TryParse(cond.Value.ToString(), out _)) throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期"); } } } else if (cond.ThresholdType == "Variable") { if (!cond.ThresholdVariableId.HasValue) throw new ServiceException($"{condLabel}:变量阈值引用的变量 ID 不能为空"); var thresholdVarDef = DbContext.glms_risk_variable .FirstOrDefault(v => v.id == cond.ThresholdVariableId.Value); if (thresholdVarDef == null) throw new ServiceException($"{condLabel}:阈值变量 ID '{cond.ThresholdVariableId.Value}' 不存在"); if (thresholdVarDef.DataType != variableDef.DataType) throw new ServiceException($"{condLabel}:阈值变量与条件变量的数据类型不一致"); } else { throw new ServiceException($"{condLabel}:阈值类型 '{cond.ThresholdType}' 不合法,仅支持 Fixed/Variable"); } } } /// /// 校验 RuleExpr(自由文本模式:括号匹配) /// private void ValidateRuleExpr(string ruleExpr) { if (ruleExpr.Length > 10000) throw new ServiceException("公式表达式长度不能超过 10000 字符"); int parenDepth = 0; foreach (char c in ruleExpr) { if (c == '(') parenDepth++; else if (c == ')') parenDepth--; if (parenDepth < 0) throw new ServiceException("公式表达式括号不匹配:多余的右括号"); } if (parenDepth != 0) throw new ServiceException("公式表达式括号不匹配:缺少右括号"); } /// /// 校验触发时点 /// private void ValidateTriggerPoints(string triggerPoints) { if (string.IsNullOrWhiteSpace(triggerPoints)) 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("至少需要指定一个触发时点"); } /// /// 校验 Scope 适用范围字段 /// private void ValidateScopeFields(CreateRiskApplicationReq req) { if (req.ScopeIsGlobal == 1) { 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, "适用对手方"); } } /// /// 校验 ID 列表格式 /// 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.GetInstance().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; } /// /// 校验规则参数(名称 + ConditionJson + RuleExpr) /// private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr) { if (string.IsNullOrWhiteSpace(ruleName)) throw new ServiceException("规则名称不能为空"); if (ruleName.Length > 200) throw new ServiceException("规则名称长度不能超过200字符"); if (string.IsNullOrWhiteSpace(ruleExpr)) throw new ServiceException("规则表达式不能为空"); if (!string.IsNullOrWhiteSpace(conditionJson)) { ValidateConditionJson(conditionJson); } ValidateRuleExpr(ruleExpr); } /// /// 解析逗号分隔的规则 ID 字符串为 List /// private List ParseRuleIds(string ruleIds) { if (string.IsNullOrWhiteSpace(ruleIds)) throw new ServiceException("关联规则 ID 不能为空"); var ids = new List(); foreach (var part in ruleIds.Split(',')) { if (long.TryParse(part.Trim(), out long id)) ids.Add(id); else throw new ServiceException($"规则 ID 列表格式不合法:'{part.Trim()}'"); } if (ids.Count == 0) throw new ServiceException("关联规则 ID 不能为空"); return ids; } /// /// 校验规则 ID 列表是否全部 Active /// private void ValidateRuleIdsActive(string ruleIds) { var ids = ParseRuleIds(ruleIds); var rules = DbContext.glms_risk_rule .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) .ToList(); if (rules.Count != ids.Count) throw new ServiceException("部分关联规则不存在或已删除"); foreach (var rule in rules) { if (rule.Status == RiskRuleStatus.Disabled) throw new ServiceException($"关联规则 '{rule.RuleName}' 已停用,无法创建/启用应用配置"); } } /// /// 根据规则 ID 字符串解析为规则名称字符串 /// private string ResolveRuleNames(string ruleIds) { if (string.IsNullOrWhiteSpace(ruleIds)) return ""; var ids = ruleIds.Split(',') .Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null) .Where(id => id.HasValue) .Select(id => id.Value) .ToList(); if (ids.Count == 0) return ""; var names = DbContext.glms_risk_rule .Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) .Select(r => r.RuleName) .ToList(); return string.Join(", ", names); } /// /// 构建 Scope 字段的 OR 过滤谓词(逗号分隔值精确匹配) /// private static IQueryable ApplyScopeFilter( IQueryable query, Expression> fieldSelector, string csvValues) { var ids = csvValues.Split(',', StringSplitOptions.RemoveEmptyEntries); if (ids.Length == 0) return query; Expression> predicate = null; var param = fieldSelector.Parameters[0]; var member = fieldSelector.Body; foreach (var trimmed in ids.Select(id => id.Trim())) { var eq = Expression.Equal(member, Expression.Constant(trimmed)); var startsExpr = Expression.Call(member, nameof(string.StartsWith), null, Expression.Constant(trimmed + ",")); var endsExpr = Expression.Call(member, nameof(string.EndsWith), null, Expression.Constant("," + trimmed)); var containsExpr = Expression.Call(member, nameof(string.Contains), null, Expression.Constant("," + trimmed + ",")); var orExpr = Expression.OrElse(Expression.OrElse(eq, startsExpr), Expression.OrElse(endsExpr, containsExpr)); var lambda = Expression.Lambda>(orExpr, param); predicate = predicate == null ? lambda : predicate.Or(lambda); } return predicate != null ? query.Where(predicate) : query; } /// /// 获取控制策略中文名 /// private static string GetStrategyName(RiskControlStrategy strategy) { return strategy switch { RiskControlStrategy.Block => "阻断", RiskControlStrategy.Approval => "审批", RiskControlStrategy.ShowTip => "提醒", _ => strategy.ToString() }; } private static readonly Dictionary TriggerPointCnMap = new Dictionary { ["BOOK_CONFIRM"] = "交易录入确认", ["CLOSE_REVIEW"] = "平仓审核", ["UPLOAD_CONFIRMATION"] = "上传确认书", ["EVENT_TRIGGER"] = "事件触发", ["FUND_PAYMENT"] = "资金支付" }; /// /// 将触发时点映射为中文 /// private static string MapTriggerPointsToChinese(string triggerPoints) { if (string.IsNullOrWhiteSpace(triggerPoints)) return ""; var parts = triggerPoints.Split(','); var translated = parts.Select(p => TriggerPointCnMap.TryGetValue(p.Trim(), out var cn) ? cn : p.Trim()); return string.Join(", ", translated); } #endregion #region Rule Management /// /// 查询规则列表(分页,含 ApplicationCount 子查询) /// 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.RuleName.Contains(req.Keyword) || r.RuleText.Contains(req.Keyword)); } if (req.Status.HasValue) { query = query.Where(r => r.Status == req.Status.Value); } if (req.VariableId.HasValue) { var varIdStr = req.VariableId.Value.ToString(); query = query.Where(r => (r.ConditionJson != null && r.ConditionJson.Contains(varIdStr)) || (r.RuleExpr != null && r.RuleExpr.Contains(varIdStr))); } var result = query.OrderByDescending(r => r.UpdateDate) .Select(r => new RiskRuleListItem { Id = r.id, RuleName = r.RuleName, RuleText = r.RuleText, ConditionJson = r.ConditionJson, RuleExpr = r.RuleExpr, Description = r.Description, 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.RuleIds.Contains(r.id.ToString()) && a.Status != RiskRuleStatus.Deleted) }); return result.ToSearchList(req); } /// /// 获取规则详情(含关联应用摘要) /// public RiskRuleDetail GetRuleDetail(long ruleId) { var rule = GetRuleOrThrow(ruleId); var ruleIdStr = ruleId.ToString(); var applications = DbContext.glms_risk_rule_application .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted) .Select(a => new RiskRuleApplicationSummary { Id = a.id, Status = a.Status, ControlStrategy = a.ControlStrategy, TriggerPoints = a.TriggerPoints }) .ToList(); return new RiskRuleDetail { Id = rule.id, RuleName = rule.RuleName, RuleText = rule.RuleText, ConditionJson = rule.ConditionJson, RuleExpr = rule.RuleExpr, Description = rule.Description, Status = rule.Status, Version = rule.Version, OptName = rule.OptName, OptDate = rule.OptDate.GetValueOrDefault(), UpdateOptName = rule.UpdateOptName, UpdateDate = rule.UpdateDate ?? rule.OptDate.GetValueOrDefault(), Applications = applications }; } /// /// 创建规则(ConditionJson 完整校验,事务包裹) /// public RiskRuleDetail CreateRule(CreateRiskRuleReq req) { ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr); var compileResult = RuleCompiler.ValidateAndCompileFormula(null, req.RuleExpr); if (!compileResult.Success) throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}"); var entity = new glms_risk_rule { RuleName = req.RuleName, RuleText = req.RuleText, ConditionJson = req.ConditionJson, RuleExpr = req.RuleExpr, Description = req.Description, 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, "创建规则", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } } RiskEngineService.GetInstance().RefreshOneRuleCache(entity.id); return GetRuleDetail(entity.id); } /// /// 更新规则(乐观锁校验,Version+1) /// public RiskRuleDetail UpdateRule(long ruleId, UpdateRiskRuleReq req) { var rule = GetRuleOrThrow(ruleId); if (rule.Version != req.Version) throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑"); ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr); var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, req.RuleExpr); if (!compileResult.Success) throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}"); rule.RuleName = req.RuleName; rule.RuleText = req.RuleText; rule.ConditionJson = req.ConditionJson; rule.RuleExpr = req.RuleExpr; rule.Description = req.Description; rule.Version = rule.Version + 1; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则"); DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); return GetRuleDetail(ruleId); } /// /// 删除规则(软删除,有 Active 应用引用时拒绝) /// public void DeleteRule(long ruleId) { var rule = GetRuleOrThrow(ruleId); var ruleIdStr = ruleId.ToString(); var activeCount = DbContext.glms_risk_rule_application .Count(a => a.RuleIds.Contains(ruleIdStr) && 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, "删除规则"); DbContext.SaveChanges(); TryRefreshCache(); } /// /// 启用规则(仅 Disabled → Active) /// 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.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则"); DbContext.SaveChanges(); TryRefreshCache(); } /// /// 停用规则(仅 Active → Disabled) /// 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.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, "停用规则"); DbContext.SaveChanges(); TryRefreshCache(); } /// /// 获取规则版本历史(从审计日志 SnapshotData 提取) /// 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 ruleIdStr = rule.id.ToString(); var activeCount = DbContext.glms_risk_rule_application .Count(a => a.RuleIds.Contains(ruleIdStr) && 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, "批量删除规则"); } DbContext.SaveChanges(); TryRefreshCache(); return new BatchOperationResult { Success = true, TotalCount = ruleIds.Count, SuccessCount = ruleIds.Count }; } /// /// 获取规则关联的应用配置列表 /// public List GetRuleApplications(long ruleId) { GetRuleOrThrow(ruleId); var ruleIdStr = ruleId.ToString(); var apps = DbContext.glms_risk_rule_application .Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted) .OrderByDescending(a => a.UpdateDate) .ToList(); var result = new List(); foreach (var app in apps) { result.Add(new RiskApplicationListItem { Id = app.id, RuleIds = app.RuleIds, Status = (int)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; } /// /// 获取所有启用规则列表(轻量字段,供下拉选择) /// public List GetAllRuleList() { return DbContext.glms_risk_rule .Where(r => r.Status == RiskRuleStatus.Active) .OrderBy(r => r.id) .Select(r => new RiskRuleSimpleItem { Id = r.id, RuleName = r.RuleName, RuleText = r.RuleText }) .ToList(); } #endregion #region Application Management /// /// 查询应用配置列表(多条件筛选,内存关联规则名称) /// public SearchListResult QueryApplicationList(QueryRiskApplicationReq req) { var query = DbContext.glms_risk_rule_application .Where(a => a.Status != RiskRuleStatus.Deleted); if (req.Status.HasValue) { query = query.Where(a => a.Status == req.Status.Value); } if (req.Strategy.HasValue) { query = query.Where(a => a.ControlStrategy == req.Strategy.Value); } if (!string.IsNullOrWhiteSpace(req.TriggerPoint)) { query = query.Where(a => a.TriggerPoints.Contains(req.TriggerPoint)); } if (!string.IsNullOrWhiteSpace(req.RuleName)) { query = query.Where(a => DbContext.glms_risk_rule .Any(r => r.Status != RiskRuleStatus.Deleted && a.RuleIds.Contains(r.id.ToString()) && r.RuleName.Contains(req.RuleName))); } if (!string.IsNullOrWhiteSpace(req.ScopeClientIds)) query = ApplyScopeFilter(query, a => a.ScopeClientIds, req.ScopeClientIds); if (!string.IsNullOrWhiteSpace(req.ScopeUnderlyingTypes)) query = ApplyScopeFilter(query, a => a.ScopeUnderlyingTypes, req.ScopeUnderlyingTypes); if (!string.IsNullOrWhiteSpace(req.ScopeTradeTypes)) query = ApplyScopeFilter(query, a => a.ScopeTradeTypes, req.ScopeTradeTypes); if (!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds)) query = ApplyScopeFilter(query, a => a.ScopeAssetBookIds, req.ScopeAssetBookIds); var pagedResult = query.OrderByDescending(a => a.UpdateDate) .Select(a => new RiskApplicationListItem { Id = a.id, RuleIds = a.RuleIds, Description = a.Description, Status = (int)a.Status, ControlStrategy = a.ControlStrategy, TriggerPoints = a.TriggerPoints, ScopeAssetBookIds = a.ScopeAssetBookIds, ScopeClientIds = a.ScopeClientIds, ScopeUnderlyingTypes = a.ScopeUnderlyingTypes, ScopeTradeTypes = a.ScopeTradeTypes, ScopeIsGlobal = a.ScopeIsGlobal, Version = a.Version, OptName = a.OptName, OptDate = a.OptDate.GetValueOrDefault(), UpdateOptName = a.UpdateOptName, UpdateDate = a.UpdateDate ?? a.OptDate.GetValueOrDefault() }) .ToSearchList(req); return pagedResult; } /// /// 获取应用配置详情(含关联规则信息) /// public RiskApplicationDetail GetApplicationDetail(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 new RiskApplicationDetail { Id = app.id, RuleIds = app.RuleIds, Description = app.Description, 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() }; } /// /// 创建应用配置(关联规则 Active 校验,事务包裹) /// public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req) { ValidateRuleIdsActive(req.RuleIds); ValidateTriggerPoints(req.TriggerPoints); ValidateScopeFields(req); var entity = new glms_risk_rule_application { RuleIds = req.RuleIds, Description = req.Description, 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, ResolveRuleNames(req.RuleIds), $"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } } RiskEngineService.GetInstance().RefreshApplication(); return GetApplicationDetail(entity.id); } /// /// 更新应用配置(乐观锁校验) /// public RiskApplicationDetail UpdateApplication(long applicationId, UpdateRiskApplicationReq req) { var app = GetApplicationOrThrow(applicationId); if (app.Version != req.Version) throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑"); if (!string.IsNullOrWhiteSpace(req.RuleIds)) ValidateRuleIdsActive(req.RuleIds); ValidateTriggerPoints(req.TriggerPoints); var scopeReq = new CreateRiskApplicationReq { ScopeAssetBookIds = req.ScopeAssetBookIds, ScopeClientIds = req.ScopeClientIds, ScopeUnderlyingTypes = req.ScopeUnderlyingTypes, ScopeTradeTypes = req.ScopeTradeTypes, ScopeIsGlobal = req.ScopeIsGlobal }; ValidateScopeFields(scopeReq); if (!string.IsNullOrWhiteSpace(req.RuleIds)) app.RuleIds = req.RuleIds; app.Description = req.Description; 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("APP_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), $"修改应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}"); DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); 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("APP_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置"); DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); } /// /// 启用应用配置(校验关联规则状态) /// 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("仅已停用的应用配置可以启用"); ValidateRuleIdsActive(app.RuleIds); app.Status = RiskRuleStatus.Active; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置"); DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); } /// /// 停用应用配置 /// 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.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_DISABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "停用应用配置"); DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); } /// /// 批量启用应用配置(逐条校验关联规则) /// 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) { ValidateRuleIdsActive(app.RuleIds); } foreach (var app in apps) { app.Status = RiskRuleStatus.Active; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置"); } DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); 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.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置"); } DbContext.SaveChanges(); RiskEngineService.GetInstance().RefreshApplication(); return new BatchOperationResult { Success = true, TotalCount = applicationIds.Count, SuccessCount = applicationIds.Count }; } #endregion #region Variable Management /// /// 查询变量列表(全量返回,Category/Keyword 筛选) /// 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 (req.DataType.HasValue) { query = query.Where(v => v.DataType == req.DataType.Value); } if (!string.IsNullOrWhiteSpace(req.VariableName)) { query = query.Where(v => v.VariableName.Contains(req.VariableName)); } var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableName) .Select(v => new RiskVariableListItem { Id = v.id, VariableName = v.VariableName, Category = v.Category, DataType = v.DataType, Unit = v.Unit, ValueDomain = v.ValueDomain, Description = v.Description, VariableExpr = v.VariableExpr, Version = v.Version, SortOrder = v.SortOrder }); return result.ToSearchList(req); } /// /// 获取变量详情(含 ReferenceCount 引用统计) /// public RiskVariableDetail GetVariableDetail(long variableId) { var variable = GetVariableOrThrow(variableId); var varExpr = variable.VariableExpr; var referenceCount = DbContext.glms_risk_rule .Count(r => r.Status == RiskRuleStatus.Active && ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); return new RiskVariableDetail { Id = variable.id, VariableName = variable.VariableName, Category = variable.Category, DataType = variable.DataType, Unit = variable.Unit, ValueDomain = variable.ValueDomain, Description = variable.Description, VariableExpr = variable.VariableExpr, Version = variable.Version, SortOrder = variable.SortOrder, OptName = variable.OptName, OptDate = variable.OptDate.GetValueOrDefault(), UpdateOptName = variable.UpdateOptName, UpdateDate = variable.UpdateDate ?? variable.OptDate.GetValueOrDefault(), ReferenceCount = referenceCount }; } /// /// 创建变量(VariableExpr 长度校验 ≤10000,事务包裹) /// public RiskVariableDetail CreateVariable(CreateRiskVariableReq req) { if (string.IsNullOrWhiteSpace(req.VariableName)) throw new ServiceException("变量名称不能为空"); if (string.IsNullOrWhiteSpace(req.VariableExpr)) throw new ServiceException("取值表达式不能为空"); if (req.VariableExpr.Length > 10000) throw new ServiceException("取值表达式长度不能超过 10000 字符"); var entity = new glms_risk_variable { VariableName = req.VariableName, Category = req.Category, DataType = req.DataType, Unit = req.Unit, ValueDomain = req.ValueDomain, Description = req.Description, VariableExpr = req.VariableExpr, 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.VariableName}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); DbContext.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } } InvalidateVariableCache(); TryRefreshCache(); return GetVariableDetail(entity.id); } /// /// 更新变量(乐观锁,DataType 变更保护,级联更新引用规则 RuleExpr) /// public RiskVariableDetail UpdateVariable(long variableId, UpdateRiskVariableReq req) { var variable = GetVariableOrThrow(variableId); if (variable.Version != req.Version) throw new ServiceException("变量已被其他用户修改,请重新加载后再编辑"); if (string.IsNullOrWhiteSpace(req.VariableName)) throw new ServiceException("变量名称不能为空"); if (string.IsNullOrWhiteSpace(req.VariableExpr)) throw new ServiceException("取值表达式不能为空"); if (req.DataType != variable.DataType) { var varExpr = variable.VariableExpr; var activeRefCount = DbContext.glms_risk_rule .Count(r => r.Status == RiskRuleStatus.Active && ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); if (activeRefCount > 0) throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型"); } var oldVariableExpr = variable.VariableExpr; 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.VariableExpr = req.VariableExpr; variable.SortOrder = req.SortOrder; variable.Version = variable.Version + 1; variable.UpdateOptId = UserId; variable.UpdateOptName = UserName; variable.UpdateDate = DateTime.Now; var variableExprChanged = oldVariableExpr != req.VariableExpr; if (variableExprChanged) { // 编译校验:变量表达式变更后,需验证所有级联更新的规则表达式可编译 var affectedRules = DbContext.glms_risk_rule .Where(r => r.RuleExpr != null && r.RuleExpr.Contains(oldVariableExpr)) .ToList(); var compileTasks = affectedRules.Select(r => Task.Run(() => RuleCompiler.ValidateAndCompileFormula( r.id, r.RuleExpr.Replace(oldVariableExpr, req.VariableExpr))) ); var compileResults = Task.WhenAll(compileTasks).GetAwaiter().GetResult(); var failed = compileResults.FirstOrDefault(r => !r.Success); if (failed is not null) throw new ServiceException($"变量表达式变更导致规则编译失败:{failed.ErrorMessage}"); using (var transaction = DbContext.Database.BeginTransaction()) { try { foreach (var rule in affectedRules) { rule.RuleExpr = rule.RuleExpr.Replace(oldVariableExpr, req.VariableExpr); rule.Version = rule.Version + 1; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; } DbContext.SaveChanges(); WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}"); foreach (var rule in affectedRules) { WriteAuditLog("VAR_UPDATE_CASCADE", "RULE", rule.id, rule.RuleName, $"变量 [{variable.VariableName}] VariableExpr 变更,级联更新规则 RuleExpr"); } DbContext.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } } InvalidateVariableCache(); foreach (var rule in affectedRules) { RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id); } } else { WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}"); DbContext.SaveChanges(); InvalidateVariableCache(); } return GetVariableDetail(variableId); } /// /// 删除变量(有 Active 引用时拒绝) /// public void DeleteVariable(long variableId) { var variable = GetVariableOrThrow(variableId); var varExpr = variable.VariableExpr; var activeRefCount = DbContext.glms_risk_rule .Count(r => r.Status == RiskRuleStatus.Active && ((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) || (r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr)))); if (activeRefCount > 0) throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除"); DbContext.glms_risk_variable.Remove(variable); WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, $"删除变量:{variable.VariableName}"); DbContext.SaveChanges(); InvalidateVariableCache(); TryRefreshCache(); } /// /// 获取所有变量列表(供下拉选择) /// public List GetAllVariableList() { var result = DbContext.glms_risk_variable .Where(v => v.VariableExpr != null && v.VariableExpr != "") .OrderBy(v => v.SortOrder) .Select(v => new RiskVariableSimpleItem { Id = v.id, VariableName = v.VariableName, Category = v.Category, DataType = v.DataType, Unit = v.Unit, VariableExpr = v.VariableExpr }) .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.OperationTypes)) { var types = req.OperationTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); query = query.Where(l => types.Contains(l.OperationType)); } if (!string.IsNullOrWhiteSpace(req.TargetType)) { query = query.Where(l => l.TargetType == req.TargetType); } if (!string.IsNullOrWhiteSpace(req.TargetName)) { query = query.Where(l => l.TargetName.Contains(req.TargetName)); } if (!string.IsNullOrWhiteSpace(req.OptName)) { query = query.Where(l => l.OptName.Contains(req.OptName)); } if (req.StartDate.HasValue) { query = query.Where(l => l.OptDate >= req.StartDate.Value.Date); } if (req.EndDate.HasValue) { var end = req.EndDate.Value.Date.AddDays(1); query = query.Where(l => l.OptDate < end); } if (!string.IsNullOrWhiteSpace(req.Keyword)) { query = query.Where(l => l.TargetName.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, OperationDetail = l.OperationDetail, Result = l.Result, OptName = l.OptName, OptDate = l.OptDate }); return result.ToSearchList(req); } /// /// 导出审计日志为 Excel /// 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, OperationDetail = log.OperationDetail, Result = log.Result, SnapshotData = log.SnapshotData, OptId = log.OptId, OptName = log.OptName, OptDate = log.OptDate }; } #endregion } }