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 } }